Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -13,16 +13,17 @@ public enum MachOFormatError
Not64BitExe, // Apphost is expected to be a 64-bit MachO executable
DuplicateLinkEdit, // Only one __LINKEDIT segment is expected in the apphost
DuplicateSymtab, // Only one SYMTAB is expected in the apphost
SignNeedsLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
SignNeedsSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
MissingLinkEdit, // CODE_SIGNATURE command must follow a Segment64 command named __LINKEDIT
MissingSymtab, // CODE_SIGNATURE command must follow the SYMTAB command
LinkEditNotLast, // __LINKEDIT must be the last segment in the binary layout
SymtabNotInLinkEdit, // SYMTAB must within the __LINKEDIT segment!
SignNotInLinkEdit, // Signature blob must be within the __LINKEDIT segment!
SignCommandNotLast, // CODE_SIGNATURE command must be the last command
SignBlobNotLast, // Signature blob must be at the very end of the file
SignDoesntFollowSymtab, // Signature blob must immediately follow the Symtab
MemoryMapAccessFault, // Error reading the memory-mapped apphost
InvalidUTF8 // UTF8 decoding failed
InvalidUTF8, // UTF8 decoding failed
SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle)
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ public static void SetAsBundle(
BitConverter.GetBytes(bundleHeaderOffset),
pad0s: false));

RetryUtil.RetryOnIOError(() =>
MachOUtils.AdjustHeadersForBundle(appHostPath));

// Memory-mapped write does not updating last write time
RetryUtil.RetryOnIOError(() =>
File.SetLastWriteTimeUtc(appHostPath, DateTime.UtcNow));
Expand Down
132 changes: 129 additions & 3 deletions src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ public static unsafe bool RemoveSignature(string filePath)
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
RuntimeHelpers.PrepareConstrainedRegions();
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Expand Down Expand Up @@ -264,8 +263,8 @@ public static unsafe bool RemoveSignature(string filePath)

if (signature != null)
{
Verify(linkEdit != null, MachOFormatError.SignNeedsLinkEdit);
Verify(symtab != null, MachOFormatError.SignNeedsSymtab);
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
Verify(symtab != null, MachOFormatError.MissingSymtab);

var symtabEnd = symtab->stroff + symtab->strsize;
var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
Expand Down Expand Up @@ -319,5 +318,132 @@ public static unsafe bool RemoveSignature(string filePath)
return false;
}
}

/// <summary>
/// This Method is a utility to adjust the apphost MachO-header
/// to include the bytes added by the single-file bundler at the end of the file.
///
/// The tool assumes the following layout of the executable
///
/// * MachoHeader (64-bit, executable, not swapped integers)
/// * LoadCommands
/// LC_SEGMENT_64 (__PAGEZERO)
/// LC_SEGMENT_64 (__TEXT)
/// LC_SEGMENT_64 (__DATA)
/// LC_SEGMENT_64 (__LINKEDIT)
/// ...
/// LC_SYMTAB
///
/// * ... Different Segments
///
/// * The __LINKEDIT Segment (last)
/// * ... Different sections ...
/// * SYMTAB (last)
///
/// The MAC codesign tool places several restrictions on the layout
/// * The __LINKEDIT segment must be the last one
/// * The __LINKEDIT segment must cover the end of the file
/// * All bytes in the __LINKEDIT segment are used by other linkage commands
/// (ex: symbol/string table, dynamic load information etc)
///
/// In order to circumvent these restrictions, we:
/// * Extend the __LINKEDIT segment to include the bundle-data
/// * Extend the string table to include all the bundle-data
/// (that is, the bundle-data appear as strings to the loader/codesign tool).
///
/// This method has certain limitations:
/// * The bytes for the bundler may be unnecessarily loaded at startup
/// * Tools that process the string table may be confused (?)
/// * The string table size is limited to 4GB. Bundles larger than that size
/// cannot be accomodated by this utility.
///
/// </summary>
/// <param name="filePath">Path to the AppHost</param>
/// <returns>
/// True if
/// - The input is a MachO binary, and
/// - The additional bytes were successfully accomodated within the MachO segments.
/// False otherwise
/// </returns>
/// <exception cref="AppHostMachOFormatException">
/// The input is a MachO file, but doesn't match the expect format of the AppHost.
/// </exception>
public static unsafe bool AdjustHeadersForBundle(string filePath)
{
ulong fileLength = (ulong)new FileInfo(filePath).Length;
using (var mappedFile = MemoryMappedFile.CreateFromFile(filePath))
{
using (var accessor = mappedFile.CreateViewAccessor())
{
byte* file = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
Verify(file != null, MachOFormatError.MemoryMapAccessFault);

MachHeader* header = (MachHeader*)file;

if (!header->IsValid())
{
// Not a MachO file.
return false;
}

Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);

file += sizeof(MachHeader);
SegmentCommand64* linkEdit = null;
SymtabCommand* symtab = null;
LinkEditDataCommand* signature = null;

for (uint i = 0; i < header->ncmds; i++)
{
LoadCommand* command = (LoadCommand*)file;
if (command->cmd == Command.LC_SEGMENT_64)
{
SegmentCommand64* segment = (SegmentCommand64*)file;
if (segment->SegName.Equals("__LINKEDIT"))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
else if (command->cmd == Command.LC_SYMTAB)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}

file += command->cmdsize;
}

Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
Verify(symtab != null, MachOFormatError.MissingSymtab);

// Update the string table to include bundle-data
ulong newStringTableSize = fileLength - symtab->stroff;
if (newStringTableSize > uint.MaxValue)
{
// Too big, too bad;
return false;
}
symtab->strsize = (uint)newStringTableSize;

// Update the __LINKEDIT segment to include bundle-data
linkEdit->filesize = fileLength - linkEdit->fileoff;
linkEdit->vmsize = linkEdit->filesize;
}
finally
{
if (file != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
}

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.DotNet.Cli.Build.Framework;
using Microsoft.DotNet.CoreSetup.Test;
using BundleTests.Helpers;
using System.Runtime.InteropServices;

namespace Microsoft.NET.HostModel.Tests
{
Expand All @@ -31,6 +32,20 @@ private void RunTheApp(string path)
.HaveStdOutContaining("Wow! We now say hello to the big world and you.");
}

private void CheckFileNotarizable(string path)
{
// attempt to remove signature data.
// no-op if the file is not signed (it should not be)
// fail if the file structure is malformed
// i: input, o: output, r: remove
Command.Create("codesign_allocate", $"-i {path} -o {path} -r")
.CaptureStdErr()
.CaptureStdOut()
.Execute()
.Should()
.Pass();
}

private void BundleRun(TestProjectFixture fixture, string publishPath)
{
var hostName = BundleHelper.GetHostName(fixture);
Expand All @@ -41,6 +56,13 @@ private void BundleRun(TestProjectFixture fixture, string publishPath)
// Bundle to a single-file
string singleFile = BundleHelper.BundleApp(fixture);

// check that the file structure is understood by codesign
var targetOS = BundleHelper.GetTargetOS(fixture.CurrentRid);
if (targetOS == OSPlatform.OSX)
{
CheckFileNotarizable(singleFile);
}

// Run the extracted app
RunTheApp(singleFile);
}
Expand Down