High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker516865345
Fix commit2b54924f963b (angle/angle) +148/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/libANGLE/renderer/vulkan/TextureVk.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.cpp
  • src/libANGLE/renderer/vulkan/vk_helpers.h
  • src/tests/gl_tests/RobustResourceInitTest.cpp
From 2b54924f963b640d10e64592cdf80ee2fdd2ed30 Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Mon, 15 Jun 2026 10:53:55 -0400
Subject: [PATCH] Vulkan: Fix robust-init bypass on array texture redefinition

Resets the clear cache (mCurrentSingleClearValue) in ImageHelper when
releasing the VkImage or removing staged updates. This prevents stale
cache values from incorrectly dropping subsequent robust resource
initialization clears on redefined array textures.

Test: angle_end2end_tests --gtest_filter="*Texture2DArrayRedefine*"
Bug: b/516865345
Change-Id: Ie7b20a680abfe47e3b97034f182842fb470b1452
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7943364
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Ran Wang <wangra@google.com>
---

diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 23c3b9b..dbb73b8 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -2647,7 +2647,7 @@
         {
             // A multi-layer texture is being redefined, remove all updates to this level; the
             // number of layers may have changed.
-            mImage->removeStagedUpdates(contextVk, levelIndexGL, levelIndexGL);
+            mImage->redefineLevels(contextVk, levelIndexGL, levelIndexGL);
         }
         else
         {
@@ -2655,8 +2655,8 @@
             // done through glTexImage2D, one per cube face (i.e. layer) and so should not remove
             // updates to the other layers.
             ASSERT(index.getLayerCount() == 1);
-            mImage->removeSingleSubresourceStagedUpdates(contextVk, levelIndexGL, layerIndex,
-                                                         index.getLayerCount());
+            mImage->redefineSingleSubresource(contextVk, levelIndexGL, layerIndex,
+                                              index.getLayerCount());
         }
 
         if (mImage->valid())
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index d493040..5729f11 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -6161,6 +6161,7 @@
     mUse.reset();
     mImageSerial          = kInvalidImageSerial;
     mMemoryAllocationType = MemoryAllocationType::InvalidEnum;
+    mCurrentSingleClearValue.reset();
     setEntireContentUndefined();
 }
 
@@ -8319,8 +8320,6 @@
                                                        uint32_t layerIndex,
                                                        uint32_t layerCount)
 {
-    mCurrentSingleClearValue.reset();
-
     // Find any staged updates for this index and remove them from the pending list.
     SubresourceUpdates *levelUpdates = getLevelUpdates(levelIndexGL);
     if (levelUpdates == nullptr)
@@ -8410,6 +8409,48 @@
     assertSubresourceUpdateRefCountsConsistent();
 }
 
