Files
runc/tests/integration/seccomp.bats
T
Kir Kolyshkin 8a3685e621 libct/configs/validate: reject seccomp errnoRet for unsupported actions
The runtime spec says about both defaultErrnoRet and errnoRet:

> When the action doesn't support an errno, the runtime MUST print and
> error and fail.

Yet runc silently ignores errnoRet set for any action other than
SCMP_ACT_ERRNO and SCMP_ACT_TRACE.

Add a check to the config validator, so the error is reported early
(before the container init is started) and clearly (including the
syscall name in case of errnoRet).

Since the validator checks all syscall rules, this also covers ones
which are later skipped as redundant (i.e. having the same action as
the default).

Add unit and integration tests, and a CHANGELOG entry.

Reported in issue 5550.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2026-10-27 23:03:30 -07:00

213 lines
6.7 KiB
Bash

#!/usr/bin/env bats
load helpers
function setup() {
setup_busybox
}
function teardown() {
teardown_bundle
}
@test "runc run [seccomp -ENOSYS handling]" {
TEST_NAME="seccomp_syscall_test1"
# Compile the test binary and update the config to run it.
gcc -static -o rootfs/seccomp_test "${TESTDATA}/${TEST_NAME}.c"
update_config ".linux.seccomp = $(<"${TESTDATA}/${TEST_NAME}.json")"
update_config '.process.args = ["/seccomp_test"]'
run -0 runc run test_busybox
}
@test "runc run [seccomp defaultErrnoRet=ENXIO]" {
TEST_NAME="seccomp_syscall_test2"
# Compile the test binary and update the config to run it.
gcc -static -o rootfs/seccomp_test2 "${TESTDATA}/${TEST_NAME}.c"
update_config ".linux.seccomp = $(<"${TESTDATA}/${TEST_NAME}.json")"
update_config '.process.args = ["/seccomp_test2"]'
run -0 runc run test_busybox
}
# TODO:
# - Test other actions like SCMP_ACT_TRAP, SCMP_ACT_TRACE, SCMP_ACT_LOG.
# - Test args (index, value, valueTwo, etc).
@test "runc run [seccomp] (SCMP_ACT_ERRNO default)" {
update_config ' .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
| .process.noNewPrivileges = false
| .linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_ERRNO"}]
}'
run ! runc run test_busybox
assert_output --regexp 'mkdir:.*/dev/shm/foo.*Operation not permitted'
}
@test "runc run [seccomp] (SCMP_ACT_ERRNO explicit errno)" {
update_config ' .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
| .process.noNewPrivileges = false
| .linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_ERRNO", "errnoRet": 100}]
}'
run ! runc run test_busybox
assert_output --partial "Network is down"
}
@test "runc run [seccomp] (errnoRet with unsupported action)" {
update_config '.process.args = ["/bin/true"]'
# defaultErrnoRet with a default action not supporting it.
update_config '.linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"defaultErrnoRet": 1
}'
run ! runc run test_busybox
assert_output --partial "defaultErrnoRet is only supported for"
# errnoRet with a syscall action not supporting it (the rule is
# redundant as it is the same as the default action).
update_config '.linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_ALLOW", "errnoRet": 1}]
}'
run ! runc run test_busybox
assert_output --regexp 'syscall .*mkdir.*: errnoRet is only supported for'
}
# Prints the numeric value of provided seccomp flags combination.
# The parameter is flags string, as supplied in OCI spec, for example
# '"SECCOMP_FILTER_FLAG_TSYNC","SECCOMP_FILTER_FLAG_LOG"'.
function flags_value() {
# Numeric values of seccomp flags.
declare -A values=(
['"SECCOMP_FILTER_FLAG_TSYNC"']=0 # Supported but ignored by runc, thus 0.
['"SECCOMP_FILTER_FLAG_LOG"']=2
['"SECCOMP_FILTER_FLAG_SPEC_ALLOW"']=4
# XXX: add new values above this line.
)
# Split the flags.
IFS=',' read -ra flags <<<"$1"
local flag v sum=0
for flag in "${flags[@]}"; do
# This will produce "values[$flag]: unbound variable"
# error for a new flag yet unknown to the test.
v=${values[$flag]}
((sum += v)) || true
done
echo $sum
}
@test "runc run [seccomp] (SECCOMP_FILTER_FLAG_*)" {
update_config ' .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
| .process.noNewPrivileges = false
| .linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["mkdir", "mkdirat"], "action":"SCMP_ACT_ERRNO"}]
}'
# Get the list of flags supported by runc/seccomp/kernel,
# or "null" if no flags are supported or runc is too old.
#
# Filter out WAIT_KILLABLE_RECV as it requires a listener,
# and thus tested separately in seccomp-notify.bats.
mapfile -t flags < <(runc features | jq -c '.linux.seccomp.supportedFlags' |
tr -d '[]\n' | tr ',' '\n' | grep -v 'WAIT_KILLABLE_RECV')
# This is a set of all possible flag combinations to test.
declare -A TEST_CASES=(
['EMPTY']=0 # Special value: empty set of flags.
['REMOVE']=0 # Special value: no flags set.
)
# If supported, runc should set SPEC_ALLOW if no flags are set.
if [[ " ${flags[*]} " == *' "SECCOMP_FILTER_FLAG_SPEC_ALLOW" '* ]]; then
TEST_CASES['REMOVE']=$(flags_value '"SECCOMP_FILTER_FLAG_SPEC_ALLOW"')
fi
# Add all possible combinations of seccomp flags
# and their expected numeric values to TEST_CASES.
if [ "${flags[0]}" != "null" ]; then
# Use shell {a,}{b,}{c,} to generate the powerset.
for fc in $(eval echo "$(printf "{'%s,',}" "${flags[@]}")"); do
# Remove the last comma.
fc="${fc/%,/}"
TEST_CASES[$fc]=$(flags_value "$fc")
done
fi
# Finally, run the tests.
for key in "${!TEST_CASES[@]}"; do
case "$key" in
'REMOVE')
update_config ' del(.linux.seccomp.flags)'
;;
'EMPTY')
update_config ' .linux.seccomp.flags = []'
;;
*)
update_config ' .linux.seccomp.flags = [ '"${key}"' ]'
;;
esac
run ! runc --debug run test_busybox
assert_output --regexp 'mkdir:.*/dev/shm/foo.*Operation not permitted'
# Check the numeric flags value, as printed in the debug log, is as expected.
exp="\"seccomp filter flags: ${TEST_CASES[$key]}\""
echo "flags $key, expecting $exp"
assert_output --partial "$exp"
done
}
@test "runc run [seccomp] (SCMP_ACT_KILL)" {
update_config ' .process.args = ["/bin/sh", "-c", "mkdir /dev/shm/foo"]
| .process.noNewPrivileges = false
| .process.rlimits = [{"type": "RLIMIT_CORE", "soft": 0, "hard": 0}]
| .linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_KILL"}]
}'
run ! runc run test_busybox
}
# check that a startContainer hook is run with the seccomp filters applied
@test "runc run [seccomp] (startContainer hook)" {
update_config ' .process.args = ["/bin/true"]
| .linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["mkdir","mkdirat"], "action":"SCMP_ACT_KILL"}]
}
| .process.rlimits = [{"type": "RLIMIT_CORE", "soft": 0, "hard": 0}]
| .hooks = {
"startContainer": [ {
"path": "/bin/sh",
"args": ["sh", "-c", "mkdir /dev/shm/foo"]
} ]
}'
run ! runc run test_busybox
assert_output --partial "error running startContainer hook"
assert_output --partial "bad system call"
}
@test "runc run [seccomp] (verify syscall compatibility after seccomp enforcement)" {
update_config ' .process.args = ["true"]
| .process.noNewPrivileges = false
| .linux.seccomp = {
"defaultAction":"SCMP_ACT_ALLOW",
"syscalls":[{"names":["close_range", "fsopen", "fsconfig", "fspick", "openat2", "open_tree", "move_mount", "mount_setattr"], "action":"SCMP_ACT_ERRNO", "errnoRet": 38}]
}'
run -0 runc run test_busybox
}