From 2fed51aea330482f4919d4d6374bfa271b6f0c4a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 9 Mar 2023 19:42:07 +0900 Subject: [PATCH] rootless: fix up unprivileged mount opts Port https://github.com/moby/moby/blob/v23.0.1/daemon/oci_linux.go#L430-L460 > // Get the set of mount flags that are set on the mount that contains the given > // path and are locked by CL_UNPRIVILEGED. This is necessary to ensure that > // bind-mounting "with options" will not fail with user namespaces, due to > // kernel restrictions that require user namespace mounts to preserve > // CL_UNPRIVILEGED locked flags. Fix issue 3098 Signed-off-by: Akihiro Suda --- cache/refs.go | 8 ++ executor/oci/spec.go | 10 +++ snapshot/localmounter_unix.go | 10 +++ util/rootless/mountopts/mountopts_linux.go | 88 +++++++++++++++++++++ util/rootless/mountopts/mountopts_others.go | 21 +++++ util/strutil/strutil.go | 30 +++++++ 6 files changed, 167 insertions(+) create mode 100644 util/rootless/mountopts/mountopts_linux.go create mode 100644 util/rootless/mountopts/mountopts_others.go create mode 100644 util/strutil/strutil.go diff --git a/cache/refs.go b/cache/refs.go index dc2cd561b01d..0af736ab70ca 100644 --- a/cache/refs.go +++ b/cache/refs.go @@ -14,6 +14,7 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/leases" "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/pkg/userns" "github.com/containerd/containerd/snapshots" "github.com/docker/docker/pkg/idtools" "github.com/hashicorp/go-multierror" @@ -27,6 +28,7 @@ import ( "github.com/moby/buildkit/util/flightcontrol" "github.com/moby/buildkit/util/leaseutil" "github.com/moby/buildkit/util/progress" + rootlessmountopts "github.com/moby/buildkit/util/rootless/mountopts" "github.com/moby/buildkit/util/winlayers" "github.com/moby/sys/mountinfo" digest "github.com/opencontainers/go-digest" @@ -1640,6 +1642,12 @@ func (sm *sharableMountable) Mount() (_ []mount.Mount, _ func() error, retErr er os.Remove(dir) } }() + if userns.RunningInUserNS() { + mounts, err = rootlessmountopts.FixUp(mounts) + if err != nil { + return nil, nil, err + } + } if err := mount.All(mounts, dir); err != nil { return nil, nil, err } diff --git a/executor/oci/spec.go b/executor/oci/spec.go index c99a23ec40b1..054c28dd474e 100644 --- a/executor/oci/spec.go +++ b/executor/oci/spec.go @@ -11,12 +11,14 @@ import ( "github.com/containerd/containerd/mount" "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/pkg/userns" "github.com/containerd/continuity/fs" "github.com/docker/docker/pkg/idtools" "github.com/mitchellh/hashstructure/v2" "github.com/moby/buildkit/executor" "github.com/moby/buildkit/snapshot" "github.com/moby/buildkit/util/network" + rootlessmountopts "github.com/moby/buildkit/util/rootless/mountopts" traceexec "github.com/moby/buildkit/util/tracing/exec" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux" @@ -193,6 +195,14 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou } s.Mounts = dedupMounts(s.Mounts) + + if userns.RunningInUserNS() { + s.Mounts, err = rootlessmountopts.FixUpOCI(s.Mounts) + if err != nil { + return nil, nil, err + } + } + return s, releaseAll, nil } diff --git a/snapshot/localmounter_unix.go b/snapshot/localmounter_unix.go index 27cff3ebdf8c..a4b7b1a9e409 100644 --- a/snapshot/localmounter_unix.go +++ b/snapshot/localmounter_unix.go @@ -8,6 +8,8 @@ import ( "syscall" "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/pkg/userns" + rootlessmountopts "github.com/moby/buildkit/util/rootless/mountopts" "github.com/pkg/errors" ) @@ -24,6 +26,14 @@ func (lm *localMounter) Mount() (string, error) { lm.release = release } + if userns.RunningInUserNS() { + var err error + lm.mounts, err = rootlessmountopts.FixUp(lm.mounts) + if err != nil { + return "", err + } + } + if len(lm.mounts) == 1 && (lm.mounts[0].Type == "bind" || lm.mounts[0].Type == "rbind") { ro := false for _, opt := range lm.mounts[0].Options { diff --git a/util/rootless/mountopts/mountopts_linux.go b/util/rootless/mountopts/mountopts_linux.go new file mode 100644 index 000000000000..92c542b19fea --- /dev/null +++ b/util/rootless/mountopts/mountopts_linux.go @@ -0,0 +1,88 @@ +package mountopts + +import ( + "github.com/containerd/containerd/mount" + "github.com/moby/buildkit/util/strutil" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +// UnprivilegedMountFlags gets the set of mount flags that are set on the mount that contains the given +// path and are locked by CL_UNPRIVILEGED. This is necessary to ensure that +// bind-mounting "with options" will not fail with user namespaces, due to +// kernel restrictions that require user namespace mounts to preserve +// CL_UNPRIVILEGED locked flags. +// +// From https://github.com/moby/moby/blob/v23.0.1/daemon/oci_linux.go#L430-L460 +func UnprivilegedMountFlags(path string) ([]string, error) { + var statfs unix.Statfs_t + if err := unix.Statfs(path, &statfs); err != nil { + return nil, err + } + + // The set of keys come from https://github.com/torvalds/linux/blob/v4.13/fs/namespace.c#L1034-L1048. + unprivilegedFlags := map[uint64]string{ + unix.MS_RDONLY: "ro", + unix.MS_NODEV: "nodev", + unix.MS_NOEXEC: "noexec", + unix.MS_NOSUID: "nosuid", + unix.MS_NOATIME: "noatime", + unix.MS_RELATIME: "relatime", + unix.MS_NODIRATIME: "nodiratime", + } + + var flags []string + for mask, flag := range unprivilegedFlags { + if uint64(statfs.Flags)&mask == mask { + flags = append(flags, flag) + } + } + + return flags, nil +} + +// FixUp is for https://github.com/moby/buildkit/issues/3098 +func FixUp(mounts []mount.Mount) ([]mount.Mount, error) { + for i, m := range mounts { + var isBind bool + for _, o := range m.Options { + switch o { + case "bind", "rbind": + isBind = true + } + } + if !isBind { + continue + } + unpriv, err := UnprivilegedMountFlags(m.Source) + if err != nil { + return nil, errors.Wrapf(err, "failed to get unprivileged mount flags for %+v", m) + } + m.Options = strutil.DedupeSlice(append(m.Options, unpriv...)) + mounts[i] = m + } + return mounts, nil +} + +func FixUpOCI(mounts []specs.Mount) ([]specs.Mount, error) { + for i, m := range mounts { + var isBind bool + for _, o := range m.Options { + switch o { + case "bind", "rbind": + isBind = true + } + } + if !isBind { + continue + } + unpriv, err := UnprivilegedMountFlags(m.Source) + if err != nil { + return nil, errors.Wrapf(err, "failed to get unprivileged mount flags for %+v", m) + } + m.Options = strutil.DedupeSlice(append(m.Options, unpriv...)) + mounts[i] = m + } + return mounts, nil +} diff --git a/util/rootless/mountopts/mountopts_others.go b/util/rootless/mountopts/mountopts_others.go new file mode 100644 index 000000000000..956c8041ffa6 --- /dev/null +++ b/util/rootless/mountopts/mountopts_others.go @@ -0,0 +1,21 @@ +//go:build !linux +// +build !linux + +package mountopts + +import ( + "github.com/containerd/containerd/mount" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +func UnprivilegedMountFlags(path string) ([]string, error) { + return []string{}, nil +} + +func FixUp(mounts []mount.Mount) ([]mount.Mount, error) { + return mounts, nil +} + +func FixUpOCI(mounts []specs.Mount) ([]specs.Mount, error) { + return mounts, nil +} diff --git a/util/strutil/strutil.go b/util/strutil/strutil.go new file mode 100644 index 000000000000..cb98555e715a --- /dev/null +++ b/util/strutil/strutil.go @@ -0,0 +1,30 @@ +/* + 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 strutil + +// DedupeSlice is from https://github.com/containerd/nerdctl/blob/v1.2.1/pkg/strutil/strutil.go#L72-L82 +func DedupeSlice(in []string) []string { + m := make(map[string]struct{}) + var res []string + for _, s := range in { + if _, ok := m[s]; !ok { + res = append(res, s) + m[s] = struct{}{} + } + } + return res +}