Security Engineering · Binary Exploitation
Stack Canaries and the Value That Terminates the Overflow
A per-process guard word placed between the buffer and the saved return address converts a silent hijack into a detected abort, and its design depends on which byte patterns an overflow cannot write.
A canary does not prevent an overflow. It detects one, after it has happened, at the moment the function is about to return. The value it adds to the frame is a random word placed between the local buffers and the saved return address, checked against a copy held elsewhere before the epilogue runs. If the check fails, the process aborts. The design question is not how to stop the write, which the compiler cannot do, but how to make the write necessarily destroy something whose corruption is detectable.
Why the check sits where it does
The frame is arranged so that the shortest path from any buffer to the return address passes through the canary slot. That means the canary must be placed above every buffer the function allocates, which is a constraint on frame layout rather than on the canary itself. A compiler implementing the feature reorders locals so that arrays sit below the guard and scalars that are not arrays may sit above it.
The check is emitted in the function’s epilogue, immediately before the saved registers and the return address are restored. The order matters: the canary must be verified while the return address is still on the stack and unmodified, so that a failure aborts before the corrupted address is loaded into rip. A check that ran after the pop of the saved registers would still catch the corruption but would have already restored a value the attacker controlled into a callee-saved register.
On x86-64 the check is a load of the canary slot, a comparison against the thread-local reference value, and a conditional jump to a failure routine. The failure routine calls __stack_chk_fail, which writes a message to standard error and raises SIGABRT. The process does not attempt recovery, because the state of the frame is unknown and continuing would mean running with a corrupted stack.
mov rax, QWORD PTR fs:0x28 ; thread-local reference value
cmp QWORD PTR [rbp-0x8], rax ; compare with the frame's copy
jne .stack_chk_fail ; mismatch: abort instead of returning
leave
ret
The comparison precedes the restoration of rbp and rip, so a failed check leaves the corrupted return address unloaded. The load from fs:0x28 is a single access through the thread pointer and is normally satisfied from cache.
The terminator byte and what it rules out
The reference value is not uniformly random. Its least significant byte is zero. On a little-endian machine, that byte occupies the lowest address of the eight-byte slot, which is the byte an overflow reaches first. Any overflow driven by a function that stops at a NUL byte must therefore leave that byte untouched: the write halts before it, or writes a NUL and stops.
The terminator exists because of the copying functions that made overflows common in the first place. strcpy, strcat, sprintf, and gets all terminate on a zero byte, and none of them can write a zero in the middle of the data they copy. A guard whose first byte is zero cannot be overwritten by any of them, so the simplest class of overflow cannot reach the return address at all without also corrupting the canary. The same property means the canary cannot be leaked by a string-based read that stops at the first zero: a printf with %s pointed at the canary prints nothing.
The cost of the terminator is that the canary has seven random bytes rather than eight, which is a reduction of eight bits in its entropy. That is irrelevant for a single guess, because the value is compared in full and a wrong guess aborts. It becomes relevant when an attacker can overwrite the canary byte by byte and observe whether the process survived, a byte-at-a-time oracle that recovers the seven random bytes in at most 7×256 attempts if the failure is distinguishable. The byte-by-byte recovery is only possible with a write primitive that does not terminate on zero, such as a length-controlled read, which is why the terminator is a defence against the string functions and not against memory writes in general.
What the reference value is and where it lives
The guard must be the same on check as it was on entry, and it must differ between processes so that a value learned from one run is useless against another. It is stored in thread-local storage, at a per-thread offset, and the canary slot in the frame is a copy of it. Each thread gets its own copy, initialised at thread creation from a process-wide master value.
On glibc the master value is derived at startup from the AT_RANDOM entry of the auxiliary vector, which the kernel fills with sixteen random bytes. The loader copies those bytes into the thread-local slot, and the terminator byte is set to zero. The result is that the canary is set before any user code runs, is inherited across fork, and is regenerated only when the process is replaced by execve.
Fork inheritance is the practical weakness. A server that forks a worker per connection gives every worker the same canary, because the child’s memory is a copy of the parent’s. An attacker who can crash workers and observe the abort can test guesses against a stable value; a leak from one worker is a leak for all of them. The defence assumes a fresh process per guess, which is exactly what a forking server does not provide.
Which functions get instrumented
GCC and Clang expose the feature at three levels. -fstack-protector instruments functions that contain a local array of any size or whose address is taken. -fstack-protector-strong widens that to functions with local arrays, or with locals whose address is passed to another function, or with local structures containing arrays. -fstack-protector-all instruments every function, which costs a load, a compare, and a branch in every prologue and epilogue, plus eight bytes of stack and the loss of one register on architectures where the reference value is kept in a register.
The distinction matters because the classic overflow target is often a function with no local array at all. A function that copies into a buffer passed in by its caller, or that writes through a pointer into a structure it received, has no local array for the instrumentation pass to notice. Under -fstack-protector such a function is not protected, and the overflow still reaches its return address. That gap is why -strong exists, and why it is the default on several distributions.
The cost is measurable but small. The instrumented prologue loads the thread-local reference, stores it to the frame, and the epilogue reverses the comparison. On x86-64 with the reference reachable through the thread pointer, the sequence is a few instructions; the load from thread-local storage is a single access on a cached segment. The eight bytes per frame matter only for functions with deep recursion, where the extra stack per level reduces the maximum depth.
The bypass: overwrite the canary with itself
A canary that is checked but never leaked can still be defeated if the attacker can write the correct value into its slot. That requires knowing the value, which requires a read. A format-string bug that reads stack slots can disclose the canary directly, because the canary is on the stack and the %p or %s conversion reads successive slots. Once known, an overflow writes the original value back into the canary slot and continues into the return address; the check passes because the value matches.
This is the reason a canary is described as an obstacle rather than a barrier. It raises the cost of an overflow from “write the return address” to “write the return address and preserve a secret”, and the secret is only as safe as the program’s ability to keep its memory contents from being printed. The pairing is deliberate: the canary defends against the overflow, and the leak defences are what protect the canary. The format-string write primitives that disclose it are the standard route.
A second bypass does not need the value. If the overflow can reach a pointer that the function uses before it returns, the attacker can corrupt that pointer and gain a write or a call without touching the canary at all. A local function pointer, a structure pointer, or a length variable placed below the guard is fair game. The instrumentation only protects the return address, so any other control data in the frame is unprotected, and compilers place such data wherever the layout allows.
Where the guard does not apply
The canary is a stack mechanism. It does not protect heap objects, global buffers, or return addresses in frames the compiler did not instrument. A global array overflow in a data section reaches whatever the linker placed after it, and no check exists for that. Heap corruption is detected only by the allocator’s own consistency checks, which are a different mechanism with a different failure mode, described in glibc heap chunks.
Longjmp buffers, C++ exception objects, and the saved context of a signal handler all live in memory that an overflow can reach, and none of them are covered by the canary check. setjmp writes a jmp_buf containing the stack pointer and program counter; corrupting it and calling longjmp transfers control without any return address being involved. The compiler’s protector knows nothing about it.
Finally, the canary is checked once per function return, so a function that never returns is never checked. A chain that ends in exit or that pivots the stack and never executes the epilogue leaves the corrupted canary in place and harmless. The check is a property of the return path, and an exploit that avoids returning avoids it.
References
- StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks — Cowan et al., USENIX Security 1998
- ProPolice: GCC Stack-Smashing Protector — IBM Research, Hiroaki Etoh
- GCC Manual: -fstack-protector and related options — GNU Project