Fix use-after-free in ScreenCast and Camera portal D-Bus callbacks

GDBus async callbacks fire on the GLib main thread with a raw pointer
to the portal object. When the portal is destroyed on another thread,
the callback accesses freed memory.

Introduce PortalGuard, a ref-counted mutex-protected wrapper that
outlives the portal. Callbacks lock the guard and check the portal
pointer before use. Stop() locks the same mutex to null the pointer,
blocking until any in-flight callback finishes. Utility functions
now take scoped_refptr<PortalGuard> and manage refs internally.

Bug: chromium:491979284
Bug: chromium:499587071
Change-Id: I80fe20c5c3b6509666554c7cc7454f09cab6c2e4
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/463800
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Jan Grulich <grulja@gmail.com>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Andreas Pehrson <apehrson@mozilla.com>
Cr-Commit-Position: refs/heads/main@{#47444}
diff --git a/modules/desktop_capture/linux/wayland/base_capturer_pipewire.cc b/modules/desktop_capture/linux/wayland/base_capturer_pipewire.cc
index 6cb394f..45537dc 100644
--- a/modules/desktop_capture/linux/wayland/base_capturer_pipewire.cc
+++ b/modules/desktop_capture/linux/wayland/base_capturer_pipewire.cc
@@ -75,6 +75,10 @@
 }
 
 BaseCapturerPipeWire::~BaseCapturerPipeWire() {
+  // Destroy the portal first. Its destructor may block until in-flight
+  // GDBus callbacks finish, and those callbacks access other members
+  // (options_, callback_) through the notifier_ pointer.
+  portal_.reset();
   options_.screencast_stream()->StopScreenCastStream();
 }
 
diff --git a/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.cc b/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.cc
index 3ea47e4..3a71218 100644
--- a/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.cc
+++ b/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.cc
@@ -15,6 +15,7 @@
 #include <cstdint>
 #include <string>
 
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "modules/portal/scoped_glib.h"
 #include "modules/portal/xdg_desktop_portal_utils.h"
@@ -73,7 +74,8 @@
     GVariant* parameters,
     GDBusConnection* connection,
     std::string& session_handle,
-    guint& session_closed_signal_id) {
+    guint& session_closed_signal_id,
+    scoped_refptr<PortalGuard> guard) {
   uint32_t portal_response = 2;
   Scoped<GVariant> response_data;
   g_variant_get(parameters, /*format_string=*/"(u@a{sv})", &portal_response,
@@ -101,7 +103,8 @@
   session_closed_signal_id = g_dbus_connection_signal_subscribe(
       connection, kDesktopBusName, kSessionInterfaceName, /*member=*/"Closed",
       session_handle.c_str(), /*arg0=*/nullptr, G_DBUS_SIGNAL_FLAGS_NONE,
-      session_close_signal_handler, this, /*user_data_free_func=*/nullptr);
+      session_close_signal_handler, guard->AddRefAndGet(),
+      portal_guard_release);
 }
 
 void ScreenCapturePortalInterface::OnStartRequestResult(GDBusProxy* proxy,
diff --git a/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.h b/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.h
index 06401f0..487a1e6 100644
--- a/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.h
+++ b/modules/desktop_capture/linux/wayland/screen_capture_portal_interface.h
@@ -16,6 +16,8 @@
 
 #include <string>
 
+#include "api/scoped_refptr.h"
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "modules/portal/xdg_session_details.h"
 #include "rtc_base/system/rtc_export.h"
@@ -65,7 +67,8 @@
       GVariant* parameters,
       GDBusConnection* connection,
       std::string& session_handle,
-      guint& session_closed_signal_id);
+      guint& session_closed_signal_id,
+      scoped_refptr<PortalGuard> guard);
   // Handles the result of session start request.
   void OnStartRequestResult(GDBusProxy* proxy, GAsyncResult* result);
 };
diff --git a/modules/desktop_capture/linux/wayland/screencast_portal.cc b/modules/desktop_capture/linux/wayland/screencast_portal.cc
index affab4d..e399d38 100644
--- a/modules/desktop_capture/linux/wayland/screencast_portal.cc
+++ b/modules/desktop_capture/linux/wayland/screencast_portal.cc
@@ -21,6 +21,7 @@
 
 #include "modules/desktop_capture/desktop_capture_types.h"
 #include "modules/portal/pipewire_utils.h"
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "modules/portal/scoped_glib.h"
 #include "modules/portal/xdg_desktop_portal_utils.h"
@@ -63,7 +64,6 @@
                        notifier,
                        OnProxyRequested,
                        OnSourcesRequestResponseSignal,
-                       this,
                        prefer_cursor_embedded) {}
 
 ScreenCastPortal::ScreenCastPortal(
@@ -71,7 +71,6 @@
     PortalNotifier* notifier,
     ProxyRequestResponseHandler proxy_request_response_handler,
     SourcesRequestResponseSignalHandler sources_request_response_signal_handler,
-    gpointer user_data,
     bool prefer_cursor_embedded)
     : notifier_(notifier),
       capture_source_type_(ToCaptureSourceType(type)),
@@ -79,14 +78,26 @@
                                           : CursorMode::kMetadata),
       proxy_request_response_handler_(proxy_request_response_handler),
       sources_request_response_signal_handler_(
-          sources_request_response_signal_handler),
-      user_data_(user_data) {}
+          sources_request_response_signal_handler) {}
 
 ScreenCastPortal::~ScreenCastPortal() {
   Stop();
 }
 
 void ScreenCastPortal::Stop() {
+  // Cancel first so that any callback entering g_dbus_proxy_call_finish()
+  // after this point gets G_IO_ERROR_CANCELLED via GTask's check_cancellable.
+  if (cancellable_)
+    g_cancellable_cancel(cancellable_);
+
+  // Lock the guard to wait for any in-flight callback on the GLib main
+  // thread that already passed the _finish() check and is currently using
+  // the portal. Once we acquire the lock, that callback has finished.
+  if (guard_) {
+    MutexLock lock(&guard_->mutex);
+    guard_->portal = nullptr;
+  }
+
   UnsubscribeSignalHandlers();
   TearDownSession(std::move(session_handle_), proxy_, cancellable_,
                   connection_);
@@ -135,8 +146,12 @@
 
 void ScreenCastPortal::Start() {
   cancellable_ = g_cancellable_new();
+
+  guard_ = scoped_refptr<PortalGuard>(new PortalGuard());
+  guard_->portal = this;
+
   RequestSessionProxy(kScreenCastInterfaceName, proxy_request_response_handler_,
-                      cancellable_, this);
+                      cancellable_, guard_);
 }
 
 xdg_portal::SessionDetails ScreenCastPortal::GetSessionDetails() {
@@ -145,8 +160,8 @@
 
 void ScreenCastPortal::OnPortalDone(RequestResponse result) {
   notifier_->OnScreenCastRequestResult(result, pw_stream_node_id_, pw_fd_);
-  if (result != RequestResponse::kSuccess) {
-    Stop();
+  if (result != RequestResponse::kSuccess && cancellable_) {
+    g_cancellable_cancel(cancellable_);
   }
 }
 
@@ -154,7 +169,9 @@
 void ScreenCastPortal::OnProxyRequested(GObject* gobject,
                                         GAsyncResult* result,
                                         gpointer user_data) {
-  static_cast<ScreenCastPortal*>(user_data)->RequestSessionUsingProxy(result);
+  ScopedPortalLock lock(user_data);
+  if (auto* that = static_cast<ScreenCastPortal*>(lock.portal()))
+    that->RequestSessionUsingProxy(result);
 }
 
 void ScreenCastPortal::RequestSession(GDBusProxy* proxy) {
@@ -162,15 +179,16 @@
   connection_ = g_dbus_proxy_get_connection(proxy_);
   SetupSessionRequestHandlers(
       "webrtc", OnSessionRequested, OnSessionRequestResponseSignal, connection_,
-      proxy_, cancellable_, portal_handle_, session_request_signal_id_, this);
+      proxy_, cancellable_, portal_handle_, session_request_signal_id_, guard_);
 }
 
 // static
 void ScreenCastPortal::OnSessionRequested(GDBusProxy* proxy,
                                           GAsyncResult* result,
                                           gpointer user_data) {
-  static_cast<ScreenCastPortal*>(user_data)->OnSessionRequestResult(proxy,
-                                                                    result);
+  ScopedPortalLock lock(user_data);
+  if (auto* that = static_cast<ScreenCastPortal*>(lock.portal()))
+    that->OnSessionRequestResult(proxy, result);
 }
 
 // static
@@ -182,11 +200,14 @@
     const char* signal_name,
     GVariant* parameters,
     gpointer user_data) {
-  ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalSignalLock lock(user_data);
+  auto* that = static_cast<ScreenCastPortal*>(lock.portal());
+  if (!that)
+    return;
+
   that->RegisterSessionClosedSignalHandler(
       OnSessionClosedSignal, parameters, that->connection_,
-      that->session_handle_, that->session_closed_signal_id_);
+      that->session_handle_, that->session_closed_signal_id_, that->guard_);
 
   // Do not continue if we don't get session_handle back. The call above will
   // already notify the capturer there is a failure, but we would still continue
