High chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in ANGLE
DescriptionMemory corruption in ANGLE
ComponentANGLE
Sub-componentInternals>GPU>ANGLE
Bug ClassMemory Corruption
Tracker556750158
Fix commit1efffc95e07e (angle/angle) +310/-70
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • src/tests/gl_tests/ClearTest.cpp
  • src/tests/gl_tests/TextureTest.cpp
  • src/libANGLE/renderer/vulkan/RenderTargetVk.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.h
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/tests/angle_end2end_tests_expectations.txt
  • src/libANGLE/renderer/vulkan/TextureVk.cpp

Original Bug Report

reported by [email protected]

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

  1. FramebufferVk::invalidateImpl (FramebufferVk.cpp:2326-2343) calls colorRenderTarget->invalidateEntireContent(), clearing bit $N$ in mLevelContentDefined.
  2. FramebufferVk::invalidateImpl then stages a robust resource clear via colorRenderTarget->getImageForWrite().stageRobustResourceClear(imageIndex, ...). As shown above, this clear is orphaned for $N \ge 1$.
  3. On the next draw, flushStagedUpdates skips the orphaned clear.
  4. In FramebufferVk::startNewRenderPass (FramebufferVk.cpp:3855-3861), colorRenderTarget->hasDefinedContent() returns false (since bit $N$ was cleared), setting loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE.
  5. On tile-based GPU architectures (common on mobile and ChromeOS devices), LOAD_OP_DONT_CARE skips tile loading/clearing, leaving stale contents from previous render passes in the on-chip tile memory.
  6. 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 via glReadPixels or texture sampling.

Suggested Reproduction Steps

  1. Initialize a WebGL 2.0 context (canvas.getContext('webgl2')) on a platform utilizing ANGLE’s Vulkan backend.
  2. Create a 3D texture with multiple depth slices: gl.texStorage3D(gl.TEXTURE_3D, 1, gl.RGBA8, 256, 256, 4).
  3. Bind depth slice $N \in [1, 3]$ to COLOR_ATTACHMENT0 using gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex, 0, N).
  4. Perform an initial full draw or clear to initialize the attachment.
  5. Call gl.invalidateFramebuffer(gl.FRAMEBUFFER, [gl.COLOR_ATTACHMENT0]) to trigger backend subresource invalidation and the compensating clear staging.
  6. Draw a single small primitive (e.g. 1-pixel triangle) to trigger a render pass with loadOp = DONT_CARE.
  7. Call gl.readPixels(0, 0, 256, 256, gl.RGBA, gl.UNSIGNED_BYTE, dstBuffer).
  8. 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:

  1. Either adjust RenderTargetVk::flushStagedUpdates so that it does not unconditionally clamp layerIndex to 0 when querying and flushing single-slice 3D attachments, ensuring it queries mLayerIndex with framebufferLayerCount.
  2. Or update 3D texture staging mechanisms and hasStagedUpdatesForSubresource to 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.

View on issue tracker