Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker501650354
Fix commitde4a5159133b (angle/angle) +109/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
From de4a5159133b6b6bff3c97b20ca91a77cc7f6f05 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Thu, 23 Apr 2026 14:47:43 -0400
Subject: [PATCH] Vulkan: Fix depth becoming read-only on stencil feedback loop

Bug: chromium:501650354
Change-Id: I76d5b2ffb6bd65b891f6fd00d147d8ef006bc6c5
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7790255
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 85886a6..a14a261 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -8443,6 +8443,7 @@
         return angle::Result::Continue;
     }
 
+    // Switch to read-only depth or stencil feedback loop if not already
     if (isStencilTexture)
     {
         if (mState.isStencilWriteEnabled(mState.getDrawFramebuffer()->getStencilBitCount()))
@@ -8457,18 +8458,19 @@
             mDepthStencilAttachmentFlags.set(vk::RenderPassUsage::StencilReadOnlyAttachment);
         }
     }
-
-    // Switch to read-only depth feedback loop if not already
-    if (mState.isDepthWriteEnabled())
+    else
     {
-        // This looks like a feedback loop, but we don't issue a warning because the application
-        // may have correctly used BASE and MAX levels to avoid it.  ANGLE doesn't track that.
-        mDepthStencilAttachmentFlags.set(vk::RenderPassUsage::DepthFeedbackLoop);
-    }
-    else if (!mDepthStencilAttachmentFlags[vk::RenderPassUsage::DepthFeedbackLoop])
-    {
-        // If we are not in the actual feedback loop mode, switch to read-only depth mode
-        mDepthStencilAttachmentFlags.set(vk::RenderPassUsage::DepthReadOnlyAttachment);
+        if (mState.isDepthWriteEnabled())
+        {
+            // This looks like a feedback loop, but we don't issue a warning because the application
+            // may have correctly used BASE and MAX levels to avoid it.  ANGLE doesn't track that.
+            mDepthStencilAttachmentFlags.set(vk::RenderPassUsage::DepthFeedbackLoop);
+        }
+        else if (!mDepthStencilAttachmentFlags[vk::RenderPassUsage::DepthFeedbackLoop])
+        {
+            // If we are not in the actual feedback loop mode, switch to read-only depth mode
+            mDepthStencilAttachmentFlags.set(vk::RenderPassUsage::DepthReadOnlyAttachment);
+        }
     }
 
     if ((mDepthStencilAttachmentFlags & vk::kDepthStencilReadOnlyBits).none())
diff --git a/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp b/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
index 1a8df19..4126a5f 100644
--- a/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
+++ b/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
@@ -781,6 +781,102 @@
     EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, GLColor::blue);
 }
 
+// Tests that sampling from stencil while simultaneously bound as read-only attachment works.  Depth
+// is cleared but is otherwise not being written to.
+TEST_P(ReadOnlyFeedbackLoopTestES31, SampleStencilWhileReadOnlyAttachmentWithDepthClear)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_read_only_depth_stencil_feedback_loops"));
+
+    constexpr GLsizei kSize = 64;
+
+    // Create FBO with color, depth and stencil
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
+
+    GLTexture depthStencil;
+    glBindTexture(GL_TEXTURE_2D, depthStencil);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
+
+    GLFramebuffer framebuffer;
+    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthStencil,
+                           0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    ASSERT_GL_NO_ERROR();
+
+    // Initialize stencil so it can be sampled from later.  Initialize depth too, which will be
+    // overwritten later.
+    ANGLE_GL_PROGRAM(red, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+    glEnable(GL_STENCIL_TEST);
+    glStencilFunc(GL_ALWAYS, 0xAA, 0xFF);
+    glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
+    glStencilMask(0xFF);
+    glEnable(GL_DEPTH_TEST);
+    glDepthFunc(GL_ALWAYS);
+    glDepthMask(GL_TRUE);
+    drawQuad(red, essl1_shaders::PositionAttrib(), 0.5f);
+    // Close the render pass
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    // Clear depth, but otherwise disable depth write.  If the clear is not taken into account,
+    // depth would look like it's in read-only mode, which is not correct.
+    glClearDepthf(0.99f);
+    glClear(GL_DEPTH_BUFFER_BIT);
+    glDepthFunc(GL_LESS);
+    glDepthMask(GL_FALSE);
+
+    // Put stencil in read-only mode, and bind it for sampling.
+    glStencilFunc(GL_EQUAL, 0xAA, 0xFF);
+    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+    constexpr char kVS[] = R"(#version 310 es
+precision highp float;
+in vec4 position;
+out vec2 texCoord;
+
+void main()
+{
+    gl_Position = position;
+    texCoord = position.xy * 0.5 + vec2(0.5);
+})";
+
+    constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+precision mediump usampler2D;
+
+in vec2 texCoord;
+uniform usampler2D stencil;
+out vec4 color;
+
+void main()
+{
+    bool stencilPass = texture(stencil, texCoord).x == 0xAAu;
+    color = vec4(0, stencilPass, 0, 1);
+}
+)";
+
+    ANGLE_GL_PROGRAM(validateStencil, kVS, kFS);
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, depthStencil);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
+    ASSERT_GL_NO_ERROR();
+
+    glUseProgram(validateStencil);
+    glUniform1i(glGetUniformLocation(validateStencil, "stencil"), 0);
+    ASSERT_GL_NO_ERROR();
+
+    drawQuad(validateStencil, "position", 0.97);
+    ASSERT_GL_NO_ERROR();
+
+    // Validate depth test passed and stencil was read correctly
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
 // Tests that sampling from depth while simultaneously bound as read-only attachment works.  Stencil
 // is being written at the same time.
 TEST_P(ReadOnlyFeedbackLoopTestES31, SampleDepthWhileReadOnlyAttachment)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp b/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