@@ -204,8 +225,10 @@
                                              const char* signal_name,
                                              GVariant* parameters,
                                              gpointer user_data) {
-  ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalSignalLock lock(user_data);
+  auto* that = static_cast<ScreenCastPortal*>(lock.portal());
+  if (!that)
+    return;
 
   RTC_LOG(LS_INFO) << "Received closed signal from session.";
 
@@ -266,23 +289,26 @@
 
   sources_handle_ = PrepareSignalHandle(variant_string.get(), connection_);
   sources_request_signal_id_ = SetupRequestResponseSignal(
-      sources_handle_.c_str(), sources_request_response_signal_handler_,
-      user_data_, connection_);
+      sources_handle_.c_str(), sources_request_response_signal_handler_, guard_,
+      connection_);
 
   RTC_LOG(LS_INFO) << "Requesting sources from the screen cast session.";
   g_dbus_proxy_call(
       proxy_, "SelectSources",
       g_variant_new("(oa{sv})", session_handle_.c_str(), &builder),
       G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, cancellable_,
-      reinterpret_cast<GAsyncReadyCallback>(OnSourcesRequested), this);
+      reinterpret_cast<GAsyncReadyCallback>(OnSourcesRequested),
+      guard_->AddRefAndGet());
 }
 
 // static
 void ScreenCastPortal::OnSourcesRequested(GDBusProxy* proxy,
                                           GAsyncResult* result,
                                           gpointer user_data) {
-  ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalLock lock(user_data);
+  auto* that = static_cast<ScreenCastPortal*>(lock.portal());
+  if (!that)
+    return;
 
   Scoped<GError> error;
   Scoped<GVariant> variant(
@@ -319,8 +345,10 @@
     const char* signal_name,
     GVariant* parameters,
     gpointer user_data) {
-  ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalSignalLock lock(user_data);
+  auto* that = static_cast<ScreenCastPortal*>(lock.portal());
+  if (!that)
+    return;
 
   RTC_LOG(LS_INFO) << "Received sources signal from session.";
 
@@ -339,15 +367,16 @@
 void ScreenCastPortal::StartRequest() {
   StartSessionRequest("webrtc", session_handle_, OnStartRequestResponseSignal,
                       OnStartRequested, proxy_, connection_, cancellable_,
-                      start_request_signal_id_, start_handle_, this);
+                      start_request_signal_id_, start_handle_, guard_);
 }
 
 // static
 void ScreenCastPortal::OnStartRequested(GDBusProxy* proxy,
                                         GAsyncResult* result,
                                         gpointer user_data) {
-  static_cast<ScreenCastPortal*>(user_data)->OnStartRequestResult(proxy,
-                                                                  result);
+  ScopedPortalLock lock(user_data);
+  if (auto* that = static_cast<ScreenCastPortal*>(lock.portal()))
+    that->OnStartRequestResult(proxy, result);
 }
 
 // static
@@ -358,8 +387,10 @@
                                                     const char* signal_name,
                                                     GVariant* parameters,
                                                     gpointer user_data) {
-  ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalSignalLock lock(user_data);
+  auto* that = static_cast<ScreenCastPortal*>(lock.portal());
+  if (!that)
+    return;
 
   RTC_LOG(LS_INFO) << "Start signal received.";
   uint32_t portal_response;
@@ -435,15 +466,17 @@
       g_variant_new("(oa{sv})", session_handle_.c_str(), &builder),
       G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, /*fd_list=*/nullptr, cancellable_,
       reinterpret_cast<GAsyncReadyCallback>(OnOpenPipeWireRemoteRequested),
-      this);
+      guard_->AddRefAndGet());
 }
 
 // static
 void ScreenCastPortal::OnOpenPipeWireRemoteRequested(GDBusProxy* proxy,
                                                      GAsyncResult* result,
                                                      gpointer user_data) {
-  ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalLock lock(user_data);
+  auto* that = static_cast<ScreenCastPortal*>(lock.portal());
+  if (!that)
+    return;
 
   Scoped<GError> error;
   Scoped<GUnixFDList> outlist;
diff --git a/modules/desktop_capture/linux/wayland/screencast_portal.h b/modules/desktop_capture/linux/wayland/screencast_portal.h
index d077756..27b01a4 100644
--- a/modules/desktop_capture/linux/wayland/screencast_portal.h
+++ b/modules/desktop_capture/linux/wayland/screencast_portal.h
@@ -18,9 +18,11 @@
 #include <cstdint>
 #include <string>
 
+#include "api/scoped_refptr.h"
 #include "modules/desktop_capture/desktop_capture_types.h"
 #include "modules/desktop_capture/linux/wayland/screen_capture_portal_interface.h"
 #include "modules/portal/pipewire_utils.h"
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "modules/portal/xdg_session_details.h"
 #include "rtc_base/system/rtc_export.h"
@@ -91,7 +93,6 @@
                    ProxyRequestResponseHandler proxy_request_response_handler,
                    SourcesRequestResponseSignalHandler
                        sources_request_response_signal_handler,
-                   gpointer user_data,
                    // TODO(chromium:1291247): Remove the default option once
                    // downstream has been adjusted.
                    bool prefer_cursor_embedded = false);
