Close Panama recovery race windows
This commit is contained in:
@@ -259,12 +259,14 @@ summary="$(run_doctor --summary)"
|
||||
|
||||
# Repairs run against a second, disposable Panama root. Every process boundary
|
||||
# records its argv, and every filesystem assertion is confined to this fixture.
|
||||
repair_root="$fixture/repair-root"
|
||||
repair_root="$home/.local/share/Panama"
|
||||
repair_log="$runtime_dir/repair.log"
|
||||
mkdir -p "$repair_root/config/dot" "$repair_root/setup/scripts"
|
||||
mkdir -p "$repair_root/config/dot" "$repair_root/config/local/share/vicinae/scripts" \
|
||||
"$repair_root/setup/scripts"
|
||||
for name in hypr quickshell uwsm vicinae; do
|
||||
mkdir -p "$repair_root/config/dot/$name"
|
||||
done
|
||||
cp "$repo_dir/setup/scripts/link-vicinae-scripts" "$repair_root/setup/scripts/link-vicinae-scripts"
|
||||
|
||||
mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe"
|
||||
cat >"$bin_dir/systemctl" <<'EOF'
|
||||
@@ -309,8 +311,14 @@ if [[ "$mode" == disappear && "$count" -ge 2 ]]; then
|
||||
/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"
|
||||
preserved_comm=systemd-inhibit
|
||||
if [[ "$mode" == preserve-altered && "$count" -ge 2 ]]; then
|
||||
preserved_comm=changed-command
|
||||
fi
|
||||
printf 'Panama %s fixture-user %s %s sleep:idle Caffeine block\n' "$uid" "$preserved" "$preserved_comm"
|
||||
if [[ "$mode" != multiplicity || "$count" -lt 2 ]]; then
|
||||
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved"
|
||||
fi
|
||||
if [[ "$mode" == altered && "$count" -ge 2 ]]; then
|
||||
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Other block\n' "$uid" "$duplicate"
|
||||
else
|
||||
@@ -322,28 +330,19 @@ printf 'Panama %s fixture-user 4997 systemd-inhibit sleep:idle Other block\n' "$
|
||||
printf 'Panama %s fixture-user 4996 systemd-inhibit sleep:idle Caffeine delay\n' "$uid"
|
||||
EOF
|
||||
|
||||
cat >"$repair_root/setup/scripts/link-vicinae-scripts" <<'EOF'
|
||||
#!/usr/bin/bash
|
||||
set -euo pipefail
|
||||
printf 'link-vicinae-scripts|%s' "$0" >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
if (( $# > 0 )); then
|
||||
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
fi
|
||||
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
|
||||
EOF
|
||||
chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" \
|
||||
"$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts"
|
||||
|
||||
run_repair() {
|
||||
HOME="$home" \
|
||||
PATH="$bin_dir" \
|
||||
PATH="$bin_dir:/usr/bin" \
|
||||
XDG_CURRENT_DESKTOP=Hyprland \
|
||||
PANAMA_DOCTOR_ROOT="$repair_root" \
|
||||
PANAMA_DOCTOR_HOME="$home" \
|
||||
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
|
||||
PANAMA_DOCTOR_STATE_HOME="$state_home" \
|
||||
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
|
||||
PANAMA_DOCTOR_PATH="$bin_dir" \
|
||||
PANAMA_DOCTOR_PATH="$bin_dir:/usr/bin" \
|
||||
PANAMA_DOCTOR_TIMEOUT=0.2 \
|
||||
/usr/bin/python3 "$doctor" "$@"
|
||||
}
|
||||
@@ -397,13 +396,19 @@ assert_repair_result desktop.vicinae true 5
|
||||
[[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \
|
||||
|| fail 'failed repair changed the authored argv'
|
||||
|
||||
# The Vicinae repair executes only the authored setup helper with no arguments.
|
||||
: >"$repair_log"
|
||||
# The real authored Vicinae helper converges the exact child link diagnosed by
|
||||
# panama-doctor under the isolated HOME.
|
||||
rm -f "$data_home/vicinae/scripts/panama"
|
||||
before_vicinae_repair="$(run_repair --json)"
|
||||
check_status "$before_vicinae_repair" panama.vicinae-commands warning
|
||||
invoke_repair panama.vicinae-commands
|
||||
[[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status"
|
||||
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")"
|
||||
after_vicinae_repair="$(run_repair --json)"
|
||||
check_status "$after_vicinae_repair" panama.vicinae-commands ok
|
||||
[[ -L "$data_home/vicinae/scripts/panama" \
|
||||
&& "$(readlink "$data_home/vicinae/scripts/panama")" == "$repair_root/config/local/share/vicinae/scripts" ]] \
|
||||
|| fail 'Vicinae repair did not install the diagnosed child link'
|
||||
|
||||
# Runtime-link repair may replace only absent links or symlinks whose lexical
|
||||
# target proves Panama ownership. Every other object remains untouched.
|
||||
@@ -448,7 +453,7 @@ invoke_repair panama.runtime-links
|
||||
[[ -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
|
||||
# An injected exchange failure occurs after the authored candidate 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'
|
||||
@@ -471,18 +476,66 @@ 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"))
|
||||
real_exchange = module.rename_exchange
|
||||
module.rename_exchange = lambda source, target: (_ for _ in ()).throw(OSError("fixture exchange failure"))
|
||||
try:
|
||||
result = module.repair_runtime_links(config)
|
||||
finally:
|
||||
module.os.replace = real_replace
|
||||
module.rename_exchange = real_exchange
|
||||
assert result.exit_code == 1
|
||||
assert destination.is_symlink()
|
||||
assert os.readlink(destination) == str(original)
|
||||
assert not list(config_home.glob(".panama-link-*"))
|
||||
PY
|
||||
|
||||
# A deterministic swap at the ownership/replacement boundary must be detected
|
||||
# from the exchanged-out object and rolled back, preserving the external link.
|
||||
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/toctou-config" "$fixture/external-race-target" <<'PY' \
|
||||
|| fail 'runtime-link exchange did not restore a boundary-swapped external link'
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
doctor_path, root_text, config_text, external_text = sys.argv[1:]
|
||||
loader = importlib.machinery.SourceFileLoader("panama_doctor_toctou", 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)
|
||||
for name, relative in module.RUNTIME_LINK_TARGETS:
|
||||
(config_home / name).symlink_to(root / relative, target_is_directory=True)
|
||||
destination = config_home / "uwsm"
|
||||
destination.unlink()
|
||||
destination.symlink_to(root / "config/dot/quickshell", target_is_directory=True)
|
||||
external = Path(external_text)
|
||||
real_exchange = module.rename_exchange
|
||||
first = True
|
||||
|
||||
def race_exchange(candidate, target):
|
||||
global first
|
||||
if first:
|
||||
first = False
|
||||
target.unlink()
|
||||
target.symlink_to(external, target_is_directory=True)
|
||||
real_exchange(candidate, target)
|
||||
|
||||
module.rename_exchange = race_exchange
|
||||
config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
|
||||
try:
|
||||
result = module.repair_runtime_links(config)
|
||||
finally:
|
||||
module.rename_exchange = real_exchange
|
||||
assert result.exit_code == 1
|
||||
assert destination.is_symlink()
|
||||
assert os.readlink(destination) == str(external)
|
||||
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 &
|
||||
@@ -524,6 +577,86 @@ 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'
|
||||
|
||||
# Changing metadata on the preserved row is also a full-identity mismatch,
|
||||
# even though every duplicate PID remains present.
|
||||
/usr/bin/sleep 30 &
|
||||
preserve_changed_keep=$!
|
||||
child_pids+=("$preserve_changed_keep")
|
||||
/usr/bin/sleep 30 &
|
||||
preserve_changed_duplicate=$!
|
||||
child_pids+=("$preserve_changed_duplicate")
|
||||
printf '%s %s\n' "$preserve_changed_keep" "$preserve_changed_duplicate" >"$runtime_dir/caffeine-pids"
|
||||
printf 'preserve-altered\n' >"$runtime_dir/caffeine-mode"
|
||||
rm -f "$runtime_dir/caffeine-list-count"
|
||||
invoke_repair panama.caffeine
|
||||
[[ "$repair_status" == 1 ]] || fail 'preserved-row metadata change was not safely refused'
|
||||
assert_repair_result panama.caffeine true 1
|
||||
kill -0 "$preserve_changed_keep" >/dev/null 2>&1 || fail 'preserved-row mismatch signaled the preserved process'
|
||||
kill -0 "$preserve_changed_duplicate" >/dev/null 2>&1 || fail 'preserved-row mismatch signaled the duplicate process'
|
||||
|
||||
# A repeated exact row disappearing between lists changes multiplicity and is
|
||||
# refused before signaling any pinned duplicate.
|
||||
/usr/bin/sleep 30 &
|
||||
multiplicity_keep=$!
|
||||
child_pids+=("$multiplicity_keep")
|
||||
/usr/bin/sleep 30 &
|
||||
multiplicity_duplicate=$!
|
||||
child_pids+=("$multiplicity_duplicate")
|
||||
printf '%s %s\n' "$multiplicity_keep" "$multiplicity_duplicate" >"$runtime_dir/caffeine-pids"
|
||||
printf 'multiplicity\n' >"$runtime_dir/caffeine-mode"
|
||||
rm -f "$runtime_dir/caffeine-list-count"
|
||||
invoke_repair panama.caffeine
|
||||
[[ "$repair_status" == 1 ]] || fail 'inhibitor row multiplicity change was not safely refused'
|
||||
assert_repair_result panama.caffeine true 1
|
||||
kill -0 "$multiplicity_keep" >/dev/null 2>&1 || fail 'multiplicity mismatch signaled the preserved process'
|
||||
kill -0 "$multiplicity_duplicate" >/dev/null 2>&1 || fail 'multiplicity mismatch signaled the duplicate process'
|
||||
|
||||
# The production pidfd release function preflights every candidate before any
|
||||
# SIGTERM. A refused second preflight leaves both disposable children alive.
|
||||
/usr/bin/sleep 30 &
|
||||
preflight_first=$!
|
||||
child_pids+=("$preflight_first")
|
||||
/usr/bin/sleep 30 &
|
||||
preflight_second=$!
|
||||
child_pids+=("$preflight_second")
|
||||
/usr/bin/python3 - "$doctor" "$preflight_first" "$preflight_second" <<'PY' \
|
||||
|| fail 'pidfd preflight failure signaled a disposable duplicate'
|
||||
import errno
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
doctor_path = sys.argv[1]
|
||||
pids = [int(value) for value in sys.argv[2:]]
|
||||
loader = importlib.machinery.SourceFileLoader("panama_doctor_preflight", 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)
|
||||
pidfds = [os.pidfd_open(pid, 0) for pid in pids]
|
||||
calls = []
|
||||
|
||||
def sender(pidfd, sig, siginfo, flags):
|
||||
calls.append(sig)
|
||||
if sig == 0 and pidfd == pidfds[1]:
|
||||
raise PermissionError(errno.EPERM, "fixture preflight refusal")
|
||||
signal.pidfd_send_signal(pidfd, sig, siginfo, flags)
|
||||
|
||||
try:
|
||||
outcome = module.signal_caffeine_pidfds(pidfds, sender)
|
||||
finally:
|
||||
for pidfd in pidfds:
|
||||
os.close(pidfd)
|
||||
assert outcome == "preflight-failed"
|
||||
assert calls == [0, 0]
|
||||
for pid in pids:
|
||||
os.kill(pid, 0)
|
||||
PY
|
||||
kill -0 "$preflight_first" >/dev/null 2>&1 || fail 'preflight refusal killed the first duplicate'
|
||||
kill -0 "$preflight_second" >/dev/null 2>&1 || fail 'preflight refusal killed the second duplicate'
|
||||
|
||||
# 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 &
|
||||
|
||||
Reference in New Issue
Block a user