Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Bug ClassUAF
Tracker532941869
Fix commitb08789681f25 (skia) +28/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

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
From b08789681f25af07a9b8ff639b1f6bf53953e54a Mon Sep 17 00:00:00 2001
From: Thomas Smith <[email protected]>
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 <[email protected]>
Commit-Queue: Thomas Smith <[email protected]>
---

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 [email protected]

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