Conversation
… portability gaps The script runs with `set -eo pipefail` (mtproxymax.sh:10). `grep -q` exits as soon as it matches, closing the read end of the pipe while the producer may still be writing; the producer then dies of SIGPIPE and pipefail makes the pipeline report 141 instead of grep's 0. The intended "did it match?" answer is replaced by "the pipeline failed", and a `!` in front of it inverts that into the opposite answer. Measured: the fault depends on how much the producer still has to write. `echo` of a short string is consistently 0; `echo` of a ~1.2 MB string is consistently 141, as is any command producer. Whether a given producer is "small enough" is a property of runtime data, not of the source, so all 31 occurrences are converted rather than a hand-picked subset. `grep -c pattern >/dev/null` reads all input, so no producer ever sees SIGPIPE and the exit status keeps its usual meaning. The concrete consequence, at the pre-flight check in run_proxy_container: a SIGPIPE made is_port_available answer "the port is free" while it was occupied, so the port-conflict guard waved the container start through. Also: - _port_listening helper with an ss -> netstat chain, replacing the two sites that assumed ss exists. On a stock Debian or Fedora neither tool is installed (iproute2 is not in the base image) and on Alpine only busybox netstat is, so the health check's "Port N listening" line and the connection count both reported a deterministic false negative. The helper preserves the previous "unknown => available" behaviour where nothing can answer, so start-up is not blocked on hosts that cannot tell. - Removed `trap _cleanup EXIT` from the generated daemon. _cleanup is defined in the manager (:57) and nothing in the daemon appends to _TEMP_FILES, so the trap could only print "_cleanup: command not found" on every daemon exit while cleaning nothing. tests/test_sigpipe_pipelines.sh pins the class, shows the fault and the fix side by side, and asserts the behavioural consequence. It fails 2/5 beforehand and passes on Debian 12, Alpine 3.20 and Fedora 41.
rvalitov
marked this pull request as draft
September 17, 2026 18:52
rvalitov
marked this pull request as ready for review
September 17, 2026 20:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This script runs with
set -eo pipefail(mtproxymax.sh:10). Under that combination, the commonproducer | grep -q patternidiom is unsafe:grep -qexits the moment it matches, closing the read end of the pipe while the producer may still be writing. The producer dies of SIGPIPE, andpipefailmakes the pipeline report 141 instead of grep's 0 — so the intended answer to "did it match?" is replaced by "the pipeline failed". Put a!in front of it, as several callers do, and the wrong answer is inverted into the opposite one.All 31 occurrences are converted. This also fixes two sites that assumed
ssis installed, and removes a cleanup trap that could never work.Why all 31, and not just the ones that look risky
The fault depends on how much data the producer still has to write, which is a property of runtime data rather than of the source. Measured:
The same
echobuiltin is safe with a small string and reliably broken with a large one, because a write larger than the pipe buffer can be interrupted once grep has exited. Since "is this producer small enough?" cannot be answered from the source, converting a hand-picked subset would leave the same latent flake behind in the rest.grep -creads all of its input, so the producer never receives SIGPIPE, and the exit status keeps its usual meaning (0 = matched, 1 = no match). Combined flags are preserved (-qE→-cE,-qw→-cw).The concrete consequence
is_port_available()feeds the pre-flight port check inrun_proxy_container, which decides whether it is safe to start the container:With the SIGPIPE, the inverted pipeline returned 0 — "the port is free" — while the port was in fact occupied, so the guard silently waved the start through. A live host measured this firing on 7 of 12 consecutive probes.
Two other callers (
run_doctor,port_check) report the same inverted answer, so their "Port N listening" line is unreliable in both directions.Two sites assumed
ssexistsssis not present on a stock Debian or Fedora (iproute2 is not in the base image) and is not on Alpine either, where only busybox'snetstatis available. Two call sites had no fallback:ssget_proxy_stats_check "Port N listening"inmtproxymax healthBoth now go through a
_port_listeninghelper that triesssthennetstat— oneawk '{print $4}'serves both, since each puts the local address in column 4 of-tlnoutput. The helper deliberately preserves the previous "unknown ⇒ available" behaviour when neither tool can answer, so hosts that cannot tell are not blocked from starting.A trap that could never run
The generated daemon script contained:
trap _cleanup EXIT_cleanupis defined in the manager (:57), not in the generated script, and nothing in the daemon ever appends to_TEMP_FILES— the array is only populated on the manager side. So the trap could only ever print_cleanup: command not foundon every daemon exit, while cleaning nothing. Removed, with a comment explaining why there is no trap here.Testing
New
tests/test_sigpipe_pipelines.sh:| grep -qmay remain anywhere in the scriptnetstatreports port 443 as listening,is_port_available 443must report not available, and a port outside the fixture's range must still report availableBefore the change it fails 2/5; after, it passes on Debian 12, Alpine 3.20 and Fedora 41, with no other test result changing.