Skip to content
This repository was archived by the owner on Jun 24, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .nanvix/z.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"c-bindings",
"dlfcn-c",
"dlfcn-global-c",
"dlfcn-init-runpath-c",
"dlfcn-needed-c",
"dlfcn-pie-c",
"echo-c",
Expand Down Expand Up @@ -101,6 +102,7 @@
# Suites that require ramfs-bundled shared libraries and only run in standalone mode.
STANDALONE_ONLY_SUITES = [
"dlfcn-c",
"dlfcn-init-runpath-c",
"dlfcn-pie-c",
]

Expand All @@ -113,6 +115,11 @@
# 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-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")],
}

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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-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))
Expand Down
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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-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
Expand Down
69 changes: 69 additions & 0 deletions src/dlfcn-init-runpath-c/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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
@if ! $(READELF) -d $(LIBRARIES_DIR)/libctor.so | grep -q '(INIT_ARRAY)'; then \
echo "Error: INIT_ARRAY missing in $(LIBRARIES_DIR)/libctor.so"; \
$(READELF) -d $(LIBRARIES_DIR)/libctor.so; \
exit 1; \
fi
@if ! $(READELF) -d $(LIBRARIES_DIR)/libctor.so | grep -q '(FINI_ARRAY)'; then \
echo "Error: FINI_ARRAY missing in $(LIBRARIES_DIR)/libctor.so"; \
$(READELF) -d $(LIBRARIES_DIR)/libctor.so; \
exit 1; \
fi
$(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
@if ! $(READELF) -d $(LIBRARIES_DIR)/libparent.so | grep -E -q '\(NEEDED\)[[:space:]]+Shared library: \[libchild\.so\]'; then \
echo "Error: DT_NEEDED libchild.so missing in $(LIBRARIES_DIR)/libparent.so"; \
$(READELF) -d $(LIBRARIES_DIR)/libparent.so; \
exit 1; \
fi
@if ! $(READELF) -d $(LIBRARIES_DIR)/libparent.so | grep -E -q '\(RUNPATH\)[[:space:]]+Library runpath: \[lib/subdir\]'; then \
echo "Error: DT_RUNPATH lib/subdir missing in $(LIBRARIES_DIR)/libparent.so"; \
$(READELF) -d $(LIBRARIES_DIR)/libparent.so; \
exit 1; \
fi
@if $(READELF) -d $(LIBRARIES_DIR)/libparent.so | grep -q '(RPATH)'; then \
echo "Error: legacy DT_RPATH present in $(LIBRARIES_DIR)/libparent.so (expected DT_RUNPATH only)"; \
$(READELF) -d $(LIBRARIES_DIR)/libparent.so; \
exit 1; \
fi

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 $@
38 changes: 38 additions & 0 deletions src/dlfcn-init-runpath-c/libs/ctor.c
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions src/dlfcn-init-runpath-c/libs/parent.c
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions src/dlfcn-init-runpath-c/libs/subdir/child.c
Original file line number Diff line number Diff line change
@@ -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;
}
146 changes: 146 additions & 0 deletions src/dlfcn-init-runpath-c/main.c
Original file line number Diff line number Diff line change
@@ -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 <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

/* 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;
}
Loading