Skip to content

Commit e8b1a9b

Browse files
authored
Merge pull request #13631 from samuelkarp/prepare-1.7.33
[release/1.7] Prepare release notes for v1.7.33
2 parents 0962898 + 7517e67 commit e8b1a9b

17 files changed

Lines changed: 421 additions & 22 deletions

container_opts.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ import (
2626
"github.com/containerd/containerd/content"
2727
"github.com/containerd/containerd/errdefs"
2828
"github.com/containerd/containerd/images"
29+
"github.com/containerd/containerd/labels"
2930
"github.com/containerd/containerd/namespaces"
3031
"github.com/containerd/containerd/oci"
3132
"github.com/containerd/containerd/protobuf"
3233
"github.com/containerd/containerd/snapshots"
34+
"github.com/containerd/log"
3335
"github.com/containerd/typeurl/v2"
3436
"github.com/opencontainers/image-spec/identity"
3537
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -114,6 +116,10 @@ func WithContainerLabels(labels map[string]string) NewContainerOpts {
114116
// The existing labels are cleared as this is expected to be the first
115117
// operation in setting up a container's labels. Use WithAdditionalContainerLabels
116118
// to add/overwrite the existing image config labels.
119+
//
120+
// Image config labels in the namespaces reserved for containerd
121+
// (containerd.io/) and the CRI plugin (io.cri-containerd) are not copied
122+
// to the container.
117123
func WithImageConfigLabels(image Image) NewContainerOpts {
118124
return func(ctx context.Context, _ *Client, c *containers.Container) error {
119125
ic, err := image.Config(ctx)
@@ -139,6 +145,15 @@ func WithImageConfigLabels(image Image) NewContainerOpts {
139145
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
140146
}
141147
c.Labels = config.Labels
148+
// Labels in the containerd.io/* namespace are interpreted by containerd
149+
// itself, and labels in the io.cri-containerd.* namespace are interpreted
150+
// by the CRI plugin, so they are not copied from untrusted image configs.
151+
for k := range c.Labels {
152+
if labels.IsReserved(k) {
153+
log.G(ctx).Warnf("skipping image label %q: the label namespace is reserved for containerd; possible malicious image attempting to alter containerd behavior", k)
154+
delete(c.Labels, k)
155+
}
156+
}
142157
return nil
143158
}
144159
}

container_opts_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 containerd
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"testing"
23+
24+
"github.com/containerd/containerd/containers"
25+
"github.com/containerd/containerd/content"
26+
"github.com/opencontainers/go-digest"
27+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
30+
)
31+
32+
// fakeImage implements the subset of Image used by WithImageConfigLabels:
33+
// Config returns a descriptor with the config blob inlined in Data, so the
34+
// content store is never consulted.
35+
type fakeImage struct {
36+
Image
37+
config ocispec.Descriptor
38+
}
39+
40+
func (i fakeImage) Config(context.Context) (ocispec.Descriptor, error) {
41+
return i.config, nil
42+
}
43+
44+
func (i fakeImage) ContentStore() content.Store {
45+
return nil
46+
}
47+
48+
func TestWithImageConfigLabels(t *testing.T) {
49+
blob, err := json.Marshal(ocispec.Image{
50+
Config: ocispec.ImageConfig{
51+
Labels: map[string]string{
52+
"foo": "bar",
53+
"containerd.io/restart.policy": "always",
54+
"io.cri-containerd.kind": "sandbox",
55+
},
56+
},
57+
})
58+
require.NoError(t, err)
59+
60+
img := fakeImage{
61+
config: ocispec.Descriptor{
62+
MediaType: ocispec.MediaTypeImageConfig,
63+
Digest: digest.FromBytes(blob),
64+
Size: int64(len(blob)),
65+
Data: blob,
66+
},
67+
}
68+
69+
var c containers.Container
70+
require.NoError(t, WithImageConfigLabels(img)(t.Context(), nil, &c))
71+
72+
// labels in the namespaces reserved for containerd and the CRI plugin
73+
// are not copied from the image config
74+
assert.Equal(t, map[string]string{"foo": "bar"}, c.Labels)
75+
}

labels/labels.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@
1616

1717
package labels
1818

19+
// ReservedPrefix is the prefix of the label namespace reserved for labels
20+
// defined and consumed by containerd itself. Labels in this namespace must
21+
// not be copied from untrusted sources such as image config labels. Use
22+
// IsReserved to check for such labels.
23+
const ReservedPrefix = "containerd.io/"
24+
25+
// CRIContainerdPrefix is the prefix of the label namespace reserved for
26+
// labels defined and consumed by containerd's CRI plugin. Labels in this
27+
// namespace must not be copied from untrusted sources such as image config
28+
// labels. Use IsReserved to check for such labels.
29+
const CRIContainerdPrefix = "io.cri-containerd"
30+
1931
// LabelUncompressed is added to compressed layer contents.
2032
// The value is digest of the uncompressed content.
2133
const LabelUncompressed = "containerd.io/uncompressed"

labels/validate.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package labels
1818

1919
import (
2020
"fmt"
21+
"strings"
2122

2223
"github.com/containerd/containerd/errdefs"
2324
)
@@ -39,3 +40,11 @@ func Validate(k, v string) error {
3940
}
4041
return nil
4142
}
43+
44+
// IsReserved returns true if the label key is in a namespace reserved for
45+
// containerd (ReservedPrefix) or its CRI plugin (CRIContainerdPrefix).
46+
// Reserved labels are interpreted by containerd and must not be copied from
47+
// untrusted sources such as image config labels.
48+
func IsReserved(k string) bool {
49+
return strings.HasPrefix(k, ReservedPrefix) || strings.HasPrefix(k, CRIContainerdPrefix)
50+
}

