5.3.15 · D2Advanced Microarchitecture

Visual walkthrough — Spectre - Meltdown speculative side channels

2,096 words10 min readBack to topic

This is the picture-walkthrough companion to Spectre - Meltdown speculative side channels. If a word here feels unexplained, it is explained here — we assume nothing.

Before we start, three plain-word ideas we will lean on (each has its own vault note if you want more):

  • Cache — a small, fast memory sitting next to the CPU. Data you touched recently lives here. See Cache Memory Hierarchy.
  • Out-of-order execution — the CPU does not run instructions strictly top-to-bottom; it runs whatever it can right now, and only later decides "was that allowed / needed?". See Out-of-Order Execution.
  • Privilege levels — memory is tagged "user" or "kernel (supervisor)". User code is forbidden from reading kernel-tagged memory. See Privilege Levels and Protection Rings.

Step 1 — The two clocks: fast cache vs. slow memory

WHAT. We first prove to ourselves that reading data takes a different amount of time depending on whether that data is already in the cache.

WHY. Everything Meltdown does at the end is measure time. If time did not depend on cache state, there would be no channel to leak through. So the very foundation is: cache = fast, main memory = slow, and the gap is huge and measurable.

PICTURE. Two bars, same task ("go fetch one byte"), wildly different lengths.

Figure — Spectre - Meltdown speculative side channels

The vs figures are the reason we say the cache leaks: the CPU never told us the secret, but the duration of a read does.


Step 2 — The forbidden read and the door that closes too late

WHAT. We ask the CPU to read a byte from a kernel address — memory we are not allowed to touch.

WHY. Normally the memory-protection hardware (the page tables) marks that page with a supervisor bit, and user access must raise a page fault () — an exception that aborts our instruction.

Reading term by term: a user-mode access aiming at a supervisor-tagged page is supposed to end in a page fault, killing the read before we see anything.

PICTURE. A user arrow slamming into a locked kernel door labelled "supervisor bit".

Figure — Spectre - Meltdown speculative side channels

The catch — which the next step makes visible — is when that check happens.


Step 3 — The gap: execute now, check later

WHAT. We split what "a load instruction" actually does into two stages in time.

WHY. In an out-of-order CPU, the execute stage (go fetch the data) runs early and eagerly. The retire stage (make the result official, and run the permission check) happens later. Between them is a window where the kernel data has already been pulled into the cache but the fault has not yet fired.

PICTURE. A timeline with three ticks; the danger zone is shaded between Time 1 and Time 2.

Figure — Spectre - Meltdown speculative side channels
Time 0:  load kernel_addr  is issued
Time 1:  load EXECUTES out-of-order  → kernel byte now sits in cache
Time 2:  load RETIRES → permission check → #PF → instruction aborted
         └──────────── the gap: data is in cache, no fault yet ─────────┘
  • Architectural state — what a program is allowed to see: registers, visible memory. Cleanly undone on a fault.
  • Microarchitectural state — the hidden speedup machinery: caches, TLBs, predictors. Not undone.

Step 4 — Encoding: turn the secret value into a cache address

WHAT. In the same speculative window, we use the forbidden byte as an index into an array we do own.

WHY. The secret byte will vanish when the fault fires — so we must spend it before it dies. We spend it by using it to choose which cache line gets loaded. That choice survives, because caching is not rolled back.

char secret = *kernel_addr;              // forbidden; executes speculatively
char dummy  = probe_array[secret * 4096]; // uses secret before the fault

Term by term:

  • secret — the byte we are stealing, to .
  • probe_array — a big array we own (allowed memory), one slot per possible byte value.
  • secret * 4096 — the multiply that maps valuelocation.

WHY multiply by 4096? A cache line is ~64 bytes; if two candidate values landed in the same line, touching one would look like touching the other (false sharing). is the page size, comfortably bigger than a line, so every one of the 256 possible values lands in its own distinct cache line. No collisions, no ambiguity.

PICTURE. A dial (secret 0–255) wired to exactly one of 256 well-separated cache lines lighting up.

Figure — Spectre - Meltdown speculative side channels

After the fault, secret is gone from the registers — but one cache line, the one at index secret, is now hot.


Step 5 — Making sure the read even happens: mistraining the branch

WHAT. We wrap the forbidden read in a branch that is architecturally always false, then train the predictor to guess "true".

WHY. We need the CPU to speculatively run the illegal load. The branch predictor guesses the outcome of if(...) before the condition is actually computed, and the CPU races ahead down the guessed path. If we first run the branch many times when it is taken, the predictor's history says "taken", so on the malicious call it speculatively enters the forbidden block — running Steps 2–4 — before discovering the branch was false and unwinding.

if (untrusted_condition) {     // trained to look "true"
    char secret = *kernel_addr;
    probe_array[secret * 4096] = 1;   // encode (Step 4)
}

PICTURE. A predictor "history register" of past outcomes tilting the fork toward the speculative (wrong) path.

Figure — Spectre - Meltdown speculative side channels

Step 6 — Reading the fingerprint: time all 256 lines

WHAT. We loop over all 256 candidate byte values and time how long each probe_array[i * 4096] takes to read.

WHY. From Step 1, one of these reads will come back fast (that line is hot from Step 4); the other 255 come back slow. The fast index is the secret byte.

for (int i = 0; i < 256; i++) {
    t0 = rdtsc();
    dummy = probe_array[i * 4096];
    dt = rdtsc() - t0;
    if (dt < CACHE_THRESHOLD) recovered = i;  // the hot line
}
  • rdtsc() — reads the CPU's cycle counter (our stopwatch).
  • CACHE_THRESHOLD — a cutoff between and (say ~100 cycles); below it = hot.
  • recovered — the winning ; that byte is the secret.

PICTURE. 256 timing bars, 255 tall (slow), exactly one short (fast) — the short one flagged as the answer.

Figure — Spectre - Meltdown speculative side channels

The full chain, now earned symbol by symbol:


Step 7 — Edge cases: what if it doesn't work cleanly?

Real attacks must survive messy cases. Each below gets handled, so the reader never hits a surprise.

Case A — the secret byte is 0. Then value 's line at probe_array[0] is the hot one. Fine — unless something already touched line 0 for unrelated reasons (noise). Fix: flush all 256 lines first with clflush so every line starts cold; only the speculative load can warm one.

Case B — noise / no line is clearly hot. Speculation is probabilistic; sometimes the window closes before the encode lands. Fix: repeat the whole attack many times and take the value that shows up fast most often. One trial is unreliable; a vote over thousands is solid.

Case C — two lines look hot. Prefetchers or a neighbour access warmed an extra line. The -byte spacing (Step 4) already minimises this; combined with voting (Case B), the true secret wins by frequency.

Case D — the mitigation is on. If KPTI (Kernel Page Table Isolation) is active, kernel pages are simply not mapped in the user page tables — so Step 2's address has no translation at all, the speculative load has nothing to fetch, and the channel closes. (Its cost: a TLB flush on every kernel entry.)

PICTURE. Four mini-panels: all-cold start, a noisy vote histogram converging on one value, a double-hot resolved by majority, and a "KPTI: page unmapped" blocked path.

Figure — Spectre - Meltdown speculative side channels

The one-picture summary

Figure — Spectre - Meltdown speculative side channels

The pipeline compressed: flush (all lines cold) → mistrain the predictor → speculate the forbidden read → encode value into one cache line → fault & rollback (registers erased, cache footprint survives) → time all lines → recover the byte.

Recall Feynman retelling — explain it to a friend with no jargon

Imagine a librarian (the CPU) who is forbidden to read a secret file, but who is super eager and always starts a task before checking if it's allowed. You dare her to read the secret. She grabs it, and — before her boss stops her — she uses that secret number to pull one specific book off a huge shelf and lay it on her desk. Then the boss shouts "you weren't allowed!" and she forgets the secret number and puts everything back… except the one book is still warm on the desk. You come along and touch every book: 255 are cold and slow, one is warm and fast. The warm one's shelf position tells you the secret number she was forbidden to reveal. To make it reliable you (1) start every book cold, (2) train her to be eager by daring her many legit times first, and (3) repeat and vote so noise averages out. The one defence that actually works: don't put the secret file anywhere she could reach in the first place (KPTI unmaps it).

Recall Quick self-check

Why must the multiply use 4096, not 64? ::: 4096 (page size) exceeds a cache line, so each of the 256 possible byte values lands in a distinct cache line — no false sharing between candidate values. What state does the CPU fail to roll back after the fault? ::: The microarchitectural state — specifically the cache line warmed by the speculative encode. Registers (architectural state) are rolled back correctly. In the timing loop, which value is the secret? ::: The index whose probe_array[i*4096] read is fast (below CACHE_THRESHOLD) — the one hot line. Why train a branch before the malicious call? ::: So the branch predictor guesses "taken" and the CPU speculatively runs the forbidden read before discovering the mispredict.