MachO: don't split subsections on N_ALT_ENTRY symbols

MachO has a mechanism where symbols can introduce "subsections", which
(as I understand it) allows a linker to garbage-collect parts of
sections without pulling in the heavy machinery of `-fdata-sections` and
`-ffunction-sections`. Essentially, symbols can be considered to
partition a section, and these boundaries are not allowed to be crossed
by memory accesses, so the linker can detect symbols which are unused
and drop the corresponding input section regions.

However, the symbol flag `N_ALT_ENTRY` indicates that a symbol should
not participate in this "splitting", and is instead an "alternate entry
point" to the previous subsection, which should continue through this
symbol.

The Mach-O linker was failing to ignore `N_ALT_ENTRY` symbols when
creating subsections, which meant that for certain link inputs, it would
create additional subsection splits, and then garbage collect the extra
sections (due to the `N_ALT_ENTRY` symbol being unused). Naturally, this
silent dropping of parts of input sections led to miscompilations.
This commit is contained in:
Matthew Lugg
2026-03-27 14:01:24 +00:00
parent 6ed9c05210
commit fca3f6f62e
+24 -5
View File
@@ -328,7 +328,9 @@ fn initSubsections(self: *Object, allocator: Allocator, nlists: anytype) !void {
if (isPtrLiteral(sect)) continue;
const nlist_start = for (nlists, 0..) |nlist, i| {
if (nlist.nlist.n_sect - 1 == n_sect) break i;
// We must ignore `alt_entry` (N_ALT_ENTRY) symbols here, because that flag indicates
// that a symbol should *not* split subsections.
if (nlist.nlist.n_sect - 1 == n_sect and !nlist.nlist.n_desc.alt_entry) break i;
} else nlists.len;
const nlist_end = for (nlists[nlist_start..], nlist_start..) |nlist, i| {
if (nlist.nlist.n_sect - 1 != n_sect) break i;
@@ -359,9 +361,24 @@ fn initSubsections(self: *Object, allocator: Allocator, nlists: anytype) !void {
const alias_start = idx;
const nlist = nlists[alias_start];
while (idx < nlist_end and
nlists[idx].nlist.n_value == nlist.nlist.n_value) : (idx += 1)
{}
// Skip past any symbols which shouldn't terminate this subsection.
while (true) {
idx += 1;
if (idx == nlist_end) {
// This subsection contains the full remainder of the section.
break;
}
if (nlists[idx].nlist.n_value == nlist.nlist.n_value) {
// Multiple symbols at the same address---don't create zero-length subsections.
continue;
}
if (nlists[idx].nlist.n_desc.alt_entry) {
// N_ALT_ENTRY indicates that this symbol does not split subsections, and is
// instead an "alternate entry point" into an existing subsection.
continue;
}
break;
}
const size = if (idx < nlist_end)
nlists[idx].nlist.n_value - nlist.nlist.n_value
@@ -385,7 +402,9 @@ fn initSubsections(self: *Object, allocator: Allocator, nlists: anytype) !void {
});
for (alias_start..idx) |i| {
self.symtab.items(.size)[nlists[i].idx] = size;
if (!nlists[i].nlist.n_desc.alt_entry) {
self.symtab.items(.size)[nlists[i].idx] = size;
}
}
}