@@ -156,7 +157,6 @@
 
   ProxyRequestResponseHandler proxy_request_response_handler_;
   SourcesRequestResponseSignalHandler sources_request_response_signal_handler_;
-  gpointer user_data_;
 
   GDBusConnection* connection_ = nullptr;
   GDBusProxy* proxy_ = nullptr;
@@ -170,6 +170,8 @@
   guint start_request_signal_id_ = 0;
   guint session_closed_signal_id_ = 0;
 
+  scoped_refptr<PortalGuard> guard_;
+
   void UnsubscribeSignalHandlers();
   static void OnProxyRequested(GObject* object,
                                GAsyncResult* result,
diff --git a/modules/portal/BUILD.gn b/modules/portal/BUILD.gn
index de8a81b..52e515c 100644
--- a/modules/portal/BUILD.gn
+++ b/modules/portal/BUILD.gn
@@ -94,6 +94,7 @@
     sources = [
       "pipewire_utils.cc",
       "pipewire_utils.h",
+      "portal_guard.h",
       "portal_request_response.h",
       "scoped_glib.cc",
       "scoped_glib.h",
@@ -109,10 +110,13 @@
     ]
 
     deps = [
+      "../../api:refcountedbase",
+      "../../api:scoped_refptr",
       "../../rtc_base:checks",
       "../../rtc_base:logging",
       "../../rtc_base:sanitizer",
       "../../rtc_base:stringutils",
+      "../../rtc_base/synchronization:mutex",
       "../../rtc_base/system:rtc_export",
       "//third_party/abseil-cpp/absl/strings",
     ]
diff --git a/modules/portal/portal_guard.h b/modules/portal/portal_guard.h
new file mode 100644
index 0000000..6c4d2b7
--- /dev/null
+++ b/modules/portal/portal_guard.h
@@ -0,0 +1,90 @@
+/*
+ *  Copyright 2026 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_PORTAL_PORTAL_GUARD_H_
+#define MODULES_PORTAL_PORTAL_GUARD_H_
+
+#include <glib.h>
+
+#include "api/ref_counted_base.h"
+#include "rtc_base/synchronization/mutex.h"
+
+namespace webrtc {
+
+// Ref-counted guard for safe cross-thread portal access from GDBus callbacks.
+// Callbacks lock the mutex and check the portal pointer before use.
+// Stop() nulls the pointer under the same mutex, waiting for any in-flight
+// callback to finish.
+struct PortalGuard : public RefCountedNonVirtual<PortalGuard> {
+  Mutex mutex;
+  gpointer portal = nullptr;
+
+  gpointer AddRefAndGet() {
+    AddRef();
+    return this;
+  }
+};
+
+inline void portal_guard_release(gpointer data) {
+  static_cast<PortalGuard*>(data)->Release();
+}
+
+// RAII lock for PortalGuard. Use ScopedPortalLock for async callbacks
+// (releases the callback's ref) or ScopedPortalSignalLock for signal
+// callbacks (ref is owned by the subscription).
+enum class RefOwnership {
+  kOwnedByCallback,      // Async: callback owns the ref, release on unlock.
+  kOwnedBySubscription,  // Signal: subscription owns the ref via
+                         // GDestroyNotify.
+};
+
+class ScopedPortalLockBase {
+ public:
+  ScopedPortalLockBase(const ScopedPortalLockBase&) = delete;
+  ScopedPortalLockBase& operator=(const ScopedPortalLockBase&) = delete;
+
+  gpointer portal() const { return guard_->portal; }
+
+ protected:
+  ScopedPortalLockBase(gpointer user_data, RefOwnership ownership)
+      : guard_(static_cast<PortalGuard*>(user_data)), ownership_(ownership) {
+    guard_->mutex.Lock();
+  }
+  ~ScopedPortalLockBase() {
+    guard_->mutex.Unlock();
+    if (ownership_ == RefOwnership::kOwnedByCallback)
+      guard_->Release();
+  }
+
+ private:
+  PortalGuard* guard_;
+  RefOwnership ownership_;
+};
+
+// For async callbacks. Releases the ref on destruction because async
+// callbacks fire exactly once and own their ref.
+class ScopedPortalLock : public ScopedPortalLockBase {
+ public:
+  explicit ScopedPortalLock(gpointer user_data)
+      : ScopedPortalLockBase(user_data, RefOwnership::kOwnedByCallback) {}
+};
+
+// For signal callbacks. Does not release the ref because the signal
+// subscription owns it. The ref is released by portal_guard_release
+// (GDestroyNotify) when the subscription is removed.
+class ScopedPortalSignalLock : public ScopedPortalLockBase {
+ public:
+  explicit ScopedPortalSignalLock(gpointer user_data)
+      : ScopedPortalLockBase(user_data, RefOwnership::kOwnedBySubscription) {}
+};
+
+}  // namespace webrtc
+
+#endif  // MODULES_PORTAL_PORTAL_GUARD_H_
diff --git a/modules/portal/xdg_desktop_portal_utils.cc b/modules/portal/xdg_desktop_portal_utils.cc
index ad4c661..723978c 100644
--- a/modules/portal/xdg_desktop_portal_utils.cc
+++ b/modules/portal/xdg_desktop_portal_utils.cc
@@ -15,6 +15,8 @@
 
 #include "absl/strings/str_replace.h"
 #include "absl/strings/string_view.h"
+#include "api/scoped_refptr.h"
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "modules/portal/scoped_glib.h"
 #include "rtc_base/logging.h"
@@ -73,24 +75,25 @@
 
 uint32_t SetupRequestResponseSignal(absl::string_view object_path,
                                     const GDBusSignalCallback callback,
-                                    gpointer user_data,
+                                    scoped_refptr<PortalGuard> guard,
                                     GDBusConnection* connection) {
   return g_dbus_connection_signal_subscribe(
       connection, kDesktopBusName, kRequestInterfaceName, "Response",
       std::string(object_path).c_str(), /*arg0=*/nullptr,
-      G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, callback, user_data,
-      /*user_data_free_func=*/nullptr);
+      G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, callback, guard->AddRefAndGet(),
+      portal_guard_release);
 }
 
 void RequestSessionProxy(absl::string_view interface_name,
                          const ProxyRequestCallback proxy_request_callback,
                          GCancellable* cancellable,
-                         gpointer user_data) {
+                         scoped_refptr<PortalGuard> guard) {
   g_dbus_proxy_new_for_bus(
       G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, /*info=*/nullptr,
       kDesktopBusName, kDesktopObjectPath, std::string(interface_name).c_str(),
       cancellable,
-      reinterpret_cast<GAsyncReadyCallback>(proxy_request_callback), user_data);
+      reinterpret_cast<GAsyncReadyCallback>(proxy_request_callback),
+      guard->AddRefAndGet());
 }
 
 void SetupSessionRequestHandlers(
@@ -102,7 +105,7 @@
     GCancellable* cancellable,
     std::string& portal_handle,
     guint& session_request_signal_id,
-    gpointer user_data) {
+    scoped_refptr<PortalGuard> guard) {
   GVariantBuilder builder;
   Scoped<char> variant_string;
 
@@ -121,7 +124,7 @@
 
   portal_handle = PrepareSignalHandle(variant_string.get(), connection);
   session_request_signal_id = SetupRequestResponseSignal(
-      portal_handle.c_str(), request_response_signale_handler, user_data,
+      portal_handle.c_str(), request_response_signale_handler, guard,
       connection);
 
   RTC_LOG(LS_INFO) << "Desktop session requested.";
@@ -129,7 +132,7 @@
       proxy, "CreateSession", g_variant_new("(a{sv})", &builder),
       G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, cancellable,
       reinterpret_cast<GAsyncReadyCallback>(session_request_callback),
-      user_data);
+      guard->AddRefAndGet());
 }
 
 void StartSessionRequest(
@@ -142,7 +145,7 @@
     GCancellable* cancellable,
     guint& start_request_signal_id,
     std::string& start_handle,
-    gpointer user_data) {
+    scoped_refptr<PortalGuard> guard) {
   GVariantBuilder builder;
   Scoped<char> variant_string;
 
@@ -155,7 +158,7 @@
 
   start_handle = PrepareSignalHandle(variant_string.get(), connection);
   start_request_signal_id = SetupRequestResponseSignal(
-      start_handle.c_str(), signal_handler, user_data, connection);
+      start_handle.c_str(), signal_handler, guard, connection);
 
   // "Identifier for the application window", this is Wayland, so not "x11:...".
   const char parent_window[] = "";
