#!/bin/bash set -euo pipefail source "$(dirname "$0")/base-test.sh" # The setuid helper below models the strongest sudo timestamp mode: one token # shared by every process for this uid. That makes a detached hook child a # faithful regression for both global timestamps and the easier tty-sharing # case. The helper is mounted over /usr/bin/sudo only in this private namespace # so the scripts must use the fixed trusted invalidation path. if [[ ${OMARCHY_UPDATE_HOOK_SECURITY_NS:-} != 1 ]]; then outer_uid=$(id -u) outer_gid=$(id -g) subuid=$(awk -F: -v user="$(id -un)" '$1 == user { print $2; exit }' /etc/subuid) subgid=$(awk -F: -v group="$(id -gn)" '$1 == group { print $2; exit }' /etc/subgid) if [[ -z $subuid || -z $subgid ]]; then pass "no subordinate uid/gid range; skipping update-hook namespace proof" exit 0 fi exec unshare --user --mount \ --map-users "0:$outer_uid:1" --map-users "1:$subuid:65536" \ --map-groups "0:$outer_gid:1" --map-groups "1:$subgid:65536" \ env OMARCHY_UPDATE_HOOK_SECURITY_NS=1 bash "$0" fi [[ $(id -u) == 0 ]] || fail "update-hook proof entered its root namespace" mount -t tmpfs -o mode=0755 tmpfs /run run_bound=1 test_tmp=$(mktemp -d -p /run omarchy-update-hook-security.XXXXXXXX) mount -t tmpfs -o mode=0755 tmpfs "$test_tmp" chmod 0755 "$test_tmp" sudo_path_bound=0 channel_paths_bound=0 channel_wrapper_tree_bound=0 aur_paths_bound=0 font_paths_bound=0 migration_pkg_paths_bound=0 etc_bound=0 persistent_pids=() cleanup() { local pid pid_file for pid_file in "$test_home"/*.pid; do if [[ -s $pid_file ]]; then persistent_pids+=("$(<"$pid_file")") fi done for pid in "${persistent_pids[@]}"; do kill "$pid" 2>/dev/null || true done sleep 0.05 for pid in "${persistent_pids[@]}"; do kill -KILL "$pid" 2>/dev/null || true done if (( sudo_path_bound )); then umount /usr/bin/sudo fi if (( channel_paths_bound )); then umount /usr/bin/pacman umount /usr/bin/omarchy-dev-unlink umount /usr/bin/omarchy-state umount /usr/bin/omarchy-refresh-pacman umount /usr/bin/omarchy-update fi if (( channel_wrapper_tree_bound )); then umount /usr/share/omarchy fi if (( aur_paths_bound )); then umount /usr/bin/omarchy-pkg-aur-accessible umount /usr/bin/yay fi if (( font_paths_bound )); then umount /usr/bin/omarchy-launch-floating-terminal-with-presentation umount /usr/bin/omarchy-pkg-add umount /usr/bin/omarchy-font-set fi if (( migration_pkg_paths_bound )); then umount /usr/bin/omarchy-pkg-missing umount /usr/bin/omarchy-pkg-add fi if (( etc_bound )); then umount /etc fi rm -rf "$test_tmp"/* umount "$test_tmp" rmdir "$test_tmp" if (( run_bound )); then umount /run fi } trap cleanup EXIT stub_bin="$test_tmp/bin" test_home="$test_tmp/home" root_dir="$test_tmp/root" token="$test_tmp/sudo-token" event_log="$test_tmp/events" hook_log="$test_home/hook-events" mkdir -p "$stub_bin" "$test_home/.config/omarchy/hooks" "$root_dir" mkdir -p "$test_tmp/default/omarchy/sudo-no-update" cp "$ROOT/default/omarchy/sudo-no-update/sudo" "$test_tmp/default/omarchy/sudo-no-update/sudo" chmod 0755 "$test_tmp/default/omarchy/sudo-no-update/sudo" mkdir -p "$test_tmp/default/pacman" cp "$ROOT/default/pacman"/* "$test_tmp/default/pacman/" chmod 0755 "$test_tmp/default" "$test_tmp/default/omarchy" \ "$test_tmp/default/omarchy/sudo-no-update" "$test_tmp/default/pacman" chmod 0644 "$test_tmp/default/pacman"/* touch "$event_log" "$hook_log" chown -R 1000:1000 "$test_home" chown 1000:1000 "$event_log" chmod 0700 "$test_home" chmod 0600 "$event_log" "$hook_log" chmod 0755 "$stub_bin" "$root_dir" cat >"$test_tmp/sudo.c" <<'C' #include #include #include #include #include #include #include static const char *required_env(const char *name) { const char *value = getenv(name); if (!value || !*value) exit(125); return value; } static void log_event(const char *event) { int fd = open(required_env("TEST_SUDO_EVENT_LOG"), O_WRONLY | O_APPEND); if (fd < 0) exit(125); if (dprintf(fd, "%s\n", event) < 0) exit(125); close(fd); } static int authenticate(void) { int fd = open(required_env("TEST_SUDO_TOKEN"), O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd < 0) return 125; close(fd); return 0; } static int token_valid(void) { struct stat st; return stat(required_env("TEST_SUDO_TOKEN"), &st) == 0 && st.st_uid == 0; } static int is_pacman_command(int argc, char **argv) { int index; for (index = 1; index < argc; index++) { const char *base = strrchr(argv[index], '/'); base = base ? base + 1 : argv[index]; if (strcmp(base, "pacman") == 0) return 1; } return 0; } int main(int argc, char **argv) { int no_update = 0; if (argc == 2 && strcmp(argv[1], "-h") == 0) { const char *disable = getenv("TEST_SUDO_NO_N"); if (disable && strcmp(disable, "1") == 0) { fputs("usage: sudo [-ABbEHknPS] command\n", stdout); } else { fputs("usage: sudo [-ABbEHkNnPS] command\n", stdout); } return 0; } if (argc > 1 && strcmp(argv[1], "-N") == 0) { no_update = 1; argc--; argv++; } if (argc > 1 && strcmp(argv[1], "--") == 0) { argc--; argv++; } if (argc == 2 && strcmp(argv[1], "--authenticate-for-test") == 0) { if (no_update) { log_event("authenticate-no-update"); return 0; } else { log_event("authenticate"); return authenticate(); } } if (argc == 2 && strcmp(argv[1], "-k") == 0) { log_event("invalidate"); if (unlink(required_env("TEST_SUDO_TOKEN")) < 0 && errno != ENOENT) return 125; return 0; } if (!token_valid() && !no_update) { if (is_pacman_command(argc, argv)) { log_event("authenticate-command"); if (authenticate() != 0) return 125; } else { log_event("deny"); fputs("sudo: a password is required\n", stderr); return 1; } } log_event(no_update ? "grant-no-update" : "grant"); if (argc < 2 || setuid(0) < 0) return 125; execvp(argv[1], &argv[1]); return 125; } C gcc -O2 -Wall -Wextra -o "$stub_bin/sudo" "$test_tmp/sudo.c" chown 0:0 "$stub_bin/sudo" chmod 4755 "$stub_bin/sudo" mount --bind "$stub_bin/sudo" /usr/bin/sudo sudo_path_bound=1 mount --bind "$ROOT/bin/omarchy-refresh-pacman" /usr/bin/omarchy-refresh-pacman mount --bind "$ROOT/bin/omarchy-update" /usr/bin/omarchy-update mount -t tmpfs -o mode=0755 tmpfs /usr/share/omarchy mkdir -p /usr/share/omarchy/default/omarchy/sudo-no-update cp "$ROOT/default/omarchy/sudo-no-update/sudo" /usr/share/omarchy/default/omarchy/sudo-no-update/sudo chmod 0755 /usr/share/omarchy/default /usr/share/omarchy/default/omarchy \ /usr/share/omarchy/default/omarchy/sudo-no-update chmod 0755 /usr/share/omarchy/default/omarchy/sudo-no-update/sudo channel_wrapper_tree_bound=1 # Authorize this private tree exactly as omarchy-dev-link authorizes a # development checkout. Hiding the host /etc keeps the proof self-contained. mount -t tmpfs -o mode=0755 tmpfs /etc etc_bound=1 mkdir -p /etc/pacman.d touch /etc/pacman.conf /etc/pacman.d/mirrorlist chmod 0644 /etc/pacman.conf /etc/pacman.d/mirrorlist printf 'root:x:0:0:root:/root:/bin/bash\n' >/etc/passwd printf 'root:x:0:\n' >/etc/group chmod 0644 /etc/passwd /etc/group write_authorized_source_root() { local source_root="$1" local quoted="$source_root" quoted=${quoted//\\/\\\\} quoted=${quoted//\"/\\\"} quoted=${quoted//\$/\\\$} quoted=${quoted//\`/\\\`} printf 'export OMARCHY_PATH="%s"\n' "$quoted" >/etc/omarchy.conf chown 0:0 /etc/omarchy.conf chmod 0644 /etc/omarchy.conf } write_authorized_source_root "$test_tmp" cat >"$test_home/launch-persistent-attack" <<'ATTACK' #!/bin/bash /usr/bin/setsid --fork /bin/bash -c ' printf "%s\n" "$$" >"$3" for (( attempt = 0; attempt < 200; attempt++ )); do if /usr/bin/sudo /usr/bin/install -o 0 -g 0 -m 0600 "$1" "$2" 2>/dev/null; then exit 0 fi /usr/bin/sleep 0.01 done ' omarchy-hook-child "$HOME/payload" "$1" "$2" ATTACK cat >"$test_home/payload" <<'PAYLOAD' RUN+="/tmp/update-hook-payload" PAYLOAD chown 1000:1000 "$test_home/launch-persistent-attack" "$test_home/payload" chmod 0700 "$test_home/launch-persistent-attack" chmod 0600 "$test_home/payload" cat >"$stub_bin/omarchy-update-lock" <<'STUB' #!/bin/bash [[ ${1:-} == held ]] STUB cat >"$stub_bin/omarchy-update-system-pkgs" <<'STUB' #!/bin/bash [[ ${TEST_SKIP_UPDATE_AUTH:-0} == 1 ]] || sudo --authenticate-for-test STUB cat >"$stub_bin/omarchy-update-orphan-pkgs" <<'STUB' #!/bin/bash [[ ${TEST_SKIP_LATE_AUTH:-0} == 1 ]] || sudo --authenticate-for-test STUB cat >"$stub_bin/omarchy-update-aur-pkgs" <<'STUB' #!/bin/bash [[ ${TEST_SKIP_LATE_AUTH:-0} == 1 ]] || sudo --authenticate-for-test STUB cat >"$stub_bin/omarchy-update-mise" <<'STUB' #!/bin/bash sudo /usr/bin/true 2>/dev/null && exit 97 "$HOME/launch-persistent-attack" "$TEST_MISE_VICTIM" "$TEST_MISE_PID" STUB cat >"$stub_bin/omarchy-update-restart" <<'STUB' #!/bin/bash printf 'restart:%s\n' "$1" >>"$TEST_HOOK_LOG" if [[ $1 == --services-only && ${TEST_SKIP_LATE_AUTH:-0} != 1 ]]; then sudo --authenticate-for-test fi STUB cat >"$stub_bin/omarchy-update-confirm" <<'STUB' #!/bin/bash exit 0 STUB cat >"$stub_bin/omarchy-migrate" <<'STUB' #!/bin/bash if [[ ${TEST_REAL_MIGRATE:-0} == 1 ]]; then exec "$TEST_ROOT/bin/omarchy-migrate" fi if [[ ${TEST_FAILING_STAGE:-} == signal ]]; then kill -TERM "$PPID" sleep 0.1 fi [[ ${TEST_FAILING_STAGE:-} != migration ]] STUB cat >"$stub_bin/omarchy-hook" <<'STUB' #!/bin/bash exec bash "$TEST_ROOT/bin/omarchy-hook" "$@" STUB cat >"$stub_bin/omarchy-dev-unlink" <<'STUB' #!/bin/bash exit 0 STUB cat >"$stub_bin/omarchy-state" <<'STUB' #!/bin/bash exit 0 STUB for command in \ omarchy-update-requires-free-space omarchy-update-pkg-prune omarchy-snapshot \ omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring \ omarchy-update-analyze-logs omarchy-update-status; do cat >"$stub_bin/$command" <<'STUB' #!/bin/bash exit 0 STUB done cat >"$stub_bin/cp" <<'STUB' #!/bin/bash printf 'cp:%s\n' "$*" >>"$TEST_SUDO_EVENT_LOG" exit 0 STUB cat >"$stub_bin/pacman" <<'STUB' #!/bin/bash [[ -z ${TEST_PACMAN_DELAY:-} ]] || /usr/bin/sleep "$TEST_PACMAN_DELAY" exit "${TEST_PACMAN_STATUS:-0}" STUB chmod 0755 "$stub_bin"/* chmod 4755 "$stub_bin/sudo" mount --bind "$stub_bin/omarchy-dev-unlink" /usr/bin/omarchy-dev-unlink mount --bind "$stub_bin/omarchy-state" /usr/bin/omarchy-state mount --bind "$stub_bin/pacman" /usr/bin/pacman channel_paths_bound=1 evil_update_bin="$test_home/evil-update-bin" evil_update_root="$test_home/evil-update-root" evil_update_marker="$test_home/evil-update-ran" mkdir -p "$evil_update_bin" "$evil_update_root/migrations" for command in script omarchy-migrate omarchy-update-system-pkgs; do cat >"$evil_update_bin/$command" <<'STUB' #!/bin/bash touch "$TEST_EVIL_UPDATE_MARKER" exit 97 STUB done cat >"$evil_update_bin/omarchy-dev-unlink" <<'STUB' #!/bin/bash touch "$TEST_EVIL_CHANNEL_HELPER_MARKER" sudo /usr/bin/install -o 0 -g 0 -m 0600 "$HOME/payload" "$TEST_EVIL_CHANNEL_VICTIM" STUB cat >"$evil_update_root/migrations/9999999999.sh" <<'STUB' #!/bin/bash sudo /usr/bin/install -o 0 -g 0 -m 0600 "$HOME/payload" "$TEST_EVIL_MIGRATION_VICTIM" touch "$TEST_EVIL_UPDATE_MARKER" STUB chown -R 1000:1000 "$evil_update_bin" "$evil_update_root" chmod 0755 "$evil_update_bin"/* "$evil_update_root/migrations/9999999999.sh" write_attack_hook() { local hook_name="$1" mkdir -p "$test_home/.config/omarchy/hooks/$hook_name.d" cat >"$test_home/.config/omarchy/hooks/$hook_name" <<'HOOK' #!/bin/bash printf 'file:%s\n' "$(id -u)" >>"$TEST_HOOK_LOG" sudo /usr/bin/install -o 0 -g 0 -m 0600 "$HOME/payload" "$TEST_ROOT_VICTIM" 2>/dev/null || true "$HOME/launch-persistent-attack" "$TEST_PERSISTENT_VICTIM" "$TEST_PERSISTENT_PID" HOOK cat >"$test_home/.config/omarchy/hooks/$hook_name.d/10-attack" <<'HOOK' #!/bin/bash printf 'directory:%s\n' "$(id -u)" >>"$TEST_HOOK_LOG" sudo /usr/bin/install -o 0 -g 0 -m 0600 "$HOME/payload" "$TEST_ROOT_DIR_VICTIM" 2>/dev/null || true HOOK chown -R 1000:1000 "$test_home/.config/omarchy/hooks/$hook_name" \ "$test_home/.config/omarchy/hooks/$hook_name.d" chmod 0700 "$test_home/.config/omarchy/hooks/$hook_name" \ "$test_home/.config/omarchy/hooks/$hook_name.d/10-attack" } reset_case() { local pid_file for pid_file in "$test_home"/*.pid; do if [[ -s $pid_file ]]; then persistent_pids+=("$(<"$pid_file")") kill "$(<"$pid_file")" 2>/dev/null || true fi done rm -f "$test_home"/*.pid "$token" "$root_dir"/* : >"$event_log" setpriv --reuid 1000 --regid 1000 --clear-groups /usr/bin/truncate -s 0 "$hook_log" } run_as_user() { setpriv --reuid 1000 --regid 1000 --clear-groups \ env HOME="$test_home" PATH="$stub_bin:/usr/bin:/bin" OMARCHY_PATH="$ROOT" \ TEST_ROOT="$ROOT" TEST_SUDO_TOKEN="$token" TEST_SUDO_EVENT_LOG="$event_log" \ TEST_HOOK_LOG="$hook_log" "$@" } authenticate_for_test() { TEST_SUDO_TOKEN="$token" TEST_SUDO_EVENT_LOG="$event_log" \ /usr/bin/sudo --authenticate-for-test } wait_for_persistent_attempts() { local pid_file="$1" local pid="" for (( attempt = 0; attempt < 100; attempt++ )); do [[ -s $pid_file ]] && break sleep 0.01 done [[ -s $pid_file ]] || fail "detached hook child did not start" pid=$(<"$pid_file") persistent_pids+=("$pid") sleep 0.2 } assert_hook_sandboxed() { local direct_victim="$1" local directory_victim="$2" local persistent_victim="$3" [[ ! -e $direct_victim && ! -e $directory_victim && ! -e $persistent_victim ]] || fail "hook code reused an Omarchy sudo credential" grep -qxF 'file:1000' "$hook_log" || fail "the regular hook did not run as the desktop user" grep -qxF 'directory:1000' "$hook_log" || fail "the hook-directory entry did not run as the desktop user" [[ ! -e $token ]] || fail "the workflow left its modeled sudo credential live" } write_attack_hook post-update update_victim="$root_dir/80-update-hook.rules" update_dir_victim="$root_dir/81-update-hook-dir.rules" update_persistent_victim="$root_dir/82-update-hook-child.rules" mise_victim="$root_dir/83-mise-child.rules" reset_case set +e run_as_user env PATH="$evil_update_bin:$stub_bin:/usr/bin:/bin" OMARCHY_PATH="$evil_update_root" \ TEST_EVIL_UPDATE_MARKER="$evil_update_marker" \ TEST_ROOT_VICTIM="$update_victim" TEST_ROOT_DIR_VICTIM="$update_dir_victim" \ TEST_PERSISTENT_VICTIM="$update_persistent_victim" TEST_PERSISTENT_PID="$test_home/update.pid" \ TEST_MISE_VICTIM="$mise_victim" TEST_MISE_PID="$test_home/mise.pid" \ OMARCHY_UPDATE_LOGGED=1 "$ROOT/bin/omarchy-update" -y \ >"$test_tmp/update.out" 2>"$test_tmp/update.err" status=$? set -e (( status == 0 )) || fail "isolated unattended update failed" "$(<"$test_tmp/update.err")" wait_for_persistent_attempts "$test_home/update.pid" wait_for_persistent_attempts "$test_home/mise.pid" assert_hook_sandboxed "$update_victim" "$update_dir_victim" "$update_persistent_victim" [[ ! -e $mise_victim ]] || fail "detached mise code observed a later update authorization" [[ ! -e $evil_update_marker ]] || fail "update trusted an inherited PATH or OMARCHY_PATH override" grep -qxF 'restart:--services-only' "$hook_log" || fail "privileged restart phase did not run before user code" grep -qxF 'restart:--reboot-only' "$hook_log" || fail "reboot-only phase did not run after user code" pass "update leaves no later sudo authentication for mise or persistent hook children" # OM-SEC-14 ends at the final cold hook boundary. Later sections exercise # separate migration, restart-marker, channel, and installer findings in their # own PRs. exit 0 # Exercise the real migration dispatcher separately from PATH spoofing. An old # update inherited the evil root here and ran its migration with the live # system-package credential; the fixed update exports the authorized root. evil_migration_victim="$root_dir/90-evil-migration.rules" reset_case set +e run_as_user env OMARCHY_PATH="$evil_update_root" TEST_REAL_MIGRATE=1 \ TEST_EVIL_UPDATE_MARKER="$evil_update_marker" TEST_EVIL_MIGRATION_VICTIM="$evil_migration_victim" \ TEST_ROOT_VICTIM="$update_victim" TEST_ROOT_DIR_VICTIM="$update_dir_victim" \ TEST_PERSISTENT_VICTIM="$update_persistent_victim" TEST_PERSISTENT_PID="$test_home/update.pid" \ TEST_MISE_VICTIM="$mise_victim" TEST_MISE_PID="$test_home/mise.pid" \ OMARCHY_MIGRATION_STATE="$test_home/migration-state" OMARCHY_UPDATE_LOGGED=1 \ "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-root-spoof.out" 2>"$test_tmp/update-root-spoof.err" status=$? set -e (( status == 0 )) || fail "authorized-root update with real migration dispatcher failed" [[ ! -e $evil_update_marker && ! -e $evil_migration_victim ]] || fail "update dispatched a migration from inherited OMARCHY_PATH" pass "update rejects inherited OMARCHY_PATH for real migration dispatch" # The login-notification workflow invokes omarchy-migrate directly, outside an # update that already normalized OMARCHY_PATH. It must independently reject an # inherited attacker tree rather than running the migration placed there. reset_case set +e run_as_user env OMARCHY_PATH="$evil_update_root" \ TEST_EVIL_UPDATE_MARKER="$evil_update_marker" TEST_EVIL_MIGRATION_VICTIM="$evil_migration_victim" \ OMARCHY_MIGRATION_STATE="$test_home/direct-migration-state" \ "$ROOT/bin/omarchy-migrate" >"$test_tmp/direct-migrate.out" 2>"$test_tmp/direct-migrate.err" status=$? set -e (( status == 0 )) || fail "direct authorized migration dispatch failed" [[ ! -e $evil_update_marker && ! -e $evil_migration_victim ]] || fail "direct migration dispatch trusted inherited OMARCHY_PATH" pass "direct migration dispatch derives its source from root-owned configuration" # Reproduce the real historical ordering that exposed the remaining gap: the # mise-wrapper migration can execute user tooling at 1784909971, while the # 1784914435 migration invokes sudo later. A following migration also invokes # the real absolute-path package helper, which cannot be protected by PATH # alone. Every later authorization must inherit the exported no-update policy. mkdir -p "$test_tmp/migrations" "$test_home/.local/bin" chmod 0755 "$test_tmp/migrations" cp "$ROOT/migrations/1784909971.sh" "$test_tmp/migrations/1784909971.sh" cp "$ROOT/migrations/1784914435.sh" "$test_tmp/migrations/1784914435.sh" cat >"$test_tmp/migrations/1784914436.sh" <<'STUB' #!/bin/bash /usr/bin/omarchy-pkg-add migration-security-fixture STUB chmod 0644 "$test_tmp/migrations/1784909971.sh" "$test_tmp/migrations/1784914435.sh" \ "$test_tmp/migrations/1784914436.sh" cat >"$test_home/.local/bin/legacy-mise-wrapper" <<'STUB' #!/bin/bash mise use -g "github:attacker/tool" exec "attacker-tool" "$@" STUB cat >"$stub_bin/omarchy-mise-install" <<'STUB' #!/bin/bash "$HOME/launch-persistent-attack" "$TEST_MIGRATION_VICTIM" "$TEST_MIGRATION_PID" STUB cat >"$stub_bin/nmcli" <<'STUB' #!/bin/bash printf '%s\n' "$(id -u)" >"$TEST_PRIV_MIGRATION_MARKER" STUB cat >"$stub_bin/omarchy-notification-dismiss" <<'STUB' #!/bin/bash exit 0 STUB chmod 0755 "$stub_bin/omarchy-mise-install" "$stub_bin/nmcli" \ "$stub_bin/omarchy-notification-dismiss" "$test_home/.local/bin/legacy-mise-wrapper" chown -R 1000:1000 "$test_home/.local" cat >"$stub_bin/omarchy-pkg-missing" <<'STUB' #!/bin/bash exit 0 STUB chmod 0755 "$stub_bin/omarchy-pkg-missing" mount --bind "$ROOT/bin/omarchy-pkg-add" /usr/bin/omarchy-pkg-add mount --bind "$stub_bin/omarchy-pkg-missing" /usr/bin/omarchy-pkg-missing migration_pkg_paths_bound=1 migration_victim="$root_dir/95-migration-child.rules" migration_marker="$root_dir/96-privileged-migration-ran" reset_case set +e run_as_user env OMARCHY_PATH="$evil_update_root" \ TEST_MIGRATION_VICTIM="$migration_victim" TEST_MIGRATION_PID="$test_home/migration.pid" \ TEST_PRIV_MIGRATION_MARKER="$migration_marker" TEST_PACMAN_DELAY=0.2 \ OMARCHY_MIGRATION_STATE="$test_home/mixed-migration-state" \ "$ROOT/bin/omarchy-migrate" >"$test_tmp/mixed-migrate.out" 2>"$test_tmp/mixed-migrate.err" status=$? set -e (( status == 0 )) || fail "real mixed-trust migration sequence failed" "$(<"$test_tmp/mixed-migrate.err")" wait_for_persistent_attempts "$test_home/migration.pid" [[ -f $migration_marker && $(<"$migration_marker") == 0 ]] || fail "later privileged migration did not run as root" [[ ! -e $migration_victim && ! -e $token ]] || fail "mise migration child reused a later migration authorization" (( $(grep -c '^grant-no-update$' "$event_log") >= 2 )) || fail "direct package-helper migration did not inherit the no-update policy" pass "real mise-before-sudo and absolute package-helper migrations publish no reusable timestamp" umount /usr/bin/omarchy-pkg-missing umount /usr/bin/omarchy-pkg-add migration_pkg_paths_bound=0 # Bash can import both BASH_ENV startup code and exported functions before an # ordinary script body. An exported sudo function used to bypass the PATH # wrapper in a later helper, publishing a global token to a BASH_ENV child that # had started before migration invalidation. Exercise both injection channels # through a normal-shebang child of the privileged migration shell. cat >"$test_tmp/migrations/9999999998.sh" <<'STUB' #!/bin/bash "$HOME/launch-persistent-attack" "$TEST_BASH_STARTUP_VICTIM" "$TEST_BASH_STARTUP_PID" omarchy-exported-function-auth STUB cat >"$stub_bin/omarchy-exported-function-auth" <<'STUB' #!/bin/bash sudo --authenticate-for-test STUB cat >"$test_home/bash-env-attack" <<'STUB' #!/bin/bash if [[ ! -e $TEST_BASH_ENV_MARKER ]]; then /usr/bin/touch "$TEST_BASH_ENV_MARKER" "$HOME/launch-persistent-attack" "$TEST_BASH_ENV_VICTIM" "$TEST_BASH_ENV_PID" fi STUB chmod 0755 "$test_tmp/migrations/9999999998.sh" "$stub_bin/omarchy-exported-function-auth" chown 1000:1000 "$test_home/bash-env-attack" chmod 0600 "$test_home/bash-env-attack" bash_startup_victim="$root_dir/99-exported-function-child.rules" bash_env_victim="$root_dir/100-bash-env-child.rules" bash_env_marker="$test_home/bash-env-ran" reset_case set +e run_as_user env \ 'BASH_FUNC_sudo%%=() { /usr/bin/sudo "$@"; }' \ TEST_MULTILINE_ENV=$'value\nBASH_FUNC_fake%%=not-an-environment-record' \ BASH_ENV="$test_home/bash-env-attack" \ TEST_BASH_ENV_MARKER="$bash_env_marker" \ TEST_BASH_ENV_VICTIM="$bash_env_victim" TEST_BASH_ENV_PID="$test_home/bash-env.pid" \ TEST_BASH_STARTUP_VICTIM="$bash_startup_victim" TEST_BASH_STARTUP_PID="$test_home/bash-startup.pid" \ OMARCHY_MIGRATION_STATE="$test_home/bash-startup-migration-state" \ "$ROOT/bin/omarchy-migrate" >"$test_tmp/bash-startup.out" 2>"$test_tmp/bash-startup.err" status=$? set -e (( status == 0 )) || fail "migration rejected a sanitized Bash startup environment" "$(<"$test_tmp/bash-startup.err")" wait_for_persistent_attempts "$test_home/bash-startup.pid" sleep 0.2 [[ ! -e $bash_env_marker && ! -e $bash_env_victim ]] || fail "BASH_ENV ran before or beneath the migration security boundary" [[ ! -e $bash_startup_victim && ! -e $token ]] || fail "an exported sudo function published a reusable migration credential" grep -q '^grant-no-update$' "$event_log" || fail "sanitized helper did not use sudo --no-update" grep -qxF 'exec /usr/bin/sudo -N -- "$@"' "$ROOT/default/omarchy/sudo-no-update/sudo" || fail "no-update sudo wrapper omitted the option terminator" pass "Bash startup injection cannot bypass the no-update sudo boundary" # Invoking a mixed-trust entrypoint with an ordinary explicit Bash bypasses its # shebang. A BASH_ENV can erase its own environment record and retain a DEBUG # trap, so environment-record cleanup alone is not a sufficient startup gate. # The exact interpreter argv/privileged-mode check must reject this process # before update authentication, leaving even its already-detached child cold. cat >"$test_home/self-erasing-bash-env" <<'STUB' #!/bin/bash unset BASH_ENV ENV trap ' if [[ ! -e $TEST_DEBUG_TRAP_MARKER ]]; then /usr/bin/touch "$TEST_DEBUG_TRAP_MARKER" "$HOME/launch-persistent-attack" "$TEST_DEBUG_TRAP_VICTIM" "$TEST_DEBUG_TRAP_PID" fi ' DEBUG STUB chown 1000:1000 "$test_home/self-erasing-bash-env" chmod 0600 "$test_home/self-erasing-bash-env" debug_trap_marker="$test_home/debug-trap-ran" debug_trap_victim="$root_dir/101-debug-trap-child.rules" reset_case set +e run_as_user env BASH_ENV="$test_home/self-erasing-bash-env" \ TEST_DEBUG_TRAP_MARKER="$debug_trap_marker" TEST_DEBUG_TRAP_VICTIM="$debug_trap_victim" \ TEST_DEBUG_TRAP_PID="$test_home/debug-trap.pid" OMARCHY_UPDATE_LOGGED=1 \ /usr/bin/bash "$ROOT/bin/omarchy-update" -y \ >"$test_tmp/unsafe-bash.out" 2>"$test_tmp/unsafe-bash.err" status=$? set -e (( status == 126 )) || fail "update did not reject an unsafe explicit Bash interpreter" [[ -e $debug_trap_marker ]] || fail "self-erasing BASH_ENV regression did not install its DEBUG trap" wait_for_persistent_attempts "$test_home/debug-trap.pid" [[ ! -e $debug_trap_victim && ! -e $token ]] || fail "unsafe Bash startup reached update authentication" ! grep -qE '^(authenticate|authenticate-command|grant|grant-no-update)$' "$event_log" || fail "unsafe Bash startup reached privileged update work" grep -q 'unsafe Bash startup' "$test_tmp/unsafe-bash.err" || fail "unsafe Bash startup rejection lacked a diagnostic" pass "self-erasing BASH_ENV and DEBUG traps cannot cross the interpreter gate" # Model a hostile yay configuration that selects absolute /usr/bin/sudo and a # refresh loop. The real AUR helper must override both on its command line, so # even a migration child already polling the global token sees no credential. cat >"$stub_bin/omarchy-pkg-aur-accessible" <<'STUB' #!/bin/bash exit 0 STUB cat >"$stub_bin/yay" <<'STUB' #!/bin/bash sudo_command=/usr/bin/sudo sudoflags="" sudoloop=true while (($#)); do case "$1" in --sudo) sudo_command="$2" shift 2 ;; --sudoloop=false) sudoloop=false shift ;; --sudoflags=-N) sudoflags=-N shift ;; *) shift ;; esac done printf 'sudo=%s sudoflags=%s sudoloop=%s\n' "$sudo_command" "$sudoflags" "$sudoloop" >"$TEST_YAY_LOG" "$sudo_command" $sudoflags --authenticate-for-test STUB chmod 0755 "$stub_bin/omarchy-pkg-aur-accessible" "$stub_bin/yay" mount --bind "$stub_bin/omarchy-pkg-aur-accessible" /usr/bin/omarchy-pkg-aur-accessible mount --bind "$stub_bin/yay" /usr/bin/yay aur_paths_bound=1 yay_victim="$root_dir/97-yay-override-child.rules" reset_case run_as_user env TEST_YAY_LOG="$test_home/yay.log" \ "$test_home/launch-persistent-attack" "$yay_victim" "$test_home/yay-child.pid" wait_for_persistent_attempts "$test_home/yay-child.pid" run_as_user env OMARCHY_PATH="$test_tmp" OMARCHY_SUDO_NO_UPDATE=1 TEST_YAY_LOG="$test_home/yay.log" \ "$ROOT/bin/omarchy-update-aur-pkgs" >"$test_tmp/yay.out" 2>"$test_tmp/yay.err" sleep 0.2 grep -qxF "sudo=/usr/bin/sudo sudoflags=-N sudoloop=false" "$test_home/yay.log" || fail "AUR update did not override hostile yay sudo settings" [[ ! -e $yay_victim && ! -e $token ]] || fail "hostile yay sudo configuration published a reusable timestamp" pass "AUR updates force no-update sudo and disable yay's credential loop" # A pre-existing credential and skipped package paths exercise the interactive # branch independently of authority acquired by update helpers. reset_case authenticate_for_test set +e run_as_user env TEST_SKIP_UPDATE_AUTH=1 TEST_SKIP_LATE_AUTH=1 \ TEST_ROOT_VICTIM="$update_victim" TEST_ROOT_DIR_VICTIM="$update_dir_victim" \ TEST_PERSISTENT_VICTIM="$update_persistent_victim" TEST_PERSISTENT_PID="$test_home/update.pid" \ TEST_MISE_VICTIM="$mise_victim" TEST_MISE_PID="$test_home/mise.pid" \ OMARCHY_UPDATE_LOGGED=1 "$ROOT/bin/omarchy-update" \ >"$test_tmp/update-interactive.out" 2>"$test_tmp/update-interactive.err" status=$? set -e (( status == 0 )) || fail "isolated interactive update failed" wait_for_persistent_attempts "$test_home/update.pid" assert_hook_sandboxed "$update_victim" "$update_dir_victim" "$update_persistent_victim" pass "interactive update invalidates a pre-existing credential before user code" for failing_stage in migration signal; do reset_case set +e run_as_user env TEST_FAILING_STAGE="$failing_stage" TEST_SKIP_LATE_AUTH=1 \ TEST_ROOT_VICTIM="$update_victim" TEST_ROOT_DIR_VICTIM="$update_dir_victim" \ TEST_PERSISTENT_VICTIM="$update_persistent_victim" TEST_PERSISTENT_PID="$test_home/update.pid" \ TEST_MISE_VICTIM="$mise_victim" TEST_MISE_PID="$test_home/mise.pid" \ OMARCHY_UPDATE_LOGGED=1 "$ROOT/bin/omarchy-update" -y \ >"$test_tmp/update-$failing_stage.out" 2>"$test_tmp/update-$failing_stage.err" status=$? set -e (( status != 0 )) || fail "update $failing_stage case unexpectedly succeeded" [[ ! -e $token ]] || fail "update $failing_stage exit left its credential live" [[ ! -s $hook_log ]] || fail "update $failing_stage case reached user-controlled stages" done pass "failed and signaled updates invalidate credentials before user code" reset_case set +e run_as_user env TEST_SUDO_NO_N=1 OMARCHY_UPDATE_LOGGED=1 \ "$ROOT/bin/omarchy-update" -y >"$test_tmp/no-update-unsupported.out" 2>"$test_tmp/no-update-unsupported.err" status=$? set -e (( status != 0 )) || fail "update accepted sudo without --no-update support" ! grep -qE '^(authenticate|grant)' "$event_log" || fail "unsupported sudo reached privileged update work" grep -q 'does not support --no-update' "$test_tmp/no-update-unsupported.err" || fail "unsupported sudo failure did not explain the missing security primitive" pass "update fails closed before privileged work when sudo lacks --no-update" # The config parser is the authority for all three scoped commands. Exercise a # different unsafe shape through each copy before restoring the valid config. chmod 0666 /etc/omarchy.conf if run_as_user env OMARCHY_UPDATE_LOGGED=1 "$ROOT/bin/omarchy-update" -y \ >"$test_tmp/untrusted-update.out" 2>"$test_tmp/untrusted-update.err"; then fail "update accepted a writable source-root authorization" fi /usr/bin/mv /etc/omarchy.conf /etc/omarchy.real /usr/bin/ln -s /etc/omarchy.real /etc/omarchy.conf if run_as_user "$ROOT/bin/omarchy-refresh-pacman" stable \ >"$test_tmp/untrusted-refresh.out" 2>"$test_tmp/untrusted-refresh.err"; then fail "pacman refresh accepted a symlinked source-root authorization" fi /usr/bin/rm /etc/omarchy.conf /usr/bin/mv /etc/omarchy.real /etc/omarchy.conf chown 1000:1000 /etc/omarchy.conf if run_as_user "$ROOT/bin/omarchy-update-restart" --services-only \ >"$test_tmp/untrusted-restart.out" 2>"$test_tmp/untrusted-restart.err"; then fail "update restart accepted a non-root source-root authorization" fi write_authorized_source_root "$test_tmp" pass "update commands reject writable, symlinked, and non-root source-root authorization" # A root-owned config may authorize a development checkout, but not a tree # another local account (or every account) can rewrite. Exercise both unsafe # directory-chain shapes before restoring the valid authorized fixture. writable_source_root="$test_tmp/writable-source-root" foreign_source_root="$test_tmp/foreign-source-root" mkdir -p "$writable_source_root" "$foreign_source_root" chown 1000:1000 "$writable_source_root" chmod 0777 "$writable_source_root" chown 1001:1001 "$foreign_source_root" chmod 0755 "$foreign_source_root" write_authorized_source_root "$writable_source_root" if run_as_user env OMARCHY_UPDATE_LOGGED=1 "$ROOT/bin/omarchy-update" -y \ >"$test_tmp/writable-source.out" 2>"$test_tmp/writable-source.err"; then fail "update accepted a group/world-writable authorized source tree" fi grep -q 'untrusted Omarchy source root' "$test_tmp/writable-source.err" || fail "writable source-root rejection happened after the trust parser" write_authorized_source_root "$foreign_source_root" if run_as_user "$ROOT/bin/omarchy-migrate" --pending \ >"$test_tmp/foreign-source.out" 2>"$test_tmp/foreign-source.err"; then fail "migration runner accepted a foreign-owned authorized source tree" fi grep -q 'untrusted Omarchy source root' "$test_tmp/foreign-source.err" || fail "foreign source-root rejection happened after the trust parser" write_authorized_source_root "$test_tmp" pass "authorized source roots reject foreign-owned and group/world-writable path components" # The restart marker is attacker-writable, but its value is now an allowlisted # selector into the configured Omarchy tree rather than a command resolved by # PATH. A dev-linked tree is honored only through root-owned /etc/omarchy.conf. reset_case restart_home="$test_tmp/restart-home" restart_log="$test_tmp/restart.log" mkdir -p "$restart_home/.local/state/omarchy" touch "$restart_log" chown -R 1000:1000 "$restart_home" chown 1000:1000 "$restart_log" touch "$restart_home/.local/state/omarchy/restart-btop-required" \ "$restart_home/.local/state/omarchy/restart-evil-required" chown 1000:1000 "$restart_home/.local/state/omarchy"/* cat >"$stub_bin/omarchy-restart-btop" <<'STUB' #!/bin/bash echo trusted-btop >>"$TEST_RESTART_LOG" STUB cat >"$stub_bin/omarchy-restart-shell" <<'STUB' #!/bin/bash echo trusted-shell >>"$TEST_RESTART_LOG" STUB evil_bin="$test_home/evil-bin" mkdir -p "$evil_bin" cat >"$evil_bin/omarchy-restart-btop" <<'STUB' #!/bin/bash echo path-btop >>"$TEST_RESTART_LOG" STUB cat >"$evil_bin/omarchy-restart-evil" <<'STUB' #!/bin/bash echo path-evil >>"$TEST_RESTART_LOG" sudo /usr/bin/true STUB chown -R 1000:1000 "$evil_bin" chmod 0755 "$stub_bin/omarchy-restart-btop" "$stub_bin/omarchy-restart-shell" "$evil_bin"/* run_as_user env HOME="$restart_home" PATH="$evil_bin:$stub_bin:/usr/bin:/bin" \ OMARCHY_PATH="$test_home/evil-root" TEST_RESTART_LOG="$restart_log" \ "$ROOT/bin/omarchy-update-restart" --services-only >"$test_tmp/restart.out" 2>"$test_tmp/restart.err" grep -qxF trusted-btop "$restart_log" || fail "allowed marker did not use the configured Omarchy command" grep -qxF trusted-shell "$restart_log" || fail "shell restart did not use the configured Omarchy command" ! grep -q '^path-' "$restart_log" || fail "restart marker resolved an attacker PATH command" [[ ! -e $restart_home/.local/state/omarchy/restart-evil-required ]] || fail "unsupported restart marker was retained" pass "restart markers use an allowlist and fixed configured command paths" special_root="$test_tmp/dev root\\checkout\$cash" special_home="$test_tmp/special-home" special_log="$test_tmp/special.log" mkdir -p "$special_root/bin" "$special_home/.local/state/omarchy" touch "$special_home/.local/state/omarchy/restart-btop-required" "$special_log" chown -R 1000:1000 "$special_home" "$special_log" cat >"$special_root/bin/omarchy-restart-btop" <<'STUB' #!/bin/bash echo special-btop >>"$TEST_RESTART_LOG" STUB cat >"$special_root/bin/omarchy-restart-shell" <<'STUB' #!/bin/bash exit 0 STUB chmod 0755 "$special_root/bin"/* chown -R 1000:1000 "$special_root" write_authorized_source_root "$special_root" run_as_user env HOME="$special_home" TEST_RESTART_LOG="$special_log" \ "$ROOT/bin/omarchy-update-restart" --services-only \ >"$test_tmp/special-root.out" 2>"$test_tmp/special-root.err" grep -qxF special-btop "$special_log" || fail "authorized quoted dev root did not dispatch its restart helper" write_authorized_source_root "$test_tmp" pass "source-root authorization decodes spaces, backslashes, and dollar signs" # The legacy pre-refresh hook now runs only after pacman; a detached child can # no longer wait for a later authentication in either tty or global mode. write_attack_hook pre-refresh-pacman refresh_victim="$root_dir/84-refresh-hook.rules" refresh_dir_victim="$root_dir/85-refresh-hook-dir.rules" refresh_persistent_victim="$root_dir/86-refresh-hook-child.rules" reset_case authenticate_for_test cat >"$evil_bin/cp" <<'STUB' #!/bin/bash touch "$TEST_EVIL_REFRESH_MARKER" exit 97 STUB chmod 0755 "$evil_bin/cp" evil_refresh_marker="$test_home/evil-refresh-ran" set +e run_as_user env PATH="$evil_bin:$stub_bin:/usr/bin:/bin" OMARCHY_PATH="$evil_update_root" \ TEST_EVIL_REFRESH_MARKER="$evil_refresh_marker" \ TEST_ROOT_VICTIM="$refresh_victim" TEST_ROOT_DIR_VICTIM="$refresh_dir_victim" \ TEST_PERSISTENT_VICTIM="$refresh_persistent_victim" TEST_PERSISTENT_PID="$test_home/refresh.pid" \ "$ROOT/bin/omarchy-refresh-pacman" stable >"$test_tmp/refresh.out" 2>"$test_tmp/refresh.err" status=$? set -e (( status == 0 )) || fail "isolated pacman refresh failed" "$(<"$test_tmp/refresh.err")" wait_for_persistent_attempts "$test_home/refresh.pid" assert_hook_sandboxed "$refresh_victim" "$refresh_dir_victim" "$refresh_persistent_victim" [[ ! -e $evil_refresh_marker ]] || fail "pacman refresh resolved cp through caller PATH" /usr/bin/cmp -s "$test_tmp/default/pacman/pacman-stable.conf" /etc/pacman.conf || fail "pacman refresh did not copy config from the authorized source root" [[ ! -e $evil_refresh_marker ]] || fail "pacman refresh copied from inherited OMARCHY_PATH or PATH" pass "pacman refresh runs its legacy hook only after all privileged work" reset_case authenticate_for_test set +e run_as_user env TEST_PACMAN_STATUS=1 TEST_ROOT_VICTIM="$refresh_victim" \ TEST_ROOT_DIR_VICTIM="$refresh_dir_victim" TEST_PERSISTENT_VICTIM="$refresh_persistent_victim" \ TEST_PERSISTENT_PID="$test_home/refresh.pid" "$ROOT/bin/omarchy-refresh-pacman" stable \ >"$test_tmp/refresh-fail.out" 2>"$test_tmp/refresh-fail.err" status=$? set -e (( status != 0 )) || fail "failing pacman refresh unexpectedly succeeded" [[ ! -e $token ]] || fail "failed pacman refresh left its credential live" [[ ! -s $hook_log ]] || fail "failed pacman transaction reached the refresh hook" pass "failed pacman refresh invalidates and does not run its hook" # Channel switching is a composite refresh caller: after refreshing it # authenticates for the package swap and runs the full update. Exercise the # real channel, refresh, update, and hook commands against the global-token # model. A detached legacy refresh-hook child must not start until that entire # chain has finished. cat >"$test_home/.config/omarchy/hooks/post-update" <<'HOOK' #!/bin/bash printf 'post-update:%s\n' "$(id -u)" >>"$TEST_HOOK_LOG" HOOK chown 1000:1000 "$test_home/.config/omarchy/hooks/post-update" chmod 0700 "$test_home/.config/omarchy/hooks/post-update" channel_victim="$root_dir/91-channel-refresh-hook.rules" channel_dir_victim="$root_dir/92-channel-refresh-dir.rules" channel_persistent_victim="$root_dir/93-channel-refresh-child.rules" channel_mise_victim="$root_dir/94-channel-mise-child.rules" reset_case set +e evil_channel_helper_marker="$test_home/evil-channel-helper-ran" evil_channel_helper_victim="$root_dir/98-channel-path-helper.rules" run_as_user env PATH="$evil_update_bin:$stub_bin:/usr/bin:/bin" \ TEST_EVIL_CHANNEL_HELPER_MARKER="$evil_channel_helper_marker" \ TEST_EVIL_CHANNEL_VICTIM="$evil_channel_helper_victim" \ TEST_ROOT_VICTIM="$channel_victim" TEST_ROOT_DIR_VICTIM="$channel_dir_victim" \ TEST_PERSISTENT_VICTIM="$channel_persistent_victim" TEST_PERSISTENT_PID="$test_home/channel.pid" \ TEST_MISE_VICTIM="$channel_mise_victim" TEST_MISE_PID="$test_home/channel-mise.pid" \ OMARCHY_UPDATE_LOGGED=1 "$ROOT/bin/omarchy-channel-set" stable \ >"$test_tmp/channel.out" 2>"$test_tmp/channel.err" status=$? set -e (( status == 0 )) || fail "isolated channel switch failed" "$(<"$test_tmp/channel.err")" wait_for_persistent_attempts "$test_home/channel.pid" wait_for_persistent_attempts "$test_home/channel-mise.pid" assert_hook_sandboxed "$channel_victim" "$channel_dir_victim" "$channel_persistent_victim" [[ ! -e $channel_mise_victim ]] || fail "channel mise child reused a later refresh-hook credential" [[ ! -e $evil_channel_helper_marker && ! -e $evil_channel_helper_victim ]] || fail "channel switch resolved a post-pacman helper through caller PATH" [[ $(grep -c '^file:1000$' "$hook_log") == 1 ]] || fail "channel switch did not run the deferred refresh hook exactly once" pass "channel switching defers its refresh hook past every later authentication" cat >"$stub_bin/omarchy-launch-floating-terminal-with-presentation" <<'STUB' #!/bin/bash exec bash -c "$1" STUB cat >"$stub_bin/omarchy-pkg-add" <<'STUB' #!/bin/bash [[ ${OMARCHY_SUDO_NO_UPDATE:-0} == 1 ]] || exit 98 /usr/bin/sudo -N -- /usr/bin/true exit "${TEST_PKG_STATUS:-0}" STUB cat >"$stub_bin/omarchy-font-set" <<'STUB' #!/bin/bash exec bash "$TEST_ROOT/bin/omarchy-font-set" "$@" STUB cat >"$stub_bin/fc-list" <<'STUB' #!/bin/bash echo 'Example Family' STUB cat >"$stub_bin/omarchy-restart-shell" <<'STUB' #!/bin/bash exit 0 STUB cat >"$stub_bin/pgrep" <<'STUB' #!/bin/bash exit 1 STUB cat >"$stub_bin/sleep" <<'STUB' #!/bin/bash exit 0 STUB chmod 0755 "$stub_bin"/* chmod 4755 "$stub_bin/sudo" mount --bind "$stub_bin/omarchy-launch-floating-terminal-with-presentation" \ /usr/bin/omarchy-launch-floating-terminal-with-presentation mount --bind "$stub_bin/omarchy-pkg-add" /usr/bin/omarchy-pkg-add mount --bind "$stub_bin/omarchy-font-set" /usr/bin/omarchy-font-set font_paths_bound=1 write_attack_hook font-set font_victim="$root_dir/87-font-hook.rules" font_dir_victim="$root_dir/88-font-hook-dir.rules" font_persistent_victim="$root_dir/89-font-hook-child.rules" reset_case set +e run_as_user env TEST_ROOT_VICTIM="$font_victim" TEST_ROOT_DIR_VICTIM="$font_dir_victim" \ TEST_PERSISTENT_VICTIM="$font_persistent_victim" TEST_PERSISTENT_PID="$test_home/font.pid" \ "$ROOT/bin/omarchy-install-font" 'Example Font' example-font 'Example Family' \ >"$test_tmp/font.out" 2>"$test_tmp/font.err" status=$? set -e (( status == 0 )) || fail "isolated font install failed" "$(<"$test_tmp/font.err")" wait_for_persistent_attempts "$test_home/font.pid" assert_hook_sandboxed "$font_victim" "$font_dir_victim" "$font_persistent_victim" pass "font installation invalidates before file, directory, and persistent hooks" reset_case set +e run_as_user env TEST_PKG_STATUS=1 TEST_ROOT_VICTIM="$font_victim" \ TEST_ROOT_DIR_VICTIM="$font_dir_victim" TEST_PERSISTENT_VICTIM="$font_persistent_victim" \ TEST_PERSISTENT_PID="$test_home/font.pid" \ "$ROOT/bin/omarchy-install-font" 'Example Font' example-font 'Example Family' \ >"$test_tmp/font-fail.out" 2>"$test_tmp/font-fail.err" status=$? set -e (( status != 0 )) || fail "failing font package installation unexpectedly succeeded" [[ ! -e $token ]] || fail "failed font package installation left its credential live" [[ ! -s $hook_log ]] || fail "failed font package installation reached the hook" pass "failed font installation invalidates without running its hook"