CVE-2026-17651
Overview
Background
- Rendering feedback loop
- a hazard where a texture bound for sampling in a shader is simultaneously attached to the active framebuffer, so the same resource may be read and written within one draw.
- Read-only depth/stencil sampling
- a legal configuration where a depth or stencil attachment is sampled while attached but never written (depth/stencil writes disabled), so no read-write conflict exists.
- Hardened context
- a non-WebGL ANGLE context (
Context::isHardenedContext) that applies WebGL-style feedback-loop validation while otherwise permitting a broader, spec-correct set of operations than strict WebGL. - `AllowedFeedbackLoop`
- the new
enum class(NoneAllowed,ReadOnlyDepthStencil) added to tellformsRenderingFeedbackLoopWithwhich depth/stencil overlaps the caller’s context is allowed to tolerate.
Root Cause Analysis
Pre-patch, Framebuffer::formsRenderingFeedbackLoopWith unconditionally returned true whenever a sampled texture overlapped the depth attachment (mState.mDepthAttachment) or stencil attachment (mState.mStencilAttachment), treating every such overlap as an illegal feedback loop regardless of whether the attachment was actually written. Because ValidateDrawStates in validationES.cpp invoked this same check for both WebGL and hardened contexts, hardened (non-WebGL) contexts incorrectly rejected legitimate read-only depth/stencil sampling with GL_INVALID_OPERATION, a false positive that diverges from correct GL semantics. This is a spec-conformance / correctness defect: the check over-rejected a safe operation, it did not fail to detect an unsafe one.
The fix adds an AllowedFeedbackLoop parameter and, for hardened contexts, passes ReadOnlyDepthStencil so the depth branch reports a loop only when glState.isDepthWriteEnabled() is true and the texture is not in stencil mode, and the stencil branch reports a loop only when glState.isStencilWriteEnabled(getStencilBitCount()) is true and the texture is in stencil mode. WebGL continues to pass NoneAllowed, preserving its strict behavior. Genuine write feedback loops — where the attachment is actually written during the draw — remain rejected both before and after the patch; only the provably safe read-only case is now permitted.
return true on the runtime write-enable state (isDepthWriteEnabled / isStencilWriteEnabled) plus the texture’s stencil mode, so hardened contexts stop over-rejecting safe read-only sampling.Attack Path
- Obtain a hardened context
Acquire a non-WebGL ANGLE context where
context->isHardenedContext()is true andcontext->isWebGL()is false, which reaches the relaxed validation branch inValidateDrawStates. - Bind a read-only depth/stencil sampler
Attach a depth or stencil texture to the draw framebuffer and also bind that same texture for sampling, forming an overlap detected by
AttachmentOverlapsWithTexture, while leaving depth/stencil writes disabled (e.g.glDepthMask(GL_FALSE)or depth test disabled). - Issue the draw
Post-patch,
formsRenderingFeedbackLoopWithis called withReadOnlyDepthStenciland, seeing no write enabled, no longer reports a loop, so the previously-rejected draw is now correctly accepted rather than failing withkFeedbackLoop/GL_INVALID_OPERATION. - Behavioral consequence The change is a relaxation of validation for a legitimate, spec-conformant configuration; enabling depth or stencil writes on the overlapping attachment still triggers rejection, so no write feedback loop becomes acceptable as a result (inferred, from the diff’s write-gated branches and the added tests).
Impact Assessment
LogicError / conformance fix: pre-patch, hardened contexts over-rejected legal read-only depth/stencil sampling, a functional-correctness / denial-of-functionality issue rather than a memory-safety escalation. The advisory classifies the item as critical with ITW: False and KEV: False, but the diff itself relaxes validation to permit a safe case and does not expose acceptance of a concurrently-written feedback loop. Any escalation beyond incorrect rejection of valid draws is not supported by the patch (inferred).Files Changed
src/libANGLE/Framebuffer.cppsrc/libANGLE/Framebuffer.hsrc/libANGLE/validationES.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/WebGLCompatibilityTest.cpp
Audit Directions
- Overlap-only validationLook for feedback-loop and aliasing checks that return on structural resource overlap alone without inspecting the runtime write-enable state, since such checks over-reject valid operations (and, if the polarity were inverted, would under-detect real hazards).
- `isWebGL()` vs `isHardenedContext()` branchesAudit every site that distinguishes WebGL from hardened contexts to confirm the relaxed hardened path still consults per-aspect write-enable and stencil-mode state before permitting an operation.
- Depth/stencil aspect handlingReview code that treats depth and stencil as one unit to ensure per-aspect state (
isStencilWriteEnabled,getStencilBitCount,isStencilMode) is consulted rather than assumed uniform, matching the depth-vs-stencil split introduced by this patch.
Patch
From 8903f2cc0bcade7cc296cc7518e6c0a2bf4d3e1c Mon Sep 17 00:00:00 2001
From: Stephen White <senorblanco@chromium.org>
Date: Tue, 09 Jun 2026 16:17:16 -0400
Subject: [PATCH] Modify validation of depth/stencil feedback loops
Allow depth/stencil read-only feedback loops on non-WebGL contexts.
Bug: angleproject:521861813
Bug: chromium:517307966
Change-Id: Ie6a3cc7460d59541941d30ff8a4d270fa26b9ead
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7914428
Commit-Queue: Stephen White <senorblanco@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp
index 035383f..bb18831 100644
--- a/src/libANGLE/Framebuffer.cpp
+++ b/src/libANGLE/Framebuffer.cpp
@@ -2380,7 +2380,8 @@
}
}
-bool Framebuffer::formsRenderingFeedbackLoopWith(const Context *context) const
+bool Framebuffer::formsRenderingFeedbackLoopWith(const Context *context,
+ AllowedFeedbackLoop allowedFeedbackLoop) const
{
const State &glState = context->getState();
const ProgramExecutable *executable = glState.getLinkedProgramExecutable(context);
@@ -2415,12 +2416,22 @@
if (AttachmentOverlapsWithTexture(mState.mDepthAttachment, texture, sampler))
{
- return true;
+ if (allowedFeedbackLoop != AllowedFeedbackLoop::ReadOnlyDepthStencil ||
+ (glState.isDepthWriteEnabled() && !texture->getState().isStencilMode()))
+ {
+ return true;
+ }
}
if (AttachmentOverlapsWithTexture(mState.mStencilAttachment, texture, sampler))
{
- return true;
+ if (allowedFeedbackLoop != AllowedFeedbackLoop::ReadOnlyDepthStencil ||
+ (glState.isStencilWriteEnabled(
+ glState.getDrawFramebuffer()->getStencilBitCount()) &&
+ texture->getState().isStencilMode()))
+ {
+ return true;
+ }
}
if (pls != nullptr)
diff --git a/src/libANGLE/Framebuffer.h b/src/libANGLE/Framebuffer.h
index 555000a..74c5664 100644
--- a/src/libANGLE/Framebuffer.h
+++ b/src/libANGLE/Framebuffer.h
@@ -456,7 +456,14 @@
// Observer implementation
void onSubjectStateChange(angle::SubjectIndex index, angle::SubjectMessage message) override;
- bool formsRenderingFeedbackLoopWith(const Context *context) const;
+ enum class AllowedFeedbackLoop : bool
+ {
+ NoneAllowed,
+ ReadOnlyDepthStencil,
+ };
+
+ bool formsRenderingFeedbackLoopWith(const Context *context,
+ AllowedFeedbackLoop allowedFeedbackLoop) const;
bool formsCopyingFeedbackLoopWith(TextureID destTextureId,
const gl::ImageIndex &destImageIndex) const;
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 73761f1..5ecde1f 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -4245,7 +4245,10 @@
if (ANGLE_UNLIKELY(context->isWebGL() || context->isHardenedContext()))
{
// UB: Detect rendering feedback loops for WebGL or hardened context.
- if (framebuffer->formsRenderingFeedbackLoopWith(context))
+ if (framebuffer->formsRenderingFeedbackLoopWith(
+ context, context->isWebGL()
+ ? Framebuffer::AllowedFeedbackLoop::NoneAllowed
+ : Framebuffer::AllowedFeedbackLoop::ReadOnlyDepthStencil))
{
return kFeedbackLoop;
}
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 4cff1cb4..fc14680 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -103,6 +103,14 @@
500721361 VULKAN SWIFTSHADER : VertexAttributeTestES3.storeStaticAttribWithLargeOffset/* = SKIP
+// Causes a write-after-write hazard on Linux/Vk
+524405499 VULKAN : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyStencil8/* = SKIP
+524405499 VULKAN : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyANGLE/* = SKIP
+
+// Asserts on Win/D3D11
+524406308 WIN D3D11 : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyStencil8/* = SKIP
+524406308 WIN D3D11 : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyANGLE/* = SKIP
+
// Windows
42262429 WIN NVIDIA D3D11 : BufferDataOverflowTest.VertexBufferIntegerOverflow/* = SKIP
40096654 WIN VULKAN : BufferDataOverflowTest.VertexBufferIntegerOverflow/* = SKIP
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 04f9929..8c94044 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -4770,6 +4770,235 @@
EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Stencil test disabled should still fail";
}
+// This test covers detection of rendering feedback loops between the FBO and a depth Texture.
+// Read-only depth/stencil feedback loops are allowed in hardened contexts and
+// should not generate an error.
+TEST_P(HardenedContextTest, RenderingFeedbackLoopWithDepthStencil)
+{
+ constexpr char kVS[] =
+ R"(#version 300 es
+in vec4 aPosition;
+out vec2 texCoord;
+void main() {
+ gl_Position = aPosition;
+ texCoord = (aPosition.xy * 0.5) + 0.5;
+})";
+
+ constexpr char kFS[] =
+ R"(#version 300 es
+precision mediump float;
+uniform sampler2D tex;
+in vec2 texCoord;
+out vec4 oColor;
+void main() {
+ oColor = texture(tex, texCoord);
+})";
+
+ GLsizei width = 8;
+ GLsizei height = 8;
+
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+
+ glViewport(0, 0, width, height);
+
+ GLint texLoc = glGetUniformLocation(program, "tex");
+ glUniform1i(texLoc, 0);
+
+ // Create textures and allocate storage
+ GLTexture tex0;
+ GLTexture tex1;
+ FillTexture2D(tex0, width, height, GLColor::black, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+ FillTexture2D(tex1, width, height, 0x80, 0, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT,
+ GL_UNSIGNED_INT);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+
+ // Test rendering and sampling feedback loop for depth buffer
+ glBindTexture(GL_TEXTURE_2D, tex1);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, tex1, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // The same image is used as depth buffer during rendering.
+ // This should be an error in hardened contexts.
+ glEnable(GL_DEPTH_TEST);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Same image as depth buffer should fail";
+
+ // The same image is used as depth buffer. But depth mask is false.
+ glDepthMask(GL_FALSE);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+
+ // The same image is used as depth buffer. But depth test is not enabled during rendering.
+ glDepthMask(GL_TRUE);
+ glDisable(GL_DEPTH_TEST);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+}
+
+// This test covers detection of rendering feedback loops between the FBO and a stencil texture with
+// GL_OES_texture_stencil8. Read-only stencil feedback loops are allowed in hardened contexts and
+// should not generate an error.
+TEST_P(HardenedContextTest, RenderingFeedbackLoopWithStencilOnlyStencil8)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_texture_stencil8"));
+
+ constexpr char kVS[] =
+ R"(#version 300 es
+in vec4 aPosition;
+out vec2 texCoord;
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 4cff1cb4..fc14680 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -103,6 +103,14 @@
500721361 VULKAN SWIFTSHADER : VertexAttributeTestES3.storeStaticAttribWithLargeOffset/* = SKIP
+// Causes a write-after-write hazard on Linux/Vk
+524405499 VULKAN : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyStencil8/* = SKIP
+524405499 VULKAN : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyANGLE/* = SKIP
+
+// Asserts on Win/D3D11
+524406308 WIN D3D11 : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyStencil8/* = SKIP
+524406308 WIN D3D11 : HardenedContextTest.RenderingFeedbackLoopWithStencilOnlyANGLE/* = SKIP
+
// Windows
42262429 WIN NVIDIA D3D11 : BufferDataOverflowTest.VertexBufferIntegerOverflow/* = SKIP
40096654 WIN VULKAN : BufferDataOverflowTest.VertexBufferIntegerOverflow/* = SKIP
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index 04f9929..8c94044 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -4770,6 +4770,235 @@
EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Stencil test disabled should still fail";
}
+// This test covers detection of rendering feedback loops between the FBO and a depth Texture.
+// Read-only depth/stencil feedback loops are allowed in hardened contexts and
+// should not generate an error.
+TEST_P(HardenedContextTest, RenderingFeedbackLoopWithDepthStencil)
+{
+ constexpr char kVS[] =
+ R"(#version 300 es
+in vec4 aPosition;
+out vec2 texCoord;
+void main() {
+ gl_Position = aPosition;
+ texCoord = (aPosition.xy * 0.5) + 0.5;
+})";
+
+ constexpr char kFS[] =
+ R"(#version 300 es
+precision mediump float;
+uniform sampler2D tex;
+in vec2 texCoord;
+out vec4 oColor;
+void main() {
+ oColor = texture(tex, texCoord);
+})";
+
+ GLsizei width = 8;
+ GLsizei height = 8;
+
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+
+ glViewport(0, 0, width, height);
+
+ GLint texLoc = glGetUniformLocation(program, "tex");
+ glUniform1i(texLoc, 0);
+
+ // Create textures and allocate storage
+ GLTexture tex0;
+ GLTexture tex1;
+ FillTexture2D(tex0, width, height, GLColor::black, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+ FillTexture2D(tex1, width, height, 0x80, 0, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT,
+ GL_UNSIGNED_INT);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+
+ // Test rendering and sampling feedback loop for depth buffer
+ glBindTexture(GL_TEXTURE_2D, tex1);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, tex1, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // The same image is used as depth buffer during rendering.
+ // This should be an error in hardened contexts.
+ glEnable(GL_DEPTH_TEST);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Same image as depth buffer should fail";
+
+ // The same image is used as depth buffer. But depth mask is false.
+ glDepthMask(GL_FALSE);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+
+ // The same image is used as depth buffer. But depth test is not enabled during rendering.
+ glDepthMask(GL_TRUE);
+ glDisable(GL_DEPTH_TEST);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+}
+
+// This test covers detection of rendering feedback loops between the FBO and a stencil texture with
+// GL_OES_texture_stencil8. Read-only stencil feedback loops are allowed in hardened contexts and
+// should not generate an error.
+TEST_P(HardenedContextTest, RenderingFeedbackLoopWithStencilOnlyStencil8)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_texture_stencil8"));
+
+ constexpr char kVS[] =
+ R"(#version 300 es
+in vec4 aPosition;
+out vec2 texCoord;
+void main() {
+ gl_Position = aPosition;
+ texCoord = (aPosition.xy * 0.5) + 0.5;
+})";
+
+ constexpr char kFS[] =
+ R"(#version 300 es
+precision mediump float;
+precision mediump usampler2D;
+uniform usampler2D tex;
+in vec2 texCoord;
+out vec4 oColor;
+void main() {
+ oColor = vec4(texture(tex, texCoord)) / 256.0;
+})";
+
+ GLsizei width = 8;
+ GLsizei height = 8;
+
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+
+ glViewport(0, 0, width, height);
+
+ GLint texLoc = glGetUniformLocation(program, "tex");
+ glUniform1i(texLoc, 0);
+
+ // Create textures and allocate storage
+ GLTexture tex0;
+ GLTexture tex1;
+ FillTexture2D(tex0, width, height, GLColor::black, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+ FillTexture2D(tex1, width, height, 0x40, 0, GL_STENCIL_INDEX8, GL_STENCIL_INDEX,
+ GL_UNSIGNED_BYTE);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, tex1, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glBindTexture(GL_TEXTURE_2D, tex1);
+
+ // The same image is used as stencil buffer during rendering.
+ // Ensure that the stencil func and op are not no-ops, so that there can
+ // be stencil writes.
+ glEnable(GL_STENCIL_TEST);
+ glStencilFunc(GL_GREATER, 0x0000, 0xFFFFFFFF);
+ glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Same image as stencil buffer should fail";
+
+ // The same image is used as stencil buffer. But stencil mask is zero.
+ glStencilMask(0x0);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+
+ // The same image is used as stencil buffer. But stencil test is not enabled during rendering.
+ glStencilMask(0xffff);
+ glDisable(GL_STENCIL_TEST);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+}
+
+// This test covers detection of rendering feedback loops between the FBO and a stencil texture with
+// GL_ANGLE_stencil_texturing. Read-only stencil feedback loops are allowed in hardened contexts and
+// should not generate an error.
+TEST_P(HardenedContextTest, RenderingFeedbackLoopWithStencilOnlyANGLE)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_stencil_texturing"));
+
+ constexpr char kVS[] =
+ R"(#version 300 es
+in vec4 aPosition;
+out vec2 texCoord;
+void main() {
+ gl_Position = aPosition;
+ texCoord = (aPosition.xy * 0.5) + 0.5;
+})";
+
+ constexpr char kFS[] =
+ R"(#version 300 es
+precision mediump float;
+precision mediump usampler2D;
+uniform usampler2D tex;
+in vec2 texCoord;
+out vec4 oColor;
+void main() {
+ oColor = vec4(texture(tex, texCoord)) / 256.0;
+})";
+
+ GLsizei width = 8;
+ GLsizei height = 8;
+
+ ANGLE_GL_PROGRAM(program, kVS, kFS);
+ glUseProgram(program);
+
+ glViewport(0, 0, width, height);
+
+ GLint texLoc = glGetUniformLocation(program, "tex");
+ glUniform1i(texLoc, 0);
+
+ // Create textures and allocate storage
+ GLTexture tex0;
+ GLTexture tex1;
+ FillTexture2D(tex0, width, height, GLColor::black, 0, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE);
+ FillTexture2D(tex1, width, height, 0x40, 0, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL,
+ GL_UNSIGNED_INT_24_8);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex0, 0);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, tex1, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glBindTexture(GL_TEXTURE_2D, tex1);
+ glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
+
+ // The same image is used as stencil buffer during rendering.
+ // Ensure that the stencil func and op are not no-ops, so that there can
+ // be stencil writes.
+ glEnable(GL_STENCIL_TEST);
+ glStencilFunc(GL_GREATER, 0x0000, 0xFFFFFFFF);
+ glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION) << "Same image as stencil buffer should fail";
+
+ // The same image is used as stencil buffer. But stencil mask is zero.
+ glStencilMask(0x0);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+
+ // The same image is used as stencil buffer. But stencil test is not enabled during rendering.
+ glStencilMask(0xffff);
+ glDisable(GL_STENCIL_TEST);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+
+ // The same image is used as stencil buffer. But only the depth component of the texture is
+ // being read.
+ glStencilMask(0xffff);
+ glEnable(GL_STENCIL_TEST);
+ glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
+ drawQuad(program, "aPosition", 0.5f, 1.0f, true);
+ EXPECT_GL_NO_ERROR();
+}
+
// The source and the target for CopyTexSubImage3D are the same 3D texture.
// But the level of the 3D texture != the level of the read attachment.
TEST_P(WebGL2CompatibilityTest, NoTextureCopyingFeedbackLoopBetween3DLevels)
Original Bug Report
Potential Android GPU Driver Memory Corruption via Dawn OpenGLES Unhardened ANGLE Context
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Dawn’s OpenGLES backend creates its EGL context without requesting a hardened ANGLE context, which disables ANGLE’s large-variable size validation checks. This allows Tint-generated storage buffer blocks up to 4GB to pass validation and reach the native Android GLES vendor shader compiler, potentially triggering integer-overflow or memory corruption vulnerabilities.
Affected files:
third_party/dawn/src/dawn/native/opengl/ContextEGL.cppthird_party/dawn/src/tint/lang/wgsl/resolver/resolver.cc
Estimated timestamp from git blame: 2026-01-13
Root Cause Analysis
In third_party/dawn/src/dawn/native/opengl/ContextEGL.cpp, Dawn’s OpenGLES backend creates its EGL context via egl.CreateContext without specifying either the EGL_CONTEXT_HARDENED_ANGLE or EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE attributes:
if (egl.HasExt(EGLExt::CreateContext)) {
switch (backend) {
case wgpu::BackendType::OpenGLES:
AddAttrib(EGL_CONTEXT_MAJOR_VERSION, 3);
AddAttrib(EGL_CONTEXT_MINOR_VERSION, 1);
break;
...
}
}
Unlike Chrome’s passthrough command-buffer path (which enforces context hardening), Dawn’s directly created EGL context bypasses this mitigation. As a result, in ANGLE’s context initialization (third_party/angle/src/libANGLE/Context.cpp), mHardenedContext remains false:
bool GetHardenedContext(const egl::AttributeMap &attribs) {
return (attribs.get(EGL_CONTEXT_HARDENED_ANGLE, EGL_FALSE) == EGL_TRUE);
}
When ANGLE compiles a shader, options.rejectWebglShadersWithLargeVariables is only set to true if context->isWebGL() || context->isHardenedContext() (third_party/angle/src/libANGLE/Shader.cpp). Because both are false on Dawn’s OpenGLES context, ANGLE’s variable size validation in ParseContext.cpp (checkVariableSize) is bypassed:
if (!mCompileOptions.rejectWebglShadersWithLargeVariables || numErrors() > 0 ||
(mShaderType != GL_VERTEX_SHADER && mShaderType != GL_FRAGMENT_SHADER)) {
return true; // Unconditionally bypassed for Dawn's OpenGLES context
}
Attacker-Controlled Shader Sizes (Tint 4GB Limit)
In third_party/dawn/src/tint/lang/wgsl/resolver/resolver.cc, Tint’s WGSL resolver explicitly exempts storage address space arrays from the standard 65,536-element cap, validating only that the total block byte size is under 4GB:
if (auto* arr = ty->As<sem::Array>()) {
if (address_space != core::AddressSpace::kStorage) {
...
if (count.has_value() && count.value() >= internal_limits::kMaxArrayElementCount) { ... }
}
}
At shader translation time, the GLSL writer emits the literal element count inside an std430 buffer block, creating a structure that can be up to 4GB (e.g., layout(binding=0, std430) buffer f_block { mat4 inner[67108863]; } v; which translates to 4,294,967,232 bytes).
Suggested / Potential Attack Steps
Note: These are potential steps, as our tooling agent has not executed this code to verify a functional proof-of-concept.
- A web page requests a WebGPU adapter with
featureLevel: "compatibility"on Android (where Vulkan is blocklisted or unavailable), which falls back to OpenGLES. - The attacker’s page submits a shader containing an exceptionally large array (e.g., ~4GB) in the
<storage>address space. - Tint verifies that the array is under 4GB and successfully translates the WGSL shader into a GLSL block with a massive literal dimension.
- Because
rejectWebglShadersWithLargeVariablesisfalse, the 4GB SSBO structure bypasses ANGLE’s variable-size validation. - During
glCompileShader, ANGLE’s GL backend (ShaderGL::postTranslate) forwards the translated GLSL containing the oversized block to the native Android GLES GPU vendor driver prior to link-time checks. - Delivered to a vendor driver compiler in the unsandboxed GPU process on Android, block size calculations exceeding
INT32_MAXbytes can overflow signed 32-bit integers, potentially causing memory corruption or heap buffer overflow in the driver.
Proposed Fix
Dawn’s OpenGLES backend should query and pass EGL_CONTEXT_HARDENED_ANGLE during context creation inside ContextEGL.cpp when the display extension is supported, or otherwise clamp maximum shader interface variable sizes to prevent them from approaching driver integer overflow limits on non-hardened compatibility contexts.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.