+void ImageHelper::redefineLevels(ErrorContext *context,
+                                 gl::LevelIndex levelGLStart,
+                                 gl::LevelIndex levelGLEnd)
+{
+    removeStagedUpdates(context, levelGLStart, levelGLEnd);
+    if (valid())
+    {
+        for (gl::LevelIndex level = levelGLStart; level <= levelGLEnd; ++level)
+        {
+            if (level >= getFirstAllocatedLevel() && level <= getLastAllocatedLevel())
+            {
+                invalidateEntireLevelContent(context, level);
+                if ((getAspectFlags() & VK_IMAGE_ASPECT_STENCIL_BIT) != 0)
+                {
+                    invalidateEntireLevelStencilContent(context, level);
+                }
+            }
+        }
+    }
+}
+
+void ImageHelper::redefineSingleSubresource(ContextVk *contextVk,
+                                            gl::LevelIndex levelIndexGL,
+                                            uint32_t layerIndex,
+                                            uint32_t layerCount)
+{
+    removeSingleSubresourceStagedUpdates(contextVk, levelIndexGL, layerIndex, layerCount);
+
+    if (valid())
+    {
+        if (levelIndexGL >= getFirstAllocatedLevel() && levelIndexGL <= getLastAllocatedLevel())
+        {
+            invalidateSubresourceContent(contextVk, levelIndexGL, layerIndex, layerCount, nullptr);
+            if ((getAspectFlags() & VK_IMAGE_ASPECT_STENCIL_BIT) != 0)
+            {
+                invalidateSubresourceStencilContent(contextVk, levelIndexGL, layerIndex, layerCount,
+                                                    nullptr);
+            }
+        }
+    }
+}
+
 angle::Result ImageHelper::stageSubresourceUpdate(ContextVk *contextVk,
                                                   const gl::ImageIndex &index,
                                                   const gl::Extents &glExtents,
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h
index 10a4e80..f6e87e4 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.h
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.h
@@ -2597,6 +2597,13 @@
     void removeStagedUpdates(ErrorContext *context,
                              gl::LevelIndex levelGLStart,
                              gl::LevelIndex levelGLEnd);
+    void redefineLevels(ErrorContext *context,
+                        gl::LevelIndex levelGLStart,
+                        gl::LevelIndex levelGLEnd);
+    void redefineSingleSubresource(ContextVk *contextVk,
+                                   gl::LevelIndex levelIndexGL,
+                                   uint32_t layerIndex,
+                                   uint32_t layerCount);
 
     angle::Result stagePartialClear(ContextVk *contextVk,
                                     const gl::Box &clearArea,
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 2a10cb7..0125832 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2951,6 +2951,101 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Test that redefining a 2D array texture doesn't bypass robust resource initialization.
+TEST_P(RobustResourceInitTestES3, Texture2DArrayRedefine)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr int kLargeWidth  = 256;
+    constexpr int kLargeHeight = 256;
+    constexpr int kLayers      = 4;
+
+    GLTexture seedTexture;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, seedTexture);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kLargeWidth, kLargeHeight, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> sentinelData(kLargeWidth * kLargeHeight * kLayers, GLColor::red);
+    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kLargeWidth, kLargeHeight, kLayers, GL_RGBA,
+                    GL_UNSIGNED_BYTE, sentinelData.data());
+
+    GLFramebuffer fb;
+    glBindFramebuffer(GL_FRAMEBUFFER, fb);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, seedTexture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    constexpr int kSmallSize = 1;
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSmallSize, kSmallSize, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    // Bind to framebuffer and read to force clear and populate cache.
+    glBindFramebuffer(GL_FRAMEBUFFER, fb);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+
+    // Redefine level 0 to the larger dimension.
+    // This should trigger release and reallocation of the underlying VkImage.
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kLargeWidth, kLargeHeight, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    static_assert(kWidth <= kLargeWidth && kHeight <= kLargeHeight);
+    for (int layer = 0; layer < kLayers; ++layer)
+    {
+        checkNonZeroPixels3D(&texture, 0, 0, 0, 0, layer, GLColor::transparentBlack);
+    }
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that redefining a 2D array texture to a compatible size (same size)
+// doesn't bypass robust resource initialization.
+TEST_P(RobustResourceInitTestES3, Texture2DArrayRedefineCompatible)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr int kSize   = 256;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 2a10cb7..0125832 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -2951,6 +2951,101 @@
     ASSERT_GL_NO_ERROR();
 }
 
+// Test that redefining a 2D array texture doesn't bypass robust resource initialization.
+TEST_P(RobustResourceInitTestES3, Texture2DArrayRedefine)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr int kLargeWidth  = 256;
+    constexpr int kLargeHeight = 256;
+    constexpr int kLayers      = 4;
+
+    GLTexture seedTexture;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, seedTexture);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kLargeWidth, kLargeHeight, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> sentinelData(kLargeWidth * kLargeHeight * kLayers, GLColor::red);
+    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kLargeWidth, kLargeHeight, kLayers, GL_RGBA,
+                    GL_UNSIGNED_BYTE, sentinelData.data());
+
+    GLFramebuffer fb;
+    glBindFramebuffer(GL_FRAMEBUFFER, fb);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, seedTexture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    constexpr int kSmallSize = 1;
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSmallSize, kSmallSize, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    // Bind to framebuffer and read to force clear and populate cache.
+    glBindFramebuffer(GL_FRAMEBUFFER, fb);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::transparentBlack);
+
+    // Redefine level 0 to the larger dimension.
+    // This should trigger release and reallocation of the underlying VkImage.
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kLargeWidth, kLargeHeight, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    static_assert(kWidth <= kLargeWidth && kHeight <= kLargeHeight);
+    for (int layer = 0; layer < kLayers; ++layer)
+    {
+        checkNonZeroPixels3D(&texture, 0, 0, 0, 0, layer, GLColor::transparentBlack);
+    }
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that redefining a 2D array texture to a compatible size (same size)
+// doesn't bypass robust resource initialization.
+TEST_P(RobustResourceInitTestES3, Texture2DArrayRedefineCompatible)
+{
+    ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+    constexpr int kSize   = 256;
+    constexpr int kLayers = 4;
+
+    GLTexture texture;
+    glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    std::vector<GLColor> sentinelData(kSize * kSize * kLayers, GLColor::red);
+    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kSize, kSize, kLayers, GL_RGBA,
+                    GL_UNSIGNED_BYTE, sentinelData.data());
+
+    GLFramebuffer fb;
+    glBindFramebuffer(GL_FRAMEBUFFER, fb);
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+    // Redefine level 0 to the same dimension.
+    // This should not trigger releaseImage(), but it must invalidate the level.
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kSize, kSize, kLayers, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, nullptr);
+
+    glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, 0);
+    EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    static_assert(kWidth <= kSize && kHeight <= kSize);
+    for (int layer = 0; layer < kLayers; ++layer)
+    {
+        checkNonZeroPixels3D(&texture, 0, 0, 0, 0, layer, GLColor::transparentBlack);
+    }
+    ASSERT_GL_NO_ERROR();
+}
+
 // Test that using TexStorage2D followed by CompressedSubImage works with robust init.
 // Taken from WebGL test conformance/extensions/webgl-compressed-texture-s3tc.
 TEST_P(RobustResourceInitTestES3, CompressedSubImage)
