Medium watch CVSS 8.1 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Sub-componentRemoting
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker523716748
Fix commit61eff806e9ea (chromium/src) +37/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-06-14 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-08-25
Patch gap49 days — fix public in source before the advisory

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 under remoting/host/linux/, so this is the Linux host only.
PipeWire stream
a pw_stream object that the host’s pipewire_audio_capturer.cc (audio capture) and pipewire_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_loop main loop, so stopping the main loop with pw_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.

Key insight
The destructor assumed that stopping the PipeWire main thread loop stopped all callbacks, but with 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

  1. 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 where PipewireAudioCapturer::IsSupported() is true.
  2. 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 Core state and pw_stream_.
  3. 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 immediately reset()). ~Core() stops only the main loop and starts resetting members.
  4. Use after free A process callback still running on the data thread reads a cleared pw_stream_ or other Core members that are already being freed. This is a timing race, not a deterministic trigger (inferred).

Impact Assessment

The bug is a use after free caused by a race in the Chrome Remote Desktop host process on Linux. It needs a PipeWire-backed audio session and the host to tear down the audio capturer or injector while a real-time process callback is in flight. Ordinary web content cannot reach it, and the patch shows nothing about how far a remote peer can influence the timing. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1. Any outcome beyond a crash in the host process, such as memory corruption usable for code execution in the host’s context, is (inferred).

Changed Functions

FunctionChangeNotes
if
remoting/host/linux/pipewire_audio_capturer.cc
modified
if
remoting/host/linux/pipewire_audio_injector.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

Files Changed

  • remoting/host/linux/BUILD.gn
  • remoting/host/linux/pipewire_audio_capturer.cc
  • remoting/host/linux/pipewire_audio_injector.cc
  • remoting/host/linux/pipewire_audio_capturer_unittest.cc

Audit Directions

  • Thread-loop stop is not a full quiesce
    Look at other PipeWire users in remoting/ and elsewhere in Chromium, such as screen-capture code, that rely on pw_thread_loop_stop() or a thread-loop lock while their streams use PW_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 live
    Find destructors that reset std::unique_ptr members 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 teardown
    Audit 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.
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/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",
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