Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker524639223
Fix commitde05d95e714f (angle/angle) +1/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • src/libANGLE/State.cpp
From de05d95e714fce4067f42129aac8ed406acb2e17 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
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 <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Geoff Lang <[email protected]>
---

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;
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
  • third_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:

  1. Target State Initialization: A compromised renderer creates a context. The attacker issues glGenFramebuffers(1, &F) and glBindFramebuffer(GL_DRAW_FRAMEBUFFER, F). GL_READ_FRAMEBUFFER remains bound to the default framebuffer.
    • State: mReadFramebuffer == default_fb, mDrawFramebuffer == F.
  2. Trigger Context Flush Workaround: The attacker dispatches the command glFlushDriverCachesCHROMIUM. On Android Adreno devices, unbind_egl_context_to_flush_driver_caches is active. This routes to GLES2DecoderPassthroughImpl::DoFlushDriverCachesCHROMIUM() (gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:4533).
  3. Initiating Rebind: The decoder executes:
    context_->ReleaseCurrent(nullptr);
    context_->MakeCurrent(surface_.get());
    
  4. ANGLE Thread Context Commitment: MakeCurrent routes to ANGLE’s Display::makeCurrent (third_party/angle/src/libANGLE/Display.cpp:1829). Crucially, ANGLE executes thread->setCurrent(context) before performing internal setup. The context is now thread-current.
  5. Nullifying Read Framebuffer: Display::makeCurrent calls Context::makeCurrent, which calls unsetDefaultFramebuffer(). Since mReadFramebuffer is default, it is set to nullptr (Context.cpp:9671). mDrawFramebuffer remains pointing to F.
    • State: mReadFramebuffer == nullptr, mDrawFramebuffer == F.
  6. Induced Failure: The attacker induces a failure during the subsequent setDefaultFramebuffer() call (e.g., via resource exhaustion or locking the surface). Context::makeCurrent early-returns an error.
  7. Ignored Return Value Bypass: Chrome’s GLContextEGL::MakeCurrentImpl returns false. However, back in DoFlushDriverCachesCHROMIUM(), the decoder ignores the return value of context_->MakeCurrent() and simply returns error::kNoError.
  8. Context Desynchronization: Because kNoError is returned, the decoder does not mark the context as lost and continues fetching commands. Meanwhile, because ANGLE already called thread->setCurrent(context) and lacks rollback logic on failure, ANGLE’s thread-local state retains the broken context.
  9. Framebuffer Deletion: The attacker sends glDeleteFramebuffers(1, &F). The decoder routes this directly to ANGLE via the cached api_ pointer.
  10. The Copy-Paste Defect Triggers: GL_DeleteFramebuffers accesses the active thread context (the broken one) and routes to Context::detachFramebuffer(F):
    // third_party/angle/src/libANGLE/Context.cpp:3381
    if (mState.removeDrawFramebufferBinding(framebuffer) && framebuffer.value != 0)
        bindDrawFramebuffer({0}); // ← Skipped!
    
    Because mReadFramebuffer is nullptr (from Step 5), removeDrawFramebufferBinding(F) short-circuits and returns false. bindDrawFramebuffer({0}) is skipped.
  11. Immediate Free: FramebufferManager::DeleteObject immediately executes delete framebuffer;. There is no reference counting.
  12. Dangling Pointer Hijack: mState.mDrawFramebuffer is a bare pointer (ANGLE is excluded from MiraclePtr) and now points to freed memory. The attacker sprays the GPU heap to reclaim the Framebuffer object with a fake vtable. Any subsequent GL command querying or using GL_DRAW_FRAMEBUFFER dereferences the dangling pointer, triggering a virtual-call hijack in the unsandboxed GPU process.

Suggested Fix

  1. In third_party/angle/src/libANGLE/State.cpp:3019, fix the copy-paste error by replacing mReadFramebuffer != nullptr with mDrawFramebuffer != nullptr.
  2. In gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:4538, check the return value of context_->MakeCurrent(). If it returns false, invoke MarkContextLost() 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_-&gt;ReleaseCurrent(nullptr);
    context_-&gt;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 the mReadFramebuffer != nullptr check resides inside removeDrawFramebufferBinding.
  • third_party/angle/src/libANGLE/Context.cpp:3381-3384: Confirmed bindDrawFramebuffer({0}) is strictly gated behind the truthy return of removeDrawFramebufferBinding(framebuffer).
  • third_party/angle/src/libANGLE/ResourceManager.cpp:399-403: Confirmed delete framebuffer; occurs immediately without reference counting mechanisms.
  • third_party/angle/src/libANGLE/State.h:1670-1671: Confirmed Framebuffer *mReadFramebuffer; Framebuffer *mDrawFramebuffer; are bare raw pointers. No raw_ptr usage exists here (MiraclePtr excluded).
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:4533-4541: Confirmed DoFlushDriverCachesCHROMIUM() calls context_->MakeCurrent(surface_.get()); but intentionally lacks a boolean evaluation or check, returning error::kNoError unconditionally.
  • third_party/angle/src/libANGLE/Display.cpp:1829: Confirmed thread->setCurrent(context); commits the context configuration to the ANGLE thread local storage prior to executing the falible context->makeCurrent() on line 1835. ANGLE_TRY causes an early return without rolling back thread assignment.
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc:996: Confirmed api_ is statically cached as a raw_ptr<gl::GLApi>. When MakeCurrentImpl returns false, Chromium’s GL context is dropped to NoContextGLApi, but the decoder’s command loop directly calls api()->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.

View on issue tracker