diff --git a/tests/test-push.sh b/tests/test-push.sh index 768b922..fcdeafa 100644 --- a/tests/test-push.sh +++ b/tests/test-push.sh @@ -175,6 +175,73 @@ assert_fail "reject port above 65535" parse_server_info "root@host:65536" printf '%s\n' "$original_comm" > "/proc/$proc_pid/comm" ) +# proc identity helpers must stay total for absent pids: the same reads run under +# both errexit modes, inside `if !` and `||` callers, and must leave the caller's +# errexit setting untouched afterwards. +proc_reader_flow_case() { + local mode="$1" e_before="" e_after="" missing_pid="" candidate="" rc=0 expected_start="" expected_uid="" live_pid="$BASHPID" + e_before=off; case $- in *e*) e_before=on ;; esac + expected_start=$(awk '{print $22}' "/proc/$live_pid/stat") || return 1 + expected_uid=$(id -u) || return 1 + read_process_stat "$live_pid" || return 1 + [[ "$PROC_START" == "$expected_start" ]] || return 1 + read_process_uid "$live_pid" || return 1 + [[ "$PROC_UID" == "$expected_uid" ]] || return 1 + printf 'live-read %s\n' "$mode" + + # /proc/0 normally does not exist; fall back to the highest addressable pids + # so the case never depends on a fixed pid. + for candidate in 0 4194303 4194302 4194301 999999; do + [[ -e "/proc/$candidate" ]] || { missing_pid="$candidate"; break; } + done + [[ -n "$missing_pid" ]] || return 1 + + if ! read_process_stat "$missing_pid"; then + printf 'if-stat %s\n' "$mode" + else + return 1 + fi + if ! read_process_uid "$missing_pid"; then + printf 'if-uid %s\n' "$mode" + else + return 1 + fi + read_process_stat "$missing_pid" || rc=$? + [[ "$rc" -ne 0 ]] || { printf 'stat-absent-succeeded %s\n' "$mode"; return 1; } + printf 'or-stat %s\n' "$mode" + rc=0 + read_process_uid "$missing_pid" || rc=$? + [[ "$rc" -ne 0 ]] || { printf 'uid-absent-succeeded %s\n' "$mode"; return 1; } + printf 'or-uid %s\n' "$mode" + + e_after=off; case $- in *e*) e_after=on ;; esac + printf 'alive %s errexit-before=%s errexit-after=%s\n' "$mode" "$e_before" "$e_after" +} + +for proc_reader_mode in errexit no-errexit; do + proc_reader_output="" + if [[ "$proc_reader_mode" == errexit ]]; then + proc_reader_output=$(set -e; proc_reader_flow_case "$proc_reader_mode") \ + || fail "proc readers aborted the calling shell under $proc_reader_mode" + else + proc_reader_output=$(set +e; proc_reader_flow_case "$proc_reader_mode") \ + || fail "proc readers aborted the calling shell under $proc_reader_mode" + fi + [[ "$proc_reader_output" == *"live-read $proc_reader_mode"* ]] \ + || fail "proc readers rejected the live pid under $proc_reader_mode: $proc_reader_output" + pass "proc readers match kernel identity for live pid under $proc_reader_mode" + for proc_reader_marker in if-stat if-uid or-stat or-uid; do + [[ "$proc_reader_output" == *"$proc_reader_marker $proc_reader_mode"* ]] \ + || fail "proc readers lost control flow after $proc_reader_marker under $proc_reader_mode: $proc_reader_output" + done + pass "proc readers reject absent pid without ending if/|| callers under $proc_reader_mode" + proc_reader_errexit=off + [[ "$proc_reader_mode" != errexit ]] || proc_reader_errexit=on + [[ "$proc_reader_output" == *"alive $proc_reader_mode errexit-before=$proc_reader_errexit errexit-after=$proc_reader_errexit"* ]] \ + || fail "proc readers changed caller errexit state under $proc_reader_mode: $proc_reader_output" + pass "proc readers preserve caller errexit state under $proc_reader_mode" +done + # Runtime initialization publishes only a verified 0700 directory and supports idempotent cleanup. ( trap - EXIT HUP INT TERM diff --git a/tools/push.sh b/tools/push.sh index ad31cd4..9820d0b 100755 --- a/tools/push.sh +++ b/tools/push.sh @@ -874,13 +874,22 @@ initialize_runtime() { } read_process_stat() { - local pid="$1" stat_line="" stat_rest="" stat_fd + local pid="$1" stat_line="" stat_rest="" read_status=0 had_errexit=false local -a fields=() [[ "$pid" =~ ^[0-9]+$ ]] || return 1 - { exec {stat_fd}<"/proc/$pid/stat"; } 2>/dev/null || return 1 - IFS= read -r stat_line <&"$stat_fd" || [[ -n "$stat_line" ]] || { exec {stat_fd}<&- || true; return 1; } - exec {stat_fd}<&- || true + # 整文件读取:read 内建在同一次 /proc 扫描中可能把上一个 status 读取 + # 遗留的缓冲字节交给这里的 stat 读取(fd 本身正确、pos=0),导致存活 + # worker 的身份校验被误判;命令替换只规避这条已观察到的跨读取路径, + # 不据此推断其他版本或实现的行为。 + # Bash 5.2 下 $(/dev/null || read_status=1 + [[ "$had_errexit" == false ]] || set -e + (( read_status == 0 )) || return 1 + [[ -n "$stat_line" ]] || return 1 [[ "$stat_line" == "$pid ("* && "$stat_line" == *") "* ]] || return 1 stat_rest=${stat_line##*) } read -r -a fields <<< "$stat_rest" @@ -893,17 +902,17 @@ read_process_stat() { } read_process_uid() { - local pid="$1" uid_line="" status_key status_value status_fd - { exec {status_fd}<"/proc/$pid/status"; } 2>/dev/null || return 1 - while read -r status_key status_value _ <&"$status_fd"; do - if [[ "$status_key" == Uid: ]]; then - uid_line=$status_value - break - fi - done - exec {status_fd}<&- || true - [[ "$uid_line" =~ ^[0-9]+$ ]] || return 1 - PROC_UID=$uid_line + local pid="$1" status_text="" read_status=0 had_errexit=false + # 同 read_process_stat:整文件读取,规避已观察到的 read 内建跨读取缓冲 + # 路径;$(/dev/null || read_status=1 + [[ "$had_errexit" == false ]] || set -e + (( read_status == 0 )) || return 1 + [[ "$status_text" =~ (^|$'\n')Uid:[[:space:]]+([0-9]+) ]] || return 1 + PROC_UID=${BASH_REMATCH[2]} } read_process_record() {