Revisiting Remote Spectre Attacks Against Cloudflare Workers
Cloudflare Workers execute untrusted JavaScript at the edge using V8 isolates, allowing many tenants to share a single OS process. Each Worker owns a separate JavaScript heap, which keeps startup latency low but means cross-tenant leakage could occur if any arbitrary-read vulnerability exists within the process. A particularly stubborn class of such vulnerabilities stems from speculative execution—commonly referred to as in-process Spectre.
How Speculative Execution Becomes a Side Channel
Modern CPUs use branch prediction to guess which path execution will take, then speculatively run instructions ahead of time. If the prediction is wrong, the architectural results are rolled back, and the wrongly executed instructions are never committed. These transient instructions leave no permanent register or memory changes, but they do leave traces in microarchitectural state, such as CPU caches.

An attacker can abuse this by causing a transient out-of-bounds memory access that encodes one bit of information into cache state. By later measuring the latency of reaccessing that data, the attacker can infer whether the bit was set. This is the essence of a Spectre attack: speculative access, cache-state encoding, and latency measurement for data exfiltration.
To defend against in-process Spectre in Workers, the runtime has historically relied on several measures: freezing local timers, forbidding multithreading and shared memory, detecting suspicious scripts, periodically shuffling memory, and isolating high-risk scripts into separate processes.
The 2021 Assessment and Its Limitations
Back in 2021, Cloudflare ran research with TU Graz to assess remote Spectre attacks against Workers. That work led to the deployment of Dynamic Process Isolation (DyPrIs), a defense that identifies scripts appearing malicious and moves them into isolated processes. At the time, the mitigations were considered sufficient. Since then, however, attackers have discovered newer techniques for stabilizing Spectre attacks, particularly in noisy production environments where activity on shared resources, interrupts, and coarse-grained timers can interfere with side-channel measurements.
A New Proof-of-Concept on Production Hardware
To determine whether those newer stabilization techniques could threaten the production Workers environment, Cloudflare's security team—working with Albert Pedersen, Haocheng Xiao, Sam Ainsworth, Nigel Topham, and Martin Schwarzl—built an updated proof-of-concept and tested it directly on the production system. That testing served as an empirical check on how real-world workloads and mitigations hold up against modern Spectre techniques.
The research uncovered a limitation in DyPrIs' implementation. The team was able to demonstrate a reliable remote Spectre attack in the production Cloudflare Workers environment, achieving a leak rate of up to 12 bits per second with 99% accuracy. While the absolute data rate is low, it proves that the defense-in-depth stack was not fully effective on its own.
Responses and Current Status
As a direct result of the findings, the Workers runtime team improved DyPrIs and added two additional layers of defense: the V8 Sandbox and a separate in-process isolation mechanism. These changes reduce the risk that a memory-disclosure vulnerability in a single Worker process can be turned into cross-tenant data leakage.
The attack described in the research is already mitigated in the current production system, and Cloudflare found no indicators of active exploitation over the last three years. A paper detailing the work, the findings, and the countermeasures is available on arXiv (referenced in the full announcement).
Building the primitives
Cloudflare Workers deliberately restricts timers as part of its Spectre mitigations. During CPU-only execution, time is effectively frozen: Date.now() and performance.now() do not provide a continuously advancing high-resolution clock. There is no shared memory and no multithreading, so the classic counter-thread timer using a SharedArrayBuffer is unavailable.
An attack in this environment must solve several problems: guarantee co-location between attacker and victim on the same process, find a reliable remote timer for stable timing measurements, deploy a Spectre gadget capable of transient 64-bit out-of-bounds reads, amplify the signal enough to overcome networking and system noise, and finally reliably evict data from the cache between measurements.
The Spectre gadget
return probeArray[
obj instanceof ObjP
? PROBEARRAY_OFFSET + ((obj.ptr[0] >> bit) & 1) * 0x800
: 0x400
];
With the right gadget, an attacker can transiently access out-of-bounds memory and encode a single bit into the cache via a probeArray. Measuring access latency then reveals the bit: a faster access means the line was cached, a slower access means it was not. Two gadget types were used in this research. The first leaks compressed heap pointers such as the isolate's heap base address (root). The second leverages a speculative type confusion to leak from an arbitrary attacker-crafted 64-bit userspace pointer.
At the time of the research, the V8 Sandbox was not yet implemented at Cloudflare Workers. Under pointer compression, most objects use 32-bit compressed pointers, but TypedArray was one of the few exceptions that still stored a raw 64-bit pointer to its backing store — exactly what the gadget abuses.
The branch in obj instanceof ObjP performs the type check that gets mistrained. Calling the gadget repeatedly on real ObjP instances, then switching to a different object with an attacker-controlled memory layout (ObjI), causes the CPU to speculate along the taken branch and follow obj.ptr[0] despite the type mismatch. One bit at a time is extracted by masking a bit and using it to select one of two probeArray cache lines.
The heap leakage gadget maps neighboring objects to locate an attacker-controlled array. The second gadget confuses two large objects spanning several cache lines so the type field sits on a different cache line than the field being read. Evicting the type field opens the speculation window while the target field remains cached, and the transient read follows an attacker-controlled 64-bit value. This converts the leak into an arbitrary-address read.

