Case 17 โ Template Instantiation ABI Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, API break |
Detected ChangeKinds |
โ |
| Source files | browse on GitHub |
Verdict: ๐ด BREAKING
Note: The verdict was previously marked COMPATIBLE in error. When app_v1 is loaded against libv2.so using LD_PRELOAD, the v2 constructor writes 24 bytes into a 16-byte stack slot โ sentinel corruption confirmed at runtime. abicheck correctly detects
struct_size_changed: Buffer<int> 16โ24= BREAKING.
What changes¶
| Version | Buffer<int> layout |
|---|---|
| v1 | { T* data_; size_t size_; } โ sizeof = 16 bytes (64-bit) |
| v2 | { T* data_; size_t size_; size_t capacity_; } โ sizeof = 24 bytes |
What breaks at binary level¶
C++ template classes with explicit instantiations are compiled into the .so like
regular classes. The mangled symbol for Buffer<int>::Buffer(size_t) is
_ZN6BufferIiEC1Em โ identical in both versions.
A caller compiled against v1 headers allocates sizeof(Buffer<int>) = 16 bytes
(e.g. on the stack or in a struct). The v2 .so constructor writes 24 bytes โ
overwriting 8 bytes past the allocated region. This is a classic stack smash / heap
corruption scenario, extremely hard to debug.
Why abidiff catches it (with DWARF)¶
When compiled with -g, DWARF records the type layout for Buffer<int>. abidiff
compares type offsets and sizes โ detects sizeof grew from 16 to 24. Without -g,
abidiff sees only the symbol table and misses the layout change.
Why ABICC catches it¶
ABICC parses the header AST and computes sizeof for all template instantiations
referenced in the headers. It sees capacity_ was added and reports:
"Size of type 'Buffer
' changed from 16 to 24 bytes."
Real-world example¶
In large numeric libraries, templated classes like HomogenNumericTable<float> are
often explicitly instantiated in the .so. When a private member is added for
thread-safety tracking, downstream bindings compiled with old headers write past
buffers. This class of bug is typically caught by ASAN in integration tests.
Code diff¶
template<typename T>
class Buffer {
private:
T* data_;
std::size_t size_;
+ std::size_t capacity_; // NEW field
};
Reproduce steps¶
cd examples/case17_template_abi
# Build with debug info
g++ -shared -fPIC -std=c++17 -g v1.cpp -o libv1.so
g++ -shared -fPIC -std=c++17 -g v2.cpp -o libv2.so
# abidiff WITH DWARF catches layout change
abidw --out-file v1.xml libv1.so
abidw --out-file v2.xml libv2.so
abidiff v1.xml v2.xml # exit 4: reports Buffer<int> size change
# Build WITHOUT debug info (strip DWARF)
g++ -shared -fPIC -std=c++17 v1.cpp -o libv1_nodebug.so
g++ -shared -fPIC -std=c++17 v2.cpp -o libv2_nodebug.so
abidw --out-file v1nd.xml libv1_nodebug.so
abidw --out-file v2nd.xml libv2_nodebug.so
abidiff v1nd.xml v2nd.xml # exit 0: MISSES the change (no DWARF)
# ABICC catches via header AST regardless of debug info
abi-compliance-checker -lib Buffer -v1 1.0 -v2 2.0 \
-header v1.hpp -header v2.hpp \
-gcc-options "-std=c++17"
Real Failure Demo¶
Severity: CRITICAL
Scenario: app compiled with v1 layout (Buffer
# Build v1 + app
g++ -shared -fPIC -std=c++17 -g v1.cpp -o libbuf.so
g++ -std=c++17 -g -O0 app.cpp -I. -L. -lbuf -Wl,-rpath,. -o app
./app
# โ sizeof(Buffer<int>) at compile time = 16
# โ before ctor: sentinel = SENTINEL
# โ after ctor: sentinel = SENTINEL (v1: only 16 bytes written)
# Swap in v2 (constructor writes 24 bytes โ 8 bytes past the 16-byte slot)
g++ -shared -fPIC -std=c++17 -g v2.cpp -o libbuf.so
./app
# โ sizeof(Buffer<int>) at compile time = 16
# โ before ctor: sentinel = SENTINEL
# โ after ctor: sentinel = โ CORRUPTED (v2 wrote 8 bytes past slot)
# โ CORRUPTION: v2 constructor wrote past Buffer slot!
# With ASAN:
g++ -shared -fPIC -std=c++17 -g -fsanitize=address v2.cpp -o libbuf.so
g++ -std=c++17 -g -fsanitize=address app.cpp -I. -L. -lbuf -Wl,-rpath,. -o app_asan
./app_asan
# โ ERROR: AddressSanitizer: stack-buffer-overflow
Why CRITICAL: The v2 constructor writes a new capacity_ field at offset 16,
but the app only reserved 16 bytes on the stack. The 8 bytes past the allocation are
overwritten โ classic stack smash, potentially corrupting return addresses.
Why runtime result may differ from verdict¶
Template instantiation in binary: symbol names unchanged
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.