Article summary — original

Cloudflare Details Production Remote Spectre Attack on Workers and Subsequent Mitigations Cloudflare researchers reassessed remote Spectre side-channel attacks against Cloudflare Workers, demonstrating a proof-of-concept that bypassed existing isolation defenses and leaked cross-tenant memory in production at up to 12 bit/s before deploying comprehensive mitigations. Researchers developed a remote Spectre attack in the Cloudflare Workers production environment achieving a 12 bit/s leak rate with over 99% accuracy. The attack chained speculative type confusion against raw 64-bit TypedArray pointers, tree-based PLRU cache amplification, and an auto-eviction strategy based on the pigeonhole principle. Execution limits and detection mechanisms were bypassed by using Durable Objects WebSockets to keep isolates alive for hours and dilute branch misprediction metrics with network I/O. Cloudflare neutralized the attack by integrating the V8 Sandbox, deploying Memory Protection Keys for hardware-enforced in-process isolation, and updating DyPrIs monitoring. Exploiting Speculative Execution in V8 Cloudflare Workers achieves low-latency multi-tenancy by sharing single operating-system processes among thousands of tenants via V8 isolates. The research demonstrated that speculative execution can break this boundary. Because Cloudflare freezes local timers and blocks shared memory, the researchers constructed speculative type-confusion gadgets. Prior to the adoption of the V8 Sandbox, TypedArray instances still stored raw 64-bit pointers to their backing stores. By mistraining branch predictors on genuine instances and then feeding an attacker-crafted layout, the CPU transiently accessed memory through arbitrary 64-bit pointers and encoded the leaked bits into CPU cache lines. Signal Amplification and Cache Eviction Overcoming microarchitectural noise and coarse-grained remote timers required extensive signal amplification. The researchers exploited the tree-based pseudo least recently used (PLRU) cache-replacement policy in L1 caches, using patterned accesses to exponentially magnify the timing disparity between cache hits and misses. To reset the microarchitectural state without relying on slow, traditional eviction-set construction, the attack leveraged the pigeonhole principle by allocating memory pools larger than the last-level cache. Randomly accessing fresh object pairs ensured target cache lines were evicted with overwhelming probability. Co-Location and Persistence via Durable Objects To exploit a target, the attacker Worker ensured co-location with the victim isolate on the same server process simply by invoking the victim via fetch requests. Because normal Worker invocations face limits on CPU time and subrequests, the attack maintained persistent execution by connecting to a Durable Object over WebSockets. Each incoming keep-alive message reset the runtime limits, enabling continuous execution from five to more than 20 hours when the script periodically yielded to the event loop. WebSocket timestamps from external servers also provided the necessary remote timing signals. Evasion of Dynamic Process Isolation The attack bypassed Cloudflare's primary production defense, Dynamic Process Isolation (DyPrIs), which monitors hardware performance counters to isolate suspicious scripts. DyPrIs previously evaluated scripts only after an invocation completed; the continuous WebSocket loop kept a single invocation active long enough to complete data leakage before evaluation occurred. Furthermore, DyPrIs normalized branch mispredictions against iTLB accesses, and the extensive network I/O traffic generated by the remote timer artificially inflated iTLB activity, lowering the metric below the alert threshold. Production Hardening and Mitigations Cloudflare resolved the vulnerability in production through multiple layers of hardening. The runtime team integrated the V8 Sandbox to remove raw 64-bit pointers from the heap, disabling the primary type-confusion gadget. Additionally, Cloudflare deployed Memory Protection Keys (MPK) to assign separate hardware-enforced memory domains to each isolate within the process. DyPrIs was also updated to monitor scripts mid-execution and treat long-lived, I/O-intensive workloads around compute sections as potential behavioral indicators rather than benign noise.

Article summary — zh

