commit 7cb63a5b610c688fbf2705c7ad21ce4cabd1de41
parent f80b5dd6d6c855d8baa47ce108d2e859564354fc
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 4 Jun 2026 16:39:18 -0700
link: emit FreeBSD PIE metadata for rtld
Diffstat:
2 files changed, 106 insertions(+), 37 deletions(-)
diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c
@@ -732,6 +732,47 @@ static void write_sym_rec(Writer* w, const SymRec* r, int class32) {
write_bytes(w, buf, sizeof buf);
}
+static void refresh_dynsym_exports(LinkImage* img, u64 img_base) {
+ LinkDynState* dyn;
+ const LinkSection* sec_dynsym;
+ const LinkSegment* seg;
+ u8* bytes;
+ u32 i;
+ if (!img || !img->dyn) return;
+ dyn = img->dyn;
+ if (!dyn->sym_dynidx || dyn->sec_dynsym == LINK_SEC_NONE) return;
+ sec_dynsym = &img->sections[dyn->sec_dynsym - 1];
+ seg = &img->segments[sec_dynsym->segment_id - 1];
+ bytes = img->segment_bytes[seg->id - 1] +
+ (size_t)(sec_dynsym->file_offset - seg->file_offset);
+
+ for (i = 0; i < LinkSyms_count(&img->syms); ++i) {
+ const LinkSymbol* s = LinkSyms_at(&img->syms, i);
+ u32 dynidx;
+ DynSymRec* r;
+ if (!s->defined || s->imported) continue;
+ if (s->id >= dyn->sym_dynidx_size) continue;
+ dynidx = dyn->sym_dynidx[s->id];
+ if (dynidx == 0 || dynidx >= dyn->ndynsym) continue;
+
+ r = &dyn->dynsym[dynidx];
+ r->st_value = (s->kind == SK_ABS) ? s->vaddr : img_base + s->vaddr;
+ r->st_size = s->size;
+ r->st_shndx = (s->kind == SK_ABS) ? SHN_ABS : 1u;
+ }
+
+ for (i = 0; i < dyn->ndynsym; ++i) {
+ u8* p = bytes + (u64)i * ELF64_SYM_SIZE;
+ const DynSymRec* r = &dyn->dynsym[i];
+ wr_u32_le(p + 0, r->st_name);
+ p[4] = r->st_info;
+ p[5] = r->st_other;
+ wr_u16_le(p + 6, r->st_shndx);
+ wr_u64_le(p + 8, r->st_value);
+ wr_u64_le(p + 16, r->st_size);
+ }
+}
+
/* ---- section header layout ---- *
*
* Per-segment cuts: each kept image segment contributes 1 .text/.rodata
@@ -937,6 +978,8 @@ void link_emit_elf(LinkImage* img, Writer* w) {
u8* dyn_bytes_at = img->segment_bytes[dseg->id - 1] +
(size_t)(sec_dynamic->file_offset - dseg->file_offset);
+ refresh_dynsym_exports(img, img_base);
+
/* Build DT_* entries in order. Layout matches count_dynamic_entries. */
u32 written = 0;
u8* p = dyn_bytes_at;
@@ -1021,9 +1064,8 @@ void link_emit_elf(LinkImage* img, Writer* w) {
/* Re-serialize .rela.dyn body. GLOB_DAT records (imports against
* .got slots) and RELATIVE records (PIE internal abs64 fixups)
- * are both populated during apply_all_relocs; .rela.dyn was empty
- * coming out of layout_dyn. Trailing capacity stays zero —
- * readers stop at the first R_AARCH64_NONE record. */
+ * are both populated during apply_all_relocs; layout_dyn pre-counts
+ * the exact number of runtime relocation records. */
{
const LinkSegment* rdseg = &img->segments[sec_reladyn->segment_id - 1];
u8* rd_bytes = img->segment_bytes[rdseg->id - 1] +
diff --git a/src/obj/elf/link_dyn.c b/src/obj/elf/link_dyn.c
@@ -76,7 +76,8 @@ static u32 dyn_alloc_sections(LinkImage* img, u32 nsec) {
LinkSection* nsections = (LinkSection*)h->realloc(
h, img->sections, sizeof(*img->sections) * img->nsections,
sizeof(*img->sections) * new_nsec, _Alignof(LinkSection));
- if (!nsections) compiler_panic(img->c, SRCLOC_NONE, "link: oom on dyn sections");
+ if (!nsections)
+ compiler_panic(img->c, SRCLOC_NONE, "link: oom on dyn sections");
img->sections = nsections;
return base;
}
@@ -157,10 +158,12 @@ static u32 gnu_hash_name(const char* s, u32 n) {
* (PLT-bound), then data (GOT-bound via GLOB_DAT). */
typedef struct ImportLists {
+ LinkSymId* exports;
LinkSymId* funcs;
u32 nfuncs;
LinkSymId* datas;
u32 ndatas;
+ u32 nexports;
} ImportLists;
static int sym_is_func_import(const LinkSymbol* s) {
@@ -198,17 +201,26 @@ static int dso_export_is_func(Linker* l, const LinkSymbol* s) {
static void collect_imports(Linker* l, LinkImage* img, Heap* h,
ImportLists* il) {
u32 i;
- u32 cap_f = 0, cap_d = 0;
+ u32 cap_e = 0, cap_f = 0, cap_d = 0;
+ il->exports = NULL;
il->funcs = NULL;
il->datas = NULL;
- il->nfuncs = il->ndatas = 0;
+ il->nexports = il->nfuncs = il->ndatas = 0;
for (i = 0; i < LinkSyms_count(&img->syms); ++i) {
LinkSymbol* s = LinkSyms_at(&img->syms, i);
- if (!s->imported) continue;
if (s->name == 0) continue;
/* Only the canonical (img->globals) entry per name. */
LinkSymId canonical = symhash_get(&img->globals, s->name);
if (canonical != LINK_SYM_NONE && canonical != s->id) continue;
+ if (s->defined && !s->imported &&
+ (s->bind == SB_GLOBAL || s->bind == SB_WEAK) && s->kind != SK_FILE &&
+ s->kind != SK_SECTION) {
+ if (VEC_GROW(h, il->exports, cap_e, il->nexports + 1u))
+ compiler_panic(img->c, SRCLOC_NONE, "link: oom on exports");
+ il->exports[il->nexports++] = s->id;
+ continue;
+ }
+ if (!s->imported) continue;
int is_func = sym_is_func_import(s) || dso_export_is_func(l, s);
if (is_func) {
if (VEC_GROW(h, il->funcs, cap_f, il->nfuncs + 1u))
@@ -223,6 +235,7 @@ static void collect_imports(Linker* l, LinkImage* img, Heap* h,
}
static void free_imports(Heap* h, ImportLists* il) {
+ if (il->exports) h->free(h, il->exports, sizeof(*il->exports) * il->nexports);
if (il->funcs) h->free(h, il->funcs, sizeof(*il->funcs) * il->nfuncs);
if (il->datas) h->free(h, il->datas, sizeof(*il->datas) * il->ndatas);
}
@@ -278,21 +291,21 @@ static void collect_needed(Linker* l, LinkImage* img, LinkDynState* dyn) {
* Slot 0: STN_UNDEF (zero entry). The loader ignores names with index
* 0; we still emit a dynstr entry at offset 0 (the leading NUL).
*
- * Slots 1..nimports: imported symbols (functions first, then data).
+ * Slots 1..nexports: executable-defined globals exported for DSO lookup.
+ * Slots after exports: imported symbols (functions first, then data).
* st_shndx = SHN_UNDEF; the loader fills in the value at bind time.
* st_value/size are zero — the static linker has no value for an
* imported symbol.
*
- * No `--export-dynamic` plumbing in Phase 4: only imports + the null
- * slot land in .dynsym. Adding exports is mechanical (walk
- * img->globals, append entries with st_shndx = matching .text/.data
- * section index) but isn't on the test/musl path. */
+ * Defined executable globals must be present too: ELF DSOs can resolve
+ * references back to the main executable, and FreeBSD libc depends on that
+ * for Scrt1.o's `environ` and `__progname` definitions. */
static void build_dynsym(LinkImage* img, LinkDynState* dyn,
const ImportLists* il, ByteBuf* dynstr) {
Heap* h = img->heap;
u32 nimports = il->nfuncs + il->ndatas;
- u32 ndynsym = 1u + nimports; /* +1 for null slot */
+ u32 ndynsym = 1u + il->nexports + nimports; /* +1 for null slot */
u32 i;
dyn->ndynsym = ndynsym;
@@ -326,12 +339,33 @@ static void build_dynsym(LinkImage* img, LinkDynState* dyn,
memset(dyn->sym_plt_vaddr, 0,
sizeof(*dyn->sym_plt_vaddr) * dyn->sym_dynidx_size);
- /* All imports have STB_GLOBAL so first_global is right after the
- * single STN_UNDEF slot. (When local exports land via
- * --export-dynamic, this needs to grow.) */
+ /* All dynamic entries we emit today are non-local, so first_global is
+ * right after the single STN_UNDEF slot. */
dyn->first_global = 1u;
u32 idx = 1u;
+ for (i = 0; i < il->nexports; ++i) {
+ LinkSymId lsid = il->exports[i];
+ LinkSymbol* s = LinkSyms_at(&img->syms, lsid - 1);
+ DynSymRec* r = &dyn->dynsym[idx];
+ Slice nm_s = pool_slice(img->c->global, s->name);
+ const char* nm = nm_s.s;
+ size_t namelen = nm_s.len;
+ u8 elf_type = elf_st_type(s->kind);
+ u8 elf_bind = elf_st_bind(s->bind);
+ r->st_name = bb_append_str(dynstr, nm, (u32)namelen);
+ r->st_info = ELF64_ST_INFO(elf_bind, elf_type);
+ r->st_other = STV_DEFAULT;
+ /* The emitter refreshes defined-symbol values after the final header
+ * shift. Any nonzero, non-special section index is enough for rtld to
+ * treat the symbol as defined; section headers are not part of runtime
+ * loading. */
+ r->st_shndx = 1;
+ r->st_value = s->vaddr;
+ r->st_size = s->size;
+ dyn->sym_dynidx[lsid] = idx;
+ ++idx;
+ }
for (i = 0; i < il->nfuncs; ++i) {
LinkSymId lsid = il->funcs[i];
LinkSymbol* s = LinkSyms_at(&img->syms, lsid - 1);
@@ -376,21 +410,18 @@ static void build_dynsym(LinkImage* img, LinkDynState* dyn,
* Hashed range is [first_global, ndynsym) — slot 0 (STN_UNDEF) is
* unhashed. Layout matches loader expectations (musl, glibc, FreeBSD).
*
- * Bucket count: max(1, hashed_count / 2), rounded up to odd so the
- * mod operation distributes more uniformly. Bloom is 1 word for
- * Phase 4 — a real implementation would scale with hashed_count, but
- * 1 word with shift=6 still satisfies the loader's correctness check
- * (any bit set is "maybe present"; false-positives only cost a chain
- * scan). */
+ * Bucket count: one. That keeps the required chain ordering trivial even as
+ * we mix executable exports and imports without sorting the dynsym table by
+ * hash bucket. Bloom is 1 word for Phase 4 — a real implementation would
+ * scale with hashed_count, but 1 word with shift=6 still satisfies the
+ * loader's correctness check (false positives only cost a chain scan). */
static void build_gnu_hash(Heap* h, LinkImage* img, LinkDynState* dyn,
const ByteBuf* dynstr) {
u32 hashed = (dyn->ndynsym > dyn->first_global)
? (dyn->ndynsym - dyn->first_global)
: 0u;
- u32 nbuckets = hashed ? hashed : 1u;
- /* Round nbuckets up to next odd number. */
- if ((nbuckets & 1u) == 0u) nbuckets += 1u;
+ u32 nbuckets = 1u;
u32 bloom_size = 1u; /* 64-bit word */
u32 bloom_shift = 6u;
u32 sym_offset = dyn->first_global;
@@ -453,11 +484,6 @@ static void build_gnu_hash(Heap* h, LinkImage* img, LinkDynState* dyn,
if (last) v |= 1u;
chains[i] = v;
}
- /* Fix bucket→first-sym indices: if multiple syms share a bucket
- * but were inserted out of contiguous order, we need them
- * contiguous. We assumed contiguity above without enforcing it.
- * For Phase 4 with small hashed sets this is fine, but flag the
- * shortcut. */
h->free(h, hashes, sizeof(u32) * hashed);
}
@@ -577,9 +603,7 @@ void layout_dyn(Linker* l, LinkImage* img) {
* .dynsym: 24 * ndynsym
* .dynstr: dynstr_len
* .gnu.hash: gnu_hash_len
- * .rela.dyn: 24 * (ndatas + cap_relative) — we reserve 4096 entries
- * for RELATIVE; emit fills them. (Quick-and-dirty: the
- * static path never has so many internal absolute relocs.)
+ * .rela.dyn: 24 * (runtime GLOB_DAT + RELATIVE records)
* .rela.plt: 24 * nfuncs
* .plt: 32 + 16 * nfuncs (PLT0 + per-slot)
* .got.plt: 8 * (3 + nfuncs)
@@ -606,7 +630,13 @@ void layout_dyn(Linker* l, LinkImage* img) {
for (ri = 0; ri < LinkRelocs_count(&img->relocs); ++ri) {
const LinkRelocApply* r = LinkRelocs_at(&img->relocs, ri);
const LinkSymbol* tgt = LinkSyms_at(&img->syms, r->target - 1);
+ const LinkSection* sec;
if (r->kind != R_ABS32 && r->kind != R_ABS64) continue;
+ if (r->link_section_id == LINK_SEC_NONE ||
+ r->link_section_id > img->nsections)
+ continue;
+ sec = &img->sections[r->link_section_id - 1];
+ if (sec->segment_id == LINK_SEG_NONE || sec->file_only) continue;
if (tgt->imported) {
cap_rel++; /* GLOB_DAT */
} else if (tgt->defined && tgt->kind != SK_ABS) {
@@ -631,10 +661,7 @@ void layout_dyn(Linker* l, LinkImage* img) {
u64 dynsym_bytes = (u64)dyn->ndynsym * ELF64_SYM_SIZE;
u64 dynstr_bytes = (u64)dyn->dynstr_len;
u64 gnuhash_bytes = (u64)dyn->gnu_hash_len;
- /* rela.dyn / rela.plt sized for full capacity; emit only writes
- * what's populated, but the section's file_size matches capacity
- * so PT_LOAD/.rela.dyn shdr sh_size add up. Trailing zero records
- * are harmless to the loader (R_AARCH64_NONE). */
+ /* rela.dyn is pre-counted exactly; rela.plt is one record per PLT slot. */
u64 rela_dyn_bytes = (u64)dyn->cap_rela_dyn * ELF64_RELA_SIZE;
u64 rela_plt_bytes = (u64)dyn->nrela_plt * ELF64_RELA_SIZE;
u64 plt_bytes =