CVE-2026-76042
Overview
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.ccgpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
Patch
From a73a143f694c18300b9d87d3d489cddec4124f73 Mon Sep 17 00:00:00 2001 From: vikas soni <[email protected]> Date: Wed, 22 Jul 2026 11:56:25 -0700 Subject: [PATCH] [GPU Security] Fail CompoundImageBacking access when no latest element. CompoundImageBacking::NotifyBeginAccess synchronizes the target backing from the permanent element holding the latest content. If a transient backing was written but its proactive copy-back in NotifyEndAccess failed, no permanent element carries the latest content id and GetElementWithLatestContent() returns nullptr. The sync block was silently skipped and the access was allowed to proceed against an unsynchronized backing. Return false in this case, matching the existing handling for CopyImage() failure, so the caller aborts the access. Content versioning is left untouched so a later Update() can restore access. Note that if there is no SHM backing (e.g. GPU-only backings) or no Update() occurs, subsequent access calls (including writes) will continue to fail, requiring client-level context/resource re-creation. Add a regression test that drives a transient write without a successful copy-back and verifies read access is refused. Bug: 536460270 Change-Id: I826b4a872bab19d9532db5f51ca17f137432e4a1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8128128 Reviewed-by: Vasiliy Telezhnikov <[email protected]> Commit-Queue: vikas soni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1666517} --- 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 95c3fff3..65bbf02e 100644 --- a/gpu/command_buffer/service/shared_image/compound_image_backing.cc +++ b/gpu/command_buffer/service/shared_image/compound_image_backing.cc @@ -1256,6 +1256,20 @@ // it's a transient backing that needs to be initialized. We must find the // permanent element that currently holds the latest content and copy from it. ElementHolder* latest_content_element = GetElementWithLatestContent(); + if (!latest_content_element) { + // No permanent element holds the most recent content, so the destination + // backing cannot be synchronized. This can happen if a transient backing + // was written to but its content could not be copied back to a permanent + // element on end access. Do not advance versioning so access can recover if + // an element re-establishes the latest content (e.g. via Update() for SHM + // backings). Note that if there is no SHM backing (e.g. GPU-only backings) + // or no Update() occurs, subsequent access calls (including writes) will + // continue to fail, requiring client-level context/resource re-creation. + LOG(ERROR) << "No element with latest content available for sync to " + << backing->GetName(); + return false; + } + bool updated_backing = false; bool copy_succeeded = false; 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 e952d7e..c088b6ea 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 @@ -507,6 +507,71 @@ EXPECT_TRUE(GetGpuHasLatestContent(compound_backing)); } +TEST_F(CompoundImageBackingTest, AccessFailsWhenLatestContentUnavailable) { + 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); + + // Simulate a write to a transient backing that is not stored as a permanent + // element. Begin access syncs it from shared memory and advances the content + // version. + auto transient = std::make_unique<TestImageBacking>( + compound_backing->mailbox(), + SharedImageInfo(compound_backing->format(), compound_backing->size(), + compound_backing->color_space(), + compound_backing->surface_origin(), + compound_backing->alpha_type(), compound_backing->usage(), + "Transient"), + kTestBackingSize); + EXPECT_TRUE(compound_backing->NotifyBeginAccess( + transient.get(), RepresentationAccessMode::kWrite, + SharedImageAccessStream::kSkia)); + EXPECT_TRUE(transient->GetUploadFromMemoryCalledAndReset()); + + // End access without the transient content being synced back to any + // permanent element (as happens when the proactive copy in end access + // fails), so no element holds the latest content version. + compound_backing->NotifyEndAccess(transient.get(), + RepresentationAccessMode::kWrite); + transient.reset(); + + EXPECT_FALSE(GetShmHasLatestContent(compound_backing)); + EXPECT_FALSE(GetGpuHasLatestContent(compound_backing)); + + // A subsequent read on the GPU backing must not proceed since there is no + // element to sync content from and the GPU backing was never initialized. + { + auto gl_access = gl_rep->BeginScopedAccess( + GLTextureImageRepresentationBase::kReadAccessMode, + SharedImageRepresentation::AllowUnclearedAccess::kNo); + EXPECT_FALSE(gl_access); + } + EXPECT_FALSE(gpu_backing->GetUploadFromMemoryCalledAndReset()); + EXPECT_FALSE(GetGpuHasLatestContent(compound_backing)); + + // After the shared memory element is marked as the latest via Update(), + // access should succeed again. + compound_backing->Update(nullptr); + EXPECT_TRUE(GetShmHasLatestContent(compound_backing)); + { + 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());
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 e952d7e..c088b6ea 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
@@ -507,6 +507,71 @@
EXPECT_TRUE(GetGpuHasLatestContent(compound_backing));
}
+TEST_F(CompoundImageBackingTest, AccessFailsWhenLatestContentUnavailable) {
+ 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);
+
+ // Simulate a write to a transient backing that is not stored as a permanent
+ // element. Begin access syncs it from shared memory and advances the content
+ // version.
+ auto transient = std::make_unique<TestImageBacking>(
+ compound_backing->mailbox(),
+ SharedImageInfo(compound_backing->format(), compound_backing->size(),
+ compound_backing->color_space(),
+ compound_backing->surface_origin(),
+ compound_backing->alpha_type(), compound_backing->usage(),
+ "Transient"),
+ kTestBackingSize);
+ EXPECT_TRUE(compound_backing->NotifyBeginAccess(
+ transient.get(), RepresentationAccessMode::kWrite,
+ SharedImageAccessStream::kSkia));
+ EXPECT_TRUE(transient->GetUploadFromMemoryCalledAndReset());
+
+ // End access without the transient content being synced back to any
+ // permanent element (as happens when the proactive copy in end access
+ // fails), so no element holds the latest content version.
+ compound_backing->NotifyEndAccess(transient.get(),
+ RepresentationAccessMode::kWrite);
+ transient.reset();
+
+ EXPECT_FALSE(GetShmHasLatestContent(compound_backing));
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+
+ // A subsequent read on the GPU backing must not proceed since there is no
+ // element to sync content from and the GPU backing was never initialized.
+ {
+ auto gl_access = gl_rep->BeginScopedAccess(
+ GLTextureImageRepresentationBase::kReadAccessMode,
+ SharedImageRepresentation::AllowUnclearedAccess::kNo);
+ EXPECT_FALSE(gl_access);
+ }
+ EXPECT_FALSE(gpu_backing->GetUploadFromMemoryCalledAndReset());
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+
+ // After the shared memory element is marked as the latest via Update(),
+ // access should succeed again.
+ compound_backing->Update(nullptr);
+ EXPECT_TRUE(GetShmHasLatestContent(compound_backing));
+ {
+ 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 read in CompoundImageBacking via failed proactive copy-back
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A vulnerability in CompoundImageBacking allows a compromised renderer to read uninitialized GPU VRAM. If a transient backing’s proactive copy-back fails under memory pressure, subsequent reads skip synchronization, allowing the read of uninitialized textures that bypass the IsCleared() gate.
Affected files:
gpu/command_buffer/service/shared_image/compound_image_backing.ccgpu/command_buffer/service/shared_image/wrapped_sk_image_backing.ccgpu/command_buffer/service/shared_image/shared_image_representation.cc
Estimated timestamp from git blame: 2026-05-07
1. Summary of the Issue (Meant for Human Triage)
A potential security vulnerability exists in CompoundImageBacking due to an oversight in handling proactive copy-back failures for transient backings. This vulnerability allows a compromised renderer to bypass synchronization and read physically uninitialized, driver-recycled GPU VRAM, presenting a significant cross-origin data exposure risk (GPU-XO).
When the features::kUseDynamicBackingAllocations flag is enabled (currently FEATURE_DISABLED_BY_DEFAULT), CompoundImageBacking supports dynamic lazy creation of GPU backings. If a transient backing is created (e.g., inside a thread-safe container like DrDC/WebView) and written to, its content must be proactively copied back to permanent backings inside NotifyEndAccess. If this proactive copy-back fails (which an attacker can induce by causing staging-buffer allocation failures via memory-pressure spraying), the code logs an error and continues without updating the permanent backing’s content_id_.
Subsequently, a read access on a lazily allocated GPU backing triggers NotifyBeginAccess. Because no permanent backing matches the latest_content_id_, GetElementWithLatestContent() returns nullptr. This causes the entire synchronization block to be skipped. NotifyBeginAccess returns true (success). Because the CompoundImageBacking container was artificially marked fully cleared upon creation with a shared memory backing, the outer IsCleared() validation passes. The renderer then reads directly from the physically uninitialized backend GPU texture, leaking cross-origin graphical content from recycled driver VRAM.
2. Proof-of-Concept & Detailed Execution Flow
Note: Our tooling agent does not have the ability to run code. The steps below are a potential, theoretically verified sequence based on static analysis of the codebase.
Step-by-Step Sequence:
- Precondition: The feature
features::kUseDynamicBackingAllocationsmust be enabled. - Container Creation: A compromised renderer (A-RENDERER) sends a
mojom::DeferredSharedImageRequest{kCreateSharedImageWithBuffer}IPC over itsGpuChannelto create aCompoundImageBackingbacked by aSHARED_MEMORY_BUFFER. - The renderer specifies usage flags (e.g.,
SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_WRITE_ONLY) causingSharedImageFactory::CreateSharedImageto fall back toCompoundImageBacking::Create. - During initialization,
CompoundImageBacking::ComputeIsThreadSafeevaluatesfactory->shared_image_manager_->display_context_on_another_thread()totrue(e.g., on DrDC or WebView). This forcesis_thread_safetotrue, making theCompoundImageBackinga thread-safe container. - The constructor initializes the primary shared memory backing as
elements_[0], setshas_shm_backing_ = true, and crucially marks the compound container as fully cleared viaSetClearedRectInternal(gfx::Rect(size))(compound_image_backing.cc:1157). - The container initializes
latest_content_id_ = 1andelements_[0].content_id_ = 1. - Transient Write Access: The renderer initiates a Write access on a stream (e.g.,
kSkia) that triggers dynamic backing allocation. GetOrAllocateBackingfalls back toSharedImageFactoryto dynamically allocate a new GPU backing (CreateBackingFromBackingFactory).- Because the compound container is thread-safe but the new GPU backing is not, the code treats it as a transient backing (
compound_image_backing.cc:2038). It is returned viaout_transient_backingand not added to the permanentelements_list. - The wrapper calls
NotifyBeginAccesson the transient backing withmode = kWrite. GetElementWithLatestContent()returnselements_[0].copy_manager_->CopyImagesuccessfully syncs data.latest_content_id_increments to2. Because it is transient,access_elementisnullptr, so no permanent element’scontent_id_is updated.- Induced Copy Failure: The wrapper calls
EndWriteAccess, invokingNotifyEndAccess(backing, kWrite). This detects the transient backing and attempts a proactive copy-back toelements_[0](compound_image_backing.cc:1361). - The attacker concurrently induces extreme GPU memory pressure (e.g., spraying SharedImages), exhausting staging/transfer buffers. The
copy_manager_->CopyImagefails. - The code logs
"DCSI: Proactive copy ... failed."but critically continues without updatingelements_[0].content_id_to2(compound_image_backing.cc:1370). The transient backing is destroyed.latest_content_id_is2, but the permanent element has1. - Uninitialized Read: The renderer initiates a Read access (e.g.,
ReadbackARGBImagePixelsINTERNALImmediate) using a stream that requires a new permanent GPU backing (e.g.,GLTextureImageBacking). GetOrAllocateBackingallocates the new GPU backing. Becausehas_shm_backing_istrue,backing->SetCleared()is called unconditionally (compound_image_backing.cc:2106).- This prematurely marks the new backing as physically initialized, despite containing recycled, uninitialized driver VRAM. It is added as
elements_[1]withcontent_id_ = 0. NotifyBeginAccessis called withmode = kRead.GetElementWithLatestContent()looks for an element withcontent_id_ == 2. Sinceelements_[0]is1andelements_[1]is0, it returnsnullptr(compound_image_backing.cc:1953).- Because
latest_content_elementisnullptr, the physical synchronization block (if (latest_content_element)) is bypassed entirely (compound_image_backing.cc:1262). NotifyBeginAccessreturnstrue(success).- The representation wrapper (e.g.,
SkiaGaneshImageRepresentation::BeginScopedReadAccess) checksIsCleared(). This delegates toCompoundImageBacking::ClearedRect(), which returns the full image bounds set in Step 5. - The gate passes. The renderer reads the newly allocated, physically uninitialized GPU backing. Backend texture allocations (Vulkan, D3D, Metal) do not zero-fill in release builds, exposing recycled driver VRAM to the attacker.
Suggested Fix:
In CompoundImageBacking::NotifyBeginAccess, if latest_content_element returns nullptr but latest_content_id_ > 1 (meaning valid data existed but was lost during a failed proactive copy-back), NotifyBeginAccess should explicitly return false to abort the read. Alternatively, in NotifyEndAccess, if the proactive copy-back fails, the container should be marked into a degraded/invalid state that forces subsequent accesses to fail gracefully.
3. Technical Verification Details (Automated Audit Logs)
> Severity: High (S1)
> Brief Notes / Reasoning:
> The primary variant described in the report is invalid (S4). NotifyBeginAccess is declared as [[nodiscard]] bool, and all representation wrappers (e.g., WrappedSkiaGaneshCompoundImageRepresentation::BeginReadAccess) explicitly check its return value. If CopyImage fails, NotifyBeginAccess returns false, and the wrapper early-returns, safely preventing the read. The report’s claim that it returns void and falls through is incorrect for the current codebase.
>
> However, the secondary sub-variant (latest_content_element == nullptr) is structurally valid. When features::kUseDynamicBackingAllocations is enabled, a transient backing can be created. During a write access on a transient backing, NotifyBeginAccess increments latest_content_id_. If the proactive copy-back to the SHM backing in NotifyEndAccess fails (e.g., due to staging buffer allocation failure from memory pressure), no permanent element’s content_id_ is updated. A subsequent read on a lazily-allocated GPU backing causes GetElementWithLatestContent() to return nullptr. NotifyBeginAccess skips the copy block entirely and returns true. The read proceeds on the GPU backing, which was marked SetCleared() at creation without being initialized. This results in a cross-origin GPU-memory disclosure (S1 - High Severity).
>
> Per the severity guidelines, since the valid variant is behind an off-by-default flag (FEATURE_DISABLED_BY_DEFAULT) with no field trial, we assess the severity AS IF the flag were on (S1), but note Security_Impact-None.
Code Reachability Proofs & Evaluated Logic:
ComputeIsThreadSafeLogic:gpu/command_buffer/service/shared_image/compound_image_backing.cc:928-932if (!is_thread_safe && base::FeatureList::IsEnabled(features::kUseDynamicBackingAllocations)) { is_thread_safe = factory->shared_image_manager_->display_context_on_another_thread(); }GetOrAllocateBackingtransient backing allocation:gpu/command_buffer/service/shared_image/compound_image_backing.cc:2038-2041if (is_thread_safe() && !new_backing->is_thread_safe()) { out_transient_backing = std::move(new_backing); return out_transient_backing.get(); }NotifyEndAccessProactive Copy-Back Failure:gpu/command_buffer/service/shared_image/compound_image_backing.cc:1367-1372if (copy_manager_->CopyImage(backing, dst_backing)) { element.content_id_ = latest_content_id_; } else { LOG(ERROR) << "DCSI: Proactive copy from " << backing->GetName() << " to " << dst_backing->GetName() << " failed."; }NotifyBeginAccessSynchronization Skip:gpu/command_buffer/service/shared_image/compound_image_backing.cc:1258-1262ElementHolder* latest_content_element = GetElementWithLatestContent(); bool updated_backing = false; bool copy_succeeded = false; if (latest_content_element) { // [Bypassed because latest_content_element is nullptr] } // Falls through to: return true;CreateBackingFromBackingFactoryPremature Clear:gpu/command_buffer/service/shared_image/compound_image_backing.cc:2105-2107if (has_shm_backing_) { backing->SetCleared(); }
Environmental Assumptions:
features::kUseDynamicBackingAllocationsmust be explicitly enabled.- Target environments include Vulkan, D3D, or Metal backends where
createBackendTextureallocations are recycled and not zero-filled in Release builds to avoid overhead (wrapped_sk_image_backing.cc:232-237).
Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.