CVE-2026-13023
Overview
Background
- Shared image backing
- a GPU-service object (
GLTextureImageBacking,EGLImageBacking) that owns the underlying texture/EGLImage storage shared between GPU consumers such as GL, Skia, and the compositor. - `IsCleared()` / `SetCleared()`
- the shared-image mechanism that tracks whether a backing’s storage has been fully initialized; uncleared backings are refused for read access to prevent leaking prior VRAM contents.
- Robust resource initialization
- a GL/driver feature that zero-fills newly allocated storage; without it, freshly allocated texture storage contains undefined VRAM residue until explicitly written.
- `glGetError`
- the GL API that returns and clears the top pending error code (e.g.
GL_OUT_OF_MEMORY), used here to detect whether a pixel upload actually succeeded.
Root Cause Analysis
When a shared image was created with initial pixel data, GLTextureImageBacking and EGLImageBacking uploaded that data via glTexSubImage2D and then unconditionally marked the backing cleared (SetCleared() / returning a valid image). If the upload failed under resource pressure (e.g. GL_OUT_OF_MEMORY), the storage allocation had already reserved undefined-content VRAM on a context lacking robust-resource-init, yet the backing was still flagged cleared, defeating the IsCleared() guard that normally blocks reads of uninitialized memory. The invariant violated is “a backing is marked cleared only if its storage is actually initialized.”
The fix drains any pre-existing GL errors before allocation (so a later error is attributable to this upload), then calls glGetError after the upload: for passthrough textures it only calls SetCleared() when the error is GL_NO_ERROR, and EGLImageBacking returns an empty gl::ScopedEGLImage() (invalid image) on error so SetCleared() is never reached. GLTextureHolder additionally passes a real cleared_rect of gfx::Rect(size_) only when pixel_data is non-empty (validating-decoder path), so the decoder texture’s cleared state reflects whether data was provided.
glGetError() == GL_NO_ERROR, leaving the backing uncleared (and thus unreadable) when the upload failed.Attack Path
- Create a shared image with pixel data
A renderer requests a shared image supplying initial pixel data, driving the GPU service into
GLTextureImageBacking/EGLImageBackingallocation with an upload viaglTexSubImage2D. - Induce upload failure
The attacker applies GPU resource pressure so the storage allocation succeeds but the subsequent upload raises
GL_OUT_OF_MEMORY, leaving the storage holding undefined VRAM residue. - Backing marked cleared anyway
Pre-patch, the code called
SetCleared()(or returned a valid EGLImage) regardless of the error, so the backing’sIsCleared()returns true despite uninitialized storage. - Read back the backing The attacker performs read access (e.g. readback or sampling) on the falsely-cleared backing, obtaining the uninitialized VRAM contents.
- Leak residue The returned bytes are VRAM allocator residue that may contain data from other GPU allocations, yielding an information leak (inferred as to what specific prior contents are recovered).
Impact Assessment
GL_OUT_OF_MEMORY) during the initial upload while the allocation itself succeeds. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 5.3. Any cross-context data recovery is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/shared_image/egl_image_backing.cc |
modified | |
forgpu/command_buffer/service/shared_image/egl_image_backing.cc |
modified | |
ifgpu/command_buffer/service/shared_image/gl_texture_image_backing.cc |
modified |
Files Changed
gpu/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
- Unconditional cleared/initialized flagsSearch for
SetCleared(),MarkInitialized, or similar calls that follow an allocation/upload without verifying the operation succeeded, especially where storage is allocated without robust-resource-init. - Ignored GL/driver error returnsAudit GPU code that issues
glTexSubImage2D,glTexImage, orvkAllocate-style calls and proceeds to mark resources usable without callingglGetError/ checking result codes. - Uninitialized-memory readback pathsReview shared-image and texture readback code for places where the
IsCleared()gate can be bypassed by state set optimistically before the initializing write actually completes.
Patch
From 7f4181fabb329920be950c95b76e69d1ad296cda Mon Sep 17 00:00:00 2001 From: Sunny Sachanandani <[email protected]> Date: Tue, 16 Jun 2026 15:23:13 -0700 Subject: [PATCH] [gpu] Check GL error before marking GLTexture/EGLImage cleared GLTextureImageBacking and EGLImageBacking currently mark the backing as cleared unconditionally when initial pixel data is provided, even if the subsequent upload via glTexSubImage2D fails (e.g. under resource pressure / GL_OUT_OF_MEMORY). This bypasses the IsCleared() check, potentially exposing uninitialized VRAM allocator residue during readback. This CL checks for GL errors before calling SetCleared(). If a GL error is detected, the backing remains uncleared so subsequent read access is refused. Landing a shared image unittest for this wasn't feasible since it needs patching GL function pointers which won't work well in any parallel test setup. However, I was able to verify the fix locally with a unit test. Fixed: 517080836 Change-Id: If9ac7f2cb1742601418db52ad163742d6a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7909296 Commit-Queue: Sunny Sachanandani <[email protected]> Auto-Submit: Sunny Sachanandani <[email protected]> Reviewed-by: Vasiliy Telezhnikov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1647933} --- 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 d04887c4..583148a 100644 --- a/gpu/command_buffer/service/shared_image/egl_image_backing.cc +++ b/gpu/command_buffer/service/shared_image/egl_image_backing.cc @@ -445,6 +445,17 @@ format_info.adjusted_format, format_info.gl_type, data); } + // The storage allocation of the sibling texture allocates undefined-content + // storage on a context without robust-resource-init. If the following upload + // upload failed (e.g. GL_OUT_OF_MEMORY) the storage is still uninitialized. + // Return an invalid image so that we don't call SetCleared() on the backing. + const GLenum error = api->glGetErrorFn(); + if (error != GL_NO_ERROR) { + LOG(ERROR) << "EGLImageBacking: initial pixel upload failed (GL error 0x" + << std::hex << error << ")"; + return gl::ScopedEGLImage(); + } + // Use service id of the texture as a source to create the EGLImage. const EGLint egl_attrib_list[] = { EGL_GL_TEXTURE_LEVEL_KHR, 0, EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; @@ -471,6 +482,12 @@ AutoLock auto_lock(this); create_egl_images = egl_images_.empty(); if (create_egl_images) { + // Drain any pre-existing GL errors so the post-allocation check below in + // 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) { + } 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 a601dc1..8c35e9e7 100644 --- a/gpu/command_buffer/service/shared_image/gl_texture_holder.cc +++ b/gpu/command_buffer/service/shared_image/gl_texture_holder.cc @@ -232,10 +232,12 @@ // that it can be used in other ES2 contexts, and so we have to pass // gl_format as the internal format in the LevelInfo. // https://crbug.com/628064 - texture_->SetLevelInfo(format_desc_.target, 0, format_desc_.data_format, - size_.width(), size_.height(), /*depth=*/1, 0, - format_desc_.data_format, format_desc_.data_type, - /*cleared_rect=*/gfx::Rect()); + const gfx::Rect cleared_rect = + !pixel_data.empty() ? gfx::Rect(size_) : gfx::Rect(); + texture_->SetLevelInfo( + format_desc_.target, /*level=*/0, format_desc_.data_format, + size_.width(), size_.height(), /*depth=*/1, /*border=*/0, + format_desc_.data_format, format_desc_.data_type, cleared_rect); texture_->SetImmutable(true, format_info.supports_storage); } else { LOG(ERROR) << "GLTextureHolder: native storage allocation failed"; 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 c3f178bc..6b2e4df 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 @@ -497,6 +497,14 @@ base::span<const uint8_t> pixel_data, gl::ProgressReporter* progress_reporter, bool framebuffer_attachment_angle) { + // Drain any pre-existing GL errors so the post-allocation check 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. + gl::GLApi* const api = gl::g_current_gl_context; + while (api->glGetErrorFn() != GL_NO_ERROR) { + } + const std::string debug_label = "GLSharedImage_" + SharedImageBacking::debug_label(); int num_planes = format().NumberOfPlanes(); @@ -510,8 +518,23 @@ debug_label); } - if (!pixel_data.empty()) { - SetCleared(); + // Update the cleared state for passthrough textures if the pixel data upload + // was successful. We don't need to update the cleared state for validating + // decoder textures because we track the cleared state in the decoder texture + // objects whose cleared state is set in TextureHolder::Initialize above. + if (is_passthrough_ && !pixel_data.empty()) { + // The storage allocation allocates undefined-content storage on a context + // without robust-resource-init. If the subsequent upload failed (e.g. + // GL_OUT_OF_MEMORY) the storage is still uninitialised; only mark the + // backing cleared if no GL error was raised. + GLenum error = api->glGetErrorFn(); + if (error == GL_NO_ERROR) { + SetCleared(); + } else { + LOG(ERROR) + << "GLTextureImageBacking: initial pixel upload failed (GL error 0x" + << std::hex << error << ")"; + } } }
Original Bug Report
Potential cross-origin VRAM leak via unchecked glTexSubImage2D in GLTexture and EGLImageBackings
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: GLTextureImageBacking and EGLImageBacking do not validate the success of glTexSubImage2D during initial pixel data upload, yet they unconditionally mark the backing as cleared. If the initial pixel upload fails under memory or resource pressure, the backing retains uninitialized VRAM storage. A compromised renderer can subsequently read back this uninitialized memory, potentially disclosing cross-origin graphics data.
Affected files:
gpu/command_buffer/service/shared_image/gl_texture_image_backing.ccgpu/command_buffer/service/shared_image/gl_texture_holder.ccgpu/command_buffer/service/shared_image/egl_image_backing.cc
Estimated timestamp from git blame: 2018-12-11
Description
There is a potential information disclosure vulnerability in GLTextureImageBacking and EGLImageBacking where texture storage allocation and initial pixel upload are executed as separate GL calls without verifying the success of the upload before marking the backing as cleared.
Because the GL context used for these operations is created with robust resource initialization disabled (to optimize performance), newly allocated texture memory is not automatically cleared by the graphics driver. If the subsequent pixel upload call fails, the texture retains its uninitialized VRAM contents, which may contain graphics residue from other processes or tabs. However, because the failure is not caught, the backing is still marked as cleared, bypassing safety checks during subsequent readback operations.
Code Analysis
1. GLTextureImageBacking Initialization Path
In gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc (lines 495-516), GLTextureImageBacking::InitializeGLTexture initializes the underlying texture and unconditionally calls SetCleared() if pixel_data is not empty:
void GLTextureImageBacking::InitializeGLTexture(...) {
...
for (int plane = 0; plane < num_planes; ++plane) {
textures_[plane].Initialize(format_info[plane],
framebuffer_attachment_angle, pixel_data,
debug_label);
}
if (!pixel_data.empty()) {
SetCleared(); // Unconditionally called
}
}
Inside GLTextureHolder::Initialize (gpu/command_buffer/service/shared_image/gl_texture_holder.cc, lines 195-208), the backing allocates storage using glTexStorage2DEXT and attempts to upload initial pixels using glTexSubImage2D back-to-back with no error validation:
api->glTexStorage2DEXTFn(format_desc_.target, /*levels=*/1,
format_info.adjusted_storage_internal_format,
size_.width(), size_.height());
if (!pixel_data.empty()) {
ScopedUnpackState scoped_unpack_state(/*uploading_data=*/true);
api->glTexSubImage2DFn(format_desc_.target, /*level=*/0, /*xoffset=*/0,
/*yoffset=*/0, size_.width(), size_.height(),
format_info.adjusted_format,
format_desc_.data_type, pixel_data.data()); // Unchecked
}
2. EGLImageBacking Sibling Generation Path
Similarly, on the Android DrDC path in EGLImageBacking::GenEGLImageSibling (gpu/command_buffer/service/shared_image/egl_image_backing.cc, lines 423-434), texture storage is allocated and uploaded without error validation:
api->glTexStorage2DEXTFn(target, 1,
format_info.adjusted_storage_internal_format,
plane_size.width(), plane_size.height());
if (!pixel_data.empty()) {
CHECK_EQ(plane, 0);
ScopedUnpackState scoped_unpack_state(/*uploading_data=*/true);
api->glTexSubImage2DFn(target, 0, 0, 0, plane_size.width(),
plane_size.height(), format_info.adjusted_format,
format_info.gl_type, pixel_data.data()); // Unchecked
}
Control then returns to EGLImageBacking::GenEGLImageSiblings where SetCleared() is called unconditionally if pixel_data is not empty (line 530).
Potential Mechanism & Exploitation
Under normal conditions, the graphics driver allocates VRAM during the first call, and the second call populates it. However, if the glTexSubImage2D call fails (for example, due to a GL_OUT_OF_MEMORY error when allocating the host-side staging buffer or driver-level upload failure under memory pressure), the upload is a no-op, and the texture retains its uninitialized contents.
Since read-access gates (such as GLTextureImageRepresentationBase::BeginScopedAccess) only verify IsCleared(), the uninitialized texture bypasses zero-clearing mechanisms and becomes readable. A compromised renderer could potentially retrieve this uninitialized VRAM content, disclosing cross-origin graphics residue.
Note: These steps are based on static analysis of the source code. Our tooling does not currently have the capability to run code or execute a live proof of concept.
Suggested Fix
Verify that the pixel data upload operation succeeded before marking the backing as cleared. This can be achieved by checking the return status of the upload (if applicable), querying for GL errors during or after the upload, or enforcing zero-clearing initialization if the upload fails.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.