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>
This commit is contained in:
Kir Kolyshkin committed 2026-10-27 23:03:30 -07:00
1 parent f4ec3e21b3
commit 8a3685e621
4 files changed
+124

No files matched your search

+4
View File
@@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Running a container which does not have its own mount namespace no longer
changes the propagation of host's `/` and of the parent mount of the
container rootfs (they were made private or slave). (#5537)
- runc now errors out when seccomp `defaultErrnoRet` or `errnoRet` is set for
an action which does not support it (i.e. anything other than
`SCMP_ACT_ERRNO` and `SCMP_ACT_TRACE`), as required by the runtime spec.
Previously, such values were silently ignored. (#5550, #5564)
## [1.6.0-rc.1] - 2026-10-06
@@ -32,6 +32,7 @@ func Validate(config *configs.Config) error {
rootfs,
rootlessEUIDCheck,
scheduler,
seccompCheck,
selinuxCheck,
sysctl,
uts,
@@ -543,3 +544,24 @@ func memoryPolicy(config *configs.Config) error {
}
return nil
}
func seccompCheck(config *configs.Config) error {
sc := config.Seccomp
if sc == nil {
return nil
}
// The runtime spec requires an error if errnoRet
// is set for an action that does not support it.
supportsErrno := func(a configs.Action) bool {
return a == configs.Errno || a == configs.Trace
}
if sc.DefaultErrnoRet != nil && !supportsErrno(sc.DefaultAction) {
return errors.New("seccomp: defaultErrnoRet is only supported for SCMP_ACT_ERRNO and SCMP_ACT_TRACE actions")
}
for _, call := range sc.Syscalls {
if call != nil && call.ErrnoRet != nil && !supportsErrno(call.Action) {
return fmt.Errorf("seccomp: syscall %q: errnoRet is only supported for SCMP_ACT_ERRNO and SCMP_ACT_TRACE actions", call.Name)
}
}
return nil
}
@@ -1168,3 +1168,80 @@ func TestDevValidName(t *testing.T) {
})
}
}
func TestValidateSeccompErrnoRet(t *testing.T) {
errno := uint(1)
testCases := []struct {
name string
seccomp *configs.Seccomp
isErr bool
}{
{
name: "defaultErrnoRet with SCMP_ACT_ERRNO",
seccomp: &configs.Seccomp{
DefaultAction: configs.Errno,
DefaultErrnoRet: &errno,
},
},
{
name: "defaultErrnoRet with SCMP_ACT_TRACE",
seccomp: &configs.Seccomp{
DefaultAction: configs.Trace,
DefaultErrnoRet: &errno,
},
},
{
name: "defaultErrnoRet with SCMP_ACT_ALLOW",
seccomp: &configs.Seccomp{
DefaultAction: configs.Allow,
DefaultErrnoRet: &errno,
},
isErr: true,
},
{
name: "errnoRet with SCMP_ACT_ERRNO",
seccomp: &configs.Seccomp{
DefaultAction: configs.Allow,
Syscalls: []*configs.Syscall{
{Name: "mkdir", Action: configs.Errno, ErrnoRet: &errno},
},
},
},
{
name: "errnoRet with SCMP_ACT_KILL_PROCESS",
seccomp: &configs.Seccomp{
DefaultAction: configs.Allow,
Syscalls: []*configs.Syscall{
{Name: "mkdir", Action: configs.KillProcess, ErrnoRet: &errno},
},
},
isErr: true,
},
{
name: "errnoRet with redundant SCMP_ACT_ALLOW",
seccomp: &configs.Seccomp{
DefaultAction: configs.Allow,
Syscalls: []*configs.Syscall{
{Name: "mkdir", Action: configs.Allow, ErrnoRet: &errno},
},
},
isErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Seccomp: tc.seccomp,
}
err := Validate(config)
if tc.isErr && err == nil {
t.Error("expected error, got nil")
}
if !tc.isErr && err != nil {
t.Errorf("expected nil, got error %v", err)
}
})
}
}
+21
View File
@@ -60,6 +60,27 @@ function teardown() {
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"'.