Skip to content

Case 104: libstdc++ Dual-ABI Flip

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break, Bad practice
Detected ChangeKinds glibcxx_dual_abi_flip_detected, func_removed
Source files examples/case104_glibcxx_dual_abi_flip/

Category: Symbol API | Verdict: โŒ BREAKING

Verdict and consumer impact

GNU libstdc++ ships two incompatible ABIs for std::string (and std::list), selected at compile time by the _GLIBCXX_USE_CXX11_ABI macro: =1 (the modern default) mangles std::string as std::__cxx11::basic_string<...>; =0 mangles it as the legacy std::basic_string<...>. The two are not link-compatible. Here the source is byte-for-byte identical between v1 and v2 โ€” only the macro flips. Every exported function that takes or returns std::string gets a different mangled name, so a caller linked against one build cannot resolve the symbols of the other: the dynamic linker fails at load, or โ€” in a partially-rebuilt process โ€” a legacy-ABI string gets silently paired with a cxx11-ABI string and corrupts memory.

Old/new diff

Source is identical; only the build flag differs:

 g++ -shared -fPIC -g -D_GLIBCXX_USE_CXX11_ABI=0 -o libv1.so v1.cpp
-g++ -shared -fPIC -g -D_GLIBCXX_USE_CXX11_ABI=0 -o libv2.so v2.cpp
+g++ -shared -fPIC -g -D_GLIBCXX_USE_CXX11_ABI=1 -o libv2.so v2.cpp

Symbol-level effect:

-_Z4joinRKSsS0_                       # std::string  (legacy ABI)
+_Z4joinRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES5_   # std::__cxx11::string

abicheck command

g++ -shared -fPIC -g -D_GLIBCXX_USE_CXX11_ABI=0 -o libv1.so v1.cpp
g++ -shared -fPIC -g -D_GLIBCXX_USE_CXX11_ABI=1 -o libv2.so v2.cpp
abicheck compare libv1.so libv2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

## Breaking Changes

- func_removed: Public function removed: concat
- func_removed: Public function removed: split
- func_removed: Public function removed: trim
- func_removed: Public function removed: repeat
- func_removed: Public function removed: upper
- func_removed: Public function removed: join
- instantiation_missing_from_binary: Template instantiation 'concat' was
  exported by the old library but is missing from the new binary.

## Deployment Risk Changes / Quality Issues

- glibcxx_dual_abi_flip_detected: libstdc++ dual ABI flip detected
  (legacy ABI -> CXX11 ABI): 6 of 12 churned symbols contain CXX11 ABI
  markers; likely caused by _GLIBCXX_USE_CXX11_ABI toggle

(Full run: 7 breaking changes, 88 deployment-risk changes, 18 compatible changes โ€” the churn is every std::string-bearing symbol getting a new mangled name.)

Minimum evidence

min_evidence: L0 โ€” the mangled names themselves carry the ABI tag (__cxx11 present or absent); the exported-symbol table alone is enough to see six symbols disappear and six new ones with __cxx11 markers appear. No headers are passed here deliberately: the case is validated binary-only by design, matching the fixture's own comment that public headers are irrelevant to this phenomenon.

Why abicheck catches it

A naive symbol diff would report a dozen unrelated func_removed/ func_added entries with no unifying explanation. abicheck's dual-ABI detector recognizes the shape of the churn: a set of public symbols disappears while an equally large set appears, and a significant fraction of the churned mangled names carry the __cxx11 marker on one side only. It collapses that pattern into a single glibcxx_dual_abi_flip_detected diagnostic layered on top of the underlying func_removed/func_added evidence, so a reviewer sees "this is one systemic cause" instead of a wall of unrelated-looking symbol churn.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against v1 (legacy ABI), swap in v2's .so (cxx11 ABI) without recompiling.

# Build old library + app (app also built with the legacy ABI macro,
# matching the library it links against)
ln -sf libv1.so libv.so
g++ -D_GLIBCXX_USE_CXX11_ABI=0 app.cpp -L. -lv -Wl,-rpath,. -o app
./app
# โ†’ exits 0 (join()/repeat() resolve to the legacy-ABI symbols)

# Swap in new library (no recompile)
ln -sf libv2.so libv.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol: _Z4joinRKSsS0_

Why CRITICAL: the app's call sites are hard-linked against the legacy-mangled symbols (_Z4joinRKSsS0_); v2's .so only exports the __cxx11-mangled equivalents, so the dynamic linker cannot resolve them and the process fails to start. In a partially-rebuilt program mixing objects from both ABIs, the failure mode can be worse than a clean load error โ€” silent memory corruption from mismatched std::string layouts.

Safe redesign

Pick one _GLIBCXX_USE_CXX11_ABI value and use it consistently across the library, all of its dependencies, and every consumer that exchanges std::string/std::list across the boundary. Never expose libstdc++ container types on a public ABI surface unless the dual-ABI setting is part of the documented build contract; prefer a C ABI or opaque handles at boundaries that must survive a toolchain/distro upgrade.

Real-world example: this is the exact failure mode GCC 5's introduction of the C++11 std::string/std::list ABI caused across the Linux distro ecosystem โ€” packages built against old and new libstdc++ defaults couldn't exchange strings, and distros had to either dual-ship or rebuild entire dependency chains in lockstep.

Cross-tool comparison

abidiff (binary-only) would see the same dozen-symbol churn but reports each removal/addition independently, without recognizing it as one systemic dual-ABI event โ€” that collapsing/labeling is unique to abicheck's dedicated detector. Not re-verified numerically in this environment (abidiff is not installed here).

References


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v2.cpp

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.