Hardening the runtime beneath your code
Running third-party code on our infrastructure is one of the harder security problems in computing: attackers get the full power of a programming language on a victim's system. The Workers Runtime is built on V8, the JavaScript engine from Chromium. That gives us a strong starting point, because V8 has been shaped in an adversarial environment and is constantly exercised by fuzzers and sanitizers. Over the years it has picked up defenses like Oilpan/cppgc and improved static analysis. But we use V8 differently than a browser does, and that difference drives some of our recent security work.
Hardware-assisted isolation with memory protection keys
Modern CPUs from Intel, AMD, and ARM support memory protection keys, also known as PKU (Protection Keys for Userspace). This extends the traditional virtual memory model: instead of only the kernel and process boundaries controlling access, individual threads within a process can be denied access to specific memory regions. That opens up a stronger version of the least-privilege principle for user-space code.
V8 already uses memory protection keys for its JIT compilers. A compiler thread needs to write optimized code, but should not be able to execute it. The regular execution thread needs to run that code, but should never modify it. Protection keys let each thread have exactly the permissions it needs, and no more. The Chromium V8 team continues to push on this, with further plans documented publicly.
Our requirements in Workers differ from Chromium's. The Workers security model separates scripts using V8 isolates, with additional mitigations layered on top to defend against Spectre-style attacks. If V8 works as intended, that separation holds. But we believe in defense in depth: overlapping layers of security controls so that a single bug does not become a full compromise.
To that end, we have deployed internal modifications to V8 that use memory protection keys to isolate isolates from one another. A modern x64 CPU offers up to 15 keys; a few are already used elsewhere in V8, leaving roughly 12 for our purposes. Each isolate is assigned a random key protecting its V8 heap data — the memory region holding the JavaScript objects a script creates at runtime. If an attacker finds a security bug that lets them read another isolate's data, the hardware detects the mismatched key and kills their script, then notifies us for investigation and remediation. With 12 keys, that hardware trap catches the attempted cross-isolate access in about 92% of cases (11 out of 12).

The remaining 8% chance of failure occurs when an attacker happens to hit an isolate that was assigned the same protection key. In the diagram, that lucky collision is the red arrow; the mismatch cases are caught and blocked. As we'll describe shortly, there is a way to improve that 92% to 100% for a particularly common scenario. First, though, we want to look at a software hardening feature in V8 that we are taking advantage of in the meantime.
The V8 sandbox: a software security boundary
V8 has been acquiring another layer of defense: the V8 sandbox (distinct from the layer 2 sandbox Workers have used from day one). This multi-year initiative arose from a recurring observation—many V8 vulnerabilities begin with corruption of objects in the V8 heap. Attackers then ride that corruption into other parts of the process, pivoting toward the browser or the wider system. The V8 sandbox is designed to sever that chain, preventing escalation from a heap corruption to a full process compromise—in part by eliminating raw pointers from the heap.
How memory corruption attacks work
A memory corruption attack tricks a program into abusing its own memory. Memory is a linear store of integers, each at an address (also a number). Programs interpret these integers variously—as text, pixels, or pointers. A pointer is an address that references another location, functioning as an arrow to a different piece of data.
Consider a classic buffer overflow. Imagine a program with a 16-character buffer immediately followed by an 8-byte pointer. An attacker sends 24 characters; due to a flaw, the first 16 fill the buffer and the remaining 8 overwrite the adjacent pointer.

The sequence of this attack is described below—and how the sandbox now blocks it.
The pointer is now redirected to sensitive data of the attacker’s choice. When the program dereferences what it believes is a normal pointer, it retrieves attacker-selected data. Such attacks proceed in stages: induce a small confusion (overflow), amplify it, and eventually exfiltrate data or plant malicious payloads the program treats as valid.
Compressed pointers in V8
Since many exploits hinge on pointer corruption, removing all pointers from memory would be ideal—but object-oriented heaps are crowded with them. A prerequisite arrived in 2020: V8 began offering compressed pointers to save memory. On 64-bit systems, the heap uses 32-bit offsets relative to a base address, capping the heap at 4 GiB. That cap is acceptable for browsers and for individual isolates on Cloudflare Workers.

An object with various fields in compressed vs. uncompressed layouts; the boxes are 64 bits wide.
If the heap fits in one 4 GiB region, the upper 32 bits of every pointer are identical and need not be stored. In the figure, all object pointers share the prefix 0x12345678, so it can be omitted—shrinking object pointer fields and integer fields from 64 to 32 bits. Fields for double precision floats and for buffer offsets (the data scripts read and write) retain 64 bits, as detailed below.
In an uncompressed heap, integers occupy the high 32 bits of a 64-bit field; with compression, they use the top 31 bits of a 32-bit field. In both cases, the low bit is 0 to mark it as an integer (distinguishing it from pointers or offsets). Compression and decompression use a base address divisible by 4 GiB:
// Decompress a 32 bit offset to a 64 bit pointer by adding a base address.
void* Decompress(uint32_t offset) { return base + offset; }
// Compress a 64 bit pointer to a 32 bit offset by discarding the high bits.
uint32_t Compress(void* pointer) { return (intptr_t)pointer & 0xffffffff; }
What began as a memory-saving feature can now serve as a sandbox foundation.
From compressed pointers to a full sandbox
Since the largest 32-bit unsigned integer is ~4 billion, the Decompress() function can never produce a pointer outside [base, base + 4 GiB]. Those pointers are trapped in that region, called the pointer cage. V8 reserves 4 GiB of virtual address space for the cage, so that only V8 objects reside within it. With all pointers eliminated from this range—and strict rules for what can remain—a memory corruption is contained within the cage. Even if an attacker corrupts a 32-bit offset, it can only yield another pointer still trapped inside.

