diff --git a/.nanvix/z.py b/.nanvix/z.py index 5a2a4bb..316f374 100644 --- a/.nanvix/z.py +++ b/.nanvix/z.py @@ -62,7 +62,9 @@ ALL_SUITES = [ "c-bindings", "dlfcn-c", + "dlfcn-diamond-c", "dlfcn-global-c", + "dlfcn-init-runpath-c", "dlfcn-needed-c", "dlfcn-pie-c", "echo-c", @@ -97,6 +99,8 @@ # Suites that require ramfs-bundled shared libraries and only run in standalone mode. STANDALONE_ONLY_SUITES = [ "dlfcn-c", + "dlfcn-diamond-c", + "dlfcn-init-runpath-c", "dlfcn-pie-c", ] @@ -109,6 +113,17 @@ # Maps suite name to a list of (source_filename_in_build_dir, ramfs_target_path). SUITE_RAMFS_LIBS: dict[str, list[tuple[str, str]]] = { "dlfcn-c": [("libmul.so", "lib/libmul.so")], + "dlfcn-diamond-c": [ + ("libbase.so", "lib/libbase.so"), + ("libleft.so", "lib/libleft.so"), + ("libright.so", "lib/libright.so"), + ("libdiamond.so", "lib/libdiamond.so"), + ], + "dlfcn-init-runpath-c": [ + ("libctor.so", "lib/libctor.so"), + ("libparent.so", "lib/libparent.so"), + ("libchild.so", "lib/subdir/libchild.so"), + ], "dlfcn-pie-c": [("libmul-pie.so", "lib/libmul-pie.so")], } diff --git a/Makefile b/Makefile index bfcb117..83ada79 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ PROCESS_MODE ?= multi-process MEMORY_SIZE ?= 128mb # Test suites to build. -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 +SUITES := c-bindings dlfcn-c dlfcn-diamond-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 # ELF binaries produced by each suite. BINARIES := $(addsuffix .elf,$(SUITES)) diff --git a/src/Makefile b/src/Makefile index 45cb2f9..1c5ce9e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -65,7 +65,7 @@ export LIBRARIES_DIR ?= $(BINARIES_DIR) # Test Suites #=============================================================================== -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 +SUITES := c-bindings dlfcn-c dlfcn-diamond-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 #=============================================================================== # Build Rules diff --git a/src/dlfcn-diamond-c/Makefile b/src/dlfcn-diamond-c/Makefile new file mode 100644 index 0000000..78555d7 --- /dev/null +++ b/src/dlfcn-diamond-c/Makefile @@ -0,0 +1,55 @@ +# Copyright(c) The Maintainers of Nanvix. +# Licensed under the MIT License. +# +# dlfcn-diamond-c: Diamond DT_NEEDED graph resolution. +# +# libdiamond.so +# ├── DT_NEEDED libleft.so ──┐ +# └── DT_NEEDED libright.so ──┴── DT_NEEDED libbase.so +# +# The loader must consolidate the two DT_NEEDED libbase.so edges onto +# a single in-memory libbase.so instance. + +PROGRAM_NAME := dlfcn-diamond-c + +SOURCES := $(wildcard *.c) +OBJECTS := $(SOURCES:.c=.o) +BINARY := $(PROGRAM_NAME).elf + +all: $(OBJECTS) libs-all + $(CC) $(LDFLAGS) -pie -rdynamic -Wl,--no-dynamic-linker $(OBJECTS) $(LIBRARIES) -o $(BINARIES_DIR)/$(BINARY) + +clean: libs-clean + rm -f $(OBJECTS) + rm -f $(BINARIES_DIR)/$(BINARY) + +# libbase.so - leaf of the diamond, no deps. +# libleft.so - DT_NEEDED libbase.so. +# libright.so - DT_NEEDED libbase.so. +# libdiamond.so - DT_NEEDED libleft.so, libright.so. +# +# `--enable-new-dtags` ensures DT_RUNPATH (not the deprecated DT_RPATH) +# is emitted; the resolver in Nanvix's loader only honours DT_RUNPATH. +libs-all: + $(CC) libs/base.c -shared -fPIC -o $(LIBRARIES_DIR)/libbase.so + $(CC) libs/left.c -shared -fPIC \ + -L$(LIBRARIES_DIR) -lbase \ + -Wl,--enable-new-dtags,-rpath,lib \ + -o $(LIBRARIES_DIR)/libleft.so + $(CC) libs/right.c -shared -fPIC \ + -L$(LIBRARIES_DIR) -lbase \ + -Wl,--enable-new-dtags,-rpath,lib \ + -o $(LIBRARIES_DIR)/libright.so + $(CC) libs/diamond.c -shared -fPIC \ + -L$(LIBRARIES_DIR) -lleft -lright \ + -Wl,--enable-new-dtags,-rpath,lib \ + -o $(LIBRARIES_DIR)/libdiamond.so + +libs-clean: + rm -f $(LIBRARIES_DIR)/libbase.so + rm -f $(LIBRARIES_DIR)/libleft.so + rm -f $(LIBRARIES_DIR)/libright.so + rm -f $(LIBRARIES_DIR)/libdiamond.so + +%.o: %.c + $(CC) $(CFLAGS) -fPIE $< -c -o $@ diff --git a/src/dlfcn-diamond-c/libs/base.c b/src/dlfcn-diamond-c/libs/base.c new file mode 100644 index 0000000..ac093a5 --- /dev/null +++ b/src/dlfcn-diamond-c/libs/base.c @@ -0,0 +1,31 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + * + * libbase.so - shared leaf of the diamond DT_NEEDED graph. + * + * libdiamond.so + * ├── DT_NEEDED libleft.so ──┐ + * └── DT_NEEDED libright.so ──┴── DT_NEEDED libbase.so + * + * Both libleft.so and libright.so declare DT_NEEDED libbase.so. When the + * loader walks libdiamond.so's dependencies, it must NOT load libbase.so + * twice — exactly one copy should appear in the registry. Otherwise the + * `unique_counter` global below would be visible at two distinct + * addresses to libleft.so and libright.so, and the assertion in + * dlfcn-diamond-c/main.c would fire. + */ + +/* Process-lifetime counter. Bumped by every call to `base_bump()`. */ +static int unique_counter = 0; + +int base_bump(void) +{ + unique_counter += 1; + return unique_counter; +} + +int base_get(void) +{ + return unique_counter; +} diff --git a/src/dlfcn-diamond-c/libs/diamond.c b/src/dlfcn-diamond-c/libs/diamond.c new file mode 100644 index 0000000..d942eb7 --- /dev/null +++ b/src/dlfcn-diamond-c/libs/diamond.c @@ -0,0 +1,28 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + * + * libdiamond.so - root of the diamond. Pulls in libleft.so and + * libright.so via DT_NEEDED. Both arms transitively depend on + * libbase.so, so a correct loader must consolidate the two + * "DT_NEEDED libbase.so" edges onto a single libbase.so instance. + */ + +extern int left_bump(void); +extern int right_bump(void); +extern int right_get(void); + +int diamond_left(void) +{ + return left_bump(); +} + +int diamond_right(void) +{ + return right_bump(); +} + +int diamond_observe(void) +{ + return right_get(); +} diff --git a/src/dlfcn-diamond-c/libs/left.c b/src/dlfcn-diamond-c/libs/left.c new file mode 100644 index 0000000..00a3094 --- /dev/null +++ b/src/dlfcn-diamond-c/libs/left.c @@ -0,0 +1,15 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + * + * libleft.so - left arm of the diamond. Calls base_bump() so the test + * can observe whether libleft and libright share the same libbase.so + * instance (single counter) or each got their own (independent counters). + */ + +extern int base_bump(void); + +int left_bump(void) +{ + return base_bump(); +} diff --git a/src/dlfcn-diamond-c/libs/right.c b/src/dlfcn-diamond-c/libs/right.c new file mode 100644 index 0000000..45263f3 --- /dev/null +++ b/src/dlfcn-diamond-c/libs/right.c @@ -0,0 +1,19 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + * + * libright.so - right arm of the diamond. Symmetric to libleft.so. + */ + +extern int base_bump(void); +extern int base_get(void); + +int right_bump(void) +{ + return base_bump(); +} + +int right_get(void) +{ + return base_get(); +} diff --git a/src/dlfcn-diamond-c/main.c b/src/dlfcn-diamond-c/main.c new file mode 100644 index 0000000..0336478 --- /dev/null +++ b/src/dlfcn-diamond-c/main.c @@ -0,0 +1,173 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + * + * dlfcn-diamond-c: Exercises diamond DT_NEEDED graph resolution. + * + * libdiamond.so + * ├── DT_NEEDED libleft.so ──┐ + * └── DT_NEEDED libright.so ──┴── DT_NEEDED libbase.so + * + * A correct loader walks libdiamond.so's dependencies, opens libleft.so, + * recurses into its DT_NEEDED list and opens libbase.so, then resumes + * the outer loop for libright.so — at which point libbase.so is already + * in the registry and must be bound, NOT re-opened. A broken loader + * either: + * (a) re-opens libbase.so, ending up with two copies and two distinct + * `unique_counter` instances, OR + * (b) trips an internal `unreachable!()` / hangs because the same + * file descriptor is now mapped to two registry entries. + * + * Both failure modes are caught by the assertions below. + */ + +#include +#include +#include +#include + +static int tests_passed = 0; +static int tests_failed = 0; + +static void pass(const char *name) +{ + printf(" PASS: %s\n", name); + fflush(stdout); + tests_passed++; +} + +static void fail(const char *name, const char *reason) +{ + printf(" FAIL: %s (%s)\n", name, reason); + fflush(stdout); + tests_failed++; +} + +/* + * Test 0 (control): dlopen libright.so directly. libright has a single + * DT_NEEDED edge to libbase.so, so this exercises the depth-2 chain + * without involving the diamond shape. If this hangs, the issue is + * not specific to diamond resolution. + */ +static void test_chain_dlopen(void) +{ + void *h = dlopen("lib/libright.so", RTLD_NOW); + if (h == NULL) { + fail("dlopen(libright.so) [depth-2]", dlerror()); + return; + } + int (*rb)(void) = (int (*)(void))dlsym(h, "right_bump"); + if (!rb || rb() != 1) { + fail("dlopen(libright.so) [depth-2]", "right_bump returned wrong value"); + dlclose(h); + return; + } + dlclose(h); + pass("dlopen(libright.so) [depth-2 chain]"); +} + +/* + * Test 1: dlopen(libdiamond.so) must succeed (not hang, not error). + * Successful return implies the loader walked the entire diamond + * graph without re-opening libbase.so or otherwise dead-locking. + */ +static void test_diamond_dlopen(void) +{ + void *h = dlopen("lib/libdiamond.so", RTLD_NOW); + if (h == NULL) { + fail("dlopen(libdiamond.so)", dlerror()); + return; + } + + int (*dl) (void) = (int (*)(void))dlsym(h, "diamond_left"); + int (*dr) (void) = (int (*)(void))dlsym(h, "diamond_right"); + int (*ob) (void) = (int (*)(void))dlsym(h, "diamond_observe"); + if (!dl || !dr || !ob) { + fail("dlopen(libdiamond.so)", "missing diamond_* symbols"); + dlclose(h); + return; + } + + /* Sanity: each arm can run code from libbase.so. */ + int left_val = dl(); /* expected 1 (counter was 0). */ + int right_val = dr(); /* expected 2 IF libbase shared. */ + int observed = ob(); /* expected 2 IF libbase shared. */ + + if (left_val != 1) { + char reason[64]; + snprintf(reason, sizeof(reason), "left_bump=%d, expected 1", left_val); + fail("dlopen(libdiamond.so)", reason); + dlclose(h); + return; + } + if (right_val != 2 || observed != 2) { + /* This is the giveaway for a duplicate libbase.so: libleft and + * libright each see their own private `unique_counter` instead + * of sharing one. */ + char reason[128]; + snprintf(reason, sizeof(reason), + "right_bump=%d (expected 2), observed=%d (expected 2) " + "-- libbase.so was loaded twice", + right_val, observed); + fail("dlopen(libdiamond.so)", reason); + dlclose(h); + return; + } + + dlclose(h); + pass("dlopen(libdiamond.so)"); +} + +/* + * Test 2: Second dlopen returns the same handle (libdiamond.so is + * already in the registry). + */ +static void test_diamond_reopen(void) +{ + void *h1 = dlopen("lib/libdiamond.so", RTLD_NOW); + if (h1 == NULL) { + fail("re-dlopen(libdiamond.so)", dlerror()); + return; + } + + void *h2 = dlopen("lib/libdiamond.so", RTLD_NOW); + if (h2 == NULL) { + fail("re-dlopen(libdiamond.so)", dlerror()); + dlclose(h1); + return; + } + + if (h1 != h2) { + fail("re-dlopen(libdiamond.so)", "second dlopen returned a different handle"); + dlclose(h1); + dlclose(h2); + return; + } + + dlclose(h1); + dlclose(h2); + pass("re-dlopen(libdiamond.so) returns same handle"); +} + +int main(int argc, const char *argv[]) +{ + (void)argc; + (void)argv; + + printf("=== dlfcn diamond DT_NEEDED tests ===\n"); + fflush(stdout); + + test_chain_dlopen(); + test_diamond_dlopen(); + test_diamond_reopen(); + + printf("\n%d passed, %d failed\n", tests_passed, tests_failed); + fflush(stdout); + + if (tests_failed == 0) { + const char *magic = "ok"; + write(STDOUT_FILENO, magic, 3); + } + + return tests_failed > 0 ? 1 : 0; +} diff --git a/src/dlfcn-init-runpath-c/Makefile b/src/dlfcn-init-runpath-c/Makefile new file mode 100644 index 0000000..70002f3 --- /dev/null +++ b/src/dlfcn-init-runpath-c/Makefile @@ -0,0 +1,44 @@ +# Copyright(c) The Maintainers of Nanvix. +# Licensed under the MIT License. +# +# dlfcn-init-runpath-c: Tests for `.init_array` / `.fini_array` +# constructor and destructor invocation, and for DT_RUNPATH-driven +# DT_NEEDED dependency search. + +PROGRAM_NAME := dlfcn-init-runpath-c + +SOURCES := $(wildcard *.c) +OBJECTS := $(SOURCES:.c=.o) +BINARY := $(PROGRAM_NAME).elf + +all: $(OBJECTS) libs-all + $(CC) $(LDFLAGS) -pie -rdynamic -Wl,--no-dynamic-linker $(OBJECTS) $(LIBRARIES) -o $(BINARIES_DIR)/$(BINARY) + +clean: libs-clean + rm -f $(OBJECTS) + rm -f $(BINARIES_DIR)/$(BINARY) + +# libctor.so - constructor/destructor witness, used by the .init_array tests. +# libchild.so - dependency of libparent.so. Will be staged into +# lib/subdir/ at ramfs time so it is only reachable via +# libparent.so's DT_RUNPATH (z.py SUITE_RAMFS_LIBS). +# libparent.so - DT_NEEDED=libchild.so, DT_RUNPATH=lib/subdir/. +# +# `--enable-new-dtags` ensures the linker emits DT_RUNPATH (not the +# deprecated DT_RPATH) for libparent.so, matching what modern +# toolchains produce by default. +libs-all: + $(CC) libs/ctor.c -shared -fPIC -o $(LIBRARIES_DIR)/libctor.so + $(CC) libs/subdir/child.c -shared -fPIC -o $(LIBRARIES_DIR)/libchild.so + $(CC) libs/parent.c -shared -fPIC \ + -L$(LIBRARIES_DIR) -lchild \ + -Wl,--enable-new-dtags,-rpath,lib/subdir \ + -o $(LIBRARIES_DIR)/libparent.so + +libs-clean: + rm -f $(LIBRARIES_DIR)/libctor.so + rm -f $(LIBRARIES_DIR)/libchild.so + rm -f $(LIBRARIES_DIR)/libparent.so + +%.o: %.c + $(CC) $(CFLAGS) -fPIE $< -c -o $@ diff --git a/src/dlfcn-init-runpath-c/libs/ctor.c b/src/dlfcn-init-runpath-c/libs/ctor.c new file mode 100644 index 0000000..5033ae2 --- /dev/null +++ b/src/dlfcn-init-runpath-c/libs/ctor.c @@ -0,0 +1,38 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * libctor.so - shared library that exercises `.init_array` and + * `.fini_array` semantics required by the System V gABI. + * + * The constructor sets `ctor_ran` to a known sentinel value, and the + * destructor writes a different sentinel into the test program's + * `g_dtor_ran` global so that observation outlives the unloaded + * library. `g_dtor_ran` is declared `extern` here and resolved by the + * Nanvix loader's global symbol table (the main executable was linked + * with `-rdynamic`). + */ + +/* Public state exposed via dlsym so the test can read it after dlopen. */ +volatile int ctor_ran = 0; + +/* Main executable owns the destructor witness. */ +extern volatile int g_dtor_ran; + +static void __attribute__((constructor)) my_ctor(void) +{ + ctor_ran = 0xC70A; +} + +static void __attribute__((destructor)) my_dtor(void) +{ + g_dtor_ran = 0xD70A; +} + +/* Sanity entry point so the test can confirm dlsym still works. */ +int ctor_value(void) +{ + return 42; +} diff --git a/src/dlfcn-init-runpath-c/libs/parent.c b/src/dlfcn-init-runpath-c/libs/parent.c new file mode 100644 index 0000000..bab54cb --- /dev/null +++ b/src/dlfcn-init-runpath-c/libs/parent.c @@ -0,0 +1,19 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * libparent.so - shared library whose DT_NEEDED entry points at + * libchild.so, but whose runtime search path (DT_RUNPATH) is + * `lib/subdir/`. The Nanvix loader must consult DT_RUNPATH before + * falling back to the default `lib/` directory, otherwise libchild.so + * will not be located and dlopen will fail. + */ + +extern int child_value(int x); + +int parent_value(int x) +{ + return child_value(x) * 2; +} diff --git a/src/dlfcn-init-runpath-c/libs/subdir/child.c b/src/dlfcn-init-runpath-c/libs/subdir/child.c new file mode 100644 index 0000000..2cdb6fc --- /dev/null +++ b/src/dlfcn-init-runpath-c/libs/subdir/child.c @@ -0,0 +1,16 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * libchild.so - dependency of libparent.so. Installed under a + * non-default directory (`lib/subdir/`) so the only way the loader + * can find it via the DT_NEEDED edge in libparent.so is by honouring + * libparent's DT_RUNPATH. + */ + +int child_value(int x) +{ + return x + 7; +} diff --git a/src/dlfcn-init-runpath-c/main.c b/src/dlfcn-init-runpath-c/main.c new file mode 100644 index 0000000..64d2a7d --- /dev/null +++ b/src/dlfcn-init-runpath-c/main.c @@ -0,0 +1,146 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * dlfcn-init-runpath-c: Tests for `.init_array` / `.fini_array` + * constructor and destructor invocation, and for DT_RUNPATH-driven + * dependency search. + * + * Test 1 (constructor): dlopen libctor.so, dlsym `ctor_ran`, and + * confirm the constructor sentinel was written. + * + * Test 2 (destructor): dlclose libctor.so and confirm the destructor + * wrote its sentinel into the main executable's `g_dtor_ran` global. + * `g_dtor_ran` must be exported via -rdynamic so the loader's global + * symbol table can satisfy the `extern volatile int g_dtor_ran;` + * reference in the library. + * + * Test 3 (DT_RUNPATH): dlopen lib/libparent.so, which has + * DT_NEEDED=libchild.so and DT_RUNPATH=lib/subdir/. The loader must + * probe DT_RUNPATH before the default `lib/` directory; otherwise + * libchild.so is not located. + */ + +#include +#include +#include +#include + +/* Witness for libctor.so's destructor — defined here so it survives the + * library being unloaded. Marked volatile to keep the optimiser away. + */ +volatile int g_dtor_ran = 0; + +static int tests_passed = 0; +static int tests_failed = 0; + +static void pass(const char *name) +{ + printf(" PASS: %s\n", name); + fflush(stdout); + tests_passed++; +} + +static void fail(const char *name, const char *reason) +{ + printf(" FAIL: %s (%s)\n", name, reason); + fflush(stdout); + tests_failed++; +} + +/* + * Test 1: Constructor in `.init_array` must run before dlopen returns. + */ +static void test_init_array(void) +{ + void *h = dlopen("lib/libctor.so", RTLD_NOW); + if (h == NULL) { + fail("init_array fires on dlopen", dlerror()); + return; + } + + volatile int *ctor_ran = (volatile int *)dlsym(h, "ctor_ran"); + if (ctor_ran == NULL) { + fail("init_array fires on dlopen", "ctor_ran symbol missing"); + dlclose(h); + return; + } + + if (*ctor_ran != 0xC70A) { + fail("init_array fires on dlopen", "constructor sentinel not set"); + dlclose(h); + return; + } + + /* Sanity check: ordinary symbol resolution still works after init. */ + int (*fn)(void) = NULL; + *(void **)(&fn) = dlsym(h, "ctor_value"); + if (fn == NULL || fn() != 42) { + fail("init_array fires on dlopen", "ctor_value() wrong"); + dlclose(h); + return; + } + + /* Reset the dtor witness so test_fini_array measures a fresh signal. */ + g_dtor_ran = 0; + dlclose(h); + + pass("init_array fires on dlopen"); + + /* Bridge directly into the destructor test while the witness is fresh. */ + if (g_dtor_ran != 0xD70A) { + fail("fini_array fires on dlclose", "destructor sentinel not set"); + return; + } + pass("fini_array fires on dlclose"); +} + +/* + * Test 3: DT_RUNPATH must be consulted when resolving DT_NEEDED bare + * names. libparent.so depends on libchild.so but libchild.so only + * exists under lib/subdir/, which is libparent's DT_RUNPATH. + */ +static void test_dt_runpath(void) +{ + void *h = dlopen("lib/libparent.so", RTLD_NOW); + if (h == NULL) { + fail("DT_RUNPATH dependency search", dlerror()); + return; + } + + int (*fn)(int) = NULL; + *(void **)(&fn) = dlsym(h, "parent_value"); + if (fn == NULL || fn(5) != 24) { + /* parent_value(5) = child_value(5) * 2 = (5 + 7) * 2 = 24 */ + fail("DT_RUNPATH dependency search", "parent_value() wrong"); + dlclose(h); + return; + } + + dlclose(h); + pass("DT_RUNPATH dependency search"); +} + +int main(int argc, const char *argv[]) +{ + (void)argc; + (void)argv; + + printf("=== dlfcn init_array + DT_RUNPATH tests ===\n"); + fflush(stdout); + + test_init_array(); + test_dt_runpath(); + + printf("\n%d passed, %d failed\n", tests_passed, tests_failed); + fflush(stdout); + + if (tests_failed == 0) { + const char *magic = "ok"; + write(STDOUT_FILENO, magic, 3); + } + + return tests_failed > 0 ? 1 : 0; +}