CVE-2026-78913
Overview
Background
- Chromoting (Chrome Remote Desktop host)
- the
remoting/host process that captures the local desktop and audio on the host machine and streams them to a remote client; the changed files sit underremoting/host/linux/, so this is the Linux host only. - PipeWire stream
- a
pw_streamobject that the host’spipewire_audio_capturer.cc(audio capture) andpipewire_audio_injector.cc(audio injection) use to exchange audio buffers with the PipeWire daemon, delivering data through a process callback (HandleStreamProcess()per the commit message). - `PW_STREAM_FLAG_RT_PROCESS`
- a stream flag that makes PipeWire run the process callback on its separate real-time data thread instead of the
pw_thread_loopmain loop, so stopping the main loop withpw_thread_loop_stop()does not stop that callback. - `pw_stream_disconnect()`
- the PipeWire call that disconnects a stream and, per the commit message, drains the real-time data thread so no process callback is still running when it returns.
Root Cause Analysis
The vulnerable path is the teardown of the internal Core object (the commit message names ~Core()) in both the PipeWire audio capturer and the PipeWire audio injector. Before the patch, the destructor called pw_->pw_thread_loop_stop(pw_main_loop_.get()), took a ScopedThreadLoopLock, and then reset pw_stream_, pw_core_, pw_context_ (and pw_registry_ in the injector) one after another. Because the streams are connected with PW_STREAM_FLAG_RT_PROCESS, the process callback runs on the real-time data thread. Neither stopping the main loop nor holding the thread-loop lock keeps that thread out, so a callback that was already running could keep using Core state while it was being destroyed. The commit message also notes that std::unique_ptr clears its stored pointer before it calls the deleter, so an in-flight HandleStreamProcess() could see pw_stream_ as null mid-teardown. The only thing that drained the data thread was the disconnect hidden inside pw_stream_destroy(), and nothing guaranteed it ran before the other members were freed.
The fix calls pw_->pw_stream_disconnect(pw_stream_.get()) while the lock is held and before pw_stream_ = nullptr, so the data thread is quiescent before any Core member is destroyed. It also adds pw_stream_disconnect to the dynamically loaded symbol list in BUILD.gn.
PW_STREAM_FLAG_RT_PROCESS the process callback runs on a separate real-time thread that only pw_stream_disconnect() drains. The fix disconnects the stream first, so teardown cannot race with an in-flight callback.Attack Path
- Linux host with PipeWire audio
The Chrome Remote Desktop host runs on Linux, and its PipeWire audio capturer or injector has connected a stream with
PW_STREAM_FLAG_RT_PROCESS. The capturer path only runs wherePipewireAudioCapturer::IsSupported()is true. - Audio flowing on the RT thread
While audio is active, PipeWire calls the process callback on its real-time data thread, and that callback touches
Corestate andpw_stream_. - Teardown during a callback
The capturer or injector is destroyed, for example when a remote session ends or the audio pipeline is restarted (inferred trigger; the regression test simply does
Start()and then immediatelyreset()).~Core()stops only the main loop and starts resetting members. - Use after free
A process callback still running on the data thread reads a cleared
pw_stream_or otherCoremembers that are already being freed. This is a timing race, not a deterministic trigger (inferred).
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/host/linux/pipewire_audio_capturer.cc |
modified | |
ifremoting/host/linux/pipewire_audio_injector.cc |
modified | |
TESTremoting/host/linux/pipewire_audio_capturer_unittest.cc |
modified | |
forremoting/host/linux/pipewire_audio_capturer_unittest.cc |
modified | |
BindRepeatingremoting/host/linux/pipewire_audio_capturer_unittest.cc |
modified |
Files Changed
remoting/host/linux/BUILD.gnremoting/host/linux/pipewire_audio_capturer.ccremoting/host/linux/pipewire_audio_injector.ccremoting/host/linux/pipewire_audio_capturer_unittest.cc
Audit Directions
- Thread-loop stop is not a full quiesceLook at other PipeWire users in
remoting/and elsewhere in Chromium, such as screen-capture code, that rely onpw_thread_loop_stop()or a thread-loop lock while their streams usePW_STREAM_FLAG_RT_PROCESS. Check that they disconnect or destroy the stream before freeing any state the callback uses. - `unique_ptr` reset while callbacks are liveFind destructors that reset
std::unique_ptrmembers with custom deleters (pw_stream_destroy,pw_core_disconnect, etc.) while a foreign-thread callback may still read the same member. The pointer is null before the deleter runs, and members are destroyed in an order the callback cannot see. - Implicit ordering inside third-party teardownAudit native-library wrappers (PipeWire, PulseAudio, ALSA, D-Bus) whose safety depends on a side effect inside a destroy call. Prefer an explicit disconnect or unregister step, done before any shared state is torn down.
Patch
From 61eff806e9ea2dc86dc96db753cfd23a51175dd2 Mon Sep 17 00:00:00 2001 From: Yuwei Huang <[email protected]> Date: Tue, 07 Jul 2026 10:31:59 -0700 Subject: [PATCH] [remoting][linux] Disconnect PipeWire stream before reset The PipeWire audio capturer and injector connect their streams with PW_STREAM_FLAG_RT_PROCESS, so the process callback runs on the real-time data thread rather than the main thread loop. pw_thread_loop_stop() only stops the latter; the data thread is drained when the stream is disconnected. Explicitly call pw_stream_disconnect() before resetting the stream unique_ptr in ~Core(). std::unique_ptr clears its stored pointer before invoking the deleter, so resetting alone could let HandleStreamProcess() observe a null stream while a callback is still in flight, and relies on the implicit disconnect inside pw_stream_destroy() for ordering. Disconnecting first ensures the data thread has drained before any Core state is torn down. Also adds a StartAndDestroy unit test that exercises the lifecycle on systems where libpipewire is loadable. Bug: 523716748 Change-Id: I7ea74d6f1a11a1b33bf5bc96df02cb9e8cf51711 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8048773 Auto-Submit: Yuwei Huang <[email protected]> Commit-Queue: Lambros Lambrou <[email protected]> Reviewed-by: Lambros Lambrou <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658066} --- diff --git a/remoting/host/linux/pipewire_audio_capturer.cc b/remoting/host/linux/pipewire_audio_capturer.cc index 1282b9f4..27fd38c 100644 --- a/remoting/host/linux/pipewire_audio_capturer.cc +++ b/remoting/host/linux/pipewire_audio_capturer.cc @@ -70,6 +70,12 @@ pw_->pw_thread_loop_stop(pw_main_loop_.get()); { ScopedThreadLoopLock lock(pw_main_loop_.get()); + // Disconnect first to ensure the real-time data thread has drained any + // in-flight process callback before `pw_stream_` is cleared and other + // members of this object are destroyed. + if (pw_stream_) { + pw_->pw_stream_disconnect(pw_stream_.get()); + } pw_stream_ = nullptr; pw_core_ = nullptr; pw_context_ = nullptr; diff --git a/remoting/host/linux/pipewire_audio_injector.cc b/remoting/host/linux/pipewire_audio_injector.cc index 9a24caf0f..bc30e27 100644 --- a/remoting/host/linux/pipewire_audio_injector.cc +++ b/remoting/host/linux/pipewire_audio_injector.cc @@ -135,6 +135,12 @@ pw_->pw_thread_loop_stop(pw_main_loop_.get()); { ScopedThreadLoopLock lock(pw_main_loop_.get()); + // Disconnect first to ensure the real-time data thread has drained any + // in-flight process callback before `pw_stream_` is cleared and other + // members of this object are destroyed. + if (pw_stream_) { + pw_->pw_stream_disconnect(pw_stream_.get()); + } pw_stream_ = nullptr; pw_registry_ = nullptr; pw_core_ = nullptr; diff --git a/remoting/host/linux/pipewire_audio_capturer_unittest.cc b/remoting/host/linux/pipewire_audio_capturer_unittest.cc index 6eb6fbf..268a165 100644 --- a/remoting/host/linux/pipewire_audio_capturer_unittest.cc +++ b/remoting/host/linux/pipewire_audio_capturer_unittest.cc @@ -4,6 +4,11 @@ #include "remoting/host/linux/pipewire_audio_capturer.h" +#include <memory> + +#include "base/functional/bind.h" +#include "base/test/task_environment.h" +#include "remoting/proto/audio.pb.h" #include "testing/gtest/include/gtest/gtest.h" namespace remoting { @@ -19,4 +24,23 @@ } } +// Verifies that a capturer can be started and immediately torn down without +// crashing. The PipeWire stream uses a real-time data thread, so the +// destruction sequence must ensure that thread has drained before stream +// resources owned by the capturer are released. +TEST(PipewireAudioCapturerTest, StartAndDestroy) { + if (!PipewireAudioCapturer::IsSupported()) { + GTEST_SKIP() << "PipeWire is not available in this environment."; + } + + base::test::TaskEnvironment task_environment; + for (int i = 0; i < 5; ++i) { + auto capturer = PipewireAudioCapturer::Create(); + ASSERT_TRUE(capturer); + capturer->Start( + base::BindRepeating([](std::unique_ptr<AudioPacket> packet) {})); + capturer.reset(); + } +} + } // namespace remoting diff --git a/remoting/host/linux/BUILD.gn b/remoting/host/linux/BUILD.gn index dcac2ba..124ad5b 100644 --- a/remoting/host/linux/BUILD.gn +++ b/remoting/host/linux/BUILD.gn @@ -140,6 +140,7 @@ "pw_properties_new", "pw_stream_new", "pw_stream_destroy", + "pw_stream_disconnect", "pw_stream_add_listener", "pw_stream_connect", "pw_stream_state_as_string",
Regression Test / PoC
diff --git a/remoting/host/linux/pipewire_audio_capturer_unittest.cc b/remoting/host/linux/pipewire_audio_capturer_unittest.cc
index 6eb6fbf..268a165 100644
--- a/remoting/host/linux/pipewire_audio_capturer_unittest.cc
+++ b/remoting/host/linux/pipewire_audio_capturer_unittest.cc
@@ -4,6 +4,11 @@
#include "remoting/host/linux/pipewire_audio_capturer.h"
+#include <memory>
+
+#include "base/functional/bind.h"
+#include "base/test/task_environment.h"
+#include "remoting/proto/audio.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace remoting {
@@ -19,4 +24,23 @@
}
}
+// Verifies that a capturer can be started and immediately torn down without
+// crashing. The PipeWire stream uses a real-time data thread, so the
+// destruction sequence must ensure that thread has drained before stream
+// resources owned by the capturer are released.
+TEST(PipewireAudioCapturerTest, StartAndDestroy) {
+ if (!PipewireAudioCapturer::IsSupported()) {
+ GTEST_SKIP() << "PipeWire is not available in this environment.";
+ }
+
+ base::test::TaskEnvironment task_environment;
+ for (int i = 0; i < 5; ++i) {
+ auto capturer = PipewireAudioCapturer::Create();
+ ASSERT_TRUE(capturer);
+ capturer->Start(
+ base::BindRepeating([](std::unique_ptr<AudioPacket> packet) {}));
+ capturer.reset();
+ }
+}
+
} // namespace remoting
Original Bug Report
Potential cross-thread Use-After-Free in Linux CRD host via PipeWire RT thread
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 potential Use-After-Free (UAF) exists in the Linux Chrome Remote Desktop host when terminating audio capture sessions. The PipewireAudioCapturer::Core object is destroyed on the main thread while the PipeWire real-time processing thread may still be executing callbacks. This can lead to Remote Code Execution (RCE) in the unsandboxed host process.
Affected files:
remoting/host/linux/pipewire_audio_injector.ccremoting/host/linux/pipewire_audio_capturer.ccremoting/host/linux/pipewire_utils.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A cross-thread Use-After-Free (UAF) vulnerability has been identified in remoting::PipewireAudioCapturer::Core (and likely PipewireAudioInjector::Core) within the Chrome Remote Desktop host for Linux. The issue arises because the PipeWire real-time (RT) processing thread is not properly synchronized with the destruction of the Core object, allowing audio processing callbacks to execute on freed memory.
Technical Details
When an audio session is initiated, CreatePipewireStream (in remoting/host/linux/pipewire_utils.cc) configures the PipeWire stream using the PW_STREAM_FLAG_RT_PROCESS flag. This flag directs PipeWire to execute the .process callback (HandleStreamProcess) on a dedicated, high-priority real-time thread, separate from the main control loop (pw_main_loop_).
The Core object registers itself as the callback context by passing its this pointer as a raw void* data argument to the C library (pw_stream_add_listener). Because this pointer crosses the C ABI boundary, it is not protected by MiraclePtr (BackupRefPtr).
When the audio session is terminated, Core::~Core() executes on the main thread:
pw_thread_loop_stop(pw_main_loop_.get())is called. This successfully stops the main control thread loop but does not wait for or stop the independent RT data thread.pw_stream_ = nullptr;is executed, invokingpw_stream_destroy(). While this disconnects and destroys the stream structure, it does not synchronously guarantee that in-flight RT thread callbacks have completed.- The
Coreobject completes destruction, and its memory is returned to the allocator.
Because the RT thread may still be executing HandleStreamProcess (or is just about to), it will use the dangling data pointer.
Potential Exploitation Steps
An authenticated attacker could potentially exploit this via the following steps:
- Initiate Session: The attacker initiates an audio capture session, causing the allocation of a
PipewireAudioCapturer::Coreobject. - Trigger Race: The attacker abruptly terminates the session. This starts the main thread teardown sequence.
- Heap Grooming: Immediately after termination, the attacker performs other standard CRD host interactions to reclaim the freed
Coreobject’s memory slot with attacker-controlled bytes. - Member Forgery: The attacker crafts the reclaimed memory to forge specific members:
pw_: Araw_refpointing to the library loader. The attacker points this to a fake structure containing a target function pointer (e.g.,system()).pw_stream_: The stream pointer. The attacker sets this to point to an arbitrary command string (e.g.,"/bin/sh").
- RCE Execution: The RT thread executes
HandleStreamProcess()on the forged object. It reaches the linepw_->pw_stream_dequeue_buffer(pw_stream_.get());. Because this function is annotated withDISABLE_CFI_DLSYM(disabling Control Flow Integrity for indirect calls), the program jumps to the attacker’s fake function pointer with their chosen argument, leading to Remote Code Execution.
Impact
The CRD host on Linux does not utilize a Chromium sandbox. Worker processes typically run under privilege separation (e.g., the network process runs as the _crd_network user, and the desktop process runs as the logged-in user). Successful exploitation provides RCE in this context, compromising the user session or the dedicated network user account.
Suggested Fix
The teardown sequence must ensure that the RT thread has completely finished processing before the Core object is destroyed. Relying on pw_stream_destroy() is insufficient if it does not block the RT thread.
Consider utilizing PipeWire’s thread-loop signaling mechanisms, or temporarily lock the data thread loop (if supported by the PipeWire API used) during destruction to guarantee synchronization. Alternatively, ensure that HandleStreamProcess safely checks validity, though this is difficult without a thread-safe ref-counting mechanism that survives the C ABI boundary.
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
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.