@@ -167,7 +170,7 @@
                     parent_window, &builder),
       G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, cancellable,
       reinterpret_cast<GAsyncReadyCallback>(session_started_handler),
-      user_data);
+      guard->AddRefAndGet());
 }
 
 void TearDownSession(absl::string_view session_handle,
diff --git a/modules/portal/xdg_desktop_portal_utils.h b/modules/portal/xdg_desktop_portal_utils.h
index feb7da3..ab9ddb3 100644
--- a/modules/portal/xdg_desktop_portal_utils.h
+++ b/modules/portal/xdg_desktop_portal_utils.h
@@ -17,6 +17,8 @@
 #include <string>
 
 #include "absl/strings/string_view.h"
+#include "api/scoped_refptr.h"
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "rtc_base/system/rtc_export.h"
 
@@ -61,19 +63,22 @@
                                            GDBusConnection* connection);
 
 // Sets up the callback to execute when a response signal is received for the
-// given object.
+// given object. Adds a ref to the guard for the signal subscription.
 RTC_EXPORT uint32_t
 SetupRequestResponseSignal(absl::string_view object_path,
                            const GDBusSignalCallback callback,
-                           gpointer user_data,
+                           scoped_refptr<PortalGuard> guard,
                            GDBusConnection* connection);
 
