commit c04e8d39488b1599fc07d0ca7633029866ef5fce
parent 97b868ff24f7d52083af2a924890ed48b14a5f6d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 11:02:06 -0700
link/elf: fix PIE relocations for code-address data
Two bugs broke PIE binaries that load a code address from data and branch
to it (jump tables, @labeladdr arrays, function pointers). Both manifest
as a SIGSEGV under a real loader (qemu-aarch64 / arm64), while the
in-process JIT path masked them.
1. RELATIVE records dropped the reloc addend: emit_relative_record was
passed tgt->vaddr, ignoring r->addend. RELA RELATIVE ignores the
in-place slot and writes load_base + r_addend, so every entry of an
addend-bearing table (a 16-way jump table targets `pick+0x88`,
`+0x94`, ...) collapsed onto the symbol base. Pass tgt->vaddr +
r->addend.
2. Reloc-bearing read-only data sat in a never-writable PT_LOAD: const
data (.rodata) carrying abs32/abs64 relocs becomes a RELATIVE/GLOB_DAT
dynamic record the loader must *write* into the slot, which faults a
PF_R page. Promote such sections to the writable segment in PIE mode
(the .data.rel.ro convention, minus PT_GNU_RELRO re-protection).
Fixes toy X-path aa64 cases: 119_static_labeladdr_data,
119_switch_strategy_hints, 123_spec_demo, 127_switch_forced_jump_table,
136_call_builtin_attrs.
Diffstat:
2 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -70,6 +70,27 @@ SegBucket link_bucket_for(u16 flags) {
return SEG_R;
}
+/* PIE `.data.rel.ro` placement: a read-only data section that carries an
+ * absolute (abs32/abs64) reloc cannot stay in a never-writable PT_LOAD.
+ * In PIE the linker rewrites those relocs into dynamic records — a
+ * RELATIVE for an internal target, a GLOB_DAT for an import — and the
+ * loader *writes* the resolved pointer into the slot at load time. A
+ * PF_R-only segment faults that store (manifesting as a SIGSEGV in the
+ * dynamic loader). Jump tables, @labeladdr arrays, and const pointer
+ * initializers all land here. Promote such sections to the writable
+ * segment; we forgo the post-relocation RELRO re-protection that a full
+ * toolchain would apply via PT_GNU_RELRO. */
+static int link_pie_ro_section_needs_write(const ObjBuilder* ob,
+ ObjSecId sid) {
+ u32 i, total = obj_reloc_total(ob);
+ for (i = 0; i < total; ++i) {
+ const Reloc* r = obj_reloc_at(ob, i);
+ if (!r || r->removed || r->section_id != sid) continue;
+ if (r->kind == R_ABS64 || r->kind == R_ABS32) return 1;
+ }
+ return 0;
+}
+
/* ---- LinkImage growth helpers ----
*
* syms / relocs back onto SegVec — pointers stay stable across pushes,
@@ -263,6 +284,9 @@ void link_layout_sections(Linker* l, LinkImage* img, const GcLive* g) {
entries[e].size = has_atoms ? a->size : section_size_for_link(s);
entries[e].name = s->name;
entries[e].bucket = link_bucket_for(s->flags);
+ if (l->emit_pie && entries[e].bucket == SEG_R &&
+ link_pie_ro_section_needs_write(ob, j))
+ entries[e].bucket = SEG_RW;
entries[e].next = PLACE_NONE;
key = place_group_key(entries[e].name, entries[e].bucket);
diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c
@@ -457,7 +457,13 @@ static void apply_all_relocs(LinkImage* img, u64 img_base) {
* the site, and the RELATIVE record tells the loader to add
* load_base on top. */
if (pie && reloc_is_abs(r->kind) && tgt->defined && tgt->kind != SK_ABS) {
- emit_relative_record(img, r->write_vaddr, tgt->vaddr);
+ /* RELA RELATIVE ignores the in-place site value: the loader writes
+ * (load_base + r_addend) into the slot. So the addend must be the
+ * full image-relative target — symbol vaddr plus the reloc's own
+ * addend — not just the symbol vaddr. Dropping r->addend collapses
+ * every entry of an addend-bearing table (jump tables, labeladdr
+ * arrays, &sym+off initializers) onto the symbol base. */
+ emit_relative_record(img, r->write_vaddr, tgt->vaddr + (u64)r->addend);
}
link_reloc_apply(img->c, r->kind, P_bytes, S, r->addend, P);
}