Case 73: Typedef Underlying Type Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
typedef_base_changed |
| Source files | examples/case73_typedef_underlying_changed/ |
Category: Type ABI | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
The typedef handle_t changes its underlying type from int (4 bytes) to
void* (8 bytes on x86-64). Every function using handle_t โ parameters
and the return value โ inherits the break from this single typedef change:
handle_open()'s return value grows from 4 to 8 bytes, and the h
parameters of handle_read()/handle_close() do too. A binary compiled
against v1 truncates the returned pointer to 32 bits and passes that
truncated value back into the library, which dereferences a corrupted
pointer. Recompilation is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
typedef int handle_t; |
typedef void *handle_t; |
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)
- typedef_base_changed: Typedef base type changed: handle_t (int -> void *)
> Underlying type changed; old code using the typedef operates on wrong
representation.
2 derived change(s) collapsed
Affected symbols: handle_close, handle_open, handle_read
- param_pointer_level_changed: Parameter pointer level changed: handle_close
param h (depth 0 -> 1)
- param_pointer_level_changed: Parameter pointer level changed: handle_read
param h (depth 0 -> 1)
- return_pointer_level_changed: Return pointer level changed: handle_open
(depth 0 -> 1)
Deployment risk:
- imported_symbol_added: New imported symbol: free@GLIBC_2.2.5, malloc@GLIBC_2.2.5
Minimum evidence¶
min_evidence: L1 โ DWARF's typedef DIE for handle_t records its
underlying type for both versions; abicheck compares that plus every
using-function's parameter/return DWARF types directly from debug info
(-g), no public headers required.
Why abicheck catches it¶
abicheck resolves handle_t's DWARF typedef target (int vs void *) and
reports the root typedef_base_changed finding, then collapses the
per-function pointer-level changes it implies (handle_open's return,
handle_read/handle_close's h parameter) under it โ the underlying
type comparison needs only debug info, no header parsing.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 .so without recompile.
# Build v1 and app
gcc -shared -fPIC -g v1.c -o libhandle.so
gcc -g app.c -L. -lhandle -Wl,-rpath,. -o app
./app
# โ handle = 1
# โ read 4 bytes
# โ Done.
# Swap in v2 (handle_t is now void*, no recompile)
gcc -shared -fPIC -g v2.c -o libhandle.so
./app
# โ Segmentation fault (core dumped)
Why CRITICAL: the app was compiled to treat handle_t as a 4-byte
int, so it truncates v2's 8-byte pointer return value. Passing that
truncated value into handle_read()/handle_close() โ now void*
parameters โ makes the library dereference or free() a corrupted
pointer, producing the segfault observed above.
Safe redesign¶
Design handles as opaque pointers from the start, or use a fixed-width integer that is already the maximum size ever needed:
/* Option 1: opaque pointer from the start */
typedef struct handle_impl *handle_t;
/* Option 2: use a large enough integer from day one */
#include <stdint.h>
typedef uintptr_t handle_t; /* always 8 bytes on 64-bit */
If the change is unavoidable, bump the SONAME and provide a migration path.
Real-world example: this is a common pattern when handles evolve โ
POSIX pid_t has varied width across platforms, Windows HANDLE is
void* but was historically int in some SDK versions, and database
client libraries sometimes widen handle types to support larger
connection pools.
Cross-tool comparison¶
References¶
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.