Critical watch CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactStack buffer overflow in GPU
DescriptionStack buffer overflow in GPU
ComponentGPU
Sub-componentGPU-Security
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker513946753
Fix commitc6735c423fa5 (chromium/src) +70/-6
CWECWE-121 (Stack buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-05-20 (public git)
Disclosed2026-06-02
Patch gap13 days — fix public in source before the advisory

Background

Dawn / WebGPU
Dawn is Chrome’s implementation of the WebGPU API, running in the GPU process; DawnCopyStrategy uses it to copy texture data into a shared-image backing.
`MapAsync`
a Dawn buffer call that asynchronously maps GPU memory for CPU access and invokes a registered callback when the map completes or fails.
`WaitAny` / `FutureWaitInfo`
Dawn synchronization primitives used to block until a future (such as the MapAsync completion) is signaled; WaitAny can itself fail (e.g. timeout or device loss).
`EventManager`
Dawn’s internal owner of pending event callbacks, which may retain and later fire a registered callback (for example during instance teardown) even after the caller has moved on.

Root Cause Analysis

In DawnCopyStrategy::CopyFromTextureToBacking, the code declared a stack-local bool success and registered a MapAsync callback that captured &success, writing *success when the callback fired. It then called WaitAny; if WaitAny returned anything other than wgpu::WaitStatus::Success, the function logged an error and returned early — destroying the success stack variable while Dawn’s EventManager still held the callback with the now-dangling pointer. When Dawn later fired that callback (e.g. at instance teardown), it dereferenced and wrote through the dangling stack pointer, a stack-use-after-return write.

The fix replaces the capturing callback with a stateless lambda that touches no stack state and instead queries staging_buffer_entry.buffer.GetMapState() after synchronization to decide success, and it additionally calls staging_buffer_entry.buffer.Unmap() on the WaitAny failure path so the pending map operation is torn down rather than left to fire later. Because no stack-local pointer is ever handed to the callback, the lifetime bug is eliminated entirely.

Key insight
The single mistake was passing a pointer to a stack-local success variable into an async MapAsync callback that Dawn could outlive-and-fire after the function returned; the fix removes all stack capture (stateless lambda + GetMapState()) and unmaps the buffer on the failure path.

Attack Path

  1. Enter the copy path A copy into a shared-image backing drives DawnCopyStrategy::CopyFromTextureToBacking, which maps each staging buffer with MapAsync, passing a callback bound to the stack-local success.
  2. Force WaitAny to fail A condition that makes WaitAny return non-Success (timeout or device loss; the test forces it by omitting the TimedWaitAny feature) causes the function to return early.
  3. Stack frame torn down with callback still pending The early return destroys success while the EventManager retains the callback holding &success, now dangling.
  4. Dangling write on later fire Dawn subsequently fires the retained callback (e.g. during instance teardown), which writes the status boolean through the dangling stack pointer — a stack-use-after-return write into whatever now occupies that stack region.

Impact Assessment

An attacker able to trigger a WaitAny failure during a Dawn staging-buffer map obtains a stack-use-after-return write in the GPU process, classified by the advisory as a stack buffer overflow in GPU; the written value is a status boolean into a reclaimed stack frame. The advisory does not report exploitation in the wild, and escalation beyond the dangling write (control-flow hijack or sandbox escape) is (inferred). Preconditions are reaching the WebGPU/Dawn copy path and inducing a WaitAny failure such as device loss or timeout.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc
modified
DawnCopyStrategyStackUARTest
gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
modified

Files Changed

  • gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc
  • gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc

Audit Directions

  • Stack pointers into async callbacks
    Audit every Dawn/WebGPU MapAsync, future, or event callback that captures &local or a raw pointer to a stack variable, since any early return before the callback fires yields a use-after-return.
  • Early returns on wait/sync failure
    Look for WaitAny/wait/poll failure paths that return without cancelling, unmapping, or otherwise tearing down still-pending async operations that hold references to caller state.
  • State-via-callback vs. state-query
    Prefer querying object state (GetMapState() and equivalents) after synchronization over writing results through captured pointers, and audit existing callbacks that report status by pointer for lifetime safety.
From c6735c423fa5b4e02fc7f7325e10b1a84278cd2f Mon Sep 17 00:00:00 2001
From: vikas soni <[email protected]>
Date: Wed, 20 May 2026 08:47:17 -0700
Subject: [PATCH] [GPU-Security] Fix stack-use-after-return in DawnCopyStrategy.

In DawnCopyStrategy::CopyFromTextureToBacking, a MapAsync callback was
registered with a stack-local success boolean pointer. If WaitAny failed
(e.g., due to timeout or device loss), the function returned early,
causing the stack-local variable to go out of scope. However, Dawn's
EventManager could still hold the callback and fire it later (e.g.,
during instance teardown), resulting in a write to a dangling stack
pointer.

This CL fixes the issue by using a stateless lambda for MapAsync and
querying the buffer's map state via GetMapState() after synchronization
with WaitAny. This avoids passing any stack-local pointers or capturing
variables, completely eliminating the lifetime issue.

A unit test is added to reproduce the issue by creating a Dawn Instance
without the TimedWaitAny feature to force WaitAny failures.

Bug: 513946753
Change-Id: Id410fa9cf4e95bd213a0d99d008427128c380e0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7855882
Commit-Queue: vikas soni <[email protected]>
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Auto-Submit: vikas soni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1633602}
---

