Skip to content

fix(build): keep the static liblbug's libstdc++ instantiations private - #1383

Open
sunsided wants to merge 1 commit into
LadybugDB:mainfrom
sunsided:fix/localize-static-liblbug-symbols
Open

sunsided wants to merge 1 commit into
LadybugDB:mainfrom
sunsided:fix/localize-static-liblbug-symbols

Conversation

@sunsided

@sunsided sunsided commented Oct 24, 2026 •

Copy link
Copy Markdown

A suggestion for fixing #1381.

Problem

The Linux prebuilt static liblbug.a is 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 GitHub ubuntu-latest), the archive's GCC 13 code ends up calling the consumer's GCC 15 std::format internals, and any COPY … TO segfaults in Binder::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.cmake post-processes the bundled archive:

  1. ld -r --force-group-allocation --whole-archive combines 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.
  2. objcopy --keep-global-symbol='*4lbug*' --keep-global-symbol='lbug_*' makes every defined global local except the lbug:: C++ API and the C API. The archive keeps private copies of its libstdc++ instantiations and bundled third-party code.
  3. objcopy --weaken-symbols handles the remaining STB_GNU_UNIQUE definitions (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.yml enables it for the Linux compat and perf static 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_64 with gcc-toolset-13 and openssl3. That's the same toolchain as the compat job, but configured directly with -DBUILD_SHELL=FALSE -DBUILD_SHARED_LBUG=FALSE -DLBUG_LOCALIZE_STATIC_SYMBOLS=TRUE. The result is one archive member, with 0 global std::__format definitions and 0 STB_GNU_UNIQUE symbols. I then used it as the Rust crate's prebuilt (LBUG_PREBUILT_CACHE_DIR):

Consumer Stock v0.21.2 prebuilt Patched prebuilt
Minimal COPY … TO repro from #1381, Ubuntu 26.04 / GCC 15.2 segfault (exit 139) ✅ exit 0
Same repro, Ubuntu 24.04 / GCC 13.3 ✅ ✅ exit 0
glyphtrail workspace tests (608, heavy lbug use), Ubuntu 26.04 segfault ✅ all pass

Not verified

  • I didn't run your own e2e, extension or language-binding suites against the patched prebuilt. The default-off option doesn't change those builds.
  • Loading extensions (INSTALL/LOAD EXTENSION) into a consumer linked with the patched archive is untested. Extensions resolve lbug::*, which stays global, so I expect no change, but it's worth a check.
  • *4lbug* also keeps std templates instantiated over lbug types global (e.g. std::vector<lbug::common::Value>). Those can in principle still merge across GCC versions. Nothing in the tests above hit that.
  • The perf variant (clang-21 + libstdc++) is enabled on the reasoning that it has the same exposure, but I didn't test it.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 plumbs ld/objcopy/nm executables into the bundling script.
  • Implements the localization pipeline in BundleStaticLibrary.cmake (combine into one object via ld -r --force-group-allocation --whole-archive, localize non-API globals, weaken remaining STB_GNU_UNIQUE definitions).
  • Enables the option for the Linux compat and perf static 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

@adsharma

Copy link
Copy Markdown
Contributor

Defer to post-0.22.0 release. Keep it open for now.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants