From cfd02183c223905397bf172b11990c8ef62ef09d Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 18:43:03 -0400 Subject: [PATCH 1/2] Avoid Linux Bash trap-table race --- adapter-tests/v1/runner.sh | 1 - scripts/test/portable-adapter-contracts.test.sh | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index c3f61f8..9e7d42c 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -64,7 +64,6 @@ terminate_active_group() { } signal_exit() { local status=${1:-1} - trap '' HUP INT TERM trap - EXIT exec >/dev/null 2>&1 terminate_active_group diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index e505c31..77b3a53 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -405,6 +405,7 @@ wait "$signal_pid" || signal_status=$? /bin/sleep 2.2 signal_stdout_bytes=$(/usr/bin/wc -c < "$signal_out" | /usr/bin/tr -d ' ') signal_stderr_bytes=$(/usr/bin/wc -c < "$signal_err" | /usr/bin/tr -d ' ') +signal_stderr_sha256=$(sha_file "$signal_err") signal_stdout_class=empty signal_stderr_class=empty [ "$signal_stdout_bytes" -eq 0 ] || signal_stdout_class=other @@ -412,6 +413,14 @@ if [ "$signal_stderr_bytes" -ne 0 ]; then if /usr/bin/grep -Eq '(^|[[:space:]])(Terminated|Killed|Hangup|Interrupt|Done)(:|[[:space:]]|$)' \ "$signal_err"; then signal_stderr_class=job-control + elif /usr/bin/grep -Eq 'warning: run_pending_traps: bad value in trap_list\[[0-9]+\]: 0x[[:xdigit:]]+' \ + "$signal_err"; then + signal_stderr_class=bash-trap-table + elif /usr/bin/grep -Eq '(^|[[:space:]])E_[A-Z_]+([[:space:]]|$)' "$signal_err"; then + signal_stderr_class=typed-error + elif /usr/bin/grep -Eq '(line [0-9]+:|unbound variable|not a child|syntax error)' \ + "$signal_err"; then + signal_stderr_class=shell-runtime else signal_stderr_class=other fi @@ -425,9 +434,10 @@ fi if [ "$signal_sent" -le 0 ] || [ "$signal_status" -ne 143 ] || [ "$signal_stdout_bytes" -ne 0 ] || [ "$signal_stderr_bytes" -ne 0 ] || [ "$signal_scratch_count" -ne 0 ] || [ "$signal_survivor" != absent ]; then - /usr/bin/printf 'signal-debug status=%s sent=%s stdout_bytes=%s stdout_class=%s stderr_bytes=%s stderr_class=%s scratch_entries=%s survivor=%s\n' \ + /usr/bin/printf 'signal-debug status=%s sent=%s stdout_bytes=%s stdout_class=%s stderr_bytes=%s stderr_sha256=%s stderr_class=%s scratch_entries=%s survivor=%s\n' \ "$signal_status" "$signal_sent" "$signal_stdout_bytes" "$signal_stdout_class" \ - "$signal_stderr_bytes" "$signal_stderr_class" "$signal_scratch_count" "$signal_survivor" >&2 + "$signal_stderr_bytes" "$signal_stderr_sha256" "$signal_stderr_class" \ + "$signal_scratch_count" "$signal_survivor" >&2 fail 'signal group cleanup' fi pass 'TERM stops adapter group and cleans scratch' From 50b24b5296134531c4903136524b0eac6238f593 Mon Sep 17 00:00:00 2001 From: ci Date: Mon, 31 Aug 2026 19:45:34 -0400 Subject: [PATCH 2/2] Harden group-wide signal cleanup --- adapter-tests/v1/runner.sh | 83 ++++++++++++++----- .../test/portable-adapter-contracts.test.sh | 30 ++++++- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/adapter-tests/v1/runner.sh b/adapter-tests/v1/runner.sh index 9e7d42c..42b0b4c 100755 --- a/adapter-tests/v1/runner.sh +++ b/adapter-tests/v1/runner.sh @@ -30,12 +30,21 @@ fixture_root=$(CDPATH='' cd -P -- "$fixture_root" && pwd -P) || runner_error E_F run_tmp=$(/usr/bin/mktemp -d "$fixture_root/scratch/run.XXXXXX") || runner_error cleanup() { + local deadline if [ -n "${run_tmp:-}" ]; then - /bin/rm -rf -- "$run_tmp" + deadline=$((SECONDS + 3)) + while { [ -e "$run_tmp" ] || [ -L "$run_tmp" ]; } && [ "$SECONDS" -lt "$deadline" ]; do + /bin/rm -rf -- "$run_tmp" 2>/dev/null || : + [ ! -e "$run_tmp" ] && [ ! -L "$run_tmp" ] && return 0 + /bin/sleep 0.01 || : + done + /bin/rm -rf -- "$run_tmp" 2>/dev/null || return 1 + [ ! -e "$run_tmp" ] && [ ! -L "$run_tmp" ] fi } ACTIVE_CHILD_GROUP='' ACTIVE_CHILD_PID='' +CHILD_SEQUENCE=0 SIGNAL_DEFER=0 SIGNAL_EXITING=0 PENDING_SIGNAL_STATUS=0 @@ -45,19 +54,29 @@ group_alive() { terminate_active_group() { local group=${ACTIVE_CHILD_GROUP:-} local leader=${ACTIVE_CHILD_PID:-} - local tick + local wait_status if [[ "$group" =~ ^[1-9][0-9]*$ ]]; then kill -TERM -- "-$group" 2>/dev/null || : - for tick in {1..5}; do + for _ in {1..5}; do group_alive "$group" || break - /bin/sleep 0.02 + /bin/sleep 0.02 || : done if group_alive "$group"; then kill -KILL -- "-$group" 2>/dev/null || : fi fi if [[ "$leader" =~ ^[1-9][0-9]*$ ]]; then - wait "$leader" 2>/dev/null || : + while :; do + wait_status=0 + wait "$leader" 2>/dev/null || wait_status=$? + case "$wait_status" in + 129|130|143) + kill -0 "$leader" 2>/dev/null && continue + break + ;; + *) break ;; + esac + done fi ACTIVE_CHILD_GROUP='' ACTIVE_CHILD_PID='' @@ -67,7 +86,9 @@ signal_exit() { trap - EXIT exec >/dev/null 2>&1 terminate_active_group - cleanup + if ! cleanup; then + exit 1 + fi exit "$status" } handle_signal() { @@ -83,6 +104,10 @@ trap 'handle_signal 129' HUP trap 'handle_signal 130' INT trap 'handle_signal 143' TERM /bin/mkdir -m 700 "$run_tmp/home" +child_notify="$run_tmp/child.notify" +/usr/bin/mkfifo "$child_notify" || runner_error +exec 4<> "$child_notify" || runner_error +/bin/rm -f -- "$child_notify" || runner_error sha_file() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } sha_text() { /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}'; } @@ -351,7 +376,7 @@ run_child() { local diagnostic=$5 local child_home=$6 local dependency=${7:-} - local pid tick=0 pending_status + local pid pending_status child_token notification='' active_marker CHILD_STATUS=0 CHILD_ERROR='' /bin/mkdir -m 700 "$child_home" @@ -359,6 +384,8 @@ run_child() { CHILD_ERROR=E_RUNTIME return 1 fi + CHILD_SEQUENCE=$((CHILD_SEQUENCE + 1)) + child_token=$CHILD_SEQUENCE PENDING_SIGNAL_STATUS=0 SIGNAL_DEFER=1 set -m @@ -367,13 +394,19 @@ run_child() { child_args=("$request" "$jq_bin" "$contract" "$core_fixture_modules") [ -z "$dependency" ] || child_args+=("$dependency") [ "$mode" = direct ] || child_args=("$mode" "${child_args[@]}") - exec /usr/bin/env -i HOME="$child_home" TMPDIR="$child_home" PATH=/usr/bin:/bin \ - LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ - GIT_NO_REPLACE_OBJECTS=1 GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ - GIT_AUTHOR_NAME=fake GIT_AUTHOR_EMAIL=fake@example.invalid \ - GIT_COMMITTER_NAME=fake GIT_COMMITTER_EMAIL=fake@example.invalid \ - GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ - /bin/bash "$executable" "${child_args[@]}" + child_status=0 + ( + exec 4>&- + exec /usr/bin/env -i HOME="$child_home" TMPDIR="$child_home" PATH=/usr/bin:/bin \ + LC_ALL=C GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \ + GIT_NO_REPLACE_OBJECTS=1 GIT_NO_LAZY_FETCH=1 GIT_TERMINAL_PROMPT=0 \ + GIT_AUTHOR_NAME=fake GIT_AUTHOR_EMAIL=fake@example.invalid \ + GIT_COMMITTER_NAME=fake GIT_COMMITTER_EMAIL=fake@example.invalid \ + GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \ + /bin/bash "$executable" "${child_args[@]}" + ) || child_status=$? + printf '%s\n' "$child_token" >&4 || : + exit "$child_status" ) > "$output" 2> "$diagnostic" & pid=$! ACTIVE_CHILD_GROUP=$pid @@ -386,15 +419,21 @@ run_child() { SIGNAL_EXITING=1 signal_exit "$pending_status" fi - while kill -0 "$pid" 2>/dev/null; do - tick=$((tick + 1)) - if [ "$tick" -ge 20 ]; then - terminate_active_group - CHILD_ERROR=E_TIMEOUT - return 1 - fi - /bin/sleep 0.05 + active_marker="$child_home/.runner-active-group" + if ! printf '%s\n' "$pid" > "$active_marker"; then + terminate_active_group + CHILD_ERROR=E_RUNTIME + return 1 + fi + while IFS= read -r -t 1 -u 4 notification; do + [ "$notification" = "$child_token" ] && break + notification='' done + if [ "$notification" != "$child_token" ] && kill -0 "$pid" 2>/dev/null; then + terminate_active_group + CHILD_ERROR=E_TIMEOUT + return 1 + fi wait "$pid" || CHILD_STATUS=$? if group_alive "${ACTIVE_CHILD_GROUP:-}"; then terminate_active_group diff --git a/scripts/test/portable-adapter-contracts.test.sh b/scripts/test/portable-adapter-contracts.test.sh index 77b3a53..32e1e01 100755 --- a/scripts/test/portable-adapter-contracts.test.sh +++ b/scripts/test/portable-adapter-contracts.test.sh @@ -379,12 +379,34 @@ pass 'late target commit recheck' signal_out="$tmp/signal.out" signal_err="$tmp/signal.err" +set -m PATH="$bin:/usr/bin:/bin" "$runner" "$inventory" "$fixture" > "$signal_out" 2> "$signal_err" & signal_pid=$! +signal_groups=$(jobs -p) +set +m +signal_group_count=$(/usr/bin/printf '%s\n' "$signal_groups" | + /usr/bin/awk 'NF { count++ } END { print count + 0 }') +signal_group=$signal_groups +if [ "$signal_group_count" -ne 1 ] || ! [[ "$signal_group" =~ ^[1-9][0-9]*$ ]] || + [ "$signal_group" != "$signal_pid" ]; then + /bin/kill "$signal_pid" 2>/dev/null || : + wait "$signal_pid" 2>/dev/null || : + fail 'isolated signal process group' +fi signal_marker='' +signal_child_group='' for _ in {1..6000}; do - signal_marker=$(/usr/bin/find "$fixture/scratch" -name 'request-payload.1' -type f -print -quit) - [ -z "$signal_marker" ] || break + signal_marker=$(/usr/bin/find "$fixture/scratch" -path '*/timeout.home/.runner-active-group' \ + -type f -print -quit) + if [ -n "$signal_marker" ]; then + signal_child_group='' + IFS= read -r signal_child_group < "$signal_marker" || : + if [[ "$signal_child_group" =~ ^[1-9][0-9]*$ ]] && + kill -0 -- "-$signal_child_group" 2>/dev/null; then + break + fi + signal_marker='' + fi /bin/kill -0 "$signal_pid" 2>/dev/null || break /bin/sleep 0.02 done @@ -396,7 +418,7 @@ fi signal_sent=0 for _ in {1..200}; do /bin/kill -0 "$signal_pid" 2>/dev/null || break - /bin/kill -TERM "$signal_pid" 2>/dev/null || break + kill -TERM -- "-$signal_group" 2>/dev/null || break signal_sent=$((signal_sent + 1)) /bin/sleep 0.005 done @@ -431,7 +453,7 @@ if /usr/bin/find "$fixture/scratch" \( -name timeout-survived -o -name descendan -print -quit | /usr/bin/grep -q .; then signal_survivor=present fi -if [ "$signal_sent" -le 0 ] || [ "$signal_status" -ne 143 ] || +if [ "$signal_sent" -lt 2 ] || [ "$signal_status" -ne 143 ] || [ "$signal_stdout_bytes" -ne 0 ] || [ "$signal_stderr_bytes" -ne 0 ] || [ "$signal_scratch_count" -ne 0 ] || [ "$signal_survivor" != absent ]; then /usr/bin/printf 'signal-debug status=%s sent=%s stdout_bytes=%s stdout_class=%s stderr_bytes=%s stderr_sha256=%s stderr_class=%s scratch_entries=%s survivor=%s\n' \