Skip to content

Commit 8686ede

Browse files
authored
Merge pull request from GHSA-c2h3-6mxw-7mvq
[release/1.5] v1 & v2 runtimes: reduce permissions for bundle dir
2 parents 1a1b383 + bc2f973 commit 8686ede

10 files changed

Lines changed: 496 additions & 5 deletions

File tree

releases/v1.5.7.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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.5.6"
10+
11+
pre_release = false
12+
13+
preface = """\
14+
The seventh patch release for containerd 1.5 is a security release to fix CVE-2021-41103.
15+
16+
### Notable Updates
17+
* **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)
18+
19+
See the changelog for complete list of changes"""

runtime/v1/linux/bundle.go

Lines changed: 55 additions & 1 deletion
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

runtime/v1/linux/bundle_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
"github.com/stretchr/testify/assert"
36+
"github.com/stretchr/testify/require"
37+
)
38+
39+
func TestNewBundle(t *testing.T) {
40+
testutil.RequiresRoot(t)
41+
tests := []struct {
42+
userns bool
43+
}{{
44+
userns: false,
45+
}, {
46+
userns: true,
47+
}}
48+
const usernsGID = 4200
49+
50+
for i, tc := range tests {
51+
t.Run(strconv.Itoa(i), func(t *testing.T) {
52+
dir, err := ioutil.TempDir("", "test-new-bundle")
53+
require.NoError(t, err, "failed to create test directory")
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+
require.NoError(t, err, "failed to marshal spec")
66+
67+
b, err := newBundle(id, work, state, specBytes)
68+
require.NoError(t, err, "newBundle should succeed")
69+
require.NotNil(t, b, "bundle should not be nil")
70+
71+
fi, err := os.Stat(b.path)
72+
assert.NoError(t, err, "should be able to stat bundle path")
73+
if tc.userns {
74+
assert.Equal(t, os.ModeDir|0710, fi.Mode(), "bundle path should be a directory with perm 0710")
75+
} else {
76+
assert.Equal(t, os.ModeDir|0700, fi.Mode(), "bundle path should be a directory with perm 0700")
77+
}
78+
stat, ok := fi.Sys().(*syscall.Stat_t)
79+
require.True(t, ok, "should assert to *syscall.Stat_t")
80+
expectedGID := uint32(0)
81+
if tc.userns {
82+
expectedGID = usernsGID
83+
}
84+
assert.Equal(t, expectedGID, stat.Gid, "gid should match")
85+
86+
})
87+
}
88+
}
89+
90+
func TestRemappedGID(t *testing.T) {
91+
tests := []struct {
92+
spec oci.Spec
93+
gid uint32
94+
}{{
95+
// empty spec
96+
spec: oci.Spec{},
97+
gid: 0,
98+
}, {
99+
// empty Linux section
100+
spec: oci.Spec{
101+
Linux: &specs.Linux{},
102+
},
103+
gid: 0,
104+
}, {
105+
// empty ID mappings
106+
spec: oci.Spec{
107+
Linux: &specs.Linux{
108+
GIDMappings: make([]specs.LinuxIDMapping, 0),
109+
},
110+
},
111+
gid: 0,
112+
}, {
113+
// valid ID mapping
114+
spec: oci.Spec{
115+
Linux: &specs.Linux{
116+
GIDMappings: []specs.LinuxIDMapping{{
117+
ContainerID: 0,
118+
HostID: 1000,
119+
}},
120+
},
121+
},
122+
gid: 1000,
123+
}, {
124+
// missing ID mapping
125+
spec: oci.Spec{
126+
Linux: &specs.Linux{
127+
GIDMappings: []specs.LinuxIDMapping{{
128+
ContainerID: 100,
129+
HostID: 1000,
130+
}},
131+
},
132+
},
133+
gid: 0,
134+
}}
135+
136+
for i, tc := range tests {
137+
t.Run(strconv.Itoa(i), func(t *testing.T) {
138+
s, err := json.Marshal(tc.spec)
139+
require.NoError(t, err, "failed to marshal spec")
140+
gid, err := remappedGID(s)
141+
assert.NoError(t, err, "should unmarshal successfully")
142+
assert.Equal(t, tc.gid, gid, "expected GID to match")
143+
})
144+
}
145+
}

runtime/v2/bundle.go

Lines changed: 4 additions & 1 deletion
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)

runtime/v2/bundle_default.go

Lines changed: 24 additions & 0 deletions
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 }

runtime/v2/bundle_linux.go

Lines changed: 74 additions & 0 deletions
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)