CVE-2026-11137
Overview
Files Changed
src/libANGLE/renderer/vulkan/vk_helpers.cppsrc/libANGLE/renderer/vulkan/vk_helpers.hsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From 2645a866a4b64c9ffceb9d73b641c2efd8ae7c34 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Thu, 23 Apr 2026 14:05:31 -0400 Subject: [PATCH] Vulkan: Fix robust clear of 2D-array textures If rendering to a layer of such a texture, the clear for the entire level (i.e. all layers) was taken as if it was clearing a single layer and applied to the render pass load op. Bug: chromium:501647943 Change-Id: I96b9c1aeaa6697d79b876e9972eb7f3287143018 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7790254 Reviewed-by: Amirali Abdolrashidi <[email protected]> Reviewed-by: Charlie Lao <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp index ad1d588..fb27753 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp +++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp @@ -8229,7 +8229,7 @@ for (size_t index = 0; index < levelUpdates->size();) { auto update = levelUpdates->begin() + index; - if (update->matchesLayerRange(layerIndex, layerCount)) + if (update->matchesLayerRange(layerIndex, layerCount, mLayerCount)) { // Update total staging buffer size mTotalStagedBufferUpdateSize -= update->updateSource == UpdateSource::Buffer @@ -8263,7 +8263,7 @@ { auto update = levelUpdates->begin() + index; if (update->updateSource == UpdateSource::ClearAfterInvalidate && - update->matchesLayerRange(layerIndex, layerCount)) + update->matchesLayerRange(layerIndex, layerCount, mLayerCount)) { // It's a clear, so doesn't need to be released. levelUpdates->erase(update); @@ -9846,7 +9846,7 @@ // On any data update or the clear does not match exact layer range, we'll need to // do a full upload. const bool isClear = IsClearOfAllChannels(update.updateSource); - if (isClear && update.matchesLayerRange(layer, layerCount)) + if (isClear && update.matchesLayerRange(layer, layerCount, mLayerCount)) { foundClear = updateIndex; } @@ -11780,13 +11780,18 @@ } bool ImageHelper::SubresourceUpdate::matchesLayerRange(uint32_t layerIndex, - uint32_t layerCount) const + uint32_t layerCount, + uint32_t imageLayerCount) const { uint32_t updateBaseLayer, updateLayerCount; getDestSubresource(gl::ImageIndex::kEntireLevel, &updateBaseLayer, &updateLayerCount); - return updateBaseLayer == layerIndex && - (updateLayerCount == layerCount || updateLayerCount == VK_REMAINING_ARRAY_LAYERS); + if (updateLayerCount == VK_REMAINING_ARRAY_LAYERS) + { + updateLayerCount = imageLayerCount; + } + + return updateBaseLayer == layerIndex && updateLayerCount == layerCount; } bool ImageHelper::SubresourceUpdate::intersectsLayerRange(uint32_t layerIndex, diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h index 46a4cf3..0cd2095 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.h +++ b/src/libANGLE/renderer/vulkan/vk_helpers.h @@ -3079,8 +3079,11 @@ void release(Renderer *renderer); // Returns true if the update's layer range exact matches [layerIndex, - // layerIndex+layerCount) range - bool matchesLayerRange(uint32_t layerIndex, uint32_t layerCount) const; + // layerIndex+layerCount) range. To support VK_REMAINING_ARRAY_LAYERS, the number of layers + // in the image is also passed in. + bool matchesLayerRange(uint32_t layerIndex, + uint32_t layerCount, + uint32_t imageLayerCount) const; // Returns true if the update is to any layer within range of [layerIndex, // layerIndex+layerCount) bool intersectsLayerRange(uint32_t layerIndex, uint32_t layerCount) const; diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp index a62a229..340bdcd 100644 --- a/src/tests/gl_tests/RobustResourceInitTest.cpp +++ b/src/tests/gl_tests/RobustResourceInitTest.cpp @@ -2696,6 +2696,43 @@ EXPECT_PIXEL_RECT_EQ(0, kSubHeight, kSubWidth, kHeight - kSubHeight, GLColor::black); } +// Test that after rendering to a layer of 2D array texture, the other layers are still cleared on +// readback. +TEST_P(RobustResourceInitTestES3, RenderTo2DArray) +{ + ANGLE_SKIP_TEST_IF(!hasGLExtension()); + + constexpr uint32_t kWidth = 53; + constexpr uint32_t kHeight = 77; + constexpr uint32_t kDepth = 3; + + GLTexture tex; + glBindTexture(GL_TEXTURE_2D_ARRAY, tex); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kDepth); + + // Render to layer 0 + GLFramebuffer framebuffer; + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, 0); + ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + + ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green()); + glViewport(0, 0, kWidth, kHeight); + drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); + + // Read back the other layers, making sure they are cleared. + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); + for (uint32_t layer = 1; layer < kDepth; ++layer) + { + glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, layer); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack); + } +} + // Test drawing to a framebuffer with not all draw buffers enabled TEST_P(RobustResourceInitTestES3, SparseDrawBuffers) {
Regression Test / PoC
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index a62a229..340bdcd 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2696,6 +2696,43 @@
EXPECT_PIXEL_RECT_EQ(0, kSubHeight, kSubWidth, kHeight - kSubHeight, GLColor::black);
}
+// Test that after rendering to a layer of 2D array texture, the other layers are still cleared on
+// readback.
+TEST_P(RobustResourceInitTestES3, RenderTo2DArray)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ constexpr uint32_t kWidth = 53;
+ constexpr uint32_t kHeight = 77;
+ constexpr uint32_t kDepth = 3;
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D_ARRAY, tex);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kDepth);
+
+ // Render to layer 0
+ GLFramebuffer framebuffer;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
+ glViewport(0, 0, kWidth, kHeight);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+
+ // Read back the other layers, making sure they are cleared.
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+ for (uint32_t layer = 1; layer < kDepth; ++layer)
+ {
+ glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, layer);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+ }
+}
+
// Test drawing to a framebuffer with not all draw buffers enabled
TEST_P(RobustResourceInitTestES3, SparseDrawBuffers)
{
Original Bug Report
Cross-origin info leak via robust resource initialization bypass in ANGLE Vulkan
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 layer range matching can cause robust resource initialization to be skipped for certain layers of 2D array textures. This allows WebGL applications to potentially read uninitialized GPU memory, which may contain sensitive data from other contexts or origins.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/RenderTargetVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/Texture.cppthird_party/angle/src/libANGLE/Framebuffer.cppthird_party/angle/src/libANGLE/FramebufferAttachment.cpp
Estimated timestamp from git blame: 2024-06-11
Summary
A potential vulnerability exists in ANGLE’s Vulkan backend where robust resource initialization can be bypassed for 2D array textures (e.g., TEXTURE_2D_ARRAY). A logic error in SubresourceUpdate::matchesLayerRange causes a staged clear intended for an entire texture level to be incorrectly matched with a single-layer operation. This results in the whole-array clear being erased from the pending updates list while only one layer is actually cleared by the hardware. Subsequent reads from other layers of the texture expose uninitialized GPU memory, which may contain sensitive data from other contexts or origins.
Technical Details
In ANGLE’s Vulkan backend, when an array texture is created without initial data, robust resource initialization stages a clear for the entire level. This update is stored with layerIndex = 0 and layerCount = VK_REMAINING_ARRAY_LAYERS (0xFFFFFFFF).
The function ImageHelper::SubresourceUpdate::matchesLayerRange in third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp determines if a staged update precisely matches a requested layer range:
bool ImageHelper::SubresourceUpdate::matchesLayerRange(uint32_t layerIndex,
uint32_t layerCount) const
{
uint32_t updateBaseLayer, updateLayerCount;
getDestSubresource(gl::ImageIndex::kEntireLevel, &updateBaseLayer, &updateLayerCount);
return updateBaseLayer == layerIndex &&
(updateLayerCount == layerCount || updateLayerCount == VK_REMAINING_ARRAY_LAYERS);
}
When getDestSubresource retrieves a whole-array clear, updateLayerCount is VK_REMAINING_ARRAY_LAYERS. The condition updateLayerCount == VK_REMAINING_ARRAY_LAYERS evaluates to true regardless of the requested layerCount, provided updateBaseLayer == layerIndex.
When a single layer (e.g., layer 0) of such an array texture is attached to a framebuffer and a draw occurs, ANGLE calls flushSingleSubresourceStagedUpdates with layer = 0 and layerCount = 1.
Due to the logic error, the staged whole-array clear is incorrectly identified as an exact match for this single-layer request. Consequently:
- The clear is deferred to the RenderPass
loadOp. Because the framebuffer attachment’sVkImageViewis restricted to layer 0, only layer 0 is cleared by the GPU hardware. - The staged whole-array clear is removed from the update list by
removeSingleSubresourceStagedUpdates, which uses the same flawedmatchesLayerRangelogic. - The ANGLE frontend marks the entire texture level as initialized (
InitState::Initialized).
Subsequent sampling or reading from layers > 0 will find the texture marked as initialized and no staged updates pending. The application then reads raw, uninitialized VkDeviceMemory.
Potential Reproduction Steps
Note: Our tooling agent does not have the ability to run code. These are suggested steps to trigger the issue:
- In a WebGL2 context using the ANGLE Vulkan backend, create a 2D array texture with multiple layers (e.g., 4 layers) using
texStorage3Dwithout providing initial pixel data. - Create a framebuffer and attach only layer 0 of the texture using
framebufferTextureLayer. - Issue any draw command (e.g.,
drawArrays). This triggers the incorrect flushing and erasure of the staged whole-array clear. - Attach a different layer (e.g., layer 2) to the framebuffer for reading.
- Use
readPixelsto retrieve data from that layer. - The returned data may contain stale GPU memory instead of zeros.
Impact
This is a potential cross-origin information leak. A malicious website could exploit this to read uninitialized GPU memory from the shared GPU process, potentially disclosing pixel data belonging to other origins, WebGL contexts, or sensitive browser UI components.
Suggested Fix
The logic in matchesLayerRange should be corrected to ensure that if updateLayerCount == VK_REMAINING_ARRAY_LAYERS, it only returns true if the requested layerCount actually encompasses the rest of the array. For example, by passing the actual image layer count into matchesLayerRange so it can properly resolve VK_REMAINING_ARRAY_LAYERS to a concrete number before comparing it to the requested layerCount.
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.