Introduction

In January 2018, researchers at Google Project Zero and several universities dropped a bombshell: every modern CPU was quietly leaking secrets through the very mechanism that makes it fast.

The trick is speculative execution. When a processor hits a conditional branch — an if-statement in machine code — it does not wait for the condition to resolve. It guesses the outcome and races ahead, executing instructions before it knows whether it should. If the guess was wrong it discards the results and tries the other path, losing only a few cycles. If the guess was right, it has saved tens or hundreds of cycles of stall time.

The problem is that discarding the results is not as clean as it sounds. The CPU does roll back the registers and memory writes. But one side effect lingers: the cache. Instructions that should never have run, on data that should never have been touched, still warm up cache lines. And cache access time is measurable.

Spectre (CVE-2017-5753, named for the spectre of speculative execution) showed that an attacker can train the branch predictor to mispredict on purpose, trick the CPU into speculatively reading a forbidden byte, encode that byte in the cache pattern, and then read it back with timing — all without any privilege, from ordinary user-level JavaScript.

It is not a bug in software. The CPU is working exactly as designed. The side channel is the microarchitecture itself.

Try It: Cache Timing Leak

The simulation below models the three phases of a Spectre-variant-1 attack. The real attack runs in nanoseconds on bare metal; here the CPU is simulated in JavaScript so you can slow it down and watch each step.

<!-- {{c_html_comment}} -->
<div class="panel" id="train-panel">
  <h3>{{phase1_title}}</h3>
  <p class="desc">{{phase1_desc}}</p>
  <div class="pred-bar-wrap">
    <div class="pred-bar" id="pred-bar"></div>
    <span class="pred-label" id="pred-label">{{pred_label_init}}</span>
  </div>
  <button id="btn-train" type="button">{{btn_train}}</button>
</div>

<div class="panel" id="probe-panel">
  <h3>{{phase2_title}}</h3>
  <p class="desc">{{phase2_desc}}</p>
  <div class="code-box" id="code-box">
    <span class="line"><span class="kw">if</span> (index &lt; array.length) {</span>
    <span class="line speculative" id="spec-line">  y = probe[array[index] * 512]  <span class="badge">{{badge_speculative}}</span></span>
    <span class="line">}</span>
  </div>
  <button id="btn-probe" type="button" disabled>{{btn_probe}}</button>
</div>

<div class="panel" id="read-panel">
  <h3>{{phase3_title}}</h3>
  <p class="desc">{{phase3_desc}}</p>
  <div class="timing-wrap">
    <canvas id="timing-canvas" width="512" height="100"></canvas>
  </div>
  <div class="result" id="result"></div>
