Bind update inhibitor cleanup to owned process identity

Keep inhibitor state in validated private directories and verify the recorded owner, PID, start time and launch token before signaling. Authenticate the held command before detaching and drop it back to the invoking user.

Serialize launch and cancellation, identify the child before publishing its state, and preserve caller-owned idle choices. Cover cross-account fallback state, process identity, cancellation, retry, and update-lock handling with isolated regressions.
This commit is contained in:
Afonso Oliveira committed 2026-09-21 16:09:04 +01:00
1 parent 56ca654dc8
commit 6af052fcc3
5 files changed
+1237 -85

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ source, target, name = map(Path,sys.argv[1:])
p=target/name
p.parent.mkdir(parents=True,exist_ok=True)
s=(source/name).read_text().replace('$HOME', '$SUDO_TEST_HOME')
for command in ['sudo','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']:
for command in ['sudo','pkexec','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']:
s=s.replace('/usr/bin/'+command, str(target/'mock'/command))
s=s.replace('PATH=/usr/bin:/usr/sbin:/bin:/sbin', 'PATH="'+str(target/'bin')+':/usr/bin:/usr/sbin:/bin:/sbin"')
p.write_text(s)
+125 -10
View File
@@ -8,12 +8,21 @@ source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
test_tmp="$boundary_tmp"
stub_bin="$SUDO_TEST_ROOT/bin"
test_home="$SUDO_TEST_HOME"
runtime_dir="$test_tmp/runtime"
mkdir -p "$runtime_dir"
runtime_dir="/run/user/$(id -u)"
test_run_id="test-$BASHPID-$RANDOM"
update_lock_name="omarchy-update-$test_run_id.lock"
stay_awake_dir_name="omarchy-update-stay-awake-$test_run_id"
trap 'rm -rf -- "$runtime_dir/$stay_awake_dir_name"; rm -f -- "$runtime_dir/$update_lock_name"; rm -rf -- "$boundary_tmp"' EXIT
for command in omarchy-update omarchy-update-lock omarchy-update-stay-awake; do
rm -f "$SUDO_TEST_ROOT/bin/$command"
copy_boundary_file "bin/$command"
done
sed -i \
-e "s/omarchy-update\.lock/$update_lock_name/g" \
-e "s#state_dir=\"\$state_base/omarchy-update-stay-awake\"#state_dir=\"\$state_base/$stay_awake_dir_name\"#" \
"$SUDO_TEST_ROOT/bin/omarchy-update" \
"$SUDO_TEST_ROOT/bin/omarchy-update-lock" \
"$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake"
cat >"$SUDO_TEST_ROOT/mock/setpriv" <<'STUB'
#!/bin/bash
while [[ ${1:-} == --* ]]; do
@@ -67,6 +76,7 @@ for command in \
done
write_stub omarchy-update-available 'exit 1'
write_stub pkexec 'exec "$@"'
ln -s ../bin/pkexec "$SUDO_TEST_ROOT/mock/pkexec"
write_stub systemd-inhibit 'while [[ $1 == --* ]]; do shift; done; exec "$@"'
ln -s ../bin/systemd-inhibit "$SUDO_TEST_ROOT/mock/systemd-inhibit"
@@ -119,7 +129,7 @@ done
inhibitor_pid=$(<"$inhibit_pid_file")
kill -0 "$inhibitor_pid" 2>/dev/null || fail "sleep inhibitor is still running when its descriptors are inspected"
lock_target=$(readlink -f "$runtime_dir/omarchy-update.lock")
lock_target=$(readlink -f "$runtime_dir/$update_lock_name")
inhibitor_holds_lock=0
for fd in /proc/"$inhibitor_pid"/fd/*; do
[[ -e $fd ]] || continue
@@ -150,10 +160,11 @@ if (( EUID != 0 )); then
#!/bin/bash
set -euo pipefail
omarchy-update-stay-awake start
[[ -s $XDG_RUNTIME_DIR/omarchy-update-stay-awake/inhibit-pid ]]
[[ -s $XDG_RUNTIME_DIR/REPLACE_STAY_AWAKE_DIR/inhibit-pid ]]
omarchy-update-stay-awake stop
[[ ! -e $XDG_RUNTIME_DIR/omarchy-update-stay-awake/inhibit-pid ]]
[[ ! -e $XDG_RUNTIME_DIR/REPLACE_STAY_AWAKE_DIR/inhibit-pid ]]
SH
sed -i "s/REPLACE_STAY_AWAKE_DIR/$stay_awake_dir_name/g" "$terminal_driver"
chmod +x "$terminal_driver"
SUDO_LOG="$sudo_log" PKEXEC_MARKER="$pkexec_marker" INHIBIT_PID_FILE="$terminal_inhibit_pid_file" \
@@ -163,6 +174,76 @@ SH
[[ ! -e $pkexec_marker ]] || fail "terminal sleep inhibition does not use pkexec"
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
pass "terminal updates use sudo instead of Polkit for sleep inhibition"
wait_for_process_exit() {
local process_pid="$1"
for _ in {1..100}; do
kill -0 "$process_pid" 2>/dev/null || return 0
[[ $(awk '{ print $3 }' "/proc/$process_pid/stat" 2>/dev/null || true) == "Z" ]] && return 0
sleep 0.02
done
return 1
}
delayed_marker="$test_tmp/delayed-inhibitor"
delayed_helper_pid_file="$test_tmp/delayed-helper-pid"
write_stub systemd-inhibit 'echo "$$" >"$DELAYED_MARKER"; sleep 0.4; while [[ $1 == --* ]]; do shift; done; exec "$@"'
# Keep the start helper's stdin attached to the private PTY so it takes the
# sudo -b branch, then signal only that helper before the held child publishes.
delayed_terminal_driver="$test_tmp/delayed-terminal-stay-awake"
cat >"$delayed_terminal_driver" <<'SH'
#!/bin/bash
set +e
omarchy-update-stay-awake start </dev/tty &
helper_pid=$!
echo "$helper_pid" >"$DELAYED_HELPER_PID_FILE"
wait "$helper_pid"
exit $?
SH
chmod +x "$delayed_terminal_driver"
DELAYED_MARKER="$delayed_marker" DELAYED_HELPER_PID_FILE="$delayed_helper_pid_file" \
run_with_lock_env script -qefc "$delayed_terminal_driver" /dev/null >"$test_tmp/delayed-terminal.out" 2>&1 &
delayed_terminal_driver_pid=$!
for _ in {1..100}; do
[[ -s $delayed_marker && -s $delayed_helper_pid_file ]] && break
sleep 0.02
done
[[ -s $delayed_marker && -s $delayed_helper_pid_file ]] || fail "terminal cancellation reaches the delayed launch window"
kill -TERM "$(<"$delayed_helper_pid_file")"
wait "$delayed_terminal_driver_pid" || true
delayed_inhibitor_pid=$(<"$delayed_marker")
wait_for_process_exit "$delayed_inhibitor_pid" || fail "terminal cancellation leaves no delayed inhibitor"
[[ ! -e $runtime_dir/$stay_awake_dir_name ]] || fail "terminal cancellation leaves no launch state"
pass "terminal cancellation rolls back delayed publication"
# With redirected stdin the same helper takes the graphical pkexec branch.
: >"$delayed_marker"
delayed_graphical_helper_pid_file="$test_tmp/delayed-graphical-helper-pid"
delayed_graphical_driver="$test_tmp/delayed-graphical-stay-awake"
cat >"$delayed_graphical_driver" <<'SH'
#!/bin/bash
echo "$$" >"$DELAYED_HELPER_PID_FILE"
exec omarchy-update-stay-awake start </dev/null
SH
chmod +x "$delayed_graphical_driver"
DELAYED_MARKER="$delayed_marker" DELAYED_HELPER_PID_FILE="$delayed_graphical_helper_pid_file" \
run_with_lock_env "$delayed_graphical_driver" >"$test_tmp/delayed-graphical.out" 2>&1 &
delayed_graphical_driver_pid=$!
for _ in {1..100}; do
[[ -s $delayed_marker && -s $delayed_graphical_helper_pid_file ]] && break
sleep 0.02
done
[[ -s $delayed_marker && -s $delayed_graphical_helper_pid_file ]] || fail "graphical cancellation reaches the delayed launch window"
delayed_graphical_helper_pid=$(<"$delayed_graphical_helper_pid_file")
kill -TERM "$delayed_graphical_helper_pid"
wait "$delayed_graphical_driver_pid" || true
delayed_inhibitor_pid=$(<"$delayed_marker")
wait_for_process_exit "$delayed_inhibitor_pid" || fail "graphical cancellation leaves no delayed inhibitor"
[[ ! -e $runtime_dir/$stay_awake_dir_name ]] || fail "graphical cancellation leaves no launch state"
pass "graphical cancellation rolls back delayed publication"
write_stub systemd-inhibit 'while [[ $1 == --* ]]; do shift; done; exec "$@"'
fi
# Update-owned Stay Awake state must be cleared before the restart helper can
@@ -198,12 +279,45 @@ OMARCHY_UPDATE_LOGGED=1 EXPECT_STAY_AWAKE=1 run_with_lock_env "$SUDO_TEST_ROOT/b
[[ -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update preserves pre-existing Stay Awake state"
pass "omarchy-update restores only its own Stay Awake state before restart handling"
# Model package replacement while the existing updater is still running:
# start writes the old two-field state; the transaction installs the real new
# helper, whose stop must clean that state before reboot handling.
cp "$stub_bin/omarchy-update-stay-awake" "$test_tmp/inhibitor-after-upgrade"
write_stub omarchy-update-stay-awake '
set -e
[[ $1 == "start" ]]
umask 022
state="$XDG_RUNTIME_DIR/$LEGACY_STATE_NAME"
mkdir -p "$state" "$SUDO_TEST_HOME/.local/state/omarchy/indicators"
( exec {OMARCHY_UPDATE_LOCK_FD}>&-; exec sleep infinity ) &
pid=$!
printf "%s %s\n" "$pid" "$(awk '\''{ print $22 }'\'' /proc/$pid/stat)" >"$state/inhibit-pid"
printf "%s:1:1\n" "$$" >"$state/idle-owner"
/usr/bin/cp "$state/idle-owner" "$SUDO_TEST_HOME/.local/state/omarchy/indicators/stay-awake"'
write_stub omarchy-update-system-pkgs '
/usr/bin/cp "$INHIBITOR_AFTER_UPGRADE" "$OMARCHY_PATH/bin/omarchy-update-stay-awake"'
write_stub omarchy-update-restart '
if [[ $1 == "--reboot-only" ]]; then
[[ ! -e $SUDO_TEST_HOME/.local/state/omarchy/indicators/stay-awake ]] || exit 91
[[ ! -e $XDG_RUNTIME_DIR/$LEGACY_STATE_NAME ]] || exit 92
touch "$UPGRADE_RESTARTED"
fi'
rm -f "$test_home/.local/state/omarchy/indicators/stay-awake"
OMARCHY_UPDATE_LOGGED=1 LEGACY_STATE_NAME="$stay_awake_dir_name" \
INHIBITOR_AFTER_UPGRADE="$test_tmp/inhibitor-after-upgrade" \
UPGRADE_RESTARTED="$test_tmp/upgrade-restarted" \
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update" -y
[[ -e $test_tmp/upgrade-restarted ]] || fail "first upgrade did not reach reboot handling"
pass "first upgrade cleans old inhibitor state with the newly installed helper"
# Stale cleanup state from a killed update must not override a Stay Awake choice
# the user made afterward.
stay_awake_helper_state="$runtime_dir/omarchy-update-stay-awake"
stay_awake_helper_state="$runtime_dir/$stay_awake_dir_name"
stay_awake_state="$test_home/.local/state/omarchy/indicators/stay-awake"
mkdir -p "$stay_awake_helper_state" "$(dirname "$stay_awake_state")"
printf '%s\n' "old-update-owner" >"$stay_awake_helper_state/idle-owner"
mkdir -m 700 -p "$stay_awake_helper_state"
mkdir -p "$(dirname "$stay_awake_state")"
printf '%s\n' "123:456:789" >"$stay_awake_helper_state/idle-owner"
chmod 600 "$stay_awake_helper_state/idle-owner"
printf '%s\n' "user-choice" >"$stay_awake_state"
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
@@ -215,8 +329,9 @@ pass "stale update ownership preserves a newer Stay Awake choice"
sleep 30 >/dev/null &
unrelated_pid=$!
unrelated_start_time=$(awk '{ print $22 }' "/proc/$unrelated_pid/stat")
mkdir -p "$stay_awake_helper_state"
printf '%s %s\n' "$unrelated_pid" "$((unrelated_start_time + 1))" >"$stay_awake_helper_state/inhibit-pid"
mkdir -m 700 -p "$stay_awake_helper_state"
printf '1 %s %s %s %032x\n' "$unrelated_pid" "$((unrelated_start_time + 1))" "$(id -u)" 1 >"$stay_awake_helper_state/inhibit-pid"
chmod 600 "$stay_awake_helper_state/inhibit-pid"
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
kill -0 "$unrelated_pid" 2>/dev/null ||
@@ -0,0 +1,626 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
test_processes=()
test_runtime_created=""
cleanup_test() {
for pid in "${test_processes[@]}"; do kill "$pid" 2>/dev/null || true; done
[[ -z ${state_dir:-} ]] || rm -rf -- "$state_dir"
[[ -z ${state_hardlink:-} ]] || rm -f -- "$state_hardlink"
rm -rf -- "$test_tmp"
[[ -z $test_runtime_created ]] || rmdir -- "$test_runtime_created" 2>/dev/null || true
}
trap cleanup_test EXIT
stub_bin="$test_tmp/bin"
mapped_root="$test_tmp/omarchy"
test_home="$test_tmp/home"
runtime_dir=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
if [[ ! -d $runtime_dir || -L $runtime_dir || $(stat -Lc '%u %a' "$runtime_dir" 2>/dev/null || true) != "$(id -u) 700" ]]; then
if (( EUID != 0 )); then
fail "test needs a private XDG runtime directory or root namespace"
fi
runtime_dir=$(mktemp -d -p /run omarchy-stay-awake-runtime.XXXXXXXX)
chmod 0700 "$runtime_dir"
test_runtime_created="$runtime_dir"
fi
test_run_id="test-$BASHPID-$RANDOM"
state_dir="$runtime_dir/omarchy-update-stay-awake-$test_run_id"
state_hardlink="$runtime_dir/.omarchy-update-stay-awake-hardlink-$test_run_id"
inhibitor_log="$test_tmp/inhibitors"
mkdir -p "$stub_bin" "$test_home" "$mapped_root/bin" "$mapped_root/default/omarchy/sudo-no-update"
: >"$inhibitor_log"
cat >"$stub_bin/pkexec" <<'SH'
#!/bin/bash
exec "$@"
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
case ${1:-} in
-h) echo 'usage: sudo [-bHkNnPS] command'; exit 0 ;;
-k|-K|-v) exit 0 ;;
esac
background=0
while (( $# )); do
case "$1" in
-N|-n) shift ;;
-b) background=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( background )); then
"$@" &
else
exec "$@"
fi
SH
cat >"$stub_bin/setpriv" <<'SH'
#!/bin/bash
while [[ ${1:-} == --* ]]; do
case "$1" in
--reuid|--regid) shift 2 ;;
--clear-groups) shift ;;
*) exit 90 ;;
esac
done
exec "$@"
SH
cat >"$stub_bin/systemd-inhibit" <<'SH'
#!/bin/bash
[[ ${SYSTEMD_FAIL:-0} == "0" ]] || exit 42
printf '%s\n' "$$" >>"$INHIBITOR_LOG"
if [[ -n ${CREATE_BAD_IDLE:-} ]]; then
ln -s "$CREATE_BAD_IDLE" "$TEST_STATE_DIR/idle-owner"
fi
trap 'exit 0' TERM
while [[ ${1:-} == --* ]]; do shift; done
exec "$@"
SH
cat >"$stub_bin/omarchy-toggle-idle" <<'SH'
#!/bin/bash
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
case "$1" in
stay-awake)
mkdir -p "$(dirname "$state_file")"
touch "$state_file"
;;
allow-idle)
rm -f "$state_file"
;;
esac
SH
chmod +x "$stub_bin"/*
ln -s "$stub_bin/omarchy-toggle-idle" "$mapped_root/bin/omarchy-toggle-idle"
mapped_helper="$mapped_root/bin/omarchy-update-stay-awake"
cp "$ROOT/bin/omarchy-update-stay-awake" "$mapped_helper"
cp "$ROOT/bin/omarchy-security-functions" "$mapped_root/bin/omarchy-security-functions"
cp "$ROOT/default/omarchy/sudo-no-update/sudo" "$mapped_root/default/omarchy/sudo-no-update/sudo"
for mapped_file in \
"$mapped_helper" \
"$mapped_root/bin/omarchy-security-functions" \
"$mapped_root/default/omarchy/sudo-no-update/sudo"; do
sed -i \
-e "s#/usr/bin/sudo#$stub_bin/sudo#g" \
-e "s#/usr/bin/pkexec#$stub_bin/pkexec#g" \
-e "s#/usr/bin/systemd-inhibit#$stub_bin/systemd-inhibit#g" \
-e "s#/usr/bin/setpriv#$stub_bin/setpriv#g" \
-e 's#state_dir="$state_base/omarchy-update-stay-awake"#state_dir="$state_base/omarchy-update-stay-awake-${OMARCHY_TEST_RUN_ID:?}"#' \
"$mapped_file"
done
chmod +x "$mapped_helper" "$mapped_root/default/omarchy/sudo-no-update/sudo"
run_helper() {
HOME="$test_home" \
XDG_RUNTIME_DIR="$runtime_dir" \
INHIBITOR_LOG="$inhibitor_log" \
TEST_STATE_DIR="$state_dir" \
OMARCHY_TEST_RUN_ID="$test_run_id" \
OMARCHY_PATH="$mapped_root" \
PATH="$stub_bin:$ROOT/bin:/usr/bin:/bin" \
"$mapped_helper" "$@"
}
wait_dead() {
local pid="$1"
for _ in {1..100}; do
kill -0 "$pid" 2>/dev/null || return 0
[[ $(awk '{ print $3 }' "/proc/$pid/stat" 2>/dev/null || true) == "Z" ]] && return 0
sleep 0.02
done
return 1
}
prepare_state_dir() {
rm -rf "$state_dir"
mkdir -m 700 "$state_dir"
}
write_inhibit_state() {
local record="$1"
printf '%s\n' "$record" >"$state_dir/inhibit-pid"
chmod 600 "$state_dir/inhibit-pid"
}
start_identity_process() {
local token="$1"
/usr/bin/bash -c 'trap "exit 0" TERM; while :; do sleep 0.05; done' \
omarchy-test "--why=Omarchy update in progress [$token]" &
identity_pid=$!
test_processes+=("$identity_pid")
identity_start=$(awk '{ print $22 }' "/proc/$identity_pid/stat")
identity_owner=$(stat -Lc '%u' "/proc/$identity_pid")
}
run_helper start
[[ -s $state_dir/inhibit-pid ]] || fail "valid XDG runtime publishes inhibitor state"
read -r version valid_pid valid_start valid_owner valid_token <"$state_dir/inhibit-pid"
[[ $version == "1" && $valid_token =~ ^[0-9a-f]{32}$ ]] || fail "inhibitor state is an exact versioned identity"
[[ $(stat -Lc '%u %a %h' "$state_dir/inhibit-pid") == "$(id -u) 600 1" ]] ||
fail "inhibitor state is private, caller-owned, and singly linked"
run_helper stop
wait_dead "$valid_pid" || fail "valid inhibitor identity is stopped"
[[ ! -e $state_dir ]] || fail "valid state is cleaned after stop"
pass "valid XDG runtime uses private atomic inhibitor state"
permissive_runtime="$test_tmp/permissive-runtime"
mkdir -m 755 "$permissive_runtime"
if HOME="$test_home" XDG_RUNTIME_DIR="$permissive_runtime" PATH="$stub_bin:$ROOT/bin:/usr/bin:/bin" \
OMARCHY_TEST_RUN_ID="$test_run_id" "$mapped_helper" stop 2>/dev/null; then
fail "permissive XDG runtime is rejected"
fi
symlink_runtime="$test_tmp/runtime-link"
ln -s "$runtime_dir" "$symlink_runtime"
if HOME="$test_home" XDG_RUNTIME_DIR="$symlink_runtime" PATH="$stub_bin:$ROOT/bin:/usr/bin:/bin" \
OMARCHY_TEST_RUN_ID="$test_run_id" "$mapped_helper" stop 2>/dev/null; then
fail "symlink XDG runtime is rejected"
fi
if HOME="$test_home" XDG_RUNTIME_DIR="$test_tmp/../${test_tmp##*/}/runtime" PATH="$stub_bin:$ROOT/bin:/usr/bin:/bin" \
OMARCHY_TEST_RUN_ID="$test_run_id" "$mapped_helper" stop 2>/dev/null; then
fail "non-canonical XDG runtime is rejected"
fi
pass "unsafe XDG runtime directories are rejected"
mkdir -m 700 "$test_tmp/state-target"
ln -s "$test_tmp/state-target" "$state_dir"
if run_helper stop 2>/dev/null; then
fail "symlink inhibitor state directory is rejected"
fi
rm -f "$state_dir"
mkdir -m 777 "$state_dir"
chmod 777 "$state_dir"
if run_helper stop 2>/dev/null; then
fail "writable-by-others inhibitor state directory is rejected"
fi
rm -rf "$state_dir"
pass "unsafe inhibitor state directories are rejected"
# Package replacement leaves the preceding helper's PID/start pair and its
# umask-derived modes for the newly installed stop command to consume.
idle_marker="$test_home/.local/state/omarchy/indicators/stay-awake"
for modes in '755 644' '750 640' '700 600'; do
read -r directory_mode file_mode <<<"$modes"
for idle_choice in update user; do
prepare_state_dir
chmod "$directory_mode" "$state_dir"
sleep infinity &
legacy_pid=$!
test_processes+=("$legacy_pid")
legacy_start=$(awk '{ print $22 }' "/proc/$legacy_pid/stat")
printf '%s %s\n' "$legacy_pid" "$legacy_start" >"$state_dir/inhibit-pid"
printf '123:456:789\n' >"$state_dir/idle-owner"
chmod "$file_mode" "$state_dir/"{inhibit-pid,idle-owner}
mkdir -p "${idle_marker%/*}"
if [[ $idle_choice == "update" ]]; then
cp "$state_dir/idle-owner" "$idle_marker"
else
printf 'user-choice\n' >"$idle_marker"
fi
run_helper stop || fail "legacy $modes cleanup fails after helper replacement"
wait_dead "$legacy_pid" || fail "legacy $modes inhibitor survives cleanup"
[[ ! -e $state_dir ]] || fail "legacy $modes state survives cleanup"
if [[ $idle_choice == "update" ]]; then
[[ ! -e $idle_marker ]] || fail "legacy cleanup leaves update-owned Stay Awake enabled"
else
[[ $(<"$idle_marker") == "user-choice" ]] || fail "legacy cleanup changes the user's Stay Awake choice"
rm -f "$idle_marker"
fi
done
done
for legacy_record in stale multiline nul; do
prepare_state_dir
chmod 755 "$state_dir"
sleep infinity &
legacy_pid=$!
test_processes+=("$legacy_pid")
legacy_start=$(awk '{ print $22 }' "/proc/$legacy_pid/stat")
case "$legacy_record" in
stale) printf '%s %s\n' "$legacy_pid" "$((legacy_start + 1))" ;;
multiline) printf '%s %s\nextra\n' "$legacy_pid" "$legacy_start" ;;
nul) printf '%s\0 %s\n' "$legacy_pid" "$legacy_start" ;;
esac >"$state_dir/inhibit-pid"
chmod 644 "$state_dir/inhibit-pid"
if [[ $legacy_record == "stale" ]]; then
run_helper stop
elif run_helper stop 2>/dev/null; then
fail "malformed legacy $legacy_record record was accepted"
fi
kill -0 "$legacy_pid" || fail "legacy $legacy_record record signaled an unrelated process"
kill "$legacy_pid"
wait "$legacy_pid" 2>/dev/null || true
done
pass "legacy upgrade cleanup preserves process identity and the user's idle choice"
prepare_state_dir
printf 'not a record\n' >"$state_dir/inhibit-pid"
chmod 600 "$state_dir/inhibit-pid"
if run_helper stop 2>/dev/null; then
fail "malformed inhibitor state is rejected"
fi
token=11111111111111111111111111111111
start_identity_process "$token"
prepare_state_dir
printf '1 %s %s %s %s\nextra\n' "$identity_pid" "$identity_start" "$identity_owner" "$token" >"$state_dir/inhibit-pid"
chmod 600 "$state_dir/inhibit-pid"
if run_helper stop 2>/dev/null; then
fail "multiline inhibitor state is rejected"
fi
kill -0 "$identity_pid" 2>/dev/null || fail "multiline state cannot signal its target"
prepare_state_dir
write_inhibit_state "1 $identity_pid $((identity_start + 1)) $identity_owner $token"
run_helper stop
kill -0 "$identity_pid" 2>/dev/null || fail "reused PID state cannot signal its target"
prepare_state_dir
write_inhibit_state "1 $identity_pid $identity_start $identity_owner 22222222222222222222222222222222"
run_helper stop
kill -0 "$identity_pid" 2>/dev/null || fail "wrong process identity cannot signal its target"
kill "$identity_pid"
wait_dead "$identity_pid" || true
pass "malformed, multiline, reused-PID, and wrong-identity records are harmless"
retry_flag="$test_tmp/allow-termination"
token=44444444444444444444444444444444
/usr/bin/bash -c '
trap "" TERM
while [[ ! -e $1 ]]; do sleep 0.05; done
trap "exit 0" TERM
while :; do sleep 0.05; done
' omarchy-retry "$retry_flag" "--why=Omarchy update in progress [$token]" &
retry_pid=$!
test_processes+=("$retry_pid")
retry_start=$(awk '{ print $22 }' "/proc/$retry_pid/stat")
retry_owner=$(stat -Lc '%u' "/proc/$retry_pid")
prepare_state_dir
write_inhibit_state "1 $retry_pid $retry_start $retry_owner $token"
if run_helper stop 2>/dev/null; then
fail "failed termination reports success"
fi
[[ -s $state_dir/inhibit-pid ]] || fail "failed termination retains authenticated retry state"
touch "$retry_flag"
sleep 0.1
run_helper stop
wait_dead "$retry_pid" || fail "retained inhibitor state permits a successful retry"
pass "failed termination retains its authenticated retry handle"
for unsafe_kind in symlink permissive hardlink legacy-symlink legacy-permissive legacy-hardlink; do
token=33333333333333333333333333333333
start_identity_process "$token"
prepare_state_dir
record="1 $identity_pid $identity_start $identity_owner $token"
[[ $unsafe_kind != legacy-* ]] || record="$identity_pid $identity_start"
case "${unsafe_kind#legacy-}" in
symlink)
printf '%s\n' "$record" >"$test_tmp/state-victim"
chmod 600 "$test_tmp/state-victim"
ln -s "$test_tmp/state-victim" "$state_dir/inhibit-pid"
;;
permissive)
write_inhibit_state "$record"
if [[ $unsafe_kind == "legacy-permissive" ]]; then
chmod 666 "$state_dir/inhibit-pid"
else
chmod 644 "$state_dir/inhibit-pid"
fi
;;
hardlink)
write_inhibit_state "$record"
ln "$state_dir/inhibit-pid" "$state_hardlink"
;;
esac
if run_helper stop 2>/dev/null; then
fail "$unsafe_kind inhibitor state is rejected"
fi
kill -0 "$identity_pid" 2>/dev/null || fail "$unsafe_kind state cannot signal its target"
kill "$identity_pid"
wait_dead "$identity_pid" || true
rm -f "$test_tmp/state-victim" "$state_hardlink"
done
pass "symlink, permissive, and multiply-linked records are harmless"
: >"$inhibitor_log"
run_helper start
first_pid=$(tail -n 1 "$inhibitor_log")
run_helper start
second_pid=$(tail -n 1 "$inhibitor_log")
[[ $first_pid != "$second_pid" ]] || fail "repeated start replaces the inhibitor"
wait_dead "$first_pid" || fail "repeated start stops the prior inhibitor"
run_helper stop
run_helper stop
wait_dead "$second_pid" || fail "repeated stop remains idempotent"
pass "repeated start and stop preserve one inhibitor"
: >"$inhibitor_log"
concurrent_jobs=()
for _ in {1..4}; do
(run_helper start; run_helper stop) &
concurrent_jobs+=("$!")
done
for job in "${concurrent_jobs[@]}"; do
wait "$job" || fail "concurrent start and stop are serialized"
done
run_helper stop
while read -r pid; do
[[ -n $pid ]] || continue
wait_dead "$pid" || fail "concurrent operation leaves no inhibitor behind"
done <"$inhibitor_log"
pass "concurrent state operations are serialized"
if SYSTEMD_FAIL=1 run_helper start; then
fail "failed systemd-inhibit launch reports success"
fi
[[ ! -e $state_dir/inhibit-pid ]] || fail "failed inhibitor launch publishes no PID state"
run_helper stop
pass "failed inhibitor launch leaves no stale process state"
: >"$inhibitor_log"
rollback_victim="$test_tmp/rollback-victim"
: >"$rollback_victim"
if CREATE_BAD_IDLE="$rollback_victim" run_helper start 2>/dev/null; then
fail "unsafe idle publication reports success"
fi
rollback_pid=$(tail -n 1 "$inhibitor_log")
wait_dead "$rollback_pid" || fail "post-publication failure rolls the inhibitor back"
[[ ! -e $state_dir ]] || fail "rollback removes published inhibitor and idle ownership state"
pass "state publication failures roll back a launched inhibitor"
# Hold each cancellation window open, including publication before child exec,
# readiness before idle setup, and publication of the update-owned idle marker.
cp "$mapped_helper" "$test_tmp/helper-before-pause"
idle_marker="$test_home/.local/state/omarchy/indicators/stay-awake"
for cancel_phase in published ready idle-temporary idle user-idle; do
rm -f "$test_tmp/cancel-ready" "$test_tmp/release-child"
if [[ $cancel_phase == "user-idle" ]]; then
mkdir -p "${idle_marker%/*}"
printf 'user-choice\n' >"$idle_marker"
fi
python3 - "$mapped_helper" "$cancel_phase" <<'PY'
from pathlib import Path
import sys
p = Path(sys.argv[1])
s = p.read_text()
pause = ': >"$TEST_CANCEL_READY"; while :; do /usr/bin/sleep 0.02; done'
if sys.argv[2] == 'published':
anchor = ' while :; do\n inhibit_record='
edits = [(anchor, ' ' + pause + '\n' + anchor),
(' exec -a "$expected"',
' while [[ ! -e $TEST_RELEASE_CHILD ]]; do /usr/bin/sleep 0.02; done\n exec -a "$expected"')]
elif sys.argv[2] == 'idle-temporary':
anchor = ' temporary=$(mktemp "$state_dir/.${state_file##*/}.XXXXXXXX") || return 1'
edits = [(anchor, anchor + '\n if [[ $state_file == "$idle_owner_file" ]]; then ' + pause + '; fi')]
elif sys.argv[2] == 'idle':
anchor = '''printf '%s\\n' "$idle_owner" >"$stay_awake_state"'''
edits = [(anchor, '{ ' + anchor + ' && { ' + pause + '; }; }')]
else:
anchor = ' if [[ ! -f $stay_awake_state ]]; then'
edits = [(anchor, ' ' + pause + '\n' + anchor)]
for old, new in edits:
assert s.count(old) == 1, old
s = s.replace(old, new)
p.write_text(s)
PY
(
export HOME="$test_home" XDG_RUNTIME_DIR="$runtime_dir" OMARCHY_PATH="$mapped_root"
export INHIBITOR_LOG="$inhibitor_log" OMARCHY_TEST_RUN_ID="$test_run_id"
export TEST_CANCEL_READY="$test_tmp/cancel-ready" TEST_RELEASE_CHILD="$test_tmp/release-child"
export PATH="$stub_bin:$ROOT/bin:/usr/bin:/bin"
exec "$mapped_helper" start
) >"$test_tmp/$cancel_phase-cancel.log" 2>&1 &
cancelled_launcher=$!
test_processes+=("$cancelled_launcher")
for _ in {1..100}; do
[[ -e $test_tmp/cancel-ready && -s $state_dir/inhibit-pid ]] && break
sleep 0.02
done
[[ -e $test_tmp/cancel-ready && -s $state_dir/inhibit-pid ]] || fail "$cancel_phase cancellation reached its barrier"
read -r _ published_pid _ <"$state_dir/inhibit-pid"
test_processes+=("$published_pid")
kill -TERM "$cancelled_launcher"
cancelled_status=0
wait "$cancelled_launcher" || cancelled_status=$?
(( cancelled_status == 143 )) || fail "$cancel_phase launch preserves cancellation status"
touch "$test_tmp/release-child"
wait_dead "$published_pid" || fail "$cancel_phase cancellation leaked the held child"
[[ ! -e $state_dir ]] || fail "$cancel_phase cancellation left launch state"
if [[ $cancel_phase == "user-idle" ]]; then
[[ $(<"$idle_marker") == "user-choice" ]] || fail "cancellation changed the user's Stay Awake choice"
rm -f "$idle_marker"
else
[[ ! -e $idle_marker ]] || fail "$cancel_phase cancellation left Stay Awake enabled"
fi
cp "$test_tmp/helper-before-pause" "$mapped_helper"
done
pass "cancellation through idle setup stops the child and restores only update-owned idle state"
namespace_args=()
namespace_probe_error="$test_tmp/namespace-probe.err"
if (( EUID == 0 )); then
namespace_args=(
unshare --user --mount --fork
--map-users=0:0:1 --map-users=1000:1000:2
--map-groups=0:0:1 --map-groups=1000:1000:2
--setuid=0 --setgid=0
)
else
subordinate_uid=$(awk -F: -v user="$(id -un)" '$1 == user { print $2; exit }' /etc/subuid 2>/dev/null || true)
subordinate_gid=$(awk -F: -v user="$(id -un)" '$1 == user { print $2; exit }' /etc/subgid 2>/dev/null || true)
if [[ $subordinate_uid =~ ^[0-9]+$ && $subordinate_gid =~ ^[0-9]+$ ]]; then
namespace_args=(
unshare --user --mount --fork
"--map-users=0:$(id -u):1" "--map-users=1000:$subordinate_uid:2"
"--map-groups=0:$(id -g):1" "--map-groups=1000:$subordinate_gid:2"
--setuid=0 --setgid=0
)
fi
fi
namespace_capable=0
if (( ${#namespace_args[@]} > 0 )) &&
"${namespace_args[@]}" /usr/bin/bash -c '
set -e
mount -t tmpfs -o mode=1777 tmpfs /tmp
setpriv --reuid=1000 --regid=1000 --clear-groups true
setpriv --reuid=1001 --regid=1001 --clear-groups true
' 2>"$namespace_probe_error"; then
namespace_capable=1
fi
if (( namespace_capable == 0 )); then
skip "cross-UID fallback probe: two-UID mount namespace unavailable"
else
# Keep the protected entrypoint, shared helper and harmless command stubs
# together, even when tmpfs hides a checkout or fixture under /tmp.
tar -C "$test_tmp" -cf "$test_tmp/namespace-fixture.tar" bin omarchy
if ! "${namespace_args[@]}" /usr/bin/bash -s -- "$test_tmp" "$test_run_id" \
9<"$test_tmp/namespace-fixture.tar" <<'SH'
set -euo pipefail
fixture=$1
run_id=$2
mount -t tmpfs -o mode=1777 tmpfs /tmp
mkdir -m 755 "$fixture"
tar -C "$fixture" -xf /proc/self/fd/9
exec 9<&-
mkdir -m 700 /tmp/victim-home
chown 1000:1000 /tmp/victim-home
setpriv --reuid=1000 --regid=1000 --clear-groups sleep 30 &
victim_pid=$!
trap 'kill "$victim_pid" "${legacy_sudo_pid:-$victim_pid}" 2>/dev/null || true; wait "$victim_pid" 2>/dev/null || true' EXIT
victim_start=$(awk '{ print $22 }' "/proc/$victim_pid/stat")
state=/tmp/omarchy-1000/omarchy-update-stay-awake-$run_id
run_fallback() {
local uid=$1 home=$2
shift 2
setpriv --reuid="$uid" --regid="$uid" --clear-groups /usr/bin/env -i \
HOME="$home" OMARCHY_PATH="$fixture/omarchy" OMARCHY_TEST_RUN_ID="$run_id" \
INHIBITOR_LOG="$home/inhibitors" PATH="$fixture/bin:/usr/bin:/bin" \
"$fixture/omarchy/bin/omarchy-update-stay-awake" "$@"
}
setpriv --reuid=1001 --regid=1001 --clear-groups /usr/bin/bash -c '
mkdir -m 755 /tmp/omarchy-1000 "$3"
printf "%s %s\n" "$1" "$2" >"$3/inhibit-pid"
chmod 644 "$3/inhibit-pid"
' attacker "$victim_pid" "$victim_start" "$state"
if run_fallback 1000 /tmp/victim-home stop 2>/tmp/refusal; then
echo "foreign fallback state was accepted" >&2
exit 1
fi
grep -q 'unsafe Omarchy update inhibitor state path' /tmp/refusal
kill -0 "$victim_pid"
rm -rf /tmp/omarchy-1000
# The old helper also left a readable fallback parent after a successful stop.
setpriv --reuid=1000 --regid=1000 --clear-groups mkdir -m 755 /tmp/omarchy-1000
run_fallback 1000 /tmp/victim-home start
[[ $(stat -Lc '%u %a' /tmp/omarchy-1000) == "1000 700" ]]
run_fallback 1000 /tmp/victim-home stop
[[ ! -e $state ]]
mkdir -m 700 "$state"
chown 1000:1000 "$state"
printf '1 %s %s 1000 %032d\n' "$victim_pid" "$victim_start" 0 \
>"$state/inhibit-pid"
chown 1001:1001 "$state/inhibit-pid"
chmod 600 "$state/inhibit-pid"
if run_fallback 1000 /tmp/victim-home stop 2>/tmp/refusal; then
echo "foreign state file was accepted" >&2
exit 1
fi
grep -q 'unsafe Omarchy update sleep inhibitor state' /tmp/refusal
kill -0 "$victim_pid"
# A legacy-looking record owned by another account is still untrusted.
mkdir -m 700 "$state"
chown 1000:1000 "$state"
printf '%s %s\n' "$victim_pid" "$victim_start" >"$state/inhibit-pid"
chown 1001:1001 "$state/inhibit-pid"
chmod 644 "$state/inhibit-pid"
if run_fallback 1000 /tmp/victim-home stop 2>/tmp/refusal; then
echo "foreign legacy state file was accepted" >&2
exit 1
fi
kill -0 "$victim_pid"
# The old terminal helper recorded sudo, with the caller's real UID but an
# effective root UID. Its /proc owner is root; the caller can still signal it.
setpriv --ruid=1000 --euid=0 --regid=1000 --clear-groups sleep 30 &
legacy_sudo_pid=$!
for _ in {1..50}; do
[[ $(awk '/^Uid:/ { print $2, $3 }' "/proc/$legacy_sudo_pid/status") == "1000 0" ]] && break
sleep .02
done
[[ $(awk '/^Uid:/ { print $2, $3 }' "/proc/$legacy_sudo_pid/status") == "1000 0" ]]
mkdir -m 755 "$state"
chown 1000:1000 "$state"
printf '%s %s\n' "$legacy_sudo_pid" "$(awk '{ print $22 }' "/proc/$legacy_sudo_pid/stat")" >"$state/inhibit-pid"
chown 1000:1000 "$state/inhibit-pid"
chmod 644 "$state/inhibit-pid"
run_fallback 1000 /tmp/victim-home stop
[[ ! -e /proc/$legacy_sudo_pid || $(awk '{ print $3 }' "/proc/$legacy_sudo_pid/stat") == "Z" ]]
wait "$legacy_sudo_pid" 2>/dev/null || true
# Root may read the record, but must not signal a live legacy target whose
# real UID differs. Preserve the retry handle instead of treating it as dead.
mkdir -m 700 /tmp/root-home
root_state=/tmp/omarchy-0/omarchy-update-stay-awake-$run_id
mkdir -p "$root_state"
printf '%s %s\n' "$victim_pid" "$victim_start" >"$root_state/inhibit-pid"
chmod 644 "$root_state/inhibit-pid"
if run_fallback 0 /tmp/root-home stop 2>/tmp/refusal; then
echo "legacy cleanup accepted another account's live process" >&2
exit 1
fi
grep -q 'retained its state for recovery' /tmp/refusal
[[ -s $root_state/inhibit-pid ]]
kill -0 "$victim_pid"
rm "$root_state/inhibit-pid"
run_fallback 0 /tmp/root-home start
[[ $(stat -Lc '%u %a' /tmp/omarchy-0) == "0 700" ]]
run_fallback 0 /tmp/root-home stop
[[ ! -e /tmp/omarchy-0/omarchy-update-stay-awake-$run_id ]]
SH
then
fail "two-UID fallback probe failed after its capability check" "$(<"$namespace_probe_error")"
fi
pass "foreign UID fallback state cannot kill a victim and safe fallback works"
fi