Case 129: Struct-Return Convention Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
struct_return_convention_changed |
| Source files | examples/case129_struct_return_convention/ |
Category: Calling Convention | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Result compute() returns a small aggregate by value. In v1, Result is
trivially copyable, so the System V AMD64 ABI returns it in registers
(RAX:XMM0). In v2, Result gains a user-declared destructor, making it
non-trivial โ the ABI now returns it through a hidden caller-provided
pointer (sret) passed in RDI instead. The function's mangled name
(_Z7computev) is byte-for-byte unchanged, so a symbol-only check sees
nothing, but a caller compiled against v1's in-register convention and
relinked against v2 without recompiling reads the return value from the
wrong location entirely.
Old/new diff¶
| v1.cpp | v2.cpp |
|---|---|
struct Result { int code; double value; }; |
struct Result { int code; double value; ~Result(); }; |
Result compute(); (in-register return) |
Result compute(); (sret return) |
abicheck command¶
g++ -shared -fPIC -g -Og v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g -Og v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- struct_return_convention_changed: Aggregate return convention changed:
compute() (ret:trivial -> ret:nontrivial)
> The aggregate return convention changed for a public function -- e.g.
a small struct that was returned in registers is now returned via a
hidden caller-provided pointer (sret), or vice versa. Callers and
callee disagree on where the result lives, so the return value is
read from the wrong location -- silent corruption or a crash.
Minimum evidence¶
min_evidence: L1 โ DWARF's per-function value-ABI facts record whether a
returned aggregate is trivial or non-trivial for the purposes of calls;
abicheck reads that trait directly from the compiled .so's debug info, no
public headers required.
Why abicheck catches it¶
DWARF's return-type facts for compute flip trivial -> nontrivial, which
crosses the in-register/sret boundary defined by the System V AMD64 ABI.
abicheck emits struct_return_convention_changed (the return-specific
refinement of the more general value_abi_trait_changed) as BREAKING, since
this is a proven calling-convention fact, not just a build-flag signal.
Runtime failure demonstration¶
Severity: CRITICAL
# Build old library + app (app expects the in-register v1 convention)
g++ -shared -fPIC -g -Og v1.cpp -o libfoo.so
g++ -g -Og app.cpp -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# -> exit: 0
# Swap in new library (no recompile)
g++ -shared -fPIC -g -Og v2.cpp -o libfoo.so
./app
# -> Segmentation fault
# -> exit: 139
Why CRITICAL: the app calls compute() expecting the result in
RAX:XMM0 (v1's in-register convention). Against v2, compute() instead
writes the result through a hidden sret pointer that the app's v1-compiled
call site never set up โ the app reads garbage register state as the
return value and crashes.
Safe redesign¶
Treat a triviality change on a by-value public return type as an ABI break: keep the type trivially copyable, or bump the SONAME and rebuild all consumers. If a destructor is genuinely required, return through an opaque handle or an out-parameter instead of by value so the convention stays explicit and stable.
Real-world example: adding a std::unique_ptr member, a user-declared
destructor, or any non-trivial member to a small struct that a public
factory returns by value is a common form of this break in C++ libraries โ
it looks like an innocuous source edit but silently changes the call ABI of
every function returning the type by value.
Cross-tool comparison¶
abidiff also reads DWARF and can see the same triviality flip in
principle, but its handling of the specific in-register/sret ABI transition
is not independently verified in this sandbox (no abidw/abidiff
installed here); treat this section as an invocation reference rather than a
verified result:
References¶
- System V AMD64 ABI: returning values / classification of aggregates
- Itanium C++ ABI: non-trivial-for-the-purposes-of-calls
Source files¶
CMakeLists.txtapp.cppv1.cppv2.cpp
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.