CVE-2026-6920
Overview
Files Changed
gpu/ipc/service/gpu_channel.cc
Patch
From 1880a4c3156b118953e2658f772afd666c0b3ed8 Mon Sep 17 00:00:00 2001 From: Vasiliy Telezhnikov <[email protected]> Date: Fri, 10 Apr 2026 14:17:34 -0700 Subject: [PATCH] Validate route_id for command buffers Some route ids are reserved, we shouldn't allow command buffer operations on them. Bug: 499891888 Change-Id: Id4c32103d8db70d9819112d4eb8d5c840d033300 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747910 Reviewed-by: Kyle Charbonneau <[email protected]> Commit-Queue: Vasiliy Telezhnikov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1613096} --- diff --git a/gpu/ipc/service/gpu_channel.cc b/gpu/ipc/service/gpu_channel.cc index 0002bd02..6224c3d7 100644 --- a/gpu/ipc/service/gpu_channel.cc +++ b/gpu/ipc/service/gpu_channel.cc @@ -994,6 +994,11 @@ return; } + if (route_id <= static_cast<int32_t>(GpuChannelReservedRoutes::kMaxValue)) { + LOG(ERROR) << "ContextResult::kFatalFailure: using reserved route"; + return; + } + int32_t stream_id = init_params->stream_id; CommandBufferId command_buffer_id = CommandBufferIdFromChannelAndRoute(client_id_, route_id); @@ -1055,6 +1060,10 @@ TRACE_EVENT1("gpu", "GpuChannel::OnDestroyCommandBuffer", "route_id", route_id); + if (route_id <= static_cast<int32_t>(GpuChannelReservedRoutes::kMaxValue)) { + return; + } + std::unique_ptr<CommandBufferStub> stub; auto it = stubs_.find(route_id); if (it != stubs_.end()) {
Original Bug Report
DestroyCommandBuffer allows renderer to erase reserved route -> Iterator invalidation in FlushDeferredRequests
Hello, for some prior context - our team has been experimenting with LLM powered security audits, and this is one of the reports. The bug is triggerable on Android, but due to lack of h/w I’ve added a unit-test which simulates the issue and showcases the bug.
A compromised renderer can call DestroyCommandBuffer(route_id=0) via Mojo to
erase the reserved kSharedImageInterface route from the GPU channel’s route_sequences_ map. A subsequent FlushDeferredRequests call while the application is backgrounded then dereferences end() on the route map, producing undefined behavior (shows up as out-of-bounds read under ASan).
Version
Validated on 18c701fd5a8a8, latest main at the time of reporting; The bug is reachable only on Android.
Issue
Two independent bugs combine:
Part 1: RemoveRoute called unconditionally in DestroyCommandBuffer
// gpu/ipc/service/gpu_channel.cc
void GpuChannel::DestroyCommandBuffer(int32_t route_id) {
auto it = stubs_.find(route_id);
if (it != stubs_.end()) { // route 0 NOT in stubs_ -> skipped
stub = std::move(it->second);
stubs_.erase(it);
}
// ...
RemoveRoute(route_id); // <- UNCONDITIONAL: erases route 0
}
kSharedImageInterface (route 0) is a reserved route registered via
AddRoute() in CreateSharedImageStub(), but it is NOT a command buffer stub
in stubs_. A renderer calling DestroyCommandBuffer(0) skips the stub guard
but still calls RemoveRoute(0), erasing the route from route_sequences_.
Part 2: Missing end() check in FlushDeferredRequests
Note: OnApplicationBackgrounded() is not renderer-reachable, and requires the app to send this notification to set application_backgrounded() to true
// gpu/ipc/service/gpu_channel.cc - FlushDeferredRequests
if (gpu_channel_->gpu_channel_manager()->application_backgrounded()) {
auto it = route_sequences_.find(
static_cast<int32_t>(GpuChannelReservedRoutes::kSharedImageInterface));
tasks.emplace_back(it->second, ...); // <- NO end() check -> UB
}
The same function’s main loop (20 lines earlier) correctly checks
it == route_sequences_.end() before dereferencing. This backgrounded code
path does not.
Attack Path
Renderer --Mojo--> DestroyCommandBuffer(route_id=0)
|
+- stubs_.find(0) -> end() -> skip (harmless)
|
+- RemoveRoute(0) <- UNCONDITIONAL
|
+- route_sequences_.erase(0) (erases kSharedImageInterface)
Later: FlushDeferredRequests (app backgrounded) // Only called on Android
|
+- route_sequences_.find(kSharedImageInterface) -> end()
+- tasks.emplace_back(it->second, ...) <- DEREF end() -> UB
Proof of Concept
Attached patch
gpu_channel_reserved_route_uaf.patch adds two tests to
gpu/ipc/service/gpu_channel_unittest.cc:
-
DestroyCommandBufferWithReservedRouteErasesSharedImageRoute- Deterministic invariant test proving Part 1 (reserved route is erased). -
DISABLED_FlushDeferredRequestsUBAfterReservedRouteErased- ASan crash repro proving Part 2 (container-overflow on end() deref).
ASan output (abbreviated)
==19704==ERROR: AddressSanitizer: container-overflow on address 0x11f0ef1c8ab4
at pc 0x7ff9f728c3ec bp 0x00bb40efef00 sp 0x00bb40efef48
READ of size 4 at 0x11f0ef1c8ab4 thread T0
#0 vector::__emplace_back_slow_path<SequenceId&, ...>
#1 gpu::GpuChannelMessageFilter::FlushDeferredRequests gpu_channel.cc:419
#2 gpu::GpuChannelTest_DISABLED_...::TestBody gpu_channel_unittest.cc:195
0x11f0ef1c8ab4 is located 4 bytes inside of 8-byte region
[0x11f0ef1c8ab0, 0x11f0ef1c8ab8) allocated by thread T0 here:
#0 operator new
#1 gpu::GpuChannelMessageFilter::AddRoute gpu_channel.cc:337
#2 gpu::GpuChannel::CreateSharedImageStub gpu_channel.cc:915
SUMMARY: AddressSanitizer: container-overflow
gpu_channel.cc:419 in FlushDeferredRequests
Suggested Fix
// Fix 1: return early if route_id is not a command buffer stub.
void GpuChannel::DestroyCommandBuffer(int32_t route_id) {
auto it = stubs_.find(route_id);
if (it == stubs_.end()) return; // not a command buffer -> bail
// ... existing cleanup ...
RemoveRoute(route_id);
}
// Fix 2: defense-in-depth: check end() in FlushDeferredRequests.
auto it = route_sequences_.find(...kSharedImageInterface);
if (it != route_sequences_.end()) {
tasks.emplace_back(it->second, ...);
}
Reproducing the issue
I’m attaching a patch file which adds 2 tests to show the invariant being broken (Part 1) and triggers the iterator invalidation by simulating the application being backgrounded (Part 2)
- Build
gpu_unittests - run
out/asan/gpu_unittests \
--gtest_filter='GpuChannelTest.DestroyCommandBufferWithReservedRouteErasesSharedImageRoute'
and
out/asan/gpu_unittests \
--gtest_also_run_disabled_tests \
--gtest_filter='*FlushDeferredRequestsUBAfterReservedRouteErased*' \
--single-process-tests