How Spectre Works: Speculative Execution, Branch Predictors, and Cache Side Channels

A bounds check that returns early is not a security boundary when the processor has already executed the instructions that follow it. Spectre shows why.

This article explains the concrete mechanism: how a mispredicted branch lets the CPU run a gadget that loads a secret into a cache line, and how an attacker later times that cache line to recover the secret. It is not a second tour of the fetch-decode-execute cycle, page tables, or system-call entry. Those topics are covered elsewhere on this site. The question here is narrower: how does speculative execution turn a software bounds check into a data leak?

The contract the attack breaks

A program is allowed to read only the memory its page tables and permission bits authorize. The architectural state (registers, the committed instruction stream) must obey that rule. Transient state does not. Speculative execution is allowed to touch data that will later be discarded, provided the discard is perfect with respect to architectural state. The Spectre family of attacks shows that the discard is not perfect with respect to microarchitectural state, specifically the cache.

Two ingredients are required:

  • A speculation window large enough to execute a gadget that loads a secret and uses it to index another array.
  • A side channel that turns the resulting cache state into a bit of information an unprivileged observer can measure.

Branch prediction and the speculation window

Modern cores do not wait for a conditional branch to resolve before fetching the next instructions. They predict the direction, fetch and execute down the predicted path, and later squash the results if the prediction was wrong. The predictor is a table of history (local, global, or hybrid) indexed by the branch PC and recent outcomes. On a correct prediction the pipeline stays full. On a misprediction the pipeline drains, but the work that already happened has already touched caches, TLBs, and other structures.

The window between the misprediction and the squash is the speculation window. Its length is measured in cycles and is large enough on contemporary cores for dozens of instructions, including loads that miss in the L1 and hit in L2 or L3. That is enough for the classic Spectre gadget.

A concrete Spectre-v1 style gadget

Consider a function that is supposed to return zero when an index is out of range:

uint8_t array1[16];
uint8_t array2[256 * 512];

uint8_t victim_function(size_t x) {
    if (x < array1_size) {
        return array2[array1[x] * 512];
    }
    return 0;
}

Architecturally the check protects the access. Speculatively the processor can predict that the branch is taken, execute the load of array1[x] even when x is attacker-controlled and out of bounds, then use the resulting secret byte as an index into array2. The second load brings one specific 512-byte stride of array2 into the cache. When the branch finally resolves and the speculation is squashed, the architectural registers are restored, but the cache line remains.

An attacker who can invoke this function (or an equivalent gadget in the same address space, or across a privilege boundary in later variants) therefore leaves a measurable footprint: exactly one of the 256 possible cache lines in the probe array is warm.

The cache side channel

The attacker prepares by flushing the probe array (or waiting until it is cold), invokes the victim gadget with a malicious index, then times accesses to each of the 256 possible strides. The stride that hits in the cache returns a lower latency. That latency difference is the leaked byte. Flush+Reload or Prime+Probe are the usual measurement methods; the exact technique is secondary to the fact that the cache hierarchy is shared and not cleared on a squash.

The same pattern appears in other Spectre variants (indirect branch, return-stack-buffer, etc.). The details of the predictor differ; the observation that transient execution can modulate the cache does not.

Why software bounds checks are not enough

The check is executed. The processor simply does not wait for its result before continuing. Inserting a memory fence or a serializing instruction closes the window, which is why many mitigations do exactly that on the sensitive paths. The cost is lost speculation and therefore lost performance on the common (correctly predicted) case.

Kernel mitigations (retpoline, IBRS, STIBP, SSBD, etc.) and browser process isolation reduce the set of usable gadgets and the ability to measure the cache across privilege or process boundaries. They do not remove the underlying microarchitectural behavior; they shrink the attack surface.

Misconceptions

  • "The bounds check failed." The check succeeded; the speculation simply ran ahead of it.
  • "Speculative execution is a bug." It is a deliberate performance feature. The bug is that the side effects of the speculation are observable.
  • "Only JavaScript in browsers is affected." Any code that can be trained and that shares a cache with an attacker-controlled probe can be a target. The original papers demonstrated both same-process and cross-privilege leaks.
  • "Meltdown is the same attack." Meltdown exploits a different window (the delay before a permission fault is raised) and is largely mitigated by kernel page-table isolation. Spectre variants survive KPTI because they do not rely on crossing a page-table permission boundary during the speculation.

Takeaways

Spectre works because the processor is allowed to execute instructions whose results will be discarded, and because the cache is not part of the architectural state that gets rolled back. A bounds check is a control-flow decision, not a barrier to speculation. The practical defenses are a combination of reduced speculation on sensitive paths, isolation that prevents the attacker from measuring the resulting cache state, and the ongoing hardware work that tries to make transient execution less observable. Understanding the gadget and the side channel is the prerequisite for judging whether a given mitigation actually closes the window that matters.

Related reading on this site: How CPUs Execute Instructions, How Virtual Memory Works, and How System Calls Work.

Next Post Previous Post