From cd31cdf3651d811766faaf4d215c6b4e35da39ed Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Fri, 10 Apr 2026 07:28:36 +0200 Subject: [PATCH 1/3] fix(linux/xdgportal): Improve pipewire stream selection and sorting This adds a few safety checks to stream selection for displays and re-adds the missing stream sorting that seems to have gotten lost during the commit restructuring in PR#4931 --- src/platform/linux/portalgrab.cpp | 32 ++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index 9bc5bfda1bf..f2e03b1b09d 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -173,15 +173,15 @@ namespace portal { }; struct pipewire_streaminfo_t { - int pipewire_node; - int width; - int height; - int pos_x; - int pos_y; + int pipewire_node = -1; + int width = 0; + int height = 0; + int pos_x = 0; + int pos_y = 0; std::string monitor_name; std::string to_display_name() { - if (monitor_name.length() > 0) { + if (!monitor_name.empty()) { return std::format("n{}", monitor_name); } return std::format("p{},{},{},{}", pos_x, pos_y, width, height); @@ -655,12 +655,20 @@ namespace portal { while (g_variant_iter_next(&iter, "(u@a{sv})", &out_pipewire_node, &value)) { int out_width; int out_height; - g_variant_lookup(value, "size", "(ii)", &out_width, &out_height, nullptr); + bool result = g_variant_lookup(value, "size", "(ii)", &out_width, &out_height, nullptr); + if (!result) { + BOOST_LOG(warning) << "[portalgrab] Ignoring stream without proper resolution on pipewire node "sv << out_pipewire_node; + continue; + } int out_pos_x; int out_pos_y; - g_variant_lookup(value, "position", "(ii)", &out_pos_x, &out_pos_y, nullptr); - + result = g_variant_lookup(value, "position", "(ii)", &out_pos_x, &out_pos_y, nullptr); + if (!result) { + BOOST_LOG(warning) << "[portalgrab] Falling back to position 0x0 for stream with resolution "sv << out_width << "x"sv << out_height << "on pipewire node "sv << out_pipewire_node; + out_pos_x = 0; + out_pos_y = 0; + } auto stream = pipewire_streaminfo_t {out_pipewire_node, out_width, out_height, out_pos_x, out_pos_y}; // Try to match the stream to a monitor_name by position/resolution and update stream info @@ -674,6 +682,12 @@ namespace portal { out_pipewire_streams.emplace_back(stream); } + // The portal call returns the streams sorted by out_pipewire_node which can shuffle displays around, so + // we have to sort pipewire streams by position here to be consistent + std::ranges::sort(out_pipewire_streams, [](const auto &a, const auto &b) { + return a.pos_x < b.pos_x || a.pos_y < b.pos_y; + }); + return 0; } From 71c055c876cb5099f3ad929e7d579924bb31caf6 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Fri, 10 Apr 2026 08:45:01 +0200 Subject: [PATCH 2/3] fix(linux/xdgportal): Drop CAP_SYS_ADMIN when initializing the first portal display Introducing multi-monitor support required connecting to the portal early for display name enumeration. This will finalize_portal_security and drop CAP_SYS_ADMIN before KMS encoder probing, breaking it. To work around the issue finalize_portal_security on first display init and return a dummy display name until portal display encoder probing starts (dropping CAP_SYS_ADMIN as before PR#4931). Display re-enumeration is done on client connect so no functionality is lost. --- src/platform/linux/portalgrab.cpp | 77 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index f2e03b1b09d..db2f5015b43 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -287,44 +287,12 @@ namespace portal { return 0; } - void finalize_portal_security() { -#if !defined(__FreeBSD__) - BOOST_LOG(debug) << "[portalgrab] Finalizing Portal security: dropping capabilities and resetting dumpable"sv; - - cap_t caps = cap_get_proc(); - if (!caps) { - BOOST_LOG(error) << "[portalgrab] Failed to get process capabilities"sv; - return; - } - - std::array effective_list {CAP_SYS_ADMIN, CAP_SYS_NICE}; - std::array permitted_list {CAP_SYS_ADMIN, CAP_SYS_NICE}; - - cap_set_flag(caps, CAP_EFFECTIVE, effective_list.size(), effective_list.data(), CAP_CLEAR); - cap_set_flag(caps, CAP_PERMITTED, permitted_list.size(), permitted_list.data(), CAP_CLEAR); - - if (cap_set_proc(caps) != 0) { - BOOST_LOG(error) << "[portalgrab] Failed to prune capabilities: "sv << std::strerror(errno); - } - cap_free(caps); - - // Reset dumpable AFTER the caps have been pruned to ensure the Portal can - // access /proc/pid/root. - if (prctl(PR_SET_DUMPABLE, 1) != 0) { - BOOST_LOG(error) << "[portalgrab] Failed to set PR_SET_DUMPABLE: "sv << std::strerror(errno); - } -#endif - } - int connect_to_portal() { g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, FALSE); g_autofree gchar *session_path = nullptr; g_autofree gchar *session_token = nullptr; create_session_path(conn, nullptr, &session_token); - // Drop CAP_SYS_ADMIN and set DUMPABLE flag to allow XDG /root access - finalize_portal_security(); - // Try combined RemoteDesktop + ScreenCast session first bool use_screencast_only = !try_remote_desktop_session(loop, &session_path, session_token); @@ -779,6 +747,40 @@ namespace portal { maxframerate_failed_ = true; } + bool is_portal_secured() { + return is_portal_secured_; + } + + void finalize_portal_security() { +#if !defined(__FreeBSD__) + BOOST_LOG(debug) << "[portalgrab] Finalizing Portal security: dropping capabilities and resetting dumpable"sv; + + cap_t caps = cap_get_proc(); + if (!caps) { + BOOST_LOG(error) << "[portalgrab] Failed to get process capabilities"sv; + return; + } + + std::array effective_list {CAP_SYS_ADMIN, CAP_SYS_NICE}; + std::array permitted_list {CAP_SYS_ADMIN, CAP_SYS_NICE}; + + cap_set_flag(caps, CAP_EFFECTIVE, effective_list.size(), effective_list.data(), CAP_CLEAR); + cap_set_flag(caps, CAP_PERMITTED, permitted_list.size(), permitted_list.data(), CAP_CLEAR); + + if (cap_set_proc(caps) != 0) { + BOOST_LOG(error) << "[portalgrab] Failed to prune capabilities: "sv << std::strerror(errno); + } + cap_free(caps); + + // Reset dumpable AFTER the caps have been pruned to ensure the Portal can + // access /proc/pid/root. + if (prctl(PR_SET_DUMPABLE, 1) != 0) { + BOOST_LOG(error) << "[portalgrab] Failed to set PR_SET_DUMPABLE: "sv << std::strerror(errno); + } +#endif + is_portal_secured_ = true; + } + private: session_cache_t() = default; @@ -787,6 +789,7 @@ namespace portal { session_cache_t &operator=(const session_cache_t &) = delete; bool maxframerate_failed_ = false; + bool is_portal_secured_ = false; }; session_cache_t &session_cache_t::instance() { @@ -1690,6 +1693,9 @@ namespace platf { return nullptr; } + // Drop CAP_SYS_ADMIN and set DUMPABLE flag to allow XDG /root access + portal::session_cache_t::instance().finalize_portal_security(); + auto portal = std::make_shared(); if (portal->init(hwdevice_type, display_name, config)) { return nullptr; @@ -1709,6 +1715,13 @@ namespace platf { pw_init(nullptr, nullptr); + if (!portal::session_cache_t::instance().is_portal_secured()) { + // We're still in the probing phase of Sunshine startup. Dropping portal security early will break KMS. + // Just return a dummy screen for now. Display re-enumeration after encoder probing will yield full result. + display_names.emplace_back("init"); + return display_names; + } + if (dbus->connect_to_portal() < 0) { BOOST_LOG(warning) << "[portalgrab] Failed to connect to portal. Cannot enumerate displays, returning empty list."; return {}; From 1b8878635958c25ad57578af3d70d5adfae2d755 Mon Sep 17 00:00:00 2001 From: Kishi85 Date: Fri, 10 Apr 2026 10:48:05 +0200 Subject: [PATCH 3/3] refactor(linux/xdgportal): Rename session_cache_t to runtime_t Since the singleton is now just storing persistent runtime information for portalgrab, reflect this in the type name --- src/platform/linux/portalgrab.cpp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/platform/linux/portalgrab.cpp b/src/platform/linux/portalgrab.cpp index db2f5015b43..91b64449df5 100644 --- a/src/platform/linux/portalgrab.cpp +++ b/src/platform/linux/portalgrab.cpp @@ -72,7 +72,7 @@ using namespace std::literals; namespace portal { // Forward declarations - class session_cache_t; + class runtime_t; class restore_token_t { public: @@ -732,12 +732,12 @@ namespace portal { }; /** - * @brief Singleton cache for persistent portalgrab session data. + * @brief Singleton for portalgrab stuff persistent during an application run. * */ - class session_cache_t { + class runtime_t { public: - static session_cache_t &instance(); + static runtime_t &instance(); bool is_maxframerate_failed() const { return maxframerate_failed_; @@ -782,19 +782,19 @@ namespace portal { } private: - session_cache_t() = default; + runtime_t() = default; // Prevent copying - session_cache_t(const session_cache_t &) = delete; - session_cache_t &operator=(const session_cache_t &) = delete; + runtime_t(const runtime_t &) = delete; + runtime_t &operator=(const runtime_t &) = delete; bool maxframerate_failed_ = false; bool is_portal_secured_ = false; }; - session_cache_t &session_cache_t::instance() { - alignas(session_cache_t) static std::array storage; - static auto instance_ = new (storage.data()) session_cache_t(); + runtime_t &runtime_t::instance() { + alignas(runtime_t) static std::array storage; + static auto instance_ = new (storage.data()) runtime_t(); return *instance_; } @@ -1067,7 +1067,7 @@ namespace portal { spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&sizes[0], &sizes[1], &sizes[2]), 0); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerates[0]), 0); - if (!session_cache_t::instance().is_maxframerate_failed()) { + if (!runtime_t::instance().is_maxframerate_failed()) { spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&framerates[0], &framerates[1], &framerates[2]), 0); } @@ -1120,9 +1120,9 @@ namespace portal { } break; case PW_STREAM_STATE_ERROR: - if (old != PW_STREAM_STATE_STREAMING && !session_cache_t::instance().is_maxframerate_failed()) { + if (old != PW_STREAM_STATE_STREAMING && !runtime_t::instance().is_maxframerate_failed()) { BOOST_LOG(warning) << "[portalgrab] Negotiation failed, will retry without maxFramerate"sv; - session_cache_t::instance().set_maxframerate_failed(); + runtime_t::instance().set_maxframerate_failed(); } [[fallthrough]]; case PW_STREAM_STATE_UNCONNECTED: @@ -1694,7 +1694,7 @@ namespace platf { } // Drop CAP_SYS_ADMIN and set DUMPABLE flag to allow XDG /root access - portal::session_cache_t::instance().finalize_portal_security(); + portal::runtime_t::instance().finalize_portal_security(); auto portal = std::make_shared(); if (portal->init(hwdevice_type, display_name, config)) { @@ -1715,7 +1715,7 @@ namespace platf { pw_init(nullptr, nullptr); - if (!portal::session_cache_t::instance().is_portal_secured()) { + if (!portal::runtime_t::instance().is_portal_secured()) { // We're still in the probing phase of Sunshine startup. Dropping portal security early will break KMS. // Just return a dummy screen for now. Display re-enumeration after encoder probing will yield full result. display_names.emplace_back("init");