Skip to content

Case 20: Enum Member Value Changed

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

Category: Breaking | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Changing the numeric value bound to a released enumerator is a semantic ABI break disguised as a "just renumbering" change. Existing binaries compiled against old were built with ERROR = 1 and compare, store, and transmit that integer. new's library still returns the constant it calls ERROR, but the value is now 99. The symbolic name is unchanged; the meaning of the integer it maps to is not โ€” any consumer built against old silently stops recognizing the error condition.

Old/new diff

old/lib.h new/lib.h
enum ErrorCode { OK = 0, ERROR = 1 }; enum ErrorCode { OK = 0, ERROR = 99 };

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 --no-scope-public-headers

Expected abicheck finding

Verdict: BREAKING (exit 4)

- enum_member_value_changed: Enum member value changed: ErrorCode::ERROR
  (1 -> 99)
  > Old binaries use stale numeric values; logic comparisons and switch
    statements silently break.

Minimum evidence

min_evidence: L1 โ€” DWARF's enumeration-type debug info (DW_TAG_enumeration_type / DW_TAG_enumerator) records ERROR's constant value for both versions, so -g alone (no public headers) carries the fact. --no-scope-public-headers is needed here because ErrorCode isn't referenced by any function or variable's type (get_result() returns plain int) โ€” with no header evidence to confirm the enum is genuinely part of the public API, abicheck's default public-surface scoping conservatively withholds the finding rather than guess; passing headers (L2, --ast-frontend clang since this sandbox has no castxml) reports the same finding without needing that flag, since a header-declared enum is recognized as public even without a signature reference.

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 value maps directly from debug info and flags a name whose value changed as enum_member_value_changed.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against old (checks result == ERROR, i.e. 1), swap in new .so without recompile.

# Build old library + app
gcc -shared -fPIC -g old/lib.c -Iold -o liberr.so
gcc -g app.c -Iold -L. -lerr -Wl,-rpath,. -o app
./app
# โ†’ Error detected (correct)
# โ†’ exit: 0

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

Why CRITICAL: the app's compiled-in comparison r == ERROR still checks against the constant 1, but new's get_result() now returns 99 for the same logical error condition. The check silently fails โ€” no crash, no warning, just a missed error. Any protocol, file format, or IPC that persisted the integer 1 under the old meaning is broken across the version boundary the same way.

Safe redesign

Never reassign a released enum member's numeric value โ€” append new constants instead of renumbering existing ones. If cross-version data needs to change meaning, introduce explicit protocol versioning so old and new peers can negotiate rather than silently disagreeing on what an integer means.

Real-world example: wire-format and status-code enums are especially exposed to this because the numeric value, not just the name, is what actually crosses a process or network boundary โ€” see general protobuf field-number/enum-value stability guidance for the same principle applied to schema evolution.

References


Source files

  • CMakeLists.txt
  • app.c

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