</div>
/* {{c_css_comment}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px 0; }
h3 { margin: 0 0 .3rem; font-size: .95rem; text-transform: uppercase;
     letter-spacing: .05em; color: #1d3557; }
.desc { font-size: .82rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .7rem .9rem;
         margin-bottom: .6rem; background: #f8fafc; }
/* {{c_predictor_bar}} */
.pred-bar-wrap { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.pred-bar { height: 14px; background: #1d3557; border-radius: 7px; width: 0%;
            transition: width .25s; flex-shrink: 0; min-width: 2px; max-width: 200px; }
.pred-label { font-size: .8rem; color: #555; }
/* {{c_code_box}} */
.code-box { background: #1e2a38; border-radius: 8px; padding: .5rem .7rem;
            font-family: ui-monospace, monospace; font-size: .8rem; color: #a8b9cc;
            display: flex; flex-direction: column; gap: 2px; margin-bottom: .5rem; }
.line { display: flex; align-items: center; gap: .4rem; }
.kw { color: #79b8ff; }
.speculative { color: #f0c070; }
.badge { font-size: .65rem; background: #e63946; color: #fff;
         border-radius: 4px; padding: 1px 4px; margin-left: .3rem; }
/* {{c_timing_canvas}} */
.timing-wrap { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden;
               margin-bottom: .5rem; background: #fff; }
canvas { display: block; }
.result { font-size: .95rem; font-weight: 700; min-height: 1.3em; color: #1d3557; }
.result.found { color: #0a7d33; }
.result.waiting { color: #888; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
// Code not found

Phase 1 — Train: click Train branch repeatedly. The simulated branch predictor counts "taken" outcomes and eventually locks onto the prediction "always take the branch". This mimics the hundreds of benign iterations an attacker uses to train the predictor.

Phase 2 — Probe: click Speculative probe. The CPU is now mis-directed into speculatively reading secret[0] — memory outside the allowed window. Even though the check fails and the access is rolled back architecturally, the byte's value has been encoded into which cache line is hot.

Phase 3 — Read: the timing panel shows which of the 256 possible byte values produced the fastest cache hit. The fastest slot reveals the secret byte — without ever having the CPU "officially" return it.

The Real Complexity

Spectre is not one vulnerability — it is a class of attacks, and its depth comes from the gap between two models of a CPU:

  • Architectural state is what the ISA guarantees: registers, memory, condition codes. If an instruction is cancelled, its architectural effects vanish.
  • Microarchitectural state is everything the hardware uses for speed: caches, TLBs, branch-predictor tables, store buffers, execution ports. Cancelled instructions can still leave fingerprints here.

The Spectre attack chain has three steps: mis-train the predictor, speculatively leak a secret byte into the cache, measure which cache line is hot. Each step exploits a different microarchitectural structure:

  1. Branch predictor training — the predictor is a shared, indexed table. An attacker in user space can fill it with chosen outcomes, steering future speculation of a victim's code.
  2. Out-of-bounds speculative read — inside the speculative window, bounds checks are pending. The CPU races ahead and reads array[secret_index * STRIDE], which encodes the secret byte as an address offset.
  3. Cache-timing oracle — a FLUSH+RELOAD or PRIME+PROBE pass measures which 64-byte cache line is warm in O(256)O(256) probes, each taking a few hundred nanoseconds.

The combined effect: the attacker reads one byte of kernel memory from JavaScript in roughly 10μs10\,\mu s. Repeated, they can dump megabytes of kernel data per second.

Why is it hard to fix? A complete fix requires either:

  • Serializing every branch (destroying the speedup — 30%\sim 30\% throughput penalty), or
  • Isolating microarchitectural state between security domains (kernel page-table isolation, IBRS, retpoline, SSBD, … each patching one gadget class), or
  • Redesigning the hardware so speculation never touches cross-domain data.

Hardware vendors have shipped dozens of mitigations since 2018. New gadget classes still appear. Spectre is not a bug you fix; it is an architectural trade-off between performance and information isolation that the industry bet wrong on for two decades.

Where It Matters

Spectre rewrote the threat model for every shared computing system:

  • Cloud and hypervisors: two VMs on the same physical core can be co-tenants with a shared branch predictor. Spectre-v2 lets one guest read host kernel memory or a sibling guest's memory, defeating decades of hypervisor isolation.
  • Browsers: JavaScript executing in a sandbox can use SharedArrayBuffer plus performance.now() as a timer to run Spectre gadgets. Chrome and Firefox responded by reducing timer resolution and disabling cross-origin shared memory — an API change forced by hardware.
  • OS kernels: the kernel maps its own memory into every process's address space for efficiency (the "kernel page-table isolation" mitigation reverses this, costing 5–30 % on syscall-heavy workloads).
  • Cryptography: constant-time implementations avoid secret-dependent branches, but speculative execution can re-introduce the very branches that were manually removed — a subtle threat to previously "safe" crypto code like AES-NI fallbacks and RSA exponentiation ladders.
  • Embedded and IoT: devices without branch predictors are immune, but every out-of-order core — ARM Cortex-A, RISC-V OoO, Apple Silicon — shares the same fundamental vulnerability class.

The deeper lesson is that performance optimizations that share state across security boundaries become potential information channels. Cache, TLB, execution ports, power consumption — all have been demonstrated as oracles. Spectre is the most studied example, but the phenomenon is general: anywhere a fast path and a slow path diverge on secret-dependent data, a timing channel exists.

See also P vs NP for why some problems are fundamentally hard to solve, and halting problem for another class of limits that cannot be eliminated by clever engineering.

Conclusion

Spectre is unsettling precisely because the CPU is doing exactly what it was designed to do. Speculative execution is not a bug — it is the reason your laptop feels responsive, your cloud workloads complete in time, and your browser renders pages at 60 frames per second. The vulnerability is that the same mechanism leaves microarchitectural fingerprints that cross security boundaries.

The mitigations are real and they help — but each one carries a performance cost, each one patches a specific gadget class, and new variants continue to appear. The fundamental tension between speed and isolation has no cost-free resolution.

What Spectre teaches is that the abstraction layer between software and hardware leaks. The halting problem tells us some things are undecidable; Spectre tells us some security properties cannot be enforced without fundamentally changing what the hardware is allowed to optimize. Both are limits — one logical, one physical — and both are irreducible.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/speculative-side-channels/Content licensed under CC BY-NC 4.0.