CVE-2026-79007
Overview
Background
- `CompoundImageBacking`
- a shared-image backing that wraps multiple sub-backings (e.g. a shared-memory backing and a GPU backing) and keeps them in sync, exposing per-API “wrapped” representations (GL, Skia, Dawn, Overlay, Vulkan, VAAPI, etc.).
- `NotifyBeginAccess`
- called by each wrapped representation before access; it ensures the backing about to be accessed holds the latest pixels, copying from the current-latest backing when needed.
- Content sync / `UploadFromMemory`
- the copy that brings a GPU backing up to date from the shared-memory backing before GPU access; it can fail (e.g.
UploadFromMemoryreturns false). - Cleared/versioning state
- the GPU backing is marked cleared at creation and content-version ids (
latest_content_id_,content_id_) track which backing has current data; a representation treats a “cleared” backing as safe to read.
Root Cause Analysis
Wrapped representations of CompoundImageBacking called NotifyBeginAccess (which returned void) and then unconditionally proceeded to wrapped_->BeginAccess()/BeginReadAccess()/BeginWriteAccess(). When the content sync from the shared-memory backing to the newly allocated GPU backing failed, the error was swallowed: the GPU backing had been prematurely marked as cleared at creation, so the representation layer assumed it was safe to read and exposed uninitialized VRAM to the renderer. The invariant violated is that access to a backing must be denied when the copy that was supposed to populate it did not succeed.
The fix changes NotifyBeginAccess to return bool ([[nodiscard]]), returning false when the required content copy (copy_succeeded == false) fails and deliberately not advancing versioning so a later access can retry the sync; every wrapped representation now checks that return and aborts access (returning false/nullptr/empty vector/SkPixmap() as appropriate) instead of reading the un-synced backing.
NotifyBeginAccess so a failed shared-memory-to-GPU content sync still allowed access to a GPU backing that was falsely marked cleared; the fix propagates a bool failure from NotifyBeginAccess through every representation’s BeginAccess path and aborts on failure.Attack Path
- Create a compound-backed shared image
A renderer/GPU client allocates a
CompoundImageBackingwith both a shared-memory and a (lazily allocated) GPU backing, the latter marked cleared at creation. - Force the content sync to fail
Trigger a state where
NotifyBeginAccessmust copy shared-memory content into the GPU backing but the copy (UploadFromMemory) fails. (inferred) - Access proceeds despite failed sync
Pre-patch,
NotifyBeginAccessreturnsvoid, the representation ignores the failure and callswrapped_->BeginReadAccess()on the GPU backing that was never populated. - Read uninitialized VRAM Because the GPU backing was prematurely marked cleared, the read returns uninitialized GPU memory contents to the client.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/shared_image/compound_image_backing.cc |
modified | |
TEST_Fgpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/compound_image_backing.hgpu/command_buffer/service/shared_image/test_image_backing.ccgpu/command_buffer/service/shared_image/test_image_backing.hgpu/command_buffer/service/shared_image/compound_image_backing_unittest.ccgpu/command_buffer/service/shared_image/compound_image_backing.cc
Audit Directions
- Ignored sync/copy return valuesSearch shared-image and backing code for content-sync or upload/readback calls whose success is not checked before the target is exposed for access.
- Premature cleared markingAudit backings marked cleared/initialized at creation and verify that any populating copy actually succeeded before a representation is allowed to read.
- `void` notify/begin hooksReview pre-access notification hooks that return
void; convert to a checked result where failure must abort the access, and confirm all representation subclasses honor it.
Patch
From c95fed050d86fc170f3d965e27f05821e9f3d3c2 Mon Sep 17 00:00:00 2001 From: vikas soni <[email protected]> Date: Thu, 09 Jul 2026 12:40:54 -0700 Subject: [PATCH] [GPU-Security]: Fix VRAM leak in CompoundImageBacking on sync failure. When synchronization from a shared memory backing to a newly allocated GPU backing fails in CompoundImageBacking, the error was previously swallowed. Because the GPU backing was prematurely marked as cleared at creation, the representation layer assumed it was safe to read, exposing uninitialized VRAM to the renderer. This CL fixes the issue by: 1. Changing CompoundImageBacking::NotifyBeginAccess to return a boolean indicating success. 2. Propagating this status through all wrapped representations, aborting access if NotifyBeginAccess returns false. 3. Adding a unit test to verify that access fails when synchronization fails. Bug: 495579602 Change-Id: I23f83b79a43a86b8b3129d13ef7b3206afd871b5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8046642 Reviewed-by: Vasiliy Telezhnikov <[email protected]> Commit-Queue: vikas soni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1659777} --- diff --git a/gpu/command_buffer/service/shared_image/compound_image_backing.cc b/gpu/command_buffer/service/shared_image/compound_image_backing.cc index 81612003..5bbf266 100644 --- a/gpu/command_buffer/service/shared_image/compound_image_backing.cc +++ b/gpu/command_buffer/service/shared_image/compound_image_backing.cc @@ -152,8 +152,10 @@ bool BeginAccess(GLenum mode) final { AccessMode access_mode = mode == kReadAccessMode ? AccessMode::kRead : AccessMode::kWrite; - compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, - SharedImageAccessStream::kGL); + if (!compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, + SharedImageAccessStream::kGL)) { + return false; + } access_mode_ = access_mode; return wrapped_->BeginAccess(mode); } @@ -216,8 +218,10 @@ bool BeginAccess(GLenum mode) override { AccessMode access_mode = mode == kReadAccessMode ? AccessMode::kRead : AccessMode::kWrite; - compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, - SharedImageAccessStream::kGL); + if (!compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, + SharedImageAccessStream::kGL)) { + return false; + } access_mode_ = access_mode; return wrapped_->BeginAccess(mode); } @@ -289,9 +293,11 @@ std::vector<GrBackendSemaphore>* begin_semaphores, std::vector<GrBackendSemaphore>* end_semaphores, std::unique_ptr<skgpu::MutableTextureState>* end_state) final { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kWrite, - SharedImageAccessStream::kSkia); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kWrite, + SharedImageAccessStream::kSkia)) { + return {}; + } return wrapped_->BeginWriteAccess(final_msaa_count, surface_props, update_rect, begin_semaphores, end_semaphores, end_state); @@ -300,9 +306,11 @@ std::vector<GrBackendSemaphore>* begin_semaphores, std::vector<GrBackendSemaphore>* end_semaphores, std::unique_ptr<skgpu::MutableTextureState>* end_state) final { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kWrite, - SharedImageAccessStream::kSkia); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kWrite, + SharedImageAccessStream::kSkia)) { + return {}; + } return wrapped_->BeginWriteAccess(begin_semaphores, end_semaphores, end_state); } @@ -316,8 +324,11 @@ std::vector<GrBackendSemaphore>* begin_semaphores, std::vector<GrBackendSemaphore>* end_semaphores, std::unique_ptr<skgpu::MutableTextureState>* end_state) final { - compound_backing()->NotifyBeginAccess( - wrapped_->backing(), AccessMode::kRead, SharedImageAccessStream::kSkia); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kRead, + SharedImageAccessStream::kSkia)) { + return {}; + } return wrapped_->BeginReadAccess(begin_semaphores, end_semaphores, end_state); } @@ -373,15 +384,19 @@ std::vector<sk_sp<SkSurface>> BeginWriteAccess( const SkSurfaceProps& surface_props, const gfx::Rect& update_rect) final { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kWrite, - SharedImageAccessStream::kSkia); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kWrite, + SharedImageAccessStream::kSkia)) { + return {}; + } return wrapped_->BeginWriteAccess(surface_props, update_rect); } std::vector<scoped_refptr<GraphiteTextureHolder>> BeginWriteAccess() final { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kWrite, - SharedImageAccessStream::kSkia); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kWrite, + SharedImageAccessStream::kSkia)) { + return {}; + } return wrapped_->BeginWriteAccess(); } void EndWriteAccess() final { @@ -391,8 +406,11 @@ } std::vector<scoped_refptr<GraphiteTextureHolder>> BeginReadAccess() final { - compound_backing()->NotifyBeginAccess( - wrapped_->backing(), AccessMode::kRead, SharedImageAccessStream::kSkia); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kRead, + SharedImageAccessStream::kSkia)) { + return {}; + } return wrapped_->BeginReadAccess(); } void EndReadAccess() final { @@ -443,8 +461,10 @@ if (internal_usage & kWriteUsage) { access_mode = AccessMode::kWrite; } - compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, - SharedImageAccessStream::kDawn); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), access_mode, SharedImageAccessStream::kDawn)) { + return nullptr; + } access_mode_ = access_mode; return wrapped_->BeginAccess(webgpu_usage, internal_usage); } @@ -502,8 +522,11 @@ AccessMode access_mode = usage & wgpu::BufferUsage::MapWrite ? AccessMode::kWrite : AccessMode::kRead; - compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, - SharedImageAccessStream::kDawnBuffer); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), access_mode, + SharedImageAccessStream::kDawnBuffer)) { + return nullptr; + } access_mode_ = access_mode; return wrapped_->BeginAccess(usage); } @@ -544,10 +567,11 @@ // OverlayImageRepresentation implementation. bool BeginReadAccess(gfx::GpuFenceHandle& acquire_fence) final { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kRead, - SharedImageAccessStream::kOverlay); - + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kRead, + SharedImageAccessStream::kOverlay)) { + return false; + } return wrapped_->BeginReadAccess(acquire_fence); } void EndReadAccess(gfx::GpuFenceHandle release_fence) final { @@ -651,9 +675,11 @@ } bool BeginAccess() override { - compound_backing()->NotifyBeginAccess( - wrapped_->backing(), AccessMode::kWrite, - SharedImageAccessStream::kWebNNTensor); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kWrite, + SharedImageAccessStream::kWebNNTensor)) { + return false; + } return wrapped_->BeginAccess(); } @@ -687,9 +713,11 @@ } SkPixmap BeginReadAccess() override { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kRead, - SharedImageAccessStream::kMemory); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kRead, + SharedImageAccessStream::kMemory)) { + return SkPixmap(); + } return wrapped_->BeginReadAccess(); } @@ -730,9 +758,11 @@ } bool BeginWriteAccess() override { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kWrite, - SharedImageAccessStream::kVaapi); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kWrite, + SharedImageAccessStream::kVaapi)) { + return false; + } return wrapped_->BeginWriteAccess(); } void EndWriteAccess() override { @@ -741,9 +771,11 @@ AccessMode::kWrite); } bool BeginReadAccess() override { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), - AccessMode::kRead, - SharedImageAccessStream::kVaapi); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), AccessMode::kRead, + SharedImageAccessStream::kVaapi)) { + return false; + } return wrapped_->BeginReadAccess(); } void EndReadAccess() override { @@ -808,8 +840,11 @@ bool BeginAccess(AccessMode access_mode, std::vector<VkSemaphore>& begin_semaphores, std::vector<VkSemaphore>& end_semaphores) override { - compound_backing()->NotifyBeginAccess(wrapped_->backing(), access_mode, - SharedImageAccessStream::kVulkan); + if (!compound_backing()->NotifyBeginAccess( + wrapped_->backing(), access_mode, + SharedImageAccessStream::kVulkan)) { + return false; + } if (!wrapped_->BeginAccess(access_mode, begin_semaphores, end_semaphores)) { return false; } @@ -1184,7 +1219,7 @@ } } -void CompoundImageBacking::NotifyBeginAccess(SharedImageBacking* backing, +bool CompoundImageBacking::NotifyBeginAccess(SharedImageBacking* backing, RepresentationAccessMode mode, SharedImageAccessStream stream) { AutoLock auto_lock(this); @@ -1201,7 +1236,7 @@ ++latest_content_id_; access_element->content_id_ = latest_content_id_; } - return; + return true; } // STALE OR TRANSIENT PATH: Either this permanent backing is out-of-date, or @@ -1235,7 +1270,7 @@ } else { LOG(ERROR) << "Failed to copy from " << latest_content_element->GetBacking()->GetName() << " to " - << backing->GetName() << ". Backing can be using stale data"; + << backing->GetName(); } UMA_HISTOGRAM_BOOLEAN("GPU.CompoundImageBacking.ContentSync.Success", @@ -1255,6 +1290,13 @@ UMA_HISTOGRAM_SPARSE( "GPU.CompoundImageBacking.ContentSync.InitialSharedImageUsage", static_cast<int32_t>(static_cast<uint32_t>(this->usage()))); + + if (!copy_succeeded) { + // The destination backing was not updated so it must not be exposed to + // the caller. Do not advance versioning so a future access can retry + // the sync. + return false; + } } // UPDATE VERSIONING: @@ -1272,6 +1314,7 @@ } else if (updated_backing && access_element) { access_element->content_id_ = latest_content_id_; } + return true; } void CompoundImageBacking::NotifyEndAccess(SharedImageBacking* backing, diff --git a/gpu/command_buffer/service/shared_image/compound_image_backing.h b/gpu/command_buffer/service/shared_image/compound_image_backing.h index 74549a6d..c1232ee 100644 --- a/gpu/command_buffer/service/shared_image/compound_image_backing.h +++ b/gpu/command_buffer/service/shared_image/compound_image_backing.h @@ -202,10 +202,11 @@ // Called by wrapped representations before access. This will update // the backing that is going to be accessed if most recent pixels are in - // a different backing. - void NotifyBeginAccess(SharedImageBacking* backing, - RepresentationAccessMode mode, - SharedImageAccessStream stream); + // a different backing. Returns false if a required content sync failed, + // in which case the caller must not proceed with the access. + [[nodiscard]] bool NotifyBeginAccess(SharedImageBacking* backing, + RepresentationAccessMode mode, + SharedImageAccessStream stream); // Called by wrapped representations during EndAccess(). This will update the // CompoundImageBacking's clear rect with the accessed backing's clear rect it diff --git a/gpu/command_buffer/service/shared_image/test_image_backing.cc b/gpu/command_buffer/service/shared_image/test_image_backing.cc index f8bcafce..71fdabe 100644 --- a/gpu/command_buffer/service/shared_image/test_image_backing.cc +++ b/gpu/command_buffer/service/shared_image/test_image_backing.cc @@ -245,7 +245,7 @@ bool TestImageBacking::UploadFromMemory(const std::vector<SkPixmap>& pixmap) { DCHECK_EQ(format().NumberOfPlanes(), static_cast<int>(pixmap.size())); upload_from_memory_called_ = true; - return true; + return upload_from_memory_succeeds_; } bool TestImageBacking::ReadbackToMemory(const std::vector<SkPixmap>& pixmaps) { diff --git a/gpu/command_buffer/service/shared_image/test_image_backing.h b/gpu/command_buffer/service/shared_image/test_image_backing.h index 94c0021..5bbdabae 100644 --- a/gpu/command_buffer/service/shared_image/test_image_backing.h +++ b/gpu/command_buffer/service/shared_image/test_image_backing.h @@ -51,6 +51,9 @@ GLuint service_id() const { return textures_[0]->service_id(); } void set_can_access(bool can_access) { can_access_ = can_access; } bool can_access() const { return can_access_; } + void set_upload_from_memory_succeeds(bool succeeds) { + upload_from_memory_succeeds_ = succeeds; + } #if BUILDFLAG(IS_APPLE) void set_in_use_by_window_server(bool in_use_by_window_server) { @@ -101,6 +104,7 @@ #endif bool upload_from_memory_called_ = false; + bool upload_from_memory_succeeds_ = true; bool readback_to_memory_called_ = false; PurgeableCallback set_purgeable_callback_; PurgeableCallback set_not_purgeable_callback_; diff --git a/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc b/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc index 9ba8c7e..15b42b3 100644 --- a/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc +++ b/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc @@ -391,6 +391,67 @@ EXPECT_TRUE(GetGpuHasLatestContent(compound_backing)); } +TEST_F(CompoundImageBackingTest, AccessFailsOnCopyFailure) { + auto backing = CreateCompoundBacking( + {SHARED_IMAGE_USAGE_GLES2_READ, SHARED_IMAGE_USAGE_DISPLAY_READ}); + auto* compound_backing = static_cast<CompoundImageBacking*>(backing.get()); + + auto factory_rep = + manager_.Register(std::move(backing), &memory_type_tracker_); + + auto gl_rep = manager_.ProduceGLTexturePassthrough( + compound_backing->mailbox(), &memory_type_tracker_); + ASSERT_TRUE(gl_rep); + ASSERT_TRUE(HasGpuBacking(compound_backing)); + + auto* gpu_backing = GetGpuBacking(compound_backing); + gpu_backing->set_upload_from_memory_succeeds(false); + + // Read access should fail since the GPU backing could not be updated with + // the latest content from shared memory. + { + auto gl_access = gl_rep->BeginScopedAccess( + GLTextureImageRepresentationBase::kReadAccessMode, + SharedImageRepresentation::AllowUnclearedAccess::kNo); + EXPECT_FALSE(gl_access); + } + EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset()); + EXPECT_FALSE(GetGpuHasLatestContent(compound_backing)); + + // The Skia read path should also fail. + std::vector<GrBackendSemaphore> begin_semaphores; + std::vector<GrBackendSemaphore> end_semaphores; ... (file diff truncated)
Regression Test / PoC
diff --git a/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc b/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
index 9ba8c7e..15b42b3 100644
--- a/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
@@ -391,6 +391,67 @@
EXPECT_TRUE(GetGpuHasLatestContent(compound_backing));
}
+TEST_F(CompoundImageBackingTest, AccessFailsOnCopyFailure) {
+ auto backing = CreateCompoundBacking(
+ {SHARED_IMAGE_USAGE_GLES2_READ, SHARED_IMAGE_USAGE_DISPLAY_READ});
+ auto* compound_backing = static_cast<CompoundImageBacking*>(backing.get());
+
+ auto factory_rep =
+ manager_.Register(std::move(backing), &memory_type_tracker_);
+
+ auto gl_rep = manager_.ProduceGLTexturePassthrough(
+ compound_backing->mailbox(), &memory_type_tracker_);
+ ASSERT_TRUE(gl_rep);
+ ASSERT_TRUE(HasGpuBacking(compound_backing));
+
+ auto* gpu_backing = GetGpuBacking(compound_backing);
+ gpu_backing->set_upload_from_memory_succeeds(false);
+
+ // Read access should fail since the GPU backing could not be updated with
+ // the latest content from shared memory.
+ {
+ auto gl_access = gl_rep->BeginScopedAccess(
+ GLTextureImageRepresentationBase::kReadAccessMode,
+ SharedImageRepresentation::AllowUnclearedAccess::kNo);
+ EXPECT_FALSE(gl_access);
+ }
+ EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset());
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+
+ // The Skia read path should also fail.
+ std::vector<GrBackendSemaphore> begin_semaphores;
+ std::vector<GrBackendSemaphore> end_semaphores;
+ auto skia_rep = manager_.ProduceSkia(compound_backing->mailbox(),
+ &memory_type_tracker_, nullptr, {});
+ {
+ auto skia_read =
+ skia_rep->BeginScopedReadAccess(&begin_semaphores, &end_semaphores);
+ EXPECT_FALSE(skia_read);
+ }
+
+ // Write access should also fail since the GPU backing could not be
+ // initialized from shared memory ahead of a partial write.
+ {
+ auto skia_write = skia_rep->BeginScopedWriteAccess(
+ &begin_semaphores, &end_semaphores,
+ SharedImageRepresentation::AllowUnclearedAccess::kNo);
+ EXPECT_FALSE(skia_write);
+ }
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+ EXPECT_TRUE(GetShmHasLatestContent(compound_backing));
+
+ // Once the copy succeeds again, read access should succeed.
+ gpu_backing->set_upload_from_memory_succeeds(true);
+ {
+ auto gl_access = gl_rep->BeginScopedAccess(
+ GLTextureImageRepresentationBase::kReadAccessMode,
+ SharedImageRepresentation::AllowUnclearedAccess::kNo);
+ EXPECT_TRUE(gl_access);
+ }
+ EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset());
+ EXPECT_TRUE(GetGpuHasLatestContent(compound_backing));
+}
+
TEST_F(CompoundImageBackingTest, LazyAllocationFailsCreate) {
auto backing = CreateCompoundBacking({SHARED_IMAGE_USAGE_GLES2_READ});
auto* compound_backing = static_cast<CompoundImageBacking*>(backing.get());
Original Bug Report
Potential uninitialized VRAM exposure in CompoundImageBacking via sync failure
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic error in CompoundImageBacking can potentially expose uninitialized GPU memory to the renderer process. If synchronization from a shared memory backing to a newly allocated GPU backing fails, the error is swallowed, and the uninitialized backing is read because it was prematurely marked as cleared.
Affected files:
gpu/command_buffer/service/shared_image/compound_image_backing.ccgpu/command_buffer/service/shared_image/shared_image_representation.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: 2026-02-18
Description
There is a potential vulnerability in CompoundImageBacking that allows for the exposure of uninitialized GPU memory (VRAM) to a renderer process. The issue arises from a failure to properly handle and propagate errors when synchronizing content from a Shared Memory (SHM) backing to a GPU backing, combined with prematurely marking the GPU backing as cleared.
CompoundImageBacking is used to manage multiple underlying representations of a SharedImage, typically bridging a CPU-accessible shared memory buffer with GPU-accessible textures. When a GPU read access is requested for a representation that does not yet contain the latest content, the backing attempts to copy the data from the SHM buffer to the GPU texture.
However, there are two interacting flaws in gpu/command_buffer/service/shared_image/compound_image_backing.cc:
-
Premature
SetCleared()Call: When a GPU backing is lazily created viaCompoundImageBacking::CreateBackingFromBackingFactory, it is unconditionally marked as cleared if the compound backing has a SHM backing:// gpu/command_buffer/service/shared_image/compound_image_backing.cc:1753 if (has_shm_backing_) { backing->SetCleared(); }This bypasses security checks (like
IsCleared()) in the representation layer that normally prevent reading from uninitialized textures. -
Swallowed Synchronization Errors: When the wrapper representation (e.g.,
WrappedGLTextureCompoundImageRepresentation) callsBeginAccess, it first callscompound_backing()->NotifyBeginAccess(...)to trigger the sync. If the copy fails (e.g., due to memory exhaustion preventing the allocation of staging buffers in Skia), the error is merely logged, andNotifyBeginAccessreturnsvoidwithout signaling an error state to the caller:// gpu/command_buffer/service/shared_image/compound_image_backing.cc:1034 copy_succeeded = copy_manager_->CopyImage( /*src_backing=*/latest_content_element->GetBacking(), /*dst_backing=*/access_element->GetBacking()); if (copy_succeeded) { // ... } else { LOG(ERROR) << "Failed to copy from " << latest_content_element->GetBacking()->GetName() << " to " << access_element->GetBacking()->GetName() << ". Backing can be using stale data"; } // Function returns void, caller assumes success or safe stale data.
Because NotifyBeginAccess does not indicate failure, WrappedGLTextureCompoundImageRepresentation::BeginAccess unconditionally proceeds to call wrapped_->BeginAccess(mode).
Since the texture was marked cleared at creation, the IsCleared() check passes. The renderer is then granted read access to a newly allocated Skia backend texture that failed to receive the SHM data, meaning the texture contains raw, uninitialized VRAM.
Impact
An attacker with renderer execution privileges could potentially induce a cross-origin data leak. Uninitialized VRAM frequently contains sensitive pixel data from other browser tabs, the OS desktop, or other applications. By reading this memory, the attacker violates cross-origin and cross-process boundaries (High Severity / S1).
Potential Attacker Steps
(Note: These are theoretical steps as this agent does not have the ability to run live exploit code to verify the precise memory pressure threshold required.)
- Gain code execution in a renderer process.
- Allocate a
SharedImagebacked by a shared memory buffer by sending themojom::CreateSharedImageWithBufferIPC to the GPU process, requesting both CPU and GPU read/write usage flags. - The GPU process instantiates a
CompoundImageBackingfor this SharedImage, wrapping the SHM buffer and preparing a lazy-instantiated GPU factory. - Apply targeted memory pressure in the GPU process. The attacker might spray allocations or exhaust specific resource pools such that basic VRAM texture allocation succeeds, but subsequent allocations for staging/upload buffers (used by
UploadFromMemory/ Skia’supdateBackendTexture) fail. - Issue a read command via a GPU API (e.g., binding the SharedImage to a WebGL texture and calling
readPixels). - The read access triggers the lazy creation of the GPU backing (e.g.,
WrappedSkImageBacking). It allocates a backend texture (uninitialized) and is immediately marked as cleared byCreateBackingFromBackingFactory. NotifyBeginAccessattempts to sync the SHM data to the GPU backing. Due to the induced memory pressure,copy_manager_->CopyImagefails.- The error is logged but swallowed. The access continues, and the renderer successfully reads the uninitialized VRAM content.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
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. Please feel free to reach out to me if you have concerns or feedback.