OM-SEC-14: Run update hooks without reusable sudo authority
This commit is contained in:
1 parent
b686ed892d
commit
1136a715c0
8 files changed
+1628
-41
No files matched your search
+231
-12
@@ -1,26 +1,245 @@
|
||||
#!/bin/bash
|
||||
#!/bin/bash -p
|
||||
|
||||
# omarchy:summary=Overwrite the package configuration for /etc/pacman with the Omarchy default of using its dedicated mirrors and repositories, then update all packages.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
|
||||
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
||||
if [[ $- != *p* ]]; then
|
||||
echo "Refusing an unsafe Bash startup for pacman refresh." >&2
|
||||
exit 126
|
||||
fi
|
||||
|
||||
channel="${1:-stable}"
|
||||
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 pacman refresh." >&2
|
||||
exit 126
|
||||
fi
|
||||
unset -f require_privileged_bash_startup
|
||||
|
||||
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
|
||||
echo "Error: Invalid channel '$channel'. Must be one of: stable, rc, edge"
|
||||
set -e
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
export OMARCHY_PATH
|
||||
user_path="${PATH:-/usr/bin:/bin}"
|
||||
PATH="$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
|
||||
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
|
||||
|
||||
pacman_source=$(trusted_omarchy_source_file "default/pacman/pacman-$channel.conf") || {
|
||||
echo "Refusing an untrusted pacman configuration source." >&2
|
||||
exit 1
|
||||
}
|
||||
mirror_source=$(trusted_omarchy_source_file "default/pacman/mirrorlist-$channel") || {
|
||||
echo "Refusing an untrusted pacman mirror source." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
as_root /usr/bin/cp -f -- /etc/pacman.conf /etc/pacman.conf.bak
|
||||
as_root /usr/bin/cp -f -- /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
|
||||
|
||||
echo "Setting channel to $channel"
|
||||
echo
|
||||
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
|
||||
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
|
||||
# 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"
|
||||
|
||||
# Allow user customization of /etc/pacman.conf before the upgrade runs
|
||||
omarchy-hook pre-refresh-pacman
|
||||
# Reset all package DBs and then update.
|
||||
as_root /usr/bin/env OMARCHY_UPDATE_PACMAN=1 /usr/bin/pacman -Syyuu --noconfirm
|
||||
|
||||
# Reset all package DBs and then update
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 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
|
||||
+236
-7
@@ -1,12 +1,217 @@
|
||||
#!/bin/bash
|
||||
#!/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"
|
||||
@@ -17,7 +222,7 @@ if ! omarchy-update-lock held; then
|
||||
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 'omarchy-update-stay-awake stop' EXIT
|
||||
trap cleanup_update EXIT
|
||||
|
||||
omarchy-update-requires-free-space
|
||||
|
||||
@@ -38,6 +243,9 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
||||
|
||||
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
|
||||
|
||||
@@ -45,20 +253,41 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
||||
# 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-hook post-update
|
||||
omarchy-update-aur-pkgs
|
||||
omarchy-update-mise
|
||||
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
|
||||
trap - EXIT
|
||||
update_stay_awake_stopped=1
|
||||
|
||||
omarchy-update-restart
|
||||
# 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
|
||||
@@ -1,17 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This hook is called by `omarchy refresh pacman` AFTER the channel template
|
||||
# is copied to /etc/pacman.conf and BEFORE `pacman -Syyuu` runs. Use it to
|
||||
# layer customizations onto the freshly-written pacman.conf so they're
|
||||
# respected by the upgrade — common cases are adding a custom repository
|
||||
# This legacy-named hook is called by `omarchy refresh pacman` AFTER the
|
||||
# channel template is copied and the privileged package transaction finishes.
|
||||
# During `omarchy channel set`, it is deferred until the package switch and
|
||||
# complete update have also finished.
|
||||
# Use it to layer customizations onto the freshly-written pacman.conf for
|
||||
# subsequent package operations — common cases are adding a custom repository
|
||||
# (e.g. CachyOS, Chaotic-AUR, an internal company repo) or extra IgnorePkg
|
||||
# lines.
|
||||
#
|
||||
# The hook runs as the invoking user with a warm sudo cache.
|
||||
# The hook runs as the invoking user after Omarchy invalidates its sudo cache.
|
||||
# A sudo command here therefore requires its own explicit authorization.
|
||||
#
|
||||
# To put it into use, remove .sample from this file name.
|
||||
|
||||
# Example: add an Include line above [core] for a custom repo.
|
||||
# Example: add an Include line above [core] for future package operations.
|
||||
# Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself.
|
||||
|
||||
CONF=/etc/pacman.conf
|
||||
|
||||
@@ -14,8 +14,8 @@ file first, if one exists.
|
||||
├── battery-low.d/ # Low battery (percentage in $1)
|
||||
├── font-set.d/ # After font change (font name in $1)
|
||||
├── post-boot.d/ # After the desktop starts
|
||||
├── post-update.d/ # During `omarchy update`, after system packages and migrations
|
||||
├── pre-refresh-pacman.d/ # Before `omarchy refresh pacman` re-syncs packages
|
||||
├── post-update.d/ # At the end of `omarchy update`, after privileged work
|
||||
├── pre-refresh-pacman.d/ # After `omarchy refresh pacman` finishes (legacy name)
|
||||
└── theme-set.d/ # After theme change (theme slug in $1)
|
||||
```
|
||||
|
||||
@@ -26,3 +26,5 @@ THEME_NAME=$1
|
||||
echo "Theme changed to: $THEME_NAME"
|
||||
# Add custom actions here
|
||||
```
|
||||
|
||||
Update-related hooks run only after the workflow's privileged work and after Omarchy invalidates its sudo timestamp. A hook that invokes `sudo` must therefore request its own explicit authorization. The legacy-named `pre-refresh-pacman` hook runs after the refresh transaction; during `omarchy channel set`, it is deferred further until the package switch and complete update finish. Executable user code cannot safely run before a later sudo authentication because a detached child could wait for the new timestamp.
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash -p
|
||||
|
||||
# Internal update/migration sudo boundary. Authentication may authorize this
|
||||
# command, but -N prevents it from publishing a timestamp that detached user
|
||||
# code can silently reuse.
|
||||
|
||||
if [[ $- != *p* ]]; then
|
||||
echo "Refusing an unsafe Bash startup for the sudo boundary." >&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 sudo boundary." >&2
|
||||
exit 126
|
||||
fi
|
||||
exec /usr/bin/sudo -N -- "$@"
|
||||
+27
-8
@@ -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/migrations/` | user | Per-user migration markers. |
|
||||
| `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. |
|
||||
| `~/.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. |
|
||||
| `~/.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. |
|
||||
|
||||
## Migration layout
|
||||
|
||||
@@ -56,6 +56,13 @@ privileged work should invoke the appropriate helper or privilege prompt.
|
||||
Migrations must be idempotent; if one user already applied a machine-wide repair,
|
||||
the migration should no-op for other users.
|
||||
|
||||
The runner derives the migration source from root-owned `/etc/omarchy.conf`,
|
||||
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
|
||||
migration names and exits `0` when any are pending. When no migrations are
|
||||
pending, it prints nothing and exits non-zero.
|
||||
@@ -125,18 +132,28 @@ omarchy-update
|
||||
│ installed but unconfigured fails the snapshot loudly, pointing at
|
||||
│ install/config/snapper.sh, and the update continues without one)
|
||||
├─ omarchy-update-stay-awake start
|
||||
├─ run package updates, migrations, hooks, and log analysis
|
||||
├─ run system-package updates
|
||||
├─ invalidate sudo, then run migrations and all later privileged work with
|
||||
│ no-update authentication
|
||||
├─ run orphan review and log analysis
|
||||
├─ omarchy-update-status
|
||||
│ └─ refresh or clear the shell update indicator
|
||||
├─ restart marked services and the shell
|
||||
├─ omarchy-update-stay-awake stop
|
||||
│ └─ release the sleep inhibitor and restore shell idle state, if changed
|
||||
└─ omarchy-update-restart
|
||||
├─ update AUR packages
|
||||
├─ invalidate sudo credentials
|
||||
├─ run the post-update hook, invalidate again, then update mise tools
|
||||
└─ offer the unprivileged reboot prompt
|
||||
```
|
||||
|
||||
Important behavior:
|
||||
|
||||
- In dev-link mode, `omarchy update` fast-forwards the active checkout from its
|
||||
configured upstream before changing system packages or running migrations.
|
||||
- `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.
|
||||
- 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.
|
||||
- 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.
|
||||
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` — a promise not to ask anything.
|
||||
Steps that would prompt (orphan removal, conflict handoff) report and skip
|
||||
instead of blocking.
|
||||
@@ -251,6 +268,9 @@ which pacman repo the mirrorlist points at (and swap between the `omarchy` and
|
||||
`omarchy-dev` packages through a guard-allowed pacman run), while `dev` links
|
||||
the runtime to a git checkout via the dev-link mechanism, after which
|
||||
`omarchy update` fast-forwards that checkout instead of upgrading a package.
|
||||
Channel switching defers the legacy `pre-refresh-pacman` hook across the package
|
||||
swap and the complete update. The hook runs exactly once at the final cold
|
||||
credential boundary; it is skipped if the composite operation fails earlier.
|
||||
|
||||
There is no version file at runtime. `omarchy-version` derives the version from
|
||||
`pacman -Q` on whichever package is installed, or reports `dev (<hash>)` for a
|
||||
@@ -272,8 +292,7 @@ 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-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-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-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-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-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`. |
|
||||
@@ -285,7 +304,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-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-restart` | Prompts for reboot after kernel/Hyprland updates, restarts components with `restart-*-required` markers, and always restarts the shell. | **Keep.** Important final step; may eventually include service-restart 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-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. |
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,8 +4,55 @@ set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
if [[ -z ${OMARCHY_UPDATE_SEQUENCE_NS:-} ]]; then
|
||||
outer_uid=$(id -u)
|
||||
outer_gid=$(id -g)
|
||||
subuid=$(awk -F: -v user="$(id -un)" '$1 == user { print $2; exit }' /etc/subuid)
|
||||
subgid=$(awk -F: -v group="$(id -gn)" '$1 == group { print $2; exit }' /etc/subgid)
|
||||
if [[ -z $subuid || -z $subgid ]]; then
|
||||
pass "no subordinate uid/gid range; skipping 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"
|
||||
@@ -49,7 +96,7 @@ run_update() {
|
||||
FAILING_STEP="${FAILING_STEP:-}" \
|
||||
OMARCHY_UPDATE_LOGGED=1 \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-update" "$@" >"$test_tmp/out" 2>"$test_tmp/err"
|
||||
"$ROOT/bin/omarchy-update" "$@" >"$test_tmp/out" 2>"$test_tmp/err"
|
||||
}
|
||||
|
||||
steps_run() {
|
||||
@@ -70,13 +117,14 @@ expected_steps() {
|
||||
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-update-stay-awake \
|
||||
omarchy-update-aur-pkgs \
|
||||
omarchy-hook \
|
||||
omarchy-update-mise \
|
||||
omarchy-update-restart
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user