CVE-2026-11098
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
error_state_gpu/command_buffer/service/raster_decoder.cc |
modified | |
active_write_mailbox_gpu/command_buffer/service/raster_decoder.cc |
modified | |
ifgpu/command_buffer/service/raster_decoder.cc |
modified | |
RasterCommandsCompletedQuerygpu/command_buffer/service/raster_decoder.cc |
modified | |
SharedImageProvidergpu/command_buffer/service/raster_decoder.h |
modified | |
DecoderClientgpu/command_buffer/service/raster_decoder.h |
modified | |
TEST_Fgpu/command_buffer/service/raster_decoder_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/raster_decoder.ccgpu/command_buffer/service/raster_decoder.hgpu/command_buffer/service/raster_decoder_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccgpu/command_buffer/service/shared_image/wrapped_sk_image_backing.ccgpu/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:
- Missing Context in
SharedImageProviderImpl: Ingpu/command_buffer/service/raster_decoder.cc, theSharedImageProviderImplis instantiated duringDoBeginRasterCHROMIUMwithout knowledge of the active write mailbox. Consequently, itsOpenSharedImageForReadmethod lacks the context required to reject read requests for the mailbox currently bound for writing. - Representation-Level Access Tracking: Certain backings, such as
WrappedSkImageBackingandWrappedGraphiteTextureBacking, do not track access state at the global backing level (unlikeD3DImageBackingwhich tracksin_write_access_). Instead, they rely on localized state within the representation objects (e.g.,SkiaImageRepresentationImpl::write_surfaces_). CallingProduceSkiatwice on the same mailbox returns two independent representations. The read representation (R2) successfully callsBeginReadAccessbecause its localwrite_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.)
- A compromised renderer creates a
SharedImagewithRASTER_READ | RASTER_WRITEusage and receives itsMailbox. - The renderer issues a
BeginRasterCHROMIUMIPC command, passing theMailboxto bind it as the output render target (creating a write representationR1). - The renderer issues a
RasterCHROMIUMIPC command. Within the serialized paint operations, it embeds aDrawImageOpwhere the source image is aPaintImageof typekMailboxreferencing the EXACT SAMEMailbox. - During deserialization,
SharedImageProviderImpl::OpenSharedImageForReadattempts to resolve the mailbox. It creates a second, independent read representation (R2) which successfully acquires read access becauseWrappedSkImageBackinglacks global access tracking. - 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.
- 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.
- The renderer issues
EndRasterCHROMIUMand usesReadbackImagePixelsto exfiltrate the leaked data.
Suggested Fix
- Decoder-Level Protection: Update the
SharedImageProviderImplconstructor ingpu/command_buffer/service/raster_decoder.ccto accept theMailboxcurrently bound for rasterization. InOpenSharedImageForRead, immediately reject and return an error if the requested mailbox matches the active output mailbox. - Backing-Level Protection: Implement global access tracking directly within
WrappedSkImageBackingandWrappedGraphiteTextureBacking. Add state members (e.g.,in_write_access_andnum_readers_) at the backing level to ensure that an active write access strictly prevents the creation or initialization of concurrent read representations, similar to theValidateBeginAccessimplementation inD3DImageBacking.
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.