Dear Bash maintainers,
After reviewing the original test and the Bash sources, I would like to correct 
the reproducer and qualify the proposed fix in my previous report.
The simplified Repeat-By I sent does not reliably exercise the intended 
failure. It reads one line, kills the process, and immediately switches files. 
However, read_builtin calls zsyncfd before returning, so synchronization 
normally takes place before that kill. The script also ignores the return 
status of the subsequent read, potentially misclassifying a failed read as 
contamination.
The original laboratory test continues reading the same /proc/PID/status after 
killing the process at the second iteration, then exits early at SigIgn. 
Existing logs for Bash 5.2.21(1)-release record a successful subsequent read 
from /proc/OTHER_PID/stat returning a line beginning with SigCgt instead of the 
expected PID.
Here is that original test, with its introductory project-specific comment 
removed. It has not been rerun during this review:

sleep 30 & p=$!
sleep 0.1
f() {
    local l n=0
    while IFS= read -r l; do
        n=$((n + 1))
        if (( n == 2 )); then
            kill -KILL "$p"
            wait "$p" 2>/dev/null
        fi
        [[ $l == SigIgn:* ]] && return 0
    done < "/proc/$p/status"
    return 1
}
f; echo "f=$?"
sleep 30 & q=$!; sleep 0.1

if { IFS= read -r st < "/proc/$q/stat"; } 2>/dev/null; then
    if [[ $st == "$q ("* ]]; then
        echo "lecture suivante OK : contenu propre (${st:0:20})"
    else
        echo "lecture suivante OK : CONTAMINATION (${st:0:20})"
    fi
else
    echo "lecture suivante EN ÉCHEC (rc=$?) : non classé comme contamination"
fi

if { IFS= read -r st < "/proc/self/stat"; } 2>/dev/null; then
    echo "autre fichier OK"
else
    echo "autre fichier EN ÉCHEC"
fi
kill $q

Two source-level corrections are also necessary:

  *
zsyncfd is called at the end of each buffered read builtin invocation, not 
specifically when leaving the while loop.
  *
The buffered character-reading functions are zreadc and zreadcintr, rather than 
zread itself.

The conditional buffer reset in zsyncfd is present in both the official 5.2.21 
and 5.3 sources. This supports the proposed mechanism, but does not establish 
reproduction on 5.3.
Finally, I should not have presented unconditional buffer invalidation as a 
validated fix. If seeking fails and reading continues from the same source, 
discarding the buffer could lose unread bytes. A possible direction is to 
prevent buffered bytes from surviving a change of input source, while 
preserving valid continued reads from the same source. Descriptor reuse and 
restoration would need careful handling.
Please treat the original “Fix” section as an untested suggestion. The 
successful cross-file contamination recorded in the original test remains the 
observation to investigate.
Thank you for reviewing these corrections.
Best regards,
Edgar Magdelaine

________________________________
De : Edgar MAGDELAINE <[email protected]>
Envoyé : mardi 13 octobre 2026 23:00
À : [email protected] <[email protected]>
Objet : [Bash 5.2.21] read builtin leaks buffered data across distinct file 
redirections when lseek fails on /proc

Dear Bash Maintainers,

First of all, I would like to express my deepest gratitude and utmost respect
for the titanic work you have accomplished over the decades in maintaining and
evolving GNU Bash. It is a monumental pillar of our daily engineering work, and
the subtle edge case described below was only uncovered because we subjected our
scripts to extreme stress-testing and syscall tracing.

Configuration Information [Automatically generated, do not change]:
Machine: x86_64
OS: linux-gnu
Compiler: gcc
Bash Version: 5.2
Patch Level: 21
Release Status: release

Description:
When a redirected read loop (`while read ...; do ...; done < /proc/PID/status`)
exits early before EOF, Bash calls `zsyncfd(0)` (`lib/sh/zread.c`) to rewind the
file descriptor by the number of unconsumed bytes remaining in `lbuf`
(`off = lused - lind`).

If the target process exits while `/proc/PID/status` is being read, the
`lseek(fd, -off, SEEK_CUR)` call in `zsyncfd()` fails with `ESRCH` or `EINVAL`
(returning -1). Because `zsyncfd()` only resets the buffer pointers when `lseek`
succeeds:

    if (r != -1)
      lused = lind = 0;

`lused` and `lind` are left untouched when `lseek` returns -1, even though fd 0
is immediately closed/restored as the redirected loop finishes.

Consequently, the next `read` builtin executed on a completely different file
redirected to fd 0 (e.g., `read -r line < /proc/OTHER_PID/stat`) does not issue
a `read(2)` syscall at all: `zread()` sees `lind < lused` and returns stale
leftover bytes (such as `SigCgt:...` lines from the dead process's `status`
file) as if they came from the new file.

Repeat-By:
Run the following standalone script on Linux:

#!/usr/bin/env bash
for (( i = 1; i <= 200; i++ )); do
    sleep 0.05 &
    p=$!
    {
        read -r _
        kill -KILL "$p" 2>/dev/null
        wait "$p" 2>/dev/null
    } < "/proc/$p/status" 2>/dev/null

    IFS= read -r stat_line < "/proc/$$/stat"
    if [[ $stat_line != "$$ "* ]]; then
        printf 'Trial %d: buffer bleed detected! Read from /proc/$$/stat got: 
%s\n' "$i" "$stat_line"
        break
    fi
done

Fix:
In `lib/sh/zread.c`, `zsyncfd()` should unconditionally invalidate the internal
buffer (`lused = lind = 0;`) regardless of whether `lseek()` succeeds or fails,
or `zreset()` should be called whenever a redirected file descriptor is closed
or restored.

I sincerely hope this minimal reproduction and analysis prove helpful. Thank you
again for your extraordinary dedication to GNU Bash over the years.

With my highest regards and most respectful salutations,

Edgar Magdelaine

Reply via email to