Overview

Critical
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Sub-componentGPU Security
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — revised by independent review by AI
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.
Independent review by AIPlausible (tier revised from patch-now)
Reviewer notes: Diff is truncated (LazyCreateBacking, GetFactoryByType not shown). Commit calls these 'potential' thread-safety issues. The dynamic path is gated by kUseDynamicBackingAllocations, and the race needs cross-thread compound backings, so the factory-destruction race and 'default' preconditions are not shown.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker511766407
Fix commit9cd9ad933858 (chromium/src) +117/-54
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-10 (per vendor advisory)
Fix landed2026-05-12 (public git)
Disclosed2026-06-30
Patch gap49 days — fix public in source before the advisory

Background

`CompoundImageBacking`
a SharedImageBacking in the GPU process that groups multiple element backings and lazily allocates a GPU-backed element on first access.
`SharedImageBackingFactory`
a factory object that constructs concrete SharedImageBacking instances; it is valid only while the factory lock is held and may be destroyed otherwise.
`SharedImageFactoryRef::Execute`
a helper that runs a lambda under the factory lock, providing safe scoped access to the live SharedImageFactory and its factories.
Thread-affine `WeakPtr`
a base::WeakPtr bound to the sequence that created it; dereferencing or copying it on another thread is undefined and can outlive its target.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Diff is truncated (LazyCreateBacking, GetFactoryByType not shown). Commit calls these 'potential' thread-safety issues. The dynamic path is gated by kUseDynamicBackingAllocations, and the race needs cross-thread compound backings, so the factory-destruction race and 'default' preconditions are not shown.
In GetOrAllocateBacking, the Execute lambda ran under the factory lock but only cached a raw SharedImageBackingFactory* (gpu_backing_factory), then returned control to the caller. The actual backing creation, CreateBackingFromBackingFactory(gpu_backing_factory->GetWeakPtr(), ...), executed OUTSIDE the lambda after the lock had been released, so both the raw pointer and the derived WeakPtr escaped the synchronization scope. Because the factory is only guaranteed live under the lock, and the WeakPtr is affine to the creating sequence, a concurrent shared-image operation on another thread could destroy the factory (or misuse the sequence-bound WeakPtr) between lock release and use. The dangling SharedImageBackingFactory* / stale WeakPtr was then dereferenced during backing allocation, yielding a use-after-free in the GPU process. The same escape existed in the lazy path, where the create_callback captured a raw factory/WeakPtr for deferred allocation.
Key insight
A pointer or WeakPtr obtained under a lock only remains valid inside that locked scope; caching it and dereferencing it after the lock is dropped defeats the synchronization entirely. Referring to the factory by a stable, lock-independent key (its SharedImageBackingType) and re-resolving it under lock removes the lifetime and thread-affinity dependency.

Attack Path

  1. Attacker (compromised or malicious renderer) issues concurrent shared-image commands that drive CompoundImageBacking to lazily allocate its GPU-backed element.
  2. Thread A enters GetOrAllocateBacking, runs Execute under lock, caches the raw SharedImageBackingFactory*, and releases the lock.
  3. Thread B concurrently performs a shared-image operation that tears down or invalidates that factory (or the cached WeakPtr is used off its owning sequence).
  4. Thread A calls CreateBackingFromBackingFactory with the now-dangling pointer/WeakPtr, dereferencing freed memory.
  5. Attacker grooms the freed factory allocation to control the UAF and achieve corruption / potential code execution in the GPU process.

Impact Assessment

Use-after-free in the GPU process, reachable from renderer-issued shared-image operations, giving memory corruption with potential for controlled writes and code execution in a privileged-adjacent process. Combined with a renderer compromise it advances a sandbox-escape chain, since the GPU process brokers device and driver access. Exploitation requires winning a concurrency race, which lowers reliability but does not prevent it.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/shared_image/compound_image_backing.cc
modified
for
gpu/command_buffer/service/shared_image/compound_image_backing.cc
modified

Files Changed

  • gpu/command_buffer/service/shared_image/compound_image_backing.cc

Audit Directions

  • Escaped lock-guarded pointers
    audit SharedImageBacking and factory code for raw pointers or WeakPtrs obtained inside a Execute/locked scope but dereferenced after the lock is released; require all factory-dependent work to complete under lock.
  • Thread-affine `WeakPtr` across sequences
    flag GetWeakPtr() results that are stored in callbacks or member state and later consumed on a different sequence; prefer resolving by stable type/key under lock over caching sequence-bound WeakPtrs.
  • Lazy-allocation callbacks capturing live objects
    review deferred create_callback/base::Bind captures in shared-image lazy allocation for captured factory pointers whose lifetime is not guaranteed at invocation time.
