diff --git a/Makefile b/Makefile index cdfc4ec1f..eea9d4253 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ TARBALL := $(TARBALL_PREFIX)-$(VERSION).$(GOOS)-$(GOARCH).tar.gz ifneq ($(GOOS),windows) BUILD_TAGS := seccomp apparmor selinux endif +export BUILDTAGS := $(BUILD_TAGS) # Add `-TEST` suffix to indicate that all binaries built from this repo are for test. GO_LDFLAGS := -X $(PROJECT)/vendor/github.com/containerd/containerd/version.Version=$(VERSION)-TEST SOURCES := $(shell find cmd/ pkg/ vendor/ -name '*.go') @@ -91,7 +92,7 @@ test: ## unit test @echo "$(WHALE) $@" $(GO) test -timeout=10m -race ./pkg/... \ -tags '$(BUILD_TAGS)' \ - -ldflags '$(GO_LDFLAGS)' \ + -ldflags '$(GO_LDFLAGS)' \ -gcflags '$(GO_GCFLAGS)' $(BUILD_DIR)/integration.test: $(INTEGRATION_SOURCES) @@ -162,7 +163,7 @@ else install.deps: .install.deps.linux ## install windows deps on linux endif -.install.deps.linux: ## install dependencies of cri (default 'seccomp apparmor' BUILDTAGS for runc build) +.install.deps.linux: ## install dependencies of cri @echo "$(WHALE) $@" @./hack/install/install-deps.sh diff --git a/hack/install/install-runc.sh b/hack/install/install-runc.sh index c99c4e66e..fc26071f3 100755 --- a/hack/install/install-runc.sh +++ b/hack/install/install-runc.sh @@ -29,7 +29,7 @@ GOPATH=$(mktemp -d /tmp/cri-install-runc.XXXX) from-vendor RUNC github.com/opencontainers/runc checkout_repo ${RUNC_PKG} ${RUNC_VERSION} ${RUNC_REPO} cd ${GOPATH}/src/${RUNC_PKG} -make static BUILDTAGS="$BUILDTAGS" VERSION=${RUNC_VERSION} +make BUILDTAGS="$BUILDTAGS" VERSION=${RUNC_VERSION} ${SUDO} make install -e DESTDIR=${RUNC_DIR} # Clean the tmp GOPATH dir. Use sudo because runc build generates diff --git a/hack/test-utils.sh b/hack/test-utils.sh index 4994bbc58..bde573b88 100755 --- a/hack/test-utils.sh +++ b/hack/test-utils.sh @@ -23,6 +23,15 @@ CONTAINERD_FLAGS="--log-level=debug " # Use a configuration file for containerd. CONTAINERD_CONFIG_FILE=${CONTAINERD_CONFIG_FILE:-""} +if [ -z "${CONTAINERD_CONFIG_FILE}" ] && command -v sestatus >/dev/null 2>&1; then + selinux_config="/tmp/containerd-config-selinux.toml" + cat >${selinux_config} <<<' +[plugins.cri] + enable_selinux = true +' + CONTAINERD_CONFIG_FILE=${CONTAINERD_CONFIG_FILE:-"${selinux_config}"} +fi + # CONTAINERD_TEST_SUFFIX is the suffix appended to the root/state directory used # by test containerd. CONTAINERD_TEST_SUFFIX=${CONTAINERD_TEST_SUFFIX:-"-test"} diff --git a/hack/utils.sh b/hack/utils.sh index 8c8612d3c..f75678a17 100755 --- a/hack/utils.sh +++ b/hack/utils.sh @@ -17,7 +17,7 @@ ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"/.. # Not from vendor.conf. -CRITOOL_VERSION=v1.18.0 +CRITOOL_VERSION=89384cc13a27bb9128553c9fe75a7cc07c6a95bb CRITOOL_PKG=github.com/kubernetes-sigs/cri-tools CRITOOL_REPO=github.com/kubernetes-sigs/cri-tools diff --git a/pkg/containerd/opts/spec_unix.go b/pkg/containerd/opts/spec_unix.go index a9e78067e..e84cb9d47 100644 --- a/pkg/containerd/opts/spec_unix.go +++ b/pkg/containerd/opts/spec_unix.go @@ -226,7 +226,7 @@ func WithMounts(osi osinterface.OS, config *runtime.ContainerConfig, extra []*ru } if mount.GetSelinuxRelabel() { - if err := label.Relabel(src, mountLabel, true); err != nil && err != unix.ENOTSUP { + if err := label.Relabel(src, mountLabel, false); err != nil && err != unix.ENOTSUP { return errors.Wrapf(err, "relabel %q with %q failed", src, mountLabel) } } diff --git a/pkg/server/container_create.go b/pkg/server/container_create.go index d239eba09..faac8f797 100644 --- a/pkg/server/container_create.go +++ b/pkg/server/container_create.go @@ -28,6 +28,7 @@ import ( "github.com/davecgh/go-spew/spew" imagespec "github.com/opencontainers/image-spec/specs-go/v1" runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -154,6 +155,18 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta return nil, errors.Wrapf(err, "failed to generate container %q spec", id) } + meta.ProcessLabel = spec.Process.SelinuxLabel + if config.GetLinux().GetSecurityContext().GetPrivileged() { + // If privileged don't set the SELinux label but still record it on the container so + // the unused MCS label can be release later + spec.Process.SelinuxLabel = "" + } + defer func() { + if retErr != nil { + _ = label.ReleaseLabel(spec.Process.SelinuxLabel) + } + }() + log.G(ctx).Debugf("Container %q spec: %#+v", id, spew.NewFormatter(spec)) // Set snapshotter before any other options. @@ -275,10 +288,9 @@ func (c *criService) volumeMounts(containerRootDir string, criMounts []*runtime. src := filepath.Join(containerRootDir, "volumes", volumeID) // addOCIBindMounts will create these volumes. mounts = append(mounts, &runtime.Mount{ - ContainerPath: dst, - HostPath: src, - // Use default mount propagation. - // TODO(random-liu): What about selinux relabel? + ContainerPath: dst, + HostPath: src, + SelinuxRelabel: true, }) } return mounts diff --git a/pkg/server/container_create_unix.go b/pkg/server/container_create_unix.go index 0324bc206..239d91872 100644 --- a/pkg/server/container_create_unix.go +++ b/pkg/server/container_create_unix.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/containerd/oci" imagespec "github.com/opencontainers/image-spec/specs-go/v1" runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -109,7 +110,7 @@ func (c *criService) containerMounts(sandboxID string, config *runtime.Container func (c *criService) containerSpec(id string, sandboxID string, sandboxPid uint32, netNSPath string, containerName string, config *runtime.ContainerConfig, sandboxConfig *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig, - extraMounts []*runtime.Mount, ociRuntime config.Runtime) (*runtimespec.Spec, error) { + extraMounts []*runtime.Mount, ociRuntime config.Runtime) (_ *runtimespec.Spec, retErr error) { specOpts := []oci.SpecOpts{ customopts.WithoutRunMount, @@ -151,11 +152,30 @@ func (c *criService) containerSpec(id string, sandboxID string, sandboxPid uint3 specOpts = append(specOpts, oci.WithEnv(env)) securityContext := config.GetLinux().GetSecurityContext() - selinuxOpt := securityContext.GetSelinuxOptions() - processLabel, mountLabel, err := initSelinuxOpts(selinuxOpt) + labelOptions, err := toLabel(securityContext.GetSelinuxOptions()) + if err != nil { + return nil, err + } + if len(labelOptions) == 0 { + // Use pod level SELinux config + if sandbox, err := c.sandboxStore.Get(sandboxID); err == nil { + labelOptions, err = label.DupSecOpt(sandbox.ProcessLabel) + if err != nil { + return nil, err + } + } + } + + processLabel, mountLabel, err := label.InitLabels(labelOptions) if err != nil { return nil, errors.Wrapf(err, "failed to init selinux options %+v", securityContext.GetSelinuxOptions()) } + defer func() { + if retErr != nil { + _ = label.ReleaseLabel(processLabel) + } + }() + specOpts = append(specOpts, customopts.WithMounts(c.os, config, extraMounts, mountLabel)) if !c.config.DisableProcMount { diff --git a/pkg/server/container_create_unix_test.go b/pkg/server/container_create_unix_test.go index b27bb38e2..0967b81b1 100644 --- a/pkg/server/container_create_unix_test.go +++ b/pkg/server/container_create_unix_test.go @@ -35,6 +35,7 @@ import ( imagespec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runc/libcontainer/devices" runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -233,6 +234,12 @@ func TestContainerCapabilities(t *testing.T) { containerConfig.Linux.SecurityContext.Capabilities = test.capability spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) require.NoError(t, err) + + if selinux.GetEnabled() { + assert.NotEqual(t, "", spec.Process.SelinuxLabel) + assert.NotEqual(t, "", spec.Linux.MountLabel) + } + specCheck(t, testID, testSandboxID, testPid, spec) for _, include := range test.includes { assert.Contains(t, spec.Process.Capabilities.Bounding, include) diff --git a/pkg/server/helpers_selinux_test.go b/pkg/server/helpers_selinux_test.go index 83b33d994..53ed59c5a 100644 --- a/pkg/server/helpers_selinux_test.go +++ b/pkg/server/helpers_selinux_test.go @@ -19,7 +19,6 @@ package server import ( - "strings" "testing" "github.com/opencontainers/selinux/go-selinux" @@ -35,23 +34,23 @@ func TestInitSelinuxOpts(t *testing.T) { for desc, test := range map[string]struct { selinuxOpt *runtime.SELinuxOption processLabel string - mountLabels []string + mountLabel string expectErr bool }{ "Should return empty strings for processLabel and mountLabel when selinuxOpt is nil": { selinuxOpt: nil, - processLabel: "", - mountLabels: []string{"", ""}, + processLabel: ".*:c[0-9]{1,3},c[0-9]{1,3}", + mountLabel: ".*:c[0-9]{1,3},c[0-9]{1,3}", }, - "Should return empty strings for processLabel and mountLabel when selinuxOpt has been initialized partially": { + "Should overlay fields on processLabel when selinuxOpt has been initialized partially": { selinuxOpt: &runtime.SELinuxOption{ User: "", Role: "user_r", Type: "", Level: "s0:c1,c2", }, - processLabel: "", - mountLabels: []string{"", ""}, + processLabel: "system_u:user_r:(container_file_t|svirt_lxc_net_t):s0:c1,c2", + mountLabel: "system_u:object_r:(container_file_t|svirt_sandbox_file_t):s0:c1,c2", }, "Should be resolved correctly when selinuxOpt has been initialized completely": { selinuxOpt: &runtime.SELinuxOption{ @@ -61,7 +60,7 @@ func TestInitSelinuxOpts(t *testing.T) { Level: "s0:c1,c2", }, processLabel: "user_u:user_r:user_t:s0:c1,c2", - mountLabels: []string{"user_u:object_r:container_file_t:s0:c1,c2", "user_u:object_r:svirt_sandbox_file_t:s0:c1,c2"}, + mountLabel: "user_u:object_r:(container_file_t|svirt_sandbox_file_t):s0:c1,c2", }, "Should be resolved correctly when selinuxOpt has been initialized with level=''": { selinuxOpt: &runtime.SELinuxOption{ @@ -70,8 +69,8 @@ func TestInitSelinuxOpts(t *testing.T) { Type: "user_t", Level: "", }, - processLabel: "user_u:user_r:user_t:s0", - mountLabels: []string{"user_u:object_r:container_file_t:s0", "user_u:object_r:svirt_sandbox_file_t:s0"}, + processLabel: "user_u:user_r:user_t:s0:c[0-9]{1,3},c[0-9]{1,3}", + mountLabel: "user_u:object_r:(container_file_t|svirt_sandbox_file_t):s0", }, "Should return error when the format of 'level' is not correct": { selinuxOpt: &runtime.SELinuxOption{ @@ -84,20 +83,12 @@ func TestInitSelinuxOpts(t *testing.T) { }, } { t.Run(desc, func(t *testing.T) { - processLabel, mountLabel, err := initSelinuxOpts(test.selinuxOpt) + processLabel, mountLabel, err := initLabelsFromOpt(test.selinuxOpt) if test.expectErr { assert.Error(t, err) } else { - assert.NoError(t, err) - if test.selinuxOpt == nil || test.selinuxOpt.Level != "" { - assert.Equal(t, test.processLabel, processLabel) - assert.Contains(t, test.mountLabels, mountLabel) - } else { - assert.Equal(t, 0, strings.LastIndex(processLabel, test.processLabel)) - contain := strings.LastIndex(mountLabel, test.mountLabels[0]) == 0 || - strings.LastIndex(mountLabel, test.mountLabels[1]) == 0 - assert.True(t, contain) - } + assert.Regexp(t, test.processLabel, processLabel) + assert.Regexp(t, test.mountLabel, mountLabel) } }) } @@ -157,13 +148,11 @@ func TestCheckSelinuxLevel(t *testing.T) { }, } { t.Run(desc, func(t *testing.T) { - ok, err := checkSelinuxLevel(test.level) + err := checkSelinuxLevel(test.level) if test.expectNoMatch { - assert.NoError(t, err) - assert.False(t, ok) + assert.Error(t, err) } else { assert.NoError(t, err) - assert.True(t, ok) } }) } diff --git a/pkg/server/helpers_unix.go b/pkg/server/helpers_unix.go index bb7440fc9..674552a04 100644 --- a/pkg/server/helpers_unix.go +++ b/pkg/server/helpers_unix.go @@ -93,47 +93,52 @@ func (c *criService) getSandboxDevShm(id string) string { return filepath.Join(c.getVolatileSandboxRootDir(id), "shm") } -func initSelinuxOpts(selinuxOpt *runtime.SELinuxOption) (string, string, error) { - if selinuxOpt == nil { - return "", "", nil - } +func toLabel(selinuxOptions *runtime.SELinuxOption) ([]string, error) { + var labels []string - // Should ignored selinuxOpts if they are incomplete. - if selinuxOpt.GetUser() == "" || - selinuxOpt.GetRole() == "" || - selinuxOpt.GetType() == "" { - return "", "", nil + if selinuxOptions == nil { + return nil, nil } - - // make sure the format of "level" is correct. - ok, err := checkSelinuxLevel(selinuxOpt.GetLevel()) - if err != nil || !ok { - return "", "", err + if err := checkSelinuxLevel(selinuxOptions.Level); err != nil { + return nil, err + } + if selinuxOptions.User != "" { + labels = append(labels, "user:"+selinuxOptions.User) + } + if selinuxOptions.Role != "" { + labels = append(labels, "role:"+selinuxOptions.Role) + } + if selinuxOptions.Type != "" { + labels = append(labels, "type:"+selinuxOptions.Type) + } + if selinuxOptions.Level != "" { + labels = append(labels, "level:"+selinuxOptions.Level) } - labelOpts := fmt.Sprintf("%s:%s:%s:%s", - selinuxOpt.GetUser(), - selinuxOpt.GetRole(), - selinuxOpt.GetType(), - selinuxOpt.GetLevel()) + return labels, nil +} - options, err := label.DupSecOpt(labelOpts) +func initLabelsFromOpt(selinuxOpts *runtime.SELinuxOption) (string, string, error) { + labels, err := toLabel(selinuxOpts) if err != nil { return "", "", err } - return label.InitLabels(options) + return label.InitLabels(labels) } -func checkSelinuxLevel(level string) (bool, error) { +func checkSelinuxLevel(level string) error { if len(level) == 0 { - return true, nil + return nil } - matched, err := regexp.MatchString(`^s\d(-s\d)??(:c\d{1,4}((.c\d{1,4})?,c\d{1,4})*(.c\d{1,4})?(,c\d{1,4}(.c\d{1,4})?)*)?$`, level) - if err != nil || !matched { - return false, errors.Wrapf(err, "the format of 'level' %q is not correct", level) + matched, err := regexp.MatchString(`^s\d(-s\d)??(:c\d{1,4}(\.c\d{1,4})?(,c\d{1,4}(\.c\d{1,4})?)*)?$`, level) + if err != nil { + return errors.Wrapf(err, "the format of 'level' %q is not correct", level) } - return true, nil + if !matched { + return fmt.Errorf("the format of 'level' %q is not correct", level) + } + return nil } func (c *criService) apparmorEnabled() bool { diff --git a/pkg/server/sandbox_run.go b/pkg/server/sandbox_run.go index becbf9e1b..43c44f754 100644 --- a/pkg/server/sandbox_run.go +++ b/pkg/server/sandbox_run.go @@ -29,6 +29,7 @@ import ( cni "github.com/containerd/go-cni" "github.com/containerd/typeurl" "github.com/davecgh/go-spew/spew" + "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -157,6 +158,18 @@ func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandbox return nil, errors.Wrap(err, "failed to generate sandbox container spec") } log.G(ctx).Debugf("Sandbox container %q spec: %#+v", id, spew.NewFormatter(spec)) + sandbox.ProcessLabel = spec.Process.SelinuxLabel + defer func() { + if retErr != nil { + _ = label.ReleaseLabel(sandbox.ProcessLabel) + } + }() + + if config.GetLinux().GetSecurityContext().GetPrivileged() { + // If privileged don't set selinux label, but we still record the MCS label so that + // the unused label can be freed later. + spec.Process.SelinuxLabel = "" + } // Generate spec options that will be applied to the spec later. specOpts, err := c.sandboxContainerSpecOpts(config, &image.ImageSpec.Config) diff --git a/pkg/server/sandbox_run_unix.go b/pkg/server/sandbox_run_unix.go index cf460722c..a86dd2197 100644 --- a/pkg/server/sandbox_run_unix.go +++ b/pkg/server/sandbox_run_unix.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/containerd/plugin" imagespec "github.com/opencontainers/image-spec/specs-go/v1" runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "golang.org/x/sys/unix" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -38,7 +39,7 @@ import ( ) func (c *criService) sandboxContainerSpec(id string, config *runtime.PodSandboxConfig, - imageConfig *imagespec.ImageConfig, nsPath string, runtimePodAnnotations []string) (*runtimespec.Spec, error) { + imageConfig *imagespec.ImageConfig, nsPath string, runtimePodAnnotations []string) (_ *runtimespec.Spec, retErr error) { // Creates a spec Generator with the default spec. // TODO(random-liu): [P1] Compare the default settings with docker and containerd default. specOpts := []oci.SpecOpts{ @@ -117,11 +118,15 @@ func (c *criService) sandboxContainerSpec(id string, config *runtime.PodSandboxC }, })) - selinuxOpt := securityContext.GetSelinuxOptions() - processLabel, mountLabel, err := initSelinuxOpts(selinuxOpt) + processLabel, mountLabel, err := initLabelsFromOpt(securityContext.GetSelinuxOptions()) if err != nil { return nil, errors.Wrapf(err, "failed to init selinux options %+v", securityContext.GetSelinuxOptions()) } + defer func() { + if retErr != nil { + _ = label.ReleaseLabel(processLabel) + } + }() supplementalGroups := securityContext.GetSupplementalGroups() specOpts = append(specOpts, diff --git a/pkg/server/sandbox_run_unix_test.go b/pkg/server/sandbox_run_unix_test.go index dddfbfb48..c74612657 100644 --- a/pkg/server/sandbox_run_unix_test.go +++ b/pkg/server/sandbox_run_unix_test.go @@ -25,6 +25,7 @@ import ( imagespec "github.com/opencontainers/image-spec/specs-go/v1" runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -76,6 +77,11 @@ func getRunPodSandboxTestData() (*runtime.PodSandboxConfig, *imagespec.ImageConf assert.Contains(t, spec.Annotations, annotations.SandboxLogDir) assert.EqualValues(t, spec.Annotations[annotations.SandboxLogDir], "test-log-directory") + + if selinux.GetEnabled() { + assert.NotEqual(t, "", spec.Process.SelinuxLabel) + assert.NotEqual(t, "", spec.Linux.MountLabel) + } } return config, imageConfig, specCheck } diff --git a/pkg/server/service.go b/pkg/server/service.go index fba3d5a42..d4a0d2817 100644 --- a/pkg/server/service.go +++ b/pkg/server/service.go @@ -25,6 +25,7 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/plugin" + "github.com/containerd/cri/pkg/store/label" cni "github.com/containerd/go-cni" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -99,12 +100,13 @@ type criService struct { // NewCRIService returns a new instance of CRIService func NewCRIService(config criconfig.Config, client *containerd.Client) (CRIService, error) { var err error + labels := label.NewStore() c := &criService{ config: config, client: client, os: osinterface.RealOS{}, - sandboxStore: sandboxstore.NewStore(), - containerStore: containerstore.NewStore(), + sandboxStore: sandboxstore.NewStore(labels), + containerStore: containerstore.NewStore(labels), imageStore: imagestore.NewStore(client), snapshotStore: snapshotstore.NewStore(), sandboxNameIndex: registrar.NewRegistrar(), diff --git a/pkg/server/service_test.go b/pkg/server/service_test.go index 33085bff9..0bef174b7 100644 --- a/pkg/server/service_test.go +++ b/pkg/server/service_test.go @@ -23,6 +23,7 @@ import ( servertesting "github.com/containerd/cri/pkg/server/testing" containerstore "github.com/containerd/cri/pkg/store/container" imagestore "github.com/containerd/cri/pkg/store/image" + "github.com/containerd/cri/pkg/store/label" sandboxstore "github.com/containerd/cri/pkg/store/sandbox" snapshotstore "github.com/containerd/cri/pkg/store/snapshot" ) @@ -39,6 +40,7 @@ const ( // newTestCRIService creates a fake criService for test. func newTestCRIService() *criService { + labels := label.NewStore() return &criService{ config: criconfig.Config{ RootDir: testRootDir, @@ -49,11 +51,11 @@ func newTestCRIService() *criService { }, imageFSPath: testImageFSPath, os: ostesting.NewFakeOS(), - sandboxStore: sandboxstore.NewStore(), + sandboxStore: sandboxstore.NewStore(labels), imageStore: imagestore.NewStore(nil), snapshotStore: snapshotstore.NewStore(), sandboxNameIndex: registrar.NewRegistrar(), - containerStore: containerstore.NewStore(), + containerStore: containerstore.NewStore(labels), containerNameIndex: registrar.NewRegistrar(), netPlugin: servertesting.NewFakeCNIPlugin(), } diff --git a/pkg/store/container/container.go b/pkg/store/container/container.go index 998a39c4f..53c0745a5 100644 --- a/pkg/store/container/container.go +++ b/pkg/store/container/container.go @@ -20,6 +20,7 @@ import ( "sync" "github.com/containerd/containerd" + "github.com/containerd/cri/pkg/store/label" "github.com/docker/docker/pkg/truncindex" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -101,13 +102,15 @@ type Store struct { lock sync.RWMutex containers map[string]Container idIndex *truncindex.TruncIndex + labels *label.Store } // NewStore creates a container store. -func NewStore() *Store { +func NewStore(labels *label.Store) *Store { return &Store{ containers: make(map[string]Container), idIndex: truncindex.NewTruncIndex([]string{}), + labels: labels, } } @@ -119,6 +122,9 @@ func (s *Store) Add(c Container) error { if _, ok := s.containers[c.ID]; ok { return store.ErrAlreadyExist } + if err := s.labels.Reserve(c.ProcessLabel); err != nil { + return err + } if err := s.idIndex.Add(c.ID); err != nil { return err } @@ -165,6 +171,7 @@ func (s *Store) Delete(id string) { // So we need to return if there are error. return } + s.labels.Release(s.containers[id].ProcessLabel) s.idIndex.Delete(id) // nolint: errcheck delete(s.containers, id) } diff --git a/pkg/store/container/container_test.go b/pkg/store/container/container_test.go index 33493654f..a88bc02c2 100644 --- a/pkg/store/container/container_test.go +++ b/pkg/store/container/container_test.go @@ -17,9 +17,12 @@ package container import ( + "strings" "testing" "time" + "github.com/containerd/cri/pkg/store/label" + "github.com/opencontainers/selinux/go-selinux" assertlib "github.com/stretchr/testify/assert" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -39,9 +42,10 @@ func TestContainerStore(t *testing.T) { Attempt: 1, }, }, - ImageRef: "TestImage-1", - StopSignal: "SIGTERM", - LogPath: "/test/log/path/1", + ImageRef: "TestImage-1", + StopSignal: "SIGTERM", + LogPath: "/test/log/path/1", + ProcessLabel: "junk:junk:junk:c1,c2", }, "2abcd": { ID: "2abcd", @@ -53,9 +57,10 @@ func TestContainerStore(t *testing.T) { Attempt: 2, }, }, - StopSignal: "SIGTERM", - ImageRef: "TestImage-2", - LogPath: "/test/log/path/2", + StopSignal: "SIGTERM", + ImageRef: "TestImage-2", + LogPath: "/test/log/path/2", + ProcessLabel: "junk:junk:junk:c1,c2", }, "4a333": { ID: "4a333", @@ -67,9 +72,10 @@ func TestContainerStore(t *testing.T) { Attempt: 3, }, }, - StopSignal: "SIGTERM", - ImageRef: "TestImage-3", - LogPath: "/test/log/path/3", + StopSignal: "SIGTERM", + ImageRef: "TestImage-3", + LogPath: "/test/log/path/3", + ProcessLabel: "junk:junk:junk:c1,c3", }, "4abcd": { ID: "4abcd", @@ -81,8 +87,9 @@ func TestContainerStore(t *testing.T) { Attempt: 1, }, }, - StopSignal: "SIGTERM", - ImageRef: "TestImage-4abcd", + StopSignal: "SIGTERM", + ImageRef: "TestImage-4abcd", + ProcessLabel: "junk:junk:junk:c1,c4", }, } statuses := map[string]Status{ @@ -136,7 +143,14 @@ func TestContainerStore(t *testing.T) { containers[id] = container } - s := NewStore() + s := NewStore(label.NewStore()) + reserved := map[string]bool{} + s.labels.Reserver = func(label string) { + reserved[strings.SplitN(label, ":", 4)[3]] = true + } + s.labels.Releaser = func(label string) { + reserved[strings.SplitN(label, ":", 4)[3]] = false + } t.Logf("should be able to add container") for _, c := range containers { @@ -155,6 +169,15 @@ func TestContainerStore(t *testing.T) { cs := s.List() assert.Len(cs, len(containers)) + if selinux.GetEnabled() { + t.Logf("should have reserved labels (requires -tag selinux)") + assert.Equal(map[string]bool{ + "c1,c2": true, + "c1,c3": true, + "c1,c4": true, + }, reserved) + } + cntrNum := len(containers) for testID, v := range containers { truncID := genTruncIndex(testID) @@ -173,6 +196,15 @@ func TestContainerStore(t *testing.T) { assert.Equal(Container{}, c) assert.Equal(store.ErrNotExist, err) } + + if selinux.GetEnabled() { + t.Logf("should have released all labels (requires -tag selinux)") + assert.Equal(map[string]bool{ + "c1,c2": false, + "c1,c3": false, + "c1,c4": false, + }, reserved) + } } func TestWithContainerIO(t *testing.T) { diff --git a/pkg/store/container/metadata.go b/pkg/store/container/metadata.go index 23e35dbee..ff9b5f2a3 100644 --- a/pkg/store/container/metadata.go +++ b/pkg/store/container/metadata.go @@ -61,6 +61,8 @@ type Metadata struct { // StopSignal is the system call signal that will be sent to the container to exit. // TODO(random-liu): Add integration test for stop signal. StopSignal string + // ProcessLabel is the SELinux process label for the container + ProcessLabel string } // MarshalJSON encodes Metadata into bytes in json format. diff --git a/pkg/store/label/label.go b/pkg/store/label/label.go new file mode 100644 index 000000000..c8c5ff924 --- /dev/null +++ b/pkg/store/label/label.go @@ -0,0 +1,90 @@ +/* + 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 label + +import ( + "sync" + + "github.com/opencontainers/selinux/go-selinux" +) + +type Store struct { + sync.Mutex + levels map[string]int + Releaser func(string) + Reserver func(string) +} + +func NewStore() *Store { + return &Store{ + levels: map[string]int{}, + Releaser: selinux.ReleaseLabel, + Reserver: selinux.ReserveLabel, + } +} + +func (s *Store) Reserve(label string) error { + s.Lock() + defer s.Unlock() + + context, err := selinux.NewContext(label) + if err != nil { + return err + } + + level := context["level"] + // no reason to count empty + if level == "" { + return nil + } + + if _, ok := s.levels[level]; !ok { + s.Reserver(label) + } + + s.levels[level]++ + return nil +} + +func (s *Store) Release(label string) { + s.Lock() + defer s.Unlock() + + context, err := selinux.NewContext(label) + if err != nil { + return + } + + level := context["level"] + if level == "" { + return + } + + count, ok := s.levels[level] + if !ok { + return + } + switch { + case count == 1: + s.Releaser(label) + delete(s.levels, level) + case count < 1: + delete(s.levels, level) + case count > 1: + s.levels[level] = count - 1 + } +} diff --git a/pkg/store/label/label_test.go b/pkg/store/label/label_test.go new file mode 100644 index 000000000..cc2c214bf --- /dev/null +++ b/pkg/store/label/label_test.go @@ -0,0 +1,116 @@ +/* + 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 label + +import ( + "testing" + + "github.com/opencontainers/selinux/go-selinux" + "github.com/stretchr/testify/assert" +) + +func TestAddThenRemove(t *testing.T) { + if !selinux.GetEnabled() { + t.Skip("selinux is not enabled") + } + + assert := assert.New(t) + store := NewStore() + releaseCount := 0 + reserveCount := 0 + store.Releaser = func(label string) { + assert.Contains(label, ":c1,c2") + releaseCount++ + assert.Equal(1, releaseCount) + } + store.Reserver = func(label string) { + assert.Contains(label, ":c1,c2") + reserveCount++ + assert.Equal(1, reserveCount) + } + + t.Log("should count to two level") + assert.NoError(store.Reserve("junk:junk:junk:c1,c2")) + assert.NoError(store.Reserve("junk2:junk2:junk2:c1,c2")) + + t.Log("should have one item") + assert.Equal(1, len(store.levels)) + + t.Log("c1,c2 count should be 2") + assert.Equal(2, store.levels["c1,c2"]) + + store.Release("junk:junk:junk:c1,c2") + store.Release("junk2:junk2:junk2:c1,c2") + + t.Log("should have 0 items") + assert.Equal(0, len(store.levels)) + + t.Log("should have reserved") + assert.Equal(1, reserveCount) + + t.Log("should have released") + assert.Equal(1, releaseCount) +} + +func TestJunkData(t *testing.T) { + if !selinux.GetEnabled() { + t.Skip("selinux is not enabled") + } + + assert := assert.New(t) + store := NewStore() + releaseCount := 0 + store.Releaser = func(label string) { + releaseCount++ + } + reserveCount := 0 + store.Reserver = func(label string) { + reserveCount++ + } + + t.Log("should ignore empty label") + assert.NoError(store.Reserve("")) + assert.Equal(0, len(store.levels)) + store.Release("") + assert.Equal(0, len(store.levels)) + assert.Equal(0, releaseCount) + assert.Equal(0, reserveCount) + + t.Log("should fail on bad label") + assert.Error(store.Reserve("junkjunkjunkc1c2")) + assert.Equal(0, len(store.levels)) + store.Release("junkjunkjunkc1c2") + assert.Equal(0, len(store.levels)) + assert.Equal(0, releaseCount) + assert.Equal(0, reserveCount) + + t.Log("should not release unknown label") + store.Release("junk2:junk2:junk2:c1,c2") + assert.Equal(0, len(store.levels)) + assert.Equal(0, releaseCount) + assert.Equal(0, reserveCount) + + t.Log("should release once even if too many deletes") + assert.NoError(store.Reserve("junk2:junk2:junk2:c1,c2")) + assert.Equal(1, len(store.levels)) + assert.Equal(1, store.levels["c1,c2"]) + store.Release("junk2:junk2:junk2:c1,c2") + store.Release("junk2:junk2:junk2:c1,c2") + assert.Equal(0, len(store.levels)) + assert.Equal(1, releaseCount) + assert.Equal(1, reserveCount) +} diff --git a/pkg/store/sandbox/metadata.go b/pkg/store/sandbox/metadata.go index 399cf2457..eb3aa8e83 100644 --- a/pkg/store/sandbox/metadata.go +++ b/pkg/store/sandbox/metadata.go @@ -61,6 +61,8 @@ type Metadata struct { RuntimeHandler string // CNIresult resulting configuration for attached network namespace interfaces CNIResult *cni.CNIResult + // ProcessLabel is the SELinux process label for the container + ProcessLabel string } // MarshalJSON encodes Metadata into bytes in json format. diff --git a/pkg/store/sandbox/sandbox.go b/pkg/store/sandbox/sandbox.go index 2e809cad2..223e88369 100644 --- a/pkg/store/sandbox/sandbox.go +++ b/pkg/store/sandbox/sandbox.go @@ -20,6 +20,7 @@ import ( "sync" "github.com/containerd/containerd" + "github.com/containerd/cri/pkg/store/label" "github.com/docker/docker/pkg/truncindex" "github.com/containerd/cri/pkg/netns" @@ -62,13 +63,15 @@ type Store struct { lock sync.RWMutex sandboxes map[string]Sandbox idIndex *truncindex.TruncIndex + labels *label.Store } // NewStore creates a sandbox store. -func NewStore() *Store { +func NewStore(labels *label.Store) *Store { return &Store{ sandboxes: make(map[string]Sandbox), idIndex: truncindex.NewTruncIndex([]string{}), + labels: labels, } } @@ -79,6 +82,9 @@ func (s *Store) Add(sb Sandbox) error { if _, ok := s.sandboxes[sb.ID]; ok { return store.ErrAlreadyExist } + if err := s.labels.Reserve(sb.ProcessLabel); err != nil { + return err + } if err := s.idIndex.Add(sb.ID); err != nil { return err } @@ -125,6 +131,7 @@ func (s *Store) Delete(id string) { // So we need to return if there are error. return } + s.labels.Release(s.sandboxes[id].ProcessLabel) s.idIndex.Delete(id) // nolint: errcheck delete(s.sandboxes, id) } diff --git a/pkg/store/sandbox/sandbox_test.go b/pkg/store/sandbox/sandbox_test.go index 5ef3316e8..4c922eeb8 100644 --- a/pkg/store/sandbox/sandbox_test.go +++ b/pkg/store/sandbox/sandbox_test.go @@ -19,6 +19,7 @@ package sandbox import ( "testing" + "github.com/containerd/cri/pkg/store/label" assertlib "github.com/stretchr/testify/assert" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" @@ -109,7 +110,7 @@ func TestSandboxStore(t *testing.T) { Status{State: StateUnknown}, ) assert := assertlib.New(t) - s := NewStore() + s := NewStore(label.NewStore()) t.Logf("should be able to add sandbox") for _, sb := range sandboxes {