Case 70: Flexible Array Member Element Type Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
flexible_array_member_changed, func_return_changed |
| Source files | browse on GitHub |
Category: Type Layout | Verdict: BREAKING
What breaks¶
The element type of the flexible array member (FAM) data[] changes from
float (4 bytes) to double (8 bytes). This causes multiple ABI breaks:
- Allocation size: Callers that allocated
sizeof(Packet) + count * sizeof(float)now have half the space needed fordoubleelements - Element access:
p->data[i]reads 8 bytes per element instead of 4 โ even indices overlap, odd indices read into uninitialized memory - Return type:
packet_sum()changes fromfloattodouble, read from different register width
This is fundamentally different from fixed-size struct changes (case07, case14)
because the FAM has zero static size in sizeof(Packet) โ the struct header
size is unchanged. Tools that only compare sizeof miss this entirely. The break
is in the dynamically-allocated tail portion.
It's also different from case45 (multi-dim array change) because a FAM has no compile-time bound โ the length is determined at runtime, making the allocation mismatch especially dangerous.
Why abicheck catches it¶
Header comparison detects that Packet::data[] changed element type from float
to double (flexible_array_member_changed). The function packet_sum also
changes return type (func_return_changed). Both are flagged as BREAKING.
Code diff¶
// v1: float elements (4 bytes each)
struct Packet {
unsigned int id;
unsigned int count;
float data[];
};
float packet_sum(const struct Packet *p);
// v2: double elements (8 bytes each)
struct Packet {
unsigned int id;
unsigned int count;
double data[];
};
double packet_sum(const struct Packet *p);
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 libpacket.so
gcc -g app.c -L. -lpacket -Wl,-rpath,. -o app
./app
# -> packet_sum = 10.0
# -> Expected: 10.0
# Swap in v2 (FAM element type changed)
gcc -shared -fPIC -g v2.c -o libpacket.so
./app
# -> packet_sum = <garbage> (buffer underallocation + type mismatch)
Why CRITICAL: The old binary calls packet_create(1, 4) which now allocates
space for 4 doubles (32 bytes of FAM) but the pointer arithmetic in the old
binary still assumes 4-byte float elements. When packet_sum() reads data[2]
and data[3], it reads beyond the allocation boundary of what the caller
expected, interpreting float bit patterns as doubles.
How to fix¶
Use an opaque allocation API and accessor functions instead of exposing the FAM directly:
/* Safe: opaque packet โ element type is hidden */
typedef struct Packet Packet;
Packet *packet_create(unsigned int id, unsigned int count);
float packet_get(const Packet *p, unsigned int index); /* accessor */
If the FAM must be public, freeze the element type and add a new struct for the new type:
struct PacketF { unsigned int id, count; float data[]; }; /* keep */
struct PacketD { unsigned int id, count; double data[]; }; /* new */
Real-world example¶
Flexible array members are common in network protocol implementations (packet
buffers), database engines (variable-length records), and audio/video codecs
(sample buffers). The Linux kernel's struct sk_buff and PostgreSQL's varlena
types use this pattern extensively. Changing the element type of a FAM is a
subtle break that has caused memory corruption bugs in these projects.
libabigail specifically tracks FAM changes because sizeof(T) stays the same โ
only DWARF inspection of the trailing array element type reveals the break.
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.