CVE-2026-19154
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/gl/GrGLGpu.cpp |
modified |
Files Changed
include/gpu/ganesh/GrDriverBugWorkaroundsAutogen.hsrc/gpu/ganesh/gl/GrGLCaps.cppsrc/gpu/ganesh/gl/GrGLGpu.cppsrc/gpu/gpu_workaround_list.txt
Patch
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
Original Bug Report
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.cppthird_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:
- A compromised renderer initiates OOP-R rasterization on a renderable SharedImage by invoking
BeginRasterCHROMIUMviaRasterInterfaceIPC. - In the GPU process, the raster decoder initiates scoped write access (
SkiaGLImageRepresentation::BeginWriteAccess), which callsSkSurfaces::WrapBackendTextureto create a surface. - Skia’s GL backend allocates the backing FBOs in
GrGLGpu::createRenderTargetObjectsand binds the FBO (FBO_A) viaGrGLGpu::bindFramebuffer. It marks FBO ownership askOwned. - Raster operations are written to
FBO_A. The framebuffer remains bound to the active GLES context. - The renderer issues
EndRasterCHROMIUM, destroying theSkiaGLImageRepresentationwrapper. - Releasing the last reference to the wrapped
SkSurfacetriggersGrGLRenderTarget::onRelease(), which invokesGrGLGpu::deleteFramebuffer(FBO_A). GrGLGpu::deleteFramebufferinvokes rawglDeleteFramebuffersEXTonFBO_Awhile it is still active. The buggy PowerVR driver deletes the FBO but retains a stale internal reference.- The renderer performs heap-grooming operations on the GPU context to reclaim the freed FBO structure’s memory.
- 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.