Close Panama recovery race windows

This commit is contained in:
Gabriel Brown
2026-08-18 11:11:53 -04:00
parent 8bfae70284
commit faa9a00716
2 changed files with 315 additions and 55 deletions
+159 -32
View File
@@ -5,6 +5,8 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import ctypes
import errno
import json import json
import os import os
import re import re
@@ -23,6 +25,11 @@ from typing import Callable, Literal
Status = Literal["ok", "warning", "error", "unconfigured"] Status = Literal["ok", "warning", "error", "unconfigured"]
Group = Literal["desktop-foundation", "input-media", "integrations", "panama-tools"] Group = Literal["desktop-foundation", "input-media", "integrations", "panama-tools"]
ActionKind = Literal["repair", "open", "instructions"] ActionKind = Literal["repair", "open", "instructions"]
InhibitorRow = tuple[str, str, str, str, str, str, str, str]
AT_FDCWD = -100
RENAME_NOREPLACE = 1
RENAME_EXCHANGE = 2
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -452,10 +459,10 @@ def check_caffeine(config: DoctorConfig) -> Check:
result = run_command(("systemd-inhibit", "--list", "--no-pager", "--no-legend"), config) result = run_command(("systemd-inhibit", "--list", "--no-pager", "--no-legend"), config)
if result.state != "ok": if result.state != "ok":
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe is unavailable.") return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe is unavailable.")
inhibitor_pids = parse_caffeine_pids(result.stdout, str(os.getuid())) inhibitor_rows = parse_caffeine_rows(result.stdout, str(os.getuid()))
if inhibitor_pids is None: if inhibitor_rows is None:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe returned an invalid result.") return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe returned an invalid result.")
inhibitors = len(dict.fromkeys(inhibitor_pids)) inhibitors = len(dict.fromkeys(int(row[3]) for row in inhibitor_rows))
if inhibitors > 1: if inhibitors > 1:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Duplicate Panama Caffeine inhibitors detected.", Action("repair", "Release duplicate inhibitors")) return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Duplicate Panama Caffeine inhibitors detected.", Action("repair", "Release duplicate inhibitors"))
if inhibitors == 1: if inhibitors == 1:
@@ -542,29 +549,120 @@ def lexical_link_target(destination: Path) -> Path:
return lexical_path(target if target.is_absolute() else destination.parent / target) return lexical_path(target if target.is_absolute() else destination.parent / target)
def atomic_symlink_replace(destination: Path, source: Path) -> None: def renameat2(source: Path, destination: Path, flags: int) -> None:
"""Install an authored sibling symlink without first removing destination.""" """Call Linux renameat2 with fixed flags selected by authored code."""
libc = ctypes.CDLL(None, use_errno=True)
function = getattr(libc, "renameat2", None)
if function is None:
raise OSError(errno.ENOSYS, "renameat2 is unavailable")
function.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
function.restype = ctypes.c_int
result = function(
AT_FDCWD,
os.fsencode(source),
AT_FDCWD,
os.fsencode(destination),
flags,
)
if result != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), destination)
def rename_exchange(source: Path, destination: Path) -> None:
renameat2(source, destination, RENAME_EXCHANGE)
def rename_noreplace(source: Path, destination: Path) -> None:
renameat2(source, destination, RENAME_NOREPLACE)
def create_symlink_candidate(destination: Path, source: Path) -> Path:
"""Create one unpredictable authored sibling candidate symlink."""
for _ in range(32): for _ in range(32):
temporary = destination.with_name( candidate = destination.with_name(
f".panama-link-{destination.name}-{os.getpid()}-{secrets.token_hex(8)}" f".panama-link-{destination.name}-{os.getpid()}-{secrets.token_hex(8)}"
) )
created = False
try: try:
os.symlink(source, temporary, target_is_directory=True) os.symlink(source, candidate, target_is_directory=True)
created = True return candidate
os.replace(temporary, destination)
return
except FileExistsError: except FileExistsError:
continue continue
finally:
if created:
try:
temporary.unlink()
except FileNotFoundError:
pass
raise OSError("Could not allocate an authored temporary link") raise OSError("Could not allocate an authored temporary link")
def cleanup_candidate(candidate: Path) -> None:
try:
candidate.unlink()
except FileNotFoundError:
pass
def install_absent_symlink(destination: Path, source: Path) -> Literal["repaired", "blocked", "failed"]:
candidate = create_symlink_candidate(destination, source)
try:
try:
rename_noreplace(candidate, destination)
except FileExistsError:
return "blocked"
except OSError:
return "failed"
return "repaired"
finally:
cleanup_candidate(candidate)
def exchange_owned_symlink(
destination: Path,
source: Path,
authored_sources: frozenset[Path],
) -> Literal["repaired", "blocked", "failed"]:
"""Exchange first, then validate the exact object removed from destination."""
candidate = create_symlink_candidate(destination, source)
exchanged = False
rolled_back = False
try:
try:
rename_exchange(candidate, destination)
exchanged = True
except OSError:
return "failed"
try:
old_is_authored = candidate.is_symlink() \
and lexical_link_target(candidate) in authored_sources
except OSError:
old_is_authored = False
if old_is_authored:
cleanup_candidate(candidate)
return "repaired"
try:
rename_exchange(candidate, destination)
rolled_back = True
except OSError:
# The displaced object remains at the unpredictable candidate path;
# never unlink it when rollback could not restore ownership.
return "failed"
try:
restored_candidate_is_ours = candidate.is_symlink() \
and lexical_link_target(candidate) == source
except OSError:
restored_candidate_is_ours = False
if not restored_candidate_is_ours:
return "failed"
cleanup_candidate(candidate)
return "blocked"
finally:
if not exchanged or rolled_back:
try:
if candidate.is_symlink() and lexical_link_target(candidate) == source:
cleanup_candidate(candidate)
except OSError:
pass
def repair_runtime_links(config: DoctorConfig) -> RepairResult: def repair_runtime_links(config: DoctorConfig) -> RepairResult:
root = lexical_path(config.root) root = lexical_path(config.root)
sources = [(name, lexical_path(config.root / relative_source)) for name, relative_source in RUNTIME_LINK_TARGETS] sources = [(name, lexical_path(config.root / relative_source)) for name, relative_source in RUNTIME_LINK_TARGETS]
@@ -591,13 +689,17 @@ def repair_runtime_links(config: DoctorConfig) -> RepairResult:
if current_target not in authored_sources: if current_target not in authored_sources:
blocked = True blocked = True
continue continue
atomic_symlink_replace(destination, source) outcome = exchange_owned_symlink(destination, source, authored_sources)
blocked = blocked or outcome == "blocked"
failed = failed or outcome == "failed"
elif destination.exists(): elif destination.exists():
# A regular file or directory is user-owned unless proven # A regular file or directory is user-owned unless proven
# otherwise. Report it, but never replace it. # otherwise. Report it, but never replace it.
blocked = True blocked = True
else: else:
atomic_symlink_replace(destination, source) outcome = install_absent_symlink(destination, source)
blocked = blocked or outcome == "blocked"
failed = failed or outcome == "failed"
except OSError: except OSError:
failed = True failed = True
@@ -618,8 +720,8 @@ def repair_vicinae_commands(config: DoctorConfig) -> RepairResult:
return RepairResult("panama.vicinae-commands", True, exit_code, message) return RepairResult("panama.vicinae-commands", True, exit_code, message)
def parse_caffeine_pids(output: str, uid: str) -> list[int] | None: def parse_caffeine_rows(output: str, uid: str) -> list[InhibitorRow] | None:
inhibitor_pids: list[int] = [] inhibitor_rows: list[InhibitorRow] = []
for line in output.splitlines(): for line in output.splitlines():
parts = line.split() parts = line.split()
if len(parts) < 2 or parts[0] != "Panama" or parts[1] != uid: if len(parts) < 2 or parts[0] != "Panama" or parts[1] != uid:
@@ -632,8 +734,8 @@ def parse_caffeine_pids(output: str, uid: str) -> list[int] | None:
continue continue
if not parts[3].isdecimal(): if not parts[3].isdecimal():
return None return None
inhibitor_pids.append(int(parts[3])) inhibitor_rows.append(tuple(parts))
return inhibitor_pids return inhibitor_rows
def close_pidfds(pidfds: list[int]) -> None: def close_pidfds(pidfds: list[int]) -> None:
@@ -644,6 +746,31 @@ def close_pidfds(pidfds: list[int]) -> None:
pass pass
def signal_caffeine_pidfds(
pidfds: list[int],
sender: Callable[..., None] | None = None,
) -> Literal["released", "preflight-failed", "incomplete"]:
send = sender or signal.pidfd_send_signal
for pidfd in pidfds:
try:
send(pidfd, 0, None, 0)
except (OSError, ValueError):
return "preflight-failed"
incomplete = False
for pidfd in pidfds:
try:
send(pidfd, signal.SIGTERM, None, 0)
except ProcessLookupError:
continue
except OSError as error:
if error.errno != errno.ESRCH:
incomplete = True
except ValueError:
incomplete = True
return "incomplete" if incomplete else "released"
def repair_caffeine(config: DoctorConfig) -> RepairResult: def repair_caffeine(config: DoctorConfig) -> RepairResult:
list_command = ("systemd-inhibit", "--list", "--no-pager", "--no-legend") list_command = ("systemd-inhibit", "--list", "--no-pager", "--no-legend")
list_exit, output = run_repair_command(list_command, config) list_exit, output = run_repair_command(list_command, config)
@@ -651,10 +778,10 @@ def repair_caffeine(config: DoctorConfig) -> RepairResult:
return RepairResult("panama.caffeine", True, list_exit, "Caffeine inhibitors could not be inspected.") return RepairResult("panama.caffeine", True, list_exit, "Caffeine inhibitors could not be inspected.")
uid = str(os.getuid()) uid = str(os.getuid())
parsed_pids = parse_caffeine_pids(output, uid) inhibitor_rows = parse_caffeine_rows(output, uid)
if parsed_pids is None: if inhibitor_rows is None:
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.") return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
inhibitor_pids = list(dict.fromkeys(parsed_pids)) inhibitor_pids = list(dict.fromkeys(int(row[3]) for row in inhibitor_rows))
if len(inhibitor_pids) <= 1: if len(inhibitor_pids) <= 1:
return RepairResult("panama.caffeine", True, 0, "No duplicate Panama Caffeine inhibitors needed release.") return RepairResult("panama.caffeine", True, 0, "No duplicate Panama Caffeine inhibitors needed release.")
@@ -673,15 +800,15 @@ def repair_caffeine(config: DoctorConfig) -> RepairResult:
second_exit, second_output = run_repair_command(list_command, config) second_exit, second_output = run_repair_command(list_command, config)
if second_exit != 0: if second_exit != 0:
return RepairResult("panama.caffeine", True, second_exit, "Caffeine inhibitors could not be revalidated; nothing was released.") return RepairResult("panama.caffeine", True, second_exit, "Caffeine inhibitors could not be revalidated; nothing was released.")
second_parsed = parse_caffeine_pids(second_output, uid) second_rows = parse_caffeine_rows(second_output, uid)
if second_parsed is None or any(pid not in set(second_parsed) for pid in duplicates): if second_rows is None or second_rows != inhibitor_rows:
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata changed; nothing was released.") return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata changed; nothing was released.")
try: signal_outcome = signal_caffeine_pidfds(pidfds)
for pidfd in pidfds: if signal_outcome == "preflight-failed":
signal.pidfd_send_signal(pidfd, signal.SIGTERM, None, 0)
except (OSError, ValueError):
return RepairResult("panama.caffeine", True, 1, "A duplicate inhibitor changed before it could be safely released.") return RepairResult("panama.caffeine", True, 1, "A duplicate inhibitor changed before it could be safely released.")
if signal_outcome == "incomplete":
return RepairResult("panama.caffeine", True, 1, "One or more duplicate inhibitors could not be released.")
finally: finally:
close_pidfds(pidfds) close_pidfds(pidfds)
return RepairResult("panama.caffeine", True, 0, "Duplicate Panama Caffeine inhibitors were released. A fresh health check will verify recovery.") return RepairResult("panama.caffeine", True, 0, "Duplicate Panama Caffeine inhibitors were released. A fresh health check will verify recovery.")
+156 -23
View File
@@ -259,12 +259,14 @@ summary="$(run_doctor --summary)"
# Repairs run against a second, disposable Panama root. Every process boundary # Repairs run against a second, disposable Panama root. Every process boundary
# records its argv, and every filesystem assertion is confined to this fixture. # 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" 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 for name in hypr quickshell uwsm vicinae; do
mkdir -p "$repair_root/config/dot/$name" mkdir -p "$repair_root/config/dot/$name"
done done
cp "$repo_dir/setup/scripts/link-vicinae-scripts" "$repair_root/setup/scripts/link-vicinae-scripts"
mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe" mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe"
cat >"$bin_dir/systemctl" <<'EOF' cat >"$bin_dir/systemctl" <<'EOF'
@@ -309,8 +311,14 @@ if [[ "$mode" == disappear && "$count" -ge 2 ]]; then
/usr/bin/sleep 0.01 /usr/bin/sleep 0.01
done done
fi fi
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved" preserved_comm=systemd-inhibit
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved" 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 if [[ "$mode" == altered && "$count" -ge 2 ]]; then
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Other block\n' "$uid" "$duplicate" printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Other block\n' "$uid" "$duplicate"
else 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" printf 'Panama %s fixture-user 4996 systemd-inhibit sleep:idle Caffeine delay\n' "$uid"
EOF 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" \ chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" \
"$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts" "$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts"
run_repair() { run_repair() {
HOME="$home" \ HOME="$home" \
PATH="$bin_dir" \ PATH="$bin_dir:/usr/bin" \
XDG_CURRENT_DESKTOP=Hyprland \ XDG_CURRENT_DESKTOP=Hyprland \
PANAMA_DOCTOR_ROOT="$repair_root" \ PANAMA_DOCTOR_ROOT="$repair_root" \
PANAMA_DOCTOR_HOME="$home" \ PANAMA_DOCTOR_HOME="$home" \
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \ PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
PANAMA_DOCTOR_STATE_HOME="$state_home" \ PANAMA_DOCTOR_STATE_HOME="$state_home" \
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \ PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
PANAMA_DOCTOR_PATH="$bin_dir" \ PANAMA_DOCTOR_PATH="$bin_dir:/usr/bin" \
PANAMA_DOCTOR_TIMEOUT=0.2 \ PANAMA_DOCTOR_TIMEOUT=0.2 \
/usr/bin/python3 "$doctor" "$@" /usr/bin/python3 "$doctor" "$@"
} }
@@ -397,13 +396,19 @@ assert_repair_result desktop.vicinae true 5
[[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \ [[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \
|| fail 'failed repair changed the authored argv' || fail 'failed repair changed the authored argv'
# The Vicinae repair executes only the authored setup helper with no arguments. # The real authored Vicinae helper converges the exact child link diagnosed by
: >"$repair_log" # 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 invoke_repair panama.vicinae-commands
[[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status" [[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status"
assert_repair_result panama.vicinae-commands true 0 assert_repair_result panama.vicinae-commands true 0
[[ "$(<"$repair_log")" == "link-vicinae-scripts|$repair_root/setup/scripts/link-vicinae-scripts" ]] \ after_vicinae_repair="$(run_repair --json)"
|| fail "Vicinae command repair argv was not exact: $(<"$repair_log")" 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 # Runtime-link repair may replace only absent links or symlinks whose lexical
# target proves Panama ownership. Every other object remains untouched. # 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" ]] \ [[ -d "$config_home/vicinae" && ! -L "$config_home/vicinae" ]] \
|| fail 'runtime-link repair replaced a user-owned directory' || 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. # made; the original link must still be intact.
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/atomic-config" <<'PY' \ /usr/bin/python3 - "$doctor" "$repair_root" "$fixture/atomic-config" <<'PY' \
|| fail 'atomic replacement failure did not preserve the original link' || fail 'atomic replacement failure did not preserve the original link'
@@ -471,18 +476,66 @@ destination = config_home / "hypr"
original = root / "config/dot/quickshell" original = root / "config/dot/quickshell"
destination.symlink_to(original, target_is_directory=True) 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) config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
real_replace = module.os.replace real_exchange = module.rename_exchange
module.os.replace = lambda source, target: (_ for _ in ()).throw(OSError("fixture replacement failure")) module.rename_exchange = lambda source, target: (_ for _ in ()).throw(OSError("fixture exchange failure"))
try: try:
result = module.repair_runtime_links(config) result = module.repair_runtime_links(config)
finally: finally:
module.os.replace = real_replace module.rename_exchange = real_exchange
assert result.exit_code == 1 assert result.exit_code == 1
assert destination.is_symlink() assert destination.is_symlink()
assert os.readlink(destination) == str(original) assert os.readlink(destination) == str(original)
assert not list(config_home.glob(".panama-link-*")) assert not list(config_home.glob(".panama-link-*"))
PY 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 # Caffeine repair deduplicates rows, pins each distinct duplicate with a
# pidfd, revalidates authored metadata, and signals only the duplicate. # pidfd, revalidates authored metadata, and signals only the duplicate.
/usr/bin/sleep 30 & /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_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' 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 # A candidate that disappears after pidfd acquisition and second-list request
# is a safe failure; an unrelated disposable process must remain untouched. # is a safe failure; an unrelated disposable process must remain untouched.
/usr/bin/sleep 30 & /usr/bin/sleep 30 &