You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug(timeout): failure to exit on strace when due to SIGTTIN interactions with child process #12979
as we confirmed when clock_nanosleep syscall returns -4 it just hangs and wait forever regardless of the time, whiches a deadlock
relunsec@relunsec:~/software/coreutils/target/debug$ strace -e inject=clock_nanosleep:retval=-4 -o /dev/null ./timeout 1s gnucat
strace: Inadvertent injection of error 4 is possible for retval=18446744073709551612
while the gnu one
relunsec@relunsec:~/software/coreutils/target/debug$ strace -e inject=clock_nanosleep:retval=-4 -o /dev/null gnutimeout 1s gnucat
strace: Inadvertent injection of error 4 is possible for retval=18446744073709551612
changed the title [-]bug(timeout): when clock_nanosleep syscall returns -4, it hangs[/-][+]bug(timeout): when clock_nanosleep syscall returns -4, it hangs forever[/+]on Jun 19, 2026
changed the title [-]bug(timeout): when clock_nanosleep syscall returns -4, it hangs forever[/-][+]bug(timeout): when clock_nanosleep syscall returns -4, it hangs/deadlocks forever[/+]on Jun 19, 2026
relunsec@relunsec:~/software/coreutils/target/debug$ time strace -e inject=clock_nanosleep:retval=-4 -o /dev/null gnutimeout 10s gnucat
strace: Inadvertent injection of error 4 is possible for retval=18446744073709551612
real 0m10.025s
user 0m0.007s
sys 0m0.020s
the gnu one behavior it retries the syscall after it fails and will sleep for 10s exactly
relunsec@relunsec:~/software/coreutils/target/debug$ time strace -e inject=clock_nanosleep:retval=-4 -o /dev/null ./timeout 10s gnucat
strace: Inadvertent injection of error 4 is possible for retval=18446744073709551612
^Z
[5]+ Stopped strace -e inject=clock_nanosleep:retval=-4 -o /dev/null ./timeout 10s gnucat
real 0m46.159s
user 0m0.001s
sys 0m0.001s
relunsec@relunsec:~/software/coreutils/target/debug$
as you can see it deadlocks forever after i CTRL+z it sleeped for 46s whiches far than the expected 10s
Ifthecallisinterruptedbyasignalhandler,clock_nanosleep()
fails with the error EINTR. Inaddition,ifremainisnotNULL,andflagswasnotTIMER_ABSTIME,itreturnstheremainingunslepttimeinremain. Thisvaluecanthenbeusedtocallclock_nanosleep() again and complete a (relative) sleep.
as you can see it is not in containers or legacy kernel, it is in any kernel
changed the title [-]bug(timeout): when clock_nanosleep syscall returns -4, it hangs/deadlocks forever[/-][+]bug(timeout): when clock_nanosleep syscall returns EINTR, it hangs/deadlocks forever[/+]on Jun 19, 2026
hmmm. It seems the error was misattributed to clock_nanosleep. We fail to exit out of strace here; notice removing the injection also leads to a stall because we get SIGTTIN'd by the kernel. This appears to happen with cat and other commands that do some funky stuff with stdio.
Mind renaming the issue to reflect the actual error? I'll try to fix it in a PR soon-ish. I think it should be enough to also catch it in our signal mask.
changed the title [-]bug(timeout): when clock_nanosleep syscall returns EINTR, it hangs/deadlocks forever[/-][+]bug(timeout): failure to exit on `strace` when due to `SIGTTIN` interactions with child process[/+]on Jul 27, 2026
Reproduced to sh -c 'trap "echo lol" SIGTTIN; ./target/release/timeout 1 cat'. We should probably get away with ignoring it, but I'll do some experimentation to see what's best.
Also reported by Anthropic's OSS Scanner as ANT-2026-URMP44MS (scan uutils-coreutils-20261015-006).
OSS Scanner report
============================================================================
ANT-2026-URMP44MS [NOT SECURITY: ordinary bug]
timeout: hangs when the command reads the tty (SIGTTIN, SIGTTOU not ignored)
Location: src/uu/timeout/src/platform/unix.rs:53 in prepare()
Commit: 7bc1eb6d1d6fa2e1dc2c9d2277cf576c05f61e47
============================================================================
Not a security issue. This is a real, reproducible bug that is NOT a security issue: by your project's own policy nobody gains anything from it that they did not already have. It is not part of the vulnerability report, is in none of its counts, needs no embargo and no advisory, and can be filed and fixed in the open like any bug.
When timeout is run from a script that has a controlling terminal, it hangs indefinitely if the command it runs reads from the terminal or changes the terminal's settings. For example, sh -c 'timeout 1 openssl pkey -in key.pem' with a passphrase-protected key never returns. openssl gives up on the prompt within milliseconds, but timeout itself is stopped, so the 1-second limit never fires and the script never continues. GNU timeout returns at once in the same situation, and timeout 1 cat shows the same difference (GNU exits with 124 after one second).
The cause is in prepare() in src/uu/timeout/src/platform/unix.rs. It calls setpgid(0, 0), which puts timeout and the command into a new process group that is a background group of the terminal, but it leaves SIGTTIN and SIGTTOU at their default action in timeout itself. When the command touches the terminal, the tty driver sends the signal to the whole group. timeout then stops (state T) inside sigtimedwait(): it never acts on its timer, never reaps the child, and stays that way until something sends SIGCONT or SIGKILL, the calling shell is killed, or the terminal is closed. The pre_exec hook already resets both signals to SIG_DFL in the child, which is only half of GNU's approach. --foreground, a job-control shell invoking timeout directly, and jobs without a controlling terminal (cron, systemd, most CI) are not affected. Scripts run by hand, over ssh -t, under docker run -t or inside tmux are. The missing disposition has been there since the first implementation (a38ee8a, 2014) and is in every release since 0.0.1. The attached reproducer runs the openssl example on a fresh pty. With GNU timeout the script finishes with status 1. With uutils, timeout is left in state T and openssl as an unreaped zombie. The attached patch ignores both signals in timeout right after setpgid() and adds a regression test that does not need a terminal.
This is an ordinary bug, not a security issue: it needs a controlling terminal and a caller without job control, and
its only effect is one hung job, the same as a script that does not use timeout at all; nothing is read, written or
gained across users.
Details
timeout moves itself and the command into a new process group, which on a terminal is a background group, but keeps
the default action for SIGTTIN and SIGTTOU. When the command reads from the terminal or changes its settings, the kernel
sends the signal to every member of the group, so timeout itself is stopped (state T). A stopped timeout does not
act on its timer and does not reap the command; the calling script never continues.
On a terminal, from a shell without job control (any script; sh -c here), with a passphrase-protected key:
$ openssl genpkey -algorithm ed25519 -aes256 -pass pass:whatever -out upload.pem
$ sh -c 'coreutils timeout 1 openssl pkey -in upload.pem -noout; echo rc=$?'
Enter pass phrase for upload.pem:
Could not find private key of key from upload.pem
rc= is never printed. ps shows coreutils timeout … in state T and openssl as a zombie, unchanged 20 s later.
With GNU timeout 9.7 the same line prints rc=1 at once. timeout 1 cat behaves the same way: uutils leaves timeout
and cat both stopped, GNU prints rc=124 after one second.
The disposition is the whole cause, and it shows without a terminal too: coreutils timeout .5 sh -c 'kill -s TTIN 0; sleep 10'
never returns (GNU: 124 after 0.5 s).
prepare() creates the group and sets up the child, src/uu/timeout/src/platform/unix.rs:52-68:
A script's shell runs its commands in its own process group, which is the terminal's foreground group. After setpgid(0, 0)timeout leads a new group that is not the foreground group, and the command is spawned into it. The pre_exec hook gives the child the default action for SIGTTIN/SIGTTOU, which only has a purpose if the parent ignores
them; nothing in timeout does. Neither signal is in timeout_signal_set()
(src/uucore/src/lib/features/process/unix.rs:99-111), so they are not blocked either, and prepare() goes straight on
to install_sigchld() and thread_block() (src/uu/timeout/src/platform/unix.rs:85-86).
For a background group the tty driver answers read() with SIGTTIN and tcsetattr() (and write() under stty tostop) with SIGTTOU, sent to the whole group. timeout is then waiting in timer.timed_sigwait()
(src/uucore/src/lib/features/process/unix.rs:81). Trace of the example; 184 is timeout, 185 is openssl:
openssl catches the signal, gives up and exits within milliseconds. SIGCHLD and, one second later, SIGALRM become
pending for timeout, but a stopped process does not return from sigtimedwait(). The shell (dash) waits with wait4(-1, …, 0, …), without WUNTRACED, and does not see the stop. It ends only when something outside sends
SIGCONT or SIGKILL, kills the calling shell, or closes the terminal (the kernel then hangs up and continues the stopped
group). Ctrl-C does not help: the shell waits for the stopped timeout before acting on it.
Not affected: --foreground (no setpgid(); timeout --foreground 1 cat gives 124), a job-control shell that starts timeout directly (timeout is then already the leader of the foreground group; bash -c 'set -m; timeout 1 cat'
gives 124), and jobs without a controlling terminal.
The missing disposition dates back to the utility's first version, a38ee8a00766 ("Implement timeout (resolves #364)",
2014-07-21), and is in every release since 0.0.1.
Impact
A script that bounds a step with timeout hangs without limit, where GNU timeout returns. It
happens whenever the command touches the terminal: a tool that prompts for a passphrase or password (an encrypted PEM
or PKCS#12 file, a password-protected archive, a client asking for credentials), a plain read such as timeout 1 cat,
or a tcsetattr() to turn echo off. In the example the tool itself fails in milliseconds; it is timeout that never
comes back. The script keeps its terminal, locks and open files until someone sends SIGCONT, kills it or its shell, or
closes the terminal. The end state is the same as a script with no timeout at all: one hung job.
Users meet it only with a controlling terminal and a caller without job control: a script started by hand from a login
shell, over ssh -t, under docker run -t, in script/tmux, or by a runner that allocates a pty. Typing timeout
directly at an interactive job-control shell, --foreground, cron jobs, systemd units and most CI runners are not
affected.
The same hang occurs when the command runs under another uid: root's timeout 1 setpriv --reuid=65534 --regid=65534 --clear-groups sh -c 'read x </dev/tty' leaves root's timeout in state T (GNU: 124 after 1 s). That command is still one the caller chose to run, so the outcome is the same hang.
Reproducing
repro needs no real terminal. It gives a small shell script a fresh pty as its controlling terminal, with the script as leader of a new session, the way a login shell, ssh -t or docker run -t would. It runs the script twice with the same passphrase-protected key: first with GNU /usr/bin/timeout as the control, then with uutils' timeout. It uses the prebuilt /src/target/debug/coreutils, so nothing is built, and it takes about 5 s. It expects its companion files at /tmp/repro.d/. Only openssl and /usr/bin/timeout (GNU) are needed from the image. ps is not needed.
The pieces:
The input: the key. repro creates it with a single openssl genpkey -algorithm ed25519 -aes256 -pass pass:whatever call. Because the key is encrypted, openssl asks for the passphrase on the terminal. Nothing else about the key matters.
The script:repro.d/check_upload.sh, which holds exactly timeout 1 openssl pkey -in "$1" -noout and then echo "check finished with status $?". The script calls plain timeout from PATH. For the uutils run, repro puts a directory first in PATH that holds a timeout symlink to target/debug/coreutils, and the multicall binary picks the utility from argv[0]. Before the runs, repro prints timeout --version for both PATHs, which shows that the first run uses GNU 9.7 and the second uses uutils 0.13.0.
Setup:repro.d/terminal_session.py runs a command on a new pty as a session leader. It collects what the terminal shows until the session ends or 5 s pass. It then reads name and state of the session's other processes from /proc/<pid>/stat and kills them.
For each run, repro prints the terminal output and the processes still left in the session. Expected output:
CONTROL GNU timeout: processes left (waited up to 5 s): []
Enter pass phrase for …/upload.pem:
Could not find private key of key from …/upload.pem
…
check finished with status 1
OBSERVED uutils timeout: processes left (waited up to 5 s): [('openssl', 'Z'), ('timeout', 'T')]
Enter pass phrase for …/upload.pem:
Could not find private key of key from …/upload.pem
…
BUG
In both runs openssl gives up on the passphrase prompt at once. With GNU the script finishes immediately. With uutils, openssl has already exited and is still waiting to be reaped (Z), timeout itself is stopped (T), and the 1 s limit never fires, so "check finished" is never printed. repro prints BUG only when all of these hold: the GNU run finished, the uutils run did not finish within 5 s, and the uutils timeout process is in state T. With the attached patch applied (uu_timeout rebuilt), the same script prints "check finished with status 1" for both runs and OK.
Suggested fix
The attached patch ignores SIGTTIN and SIGTTOU in timeout itself, in prepare() right after the setpgid(0, 0) call
that creates the exposure (src/uu/timeout/src/platform/unix.rs:53), using the same libc::signal(.., SIG_IGN) idiom
the file already uses. GNU timeout ignores both signals the same way. The pre_exec hook already restores the default
action in the child, so the command keeps normal job-control behaviour (its SigIgn mask in /proc/self/status stays
empty), and send_signal() already follows the timeout signal with SIGCONT, which wakes a command stopped at the
terminal. The calls also run under --foreground, where they are harmless.
--- a/src/uu/timeout/src/platform/unix.rs+++ b/src/uu/timeout/src/platform/unix.rs@@ -53,6 +53,16 @@ pub(crate) fn prepare(
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
}
+ // Our new process group is a background group of the controlling terminal,+ // and the tty driver sends SIGTTIN/SIGTTOU to every member of it. Ignore+ // them, or a child touching the terminal would stop us along with itself+ // and the timer would never fire. The child restores the default actions+ // in its pre-exec hook below.+ unsafe {+ let _ = libc::signal(Signal::as_raw(Signal::TTIN), libc::SIG_IGN);+ let _ = libc::signal(Signal::as_raw(Signal::TTOU), libc::SIG_IGN);+ }+
The patch also adds a regression test that needs no terminal, test_not_stopped_by_terminal_signal in tests/by-util/test_timeout.rs: timeout .5 sh -c 'kill -s TTIN 0; sleep 10' (and the same with TTOU) must exit
with 124. At this commit it hangs; with the patch it returns 124 after 0.5 s, and the reproducer prints OK on a
rebuilt clean checkout.
Running the reproducer
bugs-not-security/ANT-2026-URMP44MS/repro, next to this file, with its companion files in bugs-not-security/ANT-2026-URMP44MS/repro.d/ (check_upload.sh, terminal_session.py) — the script reads them from /tmp/repro.d/. It was written by an automated agent and our check ran it as root in a privileged container: run it INSIDE A THROWAWAY VM, with the image oss-scan-7bc1eb6d1d6f built by bash build/build-image.sh, from the archive's top directory:
Activity
[-]bug(timeout): when clock_nanosleep syscall returns -4, it hangs[/-][+]bug(timeout): when clock_nanosleep syscall returns -4, it hangs forever[/+][-]bug(timeout): when clock_nanosleep syscall returns -4, it hangs forever[/-][+]bug(timeout): when clock_nanosleep syscall returns -4, it hangs/deadlocks forever[/+]HackingRepo commentedon Jun 19, 2026
we confirmed further
the gnu one behavior it retries the syscall after it fails and will sleep for 10s exactly
as you can see it deadlocks forever after i CTRL+z it sleeped for 46s whiches far than the expected 10s
oech3 commentedon Jun 19, 2026
When does -4 happen in the actual world? Legacy kernel? or container?
HackingRepo commentedon Jun 19, 2026
-4 it means the syscall interrupted
oech3 commentedon Jun 19, 2026
Please use EINTR instead of magic number.
HackingRepo commentedon Jun 19, 2026
read https://man7.org/linux/man-pages/man2/clock_nanosleep.2.html, @oech3
as you can see it is not in containers or legacy kernel, it is in any kernel
[-]bug(timeout): when clock_nanosleep syscall returns -4, it hangs/deadlocks forever[/-][+]bug(timeout): when clock_nanosleep syscall returns EINTR, it hangs/deadlocks forever[/+]oech3 commentedon Jun 19, 2026
I think this Rust
std::threaditself's issue. It assumes always succeeds.coreutils/src/uucore/src/lib/features/process.rs
Line 163 in de30f0a
HackingRepo commentedon Jun 19, 2026
maybe, keep that issue for a lead maintener like @sylvestre, he will understand in deep and what will do with it
Alonely0 commentedon Jul 25, 2026
HackingRepo commentedon Jul 27, 2026
no not fixed @Alonely0 i recloned the repo just minutes
and samething
Alonely0 commentedon Jul 27, 2026
hmmm. It seems the error was misattributed to
clock_nanosleep. We fail to exit out ofstracehere; notice removing the injection also leads to a stall because we getSIGTTIN'd by the kernel. This appears to happen withcatand other commands that do some funky stuff with stdio.Alonely0 commentedon Jul 27, 2026
Mind renaming the issue to reflect the actual error? I'll try to fix it in a PR soon-ish. I think it should be enough to also catch it in our signal mask.
HackingRepo commentedon Jul 27, 2026
so what title you suggest to me?
Alonely0 commentedon Jul 27, 2026
bug(timeout): failure to exit on
stracewhen due toSIGTTINinteractions with child process[-]bug(timeout): when clock_nanosleep syscall returns EINTR, it hangs/deadlocks forever[/-][+]bug(timeout): failure to exit on `strace` when due to `SIGTTIN` interactions with child process[/+]Alonely0 commentedon Jul 27, 2026
Note that I won't push the fix until the Windows backend by Niko is merged, because I don't want him to deal with more of my bs hahah.
Alonely0 commentedon Jul 27, 2026
Reproduced to
sh -c 'trap "echo lol" SIGTTIN; ./target/release/timeout 1 cat'. We should probably get away with ignoring it, but I'll do some experimentation to see what's best.sylvestre commentedon Oct 25, 2026
Also reported by Anthropic's OSS Scanner as ANT-2026-URMP44MS (scan uutils-coreutils-20261015-006).
OSS Scanner report
When
timeoutis run from a script that has a controlling terminal, it hangs indefinitely if the command it runs reads from the terminal or changes the terminal's settings. For example,sh -c 'timeout 1 openssl pkey -in key.pem'with a passphrase-protected key never returns.opensslgives up on the prompt within milliseconds, buttimeoutitself is stopped, so the 1-second limit never fires and the script never continues. GNU timeout returns at once in the same situation, andtimeout 1 catshows the same difference (GNU exits with 124 after one second).The cause is in
prepare()insrc/uu/timeout/src/platform/unix.rs. It callssetpgid(0, 0), which putstimeoutand the command into a new process group that is a background group of the terminal, but it leaves SIGTTIN and SIGTTOU at their default action intimeoutitself. When the command touches the terminal, the tty driver sends the signal to the whole group.timeoutthen stops (stateT) insidesigtimedwait(): it never acts on its timer, never reaps the child, and stays that way until something sends SIGCONT or SIGKILL, the calling shell is killed, or the terminal is closed. Thepre_exechook already resets both signals toSIG_DFLin the child, which is only half of GNU's approach.--foreground, a job-control shell invokingtimeoutdirectly, and jobs without a controlling terminal (cron, systemd, most CI) are not affected. Scripts run by hand, overssh -t, underdocker run -tor inside tmux are. The missing disposition has been there since the first implementation (a38ee8a, 2014) and is in every release since 0.0.1. The attached reproducer runs theopensslexample on a fresh pty. With GNU timeout the script finishes with status 1. With uutils,timeoutis left in stateTandopensslas an unreaped zombie. The attached patch ignores both signals intimeoutright aftersetpgid()and adds a regression test that does not need a terminal.This is an ordinary bug, not a security issue: it needs a controlling terminal and a caller without job control, and
its only effect is one hung job, the same as a script that does not use
timeoutat all; nothing is read, written orgained across users.
Details
timeoutmoves itself and the command into a new process group, which on a terminal is a background group, but keepsthe default action for SIGTTIN and SIGTTOU. When the command reads from the terminal or changes its settings, the kernel
sends the signal to every member of the group, so
timeoutitself is stopped (stateT). A stoppedtimeoutdoes notact on its timer and does not reap the command; the calling script never continues.
On a terminal, from a shell without job control (any script;
sh -chere), with a passphrase-protected key:rc=is never printed.psshowscoreutils timeout …in stateTandopensslas a zombie, unchanged 20 s later.With GNU timeout 9.7 the same line prints
rc=1at once.timeout 1 catbehaves the same way: uutils leavestimeoutand
catboth stopped, GNU printsrc=124after one second.The disposition is the whole cause, and it shows without a terminal too:
coreutils timeout .5 sh -c 'kill -s TTIN 0; sleep 10'never returns (GNU: 124 after 0.5 s).
prepare()creates the group and sets up the child,src/uu/timeout/src/platform/unix.rs:52-68:A script's shell runs its commands in its own process group, which is the terminal's foreground group. After
setpgid(0, 0)timeoutleads a new group that is not the foreground group, and the command is spawned into it. Thepre_exechook gives the child the default action for SIGTTIN/SIGTTOU, which only has a purpose if the parent ignoresthem; nothing in
timeoutdoes. Neither signal is intimeout_signal_set()(
src/uucore/src/lib/features/process/unix.rs:99-111), so they are not blocked either, andprepare()goes straight onto
install_sigchld()andthread_block()(src/uu/timeout/src/platform/unix.rs:85-86).For a background group the tty driver answers
read()with SIGTTIN andtcsetattr()(andwrite()understty tostop) with SIGTTOU, sent to the whole group.timeoutis then waiting intimer.timed_sigwait()(
src/uucore/src/lib/features/process/unix.rs:81). Trace of the example; 184 istimeout, 185 isopenssl:opensslcatches the signal, gives up and exits within milliseconds. SIGCHLD and, one second later, SIGALRM becomepending for
timeout, but a stopped process does not return fromsigtimedwait(). The shell (dash) waits withwait4(-1, …, 0, …), withoutWUNTRACED, and does not see the stop. It ends only when something outside sendsSIGCONT or SIGKILL, kills the calling shell, or closes the terminal (the kernel then hangs up and continues the stopped
group). Ctrl-C does not help: the shell waits for the stopped
timeoutbefore acting on it.Not affected:
--foreground(nosetpgid();timeout --foreground 1 catgives 124), a job-control shell that startstimeoutdirectly (timeoutis then already the leader of the foreground group;bash -c 'set -m; timeout 1 cat'gives 124), and jobs without a controlling terminal.
The missing disposition dates back to the utility's first version,
a38ee8a00766("Implement timeout (resolves #364)",2014-07-21), and is in every release since 0.0.1.
Impact
A script that bounds a step with
timeouthangs without limit, where GNU timeout returns. Ithappens whenever the command touches the terminal: a tool that prompts for a passphrase or password (an encrypted PEM
or PKCS#12 file, a password-protected archive, a client asking for credentials), a plain read such as
timeout 1 cat,or a
tcsetattr()to turn echo off. In the example the tool itself fails in milliseconds; it istimeoutthat nevercomes back. The script keeps its terminal, locks and open files until someone sends SIGCONT, kills it or its shell, or
closes the terminal. The end state is the same as a script with no
timeoutat all: one hung job.Users meet it only with a controlling terminal and a caller without job control: a script started by hand from a login
shell, over
ssh -t, underdocker run -t, inscript/tmux, or by a runner that allocates a pty. Typingtimeoutdirectly at an interactive job-control shell,
--foreground, cron jobs, systemd units and most CI runners are notaffected.
The same hang occurs when the command runs under another uid: root's
timeout 1 setpriv --reuid=65534 --regid=65534 --clear-groups sh -c 'read x </dev/tty'leaves root'stimeoutin stateT(GNU: 124 after 1 s). That command is still one the caller chose to run, so the outcome is the same hang.Reproducing
reproneeds no real terminal. It gives a small shell script a fresh pty as its controlling terminal, with the script as leader of a new session, the way a login shell,ssh -tordocker run -twould. It runs the script twice with the same passphrase-protected key: first with GNU/usr/bin/timeoutas the control, then with uutils'timeout. It uses the prebuilt/src/target/debug/coreutils, so nothing is built, and it takes about 5 s. It expects its companion files at/tmp/repro.d/. Onlyopenssland/usr/bin/timeout(GNU) are needed from the image.psis not needed.The pieces:
reprocreates it with a singleopenssl genpkey -algorithm ed25519 -aes256 -pass pass:whatevercall. Because the key is encrypted,opensslasks for the passphrase on the terminal. Nothing else about the key matters.repro.d/check_upload.sh, which holds exactlytimeout 1 openssl pkey -in "$1" -nooutand thenecho "check finished with status $?". The script calls plaintimeoutfromPATH. For the uutils run,reproputs a directory first inPATHthat holds atimeoutsymlink totarget/debug/coreutils, and the multicall binary picks the utility fromargv[0]. Before the runs,reproprintstimeout --versionfor bothPATHs, which shows that the first run uses GNU 9.7 and the second uses uutils 0.13.0.repro.d/terminal_session.pyruns a command on a new pty as a session leader. It collects what the terminal shows until the session ends or 5 s pass. It then reads name and state of the session's other processes from/proc/<pid>/statand kills them.For each run,
reproprints the terminal output and the processes still left in the session. Expected output:In both runs
opensslgives up on the passphrase prompt at once. With GNU the script finishes immediately. With uutils,opensslhas already exited and is still waiting to be reaped (Z),timeoutitself is stopped (T), and the 1 s limit never fires, so "check finished" is never printed.reproprintsBUGonly when all of these hold: the GNU run finished, the uutils run did not finish within 5 s, and the uutilstimeoutprocess is in stateT. With the attached patch applied (uu_timeout rebuilt), the same script prints "check finished with status 1" for both runs andOK.Suggested fix
The attached patch ignores SIGTTIN and SIGTTOU in
timeoutitself, inprepare()right after thesetpgid(0, 0)callthat creates the exposure (
src/uu/timeout/src/platform/unix.rs:53), using the samelibc::signal(.., SIG_IGN)idiomthe file already uses. GNU timeout ignores both signals the same way. The
pre_exechook already restores the defaultaction in the child, so the command keeps normal job-control behaviour (its
SigIgnmask in/proc/self/statusstaysempty), and
send_signal()already follows the timeout signal with SIGCONT, which wakes a command stopped at theterminal. The calls also run under
--foreground, where they are harmless.The patch also adds a regression test that needs no terminal,
test_not_stopped_by_terminal_signalintests/by-util/test_timeout.rs:timeout .5 sh -c 'kill -s TTIN 0; sleep 10'(and the same withTTOU) must exitwith 124. At this commit it hangs; with the patch it returns 124 after 0.5 s, and the reproducer prints
OKon arebuilt clean checkout.
Running the reproducer
bugs-not-security/ANT-2026-URMP44MS/repro, next to this file, with its companion files inbugs-not-security/ANT-2026-URMP44MS/repro.d/(check_upload.sh, terminal_session.py) — the script reads them from/tmp/repro.d/. It was written by an automated agent and our check ran it as root in a privileged container: run it INSIDE A THROWAWAY VM, with the imageoss-scan-7bc1eb6d1d6fbuilt bybash build/build-image.sh, from the archive's top directory:It prints its evidence; the last line is
BUGif the issue triggered,OKotherwise.Alonely0 commentedon Oct 25, 2026
@sylvestre It seems the report prefers doing what I had suggested first, but we can also integrate them in our sigwait loop, which I'd say is definitely less hacky.