CVE-2026-9111
Overview
Background
- `SharedScreenCastStreamPrivate`
- the WebRTC PipeWire/Wayland desktop-capture backend that receives frames and cursor updates from the compositor and hands them to consumers.
- `mouse_cursor_` / `mouse_cursor_position_`
- a
std::unique_ptr<MouseCursor>and aDesktopVectorholding the most recent cursor bitmap and position, updated when a PipeWire cursor-metadata event arrives. - `CaptureCursor` / `CaptureCursorPosition`
- accessors called from the capture thread that read
mouse_cursor_andmouse_cursor_position_. - `latest_frame_lock_`
- a
Mutex(withRTC_GUARDED_BYannotations) intended to serialize access to the latest-frame/cursor state across the PipeWire thread and the capture thread.
Root Cause Analysis
The cursor fields mouse_cursor_ and mouse_cursor_position_ were written on the PipeWire callback thread — mouse_cursor_ = std::make_unique<MouseCursor>(...), mouse_cursor_position_.set(...), and the “invalid cursor” set(-1,-1) path — without holding any lock, while the capture thread read the same fields in CaptureCursor and CaptureCursorPosition, also without locking. This is an unsynchronized data race on shared state: in particular, the PipeWire thread’s mouse_cursor_ = std::make_unique<MouseCursor>(...) destroys the previously held MouseCursor while the capture thread may concurrently be dereferencing the old pointer read from mouse_cursor_, producing a use-after-free.
The fix adds RTC_GUARDED_BY(&latest_frame_lock_) to both members and takes MutexLock latest_frame_lock(&latest_frame_lock_) on every reader (CaptureCursor, CaptureCursorPosition) and every writer (the two cursor-update blocks and the invalid-cursor branch), so the read of the pointer/position and its replacement/destruction are mutually exclusive and no reader can observe a freed MouseCursor.
mouse_cursor_/mouse_cursor_position_ with no lock, so a write that frees the old MouseCursor could race a concurrent read; the fix places every read and write under latest_frame_lock_.Attack Path
- Start a PipeWire desktop capture
A capture session runs with the PipeWire callback thread delivering cursor-metadata updates and a separate capture thread pulling cursor data via
CaptureCursor/CaptureCursorPosition. - Drive rapid cursor updates
Frequent cursor-shape/position changes cause the PipeWire thread to repeatedly execute
mouse_cursor_ = std::make_unique<MouseCursor>(...), freeing the previously heldMouseCursoreach time. - Race a concurrent read
The capture thread reads
mouse_cursor_without synchronization at the moment the PipeWire thread replaces and destroys it, so the reader holds/uses a danglingMouseCursorpointer — a use-after-free (data race). - Corruption or crash Dereferencing the freed cursor object yields undefined behavior; any escalation to a controllable primitive is (inferred) and not shown by the patch.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmodules/desktop_capture/linux/wayland/shared_screencast_stream.cc |
modified |
Files Changed
modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
Audit Directions
- Cross-thread member access without guardsLook for capturer/stream state read on the capture thread and written on the PipeWire/compositor callback thread that lacks
RTC_GUARDED_BYand lock acquisition. - Owning-pointer replacement under raceAudit
std::unique_ptrreassignments (= std::make_unique<...>) on shared members, where the implicit destruction of the old object can race a concurrent reader holding the raw pointer. - Partial locking of a field groupCheck that all accessors of a related field set (e.g.
mouse_cursor_andmouse_cursor_position_) consistently take the same mutex, since annotating only some access sites leaves the race open.
Patch
From 90b22181ec922900e26de71463b8e0ed47a5c2dd Mon Sep 17 00:00:00 2001
From: mark a. foltz <mfoltz@chromium.org>
Date: Mon, 20 Apr 2026 15:16:47 -0700
Subject: [PATCH] [Pipewire] Fix mouse cursor data race.
This addresses a potential data race when the mouse cursor is updated
on the Pipewire thread and read by the capture thread.
Bug: chromium:504551032
Change-Id: I1afb9febe8bb41ce62c63872e2cb5514e908dd38
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/465440
Reviewed-by: Tomas Gunnarsson <tommi@webrtc.org>
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Auto-Submit: Mark Foltz <mfoltz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#47502}
---
diff --git a/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc b/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
index d611ca0..1aa8be5 100644
--- a/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
+++ b/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
@@ -128,8 +128,9 @@
Mutex latest_frame_lock_ RTC_ACQUIRED_AFTER(queue_lock_);
SharedDesktopFrame* latest_available_frame_
RTC_GUARDED_BY(&latest_frame_lock_) = nullptr;
- std::unique_ptr<MouseCursor> mouse_cursor_;
- DesktopVector mouse_cursor_position_ = DesktopVector(-1, -1);
+ std::unique_ptr<MouseCursor> mouse_cursor_ RTC_GUARDED_BY(&latest_frame_lock_);
+ DesktopVector mouse_cursor_position_ RTC_GUARDED_BY(&latest_frame_lock_) =
+ DesktopVector(-1, -1);
int64_t modifier_;
std::unique_ptr<EglDmaBuf> egl_dmabuf_;
@@ -695,6 +696,7 @@
}
std::unique_ptr<MouseCursor> SharedScreenCastStreamPrivate::CaptureCursor() {
+ MutexLock latest_frame_lock(&latest_frame_lock_);
if (!mouse_cursor_) {
return nullptr;
}
@@ -703,6 +705,7 @@
}
DesktopVector SharedScreenCastStreamPrivate::CaptureCursorPosition() {
+ MutexLock latest_frame_lock(&latest_frame_lock_);
return mouse_cursor_position_;
}
@@ -774,20 +777,28 @@
mouse_frame->CopyPixelsFrom(
bitmap_data, bitmap->stride,
DesktopRect::MakeWH(bitmap->size.width, bitmap->size.height));
- mouse_cursor_ = std::make_unique<MouseCursor>(
- mouse_frame, DesktopVector(cursor->hotspot.x, cursor->hotspot.y));
+ {
+ MutexLock latest_frame_lock(&latest_frame_lock_);
+ mouse_cursor_ = std::make_unique<MouseCursor>(
+ mouse_frame,
+ DesktopVector(cursor->hotspot.x, cursor->hotspot.y));
+ }
if (observer_) {
observer_->OnCursorShapeChanged();
}
}
- mouse_cursor_position_.set(cursor->position.x, cursor->position.y);
+ {
+ MutexLock latest_frame_lock(&latest_frame_lock_);
+ mouse_cursor_position_.set(cursor->position.x, cursor->position.y);
+ }
if (observer_) {
observer_->OnCursorPositionChanged();
}
} else {
// Indicate an invalid cursor
+ MutexLock latest_frame_lock(&latest_frame_lock_);
mouse_cursor_position_.set(-1, -1);
}
}
Original Bug Report
Potential UAF and Double-Free in Wayland Screen Capture due to data race on MouseCursor
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A data race on the std::unique_ptr<MouseCursor> mouse_cursor_ member in SharedScreenCastStreamPrivate can occur between the PipeWire thread and the capture thread. This lack of synchronization allows a dangling pointer to be moved to the composer, leading to a potential Use-After-Free read (information leak) and Double-Free in the unsandboxed browser process.
Affected files:
third_party/webrtc/modules/desktop_capture/linux/wayland/shared_screencast_stream.ccthird_party/webrtc/modules/desktop_capture/linux/wayland/mouse_cursor_monitor_pipewire.ccthird_party/webrtc/modules/desktop_capture/desktop_and_cursor_composer.cc
Estimated timestamp from git blame: 2024-06-27
Root Cause
A thread safety issue exists in SharedScreenCastStreamPrivate within the WebRTC desktop capture module for Linux/Wayland (third_party/webrtc/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc). The member mouse_cursor_ (a std::unique_ptr<MouseCursor>) is accessed and modified without synchronization from two different threads:
- Writer (PipeWire thread): The background
pipewire-main-loopthread invokesProcessBuffer(), which creates a newMouseCursorobject and assigns it tomouse_cursor_(line 777). This assignment is performed without holding any mutex. - Reader (Capture thread): The
desktopCaptureThread(an unsandboxed thread in the browser process) callsCaptureCursor(), which executesreturn std::move(mouse_cursor_);(line 702). This is also performed without synchronization.
Vulnerability Details
Because std::unique_ptr assignment and move operations are not atomic, concurrent execution leads to a data race on the internal raw pointer.
- The Reader thread executes
std::move(mouse_cursor_)and reads the raw pointer just before the Writer thread completes its assignment of a new cursor. - The Writer thread completes its assignment, which triggers the deletion of the old
MouseCursorobject. - The Reader thread completes its move, successfully constructing a new
unique_ptrthat wraps the now-freed pointer.
This leads to two distinct impacts:
- Use-After-Free (Information Leak): The dangling pointer is passed to
DesktopAndCursorComposer::OnMouseCursor(). During frame composition,DesktopFrameWithCursorcallsAlphaBlend, which reads from the freedMouseCursor’s image data and copies it into the captured video frame. This blends browser-process heap memory into the video stream sent back to the web page. - Double-Free (Potential RCE): The
DesktopAndCursorComposerstores the dangling pointer in its ownstd::unique_ptr<MouseCursor> cursor_. When a new cursor is received, or upon destruction,cursor_.reset()is called, which attempts todeletethe already-freed pointer a second time. BecauseMouseCursormanages aDesktopFramewith a virtual destructor, this provides a primitive for control-flow hijacking in the unsandboxed browser process.
Potential Attack Steps
(Note: These are suggested steps based on code analysis; a working PoC has not been executed.)
- An attacker tricks a victim on Linux (Wayland/PipeWire) into visiting a malicious site.
- The site calls
navigator.mediaDevices.getDisplayMedia({ video: true }). - Once the user grants permission, the browser process begins capturing the screen using the vulnerable Wayland module.
- The malicious page rapidly alters CSS cursor shapes or prompts the user to move the mouse rapidly while capturing frames at a high rate. This maximizes the execution frequency of both threads to win the race condition.
Suggested Fix
Access to mouse_cursor_ and mouse_cursor_position_ must be synchronized. Since queue_lock_ and latest_frame_lock_ already exist in SharedScreenCastStreamPrivate, one of these mutexes (or a new dedicated mutex) should be held during both the assignment in ProcessBuffer() and the read/move in CaptureCursor() and CaptureCursorPosition().
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.