A different kind of leak: the channel that was never a channel
Every attack in this rung so far has been an attack on correctness. An overflow writes a byte it should not; a use-after-free reads an object that no longer exists; ROP makes ret jump where it should not. The program does the wrong thing, and the wrongness is the foothold. This guide is about attacks where the program does exactly the right thing — every instruction is legal, every permission check passes, no byte of memory is corrupted — and a secret leaks anyway. That is a side channel: information that escapes not through the program's intended outputs, but through a physical side effect of how the computation ran. Time, power draw, the warmth of a cache. The CPU was never supposed to say the secret; it merely behaved differently depending on it, and behaving differently is itself a message.
Start with the gentlest example, because the principle is the whole battle. Imagine a login check that compares a password byte by byte and returns the moment two bytes differ. A wrong guess that matches the first 3 bytes runs slightly longer than one that fails on byte 1, because the loop ran three more iterations. The function never tells you the secret — but the time it takes depends on how many leading bytes you got right. Measure that time precisely and you can recover the password one byte at a time, turning a few thousand timed guesses into a key, with no overflow and no bug in the usual sense. This is a timing side channel, and the fix tells you what the whole field is about: make the running time independent of the secret — a constant-time compare that always touches every byte. The leak was never in the answer; it was in the shape of the work.
The cache, turned into an eavesdropping device
The timing channel above leaked through a loop count. The far more powerful channel — the one Spectre and Meltdown weaponize — leaks through the CPU cache. From the microarchitecture rung you know the cache line: when the CPU reads an address, it pulls a whole 64-byte line into fast cache, so the next access to anything in that line is dramatically faster. A read from cache might take a few cycles; a read that misses and goes to main memory might take a few hundred. That gap — fast-if-cached versus slow-if-not — is enormous, reliable, and measurable from software by simply timing a memory access. And here is the pivot: whether a line is cached is a footprint of what the CPU touched. If you can arrange for a secret to decide which line gets pulled into cache, you can later recover the secret by measuring which line is now fast.
The classic technique is called Flush+Reload, and it is worth walking through because it is the receiver every cache attack uses. The attacker shares some memory with the victim — a shared library, mapped read-only into both, is perfect. First the attacker flushes a chosen line out of the cache (x86 even has an instruction, clflush, that does exactly this). Then it lets the victim run. Then it reloads that same line and times the load. If the load is fast, the line was brought back into cache — meaning the victim touched it while running. If the load is slow, the victim left it alone. One flushed-and-timed line answers one yes/no question about the victim's memory accesses, and the answer is a precise number of nanoseconds.
Flush+Reload, one round, measuring one cache line L:
1. clflush(L) // evict L: guarantee it is NOT cached
2. run the victim // victim may or may not touch L
3. t0 = rdtsc() // read cycle counter
x = *L // load from L
t1 = rdtsc()
4. if (t1 - t0) is SMALL -> L was cached -> victim TOUCHED L
if (t1 - t0) is LARGE -> L was not cached -> victim did NOT
fast ~ 50 cycles (hit)
slow ~ 300 cycles (miss to DRAM)
the gap is the message.Now turn one yes/no question into a whole byte, because this is the trick the rest of the guide leans on. Lay out a probe array of 256 separate cache lines, one for each possible value of a byte (0 through 255). To leak a secret byte `s`, arrange for the victim to read `probe[s * 64]` — that touches exactly one of the 256 lines, the one whose index is the secret. Then the attacker reloads all 256 lines and times each. Exactly one comes back fast; its index is the secret value. A single yes/no primitive, replicated 256 ways, has become a full 8-bit read of a value the attacker was never allowed to see directly — and the victim merely did a legal array access.
Speculation: the CPU runs code that 'never happened'
The cache is the receiver. The transmitter is the deepest idea in modern CPUs, and the one that makes Spectre and Meltdown possible. From the performance rung you met out-of-order execution and branch prediction. A modern CPU does not wait. When it hits a branch — `if (x < array_len)` — it cannot afford to stall while the comparison resolves, so it guesses which way the branch will go and races ahead executing the predicted path. This is speculative execution: work done on a bet, before the CPU knows whether the bet was right. If the guess was correct, that work is already done and the program flew. If the guess was wrong, the CPU throws the speculative work away — it rolls back the registers, discards the results, and quietly takes the other path. Architecturally, it is as if the mis-speculated instructions never ran.
Read that last sentence again, because it hides the flaw of the decade. 'As if they never ran' is true at the architectural level — the level of registers and memory the program is allowed to see. But it is not true at the microarchitectural level. When the CPU speculatively executed those doomed instructions, if any of them performed a memory read, that read pulled a line into the cache — and the rollback does not evict it. The architectural state is cleanly restored; the cache is not. The speculative work leaves a footprint in exactly the place Flush+Reload can read. Speculation gives you instructions that execute but 'didn't happen,' and the cache remembers what they touched. Put the cache receiver and the speculation transmitter together and you have the whole attack.
Meltdown and Spectre: two ways to abuse the same gap
Both attacks, disclosed together in January 2018, use the speculation-leaves-a-cache-trace gap — but they abuse it differently, and the difference matters. Meltdown attacks memory protection itself. Normally a user-mode program that reads a kernel address gets a fault and never sees the value. But on the affected Intel CPUs, when the program speculatively read a forbidden kernel byte, the permission check and the data fetch happened in parallel — so for a brief speculative window the chip handed the secret byte to later instructions before the fault was delivered. Those later instructions used the secret byte to index the probe array, pulling one line into cache. Then the fault arrived, the architectural read was annulled, and the program saw nothing illegal happen. But the cache trace was already laid, and Flush+Reload read the kernel byte out of it. Meltdown effectively melted the wall between user and kernel memory.
Spectre is subtler, more universal, and harder to kill. It does not break permission checks; it trains the branch predictor to betray its own program. The attacker repeatedly calls a victim function with in-bounds inputs so the predictor learns 'this bounds check always passes,' then calls it once with an out-of-bounds index. The check `if (x < array_len)` will ultimately fail and the access will be rolled back — but the predictor, freshly trained, guesses the check passes and speculatively runs the body with the malicious `x`. In that speculative window the code reads `array[x]` far out of bounds — a secret it should never reach — and immediately uses that secret to index the probe array, seeding the cache. The bounds check did its job and blocked the architectural read; speculation ran the body anyway, before the check resolved. Spectre turns the victim's own correct code into the leaking gadget.
- Find a gadget. Locate code in the victim of the shape `if (x < len) { y = array2[ array1[x] * 64 ]; }` — a bounds check guarding a load whose result indexes a second array. Real binaries are full of this shape.
- Train the predictor. Call the gadget many times with valid, in-bounds x so the branch predictor strongly learns 'the check passes.' This is pure, legal warm-up — no rule is broken yet.
- Flush the receiver. clflush every line of array2 (the 256-entry probe) out of the cache, so the timing channel starts clean and any cached line afterward is a signal.
- Mis-speculate. Call the gadget with an out-of-bounds x pointing at the secret. The check will fail, but the trained predictor speculatively runs the body, reads the secret byte, and indexes array2 with it.
- Reload and read. Time a load from each of the 256 probe lines. Exactly one is fast; its index is the secret byte. Repeat across addresses to dump as much memory as you like.
Defending the gap — and being honest about the cost
Because the leak rides on hardware doing its job, the defenses are awkward, layered, and never quite free — and saying so plainly is part of understanding the topic. Meltdown, being a specific hardware flaw, got the cleaner fix: an OS change called kernel page-table isolation that simply unmaps the kernel's memory from the page tables a user process sees, so there is no kernel address present to speculatively read. It works, but it adds a page-table switch on every system call, and on syscall-heavy workloads that cost was measurable — a real slowdown traded for safety. Newer CPUs fix the underlying race in silicon, so the software workaround can be turned off there. Meltdown, at least, has an endpoint.
Spectre is the hard one, and honesty demands admitting it has no single clean cure. Because it abuses speculation in general — a feature every fast CPU depends on — you cannot just switch it off without throwing away the performance that justifies the chip. The defenses are a grab-bag, each plugging one variant. Compilers can insert a speculation barrier (on x86, the lfence instruction) after a sensitive bounds check, forcing the CPU to wait for the check to resolve before running the body — correct, but slow if sprinkled everywhere. They can also rewrite the dangerous pattern into a masking form that clamps the index into range even on the mis-speculated path, so an out-of-bounds read becomes harmless. Hardware added new instructions to flush or partition the branch predictor across privilege boundaries. None is universal; each is a patch over one way the gadget can form.
Zoom out and let this last guide close the rung honestly. The deepest defense is not a patch at all but an architectural stance: assume any secret that shares a cache, a core, or a predictor with untrusted code can leak, and keep secrets out of that blast radius. This is why browsers put each site in its own process — so a Spectre gadget in malicious JavaScript can read its own process's memory but not another site's. It is why high-assurance systems lean toward isolating sensitive computation onto separate cores, or into a trusted execution environment. Side channels are the rung's final lesson and its humblest: even with no overflow, no hijack, no corrupted byte — even when every instruction is correct — how a computation runs can betray what it computed on. Security is not only about preventing the wrong thing; it is about ensuring the right thing keeps its secrets.