Repository navigation
Conversation
The Linux prebuilt static archive is compiled with gcc-toolset-13 but is linked by consumers whose own code (e.g. the Rust crate's cxx bridge) is compiled with a newer GCC. Both define the same libstdc++ template instantiations (std::format, ...) as COMDAT groups, the linker keeps one copy per group, and code compiled against one libstdc++ layout calls the other. On Ubuntu 26.04 (GCC 15) any COPY ... TO segfaults in Binder::bindCopyToClause (LadybugDB#1381). Add LBUG_LOCALIZE_STATIC_SYMBOLS (default off, ELF only). When set, the bundling step partially links the archive into one object with --force-group-allocation, makes every defined global local except the `lbug::` and C (`lbug_*`) APIs, and weakens the remaining STB_GNU_UNIQUE definitions, which objcopy cannot localize. Enable it for the Linux compat and perf prebuilts.
There was a problem hiding this comment.
🔵 Needs a closer look
It is a low-level linker/archive symbol-manipulation change affecting released prebuilt binaries, with several author-acknowledged unverified paths (perf variant, extension loading, std templates over lbug types), so it warrants human validation.
No open issues 🎉
What changed in this PR
This PR addresses issue #1381, where COPY … TO segfaults when the prebuilt static liblbug.a (built with gcc-toolset-13) is linked next to consumer C++ compiled with a newer GCC (e.g. GCC 15 on Ubuntu 26.04). Both sides emit the same libstdc++ template instantiations (std::format, std::string, …) as COMDAT groups; the linker keeps one copy, causing cross-ABI calls and a crash in Binder::bindCopyToClause. The fix adds an opt-in build option that post-processes the bundled archive so its internal (non-API) symbols become private, preventing cross-compiler merging. It is default-off, so developer and test builds are unchanged, and enabled only for the Linux compat and perf static prebuilts.
Changes:
- Adds
LBUG_LOCALIZE_STATIC_SYMBOLS(ELF-only, default FALSE) and plumbsld/objcopy/nmexecutables into the bundling script. - Implements the localization pipeline in
BundleStaticLibrary.cmake(combine into one object viald -r --force-group-allocation --whole-archive, localize non-API globals, weaken remainingSTB_GNU_UNIQUEdefinitions). - Enables the option for the Linux
compatandperfstatic prebuilt jobs.
| File | Description |
|---|---|
CMakeLists.txt |
Declares the new LBUG_LOCALIZE_STATIC_SYMBOLS build option (default off). |
src/CMakeLists.txt |
Adds an ELF-only guard and passes LOCALIZE_SYMBOLS, LD/OBJCOPY/NM executables to the post-build bundling command. |
cmake/BundleStaticLibrary.cmake |
Adds the archive post-processing that makes internal symbols local and weakens remaining unique symbols. |
.github/workflows/precompiled-bin-workflow.yml |
Enables the option for the Linux compat and perf static prebuilt builds. |
Review effort: Balanced
|
Defer to post-0.22.0 release. Keep it open for now. |
A suggestion for fixing #1381.
Problem
The Linux prebuilt static
liblbug.ais compiled with gcc-toolset-13 (manylinux_2_28). Consumers link it next to their own C++ code, for example the Rust crate's cxx bridge, which they compile with their GCC. Both sides define the same libstdc++ template instantiations (std::format,std::string, …) as COMDAT groups, and the linker keeps only one copy per group. On a system with GCC 15 (Ubuntu 26.04, today's GitHububuntu-latest), the archive's GCC 13 code ends up calling the consumer's GCC 15std::formatinternals, and anyCOPY … TOsegfaults inBinder::bindCopyToClause. Ubuntu 24.04 (GCC 13) is unaffected. Building from source works around it, but downstream builds then pay a large C++ compile.Fix
A new option,
LBUG_LOCALIZE_STATIC_SYMBOLS(default off, ELF targets only). When it's set,BundleStaticLibrary.cmakepost-processes the bundled archive:ld -r --force-group-allocation --whole-archivecombines it into one object and turns COMDAT group members into ordinary sections, so the consumer's same-named group can't cause the archive's copy to be discarded.objcopy --keep-global-symbol='*4lbug*' --keep-global-symbol='lbug_*'makes every defined global local except thelbug::C++ API and the C API. The archive keeps private copies of its libstdc++ instantiations and bundled third-party code.objcopy --weaken-symbolshandles the remainingSTB_GNU_UNIQUEdefinitions (function-local statics, inline variables), which objcopy can't localize and which would otherwise be duplicate symbols. Only defined symbols are weakened, because a weakened undefined reference would resolve to 0.precompiled-bin-workflow.ymlenables it for the Linuxcompatandperfstatic prebuilts. Default builds, and therefore developer builds and the existing test jobs, are unchanged.Verification
I built the static archive with this patch applied to v0.21.2, using upstream's CMake in
quay.io/pypa/manylinux_2_28_x86_64with gcc-toolset-13 and openssl3. That's the same toolchain as thecompatjob, but configured directly with-DBUILD_SHELL=FALSE -DBUILD_SHARED_LBUG=FALSE -DLBUG_LOCALIZE_STATIC_SYMBOLS=TRUE. The result is one archive member, with 0 globalstd::__formatdefinitions and 0STB_GNU_UNIQUEsymbols. I then used it as the Rust crate's prebuilt (LBUG_PREBUILT_CACHE_DIR):COPY … TOrepro from #1381, Ubuntu 26.04 / GCC 15.2Not verified
INSTALL/LOAD EXTENSION) into a consumer linked with the patched archive is untested. Extensions resolvelbug::*, which stays global, so I expect no change, but it's worth a check.*4lbug*also keeps std templates instantiated overlbugtypes global (e.g.std::vector<lbug::common::Value>). Those can in principle still merge across GCC versions. Nothing in the tests above hit that.perfvariant (clang-21 + libstdc++) is enabled on the reasoning that it has the same exposure, but I didn't test it.