Case 14: C++ Class Size Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_size_changed |
| Source files | examples/case14_cpp_class_size/ |
Category: C++ ABI | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Old code allocates Buffer on the stack or via new, expecting 64 bytes.
v2's Buffer needs 128 bytes. The constructor (which zero-initializes the
data array) writes 128 bytes into whatever allocation the caller made,
corrupting adjacent memory if that allocation is still sized for v1. Any
pre-built consumer that embeds Buffer by value is broken without
recompilation; consumers recompiled against the new layout are unaffected.
Old/new diff¶
| v1.h | v2.h |
|---|---|
char data[64]; |
char data[128]; |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libbuf_v1.so
g++ -shared -fPIC -g v2.cpp -o libbuf_v2.so
abicheck compare libbuf_v1.so libbuf_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: Buffer (512 -> 1024 bits)
> Old code allocates or copies the type with the old size; heap/stack
corruption, out-of-bounds access.
Affected symbols: make_buffer
Minimum evidence¶
min_evidence: L1 โ DWARF's structure-type entry (DW_TAG_class_type size
in bytes) is enough to detect the size change; no public headers required.
Why abicheck catches it¶
DWARF records each class's total byte size for both versions; abicheck compares them directly from debug info, the same mechanism it uses for plain C structs.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app allocates Buffer by value on the stack using v1's
layout (64 bytes, -O0 for predictable stack layout). With v2 the
constructor initializes 128 bytes, writing past the allocated slot.
# Build v1 + app (use -O0 so stack layout is predictable)
g++ -shared -fPIC -g v1.cpp -o libbuf.so
g++ -g -O0 app.cpp -I. -L. -lbuf -Wl,-rpath,. -o app
./app
# โ via factory: size() = 64 (expected 64)
# โ canary = CANARY!
# โ after = AFTER!!
# Swap in v2 (sizeof Buffer = 128, constructor writes 128 bytes)
g++ -shared -fPIC -g v2.cpp -o libbuf.so
./app
# โ via factory: size() = 128 (expected 64)
# โ *** stack smashing detected ***: terminated
# โ Aborted (core dumped)
Why CRITICAL: the v2 constructor writes 128 bytes into a stack slot the
app only sized for 64. glibc's stack protector notices the canary it
inserted around the frame was overwritten and aborts immediately โ on a
build without -fstack-protector the same write instead silently
corrupts adjacent stack memory. AddressSanitizer catches the same bug from
a different angle: the factory path alone (make_buffer() returning a
128-byte new, deleted through the 64-byte v1.h view) trips a
new-delete-type-mismatch before the stack scenario even runs.
Safe redesign¶
Use the PIMPL idiom: the public Buffer class stores only a pointer to a
private BufferImpl struct whose layout can change freely without
affecting sizeof(Buffer).
Real-world example: Qt's "binary compatibility" rule explicitly
forbids changing the sizeof of any public class. Every Qt class that
needs to grow uses a d_ptr PIMPL to keep the public class size constant
across minor releases.
Cross-tool comparison¶
abidw --out-file v1.xml libbuf_v1.so
abidw --out-file v2.xml libbuf_v2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 4
Note on abidiff 2.4.0: reports
type size changed from 512 to 1024 (in bits), exit 4. Semantically breaking for any code that heap-allocatesBufferviaoperator newor embeds it by value.
References¶
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.