Skip to content

Commit 182a0b6

Browse files
committed
Fix Mach-O boundary validation
1 parent a530693 commit 182a0b6

8 files changed

Lines changed: 84 additions & 7 deletions

File tree

‎src/LibObjectFile.Tests/MachO/MachOSigningTests.cs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,27 @@ public void RejectsWhatCannotBeSigned()
254254
ByteArrayAssert.AreEqual(before, WriteToArray(file), "a failed signing changed the image");
255255
}
256256

257+
[TestMethod]
258+
public void RejectsSigningOffsetsThatCannotBeAligned()
259+
{
260+
var file = LoadMachO("unixthread_i386");
261+
var trailing = file.Content[^1];
262+
trailing.Position = uint.MaxValue - trailing.Size;
263+
var commandCount = file.LoadCommands.Count;
264+
265+
var exception = Assert.ThrowsExactly<InvalidOperationException>(() => file.AdHocSign("swkotor"));
266+
StringAssert.Contains(exception.Message, "cannot be aligned");
267+
Assert.AreEqual(commandCount, file.LoadCommands.Count, "failed signing left its load command behind");
268+
}
269+
270+
[TestMethod]
271+
public void ComputesCodeSlotsWithoutOverflow()
272+
{
273+
var builder = new MachOAdHocSignatureBuilder("swkotor") { CodeLimit = uint.MaxValue };
274+
275+
Assert.AreEqual(1_048_576u, builder.CodeSlotCount);
276+
}
277+
257278
private static Span<byte> FindCodeDirectory(byte[] image, uint signatureOffset)
258279
=> FindBlob(image, signatureOffset, CodeDirectorySlot);
259280

‎src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the license.txt file in the project root for more information.
44

55
using System;
6+
using System.Buffers.Binary;
67
using System.Collections.Generic;
78
using System.IO;
89
using System.Linq;
@@ -497,6 +498,13 @@ public void ReadsAndWritesAUniversalBinary()
497498
tooFar.Slices[1].FileOffset = 0x1_0000_0000;
498499
tooFar.Slices[1].Size = 0x10;
499500
Assert.IsTrue(tooFar.Verify().Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit));
501+
502+
// The table is what the loader uses to select a slice, so it has to describe the image
503+
// that will actually be found there.
504+
var mismatched = MachOFatFile.Read(new MemoryStream(original));
505+
mismatched.Slices[0].CpuType = MachOCpuType.X86;
506+
Assert.IsFalse(mismatched.TryWrite(new MemoryStream(), out var mismatchDiagnostics));
507+
Assert.IsTrue(mismatchDiagnostics.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_FatSliceArchitectureMismatch), string.Join("; ", mismatchDiagnostics.Messages));
500508
}
501509

