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.txtapp.c
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.