From 9cd9ad93385823a2af51f308d288de8da481a7fa Mon Sep 17 00:00:00 2001
From: vikas soni <[email protected]>
Date: Tue, 12 May 2026 13:26:53 -0700
Subject: [PATCH] [GPU Security]: Fix thread-safety issues in CompoundImageBacking.

This CL addresses potential security vulnerabilities where a raw
SharedImageBackingFactory pointer could escape the synchronization scope
or a thread-affine WeakPtr be used across threads.

The fix involves:

1. Moving dynamic backing allocation logic entirely inside the
SharedImageFactoryRef::Execute lambda in GetOrAllocateBacking.

2. Consolidating backing creation into a single method that assumes the
factory lock is held.

3. Updating the lazy allocation callback to use SharedImageFactoryRef to
safely look up the correct factory by type under lock.

4. Adding SharedImageFactory::GetFactoryByType to support safe lookup.

These changes ensure that all factory-dependent operations in
CompoundImageBacking are performed under the factory lock and are
thread-safe.

Bug: 511766407
Change-Id: Iab94d024f7cb5c43e17a4237f5572c80bf96b373
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7838134
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Commit-Queue: vikas soni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1629530}
---

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 45c1821..3846f7a 100644
--- a/gpu/command_buffer/service/shared_image/compound_image_backing.cc
+++ b/gpu/command_buffer/service/shared_image/compound_image_backing.cc
@@ -6,6 +6,7 @@
 
 #include <ostream>
 
+#include "base/check_is_test.h"
 #include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/logging.h"
@@ -1021,6 +1022,7 @@
           std::move(buffer_usage)),
       shared_image_factory_(std::move(shared_image_factory)),
       copy_manager_(std::move(copy_manager)) {
+  CHECK(gpu_backing_factory);
   // If the backing is thread-safe, the base class enables an internal lock that
   // protects the |elements_| vector and other metadata from concurrent access.
   DCHECK(shm_backing);
@@ -1050,10 +1052,11 @@
 
   // CreateBackingFromBackingFactory will be called on demand. Hence this is
   // lazy backing creation.
-  gpu_element.create_callback =
-      base::BindOnce(&CompoundImageBacking::CreateBackingFromBackingFactory,
-                     base::Unretained(this), std::move(gpu_backing_factory),
-                     si_info.debug_label, GetGpuSharedImageUsage(usage));
+  SharedImageBackingType factory_type = gpu_backing_factory->GetBackingType();
+  gpu_element.create_callback = base::BindOnce(
+      &CompoundImageBacking::LazyCreateBacking, base::Unretained(this),
+      factory_type, std::move(gpu_backing_factory),
+      GetGpuSharedImageUsage(usage), si_info.debug_label);
   elements_.push_back(std::move(gpu_element));
   max_elements_allocated_ = 2;
 }
@@ -1851,51 +1854,51 @@
   if (base::FeatureList::IsEnabled(features::kUseDynamicBackingAllocations) &&
       shared_image_factory_) {
     SharedImageUsageSet usage = GetUsageFromAccessStream(stream);
-    SharedImageBackingFactory* gpu_backing_factory = nullptr;
+    std::unique_ptr<SharedImageBacking> new_backing;
     shared_image_factory_->Execute([&](SharedImageFactory* factory) {
-      gpu_backing_factory = factory->GetFactoryByUsage(
-          usage, format(), size(),
-          /*pixel_data=*/{}, gfx::EMPTY_BUFFER, stream, &params);
+      SharedImageBackingFactory* gpu_backing_factory =
+          factory->GetFactoryByUsage(usage, format(), size(),
+                                     /*pixel_data=*/{}, gfx::EMPTY_BUFFER,
+                                     stream, &params);
+      if (gpu_backing_factory) {
+        CreateBackingFromBackingFactory(gpu_backing_factory, debug_label(),
+                                        usage, new_backing);
+      }
     });
 
-    if (gpu_backing_factory) {
-      std::unique_ptr<SharedImageBacking> new_backing;
-      CreateBackingFromBackingFactory(gpu_backing_factory->GetWeakPtr(),
-                                      debug_label(), usage, new_backing);
-      if (new_backing) {
-        UMA_HISTOGRAM_ENUMERATION(
-            "GPU.CompoundImageBacking.DynamicAllocation.BackingType",
-            new_backing->GetType());
-        UMA_HISTOGRAM_ENUMERATION(
-            "GPU.CompoundImageBacking.DynamicAllocation.AccessStream", stream);
-        UMA_HISTOGRAM_SPARSE(
-            "GPU.CompoundImageBacking.DynamicAllocation."
-            "InitialSharedImageUsage",
-            static_cast<int32_t>(static_cast<uint32_t>(this->usage())));
+    if (new_backing) {
+      UMA_HISTOGRAM_ENUMERATION(
+          "GPU.CompoundImageBacking.DynamicAllocation.BackingType",
+          new_backing->GetType());
+      UMA_HISTOGRAM_ENUMERATION(
+          "GPU.CompoundImageBacking.DynamicAllocation.AccessStream", stream);
+      UMA_HISTOGRAM_SPARSE(
+          "GPU.CompoundImageBacking.DynamicAllocation."
+          "InitialSharedImageUsage",
+          static_cast<int32_t>(static_cast<uint32_t>(this->usage())));
 
-        // If the CSI container is thread-safe, we treat newly created backings
-        // as transient if they are not thread-safe. They will be owned by the
-        // representation and destroyed after use. This ensures that a
-        // non-thread-safe backing allocated on one thread doesn't persist in
-        // the thread-safe container, which could lead to race conditions if
-        // accessed from another thread later.
-        if (is_thread_safe() && !new_backing->is_thread_safe()) {
-          out_transient_backing = std::move(new_backing);
-          return out_transient_backing.get();
-        }
-
-        // Else we treat the backing as non-transient and add it to the list of
-        // alive elements. This is done when either the container is not
-        // thread-safe or the new backing itself is thread-safe.
-        ElementHolder element;
-        element.access_streams.Put(stream);
-        element.backing = std::move(new_backing);
-        elements_.push_back(std::move(element));
-        if (elements_.size() > max_elements_allocated_) {
-          max_elements_allocated_ = elements_.size();
-        }
-        return elements_.back().GetBacking();
+      // If the CSI container is thread-safe, we treat newly created backings
+      // as transient if they are not thread-safe. They will be owned by the
+      // representation and destroyed after use. This ensures that a
+      // non-thread-safe backing allocated on one thread doesn't persist in
+      // the thread-safe container, which could lead to race conditions if
+      // accessed from another thread later.
+      if (is_thread_safe() && !new_backing->is_thread_safe()) {
+        out_transient_backing = std::move(new_backing);
+        return out_transient_backing.get();
       }
+
+      // Else we treat the backing as non-transient and add it to the list of
+      // alive elements. This is done when either the container is not
+      // thread-safe or the new backing itself is thread-safe.
+      ElementHolder element;
+      element.access_streams.Put(stream);
+      element.backing = std::move(new_backing);
+      elements_.push_back(std::move(element));
+      if (elements_.size() > max_elements_allocated_) {
+        max_elements_allocated_ = elements_.size();
+      }
+      return elements_.back().GetBacking();
     }
   }
 
@@ -1914,19 +1917,19 @@
 }
 
 void CompoundImageBacking::CreateBackingFromBackingFactory(
-    base::WeakPtr<SharedImageBackingFactory> factory,
+    SharedImageBackingFactory* backing_factory,
     std::string debug_label,
     SharedImageUsageSet usage,
     std::unique_ptr<SharedImageBacking>& backing) {
-  if (!factory) {
-    DLOG(ERROR) << "Can't allocate backing after image has been destroyed";
-    return;
-  }
+  // This method assumes the caller has already ensured the factory is alive
+  // and synchronized (e.g. by holding the SharedImageFactoryRef lock).
+  CHECK(backing_factory);
 
   SharedImageInfo si_info(format(), size(), color_space(), surface_origin(),
-                          alpha_type(), usage, debug_label);
-  backing = factory->CreateSharedImage(mailbox(), si_info, kNullSurfaceHandle,
-                                       /*is_thread_safe=*/false);
+                          alpha_type(), usage, std::move(debug_label));
+  backing =
+      backing_factory->CreateSharedImage(mailbox(), si_info, kNullSurfaceHandle,
+                                         /*is_thread_safe=*/false);
   if (!backing) {
     DLOG(ERROR) << "Failed to allocate GPU backing";
     return;
@@ -1952,13 +1955,41 @@
   // Update peak GPU memory tracking with the new estimated size.
   size_t estimated_size = 0;
   for (auto& element : elements_) {
-    if (element.backing)
+    if (element.backing) {
       estimated_size += element.backing->GetEstimatedSize();
+    }
   }
 
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in CompoundImageBacking::GetOrAllocateBacking leading to GPU RCE

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A Time-of-Check Time-of-Use (TOCTOU) Use-After-Free exists in CompoundImageBacking::GetOrAllocateBacking when the kUseDynamicBackingAllocations feature is enabled. A raw SharedImageBackingFactory* pointer escapes a lock and can be used after the factory is destroyed by the GPU main thread, potentially leading to Remote Code Execution in the GPU process.

Affected files:

  • gpu/command_buffer/service/shared_image/compound_image_backing.cc
  • gpu/command_buffer/service/shared_image/compound_image_backing.h
  • gpu/command_buffer/service/shared_image/shared_image_factory.cc
  • gpu/command_buffer/service/shared_image/shared_image_factory.h

Estimated timestamp from git blame: 2026-05-07

Summary

A Time-of-Check Time-of-Use (TOCTOU) Use-After-Free vulnerability exists in CompoundImageBacking::GetOrAllocateBacking. When the kUseDynamicBackingAllocations feature is enabled (which is enabled by default via Finch), a background thread (e.g., the DrDC compositor thread) can attempt to dynamically allocate a new backing. During this process, a raw pointer to a SharedImageBackingFactory escapes a lock. If the renderer closes its GPU channel concurrently, the SharedImageFactory and its associated SharedImageBackingFactory objects are destroyed on the GPU main thread, leading to a UAF when the compositor thread subsequently dereferences the dangling pointer.

Because the GPU process is unsandboxed on Android, this could potentially lead to a full sandbox escape directly from a compromised renderer.

Technical Details

CompoundImageBacking holds a reference to a SharedImageFactory via scoped_refptr<SharedImageFactoryRef> shared_image_factory_;. When GetOrAllocateBacking is called on a background thread and dynamic allocation is required, the following sequence occurs:

  1. At compound_image_backing.cc:1854, a local raw pointer SharedImageBackingFactory* gpu_backing_factory = nullptr; is declared on the stack.
  2. The thread calls shared_image_factory_->Execute(...) to safely interact with the factory under a lock.
  3. Inside the lambda, factory->GetFactoryByUsage(...) is called, which returns a raw pointer to a heap-allocated SharedImageBackingFactory.
  4. This raw pointer is assigned to the local gpu_backing_factory variable.
  5. The Execute method finishes and releases its lock. The raw pointer gpu_backing_factory has now successfully escaped the protected scope.

Potential Exploitation Steps

An attacker with a compromised renderer could trigger this race condition by performing the following steps:

  1. Request access to a shared image on a background thread (e.g., DrDC) with an access stream that forces dynamic allocation.
  2. Concurrently, forcefully close the renderer’s GPU channel.
  3. The GPU channel closure triggers the destruction of SharedImageStub and SharedImageFactory on the GPU main thread.
  4. The SharedImageFactory destructor destroys the std::vector<std::unique_ptr<SharedImageBackingFactory>> factories_, freeing the heap memory pointed to by gpu_backing_factory.
  5. The attacker uses heap spraying in the GPU process to reclaim the freed memory chunk with a crafted payload containing a fake vtable and a fake WeakReferenceOwner state.
  6. Execution resumes on the background thread. At compound_image_backing.cc:1863, the code calls gpu_backing_factory->GetWeakPtr() on the dangling pointer.
  7. GetWeakPtr reads ptr_ and flag_ from the attacker-controlled memory. By crafting a valid flag_, the attacker ensures the WeakPtr appears valid.
  8. The forged WeakPtr is passed to CreateBackingFromBackingFactory, which calls the virtual method factory->CreateSharedImage(...) (compound_image_backing.cc:1928).
  9. The virtual dispatch reads the fake vtable pointer, hijacking control flow and resulting in RCE in the GPU process.

Note: These are potential exploitation steps based on code analysis; we do not have a working proof-of-concept yet.

MiraclePtr Exemption

Because gpu_backing_factory is a raw pointer stored as a local stack variable, it is explicitly not protected by MiraclePtr (base::raw_ptr<T>).

Suggested Fix

The raw SharedImageBackingFactory* pointer should never escape the lock held by SharedImageFactoryRef::Execute.

One potential fix is to move the actual CreateSharedImage call into the lambda executed by Execute, so that the SharedImageBackingFactory is only accessed while the lock is held. The lambda can then return the resulting std::unique_ptr<SharedImageBacking> instead of the factory pointer.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker