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

@chilango74

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions