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 <[email protected]> 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 <[email protected]> Reviewed-by: Kenneth Russell <[email protected]> Commit-Queue: Kenneth Russell <[email protected]> --- 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