From 6e96213bff7817b80e3d7b20b751ab834a4c5734 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sun, 25 Oct 2026 00:21:18 +0800 Subject: [PATCH 1/3] fix(push): read procfs entries whole to avoid read buffer cross-talk --- tools/push.sh | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tools/push.sh b/tools/push.sh index ad31cd4..d80c914 100755 --- a/tools/push.sh +++ b/tools/push.sh @@ -874,13 +874,21 @@ 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 的身份校验被误判;命令替换不做回退寻址,不会串内容。 + # 读取失败在 errexit 下会让 shell 直接退出(if/|| 均不能豁免),故读取 + # 期间临时关闭,读完立即按原状态恢复。 + case $- in *e*) had_errexit=true ;; esac + set +e + { stat_line=$(< "/proc/$pid/stat"); } 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 +901,15 @@ 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 内建的跨读取缓冲串内容。 + case $- in *e*) had_errexit=true ;; esac + set +e + { status_text=$(< "/proc/$pid/status"); } 2>/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() { From 15966d4c092427fa9d85719ac4298008ac3060a0 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sun, 25 Oct 2026 00:52:33 +0800 Subject: [PATCH 2/3] test(push): cover proc readers under both errexit modes Read a live pid and an absent pid through read_process_stat and read_process_uid under `set -e` and `set +e`, in `if !` and `||` call positions, and assert that control flow and the caller's errexit state survive. Narrow the helper comments: the substitution only avoids the observed cross-read path, and errexit is toggled because a failed $( "/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="" + e_before=off; case $- in *e*) e_before=on ;; esac + expected_start=$(awk '{print $22}' "/proc/$BASHPID/stat") || return 1 + expected_uid=$(id -u) || return 1 + read_process_stat "$BASHPID" || return 1 + [[ "$PROC_START" == "$expected_start" ]] || return 1 + read_process_uid "$BASHPID" || 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 d80c914..9820d0b 100755 --- a/tools/push.sh +++ b/tools/push.sh @@ -880,9 +880,10 @@ read_process_stat() { [[ "$pid" =~ ^[0-9]+$ ]] || return 1 # 整文件读取:read 内建在同一次 /proc 扫描中可能把上一个 status 读取 # 遗留的缓冲字节交给这里的 stat 读取(fd 本身正确、pos=0),导致存活 - # worker 的身份校验被误判;命令替换不做回退寻址,不会串内容。 - # 读取失败在 errexit 下会让 shell 直接退出(if/|| 均不能豁免),故读取 - # 期间临时关闭,读完立即按原状态恢复。 + # worker 的身份校验被误判;命令替换只规避这条已观察到的跨读取路径, + # 不据此推断其他版本或实现的行为。 + # Bash 5.2 下 $(/dev/null || read_status=1 @@ -902,7 +903,9 @@ read_process_stat() { read_process_uid() { local pid="$1" status_text="" read_status=0 had_errexit=false - # 同 read_process_stat:整文件读取,避免 read 内建的跨读取缓冲串内容。 + # 同 read_process_stat:整文件读取,规避已观察到的 read 内建跨读取缓冲 + # 路径;$(/dev/null || read_status=1 From f689434a26245e2a57fcd0d8cc18bd7dfe8feea6 Mon Sep 17 00:00:00 2001 From: LUNO Date: Sun, 25 Oct 2026 00:57:44 +0800 Subject: [PATCH 3/3] test(push): bind the proc reader fixture to one live pid --- tests/test-push.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test-push.sh b/tests/test-push.sh index f217175..fcdeafa 100644 --- a/tests/test-push.sh +++ b/tests/test-push.sh @@ -179,13 +179,13 @@ assert_fail "reject port above 65535" parse_server_info "root@host:65536" # 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="" + 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/$BASHPID/stat") || return 1 + expected_start=$(awk '{print $22}' "/proc/$live_pid/stat") || return 1 expected_uid=$(id -u) || return 1 - read_process_stat "$BASHPID" || return 1 + read_process_stat "$live_pid" || return 1 [[ "$PROC_START" == "$expected_start" ]] || return 1 - read_process_uid "$BASHPID" || return 1 + read_process_uid "$live_pid" || return 1 [[ "$PROC_UID" == "$expected_uid" ]] || return 1 printf 'live-read %s\n' "$mode"