CVE-2026-19167
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fgpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/compound_image_backing.hgpu/command_buffer/service/shared_image/compound_image_backing_unittest.cc
Patch
From 75e83658911b277adcea35242890db6ffae7f371 Mon Sep 17 00:00:00 2001 From: vikas soni <[email protected]> Date: Wed, 22 Jul 2026 10:32:22 -0700 Subject: [PATCH] [GPU Security] Widen CompoundImageBacking content id to 64-bit. CompoundImageBacking uses content_id_ (initialized to 1) to track which element holds the latest content, with 0 serving as the sentinel value for uninitialized elements. If content_id_ wraps around 32-bit uint space, it collides with 0, causing untouched GPU sub-backings to be incorrectly reported as having current content. Widen latest_content_id_ and ElementHolder::content_id_ from uint32_t to uint64_t so overflow is impossible in practice. Add a regression test verifying that crossing the 32-bit boundary does not skip the required shm->GPU upload on first access. Bug: 536666274 Change-Id: I5f5d0056ae5a85baae14d64c08c103fdaf53cb46 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8127906 Reviewed-by: Vasiliy Telezhnikov <[email protected]> Commit-Queue: vikas soni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1666452} --- 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 f0ca4232..cbe5bd3 100644 --- a/gpu/command_buffer/service/shared_image/compound_image_backing.h +++ b/gpu/command_buffer/service/shared_image/compound_image_backing.h @@ -334,7 +334,7 @@ SharedImageBacking* GetBacking(); AccessStreamSet access_streams; - uint32_t content_id_ = 0; + uint64_t content_id_ = 0; CreateBackingCallback create_callback; std::unique_ptr<SharedImageBacking> backing; @@ -460,7 +460,8 @@ // factory from any thread. scoped_refptr<SharedImageFactoryRef> shared_image_factory_; - uint32_t latest_content_id_ GUARDED_BY(lock_) = 1; + // 64-bit so it never wraps back to a stale element's content id in practice. + uint64_t latest_content_id_ GUARDED_BY(lock_) = 1; // Holds all of the "element" backings that make up this compound backing. For // each there is a backing, set of streams and tracking for latest content. 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 15b42b3..e952d7e 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 @@ -4,6 +4,9 @@ #include "gpu/command_buffer/service/shared_image/compound_image_backing.h" +#include <cstdint> +#include <limits> + #include "components/viz/common/resources/shared_image_format.h" #include "components/viz/common/resources/shared_image_format_utils.h" #include "gpu/command_buffer/common/shared_image_info.h" @@ -143,6 +146,15 @@ return false; } + // Advances the latest content id, keeping the shared memory element as the + // one holding the latest content. Used to exercise content id values that + // would otherwise require many Update() calls to reach. + void AdvanceShmContentId(CompoundImageBacking* backing, + uint64_t content_id) NO_THREAD_SAFETY_ANALYSIS { + backing->latest_content_id_ = content_id; + backing->GetShmElement().content_id_ = content_id; + } + // Construct a CompoundImageBacking via the WrapExternalBacking constructor // (private). This mirrors CompoundImageBacking::WrapExternalBacking exactly, // minus the SharedImageFactory consultation. @@ -360,6 +372,49 @@ EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset()); } +TEST_F(CompoundImageBackingTest, UploadOnFirstAccessAfterManyUpdates) { + 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_); + + // Simulate the state reached after a very large number of shared memory + // updates before any GPU access has occurred. + AdvanceShmContentId(compound_backing, + std::numeric_limits<uint32_t>::max() - 1); + + EXPECT_TRUE(GetShmHasLatestContent(compound_backing)); + EXPECT_FALSE(GetGpuHasLatestContent(compound_backing)); + + // A further update should keep the shared memory element as the sole holder + // of the latest content and never mark the untouched GPU element as current. + compound_backing->Update(nullptr); + EXPECT_TRUE(GetShmHasLatestContent(compound_backing)); + EXPECT_FALSE(GetGpuHasLatestContent(compound_backing)); + + compound_backing->Update(nullptr); + EXPECT_TRUE(GetShmHasLatestContent(compound_backing)); + EXPECT_FALSE(GetGpuHasLatestContent(compound_backing)); + + // The first GPU read access must still trigger an upload from shared memory. + auto gl_rep = manager_.ProduceGLTexturePassthrough( + compound_backing->mailbox(), &memory_type_tracker_); + ASSERT_TRUE(gl_rep); + { + auto access = gl_rep->BeginScopedAccess( + GLTextureImageRepresentationBase::kReadAccessMode, + SharedImageRepresentation::AllowUnclearedAccess::kNo); + EXPECT_TRUE(access); + } + + ASSERT_TRUE(HasGpuBacking(compound_backing)); + auto* gpu_backing = GetGpuBacking(compound_backing); + EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset()); + EXPECT_TRUE(GetGpuHasLatestContent(compound_backing)); +} + TEST_F(CompoundImageBackingTest, ReadbackToMemory) { 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 15b42b3..e952d7e 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
@@ -4,6 +4,9 @@
#include "gpu/command_buffer/service/shared_image/compound_image_backing.h"
+#include <cstdint>
+#include <limits>
+
#include "components/viz/common/resources/shared_image_format.h"
#include "components/viz/common/resources/shared_image_format_utils.h"
#include "gpu/command_buffer/common/shared_image_info.h"
@@ -143,6 +146,15 @@
return false;
}
+ // Advances the latest content id, keeping the shared memory element as the
+ // one holding the latest content. Used to exercise content id values that
+ // would otherwise require many Update() calls to reach.
+ void AdvanceShmContentId(CompoundImageBacking* backing,
+ uint64_t content_id) NO_THREAD_SAFETY_ANALYSIS {
+ backing->latest_content_id_ = content_id;
+ backing->GetShmElement().content_id_ = content_id;
+ }
+
// Construct a CompoundImageBacking via the WrapExternalBacking constructor
// (private). This mirrors CompoundImageBacking::WrapExternalBacking exactly,
// minus the SharedImageFactory consultation.
@@ -360,6 +372,49 @@
EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset());
}
+TEST_F(CompoundImageBackingTest, UploadOnFirstAccessAfterManyUpdates) {
+ 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_);
+
+ // Simulate the state reached after a very large number of shared memory
+ // updates before any GPU access has occurred.
+ AdvanceShmContentId(compound_backing,
+ std::numeric_limits<uint32_t>::max() - 1);
+
+ EXPECT_TRUE(GetShmHasLatestContent(compound_backing));
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+
+ // A further update should keep the shared memory element as the sole holder
+ // of the latest content and never mark the untouched GPU element as current.
+ compound_backing->Update(nullptr);
+ EXPECT_TRUE(GetShmHasLatestContent(compound_backing));
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+
+ compound_backing->Update(nullptr);
+ EXPECT_TRUE(GetShmHasLatestContent(compound_backing));
+ EXPECT_FALSE(GetGpuHasLatestContent(compound_backing));
+
+ // The first GPU read access must still trigger an upload from shared memory.
+ auto gl_rep = manager_.ProduceGLTexturePassthrough(
+ compound_backing->mailbox(), &memory_type_tracker_);
+ ASSERT_TRUE(gl_rep);
+ {
+ auto access = gl_rep->BeginScopedAccess(
+ GLTextureImageRepresentationBase::kReadAccessMode,
+ SharedImageRepresentation::AllowUnclearedAccess::kNo);
+ EXPECT_TRUE(access);
+ }
+
+ ASSERT_TRUE(HasGpuBacking(compound_backing));
+ auto* gpu_backing = GetGpuBacking(compound_backing);
+ EXPECT_TRUE(gpu_backing->GetUploadFromMemoryCalledAndReset());
+ EXPECT_TRUE(GetGpuHasLatestContent(compound_backing));
+}
+
TEST_F(CompoundImageBackingTest, ReadbackToMemory) {
auto backing = CreateCompoundBacking({SHARED_IMAGE_USAGE_GLES2_READ});
auto* compound_backing = static_cast<CompoundImageBacking*>(backing.get());
Original Bug Report
Potential Cross-Origin GPU Memory Leak via CompoundImageBacking Integer Overflow
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: An integer wrap-around vulnerability exists in CompoundImageBacking where latest_content_id_ can overflow to 0 via repeated IPC updates. This wrapped value collides with the default sentinel of an uninitialized GPU backing, bypassing synchronization checks. Consequently, a compromised renderer can read recycled, uninitialized GPU memory, potentially leaking cross-origin graphics data.
Affected files:
gpu/command_buffer/service/shared_image/compound_image_backing.ccgpu/command_buffer/service/shared_image/compound_image_backing.h
Estimated timestamp from git blame: Unknown (Google3 checkout)
1. Summary of the Issue (Meant for Human Triage)
A potential integer wrap-around vulnerability exists in CompoundImageBacking within Chromium’s GPU command buffer service. The class tracks the latest version of content across multiple underlying backings (such as Shared Memory and GPU) using latest_content_id_, a 32-bit unsigned integer. This ID is incremented with an unchecked bare ++ operator every time the shared image is updated. If a compromised renderer sends 2^32 - 1 updates to a single mailbox, the ID overflows and wraps back to 0.
This wrapped value of 0 perfectly collides with the default 0 content ID sentinel on never-synchronized elements, such as lazily allocated GPU backings. On the subsequent first GPU access, the collision (0 == 0) mistakenly satisfies the fast-path condition in NotifyBeginAccess. This causes the GPU process to skip the necessary synchronization copy (CopyImage) from the shared memory backing to the GPU texture. Since the GPU backing was already marked as cleared during creation (SetCleared()) on the assumption that a copy would occur on first access, all downstream validation gates (like IsCleared()) pass. Consequently, the renderer can perform an uninitialized GPU texture read. On platforms or drivers where the GPU allocator recycles graphics memory without zeroing (and where robust_resource_init is disabled on the shared context), this leaks recycled GPU graphics allocations, potentially exposing cross-origin graphic contexts from other tabs or processes.
2. Proof-of-Concept & Detailed Execution Flow
The following is a step-by-step theoretical sequence of events tracing the vulnerability from an attacker-controlled renderer to the uninitialized GPU memory read. Note that these are potential steps, as our tooling does not yet have the ability to run code to produce a live proof of concept.
Phase 1: Initialization and Setup
- Attacker Connection: A compromised renderer process establishes a Mojo connection to the GPU process via
gpu.mojom.GpuChannel. - Creation Request: The attacker sends a
DeferredSharedImageRequest::kCreateSharedImageWithBufferMojo message, specifying aSHARED_MEMORY_BUFFERand usage flagsGLES2_READ | CPU_WRITE_ONLY. - Factory Allocation: The GPU process routes the request to
SharedImageFactory::CreateSharedImage. Becausegmb_type == gfx::SHARED_MEMORY_BUFFERand it is not thread-shared, it callsCompoundImageBacking::Create(gpu/command_buffer/service/shared_image/compound_image_backing.cc:965). - Global Tracker Initialized: In the
CompoundImageBackingconstructor,latest_content_id_(auint32_t) is initialized to1by its default member initializer (compound_image_backing.h:463). - Shared Memory Element Setup: The constructor provisions
elements_[0]to hold the Shared Memory backing and synchronizes its version:shm_element.content_id_ = latest_content_id_;(setting it to1) (compound_image_backing.cc:1150). - Logical Clearing: Because a Shared Memory backing is present (
has_shm_backing_ = true), the entire compound backing is marked logically cleared viaSetClearedRectInternal(gfx::Rect(size))(compound_image_backing.cc:1157). - Lazy GPU Element Setup: A placeholder element,
elements_[1], is added for the eventual GPU backing. Its physical creation is deferred usingLazyCreateBacking(compound_image_backing.cc:1167). - Sentinel Left Untouched: Crucially,
gpu_element.content_id_is not explicitly set in the constructor. It retains its default initializer value of0(compound_image_backing.h:337).- State Checkpoint 1:
latest_content_id_ == 1,elements_[0].content_id_ == 1,elements_[1].content_id_ == 0.
- State Checkpoint 1:
Phase 2: Bypassing Watchdogs and Mojo Constraints
- Mojo Batching Strategy: The maximum size of a single Mojo message is 128 MB (
IPC::mojom::kChannelMaximumMessageSize). A singleFlushDeferredRequestsbatch can hold a maximum of ~986,894DeferredSharedImageRequests. - Watchdog Bypass: The attacker sends these 128 MB batches sequentially. When a batch reaches the GPU IO thread,
GpuChannelMessageFilter::FlushDeferredRequestsunpacks the items into individualScheduler::Taskelements without blocking Mojo queues. As the GPU Scheduler executes each task, theGpuWatchdogThread’s hooks continuously togglearm_disarm_counter_, recognizing progress and preventing a timeout crash, permitting roughly 7 to 70 minutes of silent execution. - Service-Side Check Bypass: To prevent the GPU process from crashing when the client’s flush ID wraps around, the attacker hardcodes
flushed_deferred_message_id = 1in all IPC messages. This cleanly satisfiesCHECK_GE(version, GetSharedVersion())inSharedMemoryVersionController::SetVersionindefinitely.
Phase 3: Triggering the Wrap-Around
- The IPC Loop: The attacker executes a loop sending batches of
mojom::DeferredSharedImageRequest::kUpdateSharedImageaimed at the single shared image mailbox. - Updating the Compound Image: The GPU process routes these updates to
CompoundImageBacking::Update()(compound_image_backing.cc:1415). - No-Op Child Update: Inside
Update(),element.backing->Update()resolves toSharedMemoryImageBacking::Update, which is aCHECK(!in_fence);no-op that takes virtually no GPU cycles (shared_memory_image_backing.cc:104). - Unchecked Increment:
Update()performs the unchecked increment:
element.content_id_ = ++latest_content_id_;
- The Overflow: At an estimated speed of tens of nanoseconds per increment, executing 4.3 billion tasks on the GPU main thread takes approximately 7 to 70 minutes. Upon executing the
4,294,967,295th update request,latest_content_id_wraps around to0due to 32-bit unsigned integer overflow. - Final State Update:
elements_[0].content_id_is updated to the newlatest_content_id_value (0).elements_[1]was never accessed, so itscontent_id_remains untouched at0.
- State Checkpoint 2:
latest_content_id_ == 0,elements_[0].content_id_ == 0,elements_[1].content_id_ == 0.
Phase 4: Lazy Allocation and Bypassing Synchronization
- Forcing GPU Allocation: The attacker issues a GL command buffer command,
CreateAndTexStorage2DSharedImageINTERNALImmediate, invokingProduceGLTexturePassthrough, which entersCompoundImageBacking::GetOrAllocateBacking(SharedImageAccessStream::kGL). - Physical Allocation:
elements_[1]matches thekGLaccess stream, firing its lazy callback toCreateBackingFromBackingFactorywhich allocates the physical backing texture (compound_image_backing.cc:2071). - Premature Clearing: The code notes
if (has_shm_backing_) { backing->SetCleared(); }. It explicitly marks the physical texture as cleared—despite the driver returning raw, uninitialized memory—under the firm assumption that a synchronization copy from the Shared Memory backing will initialize it upon first access. - Initiating Access: The attacker sends
BeginSharedImageAccessDirectCHROMIUMwith read access (GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM). - Clear Checks Defeated:
BeginScopedAccessqueriesIsCleared(). Because of Step 20, this check passes seamlessly, bypassingEnsureClear(gles2_cmd_decoder_passthrough.cc:618). - The Collision:
CompoundImageBacking::NotifyBeginAccessevaluates the critical fast-path condition:
if (access_element && access_element->content_id_ == latest_content_id_) {
- The Bypass:
access_elementpoints toelements_[1], whosecontent_id_is0. Due to the wrap-around,latest_content_id_is also exactly0. The condition evaluates to0 == 0, which istrue. - Skipping the Copy: Because the fast path returns
trueimmediately (compound_image_backing.cc:1252), the system skips thecopy_manager_->CopyImageinvocation. The backing is now permanently bound without initialization.
Phase 5: Cross-Origin Data Leak
- Reading the Texture: The attacker executes
glReadPixelsor samples the texture via a fragment shader to an SSBO using standard GLES2 commands. - Uninitialized Memory: By default, the shared context state operates with
robust_resource_init = false(gles2_cmd_decoder_passthrough.cc:615). Thus, the underlying graphics driver (ANGLE-Vulkan or native GL) provides recycled, uninitialized GPU memory. - Impact: The attacker extracts the contents of the recycled GPU memory, leaking cross-origin graphics data from other origins, tabs, or background applications.
Proposed Fix
To prevent this vulnerability, latest_content_id_ should be protected against overflow. Given that uint32_t is used for content versions, base::CheckedNumeric<uint32_t> should be used to catch overflows and safely crash or handle the error, preventing wrap-around. Alternatively, use a 64-bit integer (uint64_t) for latest_content_id_ and content_id_, making an overflow impossible in a realistic timeframe.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
> Determination: verified — §G0.6/GPU-INT → GPU-XO. latest_content_id_ (uint32_t, .h:461) is incremented with bare ++ at .cc:1201,1268,1379 with no CheckedNumeric/overflow guard. After 2³²−1 A-RENDERER UpdateSharedImage IPCs it wraps to 0, colliding with the default elements_[1].content_id_ = 0 (.h:336, never assigned in ctor :1112-1123). The collision defeats NotifyBeginAccess:1198 (access_element->content_id_ == latest_content_id_ → 0==0 → fast-path return at :1204), so copy_manager_->CopyImage at :1217 is never invoked. The lazily-created GPU backing was already SetCleared() at :2031 without physical init (per the code’s own comment at :2023-2029), so both EnsureClear (gles2_cmd_decoder_passthrough.cc:618) and BeginScopedAccess (shared_image_representation.cc:152) IsCleared() gates pass → uninitialized GPU-texture read via readPixels.
Additional Verification Notes
- OOM / Watchdog Limits Verified: The prior Critic assessed whether 4.3 billion IPCs would crash the GPU process due to OOM or watchdog timeouts. It was confirmed that since Mojo requests are dispatched to the
Schedulerand executed sequentially viaRunNextTask(returning to the message loop),GpuWatchdogThread’sWillProcessTask/DidProcessTaskobserver hooks correctly arm and disarm the watchdog. By batching requests into 128 MB chunks (IPC::mojom::kChannelMaximumMessageSize), the Mojo queues do not pile up, completely avoiding OOMs. - SharedMemoryVersionController Bypass: Sending 4.3 billion requests causes
next_deferred_message_id_to wrap around. The attacker can bypass the GPU process crash (CHECK_GE(version, GetSharedVersion())inSharedMemoryVersionController::SetVersion,mojo/public/cpp/base/shared_memory_version.cc:92) by passing a staticflushed_deferred_message_id(e.g.,1) repeatedly, as this check merely evaluatesCHECK_GE(1, 1)unconditionally. - Execution Timing: The lightweight
SharedMemoryImageBacking::Updateperforms a negligibleCHECK(!in_fence)operation. The entire update step (CompoundImageBacking::Update()) takes tens of nanoseconds. The estimated processing time for $2^{32}$ tasks on the GPU thread is between 7 and 70 minutes. - Severity Alignment: The prior Critic classified this as Medium (S2) due to “extreme trigger complexity” (7-70 minutes). However, an automated, silent integer overflow triggered by a background tab loop does not require “unusual or unlikely user interaction,” and under Chromium’s threat model, automated background tasks scale effectively. Although the previous Critic assigned S2, the foundational cross-origin uninitialized-texture read (GPU-XO) boundary holds firm.
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.