diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs index a548a5358e28b..66bd6659219f0 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs @@ -6,11 +6,14 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.IO.MemoryMappedFiles; using System.Linq; using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; +using System.Text; using Microsoft.DotNet.CoreSetup; using Microsoft.NET.HostModel.AppHost; +using Microsoft.NET.HostModel.MachO; namespace Microsoft.NET.HostModel.Bundle { @@ -92,7 +95,7 @@ private bool ShouldCompress(FileType type) /// startOffset: offset of the start 'file' within 'bundle' /// compressedSize: size of the compressed data, if entry was compressed, otherwise 0 /// - private (long startOffset, long compressedSize) AddToBundle(Stream bundle, FileStream file, FileType type) + private (long startOffset, long compressedSize) AddToBundle(FileStream bundle, FileStream file, FileType type) { long startOffset = bundle.Position; if (ShouldCompress(type)) @@ -273,22 +276,25 @@ public string GenerateBundle(IReadOnlyList fileSpecs) BinaryUtils.CopyFile(hostSource, bundlePath); - if (_target.IsOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Codesign.IsAvailable) - { - Codesign.Run("--remove-signature", bundlePath); - } - // Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app // We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems // and vice versa for Windows). So it's safer to do case sensitive comparison everywhere. var relativePathToSpec = new Dictionary(StringComparer.Ordinal); long headerOffset = 0; - using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath))) + using (FileStream bundle = File.Open(bundlePath, FileMode.Open, FileAccess.ReadWrite)) + using (BinaryWriter writer = new BinaryWriter(bundle, Encoding.Default, leaveOpen: true)) { - Stream bundle = writer.BaseStream; + long? newLength; + using (MemoryMappedFile mmap = MemoryMappedFile.CreateFromFile(bundle, null, 0, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true)) + using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite)) + { + if(MachObjectFile.TryRemoveCodesign(accessor, out newLength)) + { + bundle.SetLength(newLength.Value); + } + } bundle.Position = bundle.Length; - foreach (var fileSpec in fileSpecs) { string relativePath = fileSpec.BundleRelativePath; 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 fd3934d6e1352..099813230c78a 100644 --- a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs +++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs @@ -485,12 +485,23 @@ private void ResourceWithUnknownLanguage() } } - private static readonly byte[] s_placeholderData = AppBinaryPathPlaceholderSearchValue.Concat(DotNetSearchPlaceholderValue).ToArray(); + private static readonly byte[] s_apphostPlaceholderData = AppBinaryPathPlaceholderSearchValue.Concat(DotNetSearchPlaceholderValue).ToArray(); + private static readonly byte[] s_singleFileApphostPlaceholderData = { + // 8 bytes represent the bundle header-offset + // Zero for non-bundle apphosts (default). + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 32 bytes represent the bundle signature: SHA-256 for ".net core bundle" + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae + }; + /// /// Prepares a mock executable file with the AppHost placeholder embedded in it. /// This file will not run, but can be used to test HostWriter and signing process. /// - public static string PrepareMockMachAppHostFile(string directory) + public static string PrepareMockMachAppHostFile(string directory, bool singleFile = false) { string fileName = "MockAppHost.mach.o"; string outputFilePath = Path.Combine(directory, fileName); @@ -501,7 +512,7 @@ public static string PrepareMockMachAppHostFile(string directory) // Add the placeholder - it just needs to exist somewhere in the image // We'll put it at 4096 bytes into the file - this should be in the middle of the __TEXT segment managedSignFile.Position = 4096; - managedSignFile.Write(s_placeholderData); + managedSignFile.Write(singleFile ? s_singleFileApphostPlaceholderData : s_apphostPlaceholderData); } return outputFilePath; } diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs index 0087effaa8cb2..d1efdb9227aab 100644 --- a/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs +++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs @@ -9,7 +9,11 @@ using FluentAssertions; using Microsoft.DotNet.Cli.Build.Framework; +using Microsoft.DotNet.CoreSetup; using Microsoft.DotNet.CoreSetup.Test; +using Microsoft.NET.HostModel.AppHost.Tests; +using Microsoft.NET.HostModel.MachO; +using Microsoft.NET.HostModel.MachO.CodeSign.Tests; using Xunit; namespace Microsoft.NET.HostModel.Bundle.Tests @@ -313,13 +317,12 @@ public void AssemblyAlignment() [Theory] [InlineData(true)] [InlineData(false)] - [PlatformSpecific(TestPlatforms.OSX)] - public void Codesign(bool shouldCodesign) + public void MacOSBundleIsCodeSigned(bool shouldCodesign) { TestApp app = sharedTestState.App; FileSpec[] fileSpecs = new FileSpec[] { - new FileSpec(Binaries.AppHost.FilePath, BundlerHostName), + new FileSpec(CreateAppHost.PrepareMockMachAppHostFile(app.Location, singleFile: true), BundlerHostName), new FileSpec(app.AppDll, Path.GetRelativePath(app.Location, app.AppDll)), new FileSpec(app.DepsJson, Path.GetRelativePath(app.Location, app.DepsJson)), new FileSpec(app.RuntimeConfigJson, Path.GetRelativePath(app.Location, app.RuntimeConfigJson)), @@ -328,19 +331,24 @@ public void Codesign(bool shouldCodesign) Bundler bundler = CreateBundlerInstance(macosCodesign: shouldCodesign); string bundledApp = bundler.GenerateBundle(fileSpecs); - // Check if the file is signed - CommandResult result = Command.Create("codesign", $"-v {bundledApp}") - .CaptureStdErr() - .CaptureStdOut() - .Execute(expectedToFail: !shouldCodesign); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // SingleFile is still only signed on MacOS with codesign + SigningTests.IsSigned(bundledApp).Should().BeFalse(); + return; + } + // Check if the file is signed + var result = Codesign.Run("-v", bundledApp); if (shouldCodesign) { - result.Should().Pass(); + result.ExitCode.Should().Be(0); } else { - result.Should().Fail(); + result.ExitCode.Should().NotBe(0); + // Ensure we can sign it again + Codesign.Run("-s -", bundledApp).ExitCode.Should().Be(0); } }