algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

FLUSH+RELOAD — reading secrets through the cache

2026-03-083 min readside-channel research crypto

Memory-corruption exploits read the bytes directly. Side channels are sneakier: they never read the secret, they observe a physical effect of the victim using it. FLUSH+RELOAD is the cleanest of them — it turns the CPU cache into a one-bit oracle ("did the victim access this line?") and reconstructs a secret from a sequence of those bits.

targeta process sharing read-only memory with you (a shared .so, KSM-merged page)
primitive
clflush + rdtsc (both unprivileged on x86)
leaks
the victim's memory-access pattern -> key bits, control flow, KASLR

the three steps#

The whole technique fits in its name, run in a tight loop over a shared cache line — a line in a library both you and the victim map, so a cache hit is genuinely shared state:

one probe iterationtxt
1
2
3
4
5
FLUSH   clflush(addr)        evict the shared line from all cache levels
WAIT    (let the victim execute a little)
RELOAD  t0=rdtsc; tmp=*addr; t1=rdtsc     time how long it takes to read it back
        -> t1-t0 SMALL  => victim accessed addr (it's cached again)   = HIT
        -> t1-t0 LARGE  => victim never touched it (came from DRAM)   = MISS

The gap between an L1/L3 hit and a DRAM miss is enormous and stable — tens of cycles versus a couple hundred — so a single threshold separates them:

reload + timec
1
2
3
4
5
6
7
static inline uint64_t probe(void *addr) {
    uint64_t t0 = rdtscp();
    (void)*(volatile char *)addr;          // the reload
    uint64_t dt = rdtscp() - t0;
    _mm_clflush(addr);                     // flush for the next round
    return dt;                              // < ~120 cycles ≈ HIT, else MISS
}

turning hits into a secret#

A hit/miss on one line is nothing; a pattern of them is the secret. The classic victims:

  • Square-and-multiply (RSA/DSA). The exponentiation loop calls a multiply routine only on 1 bits of the secret exponent. Probe the cache line holding multiply's code: a HIT in an iteration means that exponent bit was 1. Walk the loop, read the exponent — and from it, the private key.
  • T-table AES. Software AES indexes lookup tables by key ⊕ plaintext. Which table line gets cached reveals the index, hence key bytes.
  • KASLR / control flow. Probe function lines to learn which branch a victim took or whether a page is mapped — leaking layout, not key material.
an exponent, one bit at a time

Probing the multiply line across the exponentiation loop, HIT = 1, MISS = 0:

txt
1
2
3
4
5
iter 0: 312cy MISS  -> 0
iter 1:  74cy HIT   -> 1
iter 2:  71cy HIT   -> 1
iter 3: 298cy MISS  -> 0   ...  -> private exponent recovered
segfault{the_cache_remembers_what_you_touched}

the defences (and their cost)#

  • Constant-time code. Crypto with no secret-dependent branches or table indices has no access pattern to leak — the real fix. Scatter-gather / bitsliced AES, Montgomery ladders for exponentiation.
  • Kill the sharing. FLUSH+RELOAD needs shared memory; disabling memory deduplication / KSM removes the cross-process channel (same-address-space leaks remain).
  • Restrict the tools. Coarsening or trapping rdtsc, and limiting clflush, raises the noise floor — partial, and costly to legitimate code.

FLUSH+RELOAD is the gateway to the whole microarchitectural-leak family — Prime+ Probe when there's no shared memory, and the Spectre/Meltdown attacks that use exactly this cache-timing oracle to exfiltrate the results of speculative reads. The unifying idea: computation leaves footprints in shared hardware state, and timing reads the footprints. You don't need to see the secret — only to watch the machine react to it.