Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Chromoting
DescriptionMemory corruption in Chromoting
ComponentChromoting
Sub-componentRemoting
Bug ClassMemory Corruption
Tracker554557866
Fix commit9af78877f464 (chromium/src) +248/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc
modified
for
remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc
modified
LocalHotkeyInputMonitorX11Test
remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
modified
for
remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
modified
if
remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
modified
TEST_F
remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
modified
if
remoting/host/BUILD.gn
modified

Files Changed

  • remoting/host/BUILD.gn
  • remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc
  • remoting/host/input_monitor/local_hotkey_input_monitor_x11.h
  • remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
From 9af78877f4641a7830d4a01b1cb57248c393e9d1 Mon Sep 17 00:00:00 2001
From: Lambros Lambrou <lambroslambrou@chromium.org>
Date: Fri, 18 Sep 2026 10:43:05 -0700
Subject: [PATCH] [remoting] Ignore XTEST events in LocalHotkeyInputMonitorX11

Filter out synthetic XTEST device events in
LocalHotkeyInputMonitorX11::Core::OnEvent() by checking raw->sourceid
against XTEST slave device IDs queried via XIQueryDevice().

Track pressed Left/Right Control and Alt keys in flat_sets
(pressed_ctrl_keys_ and pressed_alt_keys_) so releasing one side
(e.g. Control_R) does not clear modifier state while the other side
(e.g. Control_L) remains physically held.

Bug: 554557866
Change-Id: I3b2c5b049714b50b35ed657cbbbe8910a92d568f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8428380
Reviewed-by: Yuwei Huang <yuweih@chromium.org>
Commit-Queue: Lambros Lambrou <lambroslambrou@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700934}
---
diff --git a/remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc b/remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc
index 12a3a39..b72fc0f 100644
--- a/remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc
+++ b/remoting/host/input_monitor/local_hotkey_input_monitor_x11.cc
@@ -4,17 +4,14 @@
 
 #include "remoting/host/input_monitor/local_hotkey_input_monitor_x11.h"
 
-#include <sys/select.h>
-#include <unistd.h>
-
 #include <utility>
 
-#include "base/compiler_specific.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/location.h"
 #include "base/logging.h"
 #include "remoting/host/input_monitor/local_input_monitor_x11_common.h"
+#include "ui/gfx/x/future.h"
 #include "ui/gfx/x/keysyms/keysyms.h"
 #include "ui/gfx/x/xinput.h"
 
@@ -73,6 +70,8 @@
     return;
   }
 
+  UpdateXTestDeviceIds();
+
   auto mask = CommonXIEventMaskForRootWindow();
   connection_->xinput().XISelectEvents(
       {connection_->default_root(),
@@ -85,6 +84,30 @@
   connection_->RemoveEventObserver(this);
 }
 
+void LocalHotkeyInputMonitorX11::Core::UpdateXTestDeviceIds() {
+  DCHECK(input_task_runner_->BelongsToCurrentThread());
+  xtest_device_ids_.clear();
+
+  auto reply =
+      connection_->xinput().XIQueryDevice({x11::Input::DeviceId::All}).Sync();
+  if (!reply) {
+    LOG(ERROR) << "XIQueryDevice failed.";
+    return;
+  }
+  for (const auto& info : reply->infos) {
+    if (info.name.ends_with("XTEST keyboard") ||
+        info.name.ends_with("XTEST pointer")) {
+      xtest_device_ids_.insert(info.deviceid);
+    }
+  }
+}
+
+bool LocalHotkeyInputMonitorX11::Core::IsXTestDevice(
+    x11::Input::DeviceId device_id) const {
+  DCHECK(input_task_runner_->BelongsToCurrentThread());
+  return xtest_device_ids_.contains(device_id);
+}
+
 void LocalHotkeyInputMonitorX11::Core::OnEvent(const x11::Event& event) {
   DCHECK(input_task_runner_->BelongsToCurrentThread());
 
@@ -104,15 +127,30 @@
     return;
   }
 
+  // Ignore events injected via XTEST so remote input cannot manipulate local
+  // modifier state or trigger local hotkeys.
+  if (IsXTestDevice(raw->sourceid)) {
+    return;
+  }
+
   const bool down = raw->opcode == x11::Input::RawDeviceEvent::RawKeyPress;
   const auto key_sym =
       connection_->KeycodeToKeysym(static_cast<x11::KeyCode>(raw->detail), 0);
 
   if (key_sym == XK_Control_L || key_sym == XK_Control_R) {
-    ctrl_pressed_ = down;
+    if (down) {
+      pressed_ctrl_keys_.insert(key_sym);
+    } else {
+      pressed_ctrl_keys_.erase(key_sym);
+    }
   } else if (key_sym == XK_Alt_L || key_sym == XK_Alt_R) {
-    alt_pressed_ = down;
-  } else if (key_sym == XK_Escape && down && alt_pressed_ && ctrl_pressed_) {
+    if (down) {
+      pressed_alt_keys_.insert(key_sym);
+    } else {
+      pressed_alt_keys_.erase(key_sym);
+    }
+  } else if (key_sym == XK_Escape && down && !pressed_alt_keys_.empty() &&
+             !pressed_ctrl_keys_.empty()) {
     caller_task_runner_->PostTask(FROM_HERE, std::move(disconnect_callback_));
   }
 }
