Skip to content

Hide statically linked libstdc++ symbols from the extension - #254

Open
sam-saffron-jarvis wants to merge 1 commit into
couchbase:mainfrom
sam-saffron-jarvis:hide-static-stdlib-symbols
Open

sam-saffron-jarvis wants to merge 1 commit into
couchbase:mainfrom
sam-saffron-jarvis:hide-static-stdlib-symbols

Conversation

@sam-saffron-jarvis

Copy link
Copy Markdown

Problem

The precompiled Linux gems are built with CB_STATIC_STDLIB=1, so libstdc++ and libgcc are linked statically into libcouchbase.so. Nothing hides those symbols, so the extension re-exports a full libstdc++ (built with GCC 11.4). In a source build of 3.8.2 with the release flags, libcouchbase.so exports 8,423 std:: symbols, 745 of them STB_GNU_UNIQUE, including the locale facet ids such as std::num_put<char>::id.

Ruby loads extensions with RTLD_GLOBAL, and glibc resolves STB_GNU_UNIQUE symbols process-wide to the first definition. As a result, when another C++ extension loads after couchbase, the system libstdc++.so.6 binds parts of itself to couchbase's private copy. LD_DEBUG=bindings shows this:

binding file /lib/libstdc++.so.6 [0] to .../couchbase/3.4/libcouchbase.so [0]: normal symbol `_ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE'

The other library ends up with facet ids from one libstdc++ and the facet table from another. The first ostream << long then dereferences a missing facet and segfaults at 0x20. With the load order reversed, couchbase itself aborts with std::bad_cast. This only shows when the system libstdc++ differs enough from couchbase's GCC 11 copy, e.g. Debian trixie (GCC 14), Arch (GCC 16) and the ruby:3.4-slim images. Bookworm happens to work.

Reported downstream in rubyjs/mini_racer#422 (minimal repro: rubyjs/mini_racer#422 (comment)):

gem install couchbase:3.8.2 mini_racer:0.22.1
ruby -e 'require "couchbase"; require "mini_racer"; p MiniRacer::Context.new.eval("1+1")'
# => [BUG] Segmentation fault at 0x0000000000000020

mini_racer isn't the only extension affected. A three-line C++ extension that does std::ostringstream os; os << 42L; crashes the same way when it is loaded after couchbase with RTLD_LOCAL|RTLD_DEEPBIND. The leaked runtime affects any C++ extension that isolates itself. Current workaround for users: LD_PRELOAD=libstdc++.so.6.

Fix

On ELF platforms, link the extension with --exclude-libs,ALL. Symbols that come from static archives (libstdc++, libgcc, BoringSSL, ...) stay local to libcouchbase.so. Init_libcouchbase and the extension's own code are unaffected. Apple and Windows/MinGW are left unchanged.

Verification

x86_64 Arch Linux (libstdc++ 6.0.36 / GCC 16), Ruby 3.4.10. couchbase 3.8.2 was built from the source gem with CB_STATIC_STDLIB=1 CB_STATIC_BORINGSSL=1 (as in bin/jenkins/build-gem.sh), before and after this change:

before after
exported std:: symbols 8,423 (745 unique) 889 (weak template instantiations from the extension's own code)
require "couchbase"; require "mini_racer"; eval segfault 3/3 ok 5/5
mini_racer + V8 first, then couchbase abort (std::bad_cast) ok 5/5
minimal C++ extension (RTLD_DEEPBIND) after couchbase segfault ok 5/5
Cluster.connect attempt (no server: expected timeout error) + V8 in the same process — ok

Not tested: operations against a live server, aarch64, Alpine/musl.

Related: rubyjs/mini_racer#422. mini_racer is considering a matching defensive change (linking its own libstdc++ privately), but the leak itself originates here.


Note: this PR was prepared by an AI assistant (Jarvis) on behalf of Sam Saffron. The fix is a single link flag, so feel free to apply it directly if handling the CLA/Gerrit flow for this PR is inconvenient.

Precompiled gems are built with CB_STATIC_STDLIB, which links libstdc++
and libgcc statically into libcouchbase.so, but every symbol from those
archives is still exported (8423 std:: symbols, 745 STB_GNU_UNIQUE).
Ruby loads extensions with RTLD_GLOBAL, so this private GCC 11 runtime
interposes on the system libstdc++ used by other C++ extensions and
crashes them, e.g. mini_racer (rubyjs/mini_racer#422).

Link with --exclude-libs,ALL on ELF platforms so archive symbols stay
local to the extension.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

2 participants