Medium watch CVSS 3.4 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
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.
Tracker535374213
Fix commit3c83e9618750 (chromium/src) +6/-5
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-07-16 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

Background

`SharedContextState::FlushGraphiteRecorder`
a GPU-service helper that captures the pending Graphite draw commands via snap() and submits them to the GPU through insertRecording, returning whether work was actually flushed.
Graphite `Recorder::snap()`
the Skia Graphite call that packages recorded commands into a Recording object, and which can return null when no valid recording can be produced.
Robust/lazy clear invariant
a shared image that is reported as “cleared” must have had its backing memory actually initialized (e.g. via a clear that was submitted to the GPU) before any reader is allowed to sample it.

Root Cause Analysis

FlushGraphiteRecorder calls gpu_main_graphite_recorder()->snap() to obtain a Recording and, in the pre-patch code, only submitted work and returned the result of insertRecording when the recording was non-null; when snap() returned null it fell through to return true. That return true falsely told callers that the flush succeeded and work was submitted to the GPU, even though nothing was recorded or inserted. Callers use this boolean to decide whether it is safe to mark a shared image as cleared/initialized, so a snap() failure caused a shared image to be treated as cleared without its clear commands ever reaching the GPU.

The fix inverts the check: if (!recording) { return false; } so a snap() failure now propagates as failure, and only a genuinely inserted recording returns the insertRecording result. This restores the invariant that a true return means clear work was actually submitted.

Key insight
The single mistake was treating a null snap() (no work recorded) as success and returning true; the fix makes the null case return false so callers no longer mark a resource cleared when its GPU clear was never submitted.

Attack Path

  1. Allocate an uninitialized GPU-backed shared image A renderer/client requests a shared image whose backing must be lazily cleared before first read. (inferred)
  2. Force `snap()` to yield no recording Trigger a state in which gpu_main_graphite_recorder()->snap() returns null so no clear commands are captured. (inferred)
  3. Flush reports false success FlushGraphiteRecorder hits the old return true, so the caller believes the clear was submitted to the GPU.
  4. Image marked cleared without initialization The caller consequently marks the shared image as cleared even though its backing memory was never written by the clear.
  5. Read back uninitialized VRAM A subsequent read access is permitted against the “cleared” image, exposing stale/uninitialized GPU memory contents. (inferred)

Impact Assessment

An attacker in a renderer/GPU client can cause a shared image to be reported as cleared while its GPU backing was never actually initialized, leading to disclosure of uninitialized GPU memory to the client. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.4. Any further escalation is (inferred). Preconditions are the ability to allocate the affected shared images and to reach a snap()-failure state.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/shared_context_state.cc
modified

Files Changed

  • gpu/command_buffer/service/shared_context_state.cc

Audit Directions

  • Success-by-default returns
    Look for flush/submit/insert helpers that return true on an early or null-result path, where a boolean nominally means “work was submitted” but is set optimistically.
  • Clear/initialization gating
    Audit call sites that mark shared images or textures as cleared based on a flush return value, and verify the return value distinguishes “nothing to do / could not record” from “clear actually submitted.”
  • snap()/optional-result handling
    Search Graphite/Skia integration points where snap() or similar returns an optional/nullable object and ensure the null case is treated as failure rather than silently ignored.
From 3c83e9618750d2d05ea02e53902547f7978f7177 Mon Sep 17 00:00:00 2001
From: kylechar <kylechar@chromium.org>
Date: Tue, 21 Jul 2026 12:06:31 -0700
Subject: [PATCH] Don't clear image if snap() fails

SharedContextState::FlushGraphiteRecorder() was updated to return a bool
to indicate if flush was successful aka work was submitted to the GPU.
It still returns true if recorder snap() call fails in which case no
work is submitted to the GPU. Have it return false for snap failure as
well.

