timeout (GNU timeout at least) by default tries to run the command in a new process group and kills that process group with SIGTERM upon timeout.
That way, if that command spawned more processes, they are also killed upon timeout.
~$ strace -fze '/[sg]etpg|exec|kill|exit' sh -c 'timeout 1 sleep 2 | sleep 3'
execve("/usr/bin/sh", ["sh", "-c", "timeout 1 sleep 2 | sleep 3"], 0x7fffe2349230 /* 69 vars */) = 0
strace: Process 316058 attached
strace: Process 316059 attached
[pid 316058] execve("/usr/bin/timeout", ["timeout", "1", "sleep", "2"], 0x561e384e43a8 /* 69 vars */) = 0
[pid 316059] execve("/usr/bin/sleep", ["sleep", "3"], 0x561e384e41c8 /* 69 vars */) = 0
[pid 316058] setpgid(0, 0) = 0
strace: Process 316060 attached
[pid 316060] execve("/usr/bin/sleep", ["sleep", "2"], 0x7ffc3ef29910 /* 69 vars */) = 0
[pid 316058] --- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_int=0, si_ptr=NULL} ---
[pid 316058] kill(316060, SIGTERM) = 0
[pid 316060] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=316058, si_uid=1000} ---
[pid 316058] kill(0, SIGTERM) = 0
[pid 316058] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=316058, si_uid=1000} ---
[pid 316060] +++ killed by SIGTERM +++
[pid 316058] kill(316060, SIGCONT <unfinished ...>
) = 0
[pid 316058] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=316060, si_uid=1000, si_status=SIGTERM, si_utime=0, si_stime=0} ---
[pid 316058] kill(0, SIGCONT) = 0
[pid 316058] --- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=316058, si_uid=1000} ---
[pid 316058] +++ exited with 124 +++
[pid 316057] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=316058, si_uid=1000, si_status=124, si_utime=0, si_stime=0} ---
[pid 316059] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=316059, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
See timeout doing a setpgid(0, 0) (same as setpgrp()) to create a new process group, you see it doing kill(316060, SIGTERM) to kill sleep 2, but also kill(0, SIGTERM) to kill its own process group (which it created earlier) which would have also killed processes spawned by sleep 2 if any (none in this case).
Now, when job control is enabled in the shell (like when interactive or called with the -m / -o monitor option), in:
timeout 1 sleep 2 | sleep 3
The shell already starts timeout and sleep 3 in a new process group, and in most shells, the process group leader will be the one that runs timeout.
So when timeout does setpgrp(), same as setpgid(0, 0), it will not create a new process group, that just becomes a no-op. So the process group will still contain timeout, the process it spawns to run sleep 2, and sleep 3 which was put there beforehand by the shell.
~$ strace -fze '/[sg]etpg|exec|kill|exit' sh -o monitor -c 'timeout 1 sleep 2 | sleep 3'
execve("/usr/bin/sh", ["sh", "-o", "monitor", "-c", "timeout 1 sleep 2 | sleep 3"], 0x7ffeb8e13640 /* 69 vars */) = 0
getpgrp() = 318590
setpgid(0, 318593) = 0
strace: Process 318594 attached
[pid 318593] setpgid(318594, 318594) = 0
[pid 318594] setpgid(0, 318594) = 0
strace: Process 318595 attached
[pid 318595] setpgid(0, 318594) = 0
[pid 318593] setpgid(318595, 318594) = 0
[pid 318595] execve("/usr/bin/sleep", ["sleep", "3"], 0x557fde7871c8 /* 69 vars */) = 0
[pid 318594] execve("/usr/bin/timeout", ["timeout", "1", "sleep", "2"], 0x557fde7873a8 /* 69 vars */) = 0
[pid 318594] setpgid(0, 0) = 0
strace: Process 318596 attached
[pid 318596] execve("/usr/bin/sleep", ["sleep", "2"], 0x7ffc60f34c60 /* 69 vars */) = 0
[pid 318594] --- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_int=0, si_ptr=NULL} ---
[pid 318594] kill(318596, SIGTERM) = 0
[pid 318596] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318596] +++ killed by SIGTERM +++
[pid 318594] kill(0, SIGTERM <unfinished ...>
) = 0
[pid 318595] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318594] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318595] +++ killed by SIGTERM +++
[pid 318594] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=318596, si_uid=1000, si_status=SIGTERM, si_utime=0, si_stime=0} ---
[pid 318593] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=318595, si_uid=1000, si_status=SIGTERM, si_utime=0, si_stime=0} ---
Terminated
[pid 318594] kill(318596, SIGCONT) = 0
[pid 318594] kill(0, SIGCONT) = 0
[pid 318594] --- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318594] +++ exited with 124 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=318594, si_uid=1000, si_status=124, si_utime=0, si_stime=0} ---
setpgid(0, 318590) = 0
+++ exited with 143 +++
This time, the kill(0, SIGTERM) kills the process running sleep 3 as it's also in the 318594 process group (as put there by the shell with setpgid(318595, 318594)).
In:
sleep 2 | timeout 1 sleep 3 | sleep 4
You'll find that the pipeline lasts 4 seconds and only sleep 3 is killed after 1 second as this time, the process group created by the shell is led by the process running sleep 2, so timeout is able to create a new process group for itself (and its child sleep 3) which is different from the process group created by the shell for the 3 commands in the pipeline (and as a result, you'll find that Ctrl+C or Ctrl+Z don't work properly whilst timeout is running as timeout is not running in the foreground process group).
When run with --foreground, timeout skips creating that extra process group and doesn't do a kill(0, SIGTERM) to kill its own process group, so the behaviour is more consistent but means grandchildren processes are not killed.
$ strace -fze '/[sg]etpg|ioctl|exec|kill|exit' sh -o monitor -c 'timeout --foreground 1 sh -c "sleep 2; exit" | sleep 3'
execve("/usr/bin/sh", ["sh", "-o", "monitor", "-c", "timeout --foreground 1 sh -c \"sl"...], 0x7ffe289c2910 /* 69 vars */) = 0
ioctl(10, TIOCGPGRP, [331754]) = 0
getpgrp() = 331754
setpgid(0, 331757) = 0
ioctl(10, TIOCSPGRP, [331757]) = 0
strace: Process 331758 attached
[pid 331757] setpgid(331758, 331758) = 0
[pid 331758] setpgid(0, 331758) = 0
[pid 331758] ioctl(10, TIOCSPGRP, [331758]) = 0
strace: Process 331759 attached
[pid 331757] setpgid(331759, 331758) = 0
[pid 331759] setpgid(0, 331758) = 0
[pid 331759] ioctl(10, TIOCSPGRP, [331758]) = 0
[pid 331759] execve("/usr/bin/sleep", ["sleep", "3"], 0x55dc52f76418 /* 69 vars */) = 0
[pid 331758] execve("/usr/bin/timeout", ["timeout", "--foreground", "1", "sh", "-c", "sleep 2; exit"], 0x55dc52f763a8 /* 69 vars */) = 0
strace: Process 331760 attached
[pid 331760] execve("/usr/bin/sh", ["sh", "-c", "sleep 2; exit"], 0x7fff756826c0 /* 69 vars */) = 0
strace: Process 331761 attached
[pid 331761] execve("/usr/bin/sleep", ["sleep", "2"], 0x557447790168 /* 69 vars */) = 0
[pid 331758] --- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_int=0, si_ptr=NULL} ---
[pid 331758] kill(331760, SIGTERM) = 0
[pid 331760] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=331758, si_uid=1000} ---
[pid 331760] +++ killed by SIGTERM +++
[pid 331758] +++ exited with 124 +++
[pid 331757] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=331758, si_uid=1000, si_status=124, si_utime=0, si_stime=0} ---
[pid 331761] +++ exited with 0 +++
[pid 331759] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=331759, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
ioctl(10, TIOCSPGRP, [331757]) = 0
ioctl(10, TIOCSPGRP, [331754]) = 0
setpgid(0, 331754) = 0
+++ exited with 0 +++
sh is killed, but not sleep 2.
That also explains why in an interactive shell in a terminal:
sh -c 'timeout 10 cat; exit'
Or:
sleep 10 | timeout 10 cat /dev/tty
cat can't read from the terminal. It ends up being suspended when it tries as it's in a new process group, so no longer in the foreground process group of the terminal.
Again, adding the --foreground option avoids the problem.