kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Msrc/obj/elf/link.c | 48+++++++++++++++++++++++++++++++++++++++++++++---
Msrc/obj/elf/link_dyn.c | 95+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
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 =