From 8565f8466a4c49eebe23637e38c8b50fab670ab4 Mon Sep 17 00:00:00 2001 From: Nathan Klisch Date: Sat, 20 Jun 2026 17:18:18 -0600 Subject: [PATCH] shim: guard PEERAGENT_BIN against pointing at a shim (infinite self-exec loop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PEERAGENT_BIN is meant to override the resolved Go binary. If a user sets it to a shim copy (this script, or plugin/bin/peeragent), step 1 does `exec "$PEERAGENT_BIN" "$@"` -> the shim re-runs step 1 with the same env var -> exec itself -> forever. Symptom: a `sh` process pinning a core with no child, spotted in the wild this session (an opus peer-review job that "ran" for 19min without ever starting the agent). Guard: in step 1, if the PEERAGENT_BIN target's first two bytes are `#!` (a shell shim; the Go binaries start with ELF/Mach-O magic), fail fast with exit 2 and a clear message instead of spinning. Catches pointing at ANY shim copy (root bin/ or plugin/bin/), not just the exact self path. A valid override to the Go binary and the unset/auto-detect path are unaffected (verified: shim->self and shim->plugin-shim now exit 2; shim->Go-binary and unset both still work). The --text flag is detected inline (wants_text is defined later in the script and sh does not parse functions ahead, so it can't be called from step 1). Re-synced plugin/bin/peeragent from bin/peeragent via package-plugin.sh. Test: added a "shim PEERAGENT_BIN self-exec-loop guard" step to scripts/validate.sh — runs the guarded path under `timeout 3` so a regression (hang/loop) fails the suite rather than passing by luck. Asserts process exit 2 (set by the shim, independent of the Go binary's exit-code semantics) and the "self-exec loop" message. Note (out of scope, flagged): the pre-existing "shim smoke" step asserts process exit 4 for `--status missing-job`, but the Go binary's process exit is 1 (the `exit_code:4` lives only in the JSON metadata) on this build — that step appears already red on clean upstream, unrelated to this change. --- bin/peeragent | 18 ++++++++++++++++++ plugin/bin/peeragent | 18 ++++++++++++++++++ scripts/validate.sh | 19 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/bin/peeragent b/bin/peeragent index 09ab630..d0161e4 100755 --- a/bin/peeragent +++ b/bin/peeragent @@ -7,6 +7,24 @@ RELEASES_URL="https://github.com/nklisch/peeragent/releases" # 1. Explicit override. if [ -n "${PEERAGENT_BIN:-}" ] && [ -x "$PEERAGENT_BIN" ]; then + # Footgun guard: PEERAGENT_BIN must point at the Go BINARY, not at a shim copy + # (this script or plugin/bin/peeragent). A shim starts with "#!"; the real + # Go binaries start with ELF/Mach-O bytes. Pointing PEERAGENT_BIN at a shim + # makes step 1 `exec` the shim again with the same env var -> infinite + # self-exec loop that burns a core and never runs the agent. Detect it, fail + # loudly (never spin). (wants_text isn't defined yet at this point in the + # script, so detect --text inline.) + if [ "$(head -c2 "$PEERAGENT_BIN" 2>/dev/null)" = "#!" ]; then + _wt=0; for _a in "$@"; do [ "$_a" = "--text" ] && _wt=1; done + msg="PEERAGENT_BIN points at a shell shim ($PEERAGENT_BIN), not the Go binary — that would self-exec loop forever. Set PEERAGENT_BIN to the compiled binary (e.g. .../plugin/bin/-/peeragent) or unset it to let this shim auto-detect." + if [ "$_wt" = 1 ]; then + printf '%s\n' "$msg" >&2 + else + _esc=$(printf '%s' "$msg" | sed 's/\\/\\\\\\\\/g; s/"/\\\"/g') + printf '{"status":"failed","summary":"PEERAGENT_BIN points at a shim, not the Go binary (would self-exec loop)","changed_files":[],"verification":[],"details":"%s","metadata":{"exit_code":2}}\n' "$_esc" >&2 + fi + exit 2 + fi exec "$PEERAGENT_BIN" "$@" fi diff --git a/plugin/bin/peeragent b/plugin/bin/peeragent index 09ab630..d0161e4 100755 --- a/plugin/bin/peeragent +++ b/plugin/bin/peeragent @@ -7,6 +7,24 @@ RELEASES_URL="https://github.com/nklisch/peeragent/releases" # 1. Explicit override. if [ -n "${PEERAGENT_BIN:-}" ] && [ -x "$PEERAGENT_BIN" ]; then + # Footgun guard: PEERAGENT_BIN must point at the Go BINARY, not at a shim copy + # (this script or plugin/bin/peeragent). A shim starts with "#!"; the real + # Go binaries start with ELF/Mach-O bytes. Pointing PEERAGENT_BIN at a shim + # makes step 1 `exec` the shim again with the same env var -> infinite + # self-exec loop that burns a core and never runs the agent. Detect it, fail + # loudly (never spin). (wants_text isn't defined yet at this point in the + # script, so detect --text inline.) + if [ "$(head -c2 "$PEERAGENT_BIN" 2>/dev/null)" = "#!" ]; then + _wt=0; for _a in "$@"; do [ "$_a" = "--text" ] && _wt=1; done + msg="PEERAGENT_BIN points at a shell shim ($PEERAGENT_BIN), not the Go binary — that would self-exec loop forever. Set PEERAGENT_BIN to the compiled binary (e.g. .../plugin/bin/-/peeragent) or unset it to let this shim auto-detect." + if [ "$_wt" = 1 ]; then + printf '%s\n' "$msg" >&2 + else + _esc=$(printf '%s' "$msg" | sed 's/\\/\\\\\\\\/g; s/"/\\\"/g') + printf '{"status":"failed","summary":"PEERAGENT_BIN points at a shim, not the Go binary (would self-exec loop)","changed_files":[],"verification":[],"details":"%s","metadata":{"exit_code":2}}\n' "$_esc" >&2 + fi + exit 2 + fi exec "$PEERAGENT_BIN" "$@" fi diff --git a/scripts/validate.sh b/scripts/validate.sh index c0a35e4..40a0e4e 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -188,4 +188,23 @@ printf '%s\n' "$status_output" | grep -q '"status":"failed"' printf '%s\n' "$status_output" | grep -q '"job_id":"missing-job"' printf '%s\n' "$status_output" | grep -q '"exit_code":4' +step "shim PEERAGENT_BIN self-exec-loop guard" +# PEERAGENT_BIN must point at the Go BINARY, not a shim copy. Pointing it at a +# shim makes step 1 `exec` the shim again with the same env var -> infinite +# self-exec loop that burns a core and never runs the agent. The guard detects +# a shebang (shim) target and fails fast (exit 2) instead of spinning. +# Run under `timeout` so a regression (guard removed/broken) hangs the test +# rather than passing by luck. +set +e +loop_output=$(timeout 3 env PEERAGENT_BIN="$ROOT/bin/peeragent" bin/peeragent --status missing-job 2>&1) +loop_code=$? +set -e +if [ "$loop_code" -ne 2 ]; then + echo "expected PEERAGENT_BIN->shim to exit 2 (guard), got $loop_code (124=spun/looped=REGRESSION)" + echo "$loop_output" + exit 1 +fi +printf '%s\n' "$loop_output" | grep -q '"exit_code":2' +printf '%s\n' "$loop_output" | grep -q 'self-exec loop' + step "validation complete"