Skip to content

Case 20: Enum Member Value Changed

Field Value
Verdict 🔴 BREAKING
Category Breaking
Classification Rule
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds enum_member_value_changed
Source files catalog/cases/case20_enum_member_value_changed/
Rule family enum-member-value-changed — Variant (public-surface) of case08_enum_value_change
Subject Enum ABI and API changes

Category: Breaking | Verdict: 🔴 BREAKING

Related rule: same enum-member-value-changed rule as case08_enum_value_change (the canonical case for this rule); this case is the variant exercising public-surface scoping — ErrorCode isn't reachable from any exported function/variable signature, so detection depends on --contract all (see "Minimum evidence" below). See examples/CLAUDE.md's "Taxonomy" section.

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 --contract all

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. --contract all 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, compile.frontend: clang (via .abicheck.yml) 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

  • new/
  • old/
  • CMakeLists.txt
  • app.c

See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: Enumerator value changed · Subject: Enum ABI and API changes.