High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse of uninitialized resource in GPU
DescriptionUse of uninitialized resource in GPU
ComponentGPU
Bug ClassUninitialized Memory
Tracker536460270
Fix commita73a143f694c (chromium/src) +79/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-18

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/shared_image/compound_image_backing.cc
modified
TEST_F
gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/shared_image/compound_image_backing.cc
  • gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
From a73a143f694c18300b9d87d3d489cddec4124f73 Mon Sep 17 00:00:00 2001
From: vikas soni <vikassoni@chromium.org>
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 <vasilyt@chromium.org>
Commit-Queue: vikas soni <vikassoni@chromium.org>
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());
Loading diff…

Regression Test / PoC

shipped with the fix
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());
Loading diff…

Original Bug Report

reported by aw...@chromium.org

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.cc
  • gpu/command_buffer/service/shared_image/wrapped_sk_image_backing.cc
  • gpu/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:

  1. Precondition: The feature features::kUseDynamicBackingAllocations must be enabled.
  2. Container Creation: A compromised renderer (A-RENDERER) sends a mojom::DeferredSharedImageRequest{kCreateSharedImageWithBuffer} IPC over its GpuChannel to create a CompoundImageBacking backed by a SHARED_MEMORY_BUFFER.
  3. The renderer specifies usage flags (e.g., SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_WRITE_ONLY) causing SharedImageFactory::CreateSharedImage to fall back to CompoundImageBacking::Create.
  4. During initialization, CompoundImageBacking::ComputeIsThreadSafe evaluates factory->shared_image_manager_->display_context_on_another_thread() to true (e.g., on DrDC or WebView). This forces is_thread_safe to true, making the CompoundImageBacking a thread-safe container.
  5. The constructor initializes the primary shared memory backing as elements_[0], sets has_shm_backing_ = true, and crucially marks the compound container as fully cleared via SetClearedRectInternal(gfx::Rect(size)) (compound_image_backing.cc:1157).
  6. The container initializes latest_content_id_ = 1 and elements_[0].content_id_ = 1.
  7. Transient Write Access: The renderer initiates a Write access on a stream (e.g., kSkia) that triggers dynamic backing allocation.
  8. GetOrAllocateBacking falls back to SharedImageFactory to dynamically allocate a new GPU backing (CreateBackingFromBackingFactory).
  9. 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 via out_transient_backing and not added to the permanent elements_ list.
  10. The wrapper calls NotifyBeginAccess on the transient backing with mode = kWrite.
  11. GetElementWithLatestContent() returns elements_[0]. copy_manager_->CopyImage successfully syncs data. latest_content_id_ increments to 2. Because it is transient, access_element is nullptr, so no permanent element’s content_id_ is updated.
  12. Induced Copy Failure: The wrapper calls EndWriteAccess, invoking NotifyEndAccess(backing, kWrite). This detects the transient backing and attempts a proactive copy-back to elements_[0] (compound_image_backing.cc:1361).
  13. The attacker concurrently induces extreme GPU memory pressure (e.g., spraying SharedImages), exhausting staging/transfer buffers. The copy_manager_->CopyImage fails.
  14. The code logs "DCSI: Proactive copy ... failed." but critically continues without updating elements_[0].content_id_ to 2 (compound_image_backing.cc:1370). The transient backing is destroyed. latest_content_id_ is 2, but the permanent element has 1.
  15. Uninitialized Read: The renderer initiates a Read access (e.g., ReadbackARGBImagePixelsINTERNALImmediate) using a stream that requires a new permanent GPU backing (e.g., GLTextureImageBacking).
  16. GetOrAllocateBacking allocates the new GPU backing. Because has_shm_backing_ is true, backing->SetCleared() is called unconditionally (compound_image_backing.cc:2106).
  17. This prematurely marks the new backing as physically initialized, despite containing recycled, uninitialized driver VRAM. It is added as elements_[1] with content_id_ = 0.
  18. NotifyBeginAccess is called with mode = kRead.
  19. GetElementWithLatestContent() looks for an element with content_id_ == 2. Since elements_[0] is 1 and elements_[1] is 0, it returns nullptr (compound_image_backing.cc:1953).
  20. Because latest_content_element is nullptr, the physical synchronization block (if (latest_content_element)) is bypassed entirely (compound_image_backing.cc:1262).
  21. NotifyBeginAccess returns true (success).
  22. The representation wrapper (e.g., SkiaGaneshImageRepresentation::BeginScopedReadAccess) checks IsCleared(). This delegates to CompoundImageBacking::ClearedRect(), which returns the full image bounds set in Step 5.
  23. 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:

  1. ComputeIsThreadSafe Logic: gpu/command_buffer/service/shared_image/compound_image_backing.cc:928-932
    if (!is_thread_safe && base::FeatureList::IsEnabled(features::kUseDynamicBackingAllocations)) {
      is_thread_safe = factory->shared_image_manager_->display_context_on_another_thread();
    }
    
  2. GetOrAllocateBacking transient backing allocation: gpu/command_buffer/service/shared_image/compound_image_backing.cc:2038-2041
    if (is_thread_safe() && !new_backing->is_thread_safe()) {
      out_transient_backing = std::move(new_backing);
      return out_transient_backing.get();
    }
    
  3. NotifyEndAccess Proactive Copy-Back Failure: gpu/command_buffer/service/shared_image/compound_image_backing.cc:1367-1372
    if (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.";
    }
    
  4. NotifyBeginAccess Synchronization Skip: gpu/command_buffer/service/shared_image/compound_image_backing.cc:1258-1262
    ElementHolder* 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;
    
  5. CreateBackingFromBackingFactory Premature Clear: gpu/command_buffer/service/shared_image/compound_image_backing.cc:2105-2107
    if (has_shm_backing_) {
      backing->SetCleared();
    }
    

Environmental Assumptions:

  • features::kUseDynamicBackingAllocations must be explicitly enabled.
  • Target environments include Vulkan, D3D, or Metal backends where createBackendTexture allocations 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.

View on issue tracker