Introduction

Every time your code reaches an if statement, the CPU faces a choice: wait until the condition resolves (stalling for several clock cycles), or guess which branch will be taken and start executing it right away. Modern processors chose to guess — and almost always get it right.

This technique, speculative execution, is one of the biggest reasons that today's processors run many times faster than a naive design would. The branch predictor learns your code's patterns over thousands of iterations and predicts with over 95% accuracy. When the guess is correct, the work is already done. When it is wrong, the processor squashes everything it computed speculatively, rolls back the architectural state, and takes the correct path instead. No harm done — or so it seemed.

In January 2018, researchers from Google Project Zero and several universities disclosed Spectre and Meltdown, two families of attacks that exploit exactly what happens during that squash. The architectural state is rolled back perfectly — but the microarchitectural state, most importantly the cache, is not. The discarded speculative work leaves a footprint in the cache that an attacker can measure, one memory access at a time. Data that was never supposed to be readable can be extracted byte by byte.

The gap between what the architecture promises and what the microarchitecture actually does turned out to be one of the most consequential security flaws in hardware history.

Try It: Branch Training

This demo simulates the core of a Spectre-style attack in three steps. First, a branch is trained so the predictor expects the "safe" path. Then one out-of-bounds access is fired speculatively. Finally, the cache timing reveals which memory line was touched — exposing the secret without ever reading it directly.

<!-- Three-phase Spectre-style demo: Train → Attack → Probe -->
<div class="panel" id="panel-train">
  <h3>Phase 1 — Train the Branch Predictor</h3>
  <p class="desc">We call victim_fn(i) for i = 0…9, always within bounds. The branch predictor learns: index < array.length → always true → take the safe path.</p>
  <div class="progress-row">
    <span class="label">Training iteration:</span>
    <div class="bar-wrap"><div class="bar" id="bar-train"></div></div>
    <span class="count" id="count-train">0 / 10</span>
  </div>
  <div class="branch-vis" id="branch-vis">
    <span class="branch-true" id="bvis-true">✓ safe path</span>
    <span class="branch-sep">|</span>
    <span class="branch-false" id="bvis-false">✗ out of bounds</span>
  </div>
  <button id="btn-train" type="button">Start Training</button>
</div>

<div class="panel" id="panel-attack" style="display:none">
  <h3>Phase 2 — Fire the Malicious Access</h3>
  <p class="desc">Now we call victim_fn(secret_offset) where secret_offset points outside the array. The predictor — still confident — speculatively executes the safe path, reading array[secret_offset] and warming a cache line. The CPU then detects the bounds violation and squashes the result. The value never reaches a register, but the cache remembers.</p>
  <div class="secret-row">
    <span class="label">Secret value:</span>
    <span class="secret-box" id="secret-display">?</span>
    <span class="label small">(hidden from program; only the cache knows)</span>
  </div>
  <button id="btn-attack" type="button">Fire Malicious Access</button>
</div>

<div class="panel" id="panel-probe" style="display:none">
  <h3>Phase 3 — Read the Cache (Timing Probe)</h3>
  <p class="desc">We time access to each probe slot. The hot slot (fast access) reveals which cache line was touched during speculation — exposing the secret value.</p>
  <div id="probe-grid" class="probe-grid"></div>
  <div class="status" id="status-probe"></div>
  <button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the three-phase Spectre demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.panel { background: #f4f7fa; border: 1px solid #d0dae4; border-radius: 10px;
         padding: 1rem 1.1rem; margin-bottom: .8rem; }
