kit

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

commit ad732e4b9733ddb81da3f861b595c7e1e6bd1388
parent 704d2a298814839a5fdc17620b078f5e12dc34e3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon,  8 Jun 2026 17:17:37 -0700

elf: emit symbol-version (Verneed/Versym) requirements; freebsd -rdynamic + bootstrap shipping

kit's linker emitted unversioned undefined references (`UND fstat`), so on
FreeBSD the runtime bound the hidden compat `fstat@FBSD_1.0` (pre-INO64
`struct stat`) instead of the default `fstat@@FBSD_1.5`, reading `st_size` at
the wrong offset. A kit-built stage2 then failed to read its own source files,
blocking the aarch64-freebsd self-build at the stage3 compile.

The ELF linker now reads each DSO's `.gnu.version_d`/`.gnu.version` and emits a
matching `.gnu.version_r` + `.gnu.version` + DT_VERSYM/VERNEED/VERNEEDNUM,
selecting each import's default (non-hidden) version. Gated on the DSO carrying
versions, so musl/static links stay byte-identical; glibc links now also carry
correct GLIBC_* requirements (verified: glibc/musl/freebsd binaries run; host
test-link/elf/smoke/cg-api/ar/debug/dwarf green). With this the aarch64-freebsd
-O0 chain reaches the byte-identical fixed point and the Toy corpus runs
1371/9/39 through the bootstrapped stage3.

