mirror of
https://github.com/opencontainers/runc.git
synced 2026-11-06 05:17:05 +00:00
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>
274 lines
8.9 KiB
Bash
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
|
|
}
|