[Security] Stop USB device names from being executed as Hyprland Lua (#8129)

* Stop device names from being executed as Hyprland Lua

Hyprland input-device and monitor names come from USB descriptors and
hyprctl output, so they are attacker-influenceable, yet the toggle and
monitor commands interpolated them straight into hyprctl eval and into
generated Lua that Hyprland re-executes on every reload. The input-device
toggle keys are bound with locked = true, so a malicious USB name reached
Lua code execution from the lock screen; a persisted disable made it run
on every start. This closes that class everywhere it appeared.

- The touchpad/touchscreen disable is now the device name in a plain-text
  sidecar file, read back by a packaged Lua module on reload, never a
  generated Lua file. hyprctl eval Lua-quotes the name and control
  characters are rejected outright.
- Dropped the shipped *-disabled.lua templates so nothing seeds a
  disabled state to /etc/skel, making the name file the single source of
  truth read from a hardcoded ~/.local/state to match the sibling tools.
- The reload loader excludes those two legacy filenames, so a leftover
  generated *-disabled.lua on a not-yet-migrated install can never be
  sourced as code again; a migration then recovers the device name from
  it and deletes it, sanitizing installs that ran the vulnerable version.
- All four monitor scripts (internal, mirror, clamshell, scaling) now
  validate an output name against a plain-connector-name pattern before
  writing it as Lua, closing the same latent pattern in the siblings.
- paths.lua treats a set-but-empty XDG_STATE_HOME as unset, matching the
  bash side so state is never read from the filesystem root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144ZDt44vtxjyF8j9Y88NrM

* Let a failing Lua assertion fail the test

lua discards the status of a chunk read from stdin, so a blown assert printed its traceback and still exited 0: the surrounding `set -euo pipefail` never fired and the following `pass` printed `ok`. Every Lua block in these two files was unenforced, including the assertion that a quoted `hyprctl eval` cannot reach `os.execute` and the negative control that proves the test can detect the injection at all. Passing the chunk as a script argument makes lua report the failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Re-apply a recovered input-device disable to the running session

The package hook reloads Hyprland during `omarchy-update-system-pkgs`, before `omarchy-migrate` runs, and at that reload the generated Lua is already excluded while the name file does not exist yet — so a touchpad or touchscreen the user had switched off comes back on, and stays on until their next login. Reload once more once the name has been recovered, which is the same path a login already takes to read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <codex@openai.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Omarchybot <omabot@omarchy.org>
Co-authored-by: Codex XHigh <codex@openai.com>
This commit is contained in:
Adrian Rangel
2026-08-25 11:03:12 +02:00
committed by GitHub
co-authored by Claude Fable 5 Codex XHigh Omarchybot
parent 4637735aa2
commit 9285b19d6a
14 changed files with 612 additions and 27 deletions
+8
View File
@@ -11,6 +11,14 @@ MONITOR_LUA="$HOME/.config/hypr/monitors.lua"
INTERNAL=$(omarchy-hyprland-monitor-laptop)
# INTERNAL is written into generated Lua and hyprctl eval/dispatch below, so a
# name that is not a plain connector string could execute on the next reload.
# Names come from hyprctl; a user-created headless output can carry anything.
if [[ -n $INTERNAL && ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Refusing unsafe internal monitor name" >&2
exit 1
fi
valid_scale() {
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
}
+7
View File
@@ -28,6 +28,13 @@ off() {
exit 1
fi
# The name is written into generated Lua below, so only a plain connector
# name may pass; anything else could execute on the next reload.
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
omarchy-notification-send -g 󰍹 "Refusing unsafe monitor name"
exit 1
fi
if ! omarchy-hyprland-monitor-external-active; then
omarchy-notification-send -g 󰍹 "Can't disable the only active display"
exit 1
@@ -22,6 +22,15 @@ on() {
exit 1
fi
# Both names are written into generated Lua below, so only plain connector
# names may pass; a user-created headless output can carry any name.
for output in "$INTERNAL" "$EXTERNAL"; do
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
omarchy-notification-send -g 󰍹 "Refusing unsafe monitor name"
exit 1
fi
done
omarchy-hyprland-toggle $DISABLE_TOGGLE off
if omarchy-hyprland-toggle-disabled $TOGGLE; then
+8
View File
@@ -80,6 +80,14 @@ set_scale() {
local width="$(echo "$monitor_info" | jq -r '.width')"
local height="$(echo "$monitor_info" | jq -r '.height')"
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
# active_monitor is written into the Lua string eval'd below, so only a plain
# connector name may pass; a hostile output name could execute otherwise.
if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Refusing unsafe monitor name" >&2
exit 1
fi
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
# factor even when the monitor scale itself is fractional.
+40 -14
View File
@@ -7,44 +7,70 @@
KIND="${1:-}"
ACTION="${2:-toggle}"
usage() {
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
}
case "$KIND" in
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
*)
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
usage
exit 1
;;
esac
# Hyprland sources this directory on reload, so the disabled state survives restarts
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
# The persisted disable is the device name stored as plain data; on every
# reload default/hypr/disabled-input-device.lua reads it back and disables the
# device. Names come from USB descriptors and must not be interpolated into
# shell or Lua. The path is hardcoded to ~/.local/state like the sibling
# toggle tools, so it keeps working when XDG_STATE_HOME diverges.
NAME_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled-name"
device="$("omarchy-hw-$KIND")"
if [[ -z $device ]]; then
echo "No $KIND device found" >&2
exit 1
fi
require_device() {
if [[ -z $device ]]; then
echo "No $KIND device found" >&2
exit 1
fi
if [[ $device == *[[:cntrl:]]* ]]; then
echo "Invalid $KIND device name" >&2
exit 1
fi
}
apply_device() {
local enabled=$1
local quoted=${device//\\/\\\\}
quoted=${quoted//\"/\\\"}
hyprctl eval "hl.device({ name = \"$quoted\", enabled = $enabled })" >/dev/null
}
enable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
rm -f "$STATE_FILE"
# Clear the persisted state before requiring a usable device, so a device
# that stops reporting a valid name can never wedge the disable in place.
rm -f "$NAME_FILE"
require_device
apply_device true
omarchy-osd -i "$ICON" -m "$LABEL enabled"
}
disable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
mkdir -p "$(dirname "$STATE_FILE")"
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
require_device
apply_device false
mkdir -p "$(dirname "$NAME_FILE")"
printf '%s\n' "$device" >"$NAME_FILE"
omarchy-osd -i "$ICON" -m "$LABEL disabled"
}
case "$ACTION" in
on) enable ;;
off) disable ;;
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
toggle) if [[ -f $NAME_FILE ]]; then enable; else disable; fi ;;
*)
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
usage
exit 1
;;
esac
+21
View File
@@ -0,0 +1,21 @@
-- Disable a Hyprland input device whose name was stored as data, not Lua.
-- Device names come from USB descriptors and must never be loaded as code.
local paths = require("default.hypr.paths")
return function(kind)
-- Hardcoded to ~/.local/state to match omarchy-toggle-input-device and the
-- sibling bash toggle tools, which all write there regardless of
-- XDG_STATE_HOME.
local file = io.open(paths.home .. "/.local/state/omarchy/toggles/hypr/" .. kind .. "-disabled-name", "r")
if not file then
return
end
local name = file:read("*l")
file:close()
if name and name ~= "" then
hl.device({ name = name, enabled = false })
end
end
+13 -3
View File
@@ -4,9 +4,19 @@
local home = os.getenv("HOME")
-- A variable that is set but empty means "unset" (XDG Base Directory spec);
-- bash's ${VAR:-fallback} in the sibling tools treats it the same way.
local function env_or(name, fallback)
local value = os.getenv(name)
if value == nil or value == "" then
return fallback
end
return value
end
return {
home = home,
config_home = os.getenv("XDG_CONFIG_HOME") or (home .. "/.config"),
state_home = os.getenv("XDG_STATE_HOME") or (home .. "/.local/state"),
omarchy_path = os.getenv("OMARCHY_PATH") or "/usr/share/omarchy",
config_home = env_or("XDG_CONFIG_HOME", home .. "/.config"),
state_home = env_or("XDG_STATE_HOME", home .. "/.local/state"),
omarchy_path = env_or("OMARCHY_PATH", "/usr/share/omarchy"),
}
+14 -8
View File
@@ -4,6 +4,8 @@
-- Pass a module prefix for normal package.path modules, e.g.
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
-- Pass nil as the prefix when the directory itself has been added to package.path.
-- Pass options.exclude as a set of base names (without ".lua") to skip; a legacy
-- file that must never be loaded as code stays on disk for a migration to remove.
local M = {}
@@ -12,19 +14,23 @@ local function shell_quote(path)
end
function M.files(dir, module_prefix, options)
local exclude = options and options.exclude or {}
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
if handle then
for filename in handle:lines() do
local module = filename:gsub("%.lua$", "")
if module_prefix then
module = module_prefix .. "." .. module
end
local name = filename:gsub("%.lua$", "")
if not exclude[name] then
local module = name
if module_prefix then
module = module_prefix .. "." .. module
end
if options and options.reload then
package.loaded[module] = nil
end
if options and options.reload then
package.loaded[module] = nil
end
require(module)
require(module)
end
end
handle:close()
end
+15 -1
View File
@@ -4,6 +4,20 @@ local require_all = require("default.hypr.require_all")
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
package.path = toggles_dir .. "/?.lua;" .. package.path
require_all.files(toggles_dir, nil, { reload = true })
-- touchpad-disabled.lua / touchscreen-disabled.lua were generated Lua in older
-- versions and could carry an injected USB device name. They must never be loaded
-- as code again: exclude them so a not-yet-migrated install cannot execute a
-- leftover payload on reload. The migration recovers the name and deletes them.
require_all.files(toggles_dir, nil, {
reload = true,
exclude = {
["touchpad-disabled"] = true,
["touchscreen-disabled"] = true,
},
})
local disabled_input_device = require("default.hypr.disabled-input-device")
disabled_input_device("touchpad")
disabled_input_device("touchscreen")
require("default.hypr.workspace-layouts")
+1 -1
View File
@@ -21,7 +21,7 @@ From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy
| Suspend | — | `omarchy toggle suspend` |
| Hybrid GPU | — | `omarchy toggle hybrid gpu` |
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled state is written back out as a small Lua file that Hyprland sources on startup.
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled device's name is saved to a small state file that Hyprland reads on startup to disable it again.
The Toggle menu also carries a few things that aren't `omarchy toggle` commands but behave the same: battery percentage in the bar, workspace layout (`Super + L`), window gaps (`Super + Shift + Backspace`), and the 1-window square aspect (`Super + Ctrl + Backspace`).
+39
View File
@@ -0,0 +1,39 @@
echo "Store Hyprland input-device names as data instead of generated Lua"
# omarchy-toggle-input-device used to interpolate hyprctl device names into
# hyprctl eval and a generated Lua file. Those names come from USB descriptors,
# so recover the plain device name as data and delete the generated Lua. A name
# that could have broken out of the old Lua string literal is discarded, not
# trusted. The old script wrote to ~/.local/state regardless of XDG_STATE_HOME.
toggles_dir="$HOME/.local/state/omarchy/toggles/hypr"
reapply=0
for kind in touchpad touchscreen; do
state_file="$toggles_dir/$kind-disabled.lua"
name_file="$toggles_dir/$kind-disabled-name"
[[ -f $state_file ]] || continue
if [[ ! -f $name_file && -r $state_file ]]; then
old=$(<"$state_file")
pattern='^hl\.device\(\{ name = "([^"\\[:cntrl:]]+)", enabled = false \}\)$'
if [[ $old =~ $pattern ]]; then
printf '%s\n' "${BASH_REMATCH[1]}" >"$name_file"
fi
fi
rm -f "$state_file"
if [[ -f $name_file ]]; then
reapply=1
fi
done
# The package hook reloads Hyprland before migrations run, so this session has
# already dropped the disable: the generated Lua is no longer loaded and the
# name file did not exist yet to replace it. Reload once more now that it does,
# or the device the user switched off stays on until their next login.
if (( reapply )); then
hyprctl reload >/dev/null 2>&1 || true
fi
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command lua
run_paths() {
lua - <<'LUA'
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
local paths = require("default.hypr.paths")
assert(paths.config_home == os.getenv("EXPECTED_CONFIG"), "config_home: " .. paths.config_home)
assert(paths.state_home == os.getenv("EXPECTED_STATE"), "state_home: " .. paths.state_home)
LUA
}
HOME="/home/test-user" OMARCHY_PATH="$ROOT" \
XDG_CONFIG_HOME= XDG_STATE_HOME= \
EXPECTED_CONFIG="/home/test-user/.config" EXPECTED_STATE="/home/test-user/.local/state" \
run_paths
pass "empty XDG path variables fall back to their defaults"
HOME="/home/test-user" OMARCHY_PATH="$ROOT" \
XDG_CONFIG_HOME="/custom/config" XDG_STATE_HOME="/custom/state" \
EXPECTED_CONFIG="/custom/config" EXPECTED_STATE="/custom/state" \
run_paths
pass "set XDG path variables are honored"
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command jq
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
stub_dir="$tmpdir/bin"
home_dir="$tmpdir/home"
monitors_json="$tmpdir/monitors.json"
flag_dir="$home_dir/.local/state/omarchy/toggles/hypr"
mkdir -p "$stub_dir" "$flag_dir"
make_stub() {
local name=$1
local body=$2
printf '#!/bin/bash\n%s\n' "$body" >"$stub_dir/$name"
chmod +x "$stub_dir/$name"
}
make_stub omarchy-notification-send ':'
make_stub omarchy-hyprland-monitor-external-active 'exit 0'
make_stub omarchy-hyprland-toggle-disabled 'exit 0'
make_stub omarchy-hyprland-toggle ':'
make_stub omarchy-hyprland-monitor-internal ':'
make_stub omarchy-hyprland-monitor-internal-mirror ':'
make_stub omarchy-hw-clamshell 'exit 0'
make_stub omarchy-hyprland-monitor-laptop 'printf "%s\n" "$LAPTOP_NAME"'
make_stub hyprctl 'case "$1" in
monitors) cat "$MONITORS_JSON" ;;
eval) printf "%s\n" "$2" >>"$EVAL_LOG" ;;
esac'
eval_log="$tmpdir/eval.log"
run_monitor() {
local command=$1
shift
: >"$eval_log"
HOME="$home_dir" \
XDG_STATE_HOME="$home_dir/.local/state" \
LAPTOP_NAME="${LAPTOP_NAME:-eDP-1}" \
MONITORS_JSON="$monitors_json" \
EVAL_LOG="$eval_log" \
PATH="$stub_dir:$ROOT/bin:$PATH" \
"$ROOT/bin/$command" "$@"
}
printf '[{"name":"eDP-1"},{"name":"DP-3"}]\n' >"$monitors_json"
disable_flag="$flag_dir/internal-monitor-disable.lua"
run_monitor omarchy-hyprland-monitor-internal off
grep -Fx 'hl.monitor({ output = "eDP-1", disabled = true })' "$disable_flag" >/dev/null ||
fail "internal off writes the connector name into the toggle flag"
pass "internal off accepts a plain connector name"
rm -f "$disable_flag"
set +e
LAPTOP_NAME='eDP-1", disabled = false })os.execute("calc")--' \
run_monitor omarchy-hyprland-monitor-internal off >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "internal off rejects a monitor name with Lua metacharacters"
[[ ! -e $disable_flag ]] || fail "an unsafe monitor name is not written as Lua"
pass "internal off refuses an unsafe monitor name"
mirror_flag="$flag_dir/internal-monitor-mirror.lua"
run_monitor omarchy-hyprland-monitor-internal-mirror on
grep -Fx 'hl.monitor({ output = "DP-3", mode = "preferred", position = "auto", scale = 1, mirror = "eDP-1" })' \
"$mirror_flag" >/dev/null ||
fail "mirror on writes the connector names into the toggle flag"
pass "mirror on accepts plain connector names"
rm -f "$mirror_flag"
printf '[{"name":"eDP-1"},{"name":"HEAD\\" })os.execute(\\"calc\\")--"}]\n' >"$monitors_json"
set +e
run_monitor omarchy-hyprland-monitor-internal-mirror on >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "mirror on rejects an external name with Lua metacharacters"
[[ ! -e $mirror_flag ]] || fail "an unsafe external monitor name is not written as Lua"
pass "mirror on refuses an unsafe headless output name"
# The clamshell sync writes the internal-monitor name into generated Lua too.
clamshell_flag="$flag_dir/internal-monitor-clamshell.lua"
printf '[{"name":"eDP-1"}]\n' >"$monitors_json"
rm -f "$clamshell_flag"
run_monitor omarchy-hyprland-monitor-clamshell
grep -Fx 'hl.monitor({ output = "eDP-1", disabled = true })' "$clamshell_flag" >/dev/null ||
fail "clamshell disable writes the connector name into the toggle flag"
pass "clamshell disable accepts a plain connector name"
rm -f "$clamshell_flag"
set +e
LAPTOP_NAME='eDP-1", disabled = true })os.execute("calc")--' \
run_monitor omarchy-hyprland-monitor-clamshell >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "clamshell rejects a monitor name with Lua metacharacters"
[[ ! -e $clamshell_flag ]] || fail "an unsafe internal monitor name is not written as clamshell Lua"
pass "clamshell refuses an unsafe internal monitor name"
# The scaling command eval's the focused-monitor name into a Lua string.
printf '[{"name":"eDP-1","focused":true,"scale":1.0,"width":1920,"height":1080,"refreshRate":60.0}]\n' \
>"$monitors_json"
run_monitor omarchy-hyprland-monitor-scaling 1.6
grep -F 'hl.monitor({ output = "eDP-1"' "$eval_log" >/dev/null ||
fail "scaling eval's the focused connector name"
pass "scaling accepts a plain connector name"
printf '[{"name":"eDP-1\\" })os.execute(\\"calc\\")--","focused":true,"scale":1.0,"width":1920,"height":1080,"refreshRate":60.0}]\n' \
>"$monitors_json"
set +e
run_monitor omarchy-hyprland-monitor-scaling 1.6 >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "scaling rejects a focused monitor name with Lua metacharacters"
[[ ! -s $eval_log ]] || fail "an unsafe focused monitor name is not eval'd as Lua"
pass "scaling refuses an unsafe focused monitor name"
+286
View File
@@ -0,0 +1,286 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command lua
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
stub_dir="$tmpdir/bin"
home_dir="$tmpdir/home"
xdg_decoy="$tmpdir/xdg-decoy"
log_file="$tmpdir/hyprctl.log"
marker="$tmpdir/marker"
mkdir -p "$stub_dir" "$home_dir" "$xdg_decoy"
state_dir="$home_dir/.local/state/omarchy/toggles/hypr"
name_file="$state_dir/touchpad-disabled-name"
state_lua="$state_dir/touchpad-disabled.lua"
cat >"$stub_dir/hyprctl" <<'EOF'
#!/bin/bash
case $1 in
eval) printf '%s\n' "$2" >>"$HYPRCTL_LOG" ;;
reload) printf 'reload\n' >>"$HYPRCTL_LOG" ;;
esac
EOF
chmod +x "$stub_dir/hyprctl"
cat >"$stub_dir/omarchy-osd" <<'EOF'
#!/bin/bash
:
EOF
chmod +x "$stub_dir/omarchy-osd"
stub_device() {
local kind=$1
local name=$2
cat >"$stub_dir/omarchy-hw-$kind" <<EOF
#!/bin/bash
printf '%s\n' '$name'
EOF
chmod +x "$stub_dir/omarchy-hw-$kind"
}
# XDG_STATE_HOME deliberately points away from HOME everywhere below: the
# input-device state is hardcoded to ~/.local/state like the sibling toggle
# tools and the pre-migration script, so nothing may read or write the XDG
# directory.
run_toggle() {
HOME="$home_dir" \
XDG_STATE_HOME="$xdg_decoy" \
HYPRCTL_LOG="$log_file" \
PATH="$stub_dir:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy-toggle-input-device" "$@"
}
assert_decoy_untouched() {
[[ -z $(find "$xdg_decoy" -mindepth 1 -print -quit 2>/dev/null) ]] ||
fail "input-device state must ignore XDG_STATE_HOME"
}
: >"$log_file"
stub_device touchpad 'elan-touchpad'
run_toggle touchpad off
[[ $(<"$name_file") == "elan-touchpad" ]] || fail "touchpad disable stores the device name as data"
[[ ! -e $state_lua ]] || fail "touchpad disable writes no generated Lua"
grep -Fx 'hl.device({ name = "elan-touchpad", enabled = false })' "$log_file" >/dev/null ||
fail "touchpad disable applies a quoted Lua device name"
assert_decoy_untouched
pass "touchpad disable persists the device name as data"
: >"$log_file"
run_toggle touchpad on
[[ ! -e $name_file ]] || fail "touchpad enable clears the persisted device name"
grep -Fx 'hl.device({ name = "elan-touchpad", enabled = true })' "$log_file" >/dev/null ||
fail "touchpad enable applies a quoted Lua device name"
pass "touchpad enable clears persisted disable state"
run_toggle touchpad
[[ -f $name_file ]] || fail "default toggle action disables an enabled touchpad"
run_toggle touchpad
[[ ! -e $name_file ]] || fail "default toggle action enables a disabled touchpad"
pass "default toggle action flips the persisted state"
: >"$log_file"
stub_device touchscreen 'wacom-hid-52eb-finger'
ts_name_file="$state_dir/touchscreen-disabled-name"
run_toggle touchscreen off
[[ $(<"$ts_name_file") == "wacom-hid-52eb-finger" ]] ||
fail "touchscreen disable stores the device name as data"
grep -Fx 'hl.device({ name = "wacom-hid-52eb-finger", enabled = false })' "$log_file" >/dev/null ||
fail "touchscreen disable applies a quoted Lua device name"
run_toggle touchscreen on
[[ ! -e $ts_name_file ]] || fail "touchscreen enable clears the persisted device name"
pass "touchscreen routes through the same persisted-name state"
: >"$log_file"
rm -f "$marker"
stub_device touchpad 'touchpad"; touch '"$marker"'; echo "'
run_toggle touchpad off
[[ ! -e $marker ]] || fail "touchpad disable does not execute metacharacters in the device name"
[[ $(<"$name_file") == 'touchpad"; touch '"$marker"'; echo "' ]] ||
fail "a hostile device name is stored only as data"
[[ ! -e $state_lua ]] || fail "a hostile device name is not written as Lua"
grep -F 'hl.device({ name = "touchpad\"' "$log_file" >/dev/null ||
fail "hyprctl eval Lua-quotes quotes in the device name" "$(<"$log_file")"
pass "touchpad disable treats USB device names as data"
HOME="$home_dir" XDG_STATE_HOME="$xdg_decoy" OMARCHY_PATH="$ROOT" MARKER="$marker" lua - <<'LUA'
local seen = {}
hl = {
device = function(opts)
table.insert(seen, opts)
end,
}
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
require("default.hypr.toggles")
assert(#seen == 1, "reload disables one device")
assert(seen[1].enabled == false)
assert(seen[1].name == 'touchpad"; touch ' .. os.getenv("MARKER") .. '; echo "', "device name is passed as a string")
LUA
pass "Hyprland reload loads the device name as a string"
# Public PoC device name: USB iProduct is interpolated into hl.device({ name = "..." }).
# os.execute is stubbed so the string is only checked as data.
poc_name='trackpad"})os.execute("~/calc&")--'
stub_device touchpad "$poc_name"
run_toggle touchpad on
: >"$log_file"
run_toggle touchpad off
[[ $(<"$name_file") == "$poc_name" ]] || fail "PoC device name is stored only as data"
[[ ! -e $state_lua ]] || fail "PoC device name is not written as Lua"
HOME="$home_dir" XDG_STATE_HOME="$xdg_decoy" OMARCHY_PATH="$ROOT" \
POC_NAME="$poc_name" EVAL_SNIPPET="$(<"$log_file")" lua - <<'LUA'
local poc = os.getenv("POC_NAME")
local snippet = os.getenv("EVAL_SNIPPET")
local seen, executed = {}, false
hl = {
device = function(opts)
table.insert(seen, opts)
end,
}
os.execute = function()
executed = true
end
assert(load(snippet, "eval", "t"))()
assert(executed == false, "quoted hyprctl eval must not run os.execute")
assert(#seen == 1)
assert(seen[1].name == poc)
assert(seen[1].enabled == false)
seen, executed = {}, false
assert(load('hl.device({ name = "' .. poc .. '", enabled = false })', "unquoted", "t"))()
assert(executed == true, "unquoted interpolation is the Lua injection")
seen, executed = {}, false
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
require("default.hypr.toggles")
assert(executed == false, "reload must not run os.execute")
assert(#seen == 1)
assert(seen[1].name == poc)
LUA
pass "PoC device name cannot execute via eval or reload"
cat >"$stub_dir/omarchy-hw-touchpad" <<'EOF'
#!/bin/bash
printf 'evil\nname\n'
EOF
chmod +x "$stub_dir/omarchy-hw-touchpad"
rm -f "$name_file"
set +e
run_toggle touchpad off >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "disable rejects a device name with a newline"
[[ ! -e $name_file ]] || fail "a rejected device name is not persisted"
pass "disable rejects control characters in a device name"
printf 'elan-touchpad\n' >"$name_file"
set +e
run_toggle touchpad on >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "enable still reports an invalid device name"
[[ ! -e $name_file ]] || fail "enable clears persisted state even with an invalid device name"
pass "a bad device name cannot wedge the persisted disable"
cat >"$stub_dir/omarchy-hw-touchpad" <<'EOF'
#!/bin/bash
:
EOF
chmod +x "$stub_dir/omarchy-hw-touchpad"
set +e
run_toggle touchpad off >/dev/null 2>&1
status=$?
set -e
(( status != 0 )) || fail "disable errors when no device is found"
[[ ! -e $name_file ]] || fail "no state is written when no device is found"
pass "disable errors when no device is found"
# The migration runs with the same XDG decoy: legacy files were written to
# ~/.local/state, so that is where it must look no matter what XDG says.
run_migration() {
HOME="$home_dir" XDG_STATE_HOME="$xdg_decoy" HYPRCTL_LOG="$log_file" \
PATH="$stub_dir:$ROOT/bin:$PATH" \
bash -euo pipefail "$ROOT/migrations/1787618700.sh" >/dev/null
}
mkdir -p "$state_dir"
rm -f "$state_dir"/*-disabled-name
printf 'hl.device({ name = "synps/2-synaptics-touchpad", enabled = false })\n' >"$state_lua"
printf 'hl.device({ name = "hostile\\"")", enabled = false })\n' >"$state_dir/touchscreen-disabled.lua"
: >"$log_file"
run_migration
[[ $(<"$name_file") == "synps/2-synaptics-touchpad" ]] ||
fail "migration recovers a device name containing a slash"
[[ ! -e $state_lua ]] || fail "migration deletes the generated touchpad Lua"
[[ ! -e $state_dir/touchscreen-disabled-name ]] ||
fail "migration does not copy a hostile name out of generated Lua"
[[ ! -e $state_dir/touchscreen-disabled.lua ]] ||
fail "migration deletes hostile generated Lua even when no name is recovered"
assert_decoy_untouched
# The package hook reloads Hyprland before migrations run, so the disable was
# already dropped for this session; the migration has to put it back.
grep -Fx 'reload' "$log_file" >/dev/null ||
fail "migration reloads so the recovered disable applies to this session"
pass "migration recovers plain names and discards hostile generated Lua"
printf 'kept-name\n' >"$name_file"
printf 'hl.device({ name = "other-touchpad", enabled = false })\n' >"$state_lua"
run_migration
[[ $(<"$name_file") == "kept-name" ]] || fail "migration keeps an existing device-name file"
[[ ! -e $state_lua ]] || fail "migration still deletes the generated Lua"
pass "migration is idempotent over an existing device-name file"
rm -f "$name_file"
printf 'garbage\n' >"$state_lua"
chmod 000 "$state_lua"
run_migration
[[ ! -e $state_lua ]] || fail "migration removes an unreadable generated Lua"
[[ ! -e $name_file ]] || fail "no name is recovered from an unreadable file"
pass "an unreadable state file does not wedge the migration"
: >"$log_file"
run_migration
[[ ! -s $log_file ]] || fail "migration with nothing to migrate does not reload"
pass "migration no-ops with nothing left to migrate"
# A compromised install carries a leftover generated touchpad-disabled.lua whose
# device name broke out into os.execute. Until the migration deletes it, a reload
# must not source it. toggles.lua excludes those two names from require_all, so the
# payload never runs, while a current name-file disable still applies.
reload_home="$tmpdir/reload-home"
reload_state="$reload_home/.local/state/omarchy/toggles/hypr"
mkdir -p "$reload_state"
reload_marker="$tmpdir/reload-executed"
rm -f "$reload_marker"
printf 'hl.device({ name = "trackpad"})os.execute("touch %s")--", enabled = false })\n' "$reload_marker" \
>"$reload_state/touchpad-disabled.lua"
printf 'elan-touchpad\n' >"$reload_state/touchpad-disabled-name"
HOME="$reload_home" XDG_STATE_HOME="$reload_home/.local/state" OMARCHY_PATH="$ROOT" lua - <<'LUA'
local disabled = {}
hl = { device = function(opts) table.insert(disabled, opts) end }
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
require("default.hypr.toggles")
assert(#disabled == 1, "only the current name-file disable is applied")
assert(disabled[1].name == "elan-touchpad", "disable uses the stored device name")
assert(disabled[1].enabled == false)
LUA
[[ ! -e $reload_marker ]] || fail "a leftover legacy generated toggle Lua must not execute on reload"
pass "reload excludes leftover legacy toggle Lua while applying the data disable"