Repository navigation
Super + K hangs and spawns CPU-bound Lua processes when config iterates hl.get_workspaces() #13979
Description
System details
- Omarchy:
4.0.4-1 - Hyprland:
0.56.2-2 - Lua:
5.5.1-1 - Kernel:
7.2.5-3-omarchy(x86_64) - CPU: AMD Ryzen 5 5600X (6 cores / 12 threads)
- GPU: NVIDIA GeForce RTX 3060
What's wrong?
Opening the keybindings menu with Super + K hangs while scanning a custom Lua Hyprland configuration containing a top-level ipairs(hl.get_workspaces()) loop. Each invocation leaves a Lua process consuming approximately 100% of one CPU core. Four concurrent processes were observed at 98–99% CPU each, with only about 3 MB RSS per process.
The configuration uses this loop to apply workspace-to-monitor rules to existing workspaces. The failure occurs in the menu's standalone Lua scanner, rather than in the compositor's execution of the configuration.
Steps to reproduce
-
In a Lua-based Hyprland configuration, add a top-level iteration over workspaces. This minimal loop is enough to exercise the scanner problem:
for _, ws in ipairs(hl.get_workspaces()) do -- Even an empty body never terminates under the menu's mock. end
-
Trigger a cache miss and run the menu. To test without changing the normal cache or leaving another runaway process, use a temporary cache and a bounded command:
task_cache_dir=$(mktemp -d) XDG_CACHE_HOME="$task_cache_dir" timeout 5s omarchy menu keybindings --print
-
The command emits no keybindings and times out with exit status
124. On a cache miss, pressing Super + K without a timeout leaves the scanner spinning; repeated attempts accumulate CPU-bound Lua processes.
Expected: The keybindings scan terminates and the menu opens. Runtime workspace queries should not cause an unbounded scan.
Actual: The scanner loops indefinitely and the menu cannot finish building its cache.
Confirmed cause
In build_lua_bind_cache(), the scanner executes the user's configuration with pcall(dofile, config), using a mocked hl API. Unimplemented methods resolve to the noop table. Its __call returns that same table, and its __index returns that same table for every key:
local noop
noop = setmetatable({}, {
__index = function()
return noop
end,
__call = function()
return noop
end,
})Consequently, hl.get_workspaces() returns noop; ipairs keeps indexing positive integers, receiving a non-nil value forever. pcall does not help because no error is raised.
The same mock is present in the official source checked at commit c05d90196fc0dd5c21e2e797d80ffcc60d5e39fa. The runtime reproduction below was performed with the installed 4.0.4-1 script.
Diagnostic evidence and candidate fix
An instruction-limited copy of the extracted scanner aborts with a traceback pointing to the top-level workspace loop in the user's hyprland.lua:
SCAN INSTRUCTION LIMIT
stack traceback:
...diagnostic.lua:5: in hook '?'
.../hyprland.lua:63: in main chunk
[C]: in function 'dofile'
[C]: in global 'pcall'
...diagnostic.lua:158: in main chunk
Adding an explicit stub to the mocked hl table fixes this specific reproduction:
get_workspaces = function()
return {}
end,Verified using the full menu script, a temporary cache, and a five-second timeout:
Original installed script: exit 124, 0 output lines
Patched temporary copy: exit 0, 224 output lines
The patched extracted Lua scanner also terminates successfully and emits 223 binding records. No installed scripts or user configuration were modified for these tests.
An explicit empty collection stub addresses get_workspaces(). A time or instruction budget around configuration scanning would additionally prevent other custom configuration code from leaving unbounded CPU-bound scanners.
Activity
manuaudio commentedon Oct 1, 2026
The self-returning
noopmock makingipairs()loop forever is already addressed in open PRs: #8876 ("Stop keybinding scans from looping on mocked APIs") and #12712 ("Fix infinite iteration in keybinding scanner API stubs"). #7564 and #12916 also harden the same scan. Yourtimeout 5sreproduction with a temporaryXDG_CACHE_HOMEis a handy way to verify whichever lands.wizenith commentedon Oct 2, 2026
Independent confirmation on
4.0.4-1with a different trigger than the original report: a module loaded byhyprland.luathat iterateshl.get_windows()(the usual pattern for applying window rules to already-open windows at load):Reproduced end-to-end with a throwaway
HOME/XDG_CACHE_HOMEwhosehyprland.luacontains onlyfor _, w in ipairs(hl.get_windows()) do end:ipairs(hl.get_windows())while true do endwhile true do endDEBUG=1Impact I hit: 11 Super+K presses within ~40 s (2026-10-01 11:23) left 11
luaprocesses spinning —Rstate, ~90% of a core each, 26 h runtime (~12 CPU-hours each), ~3 MB RSS, and only ~338 read syscalls total (pure userspace spin, no I/O). On a 12-thread Ryzen 5 4600G that pinned ~10 cores: 100% CPU, load average ~15, until I killed them manually. Each stuck scan also kept its parentomarchy-menu-keybindingsbash processes alive (22 of them) waiting on the Lua process substitution, so the menu never recovers on its own.Triage notes from testing the open PRs:
noop.__index(Fix infinite iteration in keybinding scanner API stubs #12712), not a per-method empty-list stub. The candidate fixget_workspaces = function() return {} endonly covers that one method;hl.get_windows(),hl.get_monitors(),hl.get_loaded_plugins(), … all reproduce the same loop.while true do endstill times out). fix(menu): contain the keybindings Lua bind scan #12916's instruction budget contains it and the menu still renders the hyprctl binds. Landing both would cover the whole class.keybindings.??????scratch files matches what I saw as well: killed scans leave them behind (I had 10 to clean; the records-pruning glob never matches those names).Workaround applied locally in the meantime — guard the enumeration in the user config:
After that the same command completes in ~340 ms.
Environment: Omarchy 4.0.4-1, Hyprland 0.56.2, kernel 7.2.5-4-omarchy, AMD Ryzen 5 4600G (12 threads), Radeon Vega (Renoir). All tests above used temp copies of the script; no installed files were modified.
Stop keybindings menu hanging on configs that iterate runtime queries
Stop keybindings menu hanging on configs that iterate runtime queries
Stop keybindings menu hanging on configs that iterate runtime queries
Stop keybindings menu hanging on configs that iterate runtime queries
omarchybot commentedon Oct 13, 2026
This is the same bug as #7025, which is older and still open, so I'm closing this one as its duplicate.
Both describe the same defect and the same fix:
build_lua_bind_cache()inbin/omarchy-menu-keybindingsruns the user'shyprland.luaagainst a mockedhlwhose fallbacknooptable answers every key, numeric ones included, with itself (still atbin/omarchy-menu-keybindings:209on currentquattro). Soipairs()over any unstubbed list getter never reachesnil, and theluascan spins forever. #7025 nameshl.get_workspaces()alongsideget_monitors()andget_windows(); this report and theget_windows()confirmation above are the same mechanism through a different getter.The open fixes are tracked on #7025: #7564 makes the scan safe against load-time reads, #12712 makes the stub stop answering numeric keys, and #12916 adds an instruction budget so a config that loops on its own is contained too. Which lands is the maintainer's call. The reproduction with a temporary
XDG_CACHE_HOMEandtimeout, and the measurements in the comment above comparing #12712 and #12916, are useful evidence for that decision and are worth adding on #7025.Checked by Claude Opus 5.5 against the source on
quattro, and separately by Codex at medium effort, which agreed the two are the same bug. If this turns out to be a different problem, it can be reopened.