Cloudflare 重新评估 Workers 远程 Spectre 攻击并推出多层加固防御 Cloudflare 发布了与爱丁堡大学等学者合作的最新研究,重新评估了 Workers 生产环境下的远程 Spectre 攻击风险。研究团队在生产环境中构建了 PoC,利用投机类型混淆与远程计时放大绕过了原有的动态进程隔离防御,实现了高达 12 bit/s 且准确率超 99% 的跨租户内存窃取。目前相关漏洞已在生产环境中完成修复,未发现过去三年的在途利用证据。 研究团队攻破了原有的 DyPrIs 防御,在生产环境下实现了 12 bit/s 的跨 Worker 数据窃取,准确率达到 99%。 攻击结合了 TypedArray 暴露的 64 位原生指针进行投机类型混淆,并利用 L1 缓存的 PLRU 树状替换策略实现信号放大,成功兼容远程粗粒度计时器。 借助 Durable Objects 维持 WebSocket 长连接,攻击代码可在单进程中持续运行数小时至一天,从而绕过原先仅在调用结束时触发的隔离机制,且其 I/O 流量稀释了分支预测失误指标。 Cloudflare 已通过集成 V8 沙箱、采用基于硬件 MPK 的进程内隔离以及重构 DyPrIs 检测系统来阻断此类利用。 生产环境下的攻击挑战与新型 Gadget Cloudflare Workers 允许多租户共享同一个操作系统进程,依靠 V8 隔离区实现隔离。由于平台限制了本地高精度时钟、禁止多线程与共享内存,攻击者必须克服同机共存调度、远程时钟噪声和微架构噪声等障碍。研究人员设计了两种 Spectre Gadget:一种泄露压缩堆指针,另一种利用此前 TypedArray 底层缓冲区暴露的未压缩 64 位原生指针进行投机类型混淆,将推测性越界读取转化为任意 64 位地址读取。 信号放大与无驱逐集重置策略 针对远程计时器微秒至毫秒级的网络抖动,研究利用 L1 缓存的树状伪最近最少使用(PLRU)替换策略,通过特定访存模式将单个缓存事件放大为数十倍命中与未命中的时间差异,使基于 WebSocket 的远程计时器具备亚毫秒级分辨能力。此外,攻击采用鸽巢原理,分配远超 LLC 容量的随机对象池进行轮换访存,省去了耗时构建传统逐出集的步骤,实现了高概率自动清除分支条件与探测行。 同机调度与逃逸 DyPrIs 监测机制 攻击者通过自调用发起的子请求即可轻易促使受害者脚本在同一边缘节点同进程启动,并可选择在非高峰机房降低硬件噪声。为绕过 30 秒执行限制,攻击利用 Durable Objects 处理 WebSocket 保活消息,使脚本实例维持数小时至一天。这种长周期阻塞了原先仅在调用结束后才执行的 DyPrIs 硬件计数器审计,且密集的 WebSocket I/O 带来的 iTLB 访问量稀释了分支预测错误比率,使攻击行为伪装成了普通的重度 I/O 负载。 多重防御与运行时加固落地 Cloudflare 现已部署三重加固应对此类威胁:首先集成 V8 沙箱,消除堆上的 64 位原生指针以破坏该类型 Gadget;其次在 2025 年 9 月引入基于内存保护密钥(MPK)的进程内隔离,在硬件层面阻止跨 isolate 内存读取;最后重构 DyPrIs,取消单一的事后检测,将长周期与高 I/O 脚本纳入即时分析,并尝试将异常的高频远程计时交互作为威胁识别特征。

Saved original article

