Skip to content

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:

  1. Allocation size: Callers that allocated sizeof(Packet) + count * sizeof(float) now have half the space needed for double elements
  2. Element access: p->data[i] reads 8 bytes per element instead of 4 โ€” even indices overlap, odd indices read into uninitialized memory
  3. Return type: packet_sum() changes from float to double, 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.