[Bug 60059] New: Lock-key state (CapsLock/NumLock) oscillates on every keystroke when any global WH_KEYBOARD_LL hook exists — adjust_lock_state defeated by its own deferred synthetic events
http://bugs.winehq.org/show_bug.cgi?id=60059 Bug ID: 60059 Summary: Lock-key state (CapsLock/NumLock) oscillates on every keystroke when any global WH_KEYBOARD_LL hook exists — adjust_lock_state defeated by its own deferred synthetic events Product: Wine Version: 11.13 Hardware: x86-64 OS: Linux Status: UNCONFIRMED Severity: normal Priority: P2 Component: winex11.drv Assignee: wine-bugs@list.winehq.org Reporter: me@papayadev.net Target Milestone: --- Distribution: --- Created attachment 81552 --> http://bugs.winehq.org/attachment.cgi?id=81552 minimal reproducer: pass-through global WH_KEYBOARD_LL + lock-state logging (modes: control/hook/eat) winex11's lock-key reconciliation defeats itself whenever any global WH_KEYBOARD_LL hook exists on the desktop, permanently corrupting the desktop-wide CapsLock/NumLock toggle state on every keystroke, for every process in the prefix. Mechanism, current git (byte-identical from wine-11.10 through master): update_lock_state (dlls/winex11.drv/keyboard.c:1472, called from X11DRV_KeyEvent for every X key event) detects a toggle mismatch against the X event state mask and calls adjust_lock_state (keyboard.c:1452), which sends a synthetic lock-key down+up via NtUserSendHardwareInput and immediately re-reads NtUserGetAsyncKeyboardState to detect whether a keyboard hook blocked the change. But with a global WH_KEYBOARD_LL present, the server defers the keystate commit until the hook thread replies: queue_keyboard_message only calls queue_hardware_message — which contains the sole keystate update, update_desktop_key_state at server/queue.c:1859 — after the LL hook result arrives (queue.c:2489, send_hook_ll_message; commit on reply in store_message_result). And the driver's send never waits for that: DECL_HANDLER(send_hardware_message) sets sender=NULL unless SEND_HWMSG_INJECTED (queue.c:3232, :3265), so hardware-origin sends are fire-and-forget. The immediate re-read in adjust_lock_state is therefore deterministically stale (in the common case the hook belongs to the app's main thread, which is currently inside X11DRV_KeyEvent and cannot possibly have processed the hook callback yet). The code then WARNs "keystate %x not changed, probably blocked by hooks" and force-writes the corrected toggle via set_async_key_state — the intended hook-defeat behavior — but the deferred synthetic KEYDOWN then passes the (non-blocking) hook, commits, and toggles the desktop-wide state right back (update_key_state, queue.c). The next real key event detects the mismatch again and the cycle repeats forever: with a seeded mismatch the lock toggles flip on every keystroke, visibly randomizing character case mid-line in any app on the desktop. A mismatch seed is trivial: start wineserver with physical NumLock ON (Wine's initial toggle is off), or toggle CapsLock while the Wine window is unfocused. Reproducer (attached, ditest3.c, plain user32/gdi32, i686-w64-mingw32-gcc -O2 -o ditest3.exe ditest3.c -luser32 -lgdi32): it installs a PASS-THROUGH global WH_KEYBOARD_LL (CallNextHookEx only) on its pumping thread, mimicking what many games do. With physical NumLock ON and a fresh wineserver: WINEDEBUG=-all,+key,+keyboard wine ditest3.exe hook, then type ~20 letters. Observed (wine 11.13, X11): 277 of 277 reconcile attempts fail with "probably blocked by hooks", "Adjusting NumLock state (0x40)" fires on every key event, GetKeyState(VK_NUMLOCK)&1 flips per keystroke, and after toggling CapsLock out of focus the typed text comes out with random per-character case ("the QuicK brown fox jumps Over the lazy dog"). Control run without the hook (ditest3.exe): the same mismatch converges silently after one keystroke, zero WARNs — confirming the LL hook deferral is the sole trigger. A third mode (ditest3.exe eat) has the hook genuinely eat (return 1) the lock-key events: the force-write then sticks and the state converges with exactly one WARN per genuine desync — i.e. the existing hook-defeat force-write works correctly whenever the events are truly blocked; only the delayed-but-not-blocked case breaks it. This is a regression introduced by cbc657a244d1 ("winex11.drv: Force changing lock keys state if hooks blocked keyboard input processing.", 2018). Before that commit, a correction deferred behind a non-blocking low-level hook simply committed late and converged one keystroke later. With it, the force-write plus the later deferred commit toggle the state twice per reconcile, sustaining the oscillation indefinitely. The case that commit was written for — a hook genuinely blocking lock keys — still needs the force-write, so a plain revert is not the fix either; the problem is that the "did the state change" check cannot distinguish blocked from deferred. Real-world impact: Final Fantasy XI installs such a global WH_KEYBOARD_LL, and one running instance permanently corrupts Caps/NumLock behavior for every Wine process in the prefix (randomly flipping character case while typing). Any app with a global LL keyboard hook should trigger the same. Suggested fix directions: commit the keystate change of driver-origin lock-key corrections before/independently of the LL-hook round trip; or have adjust_lock_state skip the synthetic events and only force-write when a global WH_KEYBOARD_LL is present; or make the "did it change" check wait for / account for the deferred commit rather than misreading deferral as blocking. Related but distinct existing reports (checked 2026-07-24, none cover this defect): bug 35907 (caps state confused across processes — thread-vs-async state propagation, 2014 analysis predating the adjust_lock_state force-write; no LL hook involved, no per-keystroke oscillation) ; bug 59888 (numlock changes only detected while focused — the mismatch-seeding condition, not the oscillation; reconciliation-on-next-key is expected, this report is about the reconcile then un-doing itself); bug 57958 (adjust_lock_state falsely triggering while physically HOLDING CapsLock with the toggle on — a trigger-condition bug in the same function, no hook required; open MR !11315 addresses only that trigger condition and does not touch the LL-hook deferral). The control run (no hook: converges silently in one keystroke) isolates this report from all three. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 Stian Low <wineryyyyy@gmail.com> changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |wineryyyyy@gmail.com --- Comment #1 from Stian Low <wineryyyyy@gmail.com> --- If there's a human behind this book, please train/guide your AI to produce reports better for human consumption and account for human intelligence/psychology so their eyes glaze over less. If cbc657a244d1 is a regression then clearly state it rather than hiding in a book of text. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 me@papayadev.net changed: What |Removed |Added ---------------------------------------------------------------------------- Keywords| |regression --- Comment #2 from me@papayadev.net --- Yes, I'm a real human with a real issue. Apologies for the overly dense text above — I had AI help me draft it from my testing notes and should have condensed it before posting. The attachment is just a small program that cleanly reproduces the problem; it originally showed up in real games I run. Final Fantasy XI is where I first noticed it. The short version: this is a regression from cbc657a244d1 ("winex11.drv: Force changing lock keys state if hooks blocked keyboard input processing.", 2018-10-23), and it's still present in current git. Whenever any app on the desktop has a global WH_KEYBOARD_LL hook (lots of games install one), the CapsLock and NumLock state starts flipping on every keystroke, for every process in the prefix. Typing comes out in random case, like "the QuicK brown fox jumps Over the lazy dog". To reproduce with the attached ditest3.c (no game needed): 1. Make sure physical NumLock is on, and start from a fresh wineserver. 2. WINEDEBUG=-all,+key,+keyboard wine ditest3.exe hook 3. Type a few letters into the window. You'll see "adjust_lock_state ... probably blocked by hooks" warnings on every keystroke (277 out of 277 in my log), and GetKeyState(VK_NUMLOCK)&1 flips with every key. Run it again without the "hook" argument and the same NumLock mismatch gets fixed up silently on the first keystroke and stays in sync — that's the expected behavior, and it shows the LL hook is the trigger. What's going on: adjust_lock_state() sends a synthetic lock-key down/up, then immediately re-reads the key state, and if nothing changed it assumes a hook blocked the event and force-writes the toggle bit. The problem is that when a low-level hook is installed, the server doesn't apply the key state until the hook thread has responded, and the driver's send doesn't wait for that. So the re-read always sees stale state, the force-write happens, and then the deferred synthetic KEYDOWN gets applied anyway and flips the toggle right back. That repeats on every key event. Before cbc657a244d1 the delayed event simply applied late and everything converged; the force-write is what turned this case into a permanent loop. I have a patch ready and will open an MR shortly. The approach is to not route the driver's own lock-sync events through low-level hooks at all — Windows never generates events like these, so no hook on Windows ever sees an equivalent — and that also makes the state change synchronous again, so the existing "did it change" check works as intended. Simply reverting cbc657a244d1 wouldn't be right either, since hooks that genuinely block lock keys (bug 42891) still need the force-write. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #3 from me@papayadev.net --- Created attachment 81554 --> http://bugs.winehq.org/attachment.cgi?id=81554 Log from my "ditest3.exe" run -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #4 from me@papayadev.net --- Created attachment 81555 --> http://bugs.winehq.org/attachment.cgi?id=81555 Log from my "ditest3.exe hook" run -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #5 from me@papayadev.net --- Created attachment 81556 --> http://bugs.winehq.org/attachment.cgi?id=81556 Log from my "ditest3.exe eat" run -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 Alexandre Julliard <julliard@winehq.org> changed: What |Removed |Added ---------------------------------------------------------------------------- Summary|Lock-key state |Lock-key state |(CapsLock/NumLock) |(CapsLock/NumLock) |oscillates on every |oscillates on every |keystroke when any global |keystroke when any global |WH_KEYBOARD_LL hook exists |WH_KEYBOARD_LL hook exists |— adjust_lock_state | |defeated by its own | |deferred synthetic events | -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #6 from Stian Low <wineryyyyy@gmail.com> --- Be aware that Wine has clean room policies against LLM patches because they pose too much legal risk because they could have been trained on Microsoft code which is a violation: - https://gitlab.winehq.org/wine/wine/-/wikis/Developer-FAQ LLMs also hallucinate potentially wasting dev time: https://bugs.winehq.org/show_bug.cgi?id=59830#c8 LLM reports are acceptable and appreciated but not code. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #7 from me@papayadev.net --- Understood, and thank you. I'll make sure to avoid any LLM generated code contributions. Fixing this issue will be a relatively small set of changes. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #8 from me@papayadev.net --- I've opened an MR for fixing this: https://gitlab.winehq.org/wine/wine/-/merge_requests/11476 -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 Chiitoo <chiitoo@gentoo.org> changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |chiitoo@gentoo.org --- Comment #9 from Chiitoo <chiitoo@gentoo.org> --- Pretty difficult to really understand the issue indeed, but I saw Final Fantasy XI and then the rest makes me think of a mix of https://bugs.winehq.org/show_bug.cgi?id=56532 and https://bugs.winehq.org/show_bug.cgi?id=59980 but both of those are a lot more recent than since 2018... So far I don't think I've noticed this there, but again, not sure I understand this fully, but I'll add a link to the appdb in any case. Haven't had a chance to try the merge request changes or look into them really, but I will say that the commit messages might need some work. :] (See for example: https://gitlab.winehq.org/wine/wine/-/wikis/Submitting-Patches#the-commit-me... -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #10 from Nathaniel Bass <nbass@papayadev.net> --- TLDR: Apps that use low-level keyboard hooks (which Final Fantasy XI seems to do apparently) cause various Caps/Num Lock state synchronization issues such as a Caps/Num Lock toggle occurring when the game doesn't have focus being ignored, and worse, when the system's Caps/Num Lock status is out of sync with Wine and application, results in typed characters randOmly pRoduCing flIpeD leTter casEs liKe thiS. Strangely, third-party tools like Windower, which alters how Final Fantasy XI receives keyboard input, happens to coincidentally fix this issue. It's not reproducible when using Windower. It is, however, reproducible running the vanilla game with no mods. It's also reproducible in the sample application I included. I think the reason it works on Windows and not on Wine is because Windows doesn't process Caps/Num Lock toggles through low level hooks - it skips them - and also applying that logic to Wine (see the MR) appears to fix the issue entirely. I hope this helps to illustrate the issue better. On the commit messages, it's late here but I'll review the guideline you linked tomorrow. This is my first time contributing here, so I'm not that familiar with the conventions of the project but I will try to make improvement. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #11 from Chiitoo <chiitoo@gentoo.org> --- It does seem a lot like what I have experienced with the regression discussed in bug 59980. I was thinking I had hit caps lock so much, but it ended up being something completely different. Can you still re-produce ths with current gt master? Or well, latest release, 11.14 works too. The capitalisation issues I had been seeing have been fixed already, but I will need to look into this better with better eyes and test things out too to see if it's a completely different issue that I somehow never noticed (or/and if it's somehow related to the issues I referenced). -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #12 from Nathaniel Bass <nbass@papayadev.net> --- I reproduced it on master as requested. Master still routes the synthetic lock-key down/up events through X11DRV_send_keyboard_input -> WH_KEYBOARD_LL, so an installed low-level hook can block them and desync Wine's keystate from the X server. Wine's attempt to correct for a lock-key event that occurred while no Wine application had focus therefore breaks when a low-level hook is installed: the correction itself gets blocked by the same hook, and the desync then persists across subsequent keystrokes rather than resolving. Low-level hooks are not intended to process this kind of input, so running it through them throws the lock-key state out of sync. The proposed fix is still relevant. I'll also be force pushing updated commit messages shortly to accommodate the feedback that was provided here. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 --- Comment #13 from Chiitoo <chiitoo@gentoo.org> --- Thanks! I think I got it now. I had been testing caps-lock a bunch recently, and thought the issue with that was gone, but it seems it indeed is not. It is odd that I haven't noticed it if it's been around since 2018, as I do alt-tab out from FFXI a lot, though well, I had new keyboards in testing for a while during the past weeks and I believe those made me accidenally hit caps-lock more... I couldn't compile Wine with my currently available toolchains to test the regression commit yet, but if you have confirmed it starts with cbc657a244d16d52414ab7c8319afa885c681822 I'd put that into the Regression SHA1 field, and I suppose Version found-in should then be 3.19. Didn't get to testing the proposed fix yet either, but indeed, can confirm the issues with caps-lock by, for example: - With Final Fantasy XI Online and a native Linux test editor, or Wine notepad open from a different Wine prefix, hit caps-lock in the text editor and switch to the game window to confirm weird things happening. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60059 Kagetaro078 <kagetaro1986@gmail.com> changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |kagetaro1986@gmail.com --- Comment #14 from Kagetaro078 <kagetaro1986@gmail.com> --- Created attachment 82308 --> http://bugs.winehq.org/attachment.cgi?id=82308 Test program (dinputtest.c), scripts and logs (NumLock on/off, with/without WH_KEYBOARD_LL hook) I can confirm this with Wine 11.19 and with current master (a15ee2cc, 2026-10-09: dlls/winex11.drv/keyboard.c is unchanged since 11.19), on Ubuntu 26.04.1 (KDE Plasma 6, Wayland session, Wine on XWayland) and in a plain Xvfb + openbox session. Besides the oscillating toggle state, applications also receive the fake lock key itself before every key: WM_KEYDOWN VK_NUMLOCK, and DIK_NUMLOCK from DirectInput. Plasma turns NumLock on at login, so it is on in X and off in Wine whenever a program starts. A game I use beeps on every key in its key binding screen because it gets NumLock first. Numbers from a small standalone test (attached dinputtest.c: window + DirectInput 8 keyboard, -hook installs a pass-through WH_KEYBOARD_LL hook; 14 keys sent through XTEST, no input method): - NumLock in X / hook: Wine 11.19: VK_NUMLOCK / DIK_NUMLOCK / toggle at end - on / hook: 29 / 28 / 0 (64-bit: 29 / 28 / 0) - on / no hook: 1 / 1 / 1 (64-bit: 0 / 0 / 1) - off / hook: 0 / 0 / 0 (64-bit: 0 / 0 / 0) - off / no hook: 0 / 0 / 0 (64-bit: 0 / 0 / 0) So with the hook the state is still wrong after 14 keys, and 29 fake NumLock key downs were delivered. On Windows 11 the same test with NumLock on (keys injected with SendInput) gives 0 / 0 and toggle 1, with and without the hook (logs in the archive). -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
participants (1)
-
WineHQ Bugzilla