Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in GPU
DescriptionOut of bounds read in GPU
ComponentGPU
Bug ClassOOB
Tracker499891888
Fix commit1880a4c3156b (chromium/src) +9/-0
CISA KEVNot listed
Creditedtatiwari of Microsoft
Disclosed2026-04-22

Files Changed

  • gpu/ipc/service/gpu_channel.cc
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()) {
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. DestroyCommandBufferWithReservedRouteErasesSharedImageRoute - Deterministic invariant test proving Part 1 (reserved route is erased).

  2. 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)

  1. Build gpu_unittests
  2. 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
View on issue tracker