From 4ce6916007e130d657bc58de692cf871ef38ca5f Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 2 Nov 2026 09:52:37 -0800 Subject: [PATCH] Do not catch SIGTTIN and SIGTTOU When runc run or runc exec (without -d) is run as a background job of a shell with job control, reading from (or writing to) the terminal results in the kernel sending SIGTTIN (or SIGTTOU) to runc. The default action for these signals is to stop the process, which is the expected job control behavior. Yet, runc catches all signals in order to forward them to the container. If runc catches SIGTTIN, it forwards it to the container process, and then the interrupted read is retried, resulting in another SIGTTIN, and so on, so runc ends up in a busy loop. Since commit d92dd226 the signal handler is set up in a separate goroutine, so this only happens occasionally (when the signal handler is set up before runc starts reading stdin); before that, it happened every time. SIGTTIN and SIGTTOU are job control signals of the host terminal, and there is no point in forwarding those to the container. Exclude them from the list of signals to catch, so the default action is used. Note that signal.Reset can not be used to restore the default action, since the Go runtime keeps its own handler installed for these signals (which ignores them, resulting in the same busy loop). Add an integration test. Fixes #1430. Assisted-by: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- signals.go | 2 ++ tests/integration/run.bats | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/signals.go b/signals.go index 153af4837..0138ad51f 100644 --- a/signals.go +++ b/signals.go @@ -37,6 +37,8 @@ func newSignalHandler() chan *signalHandler { switch sig := unix.Signal(i); sig { case unix.SIGCHLD: // Already set up above. + case unix.SIGTTIN, unix.SIGTTOU: + // Job control: use the default action (stop), see #1430. case unix.SIGURG: // SIGURG is used by the Go runtime for asynchronous // preemption, so runc receives it from time to time, diff --git a/tests/integration/run.bats b/tests/integration/run.bats index 34b3555c0..c8042b548 100644 --- a/tests/integration/run.bats +++ b/tests/integration/run.bats @@ -246,3 +246,28 @@ EOF # can not be removed, and the next test to use this name would fail. refute_output --partial "unable to destroy container" } + +@test "runc run [SIGTTIN and SIGTTOU are not caught]" { + update_config '.process.terminal = false | .process.args = ["sleep", "infinity"]' + + runc run test_busybox > ($1 - 1) & 1)) + } + retry 10 0.5 sig_caught 15 # SIGTERM + + # SIGTTIN and SIGTTOU should not be caught (and forwarded to the + # container), so runc in a background process group is stopped + # when accessing the terminal, see #1430. + run ! sig_caught 21 # SIGTTIN + run ! sig_caught 22 # SIGTTOU + + runc kill test_busybox KILL + wait "$pid" || true +}