The Deathray: A WebGPU Shader That Freezes Any Mac

Yesterday auberon Lopez published the Deathray: ten lines of WebGPU that hang a Mac's WindowServer so hard the kernel watchdog reboots the machine. One click, all three major browsers, M-series Macs only. Apple reproduced it in July, then in August declared it has "no security implications." I can't freeze a Mac from this Linux box — but I can tear the weapon apart, compile it, and check exactly which guard Apple's 2023 fix class would put on it. The result is worse than the writeup suggests.

What the Deathray actually does

I pulled the source — one 4,171-byte HTML file. The whole attack is a dependency cycle between two shaders sharing one storage buffer:

// compute shader: non-advancing infinite loop
@group(0) @binding(0) var<storage, read_write> data : array<vec4f>;
@compute @workgroup_size(1) fn compute() {
    for(var i = 0u; i < 1;) {   // no i++ — spins forever
        data[i+1] = data[i];
    }
}

// vertex shader: reads the same buffer the compute shader hogs
@vertex fn vert(@builtin(vertex_index) vertexIdx: u32) -> @builtin(position) vec4f {
    return data[vertexIdx];
}

The compute shader never yields. The vertex shader can't proceed because the buffer is pinned. Every WebGPU submission queues behind them — including WindowServer's. The desktop stops painting; SSH still works; when the watchdog decides WindowServer is dead, the kernel panics. There's no exploit, no memory corruption, no privilege boundary crossed. It's a traffic jam, weaponized. That's also why Apple can call it "not a security issue" — and why the browser trust model breaks anyway.

I compiled the weapon on Linux

No M-series Mac here, so I did the next best thing: extracted both shaders from the page and ran them through naga 30.0.1, the WGSL validator/translator that sits in the wgpu stack most non-Apple browsers build on.

$ naga compute.wgsl && naga render.wgsl
Validation successful
Validation successful
$ time (naga compute.wgsl > /dev/null)
real    0m0.003s

Both shaders pass validation in 3 milliseconds. Naga's control-flow analysis does not flag the non-advancing loop — and it can't, because detecting "infinite" is the halting problem. The weapon requires zero sophistication: no obfuscation, no timing tricks, nothing to fingerprint reliably.

Then I translated the compute shader to Metal — the exact code path an Apple GPU would run — and found the receipts for why Apple's 2023 fix class doesn't save you:

// naga → Metal 2.3, excerpt
uint2 loop_bound = uint2(4294967295u);
while(true) {
    if (metal::all(loop_bound == uint2(0u))) { break; }
    loop_bound -= uint2(loop_bound.y == 0u, 1u);
    ...
}

Naga inserts a loop-bound guard of 4,294,967,295 iterations — the u32 maximum. That's the same mitigation philosophy Apple shipped for ShadyShader in CVE-2023-40441: cap runaway loops at some huge bound. 4.29 billion iterations of a buffer copy is not a mitigation, it's a timer. The GPU wedges for as long as it takes to burn four billion spins — minutes of a desktop that won't repaint, before anything recovers. The guard converts an infinite hang into a very long hang. Structurally, that's the best a static checker can do, which is exactly auberon's point: you cannot validate your way out of this; you need preemption.

Why only macOS dies

On Linux and Windows the same shaders just freeze the tab; close it, move on. The difference is who can yank a runaway GPU job off the die. On M-series, the kernel can't directly preempt the GPU — that logic lives in the ASC coprocessor firmware, as Asahi Lina's writeup details. Mesa and Windows drivers expose mid-command preemption to the compositor; Apple's stack apparently doesn't route untrusted shader preemption through the path that would save WindowServer. Same code, three browsers, one OS where a webpage outranks the desktop compositor.

graph TD
  A[Untrusted page] --> B[WebGPU compute: infinite loop]
  B --> C[GPU queue blocked]
  C --> D{Preemption available?}
  D -->|Linux / Windows| E[Driver preempts, tab dies, OS fine]
  D -->|macOS M-series| F[WindowServer starved]
  F --> G[Watchdog fires]
  G --> H[Kernel panic, forced reboot]
            

The disclosure record

Timeline from the post: reported to Apple Security 7/27/26, reproduced quickly, then on 8/26/26 Apple reversed — "no security implications," no product change, routed to "enhancement considerations." To be fair to Apple: researchers auberon consulted agree the crash/hang/data-loss triangle isn't a security boundary violation. To be fair to users: ShadyShader got a CVE and a 6.5 CVSS in 2023 for the same class, and the click-to-brick trust assumption — "a link can't hurt anything but its own tab" — is the one non-technical users actually hold. Apple's own fix precedent from 2023 argues against Apple's 2026 position. That's not consistency; that's a severity score that changed while the architecture didn't.

Bottom line: this is the cheapest DoS I've ever compiled — 26 lines, validates clean in 3 ms, and no static analysis can catch it because the "malice" is just the absence of an increment. If you're building anything that lets untrusted code submit GPU work on Apple silicon, assume a submission can hold the device indefinitely and design for it: per-page submission deadlines, watchdog kill of the submitting process, anything except hoping the shader terminates. Apple called this an enhancement. Your users' beachballs disagree.