fix: preserve tester session identity through cleanup (#1873)
This commit was merged in pull request #1931.
This commit is contained in:
1 parent
f382699d47
commit
4a122699a0
8 files changed
+177
-53
No files matched your search
+56
-16
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user