[Security] Pin Plymouth asset reads before elevation
This commit is contained in:
@@ -51,6 +51,15 @@ theme_dir="/usr/share/plymouth/themes/omarchy"
|
|||||||
staging_dir=$(mktemp -d)
|
staging_dir=$(mktemp -d)
|
||||||
trap 'rm -rf "$staging_dir"' EXIT
|
trap 'rm -rf "$staging_dir"' EXIT
|
||||||
|
|
||||||
|
# Publish a staged asset without ever asking a privileged process to resolve
|
||||||
|
# its user-writable source path. The shell opens source before sudo starts, so
|
||||||
|
# the inherited descriptor pins the bytes and a replacement symlink to a
|
||||||
|
# root-only file either loses the race or fails under the caller's permissions.
|
||||||
|
publish_asset() {
|
||||||
|
local source="$1" destination="$2"
|
||||||
|
sudo tee "$destination" <"$source" >/dev/null && sudo chmod 0644 "$destination"
|
||||||
|
}
|
||||||
|
|
||||||
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
|
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
|
||||||
cp "$logo_path" "$staging_dir/logo.png"
|
cp "$logo_path" "$staging_dir/logo.png"
|
||||||
|
|
||||||
@@ -63,7 +72,9 @@ 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"
|
magick "$staging_dir/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$staging_dir/$asset"
|
||||||
done
|
done
|
||||||
|
|
||||||
sudo cp -a --no-preserve=mode,ownership "$staging_dir/." "$theme_dir/"
|
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
|
||||||
|
publish_asset "$staging_dir/$asset" "$theme_dir/$asset" || exit 1
|
||||||
|
done
|
||||||
sudo plymouth-set-default-theme omarchy
|
sudo plymouth-set-default-theme omarchy
|
||||||
|
|
||||||
if omarchy-cmd-present limine-mkinitcpio; then
|
if omarchy-cmd-present limine-mkinitcpio; then
|
||||||
@@ -81,12 +92,12 @@ sed \
|
|||||||
-e "s/#ffffff/#$text_hex/g" \
|
-e "s/#ffffff/#$text_hex/g" \
|
||||||
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
||||||
|
|
||||||
sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"
|
publish_asset "$staging_dir/logo.png" "$sddm_dir/logo.png" || exit 1
|
||||||
for asset in bullet.png entry.png lock.png; do
|
for asset in bullet.png entry.png lock.png; do
|
||||||
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset"
|
publish_asset "$staging_dir/$asset" "$sddm_dir/$asset" || exit 1
|
||||||
done
|
done
|
||||||
for asset in entry lock; do
|
for asset in entry lock; do
|
||||||
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png"
|
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png"
|
||||||
sudo cp "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png"
|
publish_asset "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png" || exit 1
|
||||||
done
|
done
|
||||||
sudo rm -f "$sddm_dir/logo.svg"
|
sudo rm -f "$sddm_dir/logo.svg"
|
||||||
|
|||||||
@@ -3,30 +3,11 @@
|
|||||||
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 'rm -rf "$test_tmp"' EXIT
|
trap 'chmod 0600 "$test_tmp/secret" 2>/dev/null || true; rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
source_dir="$test_tmp/source"
|
|
||||||
theme_dir="$test_tmp/theme"
|
|
||||||
|
|
||||||
mkdir -m 0700 "$source_dir"
|
|
||||||
mkdir -m 0755 "$theme_dir"
|
|
||||||
touch "$source_dir/logo.png"
|
|
||||||
|
|
||||||
cp -a --no-preserve=mode,ownership "$source_dir/." "$theme_dir/"
|
|
||||||
|
|
||||||
[[ $(stat -c %a "$theme_dir") == "755" ]] ||
|
|
||||||
fail "Plymouth asset copy preserves the theme directory permissions"
|
|
||||||
|
|
||||||
grep -Fq \
|
|
||||||
'cp -a --no-preserve=mode,ownership "$staging_dir/." "$theme_dir/"' \
|
|
||||||
"$ROOT/bin/omarchy-plymouth-set" ||
|
|
||||||
fail "omarchy-plymouth-set avoids copying staging directory ownership and mode"
|
|
||||||
|
|
||||||
pass "Plymouth asset copy preserves the package-owned directory metadata"
|
|
||||||
|
|
||||||
# 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, and both copies below land in world-readable
|
# ~/.config/omarchy/themes. Both installed copies are world-readable, so a
|
||||||
# /usr/share, so a symlink there would republish whatever it points at.
|
# symlink there must not republish whatever it points at.
|
||||||
secret="$test_tmp/secret"
|
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"
|
||||||
@@ -34,12 +15,9 @@ 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" 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"
|
||||||
|
|
||||||
grep -Fq 'sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"' "$ROOT/bin/omarchy-plymouth-set" ||
|
|
||||||
fail "omarchy-plymouth-set copies the staged logo to SDDM rather than rereading the caller's path as root"
|
|
||||||
|
|
||||||
pass "a themed logo cannot republish a file it merely points at"
|
pass "a themed logo cannot republish a file it merely points at"
|
||||||
|
|
||||||
# Style > Unlock picks a theme by name and hands the answer to
|
# Style > Unlock picks a theme by name and hands the answer to
|
||||||
@@ -141,3 +119,103 @@ run_unlock_action "default"
|
|||||||
[[ ! -e $set_args ]] || fail "picking default does not look up a theme named default" "$(cat "$set_args")"
|
[[ ! -e $set_args ]] || fail "picking default does not look up a theme named default" "$(cat "$set_args")"
|
||||||
|
|
||||||
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_root="$test_tmp/root"
|
||||||
|
stages="$test_tmp/stages"
|
||||||
|
attack_log="$test_tmp/attacked"
|
||||||
|
sudo_log="$test_tmp/sudo.log"
|
||||||
|
mkdir -p "$fake_bin" "$fake_root" "$stages"
|
||||||
|
|
||||||
|
cat >"$fake_bin/sudo" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
printf '%s\n' "$*" >>"$TEST_SUDO_LOG"
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
tee)
|
||||||
|
destination="$2"
|
||||||
|
mapped="$TEST_FAKE_ROOT$destination"
|
||||||
|
mkdir -p "$(dirname -- "$mapped")"
|
||||||
|
|
||||||
|
stage=$(find "$TEST_STAGES" -mindepth 1 -maxdepth 2 -type f -name omarchy.script -printf '%h\n' | head -n1)
|
||||||
|
asset=$(basename -- "$destination")
|
||||||
|
source="$stage/$asset"
|
||||||
|
pinned="$stage/.pinned-$asset"
|
||||||
|
|
||||||
|
if [[ -n $stage && -f $source && ! -L $source ]]; then
|
||||||
|
mv -T -- "$source" "$pinned"
|
||||||
|
ln -s "$TEST_SECRET" "$source"
|
||||||
|
printf '%s\n' "$asset" >>"$TEST_ATTACK_LOG"
|
||||||
|
/usr/bin/tee "$mapped"
|
||||||
|
result=$?
|
||||||
|
rm -f -- "$source"
|
||||||
|
mv -T -- "$pinned" "$source"
|
||||||
|
exit "$result"
|
||||||
|
fi
|
||||||
|
exec /usr/bin/tee "$mapped"
|
||||||
|
;;
|
||||||
|
chmod)
|
||||||
|
exec /usr/bin/chmod "$2" "$TEST_FAKE_ROOT$3"
|
||||||
|
;;
|
||||||
|
rm)
|
||||||
|
destination=${@: -1}
|
||||||
|
exec /usr/bin/rm -f -- "$TEST_FAKE_ROOT$destination"
|
||||||
|
;;
|
||||||
|
plymouth-set-default-theme | limine-mkinitcpio | mkinitcpio)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unexpected sudo command: $*" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat >"$fake_bin/magick" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
source="$1"
|
||||||
|
destination=${@: -1}
|
||||||
|
[[ $source == "$destination" ]] || /usr/bin/cp -- "$source" "$destination"
|
||||||
|
SH
|
||||||
|
|
||||||
|
cat >"$fake_bin/omarchy-cmd-present" <<'SH'
|
||||||
|
#!/bin/bash
|
||||||
|
exit 1
|
||||||
|
SH
|
||||||
|
chmod +x "$fake_bin"/*
|
||||||
|
|
||||||
|
printf 'SIMULATED ROOT-ONLY SECRET\n' >"$secret"
|
||||||
|
printf 'caller-selected logo\n' >"$test_tmp/logo.png"
|
||||||
|
|
||||||
|
output=$(PATH="$fake_bin:$ROOT/bin:$PATH" \
|
||||||
|
TMPDIR="$stages" \
|
||||||
|
OMARCHY_PATH="$ROOT" \
|
||||||
|
TEST_FAKE_ROOT="$fake_root" \
|
||||||
|
TEST_STAGES="$stages" \
|
||||||
|
TEST_SECRET="$secret" \
|
||||||
|
TEST_ATTACK_LOG="$attack_log" \
|
||||||
|
TEST_SUDO_LOG="$sudo_log" \
|
||||||
|
bash "$ROOT/bin/omarchy-plymouth-set" '#1d2021' '#ebdbb2' "$test_tmp/logo.png" 2>&1)
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
((status == 0)) || fail "Plymouth publisher succeeds while staged paths are swapped" "$output"
|
||||||
|
|
||||||
|
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'
|
||||||
|
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"
|
||||||
|
grep -Fq 'caller-selected logo' "$fake_root/usr/share/plymouth/themes/omarchy/logo.png" || fail "the descriptor did not preserve the selected logo bytes"
|
||||||
|
[[ $(stat -c %a "$fake_root/usr/share/plymouth/themes/omarchy") == 755 ]] || fail "fixed-file publication changed the theme directory mode"
|
||||||
|
|
||||||
|
if grep -F "$stages/" "$sudo_log" >/dev/null; then
|
||||||
|
fail "a privileged command received a pathname inside the user-writable stage" "$(cat "$sudo_log")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pass "privileged publication uses pinned descriptors for every staged asset"
|
||||||
|
|||||||
Reference in New Issue
Block a user