Defenses Against Return Address Manipulation in C

In C programming, memory safety vulnerabilities such as buffer overflows frequently allow malicious actors to overwrite saved return addresses on the call stack, enabling arbitrary code execution through techniques like Return-Oriented Programming (ROP). To prevent this execution hijacking, modern systems employ several hardware- and software-based defenses. This article outlines the primary mechanisms used to protect return addresses in compiled C binaries, including compiler-inserted stack canaries, hardware and software shadow stacks, backward-edge Control Flow Integrity (CFI), and ARM Pointer Authentication.

Stack Canaries

Stack canaries (also known as Stack Smashing Protection) are one of the oldest and most widely deployed defenses against linear buffer overflows. Supported by compilers like GCC and Clang via flags such as -fstack-protector-strong, this mechanism places an integrity check value—the canary—on the stack directly between local variables and the saved return address.

Before a function executes its ret instruction, it verifies that the canary value remains unchanged. If a buffer overflow overwrites the return address, it inevitably alters the canary first. The application detects the mismatch and immediately terminates execution before the corrupted return address can be used. However, canaries cannot stop targeted non-linear writes or memory disclosure attacks that leak the canary value.

Shadow Stacks

A shadow stack is a dedicated memory region used exclusively for storing return addresses, completely isolated from normal program data. When a function call occurs, the return address is pushed onto both the standard call stack and the shadow stack. Upon function return, the system compares the two values or pops the address directly from the shadow stack.

Hardware Shadow Stacks

Modern architectures integrate shadow stacks directly into the processor to minimize performance overhead and prevent tampering:

  • Intel Control-flow Enforcement Technology (CET): Implements a hardware-enforced shadow stack. The CPU maintains an internal shadow stack pointer (SSP) that cannot be modified by ordinary memory write instructions. If the return address on the data stack does not match the one on the shadow stack during a return instruction, the CPU triggers a control protection exception (#CP).
  • ARM Guarded Control Stack (GCS): Introduced in ARMv9-A, GCS functions similarly to Intel CET, utilizing privileged hardware instructions and dedicated memory page permissions to ensure that the return stack remains inaccessible to standard store operations.

Software Shadow Stacks

Compilers can also generate software-managed shadow stacks when hardware support is absent. For example, Clang offers ShadowCallStack, which reserves a dedicated CPU register (such as x18 on AArch64) to point to a private shadow stack, ensuring return addresses are saved and restored without writing them to the standard data stack.

Pointer Authentication Codes (PAC)

Introduced in the ARMv8.3-A architecture, Pointer Authentication uses cryptographic signatures to ensure pointer integrity. When compiling C code with PAC support:

  1. When entering a function, the compiler inserts an instruction (e.g., PACIASP) that generates a cryptographic hash of the return address combined with a secret processor key and a context modifier (such as the current stack pointer).
  2. The resulting signature is stored in the unused upper bits of the 64-bit return address.
  3. Before returning, the function executes an authentication instruction (e.g., AUTIASP or RETAA).

If an attacker overwrites the return address in memory, the cryptographic signature becomes invalid. When authentication fails, the processor corrupts the address into a designated illegal value, causing a fatal segmentation fault when the branch occurs.

Backward-Edge Control Flow Integrity (CFI)

Control Flow Integrity restricts the execution path of a binary to a strictly defined control-flow graph (CFG) generated at compile time. While forward-edge CFI protects indirect calls and jumps, backward-edge CFI focuses specifically on function returns.

Backward-edge CFI mechanisms record the set of valid call sites for every function. Upon returning, the binary verifies that the destination address corresponds to an allowed call site for that specific routine. While effective, software-only backward-edge CFI often incurs noticeable performance costs, leading most production environments to favor hardware shadow stacks or Pointer Authentication as more efficient alternatives.