CVE-2026-113234
Overview
Files Changed
src/tests/gl_tests/ClearTest.cppsrc/tests/gl_tests/TextureTest.cppsrc/libANGLE/renderer/vulkan/RenderTargetVk.cppsrc/libANGLE/renderer/vulkan/vk_helpers.hsrc/libANGLE/renderer/vulkan/vk_helpers.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/libANGLE/renderer/vulkan/TextureVk.cpp
Original Bug Report
ANGLE Vulkan: Dropped robust-init clears for 3D texture slices disclose GPU memory
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: In ANGLE’s Vulkan backend, robust resource initialization clears for 3D texture depth slices are staged using depth slice indices as array layers, but RenderTargetVk::flushStagedUpdates forces the layer index to 0 for VK_IMAGE_TYPE_3D. Consequently, compensating robust clears staged during framebuffer invalidation for depth slices N > 0 are never flushed. Subsequent draws execute with loadOp = DONT_CARE, potentially disclosing uninitialized or stale GPU memory across origins on tile-based architectures.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/RenderTargetVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
Estimated timestamp from git blame: 2026-03-27
Potential Cross-Origin GPU Memory Disclosure via Dropped 3D Texture Robust Resource Init Clears in ANGLE Vulkan
Summary
In ANGLE’s Vulkan backend, there is a mismatch between how subresource updates are staged and how they are flushed for 3D textures (VK_IMAGE_TYPE_3D). Specifically, RenderTargetVk::getImageIndexForClear treats 3D texture depth slices as array layers when staging clears, but RenderTargetVk::flushStagedUpdates overrides the query layer index to 0. As a result, robust-initialization compensating clears staged during invalidateFramebuffer on 3D depth slices $N \ge 1$ are permanently orphaned and never applied. Subsequent render passes on that attachment execute with VK_ATTACHMENT_LOAD_OP_DONT_CARE and VK_ATTACHMENT_STORE_OP_STORE, allowing WebGL content to read uninitialized or stale GPU memory left in on-chip tile buffers on tile-based GPU architectures.
Note: These steps and vulnerability analysis are based on source-code tracing and theoretical verification; our automated tooling does not currently have the capability to run and execute dynamic exploit code.
Vulnerability Details
1. Staging vs. Flush Layer Mismatch
When staging a robust clear on a depth slice attachment of a 3D texture, RenderTargetVk::getImageIndexForClear (third_party/angle/src/libANGLE/renderer/vulkan/RenderTargetVk.cpp:455-471) treats 3D depth slices as layers:
gl::OwnerImageIndex RenderTargetVk::getImageIndexForClear(uint32_t layerCount) const
{
if (mImage->getType() == VK_IMAGE_TYPE_3D || mImage->getLayerCount() > 1)
{
return gl::OwnerImageIndex::Make2DArrayRange(mLevelIndexGL, mLayerIndex, layerCount);
}
...
}
For slice $N$, mLayerIndex is $N$ and layerCount is $1$. The clear update is recorded in ImageHelper::mSubresourceUpdates covering layer interval $[N, N + 1)$.
However, when RenderTargetVk::flushStagedUpdates is called (RenderTargetVk.cpp:392-424):
gl::OwnerLayer layerIndex = mLayerIndex;
if (mImage->getType() == VK_IMAGE_TYPE_3D)
{
layerIndex = gl::OwnerLayer(0); // <-- Slice index discarded
deferredClears = nullptr;
deferredClearIndex = 0;
}
...
if (!image->hasStagedUpdatesForSubresource(mLevelIndexGL, layerIndex, framebufferLayerCount))
{
return angle::Result::Continue;
}
For single-slice attachments, framebufferLayerCount is $1$. The subresource query checks range $[0, 1)$ against the staged clear’s range $[N, N + 1)$. For any slice $N \ge 1$, these intervals are disjoint, so hasStagedUpdatesForSubresource returns false and the function returns early without flushing the clear.
2. Invalidation & RenderPass LoadOp Execution
When glInvalidateFramebuffer is called:
FramebufferVk::invalidateImpl(FramebufferVk.cpp:2326-2343) callscolorRenderTarget->invalidateEntireContent(), clearing bit $N$ inmLevelContentDefined.FramebufferVk::invalidateImplthen stages a robust resource clear viacolorRenderTarget->getImageForWrite().stageRobustResourceClear(imageIndex, ...). As shown above, this clear is orphaned for $N \ge 1$.- On the next draw,
flushStagedUpdatesskips the orphaned clear. - In
FramebufferVk::startNewRenderPass(FramebufferVk.cpp:3855-3861),colorRenderTarget->hasDefinedContent()returnsfalse(since bit $N$ was cleared), settingloadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE. - On tile-based GPU architectures (common on mobile and ChromeOS devices),
LOAD_OP_DONT_CAREskips tile loading/clearing, leaving stale contents from previous render passes in the on-chip tile memory. - The render pass ends with
storeOp = VK_ATTACHMENT_STORE_OP_STORE, writing the uninitialized tile data into texture memory, which can then be retrieved by the application viaglReadPixelsor texture sampling.
Suggested Reproduction Steps
- Initialize a WebGL 2.0 context (
canvas.getContext('webgl2')) on a platform utilizing ANGLE’s Vulkan backend. - Create a 3D texture with multiple depth slices:
gl.texStorage3D(gl.TEXTURE_3D, 1, gl.RGBA8, 256, 256, 4). - Bind depth slice $N \in [1, 3]$ to
COLOR_ATTACHMENT0usinggl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex, 0, N). - Perform an initial full draw or clear to initialize the attachment.
- Call
gl.invalidateFramebuffer(gl.FRAMEBUFFER, [gl.COLOR_ATTACHMENT0])to trigger backend subresource invalidation and the compensating clear staging. - Draw a single small primitive (e.g. 1-pixel triangle) to trigger a render pass with
loadOp = DONT_CARE. - Call
gl.readPixels(0, 0, 256, 256, gl.RGBA, gl.UNSIGNED_BYTE, dstBuffer). - Observe the pixel data outside the drawn primitive: on tile-based architectures, this region may disclose stale tile remnants instead of mandated zero/initial values.
Suggested Fix
Ensure consistency in 3D texture depth slice handling between staging and flushing in RenderTargetVk:
- Either adjust
RenderTargetVk::flushStagedUpdatesso that it does not unconditionally clamplayerIndexto 0 when querying and flushing single-slice 3D attachments, ensuring it queriesmLayerIndexwithframebufferLayerCount. - Or update 3D texture staging mechanisms and
hasStagedUpdatesForSubresourceto consistently map depth slices to Vulkan 3D subresources across all clear and flush paths.
Evaluated with Chrome root at commit: 53c437ac4ea52cc89d0265eb9e6d68666fdb303e
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.