Skip to content

Commit 5b46e40

Browse files
authored
Merge pull request from GHSA-c2h3-6mxw-7mvq
[release/1.4] v1 & v2 runtimes: reduce permissions for bundle dir
2 parents 8a3cfaf + adc279b commit 5b46e40

File tree

10 files changed

+539
-5
lines changed

10 files changed

+539
-5
lines changed

Diff for: releases/v1.4.11.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# commit to be tagged for new release
2+
commit = "HEAD"
3+
4+
project_name = "containerd"
5+
github_repo = "containerd/containerd"
6+
match_deps = "^github.com/(containerd/[a-zA-Z0-9-]+)$"
7+
8+
# previous release
9+
previous = "v1.4.10"
10+
11+
pre_release = false
12+
13+
preface = """\
14+
The eleventh patch release for containerd 1.4 is a security release to fix CVE-2021-41103.
15+
16+
### Notable Updates
17+
18+
* **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)
19+
20+
See the changelog for complete list of changes"""

Diff for: runtime/v1/linux/bundle.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package linux
2121
import (
2222
"context"
2323
"crypto/sha256"
24+
"encoding/json"
2425
"fmt"
2526
"io/ioutil"
2627
"os"
@@ -30,6 +31,7 @@ import (
3031
"github.com/containerd/containerd/runtime/linux/runctypes"
3132
"github.com/containerd/containerd/runtime/v1/shim"
3233
"github.com/containerd/containerd/runtime/v1/shim/client"
34+
"github.com/opencontainers/runtime-spec/specs-go"
3335
"github.com/pkg/errors"
3436
)
3537

@@ -48,14 +50,17 @@ func newBundle(id, path, workDir string, spec []byte) (b *bundle, err error) {
4850
return nil, err
4951
}
5052
path = filepath.Join(path, id)
51-
if err := os.Mkdir(path, 0711); err != nil {
53+
if err := os.Mkdir(path, 0700); err != nil {
5254
return nil, err
5355
}
5456
defer func() {
5557
if err != nil {
5658
os.RemoveAll(path)
5759
}
5860
}()
61+
if err := prepareBundleDirectoryPermissions(path, spec); err != nil {
62+
return nil, err
63+
}
5964
workDir = filepath.Join(workDir, id)
6065
if err := os.MkdirAll(workDir, 0711); err != nil {
6166
return nil, err
@@ -77,6 +82,55 @@ func newBundle(id, path, workDir string, spec []byte) (b *bundle, err error) {
7782
}, err
7883
}
7984

85+
// prepareBundleDirectoryPermissions prepares the permissions of the bundle
86+
// directory. When user namespaces are enabled, the permissions are modified
87+
// to allow the remapped root GID to access the bundle.
88+
func prepareBundleDirectoryPermissions(path string, spec []byte) error {
89+
gid, err := remappedGID(spec)
90+
if err != nil {
91+
return err
92+
}
93+
if gid == 0 {
94+
return nil
95+
}
96+
if err := os.Chown(path, -1, int(gid)); err != nil {
97+
return err
98+
}
99+
return os.Chmod(path, 0710)
100+
}
101+
102+
// ociSpecUserNS is a subset of specs.Spec used to reduce garbage during
103+
// unmarshal.
104+
type ociSpecUserNS struct {
105+
Linux *linuxSpecUserNS
106+
}
107+
108+
// linuxSpecUserNS is a subset of specs.Linux used to reduce garbage during
109+
// unmarshal.
110+
type linuxSpecUserNS struct {
111+
GIDMappings []specs.LinuxIDMapping
112+
}
113+
114+
// remappedGID reads the remapped GID 0 from the OCI spec, if it exists. If
115+
// there is no remapping, remappedGID returns 0. If the spec cannot be parsed,
116+
// remappedGID returns an error.
117+
func remappedGID(spec []byte) (uint32, error) {
118+
var ociSpec ociSpecUserNS
119+
err := json.Unmarshal(spec, &ociSpec)
120+
if err != nil {
121+
return 0, err
122+
}
123+
if ociSpec.Linux == nil || len(ociSpec.Linux.GIDMappings) == 0 {
124+
return 0, nil
125+
}
126+
for _, mapping := range ociSpec.Linux.GIDMappings {
127+
if mapping.ContainerID == 0 {
128+
return mapping.HostID, nil
129+
}
130+
}
131+
return 0, nil
132+
}
133+
80134
type bundle struct {
81135
id string
82136
path string

Diff for: runtime/v1/linux/bundle_test.go

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//go:build linux
2+
// +build linux
3+
4+
/*
5+
Copyright The containerd Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package linux
21+
22+
import (
23+
"encoding/json"
24+
"fmt"
25+
"io/ioutil"
26+
"os"
27+
"path/filepath"
28+
"strconv"
29+
"syscall"
30+
"testing"
31+
32+
"github.com/containerd/containerd/oci"
33+
"github.com/containerd/continuity/testutil"
34+
"github.com/opencontainers/runtime-spec/specs-go"
35+
)
36+
37+
func TestNewBundle(t *testing.T) {
38+
testutil.RequiresRoot(t)
39+
tests := []struct {
40+
userns bool
41+
}{{
42+
userns: false,
43+
}, {
44+
userns: true,
45+
}}
46+
const usernsGID = 4200
47+
48+
for i, tc := range tests {
49+
t.Run(strconv.Itoa(i), func(t *testing.T) {
50+
dir, err := ioutil.TempDir("", "test-new-bundle")
51+
if err != nil {
52+
t.Fatal("failed to create test directory", err)
53+
}
54+
defer os.RemoveAll(dir)
55+
work := filepath.Join(dir, "work")
56+
state := filepath.Join(dir, "state")
57+
id := fmt.Sprintf("new-bundle-%d", i)
58+
spec := oci.Spec{}
59+
if tc.userns {
60+
spec.Linux = &specs.Linux{
61+
GIDMappings: []specs.LinuxIDMapping{{ContainerID: 0, HostID: usernsGID}},
62+
}
63+
}
64+
specBytes, err := json.Marshal(&spec)
65+
if err != nil {
66+
t.Fatal("failed to marshal spec", err)
67+
}
68+
69+
b, err := newBundle(id, work, state, specBytes)
70+
if err != nil {
71+
t.Fatal("newBundle should succeed", err)
72+
}
73+
if b == nil {
74+
t.Fatal("bundle should not be nil")
75+
}
76+
77+
fi, err := os.Stat(b.path)
78+
if err != nil {
79+
t.Error("should be able to stat bundle path", err)
80+
}
81+
if tc.userns {
82+
if fi.Mode() != os.ModeDir|0710 {
83+
t.Error("bundle path should be a directory with perm 0710")
84+
}
85+
} else {
86+
if fi.Mode() != os.ModeDir|0700 {
87+
t.Error("bundle path should be a directory with perm 0700")
88+
}
89+
}
90+
stat, ok := fi.Sys().(*syscall.Stat_t)
91+
if !ok {
92+
t.Fatal("should assert to *syscall.Stat_t")
93+
}
94+
expectedGID := uint32(0)
95+
if tc.userns {
96+
expectedGID = usernsGID
97+
}
98+
if stat.Gid != expectedGID {
99+
t.Error("gid should match", expectedGID, stat.Gid)
100+
}
101+
})
102+
}
103+
}
104+
105+
func TestRemappedGID(t *testing.T) {
106+
tests := []struct {
107+
spec oci.Spec
108+
gid uint32
109+
}{{
110+
// empty spec
111+
spec: oci.Spec{},
112+
gid: 0,
113+
}, {
114+
// empty Linux section
115+
spec: oci.Spec{
116+
Linux: &specs.Linux{},
117+
},
118+
gid: 0,
119+
}, {
120+
// empty ID mappings
121+
spec: oci.Spec{
122+
Linux: &specs.Linux{
123+
GIDMappings: make([]specs.LinuxIDMapping, 0),
124+
},
125+
},
126+
gid: 0,
127+
}, {
128+
// valid ID mapping
129+
spec: oci.Spec{
130+
Linux: &specs.Linux{
131+
GIDMappings: []specs.LinuxIDMapping{{
132+
ContainerID: 0,
133+
HostID: 1000,
134+
}},
135+
},
136+
},
137+
gid: 1000,
138+
}, {
139+
// missing ID mapping
140+
spec: oci.Spec{
141+
Linux: &specs.Linux{
142+
GIDMappings: []specs.LinuxIDMapping{{
143+
ContainerID: 100,
144+
HostID: 1000,
145+
}},
146+
},
147+
},
148+
gid: 0,
149+
}}
150+
151+
for i, tc := range tests {
152+
t.Run(strconv.Itoa(i), func(t *testing.T) {
153+
s, err := json.Marshal(tc.spec)
154+
if err != nil {
155+
t.Fatal("failed to marshal spec", err)
156+
}
157+
gid, err := remappedGID(s)
158+
if err != nil {
159+
t.Error("should unmarshal successfully", err)
160+
}
161+
if gid != tc.gid {
162+
t.Error("expected GID to match", tc.gid, gid)
163+
}
164+
})
165+
}
166+
}

Diff for: runtime/v2/bundle.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ func NewBundle(ctx context.Context, root, state, id string, spec []byte) (b *Bun
7272
if err := os.MkdirAll(filepath.Dir(b.Path), 0711); err != nil {
7373
return nil, err
7474
}
75-
if err := os.Mkdir(b.Path, 0711); err != nil {
75+
if err := os.Mkdir(b.Path, 0700); err != nil {
76+
return nil, err
77+
}
78+
if err := prepareBundleDirectoryPermissions(b.Path, spec); err != nil {
7679
return nil, err
7780
}
7881
paths = append(paths, b.Path)

Diff for: runtime/v2/bundle_default.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//go:build !linux
2+
// +build !linux
3+
4+
/*
5+
Copyright The containerd Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package v2
21+
22+
// prepareBundleDirectoryPermissions prepares the permissions of the bundle
23+
// directory according to the needs of the current platform.
24+
func prepareBundleDirectoryPermissions(path string, spec []byte) error { return nil }

Diff for: runtime/v2/bundle_linux.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v2
18+
19+
import (
20+
"encoding/json"
21+
"os"
22+
23+
"github.com/opencontainers/runtime-spec/specs-go"
24+
)
25+
26+
// prepareBundleDirectoryPermissions prepares the permissions of the bundle
27+
// directory according to the needs of the current platform.
28+
// On Linux when user namespaces are enabled, the permissions are modified to
29+
// allow the remapped root GID to access the bundle.
30+
func prepareBundleDirectoryPermissions(path string, spec []byte) error {
31+
gid, err := remappedGID(spec)
32+
if err != nil {
33+
return err
34+
}
35+
if gid == 0 {
36+
return nil
37+
}
38+
if err := os.Chown(path, -1, int(gid)); err != nil {
39+
return err
40+
}
41+
return os.Chmod(path, 0710)
42+
}
43+
44+
// ociSpecUserNS is a subset of specs.Spec used to reduce garbage during
45+
// unmarshal.
46+
type ociSpecUserNS struct {
47+
Linux *linuxSpecUserNS
48+
}
49+
50+
// linuxSpecUserNS is a subset of specs.Linux used to reduce garbage during
51+
// unmarshal.
52+
type linuxSpecUserNS struct {
53+
GIDMappings []specs.LinuxIDMapping
54+
}
55+
56+
// remappedGID reads the remapped GID 0 from the OCI spec, if it exists. If
57+
// there is no remapping, remappedGID returns 0. If the spec cannot be parsed,
58+
// remappedGID returns an error.
59+
func remappedGID(spec []byte) (uint32, error) {
60+
var ociSpec ociSpecUserNS
61+
err := json.Unmarshal(spec, &ociSpec)
62+
if err != nil {
63+
return 0, err
64+
}
65+
if ociSpec.Linux == nil || len(ociSpec.Linux.GIDMappings) == 0 {
66+
return 0, nil
67+
}
68+
for _, mapping := range ociSpec.Linux.GIDMappings {
69+
if mapping.ContainerID == 0 {
70+
return mapping.HostID, nil
71+
}
72+
}
73+
return 0, nil
74+
}

0 commit comments

Comments
 (0)