502510
/// <summary>
@@ -646,5 +654,35 @@ public void ReportsRatherThanThrowsOnUnreadableInput()
646654
BitConverter.GetBytes(0u).Reverse().ToArray().CopyTo(fat, MachOFatFile.HeaderSize + 12);
647655
Assert.IsFalse(MachOFatFile.TryRead(new MemoryStream(fat), out _, out var emptySlice));
648656
Assert.IsTrue(emptySlice.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidFatArchRange), string.Join("; ", emptySlice.Messages));
657+
658+
// FAT64 offsets and sizes are unsigned 64-bit fields. An end that wraps must be rejected
659+
// before either value is narrowed for SubStream.
660+
var wrappingFatRange = new byte[MachOFatFile.HeaderSize + MachOFatFile.GetSliceEntrySize(true)];
661+
BinaryPrimitives.WriteUInt32BigEndian(wrappingFatRange, MachOMagic.FatMagic64);
662+
BinaryPrimitives.WriteUInt32BigEndian(wrappingFatRange.AsSpan(4), 1);
663+
BinaryPrimitives.WriteUInt64BigEndian(wrappingFatRange.AsSpan(MachOFatFile.HeaderSize + 8), ulong.MaxValue - 15);
664+
BinaryPrimitives.WriteUInt64BigEndian(wrappingFatRange.AsSpan(MachOFatFile.HeaderSize + 16), MachOFile.MinHeaderSize);
665+
Assert.IsFalse(MachOFatFile.TryRead(new MemoryStream(wrappingFatRange), out _, out var wrappingFatSlice));
666+
Assert.IsTrue(wrappingFatSlice.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidFatArchRange), string.Join("; ", wrappingFatSlice.Messages));
667+
668+
// The same overflow is possible in a 64-bit section size. Build the smallest image with
669+
// one LC_SEGMENT_64 and one section whose declared end wraps back into the file.
670+
const int machHeaderSize = 32;
671+
const int segmentCommandSize = 72;
672+
const int sectionSize = 80;
673+
var wrappingSectionRange = new byte[machHeaderSize + segmentCommandSize + sectionSize];
674+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange, MachOMagic.Magic64);
675+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(4), (uint)MachOCpuType.X86_64);
676+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(12), (uint)MachOFileType.Object);
677+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(16), 1);
678+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(20), segmentCommandSize + sectionSize);
679+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(machHeaderSize), (uint)MachOLoadCommandType.Segment64);
680+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(machHeaderSize + 4), segmentCommandSize + sectionSize);
681+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(machHeaderSize + 64), 1);
682+
var sectionOffset = machHeaderSize + segmentCommandSize;
683+
BinaryPrimitives.WriteUInt64LittleEndian(wrappingSectionRange.AsSpan(sectionOffset + 40), unchecked(ulong.MaxValue - (ulong)wrappingSectionRange.Length + 11));
684+
BinaryPrimitives.WriteUInt32LittleEndian(wrappingSectionRange.AsSpan(sectionOffset + 48), (uint)wrappingSectionRange.Length);
685+
Assert.IsFalse(MachOFile.TryRead(new MemoryStream(wrappingSectionRange), out _, out var wrappingSection));
686+
Assert.IsTrue(wrappingSection.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidContentFileRange), string.Join("; ", wrappingSection.Messages));
649687
}
650688
}

‎src/LibObjectFile/Diagnostics/DiagnosticId.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,5 @@ public enum DiagnosticId
241241
MACHO_ERR_MissingFatSliceImage = 5023,
242242
MACHO_ERR_UnexpectedEndOfStream = 5024,
243243
MACHO_ERR_UnexpectedArchive = 5025,
244-
}
244+
MACHO_ERR_FatSliceArchitectureMismatch = 5026,
245+
}

‎src/LibObjectFile/MachO/CodeSign/MachOAdHocSignatureBuilder.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public MachOAdHocSignatureBuilder(string identifier)
7373
/// <summary>
7474
/// Gets the number of page hashes the code directory will hold for the current <see cref="CodeLimit"/>.
7575
/// </summary>
76-
public uint CodeSlotCount => (CodeLimit + PageSize - 1) / PageSize;
76+
public uint CodeSlotCount => (uint)(((ulong)CodeLimit + PageSize - 1) / PageSize);
7777

7878
/// <summary>
7979
/// Gets the total size of the signature for the current <see cref="CodeLimit"/> and identifier.

‎src/LibObjectFile/MachO/MachOFatFile.Verify.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ public void Verify(DiagnosticBag diagnostics)
8888
$"Slice {i} repeats the architecture {slice.CpuType} (subtype 0x{slice.CpuSubType:X}) of an earlier slice");
8989
}
9090

91+
if (slice.CpuType != slice.File.CpuType || slice.CpuSubType != slice.File.CpuSubType)
92+
{
93+
diagnostics.Error(
94+
DiagnosticId.MACHO_ERR_FatSliceArchitectureMismatch,
95+
$"Slice {i} is recorded as {slice.CpuType} (subtype 0x{slice.CpuSubType:X}) but contains {slice.File.CpuType} (subtype 0x{slice.File.CpuSubType:X})");
96+
}
97+
9198
ranges.Add((slice.FileOffset, slice.FileOffset + slice.Size, i));
9299
slice.File.Verify(diagnostics);
93100
}

‎src/LibObjectFile/MachO/MachOFatFile.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public static bool TryRead(Stream stream, [NotNullWhen(true)] out MachOFatFile?
112112
diagnostics = bag;
113113

114114
var basePosition = stream.Position;
115+
var availableLength = (ulong)(stream.Length - basePosition);
115116
Span<byte> header = stackalloc byte[HeaderSize];
116117
stream.Position = basePosition;
117118
if (stream.Read(header) != HeaderSize)
@@ -188,9 +189,11 @@ public static bool TryRead(Stream stream, [NotNullWhen(true)] out MachOFatFile?
188189
return false;
189190
}
190191

191-
if (slice.FileOffset + slice.Size > (ulong)stream.Length)
192+
// Offsets are relative to the universal binary, not the underlying stream. Compare
193+
// by subtraction so FAT64 values cannot wrap and then be narrowed to invalid longs.
194+
if (slice.FileOffset > availableLength || slice.Size > availableLength - slice.FileOffset)
192195
{
193-
bag.Error(DiagnosticId.MACHO_ERR_InvalidFatArchRange, $"Slice {i} spans [0x{slice.FileOffset:X}, 0x{slice.FileOffset + slice.Size:X}) which extends past the end of the file");
196+
bag.Error(DiagnosticId.MACHO_ERR_InvalidFatArchRange, $"Slice {i} starts at 0x{slice.FileOffset:X} and spans 0x{slice.Size:X} bytes, which extends past the end of the file");
194197
file = null;
195198
return false;
196199
}

‎src/LibObjectFile/MachO/MachOFile.Read.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,9 @@ void Add(ulong offset, ulong size, MachOSection? section = null)
372372
// An offset of zero means the data is absent rather than at the start of the file.
373373
if (offset < commandsEnd || size == 0) return;
374374

375-
if (offset + size > fileLength)
375+
// Compare by subtraction so an untrusted 64-bit size cannot wrap the end back into
376+
// the file and reach the stream reader as a very large allocation.
377+
if (offset > fileLength || size > fileLength - offset)
376378
{
377379
reader.Diagnostics.Error(
378380
DiagnosticId.MACHO_ERR_InvalidContentFileRange,

‎src/LibObjectFile/MachO/MachOFile.Sign.cs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ public void AdHocSign(string identifier)
9494
throw new InvalidOperationException($"The image is 0x{contentEnd:X} bytes, too large for the 32-bit offset LC_CODE_SIGNATURE records.");
9595
}
9696

97-
var signatureOffset = AlignHelper.AlignUp((uint)contentEnd, (uint)MachOCodeSignatureConstants.SignatureAlignment);
97+
var alignedSignatureOffset = AlignHelper.AlignUp(contentEnd, (ulong)MachOCodeSignatureConstants.SignatureAlignment);
98+
if (alignedSignatureOffset > uint.MaxValue)
99+
{
100+
throw new InvalidOperationException($"The image ends at 0x{contentEnd:X}, which cannot be aligned for the 32-bit offset LC_CODE_SIGNATURE records.");
101+
}
102+
var signatureOffset = (uint)alignedSignatureOffset;
98103

99104
var builder = new MachOAdHocSignatureBuilder(identifier)
100105
{
@@ -108,7 +113,7 @@ public void AdHocSign(string identifier)
108113
command.DataOffset = signatureOffset;
109114
command.DataSize = signatureSize;
110115

111-
linkEdit.FileSize = signatureOffset + signatureSize - linkEdit.FileOffset;
116+
linkEdit.FileSize = (ulong)signatureOffset + signatureSize - linkEdit.FileOffset;
112117

113118
// The signature is far larger than the slack the linker left, so __LINKEDIT grows and
114119
// has to be rounded to a segment boundary the way codesign rounds it. Without this it

0 commit comments

Comments
 (0)