Chrome · WebGL
CVE-2026-79235
UAF in WebGL
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/RendererGL.cppsrc/libANGLE/renderer/gl/StateManagerGL.cppsrc/libANGLE/renderer/gl/StateManagerGL.h
Patch
From e33e55e0801c4ba770e39a8b52679c0205475184 Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
Date: Tue, 21 Jul 2026 14:44:35 +0000
Subject: [PATCH] GL: workaround: Flush queries before deleting or unbinding FBOs on ARM
Implement a workaround to prevent a driver crash on ARM Mali
(Avalon/Valhall) devices. The crash is triggered when a framebuffer
object is unbound or deleted while GPU queries or jobs are still
pending.
This change introduces the `flushQueriesBeforeDeletingOrUnbindingFbo`
feature. When active, ANGLE forces a synchronization of pending queries
prior to FBO deletion or unbinding. The synchronization utilizes a
`glFenceSync` flush to push pending command buffers, gracefully falling
back to `glFinish()` if sync extensions are unavailable.
TAG: agy
CONV: c1a3bcfb-0d1f-4b4e-86b4-c6731bfeebfd
Bug: chromium:534468209
Change-Id: I39a26436ce8937a7682ce61355c164a6b306ee7a
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8110977
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Kenneth Russell <kbr@chromium.org>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
---
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index c4174cd..6c4f57d 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -740,6 +740,12 @@
&members,
};
+ FeatureInfo flushQueriesBeforeDeletingOrUnbindingFbo = {
+ "flushQueriesBeforeDeletingOrUnbindingFbo",
+ FeatureCategory::OpenGLWorkarounds,
+ &members,
+ };
+
};
inline FeaturesGL::FeaturesGL() = default;
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index d83d124..3d77686 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -967,6 +967,15 @@
"Remove complex expressions from struct constructors."
],
"issue": "http://crbug.com/529509587"
+ },
+ {
+ "name": "flush_queries_before_deleting_or_unbinding_fbo",
+ "category": "Workarounds",
+ "description": [
+ "Flush pending GPU jobs/queries before unbinding or deleting a user framebuffer ",
+ "object on ARM Mali Valhall/Avalon GPUs to avoid driver crash."
+ ],
+ "issue": "http://crbug.com/534468209"
}
]
}
diff --git a/src/libANGLE/renderer/gl/RendererGL.cpp b/src/libANGLE/renderer/gl/RendererGL.cpp
index 0dbf240..d611c9f 100644
--- a/src/libANGLE/renderer/gl/RendererGL.cpp
+++ b/src/libANGLE/renderer/gl/RendererGL.cpp
@@ -238,6 +238,7 @@
mFunctions->finish();
mNeedsFlushBeforeDeleteTextures = false;
mWorkDoneSinceLastFlush = false;
+ mStateManager->onSyncedFlushOrFinish();
if (mFeatures.finishDoesNotCauseQueriesToBeAvailable.enabled && mUseDebugOutput)
{
diff --git a/src/libANGLE/renderer/gl/StateManagerGL.cpp b/src/libANGLE/renderer/gl/StateManagerGL.cpp
index ba1716a..785d7a6 100644
--- a/src/libANGLE/renderer/gl/StateManagerGL.cpp
+++ b/src/libANGLE/renderer/gl/StateManagerGL.cpp
@@ -127,6 +127,7 @@
mPlaceholderRbo(0),
mIndependentBlendStates(extensions.drawBuffersIndexedAny()),
mSampleCoverageEverChanged(false),
+ mHasUnflushedQueries(false),
mFramebufferSRGBAvailable(extensions.sRGBWriteControlEXT),
mHasSeparateFramebufferBindings(nativegl::SupportsSeparateFramebufferBindings(functions)),
mIsMultiviewEnabled(extensions.multiviewOVR),
@@ -315,6 +316,7 @@
{
if (fbo != 0)
{
+ bool wasBound = false;
if (mHasSeparateFramebufferBindings)
{
for (size_t binding = 0; binding < mState.framebuffers.size(); ++binding)
@@ -324,6 +326,7 @@
GLenum enumValue = angle::FramebufferBindingToEnum(
static_cast<angle::FramebufferBinding>(binding));
bindFramebuffer(enumValue, 0);
+ wasBound = true;
}
}
}
@@ -334,8 +337,14 @@
if (mState.framebuffers[angle::FramebufferBindingRead] == fbo)
{
bindFramebuffer(GL_FRAMEBUFFER, 0);
+ wasBound = true;
}
}
+ if (!wasBound && mHasUnflushedQueries &&
+ mFeatures.flushQueriesBeforeDeletingOrUnbindingFbo.enabled)
+ {
+ forcefullyFlush();
+ }
mFunctions->deleteFramebuffers(1, &fbo);
}
}
@@ -681,12 +690,44 @@
break;
}
- if (framebufferChanged && mFeatures.flushOnFramebufferChange.enabled)
+ if (framebufferChanged)
{
- mFunctions->flush();
+ if (mFeatures.flushOnFramebufferChange.enabled)
+ {
+ mFunctions->flush();
+ }
+ if (mHasUnflushedQueries && mFeatures.flushQueriesBeforeDeletingOrUnbindingFbo.enabled)
+ {
+ forcefullyFlush();
+ }
}
}
+void StateManagerGL::onSyncedFlushOrFinish()
+{
+ mHasUnflushedQueries = false;
+}
+
+void StateManagerGL::forcefullyFlush()
+{
+ if (mFunctions->fenceSync != nullptr && mFunctions->clientWaitSync != nullptr &&
+ mFunctions->deleteSync != nullptr)
+ {
+ GLsync sync = mFunctions->fenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if (sync != nullptr)
+ {
+ mFunctions->clientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
+ mFunctions->deleteSync(sync);
+ onSyncedFlushOrFinish();
+ return;
+ }
+ }
+
+ // Sync creation not supported or failed; fall back to finish()
+ mFunctions->finish();
+ onSyncedFlushOrFinish();
+}
+
void StateManagerGL::bindRenderbuffer(GLenum type, GLuint renderbuffer)
{
ASSERT(type == GL_RENDERBUFFER);
@@ -758,6 +799,7 @@
mQueries[type] = queryObject;
mFunctions->beginQuery(ToGLenum(type), queryId);
+ mHasUnflushedQueries = true;
if (oldFramebufferBindingDraw != mPlaceholderFbo)
{
@@ -771,6 +813,7 @@
ASSERT(mQueries[type] == queryObject);
mQueries[type] = nullptr;
mFunctions->endQuery(ToGLenum(type));
+ mHasUnflushedQueries = true;
}
void StateManagerGL::updateDrawIndirectBufferBinding(const gl::Context *context)
diff --git a/src/libANGLE/renderer/gl/StateManagerGL.h b/src/libANGLE/renderer/gl/StateManagerGL.h
index 5456796..e6f264e 100644
--- a/src/libANGLE/renderer/gl/StateManagerGL.h
+++ b/src/libANGLE/renderer/gl/StateManagerGL.h
@@ -317,6 +317,8 @@
void deleteRenderbuffer(GLuint rbo);
void deleteTransformFeedback(GLuint transformFeedback);
+ void onSyncedFlushOrFinish();
+
void useProgram(GLuint program);
void forceUseProgram(GLuint program);
void bindVertexArray(GLuint vao, VertexArrayStateGL *vaoState);
@@ -338,6 +340,7 @@
GLenum access,
GLenum format);
void bindFramebuffer(GLenum type, GLuint framebuffer);
+ void forcefullyFlush();
void bindRenderbuffer(GLenum type, GLuint renderbuffer);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/src/tests/gl_tests/OcclusionQueriesTest.cpp b/src/tests/gl_tests/OcclusionQueriesTest.cpp
index d647151..f45e80e 100644
--- a/src/tests/gl_tests/OcclusionQueriesTest.cpp
+++ b/src/tests/gl_tests/OcclusionQueriesTest.cpp
@@ -1245,6 +1245,78 @@
EXPECT_GL_TRUE(result);
}
+// Test that deleting an FBO while a query result on that FBO is pending does not crash the driver
+// when calling glGetQueryObjectuiv.
+TEST_P(OcclusionQueriesTestES3, DeleteFBOWithPendingQuery)
+{
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glViewport(0, 0, 64, 64);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ GLQueryEXT query;
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, query);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+
+ // Delete the FBO on which the query was executed, while result is still pending
+ fbo.reset();
+
+ // Retrieve query result - flushes pending jobs referencing the deleted FBO
+ GLuint result = GL_FALSE;
+ glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_GL_TRUE(result);
+}
+
+// Test that unbinding an FBO while a query result on that FBO is pending does not crash the driver
+// when calling glGetQueryObjectuiv.
+TEST_P(OcclusionQueriesTestES3, UnbindFBOWithPendingQuery)
+{
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Red());
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glViewport(0, 0, 64, 64);
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ GLQueryEXT query;
+ glBeginQuery(GL_ANY_SAMPLES_PASSED, query);
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+ glEndQuery(GL_ANY_SAMPLES_PASSED);
+
+ // Unbind user FBO by switching back to default framebuffer (0)
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ // FORCE the driver to commit the unbind and end the previous render pass.
+ // Because drivers often optimize glClear, issuing geometry (drawQuad)
+ // strictly guarantees a new render pass is initiated in the hardware.
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f);
+
+ // Retrieve query result
+ GLuint result = GL_FALSE;
+ glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_GL_TRUE(result);
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(OcclusionQueriesTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(OcclusionQueriesTestES3);
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page