The prior buffer overflow attack now fails because the attacker’s own data is confined within the pointer cage.
To construct the full sandbox, V8 pairs the 4 GiB pointer cage with an additional 4 GiB for buffers and other structures, yielding the 8 GiB sandbox. That explains why buffer offsets are 33 bits—they must reach data in the sandbox’s second half (40 bits in Chromium’s larger sandboxes). These offsets are stored in the high 33 bits and shifted down by 31 using 64-bit math when accessed, so that corruption of the low bits has no effect.
Cloudflare Workers have used compressed pointers in V8 for some time, but unlocking the full sandbox required changes. Previously, all isolates in a process had to share a single sandbox, which would limit all V8 heaps combined to under 4 GiB—far too little for an architecture serving thousands of scripts at once.
Cloudflare therefore commissioned Igalia to introduce isolate groups to V8. Each isolate group has its own sandbox and can hold one or more isolates. Building on this, Workers can now adopt the sandbox and eliminate an entire class of potential vulnerabilities at once. While multiple isolates can share a sandbox, Workers currently place just one isolate per sandbox.

Sandbox layout. Multiple isolates may share one sandbox, but all heap pages reside in the pointer cage, the first 4 GiB. Objects use 32-bit offsets rather than pointers; buffer offsets are 33 bits, reaching the entire sandbox but never outside it.
Virtual memory constraints on Linux
Adopting the sandbox was not without further obstacles. Each sandbox reserves 8 GiB of virtual address space, aligned to 4 GiB for efficiency. The sandbox consumes little physical memory, but that virtual footprint is required for its security guarantees. A Linux process with a 4-level page table has only 128 TiB of usable user-space virtual memory (another 128 TiB is kernel-reserved).
Cloudflare maximizes Workers’ efficiency to control costs and offer a generous free tier—meaning many isolates (one per sandbox) run per machine, straining the 128 TiB budget.
Sandbox placement must therefore be optimized. The mmap syscall lacks an alignment flag, so to obtain an 8 GiB area that is 4 GiB aligned, Cloudflare requests 12 GiB, locates the 8 GiB aligned region within it, then returns the unused (hatched) edges:

Allowing the kernel to place sandboxes randomly yields gaps between them; over time, both 8 GiB and 4 GiB gaps appear:

Yet the 12 GiB alignment trick prevents reuse of even the 8 GiB gaps—the OS will not return a region composed of such a gap when asked for 12 GiB. Further, the virtual address space of a Linux process is crowded: malloc may require particular pages, the executable and libraries are randomized by ASLR, and V8 allocates outside the sandbox.
Newer x64 CPUs support far larger address spaces, eliminating both issues, and newer Linux kernels can exploit the extra bits via five-level page tables. Processes opt in via a single mmap call suggesting an address beyond the 47-bit region; the opt-in exists because some programs cannot cope with exceedingly high addresses—V8 included.
That fix is straightforward in V8, but not all of the fleet has the necessary hardware yet. In the interim, Cloudflare modified V8 to allocate huge memory regions and then use mprotect syscalls to carve out tightly packed 8 GiB sandbox spaces, bypassing the awkward mmap interface.

The role of guard regions
This control over sandbox placement yields a security benefit, but only under a specific threat model. We assume an attacker can corrupt arbitrary data inside a sandbox — a capability that is itself the first step in many V8 exploits, and one that Google acknowledges in its special V8 bug bounty tier, where researchers may assume this corruption ability and still receive a reward for escalating it further.
We do not assume the attacker can execute arbitrary machine code; that would allow them to disable memory protection keys. In-sandbox memory access only reaches the attacker's own data, so the attacker must escalate by corrupting in-sandbox data to reach memory outside the sandbox.
Within the sandbox, the V8 heap is compressed to 32-bit offsets, so corruption there cannot escape the pointer cage. But the sandbox also holds arrays — data vectors with recorded sizes and indexed access. Under this threat model, an attacker can modify both array sizes and indices, potentially turning an in-sandbox array into a tool for out-of-bounds access. To contain the worst case, the V8 sandbox normally relies on guard regions: 32 GiB of virtual address space with no physical mappings around the sandbox. The limit is derived from the maximum reach of an 8-byte-element array indexed with a maximal 32-bit index: 8 times 4 billion equals 32 GiB outside the sandbox.
Such accesses should trigger an alarm rather than reach nearby memory, which unmapped guard regions accomplish automatically. But Cloudflare lacks the address space for conventional 32 GiB guard regions around every sandbox.
Protection keys as guard regions
The alternative is to use memory protection keys where guard regions would go. By carefully assigning keys to isolate groups, we ensure no sandbox within a 32 GiB range shares the same protection key. The sandboxes become each other's guard regions, protected by distinct keys, and only the start and end of the packed sandbox area require wasted 32 GiB guard regions.

With this layout, keys rotate strictly rather than being randomly chosen, eliminating the 92% failure probability associated with random key selection. Any in-sandbox security issue cannot reach a sandbox with the matching key. No memory within 32 GiB of a given sandbox carries the same protection key, so any attempt to access that range triggers an alarm, just as an unmapped guard region would.
Looking ahead
This work is largely invisible to Cloudflare customers — they do not patch their own server software, tune configurations, or worry about the security or efficiency of their setup. There is no call to action beyond the peace of mind that comes with managed infrastructure.
That said, the work is ongoing, and Cloudflare is recruiting for roles in the US and Europe, particularly for engineers with experience in V8 internals or similar language runtimes.



