Files
runc/libcontainer/configs/validate/validator_test.go
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

1248 lines
28 KiB
Go

package validate
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
)
func TestValidate(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateWithInvalidRootfs(t *testing.T) {
dir := "rootfs"
if err := os.Symlink("/var", dir); err != nil {
t.Fatal(err)
}
defer os.Remove(dir)
config := &configs.Config{
Rootfs: dir,
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateNetworkWithoutNETNamespace(t *testing.T) {
network := &configs.Network{Type: "loopback"}
config := &configs.Config{
Rootfs: "/var",
Namespaces: []configs.Namespace{},
Networks: []*configs.Network{network},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateNetworkRoutesWithoutNETNamespace(t *testing.T) {
route := &configs.Route{Gateway: "255.255.255.0"}
config := &configs.Config{
Rootfs: "/var",
Namespaces: []configs.Namespace{},
Routes: []*configs.Route{route},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateHostname(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Hostname: "runc",
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWUTS},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateUTS(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Domainname: "runc",
Hostname: "runc",
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWUTS},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateUTSWithoutUTSNamespace(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Hostname: "runc",
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
config = &configs.Config{
Rootfs: "/var",
Domainname: "runc",
}
err = Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateSecurityWithMaskPaths(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
MaskPaths: []string{"/proc/kcore"},
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWNS},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateSecurityWithROPaths(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
ReadonlyPaths: []string{"/proc/sys"},
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWNS},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateSecurityWithoutNEWNS(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
MaskPaths: []string{"/proc/kcore"},
ReadonlyPaths: []string{"/proc/sys"},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateSecurityWithReadonlyfs(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Readonlyfs: true,
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWNS},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateSecurityReadonlyfsWithoutNEWNS(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Readonlyfs: true,
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateSecurityWithReadonlyTmpfs(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Mounts: []*configs.Mount{
{
Destination: "/dev/shm",
Device: "tmpfs",
Flags: unix.MS_RDONLY,
},
},
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWNS},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateSecurityReadonlyTmpfsWithoutNEWNS(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Mounts: []*configs.Mount{
{
Destination: "/dev/shm",
Device: "tmpfs",
Flags: unix.MS_RDONLY,
},
},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
// A read-only mount which does not need a remount to be made read-only is
// fine without a mount namespace.
func TestValidateSecurityReadonlyBindWithoutNEWNS(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Mounts: []*configs.Mount{
{
Source: "/etc",
Destination: "/etc",
Device: "bind",
Flags: unix.MS_BIND | unix.MS_RDONLY,
},
},
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur: %+v", err)
}
}
func TestValidateUserNamespace(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); errors.Is(err, os.ErrNotExist) {
t.Skip("Test requires userns.")
}
config := &configs.Config{
Rootfs: "/var",
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWUSER},
},
),
UIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
GIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
}
err := Validate(config)
if err != nil {
t.Errorf("expected error to not occur %+v", err)
}
}
func TestValidateUsernsMappingWithoutNamespace(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
UIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
GIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateTimeNamespace(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/time"); errors.Is(err, os.ErrNotExist) {
t.Skip("Test requires timens.")
}
config := &configs.Config{
Rootfs: "/var",
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWTIME},
},
),
}
err := Validate(config)
if err != nil {
t.Errorf("expected error to not occur %+v", err)
}
}
func TestValidateTimeNamespaceWithBothPathAndTimeOffset(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/time"); errors.Is(err, os.ErrNotExist) {
t.Skip("Test requires timens.")
}
config := &configs.Config{
Rootfs: "/var",
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWTIME, Path: "/proc/1/ns/time"},
},
),
TimeOffsets: map[string]specs.LinuxTimeOffset{
"boottime": {Secs: 150, Nanosecs: 314159},
"monotonic": {Secs: 512, Nanosecs: 271818},
},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateTimeOffsetsWithoutTimeNamespace(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
TimeOffsets: map[string]specs.LinuxTimeOffset{
"boottime": {Secs: 150, Nanosecs: 314159},
"monotonic": {Secs: 512, Nanosecs: 271818},
},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
// TestConvertSysctlVariableToDotsSeparator tests whether the sysctl variable
// can be correctly converted to a dot as a separator.
func TestConvertSysctlVariableToDotsSeparator(t *testing.T) {
type testCase struct {
in string
out string
}
valid := []testCase{
{in: "kernel.shm_rmid_forced", out: "kernel.shm_rmid_forced"},
{in: "kernel/shm_rmid_forced", out: "kernel.shm_rmid_forced"},
{in: "net.ipv4.conf.eno2/100.rp_filter", out: "net.ipv4.conf.eno2/100.rp_filter"},
{in: "net/ipv4/conf/eno2.100/rp_filter", out: "net.ipv4.conf.eno2/100.rp_filter"},
{in: "net/ipv4/ip_local_port_range", out: "net.ipv4.ip_local_port_range"},
{in: "kernel/msgmax", out: "kernel.msgmax"},
{in: "kernel/sem", out: "kernel.sem"},
}
for _, test := range valid {
convertSysctlVal := convertSysctlVariableToDotsSeparator(test.in)
if convertSysctlVal != test.out {
t.Errorf("The sysctl variable was not converted correctly. got: %s, want: %s", convertSysctlVal, test.out)
}
}
}
func TestValidateSysctl(t *testing.T) {
sysctl := map[string]string{
"fs.mqueue.ctl": "ctl",
"fs/mqueue/ctl": "ctl",
"net.ctl": "ctl",
"net/ctl": "ctl",
"net.ipv4.conf.eno2/100.rp_filter": "ctl",
"kernel.ctl": "ctl",
"kernel/ctl": "ctl",
}
for k, v := range sysctl {
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{k: v},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
}
func TestValidateValidSysctl(t *testing.T) {
sysctl := map[string]string{
"fs.mqueue.ctl": "ctl",
"fs/mqueue/ctl": "ctl",
"net.ctl": "ctl",
"net/ctl": "ctl",
"net.ipv4.conf.eno2/100.rp_filter": "ctl",
"kernel.msgmax": "ctl",
"kernel/msgmax": "ctl",
}
for k, v := range sysctl {
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{k: v},
Namespaces: []configs.Namespace{
{
Type: configs.NEWNET,
},
{
Type: configs.NEWIPC,
},
},
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur with {%s=%s} but got: %q", k, v, err)
}
}
}
func TestValidateSysctlWithSameNs(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{"net.ctl": "ctl"},
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: "/proc/self/ns/net",
},
},
),
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateSysctlWithBindHostNetNS(t *testing.T) {
if os.Getuid() != 0 {
t.Skip("requires root")
}
const selfnet = "/proc/self/ns/net"
file := filepath.Join(t.TempDir(), "default")
fd, err := os.Create(file)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file)
fd.Close()
if err := unix.Mount(selfnet, file, "bind", unix.MS_BIND, ""); err != nil {
t.Fatalf("can't bind-mount %s to %s: %s", selfnet, file, err)
}
defer func() {
_ = unix.Unmount(file, unix.MNT_DETACH)
}()
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{"net.ctl": "ctl", "net.foo": "bar"},
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: file,
},
},
),
}
if err := Validate(config); err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateSysctlWithoutNETNamespace(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{"net.ctl": "ctl"},
Namespaces: []configs.Namespace{},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur but it was nil")
}
}
func TestValidateMounts(t *testing.T) {
testCases := []struct {
isErr bool
dest string
}{
{isErr: false, dest: "not/an/abs/path"},
{isErr: false, dest: "./rel/path"},
{isErr: false, dest: "./rel/path"},
{isErr: false, dest: "../../path"},
{isErr: false, dest: "/abs/path"},
{isErr: false, dest: "/abs/but/../unclean"},
}
for _, tc := range testCases {
config := &configs.Config{
Rootfs: "/var",
Mounts: []*configs.Mount{
{Destination: tc.dest},
},
}
err := Validate(config)
if tc.isErr && err == nil {
t.Errorf("mount dest: %s, expected error, got nil", tc.dest)
}
if !tc.isErr && err != nil {
t.Errorf("mount dest: %s, expected nil, got error %v", tc.dest, err)
}
}
}
func TestValidateBindMounts(t *testing.T) {
testCases := []struct {
isErr bool
flags int
data string
}{
{isErr: false, flags: 0, data: ""},
{isErr: false, flags: unix.MS_RDONLY | unix.MS_NOSYMFOLLOW, data: ""},
{isErr: true, flags: 0, data: "idmap"},
{isErr: true, flags: unix.MS_RDONLY, data: "custom_ext4_flag"},
{isErr: true, flags: unix.MS_NOATIME, data: "rw=foobar"},
}
for _, tc := range testCases {
for _, bind := range []string{"bind", "rbind"} {
bindFlag := map[string]int{
"bind": unix.MS_BIND,
"rbind": unix.MS_BIND | unix.MS_REC,
}[bind]
config := &configs.Config{
Rootfs: "/var",
Mounts: []*configs.Mount{
{
Destination: "/",
Flags: tc.flags | bindFlag,
Data: tc.data,
},
},
}
err := Validate(config)
if tc.isErr && err == nil {
t.Errorf("%s mount flags:0x%x data:%v, expected error, got nil", bind, tc.flags, tc.data)
}
if !tc.isErr && err != nil {
t.Errorf("%s mount flags:0x%x data:%v, expected nil, got error %v", bind, tc.flags, tc.data, err)
}
}
}
}
func TestValidateIDMapMounts(t *testing.T) {
mapping := []configs.IDMap{
{
ContainerID: 0,
HostID: 10000,
Size: 1,
},
}
testCases := []struct {
name string
isErr bool
config *configs.Config
}{
{
name: "idmap non-bind mount",
isErr: true,
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/dev/sda1",
Destination: "/abs/path/",
Device: "ext4",
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap option non-bind mount",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/dev/sda1",
Destination: "/abs/path/",
Device: "ext4",
IDMapping: &configs.MountIDMapping{},
},
},
},
},
{
name: "ridmap option non-bind mount",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/dev/sda1",
Destination: "/abs/path/",
Device: "ext4",
IDMapping: &configs.MountIDMapping{
Recursive: true,
},
},
},
},
},
{
name: "idmap mount no uid mapping",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mount no gid mapping",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
},
},
},
},
},
{
name: "rootless idmap mount",
isErr: true,
config: &configs.Config{
RootlessEUID: true,
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mounts without abs source path",
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "./rel/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mounts without abs dest path",
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "./rel/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "simple idmap mount",
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/another-abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mount with more flags",
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/another-abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND | unix.MS_RDONLY,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mount without userns mappings",
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mounts with different userns and mount mappings",
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: []configs.IDMap{
{
ContainerID: 10,
HostID: 10,
Size: 1,
},
},
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mounts with different userns and mount mappings",
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: []configs.IDMap{
{
ContainerID: 10,
HostID: 10,
Size: 1,
},
},
},
},
},
},
},
{
name: "mount with 'idmap' option but no mappings",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{},
},
},
},
},
{
name: "mount with 'ridmap' option but no mappings",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
Recursive: true,
},
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := tc.config
config.Rootfs = "/var"
err := mountsStrict(config)
if tc.isErr && err == nil {
t.Error("expected error, got nil")
}
if !tc.isErr && err != nil {
t.Error(err)
}
})
}
}
func TestValidateScheduler(t *testing.T) {
testCases := []struct {
isErr bool
policy string
niceValue int32
priority int32
runtime uint64
deadline uint64
period uint64
}{
{isErr: true, niceValue: 0},
{isErr: false, policy: "SCHED_OTHER", niceValue: 19},
{isErr: false, policy: "SCHED_OTHER", niceValue: -20},
{isErr: true, policy: "SCHED_OTHER", niceValue: 20},
{isErr: true, policy: "SCHED_OTHER", niceValue: -21},
{isErr: true, policy: "SCHED_OTHER", priority: 100},
{isErr: false, policy: "SCHED_FIFO", priority: 100},
{isErr: true, policy: "SCHED_FIFO", runtime: 20},
{isErr: true, policy: "SCHED_BATCH", deadline: 30},
{isErr: true, policy: "SCHED_IDLE", period: 40},
{isErr: true, policy: "SCHED_DEADLINE", priority: 100},
{isErr: false, policy: "SCHED_DEADLINE", runtime: 200},
{isErr: false, policy: "SCHED_DEADLINE", deadline: 300},
{isErr: false, policy: "SCHED_DEADLINE", period: 400},
{isErr: true, policy: "SCHED_OTHER", niceValue: 20},
{isErr: true, policy: "SCHED_OTHER", niceValue: -21},
{isErr: false, policy: "SCHED_FIFO", priority: 100, niceValue: 100},
}
for _, tc := range testCases {
scheduler := configs.Scheduler{
Policy: specs.LinuxSchedulerPolicy(tc.policy),
Nice: tc.niceValue,
Priority: tc.priority,
Runtime: tc.runtime,
Deadline: tc.deadline,
Period: tc.period,
}
config := &configs.Config{
Rootfs: "/var",
Scheduler: &scheduler,
}
err := Validate(config)
if tc.isErr && err == nil {
t.Errorf("scheduler: %d, expected error, got nil", tc.niceValue)
}
if !tc.isErr && err != nil {
t.Errorf("scheduler: %d, expected nil, got error %v", tc.niceValue, err)
}
}
}
func TestValidateIOPriority(t *testing.T) {
testCases := []struct {
isErr bool
priority int
class specs.IOPriorityClass
}{
{isErr: false, priority: 0, class: specs.IOPRIO_CLASS_IDLE},
{isErr: false, priority: 7, class: specs.IOPRIO_CLASS_RT},
{isErr: false, priority: 3, class: specs.IOPRIO_CLASS_BE},
// Invalid priority.
{isErr: true, priority: -1, class: specs.IOPRIO_CLASS_BE},
// Invalid class.
{isErr: true, priority: 3, class: specs.IOPriorityClass("IOPRIO_CLASS_WOW")},
}
for _, tc := range testCases {
ioPriroty := configs.IOPriority{
Priority: tc.priority,
Class: tc.class,
}
config := &configs.Config{
Rootfs: "/var",
IOPriority: &ioPriroty,
}
err := Validate(config)
if tc.isErr && err == nil {
t.Errorf("iopriority: %d, expected error, got nil", tc.priority)
}
if !tc.isErr && err != nil {
t.Errorf("iopriority: %d, expected nil, got error %v", tc.priority, err)
}
}
}
func TestValidateNetDevices(t *testing.T) {
testCases := []struct {
name string
isErr bool
config *configs.Config
}{
{
name: "network device with configured network namespace",
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: "/var/run/netns/blue",
},
},
),
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {},
},
},
},
{
name: "network device rename",
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: "/var/run/netns/blue",
},
},
),
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {
Name: "c0",
},
},
},
},
{
name: "network device network namespace created by runc",
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
},
},
),
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {},
},
},
},
{
name: "network device network namespace empty",
isErr: true,
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{},
),
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {},
},
},
},
{
name: "network device rootless EUID",
isErr: true,
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: "/var/run/netns/blue",
},
},
),
RootlessEUID: true,
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {},
},
},
},
{
name: "network device rootless",
isErr: true,
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: "/var/run/netns/blue",
},
},
),
RootlessCgroups: true,
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {},
},
},
},
{
name: "network device bad name",
isErr: true,
config: &configs.Config{
Namespaces: configs.Namespaces(
[]configs.Namespace{
{
Type: configs.NEWNET,
Path: "/var/run/netns/blue",
},
},
),
NetDevices: map[string]*configs.LinuxNetDevice{
"eth0": {
Name: "eth0/",
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := tc.config
config.Rootfs = "/var"
err := Validate(config)
if tc.isErr && err == nil {
t.Error("expected error, got nil")
}
if !tc.isErr && err != nil {
t.Error(err)
}
})
}
}
func TestValidateUserSysctlWithUserNamespace(t *testing.T) {
if _, err := os.Stat("/proc/self/ns/user"); errors.Is(err, os.ErrNotExist) {
t.Skip("Test requires userns.")
}
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{"user.max_inotify_watches": "8192"},
Namespaces: configs.Namespaces(
[]configs.Namespace{
{Type: configs.NEWUSER},
},
),
UIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
GIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
}
err := Validate(config)
if err != nil {
t.Errorf("Expected error to not occur with user.* sysctl and NEWUSER namespace: %+v", err)
}
}
func TestValidateUserSysctlWithoutUserNamespace(t *testing.T) {
config := &configs.Config{
Rootfs: "/var",
Sysctl: map[string]string{"user.max_inotify_watches": "8192"},
}
err := Validate(config)
if err == nil {
t.Error("Expected error to occur with user.* sysctl without NEWUSER namespace but it was nil")
}
}
func TestDevValidName(t *testing.T) {
testCases := []struct {
name string
valid bool
}{
{name: "", valid: false},
{name: "a", valid: true},
{name: strings.Repeat("a", unix.IFNAMSIZ), valid: true},
{name: strings.Repeat("a", unix.IFNAMSIZ+1), valid: false},
{name: ".", valid: false},
{name: "..", valid: false},
{name: "dev/null", valid: false},
{name: "valid:name", valid: false},
{name: "valid name", valid: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if devValidName(tc.name) != tc.valid {
t.Fatalf("name %q, expected valid: %v", tc.name, tc.valid)
}
})
}
}
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)
}
})
}
}