diff --git a/remoting/host/input_monitor/local_hotkey_input_monitor_x11.h b/remoting/host/input_monitor/local_hotkey_input_monitor_x11.h
index fb1c4dd..22b1487 100644
--- a/remoting/host/input_monitor/local_hotkey_input_monitor_x11.h
+++ b/remoting/host/input_monitor/local_hotkey_input_monitor_x11.h
@@ -5,6 +5,9 @@
 #ifndef REMOTING_HOST_INPUT_MONITOR_LOCAL_HOTKEY_INPUT_MONITOR_X11_H_
 #define REMOTING_HOST_INPUT_MONITOR_LOCAL_HOTKEY_INPUT_MONITOR_X11_H_
 
+#include <cstdint>
+
+#include "base/containers/flat_set.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/ref_counted.h"
 #include "base/sequence_checker.h"
@@ -12,7 +15,7 @@
 #include "remoting/host/input_monitor/local_hotkey_input_monitor.h"
 #include "ui/gfx/x/connection.h"
 #include "ui/gfx/x/event.h"
-#include "ui/gfx/x/future.h"
+#include "ui/gfx/x/xinput.h"
 
 namespace remoting {
 
@@ -45,6 +48,8 @@
     ~Core() override;
     void StartOnInputThread();
     void StopOnInputThread();
+    void UpdateXTestDeviceIds();
+    bool IsXTestDevice(x11::Input::DeviceId device_id) const;
     // x11::EventObserver:
     void OnEvent(const x11::Event& event) override;
     // Task runner on which public methods of this class must be called.
@@ -53,10 +58,11 @@
     scoped_refptr<base::SingleThreadTaskRunner> input_task_runner_;
     // Used to send session disconnect requests.
     base::OnceClosure disconnect_callback_;
-    // True when Alt is pressed.
-    bool alt_pressed_ = false;
-    // True when Ctrl is pressed.
-    bool ctrl_pressed_ = false;
+    // Local Control and Alt keysyms currently held down.
+    base::flat_set<uint32_t> pressed_ctrl_keys_;
+    base::flat_set<uint32_t> pressed_alt_keys_;
+    // Cached XTEST slave device IDs to ignore synthetic events.
+    base::flat_set<x11::Input::DeviceId> xtest_device_ids_;
     raw_ptr<x11::Connection> connection_ = nullptr;
   };
   scoped_refptr<Core> core_;
diff --git a/remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc b/remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
new file mode 100644
index 0000000..a31d2e9
--- /dev/null
+++ b/remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
@@ -0,0 +1,187 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "remoting/host/input_monitor/local_hotkey_input_monitor.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/x/connection.h"
+#include "ui/gfx/x/event.h"
+#include "ui/gfx/x/future.h"
+#include "ui/gfx/x/keysyms/keysyms.h"
+#include "ui/gfx/x/xinput.h"
+#include "ui/gfx/x/xproto.h"
+#include "ui/gfx/x/xtest.h"
+
+namespace remoting {
+
+class LocalHotkeyInputMonitorX11Test : public testing::Test {
+ public:
+  LocalHotkeyInputMonitorX11Test()
+      : task_environment_(base::test::TaskEnvironment::MainThreadType::UI) {}
+
+  void SetUp() override {
+    connection_ = x11::Connection::Get();
+    ASSERT_NE(connection_, nullptr);
+    ASSERT_TRUE(connection_->Ready());
+    ASSERT_TRUE(connection_->xinput().present());
+
+    auto reply =
+        connection_->xinput().XIQueryDevice({x11::Input::DeviceId::All}).Sync();
+    ASSERT_TRUE(reply);
+    for (const auto& info : reply->infos) {
+      if (info.type == x11::Input::DeviceType::MasterKeyboard) {
+        master_keyboard_id_ = info.deviceid;
+      } else if (info.name.ends_with("XTEST keyboard")) {
+        xtest_keyboard_id_ = info.deviceid;
+      }
+    }
+    ASSERT_NE(master_keyboard_id_, x11::Input::DeviceId{});
+    ASSERT_NE(xtest_keyboard_id_, x11::Input::DeviceId{});
+    ASSERT_NE(xtest_keyboard_id_, kLocalKeyboardId);
+
+    auto task_runner = task_environment_.GetMainThreadTaskRunner();
+    monitor_ =
+        LocalHotkeyInputMonitor::Create(task_runner, task_runner, task_runner,
+                                        disconnect_future_.GetCallback());
+    FlushTaskRunner();
+    connection_->Sync();
+    connection_->DispatchAll();
+  }
+
+  void TearDown() override {
+    monitor_.reset();
+    FlushTaskRunner();
+  }
+
+  void DispatchRawKeyEvent(uint32_t keysym,
+                           bool down,
+                           x11::Input::DeviceId source_id) {
+    x11::KeyCode keycode = connection_->KeysymToKeycode(keysym);
+    ASSERT_NE(keycode, x11::KeyCode{});
+
+    x11::Input::RawDeviceEvent raw_event;
+    raw_event.opcode = down ? x11::Input::RawDeviceEvent::RawKeyPress
+                            : x11::Input::RawDeviceEvent::RawKeyRelease;
+    raw_event.deviceid = master_keyboard_id_;
+    raw_event.sourceid = source_id;
+    raw_event.detail = static_cast<uint32_t>(keycode);
+
+    x11::Event event(false, std::move(raw_event));
+    connection_->DispatchEvent(event);
+    FlushTaskRunner();
+  }
+
+  void InjectXTestKeyEvent(uint32_t keysym, bool down) {
+    ASSERT_TRUE(connection_->xtest().present());
+    x11::KeyCode keycode = connection_->KeysymToKeycode(keysym);
+    ASSERT_NE(keycode, x11::KeyCode{});
+
+    uint8_t opcode = down ? x11::KeyEvent::Press : x11::KeyEvent::Release;
+    connection_->xtest().FakeInput({opcode, static_cast<uint8_t>(keycode)});
+    connection_->Sync();
+    connection_->DispatchAll();
+    FlushTaskRunner();
+  }
+
+  void FlushTaskRunner() {
+    base::RunLoop run_loop;
+    task_environment_.GetMainThreadTaskRunner()->PostTask(
+        FROM_HERE, run_loop.QuitClosure());
+    run_loop.Run();
+  }
+
+ protected:
+  // Headless Xorg (used by testing/xvfb.py on CQ bots) only creates the master
+  // devices and the virtual XTEST slave devices, with no physical slave
+  // keyboard attached. Use a distinct synthetic DeviceId to represent a
+  // physical slave keyboard when dispatching RawDeviceEvents.
+  static constexpr x11::Input::DeviceId kLocalKeyboardId{100};
+
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  raw_ptr<x11::Connection> connection_ = nullptr;
+  x11::Input::DeviceId master_keyboard_id_{};
+  x11::Input::DeviceId xtest_keyboard_id_{};
+  x11::Input::DeviceId local_keyboard_id_{kLocalKeyboardId};
+  base::test::TestFuture<void> disconnect_future_;
+  std::unique_ptr<LocalHotkeyInputMonitor> monitor_;
+};
+
+TEST_F(LocalHotkeyInputMonitorX11Test, LocalCtrlAltEscTriggersDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       RemoteModifierReleaseDoesNotSuppressDisconnect) {
+  // Local user physically holds Control and Alt.
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+
+  // Remote client streams modifier release events via the XTEST device.
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/false, xtest_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/false, xtest_keyboard_id_);
+
+  // Local user presses Escape while still physically holding Control and Alt.
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       LiveXTestModifierChurnDoesNotSuppressDisconnect) {
+  // Local user physically holds Control and Alt.
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+
+  // Remote client injects modifier press/release churn through live XTest.
+  InjectXTestKeyEvent(XK_Control_L, /*down=*/true);
+  InjectXTestKeyEvent(XK_Control_L, /*down=*/false);
+  InjectXTestKeyEvent(XK_Alt_L, /*down=*/true);
+  InjectXTestKeyEvent(XK_Alt_L, /*down=*/false);
+
+  // Local user presses Escape.
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       RemoteInjectedHotkeysDoNotTriggerDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, xtest_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, xtest_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, xtest_keyboard_id_);
+
+  EXPECT_FALSE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test, LocalModifierReleasePreventsDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/false, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_FALSE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       ReleasingOneCtrlWhileOtherCtrlHeldStillTriggersDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Control_R, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Control_R, /*down=*/false, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+}  // namespace remoting
diff --git a/remoting/host/BUILD.gn b/remoting/host/BUILD.gn
index 482ff1ef..dadad5e 100644
--- a/remoting/host/BUILD.gn
+++ b/remoting/host/BUILD.gn
@@ -1049,7 +1049,11 @@
   }
 
   if (remoting_use_x11) {
-    deps += [ "//remoting/host/linux:platform_impls" ]
+    sources += [ "input_monitor/local_hotkey_input_monitor_x11_unittest.cc" ]
+    deps += [
+      "//remoting/host/linux:platform_impls",
+      "//ui/gfx/x",
+    ]
   }
 
   if (is_chromeos) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc b/remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
new file mode 100644
index 0000000..a31d2e9
--- /dev/null
+++ b/remoting/host/input_monitor/local_hotkey_input_monitor_x11_unittest.cc
@@ -0,0 +1,187 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "remoting/host/input_monitor/local_hotkey_input_monitor.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/x/connection.h"
+#include "ui/gfx/x/event.h"
+#include "ui/gfx/x/future.h"
+#include "ui/gfx/x/keysyms/keysyms.h"
+#include "ui/gfx/x/xinput.h"
+#include "ui/gfx/x/xproto.h"
+#include "ui/gfx/x/xtest.h"
+
+namespace remoting {
+
+class LocalHotkeyInputMonitorX11Test : public testing::Test {
+ public:
+  LocalHotkeyInputMonitorX11Test()
+      : task_environment_(base::test::TaskEnvironment::MainThreadType::UI) {}
+
+  void SetUp() override {
+    connection_ = x11::Connection::Get();
+    ASSERT_NE(connection_, nullptr);
+    ASSERT_TRUE(connection_->Ready());
+    ASSERT_TRUE(connection_->xinput().present());
+
+    auto reply =
+        connection_->xinput().XIQueryDevice({x11::Input::DeviceId::All}).Sync();
+    ASSERT_TRUE(reply);
+    for (const auto& info : reply->infos) {
+      if (info.type == x11::Input::DeviceType::MasterKeyboard) {
+        master_keyboard_id_ = info.deviceid;
+      } else if (info.name.ends_with("XTEST keyboard")) {
+        xtest_keyboard_id_ = info.deviceid;
+      }
+    }
+    ASSERT_NE(master_keyboard_id_, x11::Input::DeviceId{});
+    ASSERT_NE(xtest_keyboard_id_, x11::Input::DeviceId{});
+    ASSERT_NE(xtest_keyboard_id_, kLocalKeyboardId);
+
+    auto task_runner = task_environment_.GetMainThreadTaskRunner();
+    monitor_ =
+        LocalHotkeyInputMonitor::Create(task_runner, task_runner, task_runner,
+                                        disconnect_future_.GetCallback());
+    FlushTaskRunner();
+    connection_->Sync();
+    connection_->DispatchAll();
+  }
+
+  void TearDown() override {
+    monitor_.reset();
+    FlushTaskRunner();
+  }
+
+  void DispatchRawKeyEvent(uint32_t keysym,
+                           bool down,
+                           x11::Input::DeviceId source_id) {
+    x11::KeyCode keycode = connection_->KeysymToKeycode(keysym);
+    ASSERT_NE(keycode, x11::KeyCode{});
+
+    x11::Input::RawDeviceEvent raw_event;
+    raw_event.opcode = down ? x11::Input::RawDeviceEvent::RawKeyPress
+                            : x11::Input::RawDeviceEvent::RawKeyRelease;
+    raw_event.deviceid = master_keyboard_id_;
+    raw_event.sourceid = source_id;
+    raw_event.detail = static_cast<uint32_t>(keycode);
+
+    x11::Event event(false, std::move(raw_event));
+    connection_->DispatchEvent(event);
+    FlushTaskRunner();
+  }
+
+  void InjectXTestKeyEvent(uint32_t keysym, bool down) {
+    ASSERT_TRUE(connection_->xtest().present());
+    x11::KeyCode keycode = connection_->KeysymToKeycode(keysym);
+    ASSERT_NE(keycode, x11::KeyCode{});
+
+    uint8_t opcode = down ? x11::KeyEvent::Press : x11::KeyEvent::Release;
+    connection_->xtest().FakeInput({opcode, static_cast<uint8_t>(keycode)});
+    connection_->Sync();
+    connection_->DispatchAll();
+    FlushTaskRunner();
+  }
+
+  void FlushTaskRunner() {
+    base::RunLoop run_loop;
+    task_environment_.GetMainThreadTaskRunner()->PostTask(
+        FROM_HERE, run_loop.QuitClosure());
+    run_loop.Run();
+  }
+
+ protected:
+  // Headless Xorg (used by testing/xvfb.py on CQ bots) only creates the master
+  // devices and the virtual XTEST slave devices, with no physical slave
+  // keyboard attached. Use a distinct synthetic DeviceId to represent a
+  // physical slave keyboard when dispatching RawDeviceEvents.
+  static constexpr x11::Input::DeviceId kLocalKeyboardId{100};
+
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  raw_ptr<x11::Connection> connection_ = nullptr;
+  x11::Input::DeviceId master_keyboard_id_{};
+  x11::Input::DeviceId xtest_keyboard_id_{};
+  x11::Input::DeviceId local_keyboard_id_{kLocalKeyboardId};
+  base::test::TestFuture<void> disconnect_future_;
+  std::unique_ptr<LocalHotkeyInputMonitor> monitor_;
+};
+
+TEST_F(LocalHotkeyInputMonitorX11Test, LocalCtrlAltEscTriggersDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       RemoteModifierReleaseDoesNotSuppressDisconnect) {
+  // Local user physically holds Control and Alt.
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+
+  // Remote client streams modifier release events via the XTEST device.
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/false, xtest_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/false, xtest_keyboard_id_);
+
+  // Local user presses Escape while still physically holding Control and Alt.
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       LiveXTestModifierChurnDoesNotSuppressDisconnect) {
+  // Local user physically holds Control and Alt.
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+
+  // Remote client injects modifier press/release churn through live XTest.
+  InjectXTestKeyEvent(XK_Control_L, /*down=*/true);
+  InjectXTestKeyEvent(XK_Control_L, /*down=*/false);
+  InjectXTestKeyEvent(XK_Alt_L, /*down=*/true);
+  InjectXTestKeyEvent(XK_Alt_L, /*down=*/false);
+
+  // Local user presses Escape.
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       RemoteInjectedHotkeysDoNotTriggerDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, xtest_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, xtest_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, xtest_keyboard_id_);
+
+  EXPECT_FALSE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test, LocalModifierReleasePreventsDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/false, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_FALSE(disconnect_future_.IsReady());
+}
+
+TEST_F(LocalHotkeyInputMonitorX11Test,
+       ReleasingOneCtrlWhileOtherCtrlHeldStillTriggersDisconnect) {
+  DispatchRawKeyEvent(XK_Control_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Control_R, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Control_R, /*down=*/false, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Alt_L, /*down=*/true, local_keyboard_id_);
+  DispatchRawKeyEvent(XK_Escape, /*down=*/true, local_keyboard_id_);
+
+  EXPECT_TRUE(disconnect_future_.IsReady());
+}
+
+}  // 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.