Skip to content

Commit 7866dbf

Browse files
Add Mach-O __LINKEDIT layout computation
The writer could only put content back where it already was, so nothing in __LINKEDIT could change size. Adds recomputing where its tables sit, packing them in their current order, rewriting the offsets the load commands record and resizing the segment around them. This is the only part of an image whose layout can be computed. A section's address is its segment's address plus its distance from the segment's file offset, so moving a section moves its address and invalidates what refers to it. Nothing addresses __LINKEDIT that way, since the loader reaches its tables only through those file offsets. The spacing between tables is preserved rather than derived, because linkers disagree on it: lld aligns the string table to a pointer while ld64 packs it against what precedes it, so no single rule reproduces both. Each table keeps the gap that preceded it and offsets are recomputed around those gaps. The code signature is aligned on top of that, since 16 bytes is required of it rather than a matter of style. - src/LibObjectFile/MachO/MachOFile.Layout.cs: gathers the tables, converts their counts to sizes, and places them - src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs: asserts a recomputed layout gives back an untouched image unchanged
1 parent 9a2f402 commit 7866dbf

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,45 @@ public void ReadsAndWritesAUniversalBinary()
229229
ByteArrayAssert.AreEqual(original, output.ToArray(), "Invalid binary diff for helloworld_fat after read -> write");
230230
}
231231

232+
/// <summary>
233+
/// A computed layout has to reproduce an image nobody touched, byte for byte. If packing the
234+
/// __LINKEDIT tables in place gives back the original, the computation agrees with what the
235+
/// linker did, and only then is it safe to use on an image that has been changed.
236+
/// </summary>
237+
[TestMethod]
238+
[DataRow("helloworld_x86_64")]
239+
[DataRow("helloworld_arm64")]
240+
[DataRow("chainedfixups_arm64")]
241+
[DataRow("libhelloworld_x86_64.dylib")]
242+
[DataRow("unixthread_i386")]
243+
[DataRow("dyldinfo_i386")]
244+
public void ComputedLayoutReproducesAnUntouchedImage(string name)
245+
{
246+
var original = File.ReadAllBytes(GetFile(name));
247+
248+
var file = MachOFile.Read(new MemoryStream(original));
249+
file.UpdateLinkEditLayout();
250+
251+
ByteArrayAssert.AreEqual(original, WriteToArray(file), $"Recomputing the layout of {name} changed it");
252+
}
253+
254+
/// <summary>
255+
/// An object file keeps its tables past the end of its only segment, so there is no
256+
/// __LINKEDIT to lay out and the attempt has to leave the image alone rather than misplace
257+
/// tables it cannot see.
258+
/// </summary>
259+
[TestMethod]
260+
public void ComputingTheLayoutOfAnObjectFileDoesNothing()
261+
{
262+
var original = File.ReadAllBytes(GetFile("helloworld_x86_64.o"));
263+
264+
var file = MachOFile.Read(new MemoryStream(original));
265+
Assert.IsNull(file.FindSegment("__LINKEDIT"));
266+
file.UpdateLinkEditLayout();
267+
268+
ByteArrayAssert.AreEqual(original, WriteToArray(file), "Recomputing the layout of an object file changed it");
269+
}
270+
232271
[TestMethod]
233272
public void RejectsANonMachOStream()
234273
{
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// This file is licensed under the BSD-Clause 2 license.
3+
// See the license.txt file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Linq;
9+
using LibObjectFile.MachO.CodeSign;
10+
11+
namespace LibObjectFile.MachO;
12+
13+
partial class MachOFile
14+
{
15+
/// <summary>
16+
/// Recomputes where everything in <c>__LINKEDIT</c> sits, packing the tables in their current
17+
/// order and resizing the segment around them.
18+
/// </summary>
19+
/// <remarks>
20+
/// This is the only part of an image whose layout can be computed. A section's address is its
21+
/// segment's address plus its distance from the segment's file offset, so moving a section
22+
/// moves its address and invalidates everything referring to it. Nothing addresses
23+
/// <c>__LINKEDIT</c> that way: the loader reaches its tables only through the file offsets in
24+
/// the load commands, which is why they can be repacked and those offsets rewritten.
25+
/// <para>
26+
/// The spacing a linker left between tables is kept rather than derived, because linkers do
27+
/// not agree on it: lld aligns the string table to a pointer while ld64 packs it against
28+
/// whatever precedes it, so no single rule reproduces both. Each table keeps the gap that
29+
/// preceded it and the offsets are recomputed around those gaps, which reproduces an
30+
/// untouched image exactly and still lays out correctly once something has resized. The code
31+
/// signature is aligned on top of that, since 16-byte alignment is required of it rather
32+
/// than a matter of style.
33+
/// </para>
34+
/// </remarks>
35+
public void UpdateLinkEditLayout()
36+
{
37+
var linkEdit = FindSegment("__LINKEDIT");
38+
if (linkEdit?.Content is null) return;
39+
40+
var blobs = CollectLinkEditBlobs();
41+
if (blobs.Count == 0) return;
42+
43+
// Read the current bytes before any offset is rewritten.
44+
var contents = new byte[blobs.Count][];
45+
for (var i = 0; i < blobs.Count; i++)
46+
{
47+
contents[i] = ReadFileBytes(blobs[i].Offset, blobs[i].Size, "a __LINKEDIT table");
48+
}
49+
50+
var content = new MemoryStream();
51+
var cursor = (uint)linkEdit.FileOffset;
52+
53+
for (var i = 0; i < blobs.Count; i++)
54+
{
55+
var blob = blobs[i];
56+
57+
var target = cursor + blob.PrecedingGap;
58+
if (blob.Alignment > 1)
59+
{
60+
target = (target + blob.Alignment - 1) & ~(blob.Alignment - 1);
61+
}
62+
63+
content.Write(new byte[target - cursor]);
64+
cursor = target;
65+
66+
blob.SetOffset(cursor);
67+
content.Write(contents[i]);
68+
cursor += blob.Size;
69+
}
70+
71+
linkEdit.Content = content;
72+
linkEdit.ContentFileOffset = linkEdit.FileOffset;
73+
linkEdit.FileSize = cursor - linkEdit.FileOffset;
74+
linkEdit.VmSize = Math.Max(linkEdit.VmSize, linkEdit.FileSize);
75+
}
76+
77+
/// <summary>
78+
/// A table inside <c>__LINKEDIT</c>, paired with the way to record where it ends up.
79+
/// </summary>
80+
private readonly record struct LinkEditBlob(uint Offset, uint Size, uint Alignment, Action<uint> SetOffset)
81+
{
82+
/// <summary>
83+
/// Gets or sets the bytes that separated this table from the one before it. Preserving
84+
/// this is what lets an untouched image come back out unchanged whichever linker built it.
85+
/// </summary>
86+
public uint PrecedingGap { get; init; }
87+
}
88+
89+
/// <summary>
90+
/// Gathers the tables the load commands point at, in the order they currently sit in the
91+
/// file. Sizes are counts in most commands, so each is converted to bytes here.
92+
/// </summary>
93+
private List<LinkEditBlob> CollectLinkEditBlobs()
94+
{
95+
var blobs = new List<LinkEditBlob>();
96+
97+
foreach (var command in LoadCommands)
98+
{
99+
switch (command)
100+
{
101+
case MachOSymbolTableCommand symtab:
102+
Add(symtab.SymbolOffset, symtab.SymbolCount * MachOSymbolTableCommand.GetSymbolSize(Is64Bit), 1, o => symtab.SymbolOffset = o);
103+
Add(symtab.StringOffset, symtab.StringSize, 1, o => symtab.StringOffset = o);
104+
break;
105+
106+
case MachODynamicSymbolTableCommand dysymtab:
107+
Add(dysymtab.TableOfContentsOffset, dysymtab.TableOfContentsCount * 8, 1, o => dysymtab.TableOfContentsOffset = o);
108+
Add(dysymtab.ModuleTableOffset, dysymtab.ModuleTableCount * (Is64Bit ? 56u : 52u), 1, o => dysymtab.ModuleTableOffset = o);
109+
Add(dysymtab.ExternalReferenceOffset, dysymtab.ExternalReferenceCount * 4, 1, o => dysymtab.ExternalReferenceOffset = o);
110+
Add(dysymtab.IndirectSymbolOffset, dysymtab.IndirectSymbolCount * 4, 1, o => dysymtab.IndirectSymbolOffset = o);
111+
Add(dysymtab.ExternalRelocationOffset, dysymtab.ExternalRelocationCount * 8, 1, o => dysymtab.ExternalRelocationOffset = o);
112+
Add(dysymtab.LocalRelocationOffset, dysymtab.LocalRelocationCount * 8, 1, o => dysymtab.LocalRelocationOffset = o);
113+
break;
114+
115+
case MachODyldInfoCommand dyldInfo:
116+
Add(dyldInfo.RebaseOffset, dyldInfo.RebaseSize, 1, o => dyldInfo.RebaseOffset = o);
117+
Add(dyldInfo.BindOffset, dyldInfo.BindSize, 1, o => dyldInfo.BindOffset = o);
118+
Add(dyldInfo.WeakBindOffset, dyldInfo.WeakBindSize, 1, o => dyldInfo.WeakBindOffset = o);
119+
Add(dyldInfo.LazyBindOffset, dyldInfo.LazyBindSize, 1, o => dyldInfo.LazyBindOffset = o);
120+
Add(dyldInfo.ExportOffset, dyldInfo.ExportSize, 1, o => dyldInfo.ExportOffset = o);
121+
break;
122+
123+
case MachOLinkEditDataCommand data:
124+
// The signature is located as a blob starting on a 16-byte boundary.
125+
var alignment = data.Type == MachOLoadCommandType.CodeSignature ? (uint)MachOCodeSignatureConstants.SignatureAlignment : 1u;
126+
Add(data.DataOffset, data.DataSize, alignment, o => data.DataOffset = o);
127+
break;
128+
129+
case MachOTwoLevelHintsCommand hints:
130+
Add(hints.Offset, hints.HintCount * 4, 1, o => hints.Offset = o);
131+
break;
132+
}
133+
}
134+
135+
blobs.Sort((left, right) => left.Offset.CompareTo(right.Offset));
136+
137+
// Record what separated each table from the one before it, now that they are in order.
138+
var previousEnd = (uint)(FindSegment("__LINKEDIT")?.FileOffset ?? 0);
139+
for (var i = 0; i < blobs.Count; i++)
140+
{
141+
blobs[i] = blobs[i] with { PrecedingGap = blobs[i].Offset > previousEnd ? blobs[i].Offset - previousEnd : 0 };
142+
previousEnd = blobs[i].Offset + blobs[i].Size;
143+
}
144+
145+
return blobs;
146+
147+
void Add(uint offset, uint size, uint alignment, Action<uint> setOffset)
148+
{
149+
// An offset of zero means the table is absent rather than at the start of the file.
150+
if (offset == 0 || size == 0) return;
151+
blobs.Add(new LinkEditBlob(offset, size, alignment, setOffset));
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)