Complete command-scoped authentication across update phases
This commit is contained in:
1 parent
7f9a401bb1
commit
35b318ed09
16 files changed
+583
-1643
No files matched your search
@@ -28,7 +28,7 @@ Three documentation trees, split by genre and audience:
|
|||||||
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
|
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
|
||||||
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
|
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
|
||||||
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
|
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
|
||||||
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`)
|
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security entrypoint may use `#!/bin/bash -p` when it must suppress inherited startup code before its first command; document the boundary and test rejection of ordinary Bash with a decoy `-p` argument.
|
||||||
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
|
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
|
||||||
|
|
||||||
# Command Naming
|
# Command Naming
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ if [[ -n $dev_checkout ]]; then
|
|||||||
omarchy-state set reboot-required
|
omarchy-state set reboot-required
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-refresh-pacman "$pacman_channel"
|
omarchy-refresh-pacman "$pacman_channel" defer-hook
|
||||||
# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites.
|
# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites.
|
||||||
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}"
|
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}"
|
||||||
|
|
||||||
@@ -97,3 +97,6 @@ if [[ -z $dev_checkout ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-update -y
|
omarchy-update -y
|
||||||
|
|
||||||
|
# No channel-owned privileged work follows the historical refresh hook.
|
||||||
|
omarchy-refresh-pacman "$pacman_channel" run-deferred
|
||||||
+31
-230
@@ -4,242 +4,43 @@
|
|||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
if [[ $- != *p* ]]; then
|
if [[ $- != *p* ]]; then
|
||||||
echo "Refusing an unsafe Bash startup for pacman refresh." >&2
|
echo "Refusing an unsafe Bash startup." >&2
|
||||||
exit 126
|
exit 126
|
||||||
fi
|
fi
|
||||||
|
|
||||||
require_privileged_bash_startup() {
|
source "${BASH_SOURCE[0]%/*}/omarchy-security-functions" || exit 126
|
||||||
[[ $- == *p* ]] || return 1
|
omarchy_security_require_privileged_bash_startup || exit 126
|
||||||
/usr/bin/env -i /usr/bin/bash -p -c '
|
|
||||||
[[ $1 =~ ^[1-9][0-9]*$ ]] || exit 1
|
|
||||||
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
|
|
||||||
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
|
|
||||||
[[ $executable == /usr/bin/bash ]]
|
|
||||||
[[ ${argv[0]:-} == /bin/bash || ${argv[0]:-} == /usr/bin/bash ]]
|
|
||||||
[[ ${argv[1]:-} == -p ]]
|
|
||||||
' omarchy-bash-startup "$$"
|
|
||||||
}
|
|
||||||
if ! require_privileged_bash_startup; then
|
|
||||||
echo "Refusing an unsafe Bash startup for pacman refresh." >&2
|
|
||||||
exit 126
|
|
||||||
fi
|
|
||||||
unset -f require_privileged_bash_startup
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
omarchy_security_sanitize_bash_environment "$0" "$@"
|
||||||
|
user_path=$PATH
|
||||||
|
omarchy_security_revoke_sudo_timestamp || exit 1
|
||||||
|
omarchy_security_install_sudo_cleanup_traps
|
||||||
|
omarchy_security_enable_no_update_sudo
|
||||||
|
|
||||||
sanitize_bash_startup_environment() {
|
channel="${1:-stable}"
|
||||||
local environment_entry environment_name
|
hook_mode="${2:-normal}"
|
||||||
local needs_reexec=0
|
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||||
local -a environment_unsets=(-u BASH_ENV -u ENV)
|
echo "Invalid channel: $channel" >&2
|
||||||
|
exit 2
|
||||||
[[ -z ${BASH_ENV+x} && -z ${ENV+x} ]] || needs_reexec=1
|
|
||||||
while IFS= read -r -d '' environment_entry; do
|
|
||||||
environment_name="${environment_entry%%=*}"
|
|
||||||
if [[ $environment_name == BASH_FUNC_*%% ]]; then
|
|
||||||
environment_unsets+=(-u "$environment_name")
|
|
||||||
needs_reexec=1
|
|
||||||
fi
|
|
||||||
done < <(/usr/bin/env -0)
|
|
||||||
|
|
||||||
if (( needs_reexec )); then
|
|
||||||
exec /usr/bin/env "${environment_unsets[@]}" /usr/bin/bash -p "$0" "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
sanitize_bash_startup_environment "$@"
|
|
||||||
unset -f sanitize_bash_startup_environment
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: omarchy-refresh-pacman [stable|rc|edge]" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Composite commands can postpone the legacy user hook until their own final
|
|
||||||
# privilege boundary. The two internal modes are deliberately paired: a caller
|
|
||||||
# that defers must invoke --run-deferred-hook exactly once after all of its
|
|
||||||
# sudo-capable work has finished.
|
|
||||||
channel=stable
|
|
||||||
hook_mode=normal
|
|
||||||
case "$#:$1:${2:-}" in
|
|
||||||
0::)
|
|
||||||
;;
|
|
||||||
1:stable: | 1:rc: | 1:edge:)
|
|
||||||
channel="$1"
|
|
||||||
;;
|
|
||||||
1:--run-deferred-hook:)
|
|
||||||
hook_mode=run-deferred
|
|
||||||
;;
|
|
||||||
2:stable:--defer-hook | 2:rc:--defer-hook | 2:edge:--defer-hook)
|
|
||||||
channel="$1"
|
|
||||||
hook_mode=defer
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
trusted_directory_chain() {
|
|
||||||
local current="$1" allow_current_user="$2" canonical owner mode current_uid
|
|
||||||
current_uid=$(/usr/bin/id -u) || return 1
|
|
||||||
|
|
||||||
while :; do
|
|
||||||
[[ -d $current && ! -L $current ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$current") || return 1
|
|
||||||
[[ $canonical == "$current" ]] || return 1
|
|
||||||
[[ $current == / ]] && break
|
|
||||||
read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$current") || return 1
|
|
||||||
if [[ $owner != 0 ]] && ! { [[ $allow_current_user == "true" && $owner == "$current_uid" ]]; }; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
(( (8#$mode & 0022) == 0 )) || return 1
|
|
||||||
current=${current%/*}
|
|
||||||
[[ -n $current ]] || current=/
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
trusted_omarchy_source_root() {
|
|
||||||
local config=/etc/omarchy.conf default_root=/usr/share/omarchy configured_root="" canonical=""
|
|
||||||
local owner="" mode="" links="" size="" line="" encoded="" decoded="" character=""
|
|
||||||
local index=0 escaped=0 lines=()
|
|
||||||
|
|
||||||
if [[ ! -e $config && ! -L $config ]]; then
|
|
||||||
configured_root="$default_root"
|
|
||||||
else
|
|
||||||
[[ -f $config && ! -L $config ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$config") || return 1
|
|
||||||
[[ $canonical == "$config" ]] || return 1
|
|
||||||
read -r owner mode links size < <(/usr/bin/stat -Lc '%u %a %h %s' -- "$config") || return 1
|
|
||||||
[[ $owner == "0" && $links == "1" ]] || return 1
|
|
||||||
(( (8#$mode & 0022) == 0 && size > 0 && size <= 4096 )) || return 1
|
|
||||||
trusted_directory_chain /etc false || return 1
|
|
||||||
mapfile -t lines <"$config" || return 1
|
|
||||||
(( ${#lines[@]} == 1 )) || return 1
|
|
||||||
line="${lines[0]}"
|
|
||||||
[[ $line == 'export OMARCHY_PATH="'*'"' ]] || return 1
|
|
||||||
encoded="${line#'export OMARCHY_PATH="'}"
|
|
||||||
encoded="${encoded%'"'}"
|
|
||||||
for (( index = 0; index < ${#encoded}; index++ )); do
|
|
||||||
character="${encoded:index:1}"
|
|
||||||
if (( escaped )); then
|
|
||||||
case "$character" in
|
|
||||||
'\' | '"' | '$' | '`') decoded+="$character" ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
escaped=0
|
|
||||||
elif [[ $character == '\' ]]; then
|
|
||||||
escaped=1
|
|
||||||
elif [[ $character == '"' ]]; then
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
decoded+="$character"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
(( escaped == 0 )) || return 1
|
|
||||||
configured_root="$decoded"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -d $configured_root && ! -L $configured_root ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$configured_root") || return 1
|
|
||||||
[[ $canonical == "$configured_root" ]] || return 1
|
|
||||||
if [[ $configured_root == "$default_root" ]]; then
|
|
||||||
trusted_directory_chain "$configured_root" false || return 1
|
|
||||||
else
|
|
||||||
trusted_directory_chain "$configured_root" true || return 1
|
|
||||||
fi
|
|
||||||
printf '%s\n' "$configured_root"
|
|
||||||
}
|
|
||||||
|
|
||||||
trusted_omarchy_source_file() {
|
|
||||||
local relative="$1" source="$OMARCHY_PATH/$1" canonical owner mode links directory current_uid
|
|
||||||
current_uid=$(/usr/bin/id -u) || return 1
|
|
||||||
[[ $relative != /* && $relative != ../* && $relative != */../* && $relative != */.. ]] || return 1
|
|
||||||
[[ -f $source && ! -L $source ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$source") || return 1
|
|
||||||
[[ $canonical == "$source" && $canonical == "$OMARCHY_PATH/"* ]] || return 1
|
|
||||||
read -r owner mode links < <(/usr/bin/stat -Lc '%u %a %h' -- "$source") || return 1
|
|
||||||
[[ $links == 1 ]] && (( (8#$mode & 0022) == 0 )) || return 1
|
|
||||||
if [[ $OMARCHY_PATH == /usr/share/omarchy ]]; then
|
|
||||||
[[ $owner == 0 ]] || return 1
|
|
||||||
else
|
|
||||||
[[ $owner == 0 || $owner == "$current_uid" ]] || return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
directory=${source%/*}
|
|
||||||
while [[ $directory == "$OMARCHY_PATH" || $directory == "$OMARCHY_PATH/"* ]]; do
|
|
||||||
[[ -d $directory && ! -L $directory ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$directory") || return 1
|
|
||||||
[[ $canonical == "$directory" ]] || return 1
|
|
||||||
read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$directory") || return 1
|
|
||||||
(( (8#$mode & 0022) == 0 )) || return 1
|
|
||||||
if [[ $OMARCHY_PATH == /usr/share/omarchy ]]; then
|
|
||||||
[[ $owner == 0 ]] || return 1
|
|
||||||
else
|
|
||||||
[[ $owner == 0 || $owner == "$current_uid" ]] || return 1
|
|
||||||
fi
|
|
||||||
[[ $directory == "$OMARCHY_PATH" ]] && break
|
|
||||||
directory=${directory%/*}
|
|
||||||
done
|
|
||||||
printf '%s\n' "$source"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! OMARCHY_PATH=$(trusted_omarchy_source_root); then
|
|
||||||
echo "Refusing to refresh pacman from an untrusted Omarchy source root." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
export OMARCHY_PATH
|
if [[ $hook_mode != "normal" && $hook_mode != "defer-hook" && $hook_mode != "run-deferred" ]]; then
|
||||||
user_path="${PATH:-/usr/bin:/bin}"
|
echo "Invalid refresh hook mode: $hook_mode" >&2
|
||||||
PATH="$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
|
exit 2
|
||||||
export PATH
|
|
||||||
|
|
||||||
as_root() {
|
|
||||||
if ((EUID == 0)); then
|
|
||||||
"$@"
|
|
||||||
elif [[ ${OMARCHY_SUDO_NO_UPDATE:-0} == 1 ]]; then
|
|
||||||
/usr/bin/sudo -N -- "$@"
|
|
||||||
else
|
|
||||||
/usr/bin/sudo -- "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_sudo_credentials() {
|
|
||||||
/usr/bin/sudo -k || true
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup_sudo_credentials EXIT
|
|
||||||
|
|
||||||
if [[ $hook_mode == "run-deferred" ]]; then
|
|
||||||
/usr/bin/sudo -k || exit 1
|
|
||||||
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman
|
|
||||||
exit
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pacman_source=$(trusted_omarchy_source_file "default/pacman/pacman-$channel.conf") || {
|
if [[ $hook_mode != "run-deferred" ]]; then
|
||||||
echo "Refusing an untrusted pacman configuration source." >&2
|
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
|
||||||
exit 1
|
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
||||||
}
|
echo "Setting channel to $channel"
|
||||||
mirror_source=$(trusted_omarchy_source_file "default/pacman/mirrorlist-$channel") || {
|
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
|
||||||
echo "Refusing an untrusted pacman mirror source." >&2
|
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
||||||
exit 1
|
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm
|
||||||
}
|
fi
|
||||||
|
|
||||||
as_root /usr/bin/cp -f -- /etc/pacman.conf /etc/pacman.conf.bak
|
# Keep the historical hook name, but finish every privileged refresh operation
|
||||||
as_root /usr/bin/cp -f -- /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
# before running user code. Callers with later root work can defer the hook.
|
||||||
|
if [[ $hook_mode != "defer-hook" ]]; then
|
||||||
echo "Setting channel to $channel"
|
omarchy_security_revoke_sudo_timestamp
|
||||||
echo
|
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \
|
||||||
|
"$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman
|
||||||
# The unprivileged shell opens the authorized source. Root consumes only the
|
|
||||||
# inherited descriptor, never a caller-writable development-checkout pathname.
|
|
||||||
as_root /usr/bin/install -T -o root -g root -m 0644 /dev/stdin /etc/pacman.conf <"$pacman_source"
|
|
||||||
as_root /usr/bin/install -T -o root -g root -m 0644 /dev/stdin /etc/pacman.d/mirrorlist <"$mirror_source"
|
|
||||||
|
|
||||||
# Reset all package DBs and then update.
|
|
||||||
as_root /usr/bin/env OMARCHY_UPDATE_PACMAN=1 /usr/bin/pacman -Syyuu --noconfirm
|
|
||||||
|
|
||||||
# This legacy hook used to run before pacman. Executable user code cannot
|
|
||||||
# safely precede a later sudo authentication: a child can wait for the new
|
|
||||||
# timestamp even if the parent invalidates around the hook. Keep the hook, but
|
|
||||||
# run it only after every privileged refresh step and with a cold credential.
|
|
||||||
if [[ $hook_mode == "normal" ]]; then
|
|
||||||
/usr/bin/sudo -k || exit 1
|
|
||||||
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman
|
|
||||||
fi
|
fi
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:hidden=true
|
||||||
|
# omarchy:summary=Provide internal helpers for command-scoped sudo authentication
|
||||||
|
|
||||||
|
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
||||||
|
echo "omarchy-security-functions is an internal function library." >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
omarchy_security_require_privileged_bash_startup() {
|
||||||
|
[[ $- == *p* ]] || return 1
|
||||||
|
/usr/bin/env -i /usr/bin/bash -p -c '
|
||||||
|
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
|
||||||
|
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
|
||||||
|
[[ $executable == "/usr/bin/bash" &&
|
||||||
|
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
|
||||||
|
${argv[1]:-} == "-p" ]]
|
||||||
|
' omarchy-bash-startup "$$"
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_sanitize_bash_environment() {
|
||||||
|
local script=$1
|
||||||
|
shift
|
||||||
|
local entry name environment_fd environment_pid
|
||||||
|
local -a unsets=()
|
||||||
|
|
||||||
|
# Read the raw environment: privileged Bash ignores exported functions, but
|
||||||
|
# leaves their records for ordinary child interpreters to import later.
|
||||||
|
exec {environment_fd}< <(/usr/bin/env -0)
|
||||||
|
environment_pid=$!
|
||||||
|
while IFS= read -r -d '' entry <&"$environment_fd"; do
|
||||||
|
name=${entry%%=*}
|
||||||
|
case "$name" in
|
||||||
|
BASH_ENV|ENV|SHELLOPTS|BASHOPTS|PS4|CDPATH|GLOBIGNORE|BASH_FUNC_*%%)
|
||||||
|
unsets+=(-u "$name")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec {environment_fd}<&-
|
||||||
|
wait "$environment_pid" || return 1
|
||||||
|
if (( ${#unsets[@]} > 0 )); then
|
||||||
|
exec /usr/bin/env "${unsets[@]}" /usr/bin/bash -p -- "$script" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_sudo_supports_no_update() {
|
||||||
|
local help
|
||||||
|
help=$(LC_ALL=C /usr/bin/sudo -h 2>&1) || return 1
|
||||||
|
/usr/bin/grep -Eq '^usage: sudo .*\[[^]]*N[^]]*\]' <<< "$help"
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_revoke_sudo_timestamp() {
|
||||||
|
/usr/bin/sudo -k
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_exit_with_revoked_sudo() {
|
||||||
|
local status=$1
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
if ! omarchy_security_revoke_sudo_timestamp; then
|
||||||
|
echo "Could not invalidate cached sudo authorization." >&2
|
||||||
|
(( status != 0 )) || status=1
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_install_signal_exit_traps() {
|
||||||
|
trap 'exit 129' HUP
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_install_sudo_cleanup_traps() {
|
||||||
|
trap 'omarchy_security_exit_with_revoked_sudo "$?"' EXIT
|
||||||
|
omarchy_security_install_signal_exit_traps
|
||||||
|
}
|
||||||
|
|
||||||
|
omarchy_security_enable_no_update_sudo() {
|
||||||
|
local wrapper_dir="$OMARCHY_PATH/default/omarchy/sudo-no-update"
|
||||||
|
if ! omarchy_security_sudo_supports_no_update; then
|
||||||
|
echo "This sudo does not support --no-update; refusing mixed-trust work." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f $wrapper_dir/sudo || ! -x $wrapper_dir/sudo ]]; then
|
||||||
|
echo "The command-scoped sudo wrapper is missing." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
PATH="$wrapper_dir:$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
|
||||||
|
OMARCHY_SUDO_NO_UPDATE=1
|
||||||
|
export PATH OMARCHY_SUDO_NO_UPDATE
|
||||||
|
}
|
||||||
+21
-206
@@ -6,210 +6,27 @@
|
|||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
if [[ $- != *p* ]]; then
|
if [[ $- != *p* ]]; then
|
||||||
echo "Refusing an unsafe Bash startup for the Omarchy update." >&2
|
echo "Refusing an unsafe Bash startup." >&2
|
||||||
exit 126
|
exit 126
|
||||||
fi
|
fi
|
||||||
|
|
||||||
require_privileged_bash_startup() {
|
source "${BASH_SOURCE[0]%/*}/omarchy-security-functions" || exit 126
|
||||||
[[ $- == *p* ]] || return 1
|
omarchy_security_require_privileged_bash_startup || exit 126
|
||||||
/usr/bin/env -i /usr/bin/bash -p -c '
|
|
||||||
[[ $1 =~ ^[1-9][0-9]*$ ]] || exit 1
|
|
||||||
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
|
|
||||||
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
|
|
||||||
[[ $executable == /usr/bin/bash ]]
|
|
||||||
[[ ${argv[0]:-} == /bin/bash || ${argv[0]:-} == /usr/bin/bash ]]
|
|
||||||
[[ ${argv[1]:-} == -p ]]
|
|
||||||
' omarchy-bash-startup "$$"
|
|
||||||
}
|
|
||||||
if ! require_privileged_bash_startup; then
|
|
||||||
echo "Refusing an unsafe Bash startup for the Omarchy update." >&2
|
|
||||||
exit 126
|
|
||||||
fi
|
|
||||||
unset -f require_privileged_bash_startup
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
omarchy_security_sanitize_bash_environment "$0" "$@"
|
||||||
|
user_path=$PATH
|
||||||
|
omarchy_security_revoke_sudo_timestamp || exit 1
|
||||||
|
omarchy_security_install_sudo_cleanup_traps
|
||||||
|
omarchy_security_enable_no_update_sudo
|
||||||
|
|
||||||
# Privileged mode prevents BASH_ENV and exported functions from running before
|
|
||||||
# this boundary. Re-exec once without their raw environment records so ordinary
|
|
||||||
# Bash helpers cannot import them again and bypass the trusted command paths.
|
|
||||||
sanitize_bash_startup_environment() {
|
|
||||||
local environment_entry environment_name
|
|
||||||
local needs_reexec=0
|
|
||||||
local -a environment_unsets=(-u BASH_ENV -u ENV)
|
|
||||||
|
|
||||||
[[ -z ${BASH_ENV+x} && -z ${ENV+x} ]] || needs_reexec=1
|
|
||||||
while IFS= read -r -d '' environment_entry; do
|
|
||||||
environment_name="${environment_entry%%=*}"
|
|
||||||
if [[ $environment_name == BASH_FUNC_*%% ]]; then
|
|
||||||
environment_unsets+=(-u "$environment_name")
|
|
||||||
needs_reexec=1
|
|
||||||
fi
|
|
||||||
done < <(/usr/bin/env -0)
|
|
||||||
|
|
||||||
if (( needs_reexec )); then
|
|
||||||
exec /usr/bin/env "${environment_unsets[@]}" /usr/bin/bash -p "$0" "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
sanitize_bash_startup_environment "$@"
|
|
||||||
unset -f sanitize_bash_startup_environment
|
|
||||||
|
|
||||||
trusted_directory_chain() {
|
|
||||||
local current="$1" allow_current_user="$2" canonical owner mode current_uid
|
|
||||||
current_uid=$(/usr/bin/id -u) || return 1
|
|
||||||
|
|
||||||
while :; do
|
|
||||||
[[ -d $current && ! -L $current ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$current") || return 1
|
|
||||||
[[ $canonical == "$current" ]] || return 1
|
|
||||||
[[ $current == / ]] && break
|
|
||||||
read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$current") || return 1
|
|
||||||
if [[ $owner != 0 ]] && ! { [[ $allow_current_user == "true" && $owner == "$current_uid" ]]; }; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
(( (8#$mode & 0022) == 0 )) || return 1
|
|
||||||
current=${current%/*}
|
|
||||||
[[ -n $current ]] || current=/
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
trusted_omarchy_source_root() {
|
|
||||||
local config=/etc/omarchy.conf
|
|
||||||
local default_root=/usr/share/omarchy
|
|
||||||
local configured_root=""
|
|
||||||
local canonical=""
|
|
||||||
local owner=""
|
|
||||||
local mode=""
|
|
||||||
local links=""
|
|
||||||
local size=""
|
|
||||||
local line=""
|
|
||||||
local encoded=""
|
|
||||||
local decoded=""
|
|
||||||
local character=""
|
|
||||||
local index=0
|
|
||||||
local escaped=0
|
|
||||||
local lines=()
|
|
||||||
|
|
||||||
if [[ ! -e $config && ! -L $config ]]; then
|
|
||||||
configured_root="$default_root"
|
|
||||||
else
|
|
||||||
[[ -f $config && ! -L $config ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$config") || return 1
|
|
||||||
[[ $canonical == "$config" ]] || return 1
|
|
||||||
read -r owner mode links size < <(/usr/bin/stat -Lc '%u %a %h %s' -- "$config") || return 1
|
|
||||||
[[ $owner == "0" && $links == "1" ]] || return 1
|
|
||||||
(( (8#$mode & 0022) == 0 && size > 0 && size <= 4096 )) || return 1
|
|
||||||
trusted_directory_chain /etc false || return 1
|
|
||||||
|
|
||||||
mapfile -t lines <"$config" || return 1
|
|
||||||
(( ${#lines[@]} == 1 )) || return 1
|
|
||||||
line="${lines[0]}"
|
|
||||||
[[ $line == 'export OMARCHY_PATH="'*'"' ]] || return 1
|
|
||||||
encoded="${line#'export OMARCHY_PATH="'}"
|
|
||||||
encoded="${encoded%'"'}"
|
|
||||||
|
|
||||||
for (( index = 0; index < ${#encoded}; index++ )); do
|
|
||||||
character="${encoded:index:1}"
|
|
||||||
if (( escaped )); then
|
|
||||||
case "$character" in
|
|
||||||
'\' | '"' | '$' | '`') decoded+="$character" ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
escaped=0
|
|
||||||
elif [[ $character == '\' ]]; then
|
|
||||||
escaped=1
|
|
||||||
elif [[ $character == '"' ]]; then
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
decoded+="$character"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
(( escaped == 0 )) || return 1
|
|
||||||
configured_root="$decoded"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -d $configured_root && ! -L $configured_root ]] || return 1
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$configured_root") || return 1
|
|
||||||
[[ $canonical == "$configured_root" ]] || return 1
|
|
||||||
|
|
||||||
if [[ $configured_root == "$default_root" ]]; then
|
|
||||||
trusted_directory_chain "$configured_root" false || return 1
|
|
||||||
else
|
|
||||||
trusted_directory_chain "$configured_root" true || return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "$configured_root"
|
|
||||||
}
|
|
||||||
|
|
||||||
sudo_supports_no_update() {
|
|
||||||
LC_ALL=C /usr/bin/sudo -h 2>&1 | /usr/bin/grep -Eq '^usage: sudo .*\[[^]]*N[^]]*\]'
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_non_reusable_sudo() {
|
|
||||||
local wrapper_dir="$OMARCHY_PATH/default/omarchy/sudo-no-update"
|
|
||||||
local wrapper="$wrapper_dir/sudo" canonical="" current="" owner="" mode=""
|
|
||||||
|
|
||||||
sudo_supports_no_update || {
|
|
||||||
echo "This sudo does not support --no-update; refusing to run a mixed-trust update." >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
[[ -f $wrapper && -x $wrapper && ! -L $wrapper ]] || {
|
|
||||||
echo "Trusted no-update sudo wrapper is missing; refusing to run a mixed-trust update." >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
canonical=$(/usr/bin/realpath -e -- "$wrapper") || return 1
|
|
||||||
[[ $canonical == "$wrapper" ]] || return 1
|
|
||||||
if [[ $OMARCHY_PATH == "/usr/share/omarchy" ]]; then
|
|
||||||
current="$wrapper"
|
|
||||||
while :; do
|
|
||||||
[[ ! -L $current ]] || return 1
|
|
||||||
read -r owner mode < <(/usr/bin/stat -Lc '%u %a' -- "$current") || return 1
|
|
||||||
[[ $owner == "0" ]] || return 1
|
|
||||||
(( (8#$mode & 0022) == 0 )) || return 1
|
|
||||||
[[ $current == "$OMARCHY_PATH" ]] && break
|
|
||||||
current=${current%/*}
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
enable_non_reusable_sudo() {
|
|
||||||
local wrapper_dir="$OMARCHY_PATH/default/omarchy/sudo-no-update"
|
|
||||||
|
|
||||||
validate_non_reusable_sudo
|
|
||||||
PATH="$wrapper_dir:$PATH"
|
|
||||||
export PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! OMARCHY_PATH=$(trusted_omarchy_source_root); then
|
|
||||||
echo "Refusing to update from an untrusted Omarchy source root." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
export OMARCHY_PATH
|
|
||||||
user_path="${PATH:-/usr/bin:/bin}"
|
|
||||||
PATH="$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
|
|
||||||
export PATH
|
|
||||||
update_stay_awake_stopped=0
|
update_stay_awake_stopped=0
|
||||||
|
|
||||||
# Verify and enable the security primitive before any update-owned privileged
|
|
||||||
# work. Every authorization in this workflow is command-scoped (`sudo -N`): it
|
|
||||||
# may prompt for the command being run, but it never publishes a reusable
|
|
||||||
# timestamp to a dev hook, migration tool, AUR build, or detached child.
|
|
||||||
validate_non_reusable_sudo || exit 1
|
|
||||||
if [[ ${OMARCHY_SUDO_NO_UPDATE:-0} == 1 ]]; then
|
|
||||||
enable_non_reusable_sudo
|
|
||||||
fi
|
|
||||||
/usr/bin/sudo -k || exit 1
|
|
||||||
enable_non_reusable_sudo
|
|
||||||
export OMARCHY_SUDO_NO_UPDATE=1
|
|
||||||
|
|
||||||
cleanup_update() {
|
cleanup_update() {
|
||||||
local status=$?
|
local status=$?
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
trap - EXIT
|
|
||||||
if (( update_stay_awake_stopped == 0 )); then
|
if (( update_stay_awake_stopped == 0 )); then
|
||||||
omarchy-update-stay-awake stop || true
|
omarchy-update-stay-awake stop || status=1
|
||||||
fi
|
fi
|
||||||
/usr/bin/sudo -k || true
|
omarchy_security_exit_with_revoked_sudo "$status"
|
||||||
exit "$status"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
|
if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
|
||||||
@@ -223,6 +40,7 @@ fi
|
|||||||
|
|
||||||
trap 'echo ""; echo -e "\033[0;31mSomething went wrong during the update!\n\nPlease review the output above carefully, correct the error, and retry the update.\n\nIf you need assistance, get help from the community at https://omarchy.org/discord\033[0m"' ERR
|
trap 'echo ""; echo -e "\033[0;31mSomething went wrong during the update!\n\nPlease review the output above carefully, correct the error, and retry the update.\n\nIf you need assistance, get help from the community at https://omarchy.org/discord\033[0m"' ERR
|
||||||
trap cleanup_update EXIT
|
trap cleanup_update EXIT
|
||||||
|
omarchy_security_install_signal_exit_traps
|
||||||
|
|
||||||
omarchy-update-requires-free-space
|
omarchy-update-requires-free-space
|
||||||
|
|
||||||
@@ -243,9 +61,7 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|||||||
|
|
||||||
omarchy-update-stay-awake start
|
omarchy-update-stay-awake start
|
||||||
|
|
||||||
# A dev link explicitly authorizes its checkout through root-owned system
|
# Preserve the established development-checkout update ordering.
|
||||||
# configuration (including sudo's secure_path), so preserve the established
|
|
||||||
# pull-before-packages/migrations ordering for that trusted mode.
|
|
||||||
omarchy-update-dev
|
omarchy-update-dev
|
||||||
omarchy-update-keyring
|
omarchy-update-keyring
|
||||||
|
|
||||||
@@ -258,7 +74,7 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|||||||
# tooling with privileged repairs. The no-update sudo wrapper has covered the
|
# tooling with privileged repairs. The no-update sudo wrapper has covered the
|
||||||
# whole update, so neither the package transaction nor a later repair can
|
# whole update, so neither the package transaction nor a later repair can
|
||||||
# publish a timestamp to a detached migration child.
|
# publish a timestamp to a detached migration child.
|
||||||
/usr/bin/sudo -k
|
omarchy_security_revoke_sudo_timestamp
|
||||||
omarchy-migrate
|
omarchy-migrate
|
||||||
omarchy-update-orphan-pkgs
|
omarchy-update-orphan-pkgs
|
||||||
|
|
||||||
@@ -275,19 +91,18 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|||||||
omarchy-update-stay-awake stop
|
omarchy-update-stay-awake stop
|
||||||
update_stay_awake_stopped=1
|
update_stay_awake_stopped=1
|
||||||
|
|
||||||
# AUR installation can refresh sudo after running package build code. No
|
# AUR package installation must also use the no-update wrapper. Finish
|
||||||
# privileged update stage may follow it: user-controlled code can outlive
|
# update-owned system work before build code, hooks, or mise can run.
|
||||||
# its parent and wait for a later timestamp even if we invalidate in between.
|
|
||||||
omarchy-update-aur-pkgs
|
omarchy-update-aur-pkgs
|
||||||
/usr/bin/sudo -k
|
omarchy_security_revoke_sudo_timestamp
|
||||||
|
|
||||||
# Hooks and mise execute user-controlled code. Give each a cold credential
|
# Hooks and mise execute user-controlled code. Give each a cold credential
|
||||||
# boundary and run mise last so it cannot wait for a legitimate hook sudo.
|
# boundary and run mise last so it cannot wait for a legitimate hook sudo.
|
||||||
# Only the unprivileged reboot prompt follows them.
|
# Only the unprivileged reboot prompt follows them.
|
||||||
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
|
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
|
||||||
/usr/bin/sudo -k
|
omarchy_security_revoke_sudo_timestamp
|
||||||
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
|
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
|
||||||
/usr/bin/sudo -k
|
omarchy_security_revoke_sudo_timestamp
|
||||||
|
|
||||||
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
|
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
|
||||||
fi
|
fi
|
||||||
@@ -2,10 +2,17 @@
|
|||||||
|
|
||||||
# omarchy:summary=Update AUR packages if any are installed
|
# omarchy:summary=Update AUR packages if any are installed
|
||||||
|
|
||||||
|
sudo_options=()
|
||||||
|
if [[ ${OMARCHY_SUDO_NO_UPDATE:-0} == "1" ]]; then
|
||||||
|
sudo_wrapper="$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo"
|
||||||
|
[[ -x $sudo_wrapper ]] || exit 1
|
||||||
|
sudo_options=(--sudo "$sudo_wrapper" --nosudoloop)
|
||||||
|
fi
|
||||||
|
|
||||||
if pacman -Qem >/dev/null; then
|
if pacman -Qem >/dev/null; then
|
||||||
if omarchy-pkg-aur-accessible; then
|
if omarchy-pkg-aur-accessible; then
|
||||||
echo -e "\e[32m\nUpdate AUR packages\e[0m"
|
echo -e "\e[32m\nUpdate AUR packages\e[0m"
|
||||||
yay -Sua --noconfirm --cleanafter --ignore gcc14,gcc14-libs
|
yay "${sudo_options[@]}" -Sua --noconfirm --cleanafter --ignore gcc14,gcc14-libs || exit 1
|
||||||
echo
|
echo
|
||||||
else
|
else
|
||||||
echo -e "\e[31m\nAUR is unavailable (so skipping updates)\e[0m"
|
echo -e "\e[31m\nAUR is unavailable (so skipping updates)\e[0m"
|
||||||
|
|||||||
+51
-36
@@ -1,51 +1,66 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Prompt for required reboot or service restarts after updates
|
# omarchy:summary=Prompt for required reboot or service restarts after updates
|
||||||
|
# omarchy:args=[--services-only|--reboot-only]
|
||||||
|
|
||||||
|
mode="${1:-all}"
|
||||||
|
case "$mode" in
|
||||||
|
all|--services-only|--reboot-only) ;;
|
||||||
|
*) echo "Unknown restart phase: $mode" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
confirm_reboot() {
|
confirm_reboot() {
|
||||||
gum confirm "$1" && { omarchy-system-reboot; exit 0; }
|
if [[ ${OMARCHY_UPDATE_UNATTENDED:-0} == "1" ]]; then
|
||||||
|
echo "$1 Run omarchy-system-reboot when ready."
|
||||||
|
elif gum confirm "$1"; then
|
||||||
|
omarchy-system-reboot
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
running_kernel=$(uname -r)
|
if [[ $mode != "--services-only" ]]; then
|
||||||
kernel_updated=true
|
running_kernel=$(uname -r)
|
||||||
|
kernel_updated=true
|
||||||
|
|
||||||
for kernel in /usr/lib/modules/*/vmlinuz; do
|
for kernel in /usr/lib/modules/*/vmlinuz; do
|
||||||
if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then
|
if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then
|
||||||
installed_kernel=$(basename "$(dirname "$kernel")")
|
installed_kernel=$(basename "$(dirname "$kernel")")
|
||||||
|
|
||||||
if [[ $installed_kernel == $running_kernel ]]; then
|
if [[ $installed_kernel == $running_kernel ]]; then
|
||||||
kernel_updated=false
|
kernel_updated=false
|
||||||
break
|
break
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
done
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $kernel_updated == "true" ]]; then
|
if [[ $kernel_updated == "true" ]]; then
|
||||||
confirm_reboot "Linux kernel has been updated. Reboot?"
|
confirm_reboot "Linux kernel has been updated. Reboot?"
|
||||||
elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then
|
elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then
|
||||||
confirm_reboot "Updates require reboot. Ready?"
|
confirm_reboot "Updates require reboot. Ready?"
|
||||||
|
fi
|
||||||
|
|
||||||
|
running_hyprland=$(readlink /proc/$(pgrep -x Hyprland)/exe 2>/dev/null)
|
||||||
|
if [[ $running_hyprland == *"(deleted)"* ]]; then
|
||||||
|
confirm_reboot "Hyprland has been updated. Reboot?"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
running_hyprland=$(readlink /proc/$(pgrep -x Hyprland)/exe 2>/dev/null)
|
if [[ $mode != "--reboot-only" ]]; then
|
||||||
if [[ $running_hyprland == *"(deleted)"* ]]; then
|
for file in "$HOME"/.local/state/omarchy/restart-*-required; do
|
||||||
confirm_reboot "Hyprland has been updated. Reboot?"
|
if [[ -f $file ]]; then
|
||||||
|
filename=$(basename "$file")
|
||||||
|
service=$(echo "$filename" | sed 's/restart-\(.*\)-required/\1/')
|
||||||
|
echo "Restarting $service"
|
||||||
|
omarchy-state clear "$filename"
|
||||||
|
omarchy-restart-"$service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Updates routinely replace the shell's QML, and a stale process can lazy-load
|
||||||
|
# new files into old code. A restart failure (locked session, ssh, TTY) only
|
||||||
|
# prints its reason: the next update or login gets a fresh shell anyway.
|
||||||
|
echo -e "\e[32m\nRestarting shell\e[0m"
|
||||||
|
echo "All plugins have been reloaded"
|
||||||
|
omarchy-restart-shell || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for file in "$HOME"/.local/state/omarchy/restart-*-required; do
|
|
||||||
if [[ -f $file ]]; then
|
|
||||||
filename=$(basename "$file")
|
|
||||||
service=$(echo "$filename" | sed 's/restart-\(.*\)-required/\1/')
|
|
||||||
echo "Restarting $service"
|
|
||||||
omarchy-state clear "$filename"
|
|
||||||
omarchy-restart-"$service"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Updates routinely replace the shell's QML, and a stale process can lazy-load
|
|
||||||
# new files into old code. A restart failure (locked session, ssh, TTY) only
|
|
||||||
# prints its reason: the next update or login gets a fresh shell anyway.
|
|
||||||
echo -e "\e[32m\nRestarting shell\e[0m"
|
|
||||||
echo "All plugins have been reloaded"
|
|
||||||
omarchy-restart-shell || true
|
|
||||||
@@ -81,46 +81,55 @@ stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
local inhibit_pid=""
|
|
||||||
local inhibit_start_time=""
|
|
||||||
local inhibit_runner=()
|
|
||||||
local idle_owner="$$:$RANDOM:$RANDOM"
|
local idle_owner="$$:$RANDOM:$RANDOM"
|
||||||
|
|
||||||
stop
|
stop
|
||||||
mkdir -p "$state_dir"
|
mkdir -p "$state_dir"
|
||||||
|
|
||||||
if omarchy-cmd-present systemd-inhibit; then
|
# sudo authenticates in the foreground, then backgrounds the inhibitor.
|
||||||
if (( EUID != 0 )); then
|
# The held command drops back to this user before publishing its PID, so stop
|
||||||
if [[ -t 0 ]]; then
|
# can release the inhibitor without another privileged operation or ticket.
|
||||||
sudo -v
|
local hold_command=(
|
||||||
inhibit_runner=(sudo)
|
/usr/bin/systemd-inhibit --what=sleep:idle --who=omarchy-update
|
||||||
else
|
--why="Omarchy update in progress" --mode=block
|
||||||
inhibit_runner=(pkexec)
|
/usr/bin/setpriv --reuid "$UID" --regid "$(id -g)" --clear-groups
|
||||||
fi
|
/usr/bin/bash -p -c '
|
||||||
fi
|
read -r process_stat <"/proc/$$/stat"
|
||||||
|
process_stat=${process_stat##*) }
|
||||||
|
read -r -a fields <<< "$process_stat"
|
||||||
|
printf "%s %s\n" "$$" "${fields[19]}" >"$1"
|
||||||
|
exec /usr/bin/sleep infinity
|
||||||
|
' omarchy-update-inhibitor "$inhibit_pid_file"
|
||||||
|
)
|
||||||
|
|
||||||
if [[ -n ${OMARCHY_UPDATE_LOCK_FD:-} ]]; then
|
if (( EUID == 0 )); then
|
||||||
"${inhibit_runner[@]}" systemd-inhibit \
|
( [[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
|
||||||
--what=sleep:idle \
|
exec "${hold_command[@]}" ) &
|
||||||
--who=omarchy-update \
|
elif [[ -t 0 ]]; then
|
||||||
--why="Omarchy update in progress" \
|
sudo -N -b -- "${hold_command[@]}"
|
||||||
--mode=block \
|
else
|
||||||
sleep infinity >/dev/null 2>&1 {OMARCHY_UPDATE_LOCK_FD}>&- &
|
( [[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
|
||||||
else
|
exec pkexec "${hold_command[@]}" ) &
|
||||||
"${inhibit_runner[@]}" systemd-inhibit \
|
|
||||||
--what=sleep:idle \
|
|
||||||
--who=omarchy-update \
|
|
||||||
--why="Omarchy update in progress" \
|
|
||||||
--mode=block \
|
|
||||||
sleep infinity >/dev/null 2>&1 &
|
|
||||||
fi
|
|
||||||
inhibit_pid=$!
|
|
||||||
inhibit_start_time=$(process_start_time "$inhibit_pid" 2>/dev/null || true)
|
|
||||||
if [[ -n $inhibit_start_time ]]; then
|
|
||||||
printf '%s %s\n' "$inhibit_pid" "$inhibit_start_time" >"$inhibit_pid_file"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# For graphical authentication the launcher may wait for a password. Wait for
|
||||||
|
# either the user-owned held command to become ready or the launcher to fail.
|
||||||
|
local launcher_pid=${!:-}
|
||||||
|
local readiness_attempts=0
|
||||||
|
while [[ ! -s $inhibit_pid_file ]]; do
|
||||||
|
if [[ -n $launcher_pid ]] && ! kill -0 "$launcher_pid" 2>/dev/null; then
|
||||||
|
wait "$launcher_pid" || return 1
|
||||||
|
echo "The update sleep inhibitor did not start." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
readiness_attempts=$((readiness_attempts + 1))
|
||||||
|
if [[ -t 0 ]] && (( EUID != 0 && readiness_attempts >= 100 )); then
|
||||||
|
echo "The update sleep inhibitor did not become ready." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
|
||||||
if [[ ! -f $stay_awake_state ]]; then
|
if [[ ! -f $stay_awake_state ]]; then
|
||||||
printf '%s\n' "$idle_owner" >"$idle_owner_file"
|
printf '%s\n' "$idle_owner" >"$idle_owner_file"
|
||||||
mkdir -p "$(dirname "$stay_awake_state")"
|
mkdir -p "$(dirname "$stay_awake_state")"
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
#!/bin/bash -p
|
#!/bin/bash -p
|
||||||
|
|
||||||
# Internal update/migration sudo boundary. Authentication may authorize this
|
# Preserve sudo options while preventing authentication from refreshing the
|
||||||
# command, but -N prevents it from publishing a timestamp that detached user
|
# credential cache. Timestamp maintenance and informational modes stand alone.
|
||||||
# code can silently reuse.
|
|
||||||
|
|
||||||
if [[ $- != *p* ]]; then
|
if [[ $- != *p* ]]; then
|
||||||
echo "Refusing an unsafe Bash startup for the sudo boundary." >&2
|
echo "Refusing an unsafe Bash startup for the sudo boundary." >&2
|
||||||
exit 126
|
exit 126
|
||||||
fi
|
fi
|
||||||
|
source "${BASH_SOURCE[0]%/*}/../../../bin/omarchy-security-functions" || exit 126
|
||||||
|
omarchy_security_require_privileged_bash_startup || exit 126
|
||||||
|
|
||||||
require_privileged_bash_startup() {
|
if (( $# == 1 )); then
|
||||||
[[ $- == *p* ]] || return 1
|
case "$1" in
|
||||||
/usr/bin/env -i /usr/bin/bash -p -c '
|
-k|--reset-timestamp|-K|--remove-timestamp|-h|--help|-V|--version)
|
||||||
[[ $1 =~ ^[1-9][0-9]*$ ]] || exit 1
|
exec /usr/bin/sudo "$@"
|
||||||
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
|
;;
|
||||||
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
|
esac
|
||||||
[[ $executable == /usr/bin/bash ]]
|
|
||||||
[[ ${argv[0]:-} == /bin/bash || ${argv[0]:-} == /usr/bin/bash ]]
|
|
||||||
[[ ${argv[1]:-} == -p ]]
|
|
||||||
' omarchy-bash-startup "$$"
|
|
||||||
}
|
|
||||||
if ! require_privileged_bash_startup; then
|
|
||||||
echo "Refusing an unsafe Bash startup for the sudo boundary." >&2
|
|
||||||
exit 126
|
|
||||||
fi
|
fi
|
||||||
exec /usr/bin/sudo -N -- "$@"
|
exec /usr/bin/sudo -N "$@"
|
||||||
+7
-11
@@ -26,7 +26,7 @@ The design goal is:
|
|||||||
| `~/.local/state/omarchy/current/` | user | Generated active theme, selected theme name, and current background symlink. |
|
| `~/.local/state/omarchy/current/` | user | Generated active theme, selected theme name, and current background symlink. |
|
||||||
| `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. |
|
| `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. |
|
||||||
| `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. |
|
| `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. |
|
||||||
| `~/.local/state/omarchy/restart-*-required` | user | Optional allowlisted service/app restart markers checked by `omarchy-update-restart`. Marker names select fixed commands from the system-authorized Omarchy tree; they are not resolved through caller `PATH`. The shell needs no marker: it is restarted unconditionally after every update. |
|
| `~/.local/state/omarchy/restart-*-required` | user | Optional service/app restart markers checked by `omarchy-update-restart`. The shell needs no marker: it is restarted unconditionally after every update. |
|
||||||
|
|
||||||
## Migration layout
|
## Migration layout
|
||||||
|
|
||||||
@@ -56,12 +56,7 @@ privileged work should invoke the appropriate helper or privilege prompt.
|
|||||||
Migrations must be idempotent; if one user already applied a machine-wide repair,
|
Migrations must be idempotent; if one user already applied a machine-wide repair,
|
||||||
the migration should no-op for other users.
|
the migration should no-op for other users.
|
||||||
|
|
||||||
The runner derives the migration source from root-owned `/etc/omarchy.conf`,
|
When invoked by the update, migrations inherit its cold credential state and no-update sudo wrapper. The standalone migration runner has its own security changes in the migration-boundary PR; this update change does not establish that standalone boundary. Historical migrations remain strictly ordered.
|
||||||
starts from a cold sudo timestamp, and routes migration sudo calls through
|
|
||||||
`sudo --no-update`. Historical migrations are strictly ordered and can mix
|
|
||||||
user-controlled tools or theme hooks with later privileged repairs; no-update
|
|
||||||
authentication lets those repairs run without publishing a credential a
|
|
||||||
detached earlier process could reuse.
|
|
||||||
|
|
||||||
For watchers and diagnostics, `omarchy-migrate --pending` prints pending
|
For watchers and diagnostics, `omarchy-migrate --pending` prints pending
|
||||||
migration names and exits `0` when any are pending. When no migrations are
|
migration names and exits `0` when any are pending. When no migrations are
|
||||||
@@ -149,9 +144,9 @@ omarchy-update
|
|||||||
|
|
||||||
Important behavior:
|
Important behavior:
|
||||||
|
|
||||||
- `omarchy update` derives its source tree from root-owned `/etc/omarchy.conf`, or the root-owned `/usr/share/omarchy` default when that file is absent. It does not trust inherited `OMARCHY_PATH` or caller `PATH` for the privileged phase.
|
- `omarchy update` uses the session’s `OMARCHY_PATH` and a fixed command search path for its system phases. User PATH is restored behind the sudo wrapper for hooks and mise.
|
||||||
- Mixed-trust update entrypoints start Bash in privileged mode, discard `BASH_ENV`, `ENV`, and exported-function records before launching helpers, and reject an ordinary `bash path/to/command` invocation. Run them as executables (normally through the `omarchy` CLI); `/usr/bin/bash -p path/to/command` is the explicit interpreter form. This keeps shell startup injection from replacing the no-update sudo boundary.
|
- Mixed-trust update entrypoints start Bash in privileged mode, discard `BASH_ENV`, `ENV`, and exported-function records before launching helpers, and reject an ordinary `bash path/to/command` invocation. Run them as executables (normally through the `omarchy` CLI); `/usr/bin/bash -p path/to/command` is the explicit interpreter form. This keeps shell startup injection from replacing the no-update sudo boundary.
|
||||||
- In dev-link mode, the root-owned configuration is the explicit authorization for the user-writable checkout. `omarchy update` fast-forwards that authorized checkout from its configured upstream before changing system packages or running migrations.
|
- In dev-link mode, `omarchy update` fast-forwards the active checkout from its configured upstream before changing system packages or running migrations.
|
||||||
- Migrations remain in chronological order even though historical entries mix user-controlled code with later privileged repairs. Before entering that mixed-trust tail, Omarchy invalidates its timestamp and forces every later sudo call—including AUR's configurable sudo command—to use `--no-update`; prompts authorize one command without publishing a reusable timestamp. Yay's credential loop is disabled for the update.
|
- Migrations remain in chronological order even though historical entries mix user-controlled code with later privileged repairs. Before entering that mixed-trust tail, Omarchy invalidates its timestamp and forces every later sudo call—including AUR's configurable sudo command—to use `--no-update`; prompts authorize one command without publishing a reusable timestamp. Yay's credential loop is disabled for the update.
|
||||||
- User-controlled post-update hooks and mise tools run only after every sudo-capable update stage. Omarchy invalidates its sudo timestamp before each boundary and on every exit; detached children therefore have no later reusable update authorization to wait for.
|
- User-controlled post-update hooks and mise tools run only after every sudo-capable update stage. Omarchy invalidates its sudo timestamp before each boundary and on every exit; detached children therefore have no later reusable update authorization to wait for.
|
||||||
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` — a promise not to ask anything.
|
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` — a promise not to ask anything.
|
||||||
@@ -292,7 +287,8 @@ scripts.
|
|||||||
| `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. |
|
| `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. |
|
||||||
| `omarchy-update-dev` | Fast-forwards the active dev-linked checkout from its configured upstream; no-ops for package-backed installs. | **Keep.** Runs before package updates so a checkout conflict stops the update before system mutation. |
|
| `omarchy-update-dev` | Fast-forwards the active dev-linked checkout from its configured upstream; no-ops for package-backed installs. | **Keep.** Runs before package updates so a checkout conflict stops the update before system mutation. |
|
||||||
| `omarchy-update-keyring` | Ensures Omarchy keyring and Arch keyring are current before the main transaction. | **Keep, but review.** It uses targeted `pacman -Sy` for keyring bootstrapping; acceptable for this special case but should remain tightly scoped. |
|
| `omarchy-update-keyring` | Ensures Omarchy keyring and Arch keyring are current before the main transaction. | **Keep, but review.** It uses targeted `pacman -Sy` for keyring bootstrapping; acceptable for this special case but should remain tightly scoped. |
|
||||||
| `omarchy-update-system-pkgs` | Runs the ordinary guarded `pacman -Syu --noconfirm`. Package-vs-package conflicts may be retried interactively with `pacman -Su`; filesystem conflicts fail closed without moving, restoring, quarantining, or broadly overwriting live paths. The production Quattro transition performs the sole explicit settings-package takeover with `--overwrite='*'`; all later production and developer package transactions obey Pacman's ownership checks. | **Keep.** Small leaf command with no generic privileged conflict handler. |
|
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with `--overwrite '/usr/share/omarchy/*'`, capturing stderr to a report file; on failure it execs `omarchy-update-system-pkgs-when-conflicted`. | **Keep for now.** Small leaf command, clear/testable. |
|
||||||
|
| `omarchy-update-system-pkgs-when-conflicted` | Hidden conflict handler: quarantines unowned conflicting files under `/var/lib/omarchy/replaced`, retries the upgrade once, restores files the upgrade didn't claim, and hands package-vs-package conflicts to an interactive pacman run (never under `-y`). | **Keep internal/hidden.** Keeps conflict recovery out of the happy path. |
|
||||||
| `omarchy-update-pkg-prune` | Trims the pacman cache to two versions per package (`paccache -rk2`) before the snapshot, keeping the offline downgrade path while capping snapshot growth. | **Keep internal/hidden.** |
|
| `omarchy-update-pkg-prune` | Trims the pacman cache to two versions per package (`paccache -rk2`) before the snapshot, keeping the offline downgrade path while capping snapshot growth. | **Keep internal/hidden.** |
|
||||||
| `omarchy-update-requires-free-space` | Aborts the update below a 10 GiB free-space threshold on `/`; silently skipped when free space cannot be determined; `OMARCHY_UPDATE_FORCE=1` bypasses. | **Keep internal/hidden.** |
|
| `omarchy-update-requires-free-space` | Aborts the update below a 10 GiB free-space threshold on `/`; silently skipped when free space cannot be determined; `OMARCHY_UPDATE_FORCE=1` bypasses. | **Keep internal/hidden.** |
|
||||||
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
|
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
|
||||||
@@ -304,7 +300,7 @@ scripts.
|
|||||||
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
|
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
|
||||||
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
|
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
|
||||||
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
|
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
|
||||||
| `omarchy-update-restart` | Restarts allowlisted components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. |
|
| `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. |
|
||||||
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
|
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
|
||||||
| `omarchy-update-time` | Restarts `systemd-timesyncd`. | **Question.** Not really an update command. Consider renaming/moving under system/time maintenance. |
|
| `omarchy-update-time` | Restarts `systemd-timesyncd`. | **Question.** Not really an update command. Consider renaming/moving under system/time maintenance. |
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ assert_log_line() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_channel stable
|
run_channel stable
|
||||||
assert_log_line $'refresh\tstable' "stable refreshes the stable pacman channel"
|
assert_log_line $'refresh\tstable\tdefer-hook' "stable refreshes the stable pacman channel"
|
||||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
||||||
assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt"
|
assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt"
|
||||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "stable runs the normal update pipeline from the package-backed path"
|
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "stable runs the normal update pipeline from the package-backed path"
|
||||||
@@ -115,13 +115,13 @@ fi
|
|||||||
pass "stable does not require reboot when already package-backed"
|
pass "stable does not require reboot when already package-backed"
|
||||||
|
|
||||||
run_channel rc
|
run_channel rc
|
||||||
assert_log_line $'refresh\trc' "rc refreshes the rc pacman channel"
|
assert_log_line $'refresh\trc\tdefer-hook' "rc refreshes the rc pacman channel"
|
||||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
||||||
assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt"
|
assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt"
|
||||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "rc runs the normal update pipeline from the package-backed path"
|
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "rc runs the normal update pipeline from the package-backed path"
|
||||||
|
|
||||||
OMARCHY_TEST_PATH="$ROOT" run_channel edge
|
OMARCHY_TEST_PATH="$ROOT" run_channel edge
|
||||||
assert_log_line $'refresh\tedge' "edge refreshes the edge pacman channel"
|
assert_log_line $'refresh\tedge\tdefer-hook' "edge refreshes the edge pacman channel"
|
||||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
||||||
assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt"
|
assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt"
|
||||||
assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev"
|
assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev"
|
||||||
@@ -137,7 +137,7 @@ if run_channel dev >"$test_tmp/occupied.out" 2>"$test_tmp/occupied.err"; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
grep -q "already exists and is not a git checkout" "$test_tmp/occupied.err" || fail "dev explains occupied checkout paths" "$(cat "$test_tmp/occupied.err")"
|
grep -q "already exists and is not a git checkout" "$test_tmp/occupied.err" || fail "dev explains occupied checkout paths" "$(cat "$test_tmp/occupied.err")"
|
||||||
if grep -Fx $'refresh\tedge' "$log_file" >/dev/null; then
|
if grep -Fx $'refresh\tedge\tdefer-hook' "$log_file" >/dev/null; then
|
||||||
fail "dev validates checkout path before changing packages" "$(cat "$log_file")"
|
fail "dev validates checkout path before changing packages" "$(cat "$log_file")"
|
||||||
fi
|
fi
|
||||||
pass "dev refuses occupied non-checkout paths before package changes"
|
pass "dev refuses occupied non-checkout paths before package changes"
|
||||||
@@ -145,15 +145,17 @@ pass "dev refuses occupied non-checkout paths before package changes"
|
|||||||
rmdir "$checkout"
|
rmdir "$checkout"
|
||||||
run_channel dev
|
run_channel dev
|
||||||
assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation"
|
assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation"
|
||||||
assert_log_line $'refresh\tedge' "dev refreshes the edge pacman channel"
|
assert_log_line $'refresh\tedge\tdefer-hook' "dev refreshes the edge pacman channel"
|
||||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||||
assert_log_line $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
|
assert_log_line $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
|
||||||
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt"
|
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt"
|
||||||
assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline"
|
assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline"
|
||||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout"
|
assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout"
|
||||||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file") == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\nsudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file" | sed '/run-deferred/d') == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\tdefer-hook\nsudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
||||||
fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")"
|
fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")"
|
||||||
pass "dev activates the checkout before changing or updating packages"
|
pass "dev activates the checkout before changing or updating packages"
|
||||||
|
[[ $(tail -1 "$log_file") == $'refresh\tedge\trun-deferred' ]] || fail "channel refresh hook must run after the complete update"
|
||||||
|
pass "channel changes defer the refresh hook until all update work finishes"
|
||||||
|
|
||||||
OMARCHY_TEST_PATH="$checkout" run_channel stable
|
OMARCHY_TEST_PATH="$checkout" run_channel stable
|
||||||
assert_log_line $'unlink\t--no-reboot' "switching from dev to stable unlinks without an early reboot prompt"
|
assert_log_line $'unlink\t--no-reboot' "switching from dev to stable unlinks without an early reboot prompt"
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test the real orchestration with fixed privileged paths redirected to harmless
|
||||||
|
# stand-ins. No host sudo, package transaction, namespace root, or exploit runs.
|
||||||
|
boundary_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$boundary_tmp"' EXIT
|
||||||
|
export SUDO_TEST_ROOT="$boundary_tmp/omarchy"
|
||||||
|
export SUDO_TEST_LOG="$boundary_tmp/events"
|
||||||
|
export SUDO_TEST_CACHE="$boundary_tmp/cache"
|
||||||
|
export OMARCHY_PATH="$SUDO_TEST_ROOT"
|
||||||
|
export SUDO_TEST_HOME="$boundary_tmp/home"
|
||||||
|
mkdir -p "$SUDO_TEST_HOME"
|
||||||
|
mkdir -p "$SUDO_TEST_ROOT/bin" "$SUDO_TEST_ROOT/mock" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update"
|
||||||
|
: >"$SUDO_TEST_LOG"
|
||||||
|
|
||||||
|
copy_boundary_file() {
|
||||||
|
python3 - "$ROOT" "$SUDO_TEST_ROOT" "$1" <<'PY'
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
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']:
|
||||||
|
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)
|
||||||
|
p.chmod((source/name).stat().st_mode & 0o777)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_boundary_file bin/omarchy-security-functions
|
||||||
|
copy_boundary_file default/omarchy/sudo-no-update/sudo
|
||||||
|
|
||||||
|
cat >"$SUDO_TEST_ROOT/mock/sudo" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
printf 'sudo' >>"$SUDO_TEST_LOG"
|
||||||
|
printf ' %q' "$@" >>"$SUDO_TEST_LOG"
|
||||||
|
printf '\n' >>"$SUDO_TEST_LOG"
|
||||||
|
if [[ ${1:-} == "-h" ]]; then
|
||||||
|
if [[ ${SUDO_TEST_UNSUPPORTED:-0} == "1" ]]; then
|
||||||
|
echo 'usage: sudo [-ABbEHknPS] command'
|
||||||
|
else
|
||||||
|
echo 'usage: sudo [-ABbEHkNnPS] command'
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ ${1:-} == "-k" || ${1:-} == "-K" ]]; then
|
||||||
|
[[ ${SUDO_TEST_REVOKE_FAIL:-0} != "1" ]] || exit 1
|
||||||
|
rm -f "$SUDO_TEST_CACHE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ ${1:-} == "-N" ]]; then
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
touch "$SUDO_TEST_CACHE"
|
||||||
|
fi
|
||||||
|
[[ ${SUDO_TEST_SUDO_FAIL:-0} != "1" ]] || exit 1
|
||||||
|
background=0
|
||||||
|
while (( $# )); do
|
||||||
|
case "$1" in
|
||||||
|
-N|-n) shift ;;
|
||||||
|
-b) background=1; shift ;;
|
||||||
|
-v) exit 0 ;;
|
||||||
|
-u|--user) shift 2 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
(( $# )) || exit 0
|
||||||
|
if (( background )); then
|
||||||
|
"$@" &
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
STUB
|
||||||
|
chmod +x "$SUDO_TEST_ROOT/mock/sudo"
|
||||||
|
|
||||||
|
cat >"$SUDO_TEST_ROOT/bin/test-step" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
step=${0##*/}
|
||||||
|
printf 'step:%s %s\n' "$step" "$*" >>"$SUDO_TEST_LOG"
|
||||||
|
if [[ $step == "omarchy-hook" || $step == "omarchy-update-mise" ]]; then
|
||||||
|
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
|
||||||
|
fi
|
||||||
|
if [[ ${SUDO_TEST_FAIL_STEP:-} == "$step" ]]; then
|
||||||
|
# Model a misbehaving child leaving state behind, then failing. Cleanup must
|
||||||
|
# still revoke it. This never invokes real sudo or exercises a privilege flaw.
|
||||||
|
touch "$SUDO_TEST_CACHE"
|
||||||
|
exit 17
|
||||||
|
fi
|
||||||
|
if [[ ${SUDO_TEST_SIGNAL_STEP:-} == "$step" ]]; then
|
||||||
|
touch "$SUDO_TEST_CACHE"
|
||||||
|
kill -TERM "$PPID"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
case "$step" in
|
||||||
|
omarchy-update-system-pkgs|omarchy-update-keyring|omarchy-snapshot)
|
||||||
|
sudo /usr/bin/true
|
||||||
|
;;
|
||||||
|
pacman) exit 0 ;;
|
||||||
|
yay)
|
||||||
|
[[ $* == *"--sudo $OMARCHY_PATH/default/omarchy/sudo-no-update/sudo"* ]] || exit 92
|
||||||
|
[[ $* == *"--nosudoloop"* ]] || exit 93
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
STUB
|
||||||
|
chmod +x "$SUDO_TEST_ROOT/bin/test-step"
|
||||||
|
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman cp yay; do
|
||||||
|
ln -s test-step "$SUDO_TEST_ROOT/bin/$step"
|
||||||
|
done
|
||||||
|
ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
|
||||||
|
|
||||||
|
reset_boundary() {
|
||||||
|
: >"$SUDO_TEST_LOG"
|
||||||
|
rm -f "$SUDO_TEST_CACHE"
|
||||||
|
unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED
|
||||||
|
}
|
||||||
|
assert_boundary_cold() {
|
||||||
|
[[ ! -e $SUDO_TEST_CACHE ]] || fail "$1 left cached authorization"
|
||||||
|
[[ $(tail -1 "$SUDO_TEST_LOG") == "sudo -k" ]] || fail "$1 did not revoke at exit" "$(<"$SUDO_TEST_LOG")"
|
||||||
|
}
|
||||||
Regular → Executable
+95
-1018
File diff suppressed because it is too large.
Load diff
@@ -4,16 +4,31 @@ set -euo pipefail
|
|||||||
|
|
||||||
source "$(dirname "$0")/base-test.sh"
|
source "$(dirname "$0")/base-test.sh"
|
||||||
|
|
||||||
test_tmp=$(mktemp -d)
|
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||||
trap 'rm -rf "$test_tmp"' EXIT
|
test_tmp="$boundary_tmp"
|
||||||
|
stub_bin="$SUDO_TEST_ROOT/bin"
|
||||||
stub_bin="$test_tmp/bin"
|
test_home="$SUDO_TEST_HOME"
|
||||||
test_home="$test_tmp/home"
|
|
||||||
runtime_dir="$test_tmp/runtime"
|
runtime_dir="$test_tmp/runtime"
|
||||||
mkdir -p "$stub_bin" "$test_home" "$runtime_dir"
|
mkdir -p "$runtime_dir"
|
||||||
|
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
|
||||||
|
cat >"$SUDO_TEST_ROOT/mock/setpriv" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
while [[ ${1:-} == --* ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--reuid|--regid) shift 2 ;;
|
||||||
|
--clear-groups) shift ;;
|
||||||
|
*) exit 90 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec "$@"
|
||||||
|
STUB
|
||||||
|
chmod +x "$SUDO_TEST_ROOT/mock/setpriv"
|
||||||
|
|
||||||
run_with_lock_env() {
|
run_with_lock_env() {
|
||||||
HOME="$test_home" \
|
SUDO_TEST_HOME="$test_home" \
|
||||||
XDG_RUNTIME_DIR="$runtime_dir" \
|
XDG_RUNTIME_DIR="$runtime_dir" \
|
||||||
XDG_STATE_HOME="$test_tmp/state" \
|
XDG_STATE_HOME="$test_tmp/state" \
|
||||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||||
@@ -24,6 +39,7 @@ write_stub() {
|
|||||||
local name="$1"
|
local name="$1"
|
||||||
local body="$2"
|
local body="$2"
|
||||||
|
|
||||||
|
rm -f "$stub_bin/$name"
|
||||||
cat >"$stub_bin/$name" <<SH
|
cat >"$stub_bin/$name" <<SH
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
$body
|
$body
|
||||||
@@ -51,13 +67,15 @@ for command in \
|
|||||||
done
|
done
|
||||||
write_stub omarchy-update-available 'exit 1'
|
write_stub omarchy-update-available 'exit 1'
|
||||||
write_stub pkexec 'exec "$@"'
|
write_stub pkexec 'exec "$@"'
|
||||||
|
write_stub systemd-inhibit 'while [[ $1 == --* ]]; do shift; done; exec "$@"'
|
||||||
|
ln -s ../bin/systemd-inhibit "$SUDO_TEST_ROOT/mock/systemd-inhibit"
|
||||||
|
|
||||||
# omarchy-update should hold the lock before snapshotting, so a second update
|
# omarchy-update should hold the lock before snapshotting, so a second update
|
||||||
# cannot even enter its pre-update snapshot.
|
# cannot even enter its pre-update snapshot.
|
||||||
update_snapshot_marker="$test_tmp/update-snapshot-started"
|
update_snapshot_marker="$test_tmp/update-snapshot-started"
|
||||||
write_stub omarchy-snapshot 'echo started >"$TEST_MARKER"; sleep 2; exit 0'
|
write_stub omarchy-snapshot 'echo started >"$TEST_MARKER"; sleep 2; exit 0'
|
||||||
|
|
||||||
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$update_snapshot_marker" run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-first.out" 2>&1 &
|
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$update_snapshot_marker" run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update" -y >"$test_tmp/update-first.out" 2>&1 &
|
||||||
update_pid=$!
|
update_pid=$!
|
||||||
|
|
||||||
for _ in {1..50}; do
|
for _ in {1..50}; do
|
||||||
@@ -67,7 +85,7 @@ done
|
|||||||
[[ -f $update_snapshot_marker ]] || fail "first omarchy-update reached snapshot under lock"
|
[[ -f $update_snapshot_marker ]] || fail "first omarchy-update reached snapshot under lock"
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$test_tmp/update-second-snapshot-started" run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-second.out" 2>&1
|
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$test_tmp/update-second-snapshot-started" run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update" -y >"$test_tmp/update-second.out" 2>&1
|
||||||
update_second_status=$?
|
update_second_status=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -85,11 +103,11 @@ pass "omarchy-update prevents overlapping top-level updates"
|
|||||||
inhibit_pid_file="$test_tmp/inhibit-pid"
|
inhibit_pid_file="$test_tmp/inhibit-pid"
|
||||||
keyring_marker="$test_tmp/keyring-started"
|
keyring_marker="$test_tmp/keyring-started"
|
||||||
write_stub omarchy-snapshot 'exit 0'
|
write_stub omarchy-snapshot 'exit 0'
|
||||||
write_stub systemd-inhibit 'echo "$$" >"$INHIBIT_PID_FILE"; exec sleep 30'
|
write_stub systemd-inhibit '[[ -z ${INHIBIT_PID_FILE:-} ]] || echo "$$" >"$INHIBIT_PID_FILE"; while [[ $1 == --* ]]; do shift; done; exec "$@"'
|
||||||
write_stub omarchy-update-keyring 'echo started >"$TEST_MARKER"; sleep 3; exit 0'
|
write_stub omarchy-update-keyring 'echo started >"$TEST_MARKER"; sleep 3; exit 0'
|
||||||
|
|
||||||
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$keyring_marker" INHIBIT_PID_FILE="$inhibit_pid_file" \
|
OMARCHY_UPDATE_LOGGED=1 TEST_MARKER="$keyring_marker" INHIBIT_PID_FILE="$inhibit_pid_file" \
|
||||||
run_with_lock_env "$ROOT/bin/omarchy-update" -y >"$test_tmp/update-inhibit.out" 2>&1 &
|
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update" -y >"$test_tmp/update-inhibit.out" 2>&1 &
|
||||||
inhibit_update_pid=$!
|
inhibit_update_pid=$!
|
||||||
|
|
||||||
for _ in {1..100}; do
|
for _ in {1..100}; do
|
||||||
@@ -123,11 +141,10 @@ if (( EUID != 0 )); then
|
|||||||
terminal_inhibit_pid_file="$test_tmp/terminal-inhibit-pid"
|
terminal_inhibit_pid_file="$test_tmp/terminal-inhibit-pid"
|
||||||
write_stub sudo '
|
write_stub sudo '
|
||||||
printf "%s\n" "$*" >>"$SUDO_LOG"
|
printf "%s\n" "$*" >>"$SUDO_LOG"
|
||||||
if [[ $1 == "-v" ]]; then
|
[[ $1 == "-N" && $2 == "-b" && $3 == "--" ]] || exit 90
|
||||||
exit 0
|
shift 3
|
||||||
fi
|
"$@" &'
|
||||||
exec "$@"'
|
write_stub pkexec '[[ -z ${PKEXEC_MARKER:-} ]] || touch "$PKEXEC_MARKER"; exec "$@"'
|
||||||
write_stub pkexec 'touch "$PKEXEC_MARKER"; exec "$@"'
|
|
||||||
|
|
||||||
# start leaves the inhibitor running on purpose, but script tears the pty down
|
# start leaves the inhibitor running on purpose, but script tears the pty down
|
||||||
# the moment its command returns, which SIGHUPs that inhibitor before it can
|
# the moment its command returns, which SIGHUPs that inhibitor before it can
|
||||||
@@ -137,7 +154,7 @@ exec "$@"'
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
omarchy-update-stay-awake start
|
omarchy-update-stay-awake start
|
||||||
for _ in {1..200}; do
|
for _ in {1..200}; do
|
||||||
grep -q '^systemd-inhibit ' "$SUDO_LOG" && break
|
grep -q -- '^-N -b -- ' "$SUDO_LOG" && break
|
||||||
sleep 0.05
|
sleep 0.05
|
||||||
done
|
done
|
||||||
SH
|
SH
|
||||||
@@ -146,10 +163,10 @@ SH
|
|||||||
SUDO_LOG="$sudo_log" PKEXEC_MARKER="$pkexec_marker" INHIBIT_PID_FILE="$terminal_inhibit_pid_file" \
|
SUDO_LOG="$sudo_log" PKEXEC_MARKER="$pkexec_marker" INHIBIT_PID_FILE="$terminal_inhibit_pid_file" \
|
||||||
run_with_lock_env script -qefc "$terminal_driver" /dev/null >/dev/null
|
run_with_lock_env script -qefc "$terminal_driver" /dev/null >/dev/null
|
||||||
|
|
||||||
grep -qx -- '-v' "$sudo_log" || fail "terminal sleep inhibition validates sudo in the foreground"
|
grep -q -- '^-N -b -- ' "$sudo_log" || fail "terminal inhibition authenticates its background command without a reusable timestamp"
|
||||||
grep -q '^systemd-inhibit ' "$sudo_log" || fail "terminal sleep inhibition runs through sudo"
|
grep -q -- '^-N -b -- ' "$sudo_log" || fail "terminal sleep inhibition runs through sudo"
|
||||||
[[ ! -e $pkexec_marker ]] || fail "terminal sleep inhibition does not use pkexec"
|
[[ ! -e $pkexec_marker ]] || fail "terminal sleep inhibition does not use pkexec"
|
||||||
run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop
|
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
|
||||||
pass "terminal updates use sudo instead of Polkit for sleep inhibition"
|
pass "terminal updates use sudo instead of Polkit for sleep inhibition"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -158,7 +175,7 @@ fi
|
|||||||
write_stub omarchy-snapshot 'exit 0'
|
write_stub omarchy-snapshot 'exit 0'
|
||||||
write_stub omarchy-update-keyring 'exit 0'
|
write_stub omarchy-update-keyring 'exit 0'
|
||||||
write_stub omarchy-toggle-idle '
|
write_stub omarchy-toggle-idle '
|
||||||
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
|
state_file="$SUDO_TEST_HOME/.local/state/omarchy/indicators/stay-awake"
|
||||||
case "$1" in
|
case "$1" in
|
||||||
stay-awake)
|
stay-awake)
|
||||||
mkdir -p "$(dirname "$state_file")"
|
mkdir -p "$(dirname "$state_file")"
|
||||||
@@ -169,20 +186,20 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
esac'
|
esac'
|
||||||
write_stub omarchy-update-restart '
|
write_stub omarchy-update-restart '
|
||||||
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
|
state_file="$SUDO_TEST_HOME/.local/state/omarchy/indicators/stay-awake"
|
||||||
if [[ ${EXPECT_STAY_AWAKE:-0} == "1" ]]; then
|
if [[ ${1:-} == "--services-only" || ${EXPECT_STAY_AWAKE:-0} == "1" ]]; then
|
||||||
[[ -f $state_file ]]
|
[[ -f $state_file ]]
|
||||||
else
|
else
|
||||||
[[ ! -f $state_file ]]
|
[[ ! -f $state_file ]]
|
||||||
fi'
|
fi'
|
||||||
|
|
||||||
rm -f "$test_home/.local/state/omarchy/indicators/stay-awake"
|
rm -f "$test_home/.local/state/omarchy/indicators/stay-awake"
|
||||||
OMARCHY_UPDATE_LOGGED=1 run_with_lock_env "$ROOT/bin/omarchy-update" -y
|
OMARCHY_UPDATE_LOGGED=1 run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update" -y
|
||||||
[[ ! -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update clears its Stay Awake state before restart handling"
|
[[ ! -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update clears its Stay Awake state before restart handling"
|
||||||
|
|
||||||
mkdir -p "$test_home/.local/state/omarchy/indicators"
|
mkdir -p "$test_home/.local/state/omarchy/indicators"
|
||||||
touch "$test_home/.local/state/omarchy/indicators/stay-awake"
|
touch "$test_home/.local/state/omarchy/indicators/stay-awake"
|
||||||
OMARCHY_UPDATE_LOGGED=1 EXPECT_STAY_AWAKE=1 run_with_lock_env "$ROOT/bin/omarchy-update" -y
|
OMARCHY_UPDATE_LOGGED=1 EXPECT_STAY_AWAKE=1 run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update" -y
|
||||||
[[ -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update preserves pre-existing Stay Awake state"
|
[[ -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"
|
pass "omarchy-update restores only its own Stay Awake state before restart handling"
|
||||||
|
|
||||||
@@ -194,7 +211,7 @@ mkdir -p "$stay_awake_helper_state" "$(dirname "$stay_awake_state")"
|
|||||||
printf '%s\n' "old-update-owner" >"$stay_awake_helper_state/idle-owner"
|
printf '%s\n' "old-update-owner" >"$stay_awake_helper_state/idle-owner"
|
||||||
printf '%s\n' "user-choice" >"$stay_awake_state"
|
printf '%s\n' "user-choice" >"$stay_awake_state"
|
||||||
|
|
||||||
run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop
|
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
|
||||||
[[ $(<"$stay_awake_state") == "user-choice" ]] ||
|
[[ $(<"$stay_awake_state") == "user-choice" ]] ||
|
||||||
fail "stale update ownership does not remove a newer Stay Awake choice"
|
fail "stale update ownership does not remove a newer Stay Awake choice"
|
||||||
pass "stale update ownership preserves a newer Stay Awake choice"
|
pass "stale update ownership preserves a newer Stay Awake choice"
|
||||||
@@ -206,7 +223,7 @@ unrelated_start_time=$(awk '{ print $22 }' "/proc/$unrelated_pid/stat")
|
|||||||
mkdir -p "$stay_awake_helper_state"
|
mkdir -p "$stay_awake_helper_state"
|
||||||
printf '%s %s\n' "$unrelated_pid" "$((unrelated_start_time + 1))" >"$stay_awake_helper_state/inhibit-pid"
|
printf '%s %s\n' "$unrelated_pid" "$((unrelated_start_time + 1))" >"$stay_awake_helper_state/inhibit-pid"
|
||||||
|
|
||||||
run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop
|
run_with_lock_env "$SUDO_TEST_ROOT/bin/omarchy-update-stay-awake" stop
|
||||||
kill -0 "$unrelated_pid" 2>/dev/null ||
|
kill -0 "$unrelated_pid" 2>/dev/null ||
|
||||||
fail "stale inhibitor state does not terminate a reused PID"
|
fail "stale inhibitor state does not terminate a reused PID"
|
||||||
kill "$unrelated_pid"
|
kill "$unrelated_pid"
|
||||||
|
|||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||||
|
rm "$SUDO_TEST_ROOT/bin/omarchy-update-restart"
|
||||||
|
copy_boundary_file bin/omarchy-update-restart
|
||||||
|
for step in omarchy-state omarchy-restart-sshd omarchy-restart-shell omarchy-system-reboot; do
|
||||||
|
ln -s test-step "$SUDO_TEST_ROOT/bin/$step"
|
||||||
|
done
|
||||||
|
cat >"$SUDO_TEST_ROOT/bin/gum" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf 'prompt:%s\n' "$*" >>"$SUDO_TEST_LOG"
|
||||||
|
exit 1
|
||||||
|
STUB
|
||||||
|
chmod +x "$SUDO_TEST_ROOT/bin/gum"
|
||||||
|
mkdir -p "$SUDO_TEST_HOME/.local/state/omarchy"
|
||||||
|
touch "$SUDO_TEST_HOME/.local/state/omarchy/reboot-required" "$SUDO_TEST_HOME/.local/state/omarchy/restart-sshd-required"
|
||||||
|
|
||||||
|
for mode in --services-only --reboot-only; do
|
||||||
|
reset_boundary
|
||||||
|
PATH="$SUDO_TEST_ROOT/bin:$PATH" "$SUDO_TEST_ROOT/bin/omarchy-update-restart" "$mode" >"$boundary_tmp/output" 2>&1
|
||||||
|
if [[ $mode == "--services-only" ]]; then
|
||||||
|
grep -q '^step:omarchy-restart-sshd ' "$SUDO_TEST_LOG" || fail "service phase did not restart a marked service"
|
||||||
|
grep -q '^step:omarchy-restart-shell ' "$SUDO_TEST_LOG" || fail "service phase did not restart the shell"
|
||||||
|
if grep -q '^prompt:' "$SUDO_TEST_LOG"; then fail "service phase offered a reboot before update cleanup"; fi
|
||||||
|
else
|
||||||
|
grep -q '^prompt:' "$SUDO_TEST_LOG" || fail "reboot phase did not offer the required reboot"
|
||||||
|
if grep -q '^step:omarchy-restart-' "$SUDO_TEST_LOG"; then fail "reboot phase performed later service work"; fi
|
||||||
|
fi
|
||||||
|
pass "restart $mode performs only its selected phase"
|
||||||
|
done
|
||||||
|
reset_boundary
|
||||||
|
OMARCHY_UPDATE_UNATTENDED=1 PATH="$SUDO_TEST_ROOT/bin:$PATH" "$SUDO_TEST_ROOT/bin/omarchy-update-restart" --reboot-only >"$boundary_tmp/output" 2>&1
|
||||||
|
if grep -Eq "^(prompt:|step:omarchy-restart-|step:omarchy-system-reboot)" "$SUDO_TEST_LOG"; then
|
||||||
|
fail "unattended reboot phase prompted or performed service work"
|
||||||
|
fi
|
||||||
|
pass "unattended reboot phase reports a required reboot without prompting"
|
||||||
@@ -2,60 +2,11 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
source "$(dirname "$0")/base-test.sh"
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
|
||||||
if [[ -z ${OMARCHY_UPDATE_SEQUENCE_NS:-} ]]; then
|
copy_boundary_file bin/omarchy-update
|
||||||
outer_uid=$(id -u)
|
test_tmp="$boundary_tmp"
|
||||||
outer_gid=$(id -g)
|
stub_bin="$SUDO_TEST_ROOT/bin"
|
||||||
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 authorized update-sequence test"
|
|
||||||
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_SEQUENCE_NS=setup bash "$0"
|
|
||||||
elif [[ $OMARCHY_UPDATE_SEQUENCE_NS == setup ]]; then
|
|
||||||
mount -t tmpfs -o mode=0755 tmpfs /run
|
|
||||||
namespace_tmp=$(mktemp -d -p /run omarchy-update-sequence.XXXXXXXX)
|
|
||||||
chmod 0755 "$namespace_tmp"
|
|
||||||
mkdir -p "$namespace_tmp/default/omarchy/sudo-no-update"
|
|
||||||
cp "$ROOT/default/omarchy/sudo-no-update/sudo" "$namespace_tmp/default/omarchy/sudo-no-update/sudo"
|
|
||||||
chmod 0755 "$namespace_tmp/default/omarchy/sudo-no-update/sudo"
|
|
||||||
cat >"$namespace_tmp/fixed-sudo" <<'STUB'
|
|
||||||
#!/bin/bash
|
|
||||||
if [[ ${1:-} == "-h" ]]; then
|
|
||||||
echo 'usage: sudo [-ABbEHkNnPS] command'
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
STUB
|
|
||||||
chmod 0755 "$namespace_tmp/fixed-sudo"
|
|
||||||
mount --bind "$namespace_tmp/fixed-sudo" /usr/bin/sudo
|
|
||||||
mount -t tmpfs -o mode=0755 tmpfs /etc
|
|
||||||
printf 'export OMARCHY_PATH="%s"\n' "$namespace_tmp" >/etc/omarchy.conf
|
|
||||||
chmod 0644 /etc/omarchy.conf
|
|
||||||
chown -R 1000:1000 "$namespace_tmp"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
setpriv --reuid 1000 --regid 1000 --clear-groups \
|
|
||||||
env OMARCHY_UPDATE_SEQUENCE_NS=run OMARCHY_AUTHORIZED_TEST_ROOT="$namespace_tmp" bash "$0"
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
umount /usr/bin/sudo
|
|
||||||
umount /etc
|
|
||||||
rm -rf "$namespace_tmp"
|
|
||||||
umount /run
|
|
||||||
exit "$status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
test_tmp="$OMARCHY_AUTHORIZED_TEST_ROOT"
|
|
||||||
trap 'rm -rf "$test_tmp"/*' EXIT
|
|
||||||
|
|
||||||
stub_bin="$test_tmp/bin"
|
|
||||||
mkdir -p "$stub_bin"
|
|
||||||
|
|
||||||
# Every step omarchy-update runs, recorded in order with the unattended flag it
|
# Every step omarchy-update runs, recorded in order with the unattended flag it
|
||||||
# was handed. One of them can be told to fail.
|
# was handed. One of them can be told to fail.
|
||||||
@@ -80,6 +31,7 @@ steps=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for step in "${steps[@]}"; do
|
for step in "${steps[@]}"; do
|
||||||
|
rm -f "$stub_bin/$step"
|
||||||
cat >"$stub_bin/$step" <<'STUB'
|
cat >"$stub_bin/$step" <<'STUB'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
printf '%s unattended=%s\n' "${0##*/}" "${OMARCHY_UPDATE_UNATTENDED:-}" >>"$STEP_LOG"
|
printf '%s unattended=%s\n' "${0##*/}" "${OMARCHY_UPDATE_UNATTENDED:-}" >>"$STEP_LOG"
|
||||||
@@ -96,7 +48,7 @@ run_update() {
|
|||||||
FAILING_STEP="${FAILING_STEP:-}" \
|
FAILING_STEP="${FAILING_STEP:-}" \
|
||||||
OMARCHY_UPDATE_LOGGED=1 \
|
OMARCHY_UPDATE_LOGGED=1 \
|
||||||
PATH="$stub_bin:$PATH" \
|
PATH="$stub_bin:$PATH" \
|
||||||
"$ROOT/bin/omarchy-update" "$@" >"$test_tmp/out" 2>"$test_tmp/err"
|
"$SUDO_TEST_ROOT/bin/omarchy-update" "$@" >"$test_tmp/out" 2>"$test_tmp/err"
|
||||||
}
|
}
|
||||||
|
|
||||||
steps_run() {
|
steps_run() {
|
||||||
|
|||||||
Reference in new issue
Block a user