High patch-now ✔︎ verified CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in GPU
DescriptionUse after free in GPU
ComponentGPU
Sub-componentGPU
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — factors set 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 AIConfirmed
Reviewer notes: none; the UAF-write (DoUnmapBuffer memcpy) and GL_OUT_OF_MEMORY driver-free behavior are not in the shown source (analysis hedges as inferred) but are corroborated by the commit message and the added regression test confirming the stale mapped_buffer_map entry.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Additionally cross-checked: a second, different model independently derived the same bug class, vulnerable function and fix from the commit.
Tracker507707838
Fix commit13532776ca8a (chromium/src) +41/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
Creditedtohafrit
Reported2026-04-29 · bounty $25000 (per vendor advisory)
Fix landed2026-05-04 (public git)
Disclosed2026-05-27
Patch gap23 days — fix public in source before the advisory

Background

`GLES2DecoderPassthroughImpl`
the GPU-process command decoder that forwards GLES2 commands straight to the driver via api() function pointers.
`mapped_buffer_map`
a map in PassthroughResources recording currently mapped buffers so a later unmap can copy renderer shared memory into the mapped region.
`CheckErrorCallbackState()`
returns true when the KHR_debug callback recorded that the preceding GL call produced an error.

Root Cause Analysis

Verify before building a PoC

The fix in this commit is confirmed, but the trigger and sink live in code this commit does not touch — find and confirm them against the source before building a PoC.
What the commit does not establish: none; the UAF-write (DoUnmapBuffer memcpy) and GL_OUT_OF_MEMORY driver-free behavior are not in the shown source (analysis hedges as inferred) but are corroborated by the commit message and the added regression test confirming the stale mapped_buffer_map entry.

GLES2DecoderPassthroughImpl::DoBufferData() (gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:612-630) clears the GL error state, calls api()->glBufferDataFn(target, size, data, usage) at :617, then checks if (CheckErrorCallbackState()) at :618 and returns early at :619 when the call errored. Only on the no-error path does it reach resources_->mapped_buffer_map.erase(bound_buffers_[target]) at :627, which removes the mapping entry because a glBufferData on a mapped buffer implicitly unmaps it (comment at :626). Because the early-return at :619 skips that erase, a glBufferData that fails (per the reporter, most impactfully GL_OUT_OF_MEMORY, where the driver frees the old backing store) leaves a stale mapped_buffer_map entry whose stored pointer now references freed driver memory. A later unmap then writes renderer-controlled data into that freed region (the unmap memcpy/DoUnmapBuffer path is described by the reporter and the commit message but is not in the shown source — (inferred)).

The fix adds resources_->mapped_buffer_map.erase(bound_buffers_[target]); inside the if (CheckErrorCallbackState()) branch (diff at pre-fix :618), so the entry is removed whether or not the call errored, restoring the invariant that a mapping entry never outlives its backing store.

Key insight
The implicit-unmap bookkeeping ran only when glBufferData succeeded; moving the mapped_buffer_map.erase() into the error branch too ensures no stale mapping survives a failed, buffer-freeing call.

Attack Path

  1. Map a buffer A compromised renderer binds a buffer and maps it with write access, adding an entry to mapped_buffer_map. (inferred: map/unmap entry points are not in the shown source.)
  2. Trigger a failing `glBufferData` It issues glBufferData with a size that makes the driver error (e.g. an out-of-memory size), which under the KHR_debug callback makes CheckErrorCallbackState() return true and DoBufferData take the early return at :619 without erasing the entry.
  3. Unmap into freed memory A subsequent unmap uses the stale entry’s pointer to copy renderer data into the now-freed driver allocation, a heap use-after-free write. (inferred: the unmap write path is not shown.)

Impact Assessment

A compromised renderer gains a heap use-after-free write in the GPU process with attacker-influenced destination, contents, and size, since the leaked driver backing store can be reclaimed. The advisory rates this high severity with no in-the-wild exploitation; sandbox-escape escalation is (inferred).

