Browsers blunted their clocks to stop timing attacks — but a processor that runs two independent instruction sequences at once is itself a stopwatch, and which one finishes first can be read off the cache long after the race is over.
The argument in one glance
-
Problem
Timing attacks need a clock, so browsers took the clocks away
Cache side channels work by measuring how long an access takes. After the first browser side-channel attacks, vendors cut timer resolution to microseconds; after Spectre, to milliseconds, with random jitter added on top.
-
What people did
Attackers built clocks out of threads, so those went too
A second thread incrementing a counter in a tight loop is a very precise timer. Shared memory between JavaScript workers was the mechanism, so it was disabled and later re-enabled only for pages that opt into cross-origin isolation.
-
Why it fell short
Both mitigations assume the attacker needs to measure time at all
They coarsen or remove sources of time. Neither prevents an attacker from asking a different question: not how long did this take, but did this finish before that? Answering that needs no clock and no second thread.
-
This paper's idea
Race two independent instruction sequences and read the winner off the cache
An out-of-order processor runs data-independent instruction sequences simultaneously. Start two together, put the thing you want to time in one, and the order they complete leaves a durable difference in cache state — which a second gadget then amplifies until a millisecond-resolution clock can see it.
Why this problem is worth solving
Browser vendors have spent a decade in an arms race over clocks. The most precise timer JavaScript can reach natively had its resolution cut to five microseconds in 2015 after the first browser cache attack; after Spectre in 2018 it fell to two milliseconds in Firefox and a hundred milliseconds with a hundred milliseconds of jitter in Chrome. The shared-memory feature that let a second thread act as a counter was removed outright, then restored only for pages opting into cross-origin isolation. Each step was a response to a specific attack, and each rested on the same premise: a A side channel is a way of learning something a system did not intend to reveal by observing an indirect physical effect of its behaviour — most commonly how long an operation takes, which betrays whether data was already in the cache.
This paper attacks the premise. Modern processors execute out of order specifically to exploit Instruction-level parallelism is the processor’s ability to work on several instructions at once when none of them depends on another’s result, which is why an out-of-order core can be much faster than the sequential program it is running would suggest.
What people did before, and where it broke
- Coarsen the native timer. The standard mitigation, applied repeatedly. It reduces the resolution of the clock, not the attacker’s ability to construct one.
- Add jitter to the clock edge. Randomly perturbing when the clock ticks was meant to destroy the fine signal. Prior work showed the resolution can be recovered by adding padding instructions until a threshold is crossed.
- Remove shared memory between threads. This closed the counting-thread timer, which was the most precise construction available. It is also a language feature that could be removed; the authors’ point is that instruction-level parallelism is not.
- Repeat the attack many times and time the whole thing. The obvious way to turn a nanosecond difference into a millisecond one. The paper shows it frequently fails outright: a cache miss avoided during the victim’s load reappears during the attacker’s reload, and the two cancel, leaving no discernible difference across the entire run.
- Use a magnifier gadget from a known Spectre attack. Amplify a single state difference into a long cascade of cache misses. Effective, and it needs the secret already converted into the specific state format the magnifier consumes.
- Defend against transient execution. A large literature of hardware defences, some of which delay cache misses until they are non-speculative, others of which quarantine speculative state and clean it up afterwards.
- The gap. Every mitigation targets either a source of time or the transient execution that carries secrets. Nothing targets the ordinary, correct, non-speculative parallelism that makes two instruction sequences race in the first place.
How a race becomes a readable measurement
From an unclocked race to a difference a blunt timer can see
-
-
new
-
new
-
new
-
new
-
new
The idea
Do not ask how long — ask which finished first. The reframing is the whole paper. Timer mitigations assume the attacker wants to measure a duration, so they degrade the instrument used to measure it. But an out-of-order core running two data-independent sequences is already comparing them: whichever has less work to do completes earlier. Put the operation you care about in one sequence, put a reference sequence of known cost beside it, start both together, and the finishing order tells you whether the operation was faster or slower than the reference — a single bit, and enough. Sweep the reference length and the bit becomes a measurement of arbitrary precision. No clock is consulted at any point, and the ingredient being exploited is the feature that makes processors fast, not a language API that can be withdrawn. Noticing that a security argument holds only over the behaviours its threat model actually names, and that a whole class sits outside them, is the same style of observation behind formalising exactly what a processor is permitted to reveal and then testing against that statement, covered elsewhere on this site.
A race is useless unless the result outlives it. The engineering problem is that the winner of a race in the pipeline is forgotten almost immediately, so it has to be converted into something persistent. Two constructions do this. The first alters how quickly a mispredicted branch gets corrected, so the finishing order decides whether a particular address is speculatively fetched — presence or absence of a cache line, durable and easy to read. The second is more interesting because it needs no misprediction whatsoever: it relies purely on the order in which two loads are inserted into the cache, which the pseudo-least-recently-used replacement state records. Both leave a two-valued microarchitectural difference. That difference is then handed to a magnifier that turns it into a cascade — a pattern of accesses where one initial state causes repeated misses and the other repeated hits, accumulating until the gap is large enough to see. Rebuilding a channel that mitigations were believed to have closed, by finding a different physical effect that carries the same information, is the same demonstration made by work showing last-level cache attacks remain practical in modern clouds, covered elsewhere on this site.
The defences guard the wrong door. The sharpest consequence is what happens when you point the non-transient version at hardware Spectre defences. Delay-on-miss holds a cache miss until it is no longer speculative — but the reorder gadget uses two entirely non-speculative loads, which the defence therefore marks as safe to execute in any order, and they still race. A defence that quarantines speculative state in a separate structure before promoting it does not change which order the promoted state ends up in. The authors state the underlying issue plainly: Spectre defences treat transient execution as the dangerous part, and do not seek to eliminate channels caused by instruction-level parallelism. They then push further with SpectreBack, which transmits its timing signal backwards to a racing gadget positioned before any misspeculation is discovered, defeating defences whose strategy is to clean up after the fact. Verifying a design against a precisely stated contract about what may be observed, rather than against a list of known attacks, is the response this line of work points toward, covered elsewhere on this site.
Following one measurement through the gadgets
Timing an operation with no clock
An attacker in a sandboxed browser page with only a five-microsecond timer, no shared-memory threads, and only arithmetic, branches and loads available.
Step 1 of 5
Build two independent paths
Two instruction sequences are constructed with no data dependency between them, so the processor is free to run them at once.
The operation to be timed goes in the measurement path; the other is a baseline path whose execution time is known and adjustable. Chains within each path are wrapped so that everything hangs off one shared starting dependency.
Why it works
- Racing needs no clock and no second thread. Two data-independent sequences run simultaneously by design → the finishing order is a comparison against a reference of known cost → so removing timers and shared memory does not remove the capability.
- The winner can be made to persist. Completion order decides either a speculative fetch or a cache insertion order → the replacement state records it → and the difference survives long after the race itself is gone.
- Magnification converts nanoseconds into microseconds. A single two-valued state seeds an access pattern that hits repeatedly in one case and misses repeatedly in the other → the gap accumulates without limit → so a hundred-microsecond signal is reachable from a difference far below any available clock.
- Racing rescues repetition attacks that otherwise fail. Naively repeating an attack can cancel its own signal, since a miss avoided in one stage reappears in another → inserting a racing gadget makes the load stage constant-time → which is what makes the repeated timing difference discernible at all.
- Nothing about it is vendor-specific. The construction depends only on out-of-order execution and cache replacement → the authors confirm it on both an Intel and an AMD system → and the one attack that failed to port did so because of the underlying side channel, not the timer.
What it costs (trade-offs and scope)
- It leaks time, not secrets. The authors say this explicitly and early: Hacky Racers are not as dangerous as Spectre, because transient-execution attacks leak data directly while this leaks a timing signal. It is a component that other attacks need, not an attack by itself.
- Magnification has a ceiling. Some of the magnifiers can only amplify to roughly the coarsest granularity a browser has ever shipped, so further coarsening could blunt them — though the authors note the cache-replacement-based ones are unlikely to be defeated this way, and all can be combined with repetition.
- The gadgets need careful construction. Paths must be synchronised through an engineered cache miss, chains wrapped in prefixes and suffixes, magnifier access patterns tuned to a specific cache geometry, and the arithmetic magnifier balanced against known instruction latencies. This is not something that appears by accident.
- Detection would be unreliable. The authors expect any run-time detector to have a very high false positive rate, because racing gadgets look like ordinary highly optimised code — parallel independent work is what compilers are trying to produce.
- Some variants still use speculation. The transient presence-absence gadget and the SpectreBack attack do rely on misprediction, and are therefore in scope for existing Spectre defences. The paper uses them mainly because they are easier to explain, and notes an attacker can substitute the reorder gadget.
- Software mitigation is conceivable but weak. Compiler diversification could obfuscate gadgets, but an attacker who controls the source can simply implement several variants and pick one that works.
The evaluation
| Property | Capability | Under current browser mitigations | With Hacky Racers |
|---|---|---|---|
| Fine-grained timing without shared-memory threads | |||
| Spectre variant 1 leakage in a browser | requires a precise timer | ||
| Defeating cleanup-after-misspeculation defences | defences assume the signal is discovered after the fact | ||
| Last-level cache eviction set generation in JavaScript | not previously possible without shared memory | ||
| Magnification reached | coarse timer of 5 microseconds |
Check yourself
Check your understanding
1. Why does coarsening the browser timer fail to stop this attack?
2. What makes the reorder racing gadget significant compared to the presence-absence one?
3. Why is a magnifier gadget needed on top of a racing gadget?
4. What is the assessment the authors themselves give of how dangerous this is?
Sources
The paper is “Hacky Racers: Exploiting Instruction-Level Parallelism to Generate Stealthy Fine-Grained Timers” by Haocheng Xiao and Sam Ainsworth, published at ASPLOS 2023 Volume 2, pages 354-369, DOI 10.1145/3575693.3575700. The authors are at the University of Edinburgh.
This explainer is written from the full text, read via the arXiv version at arxiv.org/abs/2211.14647. All figures above come from the paper’s own sections 2 through 8: the history of browser timer resolutions from five microseconds in 2015 down to two milliseconds in Firefox and one hundred milliseconds plus jitter in Chrome after Spectre, the path synchronisation and expression embedding construction, the transient presence-absence and non-transient reorder racing gadgets, the pseudo-least-recently-used, arbitrary-replacement and arithmetic-only magnifiers with their respective 100 microsecond and fifteen-thousand-repetition figures, the SpectreBack leakage rate of 4.3 kilobits per second at over 88% accuracy in Chrome 88, the eviction-set generation retaining a 100% success rate with only the timer replaced, and the analysis of why delay-on-miss and speculative-state quarantine defences do not prevent the non-transient gadget. Experiments were run on an Intel Coffee Lake system and confirmed on an AMD system.