Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
68d8141
OM-SEC-12: Bind update inhibitor cleanup to process identity
AFOliveira Aug 31, 2026
1136a71
OM-SEC-14: Run update hooks without reusable sudo authority
AFOliveira Aug 31, 2026
dd0428c
OM-SEC-17: Allowlist update restart markers and commands
AFOliveira Aug 31, 2026
87625c2
OM-SEC-01: Make passwordless sudo expiry fail closed
AFOliveira Aug 31, 2026
2f816a8
Document privileged Bash startup exception
AFOliveira Sep 1, 2026
fb36ddd
Address update boundary review
AFOliveira Sep 3, 2026
7f9a401
Merge commit 'b71dcad96e9d0b2962b7d225828a5cb6000ad720' into codex/re…
AFOliveira Sep 6, 2026
35b318e
Complete command-scoped authentication across update phases
AFOliveira Sep 6, 2026
c52381f
Keep update regressions isolated from host authentication
AFOliveira Sep 6, 2026
f2c3925
Revoke before session cleanup and protect standalone inhibition
AFOliveira Sep 6, 2026
1f8f5ba
Integrate current quattro before security review
AFOliveira Sep 6, 2026
a9e9954
Clarify unattended updates still require sudo authorization
AFOliveira Sep 6, 2026
978dffa
Use required gum directly in AI removal prompts
AFOliveira Sep 6, 2026
d41ca88
Wait for the actual Stay Awake launcher
AFOliveira Sep 6, 2026
177d572
Wait for notifications when restarting the shell
AFOliveira Sep 6, 2026
35551b0
Disable the Yay sudo loop with its supported option
AFOliveira Sep 6, 2026
aa422fc
Merge current Quattro while preserving command checks
AFOliveira Sep 7, 2026
d62eeea
Align restart marker handling with current update authorization
AFOliveira Sep 7, 2026
5b692c5
Preserve user PATH across updater relaunches
AFOliveira Sep 7, 2026
41a428d
Merge branch 'codex/review-9469-20260906' into codex/review-9472-2026…
AFOliveira Sep 7, 2026
f37c73f
Bind protected update commands to their source root
AFOliveira Sep 7, 2026
5bc41b2
Resolve security libraries beside canonical entrypoints
AFOliveira Sep 7, 2026
c869740
Keep channel transitions inside command-scoped sudo
AFOliveira Sep 7, 2026
6d8e39e
Merge branch 'codex/review-9469-20260906' into codex/review-9472-2026…
AFOliveira Sep 7, 2026
bf6c147
Use the shared source boundary for restart handling
AFOliveira Sep 7, 2026
0add43f
Check dev update support before changing the system
AFOliveira Sep 7, 2026
acc02b1
Keep restart source coverage with restart regressions
AFOliveira Sep 7, 2026
b641a65
Merge branch 'codex/review-9469-20260906' into codex/review-9472-2026…
AFOliveira Sep 7, 2026
bc235d2
Merge remote-tracking branch 'refs/remotes/portfolio/quattro' into co…
AFOliveira Sep 7, 2026
4ae25cd
Keep temporary sudo grants bounded through lifecycle failures
AFOliveira Sep 7, 2026
a6385e6
Bound sudo policy natively and guard expiry package transactions
AFOliveira Sep 7, 2026
75e58f0
Integrate the shared sudo lifecycle foundation
AFOliveira Sep 7, 2026
104c776
Merge update inhibitor identity hardening
AFOliveira Sep 7, 2026
76f0abe
Roll back interrupted inhibitor launches
AFOliveira Sep 7, 2026
0240113
Open launch authorization without recreating cancelled state
AFOliveira Sep 7, 2026
d84c05f
Integrate restart markers with reviewed inhibitor lifecycle
AFOliveira Sep 7, 2026
aaa13ca
Clarify update hook timing in the user manual
AFOliveira Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`). A security-sensitive entrypoint may use the exact `#!/bin/bash -p` form only when it must suppress `BASH_ENV` and exported-function startup injection before its first command; that exception must be explained at the boundary and covered by a regression that rejects an ordinary Bash launch with a decoy `-p` argument.
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs

# Command Naming
Expand Down
252 changes: 245 additions & 7 deletions bin/omarchy-update
Original file line number Diff line number Diff line change
@@ -1,12 +1,225 @@
#!/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

# Preserve the caller's command path across both re-execs below. The logging
# shell and update-lock shell otherwise inherit the already-sanitized PATH and
# silently hide user-installed post-update hooks and mise.
if [[ -z ${OMARCHY_UPDATE_USER_PATH+x} ]]; then
OMARCHY_UPDATE_USER_PATH="${PATH:-/usr/bin:/bin}"
fi
export OMARCHY_UPDATE_USER_PATH
user_path="$OMARCHY_UPDATE_USER_PATH"
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"
Expand All @@ -15,9 +228,10 @@ fi
if ! omarchy-update-lock held; then
exec omarchy-update-lock run "$0" "$@"
fi
unset OMARCHY_UPDATE_USER_PATH

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

Expand All @@ -38,27 +252,51 @@ 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

# 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-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

# 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-update-restart
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
fi
Loading