From 7dc29290ac622df69471018d03ae460265e0b1c1 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Fri, 13 Jun 2025 12:58:48 -0700
Subject: [PATCH 01/16] Preserve entitlements when signing mach files

---
 .../AppHost/BinaryUtils.cs                    |   8 +-
 .../AppHost/HostWriter.cs                     | 164 ++--------
 .../PlaceHolderNotFoundInAppHostException.cs  |   3 +
 .../Microsoft.NET.HostModel/Bundle/Bundler.cs | 276 +++++++++++++----
 .../Bundle/FileEntry.cs                       |  16 +
 .../Bundle/Manifest.cs                        |  24 ++
 .../BinaryFormat/Blobs/CodeDirectoryBlob.cs   |  19 ++
 .../BinaryFormat/Blobs/DerEntitlementsBlob.cs |  35 +++
 .../Blobs/EmbeddedSignatureBlob.cs            |  90 +++++-
 .../BinaryFormat/Blobs/EntitlementsBlob.cs    |  39 +++
 .../MachO/Enums/BlobMagic.cs                  |   2 +
 .../MachO/Enums/CodeDirectorySpecialSlot.cs   |   2 +
 .../MachO/MachObjectFile.cs                   | 110 ++++---
 .../ResourceUpdater.cs                        |  40 ++-
 .../MachOHostSigningTests.cs                  |  39 +++
 .../MachObjectSigning/MachObjectTests.cs      |  99 +++++++
 .../MachObjectSigning/SigningTests.cs         | 280 +++++++++---------
 .../corehost/apphost/static/CMakeLists.txt    |   4 +
 18 files changed, 847 insertions(+), 403 deletions(-)
 create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/DerEntitlementsBlob.cs
 create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EntitlementsBlob.cs
 create mode 100644 src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs

diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
index 62597baeaeafc4..e32fe97a554df2 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
@@ -11,7 +11,7 @@ public static class BinaryUtils
     {
         internal static unsafe void SearchAndReplace(
             MemoryMappedViewAccessor accessor,
-            byte[] searchPattern,
+            ReadOnlySpan<byte> searchPattern,
             byte[] patternToReplace,
             bool pad0s = true)
         {
@@ -48,7 +48,7 @@ internal static unsafe void SearchAndReplace(
             }
         }
 
-        private static unsafe void Pad0(byte[] searchPattern, byte[] patternToReplace, byte* bytes, int offset)
+        private static unsafe void Pad0(ReadOnlySpan<byte> searchPattern, ReadOnlySpan<byte> patternToReplace, byte* bytes, int offset)
         {
             if (patternToReplace.Length < searchPattern.Length)
             {
@@ -92,7 +92,7 @@ public static unsafe int SearchInFile(string filePath, byte[] searchPattern)
         }
 
         // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
-        private static int[] ComputeKMPFailureFunction(byte[] pattern)
+        private static int[] ComputeKMPFailureFunction(ReadOnlySpan<byte> pattern)
         {
             int[] table = new int[pattern.Length];
             if (pattern.Length >= 1)
@@ -128,7 +128,7 @@ private static int[] ComputeKMPFailureFunction(byte[] pattern)
         }
 
         // See: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
-        private static unsafe int KMPSearch(byte[] pattern, byte* bytes, long bytesLength)
+        private static unsafe int KMPSearch(ReadOnlySpan<byte> pattern, byte* bytes, long bytesLength)
         {
             int m = 0;
             int i = 0;
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
index 7de2c6cca43872..f0a64226fcb39a 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
@@ -2,11 +2,13 @@
 // 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.IO.MemoryMappedFiles;
 using System.Runtime.InteropServices;
 using System.Text;
+using Microsoft.NET.HostModel.Bundle;
 using Microsoft.NET.HostModel.MachO;
 
 namespace Microsoft.NET.HostModel.AppHost
@@ -124,8 +126,14 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
                     if (File.Exists(appHostDestinationFilePath))
                         File.Delete(appHostDestinationFilePath);
 
-                    using (FileStream appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.CreateNew, FileAccess.ReadWrite))
+                    long appHostSourceLength = new FileInfo(appHostSourceFilePath).Length;
+                    string destinationFileName = Path.GetFileName(appHostDestinationFilePath);
+                    long appHostDestinationLength = enableMacOSCodeSign ?
+                        appHostSourceLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostSourceLength, destinationFileName)
+                        : appHostSourceLength;
+                    using (var appHostDestinationMap = MemoryMappedFile.CreateNew(null, appHostDestinationLength))
                     {
+                        using (var appHostDestinationStream = appHostDestinationMap.CreateViewStream())
                         using (FileStream appHostSourceStream = new(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
                         {
                             isMachOImage = MachObjectFile.IsMachOImage(appHostSourceStream);
@@ -135,42 +143,39 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
                             }
                             appHostSourceStream.CopyTo(appHostDestinationStream);
                         }
-                        // Get the size of the source app host to ensure that we don't write extra data to the destination.
-                        // On Windows, the size of the view accessor is rounded up to the next page boundary.
-                        long appHostLength = appHostDestinationStream.Length;
-                        string destinationFileName = Path.GetFileName(appHostDestinationFilePath);
-                        // On Mac, we need to extend the file size to accommodate the signature.
-                        long appHostTmpCapacity = enableMacOSCodeSign ?
-                            appHostLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostLength, destinationFileName)
-                            : appHostLength;
 
-                        using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationStream, null, appHostTmpCapacity, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
-                        using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, appHostTmpCapacity, MemoryMappedFileAccess.ReadWrite))
+                        using (var memoryMappedViewAccessor = appHostDestinationMap.CreateViewAccessor())
                         {
+                            // Get the size of the source app host to ensure that we don't write extra data to the destination.
+                            // On Windows, the size of the view accessor is rounded up to the next page boundary.
                             // Transform the host file in-memory.
-                            RewriteAppHost(memoryMappedFile, memoryMappedViewAccessor);
+                            RewriteAppHost(appHostDestinationMap, memoryMappedViewAccessor);
                             if (isMachOImage)
                             {
                                 IMachOFileAccess file = new MemoryMappedMachOViewAccessor(memoryMappedViewAccessor);
+                                MachObjectFile machObjectFile = MachObjectFile.Create(file);
                                 if (enableMacOSCodeSign)
                                 {
-                                    MachObjectFile machObjectFile = MachObjectFile.Create(file);
-                                    appHostLength = machObjectFile.AdHocSignFile(file, destinationFileName);
+                                    appHostDestinationLength = machObjectFile.AdHocSignFile(file, destinationFileName);
                                 }
-                                else if (MachObjectFile.RemoveCodeSignatureIfPresent(file, out long? length))
+                                else if (machObjectFile.RemoveCodeSignatureIfPresent(file, out long? length))
                                 {
-                                    appHostLength = length.Value;
+                                    appHostDestinationLength = length.Value;
                                 }
                             }
                         }
-                        appHostDestinationStream.SetLength(appHostLength);
-
                         if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
                         {
-                            using var updater = new ResourceUpdater(appHostDestinationStream, true);
+                            using var updater = new ResourceUpdater(appHostDestinationMap, true);
                             updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
                             updater.Update();
                         }
+                        using (var appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 1))
+                        using (var appHostAccessor = appHostDestinationMap.CreateViewAccessor(0, appHostDestinationLength, MemoryMappedFileAccess.Read))
+                        {
+                            // Write the final content to the destination file.
+                            BinaryUtils.WriteToStream(appHostAccessor, appHostDestinationStream, appHostDestinationLength);
+                        }
                     }
                 });
                 Chmod755(appHostDestinationFilePath);
@@ -191,125 +196,6 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
             }
         }
 
-        /// <summary>
-        /// Set the current AppHost as a single-file bundle.
-        /// </summary>
-        /// <param name="appHostPath">The path of Apphost template, which has the place holder</param>
-        /// <param name="bundleHeaderOffset">The offset to the location of bundle header</param>
-        /// <param name="macosCodesign">Whether to ad-hoc sign the bundle as a Mach-O executable</param>
-        public static void SetAsBundle(
-            string appHostPath,
-            long bundleHeaderOffset,
-            bool macosCodesign = false)
-        {
-            byte[] bundleHeaderPlaceholder = {
-                // 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
-            };
-
-            // Re-write the destination apphost with the proper contents.
-            RetryUtil.RetryOnIOError(() =>
-            {
-                string tmpFile = null;
-                try
-                {
-                    // MacOS keeps a cache of file signatures. To avoid using the cached value,
-                    // we need to create a new inode with the contents of the old file, sign it,
-                    // and copy it the original file path.
-                    tmpFile = Path.GetTempFileName();
-                    using (FileStream newBundleStream = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite))
-                    {
-                        using (FileStream oldBundleStream = new FileStream(appHostPath, FileMode.Open, FileAccess.Read))
-                        {
-                            oldBundleStream.CopyTo(newBundleStream);
-                        }
-
-                        long bundleSize = newBundleStream.Length;
-                        long mmapFileSize = macosCodesign
-                            ? bundleSize + MachObjectFile.GetSignatureSizeEstimate((uint)bundleSize, Path.GetFileName(appHostPath))
-                            : bundleSize;
-                        using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(newBundleStream, null, mmapFileSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, leaveOpen: true))
-                        using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
-                        {
-                            BinaryUtils.SearchAndReplace(accessor,
-                                                        bundleHeaderPlaceholder,
-                                                        BitConverter.GetBytes(bundleHeaderOffset),
-                                                        pad0s: false);
-
-                            var file = new MemoryMappedMachOViewAccessor(accessor);
-                            if (MachObjectFile.IsMachOImage(file))
-                            {
-                                var machObjectFile = MachObjectFile.Create(file);
-                                if (machObjectFile.HasSignature)
-                                    throw new AppHostMachOFormatException(MachOFormatError.SignNotRemoved);
-
-                                bool wasBundled = machObjectFile.TryAdjustHeadersForBundle((ulong)bundleSize, file);
-                                if (!wasBundled)
-                                    throw new InvalidOperationException("The single-file bundle was unable to be created. This is likely because the bundled content is too large.");
-
-                                if (macosCodesign)
-                                    bundleSize = machObjectFile.AdHocSignFile(file, Path.GetFileName(appHostPath));
-                            }
-                        }
-                        newBundleStream.SetLength(bundleSize);
-                    }
-                    File.Copy(tmpFile, appHostPath, overwrite: true);
-                    Chmod755(appHostPath);
-                }
-                finally
-                {
-                    if (tmpFile is not null)
-                        File.Delete(tmpFile);
-                }
-            });
-        }
-
-        /// <summary>
-        /// Check if the an AppHost is a single-file bundle
-        /// </summary>
-        /// <param name="appHostFilePath">The path of Apphost to check</param>
-        /// <param name="bundleHeaderOffset">An out parameter containing the offset of the bundle header (if any)</param>
-        /// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
-        public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
-        {
-            byte[] bundleSignature = {
-                // 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
-            };
-
-            long headerOffset = 0;
-            void FindBundleHeader()
-            {
-                using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read))
-                {
-                    using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
-                    {
-                        int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
-                        if (position == -1)
-                        {
-                            throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
-                        }
-
-                        headerOffset = accessor.ReadInt64(position - sizeof(long));
-                    }
-                }
-            }
-
-            RetryUtil.RetryOnIOError(FindBundleHeader);
-            bundleHeaderOffset = headerOffset;
-
-            return headerOffset != 0;
-        }
-
         private static byte[] GetSearchOptionBytes(DotNetSearchOptions searchOptions)
         {
             if (Path.IsPathRooted(searchOptions.AppRelativeDotNet))
@@ -333,7 +219,7 @@ private static byte[] GetSearchOptionBytes(DotNetSearchOptions searchOptions)
             return searchOptionsBytes;
         }
 
-        private static void Chmod755(string pathName)
+        internal static void Chmod755(string pathName)
         {
             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                 return;
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs
index 4268e640154507..b2b21322faf5ac 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs
@@ -16,5 +16,8 @@ public PlaceHolderNotFoundInAppHostException(byte[] pattern)
         {
             MissingPattern = pattern;
         }
+        public PlaceHolderNotFoundInAppHostException(ReadOnlySpan<byte> pattern)
+        {
+        }
     }
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index bbe567852c922b..acbceae6a83906 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -1,11 +1,15 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+#nullable enable
+
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 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;
@@ -41,9 +45,9 @@ public Bundler(string hostName,
                        BundleOptions options = BundleOptions.None,
                        OSPlatform? targetOS = null,
                        Architecture? targetArch = null,
-                       Version targetFrameworkVersion = null,
+                       Version? targetFrameworkVersion = null,
                        bool diagnosticOutput = false,
-                       string appAssemblyName = null,
+                       string? appAssemblyName = null,
                        bool macosCodesign = true)
         {
             _tracer = new Trace(diagnosticOutput);
@@ -93,7 +97,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
         /// </returns>
-        private (long startOffset, long compressedSize) AddToBundle(FileStream bundle, FileStream file, FileType type)
+        private (long startOffset, long compressedSize) AddToBundle(Stream bundle, FileStream file, FileType type)
         {
             long startOffset = bundle.Position;
             if (ShouldCompress(type))
@@ -181,7 +185,7 @@ private static bool IsAssembly(string path, out bool isPE)
                 try
                 {
                     PEReader peReader = new PEReader(file);
-                    CorHeader corHeader = peReader.PEHeaders.CorHeader;
+                    CorHeader? corHeader = peReader.PEHeaders.CorHeader;
 
                     isPE = true; // If peReader.PEHeaders doesn't throw, it is a valid PEImage
                     return corHeader != null;
@@ -227,6 +231,17 @@ private FileType InferType(FileSpec fileSpec)
             return FileType.Unknown;
         }
 
+        public static ImmutableArray<byte> BundleHeaderPlaceholder = [
+            // 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
+        ];
+
         /// <summary>
         /// Generate a bundle, given the specification of embedded files
         /// </summary>
@@ -250,12 +265,10 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
             _tracer.Log($"Bundle  Version: {BundleManifest.BundleVersion}");
             _tracer.Log($"Target Runtime: {_target}");
             _tracer.Log($"Bundler Options: {_options}");
-
             if (fileSpecs.Any(x => !x.IsValid()))
             {
                 throw new ArgumentException("Invalid input specification: Found entry with empty source-path or bundle-relative-path.");
             }
-
             string hostSource;
             try
             {
@@ -266,86 +279,235 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
                 throw new ArgumentException("Invalid input specification: Must specify the host binary");
             }
 
+            var relativePathToSpec = GetFilteredFileSpecs(fileSpecs);
+            long bundledFilesSize = 0;
+            // Conservatively estimate the size of bundled files.
+            // Assume no compression and worst case alignment for assemblies.
+            // There's no way to know the exact compressed sizes without reading the entire file,
+            // which would be expensive.
+            // We will memory map a larger file than needed, but we'll take that trade-off.
+            foreach (var (spec, type) in relativePathToSpec)
+            {
+                bundledFilesSize += new FileInfo(spec.SourcePath).Length;
+                if (type == FileType.Assembly)
+                {
+                    // Alignment could be as much as AssemblyAlignment - 1 bytes.
+                    // Since the files may be compressed when written to the bundle we can't be sure of exactly how much space the padding will require.
+                    // So we'll consvervatively add an additional AssemblyAlignment bytes.
+                    bundledFilesSize += _target.AssemblyAlignment;
+                }
+            }
+
             string bundlePath = Path.Combine(_outputDir, _hostName);
             if (File.Exists(bundlePath))
             {
                 _tracer.Log($"Ovewriting existing File {bundlePath}");
             }
 
-            BinaryUtils.CopyFile(hostSource, 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<string, FileSpec>(StringComparer.Ordinal);
-
-            long headerOffset = 0;
-            using (FileStream bundle = File.Open(bundlePath, FileMode.Open, FileAccess.ReadWrite))
-            using (BinaryWriter writer = new BinaryWriter(bundle, Encoding.Default, leaveOpen: true))
+            string destinationDirectory = new FileInfo(bundlePath).Directory!.FullName;
+            if (!Directory.Exists(destinationDirectory))
             {
-                if (_target.IsOSX)
-                {
-                    MachObjectFile.RemoveCodeSignatureIfPresent(bundle);
-                }
-                bundle.Position = bundle.Length;
-                foreach (var fileSpec in fileSpecs)
+                Directory.CreateDirectory(destinationDirectory);
+            }
+            var bundleName = Path.GetFileName(bundlePath);
+            var hostLength = new FileInfo(hostSource).Length;
+            var bundleManifestLength = BundleManifest.GetManifestLength(BundleManifest.BundleMajorVersion, relativePathToSpec.Select(x => x.Spec.BundleRelativePath));
+            long bundleTotalSize = hostLength + bundledFilesSize + bundleManifestLength;
+            if (_target.IsOSX && _macosCodesign)
+                bundleTotalSize += MachObjectFile.GetSignatureSizeEstimate((uint)bundleTotalSize, bundleName);
+
+            using (MemoryMappedFile bundleMap = MemoryMappedFile.CreateNew(null, bundleTotalSize, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None, HandleInheritability.None))
+            {
+                long endOfHost;
+                long headerOffset;
+                using (MemoryMappedViewAccessor accessor = bundleMap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
+                using (MemoryMappedViewStream bundleStream = bundleMap.CreateViewStream(0, 0, MemoryMappedFileAccess.ReadWrite))
                 {
-                    string relativePath = fileSpec.BundleRelativePath;
-
-                    if (IsHost(relativePath))
+                    using (FileStream hostSourceStream = File.OpenRead(hostSource))
                     {
-                        continue;
+                        hostSourceStream.CopyTo(bundleStream);
                     }
+                    endOfHost = bundleStream.Position;
 
-                    if (ShouldIgnore(relativePath))
+                    Debug.Assert(endOfHost == hostLength, $"Host file size on disk does not match bytes written to the bundle. Expected {hostLength}, but got {endOfHost}. This may indicate that the host file is not a valid native binary or that it is not a single-file apphost.");
+                    MachObjectFile? machFile = null;
+                    EmbeddedSignatureBlob? signatureBlob = null;
+                    IMachOFileAccess machFileReader = null!;
+                    if (_target.IsOSX)
                     {
-                        _tracer.Log($"Ignore: {relativePath}");
-                        continue;
+                        machFileReader = new StreamBasedMachOFile(bundleStream);
+                        machFile = MachObjectFile.Create(machFileReader);
+                        signatureBlob = machFile.EmbeddedSignatureBlob;
+                        if (machFile.RemoveCodeSignatureIfPresent(machFileReader, out long? newEnd))
+                        {
+                            endOfHost = newEnd!.Value;
+                        }
                     }
-
-                    FileType type = InferType(fileSpec);
-
-                    if (ShouldExclude(type, relativePath))
+                    bundleStream.Position = endOfHost;
+                    foreach (var kvp in relativePathToSpec)
+                    {
+                        FileSpec fileSpec = kvp.Spec;
+                        FileType type = kvp.Type;
+                        string relativePath = fileSpec.BundleRelativePath;
+                        using (FileStream file = File.OpenRead(fileSpec.SourcePath))
+                        {
+                            FileType targetType = _target.TargetSpecificFileType(type);
+                            (long startOffset, long compressedSize) = AddToBundle(bundleStream, file, targetType);
+                            FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, _target.BundleMajorVersion);
+                            _tracer.Log($"Embed: {entry}");
+                        }
+                    }
+                    Debug.Assert(bundleStream.Position - endOfHost <= bundledFilesSize, $"Not enough space allocated for bundled files. Allocated {bundledFilesSize}, but written {bundleStream.Position - endOfHost}");
+                    var endOfBundledFiles = bundleStream.Position;
+                    using (BinaryWriter writer = new BinaryWriter(bundleStream, Encoding.UTF8, leaveOpen: true))
                     {
-                        _tracer.Log($"Exclude [{type}]: {relativePath}");
-                        fileSpec.Excluded = true;
-                        continue;
+                        // Write the bundle manifest
+                        headerOffset = BundleManifest.Write(writer);
+                        _tracer.Log($"Header Offset={headerOffset}");
+                        _tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}");
+                        _tracer.Log($"Bundle: Path={bundlePath}, Size={bundleStream.Length}");
                     }
-
-                    if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
+                    ulong endOfBundle = (ulong)bundleStream.Position;
+                    Debug.Assert((long)endOfBundle == endOfBundledFiles + bundleManifestLength, $"Bundle manifest is unexpected size. Expected {bundleManifestLength}, but got {(long)endOfBundle - endOfBundledFiles}");
+                    BinaryUtils.SearchAndReplace(accessor,
+                                                BundleHeaderPlaceholder.AsSpan(),
+                                                BitConverter.GetBytes(headerOffset),
+                                                pad0s: false);
+                    if (_target.IsOSX && machFile is not null)
                     {
-                        if (!string.Equals(fileSpec.SourcePath, existingFileSpec.SourcePath, StringComparison.Ordinal))
+                        Debug.Assert(machFileReader is not null, "MachO file reader should not be null if the target is macOS.");
+                        if (!machFile.TryAdjustHeadersForBundle(endOfBundle, machFileReader!))
+                        {
+                            throw new InvalidOperationException("The single-file bundle was unable to be created. This is likely because the bundled content is too large.");
+                        }
+                        if (_macosCodesign)
                         {
-                            throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
+                            endOfBundle = (ulong)machFile.AdHocSignFile(machFileReader!, bundleName, signatureBlob);
                         }
+                    }
 
-                        // Exact duplicate - intentionally skip and don't include a second copy in the bundle
-                        continue;
+                    // 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))
+                    {
+                        _tracer.Log($"Removing existing bundle file to clear signature cache: {bundlePath}");
+                        File.Delete(bundlePath);
                     }
-                    else
+                    using (FileStream bundleOutputStream = File.Open(bundlePath, FileMode.Create, FileAccess.Write, FileShare.None))
                     {
-                        relativePathToSpec.Add(fileSpec.BundleRelativePath, fileSpec);
+                        BinaryUtils.WriteToStream(accessor, bundleOutputStream, (long)endOfBundle);
                     }
+                }
+            }
+            HostWriter.Chmod755(bundlePath);
+            return bundlePath;
+        }
+
+        /// <summary>
+        /// Check if the an AppHost is a single-file bundle
+        /// </summary>
+        /// <param name="appHostFilePath">The path of Apphost to check</param>
+        /// <param name="bundleHeaderOffset">An out parameter containing the offset of the bundle header (if any)</param>
+        /// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
+        public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
+        {
+            byte[] bundleSignature = {
+                // 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
+            };
 
-                    using (FileStream file = File.OpenRead(fileSpec.SourcePath))
+            long headerOffset = 0;
+            void FindBundleHeader()
+            {
+                using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostFilePath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read))
+                {
+                    using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
                     {
-                        FileType targetType = _target.TargetSpecificFileType(type);
-                        (long startOffset, long compressedSize) = AddToBundle(bundle, file, targetType);
-                        FileEntry entry = BundleManifest.AddEntry(targetType, file, relativePath, startOffset, compressedSize, _target.BundleMajorVersion);
-                        _tracer.Log($"Embed: {entry}");
+                        int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
+                        if (position == -1)
+                        {
+                            throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
+                        }
+
+                        headerOffset = accessor.ReadInt64(position - sizeof(long));
                     }
                 }
-
-                // Write the bundle manifest
-                headerOffset = BundleManifest.Write(writer);
-                _tracer.Log($"Header Offset={headerOffset}");
-                _tracer.Log($"Meta-data Size={writer.BaseStream.Position - headerOffset}");
-                _tracer.Log($"Bundle: Path={bundlePath}, Size={bundle.Length}");
             }
 
-            HostWriter.SetAsBundle(bundlePath, headerOffset, _macosCodesign);
+            RetryUtil.RetryOnIOError(FindBundleHeader);
+            bundleHeaderOffset = headerOffset;
 
-            return bundlePath;
+            return headerOffset != 0;
+        }
+
+        private (FileSpec Spec, FileType Type)[] GetFilteredFileSpecs(IEnumerable<FileSpec> fileSpecs)
+        {
+            // 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<string, (FileSpec Spec, FileType Type)>(StringComparer.Ordinal);
+            foreach (var fileSpec in fileSpecs)
+            {
+                string relativePath = fileSpec.BundleRelativePath;
+
+                if (IsHost(relativePath))
+                {
+                    continue;
+                }
+
+                if (ShouldIgnore(relativePath))
+                {
+                    _tracer.Log($"Ignore: {relativePath}");
+                    continue;
+                }
+
+                FileType type = InferType(fileSpec);
+
+                if (ShouldExclude(type, relativePath))
+                {
+                    _tracer.Log($"Exclude [{type}]: {relativePath}");
+                    fileSpec.Excluded = true;
+                    continue;
+                }
+
+                if (relativePathToSpec.TryGetValue(fileSpec.BundleRelativePath, out var existingFileSpec))
+                {
+                    if (!string.Equals(fileSpec.SourcePath, existingFileSpec.Spec.SourcePath, StringComparison.Ordinal))
+                    {
+                        throw new ArgumentException($"Invalid input specification: Found entries '{fileSpec.SourcePath}' and '{existingFileSpec.Spec.SourcePath}' with the same BundleRelativePath '{fileSpec.BundleRelativePath}'");
+                    }
+
+                    // Exact duplicate - intentionally skip and don't include a second copy in the bundle
+                    continue;
+                }
+                else
+                {
+                    relativePathToSpec.Add(fileSpec.BundleRelativePath, (fileSpec, type));
+                }
+            }
+            return relativePathToSpec.Values.ToArray();
+        }
+
+        /// <summary>
+        /// Get the length of the string when written to a BinaryWriter.
+        /// </summary>
+        internal static uint GetBinaryWriterStringLength(string str)
+        {
+            // 1 byte for the length prefix + length of the string in bytes
+            uint stringLength = (uint)Encoding.UTF8.GetByteCount(str); // BundleID with prefixed length
+            // Prefixed length of bundle ID is 7-bit encoded
+            // Strings 0-127 chars: 1 byte prefix
+            // Strings 128-16,383 chars: 2 byte prefix
+            // Strings 16,384-2,097,151 chars: 3 byte prefix
+            // Strings 2,097,152-268,435,455 chars: 4 byte prefix
+            // Strings 268,435,456+ chars: 5 byte prefix
+            uint lengthPrefixLength = (stringLength < 128) ? 1u :
+                           (stringLength < 16384) ? 2u :
+                           (stringLength < 2097152) ? 3u :
+                           (stringLength < 268435456) ? 4u : 5u;
+            return lengthPrefixLength + stringLength;
         }
     }
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs
index e71a7faaa45789..8f1b9042b320dc 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs
@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics;
 using System.IO;
 
 namespace Microsoft.NET.HostModel.Bundle
@@ -42,6 +43,7 @@ public FileEntry(FileType fileType, string relativePath, long offset, long size,
 
         public void Write(BinaryWriter writer)
         {
+            var start = writer.BaseStream.Position;
             writer.Write(Offset);
             writer.Write(Size);
             // compression is used only in version 6.0+
@@ -51,6 +53,20 @@ public void Write(BinaryWriter writer)
             }
             writer.Write((byte)Type);
             writer.Write(RelativePath);
+            Debug.Assert(writer.BaseStream.Position - start == GetFileEntryLength(BundleMajorVersion, RelativePath),
+                $"FileEntry size mismatch. Expected: {GetFileEntryLength(BundleMajorVersion, RelativePath)}, Actual: {writer.BaseStream.Position - start}");
+        }
+
+        /// <summary>
+        /// Returns the length of the FileEntry in the manifest in bytes. This is not the size of the file itself.
+        /// </summary>
+        public static uint GetFileEntryLength(uint bundleMajorVersion, string bundleRelativePath)
+        {
+           return 8u // Offset
+                    + 8u // Size
+                    + (bundleMajorVersion >= 6 ? 8u : 0u) // CompressedSize
+                    + 1u // Type (FileType)
+                    + Bundler.GetBinaryWriterStringLength(bundleRelativePath);
         }
 
         public override string ToString() => $"{RelativePath} [{Type}] @{Offset} Sz={Size} CompressedSz={CompressedSize}";
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
index 564da05892fceb..7500aa724c756a 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Security.Cryptography;
@@ -165,10 +166,33 @@ public long Write(BinaryWriter writer)
             {
                 entry.Write(writer);
             }
+            Debug.Assert(writer.BaseStream.Position - startOffset == GetManifestLength(BundleMajorVersion, Files.Select(static f => f.RelativePath)),
+                $"Manifest size mismatch: {writer.BaseStream.Position - startOffset} != {GetManifestLength(BundleMajorVersion, Files.Select(static f => f.RelativePath))}");
 
             return startOffset;
         }
 
