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 8de8a0b7fdde36..3fa51f121ed44a 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 @@ -69,7 +69,9 @@ private CodeDirectoryBlob( HashType hashType, ExecutableSegmentFlags execSegmentFlags, byte[][] specialSlotHashes, - byte[][] codeHashes) + byte[][] codeHashes, + ulong execSegmentBase = 0, + ulong execSegmentLimit = 0) { // Always assume the executable length is the entire file size / signature start. _cdHeader = new CodeDirectoryHeader( @@ -80,8 +82,8 @@ private CodeDirectoryBlob( hashType.GetHashSize(), hashType, signatureStart, - 0, - signatureStart, + execSegmentBase, + execSegmentLimit, execSegmentFlags); _identifier = identifier; _specialSlotHashes = specialSlotHashes; @@ -120,9 +122,15 @@ public static CodeDirectoryBlob Create( string identifier, RequirementsBlob requirementsBlob, HashType hashType = HashType.SHA256, - uint pageSize = MachObjectFile.DefaultPageSize) + uint pageSize = MachObjectFile.DefaultPageSize, + ulong execSegmentBase = 0, + ulong execSegmentLimit = 0, + ulong textSegmentFileEnd = 0) { - uint codeSlotCount = GetCodeSlotCount((uint)signatureStart, pageSize); + // When textSegmentFileEnd is provided, we only hash the __TEXT segment (macOS 26+ behavior). + // Otherwise, we hash the entire file up to the signature start (legacy behavior). + long hashLimit = textSegmentFileEnd > 0 ? (long)textSegmentFileEnd : signatureStart; + uint codeSlotCount = GetCodeSlotCount((uint)hashLimit, pageSize); uint specialCodeSlotCount = (uint)CodeDirectorySpecialSlot.Requirements; var specialSlotHashes = new byte[specialCodeSlotCount][]; @@ -150,7 +158,8 @@ public static CodeDirectoryBlob Create( Array.Reverse(specialSlotHashes); // 0 - N are Code hashes - long remaining = signatureStart; + // Hash up to the hash limit (either __TEXT segment end or signature start) + long remaining = hashLimit; long buffptr = 0; int cdIndex = 0; byte[] pageBuffer = new byte[pageSize]; @@ -171,7 +180,9 @@ public static CodeDirectoryBlob Create( hashType, ExecutableSegmentFlags.MainBinary, specialSlotHashes, - codeHashes); + codeHashes, + execSegmentBase, + execSegmentLimit); } [StructLayout(LayoutKind.Sequential)] @@ -251,18 +262,29 @@ public override bool Equals(object? obj) return false; if (_identifier != other._identifier) + { + Debug.WriteLine($"Identifiers differ: '{_identifier}' vs '{other._identifier}'"); + Debug.WriteLine($"Expected (codesign):\n{other.ToCodesignString()}"); + Debug.WriteLine($"Actual (managed):\n{ToCodesignString()}"); return false; + } CodeDirectoryHeader thisHeader = _cdHeader; CodeDirectoryHeader otherHeader = other._cdHeader; if (!CodeDirectoryHeader.AreEqual(thisHeader, otherHeader)) { + Debug.WriteLine("CodeDirectory headers differ"); + Debug.WriteLine($"Expected (codesign):\n{other.ToCodesignString()}"); + Debug.WriteLine($"Actual (managed):\n{ToCodesignString()}"); return false; } for (int i = 0; i < _specialSlotHashes.Length; i++) { if (!_specialSlotHashes[i].SequenceEqual(other._specialSlotHashes[i])) { + Debug.WriteLine($"Special slot hash {-(int)SpecialSlotCount + i} differs"); + Debug.WriteLine($"Expected (codesign):\n{other.ToCodesignString()}"); + Debug.WriteLine($"Actual (managed):\n{ToCodesignString()}"); return false; } } @@ -271,6 +293,9 @@ public override bool Equals(object? obj) { if (!_codeHashes[i].SequenceEqual(other._codeHashes[i])) { + Debug.WriteLine($"Code hash {i} differs"); + Debug.WriteLine($"Expected (codesign):\n{other.ToCodesignString()}"); + Debug.WriteLine($"Actual (managed):\n{ToCodesignString()}"); return false; } } @@ -317,4 +342,43 @@ public int Write(IMachOFileWriter accessor, long offset) } return (int)Size; } + + /// + /// Formats the CodeDirectory information in a style similar to codesign's output + /// + internal string ToCodesignString() + { + var sb = new StringBuilder(); + sb.AppendLine($"Identifier={_identifier}"); + sb.AppendLine($"CodeDirectory v={(uint)_cdHeader.Version:X} size={Size} flags=0x{(uint)_cdHeader.Flags:X}({_cdHeader.Flags.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture)}) hashes={CodeSlotCount}+{SpecialSlotCount} location=embedded"); + sb.AppendLine($"Hash type={_cdHeader.HashType.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture)} size={HashSize}"); + sb.AppendLine($"Executable Segment base={_cdHeader.ExecSegmentBase}"); + sb.AppendLine($"Executable Segment limit={_cdHeader.ExecSegmentLimit}"); + sb.AppendLine($"Executable Segment flags=0x{(ulong)_cdHeader.ExecSegmentFlags:X}"); + sb.AppendLine($"Page size={(1 << _cdHeader.Log2PageSize)}"); + + // Print special slot hashes (numbered from -SpecialSlotCount to -1) + for (int i = 0; i < SpecialSlotCount; i++) + { + int slotNumber = -(int)SpecialSlotCount + i; + sb.AppendLine($" {slotNumber,3}={ToHexStringLower(_specialSlotHashes[i])}"); + } + + // Print code hashes (numbered from 0 to CodeSlotCount-1) + for (int i = 0; i < CodeSlotCount; i++) + { + sb.AppendLine($" {i,3}={ToHexStringLower(_codeHashes[i])}"); + } + + return sb.ToString(); + } + + private static string ToHexStringLower(byte[] bytes) + { +#if NET + return Convert.ToHexStringLower(bytes); +#else + return BitConverter.ToString(bytes).Replace("-", "").ToLower(System.Globalization.CultureInfo.InvariantCulture); +#endif + } } 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..baa73edef15657 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; @@ -175,15 +176,29 @@ public static void AssertEquivalent(EmbeddedSignatureBlob? a, EmbeddedSignatureB throw new ArgumentNullException("Both EmbeddedSignatureBlobs must be non-null for comparison."); if (a.GetSpecialSlotHashCount() != b.GetSpecialSlotHashCount()) + { + Debug.WriteLine($"Special slot hash counts differ: {a.GetSpecialSlotHashCount()} vs {b.GetSpecialSlotHashCount()}"); throw new ArgumentException("Special slot hash counts are not equivalent."); + } if (!a.CodeDirectoryBlob.Equals(b.CodeDirectoryBlob)) + { + Debug.WriteLine("CodeDirectory blobs are not equivalent:"); + Debug.WriteLine($"Expected (codesign):\n{b.CodeDirectoryBlob.ToCodesignString()}"); + Debug.WriteLine($"Actual (managed):\n{a.CodeDirectoryBlob.ToCodesignString()}"); throw new ArgumentException("CodeDirectory blobs are not equivalent"); + } if (a.RequirementsBlob?.Size != b.RequirementsBlob?.Size) + { + Debug.WriteLine($"Requirements blob sizes differ: {a.RequirementsBlob?.Size} vs {b.RequirementsBlob?.Size}"); throw new ArgumentException("Requirements blobs are not equivalent"); + } if (a.CmsWrapperBlob?.Size != b.CmsWrapperBlob?.Size) + { + Debug.WriteLine($"CMS Wrapper blob sizes differ: {a.CmsWrapperBlob?.Size} vs {b.CmsWrapperBlob?.Size}"); throw new ArgumentException("CMS Wrapper blobs are not equivalent"); + } } } diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs index 407eeeaf022d3b..a47f4f26a3bb3b 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs @@ -133,11 +133,29 @@ private static EmbeddedSignatureBlob CreateSignature(MachObjectFile machObject, uint signatureStart = machObject._codeSignatureLoadCommand.Command.GetDataOffset(machObject._header); RequirementsBlob requirementsBlob = RequirementsBlob.Empty; CmsWrapperBlob cmsWrapperBlob = CmsWrapperBlob.Empty; + + // Get __TEXT segment boundaries + // The VM address range is used for the CodeDirectory header metadata + ulong textSegmentVMAddress = machObject._textSegment64.Command.GetVMAddress(machObject._header); + ulong textSegmentVMSize = machObject._textSegment64.Command.GetVMSize(machObject._header); + ulong execSegmentBase = textSegmentVMAddress; + ulong execSegmentLimit = textSegmentVMAddress + textSegmentVMSize; + + // The file range is used for hashing - we only hash the __TEXT segment content + // The __TEXT segment typically starts at file offset 0 and contains the Mach header and load commands + ulong textSegmentFileOffset = machObject._textSegment64.Command.GetFileOffset(machObject._header); + ulong textSegmentFileSize = machObject._textSegment64.Command.GetFileSize(machObject._header); + Debug.Assert(textSegmentFileOffset == 0, "Expected __TEXT segment to start at file offset 0"); + ulong textSegmentFileEnd = textSegmentFileOffset + textSegmentFileSize; + var codeDirectory = CodeDirectoryBlob.Create( file, signatureStart, identifier, - requirementsBlob); + requirementsBlob, + execSegmentBase: execSegmentBase, + execSegmentLimit: execSegmentLimit, + textSegmentFileEnd: textSegmentFileEnd); return new EmbeddedSignatureBlob( codeDirectoryBlob: codeDirectory, 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 9ce6d66c54782b..b1ed8eada30fcd 100644 --- a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs +++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs @@ -77,7 +77,7 @@ public void CanUnsignAndResign(string filePath, TestArtifact _) Assert.True(IsSigned(managedSignedPath + ".resigned"), $"Failed to resign {filePath}"); } - [Theory(Skip = "Temporarily disabled due to macOS 26 codesign behavior change - only hashing __TEXT segment")] + [Theory] [MemberData(nameof(GetTestFilePaths), nameof(MatchesCodesignOutput))] [PlatformSpecific(TestPlatforms.OSX)] void MatchesCodesignOutput(string filePath, TestArtifact _)