Spectre and Meltdown

In January 2018 the whole computing industry had a very bad week. Researchers at Google Project Zero and several universities revealed that essentially every high-performance CPU built in the previous two decades — Intel, AMD, ARM, the lot — leaked secrets through the very feature that made them fast: speculative execution. The patches that followed slowed servers worldwide by measurable percentage points, and cloud providers rebooted their entire fleets. Two attacks led the headlines: Meltdown and Spectre.

The lesson is profound and a little unsettling. Everything you have learned about speculation and recovery insisted that a mis-speculated instruction is squashed — it never commits, so architecturally it never happened. That is true of the architectural state: the registers, the memory. But speculation also leaves microarchitectural footprints — chiefly in the cache — and those are not rolled back. This lesson shows how a byte the CPU "un-read" can still be reconstructed from the shadow it cast on the cache.

The gap: architectural rollback is not microarchitectural rollback

A modern out-of-order core runs instructions ahead of certain knowledge — past an unresolved branch, or before a permission check has finished. If the guess was wrong, the reorder buffer discards the results and the programmer never sees them. But along the way those transient instructions may have touched memory, and touching memory loads a line into the cache. Squashing the instruction does not evict that line. The data value is gone; the fact that a particular address was accessed survives, recorded as a difference in how fast that address responds next time.

The trick that turns a value into a measurable cache footprint is the flush+reload covert channel, and it is worth seeing on its own before we build the full attacks on top of it.

The covert channel: turning a byte into a timing

Set up a probe array with 256 slots, each on its own cache line, and imagine we want to smuggle out one secret byte s (a value 0255). The channel works in three beats:

  1. Flush every slot out of the cache, so all 256 are equally slow to access.
  2. Leak: perform \texttt{probe}[s \times \text{line}] — a single access indexed by the secret. Now exactly one slot, slot s, sits in the cache.
  3. Reload: time an access to all 256 slots. The one that responds fast is slot s — the secret, read straight out of the timings.

Notice the secret never travels through a register the attacker can read; it travels through the cache-vs-not-cache status of the array. That is the covert channel. Run it below — we simulate access latencies and recover the byte purely from which slot is fast.

// Flush+Reload, simulated: recover a secret byte from cache-timing alone. const LINE = 64; // one cache line per slot const SLOTS = 256; // one slot per possible byte value const HIT = 30, MISS = 300; // cycles: cached vs uncached access // --- attacker: flush the probe array (every slot uncached) --- const cached: boolean[] = new Array(SLOTS).fill(false); // --- victim: a transient access indexed by the secret leaves ONE slot cached --- const secret = 214; // the byte we are not "allowed" to read cached[secret] = true; // probe[secret*LINE] was touched speculatively // --- attacker: reload — time every slot, pick the fast one --- function accessTime(slot: number): number { return cached[slot] ? HIT : MISS; } let best = -1, bestTime = Infinity; for (let s = 0; s < SLOTS; s++) { const t = accessTime(s); if (t < bestTime) { bestTime = t; best = s; } } console.log("probe stride =", LINE, "bytes/slot"); console.log("fast slot found at index", best, "(", bestTime, "cycles )"); console.log("recovered secret byte =", best, " correct?", best === secret);

Meltdown: reading kernel memory you were forbidden to touch

On the affected Intel chips, a load from a kernel address you have no right to read still executes speculatively while the permission check runs in parallel. The permission fault will eventually squash the instruction — but out-of-order execution lets a second transient instruction use the forbidden byte first, before the fault retires:

// Meltdown gadget (conceptual). The fault on line 1 is delayed; // lines 2-3 run transiently and stamp the secret into the cache. const secret = kernelMemory[addr]; // 1. illegal read — will fault, but LATE const _ = probe[secret * 4096]; // 2. transient access, indexed by the secret // ... fault finally retires here and squashes 1-2 architecturally ... // 3. attacker now runs Flush+Reload on `probe` to recover `secret`

Architecturally the illegal read is annulled — the program that ran it just gets a segmentation fault. But the cache footprint of step 2 survives, and flush+reload reads the kernel byte out of it. Repeat byte by byte and you can dump the entire kernel — passwords, keys, anything — from an unprivileged process. Meltdown melts the fundamental isolation between user and kernel that the whole operating-system model depends on.

Spectre: weaponising the branch predictor

Spectre is subtler and, sadly, more durable. Instead of running an illegal instruction, it tricks a victim program into leaking its own secrets by abusing branch prediction. Consider a perfectly ordinary bounds check:

// Spectre v1 — bounds-check bypass. `x` is attacker-supplied. if (x < array1.length) { // trained to predict TRUE const y = array2[array1[x] * 4096]; // runs speculatively even when x is out of bounds }

The attacker first calls this with many in-bounds x values, training the branch predictor to expect "taken". Then they pass a huge out-of-bounds x. The predictor, still confident, speculatively executes the body — reading past \texttt{array1} into the victim's secrets and stamping the result into \texttt{array2}'s cache lines. The bounds check eventually resolves false and squashes everything architecturally, but flush+reload on \texttt{array2} recovers the out-of-bounds byte. The victim's own correct code becomes the leak.

MeltdownSpectre (v1)
Boundary brokenuser ↔ kernel isolationwithin-process bounds / sandboxes
Mechanismout-of-order use of a faulting loadmistrained branch prediction
Leak channelcache (flush+reload)cache (flush+reload)
Fixable in OS?yes — KPTI unmaps the kernelno clean fix; per-gadget hardening
Root causea specific Intel oversightspeculation itself

Mitigations, and what they cost

There is no single patch, because these are not bugs in the ordinary sense — they are consequences of doing speculation at all. The defences chip away at specific gadgets, and each has a price:

The uncomfortable summary: Meltdown was largely closed by KPTI and new silicon, but Spectre is a family, not a bug. As long as CPUs speculate — and they must, to be fast — new variants keep appearing, and mitigation is a permanent, case-by-case tax on performance.

The kernel-unmapping defence was published in 2017 as KAISER, by researchers at TU Graz, as a fix for a different problem: they wanted to stop attackers defeating kernel address-space layout randomisation (KASLR) by timing the TLB. Their remedy — don't map the kernel into user space — turned out, months later, to also block Meltdown, which the same group then co-discovered. Linux adopted it under the name KPTI, sometimes jokingly expanded as "Forcefully Unmap Complete Kernel With Interrupt Trampolines" (initials left as an exercise). A rare case where a mitigation existed before the world knew it needed one.

It is tempting to lump them together — both leak via cache timing after speculation — but they differ where it matters. Meltdown exploited a specific implementation flaw (using a faulting load's data before the fault retired) and is genuinely fixed by KPTI and by newer hardware that never forwards privileged data speculatively. Spectre attacks the concept of speculation through the branch predictor; it cannot be "patched away" because the predictor is doing its job correctly. Believing one fix covers both, or that a software update makes the class disappear, is exactly the mistake that keeps these vulnerabilities alive. Defence here is ongoing hygiene, not a one-time cure.