Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7dc2929
Preserve entitlements when signing mach files
jtschuster Jun 13, 2025
f0190e9
Merge branch 'main' of https://github.com/dotnet/runtime into Preserv…
jtschuster Jun 13, 2025
b705e32
Add tests to verify new inodes are created in apphost signing
jtschuster Jun 13, 2025
e2ead97
Update blob parser and special slot count for new blobs, add tests fo…
jtschuster Jun 13, 2025
29f6fb8
Add tests to compare EmbeddedSignatureBlob data to codesign output
jtschuster Jun 14, 2025
839c318
Make test platform specific
jtschuster Jun 16, 2025
0466408
Remove RequirementsSize from CodesignInfo, preserve entitlements in c…
jtschuster Jun 16, 2025
9d3c8c2
Make method public again
jtschuster Jun 17, 2025
df1da52
Merge branch 'main' of https://github.com/jtschuster/runtime into Pre…
jtschuster Jun 18, 2025
5a70f55
Merge branch 'PreserveEntitlementsInMachSigner' of https://github.com…
jtschuster Jun 18, 2025
89a550d
Use FileStream for ResourceUpdater until we can precalculate the size…
jtschuster Jun 18, 2025
55ee29b
Make codesign less verbose to avoid timeout
jtschuster Jun 19, 2025
913ddfd
Merge branch 'PreserveEntitlementsInMachSigner' of https://github.com…
jtschuster Jun 19, 2025
7e6d834
Add IO exception retries on test file backups
jtschuster Jun 23, 2025
d795300
Merge branch 'main' of https://github.com/dotnet/runtime into Preserv…
jtschuster Jun 24, 2025
c1d5d75
Apply suggestions from code review
jtschuster Jun 24, 2025
c668a03
Field -> Property
jtschuster Jun 24, 2025
93790cf
Merge branch 'main' into PreserveEntitlementsInMachSigner
jtschuster Jun 30, 2025
50eb029
Use ReadOnlySpan for header placeholder data
jtschuster Jun 30, 2025
b7f05cd
Make property internal
jtschuster Jun 30, 2025
2a33df4
PR Feedback: Rename ReBundle, move tests to HostModel.Tests, use help…
jtschuster Jul 1, 2025
9e2820c
Merge branch 'PreserveEntitlementsInMachSigner' of https://github.com…
jtschuster Jul 1, 2025
edaf00b
PR Feedback:
jtschuster Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
201 changes: 36 additions & 165 deletions src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,16 @@ 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);
// 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 (MemoryMappedFile appHostDestinationMap = MemoryMappedFile.CreateNew(null, appHostDestinationLength))
{
using (MemoryMappedViewStream appHostDestinationStream = appHostDestinationMap.CreateViewStream())
using (FileStream appHostSourceStream = new(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
{
isMachOImage = MachObjectFile.IsMachOImage(appHostSourceStream);
Expand All @@ -135,45 +143,50 @@ 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 (MemoryMappedViewAccessor memoryMappedViewAccessor = appHostDestinationMap.CreateViewAccessor())
{
// 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 (FileStream appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 1))
using (MemoryMappedViewAccessor appHostAccessor = appHostDestinationMap.CreateViewAccessor(0, appHostDestinationLength, MemoryMappedFileAccess.Read))
{
using var updater = new ResourceUpdater(appHostDestinationStream, true);
updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
updater.Update();
// 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);
// TODO: This could be moved to work on the MemoryMappedFile if we can precalculate the size required.
if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
{
using ResourceUpdater updater = new ResourceUpdater(appHostDestinationStream, leaveOpen: true);
updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
updater.Update();
}
}
}
});
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)
{
Expand All @@ -191,125 +204,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))
Expand All @@ -332,28 +226,5 @@ private static byte[] GetSearchOptionBytes(DotNetSearchOptions searchOptions)

return searchOptionsBytes;
}

private 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ public PlaceHolderNotFoundInAppHostException(byte[] pattern)
{
MissingPattern = pattern;
}
public PlaceHolderNotFoundInAppHostException(ReadOnlySpan<byte> pattern)
{
MissingPattern = pattern.ToArray();
}
}
}
Loading
Loading