diff --git a/gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc b/gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc
index e56810f8..7d17bfc 100644
--- a/gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc
+++ b/gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc
@@ -334,22 +334,23 @@
        plane_index < static_cast<int>(staging_buffers.size()); ++plane_index) {
     const auto& staging_buffer_entry = staging_buffers[plane_index];
 
-    bool success = false;
     wgpu::FutureWaitInfo wait_info = {staging_buffer_entry.buffer.MapAsync(
         wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
         wgpu::CallbackMode::WaitAnyOnly,
-        [](wgpu::MapAsyncStatus status, wgpu::StringView, bool* success) {
-          *success = status == wgpu::MapAsyncStatus::Success;
-        },
-        &success)};
+        [](wgpu::MapAsyncStatus status, wgpu::StringView) {
+          // MapAsync requires a callback, but we don't need to do anything in
+          // it since we check the map state via GetMapState() after waiting.
+        })};
 
     if (device.GetAdapter().GetInstance().WaitAny(1, &wait_info, UINT64_MAX) !=
         wgpu::WaitStatus::Success) {
       LOG(ERROR) << "WaitAny failed while mapping staging buffer for read.";
+      staging_buffer_entry.buffer.Unmap();
       return false;
     }
 
-    if (!wait_info.completed || !success) {
+    if (!wait_info.completed || staging_buffer_entry.buffer.GetMapState() !=
+                                    wgpu::BufferMapState::Mapped) {
       LOG(ERROR) << "MapAsync did not yield a readable mapping.";
       return false;
     }
diff --git a/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc b/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
index 5f57d3a..9bc1bf1 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
@@ -19,6 +19,9 @@
 #include "ui/gl/test/gl_surface_test_support.h"
 
 #if BUILDFLAG(SKIA_USE_DAWN)
+#include <dawn/dawn_proc.h>
+#include <dawn/native/DawnNative.h>
+
 #include "gpu/command_buffer/service/dawn_context_provider.h"
 #endif
 
@@ -151,6 +154,66 @@
   EXPECT_TRUE(
       copy_manager_->CopyImage(dawn_backing.get(), graphite_backing.get()));
 }
