Skip to content

Case 15 โ€” noexcept Changed

Field Value
Verdict ๐ŸŸก COMPATIBLE_WITH_RISK
Category Risk
Platforms Linux
Flags API break
Detected ChangeKinds โ€”
Source files browse on GitHub

Category: Risk | Verdict: ๐ŸŸก COMPATIBLE_WITH_RISK

ground_truth.json: expected: COMPATIBLE_WITH_RISK, category: risk checker_policy.py: FUNC_NOEXCEPT_REMOVED โˆˆ COMPATIBLE_KINDS; SYMBOL_VERSION_REQUIRED_ADDED โˆˆ RISK_KINDS

What changes

Version Signature Implementation
v1 void reset() noexcept; no-throw implementation
v2 void reset(); throws std::runtime_error

Why this is COMPATIBLE_WITH_RISK (not BREAKING)

The verdict has two independent components:

  1. FUNC_NOEXCEPT_REMOVED โ†’ COMPATIBLE: In the Itanium C++ ABI (GCC/Clang), noexcept does not change the mangled symbol name. The symbol is identical in both .so files. Existing binaries resolve the same symbol โ€” no linkage failure occurs. This is a source-level contract concern (C++17 function type system), not a binary ABI break.

  2. SYMBOL_VERSION_REQUIRED_ADDED โ†’ COMPATIBLE_WITH_RISK: When v2's implementation uses throw (linking __cxa_throw / std::runtime_error), the compiled .so acquires a newer GLIBCXX symbol version requirement (e.g. GLIBCXX_3.4.21). This is a deployment risk: the new library won't load on systems with an older libstdc++. But it is not a binary ABI break for the library's own symbols.

Combined verdict: COMPATIBLE_WITH_RISK โ€” binary-compatible, but deployment risk present from the new GLIBCXX requirement.

What abicheck detects

  • FUNC_NOEXCEPT_REMOVED (COMPATIBLE) โ€” detected in header mode (-H). The dumper reads the noexcept attribute from castxml output and stores it as is_noexcept on each function; the checker emits FUNC_NOEXCEPT_REMOVED when the flag changes between versions. This kind is classified as COMPATIBLE because it does not change the mangled symbol name (Itanium ABI).
  • SYMBOL_VERSION_REQUIRED_ADDED: GLIBCXX_3.4.21 (COMPATIBLE_WITH_RISK) โ€” detected via ELF VERNEED comparison. When v2's implementation uses throw, the compiled .so acquires a newer GLIBCXX symbol version requirement.

Both kinds are detected. The combined verdict is COMPATIBLE_WITH_RISK because FUNC_NOEXCEPT_REMOVED โˆˆ COMPATIBLE_KINDS and SYMBOL_VERSION_REQUIRED_ADDED โˆˆ RISK_KINDS, and RISK trumps COMPATIBLE in the verdict hierarchy.

Behavioral risk (runtime)

While the binary linkage is fine, there is a critical behavioral risk:

Code compiled against v1 may omit exception landing pads, assuming reset() never throws. If v2's reset() throws at runtime, the exception propagates through the noexcept frame and std::terminate is called โ€” no recovery possible.

This is a contract violation, not a linkage failure. The app terminates because the caller trusted the noexcept guarantee, not because symbols are missing.

Important distinction

This case demonstrates the difference between ABI verdict and runtime safety:

Aspect Assessment
Binary ABI (symbol linkage) COMPATIBLE โ€” same mangled name
Deployment risk (GLIBCXX) COMPATIBLE_WITH_RISK โ€” new version requirement
Runtime safety (behavioral) CRITICAL โ€” std::terminate if v2 throws

The tool reports COMPATIBLE_WITH_RISK because it analyzes binary compatibility. The behavioral risk (noexcept contract violation) is a separate concern that requires source-level analysis or runtime testing to detect.

Why abidiff misses it

abidiff compares DWARF type information and symbol tables. noexcept is not stored in DWARF โ€” it is purely a source-level annotation. abidiff has no way to detect the change.

Why ABICC catches it

ABICC parses C++ headers using GCC internals and sees the noexcept specifier. When v1 and v2 headers differ in noexcept, ABICC flags it as a source-level break.

Code diff

-void reset() noexcept;
+void reset();

Reproduce steps

cd examples/case15_noexcept_change

# Build v1 and v2
g++ -shared -fPIC -std=c++17 -g v1.cpp -o libv1.so
g++ -shared -fPIC -std=c++17 -g v2.cpp -o libv2.so

# abidiff: misses the change (noexcept not in DWARF)
abidw --out-file v1.xml libv1.so
abidw --out-file v2.xml libv2.so
abidiff v1.xml v2.xml || true   # exits 0 โ€” misses it!

# abicheck with headers: detects both noexcept removal and GLIBCXX bump
python3 -m abicheck.cli dump libv1.so -H v1.h -o /tmp/v1.json
python3 -m abicheck.cli dump libv2.so -H v2.h -o /tmp/v2.json
python3 -m abicheck.cli compare /tmp/v1.json /tmp/v2.json
# โ†’ COMPATIBLE_WITH_RISK
#   - func_noexcept_removed: Buffer::reset (COMPATIBLE)
#   - symbol_version_required_added: GLIBCXX_3.4.21 (RISK)

Real Failure Demo

Severity: CRITICAL (behavioral, not linkage)

Scenario: app compiled against v1 (reset() noexcept) calls v2 which throws โ€” exception propagates through a noexcept frame โ†’ std::terminate.

Important: This demo combines two changes: (1) removing noexcept from the declaration, and (2) adding throw to the implementation.

The noexcept removal itself is COMPATIBLE (same mangled symbol). The deployment risk comes from the GLIBCXX version requirement added by the throw in v2. The runtime crash is a behavioral contract violation โ€” the caller omitted landing pads because v1 declared noexcept.

# Build v1 + app (app includes v1.h which declares reset() noexcept)
g++ -shared -fPIC -std=c++17 -g v1.cpp -o libbuf.so
g++ -std=c++17 -g app.cpp -I. -L. -lbuf -Wl,-rpath,. -o app
./app
# โ†’ Calling reset()...
# โ†’ reset() completed OK

# Swap in v2 (reset() throws)
g++ -shared -fPIC -std=c++17 -g v2.cpp -o libbuf.so
./app
# โ†’ terminate called after throwing an instance of 'std::runtime_error'
#      what():  reset failed
# โ†’ Aborted (core dumped)

Why CRITICAL (behavioral): The caller was compiled with the assumption that reset() is noexcept, so no try/catch or landing pad was generated. When v2 throws, std::terminate is called unconditionally โ€” no recovery possible. Note: the binary linkage is fine (COMPATIBLE); the crash is a behavioral contract violation, not a symbol resolution failure.

Real-world example

In Folly (Facebook's C++ library), several internal reset() and destroy() methods had noexcept removed during a refactor. Downstream projects compiled with old headers started hitting silent std::terminate crashes when running with the new .so.

References


Source files

See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.