Keep Plymouth publishing working in dev mode

This commit is contained in:
Erik Melton
2026-08-29 16:19:31 +02:00
parent 079d116511
commit 363db1f569
2 changed files with 107 additions and 36 deletions
+67 -29
View File
@@ -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() {
+40 -7
View File
@@ -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