[Security] Harden Plymouth asset publication
This commit is contained in:
+231
-60
@@ -5,75 +5,258 @@
|
|||||||
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
|
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
|
||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
# Configure the Plymouth boot theme with a custom background color, text color, and logo.
|
set -euo pipefail
|
||||||
# Stages the change in a temp dir, then commits the staged files to /usr/share and
|
|
||||||
# rebuilds the initramfs. Also syncs the SDDM login screen (the post-logout
|
|
||||||
# screen) with the same colors and logo so boot/login stay visually unified.
|
|
||||||
|
|
||||||
if (( $# != 3 )); then
|
# Configure the Plymouth boot theme with a custom background color, text color,
|
||||||
|
# and logo. Assets are prepared without privileges, pinned to their recorded
|
||||||
|
# hashes, then published one at a time through root-owned temporary files.
|
||||||
|
|
||||||
|
refresh_default=false
|
||||||
|
if (( $# == 1 )) && [[ $1 == --refresh-default ]]; then
|
||||||
|
refresh_default=true
|
||||||
|
elif (( $# != 3 )); then
|
||||||
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
|
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bg_hex="${1#\#}"
|
if ! $refresh_default; then
|
||||||
text_hex="${2#\#}"
|
bg_hex="${1#\#}"
|
||||||
logo_path="$3"
|
text_hex="${2#\#}"
|
||||||
|
logo_path="$3"
|
||||||
|
|
||||||
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||||
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
|
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||||
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
|
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f $logo_path ]]; then
|
if [[ ! -f $logo_path ]]; then
|
||||||
echo "Logo file not found: $logo_path" >&2
|
echo "Logo file not found: $logo_path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# omarchy-plymouth-set-by-theme passes a theme's unlock.png straight from
|
# omarchy-plymouth-set-by-theme passes a theme's unlock.png straight from
|
||||||
# ~/.config/omarchy/themes, where an installed theme can make it a symlink to
|
# ~/.config/omarchy/themes, where an installed theme can make it a symlink to
|
||||||
# anything. The copies below land in world-readable /usr/share, so following one
|
# anything. The copies below land in world-readable /usr/share, so following
|
||||||
# would republish whatever it points at.
|
# one would republish whatever it points at.
|
||||||
if [[ -L $logo_path ]]; then
|
if [[ -L $logo_path ]]; then
|
||||||
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
|
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
|
|
||||||
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
|
|
||||||
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
|
|
||||||
|
|
||||||
theme_dir="/usr/share/plymouth/themes/omarchy"
|
theme_dir="/usr/share/plymouth/themes/omarchy"
|
||||||
staging_dir=$(mktemp -d)
|
sddm_dir="/usr/share/sddm/themes/omarchy"
|
||||||
trap 'rm -rf "$staging_dir"' EXIT
|
max_asset_size=$((64 * 1024 * 1024))
|
||||||
|
|
||||||
# Publish a staged asset without ever asking a privileged process to resolve
|
plymouth_theme_assets=(
|
||||||
# its user-writable source path. The shell opens source before sudo starts, so
|
bullet.png
|
||||||
# the inherited descriptor pins the bytes and a replacement symlink to a
|
entry.png
|
||||||
# root-only file either loses the race or fails under the caller's permissions.
|
lock.png
|
||||||
publish_asset() {
|
logo.png
|
||||||
|
omarchy.plymouth
|
||||||
|
omarchy.script
|
||||||
|
preview-unlock.png
|
||||||
|
progress_bar.png
|
||||||
|
progress_box.png
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
staging_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$staging_dir"' EXIT
|
||||||
|
chmod 0700 "$staging_dir"
|
||||||
|
plymouth_stage="$staging_dir/plymouth"
|
||||||
|
sddm_stage="$staging_dir/sddm"
|
||||||
|
mkdir -m 0700 -p "$plymouth_stage/logos" "$sddm_stage"
|
||||||
|
|
||||||
|
copy_regular_file() {
|
||||||
local source="$1" destination="$2"
|
local source="$1" destination="$2"
|
||||||
sudo tee "$destination" <"$source" >/dev/null && sudo chmod 0644 "$destination"
|
|
||||||
|
if [[ ! -f $source || -L $source ]]; then
|
||||||
|
echo "Refusing non-regular or symlinked asset: $source" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -P ensures a source swapped to a symlink is copied as a symlink instead of
|
||||||
|
# followed; the post-copy check then rejects it.
|
||||||
|
cp -P --reflink=never -- "$source" "$destination"
|
||||||
|
if [[ ! -f $destination || -L $destination ]]; then
|
||||||
|
rm -f -- "$destination"
|
||||||
|
echo "Asset changed while it was being staged: $source" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod 0600 "$destination"
|
||||||
}
|
}
|
||||||
|
|
||||||
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
|
if $refresh_default; then
|
||||||
cp "$logo_path" "$staging_dir/logo.png"
|
assets_to_stage=("${plymouth_default_assets[@]}")
|
||||||
|
else
|
||||||
|
assets_to_stage=("${plymouth_theme_assets[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
sed -i \
|
for asset in "${assets_to_stage[@]}"; do
|
||||||
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
|
copy_regular_file "$OMARCHY_PATH/default/plymouth/$asset" "$plymouth_stage/$asset"
|
||||||
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
|
|
||||||
"$staging_dir/omarchy.script"
|
|
||||||
|
|
||||||
for asset in bullet.png entry.png lock.png progress_bar.png; do
|
|
||||||
magick "$staging_dir/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$staging_dir/$asset"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
for asset in bullet.png entry.png lock.png logo.png omarchy.plymouth omarchy.script preview-unlock.png progress_bar.png progress_box.png; do
|
if ! $refresh_default; then
|
||||||
publish_asset "$staging_dir/$asset" "$theme_dir/$asset" || exit 1
|
for asset in "${sddm_theme_assets[@]}"; do
|
||||||
|
copy_regular_file "$OMARCHY_PATH/default/sddm/omarchy/$asset" "$sddm_stage/$asset"
|
||||||
|
done
|
||||||
|
|
||||||
|
copy_regular_file "$logo_path" "$plymouth_stage/logo.png"
|
||||||
|
copy_regular_file "$logo_path" "$sddm_stage/logo.png"
|
||||||
|
|
||||||
|
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||||
|
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||||
|
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
|
||||||
|
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
|
||||||
|
"$plymouth_stage/omarchy.script"
|
||||||
|
|
||||||
|
for asset in bullet.png entry.png lock.png progress_bar.png; do
|
||||||
|
magick "$plymouth_stage/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$plymouth_stage/$asset"
|
||||||
|
done
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
-e "s/#1a1b26/#$bg_hex/g" \
|
||||||
|
-e "s/#ffffff/#$text_hex/g" \
|
||||||
|
"$sddm_stage/Main.qml"
|
||||||
|
|
||||||
|
for asset in bullet.png entry.png lock.png; do
|
||||||
|
cp --reflink=never -- "$plymouth_stage/$asset" "$sddm_stage/$asset"
|
||||||
|
done
|
||||||
|
for asset in entry lock; do
|
||||||
|
magick "$plymouth_stage/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$sddm_stage/$asset-failed.png"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -A staged_hashes=()
|
||||||
|
declare -A staged_sizes=()
|
||||||
|
|
||||||
|
record_staged_asset() {
|
||||||
|
local source="$1" hash size
|
||||||
|
|
||||||
|
if [[ ! -f $source || -L $source ]]; then
|
||||||
|
echo "Refusing non-regular or symlinked staged asset: $source" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
size=$(stat -c %s -- "$source")
|
||||||
|
if (( size == 0 || size > max_asset_size )); then
|
||||||
|
echo "Staged asset is empty or exceeds the ${max_asset_size}-byte limit: $source" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
hash=$(sha256sum -- "$source")
|
||||||
|
staged_sizes["$source"]=$size
|
||||||
|
staged_hashes["$source"]=${hash%% *}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Record every asset before the first sudo prompt. Root verifies both values
|
||||||
|
# after consuming stdin, so an in-place rewrite after this point fails instead
|
||||||
|
# of changing what is published. This pins the recorded result, not the trust
|
||||||
|
# of an already-user-writable OMARCHY_PATH checkout.
|
||||||
|
for asset in "${assets_to_stage[@]}"; do
|
||||||
|
record_staged_asset "$plymouth_stage/$asset"
|
||||||
|
done
|
||||||
|
if ! $refresh_default; then
|
||||||
|
for asset in "${sddm_theme_assets[@]}"; do
|
||||||
|
record_staged_asset "$sddm_stage/$asset"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish_asset() {
|
||||||
|
local source="$1" destination="$2"
|
||||||
|
local expected_hash="${staged_hashes[$source]}"
|
||||||
|
local expected_size="${staged_sizes[$source]}"
|
||||||
|
|
||||||
|
# The caller's shell opens source before sudo starts. Root reads only stdin,
|
||||||
|
# verifies the recorded bytes, and never opens or chmods the final pathname.
|
||||||
|
# Its temporary file is on the destination filesystem, so mv is atomic for
|
||||||
|
# this one asset and replaces a destination symlink instead of following it.
|
||||||
|
sudo /bin/bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
PATH=/usr/bin:/bin
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
destination=$1
|
||||||
|
expected_hash=$2
|
||||||
|
expected_size=$3
|
||||||
|
max_size=$4
|
||||||
|
|
||||||
|
[[ $destination == /* && $destination != */ && $destination != *"/../"* ]]
|
||||||
|
[[ $expected_hash =~ ^[0-9a-f]{64}$ ]]
|
||||||
|
[[ $expected_size =~ ^[0-9]+$ && $max_size =~ ^[0-9]+$ ]]
|
||||||
|
(( expected_size > 0 && expected_size <= max_size ))
|
||||||
|
|
||||||
|
parent=${destination%/*}
|
||||||
|
filename=${destination##*/}
|
||||||
|
[[ -n $parent && -n $filename && $filename != . && $filename != .. ]]
|
||||||
|
[[ -d $parent && ! -L $parent ]]
|
||||||
|
canonical_parent=$(realpath -e -- "$parent")
|
||||||
|
[[ $canonical_parent == "$parent" ]]
|
||||||
|
[[ $(stat -c %u -- "$parent") == 0 ]]
|
||||||
|
parent_mode=$(stat -c %a -- "$parent")
|
||||||
|
(( (8#$parent_mode & 0022) == 0 ))
|
||||||
|
|
||||||
|
temporary=$(mktemp --tmpdir="$parent" ".$filename.omarchy-new.XXXXXXXX")
|
||||||
|
cleanup() { rm -f -- "$temporary"; }
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
head -c "$((expected_size + 1))" >"$temporary"
|
||||||
|
actual_size=$(stat -c %s -- "$temporary")
|
||||||
|
(( actual_size == expected_size ))
|
||||||
|
actual_hash=$(sha256sum -- "$temporary")
|
||||||
|
[[ ${actual_hash%% *} == "$expected_hash" ]]
|
||||||
|
|
||||||
|
chown 0:0 -- "$temporary"
|
||||||
|
chmod 0644 -- "$temporary"
|
||||||
|
sync -f -- "$temporary"
|
||||||
|
mv --no-copy -fT -- "$temporary" "$destination"
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
' bash "$destination" "$expected_hash" "$expected_size" "$max_asset_size" <"$source"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_legacy_asset() {
|
||||||
|
local destination="$1"
|
||||||
|
|
||||||
|
sudo /bin/bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
PATH=/usr/bin:/bin
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
destination=$1
|
||||||
|
[[ $destination == /* && $destination != */ && $destination != *"/../"* ]]
|
||||||
|
parent=${destination%/*}
|
||||||
|
[[ -d $parent && ! -L $parent ]]
|
||||||
|
canonical_parent=$(realpath -e -- "$parent")
|
||||||
|
[[ $canonical_parent == "$parent" ]]
|
||||||
|
[[ $(stat -c %u -- "$parent") == 0 ]]
|
||||||
|
parent_mode=$(stat -c %a -- "$parent")
|
||||||
|
(( (8#$parent_mode & 0022) == 0 ))
|
||||||
|
rm -f -- "$destination"
|
||||||
|
' bash "$destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
for asset in "${assets_to_stage[@]}"; do
|
||||||
|
publish_asset "$plymouth_stage/$asset" "$theme_dir/$asset"
|
||||||
done
|
done
|
||||||
sudo plymouth-set-default-theme omarchy
|
sudo plymouth-set-default-theme omarchy
|
||||||
|
|
||||||
@@ -83,21 +266,9 @@ else
|
|||||||
sudo mkinitcpio -P
|
sudo mkinitcpio -P
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sync the SDDM login screen with the same colors and logo.
|
if ! $refresh_default; then
|
||||||
sddm_dir="/usr/share/sddm/themes/omarchy"
|
for asset in "${sddm_theme_assets[@]}"; do
|
||||||
sddm_template="$OMARCHY_PATH/default/sddm/omarchy/Main.qml"
|
publish_asset "$sddm_stage/$asset" "$sddm_dir/$asset"
|
||||||
|
done
|
||||||
sed \
|
remove_legacy_asset "$sddm_dir/logo.svg"
|
||||||
-e "s/#1a1b26/#$bg_hex/g" \
|
fi
|
||||||
-e "s/#ffffff/#$text_hex/g" \
|
|
||||||
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
|
||||||
|
|
||||||
publish_asset "$staging_dir/logo.png" "$sddm_dir/logo.png" || exit 1
|
|
||||||
for asset in bullet.png entry.png lock.png; do
|
|
||||||
publish_asset "$staging_dir/$asset" "$sddm_dir/$asset" || exit 1
|
|
||||||
done
|
|
||||||
for asset in entry lock; do
|
|
||||||
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png"
|
|
||||||
publish_asset "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png" || exit 1
|
|
||||||
done
|
|
||||||
sudo rm -f "$sddm_dir/logo.svg"
|
|
||||||
|
|||||||
@@ -3,11 +3,7 @@
|
|||||||
# omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it.
|
# omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it.
|
||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
sudo cp -r "$OMARCHY_PATH/default/plymouth/." /usr/share/plymouth/themes/omarchy/
|
# Reuse the fixed-file publisher so root never resolves the source checkout or
|
||||||
sudo plymouth-set-default-theme omarchy
|
# follows a destination symlink while restoring the packaged assets.
|
||||||
|
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||||
if omarchy-cmd-present limine-mkinitcpio; then
|
exec "$script_dir/omarchy-plymouth-set" --refresh-default
|
||||||
sudo limine-mkinitcpio
|
|
||||||
else
|
|
||||||
sudo mkinitcpio -P
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||||
|
|
||||||
test_tmp=$(mktemp -d)
|
test_tmp=$(mktemp -d)
|
||||||
trap 'chmod 0600 "$test_tmp/secret" 2>/dev/null || true; rm -rf "$test_tmp"' EXIT
|
secret="$test_tmp/secret"
|
||||||
|
trap 'chmod 0600 "$secret" 2>/dev/null || true; rm -rf -- "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
plymouth_theme_assets=(
|
||||||
|
bullet.png
|
||||||
|
entry.png
|
||||||
|
lock.png
|
||||||
|
logo.png
|
||||||
|
omarchy.plymouth
|
||||||
|
omarchy.script
|
||||||
|
preview-unlock.png
|
||||||
|
progress_bar.png
|
||||||
|
progress_box.png
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
# omarchy-plymouth-set-by-theme hands over a theme's unlock.png from
|
# omarchy-plymouth-set-by-theme hands over a theme's unlock.png from
|
||||||
# ~/.config/omarchy/themes. Both installed copies are world-readable, so a
|
# ~/.config/omarchy/themes. Both installed copies are world-readable, so a
|
||||||
# symlink there must not republish whatever it points at.
|
# symlink there must not republish whatever it points at.
|
||||||
secret="$test_tmp/secret"
|
|
||||||
printf 'not yours\n' >"$secret"
|
printf 'not yours\n' >"$secret"
|
||||||
ln -s "$secret" "$test_tmp/logo-link.png"
|
ln -s "$secret" "$test_tmp/logo-link.png"
|
||||||
|
|
||||||
output=$(OMARCHY_PATH="$ROOT" bash "$ROOT/bin/omarchy-plymouth-set" '#1d2021' '#ebdbb2' "$test_tmp/logo-link.png" 2>&1)
|
output=$(OMARCHY_PATH="$ROOT" /bin/bash "$ROOT/bin/omarchy-plymouth-set" '#1d2021' '#ebdbb2' "$test_tmp/logo-link.png" 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
|
|
||||||
((status != 0)) || fail "omarchy-plymouth-set refuses a symlinked logo"
|
(( status != 0 )) || fail "omarchy-plymouth-set refuses a symlinked logo"
|
||||||
[[ $output == *"symlink"* ]] || fail "omarchy-plymouth-set says why it refused the logo" "$output"
|
[[ $output == *"symlink"* ]] || fail "omarchy-plymouth-set says why it refused the logo" "$output"
|
||||||
|
|
||||||
pass "a themed logo cannot republish a file it merely points at"
|
pass "a themed logo cannot republish a file it merely points at"
|
||||||
@@ -120,66 +136,89 @@ run_unlock_action "default"
|
|||||||
|
|
||||||
pass "the unlock picker still applies a theme and still resets on default"
|
pass "the unlock picker still applies a theme and still resets on default"
|
||||||
|
|
||||||
# Exercise the full publisher with sudo and ImageMagick shims. Immediately
|
|
||||||
# after the unprivileged shell opens each staged source, the sudo shim renames
|
|
||||||
# that source away and replaces its pathname with a symlink to a simulated
|
|
||||||
# root-only secret. Reading via the inherited stdin descriptor must still
|
|
||||||
# publish the original bytes. The shim restores the source after each read so
|
|
||||||
# every Plymouth and SDDM asset gets attacked independently.
|
|
||||||
fake_bin="$test_tmp/bin"
|
fake_bin="$test_tmp/bin"
|
||||||
fake_root="$test_tmp/root"
|
root_tools="$test_tmp/root-tools"
|
||||||
stages="$test_tmp/stages"
|
stages="$test_tmp/stages"
|
||||||
attack_log="$test_tmp/attacked"
|
mkdir -p "$fake_bin" "$root_tools" "$stages"
|
||||||
sudo_log="$test_tmp/sudo.log"
|
|
||||||
mkdir -p "$fake_bin" "$fake_root" "$stages"
|
|
||||||
|
|
||||||
cat >"$fake_bin/sudo" <<'SH'
|
cat >"$fake_bin/sudo" <<'SH'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
printf '%s\n' "$*" >>"$TEST_SUDO_LOG"
|
set -u
|
||||||
|
|
||||||
|
for argument in "$@"; do
|
||||||
|
if [[ $argument == *"$TEST_STAGES"* ]]; then
|
||||||
|
printf '%s\n' "$argument" >>"$TEST_LEAK_LOG"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
tee)
|
/bin/bash)
|
||||||
destination="$2"
|
[[ ${2:-} == -c && $# -ge 5 ]] || exit 90
|
||||||
mapped="$TEST_FAKE_ROOT$destination"
|
code=$3
|
||||||
mkdir -p "$(dirname -- "$mapped")"
|
shell_name=$4
|
||||||
|
original_destination=$5
|
||||||
|
printf 'transaction %s\n' "$original_destination" >>"$TEST_SUDO_LOG"
|
||||||
|
|
||||||
stage=$(find "$TEST_STAGES" -mindepth 1 -maxdepth 2 -type f -name omarchy.script -printf '%h\n' | head -n1)
|
if [[ ${TEST_MUTATE_DEST:-} == "$original_destination" ]]; then
|
||||||
asset=$(basename -- "$destination")
|
expected_size=${7:-0}
|
||||||
source="$stage/$asset"
|
case "$original_destination" in
|
||||||
pinned="$stage/.pinned-$asset"
|
/usr/share/plymouth/themes/omarchy/*)
|
||||||
|
relative=${original_destination#/usr/share/plymouth/themes/omarchy/}
|
||||||
if [[ -n $stage && -f $source && ! -L $source ]]; then
|
stage_kind=plymouth
|
||||||
mv -T -- "$source" "$pinned"
|
;;
|
||||||
ln -s "$TEST_SECRET" "$source"
|
/usr/share/sddm/themes/omarchy/*)
|
||||||
printf '%s\n' "$asset" >>"$TEST_ATTACK_LOG"
|
relative=${original_destination#/usr/share/sddm/themes/omarchy/}
|
||||||
/usr/bin/tee "$mapped"
|
stage_kind=sddm
|
||||||
result=$?
|
;;
|
||||||
rm -f -- "$source"
|
*) exit 91 ;;
|
||||||
mv -T -- "$pinned" "$source"
|
esac
|
||||||
exit "$result"
|
stage_root=$(find "$TEST_STAGES" -mindepth 1 -maxdepth 1 -type d -print -quit)
|
||||||
|
source="$stage_root/$stage_kind/$relative"
|
||||||
|
/usr/bin/head -c "$expected_size" /dev/zero | /usr/bin/tr '\0' X >"$source"
|
||||||
|
printf '%s\n' "$source" >>"$TEST_MUTATE_LOG"
|
||||||
fi
|
fi
|
||||||
exec /usr/bin/tee "$mapped"
|
|
||||||
;;
|
mapped_destination="$TEST_FAKE_ROOT$original_destination"
|
||||||
chmod)
|
shift 5
|
||||||
exec /usr/bin/chmod "$2" "$TEST_FAKE_ROOT$3"
|
|
||||||
;;
|
# The production helper intentionally resets PATH. For this unprivileged
|
||||||
rm)
|
# simulation only, substitute stat/chown shims so a uid-1000 test directory
|
||||||
destination=${@: -1}
|
# behaves like the root-owned /usr/share directory used in production.
|
||||||
exec /usr/bin/rm -f -- "$TEST_FAKE_ROOT$destination"
|
code=${code/PATH=\/usr\/bin:\/bin/PATH=$TEST_ROOT_TOOLS:\/usr\/bin:\/bin}
|
||||||
|
PATH="$TEST_ROOT_TOOLS:/usr/bin:/bin" \
|
||||||
|
/bin/bash -c "$code" "$shell_name" "$mapped_destination" "$@"
|
||||||
;;
|
;;
|
||||||
plymouth-set-default-theme | limine-mkinitcpio | mkinitcpio)
|
plymouth-set-default-theme | limine-mkinitcpio | mkinitcpio)
|
||||||
|
printf 'command %s\n' "$*" >>"$TEST_SUDO_LOG"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "unexpected sudo command: $*" >&2
|
echo "unexpected sudo command: $*" >&2
|
||||||
exit 1
|
exit 92
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
SH
|
SH
|
||||||
|
|
||||||
|
cat >"$root_tools/stat" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
last=${!#}
|
||||||
|
if [[ ${1:-} == -c && ${2:-} == %u && $last == "$TEST_FAKE_ROOT"* ]]; then
|
||||||
|
printf '0\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exec /usr/bin/stat "$@"
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat >"$root_tools/chown" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
last=${!#}
|
||||||
|
[[ $last == "$TEST_FAKE_ROOT"* ]] || exit 93
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
|
||||||
cat >"$fake_bin/magick" <<'SH'
|
cat >"$fake_bin/magick" <<'SH'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
source="$1"
|
source=$1
|
||||||
destination=${@: -1}
|
destination=${@: -1}
|
||||||
[[ $source == "$destination" ]] || /usr/bin/cp -- "$source" "$destination"
|
[[ $source == "$destination" ]] || /usr/bin/cp -- "$source" "$destination"
|
||||||
SH
|
SH
|
||||||
@@ -188,34 +227,227 @@ cat >"$fake_bin/omarchy-cmd-present" <<'SH'
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
exit 1
|
exit 1
|
||||||
SH
|
SH
|
||||||
chmod +x "$fake_bin"/*
|
|
||||||
|
|
||||||
printf 'SIMULATED ROOT-ONLY SECRET\n' >"$secret"
|
chmod +x "$fake_bin"/* "$root_tools"/*
|
||||||
|
|
||||||
printf 'caller-selected logo\n' >"$test_tmp/logo.png"
|
printf 'caller-selected logo\n' >"$test_tmp/logo.png"
|
||||||
|
|
||||||
output=$(PATH="$fake_bin:$ROOT/bin:$PATH" \
|
setup_run() {
|
||||||
|
run_dir=$(mktemp -d "$test_tmp/run.XXXXXXXX")
|
||||||
|
fake_root="$run_dir/root"
|
||||||
|
sudo_log="$run_dir/sudo.log"
|
||||||
|
leak_log="$run_dir/leaked-stage-path.log"
|
||||||
|
mutate_log="$run_dir/mutated.log"
|
||||||
|
theme="$fake_root/usr/share/plymouth/themes/omarchy"
|
||||||
|
sddm="$fake_root/usr/share/sddm/themes/omarchy"
|
||||||
|
|
||||||
|
mkdir -p "$theme/logos" "$sddm"
|
||||||
|
chmod 0755 \
|
||||||
|
"$fake_root/usr" \
|
||||||
|
"$fake_root/usr/share" \
|
||||||
|
"$fake_root/usr/share/plymouth" \
|
||||||
|
"$fake_root/usr/share/plymouth/themes" \
|
||||||
|
"$theme" \
|
||||||
|
"$theme/logos" \
|
||||||
|
"$fake_root/usr/share/sddm" \
|
||||||
|
"$fake_root/usr/share/sddm/themes" \
|
||||||
|
"$sddm"
|
||||||
|
|
||||||
|
local asset destination
|
||||||
|
for asset in "${plymouth_default_assets[@]}"; do
|
||||||
|
destination="$theme/$asset"
|
||||||
|
printf 'old plymouth %s\n' "$asset" >"$destination"
|
||||||
|
chmod 0600 "$destination"
|
||||||
|
done
|
||||||
|
for asset in "${sddm_theme_assets[@]}" metadata.desktop theme.conf; do
|
||||||
|
destination="$sddm/$asset"
|
||||||
|
printf 'old sddm %s\n' "$asset" >"$destination"
|
||||||
|
chmod 0600 "$destination"
|
||||||
|
done
|
||||||
|
|
||||||
|
plymouth_victim="$run_dir/plymouth-victim"
|
||||||
|
sddm_victim="$run_dir/sddm-victim"
|
||||||
|
legacy_victim="$run_dir/legacy-victim"
|
||||||
|
printf 'PLYMOUTH VICTIM\n' >"$plymouth_victim"
|
||||||
|
printf 'SDDM VICTIM\n' >"$sddm_victim"
|
||||||
|
printf 'LEGACY VICTIM\n' >"$legacy_victim"
|
||||||
|
chmod 0600 "$plymouth_victim" "$sddm_victim" "$legacy_victim"
|
||||||
|
|
||||||
|
rm -f "$theme/omarchy.script" "$sddm/Main.qml"
|
||||||
|
ln -s "$plymouth_victim" "$theme/omarchy.script"
|
||||||
|
ln -s "$sddm_victim" "$sddm/Main.qml"
|
||||||
|
ln -s "$legacy_victim" "$sddm/logo.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_set() {
|
||||||
|
local requested_umask="$1"
|
||||||
|
shift
|
||||||
|
(
|
||||||
|
umask "$requested_umask"
|
||||||
|
PATH="$fake_bin:$ROOT/bin:$PATH" \
|
||||||
TMPDIR="$stages" \
|
TMPDIR="$stages" \
|
||||||
OMARCHY_PATH="$ROOT" \
|
OMARCHY_PATH="$ROOT" \
|
||||||
TEST_FAKE_ROOT="$fake_root" \
|
TEST_FAKE_ROOT="$fake_root" \
|
||||||
TEST_STAGES="$stages" \
|
TEST_STAGES="$stages" \
|
||||||
TEST_SECRET="$secret" \
|
TEST_ROOT_TOOLS="$root_tools" \
|
||||||
TEST_ATTACK_LOG="$attack_log" \
|
|
||||||
TEST_SUDO_LOG="$sudo_log" \
|
TEST_SUDO_LOG="$sudo_log" \
|
||||||
bash "$ROOT/bin/omarchy-plymouth-set" '#1d2021' '#ebdbb2' "$test_tmp/logo.png" 2>&1)
|
TEST_LEAK_LOG="$leak_log" \
|
||||||
|
TEST_MUTATE_LOG="$mutate_log" \
|
||||||
|
"$@" \
|
||||||
|
/bin/bash "$ROOT/bin/omarchy-plymouth-set" '#1d2021' '#ebdbb2' "$test_tmp/logo.png"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_no_temporary_files() {
|
||||||
|
local directory="$1" leftovers
|
||||||
|
leftovers=$(find "$directory" -name '.*.omarchy-new.*' -print)
|
||||||
|
[[ -z $leftovers ]] || fail "failed publication cleans up its root-side temporary file" "$leftovers"
|
||||||
|
}
|
||||||
|
|
||||||
|
for requested_umask in 022 027 077; do
|
||||||
|
setup_run
|
||||||
|
output=$(run_set "$requested_umask" env 2>&1)
|
||||||
|
status=$?
|
||||||
|
(( status == 0 )) || fail "Plymouth publisher succeeds under umask $requested_umask" "$output"
|
||||||
|
|
||||||
|
for asset in "${plymouth_theme_assets[@]}"; do
|
||||||
|
destination="$theme/$asset"
|
||||||
|
[[ -f $destination && ! -L $destination ]] || fail "Plymouth $asset is a regular file under umask $requested_umask"
|
||||||
|
[[ $(stat -c %a "$destination") == 644 ]] || fail "Plymouth $asset is mode 0644 under umask $requested_umask"
|
||||||
|
[[ -s $destination ]] || fail "Plymouth $asset is nonempty under umask $requested_umask"
|
||||||
|
[[ $(grep -Fc "transaction /usr/share/plymouth/themes/omarchy/$asset" "$sudo_log") == 1 ]] || fail "Plymouth $asset is published exactly once"
|
||||||
|
done
|
||||||
|
for asset in "${sddm_theme_assets[@]}"; do
|
||||||
|
destination="$sddm/$asset"
|
||||||
|
[[ -f $destination && ! -L $destination ]] || fail "SDDM $asset is a regular file under umask $requested_umask"
|
||||||
|
[[ $(stat -c %a "$destination") == 644 ]] || fail "SDDM $asset is mode 0644 under umask $requested_umask"
|
||||||
|
[[ -s $destination ]] || fail "SDDM $asset is nonempty under umask $requested_umask"
|
||||||
|
[[ $(grep -Fc "transaction /usr/share/sddm/themes/omarchy/$asset" "$sudo_log") == 1 ]] || fail "SDDM $asset is published exactly once"
|
||||||
|
done
|
||||||
|
|
||||||
|
cmp -s "$test_tmp/logo.png" "$theme/logo.png" || fail "Plymouth receives the selected logo under umask $requested_umask"
|
||||||
|
cmp -s "$test_tmp/logo.png" "$sddm/logo.png" || fail "SDDM receives the selected logo under umask $requested_umask"
|
||||||
|
grep -Fq '#1d2021' "$sddm/Main.qml" || fail "SDDM Main.qml receives the selected background under umask $requested_umask"
|
||||||
|
grep -Fq 'Window.SetBackgroundTopColor(0.114, 0.125, 0.129);' "$theme/omarchy.script" || fail "Plymouth script receives the selected background under umask $requested_umask"
|
||||||
|
|
||||||
|
[[ $(cat "$plymouth_victim") == 'PLYMOUTH VICTIM' && $(stat -c %a "$plymouth_victim") == 600 ]] || fail "Plymouth destination symlink never changes its victim"
|
||||||
|
[[ $(cat "$sddm_victim") == 'SDDM VICTIM' && $(stat -c %a "$sddm_victim") == 600 ]] || fail "Main.qml destination symlink never changes its victim"
|
||||||
|
[[ $(cat "$legacy_victim") == 'LEGACY VICTIM' && $(stat -c %a "$legacy_victim") == 600 ]] || fail "legacy logo.svg removal never changes its victim"
|
||||||
|
[[ ! -e $sddm/logo.svg && ! -L $sddm/logo.svg ]] || fail "legacy logo.svg is removed"
|
||||||
|
|
||||||
|
[[ $(cat "$theme/logos/oma.png") == 'old plymouth logos/oma.png' && $(stat -c %a "$theme/logos/oma.png") == 600 ]] || fail "normal theme set does not broaden into the refresh-only nested asset"
|
||||||
|
[[ $(cat "$sddm/metadata.desktop") == 'old sddm metadata.desktop' ]] || fail "normal theme set leaves SDDM metadata unchanged"
|
||||||
|
[[ $(cat "$sddm/theme.conf") == 'old sddm theme.conf' ]] || fail "normal theme set leaves SDDM theme.conf unchanged"
|
||||||
|
[[ ! -s $leak_log ]] || fail "no privileged command receives a user-writable staged pathname" "$(cat "$leak_log")"
|
||||||
|
[[ $(stat -c %a "$theme") == 755 && $(stat -c %a "$sddm") == 755 && $(stat -c %a "$theme/logos") == 755 ]] || fail "publication preserves destination directory modes under umask $requested_umask"
|
||||||
|
assert_no_temporary_files "$fake_root"
|
||||||
|
done
|
||||||
|
|
||||||
|
pass "every Plymouth and SDDM destination is atomically replaced with mode 0644 across restrictive umasks"
|
||||||
|
|
||||||
|
# Swap the first staged source to an unreadable file after all hashes have been
|
||||||
|
# recorded but in the DEBUG hook immediately before Bash opens the redirection.
|
||||||
|
# The caller-side open must fail, so sudo never starts and nothing is published.
|
||||||
|
setup_run
|
||||||
|
preopen_hook="$run_dir/preopen-hook"
|
||||||
|
preopen_marker="$run_dir/preopen-marker"
|
||||||
|
printf 'ROOT ONLY\n' >"$secret"
|
||||||
|
chmod 000 "$secret"
|
||||||
|
cat >"$preopen_hook" <<'SH'
|
||||||
|
if [[ $0 == */bin/omarchy-plymouth-set ]]; then
|
||||||
|
set -T
|
||||||
|
trap '
|
||||||
|
if [[ ${destination:-} == /usr/share/plymouth/themes/omarchy/bullet.png &&
|
||||||
|
$BASH_COMMAND == sudo\ /bin/bash\ -c* &&
|
||||||
|
! -e $TEST_PREOPEN_MARKER ]]; then
|
||||||
|
mv -T -- "$source" "$source.before-preopen-swap"
|
||||||
|
ln -s -- "$TEST_SECRET" "$source"
|
||||||
|
printf "swapped\n" >"$TEST_PREOPEN_MARKER"
|
||||||
|
fi
|
||||||
|
' DEBUG
|
||||||
|
fi
|
||||||
|
SH
|
||||||
|
|
||||||
|
output=$(TEST_PREOPEN_MARKER="$preopen_marker" TEST_SECRET="$secret" BASH_ENV="$preopen_hook" run_set 077 env 2>&1)
|
||||||
|
status=$?
|
||||||
|
chmod 0600 "$secret"
|
||||||
|
|
||||||
|
(( status != 0 )) || fail "an unreadable pre-open source swap aborts publication"
|
||||||
|
[[ -s $preopen_marker ]] || fail "the pre-open source swap ran deterministically" "$output"
|
||||||
|
[[ $(cat "$theme/bullet.png") == 'old plymouth bullet.png' && $(stat -c %a "$theme/bullet.png") == 600 ]] || fail "pre-open failure leaves the live destination unchanged"
|
||||||
|
[[ $(cat "$plymouth_victim") == 'PLYMOUTH VICTIM' ]] || fail "pre-open failure leaves destination-link victims unchanged"
|
||||||
|
if [[ -e $sudo_log ]] && grep -Fq 'transaction /usr/share/plymouth/themes/omarchy/bullet.png' "$sudo_log"; then
|
||||||
|
fail "sudo started despite the caller-side open failure"
|
||||||
|
fi
|
||||||
|
assert_no_temporary_files "$fake_root"
|
||||||
|
|
||||||
|
pass "an unreadable source swap before open fails without publication"
|
||||||
|
|
||||||
|
# Rewrite a staged file in place after Bash has opened it but before root reads
|
||||||
|
# stdin. Size is preserved, so only the recorded SHA-256 can reject this race.
|
||||||
|
setup_run
|
||||||
|
mutate_destination='/usr/share/plymouth/themes/omarchy/omarchy.script'
|
||||||
|
output=$(run_set 022 env TEST_MUTATE_DEST="$mutate_destination" 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
|
|
||||||
((status == 0)) || fail "Plymouth publisher succeeds while staged paths are swapped" "$output"
|
(( status != 0 )) || fail "an in-place rewrite after open aborts publication"
|
||||||
|
[[ -s $mutate_log ]] || fail "the post-open in-place rewrite ran"
|
||||||
|
[[ -L $theme/omarchy.script ]] || fail "failed hash verification leaves the old destination symlink in place"
|
||||||
|
[[ $(cat "$plymouth_victim") == 'PLYMOUTH VICTIM' && $(stat -c %a "$plymouth_victim") == 600 ]] || fail "failed hash verification leaves the destination-link victim unchanged"
|
||||||
|
assert_no_temporary_files "$fake_root"
|
||||||
|
|
||||||
expected_assets=$'bullet.png\nentry-failed.png\nentry.png\nlock-failed.png\nlock.png\nlogo.png\nomarchy.plymouth\nomarchy.script\npreview-unlock.png\nprogress_bar.png\nprogress_box.png'
|
pass "recorded size and SHA-256 reject a same-inode rewrite after open"
|
||||||
actual_assets=$(sort -u "$attack_log")
|
|
||||||
[[ $actual_assets == "$expected_assets" ]] || fail "every staged asset is raced at its privileged publication" "$actual_assets"
|
|
||||||
|
|
||||||
! grep -Rqs 'SIMULATED ROOT-ONLY SECRET' "$fake_root" || fail "a replacement symlink was published"
|
# Root rejects both a symlinked parent and a group/world-writable parent before
|
||||||
grep -Fq 'caller-selected logo' "$fake_root/usr/share/plymouth/themes/omarchy/logo.png" || fail "the descriptor did not preserve the selected logo bytes"
|
# it creates a temporary file or touches the live destination.
|
||||||
[[ $(stat -c %a "$fake_root/usr/share/plymouth/themes/omarchy") == 755 ]] || fail "fixed-file publication changed the theme directory mode"
|
setup_run
|
||||||
|
mv "$theme" "$theme.real"
|
||||||
|
ln -s "$theme.real" "$theme"
|
||||||
|
output=$(run_set 022 env 2>&1)
|
||||||
|
status=$?
|
||||||
|
(( status != 0 )) || fail "a symlinked destination parent is rejected"
|
||||||
|
[[ $(cat "$theme.real/bullet.png") == 'old plymouth bullet.png' ]] || fail "a symlinked parent leaves its target unchanged"
|
||||||
|
assert_no_temporary_files "$fake_root"
|
||||||
|
|
||||||
if grep -F "$stages/" "$sudo_log" >/dev/null; then
|
setup_run
|
||||||
fail "a privileged command received a pathname inside the user-writable stage" "$(cat "$sudo_log")"
|
chmod 0777 "$theme"
|
||||||
fi
|
output=$(run_set 022 env 2>&1)
|
||||||
|
status=$?
|
||||||
|
(( status != 0 )) || fail "a writable destination parent is rejected"
|
||||||
|
[[ $(cat "$theme/bullet.png") == 'old plymouth bullet.png' ]] || fail "a writable parent leaves its live destination unchanged"
|
||||||
|
assert_no_temporary_files "$fake_root"
|
||||||
|
|
||||||
pass "privileged publication uses pinned descriptors for every staged asset"
|
pass "publication rejects symlinked and non-root-writable destination parents"
|
||||||
|
|
||||||
|
# Refresh uses the same publisher but its explicit contract includes the
|
||||||
|
# packaged nested logos/oma.png asset. It must not touch the SDDM theme.
|
||||||
|
setup_run
|
||||||
|
output=$(
|
||||||
|
PATH="$fake_bin:$ROOT/bin:$PATH" \
|
||||||
|
TMPDIR="$stages" \
|
||||||
|
OMARCHY_PATH="$ROOT" \
|
||||||
|
TEST_FAKE_ROOT="$fake_root" \
|
||||||
|
TEST_STAGES="$stages" \
|
||||||
|
TEST_ROOT_TOOLS="$root_tools" \
|
||||||
|
TEST_SUDO_LOG="$sudo_log" \
|
||||||
|
TEST_LEAK_LOG="$leak_log" \
|
||||||
|
TEST_MUTATE_LOG="$mutate_log" \
|
||||||
|
/bin/bash "$ROOT/bin/omarchy-refresh-plymouth" 2>&1
|
||||||
|
)
|
||||||
|
status=$?
|
||||||
|
(( status == 0 )) || fail "Plymouth refresh succeeds through the safe publisher" "$output"
|
||||||
|
|
||||||
|
for asset in "${plymouth_default_assets[@]}"; do
|
||||||
|
destination="$theme/$asset"
|
||||||
|
cmp -s "$ROOT/default/plymouth/$asset" "$destination" || fail "refresh publishes the packaged $asset bytes"
|
||||||
|
[[ -f $destination && ! -L $destination && $(stat -c %a "$destination") == 644 ]] || fail "refresh publishes $asset as a regular mode-0644 file"
|
||||||
|
[[ $(grep -Fc "transaction /usr/share/plymouth/themes/omarchy/$asset" "$sudo_log") == 1 ]] || fail "refresh publishes $asset exactly once"
|
||||||
|
done
|
||||||
|
[[ -L $sddm/Main.qml && $(cat "$sddm_victim") == 'SDDM VICTIM' ]] || fail "Plymouth refresh leaves SDDM unchanged"
|
||||||
|
! grep -Fq 'transaction /usr/share/sddm/' "$sudo_log" || fail "Plymouth refresh does not publish SDDM assets"
|
||||||
|
[[ ! -s $leak_log ]] || fail "refresh never gives root a user-writable source pathname" "$(cat "$leak_log")"
|
||||||
|
|
||||||
|
grep -Fq 'sudo /bin/bash -c' "$ROOT/bin/omarchy-plymouth-set" || fail "publisher invokes Bash by its trusted absolute path"
|
||||||
|
grep -Fq 'PATH=/usr/bin:/bin' "$ROOT/bin/omarchy-plymouth-set" || fail "root helper resets PATH before resolving utilities"
|
||||||
|
|
||||||
|
pass "refresh safely publishes its complete fixed asset set, including logos/oma.png"
|
||||||
|
|||||||
Reference in New Issue
Block a user