Signal amplification
A cache hit versus a miss differs by only nanoseconds, while remote timers are noisy at microsecond-to-millisecond scales. Amplification is therefore essential. Stephen Röttger and Artur Janc discovered a method exploiting the tree-based pseudo-LRU (PLRU) replacement policy in L1 caches. Each cache set is organized as a binary tree whose nodes point toward the least-recently-used side. With a careful access pattern, an attacker can keep a target line cached indefinitely by touching its tree neighbor whenever the pointers turn toward the target.
This behavior allows the timing difference of a single cache event to be arbitrarily amplified — a cached line produces many fast L1 hits, whereas an uncached one produces many misses.

A remote timer
With a sufficiently amplified signal, even a noisy remote timer can differentiate bits. A WebSocket connection to an external server providing high-resolution timestamps suffices; the timer can run at Cloudflare or in a data center co-located with the target. The Worker asks the timer to timestamp an event and compute a delta once the event finishes. Several timer setups were evaluated in the research, achieving reliable sub-millisecond resolutions on the median with only a handful of samples over larger topological distances.

Repeated measurements
Production machines are noisy, so each timing measurement must be repeated and classified statistically. Repeating requires resetting the cache state between rounds. Two things must be evicted: the value the speculative branch depends on and the probe line that encodes the leaked bit. If the branch resolution stalls long enough, the speculation window reopens, and a subsequent transient access can re-cache the probe line.
Classically, an eviction set — a group of addresses mapping to the same cache set as the target — accomplishes this. Röttger and Janc used eviction lists to reliably push targets out at least into L2. However, building precise eviction sets requires many timed measurements, and the available timer is noisy. A prior remote attack against Workers sidestepped the search by traversing arrays larger than L1 and L2 on every round — functional but slow.
Dougall Johnson's blog post on portable JavaScript Spectre exploitation describes a more elegant approach based on the pigeonhole principle. Allocating far more data than the cache can hold means a randomly chosen cache line is almost certainly already evicted. A 64 MB allocation against a 256 KB L2 leaves at most a 1/256 chance that a random line is still cached. Instead of evicting specific lines, the attacker simply never evicts, picking fresh random locations that are already cold with overwhelming probability. Frequently looping over such arrays creates an auto-eviction effect.
In JavaScript, this means allocating a large pool of attacker/victim object pairs exceeding the last-level cache. Each round picks a fresh random pair, so the object's map pointer — the hidden-class descriptor the speculative type check reads — is almost certainly already evicted.
Co-locating attacker and victim
Both the attacker and victim isolates must run in the same process on the same edge server. Although Cloudflare operates tens of thousands of edge servers, this turns out to be trivial to arrange. Because Workers execute on any edge server, an attacker invoking a victim script via fetch("https://victim.example") usually causes the scheduler to spin up the victim worker in the same process. The victim isolate can be kept alive by making repeated subrequests at intervals.
Furthermore, attack stability depends heavily on CPU load of the edge server. An attacker can run the operation during off-peak hours in a low-traffic colo — for instance, an Australian data center during European business hours — to reduce interference.
Defeating isolate limits
The Workers runtime enforces per-isolate limits; the relevant ones during this research were 30 seconds of CPU time and 1,000 subrequests per invocation. (These have since been raised, but the principles still apply.) Each HTTP fetch event resets the limits, yet landing sequential requests on the same server is unreliable due to load balancing.
Durable Objects solve the persistence problem. They are designed for real-time coordination, so the runtime treats every incoming WebSocket message as a new invocation that resets CPU and request budgets. Keeping a persistent WebSocket open to a Durable Object worker with regular keep-alives maintains a single live isolate and provides a bidirectional channel for sending commands.
One limitation matters: isolates are single-threaded, so incoming WebSocket messages are processed only when the script yields to the event loop. During synchronous execution, the runtime never sees the keep-alives, and CPU time is never reset. If the thread blocks past 30 seconds, the isolate is killed. This caps how much amplification can occur in one synchronous burst. Yielding regularly between bursts kept isolates alive from five to more than twenty hours.
Putting it together
The earlier remote attack relied on repetition to amplify a single cache access, achieving roughly 120 bit/hour. The new approach combines tree-based PLRU amplification with measurement loops. Each iteration re-creates the cache state, adding more timing difference per round. If an interrupt in one iteration corrupts the cache state, subsequent iterations cancel the damage. The resulting signal became strong enough to classify bits against a remote WebSocket timer.
for (let s = 0; s < SAMPLE_NUM; s++) {
timer.mark("mark S" + s);
for (let r = 0; r < OUTER_REP_NUM; r++) {
setup(); // branch mistraining and cache control
leak(secretBit); // transient access
PLRU(cacheSet, INNER_REP); // amplify
}
timer.mark("mark E" + s);
}
delta = fetchFromServer(SAMPLE_NUM);
return median(delta);
An end-to-end attack was demonstrated in the Cloudflare Workers production environment against attacker-controlled Workers. The first phase leaked memory from the attacker Worker; the second leaked a deliberately placed secret from a co-located victim Worker.
Co-location was established first among an attacker Worker, a victim Worker, and a remote timer. Durable Objects provided a long-lived execution context; WebSocket messages provided repeatable timing; and the /cdn-cgi/trace endpoint confirmed machine placement via its fl field.
A calibration step probed the timer with speculatively reachable values. Production noise makes this essential: per-invocation calibration yields separable zero and one distributions from which to classify bits.

