Skip to content

Super + K hangs and spawns CPU-bound Lua processes when config iterates hl.get_workspaces() #13979

Closed as duplicate

Description

@jermartinz

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

  1. 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
  2. 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
  3. 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

  1. manuaudio commented on Oct 1, 2026

    @manuaudio
    Contributor

    The self-returning noop mock making ipairs() 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. Your timeout 5s reproduction with a temporary XDG_CACHE_HOME is a handy way to verify whichever lands.

  2. wizenith commented on Oct 2, 2026

    @wizenith

    Independent confirmation on 4.0.4-1 with a different trigger than the original report: a module loaded by hyprland.lua that iterates hl.get_windows() (the usual pattern for applying window rules to already-open windows at load):

    local windows = hl.get_windows()
    for _, w in ipairs(windows) do protect(w) end

    Reproduced end-to-end with a throwaway HOME/XDG_CACHE_HOME whose hyprland.lua contains only for _, w in ipairs(hl.get_windows()) do end:

    script fixture result
    installed 4.0.4-1 ipairs(hl.get_windows()) exit 124 after 5 s, 0 output lines
    temp copy + PR #12712's hunk same exit 0, 235 output lines
    temp copy + PR #12916's instruction-budget hunk same exit 0, 235 output lines
    temp copy + PR #12712's hunk while true do end exit 124 — still hangs
    temp copy + PR #12916's instruction-budget hunk while true do end exit 0 in 0.66 s, budget error logged with DEBUG=1

    Impact I hit: 11 Super+K presses within ~40 s (2026-10-01 11:23) left 11 lua processes spinning — R state, ~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 parent omarchy-menu-keybindings bash 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:

    Workaround applied locally in the meantime — guard the enumeration in the user config:

    local windows = hl.get_windows()
    if type(windows) == "table" and #windows > 0 then
      for _, w in ipairs(windows) do protect(w) end
    end

    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.

  3. added 4 commits that reference this issue on Oct 5, 2026
    cbfcf6f
    0a058ce
    e42e45c
    9cafe63
  4. omarchybot commented on Oct 13, 2026

    @omarchybot
    Collaborator

    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() in bin/omarchy-menu-keybindings runs the user's hyprland.lua against a mocked hl whose fallback noop table answers every key, numeric ones included, with itself (still at bin/omarchy-menu-keybindings:209 on current quattro). So ipairs() over any unstubbed list getter never reaches nil, and the lua scan spins forever. #7025 names hl.get_workspaces() alongside get_monitors() and get_windows(); this report and the get_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_HOME and timeout, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingduplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions