Repair legacy XCompose and vulnerable power paths

This commit is contained in:
Ryan Hughes
2026-08-30 11:36:22 -04:00
parent e3b566bae8
commit 8add7b49de
4 changed files with 613 additions and 179 deletions
+1 -11
View File
@@ -167,14 +167,4 @@ Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through th
normal migration runner. Do not add compatibility migrations for old installer
layouts; put pre-4 package-layout transition work in the upgrade command instead.
Clearing a privileged file that a retired installer left on disk is the exception,
and belongs in a migration whether or not that installer was part of a package
layout transition. The upgrade command only runs on a machine still making the 3
to 4 crossing, so anything put there never reaches an install that crossed
already, and it never runs at all for an installer that was retired on its own —
while the file the installer wrote is still sitting on those machines. The upgrade
command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so
one migration reaches every population; a copy in the upgrade command would only
be a second copy of the same predicate to keep correct. Such a migration must name
the defect it clears and match what the old installer actually produced, so a file
an administrator wrote themselves is left alone.
Clearing a privileged file that a retired installer left on disk is the exception, and belongs in a migration whether or not that installer was part of a package layout transition. The upgrade command only runs on a machine still making the 3 to 4 crossing, so anything put there never reaches an install that crossed already, and it never runs at all for an installer that was retired on its own — while the file the installer wrote is still sitting on those machines. The upgrade command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so one migration reaches every population; a copy in the upgrade command would only be a second copy of the same predicate to keep correct. Such a migration must name the defect it clears and match what the old installer actually produced before deleting it. Leave safe administrator-authored files alone; if one still contains the vulnerable privileged action, preserve it under an inactive name rather than discarding custom content or leaving the action executable. A user config that depends on the same retired compatibility path may be repaired in that migration when doing so eliminates an overlapping migration, but only by matching and replacing the exact legacy path while preserving the rest of the file.
-120
View File
@@ -1,120 +0,0 @@
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}"
reload_needed_marker="${OMARCHY_UDEV_RELOAD_NEEDED_MARKER:-/var/lib/omarchy/migrations/1787946619-udev-reload-needed}"
udev_control="${OMARCHY_UDEV_CONTROL:-/run/udev/control}"
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 that absolute home path.
# 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 the Omarchy 4 upgrade command, 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="^/.+/\\.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 ]]; then
return 0
fi
done
done
done <"$file"
return 1
}
finish_pending_reload() {
# With no control socket there is no running udevd holding the deleted rule;
# the next daemon start reads the directory from disk. If a daemon is running,
# a failed reload must keep this migration pending so the in-memory root rule
# cannot outlive the per-user completion marker.
if [[ -e $udev_control ]] && ! as_root udevadm control --reload 2>/dev/null; then
echo "Could not reload udev after removing a vulnerable legacy rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if ! as_root rm -f "$reload_needed_marker"; then
echo "Could not finish the legacy udev-rule repair. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
}
# Deleting the file and reloading the daemon are one repair. A prior run may
# have removed the file and then failed before udevd accepted the new ruleset.
if [[ -e $reload_needed_marker ]]; then
finish_pending_reload
fi
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
if ! as_root install -Dm644 /dev/null "$reload_needed_marker"; then
echo "Administrator privileges are required to remove the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if ! as_root rm -f "$rule_file"; then
echo "Administrator privileges are required to remove the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
finish_pending_reload
fi
done
+257
View File
@@ -0,0 +1,257 @@
echo "Repair legacy XCompose and remove vulnerable Omarchy 3 power udev rules"
xcompose="$HOME/.XCompose"
packaged_xcompose="$OMARCHY_PATH/default/xcompose"
legacy_xcompose_pattern='^[[:space:]]*include[[:space:]]+"[^"]*/\.local/share/omarchy/default/xcompose"[[:space:]]*$'
# Omarchy 3 pointed the user's compose file through the checkout compatibility
# link. Preserve their own sequences while moving that include to the packaged
# tree. A failed live restart is harmless: the next graphical login reads the
# repaired file.
if [[ -f $xcompose ]] && grep -Eq "$legacy_xcompose_pattern" "$xcompose"; then
xcompose_replacement=${packaged_xcompose//\\/\\\\}
xcompose_replacement=${xcompose_replacement//&/\\&}
xcompose_replacement=${xcompose_replacement//|/\\|}
sed -i -E "s|^([[:space:]]*include[[:space:]]+\")[^\"]*/\\.local/share/omarchy/default/xcompose\"[[:space:]]*$|\\1$xcompose_replacement\"|" "$xcompose"
omarchy-restart-xcompose >/dev/null 2>&1 || true
fi
rules_dir=/etc/udev/rules.d
reload_marker_prefix=/var/lib/omarchy/migrations/1788102906-udev-reload-needed
udev_control=/run/udev/control
install_command=/usr/bin/install
mv_command=/usr/bin/mv
rm_command=/usr/bin/rm
udevadm_command=/usr/bin/udevadm
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 that absolute home path.
# 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 the Omarchy 4 upgrade command, 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 an exact two-line body that one of the retired installers wrote.
# A same-named file with no active vulnerable RUN survives; one that still has
# the vulnerable command but also contains administrator changes is quarantined
# under a non-.rules suffix for review.
rule_runs_from_home() {
local file="$1" binary="$2"
local run_pattern='RUN([[:space:]]*\{[^}]*\})?[[:space:]]*(\+|:)?=[[:space:]]*(e)?"([^"]*)"'
local line logical="" rest command
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_pattern ]]; do
command=${BASH_REMATCH[4]}
rest=${rest#*"${BASH_REMATCH[0]}"}
# This is only the fail-closed detector; wholesale deletion still requires
# an exact historical body below. Match spacing edits and wrapper arguments
# conservatively so a modified active rule is never mistaken for a safe one.
if [[ $command == *"/.local/share/omarchy/bin/$binary"* ]]; then
return 0
fi
done
done <"$file"
return 1
}
mark_reload_needed() {
if ! as_root "$install_command" -Dm644 /dev/null "$reload_needed_marker"; then
echo "Administrator privileges are required to repair the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
}
# The retired installers overwrote each file with one of five known two-line
# bodies. Only those exact bodies are safe to delete wholesale. If a vulnerable
# rule has since been edited or extended, preserve it under an inactive name
# rather than taking unrelated rules with it.
rule_is_exact_generated() {
local file="$1" binary="$2"
local prefix suffix home expected
local -a lines
mapfile -t lines <"$file"
(( ${#lines[@]} == 2 )) || return 1
case "$binary" in
omarchy-powerprofiles-set)
# Initial userland helper: separate AC/battery units and arguments.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile-battery --property=After=power-profiles-daemon.service '
suffix='/.local/share/omarchy/bin/omarchy-powerprofiles-set battery"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='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 '"$home"'/.local/share/omarchy/bin/omarchy-powerprofiles-set ac"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
# USB-C support: one fixed transient-unit name and no profile argument.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile --property=After=power-profiles-daemon.service '
suffix='/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='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"'/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
# The final Omarchy 3 revision dropped the fixed transient-unit name.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --property=After=power-profiles-daemon.service '
suffix='/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="USB", RUN+="/usr/bin/systemd-run --no-block --collect --property=After=power-profiles-daemon.service '"$home"'/.local/share/omarchy/bin/omarchy-powerprofiles-set"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
;;
omarchy-wifi-powersave)
# Initial Wi-Fi helper: invoke the userland command directly.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="'
suffix='/.local/share/omarchy/bin/omarchy-wifi-powersave on"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="'"$home"'/.local/share/omarchy/bin/omarchy-wifi-powersave off"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
# Later revision deferred each change through its own transient unit.
prefix='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-on '
suffix='/.local/share/omarchy/bin/omarchy-wifi-powersave on"'
if [[ ${lines[0]} == "$prefix"*"$suffix" ]]; then
home=${lines[0]#"$prefix"}
home=${home%"$suffix"}
expected='SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-off '"$home"'/.local/share/omarchy/bin/omarchy-wifi-powersave off"'
[[ $home == /* && $home != *'"'* && ${lines[1]} == "$expected" ]] && return 0
fi
;;
esac
return 1
}
finish_pending_reload() {
# With no control socket there is no running udevd holding the deleted rule;
# the next daemon start reads the directory from disk. If a daemon is running,
# a failed reload must keep this migration pending so the in-memory root rule
# cannot outlive the per-user completion marker.
if [[ -e $udev_control ]] && ! as_root "$udevadm_command" control --reload 2>/dev/null; then
echo "Could not reload udev after removing a vulnerable legacy rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if ! as_root "$rm_command" -f "$reload_needed_marker"; then
echo "Could not finish the legacy udev-rule repair. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
}
quarantine_rule() {
local rule_file="$1"
local backup="$rule_file.omarchy-disabled"
local suffix=0
# udev only loads files ending in .rules. Preserve an administrator-modified
# file byte-for-byte under a suffix udev ignores instead of either deleting
# their additions or leaving its user-controlled command active as root.
while [[ -e $backup || -L $backup ]]; do
((++suffix))
backup="$rule_file.omarchy-disabled.$suffix"
done
mark_reload_needed
if ! as_root "$mv_command" --no-clobber -- "$rule_file" "$backup"; then
echo "Administrator privileges are required to quarantine the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if [[ -e $rule_file || -L $rule_file ]]; then
echo "Could not quarantine the vulnerable legacy udev rule at $rule_file. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
finish_pending_reload
echo "Quarantined the modified legacy rule as $backup so udev cannot execute it. Review the preserved file before restoring any safe custom actions." >&2
}
if [[ -d $rules_dir && ! -x $rules_dir ]]; then
echo "Could not inspect legacy udev rules under $rules_dir. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
for legacy_rule in "99-power-profile.rules:omarchy-powerprofiles-set" "99-wifi-powersave.rules:omarchy-wifi-powersave"; do
rule_name="${legacy_rule%%:*}"
rule_file="$rules_dir/$rule_name"
binary="${legacy_rule##*:}"
reload_needed_marker="$reload_marker_prefix-$rule_name"
# The marker is written before removal so a crash cannot lose the need to
# reload. Only consume it early when the corresponding active file is already
# gone; another user's concurrent run may still be between those two steps.
if [[ -e $reload_needed_marker && ! -e $rule_file && ! -L $rule_file ]]; then
finish_pending_reload
fi
if [[ -f $rule_file && ! -r $rule_file ]]; then
echo "Could not inspect the legacy udev rule at $rule_file. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
if [[ -f $rule_file ]] && rule_is_exact_generated "$rule_file" "$binary"; then
mark_reload_needed
if ! as_root "$rm_command" -f "$rule_file"; then
echo "Administrator privileges are required to remove the vulnerable legacy udev rule. Ask an administrator to run omarchy-migrate." >&2
exit 1
fi
finish_pending_reload
elif [[ -f $rule_file ]] && rule_runs_from_home "$rule_file" "$binary"; then
quarantine_rule "$rule_file"
fi
# If an interrupted removal was followed by an administrator installing a
# safe replacement, reload that replacement before clearing the old marker.
if [[ -e $reload_needed_marker ]]; then
finish_pending_reload
fi
done
@@ -4,8 +4,13 @@ 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"
shipped_migration="$ROOT/migrations/1788102906.sh"
[[ -f $shipped_migration ]] || fail "the legacy power udev rule migration exists at $shipped_migration"
mapfile -t legacy_rule_migrations < <(grep -RIlE '99-(power-profile|wifi-powersave)' "$ROOT/migrations")
(( ${#legacy_rule_migrations[@]} == 1 )) && [[ ${legacy_rule_migrations[0]} == "$shipped_migration" ]] ||
fail "one migration exclusively owns both legacy udev rule filenames" "${legacy_rule_migrations[*]}"
pass "one migration exclusively owns both legacy udev rule filenames"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
@@ -18,6 +23,10 @@ cat >"$test_dir/bin/sudo" <<'STUB'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$CALLS"
if [[ ${1:-} == "/usr/bin/udevadm" ]]; then
shift
exec "$UDEVADM_STUB" "$@"
fi
exec "$@"
STUB
@@ -31,6 +40,33 @@ if [[ -n ${FAIL_UDEV_RELOAD_ONCE_MARKER:-} && ! -e $FAIL_UDEV_RELOAD_ONCE_MARKER
fi
STUB
cat >"$test_dir/bin/install" <<'STUB'
#!/bin/bash
echo "migration resolved install through PATH" >&2
exit 97
STUB
cat >"$test_dir/bin/rm" <<'STUB'
#!/bin/bash
echo "migration resolved rm through PATH" >&2
exit 98
STUB
cat >"$test_dir/bin/mv" <<'STUB'
#!/bin/bash
echo "migration resolved mv through PATH" >&2
exit 99
STUB
cat >"$test_dir/bin/omarchy-restart-xcompose" <<'STUB'
#!/bin/bash
echo "omarchy-restart-xcompose" >>"$CALLS"
STUB
chmod +x "$test_dir/bin/"*
mkdir -p "$test_dir/failing-bin"
@@ -43,17 +79,45 @@ STUB
chmod +x "$test_dir/failing-bin/sudo"
export CALLS="$test_dir/calls"
export UDEVADM_STUB="$test_dir/bin/udevadm"
rules_dir="$test_dir/rules.d"
home_dir="$test_dir/home"
omarchy_path="$test_dir/omarchy"
xcompose="$home_dir/.XCompose"
packaged_xcompose="include \"$omarchy_path/default/xcompose\""
power_rule="$rules_dir/99-power-profile.rules"
wifi_rule="$rules_dir/99-wifi-powersave.rules"
reload_needed_marker="$test_dir/reload-needed"
reload_marker_prefix="$test_dir/reload-needed"
power_reload_marker="$reload_marker_prefix-99-power-profile.rules"
wifi_reload_marker="$reload_marker_prefix-99-wifi-powersave.rules"
udev_control="$test_dir/udev-control"
migration="$test_dir/migration.sh"
# These paths become operands to privileged commands. Keep them fixed in the
# shipped migration and retarget a scratch copy for the unprivileged test; an
# environment override would let the caller choose what root removes.
grep -Fxq 'rules_dir=/etc/udev/rules.d' "$shipped_migration" ||
fail "the production udev rules directory is a fixed literal"
grep -Fxq 'reload_marker_prefix=/var/lib/omarchy/migrations/1788102906-udev-reload-needed' "$shipped_migration" ||
fail "the production reload marker is a fixed literal"
grep -Fxq 'udev_control=/run/udev/control' "$shipped_migration" ||
fail "the production udev control path is a fixed literal"
if grep -q 'OMARCHY_UDEV_' "$shipped_migration"; then
fail "the migration does not accept caller-controlled privileged paths"
fi
sed \
-e "s|^rules_dir=/etc/udev/rules.d$|rules_dir=$rules_dir|" \
-e "s|^reload_marker_prefix=/var/lib/omarchy/migrations/1788102906-udev-reload-needed$|reload_marker_prefix=$reload_marker_prefix|" \
-e "s|^udev_control=/run/udev/control$|udev_control=$udev_control|" \
"$shipped_migration" >"$migration"
pass "migration keeps privileged production paths caller-independent"
reset_machine() {
rm -rf "$rules_dir" "$home_dir" "$reload_needed_marker" "$udev_control"
mkdir -p "$rules_dir" "$home_dir"
rm -rf "$rules_dir" "$home_dir" "$omarchy_path" "$power_reload_marker" "$wifi_reload_marker" "$udev_control"
mkdir -p "$rules_dir" "$home_dir" "$omarchy_path/default"
touch "$omarchy_path/default/xcompose"
touch "$udev_control"
}
@@ -61,9 +125,7 @@ run_migration() {
: >"$CALLS"
HOME="$home_dir" \
OMARCHY_UDEV_RULES_DIR="$rules_dir" \
OMARCHY_UDEV_RELOAD_NEEDED_MARKER="$reload_needed_marker" \
OMARCHY_UDEV_CONTROL="$udev_control" \
OMARCHY_PATH="$omarchy_path" \
PATH="$test_dir/bin:$PATH" \
bash -euo pipefail "$migration" >/dev/null
}
@@ -72,6 +134,51 @@ reload_count() {
grep -cx 'udevadm control --reload' "$CALLS" || true
}
write_xcompose() {
local include="$1"
cat >"$xcompose" <<EOF
# Include fast emoji access
$include
# Keep the user's own sequences
<Multi_key> <space> <n> : "Test User"
<Multi_key> <space> <e> : "test@example.com"
EOF
}
# #8175's non-udev behavior belongs here so one migration owns the whole legacy
# compatibility-link repair. Omarchy 3 emitted %H, while users may have changed
# it to ~ or its expanded value.
for legacy_include in \
'include "%H/.local/share/omarchy/default/xcompose"' \
'include "~/.local/share/omarchy/default/xcompose"' \
"include \"$home_dir/.local/share/omarchy/default/xcompose\""; do
reset_machine
write_xcompose "$legacy_include"
run_migration
grep -qxF "$packaged_xcompose" "$xcompose" ||
fail "migration repoints $legacy_include at the active Omarchy tree" "$(cat "$xcompose")"
grep -qF '<Multi_key> <space> <e> : "test@example.com"' "$xcompose" ||
fail "migration discards the user's own compose sequences"
grep -qxF 'omarchy-restart-xcompose' "$CALLS" ||
fail "migration does not reload XCompose after rewriting its include" "$(cat "$CALLS")"
done
pass "migration repoints every legacy XCompose include and preserves custom sequences"
before=$(sha256sum "$xcompose")
run_migration
[[ $(sha256sum "$xcompose") == "$before" ]] || fail "migration changes an already repaired XCompose file"
[[ ! -s $CALLS ]] || fail "migration restarts XCompose when nothing changed" "$(cat "$CALLS")"
pass "migration is idempotent on an already repaired XCompose file"
reset_machine
run_migration
[[ ! -e $xcompose ]] || fail "migration creates a missing XCompose file"
[[ ! -s $CALLS ]] || fail "migration acts when XCompose and legacy udev rules are absent" "$(cat "$CALLS")"
pass "migration leaves a home without XCompose alone"
# 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() {
@@ -81,6 +188,20 @@ SUBSYSTEM=="power_supply", ATTR{type}=="USB", RUN+="/usr/bin/systemd-run --no-bl
RULE
}
write_initial_vulnerable_power_rule() {
cat >"$power_rule" <<'RULE'
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-power-profile-battery --property=After=power-profiles-daemon.service /home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set battery"
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 /home/someuser/.local/share/omarchy/bin/omarchy-powerprofiles-set ac"
RULE
}
write_final_vulnerable_power_rule() {
cat >"$power_rule" <<'RULE'
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --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 --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"
@@ -88,6 +209,13 @@ SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/home/s
RULE
}
write_systemd_vulnerable_wifi_rule() {
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/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave on"
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-off /home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave off"
RULE
}
reset_machine
write_vulnerable_power_rule
run_migration
@@ -96,10 +224,35 @@ run_migration
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" ||
grep -q '^sudo /usr/bin/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"
grep -q '^sudo /usr/bin/install -Dm644 /dev/null ' "$CALLS" &&
grep -q '^sudo /usr/bin/udevadm control --reload$' "$CALLS" ||
fail "migration pins privileged helpers to root-owned paths" "$(cat "$CALLS")"
pass "migration pins install, rm, and udevadm to root-owned paths"
reset_machine
write_initial_vulnerable_power_rule
run_migration
[[ ! -e $power_rule ]] ||
fail "migration removes the initial AC/battery power rule" "$(cat "$power_rule")"
(( $(reload_count) == 1 )) ||
fail "migration reloads udev after removing the initial power rule" "$(cat "$CALLS")"
pass "migration removes the initial AC/battery power rule body"
reset_machine
write_final_vulnerable_power_rule
run_migration
[[ ! -e $power_rule ]] ||
fail "migration removes the final Omarchy 3 power rule" "$(cat "$power_rule")"
(( $(reload_count) == 1 )) ||
fail "migration reloads udev after removing the final power rule" "$(cat "$CALLS")"
pass "migration removes the final Omarchy 3 power rule body"
reset_machine
write_vulnerable_wifi_rule
run_migration
@@ -108,6 +261,16 @@ run_migration
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"
reset_machine
write_systemd_vulnerable_wifi_rule
run_migration
[[ ! -e $wifi_rule ]] ||
fail "migration removes the systemd-run Wi-Fi rule" "$(cat "$wifi_rule")"
(( $(reload_count) == 1 )) ||
fail "migration reloads udev after removing the systemd-run Wi-Fi rule" "$(cat "$CALLS")"
pass "migration removes the systemd-run Wi-Fi rule body"
# 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 )) ||
@@ -141,17 +304,56 @@ reload_status=$?
set -e
(( reload_status != 0 )) || fail "migration fails when a running udevd cannot reload"
[[ ! -e $wifi_rule && -e $reload_needed_marker ]] ||
[[ ! -e $wifi_rule && -e $wifi_reload_marker ]] ||
fail "migration records a deleted rule whose daemon reload is still pending"
pass "migration keeps a failed udev reload pending"
run_migration
[[ ! -e $reload_needed_marker ]] || fail "migration clears the reload marker after a successful retry"
[[ ! -e $wifi_reload_marker ]] || fail "migration clears the reload marker after a successful retry"
(( $(reload_count) == 1 )) ||
fail "migration retries the pending udev reload" "$(cat "$CALLS")"
pass "migration retries and completes a previously failed udev reload"
# A marker is created before removal. A concurrent user who sees that phase
# must not consume it and reload while the vulnerable file is still active.
reset_machine
write_vulnerable_power_rule
touch "$power_reload_marker"
run_migration
remove_line=$(grep -n '^sudo /usr/bin/rm -f .*99-power-profile\.rules$' "$CALLS" | head -n1 | cut -d: -f1)
reload_line=$(grep -n '^sudo /usr/bin/udevadm control --reload$' "$CALLS" | head -n1 | cut -d: -f1)
[[ -n $remove_line && -n $reload_line ]] || fail "concurrent repair records removal and reload" "$(cat "$CALLS")"
(( remove_line < reload_line )) || fail "a concurrent repair reloads before removing the active rule" "$(cat "$CALLS")"
[[ ! -e $power_reload_marker ]] || fail "concurrent repair leaves a completed power reload pending"
pass "a concurrent run cannot consume a marker before rule removal"
# Each rule owns its reload state. Concurrent repairs of different files cannot
# clear one another's evidence that udev still needs to reload.
reset_machine
touch "$power_reload_marker" "$wifi_reload_marker"
run_migration
(( $(reload_count) == 2 )) || fail "migration finishes both independent pending reloads" "$(cat "$CALLS")"
[[ ! -e $power_reload_marker && ! -e $wifi_reload_marker ]] ||
fail "migration leaves an independent reload marker behind"
pass "power and Wi-Fi removals keep independent durable reload state"
# A safe replacement installed after an interrupted removal still needs one
# reload to displace the vulnerable ruleset already held by udevd.
reset_machine
cat >"$power_rule" <<'RULE'
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/local/bin/admin-power-hook"
RULE
touch "$power_reload_marker"
run_migration
[[ -e $power_rule ]] || fail "migration removes a safe replacement rule"
(( $(reload_count) == 1 )) || fail "migration does not reload a safe replacement after interruption" "$(cat "$CALLS")"
[[ ! -e $power_reload_marker ]] || fail "migration leaves the replacement reload pending"
pass "migration reloads a safe replacement after an interrupted removal"
# A chroot or stopped daemon has no in-memory ruleset to update. An absent udev
# control socket is therefore a completed removal, not a permanent migration
# failure waiting for a daemon that is not running.
@@ -160,7 +362,7 @@ rm -f "$udev_control"
write_vulnerable_wifi_rule
run_migration
[[ ! -e $wifi_rule && ! -e $reload_needed_marker ]] ||
[[ ! -e $wifi_rule && ! -e $wifi_reload_marker ]] ||
fail "migration completes the disk-only repair when udevd is not running"
(( $(reload_count) == 0 )) ||
fail "migration does not contact an absent udevd" "$(cat "$CALLS")"
@@ -183,6 +385,41 @@ run_migration
fail "migration touches nothing when the legacy rules are absent" "$(cat "$CALLS")"
pass "migration leaves a machine without the legacy rules alone"
# An unreadable same-named file cannot safely be classified as generated or
# custom. Require an administrator rather than silently disabling their rule.
reset_machine
write_vulnerable_power_rule
chmod 000 "$power_rule"
set +e
run_migration 2>"$test_dir/unreadable-rule.out"
unreadable_status=$?
set -e
(( unreadable_status != 0 )) || fail "migration accepts a rule it could not inspect"
[[ -e $power_rule ]] || fail "migration disables a rule it could not inspect"
grep -q 'Ask an administrator to run omarchy-migrate' "$test_dir/unreadable-rule.out" ||
fail "migration gives no administrator guidance for an unreadable rule" "$(cat "$test_dir/unreadable-rule.out")"
[[ ! -s $CALLS ]] || fail "migration escalates before classifying an unreadable rule" "$(cat "$CALLS")"
chmod 644 "$power_rule"
pass "migration fails without changing an unreadable administrator rule"
reset_machine
write_vulnerable_power_rule
chmod 600 "$rules_dir"
set +e
run_migration 2>"$test_dir/unsearchable-rules-dir.out"
unsearchable_status=$?
set -e
(( unsearchable_status != 0 )) || fail "migration accepts a rules directory it could not inspect"
grep -q 'Ask an administrator to run omarchy-migrate' "$test_dir/unsearchable-rules-dir.out" ||
fail "migration gives no administrator guidance for an unsearchable rules directory" "$(cat "$test_dir/unsearchable-rules-dir.out")"
[[ ! -s $CALLS ]] || fail "migration escalates before inspecting the rules directory" "$(cat "$CALLS")"
chmod 755 "$rules_dir"
pass "migration fails closed when it cannot search the rules directory"
# 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
@@ -207,20 +444,108 @@ run_migration
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.
# A vulnerable rule that an administrator extended is no longer the exact file
# Omarchy generated. Preserve the whole file under a suffix udev ignores rather
# than deleting their addition or leaving the vulnerable command active.
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"
write_vulnerable_power_rule
cat >>"$power_rule" <<'RULE'
ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/local/sbin/admin-power-hook"
RULE
run_migration
write_vulnerable_wifi_rule
cp "$power_rule" "$test_dir/mixed-power-rule.before"
[[ ! -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"
set +e
run_migration 2>"$test_dir/mixed-power-rule.out"
mixed_status=$?
set -e
(( mixed_status == 0 )) || fail "migration fails after safely quarantining a modified vulnerable rule" "$(cat "$test_dir/mixed-power-rule.out")"
[[ ! -e $power_rule && -e $power_rule.omarchy-disabled && ! -e $wifi_rule ]] ||
fail "migration leaves a modified vulnerable rule active"
cmp -s "$test_dir/mixed-power-rule.before" "$power_rule.omarchy-disabled" ||
fail "migration changes a mixed rule while quarantining it" "$(cat "$power_rule.omarchy-disabled")"
(( $(reload_count) == 2 )) || fail "migration does not continue through every vulnerable rule after quarantine" "$(cat "$CALLS")"
grep -q 'Quarantined.*\.omarchy-disabled' "$test_dir/mixed-power-rule.out" ||
fail "migration does not explain where it preserved a mixed rule" "$(cat "$test_dir/mixed-power-rule.out")"
[[ ! -e $power_reload_marker ]] || fail "migration leaves a completed quarantine reload pending"
grep -q '^sudo /usr/bin/mv --no-clobber -- .*99-power-profile\.rules .*99-power-profile\.rules\.omarchy-disabled$' "$CALLS" ||
fail "migration does not pin quarantine moves to root-owned mv" "$(cat "$CALLS")"
pass "migration quarantines a mixed rule and continues repairing the machine"
run_migration
[[ ! -e $power_rule && -e $power_rule.omarchy-disabled ]] ||
fail "a quarantine retry does not preserve the disabled rule"
[[ ! -s $CALLS ]] || fail "a quarantine retry changes machine state" "$(cat "$CALLS")"
pass "migration is a no-op after completing a quarantine"
# Reformatting RUN does not make the user-controlled command safe, but it does
# make the file something Omarchy cannot delete wholesale without guessing.
reset_machine
cat >"$wifi_rule" <<'RULE'
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN += "/home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave on"
RULE
cp "$wifi_rule" "$test_dir/reformatted-wifi-rule.before"
set +e
run_migration 2>"$test_dir/reformatted-wifi-rule.out"
reformatted_status=$?
set -e
(( reformatted_status == 0 )) || fail "migration fails after quarantining a reformatted vulnerable rule" "$(cat "$test_dir/reformatted-wifi-rule.out")"
[[ ! -e $wifi_rule && -e $wifi_rule.omarchy-disabled ]] ||
fail "migration leaves a reformatted vulnerable rule active"
cmp -s "$test_dir/reformatted-wifi-rule.before" "$wifi_rule.omarchy-disabled" ||
fail "migration changes a reformatted rule while quarantining it" "$(cat "$wifi_rule.omarchy-disabled")"
(( $(reload_count) == 1 )) || fail "migration does not reload udev after quarantining a reformatted rule" "$(cat "$CALLS")"
pass "migration quarantines reformatted vulnerable rules"
# All assignment forms udev accepts for RUN can execute the same user-home
# helper. None may evade the conservative quarantine detector.
variant_number=0
for run_assignment in 'RUN{program}+=' 'RUN=' 'RUN:=' 'RUN+=e'; do
((++variant_number))
reset_machine
printf 'SUBSYSTEM=="power_supply", ATTR{type}=="Mains", %s"/home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave on"\n' "$run_assignment" >"$wifi_rule"
cp "$wifi_rule" "$test_dir/run-variant-$variant_number.before"
set +e
run_migration 2>"$test_dir/run-variant-$variant_number.out"
variant_status=$?
set -e
(( variant_status == 0 )) || fail "migration fails after quarantining udev assignment $run_assignment" "$(cat "$test_dir/run-variant-$variant_number.out")"
[[ ! -e $wifi_rule && -e $wifi_rule.omarchy-disabled ]] ||
fail "migration leaves udev assignment $run_assignment active"
cmp -s "$test_dir/run-variant-$variant_number.before" "$wifi_rule.omarchy-disabled" ||
fail "migration changes udev assignment $run_assignment while quarantining it"
(( $(reload_count) == 1 )) || fail "migration does not reload after quarantining $run_assignment" "$(cat "$CALLS")"
done
pass "migration quarantines every valid RUN assignment form"
# Never overwrite an earlier preserved file. Choose another inactive suffix so
# the active vulnerability is still neutralized without losing either copy.
reset_machine
cat >"$wifi_rule" <<'RULE'
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN="/home/someuser/.local/share/omarchy/bin/omarchy-wifi-powersave on"
RULE
cp "$wifi_rule" "$test_dir/collision-active.before"
printf '%s\n' 'older preserved rule' >"$wifi_rule.omarchy-disabled"
cp "$wifi_rule.omarchy-disabled" "$test_dir/collision-backup.before"
set +e
run_migration 2>"$test_dir/quarantine-collision.out"
collision_status=$?
set -e
(( collision_status == 0 )) || fail "migration fails to resolve an existing quarantine" "$(cat "$test_dir/quarantine-collision.out")"
[[ ! -e $wifi_rule && -e $wifi_rule.omarchy-disabled.1 ]] || fail "migration leaves the colliding vulnerable rule active"
cmp -s "$test_dir/collision-backup.before" "$wifi_rule.omarchy-disabled" || fail "migration overwrites the existing quarantine"
cmp -s "$test_dir/collision-active.before" "$wifi_rule.omarchy-disabled.1" || fail "migration changes the new quarantine"
grep -q 'Quarantined.*\.omarchy-disabled\.1' "$test_dir/quarantine-collision.out" ||
fail "migration does not report the unique quarantine path" "$(cat "$test_dir/quarantine-collision.out")"
(( $(reload_count) == 1 )) || fail "migration does not reload after resolving a quarantine collision" "$(cat "$CALLS")"
pass "migration preserves both files on a quarantine collision"
# A comment that does not continue still hides nothing behind it: the file holds
# no active RUN+= at all and stays.
@@ -252,6 +577,7 @@ pass "migration keeps a legacy filename already repointed at /usr/bin"
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 /srv/retired-installer/.local/share/omarchy/bin/omarchy-wifi-powersave on"
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/systemd-run --no-block --collect --unit=omarchy-wifi-powersave-off /srv/retired-installer/.local/share/omarchy/bin/omarchy-wifi-powersave off"
RULE
run_migration
@@ -266,9 +592,6 @@ write_vulnerable_wifi_rule
set +e
HOME="$home_dir" \
OMARCHY_UDEV_RULES_DIR="$rules_dir" \
OMARCHY_UDEV_RELOAD_NEEDED_MARKER="$reload_needed_marker" \
OMARCHY_UDEV_CONTROL="$udev_control" \
PATH="$test_dir/failing-bin:$PATH" \
bash -euo pipefail "$migration" >"$test_dir/elevation-failure.out" 2>&1
failure_status=$?
@@ -289,9 +612,13 @@ cat >"$test_dir/failing-bin/sudo" <<'STUB'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$CALLS"
if [[ $* == *99-wifi-powersave.rules ]]; then
if [[ ${1:-} == "/usr/bin/rm" && ${*: -1} == */rules.d/99-wifi-powersave.rules ]]; then
exit 1
fi
if [[ ${1:-} == "/usr/bin/udevadm" ]]; then
shift
exec "$UDEVADM_STUB" "$@"
fi
exec "$@"
STUB
chmod +x "$test_dir/failing-bin/sudo"
@@ -299,9 +626,6 @@ chmod +x "$test_dir/failing-bin/sudo"
set +e
HOME="$home_dir" \
OMARCHY_UDEV_RULES_DIR="$rules_dir" \
OMARCHY_UDEV_RELOAD_NEEDED_MARKER="$reload_needed_marker" \
OMARCHY_UDEV_CONTROL="$udev_control" \
PATH="$test_dir/failing-bin:$test_dir/bin:$PATH" \
bash -euo pipefail "$migration" >"$test_dir/partial-failure.out" 2>&1
partial_status=$?
@@ -309,7 +633,7 @@ set -e
(( partial_status != 0 )) || fail "migration fails when the second rule cannot be removed"
[[ ! -e $power_rule && -e $wifi_rule ]] || fail "migration preserves the expected partial-removal state"
[[ -e $reload_needed_marker ]] || fail "migration records the second rule removal as still pending"
[[ -e $wifi_reload_marker ]] || fail "migration records the second rule removal as still pending"
(( $(reload_count) == 1 )) ||
fail "migration reloads udev before a later removal failure" "$(cat "$CALLS")"
pass "a later removal failure cannot leave an already-deleted rule loaded"
@@ -325,20 +649,3 @@ 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` reports its
# complaint on line 1 for a rule split this way, so the three lines are one live
# rule. The split falls inside the RUN+= value on purpose -- with the whole
# RUN+= below the comment the assertion passes even against an implementation
# that throws the pending half away, which is the shape this guards against.
reset_machine
cat >"$power_rule" <<'RULE'
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --unit=omarchy-power-profile \
# split for readability
/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"