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

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in Skia
DescriptionUninitialized resource in Skia
ComponentSkia
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.
Tracker497205529
Fix commit872f3f32bf81 (chromium/src) +69/-34
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-28 (per vendor advisory)
Fix landed2026-07-16 (public git)
Disclosed2026-08-25
Patch gap40 days — fix public in source before the advisory

Background

SharedImage cleared state
A SharedImage tracks whether its backing memory has been initialized via IsCleared(), SetCleared(), and SetClearedRect(), and Chrome lazily zero-clears any region reported as not-yet-cleared before it can be read.
Graphite Recorder / Recording
Under the Graphite backend, GPU work (including a clear) is not executed immediately but recorded into a skgpu::graphite::Recorder, snapped into a Recording, and only actually runs once that recording is handed to graphite_shared_context()->insertRecording(...).
`FlushWriteAccess` / `FlushGraphiteRecorder`
SharedContextState::FlushWriteAccess finalizes a scoped write to a SharedImage, and for Graphite it calls SharedContextState::FlushGraphiteRecorder, which snaps the recorder and inserts the recording into the context.
Insertion failure
insertRecording can fail (returning failure) so the recorded clear command is silently dropped and never executes on the GPU.

Root Cause Analysis

When a SharedImage was uninitialized, the affected code paths recorded a clear command into the Graphite Recorder (via drawColor(..., SkBlendMode::kSrc) or BlitRGBAToYUVA) and then unconditionally called SetCleared() / SetClearedRect() to mark the image initialized. The clear, however, only actually runs if the snapped Recording is successfully passed to insertRecording; if FlushGraphiteRecorder / GraphiteFlush fail to insert the recording for any reason, the clear command is dropped, yet the SharedImage has already been flagged as cleared. This violates the invariant that “IsCleared() is true” implies “the backing memory was actually initialized,” so subsequent accesses skip the lazy re-clear and read whatever stale bytes occupy that GPU allocation.

The fix makes FlushGraphiteRecorder, FlushWriteAccess, GraphiteFlush, and GraphiteFlushAndSubmit return the insertion success bool, and defers every SetCleared() / SetClearedRect() call until that bool confirms the recording was actually inserted. Where the cleanup ordering differs (as in copy_shared_image_helper.cc), the intent is captured in update_cleared_rect / new_cleared_rect and applied only inside the success-guarded branch. In raster_decoder.cc the same deferral is expressed with the new should_clear_shared_image_ member, which delays SetCleared() until after a successful flush.

Key insight
The core mistake was treating the recording of a clear command as equivalent to its execution: the code marked the SharedImage cleared before confirming insertRecording succeeded. The fix threads the insertion-success bool up to every callsite and gates SetCleared() / SetClearedRect() on it, so the cleared flag is only set when the clear is guaranteed to run.

Attack Path

  1. Trigger a Graphite-backed write to an uninitialized SharedImage From a renderer, drive a raster, copy, or WebGPU operation (e.g. the RasterDecoderImpl clear path or a copy_shared_image_helper.cc blit) that targets a SharedImage whose region is not yet cleared, causing a clear command to be recorded into the Graphite Recorder.
  2. Cause the recording insertion to fail Arrange conditions under which graphite_shared_context()->insertRecording(...) fails, so the recorded clear command is dropped and never executes on the GPU (the specific way to force this failure is not shown by the diff) (inferred).
  3. SharedImage is wrongly marked cleared In the unpatched code the callsite calls SetCleared() / SetClearedRect() unconditionally, so IsCleared() now returns true even though the backing memory was never initialized.
  4. Read back uninitialized GPU memory A later access to the SharedImage sees IsCleared() == true, skips the lazy re-clear, and returns the stale, uninitialized bytes to the attacker (e.g. via readback or compositing), leaking whatever data previously occupied that allocation (inferred).

Impact Assessment

An attacker driving the GPU process from a compromised or malicious renderer can obtain an uninitialized-memory read of a Graphite SharedImage, potentially disclosing stale GPU allocation contents; the advisory classifies this as an UninitializedMemory / “Uninitialized resource in Skia” issue in the Skia/GPU Component. The precondition is a Graphite-backed write path to an uninitialized SharedImage combined with a failed recording insertion, after which the never-actually-cleared image is read back. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1. Any escalation of the leaked bytes into a stronger primitive is (inferred) and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/copy_shared_image_helper.cc
modified
if
gpu/command_buffer/service/graphite_utils.cc
modified
GraphiteSharedContext
gpu/command_buffer/service/graphite_utils.h
modified
if
gpu/command_buffer/service/raster_decoder.cc
modified
if
gpu/command_buffer/service/shared_context_state.cc
modified
if
gpu/command_buffer/service/webgpu_decoder_impl.cc
modified

Files Changed

  • gpu/command_buffer/service/shared_context_state.cc
  • gpu/command_buffer/service/graphite_utils.cc
  • gpu/command_buffer/service/webgpu_decoder_impl.cc
  • gpu/command_buffer/service/shared_context_state.h
  • gpu/command_buffer/service/copy_shared_image_helper.cc
  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/graphite_utils.h

Audit Directions

  • Set-cleared-before-flush-confirmed
    Search for any SetCleared() / SetClearedRect() (or analogous initialization flags) called before or without checking that the flush/insertRecording that performs the actual clear succeeded.
  • Discarded flush/submit return values
    Audit callers of FlushWriteAccess, FlushGraphiteRecorder, GraphiteFlush, GraphiteFlushAndSubmit, insertRecording, and submit() for places that ignore the now-meaningful success bool.
  • Recorded-vs-executed assumptions
    Look across Graphite/Dawn command-recording paths for any state (layout transitions, semaphores, cleared/valid flags) updated on the assumption that recorded work always executes, and verify each is deferred until insertion is confirmed.
From 872f3f32bf81dfa4efa23352c1741267401ea14f Mon Sep 17 00:00:00 2001
From: Greg Daniel <[email protected]>
Date: Thu, 16 Jul 2026 06:50:30 -0700
Subject: [PATCH] Verify Graphite recording insertion before updating SharedImage cleared state

Currently, when a SharedImage is uninitialized, we record a clear
command into the Graphite Recorder and immediately mark the SharedImage
as cleared. However, if the Recording containing the clear fails to be
inserted into the Graphite Context for any reason, the clear command
never executes. Since the cleared flag was already updated on the
SharedImage, Chrome never attempts to clear it again on subsequent
accesses, causing uninitialized memory reads.

This CL propagates the recording insertion success (by returning a bool
from FlushGraphiteRecorder and FlushWriteAccess) up to the callsites,
and defers calling SetCleared() / SetClearedRect() until we know the
Graphite recording insertion successfully completes.

Bug: b/497205529
Change-Id: I78f0028b4a2206985417e588534ef404cdbf8a8b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8102302
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Commit-Queue: Greg Daniel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1663118}
---
diff --git a/gpu/command_buffer/service/copy_shared_image_helper.cc b/gpu/command_buffer/service/copy_shared_image_helper.cc
index 31efea78..3bc48e0 100644
--- a/gpu/command_buffer/service/copy_shared_image_helper.cc
+++ b/gpu/command_buffer/service/copy_shared_image_helper.cc
@@ -188,12 +188,12 @@
 
   dest_scoped_access->surface()->writePixels(subset, xoffset, yoffset);
 
-  shared_context_state->FlushWriteAccess(dest_scoped_access);
+  bool success = shared_context_state->FlushWriteAccess(dest_scoped_access);
   shared_context_state->SubmitIfNecessary(
       std::move(end_semaphores),
       dest_scoped_access->NeedGraphiteContextSubmit());
 
-  if (!dest_shared_image->IsCleared()) {
+  if (success && !dest_shared_image->IsCleared()) {
     dest_shared_image->SetClearedRect(dest_cleared_rect);
   }
 
@@ -291,14 +291,20 @@
   }
 
   bool need_graphite_submit = dest_scoped_access->NeedGraphiteContextSubmit();
+  bool update_cleared_rect = false;
+  gfx::Rect new_cleared_rect;
+
   // Flush dest surface and submit if necessary before exiting.
   absl::Cleanup cleanup = [&]() {
-    shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
+    bool success =
+        shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
     shared_context_state_->SubmitIfNecessary(std::move(end_semaphores),
                                              need_graphite_submit);
+    if (success && update_cleared_rect) {
+      dest_shared_image->SetClearedRect(new_cleared_rect);
+    }
   };
 
