From 5b3ad6b87854af085fe4f32a2df8742598bbb421 Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Tue, 3 Nov 2026 07:46:39 -0600 Subject: [PATCH 1/4] Probe closed stdin with an fd duplication in `_autolint_read_files0` Summary: The `--files0-from -` guard rejected a closed descriptor by probing `/dev/stdin` readability. On the ubuntu-26.04 runner that probe misreports a closed stdin, so the read loop treated it as EOF and the command exited 0 instead of 2 with the actionable message. Replace the closed-detection half of the probe with a direct fd test: duplicating fd 0 into a subshell fails exactly when the descriptor is closed, independent of `/dev` symlink semantics. (Duplicating fd 0 onto itself is a no-op even when closed, so the target must differ.) The directory checks are unchanged. Testing: Behavior matrix verified locally: closed stdin exits 2 with the actionable message (direct and via command substitution, as the test invokes it); empty pipe and `/dev/null` exit 0 quietly; directory stdin exits 2; a valid manifest reads normally; truncated input exits 2 with the NUL message. `bash -n` and ShellCheck are clean. Full-suite verification via this PR's CI checks. Note: the dup probe is proven correct on every locally testable fd state; the precise ubuntu-26.04 `/dev` mechanism behind the original misreport remains a hypothesis. --- lib/checkrun/autolint.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/checkrun/autolint.sh b/lib/checkrun/autolint.sh index b3584c2..8069b0d 100644 --- a/lib/checkrun/autolint.sh +++ b/lib/checkrun/autolint.sh @@ -106,7 +106,14 @@ _autolint_read_files0() { # field for both EOF and descriptor errors. Reject a closed descriptor and # directory source before reading so an invalid stream cannot become a # silent no-op. Pipes, regular files, sockets, and terminals remain valid. - if { [ ! -r /dev/stdin ] && [ ! -r /dev/fd/0 ]; } || + # Test descriptor openness by duplicating fd 0 into a subshell rather + # than by probing /dev/stdin readability: the /dev probe misreports on + # hosts where /dev/stdin does not track the descriptor (observed on the + # ubuntu-26.04 runner, where a closed stdin passed the probe and read + # as EOF). Duplicating a closed descriptor fails; duplicating an open + # one succeeds regardless of what it points at. (Duplicating fd 0 onto + # itself is a no-op even when closed, so the target must differ.) + if ! ( exec 3<&0 ) 2>/dev/null || [ -d /dev/stdin ] || [ -d /dev/fd/0 ]; then echo "autolint: --files0-from - requires a readable stream on standard input" >&2 return 2 From 81bf88ea724ee7daa2709c2aa5b5a46fa5210521 Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Tue, 3 Nov 2026 08:12:25 -0600 Subject: [PATCH 2/4] Temporarily diagnose closed-stdin guard on CI (DO NOT MERGE) Summary: PR checks still fail the closed-stdin assertions with the fd-dup probe, while every local replication rejects closed stdin. Add an env-gated fd-state dump (CHECKRUN_DIAG_FD0, set only by the closed-stdin test) so the runner's job log shows the real fd 0 state. Revert before merge. --- lib/checkrun/autolint.sh | 14 ++++++++++++++ test/suites/autolint-test | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/checkrun/autolint.sh b/lib/checkrun/autolint.sh index 8069b0d..a367f5c 100644 --- a/lib/checkrun/autolint.sh +++ b/lib/checkrun/autolint.sh @@ -102,6 +102,20 @@ _autolint_read_files0() { # sharing decoded paths while still supporting Bash 3.2 without namerefs. _autolint_files0_args=() if [ "$display" = "-" ]; then + # TEMPORARY DIAGNOSTIC (revert before merge): CI reports closed stdin + # passing this guard while every local replication rejects it. Dump the + # real fd 0 state when CHECKRUN_DIAG_FD0 is set (the closed-stdin test + # sets it). Writes only to stderr; no assertion input is affected. + if [ -n "${CHECKRUN_DIAG_FD0:-}" ]; then + { + echo "DIAG fd0: BASH_VERSION=$BASH_VERSION" + ls -l "/proc/$$/fd/" 2>&1 | sed 's/^/DIAG fd: /' + if [ -e "/proc/$$/fd/0" ]; then echo "DIAG fd0: EXISTS"; else echo "DIAG fd0: MISSING"; fi + if ( exec 3<&0 ) 2>/dev/null; then echo "DIAG dup-probe: OPEN"; else echo "DIAG dup-probe: CLOSED"; fi + if [ -r /dev/stdin ]; then echo "DIAG /dev/stdin: readable"; else echo "DIAG /dev/stdin: NOT readable"; fi + if [ -d /dev/stdin ]; then echo "DIAG /dev/stdin: dir"; else echo "DIAG /dev/stdin: not dir"; fi + } >&2 + fi # EOF is a valid empty manifest, but Bash gives a failed read with an empty # field for both EOF and descriptor errors. Reject a closed descriptor and # directory source before reading so an invalid stream cannot become a diff --git a/test/suites/autolint-test b/test/suites/autolint-test index d434a1a..8ec8a7c 100755 --- a/test/suites/autolint-test +++ b/test/suites/autolint-test @@ -3056,7 +3056,7 @@ _assert_exit "files0 stdin: empty input is a no-op" 0 "$rc" _assert_eq "files0 stdin: empty input is quiet" "" "$output" set +e -output=$("$AUTOLINT" --files0-from - <&- 2>&1) +output=$(CHECKRUN_DIAG_FD0=1 "$AUTOLINT" --files0-from - <&- 2>&1) rc=$? set -e _assert_exit "files0 stdin: closed descriptor exits 2" 2 "$rc" From bde72f2b00e66e7e7d8560910b20c28ebe89ddc9 Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Tue, 3 Nov 2026 08:19:53 -0600 Subject: [PATCH 3/4] Temporarily probe fresh-bash closed-stdin behavior on CI (DO NOT MERGE) Summary: Follow-up diagnostic: run a fresh `bash -c` probe with closed stdin in the same command-substitution form as the closed-stdin test, to locate where fd 0 becomes /dev/null (bash startup vs the autolint script path). Revert before merge. --- test/suites/autolint-test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/suites/autolint-test b/test/suites/autolint-test index 8ec8a7c..1947fe3 100755 --- a/test/suites/autolint-test +++ b/test/suites/autolint-test @@ -3056,6 +3056,8 @@ _assert_exit "files0 stdin: empty input is a no-op" 0 "$rc" _assert_eq "files0 stdin: empty input is quiet" "" "$output" set +e +probe_output=$(/usr/bin/env bash -c 'echo "PROBE bash: $BASH_VERSION"; ls -l /proc/$$/fd/ 2>&1 | sed "s/^/PROBE fd: /"; if ( exec 3<&0 ) 2>/dev/null; then echo PROBE-OPEN; else echo PROBE-CLOSED; fi' <&- 2>&1) +echo "PROBE-RESULT: $probe_output" >&2 output=$(CHECKRUN_DIAG_FD0=1 "$AUTOLINT" --files0-from - <&- 2>&1) rc=$? set -e From 20e41f7cb0f0c3ac1fdad83cff737f863ca55a00 Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Tue, 3 Nov 2026 08:32:32 -0600 Subject: [PATCH 4/4] Fix closed-stdin guard and test it in-process Summary: The runner's bash reopens /dev/null for a closed stdin at process startup, so a subprocess invocation can never observe the closed descriptor: replace the /dev/stdin readability probe with an fd-duplication probe, and invoke the closed-stdin test in-process (command-substitution subshell) where the closed fd survives. Diagnostics from the investigation are reverted; the dup probe and the in-process test are the whole change. --- lib/checkrun/autolint.sh | 27 +++++++-------------------- test/suites/autolint-test | 10 +++++++--- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/lib/checkrun/autolint.sh b/lib/checkrun/autolint.sh index a367f5c..05aa7b6 100644 --- a/lib/checkrun/autolint.sh +++ b/lib/checkrun/autolint.sh @@ -102,31 +102,18 @@ _autolint_read_files0() { # sharing decoded paths while still supporting Bash 3.2 without namerefs. _autolint_files0_args=() if [ "$display" = "-" ]; then - # TEMPORARY DIAGNOSTIC (revert before merge): CI reports closed stdin - # passing this guard while every local replication rejects it. Dump the - # real fd 0 state when CHECKRUN_DIAG_FD0 is set (the closed-stdin test - # sets it). Writes only to stderr; no assertion input is affected. - if [ -n "${CHECKRUN_DIAG_FD0:-}" ]; then - { - echo "DIAG fd0: BASH_VERSION=$BASH_VERSION" - ls -l "/proc/$$/fd/" 2>&1 | sed 's/^/DIAG fd: /' - if [ -e "/proc/$$/fd/0" ]; then echo "DIAG fd0: EXISTS"; else echo "DIAG fd0: MISSING"; fi - if ( exec 3<&0 ) 2>/dev/null; then echo "DIAG dup-probe: OPEN"; else echo "DIAG dup-probe: CLOSED"; fi - if [ -r /dev/stdin ]; then echo "DIAG /dev/stdin: readable"; else echo "DIAG /dev/stdin: NOT readable"; fi - if [ -d /dev/stdin ]; then echo "DIAG /dev/stdin: dir"; else echo "DIAG /dev/stdin: not dir"; fi - } >&2 - fi # EOF is a valid empty manifest, but Bash gives a failed read with an empty # field for both EOF and descriptor errors. Reject a closed descriptor and # directory source before reading so an invalid stream cannot become a # silent no-op. Pipes, regular files, sockets, and terminals remain valid. # Test descriptor openness by duplicating fd 0 into a subshell rather - # than by probing /dev/stdin readability: the /dev probe misreports on - # hosts where /dev/stdin does not track the descriptor (observed on the - # ubuntu-26.04 runner, where a closed stdin passed the probe and read - # as EOF). Duplicating a closed descriptor fails; duplicating an open - # one succeeds regardless of what it points at. (Duplicating fd 0 onto - # itself is a no-op even when closed, so the target must differ.) + # than by probing /dev/stdin readability: on the ubuntu-26.04 runner + # a closed stdin arrives as /dev/null (the runner's bash reopens it + # at process startup, verified with a fresh `bash -c` probe), so the + # /dev probe passes and the read loop treats it as EOF. Duplicating + # a closed descriptor fails; duplicating an open one succeeds + # regardless of what it points at. (Duplicating fd 0 onto itself is + # a no-op even when closed, so the target must differ.) if ! ( exec 3<&0 ) 2>/dev/null || [ -d /dev/stdin ] || [ -d /dev/fd/0 ]; then echo "autolint: --files0-from - requires a readable stream on standard input" >&2 diff --git a/test/suites/autolint-test b/test/suites/autolint-test index 1947fe3..161f1a1 100755 --- a/test/suites/autolint-test +++ b/test/suites/autolint-test @@ -3056,9 +3056,13 @@ _assert_exit "files0 stdin: empty input is a no-op" 0 "$rc" _assert_eq "files0 stdin: empty input is quiet" "" "$output" set +e -probe_output=$(/usr/bin/env bash -c 'echo "PROBE bash: $BASH_VERSION"; ls -l /proc/$$/fd/ 2>&1 | sed "s/^/PROBE fd: /"; if ( exec 3<&0 ) 2>/dev/null; then echo PROBE-OPEN; else echo PROBE-CLOSED; fi' <&- 2>&1) -echo "PROBE-RESULT: $probe_output" >&2 -output=$(CHECKRUN_DIAG_FD0=1 "$AUTOLINT" --files0-from - <&- 2>&1) +# Invoke in-process, not via bin/autolint: the ubuntu-26.04 runner's bash +# reopens /dev/null for a closed stdin at process startup (verified: a +# fresh `bash -c` probe with closed stdin arrives with fd 0 -> /dev/null), +# so a subprocess invocation can never observe the closed descriptor the +# guard is meant to reject. A command-substitution subshell forks without +# re-running bash startup, so the closed fd survives to _autolint_main. +output=$(. "$CHECKRUN_TEST_ROOT/lib/checkrun/autolint.sh"; _autolint_main --files0-from - <&- 2>&1) rc=$? set -e _assert_exit "files0 stdin: closed descriptor exits 2" 2 "$rc"