Skip to content

CONCEPT Cited by 1 source

Stack unwinding

Definition

Stack unwinding is the runtime operation of walking backwards through a thread's chain of function calls, starting from the current stack pointer and following saved frame / return-address links to reach every parent frame up to the call stack's root.

It is foundational to several runtime features:

  • Garbage collection — the GC walks goroutine stacks to locate live references to heap-allocated objects (in Go: runtime.scanstack).
  • Panic recoveryrecover() runs deferred functions by unwinding frames until it finds a matching defer.
  • Traceback generation — printing a stack trace (crash report, runtime.Stack(...)) requires unwinding every active frame.
  • Exception handling (C++, Rust panics) — the same operation in other runtimes.

The core invariant: sp must be valid

Unwinders read the stack pointer sp and dereference it to locate the calling function. The runtime assumes sp points to a consistent frame state: return addresses, saved frame-pointer, and saved registers are at the offsets the calling convention prescribes.

If sp is partially adjusted — e.g. mid-function-epilogue on an ISA where stack-pointer adjustment is split across multiple opcodes — the unwinder reads "the middle of the stack" as if it were a frame header. The data it reads is meaningless when interpreted as a return address. Two failure modes result:

  1. Return address is null → unwinder aborts with traceback did not unwind completely. In Go: finishInternal throws a fatal error.
  2. Return address is non-zero but points to non-code → unwinder assumes the goroutine is currently running and attempts to access scheduler state via m. In Go: a dereference of m.incgo at offset 0x118 → SIGSEGV.

Canonical wiki instance: sources/2025-10-08-cloudflare-we-found-a-bug-in-gos-arm64-compiler.

Triggers for unwinding during a bug's exposure window

What makes a stack-pointer invariant violation so dangerous is that it only needs to coincide with a later unwinding to crash. Any of the following can trigger the unwinder:

  • GC stack scanning (scanstack) — any time a GC cycle runs.
  • Preemption for preempt-at-safe-point (concepts/async-preemption-go in Go 1.14+).
  • Stack growth — the runtime copies the stack and must rewrite return addresses.
  • A panic()recover() chain walking deferred functions.
  • User-initiated runtime.Stack(...) or profiler sampling.

In the Cloudflare 2025-10 bug, GC's stack scan was the most common crash trigger — the service had frequent GC cycles, and each one walked every goroutine stack.

Compile-time obligation

The compiler must emit code such that no preemption boundary (any instruction boundary under async preemption) leaves sp in a partially-adjusted state that the runtime can observe. See patterns/preemption-safe-compiler-emit.

Seen in

Last updated · 200 distilled / 1,178 read