Skip to content

Commit ee7a87a

Browse files
Enrique SaurezCopilot
andcommitted
[dlfcn] E: Add tests for ctors/dtors and DT_RUNPATH
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>
1 parent 2bbad34 commit ee7a87a

8 files changed

Lines changed: 272 additions & 2 deletions

File tree

.nanvix/z.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"c-bindings",
6868
"dlfcn-c",
6969
"dlfcn-global-c",
70+
"dlfcn-init-runpath-c",
7071
"dlfcn-needed-c",
7172
"dlfcn-pie-c",
7273
"echo-c",
@@ -101,6 +102,7 @@
101102
# Suites that require ramfs-bundled shared libraries and only run in standalone mode.
102103
STANDALONE_ONLY_SUITES = [
103104
"dlfcn-c",
105+
"dlfcn-init-runpath-c",
104106
"dlfcn-pie-c",
105107
]
106108

@@ -113,6 +115,11 @@
113115
# Maps suite name to a list of (source_filename_in_build_dir, ramfs_target_path).
114116
SUITE_RAMFS_LIBS: dict[str, list[tuple[str, str]]] = {
115117
"dlfcn-c": [("libmul.so", "lib/libmul.so")],
118+
"dlfcn-init-runpath-c": [
119+
("libctor.so", "lib/libctor.so"),
120+
("libparent.so", "lib/libparent.so"),
121+
("libchild.so", "lib/subdir/libchild.so"),
122+
],
116123
"dlfcn-pie-c": [("libmul-pie.so", "lib/libmul-pie.so")],
117124
}
118125

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ PROCESS_MODE ?= multi-process
1818
MEMORY_SIZE ?= 128mb
1919

2020
# Test suites to build.
21-
SUITES := c-bindings dlfcn-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c
21+
SUITES := c-bindings dlfcn-c dlfcn-init-runpath-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c
2222

2323
# ELF binaries produced by each suite.
2424
BINARIES := $(addsuffix .elf,$(SUITES))

src/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export LIBRARIES_DIR ?= $(BINARIES_DIR)
6565
# Test Suites
6666
#===============================================================================
6767

68-
SUITES := c-bindings dlfcn-c dlfcn-global-c dlfcn-needed-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c
68+
SUITES := c-bindings dlfcn-c dlfcn-global-c dlfcn-init-runpath-c dlfcn-needed-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c
6969

7070
#===============================================================================
7171
# Build Rules