-  gfx::Rect new_cleared_rect;
   gfx::Rect old_cleared_rect = dest_shared_image->ClearedRect();
   if (!gles2::TextureManager::CombineAdjacentRects(old_cleared_rect, dest_rect,
                                                    &new_cleared_rect)) {
@@ -360,7 +366,7 @@
     }
 
     if (!dest_shared_image->IsCleared()) {
-      dest_shared_image->SetClearedRect(new_cleared_rect);
+      update_cleared_rect = true;
     }
 
     // Note, that we still generate error for the client to indicate there was
@@ -454,20 +460,26 @@
       skia::BlitRGBAToYUVA(source_image.get(), yuva_sk_surfaces, yuva_info,
                            gfx::RectToSkRect(dest_rect), false,
                            gfx::RectToSkRect(source_rect));
-      dest_shared_image->SetCleared();
+      new_cleared_rect = gfx::Rect(dest_shared_image->size());
+      update_cleared_rect = true;
     }
 
     if (!dest_shared_image->IsCleared()) {
-      dest_shared_image->SetClearedRect(new_cleared_rect);
+      update_cleared_rect = true;
     }
   }
 
   // Cancel cleanup as the cleanup order is different here.
   std::move(cleanup).Cancel();
-  shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
+  bool success =
+      shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
   source_scoped_access->ApplyBackendSurfaceEndState();
   shared_context_state_->SubmitIfNecessary(std::move(end_semaphores),
                                            need_graphite_submit);
+
+  if (success && update_cleared_rect) {
+    dest_shared_image->SetClearedRect(new_cleared_rect);
+  }
   return result;
 }
 
@@ -818,11 +830,12 @@
     }
   }
 
-  shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
+  bool success =
+      shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
   shared_context_state_->SubmitIfNecessary(std::move(end_semaphores),
                                            need_graphite_submit);
 
-  if (!dest_shared_image->IsCleared()) {
+  if (success && !dest_shared_image->IsCleared()) {
     dest_shared_image->SetClearedRect(gfx::Rect(src_width, src_height));
   }
   return base::ok();
diff --git a/gpu/command_buffer/service/graphite_utils.cc b/gpu/command_buffer/service/graphite_utils.cc
index f7518d6..32f781c 100644
--- a/gpu/command_buffer/service/graphite_utils.cc
+++ b/gpu/command_buffer/service/graphite_utils.cc
@@ -63,18 +63,20 @@
 
 }  // namespace
 
-void GraphiteFlush(GraphiteSharedContext* context,
+bool GraphiteFlush(GraphiteSharedContext* context,
                    skgpu::graphite::Recorder* recorder) {
   auto recording = recorder->snap();
   if (recording) {
-    context->insertRecording({recording.get()});
+    return context->insertRecording({recording.get()});
   }
+  return true;
 }
 
-void GraphiteFlushAndSubmit(GraphiteSharedContext* context,
+bool GraphiteFlushAndSubmit(GraphiteSharedContext* context,
                             skgpu::graphite::Recorder* recorder) {
-  GraphiteFlush(context, recorder);
+  bool success = GraphiteFlush(context, recorder);
   context->submit();
+  return success;
 }
 
 bool GraphiteReadPixelsSync(GraphiteSharedContext* context,
diff --git a/gpu/command_buffer/service/graphite_utils.h b/gpu/command_buffer/service/graphite_utils.h
index 4e35ea2..94f6ff2 100644
--- a/gpu/command_buffer/service/graphite_utils.h
+++ b/gpu/command_buffer/service/graphite_utils.h
@@ -21,11 +21,11 @@
 class GraphiteSharedContext;
 
 GPU_GLES2_EXPORT
-void GraphiteFlush(GraphiteSharedContext* context,
+bool GraphiteFlush(GraphiteSharedContext* context,
                    skgpu::graphite::Recorder* recorder);
 
 GPU_GLES2_EXPORT
-void GraphiteFlushAndSubmit(GraphiteSharedContext* context,
+bool GraphiteFlushAndSubmit(GraphiteSharedContext* context,
                             skgpu::graphite::Recorder* recorder);
 
 // Synchronously read pixels from a graphite image.
diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc
index 92f5fd2..058dff1 100644
--- a/gpu/command_buffer/service/raster_decoder.cc
+++ b/gpu/command_buffer/service/raster_decoder.cc
@@ -899,6 +899,7 @@
   std::unique_ptr<SkiaImageRepresentation> shared_image_;
   std::unique_ptr<SkiaImageRepresentation::ScopedWriteAccess>
       scoped_shared_image_write_;
+  bool should_clear_shared_image_ = false;
 
   std::unique_ptr<RasterImageRepresentation> shared_image_raster_;
   std::unique_ptr<RasterImageRepresentation::ScopedWriteAccess>
@@ -2059,12 +2060,13 @@
     return;
   }
 
-  shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
+  bool success =
+      shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
   shared_context_state_->SubmitIfNecessary(
       std::move(end_semaphores),
       dest_scoped_access->NeedGraphiteContextSubmit());
 
-  if (!dest_shared_image->IsCleared()) {
+  if (success && !dest_shared_image->IsCleared()) {
     dest_shared_image->SetClearedRect(
         gfx::Rect(x_offset, y_offset, src_width, src_height));
   }
@@ -2311,12 +2313,13 @@
         /*numLevels=*/1, release_proc, graphite_texture_ptr);
   }
 
-  shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
+  bool success =
+      shared_context_state_->FlushWriteAccess(dest_scoped_access.get());
   shared_context_state_->SubmitIfNecessary(
       std::move(end_semaphores),
       dest_scoped_access->NeedGraphiteContextSubmit());
 
-  return written;
+  return written && success;
 }
 
 void RasterDecoderImpl::DoReadbackARGBImagePixelsINTERNAL(
@@ -2951,9 +2954,13 @@
   // incorrect.
   if (needs_clear) {
     raster_canvas_->drawColor(sk_color_4f, SkBlendMode::kSrc);
-    shared_image_->SetCleared();
+    if (graphite_shared_context()) {
+      should_clear_shared_image_ = true;
+    } else {
+      shared_image_->SetCleared();
+    }
   }
-  DCHECK(shared_image_->IsCleared());
+  DCHECK(graphite_shared_context() || shared_image_->IsCleared());
 }
 
 scoped_refptr<Buffer> RasterDecoderImpl::GetShmBuffer(uint32_t shm_id) {
@@ -3133,7 +3140,8 @@
     // scoped_shared_image_write_ can be nullptr if sk_surface_ was set by
     // SetUpForRasterCHROMIUMForTest.
     if (scoped_shared_image_write_) {
-      shared_context_state_->FlushWriteAccess(scoped_shared_image_write_.get());
+      bool success = shared_context_state_->FlushWriteAccess(
+          scoped_shared_image_write_.get());
       // Flushing surface will cause vulkan command buffer to be recorded with
       // image layout transitions as necessary. Transitioning layout back to
       // desired need to be happening after.
@@ -3143,6 +3151,9 @@
           scoped_shared_image_write_->NeedGraphiteContextSubmit();
       shared_context_state_->SubmitIfNecessary(std::move(end_semaphores_),
                                                need_graphite_submit);
+      if (success && should_clear_shared_image_) {
+        shared_image_->SetCleared();
+      }
     } else {
       DCHECK(end_semaphores_.empty());
     }
@@ -3153,6 +3164,7 @@
   scoped_shared_image_write_.reset();
   shared_image_.reset();
   paint_op_shared_image_provider_.reset();
+  should_clear_shared_image_ = false;
 
   // Test only path for SetUpForRasterCHROMIUMForTest.
   sk_surface_for_testing_.reset();
diff --git a/gpu/command_buffer/service/shared_context_state.cc b/gpu/command_buffer/service/shared_context_state.cc
index 08ba87c8..eb67ed1 100644
--- a/gpu/command_buffer/service/shared_context_state.cc
+++ b/gpu/command_buffer/service/shared_context_state.cc
@@ -816,13 +816,14 @@
   return true;
 }
 
-void SharedContextState::FlushGraphiteRecorder() {
+bool SharedContextState::FlushGraphiteRecorder() {
   auto recording = gpu_main_graphite_recorder()->snap();
   if (recording) {
     skgpu::graphite::InsertRecordingInfo info = {};
     info.fRecording = recording.get();
-    graphite_shared_context()->insertRecording(info);
+    return graphite_shared_context()->insertRecording(info);
   }
+  return true;
 }
 
 void SharedContextState::FlushAndSubmit(bool sync_to_cpu) {
@@ -837,16 +838,17 @@
   }
 }
 
-void SharedContextState::FlushWriteAccess(
+bool SharedContextState::FlushWriteAccess(
     SkiaImageRepresentation::ScopedWriteAccess* access) {
   static int flush_count = 0;
   const base::TimeTicks start = base::TimeTicks::Now();
+  bool success = true;
   if (graphite_shared_context()) {
     // The only way to flush GPU work with Graphite is to snap and insert a
     // recording here. It's also necessary to submit before dropping the scoped
     // access since we want the Dawn texture to be alive on submit, but that's
     // handled in SubmitIfNecessary.
-    FlushGraphiteRecorder();
+    success = FlushGraphiteRecorder();
   } else {
     if (access->HasBackendSurfaceEndState()) {
       access->ApplyBackendSurfaceEndState();
@@ -867,6 +869,7 @@
         "GPU.RasterDecoder.TimeToFlush", base::TimeTicks::Now() - start,
         base::Microseconds(1), base::Seconds(1), 100);
   }
+  return success;
 }
 
 void SharedContextState::SubmitIfNecessary(
diff --git a/gpu/command_buffer/service/shared_context_state.h b/gpu/command_buffer/service/shared_context_state.h
index 9de74f5..d590efd 100644
--- a/gpu/command_buffer/service/shared_context_state.h
+++ b/gpu/command_buffer/service/shared_context_state.h
@@ -147,8 +147,8 @@
   bool IsGLInitialized() const { return !!feature_info_; }
 
   void FlushAndSubmit(bool sync_to_cpu);
-  void FlushGraphiteRecorder();
-  void FlushWriteAccess(SkiaImageRepresentation::ScopedWriteAccess* access);
+  bool FlushGraphiteRecorder();
+  bool FlushWriteAccess(SkiaImageRepresentation::ScopedWriteAccess* access);
   void SubmitIfNecessary(std::vector<GrBackendSemaphore> signal_semaphores,
                          bool need_graphite_submit);
 
diff --git a/gpu/command_buffer/service/webgpu_decoder_impl.cc b/gpu/command_buffer/service/webgpu_decoder_impl.cc
index bfaad93..a1bd314 100644
--- a/gpu/command_buffer/service/webgpu_decoder_impl.cc
+++ b/gpu/command_buffer/service/webgpu_decoder_impl.cc
@@ -894,12 +894,13 @@
 
       // It's ok to pass in empty GrFlushInfo here since SignalSemaphores()
       // will populate it with semaphores and call GrDirectContext::flush.
+      success = true;
       if (shared_context_state_->gr_context()) {
         skgpu::ganesh::Flush(surface);
       } else {
         DCHECK(shared_context_state_->graphite_shared_context());
         DCHECK(shared_context_state_->gpu_main_graphite_recorder());
-        GraphiteFlushAndSubmit(
+        success = GraphiteFlushAndSubmit(
             shared_context_state_->graphite_shared_context(),
             shared_context_state_->gpu_main_graphite_recorder());
       }
@@ -909,7 +910,7 @@
 
       SignalSemaphores(std::move(end_semaphores));
 
-      return true;
+      return success;
     }
 
     void WaitForSemaphores(std::vector<GrBackendSemaphore> semaphores) {
@@ -2494,17 +2495,21 @@
     clear_color = {0, 0, 0, 0};
   }
   canvas->drawColor(clear_color, SkBlendMode::kSrc);
-  representation->SetCleared();
 
   // It's ok to pass in empty GrFlushInfo here since SignalSemaphores()
   // will populate it with semaphores and call GrDirectContext::flush.
+  bool success = true;
   if (shared_context_state_->gr_context()) {
     skgpu::ganesh::Flush(surface);
   } else {
     DCHECK(shared_context_state_->graphite_shared_context());
     DCHECK(shared_context_state_->gpu_main_graphite_recorder());
-    GraphiteFlushAndSubmit(shared_context_state_->graphite_shared_context(),
-                           shared_context_state_->gpu_main_graphite_recorder());
+    success = GraphiteFlushAndSubmit(
+        shared_context_state_->graphite_shared_context(),
+        shared_context_state_->gpu_main_graphite_recorder());
+  }
+  if (success) {
+    representation->SetCleared();
   }
   // Transition the image back to the desired end state. This is used for
   // transitioning the image to the external queue for Vulkan/GL interop.
Loading diff…

Original Bug Report

reported by [email protected]

Potential cross-origin info leak via state desynchronization in Skia Graphite QueueManager

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: Skia Graphite’s QueueManager updates its tracking of the last added recording ID before completing the insertion process. If insertion subsequently fails, the tracker remains incremented, allowing an attacker to drop a recording (e.g., a SharedImage clear) without failing the strict ordering check for future recordings, leading to a potential leak of uninitialized GPU memory.

Affected files:

  • third_party/skia/src/gpu/graphite/QueueManager.cpp
  • third_party/skia/src/gpu/graphite/QueueManager.h

Estimated timestamp from git blame: 2026-01-20

Summary

A logic error in Skia Graphite’s QueueManager causes the internal tracking of recording IDs to be updated prematurely. If a recording fails during insertion after this update occurs, the system’s state becomes desynchronized. This allows subsequent recordings to pass ordering checks and execute, even though a preceding recording was dropped. This can be exploited to bypass GPU memory initialization, leading to a reliable cross-origin information leak.

Vulnerability Details

In third_party/skia/src/gpu/graphite/QueueManager.cpp, the addRecording function is responsible for adding commands from a Recording to the current command buffer. For contexts requiring ordered recordings (such as Chrome’s), it verifies that the incoming recording ID is exactly one greater than the last added ID. However, the tracking map fLastAddedRecordingIDs is updated prematurely at line 133:

128:        RETURN_FAIL_IF(recordingID && info.fRecording->priv().uniqueID() != *recordingID + 1,
129:                       InsertStatus::kOutOfOrderRecording,
130:                       "Recordings are expected to be replayed in order");
131:
132:        // Note the new Recording ID.
133:        fLastAddedRecordingIDs.set(recorderID, info.fRecording->priv().uniqueID());

Following this update, there are multiple RETURN_FAIL_IF checks for operations like command buffer setup (line 145) or lazy proxy instantiation (lines 173, 178). If any of these fail, the function returns an error, but the fLastAddedRecordingIDs state is not rolled back.

Because the counter has been advanced, a subsequent recording ($N+1$) will be accepted even if recording $N$ failed to execute. The failed recording $N$ is dropped.

Potential Exploitation Steps

Note: These are suggested steps based on code analysis; we do not yet have a working proof-of-concept.

  1. From a compromised renderer, an attacker creates a new SharedImage. Its internal IsCleared() state is initially false.
  2. The attacker binds the image and issues a BeginRasterCHROMIUM command.
  3. In RasterDecoderImpl::DoBeginRasterCHROMIUM, Chrome observes that the image needs clearing and issues a clear command to the Skia Canvas. This clear operation becomes part of recording $N$.
  4. Crucially, Chrome immediately marks the SharedImage as cleared via shared_image_->SetCleared() (in raster_decoder.cc), assuming the recording will successfully execute.
  5. The attacker intentionally exhausts GPU memory (e.g., by allocating many large textures) and flushes the context.
  6. Chrome snaps recording $N$ and submits it to QueueManager::addRecording. The ID tracker is updated to $N$.
  7. Due to the induced OOM, setupCommandBuffer() or a proxy instantiation fails. addRecording returns a failure status (e.g., kAddCommandsFailed).
  8. In Chrome’s GraphiteSharedContext::insertRecording, this failure is treated as recoverable (unlike kOutOfOrderRecording). The context is kept alive, but the clear commands are dropped.
  9. The attacker frees memory and issues a command to read from the SharedImage (recording $N+1$). Since Chrome’s state indicates it is cleared, no further clear is issued.
  10. QueueManager::addRecording accepts recording $N+1$ because the tracker expects $N+1$.
  11. The GPU executes recording $N+1$, reading uninitialized GPU memory that may contain cross-origin data.

The update to fLastAddedRecordingIDs should be deferred until the end of QueueManager::addRecording(), after all potential failure points have passed. Alternatively, a rollback mechanism should be implemented in the RETURN_FAIL_IF macro or failure paths.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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