Skip to content

Case 135: Stack Canary Removed

Field Value
Verdict 🟡 COMPATIBLE_WITH_RISK
Category Risk
Classification Rule
Platforms Linux
Flags Bad practice
Detected ChangeKinds stack_canary_removed
Source files catalog/cases/case135_stack_canary_removed/
Rule family stack-canary-removed
Subject ELF security hardening and deployment risk

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 inspects 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: Compatibility Catalog · All COMPATIBLE_WITH_RISK cases · Category: Risk · Rule: Stack protector removed · Subject: ELF security hardening and deployment risk.