Case 14: C++ Class Size Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, API break |
Detected ChangeKinds |
โ |
| Source files | browse on GitHub |
Category: C++ ABI | Verdict: ๐ก ABI CHANGE (exit 4)
Note on abidiff 2.4.0: Returns exit 4. Semantically breaking for any code that heap-allocates
Buffervia operator new or embeds it by value.
What breaks¶
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 a 64-byte allocation, corrupting adjacent memory. Any pre-built consumer
compiled against the older 64-byte v1 layout that inherits from or embeds Buffer by
value is also broken; consumers recompiled against the new 128-byte layout are unaffected.
Why abidiff catches it¶
Reports type size changed from 512 to 1024 (in bits) (64 bytes โ 128 bytes).
Code diff¶
| v1.cpp | v2.cpp |
|---|---|
char data[64]; |
char data[128]; |
Reproduce manually¶
g++ -shared -fPIC -g v1.cpp -o libbuf_v1.so
g++ -shared -fPIC -g v2.cpp -o libbuf_v2.so
abidw --out-file v1.xml libbuf_v1.so
abidw --out-file v2.xml libbuf_v2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 4
How to fix¶
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 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.
Real Failure Demo¶
Severity: CRITICAL
Scenario: app allocates Buffer by value on the stack using v1 layout (64 bytes). With v2 the constructor initializes 128 bytes, corrupting adjacent stack memory.
# 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)
# โ canary = CANARY!
# โ after = โ CORRUPTED (v2 constructor overwrote 64 bytes past the stack slot)
# โ CORRUPTION: stack overwritten by v2 constructor!
# With ASAN (stack-by-value scenario):
g++ -shared -fPIC -g -fsanitize=address v2.cpp -o libbuf.so
g++ -g -O0 -fsanitize=address app.cpp -I. -L. -lbuf -Wl,-rpath,. -o app_asan
./app_asan
# โ ERROR: AddressSanitizer: stack-buffer-overflow
Why CRITICAL: Old code allocates Buffer on the stack expecting 64 bytes. The v2
constructor initializes 128 bytes โ writing 64 bytes past the stack slot, corrupting
adjacent variables and potentially return addresses.
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.