From 2c245c7e4e9549a4a0e9535dc2e9650e20e109c3 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 7 Oct 2026 23:22:03 +0200 Subject: [PATCH] Link libprism.so with -Bsymbolic on ELF platforms The shared library exports the pm_* API for the FFI backend, and like any ELF shared object its internal calls to those exported functions go through the PLT, so the dynamic loader resolves them by searching the global scope before the library itself. When two copies of libprism are loaded in the same process and one of them was opened with RTLD_GLOBAL, the second copy's internal calls silently bind to the first copy. With different prism versions this mixes incompatible code and data layouts. Two copies are a realistic situation: the interpreter can ship one copy (as JRuby and TruffleRuby do for their bundled prism) while a newer prism gem builds another, and the proposal to expose the interpreter's parser as Ruby::Prism makes both being loaded at once a supported setup. -Wl,-Bsymbolic makes the linker bind the library's references to its own definitions at link time, so each copy is self-contained regardless of the flags it was loaded with. The symbols stay exported, so dlsym and FFI are unaffected. Mach-O's two-level namespace and PE imports already behave this way, so the flag is only added when SOEXT is "so". Verified on Linux by loading a prism 1.8.0 libprism.so with RTLD_GLOBAL and then this build: LD_DEBUG=bindings shows 25 cross-library pm_* bindings without the flag and none with it. Co-Authored-By: Claude Fable 5.1 --- Makefile | 11 ++++++++++- docs/build_system.md | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 53df8c392f..3d104e2108 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,15 @@ WASI_SDK_PATH := /opt/wasi-sdk # the rest of the C API along with the wasi-libc code reachable from it. WASM_EXPORTS := -Wl,--export=malloc,--export=free,--export=pm_buffer_new,--export=pm_buffer_value,--export=pm_buffer_length,--export=pm_buffer_free,--export=pm_serialize_parse +# Bind the library's own references to its own definitions at link time, so +# that the functions exported for FFI cannot be interposed by another copy of +# libprism loaded in the same process (for example the copy built into the +# interpreter next to the one built by a prism gem, when either copy was loaded +# with RTLD_GLOBAL). Mach-O and PE do this by default; only ELF needs the flag. +ifeq ($(SOEXT),so) +SHARED_LDFLAGS := -Wl,-Bsymbolic +endif + MAKEDIRS ?= mkdir -p RMALL ?= rm -f -r @@ -41,7 +50,7 @@ playground: doc/playground build/libprism.$(SOEXT): $(SHARED_OBJECTS) $(ECHO) "linking $@ with $(CC)" - $(Q) $(CC) $(DEBUG_FLAGS) $(CFLAGS) -shared -o $@ $(SHARED_OBJECTS) + $(Q) $(CC) $(DEBUG_FLAGS) $(CFLAGS) -shared $(SHARED_LDFLAGS) -o $@ $(SHARED_OBJECTS) build/libprism.a: $(STATIC_OBJECTS) $(ECHO) "building $@ with $(AR)" diff --git a/docs/build_system.md b/docs/build_system.md index 40db4a17fc..c141a7b776 100644 --- a/docs/build_system.md +++ b/docs/build_system.md @@ -97,6 +97,7 @@ If you want to build prism as a shared library and link against it, you should c * `-fPIC -shared` - Compile as a shared library * `-DPRISM_EXPORT_SYMBOLS` - Export the symbols (by default nothing is exported) +* `-Wl,-Bsymbolic` (ELF platforms) - Bind the library's internal calls to its own exported functions at link time, so that they cannot be redirected to another copy of libprism loaded in the same process #### Flags