Bug: 535374213
Change-Id: Ib963b1b0b9d576b3ff6bbbe078ae116c097d301f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8128097
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Kyle Charbonneau <kylechar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1665671}
---
diff --git a/gpu/command_buffer/service/shared_context_state.cc b/gpu/command_buffer/service/shared_context_state.cc
index 26c717e..bcbf59bd 100644
--- a/gpu/command_buffer/service/shared_context_state.cc
+++ b/gpu/command_buffer/service/shared_context_state.cc
@@ -822,12 +822,13 @@
 
 bool SharedContextState::FlushGraphiteRecorder() {
   auto recording = gpu_main_graphite_recorder()->snap();
-  if (recording) {
-    skgpu::graphite::InsertRecordingInfo info = {};
-    info.fRecording = recording.get();
-    return graphite_shared_context()->insertRecording(info);
+  if (!recording) {
+    return false;
   }
-  return true;
+
+  skgpu::graphite::InsertRecordingInfo info = {};
+  info.fRecording = recording.get();
+  return graphite_shared_context()->insertRecording(info);
 }
 
 void SharedContextState::FlushAndSubmit(bool sync_to_cpu) {
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU uninitialized memory leak in RasterDecoder via ignored Graphite recording failures

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: RasterDecoderImpl::DoWritePixelsINTERNALDirectTextureUpload returns record-time success for Graphite texture uploads while ignoring insert-time recording failures in SharedContextState::FlushGraphiteRecorder. This can allow a compromised renderer to prematurely mark a SharedImage as cleared, bypassing the uninitialized texture gate and reading back uninitialized GPU memory. The issue potentially allows cross-origin pixel disclosure from a singleton shared GPU process.

Affected files:

  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/shared_context_state.cc

Estimated timestamp from git blame: 2023-05-12

Root Cause Analysis

In Chromium’s GPU service, RasterDecoderImpl::DoWritePixelsINTERNALDirectTextureUpload handles direct texture uploads when using the Skia Graphite backend. However, it returns a boolean based purely on the record-time result, while the actual command execution/insertion is handled via a void helper that discards the result.

Specifically, in gpu/command_buffer/service/raster_decoder.cc:

  } else {
    CHECK(graphite_shared_context());
    ...
    written = graphite_recorder()->updateBackendTexture(
        graphite_texture_ptr->texture(), &pixmap,
        /*numLevels=*/1, release_proc, graphite_texture_ptr);
  }

  shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
  shared_context_state_->SubmitIfNecessary(
      std::move(end_semaphores),
      dest_scoped_access->NeedGraphiteContextSubmit());

  return written;

Here, written receives the return value of updateBackendTexture, which only enqueues an UploadTask into Skia Graphite’s recorder. This returns true at record time before any commands are inserted or executed.

Immediately after, FlushWriteAccess is called, which cascades into SharedContextState::FlushGraphiteRecorder() in gpu/command_buffer/service/shared_context_state.cc:

void SharedContextState::FlushGraphiteRecorder() {
  auto recording = gpu_main_graphite_recorder()->snap();
  if (recording) {
    skgpu::graphite::InsertRecordingInfo info = {};
    info.fRecording = recording.get();
    graphite_shared_context()->insertRecording(info);    // <--- Boolean return value is ignored!
  }
}

If the recording fails to insert (for instance, returning skgpu::graphite::InsertStatus::kAddCommandsFailed due to transient setup failures or allocator issues), the failure is silently discarded. No context loss is triggered because kAddCommandsFailed is categorized as a recoverable error in GraphiteSharedContext::InsertRecordingImpl.

Impact on Clear-State Tracking

Because DoWritePixelsINTERNALDirectTextureUpload returns true (the record-time success status), the caller in RasterDecoderImpl::DoWritePixelsINTERNAL prematurely marks the SharedImage as fully cleared:

    if (!dest_shared_image->IsCleared()) {
      dest_shared_image->SetClearedRect(
          gfx::Rect(src_info.width(), src_info.height()));
    }

Once marked cleared, subsequent readback requests (such as ReadbackARGBImagePixelsINTERNALImmediate) bypass the uninitialized texture gate in SkiaGraphiteImageRepresentation::BeginScopedReadAccess:

  if (!IsCleared()) {
    ...
    return nullptr; // Blocks uninitialized read
  }

Furthermore, the IsCleared() status is passed to Dawn/Vulkan during readback as begin_access_desc.initialized = true, which commands Dawn to skip lazy zero-clearing of the texture. As a result, uninitialized physical GPU memory (containing residue from previously allocated/freed textures across different origins) is read directly into the renderer-owned shared memory buffer.


Suggested Potential Trigger Steps

Note: These are suggested potential steps only; our tooling currently lacks the ability to execute code and verify this sequence dynamically.

  1. A compromised renderer allocates an uncleared single-plane SharedImage (e.g., viz::SinglePlaneFormat::kBGRA_8888) with SHARED_IMAGE_USAGE_RASTER_READ | RASTER_WRITE | DISPLAY_READ flags and no initial pixels.
  2. The renderer induces transient GPU device pressure (e.g., allocating large buffers or textures via WebGPU) to make subsequent command allocations/command buffer setup fail.
  3. The renderer calls WritePixelsINTERNALImmediate targeting the SharedImage, using dimensions matching the SharedImage’s size to trigger the direct upload fast-path.
  4. Inside the GPU process, updateBackendTexture returns true at record-time, but insertRecording returns false due to kAddCommandsFailed. This failure is ignored.
  5. The GPU process calls SetClearedRect, marking the uninitialized texture as cleared.
  6. The renderer calls ReadbackARGBImagePixelsINTERNALImmediate on the mailbox. The IsCleared() check is bypassed, and the GPU copies cross-origin texture residues into the renderer’s shared memory.

Suggested Fix

To prevent uninitialized memory disclosure, the GPU process must ensure that clear-state updates only happen if the command insertion succeeds.

We suggest updating DoWritePixelsINTERNAL and DoWritePixelsINTERNALDirectTextureUpload to propagate the actual insert status, or ensuring FlushWriteAccess can return a status indicating whether the recording was successfully inserted. If the insert fails, the SharedImage must not be marked cleared.

Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d


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