CVE-2026-87616
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc |
modified | |
ifui/views/win/user_resize_move_detector.cc |
modified |
Files Changed
ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.ccui/views/win/user_resize_move_detector.cc
Patch
From 47ffe717717435d672257187a00922b391d0a27e Mon Sep 17 00:00:00 2001 From: Daniel Clark <[email protected]> Date: Mon, 03 Aug 2026 09:38:04 -0700 Subject: [PATCH] Set g_in_move_resize_loop as soon as we get WM_ENTERSIZEMOVE When the user clicks on a window border to start window resizing, Windows' native modal loop sends WM_ENTERSIZEMOVE, and `ScopedAllowApplicationTasksInNativeNestedLoop` is set so that IPC from renderer processes can continue to be handled. If the user then drags the window border, WM_SIZING or WM_MOVING will be received, at which point `UserResizeMoveDetector` will set `g_in_move_resize_loop`. This causes `DesktopDragDropClientWin::StartDragAndDrop` to correctly guard against mouse actions during the window resize being treated as a drag/drop by checking `IsInNativeMoveResizeLoop()`. But if a compromised renderer spams `LocalFrameHost::StartDragging` IPC messages in the gap between `WM_ENTERSIZEMOVE` and `WM_SIZING`/`WM_MOVING`, the `g_in_move_resize_loop` guard isn't yet set, meaning the `IsInNativeMoveResizeLoop()` check in `DesktopDragDropClientWin::StartDragAndDrop` will return false and a payload from the renderer could be completed as a drop despite the user only having clicked the window border. Fix it by setting `g_in_move_resize_loop` as soon as WM_ENTERSIZEMOVE is received. This is consistent with the timing with which we set `g_is_in_native_menu_loop` in `HWNDMessageHandler::OnEnterMenuLoop`. Fixed: 520469117 Change-Id: I65722bfd78e418f9e43c2ee0eafe85382d7dabd4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175067 Reviewed-by: David Bienvenu <[email protected]> Commit-Queue: Dan Clark <[email protected]> Reviewed-by: Allen Bauer <[email protected]> Cr-Commit-Position: refs/heads/main@{#1672718} --- diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc index a4d0dd0..dccab27 100644 --- a/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc +++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc @@ -397,5 +397,38 @@ EXPECT_FALSE(host_b->IsInNativeMoveResizeLoop()); } +// The native size/move modal loop is already on the stack once WM_ENTERSIZEMOVE +// has been delivered, but no WM_SIZING/WM_MOVING arrives until the user moves +// the mouse. That loop pumps application tasks (including Mojo IPCs from +// renderers), so IsInNativeMoveResizeLoop() must already report true at +// WM_ENTERSIZEMOVE. Otherwise a task running in the gap sees false and can +// start an OS-level drag while the user is holding a resize border. +TEST_F(DesktopWindowTreeHostWinTest, IsInNativeMoveResizeLoopOnEnterSizeMove) { + Widget widget; + widget.Init(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET, + Widget::InitParams::TYPE_WINDOW)); + widget.Show(); + + DesktopWindowTreeHostWin* host = static_cast<DesktopWindowTreeHostWin*>( + widget.GetNativeWindow()->GetHost()); + EXPECT_FALSE(host->IsInNativeMoveResizeLoop()); + + HWND hwnd = widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget(); + + // The user has pressed the mouse on a resize border. Windows has entered the + // modal loop, but the mouse has not moved yet. + ::SendMessage(hwnd, WM_ENTERSIZEMOVE, 0, 0); + EXPECT_TRUE(host->IsInNativeMoveResizeLoop()); + + // The user now drags, producing the first WM_SIZING. + RECT rect = {}; + ::GetWindowRect(hwnd, &rect); + ::SendMessage(hwnd, WM_SIZING, WMSZ_RIGHT, reinterpret_cast<LPARAM>(&rect)); + EXPECT_TRUE(host->IsInNativeMoveResizeLoop()); + + ::SendMessage(hwnd, WM_EXITSIZEMOVE, 0, 0); + EXPECT_FALSE(host->IsInNativeMoveResizeLoop()); +} + } // namespace test } // namespace views diff --git a/ui/views/win/user_resize_move_detector.cc b/ui/views/win/user_resize_move_detector.cc index 7f76fe48..af3580d 100644 --- a/ui/views/win/user_resize_move_detector.cc +++ b/ui/views/win/user_resize_move_detector.cc @@ -19,12 +19,14 @@ void UserResizeMoveDetector::OnEnterSizeMove() { if (state_ == State::kNotResizing) { state_ = State::kInSizeMove; + // The native modal size/move loop is on the stack from here until + // WM_EXITSIZEMOVE. + g_in_move_resize_loop = true; } } void UserResizeMoveDetector::OnSizing() { if (state_ == State::kInSizeMove) { - g_in_move_resize_loop = true; state_ = State::kInSizing; hwnd_delegate_->HandleBeginUserResize(); } @@ -32,7 +34,6 @@ void UserResizeMoveDetector::OnMoving() { if (state_ == State::kInSizeMove) { - g_in_move_resize_loop = true; state_ = State::kInMoving; hwnd_delegate_->HandleBeginUserDrag(); }
Regression Test / PoC
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc
index a4d0dd0..dccab27 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_win_unittest.cc
@@ -397,5 +397,38 @@
EXPECT_FALSE(host_b->IsInNativeMoveResizeLoop());
}
+// The native size/move modal loop is already on the stack once WM_ENTERSIZEMOVE
+// has been delivered, but no WM_SIZING/WM_MOVING arrives until the user moves
+// the mouse. That loop pumps application tasks (including Mojo IPCs from
+// renderers), so IsInNativeMoveResizeLoop() must already report true at
+// WM_ENTERSIZEMOVE. Otherwise a task running in the gap sees false and can
+// start an OS-level drag while the user is holding a resize border.
+TEST_F(DesktopWindowTreeHostWinTest, IsInNativeMoveResizeLoopOnEnterSizeMove) {
+ Widget widget;
+ widget.Init(CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET,
+ Widget::InitParams::TYPE_WINDOW));
+ widget.Show();
+
+ DesktopWindowTreeHostWin* host = static_cast<DesktopWindowTreeHostWin*>(
+ widget.GetNativeWindow()->GetHost());
+ EXPECT_FALSE(host->IsInNativeMoveResizeLoop());
+
+ HWND hwnd = widget.GetNativeWindow()->GetHost()->GetAcceleratedWidget();
+
+ // The user has pressed the mouse on a resize border. Windows has entered the
+ // modal loop, but the mouse has not moved yet.
+ ::SendMessage(hwnd, WM_ENTERSIZEMOVE, 0, 0);
+ EXPECT_TRUE(host->IsInNativeMoveResizeLoop());
+
+ // The user now drags, producing the first WM_SIZING.
+ RECT rect = {};
+ ::GetWindowRect(hwnd, &rect);
+ ::SendMessage(hwnd, WM_SIZING, WMSZ_RIGHT, reinterpret_cast<LPARAM>(&rect));
+ EXPECT_TRUE(host->IsInNativeMoveResizeLoop());
+
+ ::SendMessage(hwnd, WM_EXITSIZEMOVE, 0, 0);
+ EXPECT_FALSE(host->IsInNativeMoveResizeLoop());
+}
+
} // namespace test
} // namespace views
Original Bug Report
Potential bypass of drag-drop block during window resize initialization on Windows
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 logic flaw exists in the Windows-specific window resize/move tracking where the global state tracking flag g_in_move_resize_loop is set on the first WM_SIZING/WM_MOVING message rather than on WM_ENTERSIZEMOVE. A compromised renderer can potentially exploit this timing window to bypass the drag-and-drop prevention check (IsInNativeMoveResizeLoop()) when a user clicks a resize border but has not yet moved the mouse. This could allow the renderer to initiate an unauthorized OS-level drag-and-drop operation with attacker-controlled payloads.
Affected files:
ui/views/win/user_resize_move_detector.ccui/views/widget/desktop_aura/desktop_drag_drop_client_win.ccui/views/win/hwnd_message_handler.cc
Estimated timestamp from git blame: 2026-06-04
Description
In Chromium on Windows, a protection is implemented in DesktopDragDropClientWin::StartDragAndDrop to block renderer-initiated drag-and-drop operations while the desktop window is in a native move/resize modal loop:
// ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc
if (desktop_host_->IsInNativeMoveResizeLoop()) {
return ui::PreferredDragOperation(
ui::DragDropTypes::DropEffectToDragOperation(DROPEFFECT_NONE));
}
This check relies on UserResizeMoveDetector::InMoveResizeLoop(), which returns a file-static boolean g_in_move_resize_loop defined in ui/views/win/user_resize_move_detector.cc. However, review of the codebase indicates a logic gap in how this state is initialized and updated:
// ui/views/win/user_resize_move_detector.cc
static bool g_in_move_resize_loop = false;
void UserResizeMoveDetector::OnEnterSizeMove() {
if (state_ == State::kNotResizing) {
state_ = State::kInSizeMove; // ← Does not set g_in_move_resize_loop
}
}
void UserResizeMoveDetector::OnSizing() {
if (state_ == State::kInSizeMove) {
g_in_move_resize_loop = true; // ← Set here on first resize/move message
state_ = State::kInSizing;
hwnd_delegate_->HandleBeginUserResize();
}
}
When a user clicks a window resize border, Windows enters the native modal loop, dispatching a WM_ENTERSIZEMOVE message which triggers OnEnterSizeMove(). However, the g_in_move_resize_loop flag remains false until the first WM_SIZING or WM_MOVING message is dispatched, which requires the user to actually move the mouse cursor after clicking. This creates a potential window of opportunity where the native move/resize loop is active, but the protection predicate is still false.
Potential Attack Scenario
Although our automated tooling has not yet executed a dynamic proof-of-concept, the static code path suggests the following potential exploit flow:
- Renderer Preparation: A compromised renderer continuously posts
LocalFrameHost::StartDraggingIPC messages with a maliciousDragDatapayload and setting the source tokMouse. - User Interaction: The user clicks and holds the left mouse button down on a window resize border (triggering hit-tests such as
HTLEFTorHTRIGHT). - Modal Loop Entry: The click is processed via
DefWindowProc(WM_NCLBUTTONDOWN), synchronously entering the native modal loop. The loop dispatchesWM_ENTERSIZEMOVEwhich transitionsUserResizeMoveDetectortoState::kInSizeMove, butg_in_move_resize_loopremainsfalse. - Task Dispatching: The native modal loop uses
ScopedAllowApplicationTasksInNativeNestedLoopto allow application tasks to run. One of the queuedStartDraggingMojo IPC tasks from the compromised renderer is processed on the UI thread. - Check Bypass:
DesktopDragDropClientWin::StartDragAndDropis reached. The call todesktop_host_->IsInNativeMoveResizeLoop()evaluatesg_in_move_resize_loop(which is stillfalse), bypassing the guard. - OS-Level Drag: The browser calls the blocking Win32 API
::DoDragDrop(). Since the user is holding down the left mouse button, the drag loop persists. When the user moves and releases the mouse button, the drop action is completed with the attacker’s payload.
Suggested Fix
To close this logic gap and prevent any potential bypass, g_in_move_resize_loop should be set to true immediately when the modal loop begins, within UserResizeMoveDetector::OnEnterSizeMove():
void UserResizeMoveDetector::OnEnterSizeMove() {
if (state_ == State::kNotResizing) {
state_ = State::kInSizeMove;
g_in_move_resize_loop = true; // Set to true immediately upon entering size/move loop
}
}
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
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.