+        /// <summary>
+        /// Calculates the length of the manifest in bytes.
+        /// </summary>
+        public long GetManifestLength(uint bundleMajorVersion, IEnumerable<string> fileSpecs)
+        {
+            // Size of the header
+            long size = sizeof(uint) * 2 + // BundleMajorVersion + BundleMinorVersion
+                        sizeof(int) + // NumEmbeddedFiles
+                        (bundleMajorVersion >= 2 ? (sizeof(long) * 4 + sizeof(ulong)) : 0); // DepsJson and RuntimeConfigJson offsets and sizes, and Flags
+#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.SHA256.HashData' method over 'ComputeHash'
+            size += Bundler.GetBinaryWriterStringLength(Convert.ToBase64String(SHA256.Create().ComputeHash([])).Substring(BundleIdLength).Replace('/', '_'));
+#pragma warning restore CA1850
+            // Size of each FileEntry
+            foreach (var fileSpec in fileSpecs)
+            {
+                size += FileEntry.GetFileEntryLength(bundleMajorVersion, fileSpec);
+            }
+
+            return size;
+        }
+
         public bool Contains(string relativePath)
         {
             return Files.Any(entry => relativePath.Equals(entry.RelativePath));
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index 4d95ca62d7b1da..ca307270f38386 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -102,6 +102,8 @@ public static CodeDirectoryBlob Create(
         long signatureStart,
         string identifier,
         RequirementsBlob requirementsBlob,
+        EntitlementsBlob? entitlementsBlob = null,
+        DerEntitlementsBlob? derEntitlementsBlob = null,
         HashType hashType = HashType.SHA256,
         uint pageSize = MachObjectFile.DefaultPageSize)
     {
@@ -121,12 +123,29 @@ public static CodeDirectoryBlob Create(
         // Fill in the CodeDirectory hashes
 
         // Special slot hashes
+        // -7 is the der entitlements blob hash
+        if (derEntitlementsBlob != null)
+        {
+            using var derStream = new MemoryStreamWriter((int)derEntitlementsBlob.Size);
+            derEntitlementsBlob.Write(derStream, 0);
+            specialSlotHashes[(int)CodeDirectorySpecialSlot.DerEntitlements - 1] = hasher.ComputeHash(derStream.GetBuffer());
+        }
+
+        // -5 is the entitlements blob hash
+        if (entitlementsBlob != null)
+        {
+            using var entStream = new MemoryStreamWriter((int)entitlementsBlob.Size);
+            entitlementsBlob.Write(entStream, 0);
+            specialSlotHashes[(int)CodeDirectorySpecialSlot.Entitlements - 1] = hasher.ComputeHash(entStream.GetBuffer());
+        }
+
         // -2 is the requirements blob hash
         using (var reqStream = new MemoryStreamWriter((int)requirementsBlob.Size))
         {
             requirementsBlob.Write(reqStream, 0);
             specialSlotHashes[(int)CodeDirectorySpecialSlot.Requirements - 1] = hasher.ComputeHash(reqStream.GetBuffer());
         }
+
         // -1 is the CMS blob hash (which is empty -- nothing to hash)
 
         // Reverse special slot hashes
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/DerEntitlementsBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/DerEntitlementsBlob.cs
new file mode 100644
index 00000000000000..4b0a13b94252d0
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/DerEntitlementsBlob.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+internal sealed class DerEntitlementsBlob : IBlob
+{
+    private SimpleBlob _inner;
+
+    public DerEntitlementsBlob(SimpleBlob blob)
+    {
+        _inner = blob;
+        if (blob.Size > MaxSize)
+        {
+            throw new InvalidDataException($"DerEntitlementsBlob size exceeds maximum allowed size: {blob.Data.Length} > {MaxSize}");
+        }
+        if (blob.Magic != BlobMagic.DerEntitlements)
+        {
+            throw new InvalidDataException($"Invalid magic for DerEntitlementsBlob: {blob.Magic}");
+        }
+    }
+
+    public static uint MaxSize => 1024;
+
+    /// <inheritdoc />
+    public BlobMagic Magic => ((IBlob)_inner).Magic;
+
+    /// <inheritdoc />
+    public uint Size => ((IBlob)_inner).Size;
+
+    /// <inheritdoc />
+    public int Write(IMachOFileWriter writer, long offset) => ((IBlob)_inner).Write(writer, offset);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs
index 529cdc547c3144..d90ec264a34bb8 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs
@@ -5,6 +5,7 @@
 
 using System;
 using System.Collections.Immutable;
+using System.Diagnostics;
 using System.IO;
 
 namespace Microsoft.NET.HostModel.MachO;
@@ -35,20 +36,38 @@ public EmbeddedSignatureBlob(SuperBlob superBlob)
     public EmbeddedSignatureBlob(
         CodeDirectoryBlob codeDirectoryBlob,
         RequirementsBlob requirementsBlob,
-        CmsWrapperBlob cmsWrapperBlob)
+        CmsWrapperBlob cmsWrapperBlob,
+        EntitlementsBlob? entitlementsBlob = null,
+        DerEntitlementsBlob? derEntitlementsBlob = null)
     {
-        int blobCount = 3;
+        int blobCount = 3 + (entitlementsBlob is not null ? 1 : 0) + (derEntitlementsBlob is not null ? 1 : 0);
         var blobs = ImmutableArray.CreateBuilder<IBlob>(blobCount);
         var blobIndices = ImmutableArray.CreateBuilder<BlobIndex>(blobCount);
-        uint expectedOffset = (uint)(sizeof(uint) * 3 + (BlobIndex.Size * blobCount));
+        uint nextBlobOffset = (uint)(sizeof(uint) * 3 + (BlobIndex.Size * blobCount));
+
         blobs.Add(codeDirectoryBlob);
-        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.CodeDirectory, expectedOffset));
-        expectedOffset += codeDirectoryBlob.Size;
+        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.CodeDirectory, nextBlobOffset));
+        nextBlobOffset += codeDirectoryBlob.Size;
+
         blobs.Add(requirementsBlob);
-        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.Requirements, expectedOffset));
-        expectedOffset += requirementsBlob.Size;
+        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.Requirements, nextBlobOffset));
+        nextBlobOffset += requirementsBlob.Size;
+
         blobs.Add(cmsWrapperBlob);
-        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.CmsWrapper, expectedOffset));
+        blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.CmsWrapper, nextBlobOffset));
+        nextBlobOffset += cmsWrapperBlob.Size;
+
+        if (entitlementsBlob is not null)
+        {
+            blobs.Add(entitlementsBlob);
+            blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.Entitlements, nextBlobOffset));
+            nextBlobOffset += entitlementsBlob.Size;
+        }
+        if (derEntitlementsBlob is not null)
+        {
+            blobs.Add(derEntitlementsBlob);
+            blobIndices.Add(new BlobIndex(CodeDirectorySpecialSlot.DerEntitlements, nextBlobOffset));
+        }
         _inner = new SuperBlob(BlobMagic.EmbeddedSignature, blobIndices.MoveToImmutable(), blobs.MoveToImmutable());
     }
 
@@ -71,6 +90,16 @@ public EmbeddedSignatureBlob(
     /// </summary>
     public CmsWrapperBlob? CmsWrapperBlob => GetBlob(BlobMagic.CmsWrapper) as CmsWrapperBlob;
 
+    /// <summary>
+    /// The EntitlementsBlob. This is only included in created signatures if present in the original signature.
+    /// </summary>
+    public EntitlementsBlob? EntitlementsBlob => GetBlob(BlobMagic.Entitlements) as EntitlementsBlob;
+
+    /// <summary>
+    /// The DerEntitlementsBlob. This is only included in created signatures if present in the original signature.
+    /// </summary>
+    public DerEntitlementsBlob? DerEntitlementsBlob => GetBlob(BlobMagic.DerEntitlements) as DerEntitlementsBlob;
+
     public uint GetSpecialSlotHashCount()
     {
         uint maxSlot = 0;
@@ -84,6 +113,7 @@ public uint GetSpecialSlotHashCount()
                 maxSlot = slot;
             }
         }
+        Debug.Assert((CodeDirectorySpecialSlot)maxSlot is 0 or CodeDirectorySpecialSlot.Requirements or CodeDirectorySpecialSlot.Entitlements or CodeDirectorySpecialSlot.DerEntitlements);
         return maxSlot;
     }
 
@@ -104,7 +134,7 @@ public static unsafe long GetLargestSizeEstimate(uint fileSize, string identifie
         size += sizeof(BlobMagic);
         size += sizeof(uint); // Blob size
         size += sizeof(uint); // Blob count
-        size += sizeof(BlobIndex) * 3; // 3 sub-blobs: CodeDirectory, Requirements, CmsWrapper
+        size += sizeof(BlobIndex) * 5; // 5 sub-blobs: CodeDirectory, Requirements, CmsWrapper, Entitlements, DerEntitlements
 
         // CodeDirectoryBlob
         size += sizeof(BlobMagic);
@@ -112,22 +142,41 @@ public static unsafe long GetLargestSizeEstimate(uint fileSize, string identifie
         size += sizeof(CodeDirectoryBlob.CodeDirectoryHeader); // CodeDirectory header
         size += CodeDirectoryBlob.GetIdentifierLength(identifier); // Identifier
         size += (long)CodeDirectoryBlob.GetCodeSlotCount(fileSize) * usedHashSize; // Code hashes
-        size += (long)(uint)CodeDirectorySpecialSlot.Requirements * usedHashSize; // Special code hashes
+        size += (long)(uint)CodeDirectorySpecialSlot.DerEntitlements * usedHashSize; // Special code hashes. The highest special slot is DerEntitlements.
 
         size += RequirementsBlob.Empty.Size; // Requirements is always written as an empty blob
         size += CmsWrapperBlob.Empty.Size; // CMS blob is always written as an empty blob
+        size += EntitlementsBlob.MaxSize;
+        size += DerEntitlementsBlob.MaxSize;
         return size;
     }
 
     /// <summary>
     /// Returns the size of a signature used to replace an existing one.
     /// If the existing signature is null, it will assume sizing using the default signature, which includes the Requirements and CMS blobs.
+    /// If the existing signature is not null, it will preserve the Entitlements and DER Entitlements blobs if they exist.
     /// </summary>
-    internal static unsafe long GetSignatureSize(uint fileSize, string identifier, byte? hashSize = null)
+    internal static unsafe long GetSignatureSize(uint fileSize, string identifier, EmbeddedSignatureBlob? existingSignature = null, byte? hashSize = null)
     {
         byte usedHashSize = hashSize ?? CodeDirectoryBlob.DefaultHashType.GetHashSize();
         uint specialCodeSlotCount = (uint)CodeDirectorySpecialSlot.Requirements;
         uint embeddedSignatureSubBlobCount = 3; // CodeDirectory, Requirements, CMS Wrapper are always present
+        uint entitlementsBlobSize = 0;
+        uint derEntitlementsBlobSize = 0;
+
+        if (existingSignature != null)
+        {
+            // We preserve Entitlements and DER Entitlements blobs if they exist in the old signature.
+            // We need to update the relevant sizes and counts to reflect this.
+            specialCodeSlotCount = Math.Max((uint)CodeDirectorySpecialSlot.Requirements, existingSignature.GetSpecialSlotHashCount());
+            entitlementsBlobSize = existingSignature.EntitlementsBlob?.Size ?? 0;
+            derEntitlementsBlobSize = existingSignature.DerEntitlementsBlob?.Size ?? 0;
+            // Requirements and CMSWrapper blobs are always overwritten as emtpy, but present.
+            if (existingSignature.EntitlementsBlob is not null)
+                embeddedSignatureSubBlobCount += 1;
+            if (existingSignature.DerEntitlementsBlob is not null)
+                embeddedSignatureSubBlobCount += 1;
+        }
 
         // Calculate the size of the new signature
         long size = 0;
@@ -137,16 +186,21 @@ internal static unsafe long GetSignatureSize(uint fileSize, string identifier, b
         size += sizeof(uint); // Blob count
         size += sizeof(BlobIndex) * embeddedSignatureSubBlobCount; // EmbeddedSignature sub-blobs
         // CodeDirectory
-        size += sizeof(BlobMagic); // CD Magic number
-        size += sizeof(uint); // CD Size field
+        size += sizeof(BlobMagic); // CodeDirectory Magic number
+        size += sizeof(uint); // CodeDirectory Size field
         size += sizeof(CodeDirectoryBlob.CodeDirectoryHeader); // CodeDirectory header
         size += CodeDirectoryBlob.GetIdentifierLength(identifier); // Identifier
         size += specialCodeSlotCount * usedHashSize; // Special code hashes
         size += CodeDirectoryBlob.GetCodeSlotCount(fileSize) * usedHashSize; // Code hashes
-        // RequirementsBlob
+        // RequirementsBlob is always empty
         size += RequirementsBlob.Empty.Size;
-        // CmsWrapperBlob
+        // EntitlementsBlob
+        size += entitlementsBlobSize;
+        // DER EntitlementsBlob
+        size += derEntitlementsBlobSize;
+        // CMSWrapperBlob is always empty
         size += CmsWrapperBlob.Empty.Size;
+
         return size;
     }
 
@@ -185,5 +239,11 @@ public static void AssertEquivalent(EmbeddedSignatureBlob? a, EmbeddedSignatureB
 
         if (a.CmsWrapperBlob?.Size != b.CmsWrapperBlob?.Size)
             throw new ArgumentException("CMS Wrapper blobs are not equivalent");
+
+        if (a.EntitlementsBlob?.Size != b.EntitlementsBlob?.Size)
+            throw new ArgumentException("Entitlements blobs are not equivalent");
+
+        if (a.DerEntitlementsBlob?.Size != b.DerEntitlementsBlob?.Size)
+            throw new ArgumentException("DER Entitlements blobs are not equivalent");
     }
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EntitlementsBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EntitlementsBlob.cs
new file mode 100644
index 00000000000000..fa0f8c0c41329a
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EntitlementsBlob.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+/// <summary>
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_utilities/lib/blob.h
+/// Code signature data is always big endian / network order.
+/// </summary>
+internal sealed class EntitlementsBlob : IBlob
+{
+    private SimpleBlob _inner;
+
+    public EntitlementsBlob(SimpleBlob blob)
+    {
+        _inner = blob;
+        if (blob.Magic != BlobMagic.Entitlements)
+        {
+            throw new InvalidDataException($"Invalid magic for EntitlementsBlob: {blob.Magic}");
+        }
+        if (blob.Size > MaxSize)
+        {
+            throw new InvalidDataException($"EntitlementsBlob data exceeds maximum size of {MaxSize} bytes.");
+        }
+    }
+
+    public static uint MaxSize => 2048;
+
+    /// <inheritdoc />
+    public BlobMagic Magic => ((IBlob)_inner).Magic;
+
+    /// <inheritdoc />
+    public uint Size => ((IBlob)_inner).Size;
+
+    /// <inheritdoc />
+    public int Write(IMachOFileWriter writer, long offset) => ((IBlob)_inner).Write(writer, offset);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs
index 8a709e7c066bea..b2c4f245e418f0 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs
@@ -11,5 +11,7 @@ internal enum BlobMagic : uint
     EmbeddedSignature = 0xfade0cc0,
     CodeDirectory = 0xfade0c02,
     Requirements = 0xfade0c01,
+    Entitlements = 0xfade7171,
+    DerEntitlements = 0xfade7172,
     CmsWrapper = 0xfade0b01,
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs
index 18603dda63c778..231083e272615b 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs
@@ -10,5 +10,7 @@ internal enum CodeDirectorySpecialSlot
 {
     CodeDirectory = 0,
     Requirements = 2,
+    Entitlements = 5,
+    DerEntitlements = 7,
     CmsWrapper = 0x10000,
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
index 351468b411b199..6918b3c510dd7d 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
@@ -18,9 +18,8 @@ namespace Microsoft.NET.HostModel.MachO;
 /// </summary>
 internal unsafe partial class MachObjectFile
 {
-    public const uint DefaultPageSize = 0x1000;
+    internal const uint DefaultPageSize = 0x1000;
     private const uint CodeSignatureAlignment = 0x10;
-
     private MachHeader _header;
     private (LinkEditLoadCommand Command, long FileOffset) _codeSignatureLoadCommand;
     private readonly (Segment64LoadCommand Command, long FileOffset) _textSegment64;
@@ -29,13 +28,15 @@ internal unsafe partial class MachObjectFile
 
     private EmbeddedSignatureBlob? _codeSignatureBlob;
     /// <summary>
-    /// The offset of the lowest section in the object file. This is to ensure that additional load commands do not overwrite sections.
+    /// The offset of the lowest section in the object file. Load commands should not be written past this offset.
     /// </summary>
     private readonly long _lowestSectionOffset;
     /// <summary>
     /// The offset in the object file where the next additional load command should be written.
     /// </summary>
-    private long _nextCommandPtr;
+    private long NextLoadCommandOffset => _header.SizeOfCommands + sizeof(MachHeader);
+
+    internal EmbeddedSignatureBlob? EmbeddedSignatureBlob => _codeSignatureBlob;
 
     private MachObjectFile(
         MachHeader header,
@@ -44,8 +45,7 @@ private MachObjectFile(
         (Segment64LoadCommand Command, long FileOffset) linkEditSegment64,
         (SymbolTableLoadCommand Command, long FileOffset) symtabLC,
         long lowestSection,
-        EmbeddedSignatureBlob? codeSignatureBlob,
-        long nextCommandPtr)
+        EmbeddedSignatureBlob? codeSignatureBlob)
     {
         _codeSignatureBlob = codeSignatureBlob;
         _header = header;
@@ -54,7 +54,16 @@ private MachObjectFile(
         _linkEditSegment64 = linkEditSegment64;
         _symtabCommand = symtabLC;
         _lowestSectionOffset = lowestSection;
-        _nextCommandPtr = nextCommandPtr;
+    }
+
+    public static MachObjectFile Create(MemoryMappedViewAccessor accessor)
+    {
+        return Create(new MemoryMappedMachOViewAccessor(accessor));
+    }
+
+    public static MachObjectFile Create(Stream stream)
+    {
+        return Create(new StreamBasedMachOFile(stream));
     }
 
     /// <summary>
@@ -70,7 +79,7 @@ public static MachObjectFile Create(IMachOFileReader file)
         if (!header.Is64Bit)
             throw new AppHostMachOFormatException(MachOFormatError.Not64BitExe);
 
-        long nextCommandPtr = ReadCommands(
+        ReadCommands(
             file,
             in header,
             out (LinkEditLoadCommand Command, long FileOffset) codeSignatureLC,
@@ -88,8 +97,7 @@ public static MachObjectFile Create(IMachOFileReader file)
             linkEditSegment64,
             symtabCommand,
             lowestSection,
-            codeSignatureBlob,
-            nextCommandPtr);
+            codeSignatureBlob);
     }
 
     /// <summary>
@@ -102,35 +110,52 @@ public static MachObjectFile Create(IMachOFileReader file)
     /// Writes the EmbeddedSignature blob to the file.
     /// Returns the new size of the file (the end of the signature blob).
     /// </summary>
-    public long AdHocSignFile(IMachOFileAccess file, string identifier)
+    /// <param name="file">The file to write the signature to.</param>
+    /// <param name="identifier">The identifier to use for the code signature.</param>
+    /// <param name="oldSignature">
+    /// An optional old signature to preserve entitlements metadata.
+    /// If not provided, the existing code signature blob will be used.
+    /// If the existing code signature blob is not present, a new signature will be created without entitlements.
+    /// </param>
+    public long AdHocSignFile(IMachOFileAccess file, string identifier, EmbeddedSignatureBlob? oldSignature = null)
     {
-        AllocateCodeSignatureLoadCommand(identifier);
+        oldSignature ??= _codeSignatureBlob;
+        AllocateCodeSignatureLoadCommand(identifier, oldSignature);
         _codeSignatureBlob = null;
         // The code signature includes hashes of the entire file up to the code signature.
         // In order to calculate the hashes correctly, everything up to the code signature must be written before the signature is built.
         Write(file);
-        _codeSignatureBlob = CreateSignature(this, file, identifier);
+        _codeSignatureBlob = CreateSignature(this, file, identifier, oldSignature);
         Validate();
         _codeSignatureBlob.Write(file, _codeSignatureLoadCommand.Command.GetDataOffset(_header));
         return GetFileSize();
     }
-   private static EmbeddedSignatureBlob CreateSignature(MachObjectFile machObject, IMachOFileReader file, string identifier)
+
+    private static EmbeddedSignatureBlob CreateSignature(MachObjectFile machObject, IMachOFileReader file, string identifier, EmbeddedSignatureBlob? oldSignature)
     {
+        var oldSignatureBlob = oldSignature;
+
         Debug.Assert(!machObject._codeSignatureLoadCommand.Command.IsDefault);
         uint signatureStart = machObject._codeSignatureLoadCommand.Command.GetDataOffset(machObject._header);
         RequirementsBlob requirementsBlob = RequirementsBlob.Empty;
         CmsWrapperBlob cmsWrapperBlob = CmsWrapperBlob.Empty;
+        EntitlementsBlob? entitlementsBlob = oldSignatureBlob?.EntitlementsBlob;
+        DerEntitlementsBlob? derEntitlementsBlob = oldSignatureBlob?.DerEntitlementsBlob;
 
         var codeDirectory = CodeDirectoryBlob.Create(
             file,
             signatureStart,
             identifier,
-            requirementsBlob);
+            requirementsBlob,
+            entitlementsBlob,
+            derEntitlementsBlob);
 
         return new EmbeddedSignatureBlob(
             codeDirectoryBlob: codeDirectory,
             requirementsBlob: requirementsBlob,
-            cmsWrapperBlob: cmsWrapperBlob);
+            cmsWrapperBlob: cmsWrapperBlob,
+            entitlementsBlob: entitlementsBlob,
+            derEntitlementsBlob: derEntitlementsBlob);
     }
 
     /// <summary>
@@ -141,6 +166,11 @@ private static EmbeddedSignatureBlob CreateSignature(MachObjectFile machObject,
     /// <returns>`true` if the headers were adjusted successfully, `false` otherwise.</returns>
     public bool TryAdjustHeadersForBundle(ulong fileSize, IMachOFileWriter file)
     {
+        if (_codeSignatureBlob is not null ||
+            !_codeSignatureLoadCommand.Command.IsDefault)
+        {
+            throw new InvalidOperationException("Cannot adjust headers for a Mach-O file with an existing code signature.");
+        }
         ulong newStringTableSize = fileSize - _symtabCommand.Command.GetStringTableOffset(_header);
         if (newStringTableSize > uint.MaxValue)
         {
@@ -156,15 +186,15 @@ public bool TryAdjustHeadersForBundle(ulong fileSize, IMachOFileWriter file)
         return true;
     }
 
-    public static bool IsMachOImage(IMachOFileReader memoryMappedViewAccessor)
+    public static bool IsMachOImage(IMachOFileReader file)
     {
-        memoryMappedViewAccessor.Read(0, out MachMagic magic);
+        file.Read(0, out MachMagic magic);
         return magic is MachMagic.MachHeaderCurrentEndian or MachMagic.MachHeaderOppositeEndian
             or MachMagic.MachHeader64CurrentEndian or MachMagic.MachHeader64OppositeEndian
             or MachMagic.FatMagicCurrentEndian or MachMagic.FatMagicOppositeEndian;
     }
 
-    public static bool IsMachOImage(FileStream file)
+    public static bool IsMachOImage(Stream file)
     {
         long oldPosition = file.Position;
         file.Position = 0;
@@ -197,22 +227,20 @@ public static bool IsMachOImage(string filePath)
     /// <param name="file">The file to remove the signature from.</param>
     /// <param name="newLength">The new length of the file if the signature is remove and the method returns true</param>
     /// <returns>True if a signature was present and removed, false otherwise</returns>
-    public static bool RemoveCodeSignatureIfPresent(IMachOFileAccess file, out long? newLength)
+    public bool RemoveCodeSignatureIfPresent(IMachOFileWriter file, out long? newLength)
     {
         newLength = null;
-        if (!IsMachOImage(file))
-            return false;
-
-        MachObjectFile machFile = Create(file);
+        MachObjectFile machFile = this;
         if (machFile._codeSignatureLoadCommand.Command.IsDefault)
         {
             Debug.Assert(machFile._codeSignatureBlob is null);
             return false;
         }
 
+        LinkEditLoadCommand clearedCommand = default;
+        file.Write(_codeSignatureLoadCommand.FileOffset, ref clearedCommand);
         machFile._header.NumberOfCommands -= 1;
         machFile._header.SizeOfCommands -= (uint)sizeof(LinkEditLoadCommand);
-        machFile._nextCommandPtr -= (uint)sizeof(LinkEditLoadCommand);
         machFile._linkEditSegment64.Command.SetFileSize(
             machFile._linkEditSegment64.Command.GetFileSize(machFile._header)
                 - machFile._codeSignatureLoadCommand.Command.GetFileSize(machFile._header),
@@ -228,20 +256,26 @@ public static bool RemoveCodeSignatureIfPresent(IMachOFileAccess file, out long?
     /// <summary>
     /// Removes the code signature load command and signature, and resizes the file if necessary.
     /// </summary>
-    public static void RemoveCodeSignatureIfPresent(FileStream bundle)
+    public static EmbeddedSignatureBlob? RemoveCodeSignatureIfPresent(FileStream bundle)
     {
         long? newLength;
         bool resized;
+        EmbeddedSignatureBlob? codeSignature = null;
         // Windows doesn't allow a FileStream to be resized while the file is memory mapped, so we must dispose of the memory mapped file first.
         using (MemoryMappedFile mmap = MemoryMappedFile.CreateFromFile(bundle, null, 0, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
         using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
         {
-            resized = RemoveCodeSignatureIfPresent(new MemoryMappedMachOViewAccessor(accessor), out newLength);
+            var file = new MemoryMappedMachOViewAccessor(accessor);
+            MachObjectFile machFile = Create(file);
+            codeSignature = machFile.EmbeddedSignatureBlob;
+            resized = machFile.RemoveCodeSignatureIfPresent(file, out newLength);
         }
         if (resized)
         {
+            Debug.Assert(newLength != null);
             bundle.SetLength(newLength!.Value);
         }
+        return codeSignature;
     }
 
     /// <summary>
@@ -273,6 +307,7 @@ static bool CodeSignatureLCsAreEquivalent((LinkEditLoadCommand Command, long Fil
                 return false;
             if (a.FileOffset != b.FileOffset)
                 return false;
+            // Sizes can be different due to identifier differences.
             return true;
         }
 
@@ -288,6 +323,10 @@ static bool LinkEditSegmentsAreEquivalent((Segment64LoadCommand Command, long Fi
         }
     }
 
+    /// <summary>
+    /// Gets the maximum size of additional space required for the code signature to be added to a file of size <paramref name="fileSize"/>.
+    /// Includes the size of the code signature blob and the padding to align the file to the code signature alignment.
+    /// </summary>
     public static long GetSignatureSizeEstimate(uint fileSize, string identifier)
     {
         return EmbeddedSignatureBlob.GetLargestSizeEstimate(fileSize, identifier) + (AlignUp(fileSize, CodeSignatureAlignment) - fileSize);
@@ -299,7 +338,7 @@ public static long GetSignatureSizeEstimate(uint fileSize, string identifier)
     public long Write(IMachOFileWriter file)
     {
         if (file.Capacity < GetFileSize())
-            throw new ArgumentException("File is too small", nameof(file));
+            throw new ArgumentException($"File is too small. File capacity is '{file.Capacity}' bytes, but the Mach-O requires '{GetFileSize()}' bytes. ", nameof(file));
         file.Write(0, ref _header);
         file.Write(_linkEditSegment64.FileOffset, ref _linkEditSegment64.Command);
         file.Write(_symtabCommand.FileOffset, ref _symtabCommand.Command);
@@ -315,7 +354,7 @@ public long Write(IMachOFileWriter file)
     /// Returns a pointer to the end of the commands list.
     /// Fills the content of the commands with the corresponding command if present in the file.
     /// </summary>
-    private static long ReadCommands(
+    private static void ReadCommands(
         IMachOFileReader inputFile,
         in MachHeader header,
         out (LinkEditLoadCommand Command, long FileOffset) codeSignatureLC,
@@ -394,7 +433,7 @@ private static long ReadCommands(
             // Signature blob should be right after the symbol table except for a few bytes of padding for alignment
             uint symtabEnd = symtabLC.Command.GetStringTableOffset(header) + symtabLC.Command.GetStringTableSize(header);
             uint signStart = codeSignatureLC.Command.GetDataOffset(header);
-            if (symtabEnd > signStart || signStart - symtabEnd > 32)
+            if (symtabEnd > signStart || signStart - symtabEnd > CodeSignatureAlignment)
                 throw new AppHostMachOFormatException(MachOFormatError.SignDoesntFollowSymtab);
             // Signature blob should be contained within the LinkEdit segment
             if (codeSignatureLC.Command.GetDataOffset(header) < linkEditSegment64.Command.GetFileOffset(header)
@@ -404,17 +443,17 @@ private static long ReadCommands(
                 throw new AppHostMachOFormatException(MachOFormatError.SignNotInLinkEdit);
             }
         }
-        return commandsPtr;
+        Debug.Assert(header.SizeOfCommands == commandsPtr - sizeof(MachHeader));
     }
 
     /// <summary>
     /// Clears the old signature and sets the codeSignatureLC to the proper size and offset for a new signature.
     /// </summary>
-    private void AllocateCodeSignatureLoadCommand(string identifier)
+    private void AllocateCodeSignatureLoadCommand(string identifier, EmbeddedSignatureBlob? oldSignature)
     {
         uint csOffset = GetSignatureStart();
-        uint csPtr = (uint)(_codeSignatureLoadCommand.Command.IsDefault ? _nextCommandPtr : _codeSignatureLoadCommand.FileOffset);
-        uint csSize = (uint)EmbeddedSignatureBlob.GetSignatureSize(GetSignatureStart(), identifier);
+        uint csPtr = (uint)(_codeSignatureLoadCommand.Command.IsDefault ? NextLoadCommandOffset : _codeSignatureLoadCommand.FileOffset);
+        uint csSize = (uint)EmbeddedSignatureBlob.GetSignatureSize(csOffset, identifier, oldSignature);
 
         if (_codeSignatureLoadCommand.Command.IsDefault)
         {
@@ -443,6 +482,7 @@ private uint GetSignatureStart()
         if (!_codeSignatureLoadCommand.Command.IsDefault)
         {
             Debug.Assert(_codeSignatureLoadCommand.Command.GetDataOffset(_header) % CodeSignatureAlignment == 0);
+            Debug.Assert(_codeSignatureLoadCommand.Command.GetDataOffset(_header) + _codeSignatureLoadCommand.Command.GetFileSize(_header) == GetFileSize());
             return _codeSignatureLoadCommand.Command.GetDataOffset(_header);
         }
         return AlignUp((uint)(_linkEditSegment64.Command.GetFileOffset(_header) + _linkEditSegment64.Command.GetFileSize(_header)), CodeSignatureAlignment);
@@ -465,6 +505,8 @@ private void Validate()
         Debug.Assert(linkEditFileSize <= linkEditVMSize);
         if (!_codeSignatureLoadCommand.Command.IsDefault)
         {
+            Debug.Assert(_symtabCommand.Command.GetStringTableOffset(_header) + _symtabCommand.Command.GetStringTableSize(_header) <= _codeSignatureLoadCommand.Command.GetDataOffset(_header));
+            Debug.Assert(_symtabCommand.Command.GetStringTableOffset(_header) + _symtabCommand.Command.GetStringTableSize(_header) <= GetSignatureStart());
             var csStart = _codeSignatureLoadCommand.Command.GetDataOffset(_header);
             var csEnd = csStart + _codeSignatureLoadCommand.Command.GetFileSize(_header);
             Debug.Assert(_codeSignatureBlob is not null);
diff --git a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
index e4791a6bb14e99..5bb710f2609438 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
@@ -1,6 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+#nullable enable
+
 using System;
 using System.IO;
 using System.IO.MemoryMappedFiles;
@@ -15,9 +17,11 @@ namespace Microsoft.NET.HostModel
     /// </summary>
     public class ResourceUpdater : IDisposable
     {
-        private readonly FileStream stream;
+        private readonly FileStream? stream;
+        private readonly MemoryMappedFile? _memoryMappedFile;
+        private Stream Stream => (Stream?)stream ?? _memoryMappedFile!.CreateViewStream(0, 0, MemoryMappedFileAccess.ReadWrite);
         private readonly PEReader _reader;
-        private ResourceData _resourceData;
+        private ResourceData? _resourceData;
         private readonly bool leaveOpen;
 
         /// <summary>
@@ -55,6 +59,23 @@ public ResourceUpdater(FileStream stream, bool leaveOpen = false)
             }
         }
 
+        public ResourceUpdater(MemoryMappedFile memoryMappedFile, bool leaveOpen = false)
+        {
+            this._memoryMappedFile = memoryMappedFile;
+            this.leaveOpen = leaveOpen;
+            try
+            {
+                _reader = new PEReader(this.Stream, PEStreamOptions.LeaveOpen);
+                _resourceData = new ResourceData(_reader);
+            }
+            catch (Exception)
+            {
+                if (!leaveOpen)
+                    this.stream?.Dispose();
+                throw;
+            }
+        }
+
         /// <summary>
         /// Add all resources from a source PE file. It is assumed
         /// that the input is a valid PE file. If it is not, an
@@ -69,7 +90,7 @@ public ResourceUpdater AddResourcesFromPEImage(string peFile)
 
             using var module = new PEReader(File.OpenRead(peFile));
             var moduleResources = new ResourceData(module);
-            _resourceData.CopyResourcesFrom(moduleResources);
+            _resourceData!.CopyResourcesFrom(moduleResources);
             return this;
         }
 
@@ -95,7 +116,7 @@ public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
             if (_resourceData == null)
                 ThrowExceptionForInvalidUpdate();
 
-            _resourceData.AddResource((ushort)lpName, (ushort)lpType, LangID_LangNeutral_SublangNeutral, data);
+            _resourceData!.AddResource((ushort)lpName, (ushort)lpType, LangID_LangNeutral_SublangNeutral, data);
 
             return this;
         }
@@ -115,7 +136,7 @@ public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
             if (_resourceData == null)
                 ThrowExceptionForInvalidUpdate();
 
-            _resourceData.AddResource((ushort)lpName, lpType, LangID_LangNeutral_SublangNeutral, data);
+            _resourceData!.AddResource((ushort)lpName, lpType, LangID_LangNeutral_SublangNeutral, data);
 
             return this;
         }
@@ -173,7 +194,7 @@ public void Update()
             }
 
             var objectDataBuilder = new ObjectDataBuilder();
-            _resourceData.WriteResources(rsrcVirtualAddress, ref objectDataBuilder);
+            _resourceData!.WriteResources(rsrcVirtualAddress, ref objectDataBuilder);
             var rsrcSectionData = objectDataBuilder.ToData();
 
             int rsrcSectionDataSize = rsrcSectionData.Length;
@@ -185,12 +206,12 @@ public void Update()
 
             int trailingSectionVirtualStart = rsrcVirtualAddress + rsrcOriginalVirtualSize;
             int trailingSectionStart = rsrcPointerToRawData + rsrcOriginalRawDataSize;
-            int trailingSectionLength = (int)(stream.Length - trailingSectionStart);
+            int trailingSectionLength = (int)(Stream.Length - trailingSectionStart);
 
             bool needsMoveTrailingSections = !isRsrcIsLastSection && delta > 0;
             long finalImageSize = trailingSectionStart + Math.Max(delta, 0) + trailingSectionLength;
 
-            using (var mmap = MemoryMappedFile.CreateFromFile(stream, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
+            using (var mmap = _memoryMappedFile ?? MemoryMappedFile.CreateFromFile(stream!, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
             using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, finalImageSize, MemoryMappedFileAccess.ReadWrite))
             {
                 int peSignatureOffset = ReadI32(accessor, PEOffsets.DosStub.PESignatureOffset);
@@ -336,7 +357,8 @@ public void Dispose(bool disposing)
             if (disposing && !leaveOpen)
             {
                 _reader.Dispose();
-                stream.Dispose();
+                stream?.Dispose();
+                _memoryMappedFile?.Dispose();
             }
         }
     }
diff --git a/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs b/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
index 18752f5f175fa7..f894263d52678f 100644
--- a/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
+++ b/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
@@ -8,6 +8,8 @@
 using Microsoft.DotNet.CoreSetup.Test;
 using Microsoft.DotNet.Cli.Build.Framework;
 using Microsoft.NET.HostModel.AppHost;
+using Microsoft.NET.HostModel.MachO.CodeSign.Tests;
+using Microsoft.NET.HostModel.Bundle;
 
 namespace HostActivation.Tests
 {
@@ -31,5 +33,42 @@ public void SignedAppHostRuns()
                 .Execute();
             executedCommand.Should().ExitWith(Constants.ErrorCode.AppHostExeNotBoundFailure);
         }
+
+        [Fact]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        public void SigningAppHostPreservesEntitlements()
+        {
+            using var testDirectory = TestArtifact.Create(nameof(SignedAppHostRuns));
+            var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.AppHost.FilePath));
+            File.Copy(Binaries.AppHost.FilePath, testAppHostPath);
+            long preRemovalSize = new FileInfo(testAppHostPath).Length;
+            string signedHostPath = testAppHostPath + ".signed";
+
+            HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
+
+            SigningTests.HasDerEntitlementsBlob(testAppHostPath).Should().BeTrue();
+            SigningTests.HasDerEntitlementsBlob(signedHostPath).Should().BeTrue();
+            SigningTests.HasEntitlementsBlob(testAppHostPath).Should().BeTrue();
+            SigningTests.HasEntitlementsBlob(signedHostPath).Should().BeTrue();
+        }
+
+        [Fact]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        public void BundledAppHostHasEntitlements()
+        {
+            using var testDirectory = TestArtifact.Create(nameof(BundledAppHostHasEntitlements));
+            var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.SingleFileHost.FilePath));
+            File.Copy(Binaries.SingleFileHost.FilePath, testAppHostPath);
+            long preRemovalSize = new FileInfo(testAppHostPath).Length;
+            string signedHostPath = testAppHostPath + ".signed";
+
+            HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
+            var bundlePath = new Bundler(Path.GetFileName(signedHostPath), testAppHostPath + ".bundle").GenerateBundle([new(signedHostPath, Path.GetFileName(signedHostPath))]);
+
+            SigningTests.HasEntitlementsBlob(testAppHostPath).Should().BeTrue();
+            SigningTests.HasEntitlementsBlob(bundlePath).Should().BeTrue();
+            SigningTests.HasDerEntitlementsBlob(testAppHostPath).Should().BeTrue();
+            SigningTests.HasDerEntitlementsBlob(bundlePath).Should().BeTrue();
+        }
     }
 }
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
new file mode 100644
index 00000000000000..3077a420090d33
--- /dev/null
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -0,0 +1,99 @@
+
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.IO.MemoryMappedFiles;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Microsoft.DotNet.CoreSetup.Test;
+using Microsoft.NET.HostModel.MachO;
+using Xunit;
+
+public class MachObjectTests
+{
+    private readonly List<TestArtifact> _testArtifacts = new();
+
+    [Theory]
+    [MemberData(nameof(GetTestFilePaths), nameof(StreamAndMemoryMappedFileAreTheSame))]
+    public void StreamAndMemoryMappedFileAreTheSame(string filePath, TestArtifact testArtifact)
+    {
+        using var testArtifactLocation = testArtifact;
+        MachObjectFile streamMachOFile;
+        MachObjectFile memoryMappedMachOFile;
+        using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+        {
+            streamMachOFile = MachObjectFile.Create(new StreamBasedMachOFile(stream));
+
+            using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(stream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true))
+            using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
+            {
+                memoryMappedMachOFile = MachObjectFile.Create(new MemoryMappedMachOViewAccessor(memoryMappedViewAccessor));
+            }
+        }
+        MachObjectFile.AssertEquivalent(streamMachOFile, memoryMappedMachOFile);
+    }
+
+
+    [Theory]
+    [MemberData(nameof(GetTestFilePaths), nameof(RoundTripMachObjectFileIsTheSame))]
+    void RoundTripMachObjectFileIsTheSame(string filePath, TestArtifact testArtifact)
+    {
+        using var testArtifactLocation = testArtifact;
+        using (var mmap = MemoryMappedFile.CreateFromFile(filePath))
+        using (var accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
+        {
+            var machFile = new MemoryMappedMachOViewAccessor(accessor);
+            var machObjectFile = MachObjectFile.Create(machFile);
+            machObjectFile.Write(machFile);
+            var rewrittenMachFile = MachObjectFile.Create(machFile);
+            MachObjectFile.AssertEquivalent(machObjectFile, rewrittenMachFile);
+        }
+    }
+
+    static readonly ImmutableArray<string> liveBuiltHosts = ImmutableArray.Create(Binaries.AppHost.FilePath, Binaries.SingleFileHost.FilePath);
+    public static Object[][] GetTestFilePaths(string testArtifactName)
+    {
+        List<object[]> arguments = [];
+        List<(string Name, FileInfo File)> testData = TestData.MachObjects.GetAll().ToList();
+        foreach ((string name, FileInfo file) in testData)
+        {
+            var testArtifact = TestArtifact.Create(testArtifactName + "-" + name);
+            string newFilePath = Path.Combine(testArtifact.Location, name);
+            File.Copy(file.FullName, newFilePath, true);
+            arguments.Add([newFilePath, testArtifact]);
+        }
+
+        // If we're on mac, we can use the live built binaries to test against
+        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+        {
+            foreach (var filePath in liveBuiltHosts)
+            {
+                string fileName = Path.GetFileName(filePath);
+                var testArtifact = TestArtifact.Create(testArtifactName + "-" + fileName);
+                string testFilePath = Path.Combine(testArtifact.Location, fileName);
+                File.Copy(filePath, testFilePath);
+                arguments.Add([testFilePath, testArtifact]);
+            }
+        }
+
+        return arguments.ToArray();
+    }
+
+    public void Dispose()
+    {
+        foreach (var artifact in _testArtifacts)
+        {
+            try
+            {
+                artifact.Dispose();
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"Failed to dispose test artifact: {ex.Message}");
+            }
+        }
+        _testArtifacts.Clear();
+    }
+}
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
index 5a20310e5dff46..cc0f4e612d9928 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
@@ -24,133 +24,73 @@ namespace Microsoft.NET.HostModel.MachO.CodeSign.Tests
 {
     public class SigningTests
     {
-        public static bool IsSigned(string filePath)
-        {
-            // Validate the signature if we can, otherwise, at least ensure there is a signature LoadCommand present
-            using (var appHostSourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
-            using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true))
-            using (var managedSignedAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite))
-            {
-                if (!MachObjectFile.Create(new MemoryMappedMachOViewAccessor(managedSignedAccessor)).HasSignature)
-                {
-                    return false;
-                }
-            }
-            if (Codesign.IsAvailable && Codesign.Run("--verify", filePath).ExitCode != 0)
-            {
-                return false;
-            }
-            return true;
-        }
-
-        public static bool IsMachOImage(string filePath) => MachObjectFile.IsMachOImage(filePath);
-
-        static readonly string[] liveBuiltHosts = new string[] { Binaries.AppHost.FilePath, Binaries.SingleFileHost.FilePath };
-        static List<string> GetTestFilePaths(TestArtifact testArtifact)
-        {
-            List<(string Name, FileInfo File)> testData = TestData.MachObjects.GetAll().ToList();
-            List<string> testFilePaths = new();
-            foreach ((string name, FileInfo file) in testData)
-            {
-                string newFilePath = Path.Combine(testArtifact.Location, name);
-                File.Copy(file.FullName, newFilePath, true);
-                testFilePaths.Add(newFilePath);
-            }
-
-            // If we're on mac, we can use the live built binaries to test against
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-            {
-                foreach (var filePath in liveBuiltHosts)
-                {
-                    string fileName = Path.GetFileName(filePath);
-                    string testFilePath = Path.Combine(testArtifact.Location, fileName);
-                    File.Copy(filePath, testFilePath);
-                    testFilePaths.Add(testFilePath);
-                }
-            }
-
-            return testFilePaths;
-        }
-
-        [Fact]
-        public void CanSignMachObject()
+        [Theory]
+        [MemberData(nameof(GetTestFilePaths), nameof(CanSignMachObject))]
+        public void CanSignMachObject(string filePath, TestArtifact _)
         {
-            using var testArtifact = TestArtifact.Create(nameof(CanSignMachObject));
-            foreach (var filePath in GetTestFilePaths(testArtifact))
-            {
-                string fileName = Path.GetFileName(filePath);
-                string originalFilePath = filePath;
-                string managedSignedPath = filePath + ".signed";
+            string fileName = Path.GetFileName(filePath);
+            string originalFilePath = filePath;
+            string managedSignedPath = filePath + ".signed";
 
-                // Managed signed file
-                AdHocSignFile(originalFilePath, managedSignedPath, fileName);
-                Assert.True(IsSigned(managedSignedPath), $"Failed to sign a copy of {filePath}");
-            }
+            // Managed signed file
+            AdHocSignFile(originalFilePath, managedSignedPath, fileName);
+            Assert.True(IsSigned(managedSignedPath), $"Failed to sign a copy of {filePath}");
         }
 
-        [Fact]
-        public void CanRemoveSignature()
+        [Theory]
+        [MemberData(nameof(GetTestFilePaths), nameof(CanRemoveSignature))]
+        public void CanRemoveSignature(string filePath, TestArtifact _)
         {
-            using var testArtifact = TestArtifact.Create(nameof(CanRemoveSignature));
-            foreach (var filePath in GetTestFilePaths(testArtifact))
-            {
-                string fileName = Path.GetFileName(filePath);
-                string originalFilePath = filePath;
-                string managedSignedPath = filePath + ".signed";
-                RemoveSignature(originalFilePath, managedSignedPath);
-                Assert.False(IsSigned(managedSignedPath), $"Failed to remove signature from {filePath}");
-            }
+            string fileName = Path.GetFileName(filePath);
+            string originalFilePath = filePath;
+            string managedSignedPath = filePath + ".signed";
+            RemoveSignature(originalFilePath, managedSignedPath);
+            Assert.False(IsSigned(managedSignedPath), $"Failed to remove signature from {filePath}");
         }
 
-        [Fact]
-        public void CanUnsignAndResign()
+        [Theory]
+        [MemberData(nameof(GetTestFilePaths), nameof(CanUnsignAndResign))]
+        public void CanUnsignAndResign(string filePath, TestArtifact _)
         {
-            using var testArtifact = TestArtifact.Create(nameof(CanUnsignAndResign));
-            foreach (var filePath in GetTestFilePaths(testArtifact))
-            {
-                string fileName = Path.GetFileName(filePath);
-                string originalFilePath = filePath;
-                string managedSignedPath = filePath + ".signed";
+            string fileName = Path.GetFileName(filePath);
+            string originalFilePath = filePath;
+            string managedSignedPath = filePath + ".signed";
 
-                // Managed signed file
-                AdHocSignFile(originalFilePath, managedSignedPath, fileName);
-                Assert.True(IsSigned(managedSignedPath), $"Failed to sign a copy of {filePath}");
+            // Managed signed file
+            AdHocSignFile(originalFilePath, managedSignedPath, fileName);
+            Assert.True(IsSigned(managedSignedPath), $"Failed to sign a copy of {filePath}");
 
-                // Remove signature
-                RemoveSignature(managedSignedPath, managedSignedPath + ".unsigned");
-                Assert.False(IsSigned(managedSignedPath + ".unsigned"), $"Failed to remove signature from {filePath}");
+            // Remove signature
+            RemoveSignature(managedSignedPath, managedSignedPath + ".unsigned");
+            Assert.False(IsSigned(managedSignedPath + ".unsigned"), $"Failed to remove signature from {filePath}");
 
-                // Resign
-                AdHocSignFile(managedSignedPath + ".unsigned", managedSignedPath + ".resigned", fileName);
-                Assert.True(IsSigned(managedSignedPath + ".resigned"), $"Failed to resign {filePath}");
-            }
+            // Resign
+            AdHocSignFile(managedSignedPath + ".unsigned", managedSignedPath + ".resigned", fileName);
+            Assert.True(IsSigned(managedSignedPath + ".resigned"), $"Failed to resign {filePath}");
         }
 
-        [Fact]
+        [Theory]
+        [MemberData(nameof(GetTestFilePaths), nameof(MatchesCodesignOutput))]
         [PlatformSpecific(TestPlatforms.OSX)]
-        void MatchesCodesignOutput()
+        void MatchesCodesignOutput(string filePath, TestArtifact _)
         {
-            using var testArtifact = TestArtifact.Create(nameof(MatchesCodesignOutput));
-            foreach (var filePath in GetTestFilePaths(testArtifact))
-            {
-                string fileName = Path.GetFileName(filePath);
-                string originalFilePath = filePath;
-                string codesignFilePath = filePath + ".codesigned";
-                string managedSignedPath = filePath + ".signed";
-
-                // Codesigned file
-                File.Copy(filePath, codesignFilePath);
-                Assert.True(Codesign.IsAvailable, "Could not find codesign tool");
-                Codesign.Run("--remove-signature", codesignFilePath).ExitCode.Should().Be(0, $"'codesign --remove-signature {codesignFilePath}' failed!");
-                Codesign.Run("-s - -i " + fileName, codesignFilePath).ExitCode.Should().Be(0, $"'codesign -s - {codesignFilePath}' failed!");
-
-                // Managed signed file
-                AdHocSignFile(originalFilePath, managedSignedPath, fileName);
-
-                var check = Codesign.Run("-v", managedSignedPath);
-                check.ExitCode.Should().Be(0, check.StdErr, $"Failed to sign a copy of '{filePath}'");
-                AssertMachFilesAreEquivalent(codesignFilePath, managedSignedPath, fileName);
-            }
+            string fileName = Path.GetFileName(filePath);
+            string originalFilePath = filePath;
+            string codesignFilePath = filePath + ".codesigned";
+            string managedSignedPath = filePath + ".signed";
+
+            // Codesigned file
+            File.Copy(filePath, codesignFilePath);
+            Assert.True(Codesign.IsAvailable, "Could not find codesign tool");
+            Codesign.Run("--remove-signature", codesignFilePath).ExitCode.Should().Be(0, $"'codesign --remove-signature {codesignFilePath}' failed!");
+            Codesign.Run("-s - -i " + fileName, codesignFilePath).ExitCode.Should().Be(0, $"'codesign -s - {codesignFilePath}' failed!");
+
+            // Managed signed file
+            AdHocSignFile(originalFilePath, managedSignedPath, fileName);
+
+            var check = Codesign.Run("-v", managedSignedPath);
+            check.ExitCode.Should().Be(0, check.StdErr, $"Failed to sign a copy of '{filePath}'");
+            AssertMachFilesAreEquivalent(codesignFilePath, managedSignedPath, fileName);
         }
 
         [Fact]
@@ -174,42 +114,21 @@ void SignedMachOExecutableRuns()
             }
         }
 
-        [Fact]
-        void ReadSignedMachIsTheSameAsReadAndResigned()
+        [Theory]
+        [MemberData(nameof(GetTestFilePaths), nameof(ReadSignedMachIsTheSameAsReadAndResigned))]
+        void ReadSignedMachIsTheSameAsReadAndResigned(string filePath, TestArtifact _)
         {
-            using var testArtifact = TestArtifact.Create(nameof(ReadSignedMachIsTheSameAsReadAndResigned));
-            foreach (var fileName in GetTestFilePaths(testArtifact))
-            {
-                string signedPath = fileName + ".signed";
-
-                AdHocSignFile(fileName, signedPath, fileName);
-                using (var mmap = MemoryMappedFile.CreateFromFile(signedPath))
-                using (var accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite))
-                {
-                    var signedMachFile = new MemoryMappedMachOViewAccessor(accessor);
-                    var signedObject = MachObjectFile.Create(signedMachFile);
-                    var resignedObject = MachObjectFile.Create(signedMachFile);
-                    resignedObject.AdHocSignFile(signedMachFile, fileName);
-                    MachObjectFile.AssertEquivalent(signedObject, resignedObject);
-                }
-            }
-        }
+            string signedPath = filePath + ".signed";
 
-        [Fact]
-        void RoundTripMachObjectFileIsTheSame()
-        {
-            using var testArtifact = TestArtifact.Create(nameof(RoundTripMachObjectFileIsTheSame));
-            foreach (var fileName in GetTestFilePaths(testArtifact))
+            AdHocSignFile(filePath, signedPath, filePath);
+            using (var mmap = MemoryMappedFile.CreateFromFile(signedPath))
+            using (var accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite))
             {
-                using (var mmap = MemoryMappedFile.CreateFromFile(fileName))
-                using (var accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
-                {
-                    var machFile = new MemoryMappedMachOViewAccessor(accessor);
-                    var machObjectFile = MachObjectFile.Create(machFile);
-                    machObjectFile.Write(machFile);
-                    var rewrittenMachFile = MachObjectFile.Create(machFile);
-                    MachObjectFile.AssertEquivalent(machObjectFile, rewrittenMachFile);
-                }
+                var signedMachFile = new MemoryMappedMachOViewAccessor(accessor);
+                var signedObject = MachObjectFile.Create(signedMachFile);
+                var resignedObject = MachObjectFile.Create(signedMachFile);
+                resignedObject.AdHocSignFile(signedMachFile, filePath);
+                MachObjectFile.AssertEquivalent(signedObject, resignedObject);
             }
         }
 
@@ -303,5 +222,76 @@ internal static void RemoveSignature(string originalFilePath, string removedSign
                 MachObjectFile.RemoveCodeSignatureIfPresent(appHostDestinationStream);
             }
         }
+
+        public static bool IsSigned(string filePath)
+        {
+            // Validate the signature if we can, otherwise, at least ensure there is a signature LoadCommand present
+            using (var appHostSourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
+            using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true))
+            using (var managedSignedAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite))
+            {
+                if (!MachObjectFile.Create(new MemoryMappedMachOViewAccessor(managedSignedAccessor)).HasSignature)
+                {
+                    return false;
+                }
+            }
+            if (Codesign.IsAvailable && Codesign.Run("--verify", filePath).ExitCode != 0)
+            {
+                return false;
+            }
+            return true;
+        }
+
+        public static bool IsMachOImage(string filePath) => MachObjectFile.IsMachOImage(filePath);
+
+        public static bool HasEntitlementsBlob(string filePath)
+        {
+            using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open))
+            using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
+            {
+                var machObjectFile = MachObjectFile.Create(memoryMappedViewAccessor);
+                return machObjectFile.EmbeddedSignatureBlob?.EntitlementsBlob != null;
+            }
+        }
+
+        public static bool HasDerEntitlementsBlob(string filePath)
+        {
+            using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open))
+            using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
+            {
+                var machObjectFile = MachObjectFile.Create(memoryMappedViewAccessor);
+                return machObjectFile.EmbeddedSignatureBlob?.DerEntitlementsBlob != null;
+            }
+        }
+
+        static readonly string[] liveBuiltHosts = new string[] { Binaries.AppHost.FilePath, Binaries.SingleFileHost.FilePath };
+
+        public static Object[][] GetTestFilePaths(string testArtifactName)
+        {
+            List<object[]> arguments = [];
+            List<(string Name, FileInfo File)> testData = TestData.MachObjects.GetAll().ToList();
+            foreach ((string name, FileInfo file) in testData)
+            {
+                var testArtifact = TestArtifact.Create(testArtifactName + "-" + name);
+                string newFilePath = Path.Combine(testArtifact.Location, name);
+                File.Copy(file.FullName, newFilePath, true);
+                arguments.Add([newFilePath, testArtifact]);
+            }
+
+            // If we're on mac, we can use the live built binaries to test against
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                foreach (var filePath in liveBuiltHosts)
+                {
+                    string fileName = Path.GetFileName(filePath);
+                    var testArtifact = TestArtifact.Create(testArtifactName + "-" + fileName);
+                    string testFilePath = Path.Combine(testArtifact.Location, fileName);
+                    File.Copy(filePath, testFilePath);
+                    arguments.Add([testFilePath, testArtifact]);
+                }
+            }
+
+            return arguments.ToArray();
+        }
     }
 }
