Skip to content

Case 69: Trivially Copyable to Non-Trivial (Calling Convention Change)

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS
Flags ABI break
Detected ChangeKinds value_abi_trait_changed
Source files examples/case69_trivial_to_nontrivial/

Category: Calling Convention | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

~Point() {} makes Point non-trivially copyable. Under the Itanium C++ ABI (GCC/Clang on Linux), a trivial Point is passed in FP registers (%xmm0/%xmm1); a non-trivial Point is passed via a hidden pointer instead. The function signature and mangled symbol name are unchanged โ€” this is a pure calling-convention break, not a source-level API change โ€” so source recompiles cleanly but old binaries crash or read garbage, because the caller and callee now disagree on where the arguments live.

Old/new diff

v1.h v2.h
struct Point { double x; double y; }; struct Point { double x; double y; ~Point() {} };
(trivially copyable โ†’ passed in registers) (non-trivially copyable โ†’ passed via hidden pointer)

abicheck command

g++ -shared -fPIC -g v1.cpp -o libfoo_v1.so -lm
g++ -shared -fPIC -g v2.cpp -o libfoo_v2.so -lm
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- value_abi_trait_changed: DWARF value-ABI trait changed: distance(Point, Point)
  (p0:trivial|p1:trivial -> p0:nontrivial|p1:nontrivial)

Minimum evidence

min_evidence: L1 โ€” DWARF's parameter-type DIEs carry enough information (a non-trivial special member function, i.e. the user-defined destructor) for abicheck to reclassify Point's value-ABI trait; -g alone is enough, no public headers required.

Why abicheck catches it

abicheck inspects each parameter type's DWARF representation for user-declared destructors/copy/move members that make a type non-trivially-copyable, and compares that trait between the two versions per parameter of each matched function โ€” independent of the (unchanged) mangled signature or struct size.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against v1, swap in v2 .so without recompile.

# Build v1 (trivially copyable) and app
g++ -shared -fPIC -g v1.cpp -o libpoint.so -lm
g++ -g app.cpp -L. -lpoint -Wl,-rpath,. -o app
./app
# โ†’ distance = 5.0
# โ†’ Expected: 5.0

# Swap in v2 (non-trivially copyable, no recompile)
g++ -shared -fPIC -g v2.cpp -o libpoint.so -lm
./app
# โ†’ Segmentation fault (core dumped)

Why CRITICAL: the mangled name is identical, so the dynamic linker happily resolves the symbol. The app passes Point values in FP registers (v1 convention); v2 expects hidden pointers in integer registers instead. The callee dereferences what it thinks are pointers, causing the segfault observed above โ€” with no compiler warning at either build.

Safe redesign

Treat the trivially-copyable property as part of the ABI contract. Never add user-defined special member functions to a type passed by value across a library boundary โ€” pass by pointer/reference instead, or add the destructor from day one so the ABI is established as non-trivial from the start:

/* Safe: pass by pointer, immune to trivially-copyable changes */
double distance(const Point *a, const Point *b);

Real-world example: this is a recurring theme in the C++ ABI stability debate โ€” std::string/std::unique_ptr in libstdc++ have specific trivially-copyable properties that constrain their implementation, and the Chromium project explicitly documents which IPC types must stay trivially copyable for exactly this reason.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

References


Source files

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

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