From 363db1f569a9017acca3619b0a6e22757c77696a Mon Sep 17 00:00:00 2001 From: Erik Melton Date: Sat, 29 Aug 2026 16:19:31 +0200 Subject: [PATCH] Keep Plymouth publishing working in dev mode --- bin/omarchy-plymouth-set | 96 +++++++++++++++++++++---------- test/shell.d/plymouth-set-test.sh | 47 ++++++++++++--- 2 files changed, 107 insertions(+), 36 deletions(-) diff --git a/bin/omarchy-plymouth-set b/bin/omarchy-plymouth-set index 51d37e45..f4c9b792 100755 --- a/bin/omarchy-plymouth-set +++ b/bin/omarchy-plymouth-set @@ -76,6 +76,9 @@ run_root_transaction() { [[ -z $failure_reported ]] || return 0 failure_reported=1 printf "omarchy-plymouth-set: refusing to publish: %s failed validation\n" "$failure_context" >&2 + if [[ -n ${failure_hint:-} ]]; then + printf "omarchy-plymouth-set: %s\n" "$failure_hint" >&2 + fi } trap report_failure ERR @@ -95,14 +98,65 @@ run_root_transaction() { canonical_source_root=$(realpath -e -- "$source_root") [[ $canonical_source_root == "$source_root" ]] - # The one refusal a healthy machine can hit: omarchy dev link points - # OMARCHY_PATH at a checkout the desktop user can rewrite, and executable - # boot assets cannot come from there. Say so rather than failing mute. + validate_trusted_directory() { + local directory=$1 canonical uid directory_mode + + failure_context="directory $directory" + canonical=$(realpath -e -- "$directory") + [[ $canonical == "$directory" && -d $directory && ! -L $directory ]] + + while :; do + failure_context="directory $directory (must be root-owned and not group- or world-writable)" + uid=$(stat -c %u -- "$directory") + directory_mode=$(stat -c %a -- "$directory") + (( uid == 0 )) + (( (8#$directory_mode & 0022) == 0 )) + [[ $directory == "/" ]] && break + directory=${directory%/*} + [[ -n $directory ]] || directory=/ + done + } + + validate_trusted_configuration_file() { + local configuration=$1 canonical uid configuration_mode size + + failure_context="root configuration $configuration" + [[ -f $configuration && ! -L $configuration ]] + canonical=$(realpath -e -- "$configuration") + [[ $canonical == "$configuration" ]] + validate_trusted_directory "${configuration%/*}" + uid=$(stat -c %u -- "$configuration") + configuration_mode=$(stat -c %a -- "$configuration") + size=$(stat -c %s -- "$configuration") + (( uid == 0 )) + (( (8#$configuration_mode & 0022) == 0 )) + (( size > 0 && size <= 4096 )) + } + + # A packaged tree must be root-owned. A development checkout is the one + # deliberate exception: omarchy dev link records its canonical path in a + # root-owned /etc/omarchy.conf. That is already an explicit decision to run + # privileged Omarchy commands from user-editable code in the checkout, so + # reading its packaged assets does not widen the development trust boundary. + development_source=false source_root_uid=$(stat -c %u -- "$source_root") if (( source_root_uid != 0 )); then - printf "omarchy-plymouth-set: %s is not root-owned, so it cannot supply executable boot assets.\n" "$source_root" >&2 - printf "omarchy-plymouth-set: this is expected on a development checkout; run omarchy dev unlink to publish from /usr/share/omarchy.\n" >&2 - exit 1 + omarchy_conf=/etc/omarchy.conf + failure_context="$source_root is user-owned and $omarchy_conf must contain its trusted dev-link authorization; run omarchy dev link to authorize it" + failure_hint="$source_root is user-owned; run omarchy dev link to authorize this development checkout, or omarchy dev unlink to use the packaged tree" + validate_trusted_configuration_file "$omarchy_conf" + + quoted_source_root=$source_root + quoted_source_root=${quoted_source_root//\\/\\\\} + quoted_source_root=${quoted_source_root//\"/\\\"} + quoted_source_root=${quoted_source_root//\$/\\\$} + quoted_source_root=${quoted_source_root//\`/\\\`} + expected_config_line="export OMARCHY_PATH=\"$quoted_source_root\"" + mapfile -t omarchy_config_lines <"$omarchy_conf" + (( ${#omarchy_config_lines[@]} == 1 )) + [[ ${omarchy_config_lines[0]} == "$expected_config_line" ]] + development_source=true + failure_hint= fi if [[ $mode == "set" ]]; then @@ -126,25 +180,6 @@ run_root_transaction() { plymouth_default_assets=("${plymouth_theme_assets[@]}" logos/oma.png) sddm_theme_assets=(Main.qml bullet.png entry-failed.png entry.png lock-failed.png lock.png logo.png) - validate_trusted_directory() { - local directory=$1 canonical uid directory_mode - - failure_context="directory $directory" - canonical=$(realpath -e -- "$directory") - [[ $canonical == "$directory" && -d $directory && ! -L $directory ]] - - while :; do - failure_context="directory $directory (must be root-owned and not group- or world-writable)" - uid=$(stat -c %u -- "$directory") - directory_mode=$(stat -c %a -- "$directory") - (( uid == 0 )) - (( (8#$directory_mode & 0022) == 0 )) - [[ $directory == "/" ]] && break - directory=${directory%/*} - [[ -n $directory ]] || directory=/ - done - } - validate_trusted_file() { local source=$1 canonical uid file_mode size @@ -152,13 +187,16 @@ run_root_transaction() { [[ -f $source && ! -L $source ]] canonical=$(realpath -e -- "$source") [[ $canonical == "$source" ]] - validate_trusted_directory "${source%/*}" - uid=$(stat -c %u -- "$source") file_mode=$(stat -c %a -- "$source") size=$(stat -c %s -- "$source") - (( uid == 0 )) - (( (8#$file_mode & 0022) == 0 )) (( size > 0 && size <= max_asset_size )) + + if ! $development_source; then + validate_trusted_directory "${source%/*}" + uid=$(stat -c %u -- "$source") + (( uid == 0 )) + (( (8#$file_mode & 0022) == 0 )) + fi } copy_trusted_file() { diff --git a/test/shell.d/plymouth-set-test.sh b/test/shell.d/plymouth-set-test.sh index 77d21af3..8a40cbf7 100755 --- a/test/shell.d/plymouth-set-test.sh +++ b/test/shell.d/plymouth-set-test.sh @@ -174,12 +174,14 @@ case "$1" in # simulation only, substitute trusted tools and map fixed system destinations # under the disposable fake root. code=${code/PATH=\/usr\/bin:\/bin/PATH=$TEST_ROOT_TOOLS:\/usr\/bin:\/bin} + code=${code/omarchy_conf=\/etc\/omarchy.conf/omarchy_conf=$TEST_OMARCHY_CONF} code=${code/theme_dir=\/usr\/share\/plymouth\/themes\/omarchy/theme_dir=$TEST_FAKE_ROOT\/usr\/share\/plymouth\/themes\/omarchy} code=${code/sddm_dir=\/usr\/share\/sddm\/themes\/omarchy/sddm_dir=$TEST_FAKE_ROOT\/usr\/share\/sddm\/themes\/omarchy} # Each rewrite above silently no-ops if the production text drifts, which # would point this simulation at the real /usr/share. Refuse instead. [[ $code == *"PATH=$TEST_ROOT_TOOLS:/usr/bin:/bin"* ]] || exit 94 + [[ $code == *"omarchy_conf=$TEST_OMARCHY_CONF"* ]] || exit 94 [[ $code == *"theme_dir=$TEST_FAKE_ROOT/usr/share/plymouth/themes/omarchy"* ]] || exit 94 [[ $code == *"sddm_dir=$TEST_FAKE_ROOT/usr/share/sddm/themes/omarchy"* ]] || exit 94 @@ -243,6 +245,7 @@ setup_run() { fake_root="$run_dir/root" sudo_log="$run_dir/sudo.log" leak_log="$run_dir/leaked-stage-path.log" + omarchy_conf="$run_dir/omarchy.conf" theme="$fake_root/usr/share/plymouth/themes/omarchy" sddm="$fake_root/usr/share/sddm/themes/omarchy" @@ -295,6 +298,7 @@ run_set_colors() { TEST_FAKE_ROOT="$fake_root" \ TEST_STAGES="$stages" \ TEST_ROOT_TOOLS="$root_tools" \ + TEST_OMARCHY_CONF="$omarchy_conf" \ TEST_SUDO_LOG="$sudo_log" \ TEST_LEAK_LOG="$leak_log" \ "$@" \ @@ -442,21 +446,49 @@ assert_no_temporary_files "$fake_root" pass "root rejects packaged assets that a desktop process could rewrite" -# omarchy dev link points OMARCHY_PATH at a checkout the desktop user owns, so -# this refusal fires on a working machine, not only under attack. Every check in -# the privileged transaction is a bare assertion that aborts under set -e, so -# without a diagnostic the whole Plymouth menu would just close in silence. +# A random user-owned OMARCHY_PATH remains untrusted. Only the exact canonical +# checkout recorded by root in /etc/omarchy.conf is the supported dev-link +# exception; an unrelated or stale authorization must not weaken the check. setup_run output=$(run_set 022 env TEST_UNTRUSTED_SOURCE="$ROOT" 2>&1) status=$? (( status != 0 )) || fail "a user-owned OMARCHY_PATH is rejected" -[[ $output == *"is not root-owned"* ]] || fail "the refusal names the untrusted source tree" "$output" -[[ $output == *"omarchy dev unlink"* ]] || fail "the refusal names the way back to a trusted tree" "$output" +[[ $output == *"user-owned"* ]] || fail "the refusal names the untrusted source tree" "$output" +[[ $output == *"omarchy dev link"* ]] || fail "the refusal names how to authorize a development checkout" "$output" [[ $(cat "$theme/bullet.png") == 'old plymouth bullet.png' ]] || fail "a user-owned OMARCHY_PATH leaves the live theme unchanged" assert_no_temporary_files "$fake_root" -pass "a development checkout is refused with an explanation instead of in silence" +setup_run +printf 'export OMARCHY_PATH="/some/other/checkout"\n' >"$omarchy_conf" +chmod 0644 "$omarchy_conf" +output=$(run_set 022 env TEST_UNTRUSTED_SOURCE="$ROOT" 2>&1) +status=$? + +(( status != 0 )) || fail "a stale dev-link authorization is rejected" +[[ $(cat "$theme/bullet.png") == 'old plymouth bullet.png' ]] || fail "a stale dev-link authorization leaves the live theme unchanged" +assert_no_temporary_files "$fake_root" + +setup_run +printf 'export OMARCHY_PATH="%s"\n' "$ROOT" >"$omarchy_conf" +chmod 0666 "$omarchy_conf" +output=$(run_set 022 env TEST_UNTRUSTED_SOURCE="$ROOT" 2>&1) +status=$? + +(( status != 0 )) || fail "a writable dev-link authorization is rejected" +[[ $(cat "$theme/bullet.png") == 'old plymouth bullet.png' ]] || fail "a writable dev-link authorization leaves the live theme unchanged" +assert_no_temporary_files "$fake_root" + +setup_run +printf 'export OMARCHY_PATH="%s"\n' "$ROOT" >"$omarchy_conf" +chmod 0644 "$omarchy_conf" +output=$(run_set 022 env TEST_UNTRUSTED_SOURCE="$ROOT" 2>&1) +status=$? + +(( status == 0 )) || fail "the root-authorized development checkout can publish Plymouth assets" "$output" +cmp -s "$ROOT/default/plymouth/bullet.png" "$theme/bullet.png" || fail "the authorized development checkout supplies the packaged assets" + +pass "only the checkout explicitly authorized by omarchy dev link may be user-owned" # Root rejects both a symlinked parent and a group/world-writable parent before # it creates a temporary file or touches the live destination. @@ -509,6 +541,7 @@ output=$( TEST_FAKE_ROOT="$fake_root" \ TEST_STAGES="$stages" \ TEST_ROOT_TOOLS="$root_tools" \ + TEST_OMARCHY_CONF="$omarchy_conf" \ TEST_SUDO_LOG="$sudo_log" \ TEST_LEAK_LOG="$leak_log" \ /bin/bash "$ROOT/bin/omarchy-refresh-plymouth" 2>&1