Files
runc/tests/integration/run.bats
T
Kir Kolyshkin 4ce6916007 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 <kolyshkin@gmail.com>
2026-11-02 10:33:59 -08:00

274 lines
8.9 KiB
Bash

#!/usr/bin/env bats
load helpers
function setup() {
setup_busybox
update_config '.process.args = ["/bin/echo", "Hello World"]'
}
function teardown() {
teardown_bundle
}
@test "runc run" {
run -0 runc run test_hello
run ! runc state test_hello
}
@test "runc run --keep" {
run -0 runc run --keep test_run_keep
testcontainer test_run_keep stopped
run -0 runc state test_run_keep
run -0 runc delete test_run_keep
run ! runc state test_run_keep
}
@test "runc run --keep (check cgroup exists)" {
# for systemd driver, the unit's cgroup path will be auto removed if container's all processes exited
requires no_systemd
[ $EUID -ne 0 ] && requires rootless_cgroup
set_cgroups_path
run -0 runc run --keep test_run_keep
testcontainer test_run_keep stopped
run -0 runc state test_run_keep
# check that cgroup exists
check_cgroup_value "pids.max" "max"
run -0 runc delete test_run_keep
run ! runc state test_run_keep
}
@test "runc run [hostname domainname]" {
update_config ' .process.args |= ["sh"]
| .hostname = "myhostname"
| .domainname= "mydomainname"'
run -0 runc run -d --console-socket "$CONSOLE_SOCKET" test_utc
# test hostname
run -0 runc exec test_utc hostname
assert_line --index 0 --partial 'myhostname'
# test domainname
run -0 runc exec test_utc cat /proc/sys/kernel/domainname
assert_line --index 0 --partial 'mydomainname'
}
# https://github.com/opencontainers/runc/issues/3952
@test "runc run with tmpfs" {
requires root
chmod 'a=rwx,ug+s,+t' rootfs/tmp # set all bits
mode=$(stat -c %A rootfs/tmp)
update_config '.process.args = ["sh", "-c", "stat -c %A /tmp"]'
update_config '.mounts += [{"destination": "/tmp", "type": "tmpfs", "source": "tmpfs", "options":["noexec","nosuid","nodev","rprivate"]}]'
run -0 runc run test_tmpfs
assert_line --index 0 "$mode"
}
@test "runc run with tmpfs perms" {
update_config '.process.args = ["sh", "-c", "stat -c %a /tmp/test"]'
update_config '.mounts += [{"destination": "/tmp/test", "type": "tmpfs", "source": "tmpfs", "options": ["mode=0444"]}]'
# Directory is to be created by runc.
run -0 runc run test_tmpfs
assert_line --index 0 "444"
# Run a 2nd time with the pre-existing directory.
# Ref: https://github.com/opencontainers/runc/issues/3911
run -0 runc run test_tmpfs
assert_line --index 0 "444"
# Existing directory, custom perms, no mode on the mount,
# so it should use the directory's perms.
update_config '.mounts[-1].options = []'
chmod 0710 rootfs/tmp/test
run -0 runc run test_tmpfs
assert_line --index 0 "710"
# Add back the mode on the mount, and it should use that instead.
# Just for fun, use different perms than was used earlier.
update_config '.mounts[-1].options = ["mode=0410"]'
run -0 runc run test_tmpfs
assert_line --index 0 "410"
}
@test "runc run [/proc/self/exe clone]" {
run -0 runc --debug run test_hello
assert_output --partial "Hello World"
assert_output --partial "runc exeseal: using /proc/self/exe clone"
# runc will use fsopen("overlay") if it can.
if can_fsopen overlay; then
assert_output --partial "runc exeseal: using overlayfs for sealed /proc/self/exe"
fi
}
@test "runc run [joining existing container namespaces]" {
requires timens
# Create a detached container with the namespaces we want. We notably want
# to include both userns and timens, which require config-related
# configuration.
if [ $EUID -eq 0 ]; then
update_config '.linux.namespaces += [{"type": "user"}]
| .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 100}]
| .linux.gidMappings += [{"containerID": 0, "hostID": 200000, "size": 200}]'
remap_rootfs
fi
update_config '.linux.namespaces += [{"type": "time"}]
| .linux.timeOffsets = {
"monotonic": { "secs": 7881, "nanosecs": 2718281 },
"boottime": { "secs": 1337, "nanosecs": 3141519 }
}'
update_config '.process.args = ["sleep", "infinity"]'
run -0 runc run -d --console-socket "$CONSOLE_SOCKET" target_ctr
# Modify our container's configuration such that it is just going to
# inherit all of the namespaces of the target container.
#
# NOTE: We cannot join the mount namespace of another container because of
# some quirks of the runtime-spec. In particular, we MUST pivot_root into
# root.path and root.path MUST be set in the config, so runc cannot just
# ignore root.path when joining namespaces (and root.path doesn't exist
# inside root.path, for obvious reasons).
#
# We could hack around this (create a copy of the rootfs inside the rootfs,
# or use a simpler mount namespace target), but those wouldn't be similar
# tests to the other namespace joining tests.
target_pid="$(runc state target_ctr | jq .pid)"
update_config '.linux.namespaces |= map_values(.path = if .type == "mount" then "" else "/proc/'"$target_pid"'/ns/" + ({"network": "net", "mount": "mnt"}[.type] // .type) end)'
# Remove the userns and timens configuration (they cannot be changed).
update_config '.linux |= (del(.uidMappings) | del(.gidMappings) | del(.timeOffsets))'
run -0 runc run -d --console-socket "$CONSOLE_SOCKET" attached_ctr
# Make sure there are two sleep processes in our container.
run -0 runc exec attached_ctr ps aux
run -0 grep "sleep infinity" <<<"$output"
[ "${#lines[@]}" -eq 2 ]
# ... that the userns mappings are the same...
run -0 runc exec attached_ctr cat /proc/self/uid_map
if [ $EUID -eq 0 ]; then
grep -E '^\s+0\s+100000\s+100$' <<<"$output"
else
grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output"
fi
run -0 runc exec attached_ctr cat /proc/self/gid_map
if [ $EUID -eq 0 ]; then
grep -E '^\s+0\s+200000\s+200$' <<<"$output"
else
grep -E '^\s+0\s+'$EUID'\s+1$' <<<"$output"
fi
# ... as well as the timens offsets.
run -0 runc exec attached_ctr cat /proc/self/timens_offsets
grep -E '^monotonic\s+7881\s+2718281$' <<<"$output"
grep -E '^boottime\s+1337\s+3141519$' <<<"$output"
}
@test "runc run [execve error]" {
cat <<EOF >rootfs/run.sh
#!/mmnnttbb foo bar
sh
EOF
chmod +x rootfs/run.sh
update_config '.process.args = [ "/run.sh" ]'
run ! runc run test_hello
# After the sync socket closed, we should not send error to parent
# process, or else we will get a unnecessary error log(#4171).
[ ${#lines[@]} -eq 1 ]
assert_line --index 0 --regexp 'exec .*/run\.sh.*[Nn]o such file or directory'
}
# https://github.com/opencontainers/runc/issues/4688
@test "runc run check default home" {
# cannot start containers as another user in rootless setup without idmap
[ $EUID -ne 0 ] && requires rootless_idmap
echo 'tempuser:x:2000:2000:tempuser:/home/tempuser:/bin/sh' >>rootfs/etc/passwd
# shellcheck disable=SC2016
update_config ' .process.cwd = "/root"
| .process.user.uid = 2000
| .process.args |= ["sh", "-c", "echo $HOME"]'
run -0 runc run test_busybox
assert_line --index 0 "/home/tempuser"
}
@test "runc run [rootfs is /] must fail" {
update_config '.root.path = "/"'
run -1 runc run test_hello
assert_output --partial "invalid rootfs: / is not allowed"
}
# https://github.com/opencontainers/runc/issues/5087
@test "runc run [seccomp SCMP_ACT_KILL kills init]" {
# SCMP_ACT_KILL only kills the offending thread, leaving runc init with a
# zombie thread group leader and live threads holding its ends of the sync
# socket and the exec fifo open. Make sure runc notices that init is gone,
# rather than waiting for an EOF that can never arrive.
update_config ' .process.args = ["/bin/true"]
| .process.terminal = false
| .process.rlimits = [{"type": "RLIMIT_CORE", "soft": 0, "hard": 0}]
| .linux.seccomp = {
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [{"names": ["write"], "action": "SCMP_ACT_KILL"}]
}'
# Give runc its own stdio. Should this regress, the leftover threads keep
# it open, and bats would hang on it instead of failing the test.
local log="$BATS_TEST_TMPDIR/run-5087.log" ret=0
timeout -k 5 30 runc run test_busybox </dev/null >"$log" 2>&1 || ret=$?
run -0 cat "$log"
# Not 124 or 137, which mean the timeout fired, i.e. runc has hung.
[ "$ret" -eq 1 ]
assert_output --partial "container init process died unexpectedly"
# The leftover threads have to be gone as well, or the container's cgroup
# 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 </dev/null &
local pid=$!
wait_for_container 10 0.5 test_busybox running
# Make sure runc has set up the signal forwarding (SIGTERM is caught).
sig_caught() {
local cgt
cgt=$(awk '/^SigCgt:/ {print $2}' "/proc/$pid/status")
((0x$cgt >> ($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
}