CVE-2026-17785
Overview
Files Changed
src/libANGLE/State.cppsrc/tests/gl_tests/RobustResourceInitTest.cpp
Patch
From 8e09325ebad45c7e11630a79754361e965e5fab0 Mon Sep 17 00:00:00 2001 From: Zhenyao Mo <[email protected]> Date: Thu, 11 Jun 2026 11:19:38 -0700 Subject: [PATCH] Fix robust resource initialization bypass for read framebuffer A state-tracking omission in State::setReadFramebufferBinding allowed robust resource initialization to be bypassed when binding a read framebuffer with uninitialized resources. Additionally, State::setObjectDirty was manually setting DIRTY_OBJECT_READ_FRAMEBUFFER instead of using the setReadFramebufferDirty() helper, which also bypassed the robust initialization check when attachments were modified on a bound read framebuffer. This CL fixes both bypass points by ensuring DIRTY_OBJECT_READ_ATTACHMENTS is correctly armed when robust resource initialization is enabled. Bug: chromium:513769898 Test: RobustResourceInitTest.BindReadFramebufferBypass Change-Id: I25806d19f968b606461c78cc51aaee8fe1101c29 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7927356 Auto-Submit: Zhenyao Mo <[email protected]> Commit-Queue: Zhenyao Mo <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/State.cpp b/src/libANGLE/State.cpp index 5478c12..70d56ba 100644 --- a/src/libANGLE/State.cpp +++ b/src/libANGLE/State.cpp @@ -2942,9 +2942,18 @@ mReadFramebuffer = framebuffer; mDirtyBits.set(state::DIRTY_BIT_READ_FRAMEBUFFER_BINDING); - if (mReadFramebuffer && mReadFramebuffer->hasAnyDirtyBit()) + if (mReadFramebuffer) { - mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER); + if (mReadFramebuffer->hasAnyDirtyBit()) + { + mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER); + } + + if (isRobustResourceInitEnabled() && mReadFramebuffer->hasResourceThatNeedsInit()) + { + mDirtyObjects.set(state::DIRTY_OBJECT_READ_ATTACHMENTS); + mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER); + } } } @@ -4033,13 +4042,13 @@ switch (target) { case GL_READ_FRAMEBUFFER: - mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER); + setReadFramebufferDirty(); break; case GL_DRAW_FRAMEBUFFER: setDrawFramebufferDirty(); break; case GL_FRAMEBUFFER: - mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER); + setReadFramebufferDirty(); setDrawFramebufferDirty(); break; case GL_VERTEX_ARRAY: diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp index 6d0436a..5c54ef1 100644 --- a/src/tests/gl_tests/RobustResourceInitTest.cpp +++ b/src/tests/gl_tests/RobustResourceInitTest.cpp @@ -3545,9 +3545,70 @@ EXPECT_PIXEL_COLOR_EQ(kSize - 1, kSize - 1, GLColor::green); } +// Tests that binding an FBO to GL_READ_FRAMEBUFFER correctly triggers robust resource +// initialization on read-back (glReadPixels), even if it wasn't previously bound as +// GL_DRAW_FRAMEBUFFER. +TEST_P(RobustResourceInitTest, BindReadFramebufferBypass) +{ + ANGLE_SKIP_TEST_IF(!hasGLExtension()); + + // Setup an uninitialized texture, then bind it. + GLTexture tex; + setupTexture(&tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + // Attach to a custom FBO + GLFramebuffer fbo; + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)); + + // Bind default framebuffer to GL_READ_FRAMEBUFFER to clear any dirty bits + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + // Read pixels from the default framebuffer to ensure no dirty bits remain in the context + std::vector<GLColor> defaultData(kWidth * kHeight); + glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, defaultData.data()); + + // Bind our FBO back to GL_READ_FRAMEBUFFER + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + + // Read pixels. If robust resource init is bypassed, this will return the "bad data". + // If robust resource init is working, it will return transparent black (0). + checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack); + EXPECT_GL_NO_ERROR(); +} + +// Tests that attaching a texture to a bound GL_READ_FRAMEBUFFER correctly triggers robust resource +// initialization on read-back, even if it wasn't previously bound as GL_DRAW_FRAMEBUFFER. +TEST_P(RobustResourceInitTest, AttachToBoundReadFramebufferBypass) +{ + ANGLE_SKIP_TEST_IF(!hasGLExtension()); + + // Setup an uninitialized texture, then bind it. + GLTexture tex; + setupTexture(&tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + // Create a custom FBO and bind to GL_READ_FRAMEBUFFER + GLFramebuffer fbo; + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + + // Attach texture to the bound GL_READ_FRAMEBUFFER + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)); + + // Read pixels. If robust resource init is bypassed, this will return the "bad data". + // If robust resource init is working, it will return transparent black (0). + checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack); + EXPECT_GL_NO_ERROR(); +} + ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND( RobustResourceInitTest, ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear), + ES3_METAL().enable(Feature::AllocateNonZeroTextures), + ES2_METAL().enable(Feature::AllocateNonZeroTextures), ES2_VULKAN().enable(Feature::AllocateNonZeroMemory)); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitTestES3);
Regression Test / PoC
diff --git a/src/tests/gl_tests/RobustResourceInitTest.cpp b/src/tests/gl_tests/RobustResourceInitTest.cpp
index 6d0436a..5c54ef1 100644
--- a/src/tests/gl_tests/RobustResourceInitTest.cpp
+++ b/src/tests/gl_tests/RobustResourceInitTest.cpp
@@ -3545,9 +3545,70 @@
EXPECT_PIXEL_COLOR_EQ(kSize - 1, kSize - 1, GLColor::green);
}
+// Tests that binding an FBO to GL_READ_FRAMEBUFFER correctly triggers robust resource
+// initialization on read-back (glReadPixels), even if it wasn't previously bound as
+// GL_DRAW_FRAMEBUFFER.
+TEST_P(RobustResourceInitTest, BindReadFramebufferBypass)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ // Setup an uninitialized texture, then bind it.
+ GLTexture tex;
+ setupTexture(&tex);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ // Attach to a custom FBO
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER));
+
+ // Bind default framebuffer to GL_READ_FRAMEBUFFER to clear any dirty bits
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+
+ // Read pixels from the default framebuffer to ensure no dirty bits remain in the context
+ std::vector<GLColor> defaultData(kWidth * kHeight);
+ glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, defaultData.data());
+
+ // Bind our FBO back to GL_READ_FRAMEBUFFER
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+
+ // Read pixels. If robust resource init is bypassed, this will return the "bad data".
+ // If robust resource init is working, it will return transparent black (0).
+ checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack);
+ EXPECT_GL_NO_ERROR();
+}
+
+// Tests that attaching a texture to a bound GL_READ_FRAMEBUFFER correctly triggers robust resource
+// initialization on read-back, even if it wasn't previously bound as GL_DRAW_FRAMEBUFFER.
+TEST_P(RobustResourceInitTest, AttachToBoundReadFramebufferBypass)
+{
+ ANGLE_SKIP_TEST_IF(!hasGLExtension());
+
+ // Setup an uninitialized texture, then bind it.
+ GLTexture tex;
+ setupTexture(&tex);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kWidth, kHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ // Create a custom FBO and bind to GL_READ_FRAMEBUFFER
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
+
+ // Attach texture to the bound GL_READ_FRAMEBUFFER
+ glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ EXPECT_GLENUM_EQ(GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus(GL_READ_FRAMEBUFFER));
+
+ // Read pixels. If robust resource init is bypassed, this will return the "bad data".
+ // If robust resource init is working, it will return transparent black (0).
+ checkFramebufferNonZeroPixels(0, 0, 0, 0, GLColor::transparentBlack);
+ EXPECT_GL_NO_ERROR();
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND(
RobustResourceInitTest,
ES3_METAL().enable(Feature::EmulateDontCareLoadWithRandomClear),
+ ES3_METAL().enable(Feature::AllocateNonZeroTextures),
+ ES2_METAL().enable(Feature::AllocateNonZeroTextures),
ES2_VULKAN().enable(Feature::AllocateNonZeroMemory));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustResourceInitTestES3);
Original Bug Report
ANGLE: Potential robust resource initialization bypass in State::setReadFramebufferBinding
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 state-tracking omission in ANGLE allows WebGL applications to potentially bypass robust resource initialization when binding a read framebuffer. This could enable the disclosure of uninitialized GPU memory through operations like glReadPixels, leaking data from other origins or processes. The issue stems from an asymmetry in how read and draw framebuffer bindings are handled in the libANGLE State class.
Affected files:
third_party/angle/src/libANGLE/State.cppthird_party/angle/src/libANGLE/State.hthird_party/angle/src/libANGLE/Context.cppthird_party/angle/src/libANGLE/Framebuffer.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_helpers.cppthird_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp
Estimated timestamp from git blame: 2019-05-13
Summary
A potential vulnerability has been identified in ANGLE’s state management that may allow a bypass of the robust resource initialization security feature. By manipulating framebuffer bindings, a malicious WebGL application could read uninitialized GPU VRAM through operations such as glReadPixels, glCopyTexImage2D, or glBlitFramebuffer. This uninitialized memory may contain residual data from other Chrome processes, other tabs, or other system applications.
Technical Details
The root cause appears to be an asymmetry in how ANGLE’s State class handles read and draw framebuffer bindings. While the draw framebuffer binding logic correctly arms the robust initialization state, the read framebuffer logic does not.
In third_party/angle/src/libANGLE/State.cpp, State::setReadFramebufferBinding (lines 2949-2961) updates the read framebuffer but fails to check if the new framebuffer contains resources that require initialization:
void State::setReadFramebufferBinding(Framebuffer *framebuffer)
{
if (mReadFramebuffer == framebuffer)
return;
mReadFramebuffer = framebuffer;
mDirtyBits.set(state::DIRTY_BIT_READ_FRAMEBUFFER_BINDING);
if (mReadFramebuffer && mReadFramebuffer->hasAnyDirtyBit())
{
mDirtyObjects.set(state::DIRTY_OBJECT_READ_FRAMEBUFFER);
}
}
By contrast, setDrawFramebufferBinding (lines 2981-2985) correctly arms DIRTY_OBJECT_DRAW_ATTACHMENTS if robust initialization is enabled and the framebuffer has uninitialized resources:
if (isRobustResourceInitEnabled() && mDrawFramebuffer->hasResourceThatNeedsInit())
{
mDirtyObjects.set(state::DIRTY_OBJECT_DRAW_ATTACHMENTS);
mDirtyObjects.set(state::DIRTY_OBJECT_DRAW_FRAMEBUFFER);
}
DIRTY_OBJECT_READ_ATTACHMENTS is the trigger for syncReadAttachments, which calls Framebuffer::ensureReadAttachmentsInitialized. If this bit is not armed during the binding change, subsequent read-back operations like glReadPixels skip the initialization (zeroing) step.
Furthermore, State::setObjectDirty (State.cpp:4043) fails to use the setReadFramebufferDirty() helper defined in State.h, manually setting only DIRTY_OBJECT_READ_FRAMEBUFFER for the GL_READ_FRAMEBUFFER target, which also misses the attachment initialization check.
Because many backends (like Vulkan and Metal) rely on the front-end to perform robust initialization and skip their own internal clears when this feature is enabled, this front-end bypass leads directly to reading uninitialized VRAM.
Potential Reproduction Steps
An attacker might attempt to trigger this via the following sequence:
- Create a texture with
nulldata (marking it asInitState::MayNeedInit). - Attach the texture to a Framebuffer Object (FBO).
- Ensure the FBO is not currently bound as the
GL_READ_FRAMEBUFFER. - Perform a
gl.readPixelsoperation on the default framebuffer to clear any existingDIRTY_OBJECT_READ_ATTACHMENTSbits in the context. - Bind the target FBO to
GL_READ_FRAMEBUFFER. Due to the missing logic insetReadFramebufferBinding, the attachments dirty bit is not armed. - Call
gl.readPixelsfrom the target FBO. The command may execute against uninitialized VRAM, returning residual data to the application.
Suggested Fix
- Update
State::setReadFramebufferBindinginthird_party/angle/src/libANGLE/State.cppto include a check similar to the draw path:
if (isRobustResourceInitEnabled() && mReadFramebuffer && mReadFramebuffer->hasResourceThatNeedsInit())
{
mDirtyObjects.set(state::DIRTY_OBJECT_READ_ATTACHMENTS);
}
- Modify
State::setObjectDirtyinthird_party/angle/src/libANGLE/State.cppto use thesetReadFramebufferDirty()helper for theGL_READ_FRAMEBUFFERcase.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.