Skip to content

[dlfcn] E: Add tests for diamond DT_NEEDED graphs#3

Closed
esaurez wants to merge 2 commits into
feat/dlfcn-init-array-and-runpathfrom
feat/dlfcn-diamond-needed
Closed

[dlfcn] E: Add tests for diamond DT_NEEDED graphs#3
esaurez wants to merge 2 commits into
feat/dlfcn-init-array-and-runpathfrom
feat/dlfcn-diamond-needed

Conversation

@esaurez

@esaurez esaurez commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Add a new test suite dlfcn-diamond-c that exercises diamond DT_NEEDED graphs — the canonical multi-edge shape that production shared-library chains take:

libdiamond.so ─┬─ DT_NEEDED libleft.so  ──┐
               └─ DT_NEEDED libright.so ──┴── DT_NEEDED libbase.so

The existing dlfcn-needed-c suite covers a single linear edge; this suite extends coverage to the diamond case which surfaces in every real-world .so chain.

Three assertions

  1. Depth-2 controldlopen("lib/libright.so") — a single linear edge from libright.so to libbase.so. Passes on stock Nanvix; proves the test rig works.
  2. Diamond loaddlopen("lib/libdiamond.so") — the diamond proper. On stock Nanvix this hangs in the loader (deadlock on ancestor mutex), then after the deadlock is fixed panics in dlclose. Both bugs are addressed by the companion esaurez/nanvix#28.
  3. Handle stabilitydlopen twice returns the same handle.

Detecting duplicate libbase.so

To catch the "loader silently loaded libbase.so twice" failure mode (where libleft and libright each get their own private copy of libbase.so's unique_counter global), the diamond test calls left_bump() then right_bump() and asserts the counter reaches 2 on the second call. A buggy loader gives each arm its own counter, both reach 1, and the test fails with a precise reason:

right_bump=1 (expected 2), observed=1 (expected 2) -- libbase.so was loaded twice

Hooks

File Change
src/dlfcn-diamond-c/{Makefile,main.c,libs/*.c} New suite (4 .so fixtures + main test)
Makefile, src/Makefile Register dlfcn-diamond-c in SUITES
.nanvix/z.py Register in ALL_SUITES, STANDALONE_ONLY_SUITES, and SUITE_RAMFS_LIBS (stages all 4 .so files into lib/)

Validation

End-to-end on a standalone Nanvix VM with esaurez/nanvix#28 applied:

RUN  dlfcn-diamond-c...
=== dlfcn diamond DT_NEEDED tests ===
  PASS: dlopen(libright.so) [depth-2 chain]
  PASS: dlopen(libdiamond.so)
  PASS: re-dlopen(libdiamond.so) returns same handle

3 passed, 0 failed
OK   dlfcn-diamond-c
        *** POSIX tests PASSED ***

All 18 testable suites pass (the new diamond suite + 17 pre-existing).

ELF metadata sanity check

$ readelf -d build/libdiamond.so
 0x00000001 (NEEDED)  Shared library: [libleft.so]
 0x00000001 (NEEDED)  Shared library: [libright.so]
 0x0000001d (RUNPATH) Library runpath: [lib]

$ readelf -d build/libleft.so
 0x00000001 (NEEDED)  Shared library: [libbase.so]
 0x0000001d (RUNPATH) Library runpath: [lib]

Companion PR

esaurez/nanvix feat/dlfcn-diamond-needed — the loader fix. This test suite verifies that fix and would HANG without it.

Stacking

Stacked on top of esaurez/posix-tests#2 (the .init_array/DT_RUNPATH test suite). Merge order: posix-tests#2 first, then this PR.

Enrique Saurez and others added 2 commits June 3, 2026 18:53
Add a new test suite (`dlfcn-init-runpath-c`) that exercises the
three System V ABI capabilities the Nanvix dynamic loader now
implements (esaurez/nanvix PR `feat/dlfcn-init-array-and-runpath`):

1. `libctor.so` defines `.init_array` and `.fini_array` entries via
   `__attribute__((constructor))` / `((destructor))`. The
   constructor writes a sentinel into a library-local global; the
   destructor writes a different sentinel into the test program's
   exported `g_dtor_ran` global so the witness survives the
   library being unloaded. The test asserts both sentinels appear
   in the expected order.

2. `libparent.so` is linked against `libchild.so` (creating a
   `DT_NEEDED` edge) and built with `-Wl,--enable-new-dtags,
   -rpath,lib/subdir`, which the linker emits as `DT_RUNPATH
   lib/subdir`. At runtime `libchild.so` is staged into
   `lib/subdir/` only — never `lib/` — so the only way `dlopen`
   can succeed is by honouring `libparent.so`'s `DT_RUNPATH`.

Both libraries are built flat in `build/` and re-staged into the
correct ramfs paths via the existing `SUITE_RAMFS_LIBS` mechanism
in `.nanvix/z.py`, matching the pattern already used by
`dlfcn-c`. The suite is registered in `STANDALONE_ONLY_SUITES`
because it requires ramfs-bundled `.so` files, and in
`ALL_SUITES`, the host `Makefile`, and the container
`src/Makefile`.

Test output on a standalone Nanvix VM running the updated loader:

    === dlfcn init_array + DT_RUNPATH tests ===
      PASS: init_array fires on dlopen
      PASS: fini_array fires on dlclose
      PASS: DT_RUNPATH dependency search

    3 passed, 0 failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a new test suite (`dlfcn-diamond-c`) that exercises diamond
DT_NEEDED graphs, the canonical multi-edge shape that production
shared-library chains take:

  libdiamond.so -> libleft.so  -> libbase.so
                -> libright.so -> libbase.so

Three assertions:

1. depth-2 control (`dlopen(libright.so)`) -- a single linear edge
   from libright.so to libbase.so. This currently passes on stock
   Nanvix and proves the test rig works.

2. diamond load (`dlopen(libdiamond.so)`) -- the diamond proper.
   On stock Nanvix this HANGS in the loader (deadlock on ancestor
   mutex), then after the deadlock is fixed PANICS in `dlclose`
   with an over-strict refcount assertion. Both bugs are addressed
   by the companion esaurez/nanvix#28.

3. handle stability (`dlopen` twice returns the same handle) --
   the standard idempotence guarantee of `dlopen` against an
   already-loaded library.

To detect the "libbase.so loaded twice" failure mode (where a buggy
loader gives libleft and libright each their own private copy of
libbase.so's `unique_counter` global instead of sharing one), the
diamond test calls `left_bump()` then `right_bump()` and asserts
the counter reaches 2 on the second call. Without the fix, both
arms see their own counter, both go to 1, and the test fails with
a precise reason string.

The suite is registered in `STANDALONE_ONLY_SUITES` because it
needs `SUITE_RAMFS_LIBS` staging.

End-to-end run on a standalone VM with the loader fix in place:

  === dlfcn diamond DT_NEEDED tests ===
    PASS: dlopen(libright.so) [depth-2 chain]
    PASS: dlopen(libdiamond.so)
    PASS: re-dlopen(libdiamond.so) returns same handle
  3 passed, 0 failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@esaurez esaurez force-pushed the feat/dlfcn-init-array-and-runpath branch from 04b2a38 to ee7a87a Compare June 8, 2026 21:32
@esaurez

esaurez commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

Superseded by upstream PR nanvix/posix-tests#177, which carries the same test contribution -- stacked on top of nanvix/posix-tests#174 (the init-array test branch), deep-reviewed, validated end-to-end against a local Nanvix build with the nanvix/nanvix#2478 diamond loader fix applied (all 3 cases pass; full posix-tests integration suite passes). One MUST FIX applied: stripped non-ASCII box-drawing characters and em-dashes from main.c, libs/base.c, and the suite Makefile. Two SHOULD FIXes applied: (a) added build-time readelf -d assertions in the suite Makefile for the four DT_NEEDED edges (libleft->libbase, libright->libbase, libdiamond->libleft, libdiamond->libright) to catch toolchain regressions; (b) rewrote the diamond-arm assertion to use a baseline-and-delta pattern (reading base_get() before invoking the arms) so the test stays robust against any future Nanvix dlclose policy that defers the unmap. Reworked commit-message reference from esaurez/nanvix#28 to nanvix/nanvix#2478. The upstream loader companion is nanvix/nanvix#2478; #2478's body has been updated to cross-reference this PR. Closing this fork PR; tracking continues upstream.

@esaurez esaurez closed this Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant