Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactStack buffer overflow in GPU
DescriptionStack buffer overflow in GPU
ComponentGPU
Bug ClassOOB
Tracker513946753
Fix commitc6735c423fa5 (chromium/src) +70/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

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
From c6735c423fa5b4e02fc7f7325e10b1a84278cd2f Mon Sep 17 00:00:00 2001
From: vikas soni <vikassoni@chromium.org>
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 <vikassoni@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Auto-Submit: vikas soni <vikassoni@chromium.org>
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 vm...@google.com

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