High watch CVSS 5.3 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in GPU
DescriptionUninitialized Use in GPU
ComponentGPU
Sub-componentGPU
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517080836
Fix commit7f4181fabb32 (chromium/src) +48/-6
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-06-16 (public git)
Disclosed2026-06-23
Patch gap7 days — fix public in source before the advisory

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.

Key insight
The single mistake was treating “pixel data was provided” as equivalent to “storage is initialized” and marking the backing cleared without checking that the upload actually succeeded; the fix conditions clearing on a post-upload glGetError() == GL_NO_ERROR, leaving the backing uncleared (and thus unreadable) when the upload failed.

Attack Path

  1. Create a shared image with pixel data A renderer requests a shared image supplying initial pixel data, driving the GPU service into GLTextureImageBacking / EGLImageBacking allocation with an upload via glTexSubImage2D.
  2. 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.
  3. Backing marked cleared anyway Pre-patch, the code called SetCleared() (or returned a valid EGLImage) regardless of the error, so the backing’s IsCleared() returns true despite uninitialized storage.
  4. Read back the backing The attacker performs read access (e.g. readback or sampling) on the falsely-cleared backing, obtaining the uninitialized VRAM contents.
  5. 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

An attacker in a compromised or malicious renderer who can create shared images with initial data and induce GL upload failures can cause the GPU process to expose uninitialized VRAM residue on readback, an information disclosure across GPU allocations. The precondition is triggering a GL error (e.g. 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

FunctionChangeNotes
if
gpu/command_buffer/service/shared_image/egl_image_backing.cc
modified
for
gpu/command_buffer/service/shared_image/egl_image_backing.cc
modified
if
gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc
modified

Files Changed

  • gpu/command_buffer/service/shared_image/egl_image_backing.cc
  • gpu/command_buffer/service/shared_image/gl_texture_holder.cc
  • gpu/command_buffer/service/shared_image/gl_texture_image_backing.cc

Audit Directions

  • Unconditional cleared/initialized flags
    Search 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 returns
    Audit GPU code that issues glTexSubImage2D, glTexImage, or vkAllocate-style calls and proceeds to mark resources usable without calling glGetError / checking result codes.
  • Uninitialized-memory readback paths
    Review 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.
From 7f4181fabb329920be950c95b76e69d1ad296cda Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
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 <sunnyps@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
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 << ")";
+    }
   }
 }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • gpu/command_buffer/service/shared_image/gl_texture_holder.cc
  • gpu/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.

View on issue tracker