[dlfcn] E: Add dlfcn-weak-c tests for STB_WEAK loader semantics#1
[dlfcn] E: Add dlfcn-weak-c tests for STB_WEAK loader semantics#1esaurez wants to merge 1 commit into
Conversation
New `src/dlfcn-weak-c/` regression suite covering the System V ABI rule that an unresolved STB_WEAK undefined symbol takes value zero at dynamic link time (gABI, chapter "Symbol Table"; also implemented by glibc, musl, FreeBSD rtld, and Bionic). Companion to the in-process nanvix loader work that adds STB_WEAK handling to `get_symbol_value()`. Test matrix (7 cases driven through public POSIX `dlfcn`): 1. weak UND function (GOT, R_386_GLOB_DAT), resolved via main exe 2. weak UND function (GOT, R_386_GLOB_DAT), missing -> &fn == NULL 3. weak UND data (GOT, R_386_GLOB_DAT), resolved via main exe 4. weak UND data (GOT, R_386_GLOB_DAT), missing -> &data == NULL 5. strong UND function (PLT, R_386_JUMP_SLOT), missing -> dlopen fails 6. weak UND function (PLT, R_386_JUMP_SLOT), resolved via main exe 7. weak UND function (PLT, R_386_JUMP_SLOT), missing -> dlopen succeeds Case 5 runs first because it is the only one that passes against a loader without STB_WEAK support, so a regression in the strong-undefined path is reported independently of the weak cases. Case 7 dlopens the missing-PLT variant but deliberately does not call the resolved function pointer: the JMP_SLOT was zeroed by the loader and invoking it would jump to NULL. The case only validates that `dlopen(..., RTLD_NOW)` accepts the .so despite the weak undefined symbol. The Makefile asserts via `readelf -rW` / `-WsD` that each .so contains both the intended relocation type AND the intended symbol name (and binding), so fixtures cannot silently drift to exercising the wrong symbol. Helper .so files are compiled with the suite-wide CFLAGS to keep the test representative of the project's normal optimization environment. Main executable links with `-pie -rdynamic -Wl,--no-dynamic-linker` so `main_callback` and `weak_data` are exported in the main exe .dynsym (matching the existing `dlfcn-pie-c` pattern). Validated end-to-end on the Nanvix microvm against a loader build with the STB_WEAK change: all 7 cases pass. Case 5 alone was also validated against a loader build without the change (the historical strong-undefined path), confirming the regression-guard semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per the System V ABI generic ABI (gABI, chapter "Symbol Table"), an
undefined symbol whose binding is `STB_WEAK` and which cannot be
resolved at dynamic-link time is silently taken to have the value zero
(or `NULL` for function symbols). Every mainstream ELF dynamic
loader -- glibc `elf/dl-lookup.c`, musl `ldso/dynlink.c`, FreeBSD
`libexec/rtld-elf/rtld.c`, Android Bionic
`linker/linker_relocate.cpp` -- implements this contract; the program
is responsible for null-checking the symbol before use.
The Nanvix in-process loader's `get_symbol_value()` previously
returned `Err(BadFile, "symbol not found")` unconditionally when a
referenced symbol could not be resolved, regardless of binding. As a
result, any `.so` containing a weak undefined reference -- which is
extremely common: libstdc++ keeps weak refs to `pthread_*` and
`__gmon_start__`, glibc-compatible compilers emit weak refs to
`__cxa_thread_atexit_impl` and TLS descriptors, and Rust crates often
expose optional integration hooks as weak symbols -- failed to
`dlopen()`, even though the program would have run correctly with the
spec-defined zero substitution.
This patch fills that gap:
- `src/libs/elf/src/elf32.rs` adds the `STB_LOCAL` / `STB_GLOBAL` /
`STB_WEAK` constants, the `ST_BIND_SHIFT` constant, and an
`Elf32Sym::st_bind()` accessor that extracts the binding nibble.
- `src/libs/elf/src/relocation.rs` lifts the raw `STB_*` values into
a typed `SymbolBinding` enum (`Local` / `Global` / `Weak` /
`Other`), wires `Symbol::binding()` through the goblin
`st_bind()` helper, and adds an `is_weak()` predicate. Unknown
or reserved binding values fall through to `SymbolBinding::Other`
so they are never silently treated as weak. Five unit tests
cover decoding, the `Other` fallback, the independence of the
binding nibble from the type nibble, and that `is_weak()` /
`is_undefined()` are orthogonal axes.
- `src/libs/syscall/src/dlfcn/syscall/dynlib.rs::get_symbol_value()`
intercepts the existing `lookup() == None` arm: if the referring
symbol is both `SHN_UNDEF` and `STB_WEAK`, return `Ok(0)` and log
at `debug!`. Strong undefined references continue to return the
pre-existing `BadFile` error, so `dlopen(RTLD_NOW)` still fails
on a genuine missing strong dependency.
- `src/libs/syscall/src/dlfcn/syscall/dynlib.rs::query()` skips
undefined entries before consulting `get_symbol_value()`. Without
this, `dladdr()` would report a ghost symbol at address `0` for
every weak undefined entry in the dynsym; that is correct
behaviour for relocation but is not what `dladdr` is meant to
surface.
Substituting zero is safe across the relocation types the loader
currently implements (`R_386_32`, `R_386_PC32`, `R_386_JMP_SLOT`,
`R_386_GLOB_DAT`): the resulting GOT / PLT entry or in-place 32-bit
slot is null, so any code path that actually dereferences the symbol
traps deterministically -- matching the contract the spec puts on the
program (it must null-check before use).
Validated end-to-end on the Nanvix microvm:
- The new `dlfcn-weak-c` regression suite (proposed in
nanvix/posix-tests, see esaurez/posix-tests#1) exercises all four
weak-undefined relocation classes (GOT/PLT × function/data) in
both resolved-via-main-exe and missing variants, plus a strong-
undefined regression guard. All 7 cases pass against this
branch. The strong-undefined case still fails `dlopen(RTLD_NOW)`,
preserving the existing contract.
- CPython 3.12 successfully links against libstdc++ with weak refs
to `pthread_*` etc. left unresolved at .so-load time, runs
`hello.py`, then `dlopen()`s and exercises numpy 1.26.4 to
produce `NUMPY_TEST_OK` on the guest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Superseded by upstream PR nanvix/posix-tests#176, which carries the same test contribution -- rebased onto upstream main, scope-trimmed to only the test code (the fork PR also carried unrelated local-env drift: a CI workflow downgrade from v2.2.0 to v2.0.2, a nanvix-version downgrade from 0.16.17 to 0.15.26, z.ps1/z.sh changes, .gitignore tweaks, and z.py imports cleanup -- all dropped), deep-reviewed (one SHOULD-FIX applied: removed fork-specific BINARIES_DIR/LIBRARIES_DIR/READELF ?= fallbacks and Windows REMOVE macro from the suite Makefile to match siblings; three NITs applied: stripped non-ASCII em-dashes and right-arrow, dropped 'static' qualifier on test helpers to match dlfcn-c convention, added per-case R_386_GLOB_DAT / R_386_JUMP_SLOT relocation comments). Full posix-tests integration suite passes against the 0.16.17 release nanvix. Closing this fork PR; tracking continues upstream. |
Per the System V ABI generic ABI (gABI, chapter "Symbol Table"), an
undefined symbol whose binding is `STB_WEAK` and which cannot be
resolved at dynamic-link time is silently taken to have the value zero
(or `NULL` for function symbols). Every mainstream ELF dynamic
loader -- glibc `elf/dl-lookup.c`, musl `ldso/dynlink.c`, FreeBSD
`libexec/rtld-elf/rtld.c`, Android Bionic
`linker/linker_relocate.cpp` -- implements this contract; the program
is responsible for null-checking the symbol before use.
The Nanvix in-process loader's `get_symbol_value()` previously
returned `Err(BadFile, "symbol not found")` unconditionally when a
referenced symbol could not be resolved, regardless of binding. As a
result, any `.so` containing a weak undefined reference -- which is
extremely common: libstdc++ keeps weak refs to `pthread_*` and
`__gmon_start__`, glibc-compatible compilers emit weak refs to
`__cxa_thread_atexit_impl` and TLS descriptors, and Rust crates often
expose optional integration hooks as weak symbols -- failed to
`dlopen()`, even though the program would have run correctly with the
spec-defined zero substitution.
This patch fills that gap:
- `src/libs/elf/src/elf32.rs` adds the `STB_LOCAL` / `STB_GLOBAL` /
`STB_WEAK` constants, the `ST_BIND_SHIFT` constant, and an
`Elf32Sym::st_bind()` accessor that extracts the binding nibble.
- `src/libs/elf/src/relocation.rs` lifts the raw `STB_*` values into
a typed `SymbolBinding` enum (`Local` / `Global` / `Weak` /
`Other`), wires `Symbol::binding()` through the goblin
`st_bind()` helper, and adds an `is_weak()` predicate. Unknown
or reserved binding values fall through to `SymbolBinding::Other`
so they are never silently treated as weak. Five unit tests
cover decoding, the `Other` fallback, the independence of the
binding nibble from the type nibble, and that `is_weak()` /
`is_undefined()` are orthogonal axes.
- `src/libs/syscall/src/dlfcn/syscall/dynlib.rs::get_symbol_value()`
intercepts the existing `lookup() == None` arm: if the referring
symbol is both `SHN_UNDEF` and `STB_WEAK`, return `Ok(0)` and log
at `debug!`. Strong undefined references continue to return the
pre-existing `BadFile` error, so `dlopen(RTLD_NOW)` still fails
on a genuine missing strong dependency.
- `src/libs/syscall/src/dlfcn/syscall/dynlib.rs::query()` skips
undefined entries before consulting `get_symbol_value()`. Without
this, `dladdr()` would report a ghost symbol at address `0` for
every weak undefined entry in the dynsym; that is correct
behaviour for relocation but is not what `dladdr` is meant to
surface.
Substituting zero is safe across the relocation types the loader
currently implements (`R_386_32`, `R_386_PC32`, `R_386_JMP_SLOT`,
`R_386_GLOB_DAT`): the resulting GOT / PLT entry or in-place 32-bit
slot is null, so any code path that actually dereferences the symbol
traps deterministically -- matching the contract the spec puts on the
program (it must null-check before use).
Validated end-to-end on the Nanvix microvm:
- The new `dlfcn-weak-c` regression suite (proposed in
nanvix/posix-tests, see esaurez/posix-tests#1) exercises all four
weak-undefined relocation classes (GOT/PLT × function/data) in
both resolved-via-main-exe and missing variants, plus a strong-
undefined regression guard. All 7 cases pass against this
branch. The strong-undefined case still fails `dlopen(RTLD_NOW)`,
preserving the existing contract.
- CPython 3.12 successfully links against libstdc++ with weak refs
to `pthread_*` etc. left unresolved at .so-load time, runs
`hello.py`, then `dlopen()`s and exercises numpy 1.26.4 to
produce `NUMPY_TEST_OK` on the guest.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Adds a new
src/dlfcn-weak-c/regression suite covering STB_WEAKundefined-symbol semantics through the public POSIX
dlfcninterface(
dlopen/dlsym). The suite is the canonical end-to-end test for thein-process Nanvix loader change that adds STB_WEAK handling to
get_symbol_value()— once that change lands upstream, this suitebecomes the test that pins the behaviour in place.
Why this matters
The System V gABI (chapter "Symbol Table") specifies that an unresolved
STB_WEAK undefined symbol takes value zero at dynamic link time, with
the program responsible for null-checking before use. Every mainstream
ELF loader (glibc
elf/dl-lookup.c, muslldso/dynlink.c, FreeBSDrtld-elf/rtld.c, Android Bioniclinker/linker_relocate.cpp)implements this rule. The Nanvix loader work mirrors that contract.
We need a hermetic C-level test driven through the public POSIX
dlfcnAPI — independent of any specific consumer (cpython, Rust startup
crates, libposix) — so that future loader refactors cannot silently
re-break the contract.
Test matrix
7 cases, all in
main.c. Magic string"ok"is written on fullsuccess;
assert()aborts on failure (matches the siblingdlfcn-c,dlfcn-pie-c,dlfcn-global-c,dlfcn-needed-cconvention).libstrong-missing.soR_386_JUMP_SLOTstrong_missingdlopen(RTLD_NOW)failslibweak-func-resolved.soR_386_GLOB_DATmain_callbacktry_callback()==70libweak-func-missing.soR_386_GLOB_DATmissing_callback&fn == NULL;try_callback() == -1libweak-data-resolved.soR_386_GLOB_DATweak_dataread_weak_data()==99libweak-data-missing.soR_386_GLOB_DATmissing_weak_data&data == NULL;read_weak_data() == -1libweak-plt-resolved.soR_386_JUMP_SLOTmain_callbacklibweak-plt-missing.soR_386_JUMP_SLOTmissing_plt_callbackdlopen(RTLD_NOW)succeedsCase 5 runs first so that a regression in the strong-undefined
path is reported independently of the weak cases. It is also the only
case that passes against a loader without STB_WEAK support — see the
validation section below.
Case 7 deliberately does not call the function pointer it resolves.
The JMP_SLOT was zeroed by the loader (weak undefined missing → 0);
invoking it would jump to NULL. The case only validates that
dlopen(..., RTLD_NOW)accepts the .so despite the weak undefinedPLT entry.
Relocation-class coverage
The weak NULL-guarded pattern
if (&fn) fn();(inlibs/weak_func.cand
libs/weak_data.c) forces a GOT-indirect call/load and onlyexercises
R_386_GLOB_DAT. To also exercise weak references via thePLT, this PR adds
libs/weak_func_plt.c(an unguarded call) whichemits
R_386_JUMP_SLOT. Together, the two helpers cover bothrelocation classes the loader must handle for weak undefined symbols
in i686 ELF.
The strong-undefined regression guard (
libs/strong_missing.c) isunguarded by construction and emits
R_386_JUMP_SLOT.Build-time fixture verification
The Makefile asserts via
readelf -rWandreadelf -WsDthat eachproduced
.socontains both:exercising the symbol we think it is, not some accidental CRT-injected
weak symbol).
symbol is
UND.Helper
.sofiles are compiled with the suite-wideCFLAGS, keepingthe test representative of the project's normal
-O2+ warningenvironment.
Linker flags
Main executable:
-pie -rdynamic -Wl,--no-dynamic-linker-pie+-rdynamic: exportmain_callbackandweak_datain themain exe's
.dynsymso the resolved cases can look them up(matches the existing
dlfcn-pie-cpattern).--no-dynamic-linker: Nanvix uses an in-process loader via syscalls,not a separate
ld.so.Validation
pass against a Nanvix microvm guest. The full test runs in standalone
mode via
./z test.passes (sanity-validated by temporarily commenting out cases 1-4 and
6-7 and rebuilding). This confirms the regression-guard semantics
(
dlopen(RTLD_NOW)must continue to fail on missing strong UNDsymbols regardless of the weak-symbol code path).
Merge order
This PR can land any time. Cases 1-4 and 6-7 will only pass once the
Nanvix loader STB_WEAK change is merged. If this PR lands first, the
posix-tests CI run on a Nanvix build without the loader change would
fail cases 1-4/6-7 — that's the intended signal once the matching
nanvix-side PR opens. The cleanest sequencing is to merge this PR
roughly the same release cycle as the loader change.