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:
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).