In 2021, we assessed [remote Spectre attacks](https://blog.cloudflare.com/spectre-research-with-tu-graz/) against Cloudflare Workers. Based on the results, we shipped a production defense called [Dynamic Process Isolation](https://blog.cloudflare.com/spectre-research-with-tu-graz/) (DyPrIs), which identifies maliciously looking scripts and isolates them into separate processes. Since then, newer techniques in the area of stabilizing Spectre attacks have been discovered. To understand if these techniques posed a threat to our Workers production environment, we decided to internally reassess the remote Spectre attack. Building an updated proof-of-concept on the production environment allowed us to empirically assess the risk of Spectre attacks under production workloads. To mount a successful side-channel attack in production, an external attacker has to overcome additional obstacles such as activity on shared hardware resources, interrupts, context switches, and coarse-grained timers. Our research uncovered a limitation in the implementation of DyPrIs and we managed to demonstrate a remote Spectre attack reliably leaking up to 12 bit/s with a 99% accuracy in the production environment of Cloudflare Workers. As a consequence of this research, we improved DyPrIs, integrated the V8 Sandbox and [an in-process isolation mechanism](https://blog.cloudflare.com/safe-in-the-sandbox-security-hardening-for-cloudflare-workers/) to further reduce the risk of memory disclosure attacks. Today we are [publishing a paper](https://arxiv.org/pdf/2608.17043) describing our findings, co-authored by Albert Pedersen, Haocheng Xiao, Sam Ainsworth, Nigel Topham, and Martin Schwarzl. This paper covers research done in 2024 and early 2025. Note that the presented attack is mitigated already in the production system due to countermeasures applied by Cloudflare Workers Runtime team. We did not find any indicators of active exploitation over the last three years. Cloudflare Workers runs untrusted JavaScript on the edge. Leveraging language-level isolation, in the form of V8 isolates, tens of thousands of tenants can share the same operating-system process. Each Worker has its own separate JavaScript heap. This design keeps startup latency low and lets us run many tenants very efficiently compared to full process isolation. Around the runtime we have multiple layers of defense such as automated V8 patch pipelines, a two-layered sandbox consisting of Linux namespaces and seccomp filters, Cap’n Proto RPC, and the possibility to schedule certain scripts in separate process sandboxes. Still, a single arbitrary read vulnerability within a Worker process can lead to cross-tenant leakage. One vulnerability that is very hard to mitigate exploits the nature of speculative execution, namely in-process **Spectre**. You can think of speculative execution in terms of hiking. At some point you arrive at a branch and have to predict where to go. If the prediction was correct, you saved some time and could enjoy the sun and a refreshing drink at a mountain hut. However, if you speculate in the wrong direction, you have to turn back. The trail looks untouched, but your footsteps remain in the mud. Speculative execution in CPUs works similarly. The branch prediction performs an educated guess about a branch’s outcome ahead of time and the CPU speculatively executes it. If the prediction was correct, speculative execution saved some time. However, if the prediction is incorrect, the CPU has to discard the results, roll back and execute the other branch. Because these speculatively executed instructions only exist temporarily in the CPU pipeline and are never permanently retired or committed, the literature refers to them as transient instructions and generalizes the concept as transient execution. However, due to the transient execution, there are still some traces left in the microarchitectural state for instance in CPU caches. Thus, an attacker can use Spectre to transiently access memory out of bounds, encode a single bit of information into the cache state and exploit the latency of reaccessing data to infer whether the bit was set or not. To [mitigate against in-process Spectre attacks](https://blog.cloudflare.com/spectre-research-with-tu-graz/), Cloudflare Workers freezes local timers, disallows multithreading and shared memory and actively detects, periodically shuffles memory and isolates malicious-looking scripts into separate processes. The Cloudflare Workers platform deliberately [restricts timers](https://blog.cloudflare.com/mitigating-spectre-and-other-security-threats-the-cloudflare-workers-security-model/). 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 via a `SharedArrayBuffer` is not available. To successfully mount an attack, several challenges have to be solved. First, Workers runtime is limited and co-location between an attacker and victim has to be guaranteed. Second, a reliable, ideally co-located, remote timer has to be discovered, which allows stable timing measurements. Third, the attack runs under production conditions, meaning it requires additional stability measures such as a reliable Spectre gadget enabling transient 64-bit out-of-bounds accesses, robust signal amplification to deal with systems and networking noise, and a primitive to reliably evict data out of the cache. *Speculative type confusion Spectre gadget* With the right Spectre gadget (snippet above), an attacker can transiently access out-of-bounds memory and encode a single bit into the cache (`probeArray`). The attacker then measures the memory access latency to confirm whether data has been cached or not. A faster access means the line was cached and the bit was 1. Conversely, a slower access means it was uncached and the bit was 0. In our attack, we use two different Spectre gadget types. The first one leaks compressed heap pointers, e.g., the isolate’s heap base address (root), and the other one leverages a speculative type confusion to leak from an arbitrary, attacker-crafted userspace 64-bit pointer. At the time of performing the research, the V8 Sandbox was not yet implemented at Cloudflare Workers. Under pointer compression, most objects use 32-bit compressed pointers. `TypedArray` was one of the few exceptions that still stored a raw 64-bit pointer to its backing store, which is exactly what our gadget abuses. The branch `obj instanceof ObjP` performs a type check, i.e., a branch. To mistrain the branch prediction, we call the gadget many times on real `ObjP` instances, then call it on a different object with an attacker-controlled memory layout `ObjI`. The CPU speculates on the taken branches and follows `obj.ptr[0]`, even though the object has a different type. To leak a single bit, we mask out one bit and use it to select one of two `probeArray` lines. Whether that line is cached encodes the bit. Exploiting the heap leakage gadget, we map neighboring objects and locate an attacker-controlled array. Our second gadget confuses two large objects that span several cache lines, so the type field lands on a different cache line than the field we read. Evicting the type field opens the speculation window while the target field stays cached, and the transient read follows an attacker-controlled 64-bit value. That turns the leak into an arbitrary-address read. A more thorough description of this technique can be found in the paper. **Local demo of leaking an arbitrary 64-bit address.** A cache hit and a cache miss differ by a few nanoseconds. Moreover, a remote timer is noisy at the scale of a few microseconds up to a few milliseconds. Therefore, some form of signal amplification is required to differentiate a cache hit from a miss. Stephen Röttger and Artur Janc discovered a way to [amplify a single memory access](https://security.googleblog.com/2021/03/a-spectre-proof-of-concept-for-spectre.html), by exploiting the tree-based pseudo least recently used (PLRU) cache-replacement policy in L1 caches. Tree-based PLRU organizes each cache set as a binary tree whose nodes point to the side used least recently, so the CPU evicts by following those pointers. With the right access pattern, an attacker can keep a target line cached indefinitely by touching its tree neighbor whenever the pointers turn toward the target. Quite elegant, right? Leveraging that behavior, the timing of a single cache event can be arbitrarily amplified such that it leads to a lot of L1 hits (faster) compared to lots of L1 misses in the opposite case. The figure below illustrates whether a memory address X is cached or not. If it’s not cached, the access pattern leads to a lot of cache hits. If it is present, it occupies one node in the tree, and subsequently four cache lines try to fit into three nodes, which results in a lot of L1 misses. As long as the signal can be amplified, a noisy remote timer is sufficient to differentiate an encoded bit. For instance, a WebSocket connection to an external server serving high-resolution timestamps is enough. The timer could be hosted at Cloudflare or at a co-located data center to the target data center running the Worker. The Worker asks the remote timer to mark a timestamp for a certain event and compute the delta for another request once the event has stopped. In the paper, we evaluated several different timer setups and were able to reliably achieve sub-ms resolutions on the Median with only a handful of samples even over larger topological distances. The figure below shows an amplified cache event using the tree-based PLRU amplification. A single measurement is not enough to differentiate timing-encoded data reliably. Production machines are noisy, thus an attacker has to repeat each measurement at least a few times and use some statistical discriminator. Repeating a measurement in our case means resetting the cache state. Two things have to be uncached before each round. The value the speculative branch depends on has to be evicted, so branch resolution stalls long enough to open a speculation window. The probe line that encodes the leaked bit has to be evicted, so the next transient access can re-cache it. Since there is no direct instruction available in JavaScript, the classic way to do this is to build an eviction set. An eviction set is a group of addresses that map to the same cache set as the target. Accessing them in the right pattern pushes the target out of the cache. In their attack, Stephen Röttger and Artur Janc used an eviction list to reliably evict at least into the L2 cache. This works, but it is expensive. Constructing a precise eviction set requires many timed measurements, and our timer is a noisy remote timer. The previous remote attack against Workers sidestepped the search by traversing an array larger than the L1 and L2 caches on every round. That is an option, but even slower. Dougall Johnson described a more elegant way in his really cool blog post on [portable JavaScript Spectre exploitation](https://dougallj.wordpress.com/2021/03/16/another-approach-to-portable-javascript-spectre-exploitation/). The idea follows directly from the pigeonhole principle. If you allocate far more data than the cache can hold, a randomly chosen cache line is almost certainly not cached. For a 256 KB L2 cache, allocating 64 MB leaves at most a **1/256** chance that a random cache line is still in L2. So instead of evicting a specific line, you never evict at all. You pick a fresh random location that is already evicted with overwhelming probability. The cool side effect of looping frequently over that array of objects is that this will lead to an auto-eviction effect. To leverage this in JavaScript, we allocate a large pool of attacker and victim object pairs that exceeds the last-level cache. Each measurement round selects a fresh random pair. The object's map pointer, the hidden-class descriptor that the speculative type check reads, is therefore almost certainly already evicted. For the attack to work, both the attacker and victim isolate must be scheduled in the same process on the same edge server. One might intuitively think this would be difficult, considering Cloudflare operates tens of thousands of edge servers, but this is in fact quite trivial on Cloudflare Workers. Because Cloudflare Workers are designed to execute on any Cloudflare edge server, invoking the victim script from the attacker script with a `fetch(“https://victim.example”)` will in most cases cause the scheduler to spin up an instance of the victim worker in the exact same process. The victim isolate can be kept alive by repeatedly making subrequests to it at a certain interval. What is more, because the attack stability is highly dependent on the CPU load of the edge server running the worker script, this allows an attacker to strategically run the attack in an off-peak colo (e.g. in an Australian colo during European business hours) where the traffic levels are comparatively low. The Cloudflare Workers runtime [enforces a set of limits](https://developers.cloudflare.com/workers/platform/limits/) on all isolates to protect the platform and prevent abuse. For the purposes of conducting this attack, the relevant limits were 30 seconds of CPU time and 1,000 subrequests per invocation. These limits have since been [increased](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits), but the following principles are still relevant. For a regular Worker, each HTTP request, a fetch event, is a new invocation that resets these limits. The catch is landing sequential requests on the same edge server. Load balancing and shifting network conditions make that unreliable. Durable Objects solve it for us. [Durable Objects](https://developers.cloudflare.com/durable-objects/) are built for real-time coordination between clients, so the runtime treats every incoming WebSocket message as an invocation that resets the CPU time and request limits. The attacker opens a persistent WebSocket to a Durable Object worker and sends regular keep-alive messages. This keeps a single isolate alive and gives us a persistent, bi-directional channel to run the attack over. One quirk cost us some time. An isolate is single-threaded, so incoming WebSocket messages are only processed when the script hands control back to the event loop. During synchronous code the runtime never sees the keep-alive, so it never resets the CPU time. If the thread stays blocked for more than 30 seconds, the runtime kills the isolate. This puts an upper bound on how much we can amplify in a single synchronous burst. Yielding regularly between bursts lets us keep an isolate alive from five to more than 20 hours. The previous attack relied mostly on repetition to amplify a single cache access, and therefore, was slowly leaking 120 bit/h. We combined tree-based PLRU amplification with measurement loops. Each iteration re-creates the cache state and thereby adds more timing difference. If an interrupt destroys the cache state in one iteration, it doesn’t matter, since later iterations cancel it out. This made the signal strong enough to classify bits with a remote WebSocket timer. The overall idea is now to combine. We demonstrated the full end-to-end attack in the Cloudflare Workers production environment, against Workers we controlled. We first leaked memory from the attacker Worker. From there, we leaked data from a co-located victim Worker where we had intentionally placed a secret. First, we established co-location between an attacker Worker, a victim Worker we owned, and a remote timer. Durable Objects gave us a long-lived execution context. WebSocket messages gave us a repeatable timing source. The `/cdn-cgi/trace` endpoint helped us confirm machine placement by looking at the *fl* value. Second, we added a calibration step to probe the timer with speculatively reachable values. This step matters because production machines are noisy. Per-invocation calibration lets us classify bits from the relative difference between the zero and one distribution. This last test should lead to two clearly separable distributions. As a first step, we leaked the isolate root from one Worker and in another Worker we used the speculative type confusion with 64-bit pointers to read from the isolate root. As an intermediate step, we confirmed 64-bit leakage with the second gadget by reading memory from the vDSO region. The vDSO is a convenient target because it contains human-readable strings such as `gettimeofday`. **Demo Video leaking data from the JavaScript heap** Finally, we placed a JWT token in the victim Worker and leaked it bitwise. The first byte was the character e, represented as 0b01100101. The figure below shows the per-bit classification for that byte. To classify we use a two-sided test to test for both outcomes. Using a majority vote and a percentile-based threshold, we infer the bit. In production, we achieved a leakage rate of up to 12 bit/s with an accuracy of more than 99%. Note that higher leakage rates are possible with the cost of losing accuracy. Depending on the time of the day, the utilization of a machine increases strongly. This slows down the attack since more data has to be sampled. Still, even with high CPU utilization, the attack is still feasible. DyPrIs watches hardware performance counters and isolates a script into its own process once it looks like a Spectre attack. Two things kept the attack under the radar. First, DyPrIs isolates a script only after its invocation finishes, and the Durable Object keep-alive trick we used in the attack can run for a few hours up to a day. WebSocket keep-alive messages hold a single invocation open for hours, so the leak completes long before isolation would kick in. Second, DyPrIs normalizes branch mispredictions by the number of iTLB accesses. Our remote timer is one large I/O loop, and that WebSocket traffic inflates iTLB activity. The normalized ratio drops below the detection threshold, so the attack looks like an ordinary I/O-heavy Worker. We focus on the three areas of continued V8 hardening, providing stronger in-process isolation, and improving detection. The V8 memory sandbox's final goal is to remove raw 64-bit pointers from large parts of the JavaScript heap, which reduces the usefulness of many memory-corruption primitives. It also makes the specific speculative type-confusion gadgets in this work harder to reuse, because typed-array backing stores no longer expose the same raw pointer structure. The V8 sandbox is not a complete Spectre mitigation. While the presented 64-bit leak gadget does not work anymore, there might be other Spectre variants or gadgets exploitable to achieve arbitrary out-of-bounds memory accesses. In September 2025, we deployed [in-process isolation](https://blog.cloudflare.com/safe-in-the-sandbox-security-hardening-for-cloudflare-workers/) for Workers using Memory Protection Keys (MPK). MPK lets a process divide memory into protection domains and switch access rights cheaply. Workers use it to protect each heap from being accessible to the other isolates within the same process. This changes the Spectre risk model. Each isolate heap now sits behind a hardware-enforced access boundary. A memory access to a page protected with the wrong key is denied by hardware. This blocks the straightforward cross-isolate heap read that this work relied on. Unfortunately, MPK is not a complete answer to remediate Spectre, but it strictly reduces the leakage surface. It has limits, including a finite number of hardware domains and the need to manage protection-key state carefully. We improved DyPrIs so that long-lived executions and I/O-heavy workloads are handled as first-class security cases. Detection cannot happen only after a script finishes. A Durable Object or a WebSocket-heavy Worker can run long enough that post-execution isolation arrives too late. We are currently investigating whether remote timing behavior could be added as an additional dimension to DyPrIs. While we cannot eliminate remote communication with attacker-controlled infrastructure, the timing data reveals very interesting exfiltration bit patterns. The better approach is to treat repeated timer-like I/O around compute-heavy sections as part of the behavioral signal, not as background noise. We especially thank Haocheng Xiao from University of Edinburgh and his supervisors, Sam Ainsworth and Nigel Topham, for their contributions to the reliability of Spectre in JavaScript. We are always looking for high-quality submissions through our [Bug Bounty program](https://hackerone.com/cloudflare). Memory safety bugs in the runtime are high-value targets. You can find the [Fuzzilli integration for workerd](https://github.com/cloudflare/workerd/pull/4917) and the [workerd source code](https://github.com/cloudflare/workerd) on GitHub.