Case 45: Multi-Dimensional Array Element Type Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_field_type_changed, type_size_changed |
| Source files | examples/case45_multi_dim_array_change/ |
Category: Struct Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Matrix embeds a 2D array member data[ROWS][COLS]. Its element type changes
from float (4 bytes) to double (8 bytes), so sizeof(Matrix) grows from
72 to 136 bytes and every field after data shifts to a new offset. Any
consumer compiled against v1 headers allocates and indexes the old (smaller,
differently-strided) layout โ reads land on the wrong bytes. Recompilation
against v2 is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
float data[ROWS][COLS]; โ 64 bytes |
double data[ROWS][COLS]; โ 128 bytes |
float matrix_get(const Matrix *m, int r, int c); |
double matrix_get(const Matrix *m, int r, int c); |
abicheck command¶
gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: Matrix (576 -> 1088 bits)
> Old code allocates or copies the type with the old size;
heap/stack corruption, out-of-bounds access.
Affected symbols: matrix_get, matrix_set
- type_field_type_changed: Field type changed: Matrix::data (float[] -> double[])
> Field has different size or representation; old code misinterprets the data.
- type_field_offset_changed: Field offset changed: Matrix::rows (512 -> 1024 bits)
- type_field_offset_changed: Field offset changed: Matrix::cols (544 -> 1056 bits)
- func_return_changed: Return type changed: matrix_get (float -> double)
Minimum evidence¶
min_evidence: L1 โ DWARF's array-subrange and struct-member info (element
type, element count, member offsets) is enough to compute the new size and
field shifts; no public headers needed.
Why abicheck catches it¶
DWARF represents data[ROWS][COLS] as an array type with a subrange and an
element type; abicheck reads the element type and total byte size from debug
info for both versions and diffs them directly, propagating the size change
into every downstream field offset.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 .so without recompile.
# Build old library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ a=1.500 b=2.500 sum=4.000
# โ expected: a=1.500 b=2.500 sum=4.000
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ a=1.500 b=0.000 sum=1.500
# โ expected: a=1.500 b=2.500 sum=4.000
# โ CORRUPTION: matrix element type/layout changed (float[][] vs double[][])
Why CRITICAL: the app writes float values at v1's 4-byte stride, but
matrix_get() in v2 reads the same memory at an 8-byte double stride โ
every element after the first is misaligned, so b reads as zero instead of
2.5.
Safe redesign¶
Abstract the element type behind a typedef (typedef float mat_elem_t;) so
future changes touch one declaration, not the struct layout, and never widen
an array element type in a public struct without a SONAME bump. If both
precisions are genuinely needed, ship matrix_float_get() and
matrix_double_get() as distinct symbols during a deprecation window.
Cross-tool comparison¶
abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 4
Note on abidiff: libabigail reports this as a
Subrange_Changeinside the array member, surfacing as aMatrixtype-size change with exit code 4 โ the same conclusion abicheck reaches, from the same DWARF evidence.
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.