Chrome · Chromoting › Remoting
CVE-2026-113063
Input Validation in Chromoting (Remoting)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ContinueWindowGtkremoting/host/continue_window_linux.cc |
modified | |
ifremoting/host/continue_window_linux.cc |
modified | |
ifremoting/host/disconnect_window_linux.cc |
modified | |
TEST_Fremoting/host/disconnect_window_linux_unittest.cc |
modified |
Files Changed
remoting/host/disconnect_window_linux.ccremoting/host/disconnect_window_linux_unittest.ccremoting/host/continue_window_linux.cc
Patch
From 06ba3f5e40335c1913890b3d8c1e7cf98cb4ca31 Mon Sep 17 00:00:00 2001 From: Jamie Walch <[email protected]> Date: Wed, 23 Sep 2026 13:27:36 -0700 Subject: [PATCH] [remoting] Keep disconnect and continue windows visible over fullscreen apps on Linux X11 On Linux X11, DisconnectWindowGtk and ContinueWindowGtk previously relied only on gtk_window_stick() and gtk_window_set_keep_above() (_NET_WM_STATE_ABOVE). Under EWMH window manager stacking rules, focused fullscreen windows (_NET_WM_STATE_FULLSCREEN) stack above windows in the ABOVE layer. An authenticated remote client could inject an F11 keystroke into the victim's application, hiding the consent indicator and Stop Sharing button while screen capture and input injection continued. This CL: 1. Sets override_redirect on both DisconnectWindowGtk and ContinueWindowGtk before mapping them so that window managers do not constrain them below fullscreen windows. 2. Centers ContinueWindowGtk via gtk_window_set_position. 3. Adds a periodic 1-second re-raise timer to keep the windows raised above any newly created or restacked windows. 4. Adds tests verifying override-redirect and periodic re-raising. Fixed: 562960821 Change-Id: I8311aecd19c40e118473fc0d1e4fd11cc7cbe7f9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8449130 Auto-Submit: Jamie Walch <[email protected]> Reviewed-by: Lambros Lambrou <[email protected]> Commit-Queue: Jamie Walch <[email protected]> Cr-Commit-Position: refs/heads/main@{#1703716} --- diff --git a/remoting/host/continue_window_linux.cc b/remoting/host/continue_window_linux.cc index 0bbe311..f5fbd9e0 100644 --- a/remoting/host/continue_window_linux.cc +++ b/remoting/host/continue_window_linux.cc @@ -13,12 +13,22 @@ #include "base/logging.h" #include "base/memory/raw_ptr_exclusion.h" #include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/timer/timer.h" #include "remoting/base/string_resources.h" #include "ui/base/glib/scoped_gsignal.h" #include "ui/base/l10n/l10n_util.h" namespace remoting { +namespace { + +// Interval for periodic re-raising to ensure the dialog stays above fullscreen +// applications. +constexpr base::TimeDelta kRaiseInterval = base::Seconds(1); + +} // namespace + class ContinueWindowGtk : public ContinueWindow { public: ContinueWindowGtk(); @@ -39,11 +49,17 @@ void OnResponse(GtkDialog*, int); +#if !GTK_CHECK_VERSION(3, 90, 0) + void Raise(); +#endif + raw_ptr<GtkWidget> continue_window_; bool buttons_enabled_ = false; ScopedGSignal signal_; + + base::RepeatingTimer raise_timer_; }; ContinueWindowGtk::ContinueWindowGtk() : continue_window_(nullptr) {} @@ -59,11 +75,18 @@ CreateWindow(); gtk_window_set_urgency_hint(GTK_WINDOW(continue_window_.get()), TRUE); gtk_window_present(GTK_WINDOW(continue_window_.get())); +#if !GTK_CHECK_VERSION(3, 90, 0) + Raise(); + raise_timer_.Start( + FROM_HERE, kRaiseInterval, + base::BindRepeating(&ContinueWindowGtk::Raise, base::Unretained(this))); +#endif } void ContinueWindowGtk::HideUi() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + raise_timer_.Stop(); if (continue_window_) { signal_.Reset(); gtk_widget_destroy(continue_window_.ExtractAsDangling()); @@ -105,6 +128,18 @@ // Set always-on-top, otherwise this window tends to be obscured by the // DisconnectWindow. gtk_window_set_keep_above(GTK_WINDOW(continue_window_.get()), TRUE); +#if !GTK_CHECK_VERSION(3, 90, 0) + gtk_window_set_position(GTK_WINDOW(continue_window_.get()), + GTK_WIN_POS_CENTER); + + // Override-redirect prevents the window manager from stacking fullscreen + // windows above the dialog. + gtk_widget_realize(continue_window_.get()); + GdkWindow* gdk_window = gtk_widget_get_window(continue_window_.get()); + if (gdk_window) { + gdk_window_set_override_redirect(gdk_window, TRUE); + } +#endif signal_ = ScopedGSignal(GTK_DIALOG(continue_window_.get()), "response", base::BindRepeating(&ContinueWindowGtk::OnResponse, @@ -150,6 +185,19 @@ HideUi(); } +#if !GTK_CHECK_VERSION(3, 90, 0) +void ContinueWindowGtk::Raise() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!continue_window_) { + return; + } + GdkWindow* gdk_window = gtk_widget_get_window(continue_window_.get()); + if (gdk_window) { + gdk_window_raise(gdk_window); + } +} +#endif + // static std::unique_ptr<HostWindow> HostWindow::CreateContinueWindow() { return std::make_unique<ContinueWindowGtk>(); diff --git a/remoting/host/disconnect_window_linux.cc b/remoting/host/disconnect_window_linux.cc index 4b33c3c..57d3ecf8 100644 --- a/remoting/host/disconnect_window_linux.cc +++ b/remoting/host/disconnect_window_linux.cc @@ -53,6 +53,10 @@ constexpr char kUpArrow[] = "▲"; constexpr char kDownArrow[] = "▼"; +// Interval for periodic re-raising to ensure the window stays above fullscreen +// applications. +constexpr base::TimeDelta kRaiseInterval = base::Seconds(1); + // GObject data keys for testing and position tracking. constexpr char kCurrentWidthKey[] = "current_width"; constexpr char kCurrentHeightKey[] = "current_height"; @@ -85,6 +89,7 @@ #if !GTK_CHECK_VERSION(3, 90, 0) void OnMonitorsChanged(GdkScreen* screen); gboolean OnWindowState(GtkWidget* window, GdkEventWindowState* event); + void Raise(); #endif // Positions the dialog window based on the current anchor. @@ -103,6 +108,8 @@ int current_width_ = 0; int current_height_ = 0; + base::RepeatingTimer raise_timer_; + std::vector<ScopedGSignal> signals_; }; @@ -292,6 +299,14 @@ } } + // Override-redirect prevents the window manager from stacking fullscreen + // windows above the indicator. + gtk_widget_realize(disconnect_window_.get()); + GdkWindow* gdk_window = gtk_widget_get_window(disconnect_window_.get()); + if (gdk_window) { + gdk_window_set_override_redirect(gdk_window, TRUE); + } + // GTK4 shows windows by default. gtk_widget_show_all(disconnect_window_.get()); #endif @@ -301,6 +316,13 @@ gtk_label_set_text(GTK_LABEL(message_.get()), message_text.c_str()); SetDialogPosition(); gtk_window_present(window); + +#if !GTK_CHECK_VERSION(3, 90, 0) + Raise(); + raise_timer_.Start( + FROM_HERE, kRaiseInterval, + base::BindRepeating(&DisconnectWindowGtk::Raise, base::Unretained(this))); +#endif } void DisconnectWindowGtk::OnClicked(GtkButton* button) { @@ -445,6 +467,17 @@ } return FALSE; } + +void DisconnectWindowGtk::Raise() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (!disconnect_window_) { + return; + } + GdkWindow* gdk_window = gtk_widget_get_window(disconnect_window_.get()); + if (gdk_window) { + gdk_window_raise(gdk_window); + } +} #endif gboolean DisconnectWindowGtk::OnDelete(GtkWidget* window, GdkEvent* event) { diff --git a/remoting/host/disconnect_window_linux_unittest.cc b/remoting/host/disconnect_window_linux_unittest.cc index ec6ca81..d7b9837 100644 --- a/remoting/host/disconnect_window_linux_unittest.cc +++ b/remoting/host/disconnect_window_linux_unittest.cc @@ -20,6 +20,11 @@ #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/resource/resource_bundle.h" #include "ui/base/resource/resource_scale_factor.h" +#include "ui/gfx/x/connection.h" + +#if defined(GDK_WINDOWING_X11) +#include <gdk/gdkx.h> +#endif namespace remoting { @@ -274,7 +279,8 @@ protected: base::test::TaskEnvironment task_environment_{ - base::test::TaskEnvironment::MainThreadType::UI}; + base::test::TaskEnvironment::MainThreadType::UI, + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; TestResourceBundleDelegate resource_delegate_; ui::ResourceBundle resource_bundle_{&resource_delegate_}; ui::ResourceBundle::SharedInstanceSwapperForTesting resource_swapper_{ @@ -611,6 +617,82 @@ window.reset(); } + +TEST_F(DisconnectWindowLinuxTest, WindowIsOverrideRedirectOnX11) { + if (!InitializeGtk()) { + GTEST_SKIP() << "No display available for GTK."; + } + + FakeClientSessionControl session_control(kTestUserJid); + std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow(); + ASSERT_TRUE(window); + window->Start(session_control.GetWeakPtr()); + + GtkWindow* gtk_window = FindDisconnectWindow(); + ASSERT_NE(gtk_window, nullptr); + + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(gtk_window)); + ASSERT_NE(gdk_window, nullptr); + +#if defined(GDK_WINDOWING_X11) + if (GDK_IS_X11_WINDOW(gdk_window)) { + auto* connection = x11::Connection::Get(); + auto xid = static_cast<x11::Window>(gdk_x11_window_get_xid(gdk_window)); + auto attrs = connection->GetWindowAttributes({xid}).Sync(); + ASSERT_TRUE(attrs); + EXPECT_TRUE(attrs->override_redirect); + } +#endif + + // Verify the periodic re-raise timer can fire without issue. + task_environment_.FastForwardBy(base::Seconds(5)); + + window.reset(); +} + +TEST_F(DisconnectWindowLinuxTest, ContinueWindowIsOverrideRedirectOnX11) { + if (!InitializeGtk()) { + GTEST_SKIP() << "No display available for GTK."; + } + + FakeClientSessionControl session_control(kTestUserJid); + std::unique_ptr<HostWindow> window = HostWindow::CreateContinueWindow(); + ASSERT_TRUE(window); + window->Start(session_control.GetWeakPtr()); + + // Fast forward by 30 minutes to trigger ShowUi(). + task_environment_.FastForwardBy(base::Minutes(30)); + + // Find the dialog in toplevels. + GList* toplevels = gtk_window_list_toplevels(); + GtkWindow* continue_dialog = nullptr; + for (GList* iter = toplevels; iter != nullptr; iter = g_list_next(iter)) { + if (GTK_IS_DIALOG(iter->data)) { + continue_dialog = GTK_WINDOW(iter->data); + break; + } + } + g_list_free(toplevels); + ASSERT_NE(continue_dialog, nullptr); + + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(continue_dialog)); + ASSERT_NE(gdk_window, nullptr); + +#if defined(GDK_WINDOWING_X11) + if (GDK_IS_X11_WINDOW(gdk_window)) { + auto* connection = x11::Connection::Get(); + auto xid = static_cast<x11::Window>(gdk_x11_window_get_xid(gdk_window)); + auto attrs = connection->GetWindowAttributes({xid}).Sync(); + ASSERT_TRUE(attrs); + EXPECT_TRUE(attrs->override_redirect); + } +#endif + + // Verify the periodic re-raise timer can fire without issue. + task_environment_.FastForwardBy(base::Seconds(5)); + + window.reset(); +} #endif } // namespace remoting
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/remoting/host/disconnect_window_linux_unittest.cc b/remoting/host/disconnect_window_linux_unittest.cc
index ec6ca81..d7b9837 100644
--- a/remoting/host/disconnect_window_linux_unittest.cc
+++ b/remoting/host/disconnect_window_linux_unittest.cc
@@ -20,6 +20,11 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/resource/resource_scale_factor.h"
+#include "ui/gfx/x/connection.h"
+
+#if defined(GDK_WINDOWING_X11)
+#include <gdk/gdkx.h>
+#endif
namespace remoting {
@@ -274,7 +279,8 @@
protected:
base::test::TaskEnvironment task_environment_{
- base::test::TaskEnvironment::MainThreadType::UI};
+ base::test::TaskEnvironment::MainThreadType::UI,
+ base::test::TaskEnvironment::TimeSource::MOCK_TIME};
TestResourceBundleDelegate resource_delegate_;
ui::ResourceBundle resource_bundle_{&resource_delegate_};
ui::ResourceBundle::SharedInstanceSwapperForTesting resource_swapper_{
@@ -611,6 +617,82 @@
window.reset();
}
+
+TEST_F(DisconnectWindowLinuxTest, WindowIsOverrideRedirectOnX11) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ FakeClientSessionControl session_control(kTestUserJid);
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+
+ GtkWindow* gtk_window = FindDisconnectWindow();
+ ASSERT_NE(gtk_window, nullptr);
+
+ GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(gtk_window));
+ ASSERT_NE(gdk_window, nullptr);
+
+#if defined(GDK_WINDOWING_X11)
+ if (GDK_IS_X11_WINDOW(gdk_window)) {
+ auto* connection = x11::Connection::Get();
+ auto xid = static_cast<x11::Window>(gdk_x11_window_get_xid(gdk_window));
+ auto attrs = connection->GetWindowAttributes({xid}).Sync();
+ ASSERT_TRUE(attrs);
+ EXPECT_TRUE(attrs->override_redirect);
+ }
+#endif
+
+ // Verify the periodic re-raise timer can fire without issue.
+ task_environment_.FastForwardBy(base::Seconds(5));
+
+ window.reset();
+}
+
+TEST_F(DisconnectWindowLinuxTest, ContinueWindowIsOverrideRedirectOnX11) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ FakeClientSessionControl session_control(kTestUserJid);
+ std::unique_ptr<HostWindow> window = HostWindow::CreateContinueWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+
+ // Fast forward by 30 minutes to trigger ShowUi().
+ task_environment_.FastForwardBy(base::Minutes(30));
+
+ // Find the dialog in toplevels.
+ GList* toplevels = gtk_window_list_toplevels();
+ GtkWindow* continue_dialog = nullptr;
+ for (GList* iter = toplevels; iter != nullptr; iter = g_list_next(iter)) {
+ if (GTK_IS_DIALOG(iter->data)) {
+ continue_dialog = GTK_WINDOW(iter->data);
+ break;
+ }
+ }
+ g_list_free(toplevels);
+ ASSERT_NE(continue_dialog, nullptr);
+
+ GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(continue_dialog));
+ ASSERT_NE(gdk_window, nullptr);
+
+#if defined(GDK_WINDOWING_X11)
+ if (GDK_IS_X11_WINDOW(gdk_window)) {
+ auto* connection = x11::Connection::Get();
+ auto xid = static_cast<x11::Window>(gdk_x11_window_get_xid(gdk_window));
+ auto attrs = connection->GetWindowAttributes({xid}).Sync();
+ ASSERT_TRUE(attrs);
+ EXPECT_TRUE(attrs->override_redirect);
+ }
+#endif
+
+ // Verify the periodic re-raise timer can fire without issue.
+ task_environment_.FastForwardBy(base::Seconds(5));
+
+ window.reset();
+}
#endif
} // namespace remoting
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page