Medium CVSS 6.5 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentWebCore HTML
Bug ClassUAF
Tracker315161
Fix commit034f2fbd9b69 (WebKit/WebKit) +77/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedЮлия Мерцалова
Disclosed2026-06-29

Background

objectGraphLock()
The lock serializing mutations of a WebGL context’s object graph against the concurrent GC marker.
Concurrent GC marking
JSC marks live objects on a background thread; for WebGL it walks bound objects via addMembersToOpaqueRoots.
WebGL context loss/restoration
A context can be lost and later restored, which reinitializes default bound objects via initializeNewContext.
WTF_REQUIRES_LOCK
A Clang thread-safety annotation that makes the compiler require a given lock be held when calling a function.

Root Cause Analysis

WebGL rendering-context state that participates in the GC object graph is protected by objectGraphLock(); the concurrent GC marker traverses a context’s bound objects (via addMembersToOpaqueRoots) while holding that lock. During WebGL context restoration, WebGLRenderingContextBase::initializeNewContext re-initializes that state by calling initializeContextState() and initializeDefaultObjects() — which reset and recreate bound default objects — but it did so WITHOUT holding objectGraphLock(). That is a data race: restoration can free and replace objects that the GC marker is concurrently reading, a use-after-free.

The fix wraps both calls in Locker locker { objectGraphLock() }; inside initializeNewContext and annotates the virtual initializeContextState()/initializeDefaultObjects() overrides (WebGLRenderingContext, WebGL2RenderingContext, and the base) with WTF_REQUIRES_LOCK(objectGraphLock()), so the compiler enforces that they only run under the lock.

The restored invariant is that any mutation of the WebGL object graph — including the reinitialization performed on context restore — is serialized against concurrent GC marking by objectGraphLock(). The added layout test loses and restores a WebGL2 context while forcing concurrent GC marking to overlap the restore, which crashed under ASan pre-patch.

Key insight
WebGL context restoration reinitialized GC-visible context state without holding objectGraphLock(), racing the concurrent GC marker into a use-after-free; taking the lock (and enforcing it via annotations) serializes restore against marking.

Attack Path

  1. Create a WebGL2 context Allocate a canvas and get a webgl2 context so bound default objects exist in the GC object graph.
  2. Lose the context Use WEBGL_lose_context.loseContext() to drop the context and schedule a restore.
  3. Overlap restore with GC marking Trigger GC (e.g. allocate WebAssembly.Memory / heap padding) so its concurrent marking phase runs while restoreContext() reinitializes context state without the lock.
  4. Use-after-free Restoration frees/recreates objects the GC marker is still reading, corrupting memory in the WebContent process.

Impact Assessment

A use-after-free from a data race between WebGL context restoration and concurrent GC marking, in the WebContent process. The attacker drives both sides from script (lose/restore plus GC pressure), making the race reachably triggerable; the observed effect is an ASan-detected UAF/crash, with escalation to corruption possible given control over what reclaims the freed objects. Confined to WebContent (WebGL objects, though GPU-backed). Rated medium.

Changed Functions

FunctionChangeNotes
WebGLRenderingContextBase::initializeNewContext
Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
modified Now takes Locker { objectGraphLock() } around initializeContextState() and initializeDefaultObjects() so restoration is serialized against concurrent GC marking.
initializeContextState / initializeDefaultObjects (declarations)
Source/WebCore/html/canvas/WebGLRenderingContextBase.h
modified Marked WTF_REQUIRES_LOCK(objectGraphLock()); mirrored on WebGLRenderingContext.h and WebGL2RenderingContext.h overrides to enforce the lock at compile time.

Files Changed

  • LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt
  • LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html
  • Source/WebCore/html/canvas/WebGL2RenderingContext.h
  • Source/WebCore/html/canvas/WebGLRenderingContext.h
  • Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
  • Source/WebCore/html/canvas/WebGLRenderingContextBase.h

Audit Directions

  • Other unlocked object-graph mutations
    grep WebGLRenderingContext for methods that add/remove/reset bound objects (addContextObject, detachAndRemoveAllObjects, initialize*) called without a Locker{objectGraphLock()}.
  • Restore/reset paths
    Audit context-loss/restore and reset flows for state teardown/rebuild that must be serialized against GC marking.
  • Missing REQUIRES_LOCK annotations
    Find object-graph-touching virtuals lacking WTF_REQUIRES_LOCK(objectGraphLock()); the annotation would surface other unlocked callers.
