CVE-2026-79235
Overview
Background
- ANGLE `StateManagerGL`
- ANGLE’s GL-backend state tracker that mediates framebuffer (FBO) binding/deletion and query begin/end calls to the underlying native GL driver.
- GPU query / pending job
- an asynchronous operation (e.g.
GL_ANY_SAMPLES_PASSEDocclusion query) whose result and referenced resources may still be in-flight on the GPU after the API call returns. - FBO unbind/delete
bindFramebuffer/deleteFramebuffers, which on ARM Mali (Valhall/Avalon) can be processed by the driver while a query/job that references that FBO is still pending.- `forcefullyFlush()` / `glFenceSync`
- a new synchronization primitive that pushes and waits on pending command buffers (falling back to
glFinish()), ensuring outstanding queries complete before the FBO is torn down.
Root Cause Analysis
On ARM Mali Valhall/Avalon drivers, unbinding or deleting a user framebuffer while a GPU query/job that references it is still pending leaves the driver holding a dangling reference to the now-torn-down framebuffer, which it later dereferences when the query result is retrieved — a driver-side use-after-free.
Pre-patch, StateManagerGL::deleteFramebuffer() and bindFramebuffer() performed the delete/unbind without any guarantee that outstanding queries had been flushed, so the reordering of teardown before query completion violated the invariant that a framebuffer outlives all GPU work referencing it.
The fix adds mHasUnflushedQueries tracking (set in the query begin/end paths, cleared in onSyncedFlushOrFinish()), and, under the new flushQueriesBeforeDeletingOrUnbindingFbo feature (enabled on Mali), calls forcefullyFlush() before deleting an unbound FBO and after an actual framebuffer change, forcing pending queries/jobs to complete first. Because the flush drains the referencing work before the framebuffer memory is released or rebound, the driver no longer dereferences a freed object. The added DeleteFBOWithPendingQuery / UnbindFBOWithPendingQuery tests exercise exactly this delete/unbind-then-glGetQueryObjectuiv sequence.
forcefullyFlush()) whenever there are unflushed queries and an FBO is deleted or the framebuffer binding changes.Attack Path
- Set up an FBO with a pending query
WebGL/WebGL2 content binds a user framebuffer, begins a query (
glBeginQuery(GL_ANY_SAMPLES_PASSED, ...)), issues a draw, and ends the query, leaving the result and referenced FBO pending on the GPU. - Delete or unbind the FBO before completion
The content deletes the FBO or rebinds to another/default framebuffer while the query is still pending, so pre-patch
deleteFramebuffer()/bindFramebuffer()tears it down with no flush. - Force the driver to touch the stale reference
The content retrieves the query result (
glGetQueryObjectuiv), causing the Mali driver to process the pending job that still references the freed framebuffer, triggering a use-after-free in the driver. - Crash / memory corruption The dangling dereference crashes the GPU process, with potential for further memory corruption in the driver (inferred; the patch demonstrates only the driver crash it works around).
Impact Assessment
flushQueriesBeforeDeletingOrUnbindingFbo feature applies; no exploited-in-the-wild status is stated in the advisory.Files Changed
src/libANGLE/renderer/gl/StateManagerGL.cpputil/autogen/angle_features_autogen.hsrc/libANGLE/renderer/gl/StateManagerGL.hsrc/libANGLE/renderer/gl/renderergl_utils.cppinclude/platform/autogen/FeaturesGL_autogen.hsrc/tests/gl_tests/OcclusionQueriesTest.cpputil/autogen/angle_features_autogen.cppsrc/libANGLE/renderer/gl/RendererGL.cppinclude/platform/gl_features.json
Audit Directions
- Resource teardown vs. pending GPU workAudit every path that deletes or unbinds GL resources (FBOs, textures, renderbuffers) for guarantees that in-flight queries/jobs referencing them have been flushed first, especially in driver-specific workaround gates.
- Async query lifetimeLook for query begin/end tracking that does not enforce completion before dependent objects are freed; the
mHasUnflushedQueriespattern shows where such tracking was previously missing. - Driver-specific reordering hazardsReview other Mali/vendor workarounds (
flushOnFramebufferChange,flushBeforeDeleteTextureIfCopiedTo) for analogous cases where a flush is conditionally skipped and a dangling driver reference can result.
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/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); void bindTransformFeedback(GLenum type, GLuint transformFeedback); void onTransformFeedbackStateChange(); @@ -585,6 +588,7 @@ const bool mIndependentBlendStates; bool mSampleCoverageEverChanged; + bool mHasUnflushedQueries; const bool mFramebufferSRGBAvailable; const bool mHasSeparateFramebufferBindings; diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp index e3b2a48..08284c3 100644 --- a/src/libANGLE/renderer/gl/renderergl_utils.cpp +++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp @@ -2679,6 +2679,9 @@ // https://crbug.com/1356053 ANGLE_FEATURE_CONDITION(features, bindCompleteFramebufferForTimerQueries, isMali); + // http://crbug.com/534468209 + ANGLE_FEATURE_CONDITION(features, flushQueriesBeforeDeletingOrUnbindingFbo, isMali); + // https://crbug.com/40264674 ANGLE_FEATURE_CONDITION(features, disableClipControl, IsMaliG72OrG76OrG51(functions)); 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); 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/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp index bbadf28..200856d 100644 --- a/util/autogen/angle_features_autogen.cpp +++ b/util/autogen/angle_features_autogen.cpp @@ -188,6 +188,7 @@ {Feature::FlushAfterStreamVertexData, "flushAfterStreamVertexData"}, {Feature::FlushBeforeDeleteTextureIfCopiedTo, "flushBeforeDeleteTextureIfCopiedTo"}, {Feature::FlushOnFramebufferChange, "flushOnFramebufferChange"}, + {Feature::FlushQueriesBeforeDeletingOrUnbindingFbo, "flushQueriesBeforeDeletingOrUnbindingFbo"}, {Feature::ForceAtomicValueResolution, "forceAtomicValueResolution"}, {Feature::ForceBufferGPUStorage, "forceBufferGPUStorage"}, {Feature::ForceContinuousRefreshOnSharedPresent, "forceContinuousRefreshOnSharedPresent"}, diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h index fa23735..56a97bc 100644 --- a/util/autogen/angle_features_autogen.h +++ b/util/autogen/angle_features_autogen.h @@ -188,6 +188,7 @@ FlushAfterStreamVertexData, FlushBeforeDeleteTextureIfCopiedTo, FlushOnFramebufferChange, + FlushQueriesBeforeDeletingOrUnbindingFbo, ForceAtomicValueResolution, ForceBufferGPUStorage, ForceContinuousRefreshOnSharedPresent,
Regression Test / PoC
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);