CVE-2026-17668
Overview
Files Changed
src/libANGLE/renderer/vulkan/vk_helpers.cppsrc/libANGLE/renderer/vulkan/vk_helpers.hsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/TextureTest.cpp
Patch
From dc32cd831434bf819b64b4f4aba80707cc57fd35 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Fri, 05 Jun 2026 10:02:50 -0400 Subject: [PATCH] Vulkan: Fix cubemap base-level change vs redefinition If any face of the cubemap was redefined, the entire level was dropped instead of just that face. Fix credit [email protected] Bug: chromium:513134019 Change-Id: I870dd23ba3ca791a9456d00984b75290d361b315 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7901643 Reviewed-by: Charlie Lao <[email protected]> Reviewed-by: Amirali Abdolrashidi <[email protected]> --- diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp index 20c666e..3f1e3d9 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp +++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp @@ -416,12 +416,28 @@ return std::find(haystack, haystackEnd, needle) != haystackEnd; } -gl::TexLevelMask AggregateSkipLevels(const gl::CubeFaceArray<gl::TexLevelMask> &skipLevels) +gl::TexLevelMask AggregateSkipLevelsAnyFaceSkipped( + const gl::CubeFaceArray<gl::TexLevelMask> &skipLevels) { - gl::TexLevelMask skipLevelsAllFaces = skipLevels[0]; + gl::TexLevelMask skipLevelsAnyFace = skipLevels[0]; for (size_t face = 1; face < gl::kCubeFaceCount; ++face) { - skipLevelsAllFaces |= skipLevels[face]; + skipLevelsAnyFace |= skipLevels[face]; + } + return skipLevelsAnyFace; +} + +gl::TexLevelMask AggregateSkipLevelsAllFacesSkipped( + const gl::CubeFaceArray<gl::TexLevelMask> &skipLevels, + gl::TextureType textureType) +{ + gl::TexLevelMask skipLevelsAllFaces = skipLevels[0]; + if (textureType == gl::TextureType::CubeMap) + { + for (size_t face = 1; face < gl::kCubeFaceCount; ++face) + { + skipLevelsAllFaces &= skipLevels[face]; + } } return skipLevelsAllFaces; } @@ -9883,7 +9899,8 @@ // Nothing to do if every level must be skipped const gl::TexLevelMask levelsMask(angle::BitMask<uint32_t>(levelCount) << mFirstAllocatedLevel.get()); - const gl::TexLevelMask skipLevelsAllFaces = AggregateSkipLevels(skipLevels); + const gl::TexLevelMask skipLevelsAllFaces = + AggregateSkipLevelsAllFacesSkipped(skipLevels, textureType); if ((~skipLevelsAllFaces & levelsMask).none()) { @@ -10057,7 +10074,7 @@ gl::LevelIndex levelGLEnd, uint32_t layerStart, uint32_t layerEnd, - const gl::TexLevelMask &skipLevelsAllFaces) + const gl::TexLevelMask &skipLevels) { Renderer *renderer = contextVk->getRenderer(); @@ -10099,7 +10116,7 @@ // them. This can happen when recreating an image that has been partially incompatibly // redefined, in which case only updates to the levels that haven't been redefined // should be flushed. - if (skipLevelsAllFaces.test(updateMipLevelGL.get())) + if (skipLevels.test(updateMipLevelGL.get())) { continue; } @@ -10403,8 +10420,8 @@ return angle::Result::Continue; } - const gl::TexLevelMask skipLevelsAllFaces = AggregateSkipLevels(skipLevels); - removeSupersededUpdates(contextVk, skipLevelsAllFaces); + const gl::TexLevelMask skipLevelsAnyFace = AggregateSkipLevelsAnyFaceSkipped(skipLevels); + removeSupersededUpdates(contextVk, skipLevelsAnyFace); // If a clear is requested and we know it was previously cleared with the same value, we drop // the clear. @@ -10444,7 +10461,7 @@ if (otherUpdatesToFlushOut) { ANGLE_TRY(flushStagedUpdatesImpl(contextVk, levelGLStart, levelGLEnd, layerStart, layerEnd, - skipLevelsAllFaces)); + skipLevelsAnyFace)); } // Compact mSubresourceUpdates, then check if there are any updates left. @@ -10838,8 +10855,7 @@ assertSubresourceUpdateRefCountsConsistent(); } -void ImageHelper::removeSupersededUpdates(ContextVk *contextVk, - const gl::TexLevelMask skipLevelsAllFaces) +void ImageHelper::removeSupersededUpdates(ContextVk *contextVk, const gl::TexLevelMask skipLevels) { assertSubresourceUpdateRefCountsConsistent(); @@ -10847,8 +10863,7 @@ { gl::LevelIndex levelGL = toGLLevel(levelVk); SubresourceUpdates *levelUpdates = getLevelUpdates(levelGL); - if (levelUpdates == nullptr || levelUpdates->size() == 0 || - skipLevelsAllFaces.test(levelGL.get())) + if (levelUpdates == nullptr || levelUpdates->size() == 0 || skipLevels.test(levelGL.get())) { // There are no valid updates to process, continue. continue; diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h index a119c6f..e83f98f 100644 --- a/src/libANGLE/renderer/vulkan/vk_helpers.h +++ b/src/libANGLE/renderer/vulkan/vk_helpers.h @@ -3166,7 +3166,7 @@ // Called from flushStagedUpdates, removes updates that are later superseded by another. This // cannot be done at the time the updates were staged, as the image is not created (and thus the // extents are not known). - void removeSupersededUpdates(ContextVk *contextVk, const gl::TexLevelMask skipLevelsAllFaces); + void removeSupersededUpdates(ContextVk *contextVk, const gl::TexLevelMask skipLevels); void initImageMemoryBarrierStruct(Renderer *renderer, VkImageAspectFlags aspectMask, @@ -3278,7 +3278,7 @@ gl::LevelIndex levelGLEnd, uint32_t layerStart, uint32_t layerEnd, - const gl::TexLevelMask &skipLevelsAllFaces); + const gl::TexLevelMask &skipLevels); // Limit the input level to the number of levels in subresource update list. void clipLevelToUpdateListUpperLimit(gl::LevelIndex *level) const; diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt index 608d2ce..d84c126 100644 --- a/src/tests/angle_end2end_tests_expectations.txt +++ b/src/tests/angle_end2end_tests_expectations.txt @@ -568,6 +568,7 @@ 1456243 WIN D3D11 : WebGL2CompatibilityTest.DrawWithZeroSizedBuffer/* = SKIP 42266836 WIN D3D9 : GLSLValidationTest.VectorScalarArithmeticWithSideEffectInLoop/* = SKIP 42267092 WIN D3D11 : BlitFramebufferTest.FlippedBlits/* = SKIP +520277980 WIN D3D11 : TextureCubeTestES3.CubeMapRedefinedWithBaseLevelChange/* = SKIP 432303915 WIN D3D11 : GLSLTest.VerifyMaxFragmentUniformVectorsWithSamplers/* = SKIP 432303915 WIN D3D11 : GLSLTest.VerifyMaxVertexUniformVectors/* = SKIP 432303915 WIN D3D11 : GLSLTest.VerifyMaxVertexUniformVectorsWithSamplers/* = SKIP diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 5cc4c5d..8065524 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -15057,6 +15057,84 @@ incompatibleCubeFacesThenSingleFaceCompatibleUploadAndIncompatibleAgain(GL_ALPHA); } +// Test cube map redefinition vs changing the base level. +TEST_P(TextureCubeTestES3, CubeMapRedefinedWithBaseLevelChange) +{ + constexpr GLuint kSize = 4; + + const std::vector<GLColor> kRed(kSize * kSize, GLColor::red); + const std::vector<GLColor> kGreen(kSize * kSize, GLColor::green); + + // Create a cubemap at level 1 + GLTexture cube; + glBindTexture(GL_TEXTURE_CUBE_MAP, cube); + for (GLenum face = 0; face < 6; face++) + { + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 1, GL_RGBA, kSize / 2, kSize / 2, 0, + GL_RGBA, GL_UNSIGNED_BYTE, kRed.data()); + } + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 1); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 1); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Make sure the texture is synced. + constexpr char kFS[] = R"(precision highp float; +uniform samplerCube texCube; +void main() +{ + gl_FragColor = textureCube(texCube, vec3(0)); +})"; + ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS); + drawQuad(program, essl1_shaders::PositionAttrib(), 1.0f); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); + + // Define level 0 in a way that is mip-compatible with level 1, then set base level to 0. While + // level 0 is being defined, it's outside the [base, max] levels of the texture. + for (GLenum face = 0; face < 6; face++) + { + const GLenum cubeFace = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 608d2ce..d84c126 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -568,6 +568,7 @@
1456243 WIN D3D11 : WebGL2CompatibilityTest.DrawWithZeroSizedBuffer/* = SKIP
42266836 WIN D3D9 : GLSLValidationTest.VectorScalarArithmeticWithSideEffectInLoop/* = SKIP
42267092 WIN D3D11 : BlitFramebufferTest.FlippedBlits/* = SKIP
+520277980 WIN D3D11 : TextureCubeTestES3.CubeMapRedefinedWithBaseLevelChange/* = SKIP
432303915 WIN D3D11 : GLSLTest.VerifyMaxFragmentUniformVectorsWithSamplers/* = SKIP
432303915 WIN D3D11 : GLSLTest.VerifyMaxVertexUniformVectors/* = SKIP
432303915 WIN D3D11 : GLSLTest.VerifyMaxVertexUniformVectorsWithSamplers/* = SKIP
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 5cc4c5d..8065524 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -15057,6 +15057,84 @@
incompatibleCubeFacesThenSingleFaceCompatibleUploadAndIncompatibleAgain(GL_ALPHA);
}
+// Test cube map redefinition vs changing the base level.
+TEST_P(TextureCubeTestES3, CubeMapRedefinedWithBaseLevelChange)
+{
+ constexpr GLuint kSize = 4;
+
+ const std::vector<GLColor> kRed(kSize * kSize, GLColor::red);
+ const std::vector<GLColor> kGreen(kSize * kSize, GLColor::green);
+
+ // Create a cubemap at level 1
+ GLTexture cube;
+ glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
+ for (GLenum face = 0; face < 6; face++)
+ {
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 1, GL_RGBA, kSize / 2, kSize / 2, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, kRed.data());
+ }
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+
+ // Make sure the texture is synced.
+ constexpr char kFS[] = R"(precision highp float;
+uniform samplerCube texCube;
+void main()
+{
+ gl_FragColor = textureCube(texCube, vec3(0));
+})";
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 1.0f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ // Define level 0 in a way that is mip-compatible with level 1, then set base level to 0. While
+ // level 0 is being defined, it's outside the [base, max] levels of the texture.
+ for (GLenum face = 0; face < 6; face++)
+ {
+ const GLenum cubeFace = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ glTexImage2D(cubeFace, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ kGreen.data());
+ }
+ glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 0);
+
+ // Incompatibly redefine a face of level 1
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 1, GL_RGBA, kSize, kSize, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kGreen.data());
+
+ // Attach a framebuffer to level 0. This should work despite the invalid definition of level 1.
+ // Note also that MIN_FILTER does not enable mipmapping.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X,
+ cube, 0);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, GLColor::green);
+
+ // Redefine the face of level 1 to be compatible, then make sure that none of the data to the
+ // other levels are lost.
+ glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 1, GL_RGBA, kSize / 2, kSize / 2, 0, GL_RGBA,
+ GL_UNSIGNED_BYTE, kGreen.data());
+ for (GLenum face = 0; face < 6; face++)
+ {
+ const GLenum cubeFace = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, cubeFace, cube, 1);
+ EXPECT_PIXEL_RECT_EQ(
+ 0, 0, kSize / 2, kSize / 2,
+ cubeFace == GL_TEXTURE_CUBE_MAP_NEGATIVE_X ? GLColor::green : GLColor::red);
+ }
+
+ // For completeness, verify that level 0 is also intact.
+ for (GLenum face = 0; face < 6; face++)
+ {
+ const GLenum cubeFace = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, cubeFace, cube, 0);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kSize, kSize, GLColor::green);
+ }
+
+ ASSERT_GL_NO_ERROR();
+}
+
// Test that glCopyImageSubData works with GL_TEXTURE_CUBE_MAP_ARRAY layers unique to array cubes
TEST_P(TextureCubeTestES32, CopyImageSubDataCubeMapArray)
{
Original Bug Report
ANGLE Vulkan: Potential uninitialized memory leak via incorrect cube face mask aggregation
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in ANGLE’s Vulkan backend incorrectly aggregates cube map face masks, causing it to drop valid texture data during texture respecification. This may allow a WebGL context to read uninitialized GPU memory by bypassing robust resource initialization. The leaked memory could potentially contain cross-origin data from the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/renderer/renderer_utils.cppthird_party/angle/src/libANGLE/Texture.cpp
Estimated timestamp from git blame: 2023-10-30
Summary
A potential vulnerability exists in ANGLE’s Vulkan backend where incorrect aggregation of cube map face masks leads to the disclosure of uninitialized GPU memory. When a cube map texture is respecified (e.g., due to a base level change), the backend may fail to preserve the data of non-redefined faces. Because the frontend remains unaware of this data loss, robust resource initialization is bypassed, allowing subsequent reads to access uninitialized memory from the newly allocated VkImage.
Root Cause Analysis
The issue stems from the use of OR-aggregation when determining which levels of a cube map should be skipped during data preservation. Specifically, the AggregateSkipLevels helper function in third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp computes the union of redefined level masks across all six cube faces:
gl::TexLevelMask AggregateSkipLevels(const gl::CubeFaceArray<gl::TexLevelMask> &skipLevels)
{
gl::TexLevelMask skipLevelsAllFaces = skipLevels[0];
for (size_t face = 1; face < gl::kCubeFaceCount; ++face)
skipLevelsAllFaces |= skipLevels[face]; // Incorrectly uses OR aggregation
return skipLevelsAllFaces;
}
This aggregated mask is used in ImageHelper::stageSelfAsSubresourceUpdates to decide if a VkImage must be staged for preservation before storage respecification. The function returns early if every allocated level has at least one face being redefined:
// third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cpp:9821
if ((~skipLevelsAllFaces & levelsMask).none())
{
return; // Early return drops all faces if ANY face per level is redefined
}
When this early return occurs, any faces that were NOT redefined are silently discarded. A similar logic error exists in IsTextureLevelRedefined (third_party/angle/src/libANGLE/renderer/renderer_utils.cpp), which also uses OR-aggregation. This function is used in TextureVk::reinitImageAsRenderable to skip preservation of entire levels during format-change respecification if any single face was redefined.
Bypassing Robust Resource Initialization
Robust resource initialization is intended to prevent reading uninitialized memory by zero-clearing new resources. However, it is bypassed here because:
- The frontend’s robust initialization check (
Texture::initState) relies on per-face states. Since the dropped faces were previously uploaded with valid data, their state remainsInitialized. - The frontend is unaware that the backend discarded the data and therefore does not trigger a clear of the new
VkImagememory.
Potential Reproduction Steps
- Initialize a WebGL2 context on ANGLE Vulkan with robust resource initialization enabled.
- Create a
GL_TEXTURE_CUBE_MAPand setGL_TEXTURE_BASE_LEVELto 1. - Upload valid data to all six faces at level 1 and force an allocation (e.g., via a draw call).
- Change
GL_TEXTURE_BASE_LEVELto 0. - Call
glTexImage2DforGL_TEXTURE_CUBE_MAP_POSITIVE_Xat level 1 with an incompatible size. This sets a bit inmRedefinedLevelsfor one face of level 1. - Issue a command (like
glReadPixelson level 0) that triggersrespecifyImageStorage. This hits the buggy early-return instageSelfAsSubresourceUpdates, discarding level 1 data for the other five faces. - Restore the original size for
POSITIVE_Xlevel 1 to regain completeness. - Bind one of the silently discarded faces (e.g.,
NEGATIVE_X) to a framebuffer and callglReadPixels. The read may return uninitialized GPU memory from the new allocation.
Suggested Fix
The aggregation logic in AggregateSkipLevels and IsTextureLevelRedefined should use AND-aggregation (&=) for cube maps when the intent is to skip an operation only if the entire level (all six faces) is affected. Alternatively, the early return in stageSelfAsSubresourceUpdates should be removed to allow the per-face preservation loop to execute correctly.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.