mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
`./setup --help` can hang forever on macOS, printing nothing, with no way
to tell it apart from a slow install. Eleven scripts carry the same
latent hang, `setup` itself being the one every user hits first.
bash 5.2+ delivers a heredoc body of 64KiB or less through a pipe: the
forked child writes the entire body before exec, and nothing reads the
other end until the command starts. Under macOS pipe-KVA pressure the
kernel hands a fresh pipe a 512-byte buffer instead of the usual 16-64KiB,
so any body of 512 bytes or more blocks write() permanently. The capacity
check bash would need to notice (F_GETPIPE_SZ) is Linux-only, so it never
fires here. It is pressure-dependent, which is why it reads as "worked on
my machine" — the same script runs fine all day and then wedges.
Homebrew bash is what `#!/usr/bin/env bash` resolves to on a Mac with brew
on PATH, which is most of them. Apple's /bin/bash 3.2 predates the pipe
path and is unaffected, so the bug is invisible to anyone testing with the
system shell.
The fix is `BASH_COMPAT=50` in each affected script, which restores the
pre-5.2 tempfile path:
$ bash -c 'probe() { [ -p /dev/stdin ] && echo PIPE || echo TEMPFILE; }
probe <<EOF
$(printf "x%.0s" $(seq 1 1000))
EOF'
PIPE
$ BASH_COMPAT=50 bash -c '...same...'
TEMPFILE
- Not a `#!/bin/bash` shebang swap: that pins the script to whatever bash
lives at /bin (3.2 on macOS, absent on some Linux distributions) and is
bypassed entirely by `bash script.sh` call sites. The variable survives
both.
- Not exported, so child processes keep their own compat level.
- Placed below any `--help` sed range that reads $0, so usage output is
unchanged (verified on all eleven).
- Every guarded script is bash-3.2-clean — no associative arrays, case
conversion, or mapfile — so compat level 50 costs them nothing.
test/heredoc-pipe-deadlock.test.ts scans every tracked shell script for a
heredoc body in the 512B-64KiB window and fails without the guard, and
proves the mechanism at runtime on bash 5.2+ by asserting the body moves
from PIPE to TEMPFILE. On older bash the runtime half is skipped, since
the pipe path does not exist there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Absorbed from PR #2640 with authorship preserved. Wave adaptations: the pipe-probe test skips on minimal-/dev environments without /dev/stdin (it would report OTHER for an unobservable fd), and one caveat verified during review: on bash 4.3/4.4 (e.g. Git Bash), assigning BASH_COMPAT=50 prints a non-fatal 'invalid value' warning to stderr — those bashes are already on tempfiles, so the guard is a no-op there; windows-setup-e2e exercises this empirically.