Skip to content

Case 19: Enum Member Removed

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds enum_member_removed
Source files examples/case19_enum_member_removed/

Category: Breaking | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Any code compiled against old that stores, transmits, or switches on FOO's value (2) now runs against a library whose enum no longer defines that value. This is an ABI break (a symbol using the enum was recompiled around the change) and an API break (FOO is gone from the header). The value 2 doesn't stop existing at the bit level, but it's no longer a valid Status member โ€” persisted data, protocol messages, or switch statements built around it silently mishandle it.

Old/new diff

old/lib.h new/lib.h
enum Status { OK = 0, ERROR = 1, FOO = 2 }; enum Status { OK = 0, ERROR = 1 };
old/lib.c: get_status() returns FOO new/lib.c: get_status() returns ERROR

abicheck command

gcc -shared -fPIC -g old/lib.c -Iold -o libfoo_v1.so
gcc -shared -fPIC -g new/lib.c -Inew -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- enum_member_removed: Enum member removed: Status::FOO (2)
  > Old code uses a constant that no longer exists; compile error for
    source, stale value for binaries.

Minimum evidence

min_evidence: L1 โ€” DWARF's enumeration-type debug info (DW_TAG_enumeration_type / DW_TAG_enumerator) records each named member and its value for both versions, so -g alone (no public headers) is enough to detect the removed member.

Why abicheck catches it

DWARF records every enumerator name and constant value under the enum's DW_TAG_enumeration_type entry; abicheck diffs the two versions' member sets directly from debug info and flags a name present only in old as enum_member_removed.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against old (checks for FOO), swap in new .so without recompile.

# Build old library + app
gcc -shared -fPIC -g old/lib.c -Iold -o libstatus.so
gcc -g app.c -Iold -L. -lstatus -Wl,-rpath,. -o app
./app
# โ†’ FOO
# โ†’ exit: 0

# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -Inew -o libstatus.so
./app
# โ†’ WRONG RESULT: expected FOO(2), got 1
# โ†’ exit: 1

Why CRITICAL: the app's compiled-in check if (s == FOO) still compares against the constant 2, but new's get_status() now returns ERROR (1) for the situation it used to report as FOO โ€” the app silently falls into its mismatch branch instead of the case it was built to handle. Any consumer that persisted or transmitted the value 2 under the old meaning faces the same kind of silent misinterpretation.

Safe redesign

Never remove a released enum member; deprecate it instead and keep its numeric value reserved. Map legacy values deliberately in deserialization/protocol handling, and use an explicit sentinel (STATUS_MAX) to define the valid range rather than relying on "whatever values currently exist."

Real-world example: protocol and status-code enums that get "cleaned up" between releases are a recurring source of this โ€” any peer or persisted record built against the old value set silently misinterprets removed codes.

References


Source files

  • CMakeLists.txt
  • app.c

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