signable single file apps on mac (#46558)
authorVladimir Sadov <vsadov@microsoft.com>
Thu, 14 Jan 2021 00:02:49 +0000 (16:02 -0800)
committerGitHub <noreply@github.com>
Thu, 14 Jan 2021 00:02:49 +0000 (16:02 -0800)
* extend the __LINKEDIT. section to cover the single-exe metadata.

* codesign test

src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs
src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Bundle.Tests/BundleAndRun.cs

index c52ef11..60e6d74 100644 (file)
@@ -13,8 +13,8 @@ namespace Microsoft.NET.HostModel.AppHost
         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 @@ namespace Microsoft.NET.HostModel.AppHost
         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)
     }
 
     /// <summary>
index 47d7c8a..6c20515 100644 (file)
@@ -172,6 +172,9 @@ namespace Microsoft.NET.HostModel.AppHost
                                              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));
index 251fe85..cf2d88b 100644 (file)
@@ -214,7 +214,6 @@ namespace Microsoft.NET.HostModel.AppHost
                     using (var accessor = mappedFile.CreateViewAccessor())
                     {
                         byte* file = null;
-                        RuntimeHelpers.PrepareConstrainedRegions();
                         try
                         {
                             accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
@@ -264,8 +263,8 @@ namespace Microsoft.NET.HostModel.AppHost
 
                             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 @@ namespace Microsoft.NET.HostModel.AppHost
                 return false;
             }
         }
+
+        /// <summary>
+        /// 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.
+        ///
+        /// </summary>
+        /// <param name="filePath">Path to the AppHost</param>
+        /// <returns>
+        ///  True if
+        ///    - The input is a MachO binary, and
+        ///    - The additional bytes were successfully accomodated within the MachO segments.
+        ///   False otherwise
+        /// </returns>
+        /// <exception cref="AppHostMachOFormatException">
+        /// The input is a MachO file, but doesn't match the expect format of the AppHost.
+        /// </exception>
+        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;
+        }
     }
 }
index d76af21..d717e0d 100644 (file)
@@ -7,6 +7,7 @@ using Xunit;
 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 @@ namespace Microsoft.NET.HostModel.Tests
                 .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 @@ namespace Microsoft.NET.HostModel.Tests
             // 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);
         }