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 Safari crash
ComponentWebCore
Sub-componentWebCore HTML
Bug ClassUAF
Tracker313175
Fix commit040ef6e21ffa (WebKit/WebKit) +122/-10
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
CreditedJosef Korbel, Gia Bui (@yabeow) from Calif.io
Disclosed2026-06-29

Background

Canvas rendering context (m_context)
A canvas lazily creates and caches one rendering context (2d/webgl/bitmap/webgpu) in m_context on the first getContext call.
WebIDL dictionary conversion
Converting a JS argument to a WebIDL dictionary reads its properties, which can invoke author-defined getters and thus run script mid-call.
Re-entrancy
Script run during argument conversion can call back into getContext on the same canvas before the outer call finishes.
ASSERT_WITH_SECURITY_IMPLICATION
A stronger assertion marking an invariant whose violation is security-relevant, enabled in security test/debug builds.

Root Cause Analysis

HTMLCanvasElement::getContext (and OffscreenCanvas::getContext) convert the JS arguments into a WebIDL settings/attributes dictionary before creating the rendering context, and that dictionary conversion can run author script through getters/valueOf on the supplied object. The pre-patch code checked m_context once at entry, performed the conversion, then unconditionally created a new context and assigned m_context = createContext...(). If script executed during the conversion re-entrantly called getContext() and set m_context, the outer call would overwrite m_context with a second, freshly-created context — dropping the context the inner call had already created (and possibly already returned to script), leaving dangling references to a now-freed context object: a use-after-free / memory corruption.

The fix factors the existing-context path into a getExistingContext lambda and, after every dictionary conversion that can run script, re-checks if (m_context) return getExistingContext(); and guards creation with if (!m_context), so a context established by re-entrancy is honored rather than clobbered. For OffscreenCanvas, the conversion can additionally detach the canvas, so a shouldThrowForDetachedCanvas() re-check is added after each conversion and creation is likewise guarded by if (!m_context). The ASSERT(!m_context) invariants in createContext2d/createContextWebGL/createContextBitmapRenderer/createContextWebGPU were strengthened to ASSERT_WITH_SECURITY_IMPLICATION(!m_context) to catch a double-create.

The restored invariant is that m_context is re-validated after any step that may run script, and a canvas creates exactly one rendering context exactly once.

Key insight
getContext cached m_context’s nullness across a WebIDL dictionary conversion that can run script and re-entrantly set it, then created a second context and clobbered the first — re-checking m_context (and detach state) after every script-running step closes it.

Attack Path

  1. Call getContext with a script hook Call canvas.getContext(type, settings) where settings is an object whose getter (or a coercible member) runs attacker script during WebIDL dictionary conversion.
  2. Re-enter getContext Inside the getter, call canvas.getContext(type, …) again; the inner call creates and assigns m_context and returns a context object to script.
  3. Clobber the context The outer call resumes after conversion and, pre-patch, creates a second context and overwrites m_context, dropping the inner context.
  4. Free and dangle The replaced context is destroyed (helped by gc()) while script still references it — use-after-free / memory corruption in WebContent.

Impact Assessment

A script-controlled re-entrancy UAF / double-create of a canvas rendering context. The attacker fully controls the re-entrant call and its timing via the settings-object getter, but the object and window are constrained, so the realistic outcome is a controlled crash with potential for memory corruption after heap grooming. Confined to the WebContent process (GPU-backed contexts are still brokered from WebContent). Rated medium (CVSS 6.5).

Changed Functions

FunctionChangeNotes
if
Source/WebCore/html/HTMLCanvasElement.cpp
modified
if
Source/WebCore/html/OffscreenCanvas.cpp
modified

Files Changed

  • LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
  • LayoutTests/fast/canvas/canvas-getContext-reentrant.html
  • Source/WebCore/html/HTMLCanvasElement.cpp
  • Source/WebCore/html/OffscreenCanvas.cpp

Audit Directions

  • Other getContext-style factories
    grep for a member read, then releaseReturnValue()/dictionary conversion, then an unconditional member assignment without re-checking the member — the exact clobber shape fixed here.
  • Assumptions across argument coercion
    Audit DOM methods that assume a member stays null/unchanged across convert<IDLDictionary>/argument coercion, since coercion can run script and re-enter.
  • OffscreenCanvas detach/transfer
    Check transferToImageBitmap and other OffscreenCanvas paths for state (m_detached/m_context) that can change via script during conversion.
