CVE-2026-11138
Overview
Files Changed
src/libANGLE/renderer/vulkan/ContextVk.cppsrc/tests/gl_tests/ReadOnlyFeedbackLoopTest.cpp
Patch
From de4a5159133b6b6bff3c97b20ca91a77cc7f6f05 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> 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 <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- 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)
Regression Test / PoC
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)
Original Bug Report
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.cppthird_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.
- Initialize a WebGL 2 context and enable the
WEBGL_stencil_texturingextension. - Create a depth-stencil texture (e.g.,
gl.DEPTH24_STENCIL8) and attach it to a framebuffer. - Issue a depth clear via
gl.clearBufferfv(gl.DEPTH, ...)to trigger a deferred clear in ANGLE. - Call
gl.depthMask(false)to disable depth writes and enable depth testing. - Configure the texture for stencil texturing (
ext.DEPTH_STENCIL_TEXTURE_MODEtoext.STENCIL_INDEX) and ensure it is sampler-complete. - Execute a draw call that samples from the texture.
- 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.