Critical watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Sub-componentGanesh
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker532941869
Fix commitb08789681f25 (skia) +28/-12
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-06
Patch gap16 days — fix public in source before the advisory

Background

FBO (Framebuffer Object)
a GL object (GrGLFramebuffer) that aggregates color/depth/stencil attachments as a render target; identified by a fboid and deleted with glDeleteFramebuffers.
Ganesh GL backend
Skia’s OpenGL rasterization backend (src/gpu/ganesh/gl/), where GrGLGpu issues GL calls that back canvas 2D and WebGL rendering in the GPU process.
Driver bug workaround
a per-driver conditional flag (declared in GrDriverBugWorkaroundsAutogen.h, gated by GrGLCaps) that alters Skia’s GL command stream to sidestep a known driver defect.
Imagination
the PowerVR GPU/driver vendor whose GL implementation retains an internal reference to the previously bound framebuffer.

Root Cause Analysis

On Imagination GL drivers the GL context keeps an internal, implicit reference to the framebuffer that was most recently bound as the “previous” framebuffer. When Skia calls glDeleteFramebuffers on an FBO that is still the context’s retained previous framebuffer, the driver frees the object but does not clear this internal reference, leaving a dangling pointer inside the driver. A later driver operation dereferences that stale reference, producing a use-after-free inside the GPU process. Skia’s existing deletion path in GrGLGpu::deleteFramebuffer only handled the case where the FBO equals fBoundDrawFramebuffer (via unbind_attachments_on_bound_render_fbo_delete), not the driver’s separate “previous framebuffer” retention. Because the dangling reference lives in driver-managed state rather than Skia’s tracked binding, Skia had no way to observe or invalidate it.

The fix forces bindFramebuffer(GR_GL_FRAMEBUFFER, 0) before the delete so the FBO about to be freed is no longer the context’s retained previous framebuffer.

Key insight
The hazard is not in Skia’s own binding bookkeeping but in a hidden, driver-side “previous framebuffer” reference; binding FBO 0 immediately before deletion displaces that reference so the freed object is never the one the driver later dereferences.

Attack Path

  1. Drive GPU rendering A web page uses canvas 2D or WebGL on an Imagination device, causing Skia’s Ganesh GL backend to create and bind FBOs in the GPU process.
  2. Establish retained reference Rendering binds a target FBO, making it the context’s internally retained “previous” framebuffer.
  3. Delete the referenced FBO Tearing down or recycling the render target reaches GrGLGpu::deleteFramebuffer, which (pre-fix) calls glDeleteFramebuffers while the driver still references that FBO.
  4. Trigger the dereference Subsequent GL activity causes the driver to dereference its now-dangling previous-framebuffer reference, reading or writing freed memory in the GPU process.
  5. Exploit the UAF With controlled allocation churn the freed slot can be reoccupied, turning the driver’s stale dereference into a corruption primitive in the GPU process.

Impact Assessment

This is a use-after-free in the GPU process reachable from ordinary canvas/WebGL rendering that creates and deletes FBOs, requiring no special privileges beyond loading a web page on an affected Imagination device. Because it corrupts driver-managed GPU memory, it can lead to GPU-process crashes and potentially memory corruption exploitable for further compromise. Exposure is limited to Imagination GL drivers, where the workaround is enabled.

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/gl/GrGLGpu.cpp
modified

Files Changed

  • include/gpu/ganesh/GrDriverBugWorkaroundsAutogen.h
  • src/gpu/ganesh/gl/GrGLCaps.cpp
  • src/gpu/ganesh/gl/GrGLGpu.cpp
  • src/gpu/gpu_workaround_list.txt

Audit Directions

  • Driver-retained implicit references
    Audit every glDelete* call site (framebuffers, textures, renderbuffers, VAOs) for objects the driver may still implicitly reference (previous/default binding, cached attachments) and unbind or rebind to a safe object before deletion.
  • Vendor-specific state retention
    Review other GrGLCaps driver checks for missing per-vendor workarounds where deletion order relative to binding matters, especially Imagination/PowerVR and other mobile GL drivers.
  • Bound-vs-previous binding gaps
    Where code guards only the currently bound object (e.g. fBoundDrawFramebuffer), verify it also accounts for “previously bound” or shadow bindings that survive an unbind in Skia’s tracking.
From b08789681f25af07a9b8ff639b1f6bf53953e54a Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
Date: Mon, 20 Jul 2026 15:42:34 -0400
Subject: [PATCH] [ganesh][gl] Imagination FBO deletion workaround

* Adds workaround for driver level bug where a context retains a
reference to a deleted FBO, resulting in a UAF hazard.

Bug: b/532941869
Change-Id: Ie6a1fb23fa0a72fd5da98489b1daced3409d9bd4
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1298697
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Thomas Smith <thomsmit@google.com>
---
diff --git a/include/gpu/ganesh/GrDriverBugWorkaroundsAutogen.h b/include/gpu/ganesh/GrDriverBugWorkaroundsAutogen.h
index ab6db1c..cdc0546 100644
--- a/include/gpu/ganesh/GrDriverBugWorkaroundsAutogen.h
+++ b/include/gpu/ganesh/GrDriverBugWorkaroundsAutogen.h
@@ -21,6 +21,8 @@
          disallow_large_instanced_draw)                 \
   GPU_OP(EMULATE_ABS_INT_FUNCTION,                      \
          emulate_abs_int_function)                      \
+  GPU_OP(ENSURE_PREVIOUS_FRAMEBUFFER_NOT_DELETED,       \
+         ensure_previous_framebuffer_not_deleted)       \
   GPU_OP(FLUSH_ON_FRAMEBUFFER_CHANGE,                   \
          flush_on_framebuffer_change)                   \
   GPU_OP(FORCE_UPDATE_SCISSOR_STATE_WHEN_BINDING_FBO0,  \
diff --git a/src/gpu/ganesh/gl/GrGLCaps.cpp b/src/gpu/ganesh/gl/GrGLCaps.cpp
index 9dc3137..48c28844 100644
--- a/src/gpu/ganesh/gl/GrGLCaps.cpp
+++ b/src/gpu/ganesh/gl/GrGLCaps.cpp
@@ -4786,6 +4786,12 @@
         fShaderCaps->fShaderDerivativeSupport = false;
     }
 
+    // b/532941869
+    if (ctxInfo.vendor() == GrGLVendor::kImagination ||
+        ctxInfo.driver() == GrGLDriver::kImagination) {
+        fDriverBugWorkarounds.ensure_previous_framebuffer_not_deleted = true;
+    }
+
     if (ctxInfo.driver() == GrGLDriver::kFreedreno) {
         formatWorkarounds->fDisallowUnorm16Transfers = true;
     }
diff --git a/src/gpu/ganesh/gl/GrGLGpu.cpp b/src/gpu/ganesh/gl/GrGLGpu.cpp
index ae7ba38..ff31c6e 100644
--- a/src/gpu/ganesh/gl/GrGLGpu.cpp
+++ b/src/gpu/ganesh/gl/GrGLGpu.cpp
@@ -3228,18 +3228,25 @@
     // We're relying on the GL state shadowing being correct in the workaround code below so we
     // need to handle a dirty context.
     this->handleDirtyContext();
