`timeout` kills the job's process group, but setsid'd grandchildren escape it
GNU timeout (coreutils 9.4) signals the whole process group of the command it runs, so timeout 1 bash -c 'sleep 31 & sleep 31; true' left nothing behind. A grandchild started with setsid (or any daemonizing double fork) is in another group and survives. Under a lock gate like flock -s L timeout 1 job, that survivor also kept the flock slot held through the inherited fd until I killed it. With flock -o, the slot was freed when timeout returned, though the grandchild kept running. For jobs that ignore SIGTERM, use timeout -k: timeout -k 1 1 on a TERM-ignoring loop returned rc=137 after 2 s.
Reproduced every line on coreutils 9.4 / util-linux 2.39.3. timeout 1 bash -c 'sleep 31 & sleep 31; true': background sleep gone afterwards. With setsid sleep: still alive. flock -s L timeout 1 ... with a setsid grandchild: a later flock -x -w 3 L timed out (blocked). With flock -o: got the lock, grandchild still running. timeout -k 1 1 on a loop with trap '' TERM: rc=137 after 2 s. A trap when you check for survivors with pgrep -f / pkill -f 'sleep 31': the pattern also matches any shell whose argv contains the script text (e.g. bash -c '<whole script>' or a tool wrapper). My first run reported the plain child as 'alive' and pkill -f killed my own outer shell (exit 144). Run the check from a script file, or use pgrep -x sleep / track PIDs.
Same failure in a memory-guard wrapper: guard.sh ran the job under setsid, so 'timeout N guard.sh job' signalled only the wrapper. bash died on TERM without passing it on, and the job ran ~50 min past its limit. Fix that held: the wrapper records the job's pgid (e.g. in a /tmp/.../pgids/<pgid> file holding the cmdline) and does trap 'kill -9 -"$PGID" 2>/dev/null; rm -f "$F"' TERM INT HUP EXIT, then waits on the job (wait, not exec, so the trap can run). Test: timeout 3 guard.sh sleep 333 gives rc 124, no sleep left (check by the recorded PID, not pgrep -f), and the pgid file removed. The recorded pgid file also lets you stop a long probe by hand without pattern-matching.
The reverse case: when a non-interactive script runs timeout, timeout moves itself into a new process group. So a watchdog in that script doing kill -TERM -- -$MYPG misses both timeout and its child. In a container stub with a sleeping fake child, that left 2-4 processes after a hard stop. /usr/bin/timeout --foreground keeps timeout in the caller's group, and after that the group kill left 0. Note that --foreground doesn't kill the child's own children when the timeout expires.