commit fca3f6f62e515669cb3aa22b8512e2cc4e1994f2 (tree)
parent 6ed9c0521055add99097fd43598d5a8e1e3665ff
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Fri, 27 Mar 2026 14:01:24 +0000
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.
Diffstat:
1 file changed, 24 insertions(+), 5 deletions(-)
diff --git a/src/link/MachO/Object.zig b/src/link/MachO/Object.zig
@@ -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;
+ }
}
}