Skip to content

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.txt
  • app.c
  • v1.c
  • v2.c

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