commit 13677b4af13ca7c0336624e9f49d44c792de6201
parent 907a6891029bb98c2264ba18f327e045e4e68acd
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 18:39:40 -0700
fix(macho): index x86_64 chained-fixups seg table by emitted LC order
The dyld_chained_starts_in_image seg_count / seg_info_offset[] array was
indexed by MCtx segment *slot* (PAGEZERO, TEXT, DATA_CONST, DATA, DWARF,
LINKEDIT = 6), but empty middle segments (__DATA_CONST / __DWARF with no
sections) are dropped from the emitted load commands. So a __DATA fixup
filed under slot 3 with seg_count 6 lands on the wrong segment once the
load-command order compacts (__DATA becomes emitted index 2, LINKEDIT
index 3). arm64 dyld tolerates this via the struct's segment_offset, but
x86_64's applyFixupsGeneric keys on the actual segment at the emitted
index -> a global-pointer rebase walked into __LINKEDIT and SIGBUS'd at
dyld load.
Add macho_emit_seg_indices() (mirrors the emit_load_command_segment skip
logic) and index seg_count + seg_info_offset[] by emitted LC order. Fixes
toy 15_cg_api_types_bytes_globals on x86_64-macos; arm64 unchanged.
Diffstat:
1 file changed, 36 insertions(+), 4 deletions(-)
diff --git a/src/obj/macho/link.c b/src/obj/macho/link.c
@@ -1555,6 +1555,28 @@ static int site_cmp_by_vaddr(const void* a, const void* b) {
return 0;
}
+/* Map each MCtx segment slot to its index in the emitted LC_SEGMENT_64
+ * sequence and return the emitted-segment count. Empty middle segments
+ * (__DATA_CONST / __DWARF with no sections) are omitted from the load
+ * commands (see emit_load_command_segment calls), so the emitted order is
+ * a compaction of the slot order: PAGEZERO, TEXT, [DATA_CONST], [DATA],
+ * [DWARF], LINKEDIT. Must mirror that emit logic exactly. dyld indexes
+ * dyld_chained_starts_in_image.seg_info_offset[] by this emitted order, so
+ * the chained-fixups seg table uses it rather than the raw slot index —
+ * indexing by slot leaves a non-emitted hole that x86_64 dyld's
+ * applyFixupsGeneric resolves to the wrong segment (SIGBUS at load).
+ * Slots that are not emitted get emit_idx[slot] = 0xff. */
+static u32 macho_emit_seg_indices(const MCtx* x, u8 emit_idx[MSEG_COUNT]) {
+ u32 n = 0;
+ for (u32 i = 0; i < x->nsegs; ++i) {
+ int emitted = (i == MSEG_PAGEZERO) || (i == MSEG_LINKEDIT) ||
+ (x->segs[i].nsects > 0);
+ emit_idx[i] = emitted ? (u8)n : 0xffu;
+ if (emitted) ++n;
+ }
+ return n;
+}
+
/* tiny insertion sort to avoid pulling qsort */
static void sort_sites(FixSite* a, u32 n) {
for (u32 i = 1; i < n; ++i) {
@@ -1627,10 +1649,15 @@ static void build_chained_fixups(MCtx* x, FixList* fl) {
*/
u32 starts_off = out->len;
wr_u32_le(out->data + starts_offset_pos, starts_off);
- objbb_u32(out, x->nsegs);
+ /* seg_count + seg_info_offset[] are indexed by emitted LC_SEGMENT_64
+ * order, not MCtx slot — empty middle segments are dropped from the load
+ * commands. */
+ u8 emit_seg[MSEG_COUNT];
+ u32 emit_nsegs = macho_emit_seg_indices(x, emit_seg);
+ objbb_u32(out, emit_nsegs);
/* Reserve seg_info_offset[]. */
u32 seg_info_offsets_pos = out->len;
- for (u32 i = 0; i < x->nsegs; ++i) objbb_u32(out, 0);
+ for (u32 i = 0; i < emit_nsegs; ++i) objbb_u32(out, 0);
/* Sort fixsites by vaddr globally. */
sort_sites(fl->a, fl->n);
@@ -1646,11 +1673,16 @@ static void build_chained_fixups(MCtx* x, FixList* fl) {
}
}
if (!count) continue;
+ if (emit_seg[si] == 0xffu)
+ compiler_panic(x->c, SRCLOC_NONE,
+ "link_macho: chained fixups in non-emitted segment %u",
+ (unsigned)si);
/* Page-align this struct to 4. */
objbb_align(out, 4);
u32 sis_off = out->len;
- /* Patch seg_info_offset[si] to (sis_off - starts_off). */
- wr_u32_le(out->data + seg_info_offsets_pos + si * 4u, sis_off - starts_off);
+ /* Patch seg_info_offset[emitted index] to (sis_off - starts_off). */
+ wr_u32_le(out->data + seg_info_offsets_pos + emit_seg[si] * 4u,
+ sis_off - starts_off);
/* Compute page count for this segment. */
u64 seg_va = x->segs[si].vmaddr;