From 8a3685e6219da1c5d35f5fa1ededc02201eca9bc Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Sun, 18 Oct 2026 23:28:43 -0700 Subject: [PATCH] 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 --- CHANGELOG.md | 4 + libcontainer/configs/validate/validator.go | 22 ++++++ .../configs/validate/validator_test.go | 77 +++++++++++++++++++ tests/integration/seccomp.bats | 21 +++++ 4 files changed, 124 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e249459..c9e20de71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index 5960661db..df046dd2a 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -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 +} diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go index fd7a26337..b0bf75ba6 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -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) + } + }) + } +} diff --git a/tests/integration/seccomp.bats b/tests/integration/seccomp.bats index 87dc8aa23..414dd80c9 100644 --- a/tests/integration/seccomp.bats +++ b/tests/integration/seccomp.bats @@ -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"'.