Summary
On Windows, a winit application freezes ("Not Responding") when the keyboard
layout is switched by an automatic layout switcher that installs a global
Windows hook — specifically Yandex Punto Switcher (pshook64.dll). The
window stops pumping messages and only killing the process recovers it.
This has surfaced widely in Warp (which is built on winit):
warpdotdev/warp#10050, warpdotdev/warp#8675, warpdotdev/warp#9967,
warpdotdev/warp#7073.
A fix is proposed in #4582 (originally found and fixed in Warp's fork,
warpdotdev#15).
Environment
- OS: Windows 11
- Trigger: Yandex Punto Switcher running and performing an automatic layout
switch (e.g. English ↔ Russian).
- Reproduced with a minimal winit app (built for
x86_64-pc-windows-gnu) that
just logs key/IME/focus events. Reproducing requires a real layout switch at
the keyboard.
Steps to reproduce
- Run Punto Switcher (or any tool that installs a global hook and drives
WM_INPUTLANGCHANGE).
- Run a winit app on Windows and focus its window.
- Let Punto switch the layout (type text it auto-corrects, or use its switch
hotkey).
- The window freezes / "Not Responding".
Root cause (from a full minidump of the frozen process)
A full dump was captured with procdump -ma on the frozen window (original,
unpatched code) and walked with minidump-stackwalk using symbols generated
from the local system DLLs, so the x64 .pdata unwind/CFI is exact.
Reliable top of the main thread (instruction pointer + call-frame-info frames):
0 win32u.dll!NtUserMsgWaitForMultipleObjectsEx (instruction pointer)
1 winit::...::event_loop::wait_for_messages_impl (call frame info)
2 user32.dll!CallNextHookEx
3 user32.dll!<unknown>
- The main thread is blocked in
MsgWaitForMultipleObjectsEx — winit's
event-loop wait (wait_for_messages_impl, the
MsgWaitForMultipleObjectsEx(QS_ALLINPUT, MWMO_INPUTAVAILABLE) call) —
re-entered during message dispatch.
pshook64.dll (Punto Switcher's injected global hook; present in the
module list at a fixed base and on the thread's stack via
user32!CallNextHookEx) is running on our thread during dispatch.
- No
std::sync::Mutex / parking_lot / WaitOnAddress lock-wait frames
anywhere in the unwind.
Reading the (aggressive stack-scan; order not CFI-proven) live stack, there is
an outer event-loop wait and an inner/nested one, with
DispatchMessage → WndProc → Punto hook sandwiched between them.
So this is a re-entrant Win32 message-loop wait mediated by the layout
switcher's global hook — not a Rust mutex self-deadlock.
Fix
#4582 handles WM_INPUTLANGCHANGE by refreshing the cached keyboard layout and
then deferring to DefWindowProc (kept, so the message still propagates to
first-level child windows per the
Win32 docs).
On real hardware, the layout-cache refresh is the minimal change that stops the
freeze, and keeping DefWindowProc still fixes it.
Honest limitation: the operative change was isolated empirically, and we
have proven what it is not (a mutex deadlock). The precise reason a cache
refresh prevents the re-entrant wait is not fully traced — Punto Switcher's hook
is closed-source and the dump shows no layout-loading frames at the freeze
point.
Related
Summary
On Windows, a winit application freezes ("Not Responding") when the keyboard
layout is switched by an automatic layout switcher that installs a global
Windows hook — specifically Yandex Punto Switcher (
pshook64.dll). Thewindow stops pumping messages and only killing the process recovers it.
This has surfaced widely in Warp (which is built on winit):
warpdotdev/warp#10050, warpdotdev/warp#8675, warpdotdev/warp#9967,
warpdotdev/warp#7073.
A fix is proposed in #4582 (originally found and fixed in Warp's fork,
warpdotdev#15).
Environment
switch (e.g. English ↔ Russian).
x86_64-pc-windows-gnu) thatjust logs key/IME/focus events. Reproducing requires a real layout switch at
the keyboard.
Steps to reproduce
WM_INPUTLANGCHANGE).hotkey).
Root cause (from a full minidump of the frozen process)
A full dump was captured with
procdump -maon the frozen window (original,unpatched code) and walked with
minidump-stackwalkusing symbols generatedfrom the local system DLLs, so the x64
.pdataunwind/CFI is exact.Reliable top of the main thread (instruction pointer + call-frame-info frames):
MsgWaitForMultipleObjectsEx— winit'sevent-loop wait (
wait_for_messages_impl, theMsgWaitForMultipleObjectsEx(QS_ALLINPUT, MWMO_INPUTAVAILABLE)call) —re-entered during message dispatch.
pshook64.dll(Punto Switcher's injected global hook; present in themodule list at a fixed base and on the thread's stack via
user32!CallNextHookEx) is running on our thread during dispatch.std::sync::Mutex/parking_lot/WaitOnAddresslock-wait framesanywhere in the unwind.
Reading the (aggressive stack-scan; order not CFI-proven) live stack, there is
an outer event-loop wait and an inner/nested one, with
DispatchMessage → WndProc → Punto hooksandwiched between them.So this is a re-entrant Win32 message-loop wait mediated by the layout
switcher's global hook — not a Rust mutex self-deadlock.
Fix
#4582 handles
WM_INPUTLANGCHANGEby refreshing the cached keyboard layout andthen deferring to
DefWindowProc(kept, so the message still propagates tofirst-level child windows per the
Win32 docs).
On real hardware, the layout-cache refresh is the minimal change that stops the
freeze, and keeping
DefWindowProcstill fixes it.Honest limitation: the operative change was isolated empirically, and we
have proven what it is not (a mutex deadlock). The precise reason a cache
refresh prevents the re-entrant wait is not fully traced — Punto Switcher's hook
is closed-source and the dump shows no layout-loading frames at the freeze
point.
Related
WM_INPUTLANGCHANGEmessage this PR handles)