Case 46: Pointer Chain Type Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
func_params_changed, func_return_changed |
| Source files | examples/case46_pointer_chain_type_change/ |
Category: Breaking | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
get_matrix()/set_cell()/sum_row() return and accept int**/int*const*
in v1. In v2 the ultimate pointee type changes from int to long at every
level of the chain. A caller compiled against v1 still writes 4-byte int
values into memory the v2 library now treats as 8-byte long elements โ
every write after the first lands on the wrong element, silently corrupting
adjacent cells. Recompilation against v2 is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
int **get_matrix(void); |
long **get_matrix(void); |
void set_cell(int *const *matrix, int row, int col, int val); |
void set_cell(long *const *matrix, int row, int col, long val); |
int sum_row(int *const *matrix, int row, int cols); |
long sum_row(long *const *matrix, int row, int cols); |
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)
- func_return_changed: Return type changed: sum_row (int -> long int)
> Callers expect the old return type layout in registers/stack; misinterpretation
causes data corruption.
- func_params_changed: Parameters changed: sum_row (const int **, int, int -> const long int **, int, int)
- func_params_changed: Parameters changed: set_cell (const int **, int, int, int -> const long int **, int, int, long int)
- func_return_changed: Return type changed: get_matrix (int ** -> long int **)
Minimum evidence¶
min_evidence: L1 โ DWARF's DW_TAG_pointer_type chain records the pointee
type at every indirection level, so debug info alone (no public headers) is
enough to see int become long at the end of the chain.
Why abicheck catches it¶
abicheck walks each function's parameter and return DW_TAG_pointer_type
entries down to their ultimate non-pointer type and compares that leaf type
across versions โ the same mechanism used for a direct parameter change,
applied through the indirection levels.
Runtime failure demonstration¶
Severity: CRITICAL โ silent memory corruption
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
# โ sum_row = 12
# โ expected = 12
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ sum_row = 5
# โ expected = 12
# โ CORRUPTION: pointer-chain pointee type changed (int** vs long**)
Why CRITICAL: the app writes int values at 4-byte stride into storage
that v2's sum_row() now reads as long at 8-byte stride โ the second write
(m[0][1] = 7) lands inside the first long element instead of a separate
one, so the summed value comes out wrong with no crash or diagnostic.
Safe redesign¶
Put an opaque typedef (typedef int mat_cell_t;) at the API boundary so a
future width change touches one declaration instead of the struct/pointer
chain, and never change a primitive type deep inside a multi-level pointer
chain without a SONAME bump โ consumers have no way to detect the change
short of a full ABI diff.
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 an indirect
Function_Return_Type_Change/parameter change through the pointer chain, exit code 4 โ the same conclusion abicheck reaches from the same DWARF pointer-chain evidence.
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.