Merge 48667f0537f48d533c9baa7503fae78be961bc00 into 653c9295e4e67e483b8d9da9911acb9f1065ec17

This commit is contained in:
Álex 2026-05-18 15:06:40 +00:00 committed by GitHub
commit 6d81ca4266
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 136 additions and 2 deletions

View File

@ -52,15 +52,35 @@ windows: $(BIN)
windows-lib: CXXFLAGS=$(CXXFLAGS_COMMON) -fPIC -I/include -Duint="unsigned int"
windows-lib: $(LIB)
# Localizes circuit-specific symbols so multiple circuit libraries can coexist in the
# same binary without symbol conflicts. See CONTRIBUTING.md § "Symbol Isolation".
# Only the 9 constants in $(PROJECT).cpp differ per circuit — everything else is
# identical across circuits and can be safely deduplicated by the linker as normal.
# Override with LOCALIZE_SYMS= to skip localization.
LOCALIZE_SYMS ?= get_size_of_witness get_size_of_constants get_size_of_input_hashmap \
get_main_input_signal_no get_main_input_signal_start get_total_signal_no \
get_number_of_components get_size_of_io_map get_size_of_bus_field_map
LOCAL_OBJ := $(PROJECT)_local.o
UNAME := $(shell uname -s)
# ---- Rules ----
$(BIN): $(COMMON_OBJS)
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(LIB): $(LIB_OBJS)
ar rcs $@ $^
ifeq ($(strip $(LOCALIZE_SYMS)),)
ar rcs $@ $^ # Localization disabled
else ifeq ($(UNAME),Darwin)
ar rcs $@ $^ # On macOS two-level namespace, conflicts don't arise
else
objcopy $(foreach s,$(LOCALIZE_SYMS),--localize-symbol=$(s)) $(PROJECT).o $(LOCAL_OBJ)
ar rcs $@ $(filter-out $(PROJECT).o,$^) $(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)

View File

@ -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:

8
rust/Cargo.lock generated
View File

@ -237,6 +237,14 @@ dependencies = [
"logos-blockchain-circuits-types",
]
[[package]]
name = "logos-blockchain-circuits-tests"
version = "0.5.0"
dependencies = [
"logos-blockchain-circuits-pol-sys",
"logos-blockchain-circuits-poq-sys",
]
[[package]]
name = "logos-blockchain-circuits-types"
version = "0.5.0"

View File

@ -15,6 +15,7 @@ members = [
"logos-blockchain-circuits-pol-sys",
"logos-blockchain-circuits-poq-sys",
"logos-blockchain-circuits-signature-sys",
"logos-blockchain-circuits-tests",
"logos-blockchain-circuits-types",
"logos-blockchain-circuits-common",
]

View File

@ -0,0 +1,10 @@
[package]
name = "logos-blockchain-circuits-tests"
edition.workspace = true
license.workspace = true
version.workspace = true
publish = false
[dev-dependencies]
lbc-pol-sys = { workspace = true, features = ["prebuilt"] }
lbc-poq-sys = { workspace = true, features = ["prebuilt"] }

View File

@ -0,0 +1,22 @@
pub mod roots {
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
pub static TESTS: LazyLock<PathBuf> =
LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")));
pub static REPOSITORY: LazyLock<&Path> =
LazyLock::new(|| TESTS.parent().expect("Failed to find the repository root."));
pub static POL: LazyLock<PathBuf> =
LazyLock::new(|| REPOSITORY.join("logos-blockchain-circuits-pol-sys"));
pub static POQ: LazyLock<PathBuf> =
LazyLock::new(|| REPOSITORY.join("logos-blockchain-circuits-poq-sys"));
}
pub mod inputs {
use super::roots;
use std::path::PathBuf;
use std::sync::LazyLock;
pub static POL: LazyLock<PathBuf> = LazyLock::new(|| roots::POL.join("sample.input.json"));
pub static POQ: LazyLock<PathBuf> = LazyLock::new(|| roots::POQ.join("sample.input.json"));
}

View File

@ -0,0 +1,24 @@
#[cfg(test)]
mod tests {
use lbc_poq_sys::PoqWitnessInput;
use logos_blockchain_circuits_tests::inputs;
#[test]
fn test_both_circuits_generate_witness() {
let pol_inputs_raw = std::fs::read_to_string(inputs::POL.as_path()).unwrap();
let pol_witness_input = lbc_pol_sys::PolWitnessInput::new(pol_inputs_raw).unwrap();
// Each sys crate compiles a copy of the same C++ runtime (loadCircuit, get_size_of_witness,
// ...) under identical symbol names. When two crates are linked into the same binary, the
// linker silently keeps one definition of each symbol, so one circuit ends up using the
// other's size constants — corrupting dat parsing and causing a SIGSEGV.
// This test reproduces the conflict by calling generate_witness on both circuits in the
// same binary.
let _pol_witness = lbc_pol_sys::generate_witness(&pol_witness_input);
let inputs_json_raw = std::fs::read_to_string(inputs::POQ.as_path()).unwrap();
let inputs_json = PoqWitnessInput::new(inputs_json_raw).unwrap();
let poq_result = lbc_poq_sys::generate_witness(&inputs_json);
assert!(poq_result.is_ok());
}
}