+
+class DawnCopyStrategyStackUARTest : public testing::Test {
+ public:
+  void SetUp() override {
+    dawnProcSetProcs(&dawn::native::GetProcs());
+
+    // Create instance WITHOUT TimedWaitAny
+    dawn_instance_ = std::make_unique<dawn::native::Instance>();
+
+    wgpu::RequestAdapterOptions adapter_options;
+    adapter_options.backendType = wgpu::BackendType::Vulkan;
+    std::vector<dawn::native::Adapter> adapters =
+        dawn_instance_->EnumerateAdapters(&adapter_options);
+    ASSERT_GT(adapters.size(), 0u);
+
+    wgpu::FeatureName dawn_internal_usage =
+        wgpu::FeatureName::DawnInternalUsages;
+    wgpu::DeviceDescriptor device_descriptor;
+    device_descriptor.requiredFeatureCount = 1;
+    device_descriptor.requiredFeatures = &dawn_internal_usage;
+
+    dawn_device_ =
+        wgpu::Device::Acquire(adapters[0].CreateDevice(&device_descriptor));
+    ASSERT_TRUE(dawn_device_) << "Failed to create Dawn device";
+  }
+
+  void TearDown() override {
+    dawn_device_ = wgpu::Device();
+    dawn_instance_.reset();
+  }
+
+ protected:
+  std::unique_ptr<dawn::native::Instance> dawn_instance_;
+  wgpu::Device dawn_device_;
+};
+
+TEST_F(DawnCopyStrategyStackUARTest,
+       MapAsyncCallbackOutlivesStackOnWaitAnyFailure) {
+  wgpu::TextureDescriptor texture_desc;
+  texture_desc.size = {4, 4, 1};
+  texture_desc.format = wgpu::TextureFormat::RGBA8Unorm;
+  texture_desc.usage =
+      wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment;
+  wgpu::Texture src_texture = dawn_device_.CreateTexture(&texture_desc);
+  ASSERT_TRUE(src_texture);
+
+  auto dst_backing = std::make_unique<TestImageBacking>(
+      Mailbox::Generate(),
+      SharedImageInfo(
+          viz::SinglePlaneFormat::kRGBA_8888, gfx::Size(4, 4),
+          gfx::ColorSpace::CreateSRGB(), kTopLeft_GrSurfaceOrigin,
+          kPremul_SkAlphaType,
+          SHARED_IMAGE_USAGE_CPU_READ | SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
+          "TestLabel"),
+      1024);
+
+  EXPECT_FALSE(DawnCopyStrategy::CopyFromTextureToBacking(
+      src_texture, dst_backing.get(), dawn_device_));
+}
+
 #endif  // BUILDFLAG(SKIA_USE_DAWN)
 
 }  // namespace gpu
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc b/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
index 5f57d3a..9bc1bf1 100644
--- a/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/shared_image_copy_manager_unittest.cc
@@ -19,6 +19,9 @@
 #include "ui/gl/test/gl_surface_test_support.h"
 
 #if BUILDFLAG(SKIA_USE_DAWN)
+#include <dawn/dawn_proc.h>
+#include <dawn/native/DawnNative.h>
+
 #include "gpu/command_buffer/service/dawn_context_provider.h"
 #endif
 
@@ -151,6 +154,66 @@
   EXPECT_TRUE(
       copy_manager_->CopyImage(dawn_backing.get(), graphite_backing.get()));
 }
+
+class DawnCopyStrategyStackUARTest : public testing::Test {
+ public:
+  void SetUp() override {
+    dawnProcSetProcs(&dawn::native::GetProcs());
+
+    // Create instance WITHOUT TimedWaitAny
+    dawn_instance_ = std::make_unique<dawn::native::Instance>();
+
+    wgpu::RequestAdapterOptions adapter_options;
+    adapter_options.backendType = wgpu::BackendType::Vulkan;
+    std::vector<dawn::native::Adapter> adapters =
+        dawn_instance_->EnumerateAdapters(&adapter_options);
+    ASSERT_GT(adapters.size(), 0u);
+
+    wgpu::FeatureName dawn_internal_usage =
+        wgpu::FeatureName::DawnInternalUsages;
+    wgpu::DeviceDescriptor device_descriptor;
+    device_descriptor.requiredFeatureCount = 1;
+    device_descriptor.requiredFeatures = &dawn_internal_usage;
+
+    dawn_device_ =
+        wgpu::Device::Acquire(adapters[0].CreateDevice(&device_descriptor));
+    ASSERT_TRUE(dawn_device_) << "Failed to create Dawn device";
+  }
+
+  void TearDown() override {
+    dawn_device_ = wgpu::Device();
+    dawn_instance_.reset();
+  }
+
+ protected:
+  std::unique_ptr<dawn::native::Instance> dawn_instance_;
+  wgpu::Device dawn_device_;
+};
+
+TEST_F(DawnCopyStrategyStackUARTest,
+       MapAsyncCallbackOutlivesStackOnWaitAnyFailure) {
+  wgpu::TextureDescriptor texture_desc;
+  texture_desc.size = {4, 4, 1};
+  texture_desc.format = wgpu::TextureFormat::RGBA8Unorm;
+  texture_desc.usage =
+      wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment;
+  wgpu::Texture src_texture = dawn_device_.CreateTexture(&texture_desc);
+  ASSERT_TRUE(src_texture);
+
+  auto dst_backing = std::make_unique<TestImageBacking>(
+      Mailbox::Generate(),
+      SharedImageInfo(
+          viz::SinglePlaneFormat::kRGBA_8888, gfx::Size(4, 4),
+          gfx::ColorSpace::CreateSRGB(), kTopLeft_GrSurfaceOrigin,
+          kPremul_SkAlphaType,
+          SHARED_IMAGE_USAGE_CPU_READ | SHARED_IMAGE_USAGE_CPU_WRITE_ONLY,
+          "TestLabel"),
+      1024);
+
+  EXPECT_FALSE(DawnCopyStrategy::CopyFromTextureToBacking(
+      src_texture, dst_backing.get(), dawn_device_));
+}
+
 #endif  // BUILDFLAG(SKIA_USE_DAWN)
 
 }  // namespace gpu
Loading diff…

Original Bug Report

reported by [email protected]

Potential stack write-after-return in GPU process via DawnCopyStrategy::CopyFromTextureToBacking

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: A stack-local variable’s address is leaked as userdata into a wgpu::Buffer::MapAsync callback in the GPU process. If a GPU device loss causes the subsequent synchronous wait to return early, the function returns while the callback remains pending. Upon Dawn instance shutdown, the callback is fired, resulting in a 1-byte write to the now-invalid stack address.

Affected files:

  • gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc
  • third_party/dawn/src/dawn/native/EventManager.cpp
  • third_party/dawn/src/dawn/native/Buffer.cpp

Estimated timestamp from git blame: 2025-09-17

A potential stack-use-after-return (SUAR) vulnerability exists in gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc within the CopyFromTextureToBacking method.

When synchronizing data from a Dawn texture to a non-Dawn backing (e.g., during fallback interop on Android), the code utilizes staging buffers and maps them for reading using Dawn’s MapAsync API. The implementation passes the address of a stack-local boolean variable success as the userdata for the callback:

// gpu/command_buffer/service/shared_image/dawn_copy_strategy.cc:337
bool success = false;
wgpu::FutureWaitInfo wait_info = {staging_buffer_entry.buffer.MapAsync(
    wgpu::MapMode::Read, 0, wgpu::kWholeMapSize,
    wgpu::CallbackMode::WaitAnyOnly,
    [](wgpu::MapAsyncStatus status, wgpu::StringView, bool* success) {
      *success = status == wgpu::MapAsyncStatus::Success;
    },
    &success)};

The function then blocks on a synchronous wait using device.GetAdapter().GetInstance().WaitAny (line 346). If this wait returns a non-success status (e.g., TimedOut or InstanceLost), which can be induced by a compromised renderer causing a GPU hang (TDR), the function returns early at line 349.

At this point, the stack unwinds and the success variable is destroyed. However, because the callback used wgpu::CallbackMode::WaitAnyOnly, it remains tracked by Dawn’s EventManager until it is either completed or the EventManager itself is destroyed.

When the GPU channel or WebGPU connection is closed, the wgpu::Instance is destroyed, invoking the EventManager destructor (third_party/dawn/src/dawn/native/EventManager.cpp). The destructor iterates through all pending events and executes their callbacks with an EventCompletionType::Shutdown status (line 252). This triggers the dangling MapAsync callback, which performs a 1-byte write (0x00) to the now-invalid stack address.

Potential Attack Steps

  1. A compromised renderer process creates or identifies a SharedImage that requires the DawnCopyStrategy fallback path (e.g., a GLES-backed image on a Dawn Vulkan backend).
  2. The renderer triggers an operation that invokes DawnCopyStrategy::CopyFromTextureToBacking, such as EndAccess on a DawnFallbackImageRepresentation.
  3. The renderer simultaneously induces a GPU device loss (e.g., via a long-running shader) to force the WaitAny call in the GPU process to return early.
  4. The renderer closes its connection to the GPU process, triggering the EventManager destruction and the resulting stack corruption.

On platforms like Android, where the GPU process may have limited sandboxing compared to the Renderer, this primitive represents a significant security risk.

Suggested Fix

Avoid passing pointers to stack-local variables to asynchronous callbacks. The synchronization should be refactored to ensure that the lifetime of the userdata is managed correctly (e.g., using a heap-allocated state object) or that the callback is explicitly cancelled/completed before the stack frame is destroyed.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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.

View on issue tracker