Skip to content

Case 33: Pointer Level Change

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds param_pointer_level_changed
Source files examples/case33_pointer_level/

Category: Function Signature | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

process()'s parameter and get_buffer()'s return type both gain an extra level of pointer indirection (int* โ†’ int**). A caller compiled against v1 passes a raw int* โ€” v2's process() dereferences it as int**, treating the pointed-to integer's value as a memory address. Similarly, get_buffer() now returns a pointer-to-pointer that a v1 caller still treats as a flat int* buffer. Either mismatch is an immediate segfault (dereferencing an unmapped "address" formed from ordinary integer data) or silent memory corruption. Recompilation is mandatory.

Old/new diff

v1.h v2.h
void process(int *data); void process(int **data);
int *get_buffer(void); int **get_buffer(void);

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: get_buffer (int * -> int * *)
  > Callers expect the old return type layout in registers/stack;
    misinterpretation causes data corruption.
- func_params_changed: Parameters changed: process (int * -> int * *)
  > Callers push arguments with the old layout; callee reads wrong data
    from stack/registers.
- return_pointer_level_changed: Return pointer level changed: get_buffer (depth 1 -> 2)
- param_pointer_level_changed: Parameter pointer level changed: process param data (depth 1 -> 2)

Minimum evidence

min_evidence: L1 โ€” DWARF's DW_TAG_pointer_type chain records how many levels of indirection wrap a function's parameter or return type for both binaries, so the depth-1-to-2 change is visible directly from debug info; -g alone (no public headers) is enough.

Why abicheck catches it

abicheck walks each parameter's and return value's pointer-type DIE chain and counts indirection depth. process's data parameter and get_buffer's return value both go from one DW_TAG_pointer_type layer to two, which is reported both as a generic signature change (func_params_changed/func_return_changed) and via the dedicated pointer-level detector that names the exact depth change.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app compiled against v1 (int* parameter/return) runs against v2, which reads/returns one extra level of indirection.

# Build old library + app
gcc -shared -fPIC -g v1.c -o libv1.so
gcc -g app.c -I. -L. -lv1 -Wl,-rpath,. -o app
./app
# โ†’ process(&val) succeeded, val = 42
# โ†’ get_buffer()[0] = 99

# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libv1.so
./app
# โ†’ Segmentation fault (core dumped)

Why CRITICAL: v2's process() performs **data, treating the integer value 42 the caller passed as a memory address and dereferencing it โ€” almost certainly an unmapped page, causing an immediate SIGSEGV. Even when the dereferenced address happens to be mapped, get_buffer() returning a pointer-to-pointer that a v1 caller indexes as a flat array reads attacker/caller-uncontrolled memory as data.

Safe redesign

Never change a public function's parameter or return pointer depth in place โ€” it's indistinguishable from any other signature change at the symbol-name level (C has no overloading) and silently miscompiles callers. Introduce a new function with a name that reflects the new signature (process_indirect, get_buffer_ref) and deprecate the old one.

Real-world example: APIs that evolve from "caller owns a single buffer" to "library owns a resizable/relocatable buffer" often make this exact change (T* โ†’ T** so the library can update the caller's pointer in place) without renaming the function โ€” a classic source of hard-to- diagnose crash reports after a routine library upgrade.

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: $?"

References


Source files

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

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