Skip to content

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 Buffer via 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.