Harden bounded Panama recovery actions
This commit is contained in:
@@ -16,7 +16,15 @@ fail() {
|
||||
}
|
||||
|
||||
fixture="$(mktemp -d /tmp/panama-doctor.XXXXXX)"
|
||||
trap 'rm -rf "$fixture"' EXIT
|
||||
child_pids=()
|
||||
cleanup() {
|
||||
for pid in "${child_pids[@]}"; do
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
wait "$pid" >/dev/null 2>&1 || true
|
||||
done
|
||||
rm -rf "$fixture"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
home="$fixture/home"
|
||||
config_home="$home/.config"
|
||||
@@ -25,7 +33,7 @@ runtime_dir="$fixture/runtime"
|
||||
bin_dir="$fixture/bin"
|
||||
data_home="$home/.local/share"
|
||||
|
||||
mkdir -p "$config_home" "$state_home" "$runtime_dir" "$bin_dir" "$data_home/vicinae"
|
||||
mkdir -p "$config_home" "$state_home" "$runtime_dir" "$bin_dir" "$data_home/vicinae/scripts"
|
||||
cp "$fixture_root/bin/"* "$bin_dir/"
|
||||
chmod +x "$bin_dir"/*
|
||||
|
||||
@@ -81,7 +89,7 @@ touch "$config_home/autostart/nextcloud.desktop"
|
||||
for name in hypr quickshell uwsm vicinae; do
|
||||
ln -s "$repo_dir/config/dot/$name" "$config_home/$name"
|
||||
done
|
||||
ln -s "$repo_dir/config/local/share/vicinae/scripts" "$data_home/vicinae/scripts"
|
||||
ln -s "$repo_dir/config/local/share/vicinae/scripts" "$data_home/vicinae/scripts/panama"
|
||||
|
||||
run_doctor() {
|
||||
HOME="$home" \
|
||||
@@ -130,6 +138,20 @@ check_status() {
|
||||
|
||||
snapshot="$(run_doctor --json)"
|
||||
assert_schema_and_redaction "$snapshot"
|
||||
check_status "$snapshot" panama.vicinae-commands ok
|
||||
|
||||
# The diagnostic follows the actual installer contract: the scripts parent is
|
||||
# a directory and only its Panama child is an authored link.
|
||||
rm "$data_home/vicinae/scripts/panama"
|
||||
unlinked_vicinae="$(run_doctor --json)"
|
||||
check_status "$unlinked_vicinae" panama.vicinae-commands warning
|
||||
PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_home/vicinae" HOME="$home" \
|
||||
PATH="$bin_dir:/usr/bin" "$repo_dir/setup/scripts/link-vicinae-scripts"
|
||||
relinked_vicinae="$(run_doctor --json)"
|
||||
check_status "$relinked_vicinae" panama.vicinae-commands ok
|
||||
[[ -L "$data_home/vicinae/scripts/panama" \
|
||||
&& "$(readlink "$data_home/vicinae/scripts/panama")" == "$repo_dir/config/local/share/vicinae/scripts" ]] \
|
||||
|| fail 'authored Vicinae helper did not create the diagnosed child link'
|
||||
|
||||
# A healthy systemd-backed service stays healthy.
|
||||
check_status "$snapshot" desktop.hyprpaper ok
|
||||
@@ -266,23 +288,34 @@ printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
EOF
|
||||
|
||||
cat >"$bin_dir/kill" <<'EOF'
|
||||
#!/usr/bin/bash
|
||||
set -euo pipefail
|
||||
printf 'kill' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
EOF
|
||||
|
||||
cat >"$bin_dir/systemd-inhibit" <<'EOF'
|
||||
#!/usr/bin/bash
|
||||
set -euo pipefail
|
||||
printf 'systemd-inhibit' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
count_file="$XDG_RUNTIME_DIR/caffeine-list-count"
|
||||
count=0
|
||||
[[ ! -f "$count_file" ]] || read -r count <"$count_file"
|
||||
count=$((count + 1))
|
||||
printf '%s\n' "$count" >"$count_file"
|
||||
read -r preserved duplicate <"$XDG_RUNTIME_DIR/caffeine-pids"
|
||||
uid="$(/usr/bin/id -u)"
|
||||
printf 'Panama %s fixture-user 4101 systemd-inhibit sleep:idle Caffeine block\n' "$uid"
|
||||
printf 'Panama %s fixture-user 4102 systemd-inhibit sleep:idle Caffeine block\n' "$uid"
|
||||
mode="$(<"$XDG_RUNTIME_DIR/caffeine-mode")"
|
||||
if [[ "$mode" == disappear && "$count" -ge 2 ]]; then
|
||||
/usr/bin/touch "$XDG_RUNTIME_DIR/release-disappearing-pid"
|
||||
for _ in $(/usr/bin/seq 1 100); do
|
||||
[[ ! -e "/proc/$duplicate" ]] && break
|
||||
/usr/bin/sleep 0.01
|
||||
done
|
||||
fi
|
||||
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved"
|
||||
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved"
|
||||
if [[ "$mode" == altered && "$count" -ge 2 ]]; then
|
||||
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Other block\n' "$uid" "$duplicate"
|
||||
else
|
||||
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$duplicate"
|
||||
fi
|
||||
printf 'Other %s fixture-user 4999 systemd-inhibit sleep:idle Caffeine block\n' "$uid"
|
||||
printf 'Panama 99999 fixture-user 4998 systemd-inhibit sleep:idle Caffeine block\n'
|
||||
printf 'Panama %s fixture-user 4997 systemd-inhibit sleep:idle Other block\n' "$uid"
|
||||
@@ -298,7 +331,7 @@ if (( $# > 0 )); then
|
||||
fi
|
||||
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
EOF
|
||||
chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" "$bin_dir/kill" \
|
||||
chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" \
|
||||
"$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts"
|
||||
|
||||
run_repair() {
|
||||
@@ -372,50 +405,185 @@ assert_repair_result panama.vicinae-commands true 0
|
||||
[[ "$(<"$repair_log")" == "link-vicinae-scripts|$repair_root/setup/scripts/link-vicinae-scripts" ]] \
|
||||
|| fail "Vicinae command repair argv was not exact: $(<"$repair_log")"
|
||||
|
||||
# Runtime-link repair may replace only the four authored symlink names. Broken
|
||||
# or absent links are recreated toward authored tracked destinations; regular
|
||||
# files and directories remain untouched and make the result incomplete.
|
||||
# Runtime-link repair may replace only absent links or symlinks whose lexical
|
||||
# target proves Panama ownership. Every other object remains untouched.
|
||||
for name in hypr quickshell uwsm vicinae; do
|
||||
path="$config_home/$name"
|
||||
if [[ -e "$path" || -L "$path" ]]; then
|
||||
mv "$path" "$fixture/pre-repair-$name"
|
||||
fi
|
||||
done
|
||||
ln -s "$fixture/missing-hypr" "$config_home/hypr"
|
||||
ln -s "$fixture/missing-quickshell" "$config_home/quickshell"
|
||||
printf 'user-owned file\n' >"$config_home/uwsm"
|
||||
mkdir "$config_home/vicinae"
|
||||
ln -s "$repair_root/config/dot/hypr" "$config_home/hypr"
|
||||
correct_inode="$(stat -c %i "$config_home/hypr")"
|
||||
ln -s "$repair_root/config/dot/quickshell" "$config_home/uwsm"
|
||||
ln -s "$fixture/external-broken-link" "$config_home/vicinae"
|
||||
ln -s "$fixture/untouched" "$config_home/not-panama"
|
||||
: >"$repair_log"
|
||||
invoke_repair panama.runtime-links
|
||||
[[ "$repair_status" == 1 ]] || fail "blocked runtime-link repair returned $repair_status"
|
||||
assert_repair_result panama.runtime-links true 1
|
||||
[[ -L "$config_home/hypr" && "$(readlink "$config_home/hypr")" == "$repair_root/config/dot/hypr" ]] \
|
||||
|| fail 'hypr link was not recreated toward its authored destination'
|
||||
|| fail 'correct runtime link changed'
|
||||
[[ "$(stat -c %i "$config_home/hypr")" == "$correct_inode" ]] \
|
||||
|| fail 'correct runtime link was replaced instead of left untouched'
|
||||
[[ -L "$config_home/quickshell" && "$(readlink "$config_home/quickshell")" == "$repair_root/config/dot/quickshell" ]] \
|
||||
|| fail 'quickshell link was not recreated toward its authored destination'
|
||||
[[ -f "$config_home/uwsm" && "$(<"$config_home/uwsm")" == 'user-owned file' ]] \
|
||||
|| fail 'runtime-link repair replaced a regular file'
|
||||
[[ -d "$config_home/vicinae" && ! -L "$config_home/vicinae" ]] \
|
||||
|| fail 'runtime-link repair replaced a user-owned directory'
|
||||
|| fail 'absent quickshell link was not created'
|
||||
[[ -L "$config_home/uwsm" && "$(readlink "$config_home/uwsm")" == "$repair_root/config/dot/uwsm" ]] \
|
||||
|| fail 'provably Panama-owned stale link was not repaired'
|
||||
[[ -L "$config_home/vicinae" && "$(readlink "$config_home/vicinae")" == "$fixture/external-broken-link" ]] \
|
||||
|| fail 'external broken symlink was replaced'
|
||||
[[ -L "$config_home/not-panama" && "$(readlink "$config_home/not-panama")" == "$fixture/untouched" ]] \
|
||||
|| fail 'runtime-link repair touched an unauthored link name'
|
||||
[[ ! -s "$repair_log" ]] || fail 'runtime-link repair launched a process'
|
||||
|
||||
# Caffeine repair parses exact authored metadata, keeps the first valid lock,
|
||||
# and releases only later exact matches.
|
||||
# Regular files and directories also remain untouched.
|
||||
rm "$config_home/vicinae"
|
||||
rm "$config_home/uwsm"
|
||||
printf 'user-owned file\n' >"$config_home/uwsm"
|
||||
mkdir "$config_home/vicinae"
|
||||
invoke_repair panama.runtime-links
|
||||
[[ "$repair_status" == 1 ]] || fail 'file/directory blockers did not make repair incomplete'
|
||||
[[ -f "$config_home/uwsm" && "$(<"$config_home/uwsm")" == 'user-owned file' ]] \
|
||||
|| fail 'runtime-link repair replaced a regular file'
|
||||
[[ -d "$config_home/vicinae" && ! -L "$config_home/vicinae" ]] \
|
||||
|| fail 'runtime-link repair replaced a user-owned directory'
|
||||
|
||||
# An injected os.replace failure occurs after the authored temporary symlink is
|
||||
# made; the original link must still be intact.
|
||||
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/atomic-config" <<'PY' \
|
||||
|| fail 'atomic replacement failure did not preserve the original link'
|
||||
import importlib.util
|
||||
import importlib.machinery
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
doctor_path, root_text, config_text = sys.argv[1:]
|
||||
loader = importlib.machinery.SourceFileLoader("panama_doctor_contract", doctor_path)
|
||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
loader.exec_module(module)
|
||||
root = Path(root_text)
|
||||
config_home = Path(config_text)
|
||||
config_home.mkdir(parents=True)
|
||||
destination = config_home / "hypr"
|
||||
original = root / "config/dot/quickshell"
|
||||
destination.symlink_to(original, target_is_directory=True)
|
||||
config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
|
||||
real_replace = module.os.replace
|
||||
module.os.replace = lambda source, target: (_ for _ in ()).throw(OSError("fixture replacement failure"))
|
||||
try:
|
||||
result = module.repair_runtime_links(config)
|
||||
finally:
|
||||
module.os.replace = real_replace
|
||||
assert result.exit_code == 1
|
||||
assert destination.is_symlink()
|
||||
assert os.readlink(destination) == str(original)
|
||||
assert not list(config_home.glob(".panama-link-*"))
|
||||
PY
|
||||
|
||||
# Caffeine repair deduplicates rows, pins each distinct duplicate with a
|
||||
# pidfd, revalidates authored metadata, and signals only the duplicate.
|
||||
/usr/bin/sleep 30 &
|
||||
preserved_pid=$!
|
||||
child_pids+=("$preserved_pid")
|
||||
/usr/bin/sleep 30 &
|
||||
duplicate_pid=$!
|
||||
child_pids+=("$duplicate_pid")
|
||||
printf '%s %s\n' "$preserved_pid" "$duplicate_pid" >"$runtime_dir/caffeine-pids"
|
||||
printf 'dedupe\n' >"$runtime_dir/caffeine-mode"
|
||||
rm -f "$runtime_dir/caffeine-list-count"
|
||||
: >"$repair_log"
|
||||
invoke_repair panama.caffeine
|
||||
[[ "$repair_status" == 0 ]] || fail "Caffeine repair returned $repair_status"
|
||||
assert_repair_result panama.caffeine true 0
|
||||
expected_caffeine=$'systemd-inhibit|--list|--no-pager|--no-legend\nkill|--|4102'
|
||||
expected_caffeine=$'systemd-inhibit|--list|--no-pager|--no-legend\nsystemd-inhibit|--list|--no-pager|--no-legend'
|
||||
[[ "$(<"$repair_log")" == "$expected_caffeine" ]] \
|
||||
|| fail "Caffeine repair did not preserve/filter exact inhibitors: $(<"$repair_log")"
|
||||
kill -0 "$preserved_pid" >/dev/null 2>&1 || fail 'repeated inhibitor rows killed the preserved process'
|
||||
for _ in $(seq 1 40); do
|
||||
kill -0 "$duplicate_pid" >/dev/null 2>&1 || break
|
||||
sleep 0.05
|
||||
done
|
||||
! kill -0 "$duplicate_pid" >/dev/null 2>&1 || fail 'distinct duplicate inhibitor was not terminated'
|
||||
|
||||
# Changed second-list metadata invalidates the candidate before any signal.
|
||||
/usr/bin/sleep 30 &
|
||||
altered_preserved=$!
|
||||
child_pids+=("$altered_preserved")
|
||||
/usr/bin/sleep 30 &
|
||||
altered_duplicate=$!
|
||||
child_pids+=("$altered_duplicate")
|
||||
printf '%s %s\n' "$altered_preserved" "$altered_duplicate" >"$runtime_dir/caffeine-pids"
|
||||
printf 'altered\n' >"$runtime_dir/caffeine-mode"
|
||||
rm -f "$runtime_dir/caffeine-list-count"
|
||||
invoke_repair panama.caffeine
|
||||
[[ "$repair_status" == 1 ]] || fail 'altered inhibitor metadata was not safely refused'
|
||||
assert_repair_result panama.caffeine true 1
|
||||
kill -0 "$altered_preserved" >/dev/null 2>&1 || fail 'metadata refusal signaled the preserved process'
|
||||
kill -0 "$altered_duplicate" >/dev/null 2>&1 || fail 'metadata refusal signaled the candidate process'
|
||||
|
||||
# A candidate that disappears after pidfd acquisition and second-list request
|
||||
# is a safe failure; an unrelated disposable process must remain untouched.
|
||||
/usr/bin/sleep 30 &
|
||||
unrelated_pid=$!
|
||||
child_pids+=("$unrelated_pid")
|
||||
(
|
||||
/usr/bin/sleep 30 &
|
||||
disappearing_pid=$!
|
||||
trap 'kill "$disappearing_pid" >/dev/null 2>&1 || true; wait "$disappearing_pid" >/dev/null 2>&1 || true' EXIT
|
||||
printf '%s\n' "$disappearing_pid" >"$runtime_dir/disappearing-pid"
|
||||
while [[ ! -e "$runtime_dir/release-disappearing-pid" ]]; do
|
||||
/usr/bin/sleep 0.01
|
||||
done
|
||||
kill "$disappearing_pid"
|
||||
wait "$disappearing_pid" >/dev/null 2>&1 || true
|
||||
trap - EXIT
|
||||
) &
|
||||
disappearance_controller=$!
|
||||
child_pids+=("$disappearance_controller")
|
||||
for _ in $(seq 1 100); do
|
||||
[[ -s "$runtime_dir/disappearing-pid" ]] && break
|
||||
sleep 0.01
|
||||
done
|
||||
[[ -s "$runtime_dir/disappearing-pid" ]] || fail 'disappearing PID fixture did not start'
|
||||
disappearing_pid="$(<"$runtime_dir/disappearing-pid")"
|
||||
printf '%s %s\n' "$altered_preserved" "$disappearing_pid" >"$runtime_dir/caffeine-pids"
|
||||
printf 'disappear\n' >"$runtime_dir/caffeine-mode"
|
||||
rm -f "$runtime_dir/caffeine-list-count"
|
||||
invoke_repair panama.caffeine
|
||||
[[ "$repair_status" == 1 ]] || fail 'disappeared inhibitor PID was not safely refused'
|
||||
assert_repair_result panama.caffeine true 1
|
||||
wait "$disappearance_controller"
|
||||
kill -0 "$unrelated_pid" >/dev/null 2>&1 || fail 'PID disappearance signaled an unrelated process'
|
||||
|
||||
# Rejected IDs are complete JSON, exit 2, and cause neither a process launch
|
||||
# nor a filesystem mutation.
|
||||
fixture_state() {
|
||||
find "$config_home" -mindepth 1 -printf '%P|%y|%l\n' | sort | sha256sum | awk '{print $1}'
|
||||
/usr/bin/python3 - "$fixture" <<'PY'
|
||||
import hashlib
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(root.rglob("*"), key=lambda item: os.fsencode(str(item.relative_to(root)))):
|
||||
relative = os.fsencode(str(path.relative_to(root)))
|
||||
metadata = path.lstat()
|
||||
digest.update(relative + b"\0" + oct(stat.S_IMODE(metadata.st_mode)).encode() + b"\0")
|
||||
if path.is_symlink():
|
||||
digest.update(b"link\0" + os.fsencode(os.readlink(path)) + b"\0")
|
||||
elif path.is_file():
|
||||
digest.update(b"file\0" + hashlib.sha256(path.read_bytes()).digest())
|
||||
elif path.is_dir():
|
||||
digest.update(b"dir\0")
|
||||
else:
|
||||
digest.update(b"other\0")
|
||||
print(digest.hexdigest())
|
||||
PY
|
||||
}
|
||||
for rejected_id in unknown.check integration.home-assistant input.brightness \
|
||||
desktop.notifications ../../escape 'desktop.vicinae;touch injected'; do
|
||||
|
||||
Reference in New Issue
Block a user