h3 { font-size: 1rem; font-weight: 700; margin-bottom: .4rem; color: #1d3557; }
.desc { font-size: .88rem; color: #444; line-height: 1.5; margin-bottom: .8rem; }
/* Training progress bar */
.progress-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
.label { font-size: .82rem; color: #555; white-space: nowrap; }
.label.small { font-size: .78rem; color: #777; }
.bar-wrap { flex: 1; height: 10px; background: #d5dfe8; border-radius: 5px; overflow: hidden; }
.bar { height: 100%; width: 0; background: #457b9d; border-radius: 5px; transition: width .12s; }
.count { font-size: .82rem; color: #555; white-space: nowrap; min-width: 3rem; text-align: right; }
/* Branch predictor visualizer */
.branch-vis { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem;
              font-size: .85rem; font-weight: 600; }
.branch-true { color: #0a7d33; padding: .25rem .55rem; border-radius: 6px; background: #d4f0dc; }
.branch-false { color: #888; padding: .25rem .55rem; border-radius: 6px; background: #e8eef3; }
.branch-true.active { background: #0a7d33; color: #fff; }
.branch-false.active { background: #c92f3c; color: #fff; }
.branch-sep { color: #aaa; font-weight: 400; }
/* Secret value display */
.secret-row { display: flex; align-items: center; gap: .7rem; margin-bottom: .8rem; }
.secret-box { font: 700 1.4rem ui-monospace, monospace; background: #1d3557; color: #fff;
              padding: .25rem .6rem; border-radius: 6px; letter-spacing: .05em; min-width: 2.5rem;
              text-align: center; }
/* Probe timing grid */
.probe-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; margin-bottom: .7rem; }
.probe-slot { height: 36px; border-radius: 6px; background: #d5dfe8; position: relative;
              display: flex; align-items: flex-end; justify-content: center;
              padding-bottom: 3px; font-size: .7rem; color: #555; transition: background .15s; }
.probe-slot.hot { background: #e63946; color: #fff; }
.probe-slot .slot-label { position: absolute; bottom: 4px; }
/* Status message */
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .6rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
         border: 1px solid #1d3557; background: #1d3557; color: #fff;
         border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Spectre-style speculative execution demo — simplified visual simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

// Simulation constants
const TRAIN_ROUNDS = 10;     // training iterations to convince the predictor
const PROBE_SLOTS  = 8;      // slots in the probe array (values 0-7)
const SECRET       = 5;      // the secret byte value we want to leak (index 5)

// runtime state
let trainCount = 0;
let trainTimer = null;
let attackFired = false;

// DOM element references
const barTrain    = document.getElementById('bar-train');
const countTrain  = document.getElementById('count-train');
const bvisTrue    = document.getElementById('bvis-true');
const bvisFalse   = document.getElementById('bvis-false');
const btnTrain    = document.getElementById('btn-train');
const panelTrain  = document.getElementById('panel-train');
const panelAttack = document.getElementById('panel-attack');
const panelProbe  = document.getElementById('panel-probe');
const secretDisp  = document.getElementById('secret-display');
const btnAttack   = document.getElementById('btn-attack');
const probeGrid   = document.getElementById('probe-grid');
const statusProbe = document.getElementById('status-probe');
const btnReset    = document.getElementById('btn-reset');

// Update the training progress bar
function updateTrainCount(n, total) {
  const pct = (n / total) * 100;
  barTrain.style.width = pct + '%';
  countTrain.textContent = n + ' / ' + total;
}

// One training iteration: the branch is in-bounds, so the safe path is taken.
// The predictor increments its confidence counter for the taken (safe) direction.
function trainStep() {
  trainCount++;
  // simulated check: index < ARRAY_SIZE → true → speculative path taken
  bvisTrue.classList.add('active');
  bvisFalse.classList.remove('active');
  updateTrainCount(trainCount, TRAIN_ROUNDS);
  if (trainCount >= TRAIN_ROUNDS) {
    clearInterval(trainTimer);
    trainTimer = null;
    // Training complete — transition to attack phase
    setTimeout(() => {
      panelTrain.style.display = 'none';
      panelAttack.style.display = '';
    }, 400);
  }
}

btnTrain.addEventListener('click', () => {
  // begin the training loop (180 ms per iteration)
  if (trainTimer) return;
  trainCount = 0;
  bvisTrue.classList.remove('active');
  bvisFalse.classList.remove('active');
  updateTrainCount(0, TRAIN_ROUNDS);
  trainTimer = setInterval(trainStep, 180);
});

// Attack phase: fire one out-of-bounds speculative read
btnAttack.addEventListener('click', () => {
  if (attackFired) return;
  attackFired = true;

  // Step 1 — predictor predicts safe (taken), CPU speculatively reads array[SECRET]
  // Predictor confident: index < ARRAY_SIZE predicts TRUE → speculatively reads array[SECRET]
  bvisTrue.classList.add('active'); // predictor chose safe path
  bvisFalse.classList.remove('active');

  setTimeout(() => {
    // Step 2 — CPU detects bounds violation, squashes architectural state
    // Out-of-bounds detected → squash architectural state
    bvisTrue.classList.remove('active');
    bvisFalse.classList.add('active'); // out-of-bounds detected, architectural squash

    setTimeout(() => {
      // Step 3 — cache line stays warm despite architectural squash
      // Cache line for probe_array[SECRET * stride] stays warm despite squash
      secretDisp.textContent = '?';
      panelAttack.style.display = 'none';
      panelProbe.style.display = '';
      buildProbeGrid();
    }, 500);
  }, 600);
});

// Build the probe grid showing which slot is hot
function buildProbeGrid() {
  probeGrid.innerHTML = '';
  // In a real attack timing probe_array[i * stride] — the hot slot is fast
  for (let i = 0; i < PROBE_SLOTS; i++) {
    const slot = document.createElement('div');
    slot.className = 'probe-slot' + (i === SECRET ? ' hot' : '');
    // label shows the slot index (0-7)
    const lbl = document.createElement('span');
    lbl.className = 'slot-label';
    lbl.textContent = i;
    slot.appendChild(lbl);
    probeGrid.appendChild(slot);
  }
  // The hot slot index IS the secret value
  statusProbe.textContent = i18nFmt('Cache timing reveals secret = %v% (slot %v% was fast).', { v: SECRET });
  statusProbe.className = 'status ok';
  secretDisp.textContent = SECRET;
}

btnReset.addEventListener('click', () => {
  // Reset all phases to initial state
  trainCount = 0;
  attackFired = false;
  if (trainTimer) { clearInterval(trainTimer); trainTimer = null; }
  updateTrainCount(0, TRAIN_ROUNDS);
  bvisTrue.classList.remove('active');
  bvisFalse.classList.remove('active');
  secretDisp.textContent = '?';
  probeGrid.innerHTML = '';
  statusProbe.textContent = '';
  statusProbe.className = 'status';
  panelTrain.style.display = '';
  panelAttack.style.display = 'none';
  panelProbe.style.display = 'none';
});
Result
Console

Notice how the secret leaks through timing alone. The architectural check (index < array.length) was satisfied every time during training, so the predictor is confident. On the malicious access it predicts "safe," speculatively reads array[secret_offset], and populates a cache line — before the CPU notices the index is out of bounds and squashes the result. The squash removes the value from registers but the cache line stays warm. Measuring which probe slot is fast reveals the secret.

The Real Complexity

What makes Spectre and Meltdown so difficult to fully close?

  • The ISA contract says nothing about time. The instruction-set architecture promises which values registers hold after execution. It says nothing about how long memory accesses take. Cache timing is entirely outside the contract — yet it is observable by any unprivileged code.
  • Speculation is deep. A modern out-of-order core may have hundreds of instructions in flight simultaneously. Disabling speculation entirely would erase most of the performance gains of the last 30 years.
  • There are many variants. Spectre variant 1 exploits bounds-check bypass; variant 2 exploits branch-target injection; Meltdown exploits transient reads of kernel memory. New variants (SpectreV4, MDS, TAA, …) keep appearing because the root cause is the class of microarchitectural leakage, not one specific gadget.
  • Software mitigations are costly. Retpoline, LFENCE barriers, index masking, and site isolation add 5–30% overhead depending on workload. Microcode patches for some variants cost even more on certain hardware generations.
  • Hardware fixes are partial. Intel's eIBRS and AMD's IBRS help with branch-target injection but do not address all variants. A truly speculation-free design would need a fundamental rethink of the pipeline.

Spectre is not a single bug to patch — it is a new class of vulnerability that follows from the design of every high-performance processor built in the last 25 years. See P vs NP for another case where a seemingly innocuous gap has enormous consequences.

Where It Matters

Because the attack leaks through timing rather than memory reads, it affects almost every computing environment:

  • Cloud virtual machines: a tenant VM and a co-located VM may share a physical core and its branch predictor. Spectre variant 2 lets the attacker train the predictor of the hypervisor, potentially reading hypervisor memory from guest code.
  • Web browsers: JavaScript running in a tab can measure timer-based side channels. Browsers responded by reducing SharedArrayBuffer precision and disabling high-resolution timers — restoring some later only after site isolation landed.
  • Operating system kernels: Meltdown let unprivileged user code read arbitrary kernel memory on Intel hardware before KPTI (kernel page-table isolation) was deployed, at a cost of up to 30% on syscall-heavy workloads.
  • Smartphones and embedded devices: ARM processors with speculative execution (used in virtually every smartphone) are affected by multiple Spectre variants. Patching is slower to reach end devices.
  • Cryptographic implementations: even constant-time code can leak if the CPU speculatively branches on secret-dependent conditions. Side-channel-safe code now has to reason about speculative paths, not just the nominal execution.

The halting problem showed limits on what can be decided algorithmically; Spectre shows limits on what isolation guarantees hardware can actually provide.

Conclusion

Speculative execution is a masterpiece of engineering: by guessing ahead and discarding wrong guesses, processors deliver performance that would otherwise require a far more expensive design. For decades, this worked invisibly and safely — because the discarded work left no trace anyone could see.

Spectre and Meltdown showed that the invisible traces were never truly invisible. The cache remembers what speculation touched, and timing can read the cache. The architectural contract is satisfied, but the microarchitectural reality leaks secrets that the contract says should be private.

The lesson is not that speculation is wrong — it is that every abstraction layer has a shadow, and that shadow can be measured. When performance and security both depend on the same hardware, the gap between what the specification promises and what the silicon does becomes an attack surface. Understanding that gap is now a required skill for anyone building systems that must keep secrets.

Share this article

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

Comments

Loading comments...

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