index 1a8df19..4126a5f 100644
--- a/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
+++ b/src/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
@@ -781,6 +781,102 @@
     EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, GLColor::blue);
 }
 
+// Tests that sampling from stencil while simultaneously bound as read-only attachment works.  Depth
+// is cleared but is otherwise not being written to.
+TEST_P(ReadOnlyFeedbackLoopTestES31, SampleStencilWhileReadOnlyAttachmentWithDepthClear)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_read_only_depth_stencil_feedback_loops"));
+
+    constexpr GLsizei kSize = 64;
+
+    // Create FBO with color, depth and stencil
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D, texture);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
+
+    GLTexture depthStencil;
+    glBindTexture(GL_TEXTURE_2D, depthStencil);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
+
+    GLFramebuffer framebuffer;
+    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthStencil,
+                           0);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    ASSERT_GL_NO_ERROR();
+
+    // Initialize stencil so it can be sampled from later.  Initialize depth too, which will be
+    // overwritten later.
+    ANGLE_GL_PROGRAM(red, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+    glEnable(GL_STENCIL_TEST);
+    glStencilFunc(GL_ALWAYS, 0xAA, 0xFF);
+    glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
+    glStencilMask(0xFF);
+    glEnable(GL_DEPTH_TEST);
+    glDepthFunc(GL_ALWAYS);
+    glDepthMask(GL_TRUE);
+    drawQuad(red, essl1_shaders::PositionAttrib(), 0.5f);
+    // Close the render pass
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    // Clear depth, but otherwise disable depth write.  If the clear is not taken into account,
+    // depth would look like it's in read-only mode, which is not correct.
+    glClearDepthf(0.99f);
+    glClear(GL_DEPTH_BUFFER_BIT);
+    glDepthFunc(GL_LESS);
+    glDepthMask(GL_FALSE);
+
+    // Put stencil in read-only mode, and bind it for sampling.
+    glStencilFunc(GL_EQUAL, 0xAA, 0xFF);
+    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+    constexpr char kVS[] = R"(#version 310 es
+precision highp float;
+in vec4 position;
+out vec2 texCoord;
+
+void main()
+{
+    gl_Position = position;
+    texCoord = position.xy * 0.5 + vec2(0.5);
+})";
+
+    constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+precision mediump usampler2D;
+
+in vec2 texCoord;
+uniform usampler2D stencil;
+out vec4 color;
+
+void main()
+{
+    bool stencilPass = texture(stencil, texCoord).x == 0xAAu;
+    color = vec4(0, stencilPass, 0, 1);
+}
+)";
+
+    ANGLE_GL_PROGRAM(validateStencil, kVS, kFS);
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, depthStencil);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
+    ASSERT_GL_NO_ERROR();
+
+    glUseProgram(validateStencil);
+    glUniform1i(glGetUniformLocation(validateStencil, "stencil"), 0);
+    ASSERT_GL_NO_ERROR();
+
+    drawQuad(validateStencil, "position", 0.97);
+    ASSERT_GL_NO_ERROR();
+
+    // Validate depth test passed and stencil was read correctly
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
 // Tests that sampling from depth while simultaneously bound as read-only attachment works.  Stencil
 // is being written at the same time.
 TEST_P(ReadOnlyFeedbackLoopTestES31, SampleDepthWhileReadOnlyAttachment)
Loading diff…

Original Bug Report

reported by vm...@google.com

VRAM leak via deferred clear in ContextVk::switchToReadOnlyDepthStencilMode

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 without the Chrome Security team.

