Skip to content

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:

  1. Size of the type: sizeof(handle_t) goes from 4 to 8 bytes
  2. Register class: int is passed in integer registers (%edi), while void* is also in integer registers but at different size (32-bit vs 64-bit)
  3. Struct layout: any struct containing handle_t changes size and alignment
  4. 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.