Breaking the Browser: WASM and WebGPU Exploitation
11 min read
July 18, 2026

Table of contents
👋 Introduction
Hey everyone!
We just spent three issues in Web3. This week we go as low-level as it gets: memory corruption inside the browser, the single most-attacked piece of software on the planet.
A malicious web page has no credentials, no foothold, no user interaction beyond a click. It runs untrusted JavaScript in a locked-down process and that should be the end of it. Yet every year at Pwn2Own, researchers turn one visited page into code execution on the host. The browser is where the hardest offensive engineering meets the widest attack surface, and two features made that surface much larger: WebAssembly and WebGPU.
Here is the uncomfortable part for a web pentester. The bugs that break browsers are not the web bugs you know. No XSS, no injection. These are type confusions in a compiler, out-of-bounds writes in a GPU driver, and use-after-frees across process boundaries. Different discipline, same goal: arbitrary code on the target.
This week: why the browser is really three processes wearing a trench coat, how a JIT compiler that trusts its own type guesses hands you arbitrary read and write, how WebAssembly turns that into running shellcode, why WebGPU’s GPU process is a softer target, and how attackers chain these to walk out of the sandbox entirely.
Let’s get into it 👇
🧩 One Page, Three Processes
Before any browser exploit makes sense, you need the process model. Chromium does not run a web page in one program. It splits the work by trust. The renderer process runs the untrusted stuff, your JavaScript, WebAssembly, and DOM, inside a heavy sandbox: a restricted token, a job object, low integrity, no direct access to the filesystem or network. The GPU process handles graphics and driver code in a weaker sandbox. The browser process, also called the broker, holds the real operating-system handles and has full privilege.
The Chromium sandbox design exists so that compromising the renderer buys you almost nothing. You can run arbitrary code in that process and still be trapped, unable to touch a single file. That is the whole point.
So a real browser exploit is never one bug. It is a chain. Get code execution in the renderer, then find a second bug to cross into a more privileged process, then a third to reach the broker or the kernel. Every technique below is one link. Hold that model in your head and the rest of this issue is just filling in which link goes where.
⚡ The JIT That Trusts Too Much
You want arbitrary read and write inside the renderer. The way in is the part of the engine built for speed: the JIT compiler.
V8, Chrome’s JavaScript engine, watches which functions run often and recompiles them to native machine code. To go fast, its optimizing compiler (TurboFan) speculates about types. If it decides an array only ever holds small integers, it generates code that skips the bounds check, because it “knows” the index is safe. A type-confusion bug is when that guess is wrong. The compiler eliminated a bounds check it should have kept, and now a normal-looking array access reads or writes memory outside the array.
// Shape of a JIT typer bug: the optimizer mis-models the array's length
// after a crafted operation, then removes the bounds check on this access.
function leak(arr, i) {
// optimizer "proves" i is in range; it is not
return arr[i]; // out-of-bounds read past the array buffer
}
for (let k = 0; k < 100000; k++) leak(trusted, 0); // force JIT compilation
leak(trusted, 0x1000); // now read attacker-chosen memory
Andrea Biondo’s Math.expm1 writeup is the canonical case: TurboFan’s type system forgot that Math.expm1 can return negative zero, and that single omission cascaded into bounds-check elimination and a full out-of-bounds primitive. A modern version from Theori leaks an internal sentinel value called “the Hole” to poison the type inference the same way. From that out-of-bounds access, attackers build two tools: addrof (leak the address of any object) and fakeobj (craft a fake object at an address they control). Together those are arbitrary read and write. The lesson: the fastest code in the browser is the code that trusts its own assumptions, and assumptions are what you attack.
💀 WebAssembly as a Loaded Gun
Arbitrary read and write is not code execution. You still need to run instructions. For years, WebAssembly was the cleanest way to close that gap.
WebAssembly is a compact bytecode the browser compiles to native machine code so languages like C and Rust run at near-native speed. Two properties make it an exploit primitive. Its linear memory is one big contiguous buffer you fill with fully controlled bytes, perfect for staging data. And historically the JIT wrote compiled WASM into RWX pages, memory that is writable and executable at the same time. Normally a page should be write-XOR-execute: never both. An RWX page breaks that rule and hands you somewhere to drop shellcode.
// Compile a trivial module so V8 allocates an executable WASM page
const bytes = new Uint8Array([0x00,0x61,0x73,0x6d, /* ...minimal module... */]);
const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes));
// With arbitrary write, overwrite the jump-table entry for the exported
// function with your shellcode, then call it. NX never applies: the page
// was already executable.
arbWrite(jumpTableEntry(instance.exports.f), shellcode);
instance.exports.f(); // shellcode runs
Google Project Zero’s In-the-Wild series documents real campaigns that finished exactly this way, and the starctf oob-v8 walkthrough is the classic teaching version. One caveat that changed the game: V8 now ships a heap sandbox with sandboxed pointers, so corrupting an object no longer trivially gives process control. Modern chains still start in V8 but now need an extra escape. The gun still fires. It just has a safety now.
🧱 Grooming the Heap for Reliability
A memory bug that works one time in ten is a demo, not a weapon. Reliability comes from heap grooming: arranging allocations so the object you can corrupt lands right next to the object you want to control.
The move is to spray many equal-sized allocations filled with chosen bytes, so when a vulnerable object is freed, one of yours reclaims that exact slot. Typed arrays and WebAssembly linear memory are perfect for this, because their backing buffers are large, size-controlled, and hold whatever you write into them.
// Spray controlled buffers so a freed slot gets reclaimed with our bytes
const spray = [];
for (let i = 0; i < 0x1000; i++) {
const ab = new ArrayBuffer(0x1000);
new Float64Array(ab).fill(1.9999); // fully controlled contents
spray.push(ab);
}
This is why exploit writers obsess over allocator behavior. The bug gives you the corruption. Grooming decides whether it lands on memory you control, and that gap is the difference between a proof of concept and something that fires every time.
🎨 WebGPU: A Softer Target
Here is the counterintuitive part. After years of hardening V8, the more inviting target moved to the process next door, and you reach it by typing a program the browser compiles for you at runtime.
WebGPU gives a page direct low-level access to the GPU for both graphics and general compute. A shader is a small program that runs on the GPU, and WebGPU compiles your shader source, written in a language called WGSL, on the fly. When you call WebGPU from JavaScript, the request is serialized in the renderer, sent over a GPU command buffer, and deserialized inside the GPU process by Dawn, Chrome’s WebGPU implementation. That GPU process runs a weaker sandbox and loads closed-source vendor driver code.
// Attacker-controlled shader source, compiled at runtime in the GPU process
const module = device.createShaderModule({ code: maliciousWGSL });
const pipeline = device.createComputePipeline({ /* ... */ compute: { module } });
// Command buffers cross into the GPU process, where Dawn deserializes and
// the driver executes. Validation gaps here corrupt a privileged process.
Chrome’s own security team published a WebGPU technical report mapping the bug classes: command-buffer validation gaps where the server trusts a malformed command, use-after-free of GPU buffer and texture objects, WGSL shader-compiler bugs, and the driver interaction layer. Corrupting memory in the GPU process is worth far more than the renderer, because it sits closer to the kernel and runs code no browser vendor fully controls. You expanded the attack surface the day you let web pages compile shaders.
🚪 Walking Out: The Sandbox Escape
Now assemble the chain. Renderer code execution is a cage. The escape is the point.
The pattern looks like this. A JIT bug gives you arbitrary read and write in the renderer. That renderer RCE talks to the GPU process over the command buffer, crossing a trust boundary. A Dawn or driver bug corrupts that process. From there you reach the broker, and the broker owns full OS access. Theori’s sandbox escape writeup shows the final move in clean form: a use-after-free in a browser-process interface, reclaimed with sprayed data through the same heap grooming from earlier, hijacks a virtual call.
In Theori’s case the payload rewrites the browser’s stored command line to add --no-sandbox, then spawns a fresh renderer with no sandbox at all. Every guarantee from the first section, gone. That is why browser bugs are graded not by “can you run code” but by “how far did the chain reach.” One bug is a curiosity. A chain to the broker is a working weapon, and that is what commands six figures at Pwn2Own.
This is also why process sandboxing and Site Isolation matter as defenses. They do not stop the first bug. They force an attacker to find and chain every link, which is what turns a single vulnerability from an instant compromise into a research project.
🎯 Key Takeaways
The mental shift to carry out of this issue: browser exploitation is chain-building, not bug-finding. A single memory-corruption bug in the renderer runs code in a process that cannot touch your files. Value comes from stitching bugs across trust boundaries until you reach the broker. When you read a browser advisory, ask which process each bug lives in and how many boundaries the chain crossed. That tells you the real severity better than any label.
The engine’s fast paths are the attack surface. The JIT exists to skip work, and every check it skips on the strength of a type guess is a place a wrong guess becomes an out-of-bounds primitive. WebAssembly exists to run near-native code, and that same executable-memory machinery is what turns read-write into shellcode. Performance features and exploit primitives are often the same feature seen from two directions.
WebGPU changed the math. It handed every web page a compiler and a path into a weakly sandboxed process full of driver code no vendor audits. After a decade of hardening JavaScript, the softer target is now graphics. If you are assessing attack surface on a browser or an Electron app, WebGPU and the GPU process deserve as much attention as the JS engine, and far more than most teams give them.
For getting hands-on, do not start with a zero-day. Start with d8 and a known-vulnerable V8 build, build addrof and fakeobj by hand, and finish with the WebAssembly shellcode trick. Then read the WebGPU technical report and the V8 sandbox blog to understand why modern chains need that extra escape. If you can build the renderer primitive yourself, the advisories start reading like walkthroughs instead of magic.
Practice:
- pwn.college: V8 Exploitation - nine progressive challenges building addrof, fakeobj, and arbitrary read/write on patched V8 with d8
- starctf oob-v8 in depth (faraz.faith) - the classic step-by-step V8 exploitation walkthrough
- Exploiting the Math.expm1 typing bug in V8 (Andrea Biondo) - canonical JIT-to-WASM-shellcode chain
- WebGPU technical report (Chrome Security) - the primary source on Dawn and GPU-process bug classes
- The V8 Sandbox (v8.dev) - why corrupting a V8 object no longer gives process control
- Fuzzilli (Google Project Zero) - coverage-guided JavaScript engine fuzzer
- wabt: WebAssembly Binary Toolkit (WebAssembly org) - wat2wasm, wasm-objdump, and friends for crafting modules
- m1ghtym0/browser-pwn - curated browser exploitation resources across V8, SpiderMonkey, and JSC
Thanks for reading, and happy hunting!
— Ruben
Other Issues
Previous Issue
Next Issue
💬 Comments Available
Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.