---
title: "CVE-2025-24201: How Web Content Reached an Unneeded GPU State"
description: "A reader-first reconstruction of Apple's CVE-2025-24201 WebKit fix: what the sandbox-escape advisory says, what primitive restart means, and what the public patch proves without inventing the private exploit chain."
date: 2026-08-23
analysisType: "Patch Analysis"
cves: ["CVE-2025-24201"]
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/research/apple-webkit-primitive-restart/
---

## CVE-2025-24201 in 60 seconds

Safari runs website code inside a restricted Web Content process. CVE-2025-24201 was an
out-of-bounds write that Apple says could let malicious web content break out of that sandbox. The
public WebKit patch removes one unnecessary capability from that process: a page-controlled WebGL
context could ask the graphics layer to enable or disable `PRIMITIVE_RESTART_FIXED_INDEX` and WebKit
forwarded the request to ANGLE.

The public path is:

```text
malicious web content
  -> issue a WebGL state-change request
  -> GraphicsContextGLANGLE forwards primitive-restart enable/disable
  -> ANGLE / graphics state is changed from the Web Content process
  -> Apple associates the bug with an out-of-bounds write and sandbox escape
```

The final arrow is an important evidence boundary. Apple's advisory confirms the vulnerability class
and impact, while the public patch confirms the capability that was removed. Neither public source
reveals the complete memory-corruption primitive or the original exploit chain. This article does not
pretend that twelve changed lines alone reconstruct those private details.

## What are Web Content, WebGL, ANGLE, and primitive restart?

The Web Content process parses and executes untrusted page content under sandbox restrictions.
WebGL gives that content a browser-mediated graphics API. WebKit's `GraphicsContextGLANGLE` is part
of the bridge to ANGLE, a graphics translation layer that ultimately manages lower-level GPU state.

Primitive restart is a rendering feature that lets a special index value end one geometry primitive
and begin another without a separate draw call. WebGL 2 enables fixed-index primitive restart by
default; WebGL 1 leaves it disabled. The WebKit commit says there was no use case for the Web Content
process to toggle that state. A page could request a transition that the browser already knew should
remain fixed for the context type.

## What happened?

Apple's Safari 18.3.1 advisory describes an out-of-bounds write fixed with improved checks and says
malicious web content could break out of the Web Content sandbox. It calls the release a
supplementary fix for an attack blocked in iOS 17.2 and notes a report of exploitation in an extremely
sophisticated attack against specific targeted individuals on earlier iOS versions.

WebKit commit `7d784721e4` changes `GraphicsContextGLANGLE::enable()` and `disable()`. Both now
intercept `PRIMITIVE_RESTART_FIXED_INDEX` and return before calling ANGLE; invalid state changes also
record `InvalidOperation`. The patch removes the unnecessary state-changing route rather than trying
to sanitize a collection of known inputs after they reach the graphics layer.

## Who was actually affected?

Safari 18.3.1 fixed the issue for Safari on macOS Ventura and Sonoma. Apple's exploitation note refers
specifically to targeted individuals on iOS versions before 17.2 and describes Safari 18.3.1 as a
supplementary fix. Those statements should not be collapsed into “every vulnerable Safari user was
attacked.” They establish a serious web-content entry point, a sandbox-escape impact, and evidence of
targeted use; device-specific exposure still depends on the platform release and update history.

## What I verified

- The commit changes one file and the two methods `GraphicsContextGLANGLE::enable()` and
  `GraphicsContextGLANGLE::disable()`.
- Both methods intercept `PRIMITIVE_RESTART_FIXED_INDEX` before forwarding the request to ANGLE.
- The patched branch returns immediately. On an invalid WebGL context it records
  `InvalidOperation` instead of changing graphics state.
- I matched the WebKit commit and Bugzilla identifier to Apple's Safari 18.3.1 advisory.
- This is a static patch reproduction. I verified the changed control flow, not the out-of-bounds
  write, the original exploit, or the reported targeted attacks.

## Deep dive: the public trail is unusually complete

Apple's advisory names WebKit Bugzilla issue `285858`. The corresponding WebKit commit is
`292004@main`, represented in Git as `7d784721e4`. Its subject is direct: a WebGL context
could toggle primitive-restart state from the WebContent process.

