From 1fa6bd299e365c6e3968f8a5ba45bd7a2adfd5c5 Mon Sep 17 00:00:00 2001 From: vsadov Date: Mon, 4 Jan 2021 15:52:17 -0800 Subject: [PATCH 1/2] extend the __LINKEDIT. section to cover the single-exe metadata. --- .../AppHost/AppHostMachOFormatException.cs | 7 +- .../AppHost/HostWriter.cs | 3 + .../AppHost/MachOUtils.cs | 132 +++++++++++++++++- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs index c52ef11d3b72ba..60e6d74ec591e9 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs @@ -13,8 +13,8 @@ public enum MachOFormatError Not64BitExe, // Apphost is expected to be a 64-bit MachO executable DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost DuplicateSymtab, // Only one SYMTAB is expected in the apphost - SignNeedsLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT - SignNeedsSymtab, // CODE_SIGNATURE command must follow the SYMTAB command + MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT + MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment! SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment! @@ -22,7 +22,8 @@ public enum MachOFormatError SignBlobNotLast, // Signature blob must be at the very end of the file SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab MemoryMapAccessFault, // Error reading the memory-mapped apphost - InvalidUTF8 // UTF8 decoding failed + InvalidUTF8, // UTF8 decoding failed + SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle) } /// diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs index 47d7c8a8c8e200..6c20515db7240c 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs @@ -172,6 +172,9 @@ public static void SetAsBundle( BitConverter.GetBytes(bundleHeaderOffset), pad0s: false)); + RetryUtil.RetryOnIOError(() => + MachOUtils.AdjustHeadersForBundle(appHostPath)); + // Memory-mapped write does not updating last write time RetryUtil.RetryOnIOError(() => File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow)); diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs index 251fe853767559..cf2d88bc867204 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs @@ -214,7 +214,6 @@ public static unsafe bool RemoveSignature(string filePath) using (var accessor = mappedFile.CreateViewAccessor()) { byte* file = null; - RuntimeHelpers.PrepareConstrainedRegions(); try { accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file); @@ -264,8 +263,8 @@ public static unsafe bool RemoveSignature(string filePath) if (signature != null) { - Verify(linkEdit != null, MachOFormatError.SignNeedsLinkEdit); - Verify(symtab != null, MachOFormatError.SignNeedsSymtab); + Verify(linkEdit != null, MachOFormatError.MissingLinkEdit); + Verify(symtab != null, MachOFormatError.MissingSymtab); var symtabEnd = symtab->stroff + symtab->strsize; var linkEditEnd = linkEdit->fileoff + linkEdit->filesize; @@ -319,5 +318,132 @@ public static unsafe bool RemoveSignature(string filePath) return false; } } + + /// + /// This Method is a utility to adjust the apphost MachO-header + /// to include the bytes added by the single-file bundler at the end of the file. + /// + /// The tool assumes the following layout of the executable + /// + /// * MachoHeader (64-bit, executable, not swapped integers) + /// * LoadCommands + /// LC_SEGMENT_64 (__PAGEZERO) + /// LC_SEGMENT_64 (__TEXT) + /// LC_SEGMENT_64 (__DATA) + /// LC_SEGMENT_64 (__LINKEDIT) + /// ... + /// LC_SYMTAB + /// + /// * ... Different Segments + /// + /// * The __LINKEDIT Segment (last) + /// * ... Different sections ... + /// * SYMTAB (last) + /// + /// The MAC codesign tool places several restrictions on the layout + /// * The __LINKEDIT segment must be the last one + /// * The __LINKEDIT segment must cover the end of the file + /// * All bytes in the __LINKEDIT segment are used by other linkage commands + /// (ex: symbol/string table, dynamic load information etc) + /// + /// In order to circumvent these restrictions, we: + /// * Extend the __LINKEDIT segment to include the bundle-data + /// * Extend the string table to include all the bundle-data + /// (that is, the bundle-data appear as strings to the loader/codesign tool). + /// + /// This method has certain limitations: + /// * The bytes for the bundler may be unnecessarily loaded at startup + /// * Tools that process the string table may be confused (?) + /// * The string table size is limited to 4GB. Bundles larger than that size + /// cannot be accomodated by this utility. + /// + /// + /// Path to the AppHost + /// + /// True if + /// - The input is a MachO binary, and + /// - The additional bytes were successfully accomodated within the MachO segments. + /// False otherwise + /// + /// + /// The input is a MachO file, but doesn't match the expect format of the AppHost. + /// + public static unsafe bool AdjustHeadersForBundle(string filePath) + { + ulong fileLength = (ulong)new FileInfo(filePath).Length; + using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath)) + { + using (var accessor = mappedFile.CreateViewAccessor()) + { + byte* file = null; + try + { + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file); + Verify(file != null, MachOFormatError.MemoryMapAccessFault); + + MachHeader* header = (MachHeader*)file; + + if (!header->IsValid()) + { + // Not a MachO file. + return false; + } + + Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe); + + file += sizeof(MachHeader); + SegmentCommand64* linkEdit = null; + SymtabCommand* symtab = null; + LinkEditDataCommand* signature = null; + + for (uint i = 0; i < header->ncmds; i++) + { + LoadCommand* command = (LoadCommand*)file; + if (command->cmd == Command.LC_SEGMENT_64) + { + SegmentCommand64* segment = (SegmentCommand64*)file; + if (segment->SegName.Equals("__LINKEDIT")) + { + Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit); + linkEdit = segment; + } + } + else if (command->cmd == Command.LC_SYMTAB) + { + Verify(symtab == null, MachOFormatError.DuplicateSymtab); + symtab = (SymtabCommand*)command; + } + + file += command->cmdsize; + } + + Verify(linkEdit != null, MachOFormatError.MissingLinkEdit); + Verify(symtab != null, MachOFormatError.MissingSymtab); + + // Update the string table to include bundle-data + ulong newStringTableSize = fileLength - symtab->stroff; + if (newStringTableSize > uint.MaxValue) + { + // Too big, too bad; + return false; + } + symtab->strsize = (uint)newStringTableSize; + + // Update the __LINKEDIT segment to include bundle-data + linkEdit->filesize = fileLength - linkEdit->fileoff; + linkEdit->vmsize = linkEdit->filesize; + } + finally + { + if (file != null) + { + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + } + } + } + } + + return true; + } } } From 5244a880e618e7822d6913d74a989bba134b618e Mon Sep 17 00:00:00 2001 From: vsadov Date: Tue, 5 Jan 2021 17:01:53 -0800 Subject: [PATCH 2/2] codesign test --- .../BundleAndRun.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Bundle.Tests/BundleAndRun.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Bundle.Tests/BundleAndRun.cs index d76af212452795..d717e0d45d12d9 100644 --- a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Bundle.Tests/BundleAndRun.cs +++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Bundle.Tests/BundleAndRun.cs @@ -7,6 +7,7 @@ using Microsoft.DotNet.Cli.Build.Framework; using Microsoft.DotNet.CoreSetup.Test; using BundleTests.Helpers; +using System.Runtime.InteropServices; namespace Microsoft.NET.HostModel.Tests { @@ -31,6 +32,20 @@ private void RunTheApp(string path) .HaveStdOutContaining("Wow! We now say hello to the big world and you."); } + private void CheckFileNotarizable(string path) + { + // attempt to remove signature data. + // no-op if the file is not signed (it should not be) + // fail if the file structure is malformed + // i: input, o: output, r: remove + Command.Create("codesign_allocate", $"-i {path} -o {path} -r") + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should() + .Pass(); + } + private void BundleRun(TestProjectFixture fixture, string publishPath) { var hostName = BundleHelper.GetHostName(fixture); @@ -41,6 +56,13 @@ private void BundleRun(TestProjectFixture fixture, string publishPath) // Bundle to a single-file string singleFile = BundleHelper.BundleApp(fixture); + // check that the file structure is understood by codesign + var targetOS = BundleHelper.GetTargetOS(fixture.CurrentRid); + if (targetOS == OSPlatform.OSX) + { + CheckFileNotarizable(singleFile); + } + // Run the extracted app RunTheApp(singleFile); }