diff --git a/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt
new file mode 100644
index 000000000000..c2541f4f3dd7
--- /dev/null
+++ b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html
new file mode 100644
index 000000000000..a62c55af8994
--- /dev/null
+++ b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<body>
+<!-- Exercises a race between WebGL2 context restoration (which reinitializes
+     bound-object state via initializeNewContext) and concurrent GC marking (which
+     traverses that state via addMembersToOpaqueRoots). Without objectGraphLock()
+     held during restoration, the restore path can free objects the GC marker is
+     still reading, causing a use-after-free. -->
+<script>
+if (window.testRunner) {
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+}
+if (window.internals)
+    internals.settings.setWebGLErrorsToConsoleEnabled(false);
+
+const tick = () => new Promise(r => setTimeout(r, 0));
+
+// Ensure the GC marker doesn't reach the WebGL wrapper until the concurrent phase.
+const heapPaddingSize = 300000;
+const heapPadding = new Array(heapPaddingSize);
+for (let i = 0; i < heapPaddingSize; i++)
+    heapPadding[i] = { a: i, b: { c: i } };
+
+async function loseAndRestoreContext() {
+    const canvas = document.body.appendChild(document.createElement('canvas'));
+    canvas.width = 1;
+    canvas.height = 1;
+    const gl = canvas.getContext('webgl2');
+    const ext = gl.getExtension('WEBGL_lose_context');
+    for (let i = 0; i < heapPaddingSize; i += 4096)
+        heapPadding[i].g = gl;
+
+    const contextLost = new Promise(r => {
+        canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); r(); });
+    });
+    const contextRestored = new Promise(r => {
+        canvas.addEventListener('webglcontextrestored', () => r());
+    });
+
+    ext.loseContext();
+    await contextLost;
+    await tick();
+
+    // Trigger GC so its concurrent marking phase overlaps the restore timer.
+    new WebAssembly.Memory({ initial: 1024 });
+    new WebAssembly.Memory({ initial: 1024 });
+    ext.restoreContext();
+    await tick();
+    await tick();
+    await contextRestored;
+
+    canvas.remove();
+}
+
+async function runTest() {
+    for (let i = 0; i < 50; i++)
+        await loseAndRestoreContext();
+    document.body.textContent = 'PASS if no crash.';
+    if (window.testRunner)
+        testRunner.notifyDone();
+}
+runTest();
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/html/canvas/WebGL2RenderingContext.h b/Source/WebCore/html/canvas/WebGL2RenderingContext.h
index dcf736d39056..b691cd7a4c8b 100644
--- a/Source/WebCore/html/canvas/WebGL2RenderingContext.h
+++ b/Source/WebCore/html/canvas/WebGL2RenderingContext.h
@@ -261,7 +261,7 @@ class WebGL2RenderingContext final : public WebGLRenderingContextBase {
 
 private:
     using WebGLRenderingContextBase::WebGLRenderingContextBase;
-    void initializeContextState() final;
+    void initializeContextState() WTF_REQUIRES_LOCK(objectGraphLock()) final;
 
     RefPtr<ArrayBufferView> arrayBufferViewSliceFactory(ASCIILiteral functionName, const ArrayBufferView& data, unsigned startByte, unsigned bytelength);
     RefPtr<ArrayBufferView> sliceArrayBufferView(ASCIILiteral functionName, const ArrayBufferView& data, GCGLuint srcOffset, GCGLuint length);
@@ -269,7 +269,7 @@ class WebGL2RenderingContext final : public WebGLRenderingContextBase {
     long long getInt64Parameter(GCGLenum) final;
     Vector<bool> getIndexedBooleanArrayParameter(GCGLenum pname, GCGLuint index);
 
-    void initializeDefaultObjects() final;
+    void initializeDefaultObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
     void detachAndRemoveAllObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
     bool validateBufferTarget(ASCIILiteral functionName, GCGLenum target) final;
     bool validateBufferTargetCompatibility(ASCIILiteral, GCGLenum, WebGLBuffer*);
diff --git a/Source/WebCore/html/canvas/WebGLRenderingContext.h b/Source/WebCore/html/canvas/WebGLRenderingContext.h
index 7404a87eb9a4..a9b2f1b3e9de 100644
--- a/Source/WebCore/html/canvas/WebGLRenderingContext.h
+++ b/Source/WebCore/html/canvas/WebGLRenderingContext.h
@@ -60,7 +60,7 @@ class WebGLRenderingContext final : public WebGLRenderingContextBase {
 
 private:
     using WebGLRenderingContextBase::WebGLRenderingContextBase;
-    void initializeDefaultObjects() final;
+    void initializeDefaultObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
     void detachAndRemoveAllObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
 };
 
diff --git a/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp b/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
index f128a2833781..2216b3ef9011 100644
--- a/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
+++ b/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
@@ -524,8 +524,11 @@ void WebGLRenderingContextBase::initializeNewContext(Ref<GraphicsContextGL> cont
     updateActiveOrdinal();
     if (!wasActive)
         addActiveContext(*this);
-    initializeContextState();
-    initializeDefaultObjects();
+    {
+        Locker locker { objectGraphLock() };
+        initializeContextState();
+        initializeDefaultObjects();
+    }
     // Next calls will receive the context lost callback.
     m_context->setClient(this);
 }
diff --git a/Source/WebCore/html/canvas/WebGLRenderingContextBase.h b/Source/WebCore/html/canvas/WebGLRenderingContextBase.h
index 57fc93560676..9313e4ff0bca 100644
--- a/Source/WebCore/html/canvas/WebGLRenderingContextBase.h
+++ b/Source/WebCore/html/canvas/WebGLRenderingContextBase.h
@@ -538,8 +538,8 @@ class WebGLRenderingContextBase : public GraphicsContextGL::Client, public GPUBa
     friend class ScopedWebGLRestoreTexture;
 
     void initializeNewContext(Ref<GraphicsContextGL>);
-    virtual void initializeContextState();
-    virtual void initializeDefaultObjects();
+    virtual void initializeContextState() WTF_REQUIRES_LOCK(objectGraphLock());
+    virtual void initializeDefaultObjects() WTF_REQUIRES_LOCK(objectGraphLock());
     virtual void detachAndRemoveAllObjects() WTF_REQUIRES_LOCK(objectGraphLock());
 
     // ActiveDOMObject
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.