-    if (fboid == fBoundDrawFramebuffer &&
-        this->caps()->workarounds().unbind_attachments_on_bound_render_fbo_delete) {
-        // This workaround only applies to deleting currently bound framebuffers
-        // on Adreno 420.  Because this is a somewhat rare case, instead of
-        // tracking all the attachments of every framebuffer instead just always
-        // unbind all attachments.
-        GL_CALL(FramebufferRenderbuffer(GR_GL_FRAMEBUFFER, GR_GL_COLOR_ATTACHMENT0,
-                                        GR_GL_RENDERBUFFER, 0));
-        GL_CALL(FramebufferRenderbuffer(GR_GL_FRAMEBUFFER, GR_GL_STENCIL_ATTACHMENT,
-                                        GR_GL_RENDERBUFFER, 0));
-        GL_CALL(FramebufferRenderbuffer(GR_GL_FRAMEBUFFER, GR_GL_DEPTH_ATTACHMENT,
-                                        GR_GL_RENDERBUFFER, 0));
+    if (fboid == fBoundDrawFramebuffer) {
+        if (this->caps()->workarounds().unbind_attachments_on_bound_render_fbo_delete) {
+            // This workaround only applies to deleting currently bound framebuffers
+            // on Adreno 420.  Because this is a somewhat rare case, instead of
+            // tracking all the attachments of every framebuffer instead just always
+            // unbind all attachments.
+            GL_CALL(FramebufferRenderbuffer(GR_GL_FRAMEBUFFER, GR_GL_COLOR_ATTACHMENT0,
+                                            GR_GL_RENDERBUFFER, 0));
+            GL_CALL(FramebufferRenderbuffer(GR_GL_FRAMEBUFFER, GR_GL_STENCIL_ATTACHMENT,
+                                            GR_GL_RENDERBUFFER, 0));
+            GL_CALL(FramebufferRenderbuffer(GR_GL_FRAMEBUFFER, GR_GL_DEPTH_ATTACHMENT,
+                                            GR_GL_RENDERBUFFER, 0));
+        }
+
+        if (this->caps()->workarounds().ensure_previous_framebuffer_not_deleted) {
+            // Some drivers keep an internal reference to the previously bound
+            // framebuffer, so make sure it isn't the one being deleted.
+            this->bindFramebuffer(GR_GL_FRAMEBUFFER, 0);
+        }
     }
 
     GL_CALL(DeleteFramebuffers(1, &fboid));
diff --git a/src/gpu/gpu_workaround_list.txt b/src/gpu/gpu_workaround_list.txt
index b0b38cf..a4c83b1 100644
--- a/src/gpu/gpu_workaround_list.txt
+++ b/src/gpu/gpu_workaround_list.txt
@@ -4,6 +4,7 @@
 disable_texture_storage
 disallow_large_instanced_draw
 emulate_abs_int_function
+ensure_previous_framebuffer_not_deleted
 flush_on_framebuffer_change
 force_update_scissor_state_when_binding_fbo0
 gl_clear_broken
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential driver Use-After-Free in Android GPU process via unmitigated Skia FBO deletion

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 Chromium driver-bug workaround for PowerVR/Imagination GPUs on Android mitigates a driver-level Use-After-Free when deleting currently bound Framebuffer Objects (FBOs). While this workaround (#471, ’ensure_previous_framebuffer_not_deleted’) is mitigated in the GLES2 command decoder, it is not implemented in Skia’s Ganesh GL backend. A compromised renderer could potentially exploit this missing check on the OOP-R path to trigger a driver-level Use-After-Free in the unsandboxed Android GPU process.

Affected files:

  • third_party/skia/src/gpu/ganesh/gl/GrGLGpu.cpp
  • third_party/skia/src/gpu/gpu_workaround_list.txt

Estimated timestamp from git blame: 2026-02-11

Root Cause Analysis

Chromium implements GPU-driver-bug workaround #471 (ensure_previous_framebuffer_not_deleted) to address a native GLES driver bug in PowerVR/Imagination GPUs on Android. The bug occurs when a Framebuffer Object (FBO) is deleted while currently bound to the context. Under these conditions, the driver retains an internal stale reference to the deleted FBO across subsequent glBindFramebuffer operations, resulting in a driver-internal Use-After-Free (UAF).

In the GLES2 command decoder, this is mitigated by implementing a complete-FBO sandwich on every binding transition (gpu/command_buffer/service/gles2_cmd_decoder.cc:4535-4543) and rebinding to FBO 0 before deletion (gles2_cmd_decoder.cc:12284-12293).

However, Skia’s Ganesh GL backend—which acts as an independent OpenGL producer on the same native context via SharedContextState—does not consume or implement this workaround.

In Chromium builds, Skia is compiled with SK_GPU_WORKAROUNDS_HEADER="gpu/config/gpu_driver_bug_workaround_autogen.h", which exposes ensure_previous_framebuffer_not_deleted inside the GrDriverBugWorkarounds class. However, Skia’s GL backend contains no consumer for this flag:

// third_party/skia/src/gpu/ganesh/gl/GrGLGpu.cpp:3219-3225
void GrGLGpu::bindFramebuffer(GrGLenum target, GrGLuint fboid) {
    GL_CALL(BindFramebuffer(target, fboid));   // Direct raw glBindFramebufferEXT call
    if (target == GR_GL_FRAMEBUFFER || target == GR_GL_DRAW_FRAMEBUFFER) {
        fBoundDrawFramebuffer = fboid;
    }
    this->onFBOChanged();
}
// third_party/skia/src/gpu/ganesh/gl/GrGLGpu.cpp:3227-3251
void GrGLGpu::deleteFramebuffer(GrGLuint fboid) {
    this->handleDirtyContext();
    if (fboid == fBoundDrawFramebuffer &&
        this->caps()->workarounds().unbind_attachments_on_bound_render_fbo_delete) {
        // Only checks the Adreno sibling workaround, not the Imagination one
        ...
    }
    GL_CALL(DeleteFramebuffers(1, &fboid));            // Deletes FBO while bound
    if (fboid == fBoundDrawFramebuffer) { this->onFBOChanged(); }
}

Because create_gr_gl_interface.cc maps these calls directly to the raw native GLES driver entrypoints (glBindFramebufferEXT and glDeleteFramebuffersEXT), the safety mechanisms in the GLES2 command decoder are entirely bypassed.

Potential Trigger Path

Because our tooling does not currently have the capability to run code, this represents a potential trigger path analyzed directly from the source code:

  1. A compromised renderer initiates OOP-R rasterization on a renderable SharedImage by invoking BeginRasterCHROMIUM via RasterInterface IPC.
  2. In the GPU process, the raster decoder initiates scoped write access (SkiaGLImageRepresentation::BeginWriteAccess), which calls SkSurfaces::WrapBackendTexture to create a surface.
  3. Skia’s GL backend allocates the backing FBOs in GrGLGpu::createRenderTargetObjects and binds the FBO (FBO_A) via GrGLGpu::bindFramebuffer. It marks FBO ownership as kOwned.
  4. Raster operations are written to FBO_A. The framebuffer remains bound to the active GLES context.
  5. The renderer issues EndRasterCHROMIUM, destroying the SkiaGLImageRepresentation wrapper.
  6. Releasing the last reference to the wrapped SkSurface triggers GrGLRenderTarget::onRelease(), which invokes GrGLGpu::deleteFramebuffer(FBO_A).
  7. GrGLGpu::deleteFramebuffer invokes raw glDeleteFramebuffersEXT on FBO_A while it is still active. The buggy PowerVR driver deletes the FBO but retains a stale internal reference.
  8. The renderer performs heap-grooming operations on the GPU context to reclaim the freed FBO structure’s memory.
  9. The renderer performs a subsequent rasterization or draw command, causing Skia to invoke raw glBindFramebufferEXT(GR_GL_FRAMEBUFFER, FBO_B). During this state transition, the driver dereferences the stale pointer, potentially resulting in an out-of-bounds read/write or Use-After-Free (UAF) in the unsandboxed GPU process on Android.

Suggested Fix

In GrGLGpu::deleteFramebuffer, check if the ensure_previous_framebuffer_not_deleted workaround is enabled. If the FBO being deleted is currently bound (fboid == fBoundDrawFramebuffer), bind to FBO 0 before executing the deletion.

For example:

if (fboid == fBoundDrawFramebuffer &&
    this->caps()->workarounds().ensure_previous_framebuffer_not_deleted) {
    this->bindFramebuffer(GR_GL_FRAMEBUFFER, 0);
}

Additionally, the workaround string ensure_previous_framebuffer_not_deleted should be added to Skia’s standalone workaround list in third_party/skia/src/gpu/gpu_workaround_list.txt.

Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85


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