diff --git a/Source/WebCore/html/HTMLCanvasElement.cpp b/Source/WebCore/html/HTMLCanvasElement.cpp
index c280824242a4..2bc4b33962b8 100644
--- a/Source/WebCore/html/HTMLCanvasElement.cpp
+++ b/Source/WebCore/html/HTMLCanvasElement.cpp
@@ -226,7 +226,8 @@ void HTMLCanvasElement::setSizeForControllingContext(IntSize newSize)
 
 ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::JSGlobalObject& state, const String& contextId, FixedVector<JSC::Strong<JSC::Unknown>>&& arguments)
 {
-    if (m_context) {
+    auto getExistingContext = [&]() -> ExceptionOr<std::optional<RenderingContext>> {
+        ASSERT(m_context);
         if (m_context->isPlaceholder())
             return Exception { ExceptionCode::InvalidStateError };
 
@@ -263,7 +264,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
 
         ASSERT_NOT_REACHED();
         return std::optional<RenderingContext> { std::nullopt };
-    }
+    };
+
+    if (m_context)
+        return getExistingContext();
 
     if (is2dType(contextId)) {
         Ref vm = state.vm();
@@ -273,6 +277,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
         if (settings.hasException(scope)) [[unlikely]]
             return Exception { ExceptionCode::ExistingExceptionError };
 
+        // Dictionary conversion may run script, which may have set m_context via a re-entrant getContext().
+        if (m_context) [[unlikely]]
+            return getExistingContext();
+
         RefPtr context = createContext2d(contextId, settings.releaseReturnValue());
         if (!context)
             return std::optional<RenderingContext> { std::nullopt };
@@ -287,6 +295,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
         if (settings.hasException(scope)) [[unlikely]]
             return Exception { ExceptionCode::ExistingExceptionError };
 
+        // Dictionary conversion may run script, which may have set m_context via a re-entrant getContext().
+        if (m_context) [[unlikely]]
+            return getExistingContext();
+
         RefPtr context = createContextBitmapRenderer(contextId, settings.releaseReturnValue());
         if (!context)
             return std::optional<RenderingContext> { std::nullopt };
@@ -302,6 +314,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
         if (attributes.hasException(scope)) [[unlikely]]
             return Exception { ExceptionCode::ExistingExceptionError };
 
+        // Dictionary conversion may run script, which may have set m_context via a re-entrant getContext().
+        if (m_context) [[unlikely]]
+            return getExistingContext();
+
         RefPtr context = createContextWebGL(toWebGLVersion(contextId), attributes.releaseReturnValue());
         if (!context)
             return std::optional<RenderingContext> { std::nullopt };
@@ -355,7 +371,7 @@ bool HTMLCanvasElement::is2dType(const String& type)
 CanvasRenderingContext2D* HTMLCanvasElement::createContext2d(const String& type, CanvasRenderingContext2DSettings&& settings)
 {
     ASSERT_UNUSED(HTMLCanvasElement::is2dType(type), type);
-    ASSERT(!m_context);
+    ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
 
     m_context = CanvasRenderingContext2D::create(*this, WTF::move(settings), document().inQuirksMode());
     if (!m_context)
@@ -403,7 +419,7 @@ WebGLVersion HTMLCanvasElement::toWebGLVersion(const String& type)
 
 WebGLRenderingContextBase* HTMLCanvasElement::createContextWebGL(WebGLVersion type, WebGLContextAttributes&& attrs)
 {
-    ASSERT(!m_context);
+    ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
     if (!document().settings().webGLEnabled())
         return nullptr;
 
@@ -459,7 +475,7 @@ bool HTMLCanvasElement::isBitmapRendererType(const String& type)
 ImageBitmapRenderingContext* HTMLCanvasElement::createContextBitmapRenderer(const String& type, ImageBitmapRenderingContextSettings&& settings)
 {
     ASSERT_UNUSED(type, HTMLCanvasElement::isBitmapRendererType(type));
-    ASSERT(!m_context);
+    ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
 
     auto context = ImageBitmapRenderingContext::create(*this, WTF::move(settings));
     WeakPtr weakContext = *context;
@@ -491,7 +507,7 @@ bool HTMLCanvasElement::isWebGPUType(const String& type)
 GPUCanvasContext* HTMLCanvasElement::createContextWebGPU(const String& type, GPU* gpu)
 {
     ASSERT_UNUSED(type, HTMLCanvasElement::isWebGPUType(type));
-    ASSERT(!m_context);
+    ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
 
     if (!document().settings().webGPUEnabled() || !gpu)
         return nullptr;
diff --git a/Source/WebCore/html/OffscreenCanvas.cpp b/Source/WebCore/html/OffscreenCanvas.cpp
index f67caa13c15d..5adbd49dc2d5 100644
--- a/Source/WebCore/html/OffscreenCanvas.cpp
+++ b/Source/WebCore/html/OffscreenCanvas.cpp
@@ -183,6 +183,13 @@ ExceptionOr<std::optional<OffscreenRenderingContext>> OffscreenCanvas::getContex
     if (m_detached)
         return Exception { ExceptionCode::InvalidStateError };
 
+    // Dictionary conversion may run script, which may have detached this canvas.
+    auto shouldThrowForDetachedCanvas = [&]() -> ExceptionOr<void> {
+        if (m_detached) [[unlikely]]
+            return Exception { ExceptionCode::InvalidStateError };
+        return { };
+    };
+
     if (contextType == RenderingContextType::_2d) {
         if (!m_context) {
             auto scope = DECLARE_THROW_SCOPE(state.vm());
@@ -191,7 +198,10 @@ ExceptionOr<std::optional<OffscreenRenderingContext>> OffscreenCanvas::getContex
             if (settings.hasException(scope)) [[unlikely]]
                 return Exception { ExceptionCode::ExistingExceptionError };
 
-            m_context = OffscreenCanvasRenderingContext2D::create(*this, settings.releaseReturnValue());
+            if (auto result = shouldThrowForDetachedCanvas(); result.hasException())
+                return result.releaseException();
+            if (!m_context)
+                m_context = OffscreenCanvasRenderingContext2D::create(*this, settings.releaseReturnValue());
         }
         if (RefPtr context = dynamicDowncast<OffscreenCanvasRenderingContext2D>(m_context.get()))
             return { { context.releaseNonNull() } };
@@ -205,8 +215,12 @@ ExceptionOr<std::optional<OffscreenRenderingContext>> OffscreenCanvas::getContex
             if (settings.hasException(scope)) [[unlikely]]
                 return Exception { ExceptionCode::ExistingExceptionError };
 
-            m_context = ImageBitmapRenderingContext::create(*this, settings.releaseReturnValue());
-            downcast<ImageBitmapRenderingContext>(m_context.get())->transferFromImageBitmap(nullptr);
+            if (auto result = shouldThrowForDetachedCanvas(); result.hasException())
+                return result.releaseException();
+            if (!m_context) {
+                m_context = ImageBitmapRenderingContext::create(*this, settings.releaseReturnValue());
+                downcast<ImageBitmapRenderingContext>(m_context.get())->transferFromImageBitmap(nullptr);
+            }
         }
         if (RefPtr context = dynamicDowncast<ImageBitmapRenderingContext>(m_context.get()))
             return { { context.releaseNonNull() } };
@@ -243,10 +257,12 @@ ExceptionOr<std::optional<OffscreenRenderingContext>> OffscreenCanvas::getContex
             if (attributes.hasException(scope)) [[unlikely]]
                 return Exception { ExceptionCode::ExistingExceptionError };
 
+            if (auto result = shouldThrowForDetachedCanvas(); result.hasException())
+                return result.releaseException();
             RefPtr scriptExecutionContext = this->scriptExecutionContext();
             if (scriptExecutionContext) {
                 auto& settings = scriptExecutionContext->settingsValues();
-                if (settings.webGLEnabled && (!is<WorkerGlobalScope>(scriptExecutionContext) || settings.allowWebGLInWorkers))
+                if (!m_context && settings.webGLEnabled && (!is<WorkerGlobalScope>(scriptExecutionContext) || settings.allowWebGLInWorkers))
                     m_context = WebGLRenderingContextBase::create(*this, attributes.releaseReturnValue(), webGLVersion);
             }
         }
diff --git a/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt b/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
new file mode 100644
index 000000000000..d801ccb9fae7
--- /dev/null
+++ b/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
@@ -0,0 +1,33 @@
+getContext("2d") with reentrant getContext("2d"): outer=CanvasRenderingContext2D inner=CanvasRenderingContext2D
+getContext("2d") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("2d") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("2d") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("bitmaprenderer") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("bitmaprenderer") with reentrant getContext("bitmaprenderer"): outer=ImageBitmapRenderingContext inner=ImageBitmapRenderingContext
+getContext("bitmaprenderer") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("bitmaprenderer") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("webgl") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("webgl") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("webgl") with reentrant getContext("webgl"): outer=WebGLRenderingContext inner=WebGLRenderingContext
+getContext("webgl") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("webgl2") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("webgl2") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("webgl2") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("webgl2") with reentrant getContext("webgl2"): outer=WebGL2RenderingContext inner=WebGL2RenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("2d"): outer=OffscreenCanvasRenderingContext2D inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("2d") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("bitmaprenderer"): outer=ImageBitmapRenderingContext inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("webgl") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("webgl"): outer=WebGLRenderingContext inner=WebGLRenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("webgl2") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("webgl2"): outer=WebGL2RenderingContext inner=WebGL2RenderingContext
+PASS if no crash.
diff --git a/LayoutTests/fast/canvas/canvas-getContext-reentrant.html b/LayoutTests/fast/canvas/canvas-getContext-reentrant.html
new file mode 100644
index 000000000000..e46e9d41f37d
--- /dev/null
+++ b/LayoutTests/fast/canvas/canvas-getContext-reentrant.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner)
+    testRunner.dumpAsText();
+
+function gc() {
+    if (window.GCController)
+        return GCController.collect();
+    for (let i = 0; i < 10000; i++)
+        new String("abc");
+}
+
+function reenter(outer, inner) {
+    let canvas = document.createElement("canvas");
+    let innerCtx;
+    let outerCtx = canvas.getContext(outer, {
+        get colorSpace() { innerCtx = canvas.getContext(inner); return "srgb"; },
+        get alpha() { innerCtx = canvas.getContext(inner); return true; },
+    });
+    document.write(`getContext("${outer}") with reentrant getContext("${inner}"): outer=${name(outerCtx)} inner=${name(innerCtx)}<br>`);
+}
+
+function reenterOffscreen(outer, inner) {
+    let canvas = new OffscreenCanvas(10, 10);
+    let innerCtx;
+    let outerCtx = canvas.getContext(outer, {
+        get colorSpace() { innerCtx = canvas.getContext(inner); return "srgb"; },
+        get alpha() { innerCtx = canvas.getContext(inner); return true; },
+    });
+    document.write(`OffscreenCanvas getContext("${outer}") with reentrant getContext("${inner}"): outer=${name(outerCtx)} inner=${name(innerCtx)}<br>`);
+}
+
+function name(ctx) {
+    return ctx ? ctx.constructor.name : "null";
+}
+
+for (let outer of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+    for (let inner of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+        reenter(outer, inner);
+
+for (let outer of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+    for (let inner of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+        reenterOffscreen(outer, inner);
+
+gc();
+document.write("PASS if no crash.");
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt b/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
new file mode 100644
index 000000000000..d801ccb9fae7
--- /dev/null
+++ b/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
@@ -0,0 +1,33 @@
+getContext("2d") with reentrant getContext("2d"): outer=CanvasRenderingContext2D inner=CanvasRenderingContext2D
+getContext("2d") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("2d") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("2d") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("bitmaprenderer") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("bitmaprenderer") with reentrant getContext("bitmaprenderer"): outer=ImageBitmapRenderingContext inner=ImageBitmapRenderingContext
+getContext("bitmaprenderer") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("bitmaprenderer") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("webgl") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("webgl") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("webgl") with reentrant getContext("webgl"): outer=WebGLRenderingContext inner=WebGLRenderingContext
+getContext("webgl") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("webgl2") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("webgl2") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("webgl2") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("webgl2") with reentrant getContext("webgl2"): outer=WebGL2RenderingContext inner=WebGL2RenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("2d"): outer=OffscreenCanvasRenderingContext2D inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("2d") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("bitmaprenderer"): outer=ImageBitmapRenderingContext inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("webgl") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("webgl"): outer=WebGLRenderingContext inner=WebGLRenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("webgl2") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("webgl2"): outer=WebGL2RenderingContext inner=WebGL2RenderingContext
+PASS if no crash.
diff --git a/LayoutTests/fast/canvas/canvas-getContext-reentrant.html b/LayoutTests/fast/canvas/canvas-getContext-reentrant.html
new file mode 100644
index 000000000000..e46e9d41f37d
--- /dev/null
+++ b/LayoutTests/fast/canvas/canvas-getContext-reentrant.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner)
+    testRunner.dumpAsText();
+
+function gc() {
+    if (window.GCController)
+        return GCController.collect();
+    for (let i = 0; i < 10000; i++)
+        new String("abc");
+}
+
+function reenter(outer, inner) {
+    let canvas = document.createElement("canvas");
+    let innerCtx;
+    let outerCtx = canvas.getContext(outer, {
+        get colorSpace() { innerCtx = canvas.getContext(inner); return "srgb"; },
+        get alpha() { innerCtx = canvas.getContext(inner); return true; },
+    });
+    document.write(`getContext("${outer}") with reentrant getContext("${inner}"): outer=${name(outerCtx)} inner=${name(innerCtx)}<br>`);
+}
+
+function reenterOffscreen(outer, inner) {
+    let canvas = new OffscreenCanvas(10, 10);
+    let innerCtx;
+    let outerCtx = canvas.getContext(outer, {
+        get colorSpace() { innerCtx = canvas.getContext(inner); return "srgb"; },
+        get alpha() { innerCtx = canvas.getContext(inner); return true; },
+    });
+    document.write(`OffscreenCanvas getContext("${outer}") with reentrant getContext("${inner}"): outer=${name(outerCtx)} inner=${name(innerCtx)}<br>`);
+}
+
+function name(ctx) {
+    return ctx ? ctx.constructor.name : "null";
+}
+
+for (let outer of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+    for (let inner of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+        reenter(outer, inner);
+
+for (let outer of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+    for (let inner of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+        reenterOffscreen(outer, inner);
+
+gc();
+document.write("PASS if no crash.");
+</script>
Loading diff…

Original Bug Report

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