CVE-2026-16423
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/base/x/x11_whole_screen_move_loop.cc |
modified | |
TestPlatformEventSourceui/base/x/x11_whole_screen_move_loop_unittest.cc |
modified | |
TestMoveLoopDelegateui/base/x/x11_whole_screen_move_loop_unittest.cc |
modified | |
TESTui/base/x/x11_whole_screen_move_loop_unittest.cc |
modified |
Files Changed
ui/base/x/BUILD.gnui/base/x/x11_whole_screen_move_loop.ccui/base/x/x11_whole_screen_move_loop_unittest.cc
Patch
From e310c53457aabfff077bdd670be1cf89aa8924ca Mon Sep 17 00:00:00 2001 From: Tom Anderson <[email protected]> Date: Tue, 14 Jul 2026 09:32:57 -0700 Subject: [PATCH] [X11] Fix potential use-after-free in X11WSML::EndMoveLoop EndMoveLoop() invokes OnMoveLoopEnded() on its delegate. Under some conditions (e.g., drag-and-drop operations on Linux/X11), this callback can synchronously destroy the owning window and the move loop instance itself. Upon returning to EndMoveLoop(), accessing any members on a freed 'this' causes a use-after-free (UAF) crash. This CL introduces a WeakPtr guard immediately after the delegate invocation to check if the loop is still alive before executing the remaining cleanup/teardown code. An automated unit test is added to prevent regressions. Change-Id: Ia4ea812661a1a67313af90e994eed842d5ebc913 Fixed: 534582496 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8092749 Reviewed-by: Lei Zhang <[email protected]> Commit-Queue: Thomas Anderson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1661927} --- diff --git a/ui/base/x/BUILD.gn b/ui/base/x/BUILD.gn index 190c5c0..dac89b38 100644 --- a/ui/base/x/BUILD.gn +++ b/ui/base/x/BUILD.gn @@ -167,6 +167,7 @@ "x11_cursor_loader_unittest.cc", "x11_desktop_window_move_client_unittest.cc", "x11_display_util_unittest.cc", + "x11_whole_screen_move_loop_unittest.cc", ] deps = [ ":x", @@ -176,6 +177,7 @@ "//testing/gtest", "//ui/base/cursor", "//ui/base/cursor/mojom:cursor_type_shared", + "//ui/events/platform", "//ui/gfx/geometry", "//ui/gfx/x", ] diff --git a/ui/base/x/x11_whole_screen_move_loop.cc b/ui/base/x/x11_whole_screen_move_loop.cc index 71ec09e..e200895 100644 --- a/ui/base/x/x11_whole_screen_move_loop.cc +++ b/ui/base/x/x11_whole_screen_move_loop.cc @@ -237,7 +237,11 @@ // Restore the previous dispatcher. nested_dispatcher_.reset(); + base::WeakPtr<X11WholeScreenMoveLoop> alive(weak_factory_.GetWeakPtr()); delegate_->OnMoveLoopEnded(); + if (!alive) { + return; + } grab_input_window_events_.Reset(); connection->DestroyWindow({grab_input_window_}); grab_input_window_ = x11::Window::None; diff --git a/ui/base/x/x11_whole_screen_move_loop_unittest.cc b/ui/base/x/x11_whole_screen_move_loop_unittest.cc new file mode 100644 index 0000000..99d649d --- /dev/null +++ b/ui/base/x/x11_whole_screen_move_loop_unittest.cc @@ -0,0 +1,82 @@ +// 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 "ui/base/x/x11_whole_screen_move_loop.h" + +#include <memory> + +#include "base/functional/bind.h" +#include "base/functional/callback.h" +#include "base/functional/callback_helpers.h" +#include "base/memory/weak_ptr.h" +#include "base/run_loop.h" +#include "base/task/single_thread_task_runner.h" +#include "base/test/task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/x/x11_cursor.h" +#include "ui/events/platform/platform_event_source.h" +#include "ui/gfx/geometry/point.h" + +namespace ui { + +namespace { + +class TestPlatformEventSource : public PlatformEventSource { + public: + TestPlatformEventSource() = default; + ~TestPlatformEventSource() override = default; +}; + +class TestMoveLoopDelegate : public X11MoveLoopDelegate { + public: + TestMoveLoopDelegate() = default; + ~TestMoveLoopDelegate() override = default; + + // X11MoveLoopDelegate: + void OnMouseMovement(const gfx::Point& screen_point, + int flags, + base::TimeTicks event_time) override {} + void OnMouseReleased() override {} + void OnMoveLoopEnded() override { + loop_.reset(); + } + + void Init(std::unique_ptr<X11WholeScreenMoveLoop> loop) { + loop_ = std::move(loop); + } + + bool has_loop() const { return !!loop_; } + + private: + std::unique_ptr<X11WholeScreenMoveLoop> loop_; +}; + +} // namespace + +TEST(X11WholeScreenMoveLoopTest, EndMoveLoopSurvivesSelfDeletion) { + base::test::SingleThreadTaskEnvironment task_environment( + base::test::SingleThreadTaskEnvironment::MainThreadType::UI); + + TestPlatformEventSource event_source; + + TestMoveLoopDelegate delegate; + auto move_loop = std::make_unique<X11WholeScreenMoveLoop>(&delegate); + X11WholeScreenMoveLoop* move_loop_ptr = move_loop.get(); + delegate.Init(std::move(move_loop)); + + // Post a task to end the move loop. This task will run after the nested + // run loop starts, which synchronously invokes delegate->OnMoveLoopEnded() + // and deletes the loop. + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&X11WholeScreenMoveLoop::EndMoveLoop, + base::Unretained(move_loop_ptr))); + + bool run_result = move_loop_ptr->RunMoveLoop( + /*can_grab_pointer=*/false, nullptr, nullptr, base::DoNothing()); + + EXPECT_FALSE(delegate.has_loop()); + EXPECT_FALSE(run_result); +} + +} // namespace ui
Regression Test / PoC
diff --git a/ui/base/x/x11_whole_screen_move_loop_unittest.cc b/ui/base/x/x11_whole_screen_move_loop_unittest.cc
new file mode 100644
index 0000000..99d649d
--- /dev/null
+++ b/ui/base/x/x11_whole_screen_move_loop_unittest.cc
@@ -0,0 +1,82 @@
+// 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 "ui/base/x/x11_whole_screen_move_loop.h"
+
+#include <memory>
+
+#include "base/functional/bind.h"
+#include "base/functional/callback.h"
+#include "base/functional/callback_helpers.h"
+#include "base/memory/weak_ptr.h"
+#include "base/run_loop.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/test/task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/x/x11_cursor.h"
+#include "ui/events/platform/platform_event_source.h"
+#include "ui/gfx/geometry/point.h"
+
+namespace ui {
+
+namespace {
+
+class TestPlatformEventSource : public PlatformEventSource {
+ public:
+ TestPlatformEventSource() = default;
+ ~TestPlatformEventSource() override = default;
+};
+
+class TestMoveLoopDelegate : public X11MoveLoopDelegate {
+ public:
+ TestMoveLoopDelegate() = default;
+ ~TestMoveLoopDelegate() override = default;
+
+ // X11MoveLoopDelegate:
+ void OnMouseMovement(const gfx::Point& screen_point,
+ int flags,
+ base::TimeTicks event_time) override {}
+ void OnMouseReleased() override {}
+ void OnMoveLoopEnded() override {
+ loop_.reset();
+ }
+
+ void Init(std::unique_ptr<X11WholeScreenMoveLoop> loop) {
+ loop_ = std::move(loop);
+ }
+
+ bool has_loop() const { return !!loop_; }
+
+ private:
+ std::unique_ptr<X11WholeScreenMoveLoop> loop_;
+};
+
+} // namespace
+
+TEST(X11WholeScreenMoveLoopTest, EndMoveLoopSurvivesSelfDeletion) {
+ base::test::SingleThreadTaskEnvironment task_environment(
+ base::test::SingleThreadTaskEnvironment::MainThreadType::UI);
+
+ TestPlatformEventSource event_source;
+
+ TestMoveLoopDelegate delegate;
+ auto move_loop = std::make_unique<X11WholeScreenMoveLoop>(&delegate);
+ X11WholeScreenMoveLoop* move_loop_ptr = move_loop.get();
+ delegate.Init(std::move(move_loop));
+
+ // Post a task to end the move loop. This task will run after the nested
+ // run loop starts, which synchronously invokes delegate->OnMoveLoopEnded()
+ // and deletes the loop.
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(&X11WholeScreenMoveLoop::EndMoveLoop,
+ base::Unretained(move_loop_ptr)));
+
+ bool run_result = move_loop_ptr->RunMoveLoop(
+ /*can_grab_pointer=*/false, nullptr, nullptr, base::DoNothing());
+
+ EXPECT_FALSE(delegate.has_loop());
+ EXPECT_FALSE(run_result);
+}
+
+} // namespace ui
Original Bug Report
Potential Use-After-Free in X11WholeScreenMoveLoop::EndMoveLoop via Synchronous Destruction
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) vulnerability exists in X11WholeScreenMoveLoop::EndMoveLoop during drag-and-drop operations on Linux/X11. Under specific conditions, invoking the upper-layer delegate can trigger synchronous destruction of the source window, which in turn recursively destroys the move loop object. When the call stack unwinds back to the outer execution of EndMoveLoop, member variables are dereferenced on the freed object, potentially leading to a control flow hijack.
Affected files:
ui/base/x/x11_whole_screen_move_loop.cc
Estimated timestamp from git blame: 2013-05-18
Detailed Writeup
Description
A potential Use-After-Free (UAF) vulnerability exists in X11WholeScreenMoveLoop::EndMoveLoop (ui/base/x/x11_whole_screen_move_loop.cc). The root cause is the absence of a WeakPtr guard or other lifetime checks after invoking an upper-layer delegate callback. Synchronous destruction of the source window during drag-and-drop operations can recursively free the X11WholeScreenMoveLoop instance, leading to subsequent dereferences of the freed object upon stack unwinding.
Root Cause Analysis
In ui/base/x/x11_whole_screen_move_loop.cc, the EndMoveLoop method executes the following cleanup:
void X11WholeScreenMoveLoop::EndMoveLoop() {
if (!in_move_loop_) {
return;
}
...
nested_dispatcher_.reset();
delegate_->OnMoveLoopEnded(); // (1) Can synchronously destroy |this|
grab_input_window_events_.Reset(); // (2) UAF on |this|
connection->DestroyWindow({grab_input_window_}); // (3) UAF read
grab_input_window_ = x11::Window::None; // (4) UAF write
in_move_loop_ = false; // (5) UAF write
std::move(quit_closure_).Run(); // (6) UAF read/run of OnceClosure
}
When delegate_->OnMoveLoopEnded() is called at line 240, control flows synchronously into the source window’s delegate (X11Window). Under certain conditions, such as when the drag-and-drop destination is another window in the same Chrome process, event dispatching is short-circuited synchronously via XDragDropClient::SendXClientEvent’s local window lookup.
If the target window’s drag-exit handler or associated controller (e.g., during tab dragging or other system DnD operations) spins a nested, nestable event loop (such as base::RunLoop with kNestableTasksAllowed), queued tasks on the UI thread are permitted to execute. If a window closure task for the source window (CloseNow()) runs during this window, the destructor of X11Window is invoked.
Inside ~X11Window, the member variables are destroyed in reverse order of declaration. Since drag_loop_ (std::unique_ptr<X11MoveLoop>) is declared after drag_drop_client_ in x11_window.h, drag_loop_ is destroyed first.
This invokes ~X11WholeScreenMoveLoop(), which calls EndMoveLoop() recursively. Because in_move_loop_ is still true (the outer call has not yet reached line 245), the recursive call proceeds, completes, and deletes/frees the X11WholeScreenMoveLoop instance.
When the synchronous call stack eventually unwinds back to the outer execution of EndMoveLoop() at line 240, execution resumes. However, the this pointer is now dangling, resulting in multiple UAF dereferences (lines 241-246). Most critically, executing std::move(quit_closure_).Run() on line 246 results in a virtual/indirect function call using the contents of a freed heap structure, which could potentially be exploited for Remote Code Execution (RCE) in the browser process.
(Note: As our analysis tools are static, these are potential steps and we do not have a working execution-based Proof of Concept).
Potential Trigger Steps (Conceptual)
- Initiate a drag-and-drop operation from Chrome Window A (source) on Linux/X11, which allocates
X11WholeScreenMoveLoop. - Move the cursor over Chrome Window B (target, in the same browser process) to associate the current target window.
- Terminate or cancel the move loop (e.g., via the Esc key), triggering
EndMoveLoop(). - In the target’s drag-leave flow (triggered synchronously via the same-process client short-circuit), cause the target to spin a nested, nestable run loop.
- In the nested run loop, dispatch a pending task that synchronously destroys Window A.
- The destruction of Window A recursively destroys the move loop object, which executes the inner
EndMoveLoopand deletes the memory. - As the outer call stack unwinds back to the original
EndMoveLoopframe, it dereferences the freed memory.
Suggested Fix
To prevent this potential UAF, a WeakPtr guard should be checked after the delegate invocation. The class already owns a base::WeakPtrFactory<X11WholeScreenMoveLoop> weak_factory_{this}.
We can check if this is still alive before executing the rest of the cleanups:
base::WeakPtr<X11WholeScreenMoveLoop> alive(weak_factory_.GetWeakPtr());
delegate_->OnMoveLoopEnded();
if (!alive) {
return;
}
This is identical to the pattern used in RunMoveLoop to handle synchronous deletion hazards.
Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d
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.