diff --git a/go.mod b/go.mod index 5ac51262c3fc..362f76f52676 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 replace ( github.com/containerd/continuity => github.com/k3s-io/continuity v0.0.0-20210309170710-f93269e0d5c1 - github.com/containerd/cri => github.com/k3s-io/cri v1.4.0-k3s.5 // k3s-release/1.4 + github.com/containerd/cri => github.com/k3s-io/cri v1.4.0-k3s.6 // k3s-release/1.4 github.com/golang/protobuf => github.com/golang/protobuf v1.3.5 google.golang.org/genproto => google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63 k8s.io/api => k8s.io/api v0.19.5 @@ -73,7 +73,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 github.com/sirupsen/logrus v1.7.0 - github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 + github.com/stretchr/testify v1.4.0 github.com/tchap/go-patricia v2.2.6+incompatible // indirect github.com/urfave/cli v1.22.2 go.etcd.io/bbolt v1.3.5 diff --git a/go.sum b/go.sum index 9d9b640583f8..2c46c0c845cc 100644 --- a/go.sum +++ b/go.sum @@ -402,8 +402,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k3s-io/continuity v0.0.0-20210309170710-f93269e0d5c1 h1:KEz2rd9IDbrQT8w6RibEYlwfTXiu0P6hQDE+6O4IJdI= github.com/k3s-io/continuity v0.0.0-20210309170710-f93269e0d5c1/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/k3s-io/cri v1.4.0-k3s.5 h1:Uh7QkRKibPJqSBbtCtgpjYsdNVcx3Tx+7HHlhxKBtu8= -github.com/k3s-io/cri v1.4.0-k3s.5/go.mod h1:fGPUUHMKQik/vIegSe05DtX/m4miovdtvVLqRUFAkK0= +github.com/k3s-io/cri v1.4.0-k3s.6 h1:Hn/0KRtv62B/KNT8oULl1hWnViyuJ+RB0x9y9zZsmCw= +github.com/k3s-io/cri v1.4.0-k3s.6/go.mod h1:fGPUUHMKQik/vIegSe05DtX/m4miovdtvVLqRUFAkK0= github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= diff --git a/oci/spec_opts.go b/oci/spec_opts.go index 89346fe8bf3b..5a952f616645 100644 --- a/oci/spec_opts.go +++ b/oci/spec_opts.go @@ -38,7 +38,6 @@ import ( "github.com/opencontainers/runc/libcontainer/user" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" - "github.com/syndtr/gocapability/capability" ) // SpecOpts sets spec specific information to a newly generated OCI spec @@ -274,6 +273,28 @@ func WithMounts(mounts []specs.Mount) SpecOpts { } } +// WithoutMounts removes mounts +func WithoutMounts(dests ...string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { + var ( + mounts []specs.Mount + current = s.Mounts + ) + mLoop: + for _, m := range current { + mDestination := filepath.Clean(m.Destination) + for _, dest := range dests { + if mDestination == dest { + continue mLoop + } + } + mounts = append(mounts, m) + } + s.Mounts = mounts + return nil + } +} + // WithHostNamespace allows a task to run inside the host's linux namespace func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts { return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { @@ -295,10 +316,7 @@ func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts { setLinux(s) for i, n := range s.Linux.Namespaces { if n.Type == ns.Type { - before := s.Linux.Namespaces[:i] - after := s.Linux.Namespaces[i+1:] - s.Linux.Namespaces = append(before, ns) - s.Linux.Namespaces = append(s.Linux.Namespaces, after...) + s.Linux.Namespaces[i] = ns return nil } } @@ -536,7 +554,7 @@ func WithUser(userstr string) SpecOpts { } f := func(root string) error { if username != "" { - user, err := getUserFromPath(root, func(u user.User) bool { + user, err := UserFromPath(root, func(u user.User) bool { return u.Name == username }) if err != nil { @@ -545,7 +563,7 @@ func WithUser(userstr string) SpecOpts { uid = uint32(user.Uid) } if groupname != "" { - gid, err = getGIDFromPath(root, func(g user.Group) bool { + gid, err = GIDFromPath(root, func(g user.Group) bool { return g.Name == groupname }) if err != nil { @@ -600,11 +618,11 @@ func WithUserID(uid uint32) SpecOpts { if !isRootfsAbs(s.Root.Path) { return errors.Errorf("rootfs absolute path is required") } - user, err := getUserFromPath(s.Root.Path, func(u user.User) bool { + user, err := UserFromPath(s.Root.Path, func(u user.User) bool { return u.Uid == int(uid) }) if err != nil { - if os.IsNotExist(err) || err == errNoUsersFound { + if os.IsNotExist(err) || err == ErrNoUsersFound { s.Process.User.UID, s.Process.User.GID = uid, 0 return nil } @@ -626,11 +644,11 @@ func WithUserID(uid uint32) SpecOpts { return err } return mount.WithTempMount(ctx, mounts, func(root string) error { - user, err := getUserFromPath(root, func(u user.User) bool { + user, err := UserFromPath(root, func(u user.User) bool { return u.Uid == int(uid) }) if err != nil { - if os.IsNotExist(err) || err == errNoUsersFound { + if os.IsNotExist(err) || err == ErrNoUsersFound { s.Process.User.UID, s.Process.User.GID = uid, 0 return nil } @@ -654,7 +672,7 @@ func WithUsername(username string) SpecOpts { if !isRootfsAbs(s.Root.Path) { return errors.Errorf("rootfs absolute path is required") } - user, err := getUserFromPath(s.Root.Path, func(u user.User) bool { + user, err := UserFromPath(s.Root.Path, func(u user.User) bool { return u.Name == username }) if err != nil { @@ -675,7 +693,7 @@ func WithUsername(username string) SpecOpts { return err } return mount.WithTempMount(ctx, mounts, func(root string) error { - user, err := getUserFromPath(root, func(u user.User) bool { + user, err := UserFromPath(root, func(u user.User) bool { return u.Name == username }) if err != nil { @@ -707,11 +725,11 @@ func WithAdditionalGIDs(userstr string) SpecOpts { var username string uid, err := strconv.Atoi(userstr) if err == nil { - user, err := getUserFromPath(root, func(u user.User) bool { + user, err := UserFromPath(root, func(u user.User) bool { return u.Uid == uid }) if err != nil { - if os.IsNotExist(err) || err == errNoUsersFound { + if os.IsNotExist(err) || err == ErrNoUsersFound { return nil } return err @@ -776,29 +794,6 @@ func WithCapabilities(caps []string) SpecOpts { } } -// WithAllCapabilities sets all linux capabilities for the process -var WithAllCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error { - return WithCapabilities(GetAllCapabilities())(ctx, client, c, s) -} - -// GetAllCapabilities returns all caps up to CAP_LAST_CAP -// or CAP_BLOCK_SUSPEND on RHEL6 -func GetAllCapabilities() []string { - last := capability.CAP_LAST_CAP - // hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap - if last == capability.Cap(63) { - last = capability.CAP_BLOCK_SUSPEND - } - var caps []string - for _, cap := range capability.List() { - if cap > last { - continue - } - caps = append(caps, "CAP_"+strings.ToUpper(cap.String())) - } - return caps -} - func capsContain(caps []string, s string) bool { for _, c := range caps { if c == s { @@ -869,9 +864,12 @@ func WithAmbientCapabilities(caps []string) SpecOpts { } } -var errNoUsersFound = errors.New("no users found") +// ErrNoUsersFound can be returned from UserFromPath +var ErrNoUsersFound = errors.New("no users found") -func getUserFromPath(root string, filter func(user.User) bool) (user.User, error) { +// UserFromPath inspects the user object using /etc/passwd in the specified rootfs. +// filter can be nil. +func UserFromPath(root string, filter func(user.User) bool) (user.User, error) { ppath, err := fs.RootPath(root, "/etc/passwd") if err != nil { return user.User{}, err @@ -881,14 +879,17 @@ func getUserFromPath(root string, filter func(user.User) bool) (user.User, error return user.User{}, err } if len(users) == 0 { - return user.User{}, errNoUsersFound + return user.User{}, ErrNoUsersFound } return users[0], nil } -var errNoGroupsFound = errors.New("no groups found") +// ErrNoGroupsFound can be returned from GIDFromPath +var ErrNoGroupsFound = errors.New("no groups found") -func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) { +// GIDFromPath inspects the GID using /etc/passwd in the specified rootfs. +// filter can be nil. +func GIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) { gpath, err := fs.RootPath(root, "/etc/group") if err != nil { return 0, err @@ -898,7 +899,7 @@ func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err return 0, err } if len(groups) == 0 { - return 0, errNoGroupsFound + return 0, ErrNoGroupsFound } g := groups[0] return uint32(g.Gid), nil @@ -948,16 +949,13 @@ func WithReadonlyPaths(paths []string) SpecOpts { // WithWriteableSysfs makes any sysfs mounts writeable func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { - for i, m := range s.Mounts { + for _, m := range s.Mounts { if m.Type == "sysfs" { - var options []string - for _, o := range m.Options { + for i, o := range m.Options { if o == "ro" { - o = "rw" + m.Options[i] = "rw" } - options = append(options, o) } - s.Mounts[i].Options = options } } return nil @@ -965,16 +963,13 @@ func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s // WithWriteableCgroupfs makes any cgroup mounts writeable func WithWriteableCgroupfs(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { - for i, m := range s.Mounts { + for _, m := range s.Mounts { if m.Type == "cgroup" { - var options []string - for _, o := range m.Options { + for i, o := range m.Options { if o == "ro" { - o = "rw" + m.Options[i] = "rw" } - options = append(options, o) } - s.Mounts[i].Options = options } } return nil @@ -1126,7 +1121,7 @@ func WithDefaultUnixDevices(_ context.Context, _ Client, _ *containers.Container // WithPrivileged sets up options for a privileged container var WithPrivileged = Compose( - WithAllCapabilities, + WithAllCurrentCapabilities, WithMaskedPaths(nil), WithReadonlyPaths(nil), WithWriteableSysfs, @@ -1199,15 +1194,13 @@ func WithLinuxDevices(devices []specs.LinuxDevice) SpecOpts { } } -var ErrNotADevice = errors.New("not a device node") - // WithLinuxDevice adds the device specified by path to the spec func WithLinuxDevice(path, permissions string) SpecOpts { return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { setLinux(s) setResources(s) - dev, err := deviceFromPath(path, permissions) + dev, err := deviceFromPath(path) if err != nil { return err } diff --git a/oci/spec_opts_linux.go b/oci/spec_opts_linux.go index b1eab6f9f855..9c5fd6c84d63 100644 --- a/oci/spec_opts_linux.go +++ b/oci/spec_opts_linux.go @@ -20,20 +20,17 @@ package oci import ( "context" - "io/ioutil" - "os" - "path/filepath" "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/pkg/cap" specs "github.com/opencontainers/runtime-spec/specs-go" - "golang.org/x/sys/unix" ) // WithHostDevices adds all the hosts device nodes to the container's spec func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { setLinux(s) - devs, err := getDevices("/dev") + devs, err := HostDevices() if err != nil { return err } @@ -41,83 +38,28 @@ func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Sp return nil } -func getDevices(path string) ([]specs.LinuxDevice, error) { - files, err := ioutil.ReadDir(path) - if err != nil { - return nil, err - } - var out []specs.LinuxDevice - for _, f := range files { - switch { - case f.IsDir(): - switch f.Name() { - // ".lxc" & ".lxd-mounts" added to address https://github.com/lxc/lxd/issues/2825 - // ".udev" added to address https://github.com/opencontainers/runc/issues/2093 - case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev": - continue - default: - sub, err := getDevices(filepath.Join(path, f.Name())) - if err != nil { - return nil, err - } - - out = append(out, sub...) - continue - } - case f.Name() == "console": - continue - } - device, err := deviceFromPath(filepath.Join(path, f.Name()), "rwm") +// WithDevices recursively adds devices from the passed in path and associated cgroup rules for that device. +// If devicePath is a dir it traverses the dir to add all devices in that dir. +// If devicePath is not a dir, it attempts to add the single device. +// If containerPath is not set then the device path is used for the container path. +func WithDevices(devicePath, containerPath, permissions string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { + devs, err := getDevices(devicePath, containerPath) if err != nil { - if err == ErrNotADevice { - continue - } - if os.IsNotExist(err) { - continue - } - return nil, err + return err } - out = append(out, *device) - } - return out, nil -} - -func deviceFromPath(path, permissions string) (*specs.LinuxDevice, error) { - var stat unix.Stat_t - if err := unix.Lstat(path, &stat); err != nil { - return nil, err - } - - var ( - // The type is 32bit on mips. - devNumber = uint64(stat.Rdev) // nolint: unconvert - major = unix.Major(devNumber) - minor = unix.Minor(devNumber) - ) - if major == 0 { - return nil, ErrNotADevice - } - - var ( - devType string - mode = stat.Mode - ) - switch { - case mode&unix.S_IFBLK == unix.S_IFBLK: - devType = "b" - case mode&unix.S_IFCHR == unix.S_IFCHR: - devType = "c" + for _, dev := range devs { + s.Linux.Devices = append(s.Linux.Devices, dev) + s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, specs.LinuxDeviceCgroup{ + Allow: true, + Type: dev.Type, + Major: &dev.Major, + Minor: &dev.Minor, + Access: permissions, + }) + } + return nil } - fm := os.FileMode(mode) - return &specs.LinuxDevice{ - Type: devType, - Path: path, - Major: int64(major), - Minor: int64(minor), - FileMode: &fm, - UID: &stat.Uid, - GID: &stat.Gid, - }, nil } // WithMemorySwap sets the container's swap in bytes @@ -180,3 +122,24 @@ func WithCPUCFS(quota int64, period uint64) SpecOpts { return nil } } + +// WithAllCurrentCapabilities propagates the effective capabilities of the caller process to the container process. +// The capability set may differ from WithAllKnownCapabilities when running in a container. +var WithAllCurrentCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error { + caps, err := cap.Current() + if err != nil { + return err + } + return WithCapabilities(caps)(ctx, client, c, s) +} + +// WithAllKnownCapabilities sets all the the known linux capabilities for the container process +var WithAllKnownCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error { + caps := cap.Known() + return WithCapabilities(caps)(ctx, client, c, s) +} + +// WithoutRunMount removes the `/run` inside the spec +func WithoutRunMount(ctx context.Context, client Client, c *containers.Container, s *Spec) error { + return WithoutMounts("/run")(ctx, client, c, s) +} diff --git a/oci/spec_opts_linux_test.go b/oci/spec_opts_linux_test.go new file mode 100644 index 000000000000..5b7db0aa832b --- /dev/null +++ b/oci/spec_opts_linux_test.go @@ -0,0 +1,223 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package oci + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/containerd/containerd/pkg/testutil" + specs "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" +) + +func TestAddCaps(t *testing.T) { + t.Parallel() + + var s specs.Spec + + if err := WithAddedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + for i, cl := range [][]string{ + s.Process.Capabilities.Bounding, + s.Process.Capabilities.Effective, + s.Process.Capabilities.Permitted, + s.Process.Capabilities.Inheritable, + } { + if !capsContain(cl, "CAP_CHOWN") { + t.Errorf("cap list %d does not contain added cap", i) + } + } +} + +func TestDropCaps(t *testing.T) { + t.Parallel() + + var s specs.Spec + + if err := WithAllKnownCapabilities(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + + for i, cl := range [][]string{ + s.Process.Capabilities.Bounding, + s.Process.Capabilities.Effective, + s.Process.Capabilities.Permitted, + s.Process.Capabilities.Inheritable, + } { + if capsContain(cl, "CAP_CHOWN") { + t.Errorf("cap list %d contains dropped cap", i) + } + } + + // Add all capabilities back and drop a different cap. + if err := WithAllKnownCapabilities(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + if err := WithDroppedCapabilities([]string{"CAP_FOWNER"})(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + + for i, cl := range [][]string{ + s.Process.Capabilities.Bounding, + s.Process.Capabilities.Effective, + s.Process.Capabilities.Permitted, + s.Process.Capabilities.Inheritable, + } { + if capsContain(cl, "CAP_FOWNER") { + t.Errorf("cap list %d contains dropped cap", i) + } + if !capsContain(cl, "CAP_CHOWN") { + t.Errorf("cap list %d doesn't contain non-dropped cap", i) + } + } + + // Drop all duplicated caps. + if err := WithCapabilities([]string{"CAP_CHOWN", "CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { + t.Fatal(err) + } + for i, cl := range [][]string{ + s.Process.Capabilities.Bounding, + s.Process.Capabilities.Effective, + s.Process.Capabilities.Permitted, + s.Process.Capabilities.Inheritable, + } { + if len(cl) != 0 { + t.Errorf("cap list %d is not empty", i) + } + } +} + +func TestGetDevices(t *testing.T) { + testutil.RequiresRoot(t) + + dir, err := ioutil.TempDir("/dev", t.Name()) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + zero := filepath.Join(dir, "zero") + if err := ioutil.WriteFile(zero, nil, 0600); err != nil { + t.Fatal(err) + } + + if err := unix.Mount("/dev/zero", zero, "", unix.MS_BIND, ""); err != nil { + t.Fatal(err) + } + defer unix.Unmount(filepath.Join(dir, "zero"), unix.MNT_DETACH) + + t.Run("single device", func(t *testing.T) { + t.Run("no container path", func(t *testing.T) { + devices, err := getDevices(dir, "") + if err != nil { + t.Fatal(err) + } + + if len(devices) != 1 { + t.Fatalf("expected one device %v", devices) + } + if devices[0].Path != zero { + t.Fatalf("got unexpected device path %s", devices[0].Path) + } + }) + t.Run("with container path", func(t *testing.T) { + newPath := "/dev/testNew" + devices, err := getDevices(dir, newPath) + if err != nil { + t.Fatal(err) + } + + if len(devices) != 1 { + t.Fatalf("expected one device %v", devices) + } + if devices[0].Path != filepath.Join(newPath, "zero") { + t.Fatalf("got unexpected device path %s", devices[0].Path) + } + }) + }) + t.Run("With symlink in dir", func(t *testing.T) { + if err := os.Symlink("/dev/zero", filepath.Join(dir, "zerosym")); err != nil { + t.Fatal(err) + } + devices, err := getDevices(dir, "") + if err != nil { + t.Fatal(err) + } + if len(devices) != 1 { + t.Fatalf("expected one device %v", devices) + } + if devices[0].Path != filepath.Join(dir, "zero") { + t.Fatalf("got unexpected device path, expected %q, got %q", filepath.Join(dir, "zero"), devices[0].Path) + } + }) + t.Run("No devices", func(t *testing.T) { + dir := dir + "2" + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + t.Run("empty dir", func(T *testing.T) { + devices, err := getDevices(dir, "") + if err != nil { + t.Fatal(err) + } + if len(devices) != 0 { + t.Fatalf("expected no devices, got %+v", devices) + } + }) + t.Run("symlink to device in dir", func(t *testing.T) { + if err := os.Symlink("/dev/zero", filepath.Join(dir, "zerosym")); err != nil { + t.Fatal(err) + } + defer os.Remove(filepath.Join(dir, "zerosym")) + + devices, err := getDevices(dir, "") + if err != nil { + t.Fatal(err) + } + if len(devices) != 0 { + t.Fatalf("expected no devices, got %+v", devices) + } + }) + t.Run("regular file in dir", func(t *testing.T) { + if err := ioutil.WriteFile(filepath.Join(dir, "somefile"), []byte("hello"), 0600); err != nil { + t.Fatal(err) + } + defer os.Remove(filepath.Join(dir, "somefile")) + + devices, err := getDevices(dir, "") + if err != nil { + t.Fatal(err) + } + if len(devices) != 0 { + t.Fatalf("expected no devices, got %+v", devices) + } + }) + }) +} diff --git a/oci/spec_opts_nonlinux.go b/oci/spec_opts_nonlinux.go new file mode 100644 index 000000000000..77a1636387dc --- /dev/null +++ b/oci/spec_opts_nonlinux.go @@ -0,0 +1,38 @@ +// +build !linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package oci + +import ( + "context" + + "github.com/containerd/containerd/containers" +) + +// WithAllCurrentCapabilities propagates the effective capabilities of the caller process to the container process. +// The capability set may differ from WithAllKnownCapabilities when running in a container. +//nolint: deadcode, unused +var WithAllCurrentCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error { + return WithCapabilities(nil)(ctx, client, c, s) +} + +// WithAllKnownCapabilities sets all the the known linux capabilities for the container process +//nolint: deadcode, unused +var WithAllKnownCapabilities = func(ctx context.Context, client Client, c *containers.Container, s *Spec) error { + return WithCapabilities(nil)(ctx, client, c, s) +} diff --git a/oci/spec_opts_test.go b/oci/spec_opts_test.go index 12726fed43c0..7cc94b95622b 100644 --- a/oci/spec_opts_test.go +++ b/oci/spec_opts_test.go @@ -25,6 +25,7 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "reflect" "runtime" "strings" @@ -549,90 +550,6 @@ func TestWithImageConfigArgs(t *testing.T) { } } -func TestAddCaps(t *testing.T) { - t.Parallel() - - var s specs.Spec - - if err := WithAddedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - for i, cl := range [][]string{ - s.Process.Capabilities.Bounding, - s.Process.Capabilities.Effective, - s.Process.Capabilities.Permitted, - s.Process.Capabilities.Inheritable, - } { - if !capsContain(cl, "CAP_CHOWN") { - t.Errorf("cap list %d does not contain added cap", i) - } - } -} - -func TestDropCaps(t *testing.T) { - t.Parallel() - - var s specs.Spec - - if err := WithAllCapabilities(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - - for i, cl := range [][]string{ - s.Process.Capabilities.Bounding, - s.Process.Capabilities.Effective, - s.Process.Capabilities.Permitted, - s.Process.Capabilities.Inheritable, - } { - if capsContain(cl, "CAP_CHOWN") { - t.Errorf("cap list %d contains dropped cap", i) - } - } - - // Add all capabilities back and drop a different cap. - if err := WithAllCapabilities(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - if err := WithDroppedCapabilities([]string{"CAP_FOWNER"})(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - - for i, cl := range [][]string{ - s.Process.Capabilities.Bounding, - s.Process.Capabilities.Effective, - s.Process.Capabilities.Permitted, - s.Process.Capabilities.Inheritable, - } { - if capsContain(cl, "CAP_FOWNER") { - t.Errorf("cap list %d contains dropped cap", i) - } - if !capsContain(cl, "CAP_CHOWN") { - t.Errorf("cap list %d doesn't contain non-dropped cap", i) - } - } - - // Drop all duplicated caps. - if err := WithCapabilities([]string{"CAP_CHOWN", "CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { - t.Fatal(err) - } - for i, cl := range [][]string{ - s.Process.Capabilities.Bounding, - s.Process.Capabilities.Effective, - s.Process.Capabilities.Permitted, - s.Process.Capabilities.Inheritable, - } { - if len(cl) != 0 { - t.Errorf("cap list %d is not empty", i) - } - } -} - func TestDevShmSize(t *testing.T) { t.Parallel() var ( @@ -685,3 +602,59 @@ func getShmSize(opts []string) string { } return "" } + +func TestWithoutMounts(t *testing.T) { + t.Parallel() + var s Spec + + x := func(s string) string { + if runtime.GOOS == "windows" { + return filepath.Join("C:\\", filepath.Clean(s)) + } + return s + } + opts := []SpecOpts{ + WithMounts([]specs.Mount{ + { + Destination: x("/dst1"), + Source: x("/src1"), + }, + { + Destination: x("/dst2"), + Source: x("/src2"), + }, + { + Destination: x("/dst3"), + Source: x("/src3"), + }, + }), + WithoutMounts(x("/dst2"), x("/dst3")), + WithMounts([]specs.Mount{ + { + Destination: x("/dst4"), + Source: x("/src4"), + }, + }), + } + + expected := []specs.Mount{ + { + Destination: x("/dst1"), + Source: x("/src1"), + }, + { + Destination: x("/dst4"), + Source: x("/src4"), + }, + } + + for _, opt := range opts { + if err := opt(nil, nil, nil, &s); err != nil { + t.Fatal(err) + } + } + + if !reflect.DeepEqual(expected, s.Mounts) { + t.Fatalf("expected %+v, got %+v", expected, s.Mounts) + } +} diff --git a/oci/spec_opts_unix.go b/oci/spec_opts_unix.go index 972c11c8f85d..80a522356761 100644 --- a/oci/spec_opts_unix.go +++ b/oci/spec_opts_unix.go @@ -20,20 +20,15 @@ package oci import ( "context" - "io/ioutil" - "os" - "path/filepath" "github.com/containerd/containerd/containers" - specs "github.com/opencontainers/runtime-spec/specs-go" - "golang.org/x/sys/unix" ) // WithHostDevices adds all the hosts device nodes to the container's spec func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { setLinux(s) - devs, err := getDevices("/dev") + devs, err := HostDevices() if err != nil { return err } @@ -41,82 +36,18 @@ func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Sp return nil } -func getDevices(path string) ([]specs.LinuxDevice, error) { - files, err := ioutil.ReadDir(path) - if err != nil { - return nil, err - } - var out []specs.LinuxDevice - for _, f := range files { - switch { - case f.IsDir(): - switch f.Name() { - // ".lxc" & ".lxd-mounts" added to address https://github.com/lxc/lxd/issues/2825 - // ".udev" added to address https://github.com/opencontainers/runc/issues/2093 - case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev": - continue - default: - sub, err := getDevices(filepath.Join(path, f.Name())) - if err != nil { - return nil, err - } - - out = append(out, sub...) - continue - } - case f.Name() == "console": - continue - } - device, err := deviceFromPath(filepath.Join(path, f.Name()), "rwm") +// WithDevices recursively adds devices from the passed in path and associated cgroup rules for that device. +// If devicePath is a dir it traverses the dir to add all devices in that dir. +// If devicePath is not a dir, it attempts to add the single device. +func WithDevices(devicePath, containerPath, permissions string) SpecOpts { + return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error { + devs, err := getDevices(devicePath, containerPath) if err != nil { - if err == ErrNotADevice { - continue - } - if os.IsNotExist(err) { - continue - } - return nil, err + return err } - out = append(out, *device) - } - return out, nil -} - -func deviceFromPath(path, permissions string) (*specs.LinuxDevice, error) { - var stat unix.Stat_t - if err := unix.Lstat(path, &stat); err != nil { - return nil, err - } - - var ( - devNumber = uint64(stat.Rdev) - major = unix.Major(devNumber) - minor = unix.Minor(devNumber) - ) - if major == 0 { - return nil, ErrNotADevice - } - - var ( - devType string - mode = stat.Mode - ) - switch { - case mode&unix.S_IFBLK == unix.S_IFBLK: - devType = "b" - case mode&unix.S_IFCHR == unix.S_IFCHR: - devType = "c" + s.Linux.Devices = append(s.Linux.Devices, devs...) + return nil } - fm := os.FileMode(mode) - return &specs.LinuxDevice{ - Type: devType, - Path: path, - Major: int64(major), - Minor: int64(minor), - FileMode: &fm, - UID: &stat.Uid, - GID: &stat.Gid, - }, nil } // WithCPUCFS sets the container's Completely fair scheduling (CFS) quota and period diff --git a/oci/spec_opts_windows.go b/oci/spec_opts_windows.go index d5004abb115c..126a89ec83a9 100644 --- a/oci/spec_opts_windows.go +++ b/oci/spec_opts_windows.go @@ -74,6 +74,6 @@ func WithHostDevices(_ context.Context, _ Client, _ *containers.Container, s *Sp return nil } -func deviceFromPath(path, permissions string) (*specs.LinuxDevice, error) { +func deviceFromPath(path string) (*specs.LinuxDevice, error) { return nil, errors.New("device from path not supported on Windows") } diff --git a/oci/spec_test.go b/oci/spec_test.go index e36eac72c3d2..9e9f98134e50 100644 --- a/oci/spec_test.go +++ b/oci/spec_test.go @@ -23,6 +23,7 @@ import ( "github.com/containerd/containerd/containers" "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/pkg/testutil" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -251,6 +252,10 @@ func TestPopulateDefaultUnixSpec(t *testing.T) { func TestWithPrivileged(t *testing.T) { t.Parallel() + if runtime.GOOS == "linux" { + // because WithPrivileged depends on CapEff in /proc/self/status + testutil.RequiresRoot(t) + } ctx := namespaces.WithNamespace(context.Background(), "testing") @@ -272,6 +277,10 @@ func TestWithPrivileged(t *testing.T) { t.Fatal(err) } + if runtime.GOOS != "linux" { + return + } + if len(s.Process.Capabilities.Bounding) == 0 { t.Error("Expected capabilities to be set with privileged") } diff --git a/oci/utils_unix.go b/oci/utils_unix.go new file mode 100644 index 000000000000..108cacf5b8bf --- /dev/null +++ b/oci/utils_unix.go @@ -0,0 +1,137 @@ +// +build !windows + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package oci + +import ( + "io/ioutil" + "os" + "path/filepath" + + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +var errNotADevice = errors.New("not a device node") + +// HostDevices returns all devices that can be found under /dev directory. +func HostDevices() ([]specs.LinuxDevice, error) { + return getDevices("/dev", "") +} + +func getDevices(path, containerPath string) ([]specs.LinuxDevice, error) { + stat, err := os.Stat(path) + if err != nil { + return nil, errors.Wrap(err, "error stating device path") + } + + if !stat.IsDir() { + dev, err := deviceFromPath(path) + if err != nil { + return nil, err + } + if containerPath != "" { + dev.Path = containerPath + } + return []specs.LinuxDevice{*dev}, nil + } + + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + var out []specs.LinuxDevice + for _, f := range files { + switch { + case f.IsDir(): + switch f.Name() { + // ".lxc" & ".lxd-mounts" added to address https://github.com/lxc/lxd/issues/2825 + // ".udev" added to address https://github.com/opencontainers/runc/issues/2093 + case "pts", "shm", "fd", "mqueue", ".lxc", ".lxd-mounts", ".udev": + continue + default: + var cp string + if containerPath != "" { + cp = filepath.Join(containerPath, filepath.Base(f.Name())) + } + sub, err := getDevices(filepath.Join(path, f.Name()), cp) + if err != nil { + return nil, err + } + + out = append(out, sub...) + continue + } + case f.Name() == "console": + continue + } + device, err := deviceFromPath(filepath.Join(path, f.Name())) + if err != nil { + if err == errNotADevice { + continue + } + if os.IsNotExist(err) { + continue + } + return nil, err + } + if containerPath != "" { + device.Path = filepath.Join(containerPath, filepath.Base(f.Name())) + } + out = append(out, *device) + } + return out, nil +} + +func deviceFromPath(path string) (*specs.LinuxDevice, error) { + var stat unix.Stat_t + if err := unix.Lstat(path, &stat); err != nil { + return nil, err + } + + var ( + devNumber = uint64(stat.Rdev) //nolint: unconvert // the type is 32bit on mips. + major = unix.Major(devNumber) + minor = unix.Minor(devNumber) + ) + if major == 0 { + return nil, errNotADevice + } + + var ( + devType string + mode = stat.Mode + ) + switch { + case mode&unix.S_IFBLK == unix.S_IFBLK: + devType = "b" + case mode&unix.S_IFCHR == unix.S_IFCHR: + devType = "c" + } + fm := os.FileMode(mode &^ unix.S_IFMT) + return &specs.LinuxDevice{ + Type: devType, + Path: path, + Major: int64(major), + Minor: int64(minor), + FileMode: &fm, + UID: &stat.Uid, + GID: &stat.Gid, + }, nil +} diff --git a/pkg/cap/cap_linux.go b/pkg/cap/cap_linux.go new file mode 100644 index 000000000000..35772a4d857d --- /dev/null +++ b/pkg/cap/cap_linux.go @@ -0,0 +1,192 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package cap provides Linux capability utility +package cap + +import ( + "bufio" + "io" + "os" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// FromNumber returns a cap string like "CAP_SYS_ADMIN" +// that corresponds to the given number like 21. +// +// FromNumber returns an empty string for unknown cap number. +func FromNumber(num int) string { + if num < 0 || num > len(capsLatest)-1 { + return "" + } + return capsLatest[num] +} + +// FromBitmap parses an uint64 bitmap into string slice like +// []{"CAP_SYS_ADMIN", ...}. +// +// Unknown cap numbers are returned as []int. +func FromBitmap(v uint64) ([]string, []int) { + var ( + res []string + unknown []int + ) + for i := 0; i <= 63; i++ { + if b := (v >> i) & 0x1; b == 0x1 { + if s := FromNumber(i); s != "" { + res = append(res, s) + } else { + unknown = append(unknown, i) + } + } + } + return res, unknown +} + +// Type is the type of capability +type Type int + +const ( + // Effective is CapEff + Effective Type = 1 << iota + // Permitted is CapPrm + Permitted + // Inheritable is CapInh + Inheritable + // Bounding is CapBnd + Bounding + // Ambient is CapAmb + Ambient +) + +// ParseProcPIDStatus returns uint64 bitmap value from /proc//status file +func ParseProcPIDStatus(r io.Reader) (map[Type]uint64, error) { + res := make(map[Type]uint64) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + pair := strings.SplitN(line, ":", 2) + if len(pair) != 2 { + continue + } + k := strings.TrimSpace(pair[0]) + v := strings.TrimSpace(pair[1]) + switch k { + case "CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb": + ui64, err := strconv.ParseUint(v, 16, 64) + if err != nil { + return nil, errors.Errorf("failed to parse line %q", line) + } + switch k { + case "CapInh": + res[Inheritable] = ui64 + case "CapPrm": + res[Permitted] = ui64 + case "CapEff": + res[Effective] = ui64 + case "CapBnd": + res[Bounding] = ui64 + case "CapAmb": + res[Ambient] = ui64 + } + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return res, nil +} + +// Current returns the list of the effective and the known caps of +// the current process. +// +// The result is like []string{"CAP_SYS_ADMIN", ...}. +// +// The result does not contain caps that are not recognized by +// the "github.com/syndtr/gocapability" library. +func Current() ([]string, error) { + f, err := os.Open("/proc/self/status") + if err != nil { + return nil, err + } + defer f.Close() + caps, err := ParseProcPIDStatus(f) + if err != nil { + return nil, err + } + capEff := caps[Effective] + names, _ := FromBitmap(capEff) + return names, nil +} + +var ( + // caps35 is the caps of kernel 3.5 (37 entries) + caps35 = []string{ + "CAP_CHOWN", // 2.2 + "CAP_DAC_OVERRIDE", // 2.2 + "CAP_DAC_READ_SEARCH", // 2.2 + "CAP_FOWNER", // 2.2 + "CAP_FSETID", // 2.2 + "CAP_KILL", // 2.2 + "CAP_SETGID", // 2.2 + "CAP_SETUID", // 2.2 + "CAP_SETPCAP", // 2.2 + "CAP_LINUX_IMMUTABLE", // 2.2 + "CAP_NET_BIND_SERVICE", // 2.2 + "CAP_NET_BROADCAST", // 2.2 + "CAP_NET_ADMIN", // 2.2 + "CAP_NET_RAW", // 2.2 + "CAP_IPC_LOCK", // 2.2 + "CAP_IPC_OWNER", // 2.2 + "CAP_SYS_MODULE", // 2.2 + "CAP_SYS_RAWIO", // 2.2 + "CAP_SYS_CHROOT", // 2.2 + "CAP_SYS_PTRACE", // 2.2 + "CAP_SYS_PACCT", // 2.2 + "CAP_SYS_ADMIN", // 2.2 + "CAP_SYS_BOOT", // 2.2 + "CAP_SYS_NICE", // 2.2 + "CAP_SYS_RESOURCE", // 2.2 + "CAP_SYS_TIME", // 2.2 + "CAP_SYS_TTY_CONFIG", // 2.2 + "CAP_MKNOD", // 2.4 + "CAP_LEASE", // 2.4 + "CAP_AUDIT_WRITE", // 2.6.11 + "CAP_AUDIT_CONTROL", // 2.6.11 + "CAP_SETFCAP", // 2.6.24 + "CAP_MAC_OVERRIDE", // 2.6.25 + "CAP_MAC_ADMIN", // 2.6.25 + "CAP_SYSLOG", // 2.6.37 + "CAP_WAKE_ALARM", // 3.0 + "CAP_BLOCK_SUSPEND", // 3.5 + } + // caps316 is the caps of kernel 3.16 (38 entries) + caps316 = append(caps35, "CAP_AUDIT_READ") + // caps58 is the caps of kernel 5.8 (40 entries) + caps58 = append(caps316, []string{"CAP_PERFMON", "CAP_BPF"}...) + // caps59 is the caps of kernel 5.9 (41 entries) + caps59 = append(caps58, "CAP_CHECKPOINT_RESTORE") + capsLatest = caps59 +) + +// Known returns the known cap strings of the latest kernel. +// The current latest kernel is 5.9. +func Known() []string { + return capsLatest +} diff --git a/pkg/cap/cap_linux_test.go b/pkg/cap/cap_linux_test.go new file mode 100644 index 000000000000..85b380f11e79 --- /dev/null +++ b/pkg/cap/cap_linux_test.go @@ -0,0 +1,169 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cap + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCapsList(t *testing.T) { + assert.Len(t, caps316, 38) + assert.Len(t, caps58, 40) + assert.Len(t, caps59, 41) +} + +func TestFromNumber(t *testing.T) { + assert.Equal(t, "CAP_CHOWN", FromNumber(0)) + assert.Equal(t, "CAP_SYS_ADMIN", FromNumber(21)) + assert.Equal(t, "CAP_CHECKPOINT_RESTORE", FromNumber(40)) + assert.Equal(t, "", FromNumber(-1)) + assert.Equal(t, "", FromNumber(63)) + assert.Equal(t, "", FromNumber(255)) +} + +func TestFromBitmap(t *testing.T) { + type testCase struct { + comment string + v uint64 + knownNames []string + unknown []int + } + testCases := []testCase{ + { + comment: "No cap", + v: 0x0000000000000000, + }, + { + // 3.10 (same caps as 3.5) is the oldest kernel version we want to support + comment: "All caps on kernel 3.5 (last = CAP_BLOCK_SUSPEND)", + v: 0x0000001fffffffff, + knownNames: caps35, + }, + { + comment: "All caps on kernel 3.16 (last = CAP_AUDIT_READ)", + v: 0x0000003fffffffff, + knownNames: caps316, + }, + { + comment: "All caps on kernel 5.8 (last = CAP_BPF)", + v: 0x000000ffffffffff, + knownNames: caps58, + }, + { + comment: "All caps on kernel 5.9 (last = CAP_CHECKPOINT_RESTORE)", + v: 0x000001ffffffffff, + knownNames: caps59, + }, + { + comment: "Unknown caps", + v: 0xf00001ffffffffff, + knownNames: caps59, + unknown: []int{60, 61, 62, 63}, + }, + } + + for _, tc := range testCases { + knownNames, unknown := FromBitmap(tc.v) + t.Logf("[%s] v=0x%x, got=%+v (%d entries), unknown=%v", + tc.comment, tc.v, knownNames, len(knownNames), unknown) + assert.Equal(t, tc.knownNames, knownNames) + assert.Equal(t, tc.unknown, unknown) + } +} + +func TestParseProcPIDStatus(t *testing.T) { + procPIDStatus := `Name: cat +Umask: 0022 +State: R (running) +Tgid: 170065 +Ngid: 0 +Pid: 170065 +PPid: 170064 +TracerPid: 0 +Uid: 0 0 0 0 +Gid: 0 0 0 0 +FDSize: 64 +Groups: 0 +NStgid: 170065 +NSpid: 170065 +NSpgid: 170064 +NSsid: 3784 +VmPeak: 8216 kB +VmSize: 8216 kB +VmLck: 0 kB +VmPin: 0 kB +VmHWM: 676 kB +VmRSS: 676 kB +RssAnon: 72 kB +RssFile: 604 kB +RssShmem: 0 kB +VmData: 324 kB +VmStk: 132 kB +VmExe: 20 kB +VmLib: 1612 kB +VmPTE: 56 kB +VmSwap: 0 kB +HugetlbPages: 0 kB +CoreDumping: 0 +THP_enabled: 1 +Threads: 1 +SigQ: 0/63692 +SigPnd: 0000000000000000 +ShdPnd: 0000000000000000 +SigBlk: 0000000000000000 +SigIgn: 0000000000000000 +SigCgt: 0000000000000000 +CapInh: 0000000000000000 +CapPrm: 000000ffffffffff +CapEff: 000000ffffffffff +CapBnd: 000000ffffffffff +CapAmb: 0000000000000000 +NoNewPrivs: 0 +Seccomp: 0 +Speculation_Store_Bypass: thread vulnerable +Cpus_allowed: 00000000,00000000,00000000,0000000f +Cpus_allowed_list: 0-3 +Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001 +Mems_allowed_list: 0 +voluntary_ctxt_switches: 0 +nonvoluntary_ctxt_switches: 0 +` + res, err := ParseProcPIDStatus(strings.NewReader(procPIDStatus)) + assert.NoError(t, err) + expected := map[Type]uint64{ + Inheritable: 0, + Permitted: 0xffffffffff, + Effective: 0xffffffffff, + Bounding: 0xffffffffff, + Ambient: 0, + } + assert.EqualValues(t, expected, res) +} + +func TestCurrent(t *testing.T) { + caps, err := Current() + assert.NoError(t, err) + t.Logf("verify the result manually: %+v", caps) +} + +func TestKnown(t *testing.T) { + caps := Known() + assert.EqualValues(t, caps59, caps) +}