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.
This commit is contained in:
+33
-34
@@ -14,18 +14,29 @@
|
|||||||
|
|
||||||
local function shell_quote(s) return "'" .. s:gsub("'", "'\\''") .. "'" end
|
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)
|
local function is_runtime_secure(r)
|
||||||
if not r or r == "" then return false end
|
if not r or r == "" then return false end
|
||||||
if r == "/tmp" then return false end
|
if r == "/tmp" then return false end
|
||||||
if r:sub(1, 1) ~= "/" then return false end
|
if r:sub(1, 1) ~= "/" then return false end
|
||||||
local q = shell_quote(r)
|
-- Shell ownership probes fail from Hyprland's embedded Lua environment.
|
||||||
-- Must be a directory owned by the current user and, if stat is
|
-- /run/user is root-controlled, so accepting only this process's systemd
|
||||||
-- available, mode 0700 (systemd's XDG_RUNTIME_DIR default). The
|
-- runtime path still excludes shared or caller-supplied directories.
|
||||||
-- stat check is skipped when stat is missing so we don't fail-closed
|
local uid = effective_uid()
|
||||||
-- on minimal containers.
|
return uid ~= nil and r == "/run/user/" .. uid
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local runtime = os.getenv("XDG_RUNTIME_DIR")
|
local runtime = os.getenv("XDG_RUNTIME_DIR")
|
||||||
@@ -38,36 +49,19 @@ if not is_runtime_secure(runtime) then
|
|||||||
runtime = nil
|
runtime = nil
|
||||||
end
|
end
|
||||||
local STATE_FILE = runtime and (runtime .. "/omarchy-key-visualizer.json") or nil
|
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)
|
local function secure_write(path, content)
|
||||||
if not path then return false end
|
if not runtime or (path ~= STATE_FILE and path ~= SUPER_FLAG) then return false end
|
||||||
-- Reject the exact predictable fallback that the security review flagged.
|
-- FileView watches the existing inode, so replacing the path on every key
|
||||||
-- A valid XDG_RUNTIME_DIR under /tmp with a random suffix and 0700 is
|
-- leaves Quickshell attached to an unlinked file. Updating in place keeps
|
||||||
-- allowed because it passed is_runtime_secure().
|
-- the watcher live. The containing runtime directory is private to the
|
||||||
if path == "/tmp/omarchy-key-visualizer.json" or path == "/tmp/omarchy-key-visualizer-super" then return false end
|
-- effective user and the two accepted paths are fixed above.
|
||||||
local q = shell_quote(path)
|
local f = io.open(path, "w")
|
||||||
-- 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 f then return false end
|
if not f then return false end
|
||||||
f:write(content)
|
f:write(content)
|
||||||
f:close()
|
f:close()
|
||||||
os.execute("chmod 600 " .. shell_quote(tmp) .. " 2>/dev/null")
|
os.execute("chmod 600 " .. shell_quote(path) .. " 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")
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -200,7 +194,6 @@ end
|
|||||||
-- Super-held flag: the panel/display watches this to know when to capture the
|
-- 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
|
-- SUPER+drag on the overlay (instead of a window underneath). Written only on
|
||||||
-- transitions so it does not spam the filesystem on every key.
|
-- 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 last_super = nil
|
||||||
local function super_down()
|
local function super_down()
|
||||||
return pressed[133] or pressed[134]
|
return pressed[133] or pressed[134]
|
||||||
@@ -213,6 +206,12 @@ local function emit_super()
|
|||||||
secure_write(SUPER_FLAG, down and "1" or "0")
|
secure_write(SUPER_FLAG, down and "1" or "0")
|
||||||
end
|
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
|
-- 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
|
-- 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
|
-- is released (the empty payload starts the panel's linger with the last
|
||||||
|
|||||||
Reference in New Issue
Block a user