CVE-2026-95293
Overview
Background
- `GLTextureHolder`
- GPU-process shared-image helper that owns a GL texture and handles CPU-side
UploadFromMemory/ReadbackToMemorytransfers via the GL API. - `glTexSubImage2D`
- GL call that copies client pixel data into a texture’s storage; on failure it sets a GL error and may leave texture memory uninitialized or partially written.
- `glGetError`
- GL’s asynchronous error queue; errors accumulate until drained, so a stale error can be misattributed unless the queue is flushed before a checked call.
- Uninitialized memory
- texture backing whose contents were never validly written; if surfaced as pixels it can leak prior GPU/heap contents.
Root Cause Analysis
GLTextureHolder::UploadFromMemory and ReadbackToMemory invoked glTexSubImage2D (and the readback path) and then returned true unconditionally, without checking whether the GL storage call succeeded. If the driver rejected the call and set a GL error, the texture (or readback buffer) could be left with uninitialized or partially written contents, yet the method reported success to its caller. Because GL errors are queued and only cleared by glGetError, a reliable post-call check also requires first draining any pre-existing errors so a later check is attributable to the storage call itself. Initialize() already performed this drain-and-check pattern, but the upload and readback paths did not, creating an inconsistency.
The fix adds while (api->glGetErrorFn() != GL_NO_ERROR) {} before the storage call and returns api->glGetErrorFn() == GL_NO_ERROR afterward. A false return now signals failure so callers do not treat uninitialized texture memory as valid content.
return true after glTexSubImage2D silently masks storage failures; only a drain-then-check bracket, as Initialize() already did, makes a failed upload/readback observable to the caller.Attack Path
- Trigger a failing transfer
Drive a shared-image upload/readback whose parameters cause
glTexSubImage2D(or the readback) to fail in the GPU process. - Storage left uninitialized The failed call leaves the texture or readback buffer with uninitialized or partially written memory while setting a GL error.
- Success is reported anyway
The old code returns
trueunconditionally, so the caller treats the incomplete storage as valid content. - Surface the memory Cause the shared image to be sampled, composited, or read back, exposing the uninitialized bytes as pixel data.
Impact Assessment
Files Changed
gpu/command_buffer/service/shared_image/gl_texture_holder.cc
Audit Directions
- Unchecked GL storage callsSearch other shared-image and texture backings for
glTexImage2D/glTexSubImage2D/glReadPixelssites followed by unconditionalreturn true, and flag any lacking a post-callglGetErrorcheck. - Missing error drainWhere a
glGetErrorcheck exists but no preceding drain loop, the check may be attributing stale errors; verify each checked call brackets it with a drain likeInitialize()does. - Success without initialization proofAudit callers that treat a
true/success return as a guarantee of fully written texture or buffer memory, ensuring they handle a newly possiblefalseand never sample uninitialized resources.
Patch
From 5edc4d1363bf6bf79ada91fa7ea709e0768f6f26 Mon Sep 17 00:00:00 2001 From: Brandon Jones <[email protected]> Date: Wed, 02 Sep 2026 17:37:15 -0700 Subject: [PATCH] More GL error checks in GLTextureHolder Flushes any existing GL errors and checks for new ones in GLTextureHolder::UploadFromMemory and GLTextureHolder::ReadbackToMemory. Mirrors the check done in Initialize(). Fixed: 550953039 Change-Id: I0073a2a1b9462e8501ec8b2fd9d70ca2500215c8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8340268 Reviewed-by: Kai Ninomiya <[email protected]> Auto-Submit: Brandon Jones <[email protected]> Commit-Queue: Kai Ninomiya <[email protected]> Cr-Commit-Position: refs/heads/main@{#1691225} --- 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 32faf38..d7da0e9 100644 --- a/gpu/command_buffer/service/shared_image/gl_texture_holder.cc +++ b/gpu/command_buffer/service/shared_image/gl_texture_holder.cc @@ -345,13 +345,21 @@ const void* pixels = !repacked_data.empty() ? repacked_data.data() : pixmap.addr(); gl::GLApi* api = gl::g_current_gl_context; + + // 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) { + } + { gl::ScopedProgressReporter scoped_progress_reporter(progress_reporter_); api->glTexSubImage2DFn(gl_target, /*level=*/0, 0, 0, size_.width(), size_.height(), gl_format, gl_type, pixels); } - return true; + // Report any failures + return api->glGetErrorFn() == GL_NO_ERROR; } bool GLTextureHolder::ReadbackToMemory(const SkPixmap& pixmap) { @@ -385,6 +393,13 @@ } gl::GLApi* api = gl::g_current_gl_context; + + // 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) { + } + // ScopedGLFramebuffer must be declared before ScopedFramebufferBinder // so that when this scope exits, ScopedFramebufferBinder is destroyed first // (restoring the previous framebuffer binding) before the temporary FBO is @@ -481,7 +496,8 @@ SwizzleRedAndBlue(pixmap); } - return true; + // Report any failures + return api->glGetErrorFn() == GL_NO_ERROR; } sk_sp<GrPromiseImageTexture> GLTextureHolder::GetPromiseImage(