Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions bin/fm-wake-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,45 @@ fm_pid_alive() {
kill -0 "$pid" 2>/dev/null
}

# The kernel's own start time for a pid: boot-relative clock ticks, /proc/<pid>/stat
# field 22. comm (field 2) is parenthesised and may itself contain spaces or ')',
# so fields are counted from after the LAST ')'. Absent where /proc is (macOS).
fm_pid_start_ticks() {
local pid=$1
[ -r "/proc/$pid/stat" ] || return 1
awk '{ s = $0; sub(/^.*\) /, "", s); n = split(s, f, " "); if (n < 20) exit 1; print f[20] }' \
"/proc/$pid/stat" 2>/dev/null
}

fm_pid_identity() {
local pid=$1 out
local pid=$1 start cmd
case "$pid" in
''|*[!0-9]*) return 1 ;;
esac
# Pin LC_ALL=C so lstart's date format is locale-invariant: the identity is
# written under one locale but re-read under the machine's ambient locale, which
# would otherwise mismatch on a non-C locale (e.g. ko_KR) and reject a live watcher.
out=$(LC_ALL=C ps -p "$pid" -o lstart= -o command= 2>/dev/null) || return 1
[ -n "$out" ] || return 1
printf '%s\n' "$out" | sed 's/^[[:space:]]*//'
# Pin LC_ALL=C so ps's output is locale-invariant: the identity is written under
# one locale but re-read under the machine's ambient locale, which would
# otherwise mismatch on a non-C locale (e.g. ko_KR) and reject a live watcher.
cmd=$(LC_ALL=C ps -p "$pid" -o command= 2>/dev/null) || return 1
[ -n "$cmd" ] || return 1
# The start half must be byte-stable across reads, or a live watcher's own lock
# reads as a reused pid and supervision reports itself down. `ps -o lstart=` is
# NOT byte-stable: it is DERIVED, from the kernel's btime plus the process's
# starttime ticks. starttime never moves, but btime is recomputed and jitters by
# ~1s (measured on a WSL2 host, roughly every 30s), and every jump shifts the
# derived lstart of every live process - so an lstart fingerprint goes stale
# against its OWN process, with no second writer, and reports a live watcher as a
# reused pid. That drift is the primary cause of the false "supervision off"
# alarms this fixes. /proc/<pid>/stat field 22 is boot-relative and immune, so it
# is the fix, not an optimisation: do not "simplify" this back to lstart.
# lstart survives only as the fallback where /proc does not exist, and it is an
# ACTIVELY DRIFTING primitive there, not an equivalent one. On a host with no
# /proc AND a jittering btime, this class of false alarm is NOT fixed.
start=$(fm_pid_start_ticks "$pid") \
|| start=$(LC_ALL=C ps -p "$pid" -o lstart= 2>/dev/null) \
|| return 1
start=$(printf '%s' "$start" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -n "$start" ] || return 1
printf '%s %s\n' "$start" "$cmd" | sed 's/^[[:space:]]*//'
}

fm_path_mtime() {
Expand Down
70 changes: 70 additions & 0 deletions tests/fm-watcher-lock.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -703,8 +703,78 @@ test_pid_identity_is_locale_invariant() {
pass "fm_pid_identity is locale-invariant across LC_ALL/LC_TIME"
}

test_pid_identity_is_stable_across_reads() {
# The identity is a fingerprint of ONE live process instance, so the same live pid
# must fingerprint byte-identically every time or the lock's own holder eventually
# reads as a reused pid and supervision reports itself down while a watcher is
# running. ps's lstart is NOT stable: it is derived from a btime that jitters, so it
# drifts against its own process. The identity is derived from the kernel's own
# start ticks wherever they are readable, and this asserts that stability.
local live baseline current i
sleep 300 &
live=$!
baseline=$(bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
[ -n "$baseline" ] || fail "fm_pid_identity produced no identity for a live pid"
i=0
while [ "$i" -lt 20 ]; do
current=$(bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
[ "$current" = "$baseline" ] \
|| fail "fm_pid_identity is not stable across reads of one live pid (got '$current', want '$baseline')"
sleep 0.1
i=$((i + 1))
done
kill "$live" 2>/dev/null || true
wait "$live" 2>/dev/null || true
pass "fm_pid_identity is byte-stable across repeated reads of one live pid"
}

test_pid_identity_is_derived_from_start_ticks_not_lstart() {
# The discriminator behind the drift, asserted directly rather than by the
# property it happens to satisfy. `ps -o lstart=` is DERIVED - btime (which is
# recomputed and jitters by ~1s on a WSL2 host, roughly every 30s) plus the
# process's own starttime ticks - so every btime jump shifts the derived lstart of
# EVERY live process and an lstart fingerprint goes stale against its own process
# with no second writer. /proc/<pid>/stat field 22 is boot-relative and carries no
# btime term at all, so it cannot move. This pins the fingerprint to field 22 and
# shows an lstart-derived one WOULD move across a btime shift, so a future refactor
# cannot quietly regress the cure back to lstart. btime cannot be injected, so the
# shift is applied to the derivation itself, which is exactly where the drift enters.
local live ticks identity head lstart hz derived derived_after_btime_shift
if [ ! -r "/proc/$$/stat" ]; then
echo "skip: the start-ticks fingerprint needs /proc (Linux)"
return 0
fi
sleep 300 &
live=$!
ticks=$(bash -c '. "$1"; fm_pid_start_ticks "$2"' _ "$LIB" "$live" 2>/dev/null)
identity=$(bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)
head=${identity%%[[:space:]]*}
lstart=$(LC_ALL=C ps -p "$live" -o lstart= 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -n "$ticks" ] || fail "no start ticks readable for a live pid"
[ "$head" = "$ticks" ] \
|| fail "fm_pid_identity's start half is not /proc/<pid>/stat field 22 (got '$head', want '$ticks')"
case "$identity" in
"$lstart"*) fail "fm_pid_identity is still lstart-derived, the primitive that drifts" ;;
esac
# What an lstart-derived fingerprint IS: btime + starttime/HZ. Move btime by the
# observed 1s of jitter and it moves with it, while the shipped fingerprint - which
# never reads btime - cannot.
hz=$(getconf CLK_TCK 2>/dev/null || echo 100)
derived=$(( $(awk '/^btime /{print $2}' /proc/stat) + ticks / hz ))
derived_after_btime_shift=$(( $(awk '/^btime /{print $2}' /proc/stat) + 1 + ticks / hz ))
[ "$derived" != "$derived_after_btime_shift" ] \
|| fail "the lstart derivation did not move across a btime shift; the test cannot discriminate"
[ "$(bash -c '. "$1"; fm_pid_identity "$2"' _ "$LIB" "$live" 2>/dev/null)" = "$identity" ] \
|| fail "the start-ticks fingerprint moved for a fixed live pid"
kill "$live" 2>/dev/null || true
wait "$live" 2>/dev/null || true
pass "fm_pid_identity fingerprints from /proc start ticks, immune to the btime shift that moves lstart"
}

test_singleton_start
test_pid_identity_is_locale_invariant
test_pid_identity_is_stable_across_reads
test_pid_identity_is_derived_from_start_ticks_not_lstart
test_stale_watch_lock_reclaimed
test_live_stale_watch_lock_is_actionable
test_guard_warnings
Expand Down
Loading