Three review findings on the update-hook boundary: The pre-refresh-pacman hook had been moved after the refresh transaction and, during a channel switch, deferred to the very end. That defeated the hook's purpose: custom repositories and IgnorePkg entries were not in place when the downgrade-capable -Syyuu ran. Run the hook where it used to run, after the package config is re-synced and before the transaction, but cold: revoke the timestamp, run it behind the no-update wrapper with the caller's original PATH, and revoke again before continuing. Every later privileged command authenticates with --no-update, so a detached child left by the hook has no reusable timestamp to wait for. Channel switching hands the caller's PATH to the refresh the same way the updater receives it, and no longer defers or re-runs the hook. Stay Awake was released before AUR builds, hooks and mise, so the machine could sleep during the longest part of an update. Releasing the inhibitor needs no privilege because the held command already dropped to the user, so stop it after mise and before the reboot prompt, as before. A packaged channel destination cannot be inspected before its package is installed, and a transaction can replace the running tree with a release that predates the command-scoped wrapper; from then on a bare sudo would resolve to /usr/bin/sudo and publish a timestamp, and the destination's own updater authenticates the same way. The switch used to abort only after the packages had changed, with generic rerun advice. Now it checks for the wrapper after each transaction before any further privileged step, completes what it safely can, and stops cold with instructions to run that release's update from a fresh session instead of launching it. Boundary tests pin the hook between the config copies and the transaction with a cold timestamp on both sides, the older-destination stop with its guidance and no launched updater, the new inhibitor position, and the post-update hook staying unreached on failures and signals. Docs, the manual and the sample hook describe the restored timing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
123 lines
5.5 KiB
Bash
Executable File
123 lines
5.5 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
|
copy_boundary_file bin/omarchy-update
|
|
copy_boundary_file bin/omarchy-refresh-pacman
|
|
# Replace the step symlink, preserving the real fixture dispatcher.
|
|
rm "$SUDO_TEST_ROOT/bin/omarchy-update-aur-pkgs"
|
|
copy_boundary_file bin/omarchy-update-aur-pkgs
|
|
export OMARCHY_UPDATE_LOGGED=1
|
|
|
|
run_update() {
|
|
"$SUDO_TEST_ROOT/bin/omarchy-update" "$@" >"$boundary_tmp/output" 2>&1
|
|
}
|
|
|
|
for args in '-y' ''; do
|
|
reset_boundary
|
|
touch "$SUDO_TEST_CACHE"
|
|
run_update $args || fail "update failed" "$(<"$boundary_tmp/output")"
|
|
assert_boundary_cold "successful update"
|
|
grep -q '^sudo -N /usr/bin/true$' "$SUDO_TEST_LOG" || fail "update package helpers must use no-update sudo"
|
|
python3 - "$SUDO_TEST_LOG" <<'PY'
|
|
import sys
|
|
s=open(sys.argv[1]).read().splitlines()
|
|
positions=[next(i for i,line in enumerate(s) if line.startswith(prefix)) for prefix in ['step:omarchy-update-restart --services-only','step:yay','step:omarchy-hook post-update','step:omarchy-update-mise','step:omarchy-update-stay-awake stop','step:omarchy-update-restart --reboot-only']]
|
|
assert positions==sorted(positions), s
|
|
assert not any(line.startswith('sudo -N ') for line in s[positions[2]:]), s
|
|
PY
|
|
pass "update $args runs privileged phases before hooks and exits cold"
|
|
done
|
|
|
|
for step in omarchy-update-system-pkgs yay omarchy-hook omarchy-update-mise; do
|
|
reset_boundary
|
|
export SUDO_TEST_FAIL_STEP=$step
|
|
if run_update -y; then fail "$step failure must fail the update"; fi
|
|
assert_boundary_cold "failed $step"
|
|
python3 - "$SUDO_TEST_LOG" <<'PY'
|
|
import sys
|
|
s=open(sys.argv[1]).read().splitlines()
|
|
for i,line in enumerate(s):
|
|
if line=='step:omarchy-update-stay-awake stop': assert i>0 and s[i-1]=='sudo -k',s
|
|
PY
|
|
pass "update revokes credentials after $step fails"
|
|
done
|
|
|
|
reset_boundary
|
|
export SUDO_TEST_SIGNAL_STEP=omarchy-hook
|
|
if run_update -y; then fail "interrupted update must fail"; fi
|
|
assert_boundary_cold "interrupted update"
|
|
pass "update revokes credentials on TERM"
|
|
|
|
reset_boundary
|
|
export SUDO_TEST_REVOKE_FAIL=1
|
|
if run_update -y; then fail "failed initial revocation must fail the update"; fi
|
|
if grep -q '^step:' "$SUDO_TEST_LOG"; then fail "failed revocation must precede update work"; fi
|
|
pass "a failed cold start prevents update work"
|
|
|
|
reset_boundary
|
|
export SUDO_TEST_UNSUPPORTED=1
|
|
if run_update -y; then fail "unsupported sudo must prevent mixed-trust work"; fi
|
|
assert_boundary_cold "unsupported sudo"
|
|
pass "unsupported sudo fails without running update steps"
|
|
|
|
reset_boundary
|
|
"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" stable >"$boundary_tmp/output" 2>&1 || fail "refresh failed" "$(<"$boundary_tmp/output")"
|
|
assert_boundary_cold "refresh"
|
|
python3 - "$SUDO_TEST_LOG" <<'PY'
|
|
import sys
|
|
s=open(sys.argv[1]).read().splitlines()
|
|
hook=next(i for i,l in enumerate(s) if l=='step:omarchy-hook pre-refresh-pacman')
|
|
copies=[i for i,l in enumerate(s) if l.startswith('sudo -N cp ')]
|
|
transaction=next(i for i,l in enumerate(s) if l.startswith('step:pacman '))
|
|
assert len(copies)==4 and max(copies) < hook < transaction, s
|
|
assert s[hook-1]=='sudo -k' and s[hook+1]=='sudo -k', s
|
|
assert all(l in ('sudo -h','sudo -k') or l.startswith('sudo -N ') for l in s if l.startswith('sudo ')), s
|
|
PY
|
|
pass "refresh runs the hook cold between the config re-sync and the transaction"
|
|
|
|
for step in pacman omarchy-hook; do
|
|
reset_boundary
|
|
export SUDO_TEST_FAIL_STEP=$step
|
|
if "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" stable >"$boundary_tmp/output" 2>&1; then fail "refresh must propagate $step failure"; fi
|
|
assert_boundary_cold "failed refresh $step"
|
|
pass "refresh revokes after $step failure"
|
|
done
|
|
|
|
# The wrapper must preserve sudo's own option parser, including validation and
|
|
# explicit --, while standalone timestamp maintenance cannot be combined with N.
|
|
for args in '-v' '-n /usr/bin/true' '--user test -- /usr/bin/true' '-- /usr/bin/true' '-k' '-K'; do
|
|
reset_boundary
|
|
"$SUDO_TEST_ROOT/default/omarchy/sudo-no-update/sudo" $args
|
|
case "$args" in
|
|
-k|-K) expected="sudo $args" ;;
|
|
*) expected="sudo -N $args" ;;
|
|
esac
|
|
[[ $(<"$SUDO_TEST_LOG") == "$expected" ]] || fail "wrapper changed options: $args" "$(<"$SUDO_TEST_LOG")"
|
|
[[ ! -e $SUDO_TEST_CACHE ]] || fail "wrapper refreshed credentials"
|
|
pass "sudo wrapper preserves $args"
|
|
done
|
|
|
|
for script in bin/omarchy-update bin/omarchy-refresh-pacman default/omarchy/sudo-no-update/sudo; do
|
|
reset_boundary
|
|
if /usr/bin/bash "$SUDO_TEST_ROOT/$script" -p >"$boundary_tmp/output" 2>&1; then fail "$script accepted an ordinary Bash launch"; fi
|
|
[[ ! -s $SUDO_TEST_LOG ]] || fail "$script reached sudo through an invalid interpreter"
|
|
pass "$script rejects a decoy privileged-mode argument"
|
|
done
|
|
|
|
reset_boundary
|
|
printf '%s\n' 'printf startup-ran >>"$SUDO_TEST_ROOT/startup-marker"' >"$boundary_tmp/startup"
|
|
BASH_ENV="$boundary_tmp/startup" ENV="$boundary_tmp/startup" run_update -y || fail "sanitized update failed" "$(<"$boundary_tmp/output")"
|
|
[[ ! -e $SUDO_TEST_ROOT/startup-marker ]] || fail "startup code leaked into an update helper"
|
|
pass "inherited startup files do not run in the updater or its child scripts"
|
|
|
|
reset_boundary
|
|
function printf() { /usr/bin/touch "$SUDO_TEST_ROOT/function-marker"; }
|
|
export -f printf
|
|
run_update -y || fail "update failed with inherited function" "$(<"$boundary_tmp/output")"
|
|
unset -f printf
|
|
[[ ! -e $SUDO_TEST_ROOT/function-marker ]] || fail "an inherited function reached an update helper"
|
|
pass "exported functions do not reach update helper interpreters"
|