As a first step, the isolate root was leaked from one Worker. In another Worker, the speculative type confusion gadget with 64-bit pointers read from that root.

As an intermediate validation, the second gadget was used to read from the vDSO region, which conveniently contains human-readable strings like gettimeofday, confirming true 64-bit leakage.

Finally, a JWT token was placed in the victim Worker and leaked bit by bit. The first byte was the character e — 0b01100101 — and per-bit classifications for that byte are shown below. A two-sided test covers both outcomes; majority voting with a percentile-based threshold infers the bit. In production, the attack sustained a leakage rate up to 12 bit/s with over 99% accuracy, with higher rates achievable at the cost of accuracy.



Robustness
Machine utilization rises with time of day, slowing the attack and requiring more samples. Nevertheless, the attack remains feasible even under high CPU load.

Detection Gaps and Why the Attack Missed Them
DyPrIs, Cloudflare’s runtime monitor, was designed to watch hardware performance counters and isolate suspicious scripts into their own processes once it detects a potential Spectre attack. However, the attack stayed hidden due to two practical shortcomings in how the system operates.
First, DyPrIs only isolates a script after its invocation has completed. The Durable Object keep-alive method leveraged in this attack keeps a single invocation alive for hours—even up to a day. WebSocket keep-alive messages can extend that invocation window significantly, meaning the data leak finishes well before any post-execution isolation can occur.
Second, DyPrIs normalizes branch mispredictions against the number of iTLB accesses. The remote timer built for this attack consists of a single large I/O loop, and the associated WebSocket traffic inflates iTLB activity. As a result, the normalized ratio falls below the detection threshold, causing the malicious workload to appear like any other I/O-heavy Worker running in the environment.
Hardening Measures Implemented
The response to this research focuses on three primary areas: continued V8 hardening, stronger in-process isolation mechanisms, and enhancements to DyPrIs itself.
Enhancing V8’s Memory Sandbox
The V8 memory sandbox is designed to eliminate raw 64-bit pointers from large parts of the JavaScript heap, reducing the utility of many memory-corruption primitives. The research highlighted that this architecture change makes the specific speculative type-confusion gadgets used in the attack harder to reuse, because typed-array backing stores no longer expose the same raw pointer structure.
The V8 sandbox should not be considered a complete mitigation for Spectre. While the 64-bit leak gadget demonstrated in this work is no longer viable, other Spectre variants or gadget combinations could potentially still lead to arbitrary out-of-bounds memory reads.
Hardware-Assisted Isolation with MPK
In September 2025, Cloudflare deployed in-process isolation for Workers using Memory Protection Keys (MPK). This hardware feature allows a single process to partition memory into distinct protection domains and switch access rights with minimal overhead. Workers leverage MPK to ensure that each isolate’s heap is protected from other isolates operating within the same process.
This fundamentally alters the Spectre risk model. Each isolate heap now sits behind a hardware-enforced access boundary—the CPU denies any memory access to a page protected with the wrong key. This prevents the straightforward cross-isolate heap reads that this attack depended on.
MPK is not a complete Spectre remediation, but it substantially reduces the leakage surface. The technology has inherent constraints, including a finite number of hardware domains and the need for precise management of protection-key state.
Improving DyPrIs for Long-Running Workloads
DyPrIs has been updated to treat long-lived executions and I/O-heavy workloads as first-class security cases. The previous approach—isolating only after a script finishes—was inadequate for workloads like Durable Objects or WebSocket-heavy Workers that can run for extended periods.
Cloudflare is also investigating whether remote timing behavior could serve as an additional detection dimension for DyPrIs. While remote communication with attacker-controlled infrastructure cannot be entirely eliminated, the timing patterns reveal distinctive exfiltration bit signatures. The intended approach treats repeated timer-like I/O surrounding compute-heavy sections as a behavioral signal rather than discounting it as background noise.
Contributions and Security Research
Cloudflare extended thanks to Haocheng Xiao from the University of Edinburgh and his supervisors, Sam Ainsworth and Nigel Topham, for their work on the reliability of Spectre attacks in JavaScript.
Researchers are encouraged to submit findings through the Cloudflare Bug Bounty program, with memory safety bugs in the runtime identified as particularly high-value targets. The Fuzzilli integration for workerd and the full workerd source code are both publicly available on GitHub for those interested in contributing to ongoing security research.



