CVE-2026-79007
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/shared_image/compound_image_backing.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/compound_image_backing.cc
Patch
From c95fed050d86fc170f3d965e27f05821e9f3d3c2 Mon Sep 17 00:00:00 2001
From: vikas soni <vikassoni@chromium.org>
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 <vasilyt@chromium.org>
Commit-Queue: vikas soni <vikassoni@chromium.org>
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();
}
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.