From a7a666f9279c12d4fa239249c97459032801aebb Mon Sep 17 00:00:00 2001 From: Alejandro Cabeza Romero Date: Mon, 18 May 2026 16:41:34 +0200 Subject: [PATCH] Localize internal C++ symbols to avoid resolution conflicts. --- .github/resources/witness-generator/Makefile | 21 ++++++++- CONTRIBUTING.md | 49 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.github/resources/witness-generator/Makefile b/.github/resources/witness-generator/Makefile index 5733fb3..adffcf7 100644 --- a/.github/resources/witness-generator/Makefile +++ b/.github/resources/witness-generator/Makefile @@ -52,15 +52,32 @@ windows: $(BIN) windows-lib: CXXFLAGS=$(CXXFLAGS_COMMON) -fPIC -I/include -Duint="unsigned int" windows-lib: $(LIB) +# Localizes internal C++ symbols so multiple circuit libraries can coexist in the same binary without symbol conflicts. +# See CONTRIBUTING.md § "Symbol Isolation". +# Default derived from PROJECT; override with PUBLIC_SYMBOLS= to skip localization. +PUBLIC_SYMBOLS ?= $(PROJECT)_generate_witness $(PROJECT)_generate_witness_from_files +LOCAL_OBJ := $(PROJECT)_local.o # Intermediate object file for symbol localization + +UNAME := $(shell uname -s) + # ---- Rules ---- $(BIN): $(COMMON_OBJS) $(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@ $(LIB): $(LIB_OBJS) - ar rcs $@ $^ +ifeq ($(strip $(PUBLIC_SYMBOLS)),) + ar rcs $@ $^ # Localization disabled +else ifeq ($(UNAME),Darwin) + ar rcs $@ $^ # On macOS there already is a two-level namespace so conflicts don't occur +else + ld -r -o $(LOCAL_OBJ) $^ + objcopy $(foreach s,$(PUBLIC_SYMBOLS),--keep-global-symbol=$(s)) $(LOCAL_OBJ) + ar rcs $@ $(LOCAL_OBJ) + rm $(LOCAL_OBJ) +endif %.o: %.cpp $(DEPS_HPP) $(CXX) $(CXXFLAGS) -c $< -o $@ clean: - rm -f $(COMMON_OBJS) $(LIB_ONLY_OBJS) $(BIN) $(LIB) + rm -f $(COMMON_OBJS) $(LIB_ONLY_OBJS) $(BIN) $(LIB) $(LOCAL_OBJ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 884bd75..e5db59d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,55 @@ When bumping the stable toolchain, update `channel` in `rust-toolchain.toml`. Th --- +## Symbol Isolation in Circuit Libraries + +Each circuit (PoQ, PoL, PoC, Signature) is compiled into a static archive (`libpoq.a`, `libpol.a`, etc.). +All four archives share the same internal C++ runtime — `loadCircuit`, `get_size_of_witness`, the `fr_*` field +arithmetic functions, `calcwit_*` functions, and others. They are compiled from the same source files but with +**different constant values per circuit** (e.g. `get_size_of_witness()` returns 18149 for PoQ and 20531 for PoL). + +### The Problem + +When two or more circuit libraries are linked into the same binary, the GNU linker silently picks the first definition +it encounters for each symbol and discards the rest. +No error, no warning. +The result is that one circuit's constants end up hardwired into functions shared by both circuits, corrupting witness +parsing. +In practice: the wrong `get_size_of_witness()` value causes `loadCircuit` to compute an incorrect buffer size, `pu32` +walks off the end of the buffer, reads garbage as a length field, and the subsequent `memcpy` reads past the stack guard +page, which results in a **SIGSEGV**. + +### The Fix + +The Makefile's `$(LIB)` rule uses a two-step process on Linux and Windows to localize all internal symbols before +archiving: + +1. **Partial link** (`ld -r`): merges all `.o` files into a single relocatable object without producing a final +executable. +No symbols are resolved yet; this is consolidation only. +2. **Symbol localization** (`objcopy --keep-global-symbol`): demotes every global symbol to local *except* the circuit's +two public FFI entry points. +Local symbols are invisible to the final linker, so each archive retains a private copy of every internal symbol. This +means no conflict is possible regardless of how many circuits are linked together. + +The public symbols are derived automatically from `PROJECT`: a circuit built with `PROJECT=poq` keeps +`poq_generate_witness` and `poq_generate_witness_from_files` global and localizes everything else. + +> To skip localization for a specific build (e.g. for debugging), pass `PUBLIC_SYMBOLS=` explicitly on the `make` command +> line. + +On macOS, localization is skipped because `objcopy` is a GNU Binutils tool unavailable by default there. +This is safe: macOS uses a two-level namespace by default, meaning symbols are qualified by which library they come +from, so the conflict does not arise. + +### Maintenance + +`PUBLIC_SYMBOLS` defaults to `$(PROJECT)_generate_witness` and `$(PROJECT)_generate_witness_from_files`. +If the public FFI API ever changes, meaning the entrypoints are renamed or new ones added, the Makefile default must be +updated, otherwise the affected symbols will be localized and linking will fail. + +--- + ## Triggering a New Release for Logos Blockchain Circuits To trigger a release build: