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 | browse on GitHub |
Category: Type ABI | Verdict: BREAKING
What breaks¶
The typedef handle_t changes from int (4 bytes) to void* (8 bytes on
x86-64). This changes:
- Size of the type: sizeof(handle_t) goes from 4 to 8 bytes
- Register class:
intis passed in integer registers (%edi), whilevoid*is also in integer registers but at different size (32-bit vs 64-bit) - Struct layout: any struct containing
handle_tchanges size and alignment - Return value:
handle_open()returns 4 vs 8 bytes
Old binaries compiled against v1 treat handle_t as int. When v2 returns a
pointer value, the caller truncates it to 32 bits, losing the upper half of the
address. Passing this truncated value back to handle_read() or handle_close()
causes the library to dereference a corrupted pointer.
This is different from case10 (return type changed for a plain function) because here a typedef alias is changed, which silently affects every function using that typedef. ABICC specifically flags this as a typedef underlying type change.
Why abicheck catches it¶
Header comparison detects that handle_t's underlying type changed from int to
void* (typedef_base_changed). Every function using handle_t in its signature
inherits the break, but the root cause is a single typedef change.
Code diff¶
| v1.h | v2.h |
|---|---|
typedef int handle_t; |
typedef void *handle_t; |
Real Failure Demo¶
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*)
gcc -shared -fPIC -g v2.c -o libhandle.so
./app
# -> handle = <truncated pointer as int> (upper 32 bits lost)
# -> Segfault or corruption when passing truncated handle back to library
Why CRITICAL: The old binary truncates the 8-byte pointer return to 4 bytes
(the caller was compiled to treat handle_t as int). When this truncated value
is passed to handle_read() or handle_close(), the library dereferences a
corrupt pointer, causing a segfault or heap corruption.
How to fix¶
Design handles as opaque pointers from the start, or use a fixed-width integer that is already the maximum needed size:
/* 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¶
ABICC prominently detects typedef underlying type changes. This pattern occurs in
system libraries when handles evolve: POSIX pid_t has varied 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.
References¶
Source files¶
See also: Examples overview · All BREAKING cases · Category: Breaking.