diff --git a/src/native/corehost/apphost/static/CMakeLists.txt b/src/native/corehost/apphost/static/CMakeLists.txt
index e7103871b0ef7a..30118f679da387 100644
--- a/src/native/corehost/apphost/static/CMakeLists.txt
+++ b/src/native/corehost/apphost/static/CMakeLists.txt
@@ -303,3 +303,7 @@ target_link_libraries(
 target_link_libraries(singlefilehost PRIVATE hostmisc)
 
 add_sanitizer_runtime_support(singlefilehost)
+
+if (CLR_CMAKE_HOST_APPLE)
+    adhoc_sign_with_entitlements(singlefilehost "${CLR_ENG_NATIVE_DIR}/entitlements.plist")
+endif()

From b705e322e5dd479d682145779cbd24cca7811441 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Fri, 13 Jun 2025 13:07:12 -0700
Subject: [PATCH 02/16] Add tests to verify new inodes are created in apphost
 signing

---
 .../tests/AppHost.Bundle.Tests/AppLaunch.cs   | 33 ++++++++++++
 .../AppHost/CreateAppHost.cs                  | 50 +++++++++++++++++++
 .../tests/TestUtils/SingleFileTestApp.cs      | 18 +++++++
 3 files changed, 101 insertions(+)

diff --git a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
index d859527bc58138..46a84dd32bbf95 100644
--- a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
+++ b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
@@ -6,7 +6,9 @@
 using System.Runtime.InteropServices;
 using System.Text;
 using Microsoft.DotNet.Cli.Build.Framework;
+using Microsoft.DotNet.CoreSetup;
 using Microsoft.DotNet.CoreSetup.Test;
+using Microsoft.NET.HostModel.Bundle;
 using Xunit;
 
 namespace AppHost.Bundle.Tests
@@ -80,6 +82,37 @@ private void RunApp(bool selfContained)
             }
         }
 
+        [Fact]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        private void OverwritingExistingBundleClearsMacOsSignatureCache()
+        {
+            // Bundle to a single-file and ensure it is signed
+            string singleFile = sharedTestState.SelfContainedApp.Bundle();
+            Assert.True(Codesign.Run("-v", singleFile).ExitCode == 0);
+            var firstls = Command.Create("/bin/ls", "-li", singleFile)
+                .CaptureStdErr()
+                .CaptureStdOut()
+                .Execute();
+            firstls.Should().Pass();
+            var firstInode = firstls.StdOut.Split(' ')[0];
+
+            // Rebundle to the same location.
+            // Bundler should create a new inode for the bundle which should clear the MacOS signature cache.
+            string oldFile = singleFile;
+            string dir = Path.GetDirectoryName(singleFile);
+            singleFile = sharedTestState.SelfContainedApp.ReBundle(dir, BundleOptions.BundleAllContent, out var _, new Version(5, 0));
+            Assert.True(singleFile == oldFile, "Rebundled app should have a different path than the original single-file app.");
+            var secondls = Command.Create("/bin/ls", "-li", singleFile)
+                .CaptureStdErr()
+                .CaptureStdOut()
+                .Execute();
+            secondls.Should().Pass();
+            var secondInode = secondls.StdOut.Split(' ')[0];
+            Assert.False(firstInode == secondInode, "not a different inode after rebundle");
+            // Ensure the MacOS signature cache is cleared
+            Assert.True(Codesign.Run("-v", singleFile).ExitCode == 0);
+        }
+
         [ConditionalTheory(typeof(Binaries.CetCompat), nameof(Binaries.CetCompat.IsSupported))]
         [InlineData(true)]
         [InlineData(false)]
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 8cccab87aca44b..24958b3de76749 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
@@ -20,6 +20,7 @@
 using System.Buffers.Binary;
 using System.IO.MemoryMappedFiles;
 using Microsoft.NET.HostModel.MachO.CodeSign.Tests;
+using System.ComponentModel;
 
 namespace Microsoft.NET.HostModel.AppHost.Tests
 {
@@ -285,6 +286,55 @@ public void CodeSignMachOAppHost(string subdir)
             }
         }
 
+        [Theory]
+        [InlineData("")]
+        [InlineData("dir with spaces")]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        public void SigningExistingAppHostCreatesNewInode(string subdir)
+        {
+            using (TestArtifact artifact = CreateTestDirectory())
+            {
+                string testDirectory = Path.Combine(artifact.Location, subdir);
+                Directory.CreateDirectory(testDirectory);
+                string sourceAppHostMock = Binaries.AppHost.FilePath;
+                string destinationFilePath = Path.Combine(testDirectory, Binaries.AppHost.FileName);
+                string appBinaryFilePath = "Test/App/Binary/Path.dll";
+                HostWriter.CreateAppHost(
+                   sourceAppHostMock,
+                   destinationFilePath,
+                   appBinaryFilePath,
+                   windowsGraphicalUserInterface: false,
+                   enableMacOSCodeSign: true);
+                var firstls = Command.Create("/bin/ls", "-li", destinationFilePath)
+                    .CaptureStdErr()
+                    .CaptureStdOut()
+                    .Execute();
+                firstls.Should().Pass();
+                var firstInode = firstls.StdOut.Split(' ')[0];
+
+                // Validate that there is a signature present in the apphost Mach file
+                SigningTests.IsSigned(destinationFilePath).Should().BeTrue();
+
+                HostWriter.CreateAppHost(
+                   sourceAppHostMock,
+                   destinationFilePath,
+                   appBinaryFilePath,
+                   windowsGraphicalUserInterface: false,
+                   enableMacOSCodeSign: true);
+
+                var secondls = Command.Create("/bin/ls", "-li", destinationFilePath)
+                    .CaptureStdErr()
+                    .CaptureStdOut()
+                    .Execute();
+                secondls.Should().Pass();
+                var secondInode = secondls.StdOut.Split(' ')[0];
+                // Ensure the MacOS signature cache is cleared
+                Assert.False(firstInode == secondInode, "not a different inode after rebundle");
+
+                SigningTests.IsSigned(destinationFilePath).Should().BeTrue();
+            }
+        }
+
         [Theory]
         [InlineData("")]
         [InlineData("dir with spaces")]
diff --git a/src/installer/tests/TestUtils/SingleFileTestApp.cs b/src/installer/tests/TestUtils/SingleFileTestApp.cs
index fa4ff517dd23f1..7348d893206732 100644
--- a/src/installer/tests/TestUtils/SingleFileTestApp.cs
+++ b/src/installer/tests/TestUtils/SingleFileTestApp.cs
@@ -92,6 +92,24 @@ public string Bundle(BundleOptions options = BundleOptions.None, Version? bundle
         public string Bundle(BundleOptions options, out Manifest manifest, Version? bundleVersion = null)
         {
             string bundleDirectory = GetUniqueSubdirectory("bundle");
+            return Bundle(options, bundleDirectory, out manifest, bundleVersion);
+        }
+
+        public string ReBundle(string bundleDirectory, BundleOptions options, out Manifest manifest, Version? bundleVersion = null)
+        {
+            // Reuse the existing bundle directory if it exists
+            if (!Directory.Exists(bundleDirectory))
+            {
+                throw new InvalidOperationException(
+                    $"The bundle directory '{bundleDirectory}' does not exist. " +
+                    "Please ensure the directory is created before rebundling.");
+            }
+
+            return Bundle(options, bundleDirectory, out manifest, bundleVersion);
+        }
+
+        private string Bundle(BundleOptions options, string bundleDirectory, out Manifest manifest, Version? bundleVersion = null)
+        {
             var bundler = new Bundler(
                 Binaries.GetExeName(AppName),
                 bundleDirectory,

From e2ead9780b77113fdf0ebe78f0159e8c7507ec14 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Fri, 13 Jun 2025 14:51:36 -0700
Subject: [PATCH 03/16] Update blob parser and special slot count for new
 blobs, add tests for Manifest lengths

---
 .../Microsoft.NET.HostModel/Bundle/Bundler.cs |  7 ++---
 .../MachO/BinaryFormat/Blobs/BlobParser.cs    | 13 ++++++++-
 .../BinaryFormat/Blobs/CodeDirectoryBlob.cs   |  4 ++-
 .../Bundle/BundlerConsistencyTests.cs         | 29 +++++++++++++++++++
 .../MachObjectSigning/MachObjectTests.cs      | 21 +++++++++++++-
 5 files changed, 66 insertions(+), 8 deletions(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index acbceae6a83906..259c0a82882c1a 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -500,13 +500,10 @@ internal static uint GetBinaryWriterStringLength(string str)
             // Prefixed length of bundle ID is 7-bit encoded
             // Strings 0-127 chars: 1 byte prefix
             // Strings 128-16,383 chars: 2 byte prefix
-            // Strings 16,384-2,097,151 chars: 3 byte prefix
-            // Strings 2,097,152-268,435,455 chars: 4 byte prefix
-            // Strings 268,435,456+ chars: 5 byte prefix
+            // Strings longer than 16,383 bytes are not supported and fail at runtime.
             uint lengthPrefixLength = (stringLength < 128) ? 1u :
                            (stringLength < 16384) ? 2u :
-                           (stringLength < 2097152) ? 3u :
-                           (stringLength < 268435456) ? 4u : 5u;
+                           throw new ArgumentException("Cannot write strings longer than 16,383 bytes to the bundle.");
             return lengthPrefixLength + stringLength;
         }
     }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/BlobParser.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/BlobParser.cs
index 41bc2a7b5a75ca..b8157940bd04a1 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/BlobParser.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/BlobParser.cs
@@ -1,6 +1,9 @@
 // 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.Diagnostics;
+
 namespace Microsoft.NET.HostModel.MachO;
 
 /// <summary>
@@ -23,7 +26,15 @@ public static IBlob ParseBlob(IMachOFileReader reader, long offset)
             BlobMagic.Requirements => new RequirementsBlob(SuperBlob.Read(reader, offset)),
             BlobMagic.CmsWrapper => new CmsWrapperBlob(SimpleBlob.Read(reader, offset)),
             BlobMagic.EmbeddedSignature => new EmbeddedSignatureBlob(SuperBlob.Read(reader, offset)),
-            _ => SimpleBlob.Read(reader, offset)
+            BlobMagic.Entitlements => new EntitlementsBlob(SimpleBlob.Read(reader, offset)),
+            BlobMagic.DerEntitlements => new DerEntitlementsBlob(SimpleBlob.Read(reader, offset)),
+            _ => CreateUnknownBlob(magic, reader, offset),
         };
+
+        static SimpleBlob CreateUnknownBlob(BlobMagic magic, IMachOFileReader reader, long offset)
+        {
+            Debug.Assert(!Enum.IsDefined(typeof(BlobMagic), magic), "Blob magic is known but not handled.");
+            return SimpleBlob.Read(reader, offset);
+        }
     }
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index ca307270f38386..1b9b4f8462a26d 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -108,7 +108,9 @@ public static CodeDirectoryBlob Create(
         uint pageSize = MachObjectFile.DefaultPageSize)
     {
         uint codeSlotCount = GetCodeSlotCount((uint)signatureStart, pageSize);
-        uint specialCodeSlotCount = (uint)CodeDirectorySpecialSlot.Requirements;
+        uint specialCodeSlotCount = (uint)(derEntitlementsBlob != null ? CodeDirectorySpecialSlot.DerEntitlements :
+            entitlementsBlob != null ? CodeDirectorySpecialSlot.Entitlements :
+            CodeDirectorySpecialSlot.Requirements);
 
         var specialSlotHashes = new byte[specialCodeSlotCount][];
         var codeHashes = new byte[codeSlotCount][];
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 cb9d3ca634d62d..77e347970826e1 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
@@ -314,6 +315,34 @@ public void AssemblyAlignment()
                 Assert.True((file.Type != FileType.Assembly) || (file.Offset % alignment == 0)));
         }
 
