CVE-2026-87488
Overview
Background
- ANGLE
- Chrome’s translation layer that maps WebGL/OpenGL ES calls onto the platform’s native graphics driver, here the OpenGL (
gl) backend. - `glBlitFramebuffer`
- an OpenGL operation that copies a rectangular region of pixels from a read framebuffer into a draw (destination) framebuffer, invoked in ANGLE via
functions->blitFramebuffer(...). - Base level
- the lowest mipmap level of a texture selected as usable, exposed as
Texture::getBaseLevel(); a value greater than zero means level 0 is excluded from the sampling/attachment range. - `glFinish`
- a synchronization command that blocks until all previously issued GPU commands have fully completed, exposed as
functions->finish().
Root Cause Analysis
The vulnerable path is ANGLE’s GL backend blit in FramebufferGL.cpp, which called functions->blitFramebuffer(...) into a destination framebuffer that either had multiple color attachments or a color texture attachment whose getBaseLevel() was greater than zero. On Mali GPUs the driver’s asynchronous handling of these multi-attachment or non-zero-base-level blits violated the invariant that the blit’s referenced backing memory stays valid and correctly ordered for the duration of the operation, leaving the driver operating on framebuffer/texture storage that could be freed or reused out of order. Because ANGLE issued the blit without forcing the prior work to complete, the Mali driver could dereference stale backing storage, producing a use-after-free in the GPU-process context.
The fix detects these two exact conditions and inserts a functions->finish() before the blit so all outstanding commands settle and no attachment memory is reclaimed while the driver still references it. The workaround is gated behind the new finishBeforeBlitFramebufferMultiAttachment feature, enabled only when isMali is true, matching the driver quirk to the affected hardware.
glBlitFramebuffer into a draw framebuffer with multiple color attachments or a non-zero-base-level texture attachment; the fix forces a functions->finish() in exactly those cases so the buggy asynchronous path completes before any attachment storage can be freed.Attack Path
- Set up the offending framebuffer A malicious page uses WebGL to create a draw framebuffer with either multiple color attachments or a color texture attachment whose base level is greater than zero.
- Trigger the blit
The page issues a
blitFramebuffertargeting that framebuffer, driving ANGLE down theFramebufferGL.cpppath that callsfunctions->blitFramebuffer(...)on a Mali GPU. - Exploit the async driver flaw
Because no
finish()was issued, the Mali driver processes the blit while related framebuffer/texture backing memory is freed or reused, causing a dangling reference. - Groom and reuse freed memory The attacker arranges further WebGL allocations so the freed storage is reclaimed with attacker-influenced contents before or during the driver’s stale access.
Impact Assessment
isMali) and a draw framebuffer that has multiple color attachments or a color texture attachment with base level greater than zero. Exploitation of the resulting memory corruption could lead to further compromise of the GPU process, though the diff itself only establishes the use-after-free condition.Files Changed
include/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/libANGLE/renderer/gl/FramebufferGL.cppsrc/libANGLE/renderer/gl/renderergl_utils.cpputil/autogen/angle_features_autogen.cpputil/autogen/angle_features_autogen.h
Audit Directions
- Driver-quirk workarounds gated by feature flagsAudit other ANGLE GL-backend operations where a known-buggy vendor driver path is fixed only by a synchronization call; missing
finish/flushinsertions around asynchronous blits, copies, or deletes are recurring sources of use-after-free. - Multi-attachment and non-zero base-level framebuffersReview code paths that touch draw framebuffers with multiple color attachments or attachments whose
getBaseLevel() > 0, since these less-common configurations are frequently mishandled by native drivers and under-tested in ANGLE. - Lifetime of attachment backing storage across native GL callsTrace where texture and framebuffer memory can be freed or reused while a still-in-flight native GL command references it, and confirm ordering guarantees exist before the backing store is released.
Patch
From 8efd15f71c27cd0bc2a9cf0074d77e899ca9c448 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Fri, 28 Aug 2026 19:33:51 -0700
Subject: [PATCH] GL: add finish-before-multi-attachment-blit workaround.
If performing glBlitFramebuffer to a draw framebuffer which either has
multiple color attachments, or has a color texture attachment with
base level greater than zero, glFinish() as a workaround. Apply this
to Mali GPUs.
It wasn't feasible to add a regression test for this workaround.
Bug: chromium:546252753
Change-Id: I3ef79ac6f0d25529bd22db598262704d2056802b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8311814
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index a42c2950..44df912 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -788,6 +788,12 @@
&members,
};
+ FeatureInfo finishBeforeBlitFramebufferMultiAttachment = {
+ "finishBeforeBlitFramebufferMultiAttachment",
+ FeatureCategory::OpenGLWorkarounds,
+ &members,
+ };
+
};
inline FeaturesGL::FeaturesGL() = default;
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index 276028d..06b31aa 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -1036,6 +1036,15 @@
"Validate applied driver state frequently."
],
"issue": "http://anglebug.com/532176406"
+ },
+ {
+ "name": "finish_before_blit_framebuffer_multi_attachment",
+ "category": "Workarounds",
+ "description": [
+ "Issue a glFinish before calling glBlitFramebuffer if the draw framebuffer has ",
+ "multiple color attachments or any color attachment's texture has base_level > 0."
+ ],
+ "issue": "http://crbug.com/546252753"
}
]
}
diff --git a/src/libANGLE/renderer/gl/FramebufferGL.cpp b/src/libANGLE/renderer/gl/FramebufferGL.cpp
index 625eefc..ca7caee 100644
--- a/src/libANGLE/renderer/gl/FramebufferGL.cpp
+++ b/src/libANGLE/renderer/gl/FramebufferGL.cpp
@@ -14,6 +14,7 @@
#include "libANGLE/ErrorStrings.h"
#include "libANGLE/FramebufferAttachment.h"
#include "libANGLE/State.h"
+#include "libANGLE/Texture.h"
#include "libANGLE/angletypes.h"
#include "libANGLE/formatutils.h"
#include "libANGLE/queryconversions.h"
@@ -975,6 +976,36 @@
}
}
+ if (features.finishBeforeBlitFramebufferMultiAttachment.enabled)
+ {
+ bool needFinish = false;
+ if (destFramebuffer->getState().getColorAttachmentsMask().count() > 1)
+ {
+ needFinish = true;
+ }
+ else
+ {
+ for (size_t colorIndex : destFramebuffer->getState().getColorAttachmentsMask())
+ {
+ const FramebufferAttachment *attachment =
+ destFramebuffer->getColorAttachment(colorIndex);
+ if (attachment && attachment->type() == GL_TEXTURE)
+ {
+ const Texture *texture = attachment->getTexture();
+ if (texture && texture->getBaseLevel() > 0)
+ {
+ needFinish = true;
+ break;
+ }
+ }
+ }
+ }
+ if (needFinish)
+ {
+ functions->finish();
+ }
+ }
+
functions->blitFramebuffer(finalSourceArea.x, finalSourceArea.y, finalSourceArea.x1(),
finalSourceArea.y1(), finalDestArea.x, finalDestArea.y,
finalDestArea.x1(), finalDestArea.y1(), blitMask, filter);
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index 8b20945..ba1f399 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2660,6 +2660,9 @@
// http://crbug.com/534468209
ANGLE_FEATURE_CONDITION(features, flushQueriesBeforeDeletingOrUnbindingFbo, isMali);
+ // http://crbug.com/546252753
+ ANGLE_FEATURE_CONDITION(features, finishBeforeBlitFramebufferMultiAttachment, isMali);
+
// https://crbug.com/40264674
ANGLE_FEATURE_CONDITION(features, disableClipControl, IsMaliG72OrG76OrG51(functions));
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index e59279f..9d0963f 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -185,6 +185,7 @@
{Feature::ExposeES32ForTesting, "exposeES32ForTesting"},
{Feature::ExposeNonConformantExtensionsAndVersions, "exposeNonConformantExtensionsAndVersions"},
{Feature::ExternallySynchronizePipelineCacheAccess, "externallySynchronizePipelineCacheAccess"},
+ {Feature::FinishBeforeBlitFramebufferMultiAttachment, "finishBeforeBlitFramebufferMultiAttachment"},
{Feature::FinishDoesNotCauseQueriesToBeAvailable, "finishDoesNotCauseQueriesToBeAvailable"},
{Feature::FlushAfterEndingTransformFeedback, "flushAfterEndingTransformFeedback"},
{Feature::FlushAfterStreamVertexData, "flushAfterStreamVertexData"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index bca995a..8fbc650 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -185,6 +185,7 @@
ExposeES32ForTesting,
ExposeNonConformantExtensionsAndVersions,
ExternallySynchronizePipelineCacheAccess,
+ FinishBeforeBlitFramebufferMultiAttachment,
FinishDoesNotCauseQueriesToBeAvailable,
FlushAfterEndingTransformFeedback,
FlushAfterStreamVertexData,