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 | browse on GitHub |
Category: Calling Convention | Verdict: BREAKING
What breaks¶
A user-defined destructor ~Point() {} is added to struct Point, changing it
from trivially copyable to non-trivially copyable. Under the Itanium C++
ABI (used by GCC and Clang on Linux/macOS), this fundamentally changes how the
struct is passed to functions:
- v1 (trivial):
Pointis passed in registers (%xmm0/%xmm1for the two doubles on x86-64 System V) - v2 (non-trivial):
Pointis passed via hidden pointer โ the caller allocates stack space, copies the struct there, and passes a pointer in%rdi
The function signature (double distance(Point, Point)) and the mangled symbol
name are identical in v1 and v2. The struct size is also unchanged. Yet the
binary calling convention is completely different. The callee reads registers
that contain garbage (addresses instead of doubles) or vice versa.
This is unlike any existing case: it's not a type size change (case07/14), not a signature change (case02/10), not a qualifier change (case22), and not a visibility change (case06). The break is invisible to header diffing tools that don't analyze the trivially-copyable trait.
Why abicheck catches it¶
The type analysis detects that Point gained a non-trivial destructor, changing
its ABI classification. This is reported as a value-ABI trait change
(value_abi_trait_changed) โ the struct's calling convention changed even though
its layout didn't.
Code diff¶
// v1: trivially copyable โ passed in registers
struct Point {
double x;
double y;
};
// v2: non-trivially copyable โ passed via hidden pointer
struct Point {
double x;
double y;
~Point() {} // โ this single line changes the calling convention
};
Real Failure Demo¶
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
# Build v2 (non-trivially copyable)
g++ -shared -fPIC -g v2.cpp -o libpoint.so -lm
./app
# -> distance = <garbage or crash>
# v2 expects hidden pointers in %rdi/%rsi; app passes doubles in %xmm registers
Why CRITICAL: The mangled name is identical, so the dynamic linker happily
resolves the symbol. But the caller passes Point values in FP registers while
the callee reads them as pointers from integer registers. The function
dereferences what it thinks are pointers, causing a segfault or reading garbage
memory. There is no warning whatsoever.
How to fix¶
The trivially-copyable property is part of the ABI contract. Never add user-defined special member functions to types that are passed by value across library boundaries:
- Use a separate Pimpl class for non-trivial cleanup
- Pass by pointer/reference instead of by value across ABI boundaries
- If a destructor is needed, add it 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 the core issue behind the C++ ABI stability debate. The std::string
and std::unique_ptr types in libstdc++ have specific trivially-copyable
properties that constrain their implementation. Boost.Asio's ip::address type
had this issue when executor properties were added. The Chromium project
explicitly documents which types must remain trivially copyable for their IPC
serialization ABI.
LLVM's ABI testing suite specifically tests for trivially-copyable changes because they are invisible to most diff tools but cause silent corruption at runtime.
References¶
- Itanium C++ ABI ยง3.1.2: Non-trivial types passed by invisible reference
- System V AMD64 ABI: Classification of aggregate types
- P2028R0: What is ABI, and What Should WG21 Do About It?
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.