Medium watch CVSS 3.1 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in GPU
DescriptionUninitialized resource in GPU
ComponentGPU
Sub-componentGPU-Security
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker495579602
Fix commitc95fed050d86 (chromium/src) +159/-50
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-24 (per vendor advisory)
Fix landed2026-07-09 (public git)
Disclosed2026-08-25
Patch gap47 days — fix public in source before the advisory

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. UploadFromMemory returns 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.

Key insight
The single mistake was ignoring the result of 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

  1. Create a compound-backed shared image A renderer/GPU client allocates a CompoundImageBacking with both a shared-memory and a (lazily allocated) GPU backing, the latter marked cleared at creation.
  2. Force the content sync to fail Trigger a state where NotifyBeginAccess must copy shared-memory content into the GPU backing but the copy (UploadFromMemory) fails. (inferred)
  3. Access proceeds despite failed sync Pre-patch, NotifyBeginAccess returns void, the representation ignores the failure and calls wrapped_->BeginReadAccess() on the GPU backing that was never populated.
  4. Read uninitialized VRAM Because the GPU backing was prematurely marked cleared, the read returns uninitialized GPU memory contents to the client.

Impact Assessment

A renderer/GPU client can cause reads of a GPU backing whose content sync failed, disclosing uninitialized VRAM (a GPU-memory information leak). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1. Any further escalation is (inferred). The precondition is the ability to allocate compound-backed shared images and to induce a content-sync/upload failure.

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.h
  • gpu/command_buffer/service/shared_image/test_image_backing.cc
  • gpu/command_buffer/service/shared_image/test_image_backing.h
  • gpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
  • gpu/command_buffer/service/shared_image/compound_image_backing.cc

Audit Directions

  • Ignored sync/copy return values
    Search 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 marking
    Audit backings marked cleared/initialized at creation and verify that any populating copy actually succeeded before a representation is allowed to read.
  • `void` notify/begin hooks
    Review 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.
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)
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 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());
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. Premature SetCleared() Call: When a GPU backing is lazily created via CompoundImageBacking::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.

  2. Swallowed Synchronization Errors: When the wrapper representation (e.g., WrappedGLTextureCompoundImageRepresentation) calls BeginAccess, it first calls compound_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, and NotifyBeginAccess returns void without 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.)

  1. Gain code execution in a renderer process.
  2. Allocate a SharedImage backed by a shared memory buffer by sending the mojom::CreateSharedImageWithBuffer IPC to the GPU process, requesting both CPU and GPU read/write usage flags.
  3. The GPU process instantiates a CompoundImageBacking for this SharedImage, wrapping the SHM buffer and preparing a lazy-instantiated GPU factory.
  4. 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’s updateBackendTexture) fail.
  5. Issue a read command via a GPU API (e.g., binding the SharedImage to a WebGL texture and calling readPixels).
  6. 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 by CreateBackingFromBackingFactory.
  7. NotifyBeginAccess attempts to sync the SHM data to the GPU backing. Due to the induced memory pressure, copy_manager_->CopyImage fails.
  8. 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.

View on issue tracker