CVE-2026-11090
Overview
Files Changed
src/libANGLE/Context.cppsrc/libANGLE/Observer.hsrc/libANGLE/Texture.cppsrc/libANGLE/renderer/vulkan/RenderbufferVk.cppsrc/libANGLE/renderer/vulkan/TextureVk.cppsrc/libANGLE/renderer/vulkan/vk_helpers.cppsrc/libANGLE/renderer/vulkan/vk_helpers.h
Patch
From 664858ab7de24434b35763198b2c69a4c3cbf95f Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 20 Apr 2026 17:12:19 -0400
Subject: [PATCH] Vulkan: Fix robust init vs mips
When all staged updates in an image were flushed, a message was passed
from vk::ImageHelper to gl::Texture that led to the entire texture being
marked as initialized, which is not correct.
When the texture is accessed via a framebuffer, only the mip that is
attached to the framebuffer is cleared. Once that clear is flushed, the
rest of the mips shouldn't be considered cleared. Removing this message
also exposed a bug with robust init vs storage image writes, where the
texture was considered uninitialized despite the storage write.
This change simultaneously fixes a small bug with the duplicate-clear
optimization. The levels that are stored in subresource updates are GL
levels. The level stored in mCurrentSingleClearValue was mistakenly the
VK level, but later interpreted as GL level, causing a clear to an
unrelated level to be dropped if BASE level is not zero.
All fixes are needed for the test written for the first fix to pass, so
the fixes are squashed in one commit.
Bug: chromium:500161302
Change-Id: I0d3ab64a91b512a75dab0ed8339aed1bf9793aeb
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7779748
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Charlie Lao <cclao@google.com>
---
diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index ec4aad4..9d5a6a9 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -1664,6 +1664,11 @@
GLenum format)
{
Texture *tex = mState.mTextureManager->getTexture(texture);
+ // For robust init, make sure the texture is initialized before storage writes.
+ if (tex != nullptr)
+ {
+ ANGLE_CONTEXT_TRY(tex->ensureInitialized(this));
+ }
mState.setImageUnit(this, unit, tex, level, layered, layer, access, format);
mImageObserverBindings[unit].bind(tex);
}
diff --git a/src/libANGLE/Observer.h b/src/libANGLE/Observer.h
index e8ef628..2c8fd2a 100644
--- a/src/libANGLE/Observer.h
+++ b/src/libANGLE/Observer.h
@@ -85,9 +85,6 @@
// API.
TextureIDDeleted,
- // Indicates that all pending updates are complete in the subject.
- InitializationComplete,
-
// Indicates a change in foveated rendering state in the subject.
FoveatedRenderingStateChanged,
};
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index cddf21b..c50ce7c 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -2727,10 +2727,6 @@
}
}
break;
- case angle::SubjectMessage::InitializationComplete:
- ASSERT(index == rx::kTextureImageImplObserverMessageIndex);
- setInitState(InitState::Initialized);
- break;
case angle::SubjectMessage::InternalMemoryAllocationChanged:
// Need to mark the texture dirty to give the back end a chance to handle the new
// buffer. For example, the Vulkan back end needs to create a new buffer view that
diff --git a/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp b/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
index 11fe011..3396f7e 100644
--- a/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/RenderbufferVk.cpp
@@ -394,7 +394,6 @@
{
ASSERT(index == kRenderbufferImageSubjectIndex &&
(message == angle::SubjectMessage::SubjectChanged ||
- message == angle::SubjectMessage::InitializationComplete ||
message == angle::SubjectMessage::VkImageChanged));
if (message == angle::SubjectMessage::VkImageChanged)
diff --git a/src/libANGLE/renderer/vulkan/TextureVk.cpp b/src/libANGLE/renderer/vulkan/TextureVk.cpp
index 51fe88a..f2f9ad3 100644
--- a/src/libANGLE/renderer/vulkan/TextureVk.cpp
+++ b/src/libANGLE/renderer/vulkan/TextureVk.cpp
@@ -4694,9 +4694,7 @@
void TextureVk::onSubjectStateChange(angle::SubjectIndex index, angle::SubjectMessage message)
{
- ASSERT(index == kTextureImageSubjectIndex &&
- (message == angle::SubjectMessage::SubjectChanged ||
- message == angle::SubjectMessage::InitializationComplete));
+ ASSERT(index == kTextureImageSubjectIndex && message == angle::SubjectMessage::SubjectChanged);
// Forward the notification to the parent that the staging buffer changed.
onStateChange(message);
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.cpp b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
index 0b6eeb6..ad1d588 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.cpp
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.cpp
@@ -9162,7 +9162,7 @@
const gl::Box &clearArea,
const ClearTextureMode clearMode,
gl::TextureType textureType,
- uint32_t levelIndex,
+ uint32_t levelIndexGL,
uint32_t layerIndex,
uint32_t layerCount,
GLenum type,
@@ -9231,16 +9231,16 @@
textureType == gl::TextureType::_2DArray ||
textureType == gl::TextureType::_2DMultisampleArray;
const gl::ImageIndex index = gl::ImageIndex::MakeFromType(
- textureType, levelIndex, 0, useLayerAsDepth ? clearArea.depth : 1);
+ textureType, levelIndexGL, 0, useLayerAsDepth ? clearArea.depth : 1);
- appendSubresourceUpdate(gl::LevelIndex(levelIndex),
+ appendSubresourceUpdate(gl::LevelIndex(levelIndexGL),
SubresourceUpdate(aspectFlags, clearValue, index));
}
else
{
- appendSubresourceUpdate(gl::LevelIndex(levelIndex),
- SubresourceUpdate(aspectFlags, clearValue, textureType, levelIndex,
- layerIndex, layerCount, clearArea));
+ appendSubresourceUpdate(gl::LevelIndex(levelIndexGL),
+ SubresourceUpdate(aspectFlags, clearValue, textureType,
+ levelIndexGL, layerIndex, layerCount, clearArea));
}
return angle::Result::Continue;
}
@@ -9917,7 +9917,7 @@
update->getDestSubresource(mLayerCount, &updateBaseLayer, &updateLayerCount);
const LevelIndex updateMipLevelVk = toVkLevel(updateMipLevelGL);
- update->data.clear.levelIndex = updateMipLevelVk.get();
+ update->data.clear.levelIndex = updateMipLevelGL.get();
ANGLE_TRY(clearEmulatedChannels(contextVk, update->data.clear.colorMaskFlags,
update->data.clear.value, updateMipLevelVk, updateBaseLayer,
updateLayerCount));
@@ -10139,8 +10139,10 @@
ANGLE_TRY(contextVk->getUtils().clearTexture(contextVk, this, params));
}
contextVk->getPerfCounters().fullImageClears++;
- // Remember the latest operation is a clear call.
+ // Remember the latest operation is a clear call. Note that the tracked level
+ // is the GL level.
mCurrentSingleClearValue = update.data.clear;
+ mCurrentSingleClearValue.value().levelIndex = updateMipLevelGL.get();
// Do not call onWrite as it removes mCurrentSingleClearValue, but instead call
// setContentDefined directly.
@@ -10346,7 +10348,6 @@
if (mSubresourceUpdates.empty())
{
ASSERT(mTotalStagedBufferUpdateSize == 0);
- onStateChange(angle::SubjectMessage::InitializationComplete);
}
return angle::Result::Continue;
diff --git a/src/libANGLE/renderer/vulkan/vk_helpers.h b/src/libANGLE/renderer/vulkan/vk_helpers.h
index a017845..46a4cf3 100644
--- a/src/libANGLE/renderer/vulkan/vk_helpers.h
+++ b/src/libANGLE/renderer/vulkan/vk_helpers.h
@@ -2569,7 +2569,7 @@
const gl::Box &clearArea,
const ClearTextureMode clearMode,
gl::TextureType textureType,
- uint32_t levelIndex,
+ uint32_t levelIndexGL,
uint32_t layerIndex,
uint32_t layerCount,
GLenum type,
@@ -3000,6 +3000,7 @@
}
VkImageAspectFlags aspectFlags;
VkClearValue value;
+ // Note: The level index is a GL level (gl::LevelIndex)
uint32_t levelIndex;
uint32_t layerIndex;
uint32_t layerCount;
@@ -3016,6 +3017,7 @@
}
VkImageAspectFlags aspectFlags;
VkClearValue clearValue;
+ // Note: The level index is a GL level (gl::LevelIndex)
uint32_t levelIndex;
uint32_t layerIndex;
uint32_t layerCount;
@@ -3028,11 +3030,13 @@
struct BufferUpdate
{
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3e44278..ec89164 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -412,6 +412,8 @@
448658630 MAC OPENGL : GLSLTestLoops.ForContinueInSwitchComplex/* = SKIP
475587478 MAC OPENGL : VertexAttributeTestES3.MaxAttribsWithBuiltInAttribs/* = SKIP
494270619 MAC OPENGL : BaseInstanceOverflowTest.BaseInstanceOverflow/* = SKIP
+504886981 MAC OPENGL : RobustResourceInitTestES3.PartiallyInitializedTextureWithNonZeroBase/* = SKIP
+504886981 MAC OPENGL : RobustResourceInitTestES3.ReadbackWithMippedTexture/* = SKIP
// BlitFramebufferTest.ScissoredMultisampleStencil failures
42262159 MAC INTEL OPENGL : BlitFramebufferTest.ScissoredMultisampleStencil/* = SKIP
diff --git a/src/tests/gl_tests/ComputeShaderTest.cpp b/src/tests/gl_tests/ComputeShaderTest.cpp
index 67bf3f7..99f7c89 100644
--- a/src/tests/gl_tests/ComputeShaderTest.cpp
+++ b/src/tests/gl_tests/ComputeShaderTest.cpp
@@ -5716,7 +5716,8 @@
}
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ComputeShaderTest);
-ANGLE_INSTANTIATE_TEST_ES31(ComputeShaderTest);
+ANGLE_INSTANTIATE_TEST_ES31_AND(ComputeShaderTest,
+ ES31_VULKAN().enable(Feature::ForceRobustResourceInit));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(ComputeShaderTestES3);
ANGLE_INSTANTIATE_TEST_ES3(ComputeShaderTestES3);
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 9faedd6..763ba82 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -27,7 +27,6 @@
" texcoord = vec2(position.xy * 0.5 - 0.5);\n"
"}";
-// TODO(jmadill): Would be useful in a shared place in a utils folder.
void UncompressDXTBlock(int destX,
int destY,
int destWidth,
@@ -1193,20 +1192,18 @@
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- constexpr char kVS[] =
- "attribute vec2 position;\n"
- "varying vec2 texCoord;\n"
- "void main() {\n"
- " gl_Position = vec4(position, 0, 1);\n"
- " texCoord = (position * 0.5) + 0.5;\n"
- "}";
- constexpr char kFS[] =
- "precision mediump float;\n"
- "varying vec2 texCoord;\n"
- "uniform sampler2D tex;\n"
- "void main() {\n"
- " gl_FragColor = texture2D(tex, texCoord);\n"
- "}";
+ constexpr char kVS[] = R"(attribute vec2 position;
+varying vec2 texCoord;
+void main() {
+ gl_Position = vec4(position, 0, 1);
+ texCoord = (position * 0.5) + 0.5;
+})";
+ constexpr char kFS[] = R"(precision mediump float;
+varying vec2 texCoord;
+uniform sampler2D tex;
+void main() {
+ gl_FragColor = texture2D(tex, texCoord);
+})";
ANGLE_GL_PROGRAM(program, kVS, kFS);
drawQuad(program, "position", 0.5f);
@@ -1235,20 +1232,18 @@
EXPECT_GL_NO_ERROR();
- constexpr char kVS[] =
- "attribute vec2 position;\n"
- "varying vec2 texCoord;\n"
- "void main() {\n"
- " gl_Position = vec4(position, 0, 1);\n"
- " texCoord = (position * 0.5) + 0.5;\n"
- "}";
- constexpr char kFS[] =
- "precision mediump float;\n"
- "varying vec2 texCoord;\n"
- "uniform sampler2D tex;\n"
- "void main() {\n"
- " gl_FragColor = texture2D(tex, texCoord);\n"
- "}";
+ constexpr char kVS[] = R"(attribute vec2 position;
+varying vec2 texCoord;
+void main() {
+ gl_Position = vec4(position, 0, 1);
+ texCoord = (position * 0.5) + 0.5;
+})";
+ constexpr char kFS[] = R"(precision mediump float;
+varying vec2 texCoord;
+uniform sampler2D tex;
+void main() {
+ gl_FragColor = texture2D(tex, texCoord);
+})";
ANGLE_GL_PROGRAM(program, kVS, kFS);
drawQuad(program, "position", 0.5f);
@@ -1256,6 +1251,72 @@
checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::black);
}
+// Test that readback of uninitialized mipped texture works as expected.
+TEST_P(RobustResourceInitTestES3, ReadbackWithMippedTexture)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ GLTexture tex;
+ setupTexture(&tex);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, kWidth >> 1, kHeight >> 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, kWidth >> 2, kHeight >> 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+ // Read back all the levels, verify that all levels are cleared to transparent black.
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 3);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth >> 2, kHeight >> 2, GLColor::transparentBlack);
+
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 2);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth >> 1, kHeight >> 1, GLColor::transparentBlack);
+
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::transparentBlack);
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test that robust init works for a mutable texture with non-zero base, if some of the levels are
+// initialized through other means. Regression test for a bug where the base level was not
+// accounted for when determining which levels need initialization
+TEST_P(RobustResourceInitTestES3, PartiallyInitializedTextureWithNonZeroBase)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ GLTexture tex;
+ setupTexture(&tex);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, kWidth >> 1, kHeight >> 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexImage2D(GL_TEXTURE_2D, 3, GL_RGBA, kWidth >> 2, kHeight >> 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
+
+ // Draw to level 3. Given base level, this would be level 2 of the backing image.
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 3);
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 1.0f);
+
+ // Read back all the levels, verify that levels 1 and 2 are cleared to transparent black.
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth >> 2, kHeight >> 2, GLColor::red);
+
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 2);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth >> 1, kHeight >> 1, GLColor::transparentBlack);
+
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_PIXEL_RECT_EQ(0, 0, kWidth, kHeight, GLColor::transparentBlack);
+ ASSERT_GL_NO_ERROR();
+}
+
// Reading a partially initialized texture (texImage2D) should succeed with all uninitialized bytes
// set to 0 and initialized bytes untouched.
TEST_P(RobustResourceInitTest, ReadingPartiallyInitializedTexture)
Original Bug Report
Robust Resource Initialization Bypass via Level Index Confusion in ANGLE
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 security team.
Overview: A logic error in ANGLE’s Vulkan backend causes clear updates to be incorrectly cached and compared using mixed OpenGL-space and Vulkan-space level indices. When a texture has a non-zero base level, these indices diverge, causing valid robust initialization clears to be erroneously dropped. This exposes uninitialized GPU memory to applications, potentially leaking sensitive cross-origin data.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.h
Estimated timestamp from git blame: 2025-02-20
Final Result: Information Leak via RRI Bypass
A potential vulnerability exists in ImageHelper::flushStagedUpdates that leads to the dropping of Robust Resource Initialization (RRI) clears, ultimately exposing uninitialized GPU memory to the WebGL application.
Technical Details
Initial logic and parameters are validated to map OpenGL (GL) levels to Vulkan levels using an offset based on mFirstAllocatedLevel.
However, a fatal index space confusion occurs during the caching of clear operations. The mCurrentSingleClearValue cache stores a ClearUpdate struct containing a Vulkan-space levelIndex. Later, during optimization passes in flushStagedUpdates, this cached Vulkan index is passed directly to getLevelUpdates, which expects a GL-space index.
Because the struct equality operator (ClearUpdate::operator==) relies on a raw memcmp, an attacker can engineer a collision. If the Vulkan index of a previously cached clear numerically matches the GL index of a newly staged RRI clear (e.g., all-zero clear values), the memcmp succeeds. The optimization logic erroneously concludes the RRI clear is redundant, dropping it entirely. The underlying GPU memory remains uninitialized.
Potential Reproduction Steps
Note: These are suggested/potential steps derived from code analysis; our tooling agent does not currently have the ability to run code to verify a working proof of concept.
- Standard mutable texture allocation applied, setting
GL_TEXTURE_BASE_LEVELto a non-zero value (e.g., 1) and defining multiple levels viatexImage2D(..., null)to track them as uninitialized. - Standard processing applied to clear a higher GL level (e.g., GL level 2), populating the internal clear cache with a Vulkan-space index (e.g., 1).
- Trigger a read operation (e.g.,
glReadPixels) on the base level (GL level 1). This stages a zero-value RRI clear for GL level 1. - The texture flush logic executes; the numeric collision between the cached Vulkan index (1) and the new GL index (1) triggers the early return, dropping the RRI clear.
glReadPixelscompletes, returning raw, uninitialized GPU memory to the attacker.
Suggested Fix
Modify ImageHelper::flushStagedUpdates to ensure index spaces are correctly aligned before comparison. The optimization should either convert the cached Vulkan levelIndex back to a GL levelIndex using mFirstAllocatedLevel, or ClearUpdate should explicitly store the GL index alongside the Vulkan index to prevent memcmp collisions across different coordinate spaces.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.