Loading diff…

Original Bug Report

reported by vm...@google.com

ANGLE Vulkan: Potential robust-resource-initialization bypass via removeStagedUpdates

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: A logic flaw in ANGLE’s Vulkan backend fails to reset the mCurrentSingleClearValue cache when an array texture level is redefined. This allows the stale clear cache to persist across the release and reallocation of the underlying VkImage handle. Consequently, subsequent robust-resource-initialization clears on the newly allocated image may be incorrectly dropped, potentially disclosing uninitialized GPU memory.

Affected files:

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

Estimated timestamp from git blame: 2020-06-30

Root Cause

In ANGLE’s Vulkan backend, ImageHelper::mCurrentSingleClearValue caches the last full-level clear operation to optimize away redundant identical clears. However, this cache is not invalidated when an array texture level is redefined and the underlying VkImage is released and reallocated.

Specifically, in third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp:

  • The non-array path removeSingleSubresourceStagedUpdates correctly invalidates the cache via mCurrentSingleClearValue.reset().
  • Conversely, the array path removeStagedUpdates fails to reset mCurrentSingleClearValue:
void ImageHelper::removeStagedUpdates(ErrorContext *context,
                                      gl::LevelIndex levelGLStart,
                                      gl::LevelIndex levelGLEnd)
{
    // *** No mCurrentSingleClearValue.reset() ***
    for (gl::LevelIndex level = levelGLStart; level <= levelGLEnd; ++level)
    { 
        ...
        levelUpdates->clear();
    }
}

When an array texture is redefined (for example, in TextureVk::setSubImage in TextureVk.cpp at line 2620), mImage->removeStagedUpdates is called. Because the dimension or format changed, TextureRedefineLevel is triggered, and releaseImage(contextVk) is called to destroy the underlying VkImage handle.

Crucially, the ImageHelper instance itself is not deleted but is instead reused in the subsequent ensureImageAllocated call. Because neither ImageHelper::releaseImage nor the initialization process clears mCurrentSingleClearValue, the stale clear cache survives across VkImage recreation on the same ImageHelper instance.

When a new robust-resource-initialization (RRI) clear of {0,0,0,0} is staged on the freshly-allocated VkImage, flushStagedUpdates compares the staged clear against the stale mCurrentSingleClearValue cache. Since the metadata matches (such as the target level index 0 and clear color {0,0,0,0}), the comparison succeeds and the RRI clear is silently dropped inside vk_helpers.cpp:

if (mCurrentSingleClearValue.valid())
{
    gl::LevelIndex clearLevel(mCurrentSingleClearValue.value().levelIndex);
    if (clearLevel >= levelGLStart && clearLevel < levelGLEnd)
    { 
        ...
        if (IsClearOfAllChannels(update.updateSource) &&
            mCurrentSingleClearValue.value() == update.data.clear)
        {
            update.release(renderer);
            levelUpdates->clear(); // <-- The RRI clear is dropped
        }
    }
}

Impact

This behavior bypasses robust-resource-initialization within the Vulkan backend for array textures, allowing untrusted WebGL2 contexts to read back uninitialized/recycled GPU memory. Since GPU memory pages are recycled across processes and contexts, this could potentially expose residual data belonging to other origins (such as rendered canvas data or video frames) inside the GPU process.

Potential Steps to Trigger the Issue

Note: These are potential steps based on static code analysis; our tooling does not currently have the ability to run or verify proof-of-concept code dynamically.

  1. In a Vulkan-backed WebGL2 context, create and bind a 2D array texture.
  2. Allocate an initial small layer (e.g., 1x1x4) to level 0.
  3. Bind the texture to a framebuffer and execute a read operation (e.g., gl.readPixels) to force the initial RRI clear to flush and populate the mCurrentSingleClearValue cache.
  4. Redefine level 0 of the array texture to a larger dimension (e.g., 256x256x4). Redefining an array texture invokes removeStagedUpdates (which fails to invalidate the cache) and triggers releaseImage.
  5. Reallocate the new VkImage structure. ANGLE stages a new RRI clear of {0,0,0,0} for the new size.
  6. Read back pixels or sample the texture. ANGLE compares the staged RRI clear against the stale cache value, matches them, drops the actual clear, and returns the recycled, uninitialized GPU memory pages.

Suggested Fix

In third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp, modify ImageHelper::removeStagedUpdates to ensure that mCurrentSingleClearValue is reset when staged updates are removed:

void ImageHelper::removeStagedUpdates(ErrorContext *context,
                                      gl::LevelIndex levelGLStart,
                                      gl::LevelIndex levelGLEnd)
{
    assertSubresourceUpdateRefCountsConsistent();
    
    mCurrentSingleClearValue.reset();
    ...

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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