CVE-2026-43720
Overview
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.
Attack Path
- 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.
- 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.
- Clobber the context The outer call resumes after conversion and, pre-patch, creates a second context and overwrites m_context, dropping the inner context.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/WebCore/html/HTMLCanvasElement.cpp |
modified | |
ifSource/WebCore/html/OffscreenCanvas.cpp |
modified |
Files Changed
LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txtLayoutTests/fast/canvas/canvas-getContext-reentrant.htmlSource/WebCore/html/HTMLCanvasElement.cppSource/WebCore/html/OffscreenCanvas.cpp
Audit Directions
- Other getContext-style factoriesgrep 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 coercionAudit DOM methods that assume a member stays null/unchanged across convert<IDLDictionary>/argument coercion, since coercion can run script and re-enter.
- OffscreenCanvas detach/transferCheck transferToImageBitmap and other OffscreenCanvas paths for state (m_detached/m_context) that can change via script during conversion.
Patch
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>
Regression Test / PoC
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>