When Agentic Glue Melts: Exploiting Cloudflare Code Mode and Workers Check Point Research found five memory-corruption bugs in Cloudflare's workerd runtime, which underpins both Cloudflare Workers and Cloudflare Code Mode, and exploited them to break out of the sandbox and run arbitrary code. The attacks, named URLPattern and node:zlib, allow one Worker to reach across the shared process heap and escape the V8 isolate boundary, respectively. Cloudflare has not yet commented on the findings. By Yarden Porat, Check Point Research We set out to break Cloudflare Code Mode , and ended up breaking Cloudflare Workers too. We did both by targeting workerd , the runtime beneath both: an in-process sandbox that relies entirely on V8 to isolate untrusted code. We found five memory-corruption bugs in workerd’s native C++ the “glue” between JavaScript and the runtime , and turned them into two end-to-end attacks: URLPattern lets one Worker reach across the shared process heap and node:zlib breaks out of the sandbox and runs Code Mode is Cloudflare’s take on LLM tool use. Instead of a model emitting structured tool calls one at a time, Code Mode exposes the available tools as a typed TypeScript API and lets the model write code that calls them: loops, conditionals, data shuffling and all. In the traditional MCP / tool-calling loop, the model emits one {tool, args} call, the agent runs it, feeds the result back. The model then emits the next call. Every step is a fresh model invocation, and usually a network round-trip. Code Mode collapses that: the model writes one program that orchestrates many tool calls itself looping, branching, and combining intermediate results locally and only the final output returns to the model. Cloudflare’s argument is that LLMs, trained on enormous amounts of real-world code, are simply better at writing a program against a typed API than at emitting long chains of synthetic tool calls. 4 Figure 1 – Tool calling vs. Code Mode That code has to run somewhere, and that “somewhere” is workerd , the runtime behind Cloudflare Workers. To understand workerd, start with the product it was built for: Cloudflare Workers . Workers is Cloudflare’s serverless platform: you upload a piece of code and Cloudflare runs it at the edge , in data centers close to the user, on demand for every request. There’s no server to manage and, ideally, no cold machine to wait for. That model creates a hard isolation problem. Cloudflare runs code from a huge number of different customers, and to keep latency and cost down it packs many of them onto the same machines, and, as we’ll see, into the same process. The classic answer a container or VM per tenant is far too heavy for this: each one adds tens to hundreds of milliseconds of cold start and a real memory footprint, which is exactly what an edge platform serving oceans of short requests cannot afford. Cloudflare’s answer is to isolate at the language-runtime level rather than the OS level, using V8 isolates, the same primitive Chrome uses to separate browser tabs. An isolate is a lightweight, independent JavaScript context. Many can live inside a single process, each starts in single-digit milliseconds, and the isolate is the security boundary between tenants. The trade-off is that this boundary is a software boundary inside one shared address space, not a hardware or kernel one. Untrusted code runs in-process , and the whole model rests on the isolate holding. Figure 2 – Many tenants, one process workerd is the runtime that implements all of this. It was closed-source for years: Workers launched in 2017, but Cloudflare only released workerd as open source in September 2022 . 5 It’s exactly what Code Mode runs the model’s generated code on. Code Mode has to run untrusted, model-written code, and it needs that code to reach the declared MCP tools and nothing else . workerd answers both at once. Running untrusted tenant code in-process is its day job, and it lets Code Mode lock the rest down: no filesystem, no arbitrary network fetch and connect simply throw with the tools exposed only through bindings. 6 Cloudflare didn’t build a new sandbox for Code Mode. It reused the one it already trusts to isolate millions of Workers. When you set out to break Code Mode, the obvious place to look is the seam between Code Mode and workerd. This is the integration layer: how tools become bindings, how the configuration is wired, how the two interact. Going after the runtime itself is the unusual move. It’s a bit like setting out to break an AI coding assistant and then going to audit Docker’s own source code, the container runtime itself, not the agent on top of it. Five reasons made us decide to do it anyway: V8 is one of the most heavily attacked pieces of software around, with a long history of memory bugs, so Cloudflare assumes it can break and layers defenses so a compromise of one isolate doesn’t reach the host or other tenants. 1. The V8 sandbox “the cage” . The cage confines JS-reachable objects so a corrupted one can’t forge pointers outside it. Assume arbitrary read/write inside the cage, and stop it reaching memory outside. 2. Memory protection keys. As a further layer against V8 vulnerabilities, production also tags isolate-group memory with hardware memory protection keys MPK / pkeys , so even with arbitrary read/write inside one isolate’s V8, an attacker still can’t read another tenant’s pages. 3. The L2 process sandbox. Underneath both sits a second-layer “L2” process sandbox , so even native code execution inside the process is meant to be contained. Per Cloudflare, the V8 Workers run in a strict layer-2 sandbox Linux namespaces plus seccomp that blocks all filesystem and direct network access, 8 limiting what a compromised process can reach on the host. Node. Real-world JavaScript assumes Node.js exists, and code constantly reaches for node: modules, so workerd reimplements a large slice of the Node API in C++. This is exposed to JS through JSG , its “JavaScript Glue” layer. Node was never designed for a threat model where the attacker writes the JavaScript, so this drops a great deal of extra native code onto the boundary, much of it workerd’s own, and enabled by default a Worker can just require 'node:crypto' . It also means more native objects allocated on the tcmalloc heap , which is secured by neither the cage nor the memory protection keys. Putting all of the above together, we did exactly that. We targeted workerd’s JSG code , the “JavaScript Glue” that hands native C++ to untrusted JavaScript, whether it is a Node reimplementation or one of workerd’s own API implementations . It is the code that had a fraction of V8’s scrutiny §4 , and the native objects it allocates sit on the tcmalloc heap , memory that lives outside both the cage and the memory-protection keys §5 . So a bug there is not boxed in the way a V8 bug is. It is exactly the surface those mitigations do not cover. By going after that code we found five vulnerabilities, all of them in workerd’s own native code , each covered in the Vulnerabilities section Part II . Building on those bugs, we developed two end-to-end exploits , covered in the Exploits section Part III . But to be explicit, we did not run the exploit on Cloudflare production ourselves. Both exploits were verified on the self-hosted version of workerd. The cross-tenant idea should work the same way on production, since it runs entirely from the tcmalloc heap that the mitigations do not cover, but we did not test it there. On a shared host, a memory-corruption exploit that crashes the process could take other tenants down with it, and we were not willing to risk that. URLPattern is a Web API for matching a URL against a pattern, essentially what a router does. You build a pattern such as new URLPattern { pathname: "/users/:id" } , call .exec on a URL, and read back the named capture groups { id: "…" } . workerd exposes it to Workers, and in our setting the pattern itself is attacker-controlled. workerd actually ships two URLPattern implementations. The first is the original, workerd-native one the urlpattern original compatibility flag . The second is the newer standard one backed by the Ada URL-parser library. We found the same out-of-bounds read in both implementations, and it gives the same primitive . Under the hood, URLPattern turns your pattern into a regular expression. Matching a URL then produces two parallel lists: the matched values one per capture group in the regex and the group names . A quick example of the benign case: Figure 3 – URLPattern: pattern → result URLPattern also lets you drop raw regex straight into a pattern, with named or unnamed groups. For example, / \d+ / ?