The change touches one file and two methods:

- `GraphicsContextGLANGLE::enable`
- `GraphicsContextGLANGLE::disable`

Ten lines are added and two blank lines are removed. No allocator is replaced. No large
parser is rewritten. The fix stops one capability from being passed to the graphics layer.

<figure class="diagram">
<svg viewBox="0 0 700 272" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Before the fix, WebContent could send a primitive restart state change through GraphicsContextGLANGLE into ANGLE. After the fix, WebKit intercepts that capability and returns before the graphics call.">
<defs>
<marker id="aw-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker>
</defs>
<text x="0" y="24" class="dg-crit">BEFORE</text>
<rect x="0" y="42" width="164" height="66" rx="8" class="dg-box" />
<text x="16" y="66" class="dg-label">WebContent</text>
<text x="16" y="86" class="dg-muted">page-controlled WebGL</text>
<line x1="166" y1="75" x2="218" y2="75" class="dg-line dg-crit-line" marker-end="url(#aw-a)" />
<rect x="222" y="42" width="216" height="66" rx="8" class="dg-box" />
<text x="238" y="66" class="dg-label">GraphicsContextGLANGLE</text>
<text x="238" y="86" class="dg-crit">forwards the state change</text>
<line x1="440" y1="75" x2="492" y2="75" class="dg-line dg-crit-line" marker-end="url(#aw-a)" />
<rect x="496" y="42" width="164" height="66" rx="8" class="dg-box-crit" />
<text x="512" y="66" class="dg-label">ANGLE / GPU</text>
<text x="512" y="86" class="dg-muted">graphics state changes</text>

<line x1="0" y1="136" x2="700" y2="136" class="dg-line dg-dash" />
<text x="0" y="170" class="dg-accent">AFTER</text>
<rect x="0" y="188" width="164" height="66" rx="8" class="dg-box" />
<text x="16" y="212" class="dg-label">WebContent</text>
<text x="16" y="232" class="dg-muted">same public API call</text>
<line x1="166" y1="221" x2="218" y2="221" class="dg-line" marker-end="url(#aw-a)" />
<rect x="222" y="188" width="216" height="66" rx="8" class="dg-box-accent" />
<text x="238" y="212" class="dg-label">WebKit check</text>
<text x="238" y="232" class="dg-accent">reject or return here</text>
<line x1="440" y1="221" x2="492" y2="221" class="dg-line dg-dash" />
<rect x="496" y="188" width="164" height="66" rx="8" class="dg-box" />
<text x="512" y="212" class="dg-label">ANGLE / GPU</text>
<text x="512" y="232" class="dg-muted">call never arrives</text>
</svg>
<figcaption>The patch does not add a complicated mitigation. It removes an unnecessary state-changing message before that message reaches ANGLE.</figcaption>
</figure>

## Primitive restart is not supposed to be a page-controlled switch

Primitive restart is a graphics feature used while drawing indexed geometry. A special index
value ends the current primitive and starts another one without a separate draw call.

The important part of the commit message is not the rendering detail. It is the ownership of
the state:

- WebGL 2 expects primitive restart to be enabled.
- WebGL 1 expects it to be disabled.
- Web content has no legitimate reason to flip that setting.

Before the patch, the general `enable` and `disable` paths could still forward this capability
to ANGLE. After the patch, WebKit handles the capability locally and returns before calling
the underlying graphics function.

In simplified pseudocode, the new behavior is:

```text
when enable or disable receives PRIMITIVE_RESTART_FIXED_INDEX:
    report an invalid operation when the requested state conflicts with the WebGL version
    return without forwarding the request to ANGLE
otherwise:
    continue with the normal graphics call
```

That is a small code change with a large architectural meaning: a component processing
untrusted web content loses the ability to send a state transition that the graphics layer
never needed to accept from it.

## Reading the fix backwards

A patch is strongest when it lets us state a precise before-and-after condition.

**Before:** `enable` and `disable` were generic pass-through methods after making the graphics
context current. The primitive-restart capability could travel through the same path as
ordinary capabilities.

**After:** both methods recognize the primitive-restart capability, enforce the WebGL-version
rule, and stop processing it locally.

