CVE-2026-17891
Overview
Files Changed
src/libANGLE/State.cpp
Patch
From de05d95e714fce4067f42129aac8ed406acb2e17 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Fri, 26 Jun 2026 16:21:43 +0200
Subject: [PATCH] Fix incorrect null check in removeDrawFramebufferBinding
removeDrawFramebufferBinding was checking the draw framebuffer instead
of the read buffer.
It does not appear possible to get into the state where mDrawFramebuffer
or mReadFramebuffer are null during normal execution though.
Fixed: chromium:524639223
Change-Id: Iba49febee4434551a4605c5b3b0893a02218261a
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8012109
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/libANGLE/State.cpp b/src/libANGLE/State.cpp
index 70d56ba..c49645c 100644
--- a/src/libANGLE/State.cpp
+++ b/src/libANGLE/State.cpp
@@ -3016,7 +3016,7 @@
bool State::removeDrawFramebufferBinding(FramebufferID framebuffer)
{
- if (mReadFramebuffer != nullptr && mDrawFramebuffer->id() == framebuffer)
+ if (mDrawFramebuffer != nullptr && mDrawFramebuffer->id() == framebuffer)
{
setDrawFramebufferBinding(nullptr);
return true;
Original Bug Report
Potential UAF in ANGLE due to copy-paste bug in State::removeDrawFramebufferBinding
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 copy-paste error in ANGLE’s State::removeDrawFramebufferBinding incorrectly null-checks mReadFramebuffer instead of mDrawFramebuffer. On Android devices with Adreno GPUs, a compromised renderer can exploit an ignored MakeCurrent failure in the command decoder to bypass context loss, leaving mDrawFramebuffer dangling after object deletion and leading to a potential Use-After-Free in the unsandboxed GPU process.
Affected files:
third_party/angle/src/libANGLE/State.cppgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.ccthird_party/angle/src/libANGLE/Context.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
1. Summary of the Issue (Meant for Human Triage)
An architectural copy-paste defect exists in ANGLE’s front-end state management. Specifically, in State::removeDrawFramebufferBinding (located in third_party/angle/src/libANGLE/State.cpp), the function erroneously performs a null check on mReadFramebuffer rather than mDrawFramebuffer.
Consequently, if mReadFramebuffer is nullptr while mDrawFramebuffer points to a user-defined framebuffer, removing the framebuffer binding fails and short-circuits. This skips the binding update sequence in Context::detachFramebuffer (skipping bindDrawFramebuffer({0})), and leaves mState.mDrawFramebuffer pointing to the freed memory when the framebuffer is deleted.
Because ANGLE resides outside the Chrome MiraclePtr rewriter scope, these pointers are bare C++ Framebuffer* references, making them prone to Use-After-Free (UAF). While normal GL operations ensure mReadFramebuffer is non-null, this constraint can be subverted on Android devices using Adreno GPUs via the glFlushDriverCachesCHROMIUM command. During this flush, the GLES2 passthrough decoder ignores the return value of a failed context re-binding (MakeCurrent()). Because ANGLE has already committed the thread-local context before the failure, and the Chromium decoder ignores the failure, the attacker can continue to dispatch GL commands to an inconsistent ANGLE context where mReadFramebuffer == nullptr. This exposes a direct path to a GPU process UAF, which is unsandboxed on Android, yielding a potential virtual-call hijack and Unsandboxed RCE.
2. Proof-of-Concept & Detailed Execution Flow
Disclaimer: The following steps are potential steps derived from deep static analysis of the codebase execution flow. Our tooling agent does not run code, so a working runnable PoC has not been executed yet.
The flaw lies in State::removeDrawFramebufferBinding inside third_party/angle/src/libANGLE/State.cpp:3017:
bool State::removeDrawFramebufferBinding(FramebufferID framebuffer) {
// ← Defect: Checks mReadFramebuffer, but dereferences mDrawFramebuffer
if (mReadFramebuffer != nullptr && mDrawFramebuffer->id() == framebuffer) {
setDrawFramebufferBinding(nullptr);
return true;
}
return false;
}
Potential Execution Flow to UAF:
- Target State Initialization: A compromised renderer creates a context. The attacker issues
glGenFramebuffers(1, &F)andglBindFramebuffer(GL_DRAW_FRAMEBUFFER, F).GL_READ_FRAMEBUFFERremains bound to the default framebuffer.- State:
mReadFramebuffer == default_fb,mDrawFramebuffer == F.
- State:
- Trigger Context Flush Workaround: The attacker dispatches the command
glFlushDriverCachesCHROMIUM. On Android Adreno devices,unbind_egl_context_to_flush_driver_cachesis active. This routes toGLES2DecoderPassthroughImpl::DoFlushDriverCachesCHROMIUM()(gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:4533). - Initiating Rebind: The decoder executes:
context_->ReleaseCurrent(nullptr); context_->MakeCurrent(surface_.get()); - ANGLE Thread Context Commitment:
MakeCurrentroutes to ANGLE’sDisplay::makeCurrent(third_party/angle/src/libANGLE/Display.cpp:1829). Crucially, ANGLE executesthread->setCurrent(context)before performing internal setup. The context is now thread-current. - Nullifying Read Framebuffer:
Display::makeCurrentcallsContext::makeCurrent, which callsunsetDefaultFramebuffer(). SincemReadFramebufferis default, it is set tonullptr(Context.cpp:9671).mDrawFramebufferremains pointing toF.- State:
mReadFramebuffer == nullptr,mDrawFramebuffer == F.
- State:
- Induced Failure: The attacker induces a failure during the subsequent
setDefaultFramebuffer()call (e.g., via resource exhaustion or locking the surface).Context::makeCurrentearly-returns an error. - Ignored Return Value Bypass: Chrome’s
GLContextEGL::MakeCurrentImplreturnsfalse. However, back inDoFlushDriverCachesCHROMIUM(), the decoder ignores the return value ofcontext_->MakeCurrent()and simply returnserror::kNoError. - Context Desynchronization: Because
kNoErroris returned, the decoder does not mark the context as lost and continues fetching commands. Meanwhile, because ANGLE already calledthread->setCurrent(context)and lacks rollback logic on failure, ANGLE’s thread-local state retains the broken context. - Framebuffer Deletion: The attacker sends
glDeleteFramebuffers(1, &F). The decoder routes this directly to ANGLE via the cachedapi_pointer. - The Copy-Paste Defect Triggers:
GL_DeleteFramebuffersaccesses the active thread context (the broken one) and routes toContext::detachFramebuffer(F):Because// third_party/angle/src/libANGLE/Context.cpp:3381 if (mState.removeDrawFramebufferBinding(framebuffer) && framebuffer.value != 0) bindDrawFramebuffer({0}); // ← Skipped!mReadFramebufferisnullptr(from Step 5),removeDrawFramebufferBinding(F)short-circuits and returnsfalse.bindDrawFramebuffer({0})is skipped. - Immediate Free:
FramebufferManager::DeleteObjectimmediately executesdelete framebuffer;. There is no reference counting. - Dangling Pointer Hijack:
mState.mDrawFramebufferis a bare pointer (ANGLE is excluded from MiraclePtr) and now points to freed memory. The attacker sprays the GPU heap to reclaim theFramebufferobject with a fake vtable. Any subsequent GL command querying or usingGL_DRAW_FRAMEBUFFERdereferences the dangling pointer, triggering a virtual-call hijack in the unsandboxed GPU process.
Suggested Fix
- In
third_party/angle/src/libANGLE/State.cpp:3019, fix the copy-paste error by replacingmReadFramebuffer != nullptrwithmDrawFramebuffer != nullptr. - In
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:4538, check the return value ofcontext_->MakeCurrent(). If it returns false, invokeMarkContextLost()to safely tear down the command buffer and prevent subsequent commands from executing on a broken state.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
Critic model verdict (2026-06-15 23:50:09):
* **Severity:** High (S1)
* **Brief Notes / Reasoning:**
The vulnerability report accurately identifies a copy-paste defect in `State::removeDrawFramebufferBinding` (null-checking `mReadFramebuffer` instead of `mDrawFramebuffer`), which leads to a dangling pointer and Use-After-Free if `mReadFramebuffer` is `nullptr`.
The Critic Notes incorrectly rated this as Bug (S3) by claiming that if `Context::makeCurrent()` fails, Chrome's command buffer marks the context as lost and halts all further command processing. However, the Critic missed a crucial bypass via the `glFlushDriverCachesCHROMIUM` command.
On Android devices with Adreno GPUs (a massive portion of the ecosystem), the `unbind_egl_context_to_flush_driver_caches` workaround is enabled. When a compromised renderer dispatches `FlushDriverCachesCHROMIUM`, `GLES2DecoderPassthroughImpl::DoFlushDriverCachesCHROMIUM()` executes:
```cpp
context_->ReleaseCurrent(nullptr);
context_->MakeCurrent(surface_.get());
```
Crucially, this function **ignores the return value** of `MakeCurrent()` and returns `error::kNoError`. If an attacker induces an `eglMakeCurrent` failure (e.g., via resource exhaustion) during this sequence:
1. `MakeCurrentImpl` returns `false` but fails to set `lost_ = true`.
2. Chrome's thread-local GL API is cleared, but the decoder's cached `api_` pointer still correctly points to the `GLApi` implementation.
3. Because `kNoError` is returned, `DoCommandsImpl` blindly continues processing subsequent commands in the ring buffer.
4. The attacker's next command, `glDeleteFramebuffers`, is routed to ANGLE via the cached `api_`.
5. ANGLE's `Display::makeCurrent` does not revert `thread->setCurrent(context)` on failure, meaning the ANGLE thread-local context is still active.
6. ANGLE executes `deleteFramebuffer`, where `mReadFramebuffer` is `nullptr` (left over from the failed `makeCurrent`), triggering the UAF exactly as described in the report.
Since the GPU process is unsandboxed on Android, this provides a virtual-call primitive leading to Unsandboxed RCE (Critical / S0 ceiling). However, because reliably inducing an `eglMakeCurrent` failure without crashing the GPU process requires an unusual precondition (e.g., tight racing or specific memory exhaustion), the severity is appropriately dropped from Critical to High (S1) per the severity guidelines. Probability of exploitability is ~95%.
Codebase Investigator Audit Logs:
third_party/angle/src/libANGLE/State.cpp:3017-3026: Confirmed themReadFramebuffer != nullptrcheck resides insideremoveDrawFramebufferBinding.third_party/angle/src/libANGLE/Context.cpp:3381-3384: ConfirmedbindDrawFramebuffer({0})is strictly gated behind the truthy return ofremoveDrawFramebufferBinding(framebuffer).third_party/angle/src/libANGLE/ResourceManager.cpp:399-403: Confirmeddelete framebuffer;occurs immediately without reference counting mechanisms.third_party/angle/src/libANGLE/State.h:1670-1671: ConfirmedFramebuffer *mReadFramebuffer; Framebuffer *mDrawFramebuffer;are bare raw pointers. Noraw_ptrusage exists here (MiraclePtr excluded).gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:4533-4541: ConfirmedDoFlushDriverCachesCHROMIUM()callscontext_->MakeCurrent(surface_.get());but intentionally lacks a boolean evaluation or check, returningerror::kNoErrorunconditionally.third_party/angle/src/libANGLE/Display.cpp:1829: Confirmedthread->setCurrent(context);commits the context configuration to the ANGLE thread local storage prior to executing the faliblecontext->makeCurrent()on line 1835.ANGLE_TRYcauses an early return without rolling back thread assignment.gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc:996: Confirmedapi_is statically cached as araw_ptr<gl::GLApi>. WhenMakeCurrentImplreturns false, Chromium’s GL context is dropped toNoContextGLApi, but the decoder’s command loop directly callsapi()->glDeleteFramebuffersEXTFn, executing ANGLE methods out-of-sync from Chromium’s tracked safety state.
Evaluated with Chrome root at commit: 8c517fbcbb533e59ec9cedac868c8a9bdc30beb2
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.