diff --git a/.nanvix/z.py b/.nanvix/z.py index 5a2a4bb..b10e4ad 100644 --- a/.nanvix/z.py +++ b/.nanvix/z.py @@ -65,6 +65,7 @@ "dlfcn-global-c", "dlfcn-needed-c", "dlfcn-pie-c", + "dlfcn-weak-c", "echo-c", "echo-cpp", "file-c", @@ -98,6 +99,7 @@ STANDALONE_ONLY_SUITES = [ "dlfcn-c", "dlfcn-pie-c", + "dlfcn-weak-c", ] # Suites that require host networking (passed as -allow-host-networking to nanvixd). @@ -110,6 +112,15 @@ SUITE_RAMFS_LIBS: dict[str, list[tuple[str, str]]] = { "dlfcn-c": [("libmul.so", "lib/libmul.so")], "dlfcn-pie-c": [("libmul-pie.so", "lib/libmul-pie.so")], + "dlfcn-weak-c": [ + ("libweak-func-resolved.so", "lib/libweak-func-resolved.so"), + ("libweak-func-missing.so", "lib/libweak-func-missing.so"), + ("libweak-data-resolved.so", "lib/libweak-data-resolved.so"), + ("libweak-data-missing.so", "lib/libweak-data-missing.so"), + ("libweak-plt-resolved.so", "lib/libweak-plt-resolved.so"), + ("libweak-plt-missing.so", "lib/libweak-plt-missing.so"), + ("libstrong-missing.so", "lib/libstrong-missing.so"), + ], } # Docker image for cross-compilation. diff --git a/src/Makefile b/src/Makefile index 45cb2f9..a5231f1 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-global-c dlfcn-needed-c dlfcn-pie-c dlfcn-weak-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 @@ -91,7 +91,7 @@ $(BINARIES_DIR): #=============================================================================== # Test suites that can run via plain nanvixd invocation. -# dlfcn-c and dlfcn-pie-c need shared libraries inside the VM filesystem. +# dlfcn-c, dlfcn-pie-c, and dlfcn-weak-c need shared libraries inside the VM filesystem. # file-c needs VFS access (tests open/read/write on files). # network-c needs network infrastructure (sockets). # misc-c needs environment variable setup (NANVIX_TEST). diff --git a/src/dlfcn-weak-c/Makefile b/src/dlfcn-weak-c/Makefile new file mode 100644 index 0000000..68325db --- /dev/null +++ b/src/dlfcn-weak-c/Makefile @@ -0,0 +1,109 @@ +# Copyright(c) The Maintainers of Nanvix. +# Licensed under the MIT License. + +PROGRAM_NAME := dlfcn-weak-c + +BINARIES_DIR ?= $(CURDIR)/../../build +LIBRARIES_DIR ?= $(BINARIES_DIR) +READELF ?= readelf + +ifeq ($(OS),Windows_NT) +define REMOVE +if exist "$(1)" del /F /Q "$(1)" +endef +else +define REMOVE +rm -f $(1) +endef +endif + +SOURCES := $(wildcard *.c) +OBJECTS := $(SOURCES:.c=.o) +BINARY := $(PROGRAM_NAME).elf + +# Relocation+symbol assertions for the produced .so files. +# +# We assert both the relocation type and the symbol name (and where +# meaningful, that the symbol binding is WEAK / UND) so that fixtures cannot +# silently drift to exercising the wrong symbol. +# +# Weak function refs guarded by `if (&fn) fn();` compile to a GOT load + +# indirect call, so only R_386_GLOB_DAT is emitted. Weak data refs go through +# the GOT for the same reason. Strong undefined function calls go through +# the PLT and produce R_386_JUMP_SLOT. Weak undefined function calls without +# a NULL check (the weak-PLT fixtures below) also produce R_386_JUMP_SLOT, +# allowing us to exercise the loader's handling of weak `JUMP_SLOT`. + +# Macro: assert that `$(1)` contains a `R_386_` relocation against +# the symbol named `$(2)`. Uses `readelf -rW` so symbol names are not +# truncated, and `grep -E` so the pattern stays robust against extra columns. +define ASSERT_RELOC +@if ! $(READELF) -rW $(1) | grep -E -q '$(2)[[:space:]].*[[:space:]]$(3)$$'; then \ + echo "Error: expected $(2) relocation against $(3) in $(1)"; \ + $(READELF) -rW $(1); \ + exit 1; \ +fi +endef + +# Macro: assert that `$(1)` declares `$(2)` as an undefined dynamic symbol +# with binding `$(3)` (WEAK or GLOBAL). We deliberately do not check the +# symbol type: the linker may emit `NOTYPE` for undefined function refs that +# carry no in-module definition, while OBJECT/FUNC are reserved for symbols +# with a known kind. The intended *use* is verified by the relocation-type +# assertion below; here we only prove the symbol is undefined and weak/global. +define ASSERT_DYNSYM +@if ! $(READELF) -WsD $(1) | grep -E -q '$(3)[[:space:]]+(DEFAULT|HIDDEN|PROTECTED|INTERNAL)[[:space:]]+UND[[:space:]]+$(2)$$'; then \ + echo "Error: expected $(3) UND dynamic symbol $(2) in $(1)"; \ + $(READELF) -WsD $(1); \ + exit 1; \ +fi +endef + +all: $(OBJECTS) libs-all + $(CC) $(LDFLAGS) -pie -rdynamic -Wl,--no-dynamic-linker $(OBJECTS) $(LIBRARIES) -o $(BINARIES_DIR)/$(BINARY) + +clean: libs-clean + $(call REMOVE,$(OBJECTS)) + $(call REMOVE,$(BINARIES_DIR)/$(BINARY)) + +libs-all: + # Weak function ref with NULL-guard (R_386_GLOB_DAT) — resolved variant. + $(CC) $(CFLAGS) libs/weak_func.c -shared -fPIC -DCALLBACK_NAME=main_callback -o $(LIBRARIES_DIR)/libweak-func-resolved.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libweak-func-resolved.so,main_callback,WEAK) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libweak-func-resolved.so,R_386_GLOB_DAT,main_callback) + # Weak function ref with NULL-guard (R_386_GLOB_DAT) — missing variant. + $(CC) $(CFLAGS) libs/weak_func.c -shared -fPIC -DCALLBACK_NAME=missing_callback -o $(LIBRARIES_DIR)/libweak-func-missing.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libweak-func-missing.so,missing_callback,WEAK) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libweak-func-missing.so,R_386_GLOB_DAT,missing_callback) + # Weak data ref (R_386_GLOB_DAT) — resolved variant. + $(CC) $(CFLAGS) libs/weak_data.c -shared -fPIC -DWEAK_DATA_NAME=weak_data -o $(LIBRARIES_DIR)/libweak-data-resolved.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libweak-data-resolved.so,weak_data,WEAK) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libweak-data-resolved.so,R_386_GLOB_DAT,weak_data) + # Weak data ref (R_386_GLOB_DAT) — missing variant. + $(CC) $(CFLAGS) libs/weak_data.c -shared -fPIC -DWEAK_DATA_NAME=missing_weak_data -o $(LIBRARIES_DIR)/libweak-data-missing.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libweak-data-missing.so,missing_weak_data,WEAK) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libweak-data-missing.so,R_386_GLOB_DAT,missing_weak_data) + # Weak function ref WITHOUT NULL-guard (R_386_JUMP_SLOT) — resolved variant. + $(CC) $(CFLAGS) libs/weak_func_plt.c -shared -fPIC -DCALLBACK_NAME=main_callback -o $(LIBRARIES_DIR)/libweak-plt-resolved.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libweak-plt-resolved.so,main_callback,WEAK) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libweak-plt-resolved.so,R_386_JUMP_SLOT,main_callback) + # Weak function ref WITHOUT NULL-guard (R_386_JUMP_SLOT) — missing variant. + $(CC) $(CFLAGS) libs/weak_func_plt.c -shared -fPIC -DCALLBACK_NAME=missing_plt_callback -o $(LIBRARIES_DIR)/libweak-plt-missing.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libweak-plt-missing.so,missing_plt_callback,WEAK) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libweak-plt-missing.so,R_386_JUMP_SLOT,missing_plt_callback) + # Strong undefined function call (R_386_JUMP_SLOT) — regression guard. + $(CC) $(CFLAGS) libs/strong_missing.c -shared -fPIC -o $(LIBRARIES_DIR)/libstrong-missing.so + $(call ASSERT_DYNSYM,$(LIBRARIES_DIR)/libstrong-missing.so,strong_missing,GLOBAL) + $(call ASSERT_RELOC,$(LIBRARIES_DIR)/libstrong-missing.so,R_386_JUMP_SLOT,strong_missing) + +libs-clean: + $(call REMOVE,$(LIBRARIES_DIR)/libweak-func-resolved.so) + $(call REMOVE,$(LIBRARIES_DIR)/libweak-func-missing.so) + $(call REMOVE,$(LIBRARIES_DIR)/libweak-data-resolved.so) + $(call REMOVE,$(LIBRARIES_DIR)/libweak-data-missing.so) + $(call REMOVE,$(LIBRARIES_DIR)/libweak-plt-resolved.so) + $(call REMOVE,$(LIBRARIES_DIR)/libweak-plt-missing.so) + $(call REMOVE,$(LIBRARIES_DIR)/libstrong-missing.so) + +%.o: %.c + $(CC) $(CFLAGS) -fPIE $< -c -o $@ diff --git a/src/dlfcn-weak-c/libs/strong_missing.c b/src/dlfcn-weak-c/libs/strong_missing.c new file mode 100644 index 0000000..f1c8a45 --- /dev/null +++ b/src/dlfcn-weak-c/libs/strong_missing.c @@ -0,0 +1,11 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +extern int strong_missing(void); + +int call_strong(void) +{ + return (strong_missing()); +} diff --git a/src/dlfcn-weak-c/libs/weak_data.c b/src/dlfcn-weak-c/libs/weak_data.c new file mode 100644 index 0000000..e2da7d4 --- /dev/null +++ b/src/dlfcn-weak-c/libs/weak_data.c @@ -0,0 +1,21 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +#include + +#ifndef WEAK_DATA_NAME +#define WEAK_DATA_NAME weak_data +#endif + +extern int WEAK_DATA_NAME __attribute__((weak)); + +int read_weak_data(void) +{ + if (&WEAK_DATA_NAME != NULL) { + return (WEAK_DATA_NAME); + } + + return (-1); +} diff --git a/src/dlfcn-weak-c/libs/weak_func.c b/src/dlfcn-weak-c/libs/weak_func.c new file mode 100644 index 0000000..263ca33 --- /dev/null +++ b/src/dlfcn-weak-c/libs/weak_func.c @@ -0,0 +1,21 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +#include + +#ifndef CALLBACK_NAME +#define CALLBACK_NAME main_callback +#endif + +extern int CALLBACK_NAME(int) __attribute__((weak)); + +int try_callback(void) +{ + if (&CALLBACK_NAME != NULL) { + return (CALLBACK_NAME(7)); + } + + return (-1); +} diff --git a/src/dlfcn-weak-c/libs/weak_func_plt.c b/src/dlfcn-weak-c/libs/weak_func_plt.c new file mode 100644 index 0000000..a658d69 --- /dev/null +++ b/src/dlfcn-weak-c/libs/weak_func_plt.c @@ -0,0 +1,33 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * Companion to libs/weak_func.c that exercises the *PLT* path for weak + * undefined function references. + * + * Calling the weak symbol without a preceding `&fn != NULL` check lets the + * compiler route the call through the PLT, producing a `R_386_JUMP_SLOT` + * relocation rather than the GOT-indirect `R_386_GLOB_DAT` emitted by + * libs/weak_func.c. Together the two helpers cover both relocation classes + * the dynamic loader must handle for weak undefined symbols. + * + * Safety note: the `missing` variant of this helper produces a `.so` whose + * weak symbol resolves to address zero at runtime. The main test driver + * MUST NOT invoke `call_weak_unchecked` against that variant — doing so + * would jump to NULL. Resolving `dlsym(handle, "call_weak_unchecked")` is + * safe; the goal of the missing variant is only to prove that the loader + * accepts the `.so` under RTLD_NOW despite the weak undefined PLT entry. + */ + +#ifndef CALLBACK_NAME +#define CALLBACK_NAME main_callback +#endif + +extern int CALLBACK_NAME(int) __attribute__((weak)); + +int call_weak_unchecked(int x) +{ + return (CALLBACK_NAME(x)); +} diff --git a/src/dlfcn-weak-c/main.c b/src/dlfcn-weak-c/main.c new file mode 100644 index 0000000..d1b5d33 --- /dev/null +++ b/src/dlfcn-weak-c/main.c @@ -0,0 +1,193 @@ +/* + * Copyright(c) The Maintainers of Nanvix. + * Licensed under the MIT License. + */ + +/* + * Resolved and unresolved weak cases use separate helper shared objects with + * distinct weak symbol names so this single executable can export + * main_callback/weak_data without also satisfying the missing-symbol cases. + * The strong-UND regression uses RTLD_NOW so failure is asserted at dlopen(), + * avoiding an unsafe lazy call into an unresolved strong symbol. + */ + +//================================================================================================== +// Imports +//================================================================================================== + +#include +#include +#include + +//================================================================================================== +// Standalone Functions +//================================================================================================== + +int weak_data = 99; + +int main_callback(int x) +{ + return (x * 10); +} + +static void clear_dlerror(void) +{ + (void)dlerror(); +} + +// Opens a dynamic load library. +static void *open_library(const char *path, int flags) +{ + clear_dlerror(); + + void *handle = dlopen(path, flags); + assert(handle != NULL); + + return (handle); +} + +// Closes a dynamic load library. +static void close_library(void *handle) +{ + assert(dlclose(handle) == 0); +} + +// Resolves a symbol in a dynamic load library. +static void *resolve_symbol(void *handle, const char *symbol) +{ + clear_dlerror(); + + void *sym = dlsym(handle, symbol); + const char *error = dlerror(); + assert(error == NULL); + assert(sym != NULL); + + return (sym); +} + +// Tests if a weak undefined function resolves to the main executable. +static void test_weak_und_function_resolved(const char *path) +{ + void *handle = open_library(path, RTLD_NOW); + + int (*try_callback)(void) = NULL; + *(void **)(&try_callback) = resolve_symbol(handle, "try_callback"); + assert(try_callback != NULL); + assert(try_callback() == 70); + + close_library(handle); +} + +// Tests if a missing weak undefined function resolves to zero. +static void test_weak_und_function_missing(const char *path) +{ + void *handle = open_library(path, RTLD_NOW); + + int (*try_callback)(void) = NULL; + *(void **)(&try_callback) = resolve_symbol(handle, "try_callback"); + assert(try_callback != NULL); + assert(try_callback() == -1); + + close_library(handle); +} + +// Tests if weak undefined data resolves to the main executable. +static void test_weak_und_data_resolved(const char *path) +{ + void *handle = open_library(path, RTLD_NOW); + + int (*read_weak_data)(void) = NULL; + *(void **)(&read_weak_data) = resolve_symbol(handle, "read_weak_data"); + assert(read_weak_data != NULL); + assert(read_weak_data() == 99); + + close_library(handle); +} + +// Tests if missing weak undefined data resolves to zero. +static void test_weak_und_data_missing(const char *path) +{ + void *handle = open_library(path, RTLD_NOW); + + int (*read_weak_data)(void) = NULL; + *(void **)(&read_weak_data) = resolve_symbol(handle, "read_weak_data"); + assert(read_weak_data != NULL); + assert(read_weak_data() == -1); + + close_library(handle); +} + +// Tests if a weak undefined function called via PLT resolves to the main exe. +static void test_weak_und_function_plt_resolved(const char *path) +{ + void *handle = open_library(path, RTLD_NOW); + + int (*call_weak_unchecked)(int) = NULL; + *(void **)(&call_weak_unchecked) = resolve_symbol(handle, "call_weak_unchecked"); + assert(call_weak_unchecked != NULL); + assert(call_weak_unchecked(7) == 70); + + close_library(handle); +} + +// Tests that the loader accepts a .so whose weak undefined PLT slot is missing. +// +// Note: we deliberately resolve, but DO NOT call, `call_weak_unchecked` here. +// Calling it would jump through a PLT entry whose target was zeroed by the +// loader (weak undefined missing → 0), trapping the test. The contract this +// case verifies is purely "dlopen with RTLD_NOW succeeds despite the weak +// undefined symbol", not "the call returns a value". +static void test_weak_und_function_plt_missing(const char *path) +{ + void *handle = open_library(path, RTLD_NOW); + + int (*call_weak_unchecked)(int) = NULL; + *(void **)(&call_weak_unchecked) = resolve_symbol(handle, "call_weak_unchecked"); + assert(call_weak_unchecked != NULL); + + close_library(handle); +} + +// Tests if a missing strong undefined symbol still fails to load. +static void test_strong_und_missing(const char *path) +{ + clear_dlerror(); + + void *handle = dlopen(path, RTLD_NOW); + const char *error = dlerror(); + assert(handle == NULL); + assert(error != NULL); +} + +/** + * @brief Tests STB_WEAK undefined-symbol handling in the dynamic loader. + * + * @param argc Number of command-line arguments (unused). + * @param argv List of command-line arguments (unused). + * + * @returns Always returns zero. If a test fails, the program will abort. + */ +int main(int argc, const char *argv[]) +{ + (void)argc; + (void)argv; + + // Case 5 first: this is the only case that passes today (before the + // STB_WEAK loader change lands). Running it first means a regression in + // the strong-undefined path is reported independently of the weak cases. + test_strong_und_missing("lib/libstrong-missing.so"); + test_weak_und_function_resolved("lib/libweak-func-resolved.so"); + test_weak_und_function_missing("lib/libweak-func-missing.so"); + test_weak_und_data_resolved("lib/libweak-data-resolved.so"); + test_weak_und_data_missing("lib/libweak-data-missing.so"); + test_weak_und_function_plt_resolved("lib/libweak-plt-resolved.so"); + test_weak_und_function_plt_missing("lib/libweak-plt-missing.so"); + + // Write magic string to signal that the test passed. + { + const char *magic_string = "ok"; + write(STDOUT_FILENO, magic_string, 3); + } + + return (0); +}