Case 15 โ noexcept Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | API break |
Detected ChangeKinds |
โ |
| Source files | examples/case15_noexcept_change/ |
Category: Risk | Verdict: ๐ก COMPATIBLE_WITH_RISK
ground_truth.json:
expected: COMPATIBLE_WITH_RISK,category: riskchecker_policy.py:FUNC_NOEXCEPT_REMOVEDโRISK_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:
-
FUNC_NOEXCEPT_REMOVEDโ COMPATIBLE_WITH_RISK: In the Itanium C++ ABI (GCC/Clang),noexceptdoes not change the function's mangled symbol name. The symbol is identical in both.sofiles, so existing binaries resolve it โ no linkage failure, not a binary ABI break. The risk is at the source/type level: since C++17noexceptis part of the function type, so it is encoded in function-pointer and template-argument mangling (a consumer formingvoid(*)() noexceptor using the function as a non-type template argument no longer compiles), and code relying on the no-throw guarantee can hitstd::terminate. -
SYMBOL_VERSION_REQUIRED_ADDEDโ COMPATIBLE_WITH_RISK: When v2's implementation usesthrow(linking__cxa_throw/std::runtime_error), the compiled.soacquires 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_WITH_RISK) โ detected in header mode (-H). The dumper reads thenoexceptattribute from castxml output and stores it asis_noexcepton each function; the checker emitsFUNC_NOEXCEPT_REMOVEDwhen the flag changes between versions. It is not a binary break (the function symbol is unchanged) but is flagged as a deployment/source risk:noexceptis part of the function type since C++17 and removing it can break function-pointer/template consumers or causestd::terminate.SYMBOL_VERSION_REQUIRED_ADDED: GLIBCXX_3.4.21(COMPATIBLE_WITH_RISK) โ detected via ELF VERNEED comparison. When v2's implementation usesthrow, the compiled.soacquires a newer GLIBCXX symbol version requirement.
Both kinds are detected. The combined verdict is COMPATIBLE_WITH_RISK because
both FUNC_NOEXCEPT_REMOVED and SYMBOL_VERSION_REQUIRED_ADDED โ RISK_KINDS.
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¶
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
noexceptfrom the declaration, and (2) addingthrowto the implementation.The
noexceptremoval itself is COMPATIBLE (same mangled symbol). The deployment risk comes from the GLIBCXX version requirement added by thethrowin v2. The runtime crash is a behavioral contract violation โ the caller omitted landing pads because v1 declarednoexcept.
# 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¶
- C++ noexcept specifier
- P0012R1: noexcept as part of function type
checker_policy.pyโ FUNC_NOEXCEPT_REMOVED`
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.