The vulnerability-research lesson is not “one missing `if` caused a sandbox escape.” The
public evidence does not prove that. The stronger and more accurate lesson is:

> A state-changing command controlled by web content crossed into a graphics subsystem even
> though the web-facing API had no valid reason to control that state.

That is the condition worth hunting elsewhere. Generic forwarding functions accumulate
exceptions over time. Every exception is a clue that the public API and the lower-level API
do not have identical authority.

## What the patch proves — and what it does not

The public sources support four conclusions:

1. Apple associated CVE-2025-24201 with an out-of-bounds write and possible Web Content
   sandbox escape.
2. Apple linked the CVE to WebKit Bugzilla issue `285858`.
3. The public fix for that issue blocks WebContent-controlled toggling of primitive restart.
4. The affected code sits in WebKit's ANGLE-backed graphics context.

They do **not** publish the original exploit, the corrupt object, the exact out-of-bounds
write primitive, or the rest of the reported attack chain. The patch is a root-cause clue,
not a complete exploit narrative.

This distinction matters. It would be easy to turn a twelve-line patch into an impressive
but unsupported story about GPU compromise. The responsible analysis stops at the edge of
the evidence.

## A safe way to reproduce the analysis

This research can be repeated without running exploit code or targeting a device:

1. Read Apple's Safari 18.3.1 advisory and record the CVE, impact, affected version, and
   WebKit Bugzilla number.
2. Resolve Bugzilla `285858` to WebKit commit `292004@main`.
3. Inspect the commit and confirm that only `GraphicsContextGLANGLE.cpp` changed.
4. Compare the old and new `enable` and `disable` paths.
5. Confirm that the patched paths return before the underlying graphics call for
   `PRIMITIVE_RESTART_FIXED_INDEX`.
6. Write down separately what the advisory proves, what the patch proves, and what neither
   source reveals.

This is patch analysis, not exploit validation. Reproducing the original vulnerability would
require an authorized lab, affected builds, reliable crash evidence, and an independently
verified impact demonstration.

## The next variant-hunting question

The most useful follow-up is not to imitate the unavailable exploit. It is to search WebKit
for other generic `enable`, `disable`, or state-forwarding paths where:

- web content can select a low-level capability;
- the higher-level web standard fixes that capability to one state;
- the graphics API exposes more authority than the web API requires; and
- the message crosses into a more privileged or separately isolated component.

The candidate pattern is simple: **a public API can ask for more state change than its own
contract permits**. Candidates found by that search remain hypotheses until tested on an
affected build with a safe negative control.

## Evidence matrix

| Signal                                                 | What it proves                                         | What it does not prove                  | My check                                                             |
| ------------------------------------------------------ | ------------------------------------------------------ | --------------------------------------- | -------------------------------------------------------------------- |
| Apple advisory links CVE-2025-24201 to Bugzilla 285858 | The vendor associates the public issue and fix record  | The exact exploit path                  | I matched the identifiers across Apple's advisory and WebKit history |
| Commit changes only the ANGLE graphics context         | The public fix is narrowly scoped                      | That no private fixes existed elsewhere | I inspected the complete public commit and its parent                |
| New branches return before the graphics call           | Primitive-restart toggles stop at WebKit               | The exact memory corruption mechanism   | I traced both patched control-flow paths                             |
| Apple describes a supplementary fix                    | The update strengthened an earlier blocked attack path | Which earlier mitigation blocked it     | I kept the earlier attack chain explicitly unknown                   |

## My conclusion

For users and defenders, the action is straightforward: run a Safari, iOS, iPadOS, macOS,
visionOS, or watchOS version containing Apple's fix. Patch analysis does not replace the
vendor update.

My conclusion is that the important change is not its line count but the authority it removes.
The patch deletes no capability a WebGL page needs; it prevents untrusted content from toggling a
lower-level graphics state that the web-facing contract already fixes to one value.

## Public sources

- [Apple: Safari 18.3.1 security content](https://support.apple.com/en-ca/122285)
- [WebKit commit 7d784721e4](https://github.com/WebKit/WebKit/commit/7d784721e440d04932945e2decb933720c4e0fc7)
- [Canonical WebKit commit 292004@main](https://commits.webkit.org/292004@main)