Skip to content

Case 121: Kernel BTF Struct Field Growth

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break
Detected ChangeKinds struct_size_changed
Source files examples/case121_kernel_btf_struct_field_added/

Category: Kernel BTF / Type Layout | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Linux kernels embed type layout in BTF (BPF Type Format) โ€” the .BTF section of vmlinux, produced by pahole -J. Out-of-tree kernel modules and eBPF/CO-RE programs are compiled against one kernel's view of a struct; if a later kernel grows that struct (adds a field, changing sizeof), code built against the old layout reads/writes at the wrong offsets. Here the kernel struct task_state goes from 2 fields โ†’ 3 fields, so sizeof(task_state) changes 8 โ†’ 12 bytes. An out-of-tree module recompiled against the new kernel headers is fine; one still loaded from the old build (or a non-CO-RE eBPF program with the old offsets baked in) reads past the layout it expects or writes into memory the new kernel now uses for the third field โ€” the classic "module vs. vmlinux BTF" ABI break.

Old/new diff

v1.btf v2.btf
task_state fields f0, f1 (8 bytes) f0, f1, f2 (12 bytes)

v1.btf/v2.btf are committed, hand-assembled BTF blobs (the same on-disk format pahole -J / bpftool btf dump emit) โ€” regenerate them with python gen_btf.py. There's no v1.c/v2.c or app.c here: this case models kernel type-layout drift, not a compilable userspace library.

abicheck command

abicheck compare v1.btf v2.btf

Expected abicheck finding

Verdict: BREAKING (exit 4)

- struct_size_changed: Struct size changed: task_state (8 โ†’ 12 bytes)
  > sizeof(T) changed in debug info; confirms layout break visible at
    binary level.

(The real run also reports LOW confidence and a long list of coverage gaps โ€” expected for a bare type-metadata blob with no ELF/symbol table attached; the layout detector that flags struct_size_changed doesn't need any of that.)

Minimum evidence

min_evidence: L1 โ€” BTF carries the same kind of struct-layout facts as DWARF (total size, per-member offsets), so no source headers are needed to detect the size change; the committed blob's type records alone are the floor.

Why abicheck catches it

parse_btf_from_bytes parses the BTF type section into the same type-metadata model DWARF parsing produces, then hands it to AbiSnapshot.dwarf; compare() runs the identical struct-layout detectors against it that it would against a DWARF-derived snapshot โ€” task_state's recorded byte size (DW_AT_byte_size-equivalent) differs between v1.btf and v2.btf, so struct_size_changed fires the same way it would for a compiled .so. BTF-vs-DWARF is an input-format difference, not a different detection path.

Runtime failure demonstration

There's no app.c here โ€” the consumer isn't a userspace process linking a .so, it's kernel code. The real failure mode: an out-of-tree module (or a non-CO-RE eBPF program that reads task_state fields by hard-coded offset) is built against a kernel where task_state is 8 bytes. Loaded into โ€” or, for eBPF, verified against โ€” a kernel where task_state has grown to 12 bytes, any access to a field at or past the old struct's end reads/writes memory the new kernel layout no longer means what the module thinks it means: adjacent kernel state gets corrupted or misread, with no "undefined symbol"-style load-time error, because kernel struct layout compatibility isn't checked by the loader โ€” it's checked (for CO-RE eBPF) by field-relocation logic reading BTF at load time, or not checked at all for a plain out-of-tree module.

Safe redesign

Never grow a struct the kernel exposes as stable UAPI/kABI without a version-gated accessor or explicit opt-in (the same discipline as case07's opaque-pointer guidance, applied to kernel structs). For eBPF specifically, write CO-RE programs (BPF_CORE_READ, bpf_core_field_exists) instead of hard-coded offsets โ€” CO-RE relocations resolve each field's real offset against the running kernel's BTF at load time, so a struct grown between kernel versions doesn't silently misalign a program compiled against an older layout.

Real-world example: this is exactly the problem BPF CO-RE (Compile Once โ€“ Run Everywhere) was built to solve โ€” pre-CO-RE eBPF programs baked in fixed struct offsets from the build kernel's headers and broke across kernel struct layout changes; CO-RE's BTF-based relocations are the fix.

Cross-tool comparison

abidiff/abi-compliance-checker compare ELF+DWARF (or CTF) shared objects โ€” neither has a documented workflow for diffing a bare, compiler-independent BTF blob the way parse_btf_from_bytes does here. Reproducing this comparison with those tools would mean first embedding each BTF blob into a real vmlinux/module build and extracting debug info from that, not a direct v1.btf/v2.btf diff, so no equivalent invocation is given here.


Source files

  • gen_btf.py
  • v1.btf
  • v2.btf

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.