High watch CVSS 3.4 chrome Uninitialized Memory 🔧 Commit mapped

Overview

High
Severity
3.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource in GPU
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.
Tracker562042411
Fix commitd399d25e394d (chromium/src) +69/-10
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-09-15 (per vendor advisory)
Fix landed2026-09-21 (public git)
Disclosed2026-09-29
Patch gap8 days — fix public in source before the advisory

Background

SharedImage cleared state
a per-SharedImage flag (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 DoWritePixelsINTERNAL and WritePixelsYUV) does not itself inspect glGetError() 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.h that empties the GL error queue and returns the first error encountered, or GL_NO_ERROR if 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.

Key insight
The single mistake was trusting Skia’s Ganesh-GL upload “success” return value without consulting 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

  1. 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)
  2. Trigger an upload into an uncleared SharedImage A WritePixels/WritePixelsYUV operation targets a not-yet-cleared destination SharedImage, and the Ganesh-GL upload silently fails while returning success.
  3. False clear Pre-patch, SetClearedRect() runs on the nominal success, marking the SharedImage cleared even though its backing texture was never written.
  4. Read back uninitialized memory The attacker reads the SharedImage back through a normal path, obtaining the contents of uninitialized GPU texture memory. (inferred)

Impact Assessment

An attacker able to reach the raster upload path from a renderer can, under GPU memory pressure, cause an uncleared 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

FunctionChangeNotes
if
gpu/command_buffer/service/copy_shared_image_helper.cc
modified
while
gpu/command_buffer/service/gl_utils.h
modified
if
gpu/command_buffer/service/raster_decoder.cc
modified
for
gpu/command_buffer/service/shared_image/egl_image_backing.cc
modified

Files Changed

  • gpu/command_buffer/service/gl_utils.h
  • gpu/command_buffer/service/copy_shared_image_helper.cc
  • gpu/command_buffer/service/raster_decoder.cc
  • 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

  • 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 pressure
    Review texture/buffer upload paths for handling of GL_OUT_OF_MEMORY and other latched errors, ensuring failure leaves the resource uncleared rather than partially/uninitialized-but-cleared.
  • Ad-hoc error-drain loops
    Look 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.
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();
Loading diff…