Skip to content

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_Change inside the array member, surfacing as a Matrix type-size change with exit code 4 โ€” the same conclusion abicheck reaches, from the same DWARF evidence.


Source files

  • CMakeLists.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.