CVE-2026-13023
Overview
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
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.