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("")]