Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in GPU
DescriptionInsufficient validation of untrusted input in GPU
ComponentGPU
Bug ClassLogic Error
Tracker500315455
Fix commit67aaa1d9fffd (chromium/src) +50/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
error_state_
gpu/command_buffer/service/raster_decoder.cc
modified
active_write_mailbox_
gpu/command_buffer/service/raster_decoder.cc
modified
if
gpu/command_buffer/service/raster_decoder.cc
modified
RasterCommandsCompletedQuery
gpu/command_buffer/service/raster_decoder.cc
modified
SharedImageProvider
gpu/command_buffer/service/raster_decoder.h
modified
DecoderClient
gpu/command_buffer/service/raster_decoder.h
modified
TEST_F
gpu/command_buffer/service/raster_decoder_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/raster_decoder.h
  • gpu/command_buffer/service/raster_decoder_unittest.cc
From 67aaa1d9fffd555be2cd803342d99868c6718daf Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Thu, 23 Apr 2026 06:35:03 -0700
Subject: [PATCH] [OOP-R] Reject reading active output mailbox in OpenSharedImageForRead

Prevent cross-origin pixel leaks via self-texturing feedback loop in
out-of-process rasterization. SharedImageProviderImpl now explicitly
rejects attempts to read from the active output mailbox currently bound
for writing.

Bug: b:500315455
Test: RasterDecoderOOPTest.SharedImageProviderRejectsActiveOutputMailbox
Change-Id: Iea0499e7bd7944a75f7497c1f15076236a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7788070
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619471}
---

diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc
index f0a2abf..c9ef36d8 100644
--- a/gpu/command_buffer/service/raster_decoder.cc
+++ b/gpu/command_buffer/service/raster_decoder.cc
@@ -205,12 +205,14 @@
       scoped_refptr<SharedContextState> shared_context_state,
       SkSurface* output_surface,
       std::vector<GrBackendSemaphore>* end_semaphores,
-      gles2::ErrorState* error_state)
+      gles2::ErrorState* error_state,
+      const gpu::Mailbox& active_write_mailbox)
       : shared_image_factory_(shared_image_factory),
         shared_context_state_(std::move(shared_context_state)),
         output_surface_(output_surface),
         end_semaphores_(end_semaphores),
-        error_state_(error_state) {
+        error_state_(error_state),
+        active_write_mailbox_(active_write_mailbox) {
     DCHECK(shared_image_factory_);
     DCHECK(shared_context_state_);
     DCHECK(output_surface_);
@@ -224,6 +226,16 @@
 
   sk_sp<SkImage> OpenSharedImageForRead(const gpu::Mailbox& mailbox,
                                         Error& error) override {
+    if (mailbox == active_write_mailbox_) {
+      ERRORSTATE_SET_GL_ERROR(error_state_, GL_INVALID_OPERATION,
+                              "SharedImageProviderImpl::OpenSharedImageForRead",
+                              ("Attempting to read from the active output mailbox:" +
+                               mailbox.ToDebugString())
+                                  .c_str());
+      error = Error::kNoAccess;
+      return nullptr;
+    }
+
     auto it = read_accessors_.find(mailbox);
     error = Error::kNoError;
     if (it != read_accessors_.end()) {
@@ -309,6 +321,7 @@
     sk_sp<SkImage> read_access_sk_image;
   };
   base::flat_map<gpu::Mailbox, SharedImageReadAccess> read_accessors_;
+  gpu::Mailbox active_write_mailbox_;
 };
 
 class RasterCommandsCompletedQuery : public QueryManager::Query {
@@ -484,6 +497,7 @@
   void SetUpForRasterCHROMIUMForTest() override;
   void SetOOMErrorForTest() override;
   void DisableFlushWorkaroundForTest() override;
+  cc::SharedImageProvider* GetSharedImageProviderForTest() const override;
   gles2::GLES2Util* GetGLES2Util() override { return &util_; }
 
   // DecoderContext implementation.
@@ -1673,6 +1687,10 @@
   flush_workaround_disabled_for_test_ = true;
 }
 
+cc::SharedImageProvider* RasterDecoderImpl::GetSharedImageProviderForTest() const {
+  return paint_op_shared_image_provider_.get();
+}
+
 void RasterDecoderImpl::OnContextLostError() {
   if (!WasContextLost()) {
     // Need to lose current context before broadcasting!
@@ -2912,7 +2930,7 @@
 
   paint_op_shared_image_provider_ = std::make_unique<SharedImageProviderImpl>(
       &shared_image_representation_factory_, shared_context_state_, sk_surface_,
-      &end_semaphores_, error_state_.get());
+      &end_semaphores_, error_state_.get(), mailbox);
 
   // All or nothing clearing, as no way to validate the client's input on what
   // is the "used" part of the texture.  A separate |needs_clear| flag is needed
diff --git a/gpu/command_buffer/service/raster_decoder.h b/gpu/command_buffer/service/raster_decoder.h
index c3f272272..4f6c913c 100644
--- a/gpu/command_buffer/service/raster_decoder.h
+++ b/gpu/command_buffer/service/raster_decoder.h
@@ -12,6 +12,10 @@
 #include "gpu/command_buffer/service/decoder_context.h"
 #include "gpu/gpu_gles2_export.h"
 
+namespace cc {
+class SharedImageProvider;
+}  // namespace cc
+
 namespace gpu {
 
 class DecoderClient;
@@ -92,6 +96,8 @@
   virtual void SetOOMErrorForTest() = 0;
   virtual void DisableFlushWorkaroundForTest() = 0;
 
+  virtual cc::SharedImageProvider* GetSharedImageProviderForTest() const = 0;
+
  protected:
   RasterDecoder(DecoderClient* client,
                 CommandBufferServiceBase* command_buffer_service,
diff --git a/gpu/command_buffer/service/raster_decoder_unittest.cc b/gpu/command_buffer/service/raster_decoder_unittest.cc
index eebf83c..3a80809da 100644
--- a/gpu/command_buffer/service/raster_decoder_unittest.cc
+++ b/gpu/command_buffer/service/raster_decoder_unittest.cc
@@ -4,6 +4,8 @@
 
 #include "gpu/command_buffer/service/raster_decoder.h"
 
+#include "cc/paint/paint_op_buffer.h"
+
 #include <limits>
 #include <memory>
 #include <string>
@@ -501,5 +503,26 @@
   EXPECT_FALSE(context_state_->gr_context()->abandoned());
 }
 
+TEST_F(RasterDecoderOOPTest, SharedImageProviderRejectsActiveOutputMailbox) {
+  context_state_->set_need_context_state_reset(true);
+  gpu::Mailbox mailbox =
+      CreateMailbox(viz::SinglePlaneFormat::kRGBA_8888,
+                    /*width=*/2, /*height=*/2,
+                    /*cleared=*/true);
+
+  auto& cmd = *GetImmediateAs<cmds::BeginRasterCHROMIUMImmediate>();
+  cmd.Init(0.0f, 0.0f, 0.0f, 0.0f, false, 0, kNoMSAA, false, false, 1.0f,
+           mailbox.name);
+  EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(mailbox.name)));
+
+  cc::SharedImageProvider* provider = decoder_->GetSharedImageProviderForTest();
+  ASSERT_TRUE(provider);
+
+  cc::SharedImageProvider::Error error;
+  sk_sp<SkImage> image = provider->OpenSharedImageForRead(mailbox, error);
+  EXPECT_FALSE(image);
+  EXPECT_EQ(error, cc::SharedImageProvider::Error::kNoAccess);
+}
+
 }  // namespace raster
 }  // namespace gpu
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/raster_decoder_unittest.cc b/gpu/command_buffer/service/raster_decoder_unittest.cc
index eebf83c..3a80809da 100644
--- a/gpu/command_buffer/service/raster_decoder_unittest.cc
+++ b/gpu/command_buffer/service/raster_decoder_unittest.cc
@@ -4,6 +4,8 @@
 
 #include "gpu/command_buffer/service/raster_decoder.h"
 
+#include "cc/paint/paint_op_buffer.h"
+
 #include <limits>
 #include <memory>
 #include <string>
@@ -501,5 +503,26 @@
   EXPECT_FALSE(context_state_->gr_context()->abandoned());
 }
 
+TEST_F(RasterDecoderOOPTest, SharedImageProviderRejectsActiveOutputMailbox) {
+  context_state_->set_need_context_state_reset(true);
+  gpu::Mailbox mailbox =
+      CreateMailbox(viz::SinglePlaneFormat::kRGBA_8888,
+                    /*width=*/2, /*height=*/2,
+                    /*cleared=*/true);
+
+  auto& cmd = *GetImmediateAs<cmds::BeginRasterCHROMIUMImmediate>();
+  cmd.Init(0.0f, 0.0f, 0.0f, 0.0f, false, 0, kNoMSAA, false, false, 1.0f,
+           mailbox.name);
+  EXPECT_EQ(error::kNoError, ExecuteImmediateCmd(cmd, sizeof(mailbox.name)));
+
+  cc::SharedImageProvider* provider = decoder_->GetSharedImageProviderForTest();
+  ASSERT_TRUE(provider);
+
+  cc::SharedImageProvider::Error error;
+  sk_sp<SkImage> image = provider->OpenSharedImageForRead(mailbox, error);
+  EXPECT_FALSE(image);
+  EXPECT_EQ(error, cc::SharedImageProvider::Error::kNoAccess);
+}
+
 }  // namespace raster
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin pixel leak via self-texturing feedback loop in RasterDecoder

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 without the security team.

Overview: A potential vulnerability exists where a compromised renderer can create a self-texturing feedback loop by using the same SharedImage for both reading and writing during a raster operation. This occurs due to a lack of backing-level access coordination in certain SharedImage backings and missing validation in SharedImageProviderImpl. On tile-based GPUs, this feedback loop can cause the GPU to sample stale tile-cache memory, potentially leaking cross-origin pixel data.

Affected files:

  • gpu/command_buffer/service/raster_decoder.cc
  • gpu/command_buffer/service/shared_image/wrapped_sk_image_backing.cc
  • gpu/command_buffer/service/shared_image/wrapped_graphite_texture_backing.cc

Estimated timestamp from git blame: 2025-04-03

Summary

A potential vulnerability in the Chrome GPU process allows a compromised renderer to bypass SharedImage access synchronization. By referencing the output mailbox of a BeginRasterCHROMIUM command as a source image within a subsequent RasterCHROMIUM command, an attacker can trick the graphics pipeline into using the same underlying GPU texture as both a render target and a sampler. On Tile-Based Deferred Rendering (TBDR) architectures, this undefined behavior can cause the GPU to read stale data from the tile cache, leading to an exploitable cross-origin pixel data leak.

Technical Details

The issue stems from two interconnected structural flaws:

  1. Missing Context in SharedImageProviderImpl: In gpu/command_buffer/service/raster_decoder.cc, the SharedImageProviderImpl is instantiated during DoBeginRasterCHROMIUM without knowledge of the active write mailbox. Consequently, its OpenSharedImageForRead method lacks the context required to reject read requests for the mailbox currently bound for writing.
  2. Representation-Level Access Tracking: Certain backings, such as WrappedSkImageBacking and WrappedGraphiteTextureBacking, do not track access state at the global backing level (unlike D3DImageBacking which tracks in_write_access_). Instead, they rely on localized state within the representation objects (e.g., SkiaImageRepresentationImpl::write_surfaces_). Calling ProduceSkia twice on the same mailbox returns two independent representations. The read representation (R2) successfully calls BeginReadAccess because its local write_surfaces_ list is empty, entirely ignoring the active write occurring on the first representation (R1).

Potential Steps to Trigger

(Note: These are suggested steps based on code analysis; our tooling agent does not yet have the ability to run code to provide a working proof of concept.)

  1. A compromised renderer creates a SharedImage with RASTER_READ | RASTER_WRITE usage and receives its Mailbox.
  2. The renderer issues a BeginRasterCHROMIUM IPC command, passing the Mailbox to bind it as the output render target (creating a write representation R1).
  3. The renderer issues a RasterCHROMIUM IPC command. Within the serialized paint operations, it embeds a DrawImageOp where the source image is a PaintImage of type kMailbox referencing the EXACT SAME Mailbox.
  4. During deserialization, SharedImageProviderImpl::OpenSharedImageForRead attempts to resolve the mailbox. It creates a second, independent read representation (R2) which successfully acquires read access because WrappedSkImageBacking lacks global access tracking.
  5. Skia is instructed to execute the draw operation, wrapping the same GPU texture as both the active render target and the source sampler, causing an OpenGL/Vulkan feedback loop.
  6. On TBDR GPUs (common on mobile), this forces the tile-cache into an undefined state, causing it to read uninitialized or stale memory containing cross-origin pixel data from previous render passes.
  7. The renderer issues EndRasterCHROMIUM and uses ReadbackImagePixels to exfiltrate the leaked data.

Suggested Fix

  1. Decoder-Level Protection: Update the SharedImageProviderImpl constructor in gpu/command_buffer/service/raster_decoder.cc to accept the Mailbox currently bound for rasterization. In OpenSharedImageForRead, immediately reject and return an error if the requested mailbox matches the active output mailbox.
  2. Backing-Level Protection: Implement global access tracking directly within WrappedSkImageBacking and WrappedGraphiteTextureBacking. Add state members (e.g., in_write_access_ and num_readers_) at the backing level to ensure that an active write access strictly prevents the creation or initialization of concurrent read representations, similar to the ValidateBeginAccess implementation in D3DImageBacking.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results 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