Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Bug ClassUAF
Tracker523716748
Fix commit61eff806e9ea (chromium/src) +37/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
remoting/host/linux/pipewire_audio_capturer.cc
modified
TEST
remoting/host/linux/pipewire_audio_capturer_unittest.cc
modified
for
remoting/host/linux/pipewire_audio_capturer_unittest.cc
modified
BindRepeating
remoting/host/linux/pipewire_audio_capturer_unittest.cc
modified
if
remoting/host/linux/pipewire_audio_injector.cc
modified

Files Changed

  • remoting/host/linux/BUILD.gn
  • remoting/host/linux/pipewire_audio_capturer.cc
  • remoting/host/linux/pipewire_audio_capturer_unittest.cc
  • remoting/host/linux/pipewire_audio_injector.cc
From 61eff806e9ea2dc86dc96db753cfd23a51175dd2 Mon Sep 17 00:00:00 2001
From: Yuwei Huang <yuweih@chromium.org>
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 <yuweih@chromium.org>
Commit-Queue: Lambros Lambrou <lambroslambrou@chromium.org>
Reviewed-by: Lambros Lambrou <lambroslambrou@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1658066}
---

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",
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_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/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;
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • remoting/host/linux/pipewire_audio_capturer.cc
  • remoting/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:

  1. 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.
  2. pw_stream_ = nullptr; is executed, invoking pw_stream_destroy(). While this disconnects and destroys the stream structure, it does not synchronously guarantee that in-flight RT thread callbacks have completed.
  3. The Core object 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:

  1. Initiate Session: The attacker initiates an audio capture session, causing the allocation of a PipewireAudioCapturer::Core object.
  2. Trigger Race: The attacker abruptly terminates the session. This starts the main thread teardown sequence.
  3. Heap Grooming: Immediately after termination, the attacker performs other standard CRD host interactions to reclaim the freed Core object’s memory slot with attacker-controlled bytes.
  4. Member Forgery: The attacker crafts the reclaimed memory to forge specific members:
    • pw_: A raw_ref pointing 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").
  5. RCE Execution: The RT thread executes HandleStreamProcess() on the forged object. It reaches the line pw_->pw_stream_dequeue_buffer(pw_stream_.get());. Because this function is annotated with DISABLE_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.

View on issue tracker