CVE-2026-14418
Overview
Files Changed
src/libANGLE/renderer/vulkan/TextureVk.cppsrc/libANGLE/renderer/vulkan/vk_helpers.cppsrc/libANGLE/renderer/vulkan/vk_helpers.hsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From 2b54924f963b640d10e64592cdf80ee2fdd2ed30 Mon Sep 17 00:00:00 2001 From: wangra <[email protected]> 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 <[email protected]> Reviewed-by: Charlie Lao <[email protected]> Commit-Queue: Ran Wang <[email protected]> --- 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;
Regression Test / PoC
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)
Original Bug Report
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.cppthird_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
removeSingleSubresourceStagedUpdatescorrectly invalidates the cache viamCurrentSingleClearValue.reset(). - Conversely, the array path
removeStagedUpdatesfails to resetmCurrentSingleClearValue:
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.
- In a Vulkan-backed WebGL2 context, create and bind a 2D array texture.
- Allocate an initial small layer (e.g., 1x1x4) to level 0.
- 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 themCurrentSingleClearValuecache. - 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 triggersreleaseImage. - Reallocate the new
VkImagestructure. ANGLE stages a new RRI clear of{0,0,0,0}for the new size. - 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.