Dear Bash Maintainers,
First of all, please accept my sincere gratitude and highest admiration for the
monumental work you have dedicated over the decades to building and maintaining
GNU Bash. While carefully studying `jobs.c` to understand how Bash safely
handles signal delivery against PID reuse races, we noticed a loop control flaw
in `kill_pid` that we wanted to respectfully share with you.
Configuration Information [Automatically generated, do not change]:
Machine: x86_64
OS: linux-gnu
Bash Version: 5.2
Patch Level: 21
Release Status: release
Description:
In `jobs.c` (`kill_pid`, around lines 3705-3715), when sending a signal to a job
spec (`kill -TERM %1`) while job control is disabled (`job_control == 0`), Bash
iterates over the circular process list `jobs[job]->pipe`:
p = jobs[job]->pipe;
do
{
if (PALIVE (p) == 0)
continue;
kill (p->pid, sig);
p = p->next;
}
while (p != jobs[job]->pipe);
Because `continue` jumps directly to the `while (p != jobs[job]->pipe)` test
without executing `p = p->next;`, two issues occur on multi-process pipelines:
1.
If the first process in the pipeline is alive (`PALIVE(p) != 0`) and the
*
second (or any subsequent) process has already exited (`PALIVE(p) == 0`), `p`
advances to the dead process and then `continue` loops forever because
`p != jobs[job]->pipe` remains true and `p` is never incremented (100% CPU
hang with `SIGCHLD` blocked).
2.
Conversely, if the first process (`jobs[job]->pipe`) has already exited while
*
downstream processes in the pipeline are still alive, `continue` on the first
iteration evaluates `p != jobs[job]->pipe` (which is false) and exits the
loop immediately without signalling the remaining live processes.
Repeat-By:
Run the following script with job control disabled (default in scripts):
#!/usr/bin/env bash
set +m
sleep 10 | true &
sleep 0.2 # Let the second process ('true') exit so PALIVE(p) == 0
kill -TERM %1
Fix:
Advance `p = p->next` unconditionally before checking the loop condition, by
guarding only the `kill()` call:
p = jobs[job]->pipe;
do
{
if (PALIVE (p))
kill (p->pid, sig);
p = p->next;
}
while (p != jobs[job]->pipe);
Thank you very much for your time, your review, and your tireless stewardship of
GNU Bash.
With my highest regards and most respectful salutations,
Edgar Magdelaine