294 lines
10 KiB
Bash
Executable File
294 lines
10 KiB
Bash
Executable File
#!/bin/bash -p
|
|
|
|
# omarchy:summary=Update Omarchy and system packages
|
|
# omarchy:args=[-y]
|
|
# omarchy:examples=omarchy update | omarchy update -y
|
|
# omarchy:requires-sudo=true
|
|
|
|
if [[ $- != *p* ]]; then
|
|
echo "Refusing an unsafe Bash startup for the Omarchy update." >&2
|
|
exit 126
|
|
fi
|
|
|
|
require_privileged_bash_startup() {
|
|
[[ $- == *p* ]] || return 1
|
|
/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
|
|
|
|
# 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
|
|
|
|
# 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() {
|
|
local status=$?
|
|
|
|
trap - EXIT
|
|
if (( update_stay_awake_stopped == 0 )); then
|
|
omarchy-update-stay-awake stop || true
|
|
fi
|
|
/usr/bin/sudo -k || true
|
|
exit "$status"
|
|
}
|
|
|
|
if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
|
|
script_command=$(printf '%q ' "$0" "$@")
|
|
exec env OMARCHY_UPDATE_LOGGED=1 script -qefc "$script_command" "/tmp/omarchy-update.log"
|
|
fi
|
|
|
|
if ! omarchy-update-lock held; then
|
|
exec omarchy-update-lock run "$0" "$@"
|
|
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 cleanup_update EXIT
|
|
|
|
omarchy-update-requires-free-space
|
|
|
|
# -y is a promise not to ask anything. Steps that would need an answer report
|
|
# and move on instead of waiting on a prompt nobody is here to give.
|
|
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
|
|
|
|
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
|
|
# after it frees nothing until that snapshot ages out.
|
|
omarchy-update-pkg-prune
|
|
|
|
# 127 means Snapper is deliberately absent. Any other failure already said
|
|
# what went wrong, and a missing snapshot is not worth blocking an update
|
|
# over, but it must not pass for one either.
|
|
omarchy-snapshot create || (($? == 127)) ||
|
|
echo -e "\e[33mContinuing the update without a snapshot.\e[0m" >&2
|
|
|
|
omarchy-update-stay-awake start
|
|
|
|
# A dev link explicitly authorizes its checkout through root-owned system
|
|
# configuration (including sudo's secure_path), so preserve the established
|
|
# pull-before-packages/migrations ordering for that trusted mode.
|
|
omarchy-update-dev
|
|
omarchy-update-keyring
|
|
|
|
# Migrations ship with the packages installed here and are written against
|
|
# them, so everything below waits on this finishing. An upgrade that stopped
|
|
# takes the update with it rather than migrating against what is still on disk.
|
|
omarchy-update-system-pkgs
|
|
|
|
# Historical migrations are strictly ordered and mix user hooks/downloaded
|
|
# tooling with privileged repairs. The no-update sudo wrapper has covered the
|
|
# whole update, so neither the package transaction nor a later repair can
|
|
# publish a timestamp to a detached migration child.
|
|
/usr/bin/sudo -k
|
|
omarchy-migrate
|
|
omarchy-update-orphan-pkgs
|
|
|
|
omarchy-update-analyze-logs
|
|
omarchy-update-status
|
|
|
|
# Service restart helpers can need sudo. Run them before any user-controlled
|
|
# update tooling; the reboot-only phase below performs no privileged work.
|
|
omarchy-update-restart --services-only
|
|
|
|
# Release update-owned inhibitors before offering a reboot. A confirmed
|
|
# reboot can terminate this process before its EXIT trap gets a chance to
|
|
# remove the persistent Stay Awake marker.
|
|
omarchy-update-stay-awake stop
|
|
update_stay_awake_stopped=1
|
|
|
|
# AUR installation can refresh sudo after running package build code. No
|
|
# privileged update stage may follow it: user-controlled code can outlive
|
|
# its parent and wait for a later timestamp even if we invalidate in between.
|
|
omarchy-update-aur-pkgs
|
|
/usr/bin/sudo -k
|
|
|
|
# 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.
|
|
# Only the unprivileged reboot prompt follows them.
|
|
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
|
|
/usr/bin/sudo -k
|
|
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
|
|
/usr/bin/sudo -k
|
|
|
|
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
|
|
fi
|