Changed Functions

FunctionChangeNotes
TEST_F
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
modified

Files Changed

  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc

Audit Directions

  • Error-path bookkeeping
    Audit decoder doers that release or re-point driver resources on success; the identical cleanup must run on the GL-error early-return branch.
  • Cross-decoder parity
    When a security fix lands in one decoder (validating vs passthrough), check that the sibling decoder performs the same state teardown.
  • Mapped-resource lifetime
    Review every operation that can implicitly unmap or free a buffer to confirm the corresponding host-side mapping record is cleared.
From 13532776ca8af93b5254efd1ac3cd4db62bddd6c Mon Sep 17 00:00:00 2001
From: Shrek Shao <shrekshao@google.com>
Date: Mon, 04 May 2026 13:58:01 -0700
Subject: [PATCH] gpu: Fix UAF in passthrough decoder DoBufferData

GLES2DecoderPassthroughImpl::DoBufferData does not clean up the
mapped_buffer_map entry when glBufferData returns a GL error.
The result is that DoUnmapBuffer later finds a stale entry with
map_ptr pointing to freed driver memory and writes renderer-controlled
data into it.

This CL fixes the issue by erasing the entry from mapped_buffer_map
even if glBufferData returns an error, preventing the potential
heap-use-after-free write.

Bug: 507707838
Change-Id: I51d7eb959a8f11077e05b73e86d16d9fae850047
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7808748
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Shrek Shao <shrekshao@google.com>
Cr-Commit-Position: refs/heads/main@{#1624914}
---

diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
index ceed2b38..ef823b7 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc
@@ -616,6 +616,10 @@
   CheckErrorCallbackState();
   api()->glBufferDataFn(target, size, data, usage);
   if (CheckErrorCallbackState()) {
+    // Calling buffer data on a mapped buffer will implicitly unmap it
+    // (https://registry.khronos.org/OpenGL-Refpages/es3.1/html/glMapBufferRange.xhtml)
+    // Even if it returns an error.
+    resources_->mapped_buffer_map.erase(bound_buffers_[target]);
     return error::kNoError;
   }
 
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
index cf812e2a..39005d9 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
@@ -691,5 +691,42 @@
   EXPECT_EQ(GL_NO_ERROR, GetGLError());
 }
 
+TEST_F(GLES3DecoderPassthroughTest, BufferDataGLErrorLeavesStaleMapEntry) {
+  const GLenum kTarget = GL_ARRAY_BUFFER;
+  const GLsizeiptr kSize = 64;
+  const GLbitfield kAccess = GL_MAP_WRITE_BIT;
+
+  uint32_t result_shm_id = shared_memory_id_;
+  uint32_t result_shm_offset = kSharedMemoryOffset;
+  uint32_t data_shm_id = shared_memory_id_;
+  uint32_t data_shm_offset = kSharedMemoryOffset + sizeof(uint32_t);
+
+  GenHelper<cmds::GenBuffersImmediate>(kClientBufferId);
+  DoBindBuffer(kTarget, kClientBufferId);
+  DoBufferData(kTarget, kSize, nullptr, GL_STREAM_DRAW);
+
+  // Map the buffer
+  auto* result = GetSharedMemoryAs<cmds::MapBufferRange::Result*>();
+  cmds::MapBufferRange map_cmd;
+  map_cmd.Init(kTarget, 0, kSize, kAccess, data_shm_id, data_shm_offset,
+               result_shm_id, result_shm_offset);
+  *result = 0;
+  EXPECT_EQ(error::kNoError, ExecuteCmd(map_cmd));
+  EXPECT_EQ(1u, *result);
+
+  PassthroughResources* passthrough_resources = GetPassthroughResources();
+  EXPECT_NE(passthrough_resources->mapped_buffer_map.find(kClientBufferId),
+            passthrough_resources->mapped_buffer_map.end());
+
+  // Call BufferData with invalid size to trigger GL error
+  cmds::BufferData buffer_data_cmd;
+  buffer_data_cmd.Init(kTarget, -1, 0, 0, GL_STREAM_DRAW);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_data_cmd));
+
+  // The entry should be erased even after a GL error.
+  EXPECT_EQ(passthrough_resources->mapped_buffer_map.find(kClientBufferId),
+            passthrough_resources->mapped_buffer_map.end());
+}
+
 }  // namespace gles2
 }  // namespace gpu
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
index cf812e2a..39005d9 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc
@@ -691,5 +691,42 @@
   EXPECT_EQ(GL_NO_ERROR, GetGLError());
 }
 
+TEST_F(GLES3DecoderPassthroughTest, BufferDataGLErrorLeavesStaleMapEntry) {
+  const GLenum kTarget = GL_ARRAY_BUFFER;
+  const GLsizeiptr kSize = 64;
+  const GLbitfield kAccess = GL_MAP_WRITE_BIT;
+
+  uint32_t result_shm_id = shared_memory_id_;
+  uint32_t result_shm_offset = kSharedMemoryOffset;
+  uint32_t data_shm_id = shared_memory_id_;
+  uint32_t data_shm_offset = kSharedMemoryOffset + sizeof(uint32_t);
+
+  GenHelper<cmds::GenBuffersImmediate>(kClientBufferId);
+  DoBindBuffer(kTarget, kClientBufferId);
+  DoBufferData(kTarget, kSize, nullptr, GL_STREAM_DRAW);
+
+  // Map the buffer
+  auto* result = GetSharedMemoryAs<cmds::MapBufferRange::Result*>();
+  cmds::MapBufferRange map_cmd;
+  map_cmd.Init(kTarget, 0, kSize, kAccess, data_shm_id, data_shm_offset,
+               result_shm_id, result_shm_offset);
+  *result = 0;
+  EXPECT_EQ(error::kNoError, ExecuteCmd(map_cmd));
+  EXPECT_EQ(1u, *result);
+
+  PassthroughResources* passthrough_resources = GetPassthroughResources();
+  EXPECT_NE(passthrough_resources->mapped_buffer_map.find(kClientBufferId),
+            passthrough_resources->mapped_buffer_map.end());
+
+  // Call BufferData with invalid size to trigger GL error
+  cmds::BufferData buffer_data_cmd;
+  buffer_data_cmd.Init(kTarget, -1, 0, 0, GL_STREAM_DRAW);
+  EXPECT_EQ(error::kNoError, ExecuteCmd(buffer_data_cmd));
+
+  // The entry should be erased even after a GL error.
+  EXPECT_EQ(passthrough_resources->mapped_buffer_map.find(kClientBufferId),
+            passthrough_resources->mapped_buffer_map.end());
+}
+
 }  // namespace gles2
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by pa...@gmail.com

GPU passthrough decoder UAF: DoBufferData GL error path skips mapped_buffer_map.erase()


Report description

GPU passthrough decoder UAF: DoBufferData GL error path skips mapped_buffer_map.erase()


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc


The problem

Please describe the technical details of the vulnerability

Vulnerability Description

GLES2DecoderPassthroughImpl::DoBufferData in the GPU passthrough decoder does not clean up the mapped_buffer_map entry when glBufferData returns a GL error. The result is that DoUnmapBuffer later finds a stale entry with map_ptr pointing to freed driver memory and writes renderer-controlled data into it.

The relevant code in gles2_cmd_decoder_passthrough_doers.cc (current HEAD, ~line 618):

  CheckErrorCallbackState();
  api()->glBufferDataFn(target, size, data, usage);
  if (CheckErrorCallbackState()) {
    return error::kNoError;   // early return, no erase
  }
  resources_->mapped_buffer_map.erase(bound_buffers_[target]);  // not reached on error

