fix: preserve tester session identity through cleanup (#1873)

This commit was merged in pull request #1931.
This commit is contained in:
jason.woltje committed 2026-11-11 08:22:31 -06:00
1 parent f382699d47
commit 4a122699a0
8 files changed
+177 -53

No files matched your search

+56 -16
View File
@@ -136,12 +136,15 @@ trap 'rm -rf -- "$WORK"' EXIT
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
child="" timer="" session_ref="" verified=0
stat_fields() {
local pid="$1" raw="" rest
local pid="$1" raw="" rest file="/proc/$1/stat"
if [ "$pid" = "${child:-}" ] && [ -n "${session_ref:-}" ]; then
file="/proc/self/fd/$session_ref/stat"
fi
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw 2>/dev/null < "/proc/$pid/stat" || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
IFS= read -r -d '' raw 2>/dev/null < "$file" || :
[ -n "$raw" ] || { [ -e "$file" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
@@ -154,6 +157,7 @@ clean_session() {
import os, signal, sys, time
session = int(sys.argv[1])
until = None
anchor_fd = None
def fields(pid):
try:
@@ -177,11 +181,22 @@ def fields(pid):
raise ValueError("stat")
return words[0], int(words[3])
def original_leader():
# The inherited proc directory refers to the original task, even if its
# numeric PID is reused. A live trusted leader keeps this SID allocated.
info = fields("self/fd/" + sys.argv[2])
if info is None or info[1] != session or info[0] in (b"Z", b"X"):
raise ValueError("lost session anchor")
return info
try:
anchor_fd = os.pidfd_open(session)
original_leader()
while True:
original_leader()
live = False
for pid in os.listdir("/proc"):
if not pid.isdecimal():
if not pid.isdecimal() or int(pid) == session:
continue
info = fields(pid)
if info is None or info[1] != session or info[0] in (b"Z", b"X"):
@@ -194,6 +209,10 @@ try:
try:
current = fields(pid)
if current is not None and current[1] == session and current[0] not in (b"Z", b"X"):
# Check AFTER membership: while the original anchor is
# alive, this SID cannot name a later session. The pidfd
# then prevents task reuse between this check and kill.
original_leader()
signal.pidfd_send_signal(fd, signal.SIGKILL)
finally:
os.close(fd)
@@ -203,7 +222,10 @@ try:
info = fields(pid)
if info is not None and info[0] not in (b"Z", b"X"):
raise
if not live:
if not live and original_leader()[0] == b"T":
# The trusted leader is stopped after waiting for the command.
# With all other members dead it may return the saved command code.
signal.pidfd_send_signal(anchor_fd, signal.SIGCONT)
break
now = time.monotonic()
# Inventory pass 1 remains outside the 250 ms confirmation budget.
@@ -214,7 +236,10 @@ try:
time.sleep(0.01)
except Exception:
sys.exit(1)
' "$child"
finally:
if anchor_fd is not None:
os.close(anchor_fd)
' "$child" "$session_ref"
}
finish() {
local rc="$?"
@@ -226,22 +251,35 @@ finish() {
fi
# A refused cleanup may leave an unkillable member. Do not hide rc 86
# behind an unbounded wait; the caller retains its existing outer guard.
[ "$rc" = 86 ] || [ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ "$rc" != 86 ] && [ -n "$child" ]; then
local command_rc=0
wait "$child" 2>/dev/null || command_rc=$?
[ "$rc" != 0 ] || [ "$command_rc" = 0 ] || rc=87
fi
[ -z "$session_ref" ] || exec {session_ref}<&-
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
# Hold the trusted launcher until its session is observed. Even /bin/true
# cannot exit (and be reaped by bash) before verification. No copy code runs
# in this handshake; set +m ensures setsid need not fork away from child.
"$setsid_bin" /bin/bash -c 'kill -STOP "$BASHPID"; exec "$@"' -- "$@" & child=$!
# The trusted session leader stays alive through cleanup. Copy code runs as
# its child; only after cleanup continues the stopped leader does it return
# the saved command status. An open proc directory pins its task identity.
"$setsid_bin" /bin/bash -c '
set +m
kill -STOP "$BASHPID"
"$@" & command=$!
rc=0; wait "$command" 2>/dev/null || rc=$?
kill -STOP "$BASHPID"
exit "${rc}"
' -- "$@" & child=$!
{ exec {session_ref}<"/proc/$child"; } 2>/dev/null || exit 85
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" != 3 ] || exit 85
if [ "$rc" = 0 ] && [ "$ST_SESSION" = "$child" ] && [ "$ST_STATE" = T ]; then
if [ "$rc" = 0 ] && [ "$ST_SESSION" = "$child" ] && [ "$ST_STATE" = T ] && [ "$ST_PARENT" = "$$" ]; then
verified=1
kill -CONT "$child" 2>/dev/null || exit 85
break
@@ -255,13 +293,15 @@ while :; do
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 86
case "$ST_STATE" in Z|X) exit 86 ;; esac
[ "$ST_STATE" != T ] || break
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end
+56 -16
View File
@@ -228,12 +228,15 @@ fi
IFS= read -r -d '' TESTER_SUPERVISOR <<'MOSAIC_TESTER_SUPERVISOR' || :
set +m
seconds="$1" setsid_bin="$2" sleep_bin="$3"; shift 3
child="" timer="" verified=0
child="" timer="" session_ref="" verified=0
stat_fields() {
local pid="$1" raw="" rest
local pid="$1" raw="" rest file="/proc/$1/stat"
if [ "$pid" = "${child:-}" ] && [ -n "${session_ref:-}" ]; then
file="/proc/self/fd/$session_ref/stat"
fi
# EOF is expected: stat has no NUL. Decide on content, not read's status.
IFS= read -r -d '' raw 2>/dev/null < "/proc/$pid/stat" || :
[ -n "$raw" ] || { [ -e "/proc/$pid/stat" ] && return 1; return 3; }
IFS= read -r -d '' raw 2>/dev/null < "$file" || :
[ -n "$raw" ] || { [ -e "$file" ] && return 1; return 3; }
rest="${raw##*) }"
[ "$rest" != "$raw" ] || return 1
read -r ST_STATE ST_PARENT ST_GROUP ST_SESSION _ <<< "$rest"
@@ -246,6 +249,7 @@ clean_session() {
import os, signal, sys, time
session = int(sys.argv[1])
until = None
anchor_fd = None
def fields(pid):
try:
@@ -269,11 +273,22 @@ def fields(pid):
raise ValueError("stat")
return words[0], int(words[3])
def original_leader():
# The inherited proc directory refers to the original task, even if its
# numeric PID is reused. A live trusted leader keeps this SID allocated.
info = fields("self/fd/" + sys.argv[2])
if info is None or info[1] != session or info[0] in (b"Z", b"X"):
raise ValueError("lost session anchor")
return info
try:
anchor_fd = os.pidfd_open(session)
original_leader()
while True:
original_leader()
live = False
for pid in os.listdir("/proc"):
if not pid.isdecimal():
if not pid.isdecimal() or int(pid) == session:
continue
info = fields(pid)
if info is None or info[1] != session or info[0] in (b"Z", b"X"):
@@ -286,6 +301,10 @@ try:
try:
current = fields(pid)
if current is not None and current[1] == session and current[0] not in (b"Z", b"X"):
# Check AFTER membership: while the original anchor is
# alive, this SID cannot name a later session. The pidfd
# then prevents task reuse between this check and kill.
original_leader()
signal.pidfd_send_signal(fd, signal.SIGKILL)
finally:
os.close(fd)
@@ -295,7 +314,10 @@ try:
info = fields(pid)
if info is not None and info[0] not in (b"Z", b"X"):
raise
if not live:
if not live and original_leader()[0] == b"T":
# The trusted leader is stopped after waiting for the command.
# With all other members dead it may return the saved command code.
signal.pidfd_send_signal(anchor_fd, signal.SIGCONT)
break
now = time.monotonic()
# Inventory pass 1 remains outside the 250 ms confirmation budget.
@@ -306,7 +328,10 @@ try:
time.sleep(0.01)
except Exception:
sys.exit(1)
' "$child"
finally:
if anchor_fd is not None:
os.close(anchor_fd)
' "$child" "$session_ref"
}
finish() {
local rc="$?"
@@ -318,22 +343,35 @@ finish() {
fi
# A refused cleanup may leave an unkillable member. Do not hide rc 86
# behind an unbounded wait; the caller retains its existing outer guard.
[ "$rc" = 86 ] || [ -z "$child" ] || wait "$child" 2>/dev/null || :
if [ "$rc" != 86 ] && [ -n "$child" ]; then
local command_rc=0
wait "$child" 2>/dev/null || command_rc=$?
[ "$rc" != 0 ] || [ "$command_rc" = 0 ] || rc=87
fi
[ -z "$session_ref" ] || exec {session_ref}<&-
if [ -n "$timer" ]; then kill -KILL "$timer" 2>/dev/null || :; wait "$timer" 2>/dev/null || :; fi
exit "$rc"
}
trap finish EXIT
trap 'exit 88' HUP INT TERM
"$sleep_bin" "$seconds" & timer=$!
# Hold the trusted launcher until its session is observed. Even /bin/true
# cannot exit (and be reaped by bash) before verification. No copy code runs
# in this handshake; set +m ensures setsid need not fork away from child.
"$setsid_bin" /bin/bash -c 'kill -STOP "$BASHPID"; exec "$@"' -- "$@" & child=$!
# The trusted session leader stays alive through cleanup. Copy code runs as
# its child; only after cleanup continues the stopped leader does it return
# the saved command status. An open proc directory pins its task identity.
"$setsid_bin" /bin/bash -c '
set +m
kill -STOP "$BASHPID"
"$@" & command=$!
rc=0; wait "$command" 2>/dev/null || rc=$?
kill -STOP "$BASHPID"
exit "${rc}"
' -- "$@" & child=$!
{ exec {session_ref}<"/proc/$child"; } 2>/dev/null || exit 85
# Allow session creation the whole inner budget, rather than a 200 ms margin.
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" != 3 ] || exit 85
if [ "$rc" = 0 ] && [ "$ST_SESSION" = "$child" ] && [ "$ST_STATE" = T ]; then
if [ "$rc" = 0 ] && [ "$ST_SESSION" = "$child" ] && [ "$ST_STATE" = T ] && [ "$ST_PARENT" = "$$" ]; then
verified=1
kill -CONT "$child" 2>/dev/null || exit 85
break
@@ -347,13 +385,15 @@ while :; do
"$sleep_bin" 0.002
done
[ "$verified" = 1 ] || exit 85
while kill -0 "$child" 2>/dev/null; do
while :; do
rc=0; stat_fields "$child" || rc=$?
[ "$rc" = 0 ] || exit 86
case "$ST_STATE" in Z|X) exit 86 ;; esac
[ "$ST_STATE" != T ] || break
kill -0 "$timer" 2>/dev/null || exit 81
"$sleep_bin" 0.01
done
kill -0 "$timer" 2>/dev/null || exit 81
rc=0; wait "$child" || rc=$?
[ "$rc" = 0 ] || exit 87
exit 0
MOSAIC_TESTER_SUPERVISOR
# row253-supervisor-end