diff --git a/.nanvix/z.py b/.nanvix/z.py index 0c05364c53ec1..72b51ce1f8595 100644 --- a/.nanvix/z.py +++ b/.nanvix/z.py @@ -98,6 +98,8 @@ _BUILD_OUTPUTS: list[str] = [ "libcrypto.a", "libssl.a", + "libcrypto.so", + "libssl.so", _TEST_ELF, *(f"include/openssl/{h}" for h in _GENERATED_HEADERS), ] @@ -270,10 +272,12 @@ def release(self) -> None: repo = self.repo_root libcrypto = repo / "libcrypto.a" libssl = repo / "libssl.a" + libcrypto_so = repo / "libcrypto.so" + libssl_so = repo / "libssl.so" headers_dir = repo / "include" / "openssl" test_elf = repo / _TEST_ELF - for path in (libcrypto, libssl, headers_dir): + for path in (libcrypto, libssl, libcrypto_so, libssl_so, headers_dir): if not path.exists(): log.fatal( f"release: missing artefact {path}", @@ -300,6 +304,8 @@ def release(self) -> None: # Copy libraries. shutil.copy2(libcrypto, sysroot / "lib" / "libcrypto.a") shutil.copy2(libssl, sysroot / "lib" / "libssl.a") + shutil.copy2(libcrypto_so, sysroot / "lib" / "libcrypto.so") + shutil.copy2(libssl_so, sysroot / "lib" / "libssl.so") # Copy headers. for h in sorted(headers_dir.glob("*.h")): @@ -441,6 +447,8 @@ def _verify_release(self, tarball: Path) -> None: required = { "sysroot/lib/libcrypto.a", "sysroot/lib/libssl.a", + "sysroot/lib/libcrypto.so", + "sysroot/lib/libssl.so", } with tarfile.open(tarball, "r:gz") as tf: members = set(tf.getnames()) diff --git a/Makefile.nanvix b/Makefile.nanvix index 7abd1b6cfc56b..9d43bf0307d5a 100644 --- a/Makefile.nanvix +++ b/Makefile.nanvix @@ -29,10 +29,19 @@ EXE = .elf TEST_SRC := openssl_nanvix_test.c TEST_ELF := openssl_nanvix_test$(EXE) +# Shared library outputs. Linked from the static archives via +# `--whole-archive` after stripping the bss_log.o member that +# references unimplemented POSIX `openlog`/`syslog`/`closelog` (see +# Nanvix newlib does not implement the syslog client API; OpenSSL's +# BIO_s_log -- which we don't use -- is the only consumer). +LIBCRYPTO_SO := libcrypto.so +LIBSSL_SO := libssl.so + # Marker file produced by ./Configure. CONFIGURED_MARKER := .nanvix-configured -_NANVIX_DOCKER_BUILD_GOALS := all build $(CONFIGURED_MARKER) $(TEST_ELF) +_NANVIX_DOCKER_BUILD_GOALS := all build $(CONFIGURED_MARKER) $(TEST_ELF) \ + $(LIBCRYPTO_SO) $(LIBSSL_SO) _NANVIX_GOALS := $(or $(MAKECMDGOALS),$(.DEFAULT_GOAL)) # Ensure required variables are defined. @@ -90,9 +99,10 @@ CONFIGURE_OPTS = \ --openssldir="$(INSTALL_PREFIX)" \ --prefix="$(INSTALL_PREFIX)" \ nanvix \ - no-shared \ + shared \ + no-pinshared \ + enable-trace \ threads \ - no-dso \ no-apps \ no-docs \ no-rdrand \ @@ -104,7 +114,7 @@ CONFIGURE_OPTS = \ # Build Targets # =========================================================================== -all: build $(TEST_ELF) +all: build $(LIBCRYPTO_SO) $(LIBSSL_SO) $(TEST_ELF) $(CONFIGURED_MARKER): $(CONFIGURE_ENV) ./Configure $(CONFIGURE_OPTS) @@ -112,6 +122,55 @@ $(CONFIGURED_MARKER): build: $(CONFIGURED_MARKER) $(MAKE) -j$$(nproc) all + @# Strip bss_log.o so consumers can use --whole-archive libcrypto.a. + @# bss_log.o is the implementation of OpenSSL's BIO_s_log -- a syslog + @# BIO backend that we don't use and that references the POSIX + @# `openlog`/`syslog`/`closelog` symbols Nanvix newlib does not + @# provide. Under normal archive selection nothing references + @# BIO_s_log so bss_log.o is never pulled; under --whole-archive + @# every member gets pulled and the link fails with three undefs. + @# Idempotent. + @if $(AR) t libcrypto.a 2>/dev/null | grep -q '^libcrypto-lib-bss_log\.o$$'; then \ + echo " Stripping libcrypto-lib-bss_log.o from libcrypto.a"; \ + $(AR) d libcrypto.a libcrypto-lib-bss_log.o; \ + $(RANLIB) libcrypto.a; \ + fi + +# Build libcrypto.so from the (PIC-clean) static archive. OpenSSL's +# x86 sources are already position-independent, so no -fPIC at compile +# time is required. libposix/libc/libm symbols are left UND and bind +# at dlopen time against the host executable's .dynsym (same model +# already used by libxml2.so / libffi.so on Nanvix). +# +# `-lgcc` is required because OpenSSL's bignum / hash arithmetic uses +# 64-bit operations on 32-bit i686, and the resulting calls to libgcc +# helpers (__udivdi3, __umoddi3, ...) cannot be resolved against +# python.elf at dlopen time -- libgcc marks them hidden, so they are +# absent from python.elf's .dynsym. Embedding `-lgcc` ships those +# leaf helpers inside libcrypto.so. Only stateless arithmetic helpers +# are pulled in; the structural test below asserts that no stateful +# libgcc symbols (frame registry, unwinder, ifunc CPU dispatch state, +# emutls, ...) leak in. +$(LIBCRYPTO_SO): libcrypto.a + $(CC) -shared -fPIC -nostdlib \ + -Wl,-soname,libcrypto.so -Wl,-z,noexecstack \ + -Wl,--whole-archive libcrypto.a -Wl,--no-whole-archive \ + -lgcc \ + -o $@ + @echo " OK: $@ ($$(wc -c < $@) bytes)" + +# libssl.so depends on libcrypto.so via DT_NEEDED. Order matters: +# libcrypto.so must exist before libssl.so links, so the linker +# resolves -lcrypto against the .so rather than the .a (which would +# bundle libcrypto into libssl.so and defeat the deduplication). +$(LIBSSL_SO): libssl.a $(LIBCRYPTO_SO) + $(CC) -shared -fPIC -nostdlib \ + -Wl,-soname,libssl.so -Wl,-z,noexecstack \ + -Wl,--whole-archive libssl.a -Wl,--no-whole-archive \ + -L. -lcrypto \ + -lgcc \ + -o $@ + @echo " OK: $@ ($$(wc -c < $@) bytes)" # Generate the inline test program source. $(TEST_SRC): @@ -146,6 +205,94 @@ $(TEST_SRC): libcrypto.a libssl.a include/openssl/opensslv.h: @echo " FAIL: $@ not found; run 'build' first"; exit 1 +# Stateful libgcc symbols that MUST NOT be duplicated across DSOs in +# a static-only environment (Nanvix does not ship libgcc_s.so). +# +# NOTE: this check is a WORKAROUND. The long-term fix is to +# eliminate the libgcc dependency from our .so files entirely (by +# shipping libgcc_s.so or by re-exporting helpers from python.elf's +# .dynsym). Tracked in: +# nanvix-todo/eliminate-libgcc-dependency-in-shared-objects.md +# +# Three tiers, all enforced equally: +# +# Tier 1 -- correctness-critical (crashes / wrong unwind / wrong CPU +# dispatch / TLS aliasing / SjLj chain break): +# - Frame-registration registry (unwind-dw2-fde.c) +# - Core unwinder + _Unwind_Get*/Set* (unwind-dw2.c) +# - __gcc_personality_v0 / _sj0 (LSDA reader) +# - i386 CPU feature detection (__cpu_model / __cpu_features2 / +# __cpu_indicator_init) +# - emutls (emutls.c) +# - SjLj exception chain (unwind-sjlj.c) +# +# Tier 2 -- gcov profiling state (libgcov-driver.c). Not currently +# enabled on Nanvix, but listed to catch surprise enablement (e.g. +# someone flips on -fprofile-arcs for a benchmark build): +# - __gcov_master / __gcov_error_file / __gcov_kvp_dynamic_pool* +# - __gcov_{init,exit,dump,reset} mutators +# - (__gcov_root is intentionally per-DSO; NOT blocked) +# +# Tier 3 -- split-stack state (generic-morestack*.c). Not currently +# enabled (Nanvix uses fixed stacks), but listed for the same reason +# as Tier 2: +# - __morestack* / __generic_morestack* / __splitstack_* +# +# See nanvix-todo/libgcc-stateful-symbols-blocklist.md for the full +# audit and source citations. +LIBGCC_STATEFUL_RE := \ +^(__register_frame(_info(_bases|_table(_bases)?)?|_table)?\ +|__deregister_frame(_info(_bases)?)?\ +|__frame_state_for\ +|_Unwind_(Find_FDE|RaiseException|Resume(_or_Rethrow)?|ForcedUnwind\ +|Backtrace|DeleteException|FindEnclosingFunction\ +|Get(GR|IP(Info)?|CFA|LanguageSpecificData|RegionStart|TextRelBase|DataRelBase)\ +|Set(GR|IP))\ +|__gcc_personality_(v0|sj0)\ +|__cpu_(model|indicator_init|features2)\ +|__emutls_(get_address|register_common)\ +|_Unwind_SjLj_(Register|Unregister|RaiseException|ForcedUnwind|Resume(_or_Rethrow)?)\ +|__gcov_(init|exit|dump|reset|master|error_file|kvp_dynamic_pool(_index|_size)?)\ +|__morestack(_block_signals|_unblock_signals|_fail|_release_segments\ +|_load_mmap|_allocate_stack_space|_segments|_current_segment|_initial_sp)?\ +|__generic_(morestack(_set_initial_sp)?|releasestack|findstack)\ +|__splitstack_(find|block_signals|getcontext|setcontext|makecontext\ +|resetcontext|releasecontext|block_signals_context|find_context)\ +)$$ + +# Assert that a .so does not contain any Tier-1 stateful libgcc +# symbols (defined or undefined; we want zero references too, +# because resolving a stateful symbol against a different DSO's copy +# would still create the state-coherency hazard). +# +# $(1): path to the .so to check. +define check_no_stateful_libgcc +@echo " Checking $(1) for stateful libgcc symbols..." +@BAD_DEF=$$($(NANVIX_TOOLCHAIN)/bin/i686-nanvix-nm --defined-only $(1) 2>/dev/null \ + | awk '{print $$NF}' | grep -E '$(LIBGCC_STATEFUL_RE)' || true); \ + BAD_UND=$$($(NANVIX_TOOLCHAIN)/bin/i686-nanvix-nm --undefined-only $(1) 2>/dev/null \ + | awk '{print $$NF}' | grep -E '$(LIBGCC_STATEFUL_RE)' || true); \ + if [ -n "$$BAD_DEF" ]; then \ + echo " FAIL: $(1) bundles stateful libgcc symbols (defined):"; \ + echo "$$BAD_DEF" | sed 's/^/ /'; \ + echo " These carry process-shared state and must not be"; \ + echo " duplicated across DSOs. See"; \ + echo " nanvix-todo/libgcc-stateful-symbols-blocklist.md"; \ + exit 1; \ + fi; \ + if [ -n "$$BAD_UND" ]; then \ + echo " FAIL: $(1) references stateful libgcc symbols (UND):"; \ + echo "$$BAD_UND" | sed 's/^/ /'; \ + echo " These would resolve at dlopen time against whichever"; \ + echo " DSO loaded its own copy first, creating state-coherency"; \ + echo " hazards. The fix is to export them from python.elf via"; \ + echo " an explicit -Wl,-u + visibility=default wrapper, or to"; \ + echo " remove the source dependency that pulled them in."; \ + exit 1; \ + fi; \ + echo " OK: $(1) free of stateful libgcc symbols" +endef + $(TEST_ELF): $(TEST_SRC) libcrypto.a libssl.a include/openssl/opensslv.h $(CC) -O2 \ -I$(CURDIR)/include \ @@ -153,6 +300,8 @@ $(TEST_ELF): $(TEST_SRC) libcrypto.a libssl.a include/openssl/opensslv.h -L$(CURDIR) -lssl -lcrypto \ $(NANVIX_LDFLAGS) $(NANVIX_LIBS) \ -o $@ + $(call check_no_stateful_libgcc,$(LIBCRYPTO_SO)) + $(call check_no_stateful_libgcc,$(LIBSSL_SO)) # =========================================================================== # Clean