Skip to content

[Windows] Event loop freezes when an injected global hook (Punto Switcher) re-enters the message-loop wait on keyboard layout switch #4584

Description

@chilango74

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

  1. Run Punto Switcher (or any tool that installs a global hook and drives
    WM_INPUTLANGCHANGE).
  2. Run a winit app on Windows and focus its window.
  3. Let Punto switch the layout (type text it auto-corrects, or use its switch
    hotkey).
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions