Case 135: Stack Canary Removed¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | Bad practice |
Detected ChangeKinds |
stack_canary_removed |
| Source files | examples/case135_stack_canary_removed/ |
Category: ELF / Security | Verdict: ๐ก COMPATIBLE_WITH_RISK
Verdict and consumer impact¶
Both libraries export the same functions with the same signatures โ every
existing consumer keeps working, no recompilation needed. v1 is compiled
with -fstack-protector-all, so every function (including process(),
which has an on-stack buffer) gets a stack-smashing guard โ a canary
written on entry and checked before return via a reference to
__stack_chk_fail. v2 is compiled with -fno-stack-protector,
removing all canaries and the __stack_chk_fail dependency. Stack canaries
detect contiguous stack buffer overflows before a corrupted return address
is used, so dropping them is a security regression even though the
functional ABI (symbols, types, layout) is unchanged.
Old/new diff¶
| v1.c | v2.c |
|---|---|
void process(char *out, const char *in) { char buf[64]; ... }(compiled -fstack-protector-all) |
(identical source) (compiled -fno-stack-protector) |
abicheck command¶
gcc -shared -fPIC -g -O2 v1.c -o libfoo_v1.so -fstack-protector-all
gcc -shared -fPIC -g -O2 v2.c -o libfoo_v2.so -fno-stack-protector
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: COMPATIBLE_WITH_RISK (exit 0)
- stack_canary_removed: Stack canary removed: -fstack-protector no longer
referenced
Also reported as a side effect of dropping the canary (quality/toolchain
findings, non-blocking): symbol_version_required_removed and
imported_symbol_removed for __stack_chk_fail@GLIBC_2.4, which v2 no
longer imports.
Minimum evidence¶
min_evidence: L0 โ whether a function references __stack_chk_fail is
visible directly from the ELF dynamic-symbol/relocation table; no debug
info or headers needed.
Why abicheck catches it¶
abicheck scans each binary's imported dynamic symbols for
__stack_chk_fail; its presence on one side and absence on the other is
reported as a stack-protector regression straight from L0 ELF evidence.
Runtime failure demonstration¶
No observable effect on existing binaries. process()/compute()
produce identical output before and after the swap โ this is a hardening
regression that only matters if process()'s buffer is actually
overflowed, which normal input never triggers:
gcc -shared -fPIC -g -O2 v1.c -o libfoo.so -fstack-protector-all
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ process -> hello
# โ compute(7) = 50
# Swap in new library (no recompile)
gcc -shared -fPIC -g -O2 v2.c -o libfoo.so -fno-stack-protector
./app
# โ process -> hello (unchanged)
# โ compute(7) = 50 (unchanged)
A buffer overflow that v1 would catch at return time (aborting the process before a corrupted return address is used) goes undetected in v2 โ observable only under an actual overflow, not normal operation.
Safe redesign¶
Build release shared objects with at least -fstack-protector-strong.
Distro hardening policies require it; don't let it silently regress to
-fno-stack-protector in a release build.
Cross-tool comparison¶
abidiff compares ABI XML dumps (symbols and types) and has no concept of
compiler-inserted stack-protector instrumentation, so it reports no
difference at all between these two libraries. Hardening-lint tools
(checksec) catch the canary regression but don't do ABI diffing, so a
genuine symbol/type break shipped in the same release would slip past
them. abicheck reports both from one pass over the binary.
Source files¶
CMakeLists.txtapp.cv1.cv2.c
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.