labels/validate_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ func TestInvalidLabels(t *testing.T) {
5353
}
5454
}
5555

56+
func TestIsReserved(t *testing.T) {
57+
for key, reserved := range map[string]bool{
58+
"containerd.io/": true,
59+
"containerd.io/restart.status": true,
60+
"containerd.io/gc.ref.content": true,
61+
"io.cri-containerd": true,
62+
"io.cri-containerd.kind": true,
63+
"io.cri-containerd.image": true,
64+
"io.cri-containerdfoo": true,
65+
"containerd.io": false,
66+
"io.containerd.something": false,
67+
"com.example.app": false,
68+
} {
69+
assert.Equal(t, reserved, IsReserved(key), "IsReserved(%q)", key)
70+
}
71+
}
72+
5673
func TestLongKey(t *testing.T) {
5774
key := strings.Repeat("s", keyMaxLen+1)
5875
value := strings.Repeat("v", maxSize-len(key))

oci/spec_opts.go

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"encoding/json"
2323
"errors"
2424
"fmt"
25+
"io"
2526
"math"
2627
"os"
2728
"path/filepath"
@@ -931,7 +932,12 @@ func WithAppendAdditionalGroups(groups ...string) SpecOpts {
931932
if err != nil {
932933
return err
933934
}
934-
ugroups, groupErr := user.ParseGroupFile(gpath)
935+
var ugroups []user.Group
936+
f, groupErr := openBoundedUserFile(gpath)
937+
if groupErr == nil {
938+
ugroups, groupErr = user.ParseGroup(f)
939+
f.Close()
940+
}
935941
if groupErr != nil && !os.IsNotExist(groupErr) {
936942
return groupErr
937943
}
@@ -1091,7 +1097,12 @@ func UserFromPath(root string, filter func(user.User) bool) (user.User, error) {
10911097
if err != nil {
10921098
return user.User{}, err
10931099
}
1094-
users, err := user.ParsePasswdFileFilter(ppath, filter)
1100+
f, err := openBoundedUserFile(ppath)
1101+
if err != nil {
1102+
return user.User{}, err
1103+
}
1104+
defer f.Close()
1105+
users, err := user.ParsePasswdFilter(f, filter)
10951106
if err != nil {
10961107
return user.User{}, err
10971108
}
@@ -1111,7 +1122,12 @@ func GIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err err
11111122
if err != nil {
11121123
return 0, err
11131124
}
1114-
groups, err := user.ParseGroupFileFilter(gpath, filter)
1125+
f, err := openBoundedUserFile(gpath)
1126+
if err != nil {
1127+
return 0, err
1128+
}
1129+
defer f.Close()
1130+
groups, err := user.ParseGroupFilter(f, filter)
11151131
if err != nil {
11161132
return 0, err
11171133
}
@@ -1127,7 +1143,12 @@ func getSupplementalGroupsFromPath(root string, filter func(user.Group) bool) ([
11271143
if err != nil {
11281144
return []uint32{}, err
11291145
}
1130-
groups, err := user.ParseGroupFileFilter(gpath, filter)
1146+
f, err := openBoundedUserFile(gpath)
1147+
if err != nil {
1148+
return []uint32{}, err
1149+
}
1150+
defer f.Close()
1151+
groups, err := user.ParseGroupFilter(f, filter)
11311152
if err != nil {
11321153
return []uint32{}, err
11331154
}
@@ -1671,3 +1692,58 @@ func WithWindowsNetworkNamespace(ns string) SpecOpts {
16711692
return nil
16721693
}
16731694
}
1695+
1696+
// maxUserFileBytes caps how much data is read from any user-database file
1697+
// opened via openBoundedUserFile. Real systems keep these files well under
1698+
// 1 MiB; 10 MiB is generous headroom while keeping peak memory during
1699+
// user.ParsePasswd/ParseGroup bounded to single-digit MiB.
1700+
const maxUserFileBytes = 10 << 20
1701+
1702+
// openBoundedUserFile opens path and returns an io.ReadCloser that errors out
1703+
// if more than maxUserFileBytes are read from it. Non-regular sources are
1704+
// rejected before opening, so callers never block on FIFOs or device files
1705+
// and parsers never consume bytes from them.
1706+
//
1707+
// openBoundedUserFile does NOT perform any path validation. It does not guard
1708+
// against symlink traversal or paths that escape the container rootfs, and it
1709+
// follows symlinks both when stat-ing and when opening. Callers are responsible
1710+
// for confining path to the intended root beforehand (e.g. via fs.RootPath,
1711+
// which resolves every symlink component and re-anchors absolute links to the
1712+
// root) and must not pass attacker-controlled, unresolved paths directly.
1713+
func openBoundedUserFile(path string) (io.ReadCloser, error) {
1714+
info, err := os.Stat(path)
1715+
if err != nil {
1716+
return nil, err
1717+
}
1718+
if !info.Mode().IsRegular() {
1719+
return nil, fmt.Errorf("%s is not a regular file", path)
1720+
}
1721+
f, err := os.Open(path)
1722+
if err != nil {
1723+
return nil, err
1724+
}
1725+
return &limitedFile{
1726+
Closer: f,
1727+
// Allow one byte past the cap so an overflow surfaces as an
1728+
// error rather than a silent EOF that the parser would treat as
1729+
// a clean end-of-file (and miss any entries past the cap).
1730+
r: &io.LimitedReader{R: f, N: maxUserFileBytes + 1},
1731+
name: path,
1732+
}, nil
1733+
}
1734+
1735+
// limitedFile is an io.ReadCloser whose Read returns an error once more than
1736+
// maxUserFileBytes have been read.
1737+
type limitedFile struct {
1738+
io.Closer
1739+
r *io.LimitedReader
1740+
name string
1741+
}
1742+
1743+
func (l *limitedFile) Read(p []byte) (int, error) {
1744+
n, err := l.r.Read(p)
1745+
if l.r.N == 0 {
1746+
return n, fmt.Errorf("%q exceeds %d bytes", l.name, maxUserFileBytes)
1747+
}
1748+
return n, err
1749+
}

oci/spec_opts_linux_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ func TestWithAppendAdditionalGroupsNoEtcGroup(t *testing.T) {
752752
{
753753
name: "no additional gids, append root group",
754754
groups: []string{"root"},
755-
err: fmt.Sprintf("unable to find group root: open %s: no such file or directory", filepath.Join(td, "etc", "group")),
755+
err: fmt.Sprintf("unable to find group root: stat %s: no such file or directory", filepath.Join(td, "etc", "group")),
756756
expected: []uint32{0},
757757
},
758758
{

0 commit comments

Comments
 (0)