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

Backport of the input-device name fix (PR #8129 by @acrogenesis, merged to
quattro as 9285b19d) onto the v4-0-1 release branch.

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. XF86TouchpadToggle is bound with
locked = true, so a malicious USB name reached Lua execution from the lock
screen as the logged-in user, and a persisted disable made it run on every
start. Publicly reported by Jorrit Jongma / Chainfire.

The disable is no longer executable Lua anywhere. The device name is stored as
plain-text data in a *-disabled-name sidecar and read back by a packaged module,
default/hypr/disabled-input-device.lua, on every reload; the live hyprctl eval
Lua-quotes the name and rejects control characters outright. The reload loader
excludes the two legacy filenames, so a leftover generated *-disabled.lua on a
not-yet-migrated install can never be sourced as code again, and a migration
recovers the device name from it and deletes it, sanitizing installs that ran
the vulnerable version. All four monitor scripts validate an output name against
a plain-connector-name pattern before writing it as Lua, closing the same latent
pattern in the siblings, and paths.lua treats a set-but-empty XDG_STATE_HOME as
unset to match the bash side.

Clean cherry-pick: all fourteen files are byte-identical to quattro, so merging
v4-0-1 into quattro resolves without a conflict. This branch ships no leftover
*-disabled.lua template of its own -- the tracked "disabled" files are the same
two quattro has -- so the migration is the only path that has to sanitize
anything here.

test/shell passes: 192 files, including the three this adds. The toggle suite's
public-PoC case passes here, as do the monitor scripts' accept/reject cases and
the XDG path cases. test/cli passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
This commit is contained in:
Adrian RangelandClaude Opus 5 authored and David Heinemeier Hansson committed 2026-08-25 11:07:35 +02:00
1 parent c6d676f23c
commit 2e989e35e5
14 files changed
+612 -27

No files matched your search

+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"