Critical watch CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Dawn
DescriptionInsufficient validation of untrusted input in Dawn
ComponentDawn
Bug ClassInput Validation
Action tierwatch — security-relevant signal without a confirmed exploitable primitive
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517307966
Fix commit8903f2cc0bca (angle/angle) +263/-5
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-06-16 (public git)
Disclosed2026-07-29
Patch gap43 days — fix public in source before the advisory

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 tell formsRenderingFeedbackLoopWith which 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.

Key insight
The bug was conflating any structural overlap between a sampled texture and a depth/stencil attachment with a real feedback loop, ignoring whether that attachment is actually written; the fix gates the 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

  1. Obtain a hardened context Acquire a non-WebGL ANGLE context where context->isHardenedContext() is true and context->isWebGL() is false, which reaches the relaxed validation branch in ValidateDrawStates.
  2. 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).
  3. Issue the draw Post-patch, formsRenderingFeedbackLoopWith is called with ReadOnlyDepthStencil and, seeing no write enabled, no longer reports a loop, so the previously-rejected draw is now correctly accepted rather than failing with kFeedbackLoop / GL_INVALID_OPERATION.
  4. 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

This is a 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.cpp
  • src/libANGLE/Framebuffer.h
  • src/libANGLE/validationES.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp

Audit Directions

  • Overlap-only validation
    Look 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()` branches
    Audit 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 handling
    Review 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.
From 8903f2cc0bcade7cc296cc7518e6c0a2bf4d3e1c Mon Sep 17 00:00:00 2001
From: Stephen White <[email protected]>
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 <[email protected]>
Reviewed-by: Geoff Lang <[email protected]>
Reviewed-by: Charlie Lao <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---

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;
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. A web page requests a WebGPU adapter with featureLevel: "compatibility" on Android (where Vulkan is blocklisted or unavailable), which falls back to OpenGLES.
  2. The attacker’s page submits a shader containing an exceptionally large array (e.g., ~4GB) in the <storage> address space.
  3. Tint verifies that the array is under 4GB and successfully translates the WGSL shader into a GLSL block with a massive literal dimension.
  4. Because rejectWebglShadersWithLargeVariables is false, the 4GB SSBO structure bypasses ANGLE’s variable-size validation.
  5. 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.
  6. Delivered to a vendor driver compiler in the unsandboxed GPU process on Android, block size calculations exceeding INT32_MAX bytes 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.

View on issue tracker