Complete command-scoped authentication across update phases

This commit is contained in:
Afonso Oliveira committed 2026-09-06 22:26:06 +01:00
1 parent 7f9a401bb1
commit 35b318ed09
16 files changed
+583 -1643

No files matched your search

+4 -1
View File
@@ -83,7 +83,7 @@ if [[ -n $dev_checkout ]]; then
omarchy-state set reboot-required
fi
omarchy-refresh-pacman "$pacman_channel"
omarchy-refresh-pacman "$pacman_channel" defer-hook
# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites.
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}"
@@ -97,3 +97,6 @@ if [[ -z $dev_checkout ]]; then
fi
omarchy-update -y
# No channel-owned privileged work follows the historical refresh hook.
omarchy-refresh-pacman "$pacman_channel" run-deferred
+31 -230
View File
@@ -4,242 +4,43 @@
# omarchy:requires-sudo=true
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup for pacman refresh." >&2
echo "Refusing an unsafe Bash startup." >&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 pacman refresh." >&2
exit 126
fi
unset -f require_privileged_bash_startup
source "${BASH_SOURCE[0]%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
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() {
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
channel="${1:-stable}"
hook_mode="${2:-normal}"
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Invalid channel: $channel" >&2
exit 2
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
if [[ $hook_mode != "normal" && $hook_mode != "defer-hook" && $hook_mode != "run-deferred" ]]; then
echo "Invalid refresh hook mode: $hook_mode" >&2
exit 2
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
# 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
if [[ $hook_mode != "run-deferred" ]]; then
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel"
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
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm
fi
# Keep the historical hook name, but finish every privileged refresh operation
# before running user code. Callers with later root work can defer the hook.
if [[ $hook_mode != "defer-hook" ]]; then
omarchy_security_revoke_sudo_timestamp
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" \
"$OMARCHY_PATH/bin/omarchy-hook" pre-refresh-pacman
fi
+91
View File
@@ -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
View File
@@ -6,210 +6,27 @@
# omarchy:requires-sudo=true
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup for the Omarchy update." >&2
echo "Refusing an unsafe Bash startup." >&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
source "${BASH_SOURCE[0]%/*}/omarchy-security-functions" || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
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
# 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
trap - EXIT HUP INT TERM
if (( update_stay_awake_stopped == 0 )); then
omarchy-update-stay-awake stop || true
omarchy-update-stay-awake stop || status=1
fi
/usr/bin/sudo -k || true
exit "$status"
omarchy_security_exit_with_revoked_sudo "$status"
}
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 cleanup_update EXIT
omarchy_security_install_signal_exit_traps
omarchy-update-requires-free-space
@@ -243,9 +61,7 @@ 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.
# Preserve the established development-checkout update ordering.
omarchy-update-dev
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
# 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_security_revoke_sudo_timestamp
omarchy-migrate
omarchy-update-orphan-pkgs
@@ -275,19 +91,18 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
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.
# AUR package installation must also use the no-update wrapper. Finish
# update-owned system work before build code, hooks, or mise can run.
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
# 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
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
omarchy_security_revoke_sudo_timestamp
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
omarchy_security_revoke_sudo_timestamp
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
fi
+8 -1
View File
@@ -2,10 +2,17 @@
# 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 omarchy-pkg-aur-accessible; then
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
else
echo -e "\e[31m\nAUR is unavailable (so skipping updates)\e[0m"
+51 -36
View File
@@ -1,51 +1,66 @@
#!/bin/bash
# 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
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)
kernel_updated=true
if [[ $mode != "--services-only" ]]; then
running_kernel=$(uname -r)
kernel_updated=true
for kernel in /usr/lib/modules/*/vmlinuz; do
if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then
installed_kernel=$(basename "$(dirname "$kernel")")
for kernel in /usr/lib/modules/*/vmlinuz; do
if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then
installed_kernel=$(basename "$(dirname "$kernel")")
if [[ $installed_kernel == $running_kernel ]]; then
kernel_updated=false
break
if [[ $installed_kernel == $running_kernel ]]; then
kernel_updated=false
break
fi
fi
fi
done
done
if [[ $kernel_updated == "true" ]]; then
confirm_reboot "Linux kernel has been updated. Reboot?"
elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then
confirm_reboot "Updates require reboot. Ready?"
if [[ $kernel_updated == "true" ]]; then
confirm_reboot "Linux kernel has been updated. Reboot?"
elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then
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
running_hyprland=$(readlink /proc/$(pgrep -x Hyprland)/exe 2>/dev/null)
if [[ $running_hyprland == *"(deleted)"* ]]; then
confirm_reboot "Hyprland has been updated. Reboot?"
if [[ $mode != "--reboot-only" ]]; then
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
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
+41 -32
View File
@@ -81,46 +81,55 @@ stop() {
}
start() {
local inhibit_pid=""
local inhibit_start_time=""
local inhibit_runner=()
local idle_owner="$$:$RANDOM:$RANDOM"
stop
mkdir -p "$state_dir"
if omarchy-cmd-present systemd-inhibit; then
if (( EUID != 0 )); then
if [[ -t 0 ]]; then
sudo -v
inhibit_runner=(sudo)
else
inhibit_runner=(pkexec)
fi
fi
# sudo authenticates in the foreground, then backgrounds the inhibitor.
# The held command drops back to this user before publishing its PID, so stop
# can release the inhibitor without another privileged operation or ticket.
local hold_command=(
/usr/bin/systemd-inhibit --what=sleep:idle --who=omarchy-update
--why="Omarchy update in progress" --mode=block
/usr/bin/setpriv --reuid "$UID" --regid "$(id -g)" --clear-groups
/usr/bin/bash -p -c '
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
"${inhibit_runner[@]}" systemd-inhibit \
--what=sleep:idle \
--who=omarchy-update \
--why="Omarchy update in progress" \
--mode=block \
sleep infinity >/dev/null 2>&1 {OMARCHY_UPDATE_LOCK_FD}>&- &
else
"${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
if (( EUID == 0 )); then
( [[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec "${hold_command[@]}" ) &
elif [[ -t 0 ]]; then
sudo -N -b -- "${hold_command[@]}"
else
( [[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec pkexec "${hold_command[@]}" ) &
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
printf '%s\n' "$idle_owner" >"$idle_owner_file"
mkdir -p "$(dirname "$stay_awake_state")"