Remove Omarchy 3 power udev rules that run a command out of a user home
Omarchy 3 wrote 99-power-profile.rules and 99-wifi-powersave.rules with an unquoted heredoc, baking the installing user's home into a rule udev runs as root. That path resolves through ~/.local/share/omarchy, a symlink the unprivileged user owns, so replacing it and provoking a power_supply event runs their code as root. HEAD points the rules at /usr/bin under new names, but the one-shot cleanup for the old filenames was dropped, leaving the file on every install that came up through the 3.x line. Remove a legacy file only when an active RUN+= really does run that filename's binary out of a home directory, so a rule of the same name a user wrote themselves stays, comments and all.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
echo "Remove Omarchy 3 power udev rules that run a command out of a user home"
|
||||
|
||||
rules_dir="${OMARCHY_UDEV_RULES_DIR:-/etc/udev/rules.d}"
|
||||
|
||||
as_root() {
|
||||
if (( EUID == 0 )); then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Omarchy 3 generated these two rules with an unquoted heredoc, so the installing
|
||||
# user's $HOME was expanded and the file on disk names
|
||||
# /home/<user>/.local/share/omarchy/bin/<command>. udev runs RUN+= as root, and
|
||||
# ~/.local/share/omarchy is a symlink that same unprivileged user owns: replacing
|
||||
# it with a tree of their own and provoking a power_supply event runs their code
|
||||
# as root. Quattro ships the rules as 99-omarchy-*.rules under /usr/bin, but the
|
||||
# one-shot migration that swept the old filenames was itself dropped, so an
|
||||
# install that came up through the 3.x line keeps the old file until this
|
||||
# migration removes it.
|
||||
#
|
||||
# Pre-4 layout work normally belongs in bin/omarchy-upgrade-to-quattro, but that
|
||||
# command only runs on a machine still making the crossing, so an install that
|
||||
# crossed already would never see it. The upgrade command ends by running
|
||||
# omarchy-migrate, so this covers the installs still to upgrade as well.
|
||||
#
|
||||
# Only remove a file that is actually one of those. A comment is inert to udev,
|
||||
# so the match keys on an active RUN+= whose command really is the legacy path
|
||||
# under a home directory for that filename's binary. A rule of the same name that
|
||||
# a user wrote themselves survives, including one that merely mentions the legacy
|
||||
# path in a comment, and so does a legacy file already repointed at /usr/bin.
|
||||
rule_runs_from_home() {
|
||||
local file="$1" binary="$2"
|
||||
local pattern="^(/home/[^/]+|/root)/\\.local/share/omarchy/bin/$binary\$"
|
||||
local line logical="" rest command word
|
||||
local -a words
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
# udev tests for a comment before it joins continuations, and skipping one
|
||||
# does not end a continuation already under way. Both halves verified with
|
||||
# `udevadm verify`: "# disabled \" followed by a bogus key reports the error
|
||||
# on line 2, so a comment's own trailing backslash swallows nothing, while
|
||||
# 'SUBSYSTEM=="power_supply" \' + "# c" + ', RUN+="..."' reports its style
|
||||
# warning on line 1, so the rule spans the comment. Testing the comment after
|
||||
# the join would hide a live rule; clearing the pending line here would hide
|
||||
# one just as well.
|
||||
if [[ $line =~ ^[[:space:]]*# ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# A trailing backslash continues the rule on the next line.
|
||||
if [[ $line == *\\ ]]; then
|
||||
logical+=${line%\\}
|
||||
continue
|
||||
fi
|
||||
|
||||
rest=$logical$line
|
||||
logical=""
|
||||
|
||||
while [[ $rest == *'RUN+="'* ]]; do
|
||||
rest=${rest#*'RUN+="'}
|
||||
command=${rest%%'"'*}
|
||||
rest=${rest#*'"'}
|
||||
|
||||
# The legacy rules put the binary first (wifi power save) or last, after a
|
||||
# systemd-run invocation (power profile), and some variants passed it an
|
||||
# argument. Compare whole words so no substring stands in for the path.
|
||||
read -ra words <<<"$command"
|
||||
for word in "${words[@]}"; do
|
||||
if [[ $word =~ $pattern || $word == "$HOME/.local/share/omarchy/bin/$binary" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
done <"$file"
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
removed=0
|
||||
|
||||
for legacy_rule in "99-power-profile.rules:omarchy-powerprofiles-set" "99-wifi-powersave.rules:omarchy-wifi-powersave"; do
|
||||
rule_file="$rules_dir/${legacy_rule%%:*}"
|
||||
|
||||
if [[ -f $rule_file ]] && rule_runs_from_home "$rule_file" "${legacy_rule##*:}"; then
|
||||
as_root rm -f "$rule_file"
|
||||
removed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if (( removed )); then
|
||||
# Drop the rule from the running udevd too; until it reloads, the rule that was
|
||||
# just deleted still fires on the next power_supply event. Best effort the way
|
||||
# install/post-install/udev.sh is: a machine with no udevd to talk to has
|
||||
# already had the file removed, and the next boot reads the directory fresh.
|
||||
as_root udevadm control --reload 2>/dev/null || true
|
||||
fi
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1787946619.sh"
|
||||
[[ -f $migration ]] || fail "the legacy power udev rule migration exists at $migration"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
mkdir -p "$test_dir/bin"
|
||||
|
||||
# sudo runs the real command, so the removals act on the redirected rules
|
||||
# directory below and the elevated calls land in the log beside it.
|
||||
cat >"$test_dir/bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'sudo %s\n' "$*" >>"$CALLS"
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
cat >"$test_dir/bin/udevadm" <<'STUB'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'udevadm %s\n' "$*" >>"$CALLS"
|
||||
STUB
|
||||
|
||||
chmod +x "$test_dir/bin/"*
|
||||
|
||||
export CALLS="$test_dir/calls"
|
||||
|
||||
rules_dir="$test_dir/rules.d"
|
||||
home_dir="$test_dir/home"
|
||||
power_rule="$rules_dir/99-power-profile.rules"
|
||||
wifi_rule="$rules_dir/99-wifi-powersave.rules"
|
||||
|
||||
reset_machine() {
|
||||
rm -rf "$rules_dir" "$home_dir"
|
||||
mkdir -p "$rules_dir" "$home_dir"
|
||||
}
|
||||
|
||||
run_migration() {
|
||||
: >"$CALLS"
|
||||
|
||||
HOME="$home_dir" \
|
||||
OMARCHY_UDEV_RULES_DIR="$rules_dir" \
|
||||
PATH="$test_dir/bin:$PATH" \
|
||||
bash -euo pipefail "$migration" >/dev/null
|
||||
}
|
||||
|
||||
reload_count() {
|
||||
grep -cx 'udevadm control --reload' "$CALLS" || true
|
||||
}
|
||||
|
||||
# What Omarchy 3's unquoted heredoc actually left on disk: the installing user's
|
||||
# home expanded into a rule root runs on every power_supply event.
|
||||
write_vulnerable_power_rule() {
|
||||
cat >"$power_rule" <<'RULE'
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile --property=After=power-profiles-daemon.service /home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="USB", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile --property=After=power-profiles-daemon.service /home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||
RULE
|
||||
}
|
||||
|
||||
write_vulnerable_wifi_rule() {
|
||||
cat >"$wifi_rule" <<'RULE'
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave on"
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave off"
|
||||
RULE
|
||||
}
|
||||
|
||||
reset_machine
|
||||
write_vulnerable_power_rule
|
||||
run_migration
|
||||
|
||||
[[ ! -e $power_rule ]] ||
|
||||
fail "migration removes a power profile rule that runs out of a user home" "$(cat "$power_rule")"
|
||||
pass "migration removes a power profile rule that runs out of a user home"
|
||||
|
||||
grep -q '^sudo rm -f .*99-power-profile\.rules$' "$CALLS" ||
|
||||
fail "migration removes the rule with elevated privileges" "$(cat "$CALLS")"
|
||||
pass "migration removes the rule with elevated privileges"
|
||||
|
||||
reset_machine
|
||||
write_vulnerable_wifi_rule
|
||||
run_migration
|
||||
|
||||
[[ ! -e $wifi_rule ]] ||
|
||||
fail "migration removes a Wi-Fi power save rule that runs out of a user home" "$(cat "$wifi_rule")"
|
||||
pass "migration removes a Wi-Fi power save rule that runs out of a user home"
|
||||
|
||||
# udevd keeps running the rule it already parsed, so the file being gone from
|
||||
# disk is only half the fix until it reloads.
|
||||
(( $(reload_count) == 1 )) ||
|
||||
fail "migration reloads udev after removing a rule" "$(cat "$CALLS")"
|
||||
pass "migration reloads udev after removing a rule"
|
||||
|
||||
# Both files gone is still one machine-wide reload, not one per file.
|
||||
reset_machine
|
||||
write_vulnerable_power_rule
|
||||
write_vulnerable_wifi_rule
|
||||
run_migration
|
||||
|
||||
[[ ! -e $power_rule && ! -e $wifi_rule ]] ||
|
||||
fail "migration removes both legacy rules in one pass"
|
||||
(( $(reload_count) == 1 )) ||
|
||||
fail "migration reloads udev once for both removals" "$(cat "$CALLS")"
|
||||
pass "migration removes both legacy rules and reloads udev once"
|
||||
|
||||
# The second run is what every other account on the machine does, and what a
|
||||
# user gets from running omarchy-migrate again.
|
||||
run_migration
|
||||
|
||||
(( $(reload_count) == 0 )) ||
|
||||
fail "migration does not reload udev on a second run" "$(cat "$CALLS")"
|
||||
[[ ! -s $CALLS ]] ||
|
||||
fail "migration touches nothing on a second run" "$(cat "$CALLS")"
|
||||
pass "migration is a no-op on a second run"
|
||||
|
||||
reset_machine
|
||||
run_migration
|
||||
|
||||
[[ ! -s $CALLS ]] ||
|
||||
fail "migration touches nothing when the legacy rules are absent" "$(cat "$CALLS")"
|
||||
pass "migration leaves a machine without the legacy rules alone"
|
||||
|
||||
# A user who wrote their own rule under one of these names keeps it, even when
|
||||
# the file talks about the legacy checkout. udev never runs a comment.
|
||||
reset_machine
|
||||
cat >"$power_rule" <<'RULE'
|
||||
# Replaces the rule Omarchy used to install from
|
||||
# /home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set
|
||||
#SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/local/bin/my-own-power-hook"
|
||||
RULE
|
||||
cat >"$wifi_rule" <<'RULE'
|
||||
# Kept from the old local/share/omarchy setup, rewritten to my own script
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/local/bin/my-own-wifi-hook off"
|
||||
RULE
|
||||
before=$(cat "$power_rule" "$wifi_rule")
|
||||
run_migration
|
||||
|
||||
[[ -e $power_rule && -e $wifi_rule ]] ||
|
||||
fail "migration keeps same-named rules that only mention the legacy path"
|
||||
[[ $(cat "$power_rule" "$wifi_rule") == "$before" ]] ||
|
||||
fail "migration leaves the user's own rules byte for byte"
|
||||
[[ ! -s $CALLS ]] ||
|
||||
fail "migration escalates nothing when it removes nothing" "$(cat "$CALLS")"
|
||||
pass "migration keeps same-named rules that only mention the legacy path"
|
||||
|
||||
# udev discards a '#' line before it ever looks for a trailing backslash, so the
|
||||
# rule below the comment is live and root still runs it. `udevadm verify` on this
|
||||
# exact shape, with a bogus key on the second line, reports the error on line 2.
|
||||
# The file has to go.
|
||||
reset_machine
|
||||
cat >"$power_rule" <<'RULE'
|
||||
# Disabled while I test the packaged rule: \
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||
RULE
|
||||
run_migration
|
||||
|
||||
[[ ! -e $power_rule ]] ||
|
||||
fail "migration removes a rule left live under a commented continuation" "$(cat "$power_rule")"
|
||||
pass "migration removes a rule left live under a commented continuation"
|
||||
|
||||
# A comment that does not continue still hides nothing behind it: the file holds
|
||||
# no active RUN+= at all and stays.
|
||||
reset_machine
|
||||
cat >"$power_rule" <<'RULE'
|
||||
# SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||
RULE
|
||||
run_migration
|
||||
|
||||
[[ -e $power_rule ]] ||
|
||||
fail "migration keeps a rule that is only ever mentioned in a comment"
|
||||
pass "migration keeps a rule that is only ever mentioned in a comment"
|
||||
|
||||
# The May 2026 rename left an intermediate variant under the old filename that
|
||||
# already ran out of /usr/bin. It duplicates the packaged rule but is not the
|
||||
# privilege escalation this migration exists to clear, so it is not ours to take.
|
||||
reset_machine
|
||||
cat >"$power_rule" <<'RULE'
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile-ac --property=After=power-profiles-daemon.service /usr/bin/powerprofilesctl set performance"
|
||||
RULE
|
||||
run_migration
|
||||
|
||||
[[ -e $power_rule ]] ||
|
||||
fail "migration keeps a legacy filename already repointed at /usr/bin"
|
||||
pass "migration keeps a legacy filename already repointed at /usr/bin"
|
||||
|
||||
# Homes are not all under /home, so the running user's own home counts too, and
|
||||
# the argument the later variants passed must not hide the path.
|
||||
reset_machine
|
||||
cat >"$wifi_rule" <<RULE
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-on $home_dir/.local/share/omarchy/bin/omarchy-wifi-powersave on"
|
||||
RULE
|
||||
run_migration
|
||||
|
||||
[[ ! -e $wifi_rule ]] ||
|
||||
fail "migration removes a rule that runs out of a home outside /home" "$(cat "$wifi_rule")"
|
||||
pass "migration removes a rule that runs out of a home outside /home"
|
||||
|
||||
# Nothing named the wrong binary is ours: the same path with a different command
|
||||
# is a rule this migration cannot claim to know anything about.
|
||||
reset_machine
|
||||
cat >"$power_rule" <<'RULE'
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave on"
|
||||
RULE
|
||||
run_migration
|
||||
|
||||
[[ -e $power_rule ]] ||
|
||||
fail "migration matches the binary the filename promises, not any home path"
|
||||
pass "migration matches the binary the filename promises, not any home path"
|
||||
|
||||
# udev resumes a continuation across a comment: `udevadm verify` on
|
||||
# 'SUBSYSTEM=="power_supply" \' + "# c" + ', RUN+="..."' reports its style warning
|
||||
# on line 1, so those three lines are one rule and the rule is live.
|
||||
reset_machine
|
||||
cat >"$power_rule" <<'RULE'
|
||||
SUBSYSTEM=="power_supply", ATTR{type}=="Mains" \
|
||||
# split for readability
|
||||
, RUN+="/home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set"
|
||||
RULE
|
||||
run_migration
|
||||
|
||||
[[ ! -e $power_rule ]] ||
|
||||
fail "migration removes a rule that continues across a comment" "$(cat "$power_rule")"
|
||||
pass "migration removes a rule that continues across a comment"
|
||||
Reference in New Issue
Block a user