src/dlfcn-init-runpath-c/Makefile

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright(c) The Maintainers of Nanvix.
2+
# Licensed under the MIT License.
3+
#
4+
# dlfcn-init-runpath-c: Tests for `.init_array` / `.fini_array`
5+
# constructor and destructor invocation, and for DT_RUNPATH-driven
6+
# DT_NEEDED dependency search.
7+
8+
PROGRAM_NAME := dlfcn-init-runpath-c
9+
10+
SOURCES := $(wildcard *.c)
11+
OBJECTS := $(SOURCES:.c=.o)
12+
BINARY := $(PROGRAM_NAME).elf
13+
14+
all: $(OBJECTS) libs-all
15+
$(CC) $(LDFLAGS) -pie -rdynamic -Wl,--no-dynamic-linker $(OBJECTS) $(LIBRARIES) -o $(BINARIES_DIR)/$(BINARY)
16+
17+
clean: libs-clean
18+
rm -f $(OBJECTS)
19+
rm -f $(BINARIES_DIR)/$(BINARY)
20+
21+
# libctor.so - constructor/destructor witness, used by the .init_array tests.
22+
# libchild.so - dependency of libparent.so. Will be staged into
23+
# lib/subdir/ at ramfs time so it is only reachable via
24+
# libparent.so's DT_RUNPATH (z.py SUITE_RAMFS_LIBS).
25+
# libparent.so - DT_NEEDED=libchild.so, DT_RUNPATH=lib/subdir/.
26+
#
27+
# `--enable-new-dtags` ensures the linker emits DT_RUNPATH (not the
28+
# deprecated DT_RPATH) for libparent.so, matching what modern
29+
# toolchains produce by default.
30+
libs-all:
31+
$(CC) libs/ctor.c -shared -fPIC -o $(LIBRARIES_DIR)/libctor.so
32+
$(CC) libs/subdir/child.c -shared -fPIC -o $(LIBRARIES_DIR)/libchild.so
33+
$(CC) libs/parent.c -shared -fPIC \
34+
-L$(LIBRARIES_DIR) -lchild \
35+
-Wl,--enable-new-dtags,-rpath,lib/subdir \
36+
-o $(LIBRARIES_DIR)/libparent.so
37+
38+
libs-clean:
39+
rm -f $(LIBRARIES_DIR)/libctor.so
40+
rm -f $(LIBRARIES_DIR)/libchild.so
41+
rm -f $(LIBRARIES_DIR)/libparent.so
42+
43+
%.o: %.c
44+
$(CC) $(CFLAGS) -fPIE $< -c -o $@
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright(c) The Maintainers of Nanvix.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/*
7+
* libctor.so - shared library that exercises `.init_array` and
8+
* `.fini_array` semantics required by the System V gABI.
9+
*
10+
* The constructor sets `ctor_ran` to a known sentinel value, and the
11+
* destructor writes a different sentinel into the test program's
12+
* `g_dtor_ran` global so that observation outlives the unloaded
13+
* library. `g_dtor_ran` is declared `extern` here and resolved by the
14+
* Nanvix loader's global symbol table (the main executable was linked
15+
* with `-rdynamic`).
16+
*/
17+
18+
/* Public state exposed via dlsym so the test can read it after dlopen. */
19+
volatile int ctor_ran = 0;
20+
21+
/* Main executable owns the destructor witness. */
22+
extern volatile int g_dtor_ran;
23+
24+
static void __attribute__((constructor)) my_ctor(void)
25+
{
26+
ctor_ran = 0xC70A;
27+
}
28+
29+
static void __attribute__((destructor)) my_dtor(void)
30+
{
31+
g_dtor_ran = 0xD70A;
32+
}
33+
34+
/* Sanity entry point so the test can confirm dlsym still works. */
35+
int ctor_value(void)
36+
{
37+
return 42;
38+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright(c) The Maintainers of Nanvix.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/*
7+
* libparent.so - shared library whose DT_NEEDED entry points at
8+
* libchild.so, but whose runtime search path (DT_RUNPATH) is
9+
* `lib/subdir/`. The Nanvix loader must consult DT_RUNPATH before
10+
* falling back to the default `lib/` directory, otherwise libchild.so
11+
* will not be located and dlopen will fail.
12+
*/
13+
14+
extern int child_value(int x);
15+
16+
int parent_value(int x)
17+
{
18+
return child_value(x) * 2;
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright(c) The Maintainers of Nanvix.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/*
7+
* libchild.so - dependency of libparent.so. Installed under a
8+
* non-default directory (`lib/subdir/`) so the only way the loader
9+
* can find it via the DT_NEEDED edge in libparent.so is by honouring
10+
* libparent's DT_RUNPATH.
11+
*/
12+
13+
int child_value(int x)
14+
{
15+
return x + 7;
16+
}

src/dlfcn-init-runpath-c/main.c

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright(c) The Maintainers of Nanvix.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/*
7+
* dlfcn-init-runpath-c: Tests for `.init_array` / `.fini_array`
8+
* constructor and destructor invocation, and for DT_RUNPATH-driven
9+
* dependency search.
10+
*
11+
* Test 1 (constructor): dlopen libctor.so, dlsym `ctor_ran`, and
12+
* confirm the constructor sentinel was written.
13+
*
14+
* Test 2 (destructor): dlclose libctor.so and confirm the destructor
15+
* wrote its sentinel into the main executable's `g_dtor_ran` global.
16+
* `g_dtor_ran` must be exported via -rdynamic so the loader's global
17+
* symbol table can satisfy the `extern volatile int g_dtor_ran;`
18+
* reference in the library.
19+
*
20+
* Test 3 (DT_RUNPATH): dlopen lib/libparent.so, which has
21+
* DT_NEEDED=libchild.so and DT_RUNPATH=lib/subdir/. The loader must
22+
* probe DT_RUNPATH before the default `lib/` directory; otherwise
23+
* libchild.so is not located.
24+
*/
25+
26+
#include <dlfcn.h>
27+
#include <stdio.h>
28+
#include <string.h>
29+
#include <unistd.h>
30+
31+
/* Witness for libctor.so's destructor — defined here so it survives the
32+
* library being unloaded. Marked volatile to keep the optimiser away.
33+
*/
34+
volatile int g_dtor_ran = 0;
35+
36+
static int tests_passed = 0;
37+
static int tests_failed = 0;
38+
39+
static void pass(const char *name)
40+
{
41+
printf(" PASS: %s\n", name);
42+
fflush(stdout);
43+
tests_passed++;
44+
}
45+
46+
static void fail(const char *name, const char *reason)
47+
{
48+
printf(" FAIL: %s (%s)\n", name, reason);
49+
fflush(stdout);
50+
tests_failed++;
51+
}
52+
53+
/*
54+
* Test 1: Constructor in `.init_array` must run before dlopen returns.
55+
*/
56+
static void test_init_array(void)
57+
{
58+
void *h = dlopen("lib/libctor.so", RTLD_NOW);
59+
if (h == NULL) {
60+
fail("init_array fires on dlopen", dlerror());
61+
return;
62+
}
63+
64+
volatile int *ctor_ran = (volatile int *)dlsym(h, "ctor_ran");
65+
if (ctor_ran == NULL) {
66+
fail("init_array fires on dlopen", "ctor_ran symbol missing");
67+
dlclose(h);
68+
return;
69+
}
70+
71+
if (*ctor_ran != 0xC70A) {
72+
fail("init_array fires on dlopen", "constructor sentinel not set");
73+
dlclose(h);
74+
return;
75+
}
76+
77+
/* Sanity check: ordinary symbol resolution still works after init. */
78+
int (*fn)(void) = NULL;
79+
*(void **)(&fn) = dlsym(h, "ctor_value");
80+
if (fn == NULL || fn() != 42) {
81+
fail("init_array fires on dlopen", "ctor_value() wrong");
82+
dlclose(h);
83+
return;
84+
}
85+
86+
/* Reset the dtor witness so test_fini_array measures a fresh signal. */
87+
g_dtor_ran = 0;
88+
dlclose(h);
89+
90+
pass("init_array fires on dlopen");
91+
92+
/* Bridge directly into the destructor test while the witness is fresh. */
93+
if (g_dtor_ran != 0xD70A) {
94+
fail("fini_array fires on dlclose", "destructor sentinel not set");
95+
return;
96+
}
97+
pass("fini_array fires on dlclose");
98+
}
99+
100+
/*
101+
* Test 3: DT_RUNPATH must be consulted when resolving DT_NEEDED bare
102+
* names. libparent.so depends on libchild.so but libchild.so only
103+
* exists under lib/subdir/, which is libparent's DT_RUNPATH.
104+
*/
105+
static void test_dt_runpath(void)
106+
{
107+
void *h = dlopen("lib/libparent.so", RTLD_NOW);
108+
if (h == NULL) {
109+
fail("DT_RUNPATH dependency search", dlerror());
110+
return;
111+
}
112+
113+
int (*fn)(int) = NULL;
114+
*(void **)(&fn) = dlsym(h, "parent_value");
115+
if (fn == NULL || fn(5) != 24) {
116+
/* parent_value(5) = child_value(5) * 2 = (5 + 7) * 2 = 24 */
117+
fail("DT_RUNPATH dependency search", "parent_value() wrong");
118+
dlclose(h);
119+
return;
120+
}
121+
122+
dlclose(h);
123+
pass("DT_RUNPATH dependency search");
124+
}
125+
126+
int main(int argc, const char *argv[])
127+
{
128+
(void)argc;
129+
(void)argv;
130+
131+
printf("=== dlfcn init_array + DT_RUNPATH tests ===\n");
132+
fflush(stdout);
133+
134+
test_init_array();
135+
test_dt_runpath();
136+
137+
printf("\n%d passed, %d failed\n", tests_passed, tests_failed);
138+
fflush(stdout);
139+
140+
if (tests_failed == 0) {
141+
const char *magic = "ok";
142+
write(STDOUT_FILENO, magic, 3);
143+
}
144+
145+
return tests_failed > 0 ? 1 : 0;
146+
}

0 commit comments

Comments
 (0)