+// Requests a D-Bus proxy. Adds a ref to the guard for the async callback.
 RTC_EXPORT void RequestSessionProxy(
     absl::string_view interface_name,
     const ProxyRequestCallback proxy_request_callback,
     GCancellable* cancellable,
-    gpointer user_data);
+    scoped_refptr<PortalGuard> guard);
 
+// Sets up signal subscription and async call for session creation.
+// Adds refs to the guard for both.
 RTC_EXPORT void SetupSessionRequestHandlers(
     absl::string_view portal_prefix,
     const SessionRequestCallback session_request_callback,
@@ -83,8 +88,10 @@
     GCancellable* cancellable,
     std::string& portal_handle,
     guint& session_request_signal_id,
-    gpointer user_data);
+    scoped_refptr<PortalGuard> guard);
 
+// Starts a portal session. Adds refs to the guard for both signal
+// subscription and async call.
 RTC_EXPORT void StartSessionRequest(
     absl::string_view prefix,
     absl::string_view session_handle,
@@ -95,7 +102,7 @@
     GCancellable* cancellable,
     guint& start_request_signal_id,
     std::string& start_handle,
-    gpointer user_data);
+    scoped_refptr<PortalGuard> guard);
 
 // Tears down the portal session and cleans up related objects.
 RTC_EXPORT void TearDownSession(absl::string_view session_handle,
diff --git a/modules/video_capture/linux/camera_portal.cc b/modules/video_capture/linux/camera_portal.cc
index b6c703f..5a9c88b 100644
--- a/modules/video_capture/linux/camera_portal.cc
+++ b/modules/video_capture/linux/camera_portal.cc
@@ -16,11 +16,12 @@
 #include <memory>
 #include <string>
 
+#include "api/scoped_refptr.h"
 #include "modules/portal/pipewire_utils.h"
+#include "modules/portal/portal_guard.h"
 #include "modules/portal/portal_request_response.h"
 #include "modules/portal/scoped_glib.h"
 #include "modules/portal/xdg_desktop_portal_utils.h"
-#include "rtc_base/checks.h"
 #include "rtc_base/logging.h"
 #include "rtc_base/synchronization/mutex.h"
 #include "rtc_base/thread_annotations.h"
@@ -71,6 +72,7 @@
   GDBusProxy* proxy_ = nullptr;
   GCancellable* cancellable_ = nullptr;
   guint access_request_signal_id_ = 0;
+  scoped_refptr<PortalGuard> guard_;
 };
 
 CameraPortalPrivate::CameraPortalPrivate(CameraPortal::PortalNotifier* notifier)
