Memory Systems · Memory Safety
Shadow Stacks and Protecting the Return Address
Why a parallel copy of the return address catches overflows that canaries miss, how the check is wired into the epilogue, and what the hardware support changes.
The return address is the single most valuable word in a stack frame, because the instruction that uses it transfers control. An overflow that reaches it can redirect execution to any address the attacker can name, subject to the mitigations that make naming an address hard. A canary between the buffer and the address makes the overwrite detectable, but only if the overwrite is linear and travels through the canary’s position. A write that reaches the return address some other way, through a wild pointer or an out-of-bounds index into a structure in the frame, leaves the guard intact.
A shadow stack addresses the target rather than the path. It keeps a second copy of every return address, in memory the program’s ordinary writes cannot reach, and compares the two at the moment the address is used. The comparison does not care how the primary copy was corrupted.
Two stacks, one call sequence
The mechanism is a parallel stack of return addresses maintained by the call and return sequences. On entry to a function the return address is pushed onto both the ordinary stack, where the epilogue will load it, and the shadow stack, where nothing but the call and return machinery writes. On return, the epilogue pops the shadow copy, compares it against the address it is about to jump to, and terminates if they differ.
# The comparison at a protected return, in the shape hardware support provides.
call foo # pushes the return address on both stacks
...
foo:
...
ret # pops the shadow entry; mismatch is a fault
The shadow stack is not a full second stack. It holds return addresses only, no locals, no spilled registers, no frame pointers. That keeps it small, four or eight bytes per call, and it means the shadow stack’s contents are entirely predictable from the call sequence, which is what makes the hardware implementation tractable.
Two properties make the copy trustworthy. The shadow stack’s pages are mapped by the kernel and not writable by ordinary stores: writing to them requires an instruction the hardware recognises as a call or return, and an attempt to store through a normal store instruction faults. The shadow stack pointer itself is not a general-purpose register the program can load, so the program cannot compute an address inside the shadow stack and cannot redirect the pointer.
The software-only versions of the scheme predate the hardware and are weaker in exactly the ways one would expect. A compiler can keep the shadow stack in a separate region and instrument every call and return, but the region is ordinary memory that a wild write can reach, and the shadow pointer is ordinary memory too. The protection is then a reduction in the attacker’s options rather than a guarantee: the shadow copy is in a place the attacker does not know, rather than in a place they cannot write.
The epilogue check and its cost
In hardware implementations the check is fused into the return instruction. The processor pops the shadow entry, compares, and on mismatch raises a control-protection fault rather than branching. The cost is one additional memory read from a region that is hot in cache, and the shadow stack’s top entries are accessed with the same locality as the ordinary stack’s, so the read is usually a cache hit.
Software implementations pay more. Instrumenting every call and return adds two stores and a load per call, and the comparison at return is a load, a compare, and a branch. Because a call is already one of the more expensive instructions in terms of pipeline effects, the relative cost is modest but not zero, and it applies to every function including leaf calls that never touch memory.
There is a subtler cost in the interaction with exceptions and unwinding. A shadow stack assumes that returns match calls in LIFO order. An exception that unwinds through several frames does not execute the epilogues of the frames it skips, so the shadow stack must be unwound by the runtime using the same unwind tables the language runtime already needs. If the unwinder and the shadow stack disagree, the shadow pointer is left pointing at entries that no longer correspond to live frames, and the next return faults. Getting this right requires the unwinder to be aware of the shadow stack, which is why the hardware design includes instructions for adjusting the shadow pointer during unwinding rather than leaving it to the runtime.
Long jumps and setjmp/longjmp have the same problem in miniature. A longjmp restores a saved stack pointer and continues, discarding the intervening frames without returning through them. The shadow stack’s entries for those frames are stale, and the shadow pointer must be reset to match the target frame. Implementations handle this by having the save record include the shadow pointer, so the restore can rewind it.
What the check catches that a canary does not
The set of overflows a shadow stack catches is larger, and the difference is worth stating precisely.
A canary catches a write that crosses the canary’s address on its way to the return address. A shadow stack catches any modification of the return address, however it was made, because the comparison is between the value about to be used and a copy that the modification could not have reached. A non-linear write that skips the canary entirely, such as an out-of-bounds index into a struct whose field happens to sit at the return address’s offset, is invisible to the canary and caught by the shadow stack.
It also catches the consequences of a corrupted saved frame pointer. The shadow stack does not protect the frame pointer directly, and a function that returns through a corrupted frame pointer usually loads its return address from the wrong place, which the shadow stack then reports.
The gaps are equally clear. A shadow stack protects return addresses and nothing else. A corrupted function pointer in a structure, a corrupted vtable, a corrupted saved register that is later used as an address. None of these pass through the check. That is the motivation for the broader class of control-flow integrity schemes, which verify that every indirect branch targets a location the program could legitimately have reached. The forward-edge and backward-edge distinction is exactly this split: the shadow stack is a backward-edge mechanism, and the forward edge needs its own answer.
Pointer authentication as the alternative
Arm’s pointer authentication takes a different route to the same guarantee. Instead of storing a second copy, it appends a cryptographic signature to the pointer itself. The signature is computed over the pointer value and a context using a key held in a register the program cannot read, and it occupies the bits of the 64-bit address that the architecture leaves unused. Signing on entry and verifying on return gives the same detection property without a second stack, in the spirit of the pointer provenance that capability architectures enforce.
| 63 .. 56 | 55 .. 48 | 47 .. 0 |
PAC bits more PAC virtual address
The trade differs from a shadow stack in three ways. It needs no additional memory, because the signature lives in the pointer’s spare bits, which matters on systems with constrained memory. It covers more than return addresses: the same construction signs function pointers and data pointers, so it protects the forward edge too, provided the compiler signs and verifies at the right places. And it costs arithmetic rather than a memory access: a few instructions to compute and check the signature, on the critical path of every call and return.
Its weakness is that the signature is a value in memory. An attacker with a read primitive can capture a signed pointer and reuse it in the same context, which is a replay rather than a forgery. The context parameter is what limits the replay: signing a return address with the stack pointer as context means a captured signature is valid only for the same stack location, and a signature captured at one call site cannot be used at another. Context discipline is therefore part of the scheme’s strength, and a compiler that signs without a discriminating context leaves the replay open.
Deployment and its friction
Hardware shadow stacks require the operating system, the compiler, and the loader to agree. The kernel maps the shadow stack region and enables the feature per process; the compiler emits the instructions that maintain it; the loader must not map anything that conflicts with the region. A program that uses a custom stack switching mechanism, such as a coroutine library or a language runtime with its own stacks, has to maintain the shadow stack across switches, which means the runtime must be aware of the feature or must disable it for the affected code.
The incompatibility with hand-written assembly is the practical obstacle. A function written in assembly that manipulates the stack pointer and returns by popping a saved address will fault on return unless it also updates the shadow stack, and assembly that predates the feature has no reason to. Implementations provide per-object opt-out, which means the guarantee is per-binary rather than per-process, and the opt-out is a hole that an attacker can aim for.
This is the shape of every mitigation of this kind: the mechanism is sound where it is applied, the coverage is decided by build and deployment choices, and the residual risk is concentrated in the code that opted out. The canary sits at the cheap end of this spectrum and the shadow stack at the strong end, with the same underlying idea, comparing a value against a copy the write could not reach, implemented at increasing cost and coverage.
References
- Control-Flow Integrity: Principles, Implementations, and Applications — Abadi et al., ACM CCS 2005
- Intel Control-flow Enforcement Technology Specification — Intel Corporation
- Intel 64 and IA-32 Architectures Software Developer Manuals — Intel Corporation
- Arm Architecture Reference Manual for A-profile architecture — Arm Limited