Attacking Browsers via WebGPU
Lukas Bernhard (Security Researcher · Independent / Google Bug Bounty)
OffensiveCon 2025 · Day 1 · Main · Briefings
Overview
Lukas Bernhard built a grammar-based fuzzer for WebGPU's shading language (WGSL) and aimed it at the shader compilers lurking inside Chrome's GPU process — components never designed to withstand adversarial inputs. The campaign turned up 21 bugs across Windows's DirectX Shader Compiler and Android's Qualcomm Adreno and Arm Mali stacks, most of them use-after-free vulnerabilities rooted in outdated LLVM versions that GPU vendors have never bothered to update. ---

Key moments
- 2:59 GPU process fully unsandboxed on Android — one-click attacker reach
- 7:18 Android GPU shader compilers shipped as binary-only closed-source blobs
- 8:38 Typed IR fuzzer preserves WGSL semantics to reach vulnerable backends
- 12:25 DirectX Shader Compiler based on decade-old LLVM 3.7 fork
- 13:05 First DXC crash found during initial fuzzer development
- 14:00 Struct self-assignment (s=s) triggers double-free via identical memcpy source/dest pointers
- 16:01 DXC obliterated by million-CPU-hour campaign: 21 bugs total
- 22:51 Chrome mitigated only 1 of 4 Android GPU bugs
Attacking Browsers via WebGPU
Speaker: Lukas Bernhard (Independent / Google Bug Bounty)
Conference: OffensiveCon 2025 — May 16-17, 2025, Berlin
YouTube: https://www.youtube.com/watch?v=8Uv6KdATePc
Reading time: ~8 minutes
TL;DR
Lukas Bernhard built a grammar-based fuzzer for WebGPU's shading language (WGSL) and aimed it at the shader compilers lurking inside Chrome's GPU process — components never designed to withstand adversarial inputs. The campaign turned up 21 bugs across Windows's DirectX Shader Compiler and Android's Qualcomm Adreno and Arm Mali stacks, most of them use-after-free vulnerabilities rooted in outdated LLVM versions that GPU vendors have never bothered to update.
Introduction
WebGPU is rapidly displacing WebGL as the standard for hardware-accelerated graphics and compute on the web, and its adoption introduces an underexplored attack surface: shader compilers. When a web page submits a WebGPU shader program, Chrome's architecture means that program travels from the heavily sandboxed renderer process into the GPU process — a process that is only weakly sandboxed on Windows and entirely unsandboxed on Android — where it triggers a complex, multi-stage compilation pipeline involving third-party drivers that were never hardened for security. Bernhard's talk at OffensiveCon 2025 is the first systematic fuzzing study of this pipeline, and the results expose a structural problem that Chrome alone cannot fix.
Chrome's WebGPU Architecture and the Attack Surface
▶ Watch: Chrome's multi-process WebGPU architecture (2:00)
Chrome implements WebGPU through a component called Dawn, split into a Dawn Client running in the renderer process and a Dawn Server running in the GPU process. When web content makes any WebGPU API call, the Dawn Client serializes the request over IPC and hands it to the Dawn Server for all substantive processing. On Android, the GPU process has no sandbox at all; on Windows, it is only weakly sandboxed.
The critical data type passing through this boundary is the WGSL (WebGPU Shading Language) shader program. The Dawn Server translates WGSL into SPIR-V (for Vulkan/Android) or HLSL (for Direct3D/Windows) before passing it to the user-mode graphics driver. Behind the driver interface sits a further optimizing compiler that translates SPIR-V or HLSL into the GPU's native instruction set — one written entirely for performance, with no security hardening whatsoever.
The key insight Bernhard emphasizes: web content can reach these optimizing compilers with a single click, yet those compilers were designed as offline development tools, not as components of a one-click attack surface.
Designing a Semantics-Aware Shader Fuzzer
▶ Watch: Fuzzing pipeline and intermediate representation (8:00)
Naïve mutation strategies — bit-flipping, byte substitution — fail immediately when applied to WGSL because the language is statically typed and syntactically strict. Any mutation that breaks type constraints causes the front-end translator Tint to reject the shader, meaning the fuzzer never exercises the interesting back-end compilation paths.
Bernhard's solution is an intermediate representation (IR) that mirrors WGSL's static type system. The fuzzer:
- Parses corpus shaders (sourced from Chrome's own test repository) into the typed IR.
- Applies semantics-preserving mutations: literal substitution, type-safe expression replacement, code generation, and splicing between corpus entries.
- Regenerates valid WGSL from the mutated IR.
- Feeds the WGSL through Tint's translation step and into the target back-end compiler.
- Collects coverage feedback from the back-end.
Because the intermediate representation enforces type correctness throughout, every generated shader is syntactically and type-theoretically valid — the fuzzer wastes no compute on parsing failures and continuously reaches the optimizing compilation passes where the real bugs live. On Android, where the compiler is shipped as a binary-only shared object (no source, no symbols), Bernhard implemented in-memory ARM64 binary rewriting to inject coverage instrumentation by identifying conditional branches and stubbing in edge-tracking callbacks.
DirectX Shader Compiler: An Ancient LLVM Fork
▶ Watch: DirectX Shader Compiler first crash (12:00)
The Windows back-end target is the DirectX Shader Compiler (DXC), a compiler Microsoft based on LLVM 3.7 — a version approximately ten years old at the time of the research. DXC is open source, which made it straightforward to build with AddressSanitizer and libFuzzer instrumentation.
The first crash appeared within minutes of running the fuzzer on a laptop, triggered by a deceptively trivial shader: a function containing a single s = s struct self-assignment. LLVM's optimizer lowered this to a memcpy(s, s, sizeof(s)) where source and destination are identical. A subsequent dead-code elimination pass correctly identified the memcpy as a no-op and removed it — but the compiler's internal cleanup phase then attempted to free both the source and destination operands sequentially. Since they were the same pointer, this produced a double free, manifesting at runtime as a use-after-free.
Critically, these bugs are not protected by Chrome's MiraclePtr mitigation because DXC is external to the Chromium codebase; MiraclePtr only covers Chrome's own heap allocations.
Bernhard scaled the fuzzing campaign to approximately 1,000 cores running for one month, investing roughly one million CPU-hours. The result: the DirectX shader compiler was, in his words, "obliterated" — yielding the majority of the 21 bugs discovered overall. Many of the root causes trace to LLVM 3.7 bugs that were fixed in later LLVM versions but never backported by Microsoft into DXC's frozen fork.
Android: Binary-Only Targets and Novel Oracle Techniques
▶ Watch: Android targets and instrumentation approach (14:00+)
On Android, the situation is structurally different. GPU vendors ship shader compilers as pre-compiled shared libraries with no source code or debug symbols available. The research targeted both the Arm Mali GPU stack and the Qualcomm Adreno GPU stack on Pixel devices, finding four issues total (two per stack).
Detecting crashes without source-level instrumentation required two distinct oracle strategies:
- Mali/Pixel: Pixel devices enable MTE (Memory Tagging Extension) in hardware, which provides an AddressSanitizer-equivalent crash signal even in closed-source code. This gave a heap-only bug oracle essentially for free.
- Adreno/Qualcomm: Without MTE support on these devices, Bernhard used page-end allocation combined with poison pages: allocations placed at page boundaries so any out-of-bounds access immediately faults, and freed memory is overwritten with poison bytes to detect use-after-free on subsequent access.
A use-after-free found in the Mali stack was traced to a bug that had already been fixed in upstream LLVM but never backported by Arm into the GPU driver binary they ship to device manufacturers.
The Patch Distribution Problem
▶ Watch: Defense challenges and patch distribution (20:00+)
Chrome's ability to mitigate these vulnerabilities is severely limited. Of the four Android issues found, Chrome's workarounds succeeded in only one of four cases; the remaining three are unmitigated at the browser level. The fundamental problem is that shader compiler bugs live in GPU driver code that Chrome does not control and cannot update. Fixes must propagate from the research disclosure to GPU vendors (Arm, Qualcomm) to OEM device manufacturers to end-user Android updates — a supply chain with many points of delay or failure.
The Adreno and Mali compiler binaries are old LLVM forks, with Qualcomm's binary containing approximately 500,000 conditional branches (~1 million instrumented edges). The sheer size and age of these components, combined with the near-impossibility of hardening them after the fact, represents a structural risk that WebGPU has imported into the browser's attack surface.
Notable Quotes
"This compiler has mostly been optimized for performance and has not been designed with this idea in mind: 'Hey, one day this will be part of the one-click surface.' Yet here we are." — Lukas Bernhard, ▶ (6:00)
"You might wonder: how is it even possible that such a simple shader manages to trigger interesting behavior in this LLVM-plus-Microsoft-extensions compiler known as DirectX?" — Lukas Bernhard, ▶ (12:00)
"Chrome's workarounds were successful in one out of four cases. Three out of four are unmitigated." — Lukas Bernhard, ▶ (20:00+)
Key Takeaways
- 21 bugs discovered across the Windows DirectX Shader Compiler and Android Adreno/Mali stacks using a single grammar-aware WGSL fuzzer campaign run at ~1 million CPU-hours.
- Grammar-based fuzzing is essential for statically typed shader languages: naïve byte-level mutations generate only parse failures and never reach the vulnerable back-end compiler passes.
- Outdated LLVM forks in GPU drivers are a systemic root cause: both the DirectX Shader Compiler (based on LLVM 3.7) and Android GPU drivers run LLVM versions a decade behind upstream, carrying bugs long since fixed in trunk.
- Chrome cannot protect users unilaterally: GPU process sandbox weaknesses on Windows and Android, combined with the browser's inability to patch third-party driver code, leave many of these bugs unmitigated at the browser layer.
- MTE on Pixel devices provides a practical fuzzing oracle for closed-source GPU shader compilers on Android, enabling detection of heap memory corruption without source-level instrumentation.
Reviews
Dr. Zero (Offensive Security Researcher) — STRONG ACCEPT
Bernhard built a semantics-aware WGSL fuzzer, ran it at 1 million CPU-hours, and found 21 bugs across GPU shader compilers that are embedded in the Chrome one-click attack surface — compilers built on decade-old LLVM forks that have never been hardened for adversarial inputs. The structural problem he exposes (GPU drivers as unmitigatable browser attack surface) is a genuine systems-level finding, not just a bug count.
Heather Calloway (CISO) — SOLID
Lukas Bernhard found 21 bugs in WebGPU shader compilers — including LLVM forks inside Chrome that Chrome cannot unilaterally patch because the fix lives in GPU vendor driver stacks outside the browser. Three of four Android bugs remain unmitigated at time of presentation.