+        [Fact]
+        [Conditional("DEBUG")] // Relies on debug asserts in product code
+        public void LongFileNames()
+        {
+            var app = sharedTestState.App;
+            List<FileSpec> fileSpecs = new List<FileSpec>
+            {
+                new FileSpec(Binaries.AppHost.FilePath, BundlerHostName),
+                new FileSpec(app.AppDll, Path.Join(
+                    Path.GetDirectoryName(Path.GetRelativePath(app.Location, app.AppDll)),
+                    Path.GetFileNameWithoutExtension(app.AppDll) + new string('a', 260) + Path.GetExtension(app.AppDll))),
+            };
+
+            fileSpecs.AddRange(SingleFileTestApp.GetRuntimeFilesToBundle());
+            Bundler bundler = CreateBundlerInstance();
+            // Debug asserts in the Manifest and Bundler should catch size calculation issues related to long file names
+            var bundledPath = bundler.GenerateBundle(fileSpecs);
+
+            fileSpecs.Add(new FileSpec(app.AppDll, Path.Join(
+                Path.GetDirectoryName(Path.GetRelativePath(app.Location, app.AppDll)),
+                Path.GetFileNameWithoutExtension(app.AppDll) + new string('a', 16385) + Path.GetExtension(app.AppDll))));
+            Assert.Throws<ArgumentException>(() =>
+            {
+                // This should throw an exception due to the long file name exceeding the maximum allowed length
+                bundler.GenerateBundle(fileSpecs);
+            });
+        }
+
         [Theory]
         [InlineData(true)]
         [InlineData(false)]
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index 3077a420090d33..eb7566543a9ef2 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -40,7 +40,8 @@ public void StreamAndMemoryMappedFileAreTheSame(string filePath, TestArtifact te
     [MemberData(nameof(GetTestFilePaths), nameof(RoundTripMachObjectFileIsTheSame))]
     void RoundTripMachObjectFileIsTheSame(string filePath, TestArtifact testArtifact)
     {
-        using var testArtifactLocation = testArtifact;
+        var backupFilePath = filePath + ".bak";
+        File.Copy(filePath, backupFilePath);
         using (var mmap = MemoryMappedFile.CreateFromFile(filePath))
         using (var accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
         {
@@ -50,6 +51,24 @@ void RoundTripMachObjectFileIsTheSame(string filePath, TestArtifact testArtifact
             var rewrittenMachFile = MachObjectFile.Create(machFile);
             MachObjectFile.AssertEquivalent(machObjectFile, rewrittenMachFile);
         }
+        using (FileStream original = new FileStream(backupFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+        using (FileStream written = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+        {
+            Assert.Equal(original.Length, written.Length);
+            byte[] originalBuffer = new byte[4096];
+            byte[] writtenBuffer = new byte[4096];
+            while (true)
+            {
+                int bytesReadOriginal = original.Read(originalBuffer, 0, originalBuffer.Length);
+                int bytesReadWritten = written.Read(writtenBuffer, 0, writtenBuffer.Length);
+                Assert.Equal(bytesReadOriginal, bytesReadWritten);
+
+                if (bytesReadOriginal == 0)
+                    break;
+
+                Assert.True(originalBuffer.Take(bytesReadOriginal).SequenceEqual(writtenBuffer.Take(bytesReadWritten)));
+            }
+        }
     }
 
     static readonly ImmutableArray<string> liveBuiltHosts = ImmutableArray.Create(Binaries.AppHost.FilePath, Binaries.SingleFileHost.FilePath);

From 29f6fb8121f0b391b762371086fb5abdfa6c36dc Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Fri, 13 Jun 2025 18:13:14 -0700
Subject: [PATCH 04/16] Add tests to compare EmbeddedSignatureBlob data to
 codesign output

---
 .../BinaryFormat/Blobs/CodeDirectoryBlob.cs   |  22 +-
 .../MachObjectSigning/MachObjectTests.cs      | 252 +++++++++++++++++-
 2 files changed, 257 insertions(+), 17 deletions(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index 1b9b4f8462a26d..c712b17c73dfbe 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -4,6 +4,8 @@
 #nullable enable
 
 using System;
+using System.Collections;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
@@ -97,6 +99,21 @@ private CodeDirectoryBlob(
         + SpecialSlotCount * HashSize
         + CodeSlotCount * HashSize;
 
+    public string Identifier => _identifier;
+    public CodeDirectoryFlags Flags => (CodeDirectoryFlags)((uint)_cdHeader._flags).ConvertFromBigEndian();
+    public CodeDirectoryVersion Version => (CodeDirectoryVersion)((uint)_cdHeader._version).ConvertFromBigEndian();
+    public IReadOnlyList<IReadOnlyList<byte>> SpecialSlotHashes => _specialSlotHashes;
+    public IReadOnlyList<IReadOnlyList<byte>> CodeHashes => _codeHashes;
+    public ulong ExecutableSegmentBase => _cdHeader._execSegmentBase.ConvertFromBigEndian();
+    public ulong ExecutableSegmentLimit => _cdHeader._execSegmentLimit.ConvertFromBigEndian();
+    public ExecutableSegmentFlags ExecutableSegmentFlags => (ExecutableSegmentFlags)((ulong)_cdHeader._execSegmentFlags).ConvertFromBigEndian();
+
+    private uint SpecialSlotCount => _cdHeader._specialSlotCount.ConvertFromBigEndian();
+    private uint CodeSlotCount => _cdHeader._codeSlotCount.ConvertFromBigEndian();
+    private byte HashSize => _cdHeader.HashSize;
+    private uint HashesOffset => _cdHeader._hashesOffset.ConvertFromBigEndian();
+
+
     public static CodeDirectoryBlob Create(
         IMachOFileReader accessor,
         long signatureStart,
@@ -226,11 +243,6 @@ public CodeDirectoryHeader(string identifier, uint codeSlotCount, uint specialCo
         }
     }
 
-    public uint HashesOffset => _cdHeader._hashesOffset.ConvertFromBigEndian();
-    public uint SpecialSlotCount => _cdHeader._specialSlotCount.ConvertFromBigEndian();
-    public uint CodeSlotCount => _cdHeader._codeSlotCount.ConvertFromBigEndian();
-    public byte HashSize => _cdHeader.HashSize;
-
     public override bool Equals(object? obj)
     {
         if (obj is not CodeDirectoryBlob other)
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index eb7566543a9ef2..8f221639ae6b11 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -7,19 +7,25 @@
 using System.IO.MemoryMappedFiles;
 using System.Linq;
 using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using Microsoft.DotNet.CoreSetup;
 using Microsoft.DotNet.CoreSetup.Test;
 using Microsoft.NET.HostModel.MachO;
+using Microsoft.NET.HostModel.MachO.CodeSign.Tests;
 using Xunit;
+using Xunit.Abstractions;
 
 public class MachObjectTests
 {
-    private readonly List<TestArtifact> _testArtifacts = new();
-
+    ITestOutputHelper output;
+    public MachObjectTests(ITestOutputHelper output)
+    {
+        this.output = output;
+    }
     [Theory]
     [MemberData(nameof(GetTestFilePaths), nameof(StreamAndMemoryMappedFileAreTheSame))]
-    public void StreamAndMemoryMappedFileAreTheSame(string filePath, TestArtifact testArtifact)
+    public void StreamAndMemoryMappedFileAreTheSame(string filePath, TestArtifact _)
     {
-        using var testArtifactLocation = testArtifact;
         MachObjectFile streamMachOFile;
         MachObjectFile memoryMappedMachOFile;
         using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
@@ -66,7 +72,7 @@ void RoundTripMachObjectFileIsTheSame(string filePath, TestArtifact testArtifact
                 if (bytesReadOriginal == 0)
                     break;
 
-                Assert.True(originalBuffer.Take(bytesReadOriginal).SequenceEqual(writtenBuffer.Take(bytesReadWritten)));
+                Assert.True(originalBuffer.SequenceEqual(writtenBuffer));
             }
         }
     }
@@ -100,19 +106,241 @@ public static Object[][] GetTestFilePaths(string testArtifactName)
         return arguments.ToArray();
     }
 
-    public void Dispose()
+    [Fact]
+    public void CanParseCodesignOutput()
+    {
+        var parsed = CodesignOutputInfo.ParseFromCodeSignOutput(SampleCodesignOutput);
+        Assert.NotNull(parsed);
+        output.WriteLine(parsed.ToString());
+        var expected = new CodesignOutputInfo
+        {
+            Identifier = "singlefilehost-5555494409d4df688bf436b291061028f736b11c",
+            CodeDirectoryFlags = CodeDirectoryFlags.Adhoc,
+            CodeDirectoryVersion = CodeDirectoryVersion.SupportsExecSegment,
+            ExecutableSegmentBase = 0,
+            ExecutableSegmentLimit = 8949760,
+            ExecutableSegmentFlags = ExecutableSegmentFlags.MainBinary,
+            RequirementsSize = 12,
+            SpecialSlotHashes = [
+                    [0x4d, 0x8d, 0x4b, 0x9e, 0x41, 0x16, 0xe8, 0xed, 0xd9, 0x96, 0x17, 0x6b, 0x55, 0x53, 0x46, 0x3a, 0xcb, 0x64, 0x28, 0x7b, 0xb6, 0x35, 0xe7, 0xf1, 0x41, 0x15, 0x55, 0x29, 0xe2, 0x04, 0x57, 0xbc],
+                    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+                    [0xcc, 0xa8, 0xaf, 0xe7, 0x24, 0x25, 0x46, 0x3c, 0x13, 0xb8, 0x13, 0xda, 0x9a, 0xe4, 0x68, 0xae, 0x3b, 0x5f, 0xe2, 0x0f, 0xe5, 0xfe, 0x1d, 0x3f, 0x34, 0x30, 0x2b, 0xa2, 0xf1, 0x57, 0x22, 0xf2],
+                    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+                    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+                    [0x98, 0x79, 0x20, 0x90, 0x4e, 0xab, 0x65, 0x0e, 0x75, 0x78, 0x8c, 0x05, 0x4a, 0xa0, 0xb0, 0x52, 0x4e, 0x6a, 0x80, 0xbf, 0xc7, 0x1a, 0xa3, 0x2d, 0xf8, 0xd2, 0x37, 0xa6, 0x17, 0x43, 0xf9, 0x86],
+                    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+                ],
+            CodeHashes = [
+                    [0x20, 0x04, 0x29, 0x93, 0x66, 0x56, 0x11, 0xbf, 0x5d, 0x01, 0xd3, 0x5a, 0x46, 0x09, 0x2c, 0x2d, 0x43, 0xa0, 0x78, 0x83, 0xf3, 0x12, 0x47, 0xa0, 0x3b, 0x56, 0x00, 0xc3, 0x01, 0xf5, 0xc0, 0x39],
+                    [0xa9, 0x7f, 0xad, 0x07, 0xcc, 0x9d, 0x6e, 0xab, 0xad, 0x27, 0xa7, 0x7e, 0x32, 0xb6, 0x9c, 0x3d, 0xa5, 0x93, 0x72, 0xfa, 0x79, 0x87, 0xa1, 0x3c, 0x2b, 0x8d, 0x23, 0xf3, 0x78, 0x38, 0x04, 0x76],
+                    [0xad, 0x7f, 0xac, 0xb2, 0x58, 0x6f, 0xc6, 0xe9, 0x66, 0xc0, 0x04, 0xd7, 0xd1, 0xd1, 0x6b, 0x02, 0x4f, 0x58, 0x05, 0xff, 0x7c, 0xb4, 0x7c, 0x7a, 0x85, 0xda, 0xbd, 0x8b, 0x48, 0x89, 0x2c, 0xa7],
+                    [0xad, 0x7f, 0xac, 0xb2, 0x58, 0x6f, 0xc6, 0xe9, 0x66, 0xc0, 0x04, 0xd7, 0xd1, 0xd1, 0x6b, 0x02, 0x4f, 0x58, 0x05, 0xff, 0x7c, 0xb4, 0x7c, 0x7a, 0x85, 0xda, 0xbd, 0x8b, 0x48, 0x89, 0x2c, 0xa7],
+                    [0xad, 0x7f, 0xac, 0xb2, 0x58, 0x6f, 0xc6, 0xe9, 0x66, 0xc0, 0x04, 0xd7, 0xd1, 0xd1, 0x6b, 0x02, 0x4f, 0x58, 0x05, 0xff, 0x7c, 0xb4, 0x7c, 0x7a, 0x85, 0xda, 0xbd, 0x8b, 0x48, 0x89, 0x2c, 0xa7],
+                    [0xb3, 0xd2, 0x30, 0x34, 0x0a, 0xa5, 0xed, 0x09, 0xc7, 0x88, 0xc3, 0x90, 0x81, 0xc2, 0x07, 0xa7, 0x43, 0x0b, 0x83, 0xd2, 0x2c, 0x94, 0x89, 0xd8, 0x4d, 0x4e, 0xde, 0x3e, 0xd3, 0x20, 0xf4, 0x7b],
+                    [0x82, 0x5b, 0x7a, 0xa1, 0x61, 0x70, 0xa9, 0xb7, 0x39, 0xa4, 0x68, 0x9b, 0xa8, 0x87, 0x83, 0x91, 0xbc, 0xae, 0x87, 0xef, 0xd6, 0x3e, 0x3b, 0x17, 0x47, 0x38, 0xc3, 0x82, 0x02, 0x00, 0x31, 0xc1],
+                    [0xe3, 0x60, 0x15, 0x9e, 0xe0, 0xad, 0xae, 0xba, 0x5a, 0xc5, 0xf5, 0x62, 0xc4, 0x5e, 0xc5, 0x51, 0xdb, 0xe8, 0xb7, 0x3f, 0xbc, 0x85, 0x8b, 0xec, 0xa2, 0x98, 0x61, 0x03, 0x12, 0xdf, 0x33, 0xb3],
+                    [0x20, 0x58, 0x5e, 0xf0, 0xbc, 0x02, 0x87, 0xc5, 0xb7, 0xa9, 0xb5, 0x4f, 0x26, 0x69, 0x70, 0x4c, 0xdc, 0x31, 0xce, 0xa7, 0xd7, 0xb1, 0x70, 0x2b, 0x33, 0x6f, 0xcf, 0x93, 0xa9, 0xf0, 0x1c, 0xa2],
+                    [0x41, 0x4a, 0xe6, 0x56, 0x3e, 0x58, 0x81, 0xb2, 0x15, 0xa0, 0x8b, 0xb3, 0x3f, 0xc5, 0x39, 0xfb, 0x0c, 0x90, 0xc3, 0xa5, 0x53, 0x2f, 0x6e, 0x15, 0xa7, 0x26, 0xed, 0x6c, 0xdc, 0x25, 0x55, 0x50],
+                    [0xb6, 0x72, 0xb6, 0x67, 0xeb, 0x31, 0xb4, 0x8d, 0x02, 0x7b, 0xd5, 0xf1, 0xcf, 0x75, 0xba, 0xd5, 0xa8, 0x55, 0x2b, 0x4d, 0x6b, 0x64, 0x9c, 0xbd, 0xae, 0x35, 0x69, 0x91, 0x52, 0xfb, 0x8a, 0x1b]
+                ],
+        };
+        Assert.Equal(expected, parsed);
+    }
+
+    // test all the binaries compared to codesinginfo from codesign output
+    [Theory]
+    [MemberData(nameof(GetTestFilePaths), nameof(EmbeddedSignatureBlobMatchesCodesignInfo
+))]
+    public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifact testArtifact)
+    {
+        if(!SigningTests.IsSigned(filePath))
+        {
+            return;
+        }
+        using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+        {
+            MachObjectFile machObjectFile = MachObjectFile.Create(new StreamBasedMachOFile(stream));
+            Assert.True(machObjectFile.HasSignature, "Expected MachObjectFile to have a signature");
+            EmbeddedSignatureBlob? embeddedSignatureBlob = machObjectFile.EmbeddedSignatureBlob;
+            Assert.NotNull(embeddedSignatureBlob);
+            var (exitcode, stderr) = Codesign.Run("--display --verbose=8", filePath);
+            if (exitcode != 0)
+            {
+                output.WriteLine($"Codesign command failed with exit code {exitcode}: {stderr}");
+                Assert.True(false, "Codesign command failed");
+            }
+            output.WriteLine($"Codesign output for {filePath}:\n{stderr}");
+            CodesignOutputInfo codesignInfo = CodesignOutputInfo.ParseFromCodeSignOutput(stderr);
+            output.WriteLine($"Comparing {filePath} to codesign info: {codesignInfo}");
+            output.WriteLine($"specialSlotHashes: {string.Join(", ", codesignInfo.SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}");
+            output.WriteLine($"machObjectFile specialSlotHashes: {string.Join(", ", embeddedSignatureBlob.CodeDirectoryBlob.SpecialSlotHashes.Select(h => BitConverter.ToString(h.ToArray()).Replace("-", "")))}");
+            AssertEqual(codesignInfo, embeddedSignatureBlob);
+        }
+    }
+
+    static void AssertEqual(CodesignOutputInfo csi, EmbeddedSignatureBlob b)
+    {
+        Assert.True(csi.Identifier == b.CodeDirectoryBlob.Identifier, "Identifiers do not match");
+        Assert.True(csi.CodeDirectoryFlags == b.CodeDirectoryBlob.Flags, "CodeDirectoryFlags do not match");
+        Assert.True(csi.CodeDirectoryVersion == b.CodeDirectoryBlob.Version, "CodeDirectoryVersion do not match");
+        Assert.True(csi.ExecutableSegmentBase == b.CodeDirectoryBlob.ExecutableSegmentBase, "ExecutableSegmentBase do not match");
+        Assert.True(csi.ExecutableSegmentLimit == b.CodeDirectoryBlob.ExecutableSegmentLimit, "ExecutableSegmentLimit do not match");
+        Assert.True(csi.ExecutableSegmentFlags == b.CodeDirectoryBlob.ExecutableSegmentFlags, "ExecutableSegmentFlags do not match");
+        Assert.True(csi.RequirementsSize == b.RequirementsBlob?.Size, "RequirementsSize do not match");
+
+        Assert.True(csi.SpecialSlotHashes.Zip(b.CodeDirectoryBlob.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
+        Assert.True(csi.CodeHashes.Zip(b.CodeDirectoryBlob.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
+    }
+
+    /// <summary>
+    /// Info related to the code signature that can be extracted from the output of the `codesign` command.
+    /// </summary>
+    internal sealed record CodesignOutputInfo
     {
-        foreach (var artifact in _testArtifacts)
+        public string Identifier { get; init; }
+        public CodeDirectoryFlags CodeDirectoryFlags { get; init; }
+        public CodeDirectoryVersion CodeDirectoryVersion { get; init; }
+        public ulong ExecutableSegmentBase { get; init; }
+        public ulong ExecutableSegmentLimit { get; init; }
+        public ExecutableSegmentFlags ExecutableSegmentFlags { get; init; }
+        public uint RequirementsSize { get; init; }
+        public byte[][] SpecialSlotHashes { get; init; }
+        public byte[][] CodeHashes { get; init; }
+
+        public bool Equals(CodesignOutputInfo? obj)
+        {
+            if (obj is not CodesignOutputInfo other)
+                return false;
+
+            return Identifier == other.Identifier &&
+                CodeDirectoryFlags == other.CodeDirectoryFlags &&
+                CodeDirectoryVersion == other.CodeDirectoryVersion &&
+                ExecutableSegmentBase == other.ExecutableSegmentBase &&
+                ExecutableSegmentLimit == other.ExecutableSegmentLimit &&
+                ExecutableSegmentFlags == other.ExecutableSegmentFlags &&
+                RequirementsSize == other.RequirementsSize &&
+                SpecialSlotHashes.Length == other.SpecialSlotHashes.Length &&
+                CodeHashes.Length == other.CodeHashes.Length &&
+                SpecialSlotHashes.Zip(other.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x) &&
+                CodeHashes.Zip(other.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x);
+        }
+        public override int GetHashCode()
         {
-            try
+            return HashCode.Combine(Identifier, CodeDirectoryFlags, CodeDirectoryVersion, ExecutableSegmentLimit, ExecutableSegmentFlags, RequirementsSize, SpecialSlotHashes, CodeHashes);
+        }
+
+        /// <summary>
+        /// Parses the output of the `codesign` command to extract codesign information.
+        /// </summary>
+        public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
+        {
+            var lines = output.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+            var Identifier = lines[1].Split('=')[1].Trim();
+            var cdInfo = lines[3].Split(' ');
+            var CodeDirectoryVersion = (CodeDirectoryVersion)Convert.ToUInt32(cdInfo[1].Split('=')[1].Trim(), 16);
+            var CodeDirectoryFlags = (CodeDirectoryFlags)Convert.ToUInt32(cdInfo[3].Split('=', '(')[1].Trim().TrimStart("0x").ToString(), 16);
+            Assert.True(lines[13].StartsWith("Executable Segment base="), "Expected 'Executable Segment base=' at line 13");
+            Assert.True(lines[14].StartsWith("Executable Segment limit="), "Expected 'Executable Segment limit=' at line 14");
+            Assert.True(lines[15].StartsWith("Executable Segment flags="), "Expected 'Executable Segment flags=' at line 15");
+            var ExecutableSegmentBase = ulong.Parse(lines[13].Split('=')[1].Trim());
+            var ExecutableSegmentLimit = ulong.Parse(lines[14].Split('=')[1].Trim());
+            var ExecutableSegmentFlags = (ExecutableSegmentFlags)Convert.ToUInt64(lines[15].Split('=')[1].Trim().TrimStart("0x").ToString(), 16);
+            Assert.True(lines[16].StartsWith("Page size=4096"), "Expected 'Page size=4096' at line 16");
+            var (SpecialSlotHashes, CodeHashes) = ExtractHashes(lines.Skip(17));
+            var RequirementsSize = uint.Parse(lines[^1].Split('=')[2].Trim());
+
+            return new CodesignOutputInfo
             {
-                artifact.Dispose();
+                Identifier = Identifier,
+                CodeDirectoryFlags = CodeDirectoryFlags,
+                CodeDirectoryVersion = CodeDirectoryVersion,
+                ExecutableSegmentBase = ExecutableSegmentBase,
+                ExecutableSegmentLimit = ExecutableSegmentLimit,
+                ExecutableSegmentFlags = ExecutableSegmentFlags,
+                SpecialSlotHashes = SpecialSlotHashes,
+                CodeHashes = CodeHashes,
+                RequirementsSize = RequirementsSize
+            };
+
+            static (byte[][] SpecialSlotHashes, byte[][] CodeHashes) ExtractHashes(IEnumerable<string> lines)
+            {
+                List<byte[]> specialSlotHashes = [];
+                List<byte[]> codeHashes = [];
+                foreach (var line in lines)
+                {
+                    if (line[0] is not ' ' or '\t')
+                        break;
+                    var hash = line.Split('=')[1].Trim();
+                    var index = int.Parse(line.Split('=')[0].Trim());
+                    if (index < 0)
+                    {
+                        // specialSlot
+                        specialSlotHashes.Add(ParseByteArray(hash));
+
+                    }
+                    else
+                    {
+                        // codeHashes
+                        codeHashes.Add(ParseByteArray(hash));
+                    }
+                }
+                return (specialSlotHashes.ToArray(), codeHashes.ToArray());
             }
-            catch (Exception ex)
+            static byte[] ParseByteArray(string hex)
             {
-                Console.WriteLine($"Failed to dispose test artifact: {ex.Message}");
+                if (hex.Length % 2 != 0)
+                    throw new ArgumentException("Hex string must have an even length.");
+                byte[] bytes = new byte[hex.Length / 2];
+                for (int i = 0; i < hex.Length; i += 2)
+                {
+                    bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
+                }
+                return bytes;
             }
         }
-        _testArtifacts.Clear();
     }
+
+    string SampleCodesignOutput = """
+    Executable=/Users/jacksonschuster/source/runtime3/artifacts/bin/osx-x64.Debug/corehost/singlefilehost
+    Identifier=singlefilehost-5555494409d4df688bf436b291061028f736b11c
+    Format=Mach-O thin (x86_64)
+    CodeDirectory v=20400 size=89264 flags=0x2(adhoc) hashes=2778+7 location=embedded
+    VersionPlatform=1
+    VersionMin=786432
+    VersionSDK=984064
+    Hash type=sha256 size=32
+    CandidateCDHash sha256=6fee638e9fe544a66b0acf9489ebc59e073b3e39
+    CandidateCDHashFull sha256=6fee638e9fe544a66b0acf9489ebc59e073b3e39082903247c5413f555ec2857
+    Hash choices=sha256
+    CMSDigest=6fee638e9fe544a66b0acf9489ebc59e073b3e39082903247c5413f555ec2857
+    CMSDigestType=2
+    Executable Segment base=0
+    Executable Segment limit=8949760
+    Executable Segment flags=0x1
+    Page size=4096
+        -7=4d8d4b9e4116e8edd996176b5553463acb64287bb635e7f141155529e20457bc
+        -6=0000000000000000000000000000000000000000000000000000000000000000
+        -5=cca8afe72425463c13b813da9ae468ae3b5fe20fe5fe1d3f34302ba2f15722f2
+        -4=0000000000000000000000000000000000000000000000000000000000000000
+        -3=0000000000000000000000000000000000000000000000000000000000000000
+        -2=987920904eab650e75788c054aa0b0524e6a80bfc71aa32df8d237a61743f986
+        -1=0000000000000000000000000000000000000000000000000000000000000000
+        0=20042993665611bf5d01d35a46092c2d43a07883f31247a03b5600c301f5c039
+        1=a97fad07cc9d6eabad27a77e32b69c3da59372fa7987a13c2b8d23f378380476
+        2=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
+        3=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
+        4=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
+        5=b3d230340aa5ed09c788c39081c207a7430b83d22c9489d84d4ede3ed320f47b
+        6=825b7aa16170a9b739a4689ba8878391bcae87efd63e3b174738c382020031c1
+        7=e360159ee0adaeba5ac5f562c45ec551dbe8b73fbc858beca298610312df33b3
+        8=20585ef0bc0287c5b7a9b54f2669704cdc31cea7d7b1702b336fcf93a9f01ca2
+        9=414ae6563e5881b215a08bb33fc539fb0c90c3a5532f6e15a726ed6cdc255550
+        10=b672b667eb31b48d027bd5f1cf75bad5a8552b4d6b649cbdae35699152fb8a1b
+    CDHash=6fee638e9fe544a66b0acf9489ebc59e073b3e39
+    Signature=adhoc
+    Info.plist=not bound
+    TeamIdentifier=not set
+    Sealed Resources=none
+    Internal requirements count=0 size=12
+    """;
 }

From 839c318345508240e98757b95d8031c14cbadfc4 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Mon, 16 Jun 2025 12:13:38 -0700
Subject: [PATCH 05/16] Make test platform specific

---
 .../MachObjectSigning/MachObjectTests.cs                      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index 8f221639ae6b11..59fe912f84d564 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -149,8 +149,8 @@ public void CanParseCodesignOutput()
 
     // test all the binaries compared to codesinginfo from codesign output
     [Theory]
-    [MemberData(nameof(GetTestFilePaths), nameof(EmbeddedSignatureBlobMatchesCodesignInfo
-))]
+    [MemberData(nameof(GetTestFilePaths), nameof(EmbeddedSignatureBlobMatchesCodesignInfo))]
+    [PlatformSpecific(TestPlatforms.OSX)]
     public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifact testArtifact)
     {
         if(!SigningTests.IsSigned(filePath))

From 04664086e5e9a33bc18c49b28bbf1bd97bb72323 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Mon, 16 Jun 2025 16:27:31 -0700
Subject: [PATCH 06/16] Remove RequirementsSize from CodesignInfo, preserve
 entitlements in codesign tests

---
 .../BinaryFormat/Blobs/CodeDirectoryBlob.cs   |  1 -
 .../ResourceUpdater.cs                        | 21 +++++---
 .../MachObjectSigning/MachObjectTests.cs      | 51 +++++++++++--------
 .../MachObjectSigning/SigningTests.cs         |  7 ++-
 4 files changed, 47 insertions(+), 33 deletions(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index c712b17c73dfbe..da4504664efbca 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -113,7 +113,6 @@ private CodeDirectoryBlob(
     private byte HashSize => _cdHeader.HashSize;
     private uint HashesOffset => _cdHeader._hashesOffset.ConvertFromBigEndian();
 
-
     public static CodeDirectoryBlob Create(
         IMachOFileReader accessor,
         long signatureStart,
diff --git a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
index 5bb710f2609438..8476121dc62a23 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
@@ -19,7 +19,7 @@ public class ResourceUpdater : IDisposable
     {
         private readonly FileStream? stream;
         private readonly MemoryMappedFile? _memoryMappedFile;
-        private Stream Stream => (Stream?)stream ?? _memoryMappedFile!.CreateViewStream(0, 0, MemoryMappedFileAccess.ReadWrite);
+        private Stream Stream => stream as Stream ?? _memoryMappedFile!.CreateViewStream(0, 0, MemoryMappedFileAccess.ReadWrite);
         private readonly PEReader _reader;
         private ResourceData? _resourceData;
         private readonly bool leaveOpen;
@@ -71,7 +71,7 @@ public ResourceUpdater(MemoryMappedFile memoryMappedFile, bool leaveOpen = false
             catch (Exception)
             {
                 if (!leaveOpen)
-                    this.stream?.Dispose();
+                    this._memoryMappedFile?.Dispose();
                 throw;
             }
         }
@@ -211,12 +211,19 @@ public void Update()
             bool needsMoveTrailingSections = !isRsrcIsLastSection && delta > 0;
             long finalImageSize = trailingSectionStart + Math.Max(delta, 0) + trailingSectionLength;
 
-            using (var mmap = _memoryMappedFile ?? MemoryMappedFile.CreateFromFile(stream!, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
-            using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, finalImageSize, MemoryMappedFileAccess.ReadWrite))
+            // Create a memory-mapped file if we weren't provided one when constructed
+            // and make sure we dispose it after use
+            using var mmap = stream is not null ?
+                MemoryMappedFile.CreateFromFile(stream!, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true)
+                : null;
+
+            // Don't dispose the memory-mapped file if it was provided in the constructor
+            var mmappedFile = mmap ?? _memoryMappedFile!;
+            using (MemoryMappedViewAccessor accessor = mmappedFile.CreateViewAccessor(0, finalImageSize, MemoryMappedFileAccess.ReadWrite))
             {
                 int peSignatureOffset = ReadI32(accessor, PEOffsets.DosStub.PESignatureOffset);
                 int sectionBase = peSignatureOffset + PEOffsets.PEHeaderSize +
-                                  (ushort)_reader.PEHeaders.CoffHeader.SizeOfOptionalHeader;
+                                    (ushort)_reader!.PEHeaders.CoffHeader.SizeOfOptionalHeader;
 
                 if (needsAddSection)
                 {
@@ -279,7 +286,7 @@ void PatchRVA(int offset)
                             pointer => pointer >= trailingSectionVirtualStart ? pointer + virtualDelta : pointer);
                     }
 
-                    int dataDirectoriesOffset = _reader.PEHeaders.PEHeader.Magic == PEMagic.PE32Plus
+                    int dataDirectoriesOffset = _reader.PEHeaders.PEHeader!.Magic == PEMagic.PE32Plus
                         ? peSignatureOffset + PEOffsets.PEHeader.PE64DataDirectories
                         : peSignatureOffset + PEOffsets.PEHeader.PE32DataDirectories;
 
@@ -294,7 +301,7 @@ void PatchRVA(int offset)
                         // fix RVA in DataDirectory
                         for (int i = 0; i < _reader.PEHeaders.PEHeader.NumberOfRvaAndSizes; i++)
                             PatchRVA(dataDirectoriesOffset + i * PEOffsets.DataDirectoryEntrySize +
-                                     PEOffsets.DataDirectoryEntry.VirtualAddressOffset);
+                                        PEOffsets.DataDirectoryEntry.VirtualAddressOffset);
                     }
 
                     // update the ResourceTable in DataDirectories
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index 59fe912f84d564..f9ed77d368d0e2 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -7,7 +7,6 @@
 using System.IO.MemoryMappedFiles;
 using System.Linq;
 using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
 using Microsoft.DotNet.CoreSetup;
 using Microsoft.DotNet.CoreSetup.Test;
 using Microsoft.NET.HostModel.MachO;
@@ -15,6 +14,8 @@
 using Xunit;
 using Xunit.Abstractions;
 
+namespace Microsoft.NET.HostModel.Tests;
+
 public class MachObjectTests
 {
     ITestOutputHelper output;
@@ -44,7 +45,7 @@ public void StreamAndMemoryMappedFileAreTheSame(string filePath, TestArtifact _)
 
     [Theory]
     [MemberData(nameof(GetTestFilePaths), nameof(RoundTripMachObjectFileIsTheSame))]
-    void RoundTripMachObjectFileIsTheSame(string filePath, TestArtifact testArtifact)
+    void RoundTripMachObjectFileIsTheSame(string filePath, TestArtifact _)
     {
         var backupFilePath = filePath + ".bak";
         File.Copy(filePath, backupFilePath);
@@ -120,7 +121,6 @@ public void CanParseCodesignOutput()
             ExecutableSegmentBase = 0,
             ExecutableSegmentLimit = 8949760,
             ExecutableSegmentFlags = ExecutableSegmentFlags.MainBinary,
-            RequirementsSize = 12,
             SpecialSlotHashes = [
                     [0x4d, 0x8d, 0x4b, 0x9e, 0x41, 0x16, 0xe8, 0xed, 0xd9, 0x96, 0x17, 0x6b, 0x55, 0x53, 0x46, 0x3a, 0xcb, 0x64, 0x28, 0x7b, 0xb6, 0x35, 0xe7, 0xf1, 0x41, 0x15, 0x55, 0x29, 0xe2, 0x04, 0x57, 0xbc],
                     [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
@@ -151,7 +151,7 @@ public void CanParseCodesignOutput()
     [Theory]
     [MemberData(nameof(GetTestFilePaths), nameof(EmbeddedSignatureBlobMatchesCodesignInfo))]
     [PlatformSpecific(TestPlatforms.OSX)]
-    public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifact testArtifact)
+    public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifact _)
     {
         if(!SigningTests.IsSigned(filePath))
         {
@@ -167,7 +167,7 @@ public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifa
             if (exitcode != 0)
             {
                 output.WriteLine($"Codesign command failed with exit code {exitcode}: {stderr}");
-                Assert.True(false, "Codesign command failed");
+                Assert.Fail("Codesign command failed");
             }
             output.WriteLine($"Codesign output for {filePath}:\n{stderr}");
             CodesignOutputInfo codesignInfo = CodesignOutputInfo.ParseFromCodeSignOutput(stderr);
@@ -186,7 +186,6 @@ static void AssertEqual(CodesignOutputInfo csi, EmbeddedSignatureBlob b)
         Assert.True(csi.ExecutableSegmentBase == b.CodeDirectoryBlob.ExecutableSegmentBase, "ExecutableSegmentBase do not match");
         Assert.True(csi.ExecutableSegmentLimit == b.CodeDirectoryBlob.ExecutableSegmentLimit, "ExecutableSegmentLimit do not match");
         Assert.True(csi.ExecutableSegmentFlags == b.CodeDirectoryBlob.ExecutableSegmentFlags, "ExecutableSegmentFlags do not match");
-        Assert.True(csi.RequirementsSize == b.RequirementsBlob?.Size, "RequirementsSize do not match");
 
         Assert.True(csi.SpecialSlotHashes.Zip(b.CodeDirectoryBlob.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
         Assert.True(csi.CodeHashes.Zip(b.CodeDirectoryBlob.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
@@ -203,7 +202,6 @@ internal sealed record CodesignOutputInfo
         public ulong ExecutableSegmentBase { get; init; }
         public ulong ExecutableSegmentLimit { get; init; }
         public ExecutableSegmentFlags ExecutableSegmentFlags { get; init; }
-        public uint RequirementsSize { get; init; }
         public byte[][] SpecialSlotHashes { get; init; }
         public byte[][] CodeHashes { get; init; }
 
@@ -218,15 +216,28 @@ public bool Equals(CodesignOutputInfo? obj)
                 ExecutableSegmentBase == other.ExecutableSegmentBase &&
                 ExecutableSegmentLimit == other.ExecutableSegmentLimit &&
                 ExecutableSegmentFlags == other.ExecutableSegmentFlags &&
-                RequirementsSize == other.RequirementsSize &&
                 SpecialSlotHashes.Length == other.SpecialSlotHashes.Length &&
                 CodeHashes.Length == other.CodeHashes.Length &&
                 SpecialSlotHashes.Zip(other.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x) &&
                 CodeHashes.Zip(other.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x);
         }
-        public override int GetHashCode()
+
+        public override string ToString()
+            {
+                return $$"""
+                Identifier: {{Identifier}},
+                CodeDirectoryFlags: {{CodeDirectoryFlags}},
+                CodeDirectoryVersion: {{CodeDirectoryVersion}},
+                ExecutableSegmentBase: {{ExecutableSegmentBase}},
+                ExecutableSegmentLimit: {{ExecutableSegmentLimit}},
+                ExecutableSegmentFlags: {{ExecutableSegmentFlags}},
+                SpecialSlotHashes: [{{string.Join(", ", SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}],
+                CodeHashes: [{{string.Join(", ", CodeHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}]
+                """;
+            }
+                public override int GetHashCode()
         {
-            return HashCode.Combine(Identifier, CodeDirectoryFlags, CodeDirectoryVersion, ExecutableSegmentLimit, ExecutableSegmentFlags, RequirementsSize, SpecialSlotHashes, CodeHashes);
+            return HashCode.Combine(Identifier, CodeDirectoryFlags, CodeDirectoryVersion, ExecutableSegmentLimit, ExecutableSegmentFlags, SpecialSlotHashes, CodeHashes);
         }
 
         /// <summary>
@@ -234,20 +245,20 @@ public override int GetHashCode()
         /// </summary>
         public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
         {
-            var lines = output.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
-            var Identifier = lines[1].Split('=')[1].Trim();
+            var splitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
+            var lines = output.Split(new[] { '\n', '\r' }, splitOptions);
+            var Identifier = lines[1].Split('=', splitOptions)[1];
             var cdInfo = lines[3].Split(' ');
-            var CodeDirectoryVersion = (CodeDirectoryVersion)Convert.ToUInt32(cdInfo[1].Split('=')[1].Trim(), 16);
-            var CodeDirectoryFlags = (CodeDirectoryFlags)Convert.ToUInt32(cdInfo[3].Split('=', '(')[1].Trim().TrimStart("0x").ToString(), 16);
+            var CodeDirectoryVersion = (CodeDirectoryVersion)Convert.ToUInt32(cdInfo[1].Split('=', splitOptions)[1], 16);
+            var CodeDirectoryFlags = (CodeDirectoryFlags)Convert.ToUInt32(cdInfo[3].Split(['=', '('], splitOptions)[1].TrimStart("0x").ToString(), 16);
             Assert.True(lines[13].StartsWith("Executable Segment base="), "Expected 'Executable Segment base=' at line 13");
             Assert.True(lines[14].StartsWith("Executable Segment limit="), "Expected 'Executable Segment limit=' at line 14");
             Assert.True(lines[15].StartsWith("Executable Segment flags="), "Expected 'Executable Segment flags=' at line 15");
-            var ExecutableSegmentBase = ulong.Parse(lines[13].Split('=')[1].Trim());
-            var ExecutableSegmentLimit = ulong.Parse(lines[14].Split('=')[1].Trim());
-            var ExecutableSegmentFlags = (ExecutableSegmentFlags)Convert.ToUInt64(lines[15].Split('=')[1].Trim().TrimStart("0x").ToString(), 16);
+            var ExecutableSegmentBase = ulong.Parse(lines[13].Split('=', splitOptions)[1]);
+            var ExecutableSegmentLimit = ulong.Parse(lines[14].Split('=', splitOptions)[1]);
+            var ExecutableSegmentFlags = (ExecutableSegmentFlags)Convert.ToUInt64(lines[15].Split('=', splitOptions)[1].TrimStart("0x").ToString(), 16);
             Assert.True(lines[16].StartsWith("Page size=4096"), "Expected 'Page size=4096' at line 16");
             var (SpecialSlotHashes, CodeHashes) = ExtractHashes(lines.Skip(17));
-            var RequirementsSize = uint.Parse(lines[^1].Split('=')[2].Trim());
 
             return new CodesignOutputInfo
             {
@@ -259,7 +270,6 @@ public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
                 ExecutableSegmentFlags = ExecutableSegmentFlags,
                 SpecialSlotHashes = SpecialSlotHashes,
                 CodeHashes = CodeHashes,
-                RequirementsSize = RequirementsSize
             };
 
             static (byte[][] SpecialSlotHashes, byte[][] CodeHashes) ExtractHashes(IEnumerable<string> lines)
@@ -268,7 +278,7 @@ public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
                 List<byte[]> codeHashes = [];
                 foreach (var line in lines)
                 {
-                    if (line[0] is not ' ' or '\t')
+                    if (line[0] is not ('-' or '0' or '1' or '2' or '3' or '4' or '5' or '6' or '7' or '8' or '9'))
                         break;
                     var hash = line.Split('=')[1].Trim();
                     var index = int.Parse(line.Split('=')[0].Trim());
@@ -276,7 +286,6 @@ public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
                     {
                         // specialSlot
                         specialSlotHashes.Add(ParseByteArray(hash));
-
                     }
                     else
                     {
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
index cc0f4e612d9928..f3ac763b8177aa 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
@@ -82,8 +82,7 @@ void MatchesCodesignOutput(string filePath, TestArtifact _)
             // Codesigned file
             File.Copy(filePath, codesignFilePath);
             Assert.True(Codesign.IsAvailable, "Could not find codesign tool");
-            Codesign.Run("--remove-signature", codesignFilePath).ExitCode.Should().Be(0, $"'codesign --remove-signature {codesignFilePath}' failed!");
-            Codesign.Run("-s - -i " + fileName, codesignFilePath).ExitCode.Should().Be(0, $"'codesign -s - {codesignFilePath}' failed!");
+            Codesign.Run("-s - -f --preserve-metadata=entitlements -i" + fileName, codesignFilePath).ExitCode.Should().Be(0, $"'codesign -s - {codesignFilePath}' failed!");
 
             // Managed signed file
             AdHocSignFile(originalFilePath, managedSignedPath, fileName);
@@ -152,7 +151,7 @@ static void AssertMachFilesAreEquivalent(string codesignedPath, string managedSi
         /// <summary>
         /// AdHoc sign a test file. This should look similar to HostWriter.CreateAppHost.
         /// </summary>
-        public static void AdHocSignFile(string originalFilePath, string managedSignedPath, string fileName)
+        internal static void AdHocSignFile(string originalFilePath, string managedSignedPath, string fileName)
         {
             Assert.NotEqual(originalFilePath, managedSignedPath);
             // Open the source host file.
@@ -176,7 +175,7 @@ public static void AdHocSignFile(string originalFilePath, string managedSignedPa
             }
         }
 
-        public static void AdHocSignFileInPlace(string managedSignedPath)
+        internal static void AdHocSignFileInPlace(string managedSignedPath)
         {
             var tmpFile = Path.GetTempFileName();
             var mode = File.GetUnixFileMode(managedSignedPath);

From 9d3c8c231c031043ee5c13c64c8b117c4aa83889 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Tue, 17 Jun 2025 14:20:13 -0700
Subject: [PATCH 07/16] Make method public again

---
 .../MachObjectSigning/SigningTests.cs                         | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
index f3ac763b8177aa..a54fd3056d4e01 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
@@ -175,7 +175,9 @@ internal static void AdHocSignFile(string originalFilePath, string managedSigned
             }
         }
 
-        internal static void AdHocSignFileInPlace(string managedSignedPath)
+#pragma warning disable xUnit1013 // Public method should be marked as test
+        public static void AdHocSignFileInPlace(string managedSignedPath)
+#pragma warning restore xUnit1013 // Public method should be marked as test
         {
             var tmpFile = Path.GetTempFileName();
             var mode = File.GetUnixFileMode(managedSignedPath);

From 89a550db01b0ea19a36532eeb7a3e910b15ec4fd Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Wed, 18 Jun 2025 11:55:14 -0700
Subject: [PATCH 08/16] Use FileStream for ResourceUpdater until we can
 precalculate the size required.

---
 .../AppHost/HostWriter.cs                     | 15 ++---
 .../ResourceUpdater.cs                        | 55 +++++--------------
 2 files changed, 21 insertions(+), 49 deletions(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
index f0a64226fcb39a..0e6e399bbb5e66 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
@@ -164,17 +164,18 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
                                 }
                             }
                         }
-                        if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
-                        {
-                            using var updater = new ResourceUpdater(appHostDestinationMap, true);
-                            updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
-                            updater.Update();
-                        }
-                        using (var appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 1))
+                        using (var appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 1))
                         using (var appHostAccessor = appHostDestinationMap.CreateViewAccessor(0, appHostDestinationLength, MemoryMappedFileAccess.Read))
                         {
                             // Write the final content to the destination file.
                             BinaryUtils.WriteToStream(appHostAccessor, appHostDestinationStream, appHostDestinationLength);
+                            // This could be moved to work on the MemoryMappedFile if we can precalculate the size required.
+                            if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
+                            {
+                                using var updater = new ResourceUpdater(appHostDestinationStream, leaveOpen: true);
+                                updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
+                                updater.Update();
+                            }
                         }
                     }
                 });
diff --git a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
index 8476121dc62a23..e4791a6bb14e99 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/ResourceUpdater.cs
@@ -1,8 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-#nullable enable
-
 using System;
 using System.IO;
 using System.IO.MemoryMappedFiles;
@@ -17,11 +15,9 @@ namespace Microsoft.NET.HostModel
     /// </summary>
     public class ResourceUpdater : IDisposable
     {
-        private readonly FileStream? stream;
-        private readonly MemoryMappedFile? _memoryMappedFile;
-        private Stream Stream => stream as Stream ?? _memoryMappedFile!.CreateViewStream(0, 0, MemoryMappedFileAccess.ReadWrite);
+        private readonly FileStream stream;
         private readonly PEReader _reader;
-        private ResourceData? _resourceData;
+        private ResourceData _resourceData;
         private readonly bool leaveOpen;
 
         /// <summary>
@@ -59,23 +55,6 @@ public ResourceUpdater(FileStream stream, bool leaveOpen = false)
             }
         }
 
-        public ResourceUpdater(MemoryMappedFile memoryMappedFile, bool leaveOpen = false)
-        {
-            this._memoryMappedFile = memoryMappedFile;
-            this.leaveOpen = leaveOpen;
-            try
-            {
-                _reader = new PEReader(this.Stream, PEStreamOptions.LeaveOpen);
-                _resourceData = new ResourceData(_reader);
-            }
-            catch (Exception)
-            {
-                if (!leaveOpen)
-                    this._memoryMappedFile?.Dispose();
-                throw;
-            }
-        }
-
         /// <summary>
         /// Add all resources from a source PE file. It is assumed
         /// that the input is a valid PE file. If it is not, an
@@ -90,7 +69,7 @@ public ResourceUpdater AddResourcesFromPEImage(string peFile)
 
             using var module = new PEReader(File.OpenRead(peFile));
             var moduleResources = new ResourceData(module);
-            _resourceData!.CopyResourcesFrom(moduleResources);
+            _resourceData.CopyResourcesFrom(moduleResources);
             return this;
         }
 
@@ -116,7 +95,7 @@ public ResourceUpdater AddResource(byte[] data, IntPtr lpType, IntPtr lpName)
             if (_resourceData == null)
                 ThrowExceptionForInvalidUpdate();
 
-            _resourceData!.AddResource((ushort)lpName, (ushort)lpType, LangID_LangNeutral_SublangNeutral, data);
+            _resourceData.AddResource((ushort)lpName, (ushort)lpType, LangID_LangNeutral_SublangNeutral, data);
 
             return this;
         }
@@ -136,7 +115,7 @@ public ResourceUpdater AddResource(byte[] data, string lpType, IntPtr lpName)
             if (_resourceData == null)
                 ThrowExceptionForInvalidUpdate();
 
-            _resourceData!.AddResource((ushort)lpName, lpType, LangID_LangNeutral_SublangNeutral, data);
+            _resourceData.AddResource((ushort)lpName, lpType, LangID_LangNeutral_SublangNeutral, data);
 
             return this;
         }
@@ -194,7 +173,7 @@ public void Update()
             }
 
             var objectDataBuilder = new ObjectDataBuilder();
-            _resourceData!.WriteResources(rsrcVirtualAddress, ref objectDataBuilder);
+            _resourceData.WriteResources(rsrcVirtualAddress, ref objectDataBuilder);
             var rsrcSectionData = objectDataBuilder.ToData();
 
             int rsrcSectionDataSize = rsrcSectionData.Length;
@@ -206,24 +185,17 @@ public void Update()
 
             int trailingSectionVirtualStart = rsrcVirtualAddress + rsrcOriginalVirtualSize;
             int trailingSectionStart = rsrcPointerToRawData + rsrcOriginalRawDataSize;
-            int trailingSectionLength = (int)(Stream.Length - trailingSectionStart);
+            int trailingSectionLength = (int)(stream.Length - trailingSectionStart);
 
             bool needsMoveTrailingSections = !isRsrcIsLastSection && delta > 0;
             long finalImageSize = trailingSectionStart + Math.Max(delta, 0) + trailingSectionLength;
 
-            // Create a memory-mapped file if we weren't provided one when constructed
-            // and make sure we dispose it after use
-            using var mmap = stream is not null ?
-                MemoryMappedFile.CreateFromFile(stream!, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true)
-                : null;
-
-            // Don't dispose the memory-mapped file if it was provided in the constructor
-            var mmappedFile = mmap ?? _memoryMappedFile!;
-            using (MemoryMappedViewAccessor accessor = mmappedFile.CreateViewAccessor(0, finalImageSize, MemoryMappedFileAccess.ReadWrite))
+            using (var mmap = MemoryMappedFile.CreateFromFile(stream, null, finalImageSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
+            using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, finalImageSize, MemoryMappedFileAccess.ReadWrite))
             {
                 int peSignatureOffset = ReadI32(accessor, PEOffsets.DosStub.PESignatureOffset);
                 int sectionBase = peSignatureOffset + PEOffsets.PEHeaderSize +
-                                    (ushort)_reader!.PEHeaders.CoffHeader.SizeOfOptionalHeader;
+                                  (ushort)_reader.PEHeaders.CoffHeader.SizeOfOptionalHeader;
 
                 if (needsAddSection)
                 {
@@ -286,7 +258,7 @@ void PatchRVA(int offset)
                             pointer => pointer >= trailingSectionVirtualStart ? pointer + virtualDelta : pointer);
                     }
 
-                    int dataDirectoriesOffset = _reader.PEHeaders.PEHeader!.Magic == PEMagic.PE32Plus
+                    int dataDirectoriesOffset = _reader.PEHeaders.PEHeader.Magic == PEMagic.PE32Plus
                         ? peSignatureOffset + PEOffsets.PEHeader.PE64DataDirectories
                         : peSignatureOffset + PEOffsets.PEHeader.PE32DataDirectories;
 
@@ -301,7 +273,7 @@ void PatchRVA(int offset)
                         // fix RVA in DataDirectory
                         for (int i = 0; i < _reader.PEHeaders.PEHeader.NumberOfRvaAndSizes; i++)
                             PatchRVA(dataDirectoriesOffset + i * PEOffsets.DataDirectoryEntrySize +
-                                        PEOffsets.DataDirectoryEntry.VirtualAddressOffset);
+                                     PEOffsets.DataDirectoryEntry.VirtualAddressOffset);
                     }
 
                     // update the ResourceTable in DataDirectories
@@ -364,8 +336,7 @@ public void Dispose(bool disposing)
             if (disposing && !leaveOpen)
             {
                 _reader.Dispose();
-                stream?.Dispose();
-                _memoryMappedFile?.Dispose();
+                stream.Dispose();
             }
         }
     }

From 55ee29bc823c3ed0d733a1a3ed83786bf426152a Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Wed, 18 Jun 2025 17:14:03 -0700
Subject: [PATCH 09/16] Make codesign less verbose to avoid timeout

---
 .../MachObjectSigning/MachObjectTests.cs      | 48 ++++++++++---------
 1 file changed, 26 insertions(+), 22 deletions(-)

diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index f9ed77d368d0e2..76b95711405f32 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -153,29 +153,33 @@ public void CanParseCodesignOutput()
     [PlatformSpecific(TestPlatforms.OSX)]
     public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifact _)
     {
-        if(!SigningTests.IsSigned(filePath))
+        if (!SigningTests.IsSigned(filePath))
         {
             return;
         }
-        using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+        MachObjectFile machObjectFile;
+        EmbeddedSignatureBlob? embeddedSignatureBlob;
+        using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read))
+        using (var memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
         {
-            MachObjectFile machObjectFile = MachObjectFile.Create(new StreamBasedMachOFile(stream));
+            machObjectFile = MachObjectFile.Create(new MemoryMappedMachOViewAccessor(memoryMappedViewAccessor));
             Assert.True(machObjectFile.HasSignature, "Expected MachObjectFile to have a signature");
-            EmbeddedSignatureBlob? embeddedSignatureBlob = machObjectFile.EmbeddedSignatureBlob;
+            embeddedSignatureBlob = machObjectFile.EmbeddedSignatureBlob;
             Assert.NotNull(embeddedSignatureBlob);
-            var (exitcode, stderr) = Codesign.Run("--display --verbose=8", filePath);
-            if (exitcode != 0)
-            {
-                output.WriteLine($"Codesign command failed with exit code {exitcode}: {stderr}");
-                Assert.Fail("Codesign command failed");
-            }
-            output.WriteLine($"Codesign output for {filePath}:\n{stderr}");
-            CodesignOutputInfo codesignInfo = CodesignOutputInfo.ParseFromCodeSignOutput(stderr);
-            output.WriteLine($"Comparing {filePath} to codesign info: {codesignInfo}");
-            output.WriteLine($"specialSlotHashes: {string.Join(", ", codesignInfo.SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}");
-            output.WriteLine($"machObjectFile specialSlotHashes: {string.Join(", ", embeddedSignatureBlob.CodeDirectoryBlob.SpecialSlotHashes.Select(h => BitConverter.ToString(h.ToArray()).Replace("-", "")))}");
-            AssertEqual(codesignInfo, embeddedSignatureBlob);
         }
+
+        var (exitcode, stderr) = Codesign.Run("--display --verbose=4", filePath);
+        if (exitcode != 0)
+        {
+            output.WriteLine($"Codesign command failed with exit code {exitcode}: {stderr}");
+            Assert.Fail("Codesign command failed");
+        }
+        output.WriteLine($"Codesign output for {filePath}:\n{stderr}");
+        CodesignOutputInfo codesignInfo = CodesignOutputInfo.ParseFromCodeSignOutput(stderr);
+        output.WriteLine($"Comparing {filePath} to codesign info: {codesignInfo}");
+        output.WriteLine($"specialSlotHashes: {string.Join(", ", codesignInfo.SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}");
+        output.WriteLine($"machObjectFile specialSlotHashes: {string.Join(", ", embeddedSignatureBlob.CodeDirectoryBlob.SpecialSlotHashes.Select(h => BitConverter.ToString(h.ToArray()).Replace("-", "")))}");
+        AssertEqual(codesignInfo, embeddedSignatureBlob);
     }
 
     static void AssertEqual(CodesignOutputInfo csi, EmbeddedSignatureBlob b)
@@ -187,8 +191,8 @@ static void AssertEqual(CodesignOutputInfo csi, EmbeddedSignatureBlob b)
         Assert.True(csi.ExecutableSegmentLimit == b.CodeDirectoryBlob.ExecutableSegmentLimit, "ExecutableSegmentLimit do not match");
         Assert.True(csi.ExecutableSegmentFlags == b.CodeDirectoryBlob.ExecutableSegmentFlags, "ExecutableSegmentFlags do not match");
 
-        Assert.True(csi.SpecialSlotHashes.Zip(b.CodeDirectoryBlob.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
-        Assert.True(csi.CodeHashes.Zip(b.CodeDirectoryBlob.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
+        // Assert.True(csi.SpecialSlotHashes.Zip(b.CodeDirectoryBlob.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
+        // Assert.True(csi.CodeHashes.Zip(b.CodeDirectoryBlob.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
     }
 
     /// <summary>
@@ -223,8 +227,8 @@ public bool Equals(CodesignOutputInfo? obj)
         }
 
         public override string ToString()
-            {
-                return $$"""
+        {
+            return $$"""
                 Identifier: {{Identifier}},
                 CodeDirectoryFlags: {{CodeDirectoryFlags}},
                 CodeDirectoryVersion: {{CodeDirectoryVersion}},
@@ -234,8 +238,8 @@ public override string ToString()
                 SpecialSlotHashes: [{{string.Join(", ", SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}],
                 CodeHashes: [{{string.Join(", ", CodeHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}]
                 """;
-            }
-                public override int GetHashCode()
+        }
+        public override int GetHashCode()
         {
             return HashCode.Combine(Identifier, CodeDirectoryFlags, CodeDirectoryVersion, ExecutableSegmentLimit, ExecutableSegmentFlags, SpecialSlotHashes, CodeHashes);
         }

From 7e6d8348f3d153e001f1e5ea30ce4c9499f47b51 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Mon, 23 Jun 2025 10:32:58 -0700
Subject: [PATCH 10/16] Add IO exception retries on test file backups

---
 .../RegisteredInstallLocationOverride.cs      | 10 ++++--
 .../tests/TestUtils/TestFileBackup.cs         | 33 ++++++++++++-------
 2 files changed, 28 insertions(+), 15 deletions(-)

diff --git a/src/installer/tests/HostActivation.Tests/RegisteredInstallLocationOverride.cs b/src/installer/tests/HostActivation.Tests/RegisteredInstallLocationOverride.cs
index 4cd7a495e9401c..bb2fe9f7785bf1 100644
--- a/src/installer/tests/HostActivation.Tests/RegisteredInstallLocationOverride.cs
+++ b/src/installer/tests/HostActivation.Tests/RegisteredInstallLocationOverride.cs
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using Microsoft.DotNet.Cli.Build.Framework;
+using Microsoft.NET.HostModel;
 using Microsoft.Win32;
 using System;
 using System.Collections.Generic;
@@ -98,10 +99,13 @@ public void Dispose()
             }
             else
             {
-                if (File.Exists(PathValueOverride))
+                RetryUtil.RetryOnIOError(() =>
                 {
-                    File.Delete(PathValueOverride);
-                }
+                    if (File.Exists(PathValueOverride))
+                    {
+                        File.Delete(PathValueOverride);
+                    }
+                });
             }
 
             if (_testOnlyProductBehavior != null)
diff --git a/src/installer/tests/TestUtils/TestFileBackup.cs b/src/installer/tests/TestUtils/TestFileBackup.cs
index ffc7d62946ad80..c17f5d96f7f18f 100644
--- a/src/installer/tests/TestUtils/TestFileBackup.cs
+++ b/src/installer/tests/TestUtils/TestFileBackup.cs
@@ -68,22 +68,31 @@ public void Backup(string path)
 
         public void Dispose()
         {
-            if (Directory.Exists(_backupPath))
+            RetryOnIOError(() =>
             {
-                CopyOverDirectory(_backupPath, _basePath);
+                if (Directory.Exists(_backupPath))
+                {
+                    // Copying may fail if the file is still mapped from a process that is exiting
+                    CopyOverDirectory(_backupPath, _basePath);
+                }
+                return true;
+            }, $"Failed to restore files from the backup directory {_backupPath} even after retries");
 
-                // Directory.Delete sometimes fails with error that the directory is not empty.
-                // This is a known problem where the actual Delete call is not 100% synchronous
-                // the OS reports a success but the file/folder is not fully removed yet.
-                // So implement a simple retry with a short timeout.
-                RetryOnIOError(() =>
+            RetryOnIOError(() =>
+                {
+                    if (Directory.Exists(_backupPath))
                     {
+                        // Directory.Delete sometimes fails with error that the directory is not empty.
+                        // This is a known problem where the actual Delete call is not 100% synchronous
+                        // the OS reports a success but the file/folder is not fully removed yet.
+                        // So implement a simple retry with a short timeout.
                         Directory.Delete(_backupPath, recursive: true);
                         return !Directory.Exists(_backupPath);
-                    },
-                    $"Failed to delete the backup folder {_backupPath} even after retries."
-                );
-            }
+                    }
+                    return true;
+                },
+                $"Failed to delete the backup folder {_backupPath} even after retries."
+            );
         }
 
         private static void CopyOverDirectory(string source, string destination)
@@ -119,7 +128,7 @@ private static void RetryOnIOError(Func<bool> action, string errorMessage, int m
                         return;
                     }
                 }
-                catch (IOException e) 
+                catch (IOException e)
                 {
                     exception = e;
                 }

From c1d5d755c71409abb03f2cd652839555fc8399cf Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Tue, 24 Jun 2025 16:51:09 -0700
Subject: [PATCH 11/16] Apply suggestions from code review

- use explicit types for "using" statements
- Make CodeDirectoryHeader fields private and expose properties that convert them from bigendian
- Reenable codesign hash checks in tests
- improve readability
---
 .../AppHost/BinaryUtils.cs                    |  2 +-
 .../AppHost/HostWriter.cs                     | 24 ++---
 .../PlaceHolderNotFoundInAppHostException.cs  |  1 +
 .../Microsoft.NET.HostModel/Bundle/Bundler.cs | 28 +++---
 .../Bundle/FileEntry.cs                       |  8 +-
 .../Bundle/Manifest.cs                        | 15 ++-
 .../BinaryFormat/Blobs/CodeDirectoryBlob.cs   | 98 ++++++++++++-------
 .../Blobs/EmbeddedSignatureBlob.cs            | 12 ++-
 .../MachO/MachObjectFile.cs                   | 27 +++--
 .../MachObjectSigning/MachObjectTests.cs      | 25 ++++-
 src/installer/tests/TestUtils/Codesign.cs     |  8 +-
 11 files changed, 149 insertions(+), 99 deletions(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
index e32fe97a554df2..94e852ee34b71d 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs
@@ -74,7 +74,7 @@ public static unsafe void SearchAndReplace(
             }
         }
 
-        internal static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, byte[] searchPattern)
+        internal static unsafe int SearchInFile(MemoryMappedViewAccessor accessor, ReadOnlySpan<byte> searchPattern)
         {
             var safeBuffer = accessor.SafeMemoryMappedViewHandle;
             return KMPSearch(searchPattern, (byte*)safeBuffer.DangerousGetHandle(), (int)safeBuffer.ByteLength);
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
index 0e6e399bbb5e66..a0fa93553db58e 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
@@ -2,13 +2,11 @@
 // 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.IO.MemoryMappedFiles;
 using System.Runtime.InteropServices;
 using System.Text;
-using Microsoft.NET.HostModel.Bundle;
 using Microsoft.NET.HostModel.MachO;
 
 namespace Microsoft.NET.HostModel.AppHost
@@ -128,12 +126,14 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
 
                     long appHostSourceLength = new FileInfo(appHostSourceFilePath).Length;
                     string destinationFileName = Path.GetFileName(appHostDestinationFilePath);
+                    // Memory-mapped files cannot be resized, so calculate
+                    // the maximum length of the destination file upfront.
                     long appHostDestinationLength = enableMacOSCodeSign ?
                         appHostSourceLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostSourceLength, destinationFileName)
                         : appHostSourceLength;
-                    using (var appHostDestinationMap = MemoryMappedFile.CreateNew(null, appHostDestinationLength))
+                    using (MemoryMappedFile appHostDestinationMap = MemoryMappedFile.CreateNew(null, appHostDestinationLength))
                     {
-                        using (var appHostDestinationStream = appHostDestinationMap.CreateViewStream())
+                        using (MemoryMappedViewStream appHostDestinationStream = appHostDestinationMap.CreateViewStream())
                         using (FileStream appHostSourceStream = new(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
                         {
                             isMachOImage = MachObjectFile.IsMachOImage(appHostSourceStream);
@@ -144,10 +144,8 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
                             appHostSourceStream.CopyTo(appHostDestinationStream);
                         }
 
-                        using (var memoryMappedViewAccessor = appHostDestinationMap.CreateViewAccessor())
+                        using (MemoryMappedViewAccessor memoryMappedViewAccessor = appHostDestinationMap.CreateViewAccessor())
                         {
-                            // Get the size of the source app host to ensure that we don't write extra data to the destination.
-                            // On Windows, the size of the view accessor is rounded up to the next page boundary.
                             // Transform the host file in-memory.
                             RewriteAppHost(appHostDestinationMap, memoryMappedViewAccessor);
                             if (isMachOImage)
@@ -164,15 +162,17 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
                                 }
                             }
                         }
-                        using (var appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 1))
-                        using (var appHostAccessor = appHostDestinationMap.CreateViewAccessor(0, appHostDestinationLength, MemoryMappedFileAccess.Read))
+                        using (FileStream appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 1))
+                        using (MemoryMappedViewAccessor appHostAccessor = appHostDestinationMap.CreateViewAccessor(0, appHostDestinationLength, MemoryMappedFileAccess.Read))
                         {
-                            // Write the final content to the destination file.
+                            // Write the final content to the destination file, only up to the total length of the host, not the entire mapped file.
+                            // On Windows, memory-mapped files are rounded up to the next page size.
+                            // On MacOS, the memory-mapped file is created with a conservative estimate of the size of the signature.
                             BinaryUtils.WriteToStream(appHostAccessor, appHostDestinationStream, appHostDestinationLength);
-                            // This could be moved to work on the MemoryMappedFile if we can precalculate the size required.
+                            // TODO: This could be moved to work on the MemoryMappedFile if we can precalculate the size required.
                             if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
                             {
-                                using var updater = new ResourceUpdater(appHostDestinationStream, leaveOpen: true);
+                                using ResourceUpdater updater = new ResourceUpdater(appHostDestinationStream, leaveOpen: true);
                                 updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
                                 updater.Update();
                             }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs
index b2b21322faf5ac..7988aae7b5dcca 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/PlaceHolderNotFoundInAppHostException.cs
@@ -18,6 +18,7 @@ public PlaceHolderNotFoundInAppHostException(byte[] pattern)
         }
         public PlaceHolderNotFoundInAppHostException(ReadOnlySpan<byte> pattern)
         {
+            MissingPattern = pattern.ToArray();
         }
     }
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index 259c0a82882c1a..f455e828f719bd 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -50,8 +50,11 @@ public Bundler(string hostName,
                        string? appAssemblyName = null,
                        bool macosCodesign = true)
         {
+            if (!string.IsNullOrEmpty(Path.GetDirectoryName(hostName)))
+            {
+                throw new ArgumentException("Host name must be a file name, not a path", nameof(hostName));
+            }
             _tracer = new Trace(diagnosticOutput);
-
             _hostName = hostName;
             _outputDir = Path.GetFullPath(string.IsNullOrEmpty(outputDir) ? Environment.CurrentDirectory : outputDir);
             _target = new TargetInfo(targetOS, targetArch, targetFrameworkVersion);
@@ -242,6 +245,8 @@ private FileType InferType(FileSpec fileSpec)
             0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
         ];
 
+        public static ReadOnlySpan<byte> BundleHeaderSignature => BundleHeaderPlaceholder.AsSpan().Slice(8);
+
         /// <summary>
         /// Generate a bundle, given the specification of embedded files
         /// </summary>
@@ -279,7 +284,7 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
                 throw new ArgumentException("Invalid input specification: Must specify the host binary");
             }
 
-            var relativePathToSpec = GetFilteredFileSpecs(fileSpecs);
+            (FileSpec Spec, FileType Type)[] relativePathToSpec = GetFilteredFileSpecs(fileSpecs);
             long bundledFilesSize = 0;
             // Conservatively estimate the size of bundled files.
             // Assume no compression and worst case alignment for assemblies.
@@ -309,12 +314,11 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
             {
                 Directory.CreateDirectory(destinationDirectory);
             }
-            var bundleName = Path.GetFileName(bundlePath);
             var hostLength = new FileInfo(hostSource).Length;
-            var bundleManifestLength = BundleManifest.GetManifestLength(BundleManifest.BundleMajorVersion, relativePathToSpec.Select(x => x.Spec.BundleRelativePath));
+            var bundleManifestLength = Manifest.GetManifestLength(BundleManifest.BundleMajorVersion, relativePathToSpec.Select(x => x.Spec.BundleRelativePath));
             long bundleTotalSize = hostLength + bundledFilesSize + bundleManifestLength;
             if (_target.IsOSX && _macosCodesign)
-                bundleTotalSize += MachObjectFile.GetSignatureSizeEstimate((uint)bundleTotalSize, bundleName);
+                bundleTotalSize += MachObjectFile.GetSignatureSizeEstimate((uint)bundleTotalSize, _hostName);
 
             using (MemoryMappedFile bundleMap = MemoryMappedFile.CreateNew(null, bundleTotalSize, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None, HandleInheritability.None))
             {
@@ -382,7 +386,7 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
                         }
                         if (_macosCodesign)
                         {
-                            endOfBundle = (ulong)machFile.AdHocSignFile(machFileReader!, bundleName, signatureBlob);
+                            endOfBundle = (ulong)machFile.AdHocSignFile(machFileReader!, _hostName, signatureBlob);
                         }
                     }
 
@@ -410,14 +414,6 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
         /// <returns>True if the AppHost is a single-file bundle, false otherwise</returns>
         public static bool IsBundle(string appHostFilePath, out long bundleHeaderOffset)
         {
-            byte[] bundleSignature = {
-                // 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
-            };
-
             long headerOffset = 0;
             void FindBundleHeader()
             {
@@ -425,10 +421,10 @@ void FindBundleHeader()
                 {
                     using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
                     {
-                        int position = BinaryUtils.SearchInFile(accessor, bundleSignature);
+                        int position = BinaryUtils.SearchInFile(accessor, BundleHeaderSignature);
                         if (position == -1)
                         {
-                            throw new PlaceHolderNotFoundInAppHostException(bundleSignature);
+                            throw new PlaceHolderNotFoundInAppHostException(BundleHeaderSignature);
                         }
 
                         headerOffset = accessor.ReadInt64(position - sizeof(long));
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs
index 8f1b9042b320dc..abed8b4500e5a8 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs
@@ -62,10 +62,10 @@ public void Write(BinaryWriter writer)
         /// </summary>
         public static uint GetFileEntryLength(uint bundleMajorVersion, string bundleRelativePath)
         {
-           return 8u // Offset
-                    + 8u // Size
-                    + (bundleMajorVersion >= 6 ? 8u : 0u) // CompressedSize
-                    + 1u // Type (FileType)
+           return sizeof(long) // Offset
+                    + sizeof(long) // Size
+                    + (bundleMajorVersion >= 6 ? sizeof(long) : 0u) // CompressedSize
+                    + sizeof(FileType) // Type (FileType)
                     + Bundler.GetBinaryWriterStringLength(bundleRelativePath);
         }
 
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
index 4ecad05b4d8b10..4228b498ec2dd8 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers.Text;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
@@ -68,7 +69,7 @@ private enum HeaderFlags : ulong
         // with path-names so that the AppHost can use it in
         // extraction path.
         public string BundleID { get; private set; }
-        private const int BundleIdLength = 32;
+        private const int BundleIdLength = 12;
         private SHA256 bundleHash = SHA256.Create();
         public readonly uint BundleMajorVersion;
         // The Minor version is currently unused, and is always zero
@@ -133,7 +134,11 @@ private string GenerateDeterministicId()
             byte[] manifestHash = bundleHash.Hash;
             bundleHash.Dispose();
             bundleHash = null;
+#if NET
+            string id = Base64Url.EncodeToString(manifestHash).Substring(0, BundleIdLength);
+#else
             string id = Convert.ToBase64String(manifestHash).Substring(0, BundleIdLength).Replace('/', '_');
+#endif
             Debug.Assert(id.Length == BundleIdLength);
             return id;
         }
@@ -175,15 +180,15 @@ public long Write(BinaryWriter writer)
         /// <summary>
         /// Calculates the length of the manifest in bytes.
         /// </summary>
-        public long GetManifestLength(uint bundleMajorVersion, IEnumerable<string> fileSpecs)
+        public static long GetManifestLength(uint bundleMajorVersion, IEnumerable<string> fileSpecs)
         {
+            const string dummyBundleId = "FakeBundleID";
+            Debug.Assert(dummyBundleId.Length == BundleIdLength);
             // Size of the header
             long size = sizeof(uint) * 2 + // BundleMajorVersion + BundleMinorVersion
                         sizeof(int) + // NumEmbeddedFiles
                         (bundleMajorVersion >= 2 ? (sizeof(long) * 4 + sizeof(ulong)) : 0); // DepsJson and RuntimeConfigJson offsets and sizes, and Flags
-#pragma warning disable CA1850 // Prefer static 'System.Security.Cryptography.SHA256.HashData' method over 'ComputeHash'
-            size += Bundler.GetBinaryWriterStringLength(Convert.ToBase64String(SHA256.Create().ComputeHash([])).Substring(BundleIdLength).Replace('/', '_'));
-#pragma warning restore CA1850
+            size += Bundler.GetBinaryWriterStringLength(dummyBundleId);
             // Size of each FileEntry
             foreach (var fileSpec in fileSpecs)
             {
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index da4504664efbca..ad5917adaa7c0e 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -4,7 +4,6 @@
 #nullable enable
 
 using System;
-using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
@@ -32,14 +31,14 @@ public CodeDirectoryBlob(SimpleBlob blob)
         var data = blob.Data;
         var cdHeader = MemoryMarshal.Read<CodeDirectoryHeader>(data);
 
-        int identifierDataOffset = GetDataOffset(cdHeader._identifierOffset.ConvertFromBigEndian());
+        int identifierDataOffset = GetDataOffset(cdHeader.IdentifierOffset);
         int nullTerminatorIndex = data.AsSpan().Slice(identifierDataOffset).IndexOf((byte)0x00);
         string identifier = Encoding.UTF8.GetString(data, identifierDataOffset, nullTerminatorIndex);
 
-        var specialSlotCount = cdHeader._specialSlotCount.ConvertFromBigEndian();
-        var codeSlotCount = cdHeader._codeSlotCount.ConvertFromBigEndian();
+        var specialSlotCount = cdHeader.SpecialSlotCount;
+        var codeSlotCount = cdHeader.CodeSlotCount;
         var hashSize = cdHeader.HashSize;
-        var hashesDataOffset = GetDataOffset(cdHeader._hashesOffset.ConvertFromBigEndian());
+        var hashesDataOffset = GetDataOffset(cdHeader.HashesOffset);
 
         var specialSlotHashes = new byte[specialSlotCount][];
         var codeHashes = new byte[codeSlotCount][];
@@ -100,18 +99,21 @@ private CodeDirectoryBlob(
         + CodeSlotCount * HashSize;
 
     public string Identifier => _identifier;
-    public CodeDirectoryFlags Flags => (CodeDirectoryFlags)((uint)_cdHeader._flags).ConvertFromBigEndian();
-    public CodeDirectoryVersion Version => (CodeDirectoryVersion)((uint)_cdHeader._version).ConvertFromBigEndian();
+    public CodeDirectoryFlags Flags => _cdHeader.Flags;
+    public CodeDirectoryVersion Version => _cdHeader.Version;
     public IReadOnlyList<IReadOnlyList<byte>> SpecialSlotHashes => _specialSlotHashes;
-    public IReadOnlyList<IReadOnlyList<byte>> CodeHashes => _codeHashes;
-    public ulong ExecutableSegmentBase => _cdHeader._execSegmentBase.ConvertFromBigEndian();
-    public ulong ExecutableSegmentLimit => _cdHeader._execSegmentLimit.ConvertFromBigEndian();
-    public ExecutableSegmentFlags ExecutableSegmentFlags => (ExecutableSegmentFlags)((ulong)_cdHeader._execSegmentFlags).ConvertFromBigEndian();
 
-    private uint SpecialSlotCount => _cdHeader._specialSlotCount.ConvertFromBigEndian();
-    private uint CodeSlotCount => _cdHeader._codeSlotCount.ConvertFromBigEndian();
+    // Fields for test assertions only
+    internal IReadOnlyList<IReadOnlyList<byte>> CodeHashes => _codeHashes;
+    internal ulong ExecutableSegmentBase => _cdHeader.ExecSegmentBase;
+    internal ulong ExecutableSegmentLimit => _cdHeader.ExecSegmentLimit;
+    internal ExecutableSegmentFlags ExecutableSegmentFlags => _cdHeader.ExecSegmentFlags;
+
+    private uint SpecialSlotCount => _cdHeader.SpecialSlotCount;
+    private uint CodeSlotCount => _cdHeader.CodeSlotCount;
     private byte HashSize => _cdHeader.HashSize;
-    private uint HashesOffset => _cdHeader._hashesOffset.ConvertFromBigEndian();
+    private uint HashesOffset => _cdHeader.HashesOffset;
+
 
     public static CodeDirectoryBlob Create(
         IMachOFileReader accessor,
@@ -124,9 +126,11 @@ public static CodeDirectoryBlob Create(
         uint pageSize = MachObjectFile.DefaultPageSize)
     {
         uint codeSlotCount = GetCodeSlotCount((uint)signatureStart, pageSize);
-        uint specialCodeSlotCount = (uint)(derEntitlementsBlob != null ? CodeDirectorySpecialSlot.DerEntitlements :
-            entitlementsBlob != null ? CodeDirectorySpecialSlot.Entitlements :
-            CodeDirectorySpecialSlot.Requirements);
+        uint specialCodeSlotCount = (uint)(derEntitlementsBlob != null
+            ? CodeDirectorySpecialSlot.DerEntitlements
+            : entitlementsBlob != null
+                ? CodeDirectorySpecialSlot.Entitlements
+                : CodeDirectorySpecialSlot.Requirements);
 
         var specialSlotHashes = new byte[specialCodeSlotCount][];
         var codeHashes = new byte[codeSlotCount][];
@@ -197,29 +201,44 @@ public static CodeDirectoryBlob Create(
     [StructLayout(LayoutKind.Sequential)]
     internal struct CodeDirectoryHeader
     {
-        public CodeDirectoryVersion _version;
-        public CodeDirectoryFlags _flags;
-        public uint _hashesOffset;
-        public uint _identifierOffset;
-        public uint _specialSlotCount;
-        public uint _codeSlotCount;
-        public uint _executableLength;
+        private CodeDirectoryVersion _version;
+        private CodeDirectoryFlags _flags;
+        private uint _hashesOffset;
+        private uint _identifierOffset;
+        private uint _specialSlotCount;
+        private uint _codeSlotCount;
+        private uint _executableLength;
         public byte HashSize;
         public HashType HashType;
         public byte Platform;
         public byte Log2PageSize;
 #pragma warning disable CA1805 // Do not initialize unnecessarily
-        public readonly uint _reserved = 0;
-        public readonly uint _scatterOffset = 0;
-        public readonly uint _teamIdOffset = 0;
-        public readonly uint _reserved2 = 0;
+        private readonly uint _reserved = 0;
+        private readonly uint _scatterOffset = 0;
+        private readonly uint _teamIdOffset = 0;
+        private readonly uint _reserved2 = 0;
 #pragma warning restore CA1805 // Do not initialize unnecessarily
-        public ulong _codeLimit64;
-        public ulong _execSegmentBase;
-        public ulong _execSegmentLimit;
-        public ExecutableSegmentFlags _execSegmentFlags;
+        private ulong _codeLimit64;
+        private ulong _execSegmentBase;
+        private ulong _execSegmentLimit;
+        private ExecutableSegmentFlags _execSegmentFlags;
 
         public static readonly uint Size = GetSize();
+
+        public CodeDirectoryVersion Version => (CodeDirectoryVersion)((uint)_version).ConvertFromBigEndian();
+        public CodeDirectoryFlags Flags => (CodeDirectoryFlags)((uint)_flags).ConvertFromBigEndian();
+        public uint HashesOffset => _hashesOffset.ConvertFromBigEndian();
+        public uint IdentifierOffset => _identifierOffset.ConvertFromBigEndian();
+        public uint SpecialSlotCount => _specialSlotCount.ConvertFromBigEndian();
+        public uint CodeSlotCount => _codeSlotCount.ConvertFromBigEndian();
+        public ulong ExecSegmentBase => _execSegmentBase.ConvertFromBigEndian();
+        public ulong ExecSegmentLimit
+        {
+            get => _execSegmentLimit.ConvertFromBigEndian();
+            private set => _execSegmentLimit = value < uint.MaxValue ? 0 : value.ConvertToBigEndian();
+        }
+        public ExecutableSegmentFlags ExecSegmentFlags => (ExecutableSegmentFlags)((ulong)_execSegmentFlags).ConvertFromBigEndian();
+
         private static unsafe uint GetSize() => (uint)sizeof(CodeDirectoryHeader);
 
         public CodeDirectoryHeader(string identifier, uint codeSlotCount, uint specialCodeSlotCount, uint executableLength, byte hashSize, HashType hashType, ulong signatureStart, ulong execSegmentBase, ulong execSegmentLimit, ExecutableSegmentFlags execSegmentFlags)
@@ -240,6 +259,14 @@ public CodeDirectoryHeader(string identifier, uint codeSlotCount, uint specialCo
             _execSegmentLimit = execSegmentLimit.ConvertToBigEndian();
             _execSegmentFlags = (ExecutableSegmentFlags)((ulong)execSegmentFlags).ConvertToBigEndian();
         }
+
+        public static bool AreEqual(CodeDirectoryHeader first, CodeDirectoryHeader second)
+        {
+            // Ignore the exec segment limit for equality checks, as it may differ between codesign and the managed implementation.
+            first.ExecSegmentLimit = 0;
+            second.ExecSegmentLimit = 0;
+            return first.Equals(second);
+        }
     }
 
     public override bool Equals(object? obj)
@@ -252,10 +279,7 @@ public override bool Equals(object? obj)
 
         CodeDirectoryHeader thisHeader = _cdHeader;
         CodeDirectoryHeader otherHeader = other._cdHeader;
-        // Ignore the exec segment limit for equality checks, as it may differ
-        thisHeader._execSegmentLimit = 0;
-        otherHeader._execSegmentLimit = 0;
-        if (!thisHeader.Equals(otherHeader))
+        if (!CodeDirectoryHeader.AreEqual(thisHeader, otherHeader))
         {
             return false;
         }
@@ -299,7 +323,7 @@ public int Write(IMachOFileWriter accessor, long offset)
         accessor.WriteUInt32BigEndian(offset + sizeof(uint), Size);
         accessor.Write(offset + sizeof(uint) * 2, ref _cdHeader);
         var identifierBytes = Encoding.UTF8.GetBytes(_identifier);
-        Debug.Assert(sizeof(uint) * 2 + CodeDirectoryHeader.Size == _cdHeader._identifierOffset.ConvertFromBigEndian());
+        Debug.Assert(sizeof(uint) * 2 + CodeDirectoryHeader.Size == _cdHeader.IdentifierOffset);
         accessor.WriteExactly(offset + sizeof(uint) * 2 + CodeDirectoryHeader.Size, identifierBytes);
         accessor.WriteByte(offset + sizeof(uint) * 2 + CodeDirectoryHeader.Size + identifierBytes.Length, 0x00); // null terminator
         int specialSlotHashesOffset = (int)(offset + sizeof(uint) * 2 + CodeDirectoryHeader.Size + identifierBytes.Length + 1);
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs
index d90ec264a34bb8..0d9174d3a203c8 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/EmbeddedSignatureBlob.cs
@@ -159,8 +159,9 @@ public static unsafe long GetLargestSizeEstimate(uint fileSize, string identifie
     internal static unsafe long GetSignatureSize(uint fileSize, string identifier, EmbeddedSignatureBlob? existingSignature = null, byte? hashSize = null)
     {
         byte usedHashSize = hashSize ?? CodeDirectoryBlob.DefaultHashType.GetHashSize();
+        // CodeDirectory, Requirements, CMS Wrapper are always present
         uint specialCodeSlotCount = (uint)CodeDirectorySpecialSlot.Requirements;
-        uint embeddedSignatureSubBlobCount = 3; // CodeDirectory, Requirements, CMS Wrapper are always present
+        uint embeddedSignatureSubBlobCount = 3;
         uint entitlementsBlobSize = 0;
         uint derEntitlementsBlobSize = 0;
 
@@ -169,13 +170,16 @@ internal static unsafe long GetSignatureSize(uint fileSize, string identifier, E
             // We preserve Entitlements and DER Entitlements blobs if they exist in the old signature.
             // We need to update the relevant sizes and counts to reflect this.
             specialCodeSlotCount = Math.Max((uint)CodeDirectorySpecialSlot.Requirements, existingSignature.GetSpecialSlotHashCount());
-            entitlementsBlobSize = existingSignature.EntitlementsBlob?.Size ?? 0;
-            derEntitlementsBlobSize = existingSignature.DerEntitlementsBlob?.Size ?? 0;
-            // Requirements and CMSWrapper blobs are always overwritten as emtpy, but present.
             if (existingSignature.EntitlementsBlob is not null)
+            {
+                entitlementsBlobSize = existingSignature.EntitlementsBlob.Size;
                 embeddedSignatureSubBlobCount += 1;
+            }
             if (existingSignature.DerEntitlementsBlob is not null)
+            {
+                derEntitlementsBlobSize = existingSignature.DerEntitlementsBlob.Size;
                 embeddedSignatureSubBlobCount += 1;
+            }
         }
 
         // Calculate the size of the new signature
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
index 6918b3c510dd7d..ecb7e3d96165c1 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
@@ -230,26 +230,25 @@ public static bool IsMachOImage(string filePath)
     public bool RemoveCodeSignatureIfPresent(IMachOFileWriter file, out long? newLength)
     {
         newLength = null;
-        MachObjectFile machFile = this;
-        if (machFile._codeSignatureLoadCommand.Command.IsDefault)
+        if (_codeSignatureLoadCommand.Command.IsDefault)
         {
-            Debug.Assert(machFile._codeSignatureBlob is null);
+            Debug.Assert(_codeSignatureBlob is null);
             return false;
         }
 
         LinkEditLoadCommand clearedCommand = default;
         file.Write(_codeSignatureLoadCommand.FileOffset, ref clearedCommand);
-        machFile._header.NumberOfCommands -= 1;
-        machFile._header.SizeOfCommands -= (uint)sizeof(LinkEditLoadCommand);
-        machFile._linkEditSegment64.Command.SetFileSize(
-            machFile._linkEditSegment64.Command.GetFileSize(machFile._header)
-                - machFile._codeSignatureLoadCommand.Command.GetFileSize(machFile._header),
-            machFile._header);
-        newLength = machFile.GetFileSize();
-        machFile._codeSignatureLoadCommand = default;
-        machFile._codeSignatureBlob = null;
-        machFile.Validate();
-        machFile.Write(file);
+        _header.NumberOfCommands -= 1;
+        _header.SizeOfCommands -= (uint)sizeof(LinkEditLoadCommand);
+        _linkEditSegment64.Command.SetFileSize(
+            _linkEditSegment64.Command.GetFileSize(_header)
+                - _codeSignatureLoadCommand.Command.GetFileSize(_header),
+            _header);
+        newLength = GetFileSize();
+        _codeSignatureLoadCommand = default;
+        _codeSignatureBlob = null;
+        Validate();
+        Write(file);
         return true;
     }
 
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index 76b95711405f32..523b88b0371053 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -1,4 +1,5 @@
-
+// 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;
@@ -168,7 +169,8 @@ public void EmbeddedSignatureBlobMatchesCodesignInfo(string filePath, TestArtifa
             Assert.NotNull(embeddedSignatureBlob);
         }
 
-        var (exitcode, stderr) = Codesign.Run("--display --verbose=4", filePath);
+        var (exitcode, stderr) = Codesign.Run("--display --verbose=6", filePath);
+#pragma warning disable CS0162
         if (exitcode != 0)
         {
             output.WriteLine($"Codesign command failed with exit code {exitcode}: {stderr}");
@@ -191,8 +193,23 @@ static void AssertEqual(CodesignOutputInfo csi, EmbeddedSignatureBlob b)
         Assert.True(csi.ExecutableSegmentLimit == b.CodeDirectoryBlob.ExecutableSegmentLimit, "ExecutableSegmentLimit do not match");
         Assert.True(csi.ExecutableSegmentFlags == b.CodeDirectoryBlob.ExecutableSegmentFlags, "ExecutableSegmentFlags do not match");
 
-        // Assert.True(csi.SpecialSlotHashes.Zip(b.CodeDirectoryBlob.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
-        // Assert.True(csi.CodeHashes.Zip(b.CodeDirectoryBlob.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
+        AssertEqual(csi.SpecialSlotHashes, b.CodeDirectoryBlob.SpecialSlotHashes);
+        AssertEqual(csi.CodeHashes, b.CodeDirectoryBlob.CodeHashes);
+
+        static void AssertEqual(byte[][] hashes1, IReadOnlyList<IReadOnlyList<byte>> hashes2)
+        {
+            Assert.Equal(hashes1.Length, hashes2.Count);
+
+            for (int i = 0; i < hashes1.Length; i++)
+            {
+                Assert.Equal(hashes1[i].Length, hashes2[i].Count);
+
+                for(int j = 0; j < hashes1[i].Length; j++)
+                {
+                    Assert.Equal(hashes1[i][j], hashes2[i][j]);
+                }
+            }
+        }
     }
 
     /// <summary>
diff --git a/src/installer/tests/TestUtils/Codesign.cs b/src/installer/tests/TestUtils/Codesign.cs
index e31f8bc706ad76..15ad429b2f8b5b 100644
--- a/src/installer/tests/TestUtils/Codesign.cs
+++ b/src/installer/tests/TestUtils/Codesign.cs
@@ -4,6 +4,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.Runtime.InteropServices;
+using System.Threading.Tasks;
 
 namespace Microsoft.DotNet.CoreSetup
 {
@@ -30,8 +31,11 @@ public static (int ExitCode, string StdErr) Run(string args, string binaryPath)
             {
                 if (p == null)
                     return (-1, "Failed to start process");
-                p.WaitForExit();
-                return (p.ExitCode, p.StandardError.ReadToEnd());
+
+                var stderrRead = p.StandardError.ReadToEndAsync();
+                var processWait = p.WaitForExitAsync();
+                Task.WaitAll(stderrRead, processWait);
+                return (p.ExitCode, stderrRead.Result);
             }
         }
     }

From c668a0327ed2446a2ce4f069c0d9f817fa5685b6 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Tue, 24 Jun 2025 16:55:48 -0700
Subject: [PATCH 12/16] Field -> Property

---
 .../MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs               | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index ad5917adaa7c0e..fabe3859c2aa26 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -103,7 +103,7 @@ private CodeDirectoryBlob(
     public CodeDirectoryVersion Version => _cdHeader.Version;
     public IReadOnlyList<IReadOnlyList<byte>> SpecialSlotHashes => _specialSlotHashes;
 
-    // Fields for test assertions only
+    // Properties for test assertions only
     internal IReadOnlyList<IReadOnlyList<byte>> CodeHashes => _codeHashes;
     internal ulong ExecutableSegmentBase => _cdHeader.ExecSegmentBase;
     internal ulong ExecutableSegmentLimit => _cdHeader.ExecSegmentLimit;

From 50eb0298387b5b009f41ff44f0e59b115fadf000 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Mon, 30 Jun 2025 09:58:49 -0700
Subject: [PATCH 13/16] Use ReadOnlySpan for header placeholder data

---
 .../managed/Microsoft.NET.HostModel/Bundle/Bundler.cs       | 6 +++---
 .../MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs           | 1 -
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index f455e828f719bd..dfc1ea9bec6a7b 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -234,7 +234,7 @@ private FileType InferType(FileSpec fileSpec)
             return FileType.Unknown;
         }
 
-        public static ImmutableArray<byte> BundleHeaderPlaceholder = [
+        internal static ReadOnlySpan<byte> BundleHeaderPlaceholder => [
             // 8 bytes represent the bundle header-offset
             // Zero for non-bundle apphosts (default).
             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -245,7 +245,7 @@ private FileType InferType(FileSpec fileSpec)
             0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
         ];
 
-        public static ReadOnlySpan<byte> BundleHeaderSignature => BundleHeaderPlaceholder.AsSpan().Slice(8);
+        public static ReadOnlySpan<byte> BundleHeaderSignature => BundleHeaderPlaceholder.Slice(8);
 
         /// <summary>
         /// Generate a bundle, given the specification of embedded files
@@ -374,7 +374,7 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
                     ulong endOfBundle = (ulong)bundleStream.Position;
                     Debug.Assert((long)endOfBundle == endOfBundledFiles + bundleManifestLength, $"Bundle manifest is unexpected size. Expected {bundleManifestLength}, but got {(long)endOfBundle - endOfBundledFiles}");
                     BinaryUtils.SearchAndReplace(accessor,
-                                                BundleHeaderPlaceholder.AsSpan(),
+                                                BundleHeaderPlaceholder,
                                                 BitConverter.GetBytes(headerOffset),
                                                 pad0s: false);
                     if (_target.IsOSX && machFile is not null)
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
index fabe3859c2aa26..b56c60b02e70eb 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Blobs/CodeDirectoryBlob.cs
@@ -114,7 +114,6 @@ private CodeDirectoryBlob(
     private byte HashSize => _cdHeader.HashSize;
     private uint HashesOffset => _cdHeader.HashesOffset;
 
-
     public static CodeDirectoryBlob Create(
         IMachOFileReader accessor,
         long signatureStart,

From b7f05cd78da27007f3dc378696697c0268251a58 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Mon, 30 Jun 2025 09:59:38 -0700
Subject: [PATCH 14/16] Make property internal

---
 src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index dfc1ea9bec6a7b..0952331b076df1 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -245,7 +245,7 @@ private FileType InferType(FileSpec fileSpec)
             0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
         ];
 
-        public static ReadOnlySpan<byte> BundleHeaderSignature => BundleHeaderPlaceholder.Slice(8);
+        internal static ReadOnlySpan<byte> BundleHeaderSignature => BundleHeaderPlaceholder.Slice(8);
 
         /// <summary>
         /// Generate a bundle, given the specification of embedded files

From 2a33df4ce26149f8d228b51d418f4051d646f1c4 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Tue, 1 Jul 2025 11:12:41 -0700
Subject: [PATCH 15/16] PR Feedback: Rename ReBundle, move tests to
 HostModel.Tests, use helper for getting inode

---
 .../tests/AppHost.Bundle.Tests/AppLaunch.cs   |  31 ----
 .../MachOHostSigningTests.cs                  |  37 ----
 .../AppHost/CreateAppHost.cs                  |  20 +-
 .../MachObjectSigning/CodesignOutputInfo.cs   | 172 ++++++++++++++++++
 .../MachObjectSigning/MachObjectTests.cs      | 164 +----------------
 .../MachObjectSigning/SigningTests.cs         |  91 ++++++++-
 src/installer/tests/TestUtils/Inode.cs        |  28 +++
 .../tests/TestUtils/SingleFileTestApp.cs      |   2 +-
 8 files changed, 293 insertions(+), 252 deletions(-)
 create mode 100644 src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/CodesignOutputInfo.cs
 create mode 100644 src/installer/tests/TestUtils/Inode.cs

diff --git a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
index 46a84dd32bbf95..1dbd8a40f87532 100644
--- a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
+++ b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs
@@ -82,37 +82,6 @@ private void RunApp(bool selfContained)
             }
         }
 
-        [Fact]
-        [PlatformSpecific(TestPlatforms.OSX)]
-        private void OverwritingExistingBundleClearsMacOsSignatureCache()
-        {
-            // Bundle to a single-file and ensure it is signed
-            string singleFile = sharedTestState.SelfContainedApp.Bundle();
-            Assert.True(Codesign.Run("-v", singleFile).ExitCode == 0);
-            var firstls = Command.Create("/bin/ls", "-li", singleFile)
-                .CaptureStdErr()
-                .CaptureStdOut()
-                .Execute();
-            firstls.Should().Pass();
-            var firstInode = firstls.StdOut.Split(' ')[0];
-
-            // Rebundle to the same location.
-            // Bundler should create a new inode for the bundle which should clear the MacOS signature cache.
-            string oldFile = singleFile;
-            string dir = Path.GetDirectoryName(singleFile);
-            singleFile = sharedTestState.SelfContainedApp.ReBundle(dir, BundleOptions.BundleAllContent, out var _, new Version(5, 0));
-            Assert.True(singleFile == oldFile, "Rebundled app should have a different path than the original single-file app.");
-            var secondls = Command.Create("/bin/ls", "-li", singleFile)
-                .CaptureStdErr()
-                .CaptureStdOut()
-                .Execute();
-            secondls.Should().Pass();
-            var secondInode = secondls.StdOut.Split(' ')[0];
-            Assert.False(firstInode == secondInode, "not a different inode after rebundle");
-            // Ensure the MacOS signature cache is cleared
-            Assert.True(Codesign.Run("-v", singleFile).ExitCode == 0);
-        }
-
         [ConditionalTheory(typeof(Binaries.CetCompat), nameof(Binaries.CetCompat.IsSupported))]
         [InlineData(true)]
         [InlineData(false)]
diff --git a/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs b/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
index f894263d52678f..70bc3b86a51eff 100644
--- a/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
+++ b/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
@@ -33,42 +33,5 @@ public void SignedAppHostRuns()
                 .Execute();
             executedCommand.Should().ExitWith(Constants.ErrorCode.AppHostExeNotBoundFailure);
         }
-
-        [Fact]
-        [PlatformSpecific(TestPlatforms.OSX)]
-        public void SigningAppHostPreservesEntitlements()
-        {
-            using var testDirectory = TestArtifact.Create(nameof(SignedAppHostRuns));
-            var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.AppHost.FilePath));
-            File.Copy(Binaries.AppHost.FilePath, testAppHostPath);
-            long preRemovalSize = new FileInfo(testAppHostPath).Length;
-            string signedHostPath = testAppHostPath + ".signed";
-
-            HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
-
-            SigningTests.HasDerEntitlementsBlob(testAppHostPath).Should().BeTrue();
-            SigningTests.HasDerEntitlementsBlob(signedHostPath).Should().BeTrue();
-            SigningTests.HasEntitlementsBlob(testAppHostPath).Should().BeTrue();
-            SigningTests.HasEntitlementsBlob(signedHostPath).Should().BeTrue();
-        }
-
-        [Fact]
-        [PlatformSpecific(TestPlatforms.OSX)]
-        public void BundledAppHostHasEntitlements()
-        {
-            using var testDirectory = TestArtifact.Create(nameof(BundledAppHostHasEntitlements));
-            var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.SingleFileHost.FilePath));
-            File.Copy(Binaries.SingleFileHost.FilePath, testAppHostPath);
-            long preRemovalSize = new FileInfo(testAppHostPath).Length;
-            string signedHostPath = testAppHostPath + ".signed";
-
-            HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
-            var bundlePath = new Bundler(Path.GetFileName(signedHostPath), testAppHostPath + ".bundle").GenerateBundle([new(signedHostPath, Path.GetFileName(signedHostPath))]);
-
-            SigningTests.HasEntitlementsBlob(testAppHostPath).Should().BeTrue();
-            SigningTests.HasEntitlementsBlob(bundlePath).Should().BeTrue();
-            SigningTests.HasDerEntitlementsBlob(testAppHostPath).Should().BeTrue();
-            SigningTests.HasDerEntitlementsBlob(bundlePath).Should().BeTrue();
-        }
     }
 }
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 24958b3de76749..e128cd7b6cbec5 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
@@ -305,15 +305,10 @@ public void SigningExistingAppHostCreatesNewInode(string subdir)
                    appBinaryFilePath,
                    windowsGraphicalUserInterface: false,
                    enableMacOSCodeSign: true);
-                var firstls = Command.Create("/bin/ls", "-li", destinationFilePath)
-                    .CaptureStdErr()
-                    .CaptureStdOut()
-                    .Execute();
-                firstls.Should().Pass();
-                var firstInode = firstls.StdOut.Split(' ')[0];
+                var firstInode = Inode.GetInode(destinationFilePath);
 
                 // Validate that there is a signature present in the apphost Mach file
-                SigningTests.IsSigned(destinationFilePath).Should().BeTrue();
+                Assert.True(SigningTests.IsSigned(destinationFilePath));
 
                 HostWriter.CreateAppHost(
                    sourceAppHostMock,
@@ -321,17 +316,12 @@ public void SigningExistingAppHostCreatesNewInode(string subdir)
                    appBinaryFilePath,
                    windowsGraphicalUserInterface: false,
                    enableMacOSCodeSign: true);
+                var secondInode = Inode.GetInode(destinationFilePath);
 
-                var secondls = Command.Create("/bin/ls", "-li", destinationFilePath)
-                    .CaptureStdErr()
-                    .CaptureStdOut()
-                    .Execute();
-                secondls.Should().Pass();
-                var secondInode = secondls.StdOut.Split(' ')[0];
                 // Ensure the MacOS signature cache is cleared
-                Assert.False(firstInode == secondInode, "not a different inode after rebundle");
+                Assert.False(firstInode == secondInode, "not a different inode after re-bundling");
 
-                SigningTests.IsSigned(destinationFilePath).Should().BeTrue();
+                Assert.True(SigningTests.IsSigned(destinationFilePath));
             }
         }
 
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/CodesignOutputInfo.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/CodesignOutputInfo.cs
new file mode 100644
index 00000000000000..1179fab0d79039
--- /dev/null
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/CodesignOutputInfo.cs
@@ -0,0 +1,172 @@
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.NET.HostModel.MachO;
+using Xunit;
+
+namespace Microsoft.NET.HostModel.Tests;
+
+/// <summary>
+/// Info related to the code signature that can be extracted from the output of the `codesign` command.
+/// </summary>
+internal sealed record CodesignOutputInfo
+{
+    public string Identifier { get; init; }
+    public CodeDirectoryFlags CodeDirectoryFlags { get; init; }
+    public CodeDirectoryVersion CodeDirectoryVersion { get; init; }
+    public ulong ExecutableSegmentBase { get; init; }
+    public ulong ExecutableSegmentLimit { get; init; }
+    public ExecutableSegmentFlags ExecutableSegmentFlags { get; init; }
+    public byte[][] SpecialSlotHashes { get; init; }
+    public byte[][] CodeHashes { get; init; }
+
+    public bool Equals(CodesignOutputInfo? obj)
+    {
+        if (obj is not CodesignOutputInfo other)
+            return false;
+
+        return Identifier == other.Identifier &&
+            CodeDirectoryFlags == other.CodeDirectoryFlags &&
+            CodeDirectoryVersion == other.CodeDirectoryVersion &&
+            ExecutableSegmentBase == other.ExecutableSegmentBase &&
+            ExecutableSegmentLimit == other.ExecutableSegmentLimit &&
+            ExecutableSegmentFlags == other.ExecutableSegmentFlags &&
+            SpecialSlotHashes.Length == other.SpecialSlotHashes.Length &&
+            CodeHashes.Length == other.CodeHashes.Length &&
+            SpecialSlotHashes.Zip(other.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x) &&
+            CodeHashes.Zip(other.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x);
+    }
+
+    public override string ToString()
+    {
+        return $$"""
+                Identifier: {{Identifier}},
+                CodeDirectoryFlags: {{CodeDirectoryFlags}},
+                CodeDirectoryVersion: {{CodeDirectoryVersion}},
+                ExecutableSegmentBase: {{ExecutableSegmentBase}},
+                ExecutableSegmentLimit: {{ExecutableSegmentLimit}},
+                ExecutableSegmentFlags: {{ExecutableSegmentFlags}},
+                SpecialSlotHashes: [{{string.Join(", ", SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}],
+                CodeHashes: [{{string.Join(", ", CodeHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}]
+                """;
+    }
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(Identifier, CodeDirectoryFlags, CodeDirectoryVersion, ExecutableSegmentLimit, ExecutableSegmentFlags, SpecialSlotHashes, CodeHashes);
+    }
+
+    /// <summary>
+    /// Parses the output of the `codesign` command to extract codesign information.
+    /// </summary>
+    public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
+    {
+        var splitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
+        var lines = output.Split(new[] { '\n', '\r' }, splitOptions);
+        var Identifier = lines[1].Split('=', splitOptions)[1];
+        var cdInfo = lines[3].Split(' ');
+        var CodeDirectoryVersion = (CodeDirectoryVersion)Convert.ToUInt32(cdInfo[1].Split('=', splitOptions)[1], 16);
+        var CodeDirectoryFlags = (CodeDirectoryFlags)Convert.ToUInt32(cdInfo[3].Split(['=', '('], splitOptions)[1].TrimStart("0x").ToString(), 16);
+        Assert.True(lines[13].StartsWith("Executable Segment base="), "Expected 'Executable Segment base=' at line 13");
+        Assert.True(lines[14].StartsWith("Executable Segment limit="), "Expected 'Executable Segment limit=' at line 14");
+        Assert.True(lines[15].StartsWith("Executable Segment flags="), "Expected 'Executable Segment flags=' at line 15");
+        var ExecutableSegmentBase = ulong.Parse(lines[13].Split('=', splitOptions)[1]);
+        var ExecutableSegmentLimit = ulong.Parse(lines[14].Split('=', splitOptions)[1]);
+        var ExecutableSegmentFlags = (ExecutableSegmentFlags)Convert.ToUInt64(lines[15].Split('=', splitOptions)[1].TrimStart("0x").ToString(), 16);
+        Assert.True(lines[16].StartsWith("Page size=4096"), "Expected 'Page size=4096' at line 16");
+        var (SpecialSlotHashes, CodeHashes) = ExtractHashes(lines.Skip(17));
+
+        return new CodesignOutputInfo
+        {
+            Identifier = Identifier,
+            CodeDirectoryFlags = CodeDirectoryFlags,
+            CodeDirectoryVersion = CodeDirectoryVersion,
+            ExecutableSegmentBase = ExecutableSegmentBase,
+            ExecutableSegmentLimit = ExecutableSegmentLimit,
+            ExecutableSegmentFlags = ExecutableSegmentFlags,
+            SpecialSlotHashes = SpecialSlotHashes,
+            CodeHashes = CodeHashes,
+        };
+
+        static (byte[][] SpecialSlotHashes, byte[][] CodeHashes) ExtractHashes(IEnumerable<string> lines)
+        {
+            List<byte[]> specialSlotHashes = [];
+            List<byte[]> codeHashes = [];
+            foreach (var line in lines)
+            {
+                if (line[0] is not ('-' or '0' or '1' or '2' or '3' or '4' or '5' or '6' or '7' or '8' or '9'))
+                    break;
+                var hash = line.Split('=')[1].Trim();
+                var index = int.Parse(line.Split('=')[0].Trim());
+                if (index < 0)
+                {
+                    // specialSlot
+                    specialSlotHashes.Add(ParseByteArray(hash));
+                }
+                else
+                {
+                    // codeHashes
+                    codeHashes.Add(ParseByteArray(hash));
+                }
+            }
+            return (specialSlotHashes.ToArray(), codeHashes.ToArray());
+        }
+        static byte[] ParseByteArray(string hex)
+        {
+            if (hex.Length % 2 != 0)
+                throw new ArgumentException("Hex string must have an even length.");
+            byte[] bytes = new byte[hex.Length / 2];
+            for (int i = 0; i < hex.Length; i += 2)
+            {
+                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
+            }
+            return bytes;
+        }
+
+    }
+
+    public const string SampleCodesignOutput = """
+    Executable=/Users/jacksonschuster/source/runtime3/artifacts/bin/osx-x64.Debug/corehost/singlefilehost
+    Identifier=singlefilehost-5555494409d4df688bf436b291061028f736b11c
+    Format=Mach-O thin (x86_64)
+    CodeDirectory v=20400 size=89264 flags=0x2(adhoc) hashes=2778+7 location=embedded
+    VersionPlatform=1
+    VersionMin=786432
+    VersionSDK=984064
+    Hash type=sha256 size=32
+    CandidateCDHash sha256=6fee638e9fe544a66b0acf9489ebc59e073b3e39
+    CandidateCDHashFull sha256=6fee638e9fe544a66b0acf9489ebc59e073b3e39082903247c5413f555ec2857
+    Hash choices=sha256
+    CMSDigest=6fee638e9fe544a66b0acf9489ebc59e073b3e39082903247c5413f555ec2857
+    CMSDigestType=2
+    Executable Segment base=0
+    Executable Segment limit=8949760
+    Executable Segment flags=0x1
+    Page size=4096
+        -7=4d8d4b9e4116e8edd996176b5553463acb64287bb635e7f141155529e20457bc
+        -6=0000000000000000000000000000000000000000000000000000000000000000
+        -5=cca8afe72425463c13b813da9ae468ae3b5fe20fe5fe1d3f34302ba2f15722f2
+        -4=0000000000000000000000000000000000000000000000000000000000000000
+        -3=0000000000000000000000000000000000000000000000000000000000000000
+        -2=987920904eab650e75788c054aa0b0524e6a80bfc71aa32df8d237a61743f986
+        -1=0000000000000000000000000000000000000000000000000000000000000000
+        0=20042993665611bf5d01d35a46092c2d43a07883f31247a03b5600c301f5c039
+        1=a97fad07cc9d6eabad27a77e32b69c3da59372fa7987a13c2b8d23f378380476
+        2=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
+        3=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
+        4=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
+        5=b3d230340aa5ed09c788c39081c207a7430b83d22c9489d84d4ede3ed320f47b
+        6=825b7aa16170a9b739a4689ba8878391bcae87efd63e3b174738c382020031c1
+        7=e360159ee0adaeba5ac5f562c45ec551dbe8b73fbc858beca298610312df33b3
+        8=20585ef0bc0287c5b7a9b54f2669704cdc31cea7d7b1702b336fcf93a9f01ca2
+        9=414ae6563e5881b215a08bb33fc539fb0c90c3a5532f6e15a726ed6cdc255550
+        10=b672b667eb31b48d027bd5f1cf75bad5a8552b4d6b649cbdae35699152fb8a1b
+    CDHash=6fee638e9fe544a66b0acf9489ebc59e073b3e39
+    Signature=adhoc
+    Info.plist=not bound
+    TeamIdentifier=not set
+    Sealed Resources=none
+    Internal requirements count=0 size=12
+    """;
+}
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
index f9ed77d368d0e2..5211f92f9252ea 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/MachObjectTests.cs
@@ -110,7 +110,7 @@ public static Object[][] GetTestFilePaths(string testArtifactName)
     [Fact]
     public void CanParseCodesignOutput()
     {
-        var parsed = CodesignOutputInfo.ParseFromCodeSignOutput(SampleCodesignOutput);
+        var parsed = CodesignOutputInfo.ParseFromCodeSignOutput(CodesignOutputInfo.SampleCodesignOutput);
         Assert.NotNull(parsed);
         output.WriteLine(parsed.ToString());
         var expected = new CodesignOutputInfo
@@ -190,166 +190,4 @@ static void AssertEqual(CodesignOutputInfo csi, EmbeddedSignatureBlob b)
         Assert.True(csi.SpecialSlotHashes.Zip(b.CodeDirectoryBlob.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
         Assert.True(csi.CodeHashes.Zip(b.CodeDirectoryBlob.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x));
     }
-
-    /// <summary>
-    /// Info related to the code signature that can be extracted from the output of the `codesign` command.
-    /// </summary>
-    internal sealed record CodesignOutputInfo
-    {
-        public string Identifier { get; init; }
-        public CodeDirectoryFlags CodeDirectoryFlags { get; init; }
-        public CodeDirectoryVersion CodeDirectoryVersion { get; init; }
-        public ulong ExecutableSegmentBase { get; init; }
-        public ulong ExecutableSegmentLimit { get; init; }
-        public ExecutableSegmentFlags ExecutableSegmentFlags { get; init; }
-        public byte[][] SpecialSlotHashes { get; init; }
-        public byte[][] CodeHashes { get; init; }
-
-        public bool Equals(CodesignOutputInfo? obj)
-        {
-            if (obj is not CodesignOutputInfo other)
-                return false;
-
-            return Identifier == other.Identifier &&
-                CodeDirectoryFlags == other.CodeDirectoryFlags &&
-                CodeDirectoryVersion == other.CodeDirectoryVersion &&
-                ExecutableSegmentBase == other.ExecutableSegmentBase &&
-                ExecutableSegmentLimit == other.ExecutableSegmentLimit &&
-                ExecutableSegmentFlags == other.ExecutableSegmentFlags &&
-                SpecialSlotHashes.Length == other.SpecialSlotHashes.Length &&
-                CodeHashes.Length == other.CodeHashes.Length &&
-                SpecialSlotHashes.Zip(other.SpecialSlotHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x) &&
-                CodeHashes.Zip(other.CodeHashes, static (a, b) => a.SequenceEqual(b)).All(static x => x);
-        }
-
-        public override string ToString()
-            {
-                return $$"""
-                Identifier: {{Identifier}},
-                CodeDirectoryFlags: {{CodeDirectoryFlags}},
-                CodeDirectoryVersion: {{CodeDirectoryVersion}},
-                ExecutableSegmentBase: {{ExecutableSegmentBase}},
-                ExecutableSegmentLimit: {{ExecutableSegmentLimit}},
-                ExecutableSegmentFlags: {{ExecutableSegmentFlags}},
-                SpecialSlotHashes: [{{string.Join(", ", SpecialSlotHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}],
-                CodeHashes: [{{string.Join(", ", CodeHashes.Select(h => BitConverter.ToString(h).Replace("-", "")))}}]
-                """;
-            }
-                public override int GetHashCode()
-        {
-            return HashCode.Combine(Identifier, CodeDirectoryFlags, CodeDirectoryVersion, ExecutableSegmentLimit, ExecutableSegmentFlags, SpecialSlotHashes, CodeHashes);
-        }
-
-        /// <summary>
-        /// Parses the output of the `codesign` command to extract codesign information.
-        /// </summary>
-        public static CodesignOutputInfo ParseFromCodeSignOutput(string output)
-        {
-            var splitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
-            var lines = output.Split(new[] { '\n', '\r' }, splitOptions);
-            var Identifier = lines[1].Split('=', splitOptions)[1];
-            var cdInfo = lines[3].Split(' ');
-            var CodeDirectoryVersion = (CodeDirectoryVersion)Convert.ToUInt32(cdInfo[1].Split('=', splitOptions)[1], 16);
-            var CodeDirectoryFlags = (CodeDirectoryFlags)Convert.ToUInt32(cdInfo[3].Split(['=', '('], splitOptions)[1].TrimStart("0x").ToString(), 16);
-            Assert.True(lines[13].StartsWith("Executable Segment base="), "Expected 'Executable Segment base=' at line 13");
-            Assert.True(lines[14].StartsWith("Executable Segment limit="), "Expected 'Executable Segment limit=' at line 14");
-            Assert.True(lines[15].StartsWith("Executable Segment flags="), "Expected 'Executable Segment flags=' at line 15");
-            var ExecutableSegmentBase = ulong.Parse(lines[13].Split('=', splitOptions)[1]);
-            var ExecutableSegmentLimit = ulong.Parse(lines[14].Split('=', splitOptions)[1]);
-            var ExecutableSegmentFlags = (ExecutableSegmentFlags)Convert.ToUInt64(lines[15].Split('=', splitOptions)[1].TrimStart("0x").ToString(), 16);
-            Assert.True(lines[16].StartsWith("Page size=4096"), "Expected 'Page size=4096' at line 16");
-            var (SpecialSlotHashes, CodeHashes) = ExtractHashes(lines.Skip(17));
-
-            return new CodesignOutputInfo
-            {
-                Identifier = Identifier,
-                CodeDirectoryFlags = CodeDirectoryFlags,
-                CodeDirectoryVersion = CodeDirectoryVersion,
-                ExecutableSegmentBase = ExecutableSegmentBase,
-                ExecutableSegmentLimit = ExecutableSegmentLimit,
-                ExecutableSegmentFlags = ExecutableSegmentFlags,
-                SpecialSlotHashes = SpecialSlotHashes,
-                CodeHashes = CodeHashes,
-            };
-
-            static (byte[][] SpecialSlotHashes, byte[][] CodeHashes) ExtractHashes(IEnumerable<string> lines)
-            {
-                List<byte[]> specialSlotHashes = [];
-                List<byte[]> codeHashes = [];
-                foreach (var line in lines)
-                {
-                    if (line[0] is not ('-' or '0' or '1' or '2' or '3' or '4' or '5' or '6' or '7' or '8' or '9'))
-                        break;
-                    var hash = line.Split('=')[1].Trim();
-                    var index = int.Parse(line.Split('=')[0].Trim());
-                    if (index < 0)
-                    {
-                        // specialSlot
-                        specialSlotHashes.Add(ParseByteArray(hash));
-                    }
-                    else
-                    {
-                        // codeHashes
-                        codeHashes.Add(ParseByteArray(hash));
-                    }
-                }
-                return (specialSlotHashes.ToArray(), codeHashes.ToArray());
-            }
-            static byte[] ParseByteArray(string hex)
-            {
-                if (hex.Length % 2 != 0)
-                    throw new ArgumentException("Hex string must have an even length.");
-                byte[] bytes = new byte[hex.Length / 2];
-                for (int i = 0; i < hex.Length; i += 2)
-                {
-                    bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
-                }
-                return bytes;
-            }
-        }
-    }
-
-    string SampleCodesignOutput = """
-    Executable=/Users/jacksonschuster/source/runtime3/artifacts/bin/osx-x64.Debug/corehost/singlefilehost
-    Identifier=singlefilehost-5555494409d4df688bf436b291061028f736b11c
-    Format=Mach-O thin (x86_64)
-    CodeDirectory v=20400 size=89264 flags=0x2(adhoc) hashes=2778+7 location=embedded
-    VersionPlatform=1
-    VersionMin=786432
-    VersionSDK=984064
-    Hash type=sha256 size=32
-    CandidateCDHash sha256=6fee638e9fe544a66b0acf9489ebc59e073b3e39
-    CandidateCDHashFull sha256=6fee638e9fe544a66b0acf9489ebc59e073b3e39082903247c5413f555ec2857
-    Hash choices=sha256
-    CMSDigest=6fee638e9fe544a66b0acf9489ebc59e073b3e39082903247c5413f555ec2857
-    CMSDigestType=2
-    Executable Segment base=0
-    Executable Segment limit=8949760
-    Executable Segment flags=0x1
-    Page size=4096
-        -7=4d8d4b9e4116e8edd996176b5553463acb64287bb635e7f141155529e20457bc
-        -6=0000000000000000000000000000000000000000000000000000000000000000
-        -5=cca8afe72425463c13b813da9ae468ae3b5fe20fe5fe1d3f34302ba2f15722f2
-        -4=0000000000000000000000000000000000000000000000000000000000000000
-        -3=0000000000000000000000000000000000000000000000000000000000000000
-        -2=987920904eab650e75788c054aa0b0524e6a80bfc71aa32df8d237a61743f986
-        -1=0000000000000000000000000000000000000000000000000000000000000000
-        0=20042993665611bf5d01d35a46092c2d43a07883f31247a03b5600c301f5c039
-        1=a97fad07cc9d6eabad27a77e32b69c3da59372fa7987a13c2b8d23f378380476
-        2=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
-        3=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
-        4=ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
-        5=b3d230340aa5ed09c788c39081c207a7430b83d22c9489d84d4ede3ed320f47b
-        6=825b7aa16170a9b739a4689ba8878391bcae87efd63e3b174738c382020031c1
-        7=e360159ee0adaeba5ac5f562c45ec551dbe8b73fbc858beca298610312df33b3
-        8=20585ef0bc0287c5b7a9b54f2669704cdc31cea7d7b1702b336fcf93a9f01ca2
-        9=414ae6563e5881b215a08bb33fc539fb0c90c3a5532f6e15a726ed6cdc255550
-        10=b672b667eb31b48d027bd5f1cf75bad5a8552b4d6b649cbdae35699152fb8a1b
-    CDHash=6fee638e9fe544a66b0acf9489ebc59e073b3e39
-    Signature=adhoc
-    Info.plist=not bound
-    TeamIdentifier=not set
-    Sealed Resources=none
-    Internal requirements count=0 size=12
-    """;
 }
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
index a54fd3056d4e01..452fe8a8a1b32e 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
@@ -19,11 +19,19 @@
 using System.Collections.Generic;
 using Microsoft.DotNet.Cli.Build.Framework;
 using System.Security.AccessControl;
+using Microsoft.NET.HostModel.Bundle;
 
 namespace Microsoft.NET.HostModel.MachO.CodeSign.Tests
 {
-    public class SigningTests
+    public class SigningTests :IClassFixture<SigningTests.SharedTestState>
     {
+        private SharedTestState sharedTestState;
+
+        public SigningTests(SharedTestState fixture)
+        {
+            sharedTestState = fixture;
+        }
+
         [Theory]
         [MemberData(nameof(GetTestFilePaths), nameof(CanSignMachObject))]
         public void CanSignMachObject(string filePath, TestArtifact _)
@@ -82,13 +90,14 @@ void MatchesCodesignOutput(string filePath, TestArtifact _)
             // Codesigned file
             File.Copy(filePath, codesignFilePath);
             Assert.True(Codesign.IsAvailable, "Could not find codesign tool");
-            Codesign.Run("-s - -f --preserve-metadata=entitlements -i" + fileName, codesignFilePath).ExitCode.Should().Be(0, $"'codesign -s - {codesignFilePath}' failed!");
+            var (exitCode, stdErr) = Codesign.Run("-s - -f --preserve-metadata=entitlements -i" + fileName, codesignFilePath);
+            Assert.Equal(0, exitCode);
 
             // Managed signed file
             AdHocSignFile(originalFilePath, managedSignedPath, fileName);
 
-            var check = Codesign.Run("-v", managedSignedPath);
-            check.ExitCode.Should().Be(0, check.StdErr, $"Failed to sign a copy of '{filePath}'");
+            (exitCode, stdErr) = Codesign.Run("-v", managedSignedPath);
+            Assert.Equal(0, exitCode);
             AssertMachFilesAreEquivalent(codesignFilePath, managedSignedPath, fileName);
         }
 
@@ -109,7 +118,7 @@ void SignedMachOExecutableRuns()
                 File.SetUnixFileMode(signedPath, UnixFileMode.UserRead | UnixFileMode.UserExecute);
 
                 var result = Command.Create(signedPath).CaptureStdErr().CaptureStdOut().Execute();
-                result.ExitCode.Should().Be(0, result.StdErr);
+                Assert.Equal(0, result.ExitCode);
             }
         }
 
@@ -131,6 +140,78 @@ void ReadSignedMachIsTheSameAsReadAndResigned(string filePath, TestArtifact _)
             }
         }
 
+        [Fact]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        public void SigningAppHostPreservesEntitlements()
+        {
+            using var testDirectory = TestArtifact.Create(nameof(SigningAppHostPreservesEntitlements));
+            var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.AppHost.FilePath));
+            File.Copy(Binaries.AppHost.FilePath, testAppHostPath);
+            string signedHostPath = testAppHostPath + ".signed";
+
+            HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
+
+            Assert.True(SigningTests.HasEntitlementsBlob(testAppHostPath));
+            Assert.True(SigningTests.HasEntitlementsBlob(signedHostPath));
+            Assert.True(SigningTests.HasDerEntitlementsBlob(testAppHostPath));
+            Assert.True(SigningTests.HasDerEntitlementsBlob(signedHostPath));
+        }
+
+        [Fact]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        public void BundledAppHostHasEntitlements()
+        {
+            using var testDirectory = TestArtifact.Create(nameof(BundledAppHostHasEntitlements));
+            var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.SingleFileHost.FilePath));
+            File.Copy(Binaries.SingleFileHost.FilePath, testAppHostPath);
+            string signedHostPath = testAppHostPath + ".signed";
+
+            HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
+            var bundlePath = new Bundler(Path.GetFileName(signedHostPath), testAppHostPath + ".bundle").GenerateBundle([new(signedHostPath, Path.GetFileName(signedHostPath))]);
+
+            Assert.True(SigningTests.HasEntitlementsBlob(testAppHostPath));
+            Assert.True(SigningTests.HasEntitlementsBlob(bundlePath));
+            Assert.True(SigningTests.HasDerEntitlementsBlob(testAppHostPath));
+            Assert.True(SigningTests.HasDerEntitlementsBlob(bundlePath));
+        }
+
+        [Fact]
+        [PlatformSpecific(TestPlatforms.OSX)]
+        public void OverwritingExistingBundleClearsMacOsSignatureCache()
+        {
+            // Bundle to a single-file and ensure it is signed
+            string singleFile = sharedTestState.SelfContainedApp.Bundle();
+            Assert.True(SigningTests.IsSigned(singleFile));
+
+            var firstInode = Inode.GetInode(singleFile);
+
+            // Rebundle to the same location.
+            // Bundler should create a new inode for the bundle which should clear the MacOS signature cache.
+            string oldFile = singleFile;
+            string dir = Path.GetDirectoryName(singleFile);
+            singleFile = sharedTestState.SelfContainedApp.Rebundle(dir, BundleOptions.BundleAllContent, out var _, new Version(5, 0));
+            Assert.True(singleFile == oldFile, "Rebundled app should have the same path as the original single-file app.");
+            var secondInode = Inode.GetInode(singleFile);
+            Assert.False(firstInode == secondInode, "not a different inode after re-bundling");
+            // Ensure the MacOS signature cache is cleared
+            Assert.True(Codesign.Run("-v", singleFile).ExitCode == 0);
+        }
+
+        public class SharedTestState : IDisposable
+        {
+            public SingleFileTestApp SelfContainedApp { get; }
+
+            public SharedTestState()
+            {
+                SelfContainedApp = SingleFileTestApp.CreateSelfContained("HelloWorld");
+            }
+
+            public void Dispose()
+            {
+                SelfContainedApp.Dispose();
+            }
+        }
+
         static void AssertMachFilesAreEquivalent(string codesignedPath, string managedSignedPath, string fileName)
         {
             using var managedFileStream = new FileStream(managedSignedPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1);
diff --git a/src/installer/tests/TestUtils/Inode.cs b/src/installer/tests/TestUtils/Inode.cs
new file mode 100644
index 00000000000000..0969b57362c6ec
--- /dev/null
+++ b/src/installer/tests/TestUtils/Inode.cs
@@ -0,0 +1,28 @@
+﻿// 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.Runtime.Versioning;
+using System.Text;
+using Microsoft.DotNet.Cli.Build.Framework;
+using Xunit;
+
+namespace Microsoft.DotNet.CoreSetup.Test
+{
+    [SupportedOSPlatform("linux")]
+    [SupportedOSPlatform("macos")]
+    public static class Inode
+    {
+        public static string GetInode(string path)
+        {
+            var firstls = Command.Create("/bin/ls", "-li", path)
+                .CaptureStdErr()
+                .CaptureStdOut()
+                .Execute();
+            firstls.Should().Pass();
+            var firstInode = firstls.StdOut.Split(' ')[0];
+            return firstInode;
+        }
+    }
+}
diff --git a/src/installer/tests/TestUtils/SingleFileTestApp.cs b/src/installer/tests/TestUtils/SingleFileTestApp.cs
index 7348d893206732..74a6df5cdf05da 100644
--- a/src/installer/tests/TestUtils/SingleFileTestApp.cs
+++ b/src/installer/tests/TestUtils/SingleFileTestApp.cs
@@ -95,7 +95,7 @@ public string Bundle(BundleOptions options, out Manifest manifest, Version? bund
             return Bundle(options, bundleDirectory, out manifest, bundleVersion);
         }
 
-        public string ReBundle(string bundleDirectory, BundleOptions options, out Manifest manifest, Version? bundleVersion = null)
+        public string Rebundle(string bundleDirectory, BundleOptions options, out Manifest manifest, Version? bundleVersion = null)
         {
             // Reuse the existing bundle directory if it exists
             if (!Directory.Exists(bundleDirectory))

From edaf00b039f1c85616f07da56aa915a45c576ce5 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Wed, 2 Jul 2025 09:05:03 -0700
Subject: [PATCH 16/16] PR Feedback:

- Remove ConditionAttribute on test
- Use File.SetUnixFileMode
---
 .../AppHost/HostWriter.cs                     | 32 +++------
 .../Microsoft.NET.HostModel/Bundle/Bundler.cs |  9 ++-
 .../Utils/UnixUtils.cs                        | 69 +++++++++++++++++++
 .../Bundle/BundlerConsistencyTests.cs         |  1 -
 4 files changed, 85 insertions(+), 26 deletions(-)
 create mode 100644 src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs

diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
index a0fa93553db58e..47f64f4669c60c 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
@@ -179,7 +179,14 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
                         }
                     }
                 });
-                Chmod755(appHostDestinationFilePath);
+                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    // chmod +755
+                    File.SetUnixFileMode(appHostDestinationFilePath,
+                        UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+                        UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+                        UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
+                }
             }
             catch (Exception ex)
             {
@@ -219,28 +226,5 @@ private static byte[] GetSearchOptionBytes(DotNetSearchOptions searchOptions)
 
             return searchOptionsBytes;
         }
-
-        internal static void Chmod755(string pathName)
-        {
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-                return;
-            var filePermissionOctal = Convert.ToInt32("755", 8); // -rwxr-xr-x
-            const int EINTR = 4;
-            int chmodReturnCode;
-
-            do
-            {
-                chmodReturnCode = chmod(pathName, filePermissionOctal);
-            }
-            while (chmodReturnCode == -1 && Marshal.GetLastWin32Error() == EINTR);
-
-            if (chmodReturnCode == -1)
-            {
-                throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {Convert.ToString(filePermissionOctal, 8)} for {pathName}.");
-            }
-        }
-
-        [LibraryImport("libc", SetLastError = true)]
-        private static partial int chmod([MarshalAs(UnmanagedType.LPStr)] string pathname, int mode);
     }
 }
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index 0952331b076df1..a5e8b593484198 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -402,7 +402,14 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
                     }
                 }
             }
-            HostWriter.Chmod755(bundlePath);
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                // chmod +755
+                File.SetUnixFileMode(bundlePath,
+                     UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+                     UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+                     UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
+            }
             return bundlePath;
         }
 
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs
new file mode 100644
index 00000000000000..58306e0bb846ac
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/Utils/UnixUtils.cs
@@ -0,0 +1,69 @@
+﻿// 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/Bundle/BundlerConsistencyTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs
index 77e347970826e1..7cce3c76fdd8bb 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs
@@ -316,7 +316,6 @@ public void AssemblyAlignment()
         }
 
         [Fact]
-        [Conditional("DEBUG")] // Relies on debug asserts in product code
         public void LongFileNames()
         {
             var app = sharedTestState.App;
