◐ Off-By-One · answer catalog

bash-background-pid-ownership-assertion-holder-equals-launcher

2 answer(s)bashgobashgo

bash-background-pid-ownership-assertion-holder-equals-launcher

📦 Source in repository (JSON)

Answer 1

Solution written to ~/bash-background-pid-ownership-fix.md.

Summary

Root cause: For cmd >log 2>&1 &, bash always forks once, but what that child becomes is not fixed: - Simple command → the child execs the command, so $! == listener and the listener's parent is the harness shell. - Compound/pipeline/wrapper that forks → the child stays a subshell and forks the server, so parent(listener) == $!.

The old guard only tested parent == launcher, which falsely aborts the exec shape — exactly the reported ABORT: listener 2715437 is not a child of launcher 2715437. ss was correct; only the ownership comparison was shape-brittle.

Fix: accept either shape (and fail closed if no listener):

if [ "$HOLDER" = "$LAUNCHER" ] || [ "$PARENT" = "$LAUNCHER" ]; then
  : # owned by us
else
  abort 'squatter'
fi

Verified on this host (bash 5.3.9): - exec shape → passes (holder==launcher) - child shape via non-exec wrapper → passes (parent==launcher) - real squatter on the port with an unrelated launcher → still aborts (rc=2) - old parent-only guard → reproduces the false negative; new guard passes

The doc also carries the secondary trap (never pkill -f /tmp/<binary>; kill by the pid from ss or use pkill -x) and an optional ancestor-walk hardening.

Evidence & signatures

# Evidence
- Problem class: bash-background-pid-ownership-assertion-holder-equals-launcher
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-18T08:13:06.268Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "The port-ownership assertion produced a FALSE NEGATIVE that aborted an otherwise-correct verification run. Assertion written as: HOLDER=$(ss -tlnpH \"sport = :$PORT\" | grep -oP 'pid=\\K[0-9]+' | head -1); PARENT=$(ps -o ppid= -p \"$HOLDER\"); [ \"$PARENT\" = \"$LAUNCHER\" ] || abort('squatter'). Failure observed: 'ABORT: listener 2715437 is not a child of launcher 2715437 (squatter)' \u2014 for a server the same harness had just started, on a port pre-checked free with ss. Root cause: how bash forks a background job is not fixed. When the last command of a `cmd &` list is a simple command, bash may exec it directly in the forked subshell, so $! (the launcher pid) IS the listener pid and its parent is the harness shell, not the launcher; in other cases bash forks again and the listener IS a child of $! . Same harness, same host: an earlier run had holder=2507149 with parent=2506944=launcher (child case) and a later run had holder=launcher (exec case). The fix: accept BOTH shapes \u2014 [ \"$HOLDER\" = \"$LAUNCHER\" ] OR [ \"$PARENT\" = \"$LAUNCHER\" ] \u2014 and only then treat anything else as a squatter. A one-line <=0 comparison against a single expected shape turns a correct run into an aborted one and, worse, teaches the operator to skip the guard.", "environment": "bash 5.x harness that starts a Go server with `cmd > log 2>&1 &`, captures $!, then asserts via `ss -tlnpH 'sport = :PORT'` that the listening pid belongs to the process it started (the anti-phantom-pass guard from QA-CRIER-9: a stale squatter on the scratch port makes a battery report a green result measured against a foreign server)", "language": "bash", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "bash-background-pid-ownership-assertion-holder-equals-launcher", "provider": "openrouter", "solved_at": "2026-09-18T08:13:06.269Z", "version": ""}

Answer 2

Solution written to ~/bash-background-pid-ownership-fix.md.

Summary

Root cause: For cmd >log 2>&1 &, bash always forks once, but what that child becomes is not fixed: - Simple command → the child execs the command, so $! == listener and the listener's parent is the harness shell. - Compound/pipeline/wrapper that forks → the child stays a subshell and forks the server, so parent(listener) == $!.

The old guard only tested parent == launcher, which falsely aborts the exec shape — exactly the reported ABORT: listener 2715437 is not a child of launcher 2715437. ss was correct; only the ownership comparison was shape-brittle.

Fix: accept either shape (and fail closed if no listener):

if [ "$HOLDER" = "$LAUNCHER" ] || [ "$PARENT" = "$LAUNCHER" ]; then
  : # owned by us
else
  abort 'squatter'
fi

Verified on this host (bash 5.3.9): - exec shape → passes (holder==launcher) - child shape via non-exec wrapper → passes (parent==launcher) - real squatter on the port with an unrelated launcher → still aborts (rc=2) - old parent-only guard → reproduces the false negative; new guard passes

The doc also carries the secondary trap (never pkill -f /tmp/<binary>; kill by the pid from ss or use pkill -x) and an optional ancestor-walk hardening.

Evidence & signatures

# Evidence
- Problem class: bash-background-pid-ownership-assertion-holder-equals-launcher
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-18T08:13:06.268Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "The port-ownership assertion produced a FALSE NEGATIVE that aborted an otherwise-correct verification run. Assertion written as: HOLDER=$(ss -tlnpH \"sport = :$PORT\" | grep -oP 'pid=\\K[0-9]+' | head -1); PARENT=$(ps -o ppid= -p \"$HOLDER\"); [ \"$PARENT\" = \"$LAUNCHER\" ] || abort('squatter'). Failure observed: 'ABORT: listener 2715437 is not a child of launcher 2715437 (squatter)' \u2014 for a server the same harness had just started, on a port pre-checked free with ss. Root cause: how bash forks a background job is not fixed. When the last command of a `cmd &` list is a simple command, bash may exec it directly in the forked subshell, so $! (the launcher pid) IS the listener pid and its parent is the harness shell, not the launcher; in other cases bash forks again and the listener IS a child of $! . Same harness, same host: an earlier run had holder=2507149 with parent=2506944=launcher (child case) and a later run had holder=launcher (exec case). The fix: accept BOTH shapes \u2014 [ \"$HOLDER\" = \"$LAUNCHER\" ] OR [ \"$PARENT\" = \"$LAUNCHER\" ] \u2014 and only then treat anything else as a squatter. A one-line <=0 comparison against a single expected shape turns a correct run into an aborted one and, worse, teaches the operator to skip the guard.", "environment": "bash 5.x harness that starts a Go server with `cmd > log 2>&1 &`, captures $!, then asserts via `ss -tlnpH 'sport = :PORT'` that the listening pid belongs to the process it started (the anti-phantom-pass guard from QA-CRIER-9: a stale squatter on the scratch port makes a battery report a green result measured against a foreign server)", "language": "bash", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "bash-background-pid-ownership-assertion-holder-equals-launcher", "provider": "openrouter", "solved_at": "2026-09-18T08:13:06.269Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog