fix: harden demo lifecycle and scanner

This commit is contained in:
golem
2026-08-21 04:31:37 -06:00
parent 2446f0b2ef
commit a710106932
8 changed files with 509 additions and 128 deletions
+25 -10
View File
@@ -4,16 +4,26 @@ set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
# shellcheck source=process-lib.sh
source "$ROOT/tools/process-lib.sh"
ANVIL_TEST_PHRASE='test test test test test test test test test test test junk'
ANVIL_DEV_WORDS=(test test test test test test test test test test test junk)
CLEANING=0
ANVIL_PID='' ANVIL_START='' ANVIL_PGID=''
VITE_PID='' VITE_START='' VITE_PGID=''
cleanup() {
local status=$? cleanup_status=0
((CLEANING == 0)) || return
CLEANING=1
trap - INT TERM EXIT
demo_stop_recorded "$ROOT" vite || cleanup_status=1
demo_stop_recorded "$ROOT" anvil || cleanup_status=1
if [[ -n "$VITE_PID" && -n "$VITE_START" && -n "$VITE_PGID" ]]; then
demo_stop_launch "$ROOT" vite "$VITE_PID" "$VITE_START" "$VITE_PGID" || cleanup_status=1
else
demo_stop_recorded "$ROOT" vite || cleanup_status=1
fi
if [[ -n "$ANVIL_PID" && -n "$ANVIL_START" && -n "$ANVIL_PGID" ]]; then
demo_stop_launch "$ROOT" anvil "$ANVIL_PID" "$ANVIL_START" "$ANVIL_PGID" || cleanup_status=1
else
demo_stop_recorded "$ROOT" anvil || cleanup_status=1
fi
((status == 0 && cleanup_status != 0)) && status=$cleanup_status
exit "$status"
}
@@ -21,14 +31,19 @@ trap 'exit 130' INT
trap 'exit 143' TERM
trap cleanup EXIT
record_launched() {
local kind=$1 pid=$2
capture_and_publish() {
local kind=$1 pid=$2 start pgid
for _ in {1..50}; do
if demo_record_process "$ROOT" "$kind" "$pid"; then return 0; fi
if demo_capture_process "$ROOT" "$kind" "$pid" start pgid; then
if [[ "$kind" == anvil ]]; then ANVIL_START=$start; ANVIL_PGID=$pgid
else VITE_START=$start; VITE_PGID=$pgid; fi
demo_publish_process "$ROOT" "$kind" "$pid" "$start" "$pgid"
return
fi
demo_pid_is_running "$pid" || break
sleep 0.02
done
printf 'Could not prove identity for launched %s PID %s; stopping without targeting an unverified PID.\n' "$kind" "$pid" >&2
printf 'Could not prove identity for launched %s PID %s; no signal will be guessed.\n' "$kind" "$pid" >&2
return 1
}
@@ -37,9 +52,9 @@ bash tools/reset-local.sh
bash tools/doctor.sh
install -d -m 0700 "$ROOT/.demo"
setsid anvil --host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "$ANVIL_TEST_PHRASE" >"$ROOT/.demo/anvil.log" 2>&1 &
setsid anvil --host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "${ANVIL_DEV_WORDS[*]}" >"$ROOT/.demo/anvil.log" 2>&1 &
ANVIL_PID=$!
record_launched anvil "$ANVIL_PID"
capture_and_publish anvil "$ANVIL_PID"
for _ in {1..100}; do
if [[ $(cast chain-id --rpc-url http://127.0.0.1:8545 2>/dev/null || true) == 31337 ]]; then ANVIL_READY=1; break; fi
demo_pid_is_running "$ANVIL_PID" || break
@@ -57,7 +72,7 @@ node tools/publish-web-manifest.mjs
setsid "$ROOT/web/node_modules/.bin/vite" web --host 127.0.0.1 --port 5173 >"$ROOT/.demo/vite.log" 2>&1 &
VITE_PID=$!
record_launched vite "$VITE_PID"
capture_and_publish vite "$VITE_PID"
for _ in {1..100}; do
if curl --fail --silent --output /dev/null http://127.0.0.1:5173/; then VITE_READY=1; break; fi
demo_pid_is_running "$VITE_PID" || break
+2 -2
View File
@@ -17,7 +17,7 @@ const NETWORKS = {
const REQUIRED_MANIFEST_FIELDS = ["schemaVersion", "network", "chainId", "deploymentBlock", "token", "proxy", "implementation", "owner", "actors"];
const OPTIONAL_MANIFEST_FIELDS = ["rpcUrl", "explorerBaseUrl"];
const ANVIL_TEST_PHRASE = "test test test test test test test test test test test junk";
const ANVIL_TEST_WORDS = [...Array(11).fill("test"), "junk"].join(" ");
const PROHIBITED_STRING_VALUE = /(?:private[_ -]?key|mnemonic|secret|password|credential|api[_ -]?key)|0x[a-fA-F0-9]{64}/i;
export function networkSpec(network) {
@@ -202,7 +202,7 @@ function assertActorConfiguration(manifest) {
function rejectProhibitedStringValues(value, path = "") {
if (typeof value === "string") {
if (value.includes(ANVIL_TEST_PHRASE) || PROHIBITED_STRING_VALUE.test(value)) {
if (value.includes(ANVIL_TEST_WORDS) || PROHIBITED_STRING_VALUE.test(value)) {
throw new Error(`manifest contains prohibited secret material at ${path}`);
}
return;
+252 -90
View File
@@ -8,6 +8,10 @@ demo_is_uint() {
[[ ${1:-} =~ ^[0-9]+$ ]]
}
demo_is_process_id() {
[[ ${1:-} =~ ^[1-9][0-9]*$ ]] && ((10#$1 > 1))
}
demo_read_one_line() {
local path=$1 destination=$2 value extra
IFS= read -r value <"$path" || return 1
@@ -15,33 +19,83 @@ demo_read_one_line() {
printf -v "$destination" '%s' "$value"
}
demo_start_tick() {
local pid=$1 stat rest
demo_read_process_identity() {
local pid=$1 state_name=$2 start_name=$3 pgid_name=$4 sid_name=$5 stat rest
local -a fields
demo_is_uint "$pid" || return 1
[[ -r "/proc/$pid/stat" ]] || return 1
stat=$(<"/proc/$pid/stat")
demo_is_process_id "$pid" || return 1
{ IFS= read -r stat <"/proc/$pid/stat"; } 2>/dev/null || return 1
rest=${stat##*) }
read -r -a fields <<<"$rest"
demo_is_uint "${fields[19]:-}" || return 1
printf '%s\n' "${fields[19]}"
((${#fields[@]} >= 20)) || return 1
demo_is_uint "${fields[2]}" && demo_is_uint "${fields[3]}" && demo_is_uint "${fields[19]}" || return 1
printf -v "$state_name" '%s' "${fields[0]}"
printf -v "$pgid_name" '%s' "${fields[2]}"
printf -v "$sid_name" '%s' "${fields[3]}"
printf -v "$start_name" '%s' "${fields[19]}"
}
demo_start_tick() {
local state start pgid sid
demo_read_process_identity "$1" state start pgid sid || return 1
printf '%s\n' "$start"
}
demo_process_group() {
local pid=$1 pgid
pgid=$(ps -o pgid= -p "$pid" 2>/dev/null) || return 1
pgid=${pgid//[[:space:]]/}
demo_is_uint "$pgid" || return 1
local state start pgid sid
demo_read_process_identity "$1" state start pgid sid || return 1
printf '%s\n' "$pgid"
}
demo_has_sequence() {
local array_name=$1
demo_pid_is_running() {
local state start pgid sid
demo_read_process_identity "$1" state start pgid sid && [[ "$state" != Z ]]
}
demo_process_has_command_line() {
local pid=$1 first
{ IFS= read -r -d '' first <"/proc/$pid/cmdline"; } 2>/dev/null || return 1
[[ -n "$first" ]]
}
demo_resolve_command() {
local value=$1 resolved
if [[ "$value" == */* ]]; then
readlink -f -- "$value" 2>/dev/null
else
resolved=$(command -v -- "$value" 2>/dev/null) || return 1
readlink -f -- "$resolved" 2>/dev/null
fi
}
demo_entry_index() {
local expected=$1 array_name=$2 argument resolved index
local -n argv_ref=$array_name
shift
for index in "${!argv_ref[@]}"; do
argument=${argv_ref[index]}
resolved=$(demo_resolve_command "$argument" 2>/dev/null || true)
if [[ "$resolved" == "$expected" ]]; then printf '%s\n' "$index"; return 0; fi
done
return 1
}
demo_interpreter_for_entry() {
local entry=$1 line interpreter
IFS= read -r line <"$entry" 2>/dev/null || return 1
[[ "$line" == '#!'* ]] || return 1
line=${line#\#!}
read -r -a interpreter <<<"$line"
((${#interpreter[@]} > 0)) || return 1
if [[ ${interpreter[0]} == */env && ${interpreter[1]:-} ]]; then demo_resolve_command "${interpreter[1]}"
else demo_resolve_command "${interpreter[0]}"; fi
}
demo_has_sequence_from() {
local array_name=$1 begin=$2
local -n argv_ref=$array_name
shift 2
local -a wanted=("$@")
local i j
for ((i = 0; i + ${#wanted[@]} <= ${#argv_ref[@]}; i++)); do
for ((i = begin; i + ${#wanted[@]} <= ${#argv_ref[@]}; i++)); do
for ((j = 0; j < ${#wanted[@]}; j++)); do
[[ ${argv_ref[i+j]} == "${wanted[j]}" ]] || break
done
@@ -51,26 +105,54 @@ demo_has_sequence() {
}
demo_command_matches() {
local pid=$1 kind=$2 argument base found=0
local root=$1 pid=$2 kind=$3 expected executable interpreter entry_index
local -a arguments=()
[[ -r "/proc/$pid/cmdline" ]] || return 1
[[ -r "/proc/$pid/cmdline" && -e "/proc/$pid/exe" ]] || return 1
mapfile -d '' -t arguments <"/proc/$pid/cmdline"
((${#arguments[@]} > 0)) || return 1
for argument in "${arguments[@]}"; do
base=${argument##*/}
if [[ "$kind" == anvil && "$base" == anvil ]]; then found=1; fi
if [[ "$kind" == vite && ( "$base" == vite || "$base" == vite.js ) ]]; then found=1; fi
done
((found == 1)) || return 1
executable=$(readlink -f -- "/proc/$pid/exe") || return 1
if [[ "$kind" == anvil ]]; then
demo_has_sequence arguments --host 127.0.0.1 || return 1
demo_has_sequence arguments --port 8545 || return 1
demo_has_sequence arguments --chain-id 31337 || return 1
expected=$(demo_resolve_command anvil) || return 1
elif [[ "$kind" == vite ]]; then
demo_has_sequence arguments web --host 127.0.0.1 --port 5173 || return 1
expected=$(readlink -f -- "$root/web/node_modules/.bin/vite" 2>/dev/null) || return 1
else
return 1
fi
entry_index=$(demo_entry_index "$expected" arguments) || return 1
if [[ "$executable" != "$expected" ]]; then
interpreter=$(demo_interpreter_for_entry "$expected") || return 1
[[ "$executable" == "$interpreter" && "$entry_index" -eq 1 ]] || return 1
else
[[ "$entry_index" -eq 0 ]] || return 1
fi
if [[ "$kind" == anvil ]]; then
demo_has_sequence_from arguments "$((entry_index + 1))" --host 127.0.0.1 || return 1
demo_has_sequence_from arguments "$((entry_index + 1))" --port 8545 || return 1
demo_has_sequence_from arguments "$((entry_index + 1))" --chain-id 31337 || return 1
else
local -a expected_tail=(web --host 127.0.0.1 --port 5173)
local -a actual_tail=("${arguments[@]:entry_index+1}")
[[ "${actual_tail[*]}" == "${expected_tail[*]}" && ${#actual_tail[@]} -eq ${#expected_tail[@]} ]] || return 1
fi
}
demo_identity_matches() {
local root=$1 kind=$2 pid=$3 expected_start=$4 expected_pgid=$5 state start pgid sid
demo_read_process_identity "$pid" state start pgid sid || return 1
[[ "$state" != Z && "$start" == "$expected_start" && "$pgid" == "$expected_pgid" \
&& "$sid" == "$expected_pgid" && "$pid" == "$expected_pgid" ]] || return 1
demo_command_matches "$root" "$pid" "$kind"
}
demo_capture_process() {
local root=$1 kind=$2 pid=$3 start_name=$4 pgid_name=$5 state actual_start actual_pgid sid
demo_read_process_identity "$pid" state actual_start actual_pgid sid || return 1
[[ "$state" != Z && "$pid" == "$actual_pgid" && "$pid" == "$sid" ]] || return 1
demo_command_matches "$root" "$pid" "$kind" || return 1
printf -v "$start_name" '%s' "$actual_start"
printf -v "$pgid_name" '%s' "$actual_pgid"
}
demo_remove_record() {
@@ -83,82 +165,162 @@ demo_remove_record() {
done
}
demo_record_process() {
local root=$1 kind=$2 pid=$3 start pgid
[[ "$kind" == anvil || "$kind" == vite ]] || return 1
demo_is_uint "$pid" || return 1
start=$(demo_start_tick "$pid") || return 1
pgid=$(demo_process_group "$pid") || return 1
[[ "$pid" == "$pgid" ]] || return 1
demo_command_matches "$pid" "$kind" || return 1
demo_publish_process() {
local root=$1 kind=$2 pid=$3 start=$4 pgid=$5 path
demo_is_process_id "$pid" && demo_is_uint "$start" && demo_is_process_id "$pgid" && [[ "$pid" == "$pgid" ]] || return 1
demo_identity_matches "$root" "$kind" "$pid" "$start" "$pgid" || return 1
mkdir -p "$root/.demo"
chmod 0700 "$root/.demo"
printf '%s\n' "$pid" >"$root/.demo/$kind.pid"
for path in "$root/.demo/$kind.pid" "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"; do [[ ! -e "$path" ]] || return 1; done
printf '%s\n' "$start" >"$root/.demo/$kind.start"
printf '%s\n' "$pgid" >"$root/.demo/$kind.pgid"
chmod 0600 "$root/.demo/$kind.pid" "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"
chmod 0600 "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"
demo_identity_matches "$root" "$kind" "$pid" "$start" "$pgid" || return 1
# PID is the atomic commit marker. PID equals PGID, so a hard link publishes
# the validated value without a temporary path or separately visible write.
ln -- "$root/.demo/$kind.pgid" "$root/.demo/$kind.pid" || return 1
chmod 0600 "$root/.demo/$kind.pid"
}
demo_pid_is_running() {
local pid=$1 stat rest state
{ IFS= read -r stat <"/proc/$pid/stat"; } 2>/dev/null || return 1
rest=${stat##*) }
state=${rest%% *}
[[ "$state" != Z ]]
demo_record_process() {
local root=$1 kind=$2 pid=$3 start pgid
demo_capture_process "$root" "$kind" "$pid" start pgid || return 1
demo_publish_process "$root" "$kind" "$pid" "$start" "$pgid"
}
demo_collect_group_members() {
local expected_pgid=$1 expected_sid=$2 output_name=$3 pid ps_pgid ps_sid ps_state state start pgid sid
local -n output_ref=$output_name
output_ref=()
while read -r pid ps_pgid ps_sid ps_state; do
[[ "$ps_pgid" == "$expected_pgid" && "$ps_sid" == "$expected_sid" && "$ps_state" != Z* ]] || continue
demo_read_process_identity "$pid" state start pgid sid || continue
[[ "$state" != Z && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || continue
output_ref+=("$pid:$start")
done < <(ps -eo pid=,pgid=,sid=,stat=)
}
demo_anchor_allows_signal() {
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 state start pgid sid
if ! demo_read_process_identity "$leader" state start pgid sid; then return 0; fi
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_pgid" ]] || return 1
[[ "$state" == Z ]] && return 0
demo_process_has_command_line "$leader" || return 0
demo_command_matches "$root" "$leader" "$kind"
}
demo_signal_member() {
local pid=$1 expected_start=$2 expected_pgid=$3 expected_sid=$4 signal=$5 state start pgid sid
if ! demo_read_process_identity "$pid" state start pgid sid; then return 0; fi
[[ "$state" != Z ]] || return 0
[[ "$start" == "$expected_start" && "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || return 2
kill -"$signal" -- "$pid"
}
demo_group_has_live_members() {
local pgid=$1 sid=$2
local -a members
demo_collect_group_members "$pgid" "$sid" members
((${#members[@]} > 0))
}
demo_stop_launch() {
local root=$1 kind=$2 leader=$3 expected_start=$4 expected_pgid=$5 member member_pid member_start result
local -a members
demo_is_process_id "$leader" && demo_is_uint "$expected_start" && demo_is_process_id "$expected_pgid" \
&& [[ "$leader" == "$expected_pgid" ]] || return 1
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || {
printf 'Refusing %s PID %s: process identity changed\n' "$kind" "$leader" >&2
return 1
}
demo_collect_group_members "$expected_pgid" "$expected_pgid" members
if ((${#members[@]} == 0)); then
demo_remove_record "$root" "$kind"
return 0
fi
printf 'Stopping validated %s session/process group %s\n' "$kind" "$expected_pgid"
for member in "${members[@]}"; do
IFS=: read -r member_pid member_start <<<"$member"
[[ "$member_pid" != "$leader" ]] || continue
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1
demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1
done
for member in "${members[@]}"; do
IFS=: read -r member_pid member_start <<<"$member"
[[ "$member_pid" == "$leader" ]] || continue
demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid" || return 1
demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" TERM || return 1
done
for ((result = 0; result < ${DEMO_TERM_WAIT_ATTEMPTS:-50}; result++)); do
demo_group_has_live_members "$expected_pgid" "$expected_pgid" || break
sleep 0.1
done
if demo_group_has_live_members "$expected_pgid" "$expected_pgid"; then
printf 'Escalating surviving validated %s session members to KILL\n' "$kind"
for ((result = 0; result < ${DEMO_KILL_WAIT_ATTEMPTS:-20}; result++)); do
demo_collect_group_members "$expected_pgid" "$expected_pgid" members
((${#members[@]} > 0)) || break
for member in "${members[@]}"; do
IFS=: read -r member_pid member_start <<<"$member"
if ! demo_anchor_allows_signal "$root" "$kind" "$leader" "$expected_start" "$expected_pgid"; then
printf 'Refusing KILL escalation: %s leader identity changed\n' "$kind" >&2
return 1
fi
if demo_signal_member "$member_pid" "$member_start" "$expected_pgid" "$expected_pgid" KILL; then
:
else
result=$?
printf 'Refusing KILL escalation: %s member identity changed (%s)\n' "$kind" "$result" >&2
return 1
fi
done
sleep 0.05
done
fi
wait "$leader" 2>/dev/null || true
if demo_group_has_live_members "$expected_pgid" "$expected_pgid"; then
printf 'Validated %s session/process group %s did not stop\n' "$kind" "$expected_pgid" >&2
return 1
fi
demo_remove_record "$root" "$kind"
}
demo_stop_recorded() {
local root=$1 kind=$2 pid start pgid actual_start actual_pgid
local pid_path="$root/.demo/$kind.pid"
local start_path="$root/.demo/$kind.start"
local pgid_path="$root/.demo/$kind.pgid"
local root=$1 kind=$2 pid start pgid
local pid_path="$root/.demo/$kind.pid" start_path="$root/.demo/$kind.start" pgid_path="$root/.demo/$kind.pgid"
[[ "$kind" == anvil || "$kind" == vite ]] || return 1
[[ -e "$pid_path" ]] || return 0
if ! demo_read_one_line "$pid_path" pid || [[ ! "$pid" =~ ^[1-9][0-9]*$ ]] || ((10#$pid <= 1)); then
printf 'Refusing invalid %s PID record\n' "$kind" >&2
return 1
if [[ ! -e "$pid_path" ]]; then
if [[ ! -e "$start_path" && ! -e "$pgid_path" ]]; then return 0; fi
if [[ -e "$start_path" && -e "$pgid_path" ]] && demo_read_one_line "$start_path" start \
&& demo_read_one_line "$pgid_path" pgid && demo_is_uint "$start" && demo_is_process_id "$pgid"; then
pid=$pgid
printf 'Recovering partial %s launch record with validated in-memory shape\n' "$kind"
else
printf 'Removing incomplete %s launch metadata without signaling\n' "$kind"
demo_remove_record "$root" "$kind"
return 0
fi
else
if ! demo_read_one_line "$pid_path" pid || ! demo_is_process_id "$pid"; then
printf 'Refusing invalid %s PID record\n' "$kind" >&2
return 1
fi
if ! demo_read_one_line "$start_path" start || ! demo_is_uint "$start"; then
printf 'Refusing incomplete %s start-tick record\n' "$kind" >&2
return 1
fi
if ! demo_read_one_line "$pgid_path" pgid || ! demo_is_process_id "$pgid"; then
printf 'Refusing incomplete %s process-group record\n' "$kind" >&2
return 1
fi
fi
if ! demo_pid_is_running "$pid"; then
[[ "$pid" == "$pgid" ]] || { printf 'Refusing %s: PID/PGID ownership mismatch\n' "$kind" >&2; return 1; }
if ! demo_group_has_live_members "$pgid" "$pgid" && [[ ! -e "/proc/$pid/stat" ]]; then
printf 'Discarding stale %s process record for PID %s\n' "$kind" "$pid"
demo_remove_record "$root" "$kind"
return 0
fi
if ! demo_read_one_line "$start_path" start || ! demo_is_uint "$start"; then
printf 'Refusing incomplete %s start-tick record\n' "$kind" >&2
return 1
fi
if ! demo_read_one_line "$pgid_path" pgid || [[ ! "$pgid" =~ ^[1-9][0-9]*$ ]] || ((10#$pgid <= 1)); then
printf 'Refusing incomplete %s process-group record\n' "$kind" >&2
return 1
fi
actual_start=$(demo_start_tick "$pid") || return 1
actual_pgid=$(demo_process_group "$pid") || return 1
if [[ "$actual_start" != "$start" || "$actual_pgid" != "$pgid" || "$pid" != "$pgid" ]]; then
printf 'Refusing %s PID %s: recorded identity does not match\n' "$kind" "$pid" >&2
return 1
fi
if ! demo_command_matches "$pid" "$kind"; then
printf 'Refusing %s PID %s: command signature does not match\n' "$kind" "$pid" >&2
return 1
fi
printf 'Stopping validated %s process group %s\n' "$kind" "$pgid"
kill -TERM -- "-$pgid"
for _ in {1..50}; do
demo_pid_is_running "$pid" || break
sleep 0.1
done
if demo_pid_is_running "$pid"; then
printf 'Escalating validated %s process group %s to KILL\n' "$kind" "$pgid"
kill -KILL -- "-$pgid"
for _ in {1..20}; do
demo_pid_is_running "$pid" || break
sleep 0.1
done
fi
wait "$pid" 2>/dev/null || true
demo_pid_is_running "$pid" && {
printf 'Validated %s process group %s did not stop\n' "$kind" "$pgid" >&2
return 1
}
demo_remove_record "$root" "$kind"
demo_stop_launch "$root" "$kind" "$pid" "$start" "$pgid"
}
+15 -4
View File
@@ -13,7 +13,9 @@ filler_pattern='lorem[[:space:]]+ip''sum|fill''er[[:space:]]+text'
unsafe_pattern='unsafe''Allow|unsafe''SkipStorageCheck|unsafe''SkipAllChecks|oz-upgrades-unsafe-allow'
allowed_annotation=' /// @custom:oz-upgrades-unsafe-allow constructor'
fixture_name='ANVIL_''TEST_PHRASE'
fixture_value='test test test test test test test test test test test junk'
allowed_fixture_path='script/lib/DemoScript.sol'
allowed_fixture_line=' string internal constant ANVIL_''TEST_PHRASE = "test test test test test test test test test test test junk";'
fixture_count=0
violations=0
report_matches() {
@@ -34,9 +36,13 @@ for path in "${TRACKED[@]}"; do
report_matches "$path" "$unfinished_pattern" 'unfinished marker'
report_matches "$path" "$filler_pattern" 'filler content'
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == *"$fixture_name"*'='* && "$line" != *"$fixture_value"* ]]; then
printf 'forbidden non-fixture local phrase assignment: %s:%s\n' "$path" "$line" >&2
violations=$((violations + 1))
if [[ "$line" == *"$fixture_name"*'='* ]]; then
if [[ "$path" == "$allowed_fixture_path" && "$line" == "$allowed_fixture_line" ]]; then
fixture_count=$((fixture_count + 1))
else
printf 'forbidden local phrase assignment outside exact fixture: %s:%s\n' "$path" "$line" >&2
violations=$((violations + 1))
fi
fi
done <"$path"
if [[ "$path" == src/* || "$path" == test/* || "$path" == script/* ]]; then
@@ -49,5 +55,10 @@ for path in "${TRACKED[@]}"; do
fi
done
if ((fixture_count != 1)); then
printf 'expected exactly one local phrase fixture assignment, found %d\n' "$fixture_count" >&2
violations=$((violations + 1))
fi
((violations == 0)) || { printf 'Project scan failed with %d violation(s).\n' "$violations" >&2; exit 1; }
printf 'Project scan passed across %d tracked paths.\n' "${#TRACKED[@]}"
+153 -20
View File
@@ -5,19 +5,32 @@ ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
PROCESS_LIB="$ROOT/tools/process-lib.sh"
RESET_SCRIPT="$ROOT/tools/reset-local.sh"
TEST_ROOT=$(mktemp -d /tmp/uups-bank-process-test.XXXXXX)
declare -a TEST_PIDS=()
PATH="$TEST_ROOT/bin:$PATH"
export DEMO_TERM_WAIT_ATTEMPTS=5
export DEMO_KILL_WAIT_ATTEMPTS=10
declare -a TEST_IDENTITIES=()
PASSED=0
STARTED_PID=
cleanup() {
local pid
for pid in "${TEST_PIDS[@]}"; do
if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM -- "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
fi
local identity root relative
for identity in "${TEST_IDENTITIES[@]}"; do test_safe_stop "$identity"; done
for root in absent stale nonnumeric wrong-command false-anvil false-vite reused atomic matching group mutated partial local-reset base-canonical base-active sentinel; do
for relative in \
.demo/anvil.pid .demo/anvil.start .demo/anvil.pgid .demo/anvil.log \
.demo/vite.pid .demo/vite.start .demo/vite.pgid .demo/vite.log \
.demo/sentinel .demo/adjacent.keep tools/process-lib.sh tools/reset-local.sh \
deployments/pending.json deployments/anvil.json deployments/active.json deployments/base-sepolia.json \
web/public/deployment.json web/src/generated/contracts.ts web/node_modules/.bin/vite; do
rm -f -- "$TEST_ROOT/$root/$relative"
done
rmdir -- "$TEST_ROOT/$root/web/node_modules/.bin" "$TEST_ROOT/$root/web/node_modules" \
"$TEST_ROOT/$root/web/src/generated" "$TEST_ROOT/$root/web/src" "$TEST_ROOT/$root/web/public" \
"$TEST_ROOT/$root/web" "$TEST_ROOT/$root/deployments" "$TEST_ROOT/$root/tools" "$TEST_ROOT/$root/.demo" \
"$TEST_ROOT/$root" 2>/dev/null || true
done
rm -rf -- "$TEST_ROOT"
rm -f -- "$TEST_ROOT/bin/anvil" "$TEST_ROOT/bin/vite" "$TEST_ROOT/vite-child.pid"
rmdir -- "$TEST_ROOT/bin" "$TEST_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
@@ -25,8 +38,65 @@ fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
pass() { PASSED=$((PASSED + 1)); printf 'ok %d - %s\n' "$PASSED" "$1"; }
assert_exists() { [[ -e "$1" ]] || fail "expected $1 to exist"; }
assert_absent() { [[ ! -e "$1" ]] || fail "expected $1 to be absent"; }
assert_dead() { ! kill -0 "$1" 2>/dev/null || fail "expected PID $1 to be stopped"; }
assert_alive() { kill -0 "$1" 2>/dev/null || fail "expected PID $1 to remain alive"; }
assert_dead() {
local state start pgid sid
! test_process_identity "$1" state start pgid sid || [[ "$state" == Z ]] || fail "expected PID $1 to be stopped"
}
assert_alive() {
local state start pgid sid
test_process_identity "$1" state start pgid sid && [[ "$state" != Z ]] || fail "expected PID $1 to remain alive"
}
test_process_identity() {
local pid=$1 state_name=$2 start_name=$3 pgid_name=$4 sid_name=$5 stat rest
local -a fields
{ IFS= read -r stat <"/proc/$pid/stat"; } 2>/dev/null || return 1
rest=${stat##*) }
read -r -a fields <<<"$rest"
printf -v "$state_name" '%s' "${fields[0]}"
printf -v "$pgid_name" '%s' "${fields[2]}"
printf -v "$sid_name" '%s' "${fields[3]}"
printf -v "$start_name" '%s' "${fields[19]}"
}
track_process() {
local pid=$1 state start pgid sid
for _ in {1..50}; do
if test_process_identity "$pid" state start pgid sid && [[ "$pid" == "$pgid" && "$pid" == "$sid" ]]; then
TEST_IDENTITIES+=("$pid:$start:$pgid:$sid")
return 0
fi
sleep 0.02
done
fail "could not capture test process identity for PID $pid"
}
test_safe_stop() {
local identity=$1 leader expected_start expected_pgid expected_sid state start pgid sid signal line member member_start
local current_state current_start current_pgid current_sid
IFS=: read -r leader expected_start expected_pgid expected_sid <<<"$identity"
if test_process_identity "$leader" state start pgid sid && [[ "$start" != "$expected_start" ]]; then return 0; fi
for signal in TERM KILL; do
while read -r member pgid sid state; do
[[ "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" && "$state" != Z ]] || continue
test_process_identity "$member" state member_start pgid sid || continue
[[ "$pgid" == "$expected_pgid" && "$sid" == "$expected_sid" ]] || continue
test_process_identity "$leader" current_state current_start current_pgid current_sid || continue
[[ "$current_start" == "$expected_start" && "$current_pgid" == "$expected_pgid" \
&& "$current_sid" == "$expected_sid" ]] || continue
test_process_identity "$member" current_state current_start current_pgid current_sid || continue
[[ "$current_state" != Z && "$current_start" == "$member_start" \
&& "$current_pgid" == "$expected_pgid" && "$current_sid" == "$expected_sid" ]] || continue
kill -"$signal" -- "$member" 2>/dev/null || true
done < <(ps -eo pid=,pgid=,sid=,stat=)
for _ in {1..20}; do
line=$(ps -eo pgid=,sid=,stat= | awk -v p="$expected_pgid" -v s="$expected_sid" '$1 == p && $2 == s && $3 !~ /^Z/ { print; exit }')
[[ -z "$line" ]] && break
sleep 0.02
done
done
wait "$leader" 2>/dev/null || true
}
# RED gate: these are the missing production interfaces this suite specifies.
[[ -f "$PROCESS_LIB" ]] || fail "missing process library: $PROCESS_LIB"
@@ -73,12 +143,16 @@ HELPER
setsid "$helper" web --host 127.0.0.1 --port 5173 >/dev/null 2>&1 &
fi
local pid=$!
TEST_PIDS+=("$pid")
write_record "$root" "$kind" "$pid" "$(start_tick "$pid")" "$pid"
track_process "$pid"
for _ in {1..50}; do
if demo_record_process "$root" "$kind" "$pid"; then break; fi
sleep 0.02
done
assert_exists "$root/.demo/$kind.pid"
STARTED_PID=$pid
}
printf '1..11\n'
printf '1..16\n'
# Catches cleanup treating a missing record as an error or signaling an inferred PID.
root=$(make_root absent)
@@ -104,12 +178,33 @@ pass 'a nonnumeric PID is rejected'
# Catches PID collision signaling an unrelated live process with the wrong command.
root=$(make_root wrong-command)
setsid /bin/sleep 120 & wrong_pid=$!
TEST_PIDS+=("$wrong_pid")
track_process "$wrong_pid"
write_record "$root" anvil "$wrong_pid" "$(start_tick "$wrong_pid")" "$wrong_pid"
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'wrong command signature was accepted'; fi
assert_alive "$wrong_pid"
pass 'a live PID with the wrong command signature is never signaled'
# Catches accepting anvil merely because it appears as a non-executable argument.
root=$(make_root false-anvil)
setsid /bin/bash -c 'sleep 120 & wait' anvil --host 127.0.0.1 --port 8545 --chain-id 31337 & false_anvil_pid=$!
track_process "$false_anvil_pid"
write_record "$root" anvil "$false_anvil_pid" "$(start_tick "$false_anvil_pid")" "$false_anvil_pid"
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'argument-only anvil signature was accepted'; fi
assert_alive "$false_anvil_pid"
pass 'Anvil identity requires its actual executable and argv zero'
# Catches accepting a project-local Vite-looking argument under an unrelated executable.
root=$(make_root false-vite)
mkdir -p "$root/web/node_modules/.bin"
printf '#!/usr/bin/env bash\n' >"$root/web/node_modules/.bin/vite"
chmod +x "$root/web/node_modules/.bin/vite"
setsid /bin/bash -c 'sleep 120 & wait' "$root/web/node_modules/.bin/vite" web --host 127.0.0.1 --port 5173 & false_vite_pid=$!
track_process "$false_vite_pid"
write_record "$root" vite "$false_vite_pid" "$(start_tick "$false_vite_pid")" "$false_vite_pid"
if demo_stop_recorded "$root" vite 2>/dev/null; then fail 'argument-only Vite signature was accepted'; fi
assert_alive "$false_vite_pid"
pass 'Vite identity requires the exact project-local entry and interpreter'
# Catches recycled PID ownership being inferred from PID and command alone.
root=$(make_root reused)
start_owned "$root" anvil
@@ -119,6 +214,13 @@ if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'mismatched start tic
assert_alive "$reused_pid"
pass 'PID reuse is rejected by recorded process start tick'
# Catches publishing the PID before the complete identity has an atomic commit marker.
root=$(make_root atomic)
start_owned "$root" anvil
[[ $(stat -c '%i' "$root/.demo/anvil.pid") == $(stat -c '%i' "$root/.demo/anvil.pgid") ]] || fail 'PID is not an atomic hard-link commit marker'
demo_stop_recorded "$root" anvil
pass 'the PID commit marker is atomically linked only after start and group metadata'
# Catches a validated project child not being terminated and reaped.
root=$(make_root matching)
start_owned "$root" anvil
@@ -129,31 +231,62 @@ assert_dead "$matching_pid"
assert_absent "$root/.demo/anvil.pid"
pass 'a matching project-started child is terminated and reaped'
# Catches stopping only the Vite leader and orphaning a child, or broad group signaling.
# Catches treating leader exit as group exit and orphaning a TERM-resistant Vite child.
root=$(make_root group)
group_helper="$TEST_ROOT/bin/vite"
mkdir -p "$root/web/node_modules/.bin"
group_helper="$root/web/node_modules/.bin/vite"
child_file="$TEST_ROOT/vite-child.pid"
cat >"$group_helper" <<'HELPER'
#!/usr/bin/env bash
sleep 120 &
bash -c 'trap "" TERM; exec sleep 120' &
printf '%s\n' "$!" >"$DEMO_CHILD_PID_FILE"
wait
HELPER
chmod +x "$group_helper"
DEMO_CHILD_PID_FILE="$child_file" setsid "$group_helper" web --host 127.0.0.1 --port 5173 & group_pid=$!
TEST_PIDS+=("$group_pid")
track_process "$group_pid"
for _ in {1..50}; do [[ -s "$child_file" ]] && break; sleep 0.02; done
[[ -s "$child_file" ]] || fail 'Vite helper child did not start'
child_pid=$(<"$child_file")
setsid /bin/sleep 120 & unrelated_pid=$!
TEST_PIDS+=("$unrelated_pid")
track_process "$unrelated_pid"
write_record "$root" vite "$group_pid" "$(start_tick "$group_pid")" "$group_pid"
demo_stop_recorded "$root" vite
wait "$group_pid" 2>/dev/null || true
for _ in {1..50}; do ! kill -0 "$child_pid" 2>/dev/null && break; sleep 0.02; done
assert_dead "$child_pid"
assert_alive "$unrelated_pid"
pass 'an owned process group is stopped completely while an unrelated group survives'
pass 'bounded KILL removes a TERM-resistant owned child while an unrelated group survives'
# Catches escalating after the recorded leader changes to a different command identity.
root=$(make_root mutated)
mutating_helper="$TEST_ROOT/bin/anvil"
cat >"$mutating_helper" <<'HELPER'
#!/usr/bin/env bash
trap 'exec -a changed-after-term /bin/sleep 120' TERM
while :; do sleep 0.05; done
HELPER
chmod +x "$mutating_helper"
setsid "$mutating_helper" --host 127.0.0.1 --port 8545 --chain-id 31337 & mutated_pid=$!
track_process "$mutated_pid"
write_record "$root" anvil "$mutated_pid" "$(start_tick "$mutated_pid")" "$mutated_pid"
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'changed identity was escalated instead of rejected'; fi
assert_alive "$mutated_pid"
pass 'identity is revalidated and a changed leader is never escalated'
# Catches launch-record publication failure leaving an independently known child alive.
root=$(make_root partial)
start_owned "$root" anvil
partial_pid=$STARTED_PID
partial_start=$(start_tick "$partial_pid")
rm -f -- "$root/.demo/anvil.pid" "$root/.demo/anvil.start" "$root/.demo/anvil.pgid"
printf '%s\n' "$partial_start" >"$root/.demo/anvil.start"
printf '%s\n' "$partial_pid" >"$root/.demo/anvil.pgid"
demo_stop_launch "$root" anvil "$partial_pid" "$partial_start" "$partial_pid"
assert_dead "$partial_pid"
assert_absent "$root/.demo/anvil.start"
assert_absent "$root/.demo/anvil.pgid"
pass 'in-memory launch identity safely cleans a partial unpublished record'
# Catches reset deleting arbitrary neighbors or leaving known reproducible local artifacts.
root=$(make_root local-reset)
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
TEST_ROOT=$(mktemp -d /tmp/uups-bank-scan-test.XXXXXX)
fixture_name='ANVIL_''TEST_PHRASE'
fixture_value='test test test test test test test test test test test junk'
cleanup() {
rm -f -- "$TEST_ROOT/script/lib/DemoScript.sol" "$TEST_ROOT/tools/scan-project.sh" "$TEST_ROOT/duplicate.sh" \
"$TEST_ROOT/.git/index" "$TEST_ROOT/.git/HEAD" "$TEST_ROOT/.git/config" "$TEST_ROOT/.git/description" "$TEST_ROOT/.git/info/exclude"
rmdir -- "$TEST_ROOT/.git/objects/pack" "$TEST_ROOT/.git/objects/info" "$TEST_ROOT/.git/objects" \
"$TEST_ROOT/.git/refs/tags" "$TEST_ROOT/.git/refs/heads" "$TEST_ROOT/.git/refs" \
"$TEST_ROOT/.git/branches" "$TEST_ROOT/.git/hooks" "$TEST_ROOT/.git/info" "$TEST_ROOT/.git" \
"$TEST_ROOT/script/lib" "$TEST_ROOT/script" "$TEST_ROOT/tools" "$TEST_ROOT" 2>/dev/null || true
}
trap cleanup EXIT
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
mkdir -p "$TEST_ROOT/script/lib" "$TEST_ROOT/tools"
cp "$ROOT/tools/scan-project.sh" "$TEST_ROOT/tools/scan-project.sh"
git -C "$TEST_ROOT" init -q --template=
printf ' string internal constant %s = "%s";\n' "$fixture_name" "$fixture_value" >"$TEST_ROOT/script/lib/DemoScript.sol"
git -C "$TEST_ROOT" add script/lib/DemoScript.sol tools/scan-project.sh
(cd "$TEST_ROOT" && bash tools/scan-project.sh >/dev/null) || fail 'exact intended fixture assignment was rejected'
# Catches accepting the known phrase assignment in a duplicate tracked path.
printf '%s="%s"\n' "$fixture_name" "$fixture_value" >"$TEST_ROOT/duplicate.sh"
git -C "$TEST_ROOT" add duplicate.sh
if (cd "$TEST_ROOT" && bash tools/scan-project.sh >/dev/null 2>&1); then fail 'duplicate fixture assignment was accepted'; fi
git -C "$TEST_ROOT" rm -q --cached duplicate.sh
rm -f -- "$TEST_ROOT/duplicate.sh"
# Catches substring matching that accepts appended content on the intended line.
printf ' string internal constant %s = "%s"; appended\n' "$fixture_name" "$fixture_value" >"$TEST_ROOT/script/lib/DemoScript.sol"
if (cd "$TEST_ROOT" && bash tools/scan-project.sh >/dev/null 2>&1); then fail 'appended fixture assignment was accepted'; fi
printf '%s\n' 'PASS: scanner fixture exception is exact, unique, and path-anchored'