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