CheckErrorCallbackState() works through the KHR_debug callback (PassthroughGLDebugMessageCallback -> OnDebugMessage -> had_error_callback_ = true). Any GL error from glBufferDataFn causes the early return. The most impactful case is GL_OUT_OF_MEMORY, where the driver frees the old backing store and the map_ptr in the remaining mapped_buffer_map entry now points to freed memory.

The same vulnerability was fixed in the validating decoder in commit daeb9ba8d7ce (Bug 498715368) with this comment in buffer_manager.cc:

  // SECURITY: ... Clear it here so that UnmapBufferHelper does not memcpy
  // renderer-controlled SHM into freed driver memory.
  buffer->ClearMapping();

The passthrough decoder was not included in that fix.

Attack Preconditions

Attacker has a compromised renderer process (standard precondition for GPU process bugs). The GPU passthrough decoder must be active, which is the case by default on Windows, macOS, and Linux: ui/gl/features.gni sets enable_validating_command_decoder = is_android, so on desktop builds UsePassthroughCommandDecoder() in gl_utils.cc returns true unconditionally.

Reproduction Steps / POC

I added a regression test to the existing passthrough decoder test suite: gpu/command_buffer/service/gles2_cmd_decoder_passthrough_unittest_buffers.cc

Test name: GLES3DecoderPassthroughTest.BufferDataGLErrorLeavesStaleMapEntry

Steps the test performs:

  1. Create and bind a buffer, map it with GL_MAP_WRITE_BIT. Confirms the entry is added to mapped_buffer_map.
  2. Submit a BufferData command with size=-1. This triggers GL_INVALID_VALUE, which fires the KHR_debug callback and causes DoBufferData to take the early-return path. Functionally the same as GL_OUT_OF_MEMORY for this code path.
  3. Assert that the mapped_buffer_map entry is still present (bug confirmed).

Output from the ASAN build (out/asan/gpu_unittests, macOS arm64, ANGLE null backend):

  [ RUN      ] GLES3DecoderPassthroughTest.BufferDataGLErrorLeavesStaleMapEntry
  [ERROR] GL_INVALID_VALUE: glBufferData: Negative size.
  [       OK ] GLES3DecoderPassthroughTest.BufferDataGLErrorLeavesStaleMapEntry (3584 ms)

The EXPECT_NE assertion passes, confirming the stale entry is present after the GL error. On real hardware with GL_OUT_OF_MEMORY the map_ptr would point to freed driver memory, and a subsequent glUnmapBuffer call from the renderer would trigger:

memcpy(map_info.map_ptr, renderer_shm, map_info.size)

That memcpy is a heap-use-after-free write in the GPU process with renderer-controlled content and size.

Fix: add the erase before the early return in DoBufferData:

  if (CheckErrorCallbackState()) {
      resources_->mapped_buffer_map.erase(bound_buffers_[target]);
      return error::kNoError;
  }

Impact analysis

A compromised renderer can trigger this by mapping a GPU buffer, then calling glBufferData with a large enough size to cause GL_OUT_OF_MEMORY. The GPU driver frees the old backing store. DoBufferData returns early without erasing the mapped_buffer_map entry. The renderer then calls glUnmapBuffer, which causes the GPU process to write attacker-controlled data into freed driver memory.

This gives an attacker a heap-use-after-free write primitive in the GPU process, with control over the write destination (via timing/heap grooming), the content (shared memory), and the size. The GPU process is a sandboxed process separate from the renderer. Exploiting this primitive to achieve GPU process code execution would constitute a sandbox escape from a compromised renderer.

The bug affects all desktop Chrome users on Windows, macOS, and Linux, since the passthrough decoder is enabled by default on those platforms.


The cause

What version of Chrome have you found the security issue in?

149.0.7815.0 (dev/canary) - confirmed on HEAD commit fc933fae76 (2026-04-28) 147.0.7727.138 (stable) - affected, bug predates current HEAD

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a sandboxed process)

How would you like to be publicly acknowledged for your report?

tohafrit

View on issue tracker