@@ -82,13 +84,20 @@
     notifier_ = nullptr;
   }
 
+  if (cancellable_)
+    g_cancellable_cancel(cancellable_);
+
+  if (guard_) {
+    MutexLock lock(&guard_->mutex);
+    guard_->portal = nullptr;
+  }
+
   if (access_request_signal_id_) {
     g_dbus_connection_signal_unsubscribe(connection_,
                                          access_request_signal_id_);
     access_request_signal_id_ = 0;
   }
   if (cancellable_) {
-    g_cancellable_cancel(cancellable_);
     g_object_unref(cancellable_);
     cancellable_ = nullptr;
   }
@@ -101,20 +110,24 @@
 
 void CameraPortalPrivate::Start() {
   cancellable_ = g_cancellable_new();
-  Scoped<GError> error;
+  guard_ = scoped_refptr<PortalGuard>(new PortalGuard());
+  guard_->portal = this;
   RequestSessionProxy(kCameraInterfaceName, OnProxyRequested, cancellable_,
-                      this);
+                      guard_);
 }
 
 // static
 void CameraPortalPrivate::OnProxyRequested(GObject* gobject,
                                            GAsyncResult* result,
                                            gpointer user_data) {
-  CameraPortalPrivate* that = static_cast<CameraPortalPrivate*>(user_data);
+  ScopedPortalLock lock(user_data);
+  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
+  if (!that)
+    return;
+
   Scoped<GError> error;
   GDBusProxy* proxy = g_dbus_proxy_new_finish(result, error.receive());
   if (!proxy) {
-    // Ignore the error caused by user cancelling the request via `cancellable_`
     if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
       return;
     RTC_LOG(LS_ERROR) << "Failed to get a proxy for the portal: "
@@ -144,21 +157,23 @@
   access_handle =
       xdg_portal::PrepareSignalHandle(variant_string.get(), connection_);
   access_request_signal_id_ = xdg_portal::SetupRequestResponseSignal(
-      access_handle.c_str(), OnResponseSignalEmitted, this, connection_);
+      access_handle.c_str(), OnResponseSignalEmitted, guard_, connection_);
 
   RTC_LOG(LS_VERBOSE) << "Requesting camera access from the portal.";
   g_dbus_proxy_call(proxy_, "AccessCamera", g_variant_new("(a{sv})", &builder),
                     G_DBUS_CALL_FLAGS_NONE, /*timeout_msec=*/-1, cancellable_,
                     reinterpret_cast<GAsyncReadyCallback>(OnAccessResponse),
-                    this);
+                    guard_->AddRefAndGet());
 }
 
 // static
 void CameraPortalPrivate::OnAccessResponse(GDBusProxy* proxy,
                                            GAsyncResult* result,
                                            gpointer user_data) {
-  CameraPortalPrivate* that = static_cast<CameraPortalPrivate*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalLock lock(user_data);
+  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
+  if (!that)
+    return;
 
   Scoped<GError> error;
   Scoped<GVariant> variant(
@@ -184,8 +199,10 @@
                                                   const char* signal_name,
                                                   GVariant* parameters,
                                                   gpointer user_data) {
-  CameraPortalPrivate* that = static_cast<CameraPortalPrivate*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalSignalLock lock(user_data);
+  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
+  if (!that)
+    return;
 
   uint32_t portal_response;
   g_variant_get(parameters, "(u@a{sv})", &portal_response, nullptr);
@@ -200,17 +217,20 @@
   GVariantBuilder builder;
   g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
 
-  g_dbus_proxy_call(
-      that->proxy_, "OpenPipeWireRemote", g_variant_new("(a{sv})", &builder),
-      G_DBUS_CALL_FLAGS_NONE, /*timeout_msec=*/-1, that->cancellable_,
-      reinterpret_cast<GAsyncReadyCallback>(OnOpenResponse), that);
+  g_dbus_proxy_call(that->proxy_, "OpenPipeWireRemote",
+                    g_variant_new("(a{sv})", &builder), G_DBUS_CALL_FLAGS_NONE,
+                    /*timeout_msec=*/-1, that->cancellable_,
+                    reinterpret_cast<GAsyncReadyCallback>(OnOpenResponse),
+                    that->guard_->AddRefAndGet());
 }
 
 void CameraPortalPrivate::OnOpenResponse(GDBusProxy* proxy,
                                          GAsyncResult* result,
                                          gpointer user_data) {
-  CameraPortalPrivate* that = static_cast<CameraPortalPrivate*>(user_data);
-  RTC_DCHECK(that);
+  ScopedPortalLock lock(user_data);
+  auto* that = static_cast<CameraPortalPrivate*>(lock.portal());
+  if (!that)
+    return;
 
   Scoped<GError> error;
   Scoped<GUnixFDList> outlist;