Overview: A logic error in ANGLE’s Vulkan backend prevents deferred depth clears from flushing when a depth-stencil attachment transitions to a read-only layout during stencil texturing. This results in an invalid Vulkan render pass state where a clear operation is requested on a read-only layout, which drivers drop. This bypasses robust resource initialization, exposing uninitialized VRAM content.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp

Estimated timestamp from git blame: 2024-11-15

Summary

In ANGLE’s Vulkan backend, ContextVk::switchToReadOnlyDepthStencilMode handles transitioning depth-stencil attachments to read-only layouts to allow for feedback loops (like sampling from the texture while it is attached to a framebuffer). Because Vulkan render passes cannot clear an attachment (a write operation) while it is in a read-only layout, pending deferred clears must be flushed explicitly before the transition.

A logic error in this function fails to flush pending deferred depth clears when stencil texturing is active. Consequently, the render pass begins in an invalid Vulkan state (loadOp=VK_ATTACHMENT_LOAD_OP_CLEAR with a layout like VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL). Vulkan drivers typically drop the invalid clear operation, allowing a WebGL context to read uninitialized GPU memory, resulting in a potential cross-origin information leak.

Technical Details

When an attacker binds a depth-stencil texture, defers a clear, disables depth writes, and configures the texture for stencil texturing, ANGLE triggers ContextVk::switchToReadOnlyDepthStencilMode with isStencilTexture = true.

Inside this function, ANGLE correctly identifies that depth writes are disabled and sets the vk::RenderPassUsage::DepthReadOnlyAttachment flag on mDepthStencilAttachmentFlags.

However, the subsequent logic to flush deferred clears is flawed:

if ((!isStencilTexture && drawFramebuffer->hasDeferredDepthClear()) ||
    (isStencilTexture && drawFramebuffer->hasDeferredStencilClear()))
{
    ANGLE_TRY(drawFramebuffer->flushDepthStencilDeferredClear(
        this, isStencilTexture ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT));
}

If isStencilTexture is true, the left side of the || short-circuits. If there is a pending depth clear, it is completely ignored and not flushed.

When the draw call continues, FramebufferVk::startNewRenderPass consumes the unflushed depth clear by setting the depth attachment’s loadOp to VK_ATTACHMENT_LOAD_OP_CLEAR. Simultaneously, updateStartedRenderPassWithDepthStencilMode reads the previously set DepthReadOnlyAttachment flag and forces the attachment into a read-only mode.

Finally, finalizeDepthStencilImageLayout chooses a read-only layout. The render pass starts with a clear operation on a read-only layout, violating Vulkan specifications (e.g., VUID-VkAttachmentDescription2-format-06244). The Vulkan driver will drop the clear operation to prevent undefined behavior.

Suggested Reproduction Steps

Note: These are potential steps as our tooling agent cannot execute code to provide a working PoC.

  1. Initialize a WebGL 2 context and enable the WEBGL_stencil_texturing extension.
  2. Create a depth-stencil texture (e.g., gl.DEPTH24_STENCIL8) and attach it to a framebuffer.
  3. Issue a depth clear via gl.clearBufferfv(gl.DEPTH, ...) to trigger a deferred clear in ANGLE.
  4. Call gl.depthMask(false) to disable depth writes and enable depth testing.
  5. Configure the texture for stencil texturing (ext.DEPTH_STENCIL_TEXTURE_MODE to ext.STENCIL_INDEX) and ensure it is sampler-complete.
  6. Execute a draw call that samples from the texture.
  7. The Vulkan driver drops the clear, leaving the depth buffer uninitialized. The attacker’s draw call can conditionally render pixels based on this uninitialized depth data and read it back via gl.readPixels().

Suggested Fix

The condition in ContextVk::switchToReadOnlyDepthStencilMode should be updated to independently check and flush the depth and stencil aspects if they are transitioning to read-only, regardless of the value of isStencilTexture.

if (drawFramebuffer->hasDeferredDepthClear() && mDepthStencilAttachmentFlags.test(vk::RenderPassUsage::DepthReadOnlyAttachment))
{
    ANGLE_TRY(drawFramebuffer->flushDepthStencilDeferredClear(this, VK_IMAGE_ASPECT_DEPTH_BIT));
}
if (drawFramebuffer->hasDeferredStencilClear() && mDepthStencilAttachmentFlags.test(vk::RenderPassUsage::StencilReadOnlyAttachment))
{
    ANGLE_TRY(drawFramebuffer->flushDepthStencilDeferredClear(this, VK_IMAGE_ASPECT_STENCIL_BIT));
}

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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