From 7b3fa2f0b3049c16dbfa244c716f8c5c6bf63afa Mon Sep 17 00:00:00 2001 From: Bob Myrick Date: Tue, 25 Aug 2026 21:20:05 -0400 Subject: [PATCH] fix(capture): restore key overlays after hardening Validate the systemd runtime path using the process effective UID instead of shell ownership probes that fail in Hyprland's embedded Lua runtime. Keep Quickshell FileView watchers live by updating their fixed, private state paths in place and seed both state files when the hook loads. --- key-visualizer.lua | 67 +++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/key-visualizer.lua b/key-visualizer.lua index 089b671..b6d03c8 100644 --- a/key-visualizer.lua +++ b/key-visualizer.lua @@ -14,18 +14,29 @@ local function shell_quote(s) return "'" .. s:gsub("'", "'\\''") .. "'" end +local function effective_uid() + local f = io.open("/proc/self/status", "r") + if not f then return nil end + for line in f:lines() do + local uid = line:match("^Uid:%s+%d+%s+(%d+)") + if uid then + f:close() + return uid + end + end + f:close() + return nil +end + local function is_runtime_secure(r) if not r or r == "" then return false end if r == "/tmp" then return false end if r:sub(1, 1) ~= "/" then return false end - local q = shell_quote(r) - -- Must be a directory owned by the current user and, if stat is - -- available, mode 0700 (systemd's XDG_RUNTIME_DIR default). The - -- stat check is skipped when stat is missing so we don't fail-closed - -- on minimal containers. - local cmd = "test -d " .. q .. " && test -O " .. q .. " && { p=$(stat -c %a " .. q .. " 2>/dev/null); [ -z \"$p\" ] || [ \"$p\" = \"700\" ]; }" - local res = os.execute(cmd) - return res == 0 or res == true + -- Shell ownership probes fail from Hyprland's embedded Lua environment. + -- /run/user is root-controlled, so accepting only this process's systemd + -- runtime path still excludes shared or caller-supplied directories. + local uid = effective_uid() + return uid ~= nil and r == "/run/user/" .. uid end local runtime = os.getenv("XDG_RUNTIME_DIR") @@ -38,36 +49,19 @@ if not is_runtime_secure(runtime) then runtime = nil end local STATE_FILE = runtime and (runtime .. "/omarchy-key-visualizer.json") or nil +local SUPER_FLAG = runtime and (runtime .. "/omarchy-key-visualizer-super") or nil local function secure_write(path, content) - if not path then return false end - -- Reject the exact predictable fallback that the security review flagged. - -- A valid XDG_RUNTIME_DIR under /tmp with a random suffix and 0700 is - -- allowed because it passed is_runtime_secure(). - if path == "/tmp/omarchy-key-visualizer.json" or path == "/tmp/omarchy-key-visualizer-super" then return false end - local q = shell_quote(path) - -- Refuse to follow a symlink at the destination (O_NOFOLLOW mitigation). - -- `test ! -L` succeeds when the file does not exist or is not a symlink; - -- it fails only when the destination is a symlink, which we must not follow. - local not_symlink = os.execute("test ! -L " .. q) - if not (not_symlink == 0 or not_symlink == true) then - print("[key-visualizer] refusing to write symlink: " .. path) - return false - end - local tmp = path .. ".tmp" - local f = io.open(tmp, "w") + if not runtime or (path ~= STATE_FILE and path ~= SUPER_FLAG) then return false end + -- FileView watches the existing inode, so replacing the path on every key + -- leaves Quickshell attached to an unlinked file. Updating in place keeps + -- the watcher live. The containing runtime directory is private to the + -- effective user and the two accepted paths are fixed above. + local f = io.open(path, "w") if not f then return false end f:write(content) f:close() - os.execute("chmod 600 " .. shell_quote(tmp) .. " 2>/dev/null") - -- Atomic replace; avoids truncating a file that may have been swapped - -- between the symlink check and the open (TOCTOU mitigation). - local ok = os.rename(tmp, path) - if not ok then - os.execute("rm -f " .. shell_quote(tmp) .. " 2>/dev/null") - return false - end - os.execute("chmod 600 " .. q .. " 2>/dev/null") + os.execute("chmod 600 " .. shell_quote(path) .. " 2>/dev/null") return true end @@ -200,7 +194,6 @@ end -- Super-held flag: the panel/display watches this to know when to capture the -- SUPER+drag on the overlay (instead of a window underneath). Written only on -- transitions so it does not spam the filesystem on every key. -local SUPER_FLAG = runtime and (runtime .. "/omarchy-key-visualizer-super") or nil local last_super = nil local function super_down() return pressed[133] or pressed[134] @@ -213,6 +206,12 @@ local function emit_super() secure_write(SUPER_FLAG, down and "1" or "0") end +-- Create both files when the hook loads. This clears stale state after a +-- compositor restart and gives FileView stable paths to watch before the +-- first keyboard event arrives. +emit() +emit_super() + -- Combos: a combination of keys is treated as a unit. The display only -- updates on key-down (the combo grows as you press) and when the last key -- is released (the empty payload starts the panel's linger with the last