VYPR
Moderate severityNVD Advisory· Published Oct 4, 2021· Updated Aug 4, 2024

Insufficiently restricted permissions on plugin directories

CVE-2021-41103

Description

containerd is an open source container runtime with an emphasis on simplicity, robustness and portability. A bug was found in containerd where container root directories and some plugins had insufficiently restricted permissions, allowing otherwise unprivileged Linux users to traverse directory contents and execute programs. When containers included executable programs with extended permission bits (such as setuid), unprivileged Linux users could discover and execute those programs. When the UID of an unprivileged Linux user on the host collided with the file owner or group inside a container, the unprivileged Linux user on the host could discover, read, and modify those files. This vulnerability has been fixed in containerd 1.4.11 and containerd 1.5.7. Users should update to these version when they are released and may restart containers or update directory permissions to mitigate the vulnerability. Users unable to update should limit access to the host to trusted users. Update directory permission on container bundles directories.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/containerd/containerdGo
< 1.4.111.4.11
github.com/containerd/containerdGo
>= 1.5.0, < 1.5.71.5.7

Affected products

1

Patches

1
5b46e404f6b9

Merge pull request from GHSA-c2h3-6mxw-7mvq

https://github.com/containerd/containerdDerek McGowanOct 4, 2021via ghsa
10 files changed · +539 5
  • releases/v1.4.11.toml+20 0 added
    @@ -0,0 +1,20 @@
    +# commit to be tagged for new release
    +commit = "HEAD"
    +
    +project_name = "containerd"
    +github_repo = "containerd/containerd"
    +match_deps = "^github.com/(containerd/[a-zA-Z0-9-]+)$"
    +
    +# previous release
    +previous = "v1.4.10"
    +
    +pre_release = false
    +
    +preface = """\
    +The eleventh patch release for containerd 1.4 is a security release to fix CVE-2021-41103.
    +
    +### Notable Updates
    +
    +* **Fix insufficiently restricted permissions on container root and plugin directories** [GHSA-c2h3-6mxw-7mvq](https://github.com/containerd/containerd/security/advisories/GHSA-c2h3-6mxw-7mvq)
    +
    +See the changelog for complete list of changes"""
    
  • runtime/v1/linux/bundle.go+55 1 modified
    @@ -21,6 +21,7 @@ package linux
     import (
     	"context"
     	"crypto/sha256"
    +	"encoding/json"
     	"fmt"
     	"io/ioutil"
     	"os"
    @@ -30,6 +31,7 @@ import (
     	"github.com/containerd/containerd/runtime/linux/runctypes"
     	"github.com/containerd/containerd/runtime/v1/shim"
     	"github.com/containerd/containerd/runtime/v1/shim/client"
    +	"github.com/opencontainers/runtime-spec/specs-go"
     	"github.com/pkg/errors"
     )
     
    @@ -48,14 +50,17 @@ func newBundle(id, path, workDir string, spec []byte) (b *bundle, err error) {
     		return nil, err
     	}
     	path = filepath.Join(path, id)
    -	if err := os.Mkdir(path, 0711); err != nil {
    +	if err := os.Mkdir(path, 0700); err != nil {
     		return nil, err
     	}
     	defer func() {
     		if err != nil {
     			os.RemoveAll(path)
     		}
     	}()
    +	if err := prepareBundleDirectoryPermissions(path, spec); err != nil {
    +		return nil, err
    +	}
     	workDir = filepath.Join(workDir, id)
     	if err := os.MkdirAll(workDir, 0711); err != nil {
     		return nil, err
    @@ -77,6 +82,55 @@ func newBundle(id, path, workDir string, spec []byte) (b *bundle, err error) {
     	}, err
     }
     
    +// prepareBundleDirectoryPermissions prepares the permissions of the bundle
    +// directory. When user namespaces are enabled, the permissions are modified
    +// to allow the remapped root GID to access the bundle.
    +func prepareBundleDirectoryPermissions(path string, spec []byte) error {
    +	gid, err := remappedGID(spec)
    +	if err != nil {
    +		return err
    +	}
    +	if gid == 0 {
    +		return nil
    +	}
    +	if err := os.Chown(path, -1, int(gid)); err != nil {
    +		return err
    +	}
    +	return os.Chmod(path, 0710)
    +}
    +
    +// ociSpecUserNS is a subset of specs.Spec used to reduce garbage during
    +// unmarshal.
    +type ociSpecUserNS struct {
    +	Linux *linuxSpecUserNS
    +}
    +
    +// linuxSpecUserNS is a subset of specs.Linux used to reduce garbage during
    +// unmarshal.
    +type linuxSpecUserNS struct {
    +	GIDMappings []specs.LinuxIDMapping
    +}
    +
    +// remappedGID reads the remapped GID 0 from the OCI spec, if it exists. If
    +// there is no remapping, remappedGID returns 0. If the spec cannot be parsed,
    +// remappedGID returns an error.
    +func remappedGID(spec []byte) (uint32, error) {
    +	var ociSpec ociSpecUserNS
    +	err := json.Unmarshal(spec, &ociSpec)
    +	if err != nil {
    +		return 0, err
    +	}
    +	if ociSpec.Linux == nil || len(ociSpec.Linux.GIDMappings) == 0 {
    +		return 0, nil
    +	}
    +	for _, mapping := range ociSpec.Linux.GIDMappings {
    +		if mapping.ContainerID == 0 {
    +			return mapping.HostID, nil
    +		}
    +	}
    +	return 0, nil
    +}
    +
     type bundle struct {
     	id      string
     	path    string
    
  • runtime/v1/linux/bundle_test.go+166 0 added
    @@ -0,0 +1,166 @@
    +//go:build linux
    +// +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 linux
    +
    +import (
    +	"encoding/json"
    +	"fmt"
    +	"io/ioutil"
    +	"os"
    +	"path/filepath"
    +	"strconv"
    +	"syscall"
    +	"testing"
    +
    +	"github.com/containerd/containerd/oci"
    +	"github.com/containerd/continuity/testutil"
    +	"github.com/opencontainers/runtime-spec/specs-go"
    +)
    +
    +func TestNewBundle(t *testing.T) {
    +	testutil.RequiresRoot(t)
    +	tests := []struct {
    +		userns bool
    +	}{{
    +		userns: false,
    +	}, {
    +		userns: true,
    +	}}
    +	const usernsGID = 4200
    +
    +	for i, tc := range tests {
    +		t.Run(strconv.Itoa(i), func(t *testing.T) {
    +			dir, err := ioutil.TempDir("", "test-new-bundle")
    +			if err != nil {
    +				t.Fatal("failed to create test directory", err)
    +			}
    +			defer os.RemoveAll(dir)
    +			work := filepath.Join(dir, "work")
    +			state := filepath.Join(dir, "state")
    +			id := fmt.Sprintf("new-bundle-%d", i)
    +			spec := oci.Spec{}
    +			if tc.userns {
    +				spec.Linux = &specs.Linux{
    +					GIDMappings: []specs.LinuxIDMapping{{ContainerID: 0, HostID: usernsGID}},
    +				}
    +			}
    +			specBytes, err := json.Marshal(&spec)
    +			if err != nil {
    +				t.Fatal("failed to marshal spec", err)
    +			}
    +
    +			b, err := newBundle(id, work, state, specBytes)
    +			if err != nil {
    +				t.Fatal("newBundle should succeed", err)
    +			}
    +			if b == nil {
    +				t.Fatal("bundle should not be nil")
    +			}
    +
    +			fi, err := os.Stat(b.path)
    +			if err != nil {
    +				t.Error("should be able to stat bundle path", err)
    +			}
    +			if tc.userns {
    +				if fi.Mode() != os.ModeDir|0710 {
    +					t.Error("bundle path should be a directory with perm 0710")
    +				}
    +			} else {
    +				if fi.Mode() != os.ModeDir|0700 {
    +					t.Error("bundle path should be a directory with perm 0700")
    +				}
    +			}
    +			stat, ok := fi.Sys().(*syscall.Stat_t)
    +			if !ok {
    +				t.Fatal("should assert to *syscall.Stat_t")
    +			}
    +			expectedGID := uint32(0)
    +			if tc.userns {
    +				expectedGID = usernsGID
    +			}
    +			if stat.Gid != expectedGID {
    +				t.Error("gid should match", expectedGID, stat.Gid)
    +			}
    +		})
    +	}
    +}
    +
    +func TestRemappedGID(t *testing.T) {
    +	tests := []struct {
    +		spec oci.Spec
    +		gid  uint32
    +	}{{
    +		// empty spec
    +		spec: oci.Spec{},
    +		gid:  0,
    +	}, {
    +		// empty Linux section
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{},
    +		},
    +		gid: 0,
    +	}, {
    +		// empty ID mappings
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{
    +				GIDMappings: make([]specs.LinuxIDMapping, 0),
    +			},
    +		},
    +		gid: 0,
    +	}, {
    +		// valid ID mapping
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{
    +				GIDMappings: []specs.LinuxIDMapping{{
    +					ContainerID: 0,
    +					HostID:      1000,
    +				}},
    +			},
    +		},
    +		gid: 1000,
    +	}, {
    +		// missing ID mapping
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{
    +				GIDMappings: []specs.LinuxIDMapping{{
    +					ContainerID: 100,
    +					HostID:      1000,
    +				}},
    +			},
    +		},
    +		gid: 0,
    +	}}
    +
    +	for i, tc := range tests {
    +		t.Run(strconv.Itoa(i), func(t *testing.T) {
    +			s, err := json.Marshal(tc.spec)
    +			if err != nil {
    +				t.Fatal("failed to marshal spec", err)
    +			}
    +			gid, err := remappedGID(s)
    +			if err != nil {
    +				t.Error("should unmarshal successfully", err)
    +			}
    +			if gid != tc.gid {
    +				t.Error("expected GID to match", tc.gid, gid)
    +			}
    +		})
    +	}
    +}
    
  • runtime/v2/bundle_default.go+24 0 added
    @@ -0,0 +1,24 @@
    +//go:build !linux
    +// +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 v2
    +
    +// prepareBundleDirectoryPermissions prepares the permissions of the bundle
    +// directory according to the needs of the current platform.
    +func prepareBundleDirectoryPermissions(path string, spec []byte) error { return nil }
    
  • runtime/v2/bundle.go+4 1 modified
    @@ -72,7 +72,10 @@ func NewBundle(ctx context.Context, root, state, id string, spec []byte) (b *Bun
     	if err := os.MkdirAll(filepath.Dir(b.Path), 0711); err != nil {
     		return nil, err
     	}
    -	if err := os.Mkdir(b.Path, 0711); err != nil {
    +	if err := os.Mkdir(b.Path, 0700); err != nil {
    +		return nil, err
    +	}
    +	if err := prepareBundleDirectoryPermissions(b.Path, spec); err != nil {
     		return nil, err
     	}
     	paths = append(paths, b.Path)
    
  • runtime/v2/bundle_linux.go+74 0 added
    @@ -0,0 +1,74 @@
    +/*
    +   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 v2
    +
    +import (
    +	"encoding/json"
    +	"os"
    +
    +	"github.com/opencontainers/runtime-spec/specs-go"
    +)
    +
    +// prepareBundleDirectoryPermissions prepares the permissions of the bundle
    +// directory according to the needs of the current platform.
    +// On Linux when user namespaces are enabled, the permissions are modified to
    +// allow the remapped root GID to access the bundle.
    +func prepareBundleDirectoryPermissions(path string, spec []byte) error {
    +	gid, err := remappedGID(spec)
    +	if err != nil {
    +		return err
    +	}
    +	if gid == 0 {
    +		return nil
    +	}
    +	if err := os.Chown(path, -1, int(gid)); err != nil {
    +		return err
    +	}
    +	return os.Chmod(path, 0710)
    +}
    +
    +// ociSpecUserNS is a subset of specs.Spec used to reduce garbage during
    +// unmarshal.
    +type ociSpecUserNS struct {
    +	Linux *linuxSpecUserNS
    +}
    +
    +// linuxSpecUserNS is a subset of specs.Linux used to reduce garbage during
    +// unmarshal.
    +type linuxSpecUserNS struct {
    +	GIDMappings []specs.LinuxIDMapping
    +}
    +
    +// remappedGID reads the remapped GID 0 from the OCI spec, if it exists. If
    +// there is no remapping, remappedGID returns 0. If the spec cannot be parsed,
    +// remappedGID returns an error.
    +func remappedGID(spec []byte) (uint32, error) {
    +	var ociSpec ociSpecUserNS
    +	err := json.Unmarshal(spec, &ociSpec)
    +	if err != nil {
    +		return 0, err
    +	}
    +	if ociSpec.Linux == nil || len(ociSpec.Linux.GIDMappings) == 0 {
    +		return 0, nil
    +	}
    +	for _, mapping := range ociSpec.Linux.GIDMappings {
    +		if mapping.ContainerID == 0 {
    +			return mapping.HostID, nil
    +		}
    +	}
    +	return 0, nil
    +}
    
  • runtime/v2/bundle_linux_test.go+166 0 added
    @@ -0,0 +1,166 @@
    +/*
    +   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 v2
    +
    +import (
    +	"context"
    +	"encoding/json"
    +	"fmt"
    +	"io/ioutil"
    +	"os"
    +	"path/filepath"
    +	"strconv"
    +	"syscall"
    +	"testing"
    +
    +	"github.com/containerd/containerd/namespaces"
    +	"github.com/containerd/containerd/oci"
    +	"github.com/containerd/containerd/pkg/testutil"
    +	"github.com/opencontainers/runtime-spec/specs-go"
    +)
    +
    +func TestNewBundle(t *testing.T) {
    +	testutil.RequiresRoot(t)
    +	tests := []struct {
    +		userns bool
    +	}{{
    +		userns: false,
    +	}, {
    +		userns: true,
    +	}}
    +	const usernsGID = 4200
    +
    +	for i, tc := range tests {
    +		t.Run(strconv.Itoa(i), func(t *testing.T) {
    +			dir, err := ioutil.TempDir("", "test-new-bundle")
    +			if err != nil {
    +				t.Fatal("failed to create test directory", err)
    +			}
    +			defer os.RemoveAll(dir)
    +			work := filepath.Join(dir, "work")
    +			state := filepath.Join(dir, "state")
    +			id := fmt.Sprintf("new-bundle-%d", i)
    +			spec := oci.Spec{}
    +			if tc.userns {
    +				spec.Linux = &specs.Linux{
    +					GIDMappings: []specs.LinuxIDMapping{{ContainerID: 0, HostID: usernsGID}},
    +				}
    +			}
    +			specBytes, err := json.Marshal(&spec)
    +			if err != nil {
    +				t.Fatal("failed to marshal spec", err)
    +			}
    +
    +			ctx := namespaces.WithNamespace(context.TODO(), namespaces.Default)
    +			b, err := NewBundle(ctx, work, state, id, specBytes)
    +			if err != nil {
    +				t.Fatal("NewBundle should succeed", err)
    +			}
    +			if b == nil {
    +				t.Fatal("bundle should not be nil")
    +			}
    +
    +			fi, err := os.Stat(b.Path)
    +			if err != nil {
    +				t.Error("should be able to stat bundle path", err)
    +			}
    +			if tc.userns {
    +				if fi.Mode() != os.ModeDir|0710 {
    +					t.Error("bundle path should be a directory with perm 0710")
    +				}
    +			} else {
    +				if fi.Mode() != os.ModeDir|0700 {
    +					t.Error("bundle path should be a directory with perm 0700")
    +				}
    +			}
    +			stat, ok := fi.Sys().(*syscall.Stat_t)
    +			if !ok {
    +				t.Fatal("should assert to *syscall.Stat_t")
    +			}
    +			expectedGID := uint32(0)
    +			if tc.userns {
    +				expectedGID = usernsGID
    +			}
    +			if expectedGID != stat.Gid {
    +				t.Error("gid should match", expectedGID, stat.Gid)
    +			}
    +		})
    +	}
    +}
    +
    +func TestRemappedGID(t *testing.T) {
    +	tests := []struct {
    +		spec oci.Spec
    +		gid  uint32
    +	}{{
    +		// empty spec
    +		spec: oci.Spec{},
    +		gid:  0,
    +	}, {
    +		// empty Linux section
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{},
    +		},
    +		gid: 0,
    +	}, {
    +		// empty ID mappings
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{
    +				GIDMappings: make([]specs.LinuxIDMapping, 0),
    +			},
    +		},
    +		gid: 0,
    +	}, {
    +		// valid ID mapping
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{
    +				GIDMappings: []specs.LinuxIDMapping{{
    +					ContainerID: 0,
    +					HostID:      1000,
    +				}},
    +			},
    +		},
    +		gid: 1000,
    +	}, {
    +		// missing ID mapping
    +		spec: oci.Spec{
    +			Linux: &specs.Linux{
    +				GIDMappings: []specs.LinuxIDMapping{{
    +					ContainerID: 100,
    +					HostID:      1000,
    +				}},
    +			},
    +		},
    +		gid: 0,
    +	}}
    +
    +	for i, tc := range tests {
    +		t.Run(strconv.Itoa(i), func(t *testing.T) {
    +			s, err := json.Marshal(tc.spec)
    +			if err != nil {
    +				t.Fatal("failed to marshal spec", err)
    +			}
    +			gid, err := remappedGID(s)
    +			if err != nil {
    +				t.Error("should unmarshal successfully", err)
    +			}
    +			if tc.gid != gid {
    +				t.Error("expected GID to match", tc.gid, gid)
    +			}
    +		})
    +	}
    +}
    
  • runtime/v2/bundle_test.go+23 0 added
    @@ -0,0 +1,23 @@
    +/*
    +   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 v2
    +
    +import (
    +	// When testutil is imported for one platform (bundle_linux_test.go) it
    +	// should be imported for all platforms.
    +	_ "github.com/containerd/containerd/pkg/testutil"
    +)
    
  • snapshots/btrfs/btrfs.go+6 2 modified
    @@ -63,11 +63,15 @@ type snapshotter struct {
     // root needs to be a mount point of btrfs.
     func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
     	// If directory does not exist, create it
    -	if _, err := os.Stat(root); err != nil {
    +	if st, err := os.Stat(root); err != nil {
     		if !os.IsNotExist(err) {
     			return nil, err
     		}
    -		if err := os.Mkdir(root, 0755); err != nil {
    +		if err := os.Mkdir(root, 0700); err != nil {
    +			return nil, err
    +		}
    +	} else if st.Mode()&os.ModePerm != 0700 {
    +		if err := os.Chmod(root, 0700); err != nil {
     			return nil, err
     		}
     	}
    
  • version/version.go+1 1 modified
    @@ -23,7 +23,7 @@ var (
     	Package = "github.com/containerd/containerd"
     
     	// Version holds the complete version number. Filled in at linking time.
    -	Version = "1.4.10+unknown"
    +	Version = "1.4.11+unknown"
     
     	// Revision is filled with the VCS (e.g. git) revision being used to build
     	// the program at linking time.
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

15

News mentions

0

No linked articles in our index yet.