diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
index 777fab812578b0..3aa8b097a22cfb 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
@@ -2,10 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
-using System.ComponentModel;
using System.IO;
using System.IO.MemoryMappedFiles;
-using System.Runtime.InteropServices;
using System.Text;
using Microsoft.NET.HostModel.MachO;
@@ -162,7 +160,7 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
}
}
}
- using (FileStream appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 1))
+ using (FileStream appHostDestinationStream = HostModelUtils.CreateFileStreamForHost(appHostDestinationFilePath, FileAccess.ReadWrite, FileShare.None, bufferSize: 1))
using (MemoryMappedViewAccessor appHostAccessor = appHostDestinationMap.CreateViewAccessor(0, appHostDestinationLength, MemoryMappedFileAccess.Read))
{
// Write the final content to the destination file, only up to the total length of the host, not the entire mapped file.
@@ -179,14 +177,8 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
}
}
});
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // chmod +755
- File.SetUnixFileMode(appHostDestinationFilePath,
- UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
- UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
- UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
- }
+
+ HostModelUtils.SetPermissionsForHost(appHostDestinationFilePath);
}
catch (Exception ex)
{
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index 17263c70d651f7..30ef7285fd1e5e 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -399,26 +399,24 @@ public string GenerateBundle(BundleContents bundleContents)
}
}
- // MacOS keeps a cache of file signatures, so we must create a new inode to ensure the file signature is properly updated.
- if (_macosCodesign && File.Exists(bundlePath))
+ // On non-Windows, delete any existing bundle so the output is written to a new inode.
+ // FileStreamOptions.UnixCreateMode only applies when a file is created, not when an
+ // existing file is truncated in place, so without this an existing non-executable
+ // bundle would keep its permissions and require a chmod (which can fail on some
+ // filesystems, e.g. bind-mounted volumes in rootless containers). On macOS this also
+ // ensures the kernel's code signature cache is not reused for the new contents.
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && File.Exists(bundlePath))
{
- _tracer.Log($"Removing existing bundle file to clear signature cache: {bundlePath}");
+ _tracer.Log($"Removing existing bundle file: {bundlePath}");
File.Delete(bundlePath);
}
- using (FileStream bundleOutputStream = File.Open(bundlePath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (FileStream bundleOutputStream = HostModelUtils.CreateFileStreamForHost(bundlePath, FileAccess.Write, FileShare.None))
{
BinaryUtils.WriteToStream(accessor, bundleOutputStream, (long)endOfBundle);
}
}
}
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // chmod +755
- File.SetUnixFileMode(bundlePath,
- UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
- UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
- UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
- }
+ HostModelUtils.SetPermissionsForHost(bundlePath);
return bundlePath;
}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/HostModelUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/HostModelUtils.cs
index 8fcb58da9b899a..0d6c60423540ae 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/HostModelUtils.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/HostModelUtils.cs
@@ -2,18 +2,77 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-#if NETFRAMEWORK
-using Microsoft.IO;
-#else
using System.IO;
-#endif
using System.Runtime.InteropServices;
+#if NETFRAMEWORK
+using FileInfo = Microsoft.IO.FileInfo;
+#endif
namespace Microsoft.NET.HostModel
{
internal static class HostModelUtils
{
+#if NET
+ // -rwxr-xr-x: the permissions an apphost or single-file bundle should have.
+ private const UnixFileMode AppHostFileMode =
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
+#endif
+
+ ///
+ /// Creates a for writing an apphost or bundle, requesting the desired
+ /// Unix permissions at creation time on platforms that support them.
+ /// When is , the default buffer size is used.
+ ///
+ public static FileStream CreateFileStreamForHost(
+ string path,
+ FileAccess access,
+ FileShare share,
+ int? bufferSize = null)
+ {
+#if NET
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ FileStreamOptions options = new()
+ {
+ Mode = FileMode.Create,
+ Access = access,
+ Share = share,
+ UnixCreateMode = AppHostFileMode,
+ };
+
+ if (bufferSize is int size)
+ {
+ options.BufferSize = size;
+ }
+
+ return new FileStream(path, options);
+ }
+#endif
+ return bufferSize.HasValue
+ ? new FileStream(path, FileMode.Create, access, share, bufferSize.Value)
+ : new FileStream(path, FileMode.Create, access, share);
+ }
+
+ ///
+ /// Ensures a host file has the required permissions.
+ ///
+ public static void SetPermissionsForHost(string filePath)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+#if NET
+ // File already has required permissions
+ if ((File.GetUnixFileMode(filePath) & AppHostFileMode) == AppHostFileMode)
+ return;
+
+ File.SetUnixFileMode(filePath, AppHostFileMode);
+#endif
+ }
+
private const string CodesignPath = @"/usr/bin/codesign";
public static bool IsCodesignAvailable() => File.Exists(CodesignPath);
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs
deleted file mode 100644
index 58306e0bb846ac..00000000000000
--- a/src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace System.IO;
-
-#if NETFRAMEWORK
-
-[Flags]
-internal enum UnixFileMode
-{
- None = 0,
- OtherExecute = 1,
- OtherWrite = 2,
- OtherRead = 4,
- GroupExecute = 8,
- GroupWrite = 16,
- GroupRead = 32,
- UserExecute = 64,
- UserWrite = 128,
- UserRead = 256,
- StickyBit = 512,
- SetGroup = 1024,
- SetUser = 2048,
-}
-
-internal static class FileExtensions
-{
- extension(File)
- {
- public static void SetUnixFileMode(string path, UnixFileMode mode)
- {
- int user = ((mode & UnixFileMode.UserRead) != 0 ? 4 : 0)
- | ((mode & UnixFileMode.UserWrite) != 0 ? 2 : 0)
- | ((mode & UnixFileMode.UserExecute) != 0 ? 1 : 0);
- int group = ((mode & UnixFileMode.GroupRead) != 0 ? 4 : 0)
- | ((mode & UnixFileMode.GroupWrite) != 0 ? 2 : 0)
- | ((mode & UnixFileMode.GroupExecute) != 0 ? 1 : 0);
- int other = ((mode & UnixFileMode.OtherRead) != 0 ? 4 : 0)
- | ((mode & UnixFileMode.OtherWrite) != 0 ? 2 : 0)
- | ((mode & UnixFileMode.OtherExecute) != 0 ? 1 : 0);
- int octal = (user << 6) | (group << 3) | other;
-
- const int EINTR = 4;
- int res;
- int iterations = 0;
- do
- {
- res = chmod(path, octal);
- } while (res == -1
- && Marshal.GetLastWin32Error() == EINTR
- && iterations++ < 8);
- if (res == -1)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {Convert.ToString(octal, 8)} for {path}.");
- }
- }
- }
-
- [DllImport("libc", SetLastError = true)]
- private static extern int chmod([MarshalAs(UnmanagedType.LPStr)] string pathname, int mode);
-}
-#endif
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
index dc69ddb8adc80a..89da4cd09a5f64 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
@@ -261,6 +261,38 @@ public void ExecutableImage()
.Be(expectedPermissions);
}
+ [Fact]
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public void ExecutableImage_ExistingNonExecutableFile()
+ {
+ using TestArtifact artifact = CreateTestDirectory();
+ string sourceAppHostMock = PrepareAppHostMockFile(artifact.Location);
+ string destinationFilePath = Path.Combine(artifact.Location, "DestinationAppHost.exe.mock");
+ string appBinaryFilePath = "Test/App/Binary/Path.dll";
+
+ // A non-executable file already exists at the destination (-rw-r--r--).
+ File.WriteAllText(destinationFilePath, "pre-existing content");
+ File.SetUnixFileMode(destinationFilePath,
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead);
+
+ // -rwxr-xr-x
+ const UnixFileMode expectedPermissions = UnixFileMode.UserRead | UnixFileMode.UserExecute | UnixFileMode.UserWrite |
+ UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
+
+ HostWriter.CreateAppHost(
+ sourceAppHostMock,
+ destinationFilePath,
+ appBinaryFilePath,
+ windowsGraphicalUserInterface: true);
+
+ // assert that the generated app has executable permissions
+ // despite the non-executable permissions on the pre-existing destination file.
+ File.GetUnixFileMode(destinationFilePath)
+ .Should()
+ .Be(expectedPermissions);
+ }
+
[Theory]
[PlatformSpecific(TestPlatforms.OSX)]
[InlineData("")]