diff --git a/remotes/docker/resolver.go b/remotes/docker/resolver.go index 5258d7179..2188ae383 100644 --- a/remotes/docker/resolver.go +++ b/remotes/docker/resolver.go @@ -521,7 +521,10 @@ func (r *request) do(ctx context.Context) (*http.Response, error) { if err != nil { return nil, err } - req.Header = r.header + req.Header = http.Header{} // headers need to be copied to avoid concurrent map access + for k, v := range r.header { + req.Header[k] = v + } if r.body != nil { body, err := r.body() if err != nil { diff --git a/snapshots/overlay/check.go b/snapshots/overlay/check.go index cec46df03..f4a3ee9a8 100644 --- a/snapshots/overlay/check.go +++ b/snapshots/overlay/check.go @@ -26,6 +26,7 @@ import ( "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/sys" "github.com/containerd/continuity/fs" "github.com/pkg/errors" ) @@ -86,3 +87,84 @@ func Supported(root string) error { } return supportsMultipleLowerDir(root) } + +// NeedsUserXAttr returns whether overlayfs should be mounted with the "userxattr" mount option. +// +// The "userxattr" option is needed for mounting overlayfs inside a user namespace with kernel >= 5.11. +// +// The "userxattr" option is NOT needed for the initial user namespace (aka "the host"). +// +// Also, Ubuntu (since circa 2015) and Debian (since 10) with kernel < 5.11 can mount +// the overlayfs in a user namespace without the "userxattr" option. +// +// The corresponding kernel commit: https://github.com/torvalds/linux/commit/2d2f2d7322ff43e0fe92bf8cccdc0b09449bf2e1 +// > ovl: user xattr +// > +// > Optionally allow using "user.overlay." namespace instead of "trusted.overlay." +// > ... +// > Disable redirect_dir and metacopy options, because these would allow privilege escalation through direct manipulation of the +// > "user.overlay.redirect" or "user.overlay.metacopy" xattrs. +// > ... +// +// The "userxattr" support is not exposed in "/sys/module/overlay/parameters". +func NeedsUserXAttr(d string) (bool, error) { + if !sys.RunningInUserNS() { + // we are the real root (i.e., the root in the initial user NS), + // so we do never need "userxattr" opt. + return false, nil + } + + // TODO: add fast path for kernel >= 5.11 . + // + // Keep in mind that distro vendors might be going to backport the patch to older kernels. + // So we can't completely remove the check. + + tdRoot := filepath.Join(d, "userxattr-check") + if err := os.RemoveAll(tdRoot); err != nil { + log.L.WithError(err).Warnf("Failed to remove check directory %v", tdRoot) + } + + if err := os.MkdirAll(tdRoot, 0700); err != nil { + return false, err + } + + defer func() { + if err := os.RemoveAll(tdRoot); err != nil { + log.L.WithError(err).Warnf("Failed to remove check directory %v", tdRoot) + } + }() + + td, err := ioutil.TempDir(tdRoot, "") + if err != nil { + return false, err + } + + for _, dir := range []string{"lower1", "lower2", "upper", "work", "merged"} { + if err := os.Mkdir(filepath.Join(td, dir), 0755); err != nil { + return false, err + } + } + + opts := []string{ + fmt.Sprintf("lowerdir=%s:%s,upperdir=%s,workdir=%s", filepath.Join(td, "lower2"), filepath.Join(td, "lower1"), filepath.Join(td, "upper"), filepath.Join(td, "work")), + "userxattr", + } + + m := mount.Mount{ + Type: "overlay", + Source: "overlay", + Options: opts, + } + + dest := filepath.Join(td, "merged") + if err := m.Mount(dest); err != nil { + // Probably the host is running Ubuntu/Debian kernel (< 5.11) with the userns patch but without the userxattr patch. + // Return false without error. + log.L.WithError(err).Debugf("cannot mount overlay with \"userxattr\", probably the kernel does not support userxattr") + return false, nil + } + if err := mount.UnmountAll(dest, 0); err != nil { + log.L.WithError(err).Warnf("Failed to unmount check directory %v", dest) + } + return true, nil +} diff --git a/snapshots/overlay/overlay.go b/snapshots/overlay/overlay.go index 1e55fb2f3..f224f853a 100644 --- a/snapshots/overlay/overlay.go +++ b/snapshots/overlay/overlay.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/containerd/snapshots/storage" "github.com/containerd/continuity/fs" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // SnapshotterConfig is used to configure the overlay snapshotter instance @@ -57,6 +58,7 @@ type snapshotter struct { ms *storage.MetaStore asyncRemove bool indexOff bool + userxattr bool // whether to enable "userxattr" mount option } // NewSnapshotter returns a Snapshotter which uses overlayfs. The overlayfs @@ -95,11 +97,18 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) { indexOff = true } + // figure out whether "userxattr" option is recognized by the kernel && needed + userxattr, err := NeedsUserXAttr(root) + if err != nil { + logrus.WithError(err).Warnf("cannot detect whether \"userxattr\" option needs to be used, assuming to be %v", userxattr) + } + return &snapshotter{ root: root, ms: ms, asyncRemove: config.asyncRemove, indexOff: indexOff, + userxattr: userxattr, }, nil } @@ -464,6 +473,10 @@ func (o *snapshotter) mounts(s storage.Snapshot) []mount.Mount { options = append(options, "index=off") } + if o.userxattr { + options = append(options, "userxattr") + } + if s.Kind == snapshots.KindActive { options = append(options, fmt.Sprintf("workdir=%s", o.workPath(s.ID)), diff --git a/snapshots/overlay/overlay_test.go b/snapshots/overlay/overlay_test.go index 7671b83dc..1ebffadb7 100644 --- a/snapshots/overlay/overlay_test.go +++ b/snapshots/overlay/overlay_test.go @@ -173,11 +173,21 @@ func testOverlayOverlayMount(t *testing.T, newSnapshotter testsuite.SnapshotterF upper = "upperdir=" + filepath.Join(bp, "fs") lower = "lowerdir=" + getParents(ctx, o, root, "/tmp/layer2")[0] ) - for i, v := range []string{ + + expected := []string{ + "index=off", + } + if userxattr, err := NeedsUserXAttr(root); err != nil { + t.Fatal(err) + } else if userxattr { + expected = append(expected, "userxattr") + } + expected = append(expected, []string{ work, upper, lower, - } { + }...) + for i, v := range expected { if m.Options[i] != v { t.Errorf("expected %q but received %q", v, m.Options[i]) } @@ -334,12 +344,26 @@ func testOverlayView(t *testing.T, newSnapshotter testsuite.SnapshotterFunc) { if m.Source != "overlay" { t.Errorf("mount source should be overlay but received %q", m.Source) } - if len(m.Options) != 1 { - t.Errorf("expected 1 mount option but got %d", len(m.Options)) + + expectedOptions := 2 + userxattr, err := NeedsUserXAttr(root) + if err != nil { + t.Fatal(err) + } + if userxattr { + expectedOptions++ + } + + if len(m.Options) != expectedOptions { + t.Errorf("expected %d additional mount option but got %d", expectedOptions, len(m.Options)) } lowers := getParents(ctx, o, root, "/tmp/view2") expected = fmt.Sprintf("lowerdir=%s:%s", lowers[0], lowers[1]) - if m.Options[0] != expected { - t.Errorf("expected option %q but received %q", expected, m.Options[0]) + optIdx := 1 + if userxattr { + optIdx++ + } + if m.Options[optIdx] != expected { + t.Errorf("expected option %q but received %q", expected, m.Options[optIdx]) } }