CVE-2026-13775
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/shared_image/compound_image_backing.cc |
modified | |
forgpu/command_buffer/service/shared_image/compound_image_backing.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/compound_image_backing.cc
Patch
From 9cd9ad93385823a2af51f308d288de8da481a7fa Mon Sep 17 00:00:00 2001
From: vikas soni <vikassoni@chromium.org>
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 <vasilyt@chromium.org>
Commit-Queue: vikas soni <vikassoni@chromium.org>
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, ¶ms);
+ SharedImageBackingFactory* gpu_backing_factory =
+ factory->GetFactoryByUsage(usage, format(), size(),
+ /*pixel_data=*/{}, gfx::EMPTY_BUFFER,
+ stream, ¶ms);
+ 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();
+ }
}
Original Bug Report
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.ccgpu/command_buffer/service/shared_image/compound_image_backing.hgpu/command_buffer/service/shared_image/shared_image_factory.ccgpu/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:
- At
compound_image_backing.cc:1854, a local raw pointerSharedImageBackingFactory* gpu_backing_factory = nullptr;is declared on the stack. - The thread calls
shared_image_factory_->Execute(...)to safely interact with the factory under a lock. - Inside the lambda,
factory->GetFactoryByUsage(...)is called, which returns a raw pointer to a heap-allocatedSharedImageBackingFactory. - This raw pointer is assigned to the local
gpu_backing_factoryvariable. - The
Executemethod finishes and releases its lock. The raw pointergpu_backing_factoryhas 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:
- Request access to a shared image on a background thread (e.g., DrDC) with an access stream that forces dynamic allocation.
- Concurrently, forcefully close the renderer’s GPU channel.
- The GPU channel closure triggers the destruction of
SharedImageStubandSharedImageFactoryon the GPU main thread. - The
SharedImageFactorydestructor destroys thestd::vector<std::unique_ptr<SharedImageBackingFactory>> factories_, freeing the heap memory pointed to bygpu_backing_factory. - 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
WeakReferenceOwnerstate. - Execution resumes on the background thread. At
compound_image_backing.cc:1863, the code callsgpu_backing_factory->GetWeakPtr()on the dangling pointer. GetWeakPtrreadsptr_andflag_from the attacker-controlled memory. By crafting a validflag_, the attacker ensures theWeakPtrappears valid.- The forged
WeakPtris passed toCreateBackingFromBackingFactory, which calls the virtual methodfactory->CreateSharedImage(...)(compound_image_backing.cc:1928). - 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.