From f6007871944c46bd0b15f6eeeff1fa6a08315f31 Mon Sep 17 00:00:00 2001 From: Komov Date: Tue, 26 May 2026 14:04:38 +0300 Subject: [PATCH 1/2] windows: fix deadlock on keyboard layout switch (PuntoSwitcher, IME) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WM_KEYDOWN handler in KeyEventBuilder::process_message() called next_kbd_msg() (PeekMessageW) before acquiring the LAYOUT_CACHE mutex. PeekMessageW can synchronously dispatch pending messages — notably WM_INPUTLANGCHANGE sent by layout-switching tools like PuntoSwitcher. If the dispatched message re-enters the WNDPROC and tries to acquire LAYOUT_CACHE, the non-reentrant std::sync::Mutex deadlocks. The WM_KEYUP handler already had the correct ordering (lock first, drop, then PeekMessageW) with a comment explaining the deadlock risk. This commit applies the same pattern to WM_KEYDOWN. Additionally, handle WM_INPUTLANGCHANGE explicitly to refresh the layout cache and update modifier state, instead of falling through to DefWindowProc which may dispatch further IME messages in an unpredictable re-entrant context. Fixes warpdotdev/warp#8675. Fixes warpdotdev/warp#10050. Co-Authored-By: Claude Opus 4.7 --- src/changelog/unreleased.md | 5 +++++ src/platform_impl/windows/event_loop.rs | 28 +++++++++++++++++++------ src/platform_impl/windows/keyboard.rs | 12 +++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index c42c259352..dd0de39dfd 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -217,6 +217,11 @@ changelog entry. ### Fixed +- On Windows, fix deadlock when switching keyboard layout via tools like PuntoSwitcher or IME + switchers. The `LAYOUT_CACHE` mutex was held across a `PeekMessageW` call in the `WM_KEYDOWN` + handler, which could dispatch re-entrant messages that tried to re-acquire the same lock. +- On Windows, handle `WM_INPUTLANGCHANGE` explicitly to refresh the layout cache and avoid + re-entrant deadlocks through `DefWindowProc`. - On Orbital, `MonitorHandle::name()` now returns `None` instead of a dummy name. - On iOS, fixed `SurfaceResized` and `Window::surface_size` not reporting the size of the actual surface. - On macOS, fixed the scancode conversion for audio volume keys. diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index abbaeaa3b9..797b1a76d2 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -49,12 +49,12 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, - WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, - WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, - WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, - WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, - WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, - WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, + WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, + WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, + WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, + WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, + WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, + WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, }; @@ -1569,6 +1569,22 @@ unsafe fn public_window_callback_inner( result = ProcResult::DefWindowProc(wparam); }, + WM_INPUTLANGCHANGE => { + // Refresh the layout cache so subsequent key events use the new + // layout. This message is sent by Windows (or tools like + // PuntoSwitcher) when the keyboard layout changes. We handle it + // explicitly to avoid re-entrant deadlocks: without this handler + // the message falls through to DefWindowProc which may + // synchronously dispatch IME messages while LAYOUT_CACHE is held + // by a parent WNDPROC frame. + { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + layouts.get_current_layout(); + } + update_modifiers(window, userdata); + result = ProcResult::Value(1); + }, + // this is necessary for us to maintain minimize/restore state WM_SYSCOMMAND => { if wparam == SC_RESTORE as usize { diff --git a/src/platform_impl/windows/keyboard.rs b/src/platform_impl/windows/keyboard.rs index d4e7cc3407..8c7a4f1e3e 100644 --- a/src/platform_impl/windows/keyboard.rs +++ b/src/platform_impl/windows/keyboard.rs @@ -111,8 +111,6 @@ impl KeyEventBuilder { let pending_token = self.pending.add_pending(); *result = ProcResult::Value(0); - let next_msg = next_kbd_msg(hwnd); - let mut layouts = LAYOUT_CACHE.lock().unwrap(); let mut finished_event_info = Some(PartialKeyEventInfo::from_message( wparam, @@ -120,6 +118,15 @@ impl KeyEventBuilder { ElementState::Pressed, &mut layouts, )); + // We MUST release the layout lock before calling `next_kbd_msg`, + // otherwise PeekMessageW may dispatch re-entrant messages (e.g. + // WM_INPUTLANGCHANGE from PuntoSwitcher or other IME switchers) + // that try to re-acquire LAYOUT_CACHE on the same thread, causing + // a deadlock. + drop(layouts); + + let next_msg = next_kbd_msg(hwnd); + let mut event_info = self.event_info.lock().unwrap(); *event_info = None; if let Some(next_msg) = next_msg { @@ -133,6 +140,7 @@ impl KeyEventBuilder { // store the partial information, and add to it in the upcoming events *event_info = finished_event_info.take(); } else { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); let (_, layout) = layouts.get_current_layout(); let is_fake = { let curr_event = finished_event_info.as_ref().unwrap(); From a8a69b556da0acab85efa90a0e7694d90c504913 Mon Sep 17 00:00:00 2001 From: Komov Date: Thu, 28 May 2026 08:08:21 +0300 Subject: [PATCH 2/2] windows: fix layout-switch freeze by refreshing layout cache on WM_INPUTLANGCHANGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revise the fix after a minidump of the frozen process disproved the original explanation. The freeze is NOT a `LAYOUT_CACHE` mutex deadlock: the captured dump shows the main thread blocked in `NtUserMsgWaitForMultipleObjectsEx` (winit's event-loop wait) re-entered through Punto Switcher's injected hook (`pshook64.dll` / `CallNextHookEx`) — no mutex frames anywhere. Accordingly: - Revert the `WM_KEYDOWN` lock reorder in keyboard.rs; it was irrelevant (no mutex is involved, and `next_kbd_msg` already ran before the lock). - Handle `WM_INPUTLANGCHANGE` by refreshing the cached keyboard layout and then deferring to `DefWindowProc` (kept, per the Win32 docs, so the message still propagates to first-level child windows). Isolation testing on Windows with Punto Switcher: refreshing the layout cache in this handler is the minimal change that stops the freeze; `update_modifiers` and swallowing the message are not needed. Co-Authored-By: Claude Opus 4.7 --- src/changelog/unreleased.md | 8 +++----- src/platform_impl/windows/event_loop.rs | 17 ++++++++--------- src/platform_impl/windows/keyboard.rs | 12 ++---------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index dd0de39dfd..0490a50e51 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -217,11 +217,9 @@ changelog entry. ### Fixed -- On Windows, fix deadlock when switching keyboard layout via tools like PuntoSwitcher or IME - switchers. The `LAYOUT_CACHE` mutex was held across a `PeekMessageW` call in the `WM_KEYDOWN` - handler, which could dispatch re-entrant messages that tried to re-acquire the same lock. -- On Windows, handle `WM_INPUTLANGCHANGE` explicitly to refresh the layout cache and avoid - re-entrant deadlocks through `DefWindowProc`. +- On Windows, fix freeze when switching keyboard layout with tools like Punto Switcher. The + `WM_INPUTLANGCHANGE` message is now handled to refresh the cached keyboard layout, while still + deferring to `DefWindowProc` for normal propagation. - On Orbital, `MonitorHandle::name()` now returns `None` instead of a dummy name. - On iOS, fixed `SurfaceResized` and `Window::surface_size` not reporting the size of the actual surface. - On macOS, fixed the scancode conversion for audio volume keys. diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 797b1a76d2..88cef21622 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -1570,19 +1570,18 @@ unsafe fn public_window_callback_inner( }, WM_INPUTLANGCHANGE => { - // Refresh the layout cache so subsequent key events use the new - // layout. This message is sent by Windows (or tools like - // PuntoSwitcher) when the keyboard layout changes. We handle it - // explicitly to avoid re-entrant deadlocks: without this handler - // the message falls through to DefWindowProc which may - // synchronously dispatch IME messages while LAYOUT_CACHE is held - // by a parent WNDPROC frame. + // Refresh the cached keyboard layout for the newly activated input + // language. This message is sent (by Windows or by layout switchers + // such as Punto Switcher) after the layout changes. Refreshing the + // cache here prevents a freeze that otherwise occurs when switching + // layout via such tools. We still defer to `DefWindowProc` so the + // message keeps propagating to first-level child windows, as the + // Win32 documentation requires. { let mut layouts = LAYOUT_CACHE.lock().unwrap(); layouts.get_current_layout(); } - update_modifiers(window, userdata); - result = ProcResult::Value(1); + result = ProcResult::DefWindowProc(wparam); }, // this is necessary for us to maintain minimize/restore state diff --git a/src/platform_impl/windows/keyboard.rs b/src/platform_impl/windows/keyboard.rs index 8c7a4f1e3e..d4e7cc3407 100644 --- a/src/platform_impl/windows/keyboard.rs +++ b/src/platform_impl/windows/keyboard.rs @@ -111,6 +111,8 @@ impl KeyEventBuilder { let pending_token = self.pending.add_pending(); *result = ProcResult::Value(0); + let next_msg = next_kbd_msg(hwnd); + let mut layouts = LAYOUT_CACHE.lock().unwrap(); let mut finished_event_info = Some(PartialKeyEventInfo::from_message( wparam, @@ -118,15 +120,6 @@ impl KeyEventBuilder { ElementState::Pressed, &mut layouts, )); - // We MUST release the layout lock before calling `next_kbd_msg`, - // otherwise PeekMessageW may dispatch re-entrant messages (e.g. - // WM_INPUTLANGCHANGE from PuntoSwitcher or other IME switchers) - // that try to re-acquire LAYOUT_CACHE on the same thread, causing - // a deadlock. - drop(layouts); - - let next_msg = next_kbd_msg(hwnd); - let mut event_info = self.event_info.lock().unwrap(); *event_info = None; if let Some(next_msg) = next_msg { @@ -140,7 +133,6 @@ impl KeyEventBuilder { // store the partial information, and add to it in the upcoming events *event_info = finished_event_info.take(); } else { - let mut layouts = LAYOUT_CACHE.lock().unwrap(); let (_, layout) = layouts.get_current_layout(); let is_fake = { let curr_event = finished_event_info.as_ref().unwrap();