CVE-2026-102319
Overview
Background
- SharedImage cleared state
- a per-
SharedImageflag (IsCleared()/SetClearedRect()) that records whether a texture’s backing memory has been fully initialized, gating whether later reads are allowed to expose its contents. - Ganesh-GL
- Skia’s GL-backed GPU backend, whose texture-upload path (used by
DoWritePixelsINTERNALandWritePixelsYUV) does not itself inspectglGetError()after issuing uploads. - `glGetError()` / error latching
- GL drivers queue error codes (e.g.
GL_OUT_OF_MEMORY) that persist until drained, so an upload can silently fail while the calling code still believes it succeeded. - `DrainGLErrors`
- a new helper in
gl_utils.hthat empties the GL error queue and returns the first error encountered, orGL_NO_ERRORif the queue was empty.
Root Cause Analysis
In the raster upload paths, DoWritePixelsINTERNAL and WritePixelsYUV marked the destination SharedImage as cleared via SetClearedRect() whenever the Skia upload call returned nominal success. On Ganesh-GL, however, Skia does not check glGetError(), so a driver under memory pressure could latch GL_OUT_OF_MEMORY and drop the actual texture upload while still reporting success to the caller. This violated the invariant that a SharedImage is only marked cleared once its backing memory is genuinely initialized: the image was flagged cleared despite holding uninitialized texture memory, which could then be legitimately read back.
The fix drains any pre-existing GL errors before the upload and, when running on Ganesh-GL without context loss, re-checks the error queue with DrainGLErrors after the upload; if a real error occurred, the upload is treated as failed (returning a GLError / LOCAL_SET_GL_ERROR) and the SharedImage is left uncleared. This works because a still-uncleared SharedImage is subsequently zero-initialized before it can be read, closing the disclosure.
glGetError(), so a silently-dropped upload marked uninitialized memory as cleared; the fix bridges that gap by explicitly draining and checking driver errors before setting the cleared flag.Attack Path
- Induce GPU memory pressure
An attacker with access to the GPU raster path (via renderer-driven canvas/2D or media uploads) drives the GL driver into a low-memory state so subsequent texture uploads can be dropped with a latched
GL_OUT_OF_MEMORY. (inferred) - Trigger an upload into an uncleared SharedImage
A
WritePixels/WritePixelsYUVoperation targets a not-yet-cleared destinationSharedImage, and the Ganesh-GL upload silently fails while returning success. - False clear
Pre-patch,
SetClearedRect()runs on the nominal success, marking theSharedImagecleared even though its backing texture was never written. - Read back uninitialized memory
The attacker reads the
SharedImageback through a normal path, obtaining the contents of uninitialized GPU texture memory. (inferred)
Impact Assessment
SharedImage to be treated as initialized and read back uninitialized GPU texture memory, an information-disclosure primitive in the GPU process. The advisory classifies this as an uninitialized-resource issue in GPU with no exploited-in-the-wild claim stated; any use of the leaked contents to defeat memory-layout defenses is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/copy_shared_image_helper.cc |
modified | |
whilegpu/command_buffer/service/gl_utils.h |
modified | |
ifgpu/command_buffer/service/raster_decoder.cc |
modified | |
forgpu/command_buffer/service/shared_image/egl_image_backing.cc |
modified |
Files Changed
gpu/command_buffer/service/gl_utils.hgpu/command_buffer/service/copy_shared_image_helper.ccgpu/command_buffer/service/raster_decoder.ccgpu/command_buffer/service/shared_image/egl_image_backing.ccgpu/command_buffer/service/shared_image/gl_texture_holder.ccgpu/command_buffer/service/shared_image/gl_texture_image_backing.cc
Audit Directions
- Trusting backend "success" without `glGetError()`Audit every site that sets
SetClearedRect()or otherwise marks a resource initialized after a Skia/Ganesh-GL operation, since those backends do not surface driver errors in their return values. - Uploads that can be silently dropped under resource pressureReview texture/buffer upload paths for handling of
GL_OUT_OF_MEMORYand other latched errors, ensuring failure leaves the resource uncleared rather than partially/uninitialized-but-cleared. - Ad-hoc error-drain loopsLook for remaining
while (glGetErrorFn() != GL_NO_ERROR)patterns that discard errors; each is a candidate site where a real driver failure is being swallowed instead of checked.
Patch
From d399d25e394d5c50d10ce8e63effd39ba4a365a2 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Mon, 21 Sep 2026 14:50:55 -0700 Subject: [PATCH] Check for GL driver errors before marking SharedImage cleared in RasterDecoder In Ganesh-GL, Skia's texture upload path does not check glGetError() and may report success even when an underlying GL driver under memory pressure drops texture uploads and latches GL_OUT_OF_MEMORY. If an uncleared SharedImage is uploaded under these conditions, it could be falsely marked cleared and subsequently read back, exposing uninitialized texture memory. This CL: 1. Adds DrainGLErrors(gl::GLApi* gl_api) to gpu/command_buffer/service/gl_utils.h to drain driver error queues and return the first encountered error. 2. Drains and verifies GL driver errors before marking the destination SharedImage cleared in both DoWritePixelsINTERNAL and WritePixelsYUV when running on Ganesh-GL without context loss. If a GL error occurs, the upload fails and the SharedImage remains uncleared. TAG=agy CONV=ce4f2943-8656-4843-9fb6-351b575b6034 Bug: 562042411 Change-Id: Ifb68f72b16dbfe0bb6df2dd8915d20dc47865c8d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8419764 Reviewed-by: Zhenyao Mo <[email protected]> Commit-Queue: Jonathan Ross <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702073} --- diff --git a/gpu/command_buffer/service/copy_shared_image_helper.cc b/gpu/command_buffer/service/copy_shared_image_helper.cc index 473f8539..6d529e7 100644 --- a/gpu/command_buffer/service/copy_shared_image_helper.cc +++ b/gpu/command_buffer/service/copy_shared_image_helper.cc @@ -13,6 +13,7 @@ #include "base/types/expected.h" #include "base/types/expected_macros.h" #include "gpu/command_buffer/common/mailbox.h" +#include "gpu/command_buffer/service/gl_utils.h" #include "gpu/command_buffer/service/graphite_shared_context.h" #include "gpu/command_buffer/service/shared_context_state.h" #include "gpu/command_buffer/service/shared_image/shared_image_factory.h" @@ -803,6 +804,15 @@ auto* gr_context = shared_context_state_->gr_context(); const bool need_graphite_submit = dest_scoped_access->NeedGraphiteContextSubmit(); + const bool check_gl_upload_errors = !dest_shared_image->IsCleared() && + shared_context_state_->GrContextIsGL() && + !shared_context_state_->context_lost(); + gl::GLApi* const gl_api = + check_gl_upload_errors ? gl::g_current_gl_context : nullptr; + if (gl_api) { + DrainGLErrors(gl_api); + } + for (int plane = 0; plane < dest_format.NumberOfPlanes(); plane++) { bool written = false; if (gr_context) { @@ -840,6 +850,14 @@ need_graphite_submit); if (success && !dest_shared_image->IsCleared()) { + if (gl_api) { + GLenum error = DrainGLErrors(gl_api); + if (error != GL_NO_ERROR) { + return base::unexpected( + GLError(error, "WritePixelsYUV", + "GL driver reported an error during texture upload")); + } + } dest_shared_image->SetClearedRect(gfx::Rect(src_width, src_height)); } return base::ok(); diff --git a/gpu/command_buffer/service/gl_utils.h b/gpu/command_buffer/service/gl_utils.h index b68069c..cf2d4c0 100644 --- a/gpu/command_buffer/service/gl_utils.h +++ b/gpu/command_buffer/service/gl_utils.h @@ -183,6 +183,18 @@ Texture* CreateGLES2TextureWithLightRef(GLuint service_id, GLenum target); } // namespace gles2 + +// Drains all GL errors from `gl_api` and returns the first error encountered, +// or GL_NO_ERROR if there were no errors. +inline GLenum DrainGLErrors(gl::GLApi* gl_api) { + GLenum error = gl_api->glGetErrorFn(); + GLenum first_error = error; + while (error != GL_NO_ERROR) { + error = gl_api->glGetErrorFn(); + } + return first_error; +} + } // namespace gpu #endif // GPU_COMMAND_BUFFER_SERVICE_GL_UTILS_H_ diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc index a119658..5d1bfde 100644 --- a/gpu/command_buffer/service/raster_decoder.cc +++ b/gpu/command_buffer/service/raster_decoder.cc @@ -1984,6 +1984,15 @@ return; } + const bool check_gl_upload_errors = !dest_shared_image->IsCleared() && + shared_context_state_->GrContextIsGL() && + !WasContextLost(); + gl::GLApi* const gl_api = + check_gl_upload_errors ? gl::g_current_gl_context : nullptr; + if (gl_api) { + DrainGLErrors(gl_api); + } + // Try a direct texture upload without using SkSurface. if (gfx::Size(src_width, src_height) == dest_shared_image->size() && x_offset == 0 && y_offset == 0 && @@ -1995,12 +2004,27 @@ DoWritePixelsINTERNALDirectTextureUpload( dest_shared_image.get(), src_info, pixel_data, row_bytes)) { if (!dest_shared_image->IsCleared()) { + if (gl_api) { + GLenum error = DrainGLErrors(gl_api); + if (error != GL_NO_ERROR) { + LOCAL_SET_GL_ERROR( + error, "glWritePixels", + "GL driver reported an error during texture upload"); + return; + } + } dest_shared_image->SetClearedRect( gfx::Rect(src_info.width(), src_info.height())); } return; } + // If direct upload was skipped or failed, drain any errors from the attempt + // before proceeding with the SkSurface fallback. + if (gl_api) { + DrainGLErrors(gl_api); + } + std::vector<GrBackendSemaphore> begin_semaphores; std::vector<GrBackendSemaphore> end_semaphores; @@ -2045,6 +2069,14 @@ dest_scoped_access->NeedGraphiteContextSubmit()); if (success && !dest_shared_image->IsCleared()) { + if (gl_api) { + GLenum error = DrainGLErrors(gl_api); + if (error != GL_NO_ERROR) { + LOCAL_SET_GL_ERROR(error, "glWritePixels", + "GL driver reported an error during texture upload"); + return; + } + } dest_shared_image->SetClearedRect( gfx::Rect(x_offset, y_offset, src_width, src_height)); } diff --git a/gpu/command_buffer/service/shared_image/egl_image_backing.cc b/gpu/command_buffer/service/shared_image/egl_image_backing.cc index fb581fe2..bf6f2a9e 100644 --- a/gpu/command_buffer/service/shared_image/egl_image_backing.cc +++ b/gpu/command_buffer/service/shared_image/egl_image_backing.cc @@ -491,8 +491,7 @@ // GenEGLImageSibling is attributable to the storage call. Silently // squelching these errors is unfortunate, but is done in order to mirror // other allocation checks done in the command decoder. - while (api->glGetErrorFn() != GL_NO_ERROR) { - } + DrainGLErrors(api); for (int plane = 0; plane < num_planes; plane++) { gl::ScopedEGLImage egl_image = GenEGLImageSibling(pixel_data, service_ids, plane); diff --git a/gpu/command_buffer/service/shared_image/gl_texture_holder.cc b/gpu/command_buffer/service/shared_image/gl_texture_holder.cc index d7da0e9..02de614 100644 --- a/gpu/command_buffer/service/shared_image/gl_texture_holder.cc +++ b/gpu/command_buffer/service/shared_image/gl_texture_holder.cc @@ -9,6 +9,7 @@ #include "base/bits.h" #include "base/memory/raw_ptr.h" #include "build/build_config.h" +#include "gpu/command_buffer/service/gl_utils.h" #include "gpu/command_buffer/service/shared_context_state.h" #include "gpu/command_buffer/service/shared_image/gl_repack_utils.h" #include "gpu/command_buffer/service/shared_image/shared_image_gl_utils.h" @@ -156,8 +157,7 @@ // below is attributable to the storage call. Silently squelching // these errors is unfortunate, but is done in order to mirror other // allocation checks done in the command decoder. - while (api->glGetErrorFn() != GL_NO_ERROR) { - } + DrainGLErrors(api); // Initialize the texture storage/image parameters and upload initial pixels // if available. @@ -349,8 +349,7 @@ // Drain any pre-existing GL errors so the post-allocation check below is // attributable to the storage call. Mirrors other allocation checks done in // the command decoder. - while (api->glGetErrorFn() != GL_NO_ERROR) { - } + DrainGLErrors(api); { gl::ScopedProgressReporter scoped_progress_reporter(progress_reporter_); @@ -397,8 +396,7 @@ // Drain any pre-existing GL errors so the post-allocation check below is // attributable to the storage call. Mirrors other allocation checks done in // the command decoder. - while (api->glGetErrorFn() != GL_NO_ERROR) { - } + DrainGLErrors(api); // ScopedGLFramebuffer must be declared before ScopedFramebufferBinder // so that when this scope exits, ScopedFramebufferBinder is destroyed first diff --git a/gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc b/gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc index 4733e84..3ff097a 100644 --- a/gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc +++ b/gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc @@ -17,6 +17,7 @@ #include "gpu/command_buffer/common/shared_image_info.h" #include "gpu/command_buffer/common/shared_image_usage.h" #include "gpu/command_buffer/service/context_state.h" +#include "gpu/command_buffer/service/gl_utils.h" #include "gpu/command_buffer/service/gles2_cmd_decoder.h" #include "gpu/command_buffer/service/shared_context_state.h" #include "gpu/command_buffer/service/shared_image/shared_image_factory.h" @@ -508,8 +509,7 @@ // unfortunate, but is done in order to mirror other allocation checks done in // the command decoder. gl::GLApi* const api = gl::g_current_gl_context; - while (api->glGetErrorFn() != GL_NO_ERROR) { - } + DrainGLErrors(api); const std::string debug_label = "GLSharedImage_" + SharedImageBacking::debug_label();