- src/obj/obj.h: ObjImageSym.version (a DSO export's default-version name)
- src/obj/elf/elf.h: DT_VER* tags, Verdef/Verneed wire sizes, VERSYM_* flags
- src/obj/elf/read.c: read_elf_verdefs() + versym parse in read_elf_image AND
  read_elf_dso (the linker's DSO reader now builds an ObjImage to carry versions)
- src/obj/elf/link_dyn.c: build_versions() — per-import requirements, versym +
  verneed bytes (only .dynstr offsets/indices, final at layout time)
- src/obj/elf/link.c: emit DT_VER* + SHT_GNU_VERSYM/VERNEED section types
- driver/cmd/cc.c: accept -rdynamic/-export-dynamic (FreeBSD's HOST_ENV_LDFLAGS
  passes -rdynamic; kit's ELF linker already exports DSO-referenced symbols)
- scripts/freebsd_bootstrap.sh: ship working-tree content of all tracked files
  with COPYFILE_DISABLE=1 (drops macOS AppleDouble `._*` xattr sidecars that
  FreeBSD tar would extract as bogus `._*.c` sources); Toy hook uses bash and
  no longer aborts before Toy when a chain fails

The -O1 release chain is still blocked, before the fixed-point check, by a
pre-existing FreeBSD-target codegen bug (deferred const-data .Lkit_ro/.Lkit_jt
symbols emitted GLOBAL instead of LOCAL; LOCAL on linux/macos). Not addressed here.

Diffstat:
Mdriver/cmd/cc.c | 11+++++++++++
Mscripts/freebsd_bootstrap.sh | 49++++++++++++++++++++++++++++++++++---------------
Msrc/link/link_internal.h | 15+++++++++++++++
Msrc/obj/elf/elf.h | 26++++++++++++++++++++++++++
Msrc/obj/elf/link.c | 26+++++++++++++++++++++++++-
Msrc/obj/elf/link_dyn.c | 251++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/obj/elf/read.c | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/obj/macho/read.c | 1+
Msrc/obj/obj.h | 8++++++++
9 files changed, 495 insertions(+), 18 deletions(-)

diff --git a/driver/cmd/cc.c b/driver/cmd/cc.c @@ -1192,6 +1192,17 @@ static int cc_parse(int argc, char** argv, CcOptions* o) { o->pie = 0; continue; } + if (driver_streq(a, "-rdynamic") || driver_streq(a, "-export-dynamic")) { + /* GCC/clang: ask the linker to promote defined globals into .dynsym + * (--export-dynamic). FreeBSD's HOST_ENV_LDFLAGS passes -rdynamic so a + * dynamically linked exe re-exports symbols (e.g. `environ`) for libc.so + * and for runtime symbolization. kit's ELF linker already exports the + * defined symbols that the DSOs it links reference, which is what hosted + * libc startup needs, so accept the flag for toolchain compatibility + * rather than reject it. (The standalone `kit ld` carries the explicit + * -E/--export-dynamic option for the full all-globals promotion.) */ + continue; + } if (driver_streq(a, "-Bstatic")) { o->cur_link_mode = KIT_LM_STATIC; continue; diff --git a/scripts/freebsd_bootstrap.sh b/scripts/freebsd_bootstrap.sh @@ -55,15 +55,21 @@ if ! scripts/freebsd_vm.sh ssh "$ARCH" true 2>/dev/null; then vm_started=1 fi -# Send the source tree to the VM. We use a git archive to avoid shipping -# build/ artifacts -- the VM will build into /home/kit/work/build/. +# Send the source tree to the VM. Ship the working-tree content of every tracked +# file (not `git archive HEAD`, which only sees committed state) so an +# in-progress bootstrap test reflects uncommitted changes across the whole tree, +# not just Makefile/mk/. git ls-files keeps build/ and other untracked/ignored +# artifacts out -- the VM builds into /home/kit/work/build/. +# +# COPYFILE_DISABLE=1 stops macOS bsdtar from archiving each file's extended +# attributes (every checkout carries a `com.apple.provenance` xattr) as an +# AppleDouble `._<name>` pax sidecar. macOS tar merges those back on listing so +# they look absent here, but FreeBSD's tar extracts them as literal `._*.c` +# files, which the Makefile's `*.c` globs then try (and fail) to compile. printf 'freebsd_bootstrap: sending source tree to %s VM\n' "$ARCH" ssh_vm 'rm -rf /home/kit/work && mkdir -p /home/kit/work' -git -C "$ROOT" archive HEAD | ssh_vm 'tar -C /home/kit/work -xf -' - -# Also copy any uncommitted Makefile + mk/ changes so a work-in-progress -# bootstrap test sees the current state. -(cd "$ROOT" && tar cf - Makefile mk/ scripts/ | ssh_vm 'tar -C /home/kit/work -xf -') +git -C "$ROOT" ls-files -z | (cd "$ROOT" && COPYFILE_DISABLE=1 tar --null -T - -cf -) \ + | ssh_vm 'tar -C /home/kit/work -xf -' printf 'freebsd_bootstrap: running %s bootstrap on %s\n' "$TARGET" "$ARCH" @@ -74,16 +80,29 @@ echo "=== freebsd_bootstrap: \$(uname -m) FreeBSD / $TARGET ===" clang --version | head -1 # FreeBSD ships BSD make as 'make'; the kit Makefile requires GNU make (gmake). MAKE=\$(which gmake 2>/dev/null || which make) -\$MAKE $TARGET BUILD_DIR='$BUILD_DIR' CC=clang AR=ar MAKE="\$MAKE" +# Capture the bootstrap result without aborting the script: a chain that fails +# (e.g. the -O1 release link) should still let the Toy corpus run through the +# stage3 of any chain that did reach its fixed point. +boot_rc=0 +\$MAKE $TARGET BUILD_DIR='$BUILD_DIR' CC=clang AR=ar MAKE="\$MAKE" || boot_rc=\$? if [ "$TOY" = "1" ]; then - for m in debug release; do - s3="$BUILD_DIR/\$m/bootstrap/stage3/kit" - [ -x "\$s3" ] || continue - echo "=== Toy corpus through \$m stage3 ===" - KIT="\$(pwd)/\$s3" sh test/toy/run.sh || true - done + # test/toy/run.sh needs bash (it uses arrays); the FreeBSD base system ships + # only the POSIX /bin/sh, so a bash package must be installed (pkg install + # bash). Skip with a clear note rather than fail cryptically when it is not. + TOYSH=\$(which bash 2>/dev/null || true) + if [ -z "\$TOYSH" ]; then + echo "=== Toy corpus: SKIPPED (bash not installed; run 'pkg install bash') ===" + else + for m in debug release; do + s3="$BUILD_DIR/\$m/bootstrap/stage3/kit" + [ -x "\$s3" ] || continue + echo "=== Toy corpus through \$m stage3 ===" + KIT="\$(pwd)/\$s3" "\$TOYSH" test/toy/run.sh || true + done + fi fi -echo "=== freebsd_bootstrap: DONE $TARGET ===" +echo "=== freebsd_bootstrap: DONE $TARGET (bootstrap rc=\$boot_rc) ===" +exit \$boot_rc EOF # Bring back the stage3 binaries for inspection. diff --git a/src/link/link_internal.h b/src/link/link_internal.h @@ -444,6 +444,21 @@ typedef struct LinkDynState { u8* gnu_hash; u32 gnu_hash_len; + /* GNU symbol versioning. Emitted only when at least one imported symbol + * binds to a versioned DSO export (nverneed > 0); otherwise all three are + * zero and no version sections / DT_VER* entries are produced (musl/static + * links are unchanged). .gnu.version is one u16 per .dynsym entry; + * .gnu.version_r holds Verneed/Vernaux requirements keyed by DT_NEEDED + * soname. Both carry only .dynstr offsets + version indices (no vaddrs), so + * the bytes are final at layout time and copied verbatim during emit. */ + LinkSectionId sec_gnu_version; + u8* versym; /* ndynsym * 2 bytes */ + u32 versym_len; + LinkSectionId sec_gnu_version_r; + u8* verneed; /* nverneed Verneed records + their Vernaux */ + u32 verneed_len; + u32 nverneed; /* DT_VERNEEDNUM */ + /* .rela.dyn — R_AARCH64_GLOB_DAT (imports against .got slots) and * R_AARCH64_RELATIVE (PIE internal abs64 fixups, populated during * Phase 6 emit). Pre-sized at layout time; the RELATIVE tail is diff --git a/src/obj/elf/elf.h b/src/obj/elf/elf.h @@ -254,6 +254,10 @@ static inline u8 elf_st_other(u8 vis /* SymVis */) { #define DT_GNU_HASH 0x6ffffef5 #define DT_FLAGS_1 0x6ffffffb #define DF_1_NOW 0x00000001 +/* GNU symbol-versioning dynamic tags. */ +#define DT_VERSYM 0x6ffffff0 /* address of the .gnu.version table */ +#define DT_VERNEED 0x6ffffffe /* address of the .gnu.version_r table */ +#define DT_VERNEEDNUM 0x6fffffff /* number of .gnu.version_r entries */ /* ---- extra section types we need to recognize in DSO inputs ---- */ #define SHT_DYNAMIC 6 @@ -263,6 +267,28 @@ static inline u8 elf_st_other(u8 vis /* SymVis */) { #define SHT_GNU_VERNEED 0x6ffffffe #define SHT_GNU_VERDEF 0x6ffffffd +/* ---- GNU symbol-versioning wire layout (ELFCLASS64) ---- + * .gnu.version (SHT_GNU_VERSYM): u16 per .dynsym entry. Special indices: + * 0 = local (the null slot / unversioned undefined reference) + * 1 = global, base version (a defined symbol with no explicit version) + * >=2 = index of a version requirement (Vernaux.vna_other) for an + * undefined reference, or a version definition for a defined symbol. + * The 0x8000 bit (VERSYM_HIDDEN) marks a non-default version definition. */ +#define VER_NDX_LOCAL 0 +#define VER_NDX_GLOBAL 1 +#define VERSYM_HIDDEN 0x8000u +#define VERSYM_VERSION 0x7fffu +#define VER_FLG_BASE 0x1 /* Verdef: the file's base (soname) version entry */ + +/* Elf64_Verdef (20 bytes) + Elf64_Verdaux (8 bytes) — version definitions in a + * DSO's .gnu.version_d. Read-only on our side (we never emit a verdef). */ +#define ELF_VERDEF_SIZE 20 +#define ELF_VERDAUX_SIZE 8 +/* Elf64_Verneed (16 bytes) + Elf64_Vernaux (16 bytes) — version requirements + * in .gnu.version_r, which we both read (DSO inputs) and emit (executables). */ +#define ELF_VERNEED_SIZE 16 +#define ELF_VERNAUX_SIZE 16 + /* ---- AArch64 ELF wire-format relocation type codes ---- * Prefixed ELF_ to avoid collision with the kit-canonical RelocKind * enum values in obj.h (R_AARCH64_*). */ diff --git a/src/obj/elf/link.c b/src/obj/elf/link.c @@ -988,6 +988,14 @@ void link_emit_elf(LinkImage* img, Writer* w) { const LinkSection* sec_gotplt = (dyn->sec_got_plt != LINK_SEC_NONE) ? &img->sections[dyn->sec_got_plt - 1] : NULL; + const LinkSection* sec_versym = + (dyn->sec_gnu_version != LINK_SEC_NONE) + ? &img->sections[dyn->sec_gnu_version - 1] + : NULL; + const LinkSection* sec_verneed = + (dyn->sec_gnu_version_r != LINK_SEC_NONE) + ? &img->sections[dyn->sec_gnu_version_r - 1] + : NULL; const LinkSegment* dseg = &img->segments[sec_dynamic->segment_id - 1]; u8* dyn_bytes_at = img->segment_bytes[dseg->id - 1] + (size_t)(sec_dynamic->file_offset - dseg->file_offset); @@ -1049,6 +1057,12 @@ void link_emit_elf(LinkImage* img, Writer* w) { DT_PUT(DT_SYMTAB, img_base + sec_dynsym->vaddr); DT_PUT(DT_SYMENT, 24); DT_PUT(DT_GNU_HASH, img_base + sec_gnuhash->vaddr); + /* Symbol-version tables (only when an import bound a versioned export). */ + if (dyn->nverneed && sec_versym && sec_verneed) { + DT_PUT(DT_VERSYM, img_base + sec_versym->vaddr); + DT_PUT(DT_VERNEED, img_base + sec_verneed->vaddr); + DT_PUT(DT_VERNEEDNUM, dyn->nverneed); + } /* DT_PLT* / DT_JMPREL only make sense when there's a PLT. Emitting * them with size=0 / vaddr=0 (or pointing past the end of any * PT_LOAD) trips llvm-readelf's "address not in any segment" check @@ -1664,7 +1678,7 @@ void link_emit_elf(LinkImage* img, Writer* w) { * comparing OutShdr.name to the same Sym values. */ Sym n_dynsym = 0, n_dynstr = 0, n_gnuhash = 0; Sym n_reladyn = 0, n_relaplt = 0, n_dynamic = 0; - Sym n_gotplt = 0; + Sym n_gotplt = 0, n_gnuver = 0, n_gnuver_r = 0; if (pie && img->dyn) { n_dynsym = pool_intern_slice(c->global, SLICE_LIT(".dynsym")); n_dynstr = pool_intern_slice(c->global, SLICE_LIT(".dynstr")); @@ -1673,6 +1687,8 @@ void link_emit_elf(LinkImage* img, Writer* w) { n_relaplt = pool_intern_slice(c->global, SLICE_LIT(".rela.plt")); n_dynamic = pool_intern_slice(c->global, SLICE_LIT(".dynamic")); n_gotplt = pool_intern_slice(c->global, SLICE_LIT(".got.plt")); + n_gnuver = pool_intern_slice(c->global, SLICE_LIT(".gnu.version")); + n_gnuver_r = pool_intern_slice(c->global, SLICE_LIT(".gnu.version_r")); } /* Two-pass: first find dynsym/dynstr/gotplt indices for sh_link * fixups, then emit. */ @@ -1739,6 +1755,14 @@ void link_emit_elf(LinkImage* img, Writer* w) { sh.sh_type = SHT_DYNAMIC; sh.sh_link = idx_dynstr; sh.sh_entsize = 16; + } else if (o->name == n_gnuver) { + sh.sh_type = SHT_GNU_VERSYM; + sh.sh_link = idx_dynsym; + sh.sh_entsize = 2; + } else if (o->name == n_gnuver_r) { + sh.sh_type = SHT_GNU_VERNEED; + sh.sh_link = idx_dynstr; + sh.sh_info = img->dyn->nverneed; } else if (o->name == n_gotplt) { sh.sh_entsize = 8; } diff --git a/src/obj/elf/link_dyn.c b/src/obj/elf/link_dyn.c @@ -405,6 +405,213 @@ static void build_dynsym(LinkImage* img, LinkDynState* dyn, } } +/* ---- GNU symbol versioning (.gnu.version + .gnu.version_r) ---- + * + * For each imported symbol that binds to a versioned DSO export, require that + * export's *default* version (read into ObjImageSym.version at input time) so + * the runtime binds the right one. On FreeBSD this is mandatory: the INO64 + * transition left `stat`/`fstat`/... as two incompatible struct-stat ABIs, the + * compat behind a hidden FBSD_1.0 and the modern one as the default FBSD_1.5; + * an unversioned reference binds the compat and reads st_size at the wrong + * offset. We emit: + * .gnu.version — one u16 per .dynsym entry: 0 (null/unversioned import), + * 1 (defined export), or >=2 (a version requirement index). + * .gnu.version_r — Verneed per DT_NEEDED soname + Vernaux per required + * version, numbered 2.. in first-seen order. + * Both reference only .dynstr offsets and indices (no vaddrs), so the bytes are + * final at layout time. Nothing is emitted when no import is versioned, leaving + * musl/glibc-without-version and static links byte-for-byte unchanged. */ + +static u32 elf_sysv_hash(const char* s, u32 n) { + u32 h = 0, g, i; + for (i = 0; i < n; ++i) { + h = (h << 4) + (u8)s[i]; + g = h & 0xf0000000u; + if (g) h ^= g >> 24; + h &= ~g; + } + return h; +} + +/* Default version name the DSO `in` exports for `name`, or 0 if `in` carries no + * versioning / doesn't export `name` with a default version. */ +static Sym dso_default_version(LinkInput* in, Sym name) { + const ObjImage* im = in->obj ? obj_image(in->obj) : NULL; + u32 i, n; + if (!im) return 0; + n = obj_image_ndynsyms(im); + for (i = 0; i < n; ++i) { + const ObjImageSym* s = obj_image_dynsym(im, i); + if (s->name == name && s->version != 0) return s->version; + } + return 0; +} + +typedef struct VerReq { + Sym soname; + Sym version; + u16 index; +} VerReq; + +typedef struct VerBuild { + Heap* h; + Linker* l; + LinkImage* img; + LinkDynState* dyn; + u8* vs; /* versym bytes being filled */ + VerReq* reqs; + u32 nreq; + u32 capreq; +} VerBuild; + +/* Resolve one imported symbol's version requirement: look up its providing + * DSO's default version for the name, intern a (soname, version) requirement + * (assigning the next index), and stamp the symbol's versym slot. */ +static void ver_process_import(VerBuild* vb, LinkSymId lsid) { + LinkSymbol* s = LinkSyms_at(&vb->img->syms, lsid - 1); + u32 di = vb->dyn->sym_dynidx[lsid]; + LinkInput* in; + Sym ver; + u16 vidx = 0; + u32 r; + if (!di || s->dso_input_id == LINK_INPUT_NONE) return; + if (s->dso_input_id - 1u >= LinkInputs_count(&vb->l->inputs)) return; + in = LinkInputs_at(&vb->l->inputs, s->dso_input_id - 1u); + if (in->soname == 0) return; + ver = dso_default_version(in, s->name); + if (ver == 0) return; + for (r = 0; r < vb->nreq; ++r) + if (vb->reqs[r].soname == in->soname && vb->reqs[r].version == ver) { + vidx = vb->reqs[r].index; + break; + } + if (!vidx) { + if (VEC_GROW(vb->h, vb->reqs, vb->capreq, vb->nreq + 1u)) + compiler_panic(vb->img->c, SRCLOC_NONE, "link: oom on version reqs"); + vidx = (u16)(2u + vb->nreq); + vb->reqs[vb->nreq].soname = in->soname; + vb->reqs[vb->nreq].version = ver; + vb->reqs[vb->nreq].index = vidx; + vb->nreq++; + } + wr_u16_le(vb->vs + (u64)di * 2u, vidx); +} + +static void build_versions(Linker* l, LinkImage* img, LinkDynState* dyn, + const ImportLists* il, ByteBuf* dynstr) { + Heap* h = img->heap; + VerBuild vb; + u32 i; + + dyn->versym = NULL; + dyn->versym_len = 0; + dyn->verneed = NULL; + dyn->verneed_len = 0; + dyn->nverneed = 0; + if (dyn->ndynsym == 0) return; + + /* versym: default 0 (local/unversioned); defined exports -> GLOBAL. */ + vb.h = h; + vb.l = l; + vb.img = img; + vb.dyn = dyn; + vb.reqs = NULL; + vb.nreq = 0; + vb.capreq = 0; + vb.vs = (u8*)h->alloc(h, (size_t)dyn->ndynsym * 2u, 2); + if (!vb.vs) compiler_panic(img->c, SRCLOC_NONE, "link: oom on versym"); + memset(vb.vs, 0, (size_t)dyn->ndynsym * 2u); + for (i = 0; i < il->nexports; ++i) { + u32 di = dyn->sym_dynidx[il->exports[i]]; + if (di) wr_u16_le(vb.vs + (u64)di * 2u, (u16)VER_NDX_GLOBAL); + } + for (i = 0; i < il->nfuncs; ++i) ver_process_import(&vb, il->funcs[i]); + for (i = 0; i < il->ndatas; ++i) ver_process_import(&vb, il->datas[i]); + + if (vb.nreq == 0) { + /* No versioned imports: emit nothing, keep the link unchanged. */ + h->free(h, vb.vs, (size_t)dyn->ndynsym * 2u); + if (vb.reqs) h->free(h, vb.reqs, sizeof(*vb.reqs) * vb.capreq); + return; + } + dyn->versym = vb.vs; + dyn->versym_len = dyn->ndynsym * 2u; + + /* Group requirements by soname (first-seen order) into Verneed/Vernaux. */ + { + Sym* sonames = NULL; + u32 nson = 0, capson = 0; + u32 r; + for (r = 0; r < vb.nreq; ++r) { + u32 k; + int seen = 0; + for (k = 0; k < nson; ++k) + if (sonames[k] == vb.reqs[r].soname) { + seen = 1; + break; + } + if (!seen) { + if (VEC_GROW(h, sonames, capson, nson + 1u)) + compiler_panic(img->c, SRCLOC_NONE, "link: oom on verneed sonames"); + sonames[nson++] = vb.reqs[r].soname; + } + } + { + u32 total = nson * (u32)ELF_VERNEED_SIZE + vb.nreq * (u32)ELF_VERNAUX_SIZE; + u8* vn = (u8*)h->alloc(h, total, 4); + u8* p; + u32 si; + if (!vn) compiler_panic(img->c, SRCLOC_NONE, "link: oom on verneed"); + memset(vn, 0, total); + p = vn; + for (si = 0; si < nson; ++si) { + Slice so_s = pool_slice(l->c->global, sonames[si]); + u32 file_off = bb_append_str(dynstr, so_s.s, (u32)so_s.len); + u8* vn_rec = p; + u32 cnt = 0; + u8* aux; + p += ELF_VERNEED_SIZE; + aux = p; + for (r = 0; r < vb.nreq; ++r) { + Slice ver_s; + u32 name_off; + if (vb.reqs[r].soname != sonames[si]) continue; + ver_s = pool_slice(l->c->global, vb.reqs[r].version); + name_off = bb_append_str(dynstr, ver_s.s, (u32)ver_s.len); + wr_u32_le(p + 0, elf_sysv_hash(ver_s.s, (u32)ver_s.len)); /* vna_hash */ + wr_u16_le(p + 4, 0); /* vna_flags */ + wr_u16_le(p + 6, vb.reqs[r].index); /* vna_other */ + wr_u32_le(p + 8, name_off); /* vna_name */ + /* vna_next: filled after we know if another aux follows. */ + p += ELF_VERNAUX_SIZE; + ++cnt; + } + /* Verneed header. vn_aux is the byte offset to the first Vernaux. */ + wr_u16_le(vn_rec + 0, 1); /* vn_version */ + wr_u16_le(vn_rec + 2, (u16)cnt); /* vn_cnt */ + wr_u32_le(vn_rec + 4, file_off); /* vn_file */ + wr_u32_le(vn_rec + 8, (u32)(aux - vn_rec)); /* vn_aux */ + wr_u32_le(vn_rec + 12, + si + 1u < nson ? (u32)(p - vn_rec) : 0u); /* vn_next */ + /* Link the Vernaux chain (each entry -> next, last -> 0). */ + { + u8* a = aux; + u32 j; + for (j = 0; j < cnt; ++j) { + wr_u32_le(a + 12, j + 1u < cnt ? (u32)ELF_VERNAUX_SIZE : 0u); + a += ELF_VERNAUX_SIZE; + } + } + } + dyn->verneed = vn; + dyn->verneed_len = total; + dyn->nverneed = nson; + } + if (sonames) h->free(h, sonames, sizeof(*sonames) * capson); + } + if (vb.reqs) h->free(h, vb.reqs, sizeof(*vb.reqs) * vb.capreq); +} + /* ---- .gnu.hash builder ---- * * Hashed range is [first_global, ndynsym) — slot 0 (STN_UNDEF) is @@ -517,6 +724,7 @@ static u32 count_dynamic_entries(const LinkDynState* dyn) { n += 7; /* 5 fixed + DT_FLAGS_1 + DT_NULL */ if (dyn->cap_rela_dyn) n += 3; /* DT_RELA + DT_RELASZ + DT_RELAENT */ if (dyn->nrela_plt) n += 4; /* PLT-only entries */ + if (dyn->nverneed) n += 3; /* DT_VERSYM + DT_VERNEED + DT_VERNEEDNUM */ return n; } @@ -602,6 +810,10 @@ void layout_dyn(Linker* l, LinkImage* img) { if (s && slen) (void)bb_append_str(&dynstr, s, (u32)slen); } } + /* Symbol versioning: assign per-import version requirements and append the + * version strings ("FBSD_1.5", ...) to .dynstr. Must run before .dynstr is + * finalized below; emits nothing when no import is versioned. */ + build_versions(l, img, dyn, &imports, &dynstr); dyn->dynstr = dynstr.data; dyn->dynstr_len = dynstr.len; @@ -671,6 +883,9 @@ 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; + int has_ver = dyn->nverneed > 0; + u64 versym_bytes = (u64)dyn->versym_len; + u64 verneed_bytes = (u64)dyn->verneed_len; /* 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; @@ -727,6 +942,12 @@ void layout_dyn(Linker* l, LinkImage* img) { off = ALIGN_UP(off + rela_dyn_bytes, 8u); u64 rela_plt_off = off; off = ALIGN_UP(off + rela_plt_bytes, 8u); + /* .gnu.version + .gnu.version_r (zero-sized and skipped when no import is + * versioned, so the ro segment is unchanged for unversioned links). */ + u64 versym_off = off; + off = ALIGN_UP(off + versym_bytes, 8u); + u64 verneed_off = off; + off = ALIGN_UP(off + verneed_bytes, 8u); u64 ro_seg_size = off; /* When no PLT is needed, suppress the RX/.plt segment entirely. */ @@ -760,7 +981,7 @@ void layout_dyn(Linker* l, LinkImage* img) { ro_seg->file_size = ro_seg_size; ro_seg->mem_size = ro_seg_size; ro_seg->align = (u32)page; - ro_seg->nsections = 6; + ro_seg->nsections = 6u + (has_ver ? 2u : 0u); img->segment_bytes[ro_seg_idx] = ro_seg_size ? (u8*)h->alloc(h, (size_t)ro_seg_size, 16) : NULL; img->segment_bytes_cap[ro_seg_idx] = (size_t)ro_seg_size; @@ -838,7 +1059,7 @@ void layout_dyn(Linker* l, LinkImage* img) { /* Step 6: synthetic LinkSection entries. Order in img->sections * matches the loader-friendly file order and feeds emit's * outshdr-merge pass. */ - u32 nsec = 7u + (has_plt ? 2u : 0u); + u32 nsec = 7u + (has_plt ? 2u : 0u) + (has_ver ? 2u : 0u); u32 sec_base = dyn_alloc_sections(img, nsec); /* helper: populate a fresh LinkSection for a segment-internal range */ @@ -852,6 +1073,9 @@ void layout_dyn(Linker* l, LinkImage* img) { Sym name_dynamic = pool_intern_slice(l->c->global, SLICE_LIT(".dynamic")); Sym name_plt = pool_intern_slice(l->c->global, SLICE_LIT(".plt")); Sym name_got_plt = pool_intern_slice(l->c->global, SLICE_LIT(".got.plt")); + Sym name_gnu_version = pool_intern_slice(l->c->global, SLICE_LIT(".gnu.version")); + Sym name_gnu_version_r = + pool_intern_slice(l->c->global, SLICE_LIT(".gnu.version_r")); #define INIT_SEC(IDX, NAME, SEG_IDX, OFF_IN_SEG, SIZE, ALIGN, FLAGS, SEM) \ do { \ @@ -904,6 +1128,19 @@ void layout_dyn(Linker* l, LinkImage* img) { dyn->sec_plt = (LinkSectionId)(sec_base + 7 + 1u); dyn->sec_got_plt = (LinkSectionId)(sec_base + 8 + 1u); } + if (has_ver) { + /* Appended after the optional PLT slots; emit sorts the section-header + * table by (segment, vaddr), so array order here is not load-bearing. The + * SSEM_PROGBITS sem just parks the bytes in the ro segment — the runtime + * reads them via DT_VERSYM/DT_VERNEED, not the section headers. */ + u32 vb0 = 7u + (has_plt ? 2u : 0u); + INIT_SEC(vb0, name_gnu_version, ro_seg_idx, versym_off, versym_bytes, 2, + SF_ALLOC, SSEM_PROGBITS); + INIT_SEC(vb0 + 1u, name_gnu_version_r, ro_seg_idx, verneed_off, + verneed_bytes, 4, SF_ALLOC, SSEM_PROGBITS); + dyn->sec_gnu_version = (LinkSectionId)(sec_base + vb0 + 1u); + dyn->sec_gnu_version_r = (LinkSectionId)(sec_base + vb0 + 1u + 1u); + } #undef INIT_SEC img->nsections += nsec; @@ -941,6 +1178,14 @@ void layout_dyn(Linker* l, LinkImage* img) { if (gnuhash_bytes && ro_bytes && dyn->gnu_hash) memcpy(ro_bytes + gnuhash_off, dyn->gnu_hash, dyn->gnu_hash_len); + /* .gnu.version + .gnu.version_r (no vaddrs inside; copied verbatim). */ + if (has_ver && ro_bytes) { + if (versym_bytes && dyn->versym) + memcpy(ro_bytes + versym_off, dyn->versym, dyn->versym_len); + if (verneed_bytes && dyn->verneed) + memcpy(ro_bytes + verneed_off, dyn->verneed, dyn->verneed_len); + } + /* .rela.plt: emit JUMP_SLOT records, one per imported function, and * stash each import's PLT-entry vaddr in `sym_plt_vaddr` so the * apply pass can redirect CALL26/JUMP26 against the import. The @@ -1019,6 +1264,8 @@ void link_dyn_state_free(LinkImage* img) { if (dyn->dynsym) h->free(h, dyn->dynsym, sizeof(*dyn->dynsym) * dyn->ndynsym); if (dyn->dynstr) h->free(h, dyn->dynstr, dyn->dynstr_len); if (dyn->gnu_hash) h->free(h, dyn->gnu_hash, dyn->gnu_hash_len); + if (dyn->versym) h->free(h, dyn->versym, dyn->versym_len); + if (dyn->verneed) h->free(h, dyn->verneed, dyn->verneed_len); if (dyn->rela_dyn) h->free(h, dyn->rela_dyn, sizeof(*dyn->rela_dyn) * dyn->cap_rela_dyn); if (dyn->rela_plt) diff --git a/src/obj/elf/read.c b/src/obj/elf/read.c @@ -268,6 +268,72 @@ static u32 elf_default_version_namelen(const char* nm, u32 nlen) { return nlen; } +/* Parse a DSO's .gnu.version_d (SHT_GNU_VERDEF) into an index->version-name + * table so .dynsym entries (whose version lives in the parallel .gnu.version) + * can be labelled. Returns an arena table indexed by version index (0/1 unused, + * matching VER_NDX_LOCAL/GLOBAL) and sets *out_max to the highest index seen; + * NULL when the input has no verdef. The Verdef/Verdaux wire layout is identical + * on ELFCLASS32/64 (all Half/Word fields), so this is width-agnostic. */ +static Sym* read_elf_verdefs(Compiler* c, const u8* data, size_t len, + const ShdrRec* shdrs, u16 e_shnum, u32* out_max) { + u32 i, verdef_idx = 0, max_ndx = 0; + const ShdrRec* sh; + const ShdrRec* str_sh; + const u8* strtab; + const u8* base; + u64 strtab_sz, size, off; + Sym* tbl; + *out_max = 0; + for (i = 1; i < e_shnum; ++i) + if (shdrs[i].sh_type == SHT_GNU_VERDEF) { + verdef_idx = i; + break; + } + if (!verdef_idx) return NULL; + sh = &shdrs[verdef_idx]; + if (sh->sh_link >= e_shnum) return NULL; + str_sh = &shdrs[sh->sh_link]; + if (sh->sh_offset + sh->sh_size > len || + str_sh->sh_offset + str_sh->sh_size > len) + return NULL; + strtab = data + str_sh->sh_offset; + strtab_sz = str_sh->sh_size; + base = data + sh->sh_offset; + size = sh->sh_size; + + /* Pass 1: highest version index, to size the table. */ + off = 0; + while (off + ELF_VERDEF_SIZE <= size) { + u32 ndx = (u32)(rd_u16_le(base + off + 4) & VERSYM_VERSION); + u32 vd_next = rd_u32_le(base + off + 16); + if (ndx > max_ndx) max_ndx = ndx; + if (!vd_next) break; + off += vd_next; + } + tbl = arena_zarray(c->scratch, Sym, (size_t)max_ndx + 1u); + + /* Pass 2: record each non-base version's name (its first Verdaux). */ + off = 0; + while (off + ELF_VERDEF_SIZE <= size) { + u16 vd_flags = rd_u16_le(base + off + 2); + u32 ndx = (u32)(rd_u16_le(base + off + 4) & VERSYM_VERSION); + u32 vd_aux = rd_u32_le(base + off + 12); + u32 vd_next = rd_u32_le(base + off + 16); + if (!(vd_flags & VER_FLG_BASE) && ndx <= max_ndx && + off + vd_aux + ELF_VERDAUX_SIZE <= size) { + u32 nlen; + const char* nm = + strtab_lookup(strtab, strtab_sz, rd_u32_le(base + off + vd_aux), &nlen); + if (nlen) + tbl[ndx] = pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen}); + } + if (!vd_next) break; + off += vd_next; + } + *out_max = max_ndx; + return tbl; +} + /* Populate the builder's ObjImage from an ET_EXEC / ET_DYN input: the * program-header segment table (+ interp + image base), the .dynamic * dependency view (DT_NEEDED / DT_SONAME / DT_RPATH / DT_RUNPATH), the @@ -415,6 +481,23 @@ static void read_elf_image(Compiler* c, ObjBuilder* ob, const u8* data, const u8* base = data + sh->sh_offset; ndynsym = (u32)(sh->sh_size / sym_size); dynsym_names = arena_zarray(c->scratch, Sym, ndynsym ? ndynsym : 1); + /* Parallel symbol-version tables: .gnu.version_d names indexed by + * version index, and .gnu.version (one u16 per dynsym entry). A + * defined entry whose versym lacks VERSYM_HIDDEN is the *default* + * version of its name — the version a plain reference should bind. */ + u32 verdef_max = 0; + Sym* verdef_tbl = read_elf_verdefs(c, data, len, shdrs, e_shnum, + &verdef_max); + const u8* versym = NULL; + u32 nversym = 0; + for (u16 vi = 1; vi < e_shnum; ++vi) { + if (shdrs[vi].sh_type != SHT_GNU_VERSYM) continue; + if (shdrs[vi].sh_offset + shdrs[vi].sh_size <= len && + shdrs[vi].sh_entsize == 2) + versym = data + shdrs[vi].sh_offset, + nversym = (u32)(shdrs[vi].sh_size / 2u); + break; + } for (u32 i = 1; i < ndynsym; ++i) { const u8* p = base + (u64)i * sym_size; /* Elf32_Sym REORDERS: st_name@0, st_value@4, st_size@8, @@ -442,6 +525,13 @@ static void read_elf_image(Compiler* c, ObjBuilder* ob, const u8* data, : elf_to_obj[st_shndx]; ds.value = st_value; ds.size = st_size; + ds.version = 0; + if (versym && verdef_tbl && i < nversym && st_shndx != SHN_UNDEF) { + u16 v = rd_u16_le(versym + (u64)i * 2u); + u32 ndx = (u32)(v & VERSYM_VERSION); + if (!(v & VERSYM_HIDDEN) && ndx >= 2u && ndx <= verdef_max) + ds.version = verdef_tbl[ndx]; + } obj_image_add_dynsym(im, &ds); } } @@ -1027,6 +1117,25 @@ ObjBuilder* read_elf_dso(Compiler* c, const char* name, const u8* data, ObjBuilder* ob = obj_new(c); if (!ob) compiler_panic(c, SRCLOC_NONE, "read_elf_dso: obj_new failed"); + /* Symbol versioning: when the DSO carries .gnu.version(_d), attach an + * ObjImage whose dynsyms record each export's default version so the linker + * can emit a matching .gnu.version_r requirement (see build_versions in + * link_dyn.c). Absent versioning (musl) leaves im NULL and changes nothing. */ + u32 verdef_max = 0; + Sym* verdef_tbl = read_elf_verdefs(c, data, len, shdrs, e_shnum, &verdef_max); + const u8* versym = NULL; + u32 nversym = 0; + for (u32 i = 1; i < e_shnum; ++i) { + if (shdrs[i].sh_type != SHT_GNU_VERSYM) continue; + if (shdrs[i].sh_offset + shdrs[i].sh_size <= len && shdrs[i].sh_entsize == 2) + versym = data + shdrs[i].sh_offset, + nversym = (u32)(shdrs[i].sh_size / 2u); + break; + } + ObjImage* im = + (versym && verdef_tbl) ? obj_image_ensure(ob, OBJ_KIND_DYN) : NULL; + if (im && soname) obj_image_set_soname(im, soname); + u32 nsyms = (u32)(sh->sh_size / ELF64_SYM_SIZE); const u8* base = data + sh->sh_offset; for (u32 i = 1; i < nsyms; ++i) { /* skip index 0 */ @@ -1061,6 +1170,23 @@ ObjBuilder* read_elf_dso(Compiler* c, const char* name, const u8* data, (SymKind)kind, OBJ_SEC_NONE, 0, 0, 0); obj_sym_mark_referenced(ob, did); } + if (im) { + ObjImageSym ds; + ds.name = sn; + ds.bind = (SymBind)bind; + ds.kind = (SymKind)kind; + ds.section = OBJ_SEC_NONE; + ds.value = 0; + ds.size = 0; + ds.version = 0; + if (i < nversym) { + u16 v = rd_u16_le(versym + (u64)i * 2u); + u32 ndx = (u32)(v & VERSYM_VERSION); + if (!(v & VERSYM_HIDDEN) && ndx >= 2u && ndx <= verdef_max) + ds.version = verdef_tbl[ndx]; + } + obj_image_add_dynsym(im, &ds); + } } obj_finalize(ob); diff --git a/src/obj/macho/read.c b/src/obj/macho/read.c @@ -284,6 +284,7 @@ static void read_macho_image(Compiler* c, ObjBuilder* ob, const u8* data, u8 type_field = (u8)(n_type & N_TYPE); ObjImageSym ds; + ds.version = 0; /* Mach-O has no ELF-style symbol versioning */ ds.name = pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen}); ds.bind = (n_desc & (N_WEAK_DEF | N_WEAK_REF)) ? SB_WEAK : SB_GLOBAL; ds.value = (type_field == N_SECT || type_field == N_ABS) ? n_value : 0; diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -916,6 +916,14 @@ typedef struct ObjImageSym { ObjSecId section; /* OBJ_SEC_NONE for undefined imports */ u64 value; u64 size; + /* ELF symbol-version name (interned), set only for a DSO export that is the + * *default* (non-hidden) version of `name` — e.g. libc.so.7's + * fstat@@FBSD_1.5. 0 when the input carries no versioning, or this entry is a + * hidden compatibility alias (fstat@FBSD_1.0). The linker uses it to emit a + * matching .gnu.version_r requirement so the runtime binds the right version + * (mandatory on FreeBSD, where the INO64 transition gave `fstat`/`stat` two + * incompatible struct-stat layouts behind FBSD_1.0 vs FBSD_1.5). */ + Sym version; } ObjImageSym; /* Dynamic relocation (.rela.dyn / .rela.plt, dyld binds, PE base relocs).