Move mount to core/mount

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2024-01-17 09:52:12 -08:00
parent 1a1e0e8c81
commit 6e5408dcec
93 changed files with 74 additions and 74 deletions

View File

@@ -0,0 +1,116 @@
/*
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 mount
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
// containerd/pkg/testutil has circular dependency on this mount pkg.
// so we use continuity/testutil instead.
"github.com/containerd/continuity/testutil"
"github.com/containerd/continuity/testutil/loopback"
"github.com/stretchr/testify/assert"
)
func checkLookup(t *testing.T, fsType, mntPoint, dir string) {
t.Helper()
info, err := Lookup(dir)
assert.NoError(t, err)
assert.Equal(t, fsType, info.FSType)
assert.Equal(t, mntPoint, info.Mountpoint)
}
func testLookup(t *testing.T, fsType string) {
testutil.RequiresRoot(t)
mnt := t.TempDir()
loop, err := loopback.New(100 << 20) // 100 MB
if err != nil {
t.Fatal(err)
}
if out, err := exec.Command("mkfs", "-t", fsType, loop.Device).CombinedOutput(); err != nil {
// not fatal
loop.Close()
t.Skipf("could not mkfs (%s) %s: %v (out: %q)", fsType, loop.Device, err, string(out))
}
if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil {
// not fatal
loop.Close()
t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out))
}
defer func() {
testutil.Unmount(t, mnt)
loop.Close()
}()
assert.True(t, strings.HasPrefix(loop.Device, "/dev/loop"))
checkLookup(t, fsType, mnt, mnt)
newMnt := t.TempDir()
if out, err := exec.Command("mount", "--bind", mnt, newMnt).CombinedOutput(); err != nil {
t.Fatalf("could not mount %s to %s: %v (out: %q)", mnt, newMnt, err, string(out))
}
defer func() {
testutil.Unmount(t, newMnt)
}()
checkLookup(t, fsType, newMnt, newMnt)
subDir := filepath.Join(newMnt, "subDir")
err = os.MkdirAll(subDir, 0700)
if err != nil {
t.Fatal(err)
}
checkLookup(t, fsType, newMnt, subDir)
}
func TestLookupWithExt4(t *testing.T) {
testLookup(t, "ext4")
}
func TestLookupWithXFS(t *testing.T) {
testLookup(t, "xfs")
}
func TestLookupWithOverlay(t *testing.T) {
lower := t.TempDir()
upper := t.TempDir()
work := t.TempDir()
overlay := t.TempDir()
if out, err := exec.Command("mount", "-t", "overlay", "overlay", "-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s",
lower, upper, work), overlay).CombinedOutput(); err != nil {
// not fatal
t.Skipf("could not mount overlay: %v (out: %q)", err, string(out))
}
defer testutil.Unmount(t, overlay)
testdir := filepath.Join(overlay, "testdir")
err := os.Mkdir(testdir, 0777)
assert.NoError(t, err)
testfile := filepath.Join(overlay, "testfile")
_, err = os.Create(testfile)
assert.NoError(t, err)
checkLookup(t, "overlay", overlay, testdir)
checkLookup(t, "overlay", overlay, testfile)
}

51
core/mount/lookup_unix.go Normal file
View File

@@ -0,0 +1,51 @@
//go:build !windows
/*
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 mount
import (
"fmt"
"github.com/moby/sys/mountinfo"
)
// Lookup returns the mount info corresponds to the path.
func Lookup(dir string) (Info, error) {
resolvedDir, err := CanonicalizePath(dir)
if err != nil {
return Info{}, err
}
m, err := mountinfo.GetMounts(mountinfo.ParentsFilter(resolvedDir))
if err != nil {
return Info{}, fmt.Errorf("failed to find the mount info for %q: %w", resolvedDir, err)
}
if len(m) == 0 {
return Info{}, fmt.Errorf("failed to find the mount info for %q", resolvedDir)
}
// find the longest matching mount point
var idx, maxlen int
for i := range m {
if len(m[i].Mountpoint) > maxlen {
maxlen = len(m[i].Mountpoint)
idx = i
}
}
return *m[idx], nil
}

View File

@@ -0,0 +1,29 @@
//go:build windows
/*
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 mount
import (
"fmt"
"runtime"
)
// Lookup returns the mount info corresponds to the path.
func Lookup(dir string) (Info, error) {
return Info{}, fmt.Errorf("mount.Lookup is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

242
core/mount/losetup_linux.go Normal file
View File

@@ -0,0 +1,242 @@
/*
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 mount
import (
"errors"
"fmt"
"os"
"strings"
"syscall"
"time"
"unsafe"
kernel "github.com/containerd/containerd/v2/contrib/seccomp/kernelversion"
"github.com/containerd/containerd/v2/pkg/randutil"
"golang.org/x/sys/unix"
)
const (
loopControlPath = "/dev/loop-control"
loopDevFormat = "/dev/loop%d"
ebusyString = "device or resource busy"
loopConfigureIoctl = 0x4c0a
)
type LoopConfig struct {
Fd uint32
BlockSize uint32
Info unix.LoopInfo64
Reserved [8]uint64
}
func ioctlConfigure(fd int, value *LoopConfig) error {
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(loopConfigureIoctl), uintptr(unsafe.Pointer(value)))
if err == 0 {
return nil
}
return err
}
// LoopParams parameters to control loop device setup
type LoopParams struct {
// Loop device should forbid write
Readonly bool
// Loop device is automatically cleared by kernel when the
// last opener closes it
Autoclear bool
// Use direct IO to access the loop backing file
Direct bool
}
func getFreeLoopDev() (uint32, error) {
ctrl, err := os.OpenFile(loopControlPath, os.O_RDWR, 0)
if err != nil {
return 0, fmt.Errorf("could not open %v: %v", loopControlPath, err)
}
defer ctrl.Close()
num, err := unix.IoctlRetInt(int(ctrl.Fd()), unix.LOOP_CTL_GET_FREE)
if err != nil {
return 0, fmt.Errorf("could not get free loop device: %w", err)
}
return uint32(num), nil
}
// setupLoopDev attaches the backing file to the loop device and returns
// the file handle for the loop device. The caller is responsible for
// closing the file handle.
func setupLoopDev(backingFile, loopDev string, param LoopParams) (_ *os.File, retErr error) {
// 1. Open backing file and loop device
flags := os.O_RDWR
if param.Readonly {
flags = os.O_RDONLY
}
back, err := os.OpenFile(backingFile, flags, 0)
if err != nil {
return nil, fmt.Errorf("could not open backing file: %s: %w", backingFile, err)
}
defer back.Close()
loop, err := os.OpenFile(loopDev, flags, 0)
if err != nil {
return nil, fmt.Errorf("could not open loop device: %s: %w", loopDev, err)
}
defer func() {
if retErr != nil {
loop.Close()
}
}()
fiveDotEight := kernel.KernelVersion{Kernel: 5, Major: 8}
if ok, err := kernel.GreaterEqualThan(fiveDotEight); err == nil && ok {
config := LoopConfig{
Fd: uint32(back.Fd()),
}
copy(config.Info.File_name[:], backingFile)
if param.Readonly {
config.Info.Flags |= unix.LO_FLAGS_READ_ONLY
}
if param.Autoclear {
config.Info.Flags |= unix.LO_FLAGS_AUTOCLEAR
}
if param.Direct {
config.Info.Flags |= unix.LO_FLAGS_DIRECT_IO
}
if err := ioctlConfigure(int(loop.Fd()), &config); err != nil {
return nil, fmt.Errorf("failed to configure loop device: %s: %w", loopDev, err)
}
return loop, nil
}
// 2. Set FD
if err := unix.IoctlSetInt(int(loop.Fd()), unix.LOOP_SET_FD, int(back.Fd())); err != nil {
return nil, fmt.Errorf("could not set loop fd for device: %s: %w", loopDev, err)
}
defer func() {
if retErr != nil {
_ = unix.IoctlSetInt(int(loop.Fd()), unix.LOOP_CLR_FD, 0)
}
}()
// 3. Set Info
info := unix.LoopInfo64{}
copy(info.File_name[:], backingFile)
if param.Readonly {
info.Flags |= unix.LO_FLAGS_READ_ONLY
}
if param.Autoclear {
info.Flags |= unix.LO_FLAGS_AUTOCLEAR
}
err = unix.IoctlLoopSetStatus64(int(loop.Fd()), &info)
if err != nil {
return nil, fmt.Errorf("failed to set loop device info: %w", err)
}
// 4. Set Direct IO
if param.Direct {
err = unix.IoctlSetInt(int(loop.Fd()), unix.LOOP_SET_DIRECT_IO, 1)
if err != nil {
return nil, fmt.Errorf("failed to setup loop with direct: %w", err)
}
}
return loop, nil
}
// setupLoop looks for (and possibly creates) a free loop device, and
// then attaches backingFile to it.
//
// When autoclear is true, caller should take care to close it when
// done with the loop device. The loop device file handle keeps
// loFlagsAutoclear in effect and we rely on it to clean up the loop
// device. If caller closes the file handle after mounting the device,
// kernel will clear the loop device after it is umounted. Otherwise
// the loop device is cleared when the file handle is closed.
//
// When autoclear is false, caller should be responsible to remove
// the loop device when done with it.
//
// Upon success, the file handle to the loop device is returned.
func setupLoop(backingFile string, param LoopParams) (*os.File, error) {
for retry := 1; retry < 100; retry++ {
num, err := getFreeLoopDev()
if err != nil {
return nil, err
}
loopDev := fmt.Sprintf(loopDevFormat, num)
file, err := setupLoopDev(backingFile, loopDev, param)
if err != nil {
// Per util-linux/sys-utils/losetup.c:create_loop(),
// free loop device can race and we end up failing
// with EBUSY when trying to set it up.
if strings.Contains(err.Error(), ebusyString) {
// Fallback a bit to avoid live lock
time.Sleep(time.Millisecond * time.Duration(randutil.Intn(retry*10)))
continue
}
return nil, err
}
return file, nil
}
return nil, errors.New("timeout creating new loopback device")
}
func removeLoop(loopdev string) error {
file, err := os.Open(loopdev)
if err != nil {
return err
}
defer file.Close()
return unix.IoctlSetInt(int(file.Fd()), unix.LOOP_CLR_FD, 0)
}
// AttachLoopDevice attaches a specified backing file to a loop device
func AttachLoopDevice(backingFile string) (string, error) {
file, err := setupLoop(backingFile, LoopParams{})
if err != nil {
return "", err
}
defer file.Close()
return file.Name(), nil
}
// DetachLoopDevice detaches the provided loop devices
func DetachLoopDevice(devices ...string) error {
for _, dev := range devices {
if err := removeLoop(dev); err != nil {
return fmt.Errorf("failed to remove loop device: %s: %w", dev, err)
}
}
return nil
}

View File

@@ -0,0 +1,165 @@
/*
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 mount
import (
"os"
"testing"
"time"
"github.com/containerd/continuity/testutil"
)
var randomData = []byte("randomdata")
func createTempFile(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp("", "losetup")
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err = f.Truncate(512); err != nil {
t.Fatal(err)
}
return f.Name()
}
func TestNonExistingLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := "setup-loop-test-no-such-file"
_, err := setupLoop(backingFile, LoopParams{})
if err == nil {
t.Fatalf("setupLoop with non-existing file should fail")
}
}
func TestRoLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := createTempFile(t)
defer func() {
if err := os.Remove(backingFile); err != nil {
t.Fatal(err)
}
}()
file, err := setupLoop(backingFile, LoopParams{Readonly: true, Autoclear: true})
if err != nil {
t.Fatal(err)
}
defer file.Close()
if _, err := file.Write(randomData); err == nil {
t.Fatalf("writing to readonly loop device should fail")
}
}
func TestRwLoop(t *testing.T) {
testutil.RequiresRoot(t)
backingFile := createTempFile(t)
defer func() {
if err := os.Remove(backingFile); err != nil {
t.Fatal(err)
}
}()
file, err := setupLoop(backingFile, LoopParams{Autoclear: true})
if err != nil {
t.Fatal(err)
}
defer file.Close()
if _, err := file.Write(randomData); err != nil {
t.Fatal(err)
}
}
func TestAttachDetachLoopDevice(t *testing.T) {
testutil.RequiresRoot(t)
path := createTempFile(t)
defer func() {
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
}()
dev, err := AttachLoopDevice(path)
if err != nil {
t.Fatal(err)
}
if err = DetachLoopDevice(dev); err != nil {
t.Fatal(err)
}
}
func TestAutoclearTrueLoop(t *testing.T) {
testutil.RequiresRoot(t)
dev := func() string {
backingFile := createTempFile(t)
defer func() {
if err := os.Remove(backingFile); err != nil {
t.Fatal(err)
}
}()
file, err := setupLoop(backingFile, LoopParams{Autoclear: true})
if err != nil {
t.Fatal(err)
}
dev := file.Name()
file.Close()
return dev
}()
time.Sleep(100 * time.Millisecond)
if err := removeLoop(dev); err == nil {
t.Fatalf("removeLoop should fail if Autoclear is true")
}
}
func TestAutoclearFalseLoop(t *testing.T) {
testutil.RequiresRoot(t)
dev := func() string {
backingFile := createTempFile(t)
defer func() {
if err := os.Remove(backingFile); err != nil {
t.Fatal(err)
}
}()
file, err := setupLoop(backingFile, LoopParams{Autoclear: false})
if err != nil {
t.Fatal(err)
}
dev := file.Name()
file.Close()
return dev
}()
time.Sleep(100 * time.Millisecond)
if err := removeLoop(dev); err != nil {
t.Fatal(err)
}
}

176
core/mount/mount.go Normal file
View File

@@ -0,0 +1,176 @@
/*
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 mount
import (
"fmt"
"path/filepath"
"strings"
"github.com/containerd/containerd/v2/api/types"
"github.com/containerd/continuity/fs"
)
// Mount is the lingua franca of containerd. A mount represents a
// serialized mount syscall. Components either emit or consume mounts.
type Mount struct {
// Type specifies the host-specific of the mount.
Type string
// Source specifies where to mount from. Depending on the host system, this
// can be a source path or device.
Source string
// Target specifies an optional subdirectory as a mountpoint. It assumes that
// the subdirectory exists in a parent mount.
Target string
// Options contains zero or more fstab-style mount options. Typically,
// these are platform specific.
Options []string
}
// All mounts all the provided mounts to the provided target. If submounts are
// present, it assumes that parent mounts come before child mounts.
func All(mounts []Mount, target string) error {
for _, m := range mounts {
if err := m.Mount(target); err != nil {
return err
}
}
return nil
}
// UnmountMounts unmounts all the mounts under a target in the reverse order of
// the mounts array provided.
func UnmountMounts(mounts []Mount, target string, flags int) error {
for i := len(mounts) - 1; i >= 0; i-- {
mountpoint, err := fs.RootPath(target, mounts[i].Target)
if err != nil {
return err
}
if err := UnmountAll(mountpoint, flags); err != nil {
if i == len(mounts)-1 { // last mount
return err
}
}
}
return nil
}
// CanonicalizePath makes path absolute and resolves symlinks in it.
// Path must exist.
func CanonicalizePath(path string) (string, error) {
// Abs also does Clean, so we do not need to call it separately
path, err := filepath.Abs(path)
if err != nil {
return "", err
}
return filepath.EvalSymlinks(path)
}
// ReadOnly returns a boolean value indicating whether this mount has the "ro"
// option set.
func (m *Mount) ReadOnly() bool {
for _, option := range m.Options {
if option == "ro" {
return true
}
}
return false
}
// Mount to the provided target path.
func (m *Mount) Mount(target string) error {
target, err := fs.RootPath(target, m.Target)
if err != nil {
return fmt.Errorf("failed to join path %q with root %q: %w", m.Target, target, err)
}
return m.mount(target)
}
// readonlyMounts modifies the received mount options
// to make them readonly
func readonlyMounts(mounts []Mount) []Mount {
for i, m := range mounts {
if m.Type == "overlay" {
mounts[i].Options = readonlyOverlay(m.Options)
continue
}
opts := make([]string, 0, len(m.Options))
for _, opt := range m.Options {
if opt != "rw" && opt != "ro" { // skip `ro` too so we don't append it twice
opts = append(opts, opt)
}
}
opts = append(opts, "ro")
mounts[i].Options = opts
}
return mounts
}
// readonlyOverlay takes mount options for overlay mounts and makes them readonly by
// removing workdir and upperdir (and appending the upperdir layer to lowerdir) - see:
// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#multiple-lower-layers
func readonlyOverlay(opt []string) []string {
out := make([]string, 0, len(opt))
upper := ""
for _, o := range opt {
if strings.HasPrefix(o, "upperdir=") {
upper = strings.TrimPrefix(o, "upperdir=")
} else if !strings.HasPrefix(o, "workdir=") {
out = append(out, o)
}
}
if upper != "" {
for i, o := range out {
if strings.HasPrefix(o, "lowerdir=") {
out[i] = "lowerdir=" + upper + ":" + strings.TrimPrefix(o, "lowerdir=")
}
}
}
return out
}
// ToProto converts from [Mount] to the containerd
// APIs protobuf definition of a Mount.
func ToProto(mounts []Mount) []*types.Mount {
apiMounts := make([]*types.Mount, len(mounts))
for i, m := range mounts {
apiMounts[i] = &types.Mount{
Type: m.Type,
Source: m.Source,
Target: m.Target,
Options: m.Options,
}
}
return apiMounts
}
// FromProto converts from the protobuf definition [types.Mount] to
// [Mount].
func FromProto(mm []*types.Mount) []Mount {
mounts := make([]Mount, len(mm))
for i, m := range mm {
mounts[i] = Mount{
Type: m.Type,
Source: m.Source,
Target: m.Target,
Options: m.Options,
}
}
return mounts
}

133
core/mount/mount_freebsd.go Normal file
View File

@@ -0,0 +1,133 @@
/*
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 mount
import (
"errors"
"fmt"
"os"
"os/exec"
"time"
"golang.org/x/sys/unix"
)
var (
// ErrNotImplementOnUnix is returned for methods that are not implemented
ErrNotImplementOnUnix = errors.New("not implemented under unix")
)
// Mount to the provided target.
//
// The "syscall" and "golang.org/x/sys/unix" packages do not define a Mount
// function for FreeBSD, so instead we execute mount(8) and trust it to do
// the right thing
func (m *Mount) mount(target string) error {
// target: "/foo/target"
// command: "mount -o ro -t nullfs /foo/source /foo/merged"
// Note: FreeBSD mount(8) is particular about the order of flags and arguments
var args []string
for _, o := range m.Options {
args = append(args, "-o", o)
}
args = append(args, "-t", m.Type)
args = append(args, m.Source, target)
infoBeforeMount, err := Lookup(target)
if err != nil {
return err
}
// cmd.CombinedOutput() may intermittently return ECHILD because of our signal handling in shim.
// See #4387 and wait(2).
const retriesOnECHILD = 10
for i := 0; i < retriesOnECHILD; i++ {
cmd := exec.Command("mount", args...)
out, err := cmd.CombinedOutput()
if err == nil {
return nil
}
if !errors.Is(err, unix.ECHILD) {
return fmt.Errorf("mount [%v] failed: %q: %w", args, string(out), err)
}
// We got ECHILD, we are not sure whether the mount was successful.
// If the mount ID has changed, we are sure we got some new mount, but still not sure it is fully completed.
// So we attempt to unmount the new mount before retrying.
infoAfterMount, err := Lookup(target)
if err != nil {
return err
}
if infoAfterMount.ID != infoBeforeMount.ID {
_ = unmount(target, 0)
}
}
return fmt.Errorf("mount [%v] failed with ECHILD (retried %d times)", args, retriesOnECHILD)
}
// Unmount the provided mount path with the flags
func Unmount(target string, flags int) error {
if err := unmount(target, flags); err != nil && err != unix.EINVAL {
return err
}
return nil
}
func unmount(target string, flags int) error {
for i := 0; i < 50; i++ {
if err := unix.Unmount(target, flags); err != nil {
switch err {
case unix.EBUSY:
time.Sleep(50 * time.Millisecond)
continue
default:
return err
}
}
return nil
}
return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY)
}
// UnmountAll repeatedly unmounts the given mount point until there
// are no mounts remaining (EINVAL is returned by mount), which is
// useful for undoing a stack of mounts on the same mount point.
// UnmountAll all is noop when the first argument is an empty string.
// This is done when the containerd client did not specify any rootfs
// mounts (e.g. because the rootfs is managed outside containerd)
// UnmountAll is noop when the mount path does not exist.
func UnmountAll(mount string, flags int) error {
if mount == "" {
return nil
}
if _, err := os.Stat(mount); os.IsNotExist(err) {
return nil
}
for {
if err := unmount(mount, flags); err != nil {
// EINVAL is returned if the target is not a
// mount point, indicating that we are
// done. It can also indicate a few other
// things (such as invalid flags) which we
// unfortunately end up squelching here too.
if err == unix.EINVAL {
return nil
}
return err
}
}
}

View File

@@ -0,0 +1,212 @@
/*
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 mount
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"golang.org/x/sys/unix"
"github.com/containerd/containerd/v2/sys"
)
// TODO: Support multiple mappings in future
func parseIDMapping(mapping string) ([]syscall.SysProcIDMap, error) {
parts := strings.Split(mapping, ":")
if len(parts) != 3 {
return nil, fmt.Errorf("user namespace mappings require the format `container-id:host-id:size`")
}
cID, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("invalid container id for user namespace remapping, %w", err)
}
hID, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid host id for user namespace remapping, %w", err)
}
size, err := strconv.Atoi(parts[2])
if err != nil {
return nil, fmt.Errorf("invalid size for user namespace remapping, %w", err)
}
if cID < 0 || hID < 0 || size < 0 {
return nil, fmt.Errorf("invalid mapping %s, all IDs and size must be positive integers", mapping)
}
return []syscall.SysProcIDMap{
{
ContainerID: cID,
HostID: hID,
Size: size,
},
}, nil
}
// IDMapMount applies GID/UID shift according to gidmap/uidmap for target path
func IDMapMount(source, target string, usernsFd int) (err error) {
var (
attr unix.MountAttr
)
attr.Attr_set = unix.MOUNT_ATTR_IDMAP
attr.Attr_clr = 0
attr.Propagation = 0
attr.Userns_fd = uint64(usernsFd)
dFd, err := unix.OpenTree(-int(unix.EBADF), source, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH))
if err != nil {
return fmt.Errorf("Unable to open tree for %s: %w", target, err)
}
defer unix.Close(dFd)
if err = unix.MountSetattr(dFd, "", unix.AT_EMPTY_PATH, &attr); err != nil {
return fmt.Errorf("Unable to shift GID/UID for %s: %w", target, err)
}
if err = unix.MoveMount(dFd, "", -int(unix.EBADF), target, unix.MOVE_MOUNT_F_EMPTY_PATH); err != nil {
return fmt.Errorf("Unable to attach mount tree to %s: %w", target, err)
}
return nil
}
// GetUsernsFD forks the current process and creates a user namespace using
// the specified mappings.
func GetUsernsFD(uidmap, gidmap string) (_usernsFD *os.File, _ error) {
uidMaps, err := parseIDMapping(uidmap)
if err != nil {
return nil, err
}
gidMaps, err := parseIDMapping(gidmap)
if err != nil {
return nil, err
}
return getUsernsFD(uidMaps, gidMaps)
}
func getUsernsFD(uidMaps, gidMaps []syscall.SysProcIDMap) (_usernsFD *os.File, retErr error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
pid, pidfd, errno := sys.ForkUserns()
if errno != 0 {
return nil, errno
}
pidFD := os.NewFile(pidfd, "pidfd")
defer func() {
unix.PidfdSendSignal(int(pidFD.Fd()), unix.SIGKILL, nil, 0)
pidfdWaitid(pidFD)
pidFD.Close()
}()
// NOTE:
//
// The usernsFD will hold the userns reference in kernel. Even if the
// child process is reaped, the usernsFD is still valid.
usernsFD, err := os.Open(fmt.Sprintf("/proc/%d/ns/user", pid))
if err != nil {
return nil, fmt.Errorf("failed to get userns file descriptor for /proc/%d/user/ns: %w", pid, err)
}
defer func() {
if retErr != nil {
usernsFD.Close()
}
}()
uidmapFile, err := os.OpenFile(fmt.Sprintf("/proc/%d/%s", pid, "uid_map"), os.O_WRONLY, 0600)
if err != nil {
return nil, fmt.Errorf("failed to open /proc/%d/uid_map: %w", pid, err)
}
defer uidmapFile.Close()
gidmapFile, err := os.OpenFile(fmt.Sprintf("/proc/%d/%s", pid, "gid_map"), os.O_WRONLY, 0600)
if err != nil {
return nil, fmt.Errorf("failed to open /proc/%d/gid_map: %w", pid, err)
}
defer gidmapFile.Close()
testHookKillChildBeforePidfdSendSignal(pid, pidFD)
// Ensure the child process is still alive. If the err is ESRCH, we
// should return error because we can't guarantee the usernsFD and
// u[g]idmapFile are valid. It's safe to return error and retry.
if err := unix.PidfdSendSignal(int(pidFD.Fd()), 0, nil, 0); err != nil {
return nil, fmt.Errorf("failed to ensure child process is alive: %w", err)
}
testHookKillChildAfterPidfdSendSignal(pid, pidFD)
// NOTE:
//
// The u[g]id_map file descriptor is still valid if the child process
// is reaped.
writeMappings := func(f *os.File, idmap []syscall.SysProcIDMap) error {
mappings := ""
for _, m := range idmap {
mappings = fmt.Sprintf("%s%d %d %d\n", mappings, m.ContainerID, m.HostID, m.Size)
}
_, err := f.Write([]byte(mappings))
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
if err := writeMappings(uidmapFile, uidMaps); err != nil {
return nil, fmt.Errorf("failed to write uid_map: %w", err)
}
if err := writeMappings(gidmapFile, gidMaps); err != nil {
return nil, fmt.Errorf("failed to write gid_map: %w", err)
}
return usernsFD, nil
}
func pidfdWaitid(pidFD *os.File) error {
// https://elixir.bootlin.com/linux/v5.4.258/source/include/uapi/linux/wait.h#L20
const PPidFD = 3
var e syscall.Errno
for {
_, _, e = syscall.Syscall6(syscall.SYS_WAITID, PPidFD, pidFD.Fd(), 0, syscall.WEXITED, 0, 0)
if e != syscall.EINTR {
break
}
}
return e
}
var (
testHookLock sync.Mutex
testHookKillChildBeforePidfdSendSignal = func(_pid uintptr, _pidFD *os.File) {}
testHookKillChildAfterPidfdSendSignal = func(_pid uintptr, _pidFD *os.File) {}
)

View File

@@ -0,0 +1,243 @@
/*
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 mount
import (
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
kernel "github.com/containerd/containerd/v2/contrib/seccomp/kernelversion"
"github.com/containerd/continuity/testutil"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
var (
testUIDMaps = []syscall.SysProcIDMap{
{ContainerID: 1000, HostID: 0, Size: 100},
{ContainerID: 5000, HostID: 2000, Size: 100},
{ContainerID: 10000, HostID: 3000, Size: 100},
}
testGIDMaps = []syscall.SysProcIDMap{
{ContainerID: 1000, HostID: 0, Size: 100},
{ContainerID: 5000, HostID: 2000, Size: 100},
{ContainerID: 10000, HostID: 3000, Size: 100},
}
)
func TestGetUsernsFD(t *testing.T) {
testutil.RequiresRoot(t)
k512 := kernel.KernelVersion{Kernel: 5, Major: 12}
ok, err := kernel.GreaterEqualThan(k512)
require.NoError(t, err)
if !ok {
t.Skip("GetUsernsFD requires kernel >= 5.12")
}
t.Run("basic", testGetUsernsFDBasic)
t.Run("when kill child process before write u[g]id maps", testGetUsernsFDKillChildWhenWriteUGIDMaps)
t.Run("when kill child process after open u[g]id_map file", testGetUsernsFDKillChildAfterOpenUGIDMapFiles)
}
func testGetUsernsFDBasic(t *testing.T) {
for idx, tc := range []struct {
uidMaps string
gidMaps string
hasErr bool
}{
{
uidMaps: "0:1000:100",
gidMaps: "0:1000:100",
hasErr: false,
},
{
uidMaps: "100:1000:100",
gidMaps: "0:-1:100",
hasErr: true,
},
{
uidMaps: "100:1000:100",
gidMaps: "-1:1000:100",
hasErr: true,
},
{
uidMaps: "100:1000:100",
gidMaps: "0:1000:-1",
hasErr: true,
},
} {
t.Run(fmt.Sprintf("#%v", idx), func(t *testing.T) {
_, err := GetUsernsFD(tc.uidMaps, tc.gidMaps)
if tc.hasErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func testGetUsernsFDKillChildWhenWriteUGIDMaps(t *testing.T) {
hookFunc := func(reap bool) func(uintptr, *os.File) {
return func(_pid uintptr, pidFD *os.File) {
err := unix.PidfdSendSignal(int(pidFD.Fd()), unix.SIGKILL, nil, 0)
require.NoError(t, err)
if reap {
pidfdWaitid(pidFD)
}
}
}
for _, tcReap := range []bool{true, false} {
t.Run(fmt.Sprintf("#reap=%v", tcReap), func(t *testing.T) {
updateTestHookKillForGetUsernsFD(t, nil, hookFunc(tcReap))
usernsFD, err := getUsernsFD(testUIDMaps, testGIDMaps)
require.NoError(t, err)
defer usernsFD.Close()
srcDir, checkFunc := initIDMappedChecker(t, testUIDMaps, testGIDMaps)
destDir := t.TempDir()
defer func() {
require.NoError(t, UnmountAll(destDir, 0))
}()
err = IDMapMount(srcDir, destDir, int(usernsFD.Fd()))
usernsFD.Close()
require.NoError(t, err)
checkFunc(destDir)
})
}
}
func testGetUsernsFDKillChildAfterOpenUGIDMapFiles(t *testing.T) {
hookFunc := func(reap bool) func(uintptr, *os.File) {
return func(_pid uintptr, pidFD *os.File) {
err := unix.PidfdSendSignal(int(pidFD.Fd()), unix.SIGKILL, nil, 0)
require.NoError(t, err)
if reap {
pidfdWaitid(pidFD)
}
}
}
for _, tc := range []struct {
reap bool
expected error
}{
{
reap: false,
expected: nil,
},
{
reap: true,
expected: syscall.ESRCH,
},
} {
t.Run(fmt.Sprintf("#reap=%v", tc.reap), func(t *testing.T) {
updateTestHookKillForGetUsernsFD(t, hookFunc(tc.reap), nil)
usernsFD, err := getUsernsFD(testUIDMaps, testGIDMaps)
if tc.expected != nil {
require.Error(t, tc.expected, err)
return
}
require.NoError(t, err)
defer usernsFD.Close()
srcDir, checkFunc := initIDMappedChecker(t, testUIDMaps, testGIDMaps)
destDir := t.TempDir()
defer func() {
require.NoError(t, UnmountAll(destDir, 0))
}()
err = IDMapMount(srcDir, destDir, int(usernsFD.Fd()))
usernsFD.Close()
require.NoError(t, err)
checkFunc(destDir)
})
}
}
func updateTestHookKillForGetUsernsFD(t *testing.T, newBeforeFunc, newAfterFunc func(uintptr, *os.File)) {
testHookLock.Lock()
oldBefore := testHookKillChildBeforePidfdSendSignal
oldAfter := testHookKillChildAfterPidfdSendSignal
t.Cleanup(func() {
testHookKillChildBeforePidfdSendSignal = oldBefore
testHookKillChildAfterPidfdSendSignal = oldAfter
testHookLock.Unlock()
})
if newBeforeFunc != nil {
testHookKillChildBeforePidfdSendSignal = newBeforeFunc
}
if newAfterFunc != nil {
testHookKillChildAfterPidfdSendSignal = newAfterFunc
}
}
func initIDMappedChecker(t *testing.T, uidMaps, gidMaps []syscall.SysProcIDMap) (_srcDir string, _verifyFunc func(destDir string)) {
testutil.RequiresRoot(t)
srcDir := t.TempDir()
require.Equal(t, len(uidMaps), len(gidMaps))
for idx := range uidMaps {
file := filepath.Join(srcDir, fmt.Sprintf("%v", idx))
f, err := os.Create(file)
require.NoError(t, err, fmt.Sprintf("create file %s", file))
defer f.Close()
uid, gid := uidMaps[idx].ContainerID, gidMaps[idx].ContainerID
err = f.Chown(uid, gid)
require.NoError(t, err, fmt.Sprintf("chown %v:%v for file %s", uid, gid, file))
}
return srcDir, func(destDir string) {
for idx := range uidMaps {
file := filepath.Join(destDir, fmt.Sprintf("%v", idx))
f, err := os.Open(file)
require.NoError(t, err, fmt.Sprintf("open file %s", file))
defer f.Close()
stat, err := f.Stat()
require.NoError(t, err, fmt.Sprintf("stat file %s", file))
sysStat := stat.Sys().(*syscall.Stat_t)
uid, gid := uidMaps[idx].HostID, gidMaps[idx].HostID
require.Equal(t, uint32(uid), sysStat.Uid, fmt.Sprintf("check file %s uid", file))
require.Equal(t, uint32(gid), sysStat.Gid, fmt.Sprintf("check file %s gid", file))
t.Logf("IDMapped File %s uid=%v, gid=%v", file, uid, gid)
}
}
}

560
core/mount/mount_linux.go Normal file
View File

@@ -0,0 +1,560 @@
/*
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 mount
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
type mountOpt struct {
flags int
data []string
losetup bool
uidmap string
gidmap string
}
var (
pagesize = 4096
allowedHelperBinaries = []string{"mount.fuse", "mount.fuse3"}
)
func init() {
pagesize = os.Getpagesize()
}
// prepareIDMappedOverlay is a helper function to obtain
// actual "lowerdir=..." mount options. It creates and
// applies id mapping for each lowerdir.
//
// It returns:
// 1. New options that include new "lowedir=..." mount option.
// 2. "Clean up" function -- it should be called as a defer one before
// checking for error, because if do the second and avoid calling "clean up",
// you're going to have "dirty" setup -- there's no guarantee that those
// temporary mount points for lowedirs will be cleaned properly.
// 3. Error -- nil if everything's fine, otherwise an error.
func prepareIDMappedOverlay(usernsFd int, options []string) ([]string, func(), error) {
lowerIdx, lowerDirs := findOverlayLowerdirs(options)
if lowerIdx == -1 {
return options, nil, fmt.Errorf("failed to parse overlay lowerdir's from given options")
}
tmpLowerdirs, idMapCleanUp, err := doPrepareIDMappedOverlay(lowerDirs, usernsFd)
if err != nil {
return options, idMapCleanUp, fmt.Errorf("failed to create idmapped mount: %w", err)
}
options = append(options[:lowerIdx], options[lowerIdx+1:]...)
options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(tmpLowerdirs, ":")))
return options, idMapCleanUp, nil
}
// Mount to the provided target path.
//
// If m.Type starts with "fuse." or "fuse3.", "mount.fuse" or "mount.fuse3"
// helper binary is called.
func (m *Mount) mount(target string) (err error) {
for _, helperBinary := range allowedHelperBinaries {
// helperBinary = "mount.fuse", typePrefix = "fuse."
typePrefix := strings.TrimPrefix(helperBinary, "mount.") + "."
if strings.HasPrefix(m.Type, typePrefix) {
return m.mountWithHelper(helperBinary, typePrefix, target)
}
}
var (
chdir string
recalcOpt bool
usernsFd *os.File
options = m.Options
)
opt := parseMountOptions(options)
// The only remapping of both GID and UID is supported
if opt.uidmap != "" && opt.gidmap != "" {
if usernsFd, err = GetUsernsFD(opt.uidmap, opt.gidmap); err != nil {
return err
}
defer usernsFd.Close()
// overlay expects lowerdir's to be remapped instead
if m.Type == "overlay" {
var (
userNsCleanUp func()
)
options, userNsCleanUp, err = prepareIDMappedOverlay(int(usernsFd.Fd()), options)
defer userNsCleanUp()
if err != nil {
return fmt.Errorf("failed to prepare idmapped overlay: %w", err)
}
// To not meet concurrency issues while using the same lowedirs
// for different containers, replace them by temporary directories,
if optionsSize(options) >= pagesize-512 {
recalcOpt = true
} else {
opt = parseMountOptions(options)
}
}
}
// avoid hitting one page limit of mount argument buffer
//
// NOTE: 512 is a buffer during pagesize check.
if m.Type == "overlay" && optionsSize(options) >= pagesize-512 {
chdir, options = compactLowerdirOption(options)
// recalculate opt in case of lowerdirs have been replaced
// by idmapped ones OR idmapped mounts' not used/supported.
if recalcOpt || (opt.uidmap == "" || opt.gidmap == "") {
opt = parseMountOptions(options)
}
}
// propagation types.
const ptypes = unix.MS_SHARED | unix.MS_PRIVATE | unix.MS_SLAVE | unix.MS_UNBINDABLE
// Ensure propagation type change flags aren't included in other calls.
oflags := opt.flags &^ ptypes
var loopParams LoopParams
if opt.losetup {
loopParams = LoopParams{
Readonly: oflags&unix.MS_RDONLY == unix.MS_RDONLY,
Autoclear: true,
}
loopParams.Direct, opt.data = hasDirectIO(opt.data)
}
dataInStr := strings.Join(opt.data, ",")
if len(dataInStr) > pagesize {
return errors.New("mount options is too long")
}
// In the case of remounting with changed data (dataInStr != ""), need to call mount (moby/moby#34077).
if opt.flags&unix.MS_REMOUNT == 0 || dataInStr != "" {
// Initial call applying all non-propagation flags for mount
// or remount with changed data
source := m.Source
if opt.losetup {
loFile, err := setupLoop(m.Source, loopParams)
if err != nil {
return err
}
defer loFile.Close()
// Mount the loop device instead
source = loFile.Name()
}
if err := mountAt(chdir, source, target, m.Type, uintptr(oflags), dataInStr); err != nil {
return err
}
}
if opt.flags&ptypes != 0 {
// Change the propagation type.
const pflags = ptypes | unix.MS_REC | unix.MS_SILENT
if err := unix.Mount("", target, "", uintptr(opt.flags&pflags), ""); err != nil {
return err
}
}
const broflags = unix.MS_BIND | unix.MS_RDONLY
if oflags&broflags == broflags {
// Remount the bind to apply read only.
return unix.Mount("", target, "", uintptr(oflags|unix.MS_REMOUNT), "")
}
// remap non-overlay mount point
if opt.uidmap != "" && opt.gidmap != "" && m.Type != "overlay" {
if err := IDMapMount(target, target, int(usernsFd.Fd())); err != nil {
return err
}
}
return nil
}
func doPrepareIDMappedOverlay(lowerDirs []string, usernsFd int) (tmpLowerDirs []string, _ func(), _ error) {
td, err := os.MkdirTemp(tempMountLocation, "ovl-idmapped")
if err != nil {
return nil, nil, err
}
cleanUp := func() {
for _, lowerDir := range tmpLowerDirs {
if err := unix.Unmount(lowerDir, 0); err != nil {
logrus.WithError(err).Warnf("failed to unmount temp lowerdir %s", lowerDir)
}
}
if terr := os.RemoveAll(filepath.Clean(filepath.Join(tmpLowerDirs[0], ".."))); terr != nil {
logrus.WithError(terr).Warnf("failed to remove temporary overlay lowerdir's")
}
}
for i, lowerDir := range lowerDirs {
tmpLowerDir := filepath.Join(td, strconv.Itoa(i))
tmpLowerDirs = append(tmpLowerDirs, tmpLowerDir)
if err = os.MkdirAll(tmpLowerDir, 0700); err != nil {
return nil, cleanUp, fmt.Errorf("failed to create temporary dir: %w", err)
}
if err = IDMapMount(lowerDir, tmpLowerDir, usernsFd); err != nil {
return nil, cleanUp, err
}
}
return tmpLowerDirs, cleanUp, nil
}
// Unmount the provided mount path with the flags
func Unmount(target string, flags int) error {
if err := unmount(target, flags); err != nil && err != unix.EINVAL {
return err
}
return nil
}
// fuseSuperMagic is defined in statfs(2)
const fuseSuperMagic = 0x65735546
func isFUSE(dir string) bool {
var st unix.Statfs_t
if err := unix.Statfs(dir, &st); err != nil {
return false
}
return st.Type == fuseSuperMagic
}
// unmountFUSE attempts to unmount using fusermount/fusermount3 helper binary.
//
// For FUSE mounts, using these helper binaries is preferred, see:
// https://github.com/containerd/containerd/pull/3765#discussion_r342083514
func unmountFUSE(target string) error {
var err error
for _, helperBinary := range []string{"fusermount3", "fusermount"} {
cmd := exec.Command(helperBinary, "-u", target)
err = cmd.Run()
if err == nil {
return nil
}
}
return err
}
func unmount(target string, flags int) error {
if isFUSE(target) {
if err := unmountFUSE(target); err == nil {
return nil
}
}
for i := 0; i < 50; i++ {
if err := unix.Unmount(target, flags); err != nil {
switch err {
case unix.EBUSY:
time.Sleep(50 * time.Millisecond)
continue
default:
return err
}
}
return nil
}
return fmt.Errorf("failed to unmount target %s: %w", target, unix.EBUSY)
}
// UnmountAll repeatedly unmounts the given mount point until there
// are no mounts remaining (EINVAL is returned by mount), which is
// useful for undoing a stack of mounts on the same mount point.
// UnmountAll all is noop when the first argument is an empty string.
// This is done when the containerd client did not specify any rootfs
// mounts (e.g. because the rootfs is managed outside containerd)
// UnmountAll is noop when the mount path does not exist.
func UnmountAll(mount string, flags int) error {
if mount == "" {
return nil
}
if _, err := os.Stat(mount); os.IsNotExist(err) {
return nil
}
for {
if err := unmount(mount, flags); err != nil {
// EINVAL is returned if the target is not a
// mount point, indicating that we are
// done. It can also indicate a few other
// things (such as invalid flags) which we
// unfortunately end up squelching here too.
if err == unix.EINVAL {
return nil
}
return err
}
}
}
// parseMountOptions takes fstab style mount options and parses them for
// use with a standard mount() syscall
func parseMountOptions(options []string) (opt mountOpt) {
loopOpt := "loop"
flagsMap := map[string]struct {
clear bool
flag int
}{
"async": {true, unix.MS_SYNCHRONOUS},
"atime": {true, unix.MS_NOATIME},
"bind": {false, unix.MS_BIND},
"defaults": {false, 0},
"dev": {true, unix.MS_NODEV},
"diratime": {true, unix.MS_NODIRATIME},
"dirsync": {false, unix.MS_DIRSYNC},
"exec": {true, unix.MS_NOEXEC},
"mand": {false, unix.MS_MANDLOCK},
"noatime": {false, unix.MS_NOATIME},
"nodev": {false, unix.MS_NODEV},
"nodiratime": {false, unix.MS_NODIRATIME},
"noexec": {false, unix.MS_NOEXEC},
"nomand": {true, unix.MS_MANDLOCK},
"norelatime": {true, unix.MS_RELATIME},
"nostrictatime": {true, unix.MS_STRICTATIME},
"nosuid": {false, unix.MS_NOSUID},
"rbind": {false, unix.MS_BIND | unix.MS_REC},
"relatime": {false, unix.MS_RELATIME},
"remount": {false, unix.MS_REMOUNT},
"ro": {false, unix.MS_RDONLY},
"rw": {true, unix.MS_RDONLY},
"strictatime": {false, unix.MS_STRICTATIME},
"suid": {true, unix.MS_NOSUID},
"sync": {false, unix.MS_SYNCHRONOUS},
}
for _, o := range options {
// If the option does not exist in the flags table or the flag
// is not supported on the platform,
// then it is a data value for a specific fs type
if f, exists := flagsMap[o]; exists && f.flag != 0 {
if f.clear {
opt.flags &^= f.flag
} else {
opt.flags |= f.flag
}
} else if o == loopOpt {
opt.losetup = true
} else if strings.HasPrefix(o, "uidmap=") {
opt.uidmap = strings.TrimPrefix(o, "uidmap=")
} else if strings.HasPrefix(o, "gidmap=") {
opt.gidmap = strings.TrimPrefix(o, "gidmap=")
} else {
opt.data = append(opt.data, o)
}
}
return
}
func hasDirectIO(opts []string) (bool, []string) {
for idx, opt := range opts {
if opt == "direct-io" {
return true, append(opts[:idx], opts[idx+1:]...)
}
}
return false, opts
}
// compactLowerdirOption updates overlay lowdir option and returns the common
// dir among all the lowdirs.
func compactLowerdirOption(opts []string) (string, []string) {
idx, dirs := findOverlayLowerdirs(opts)
if idx == -1 || len(dirs) == 1 {
// no need to compact if there is only one lowerdir
return "", opts
}
// find out common dir
commondir := longestCommonPrefix(dirs)
if commondir == "" {
return "", opts
}
// NOTE: the snapshot id is based on digits.
// in order to avoid to get snapshots/x, should be back to parent dir.
// however, there is assumption that the common dir is ${root}/io.containerd.v1.overlayfs/snapshots.
commondir = path.Dir(commondir)
if commondir == "/" || commondir == "." {
return "", opts
}
commondir = commondir + "/"
newdirs := make([]string, 0, len(dirs))
for _, dir := range dirs {
if len(dir) <= len(commondir) {
return "", opts
}
newdirs = append(newdirs, dir[len(commondir):])
}
newopts := copyOptions(opts)
newopts = append(newopts[:idx], newopts[idx+1:]...)
newopts = append(newopts, fmt.Sprintf("lowerdir=%s", strings.Join(newdirs, ":")))
return commondir, newopts
}
// findOverlayLowerdirs returns the index of lowerdir in mount's options and
// all the lowerdir target.
func findOverlayLowerdirs(opts []string) (int, []string) {
var (
idx = -1
prefix = "lowerdir="
)
for i, opt := range opts {
if strings.HasPrefix(opt, prefix) {
idx = i
break
}
}
if idx == -1 {
return -1, nil
}
return idx, strings.Split(opts[idx][len(prefix):], ":")
}
// longestCommonPrefix finds the longest common prefix in the string slice.
func longestCommonPrefix(strs []string) string {
if len(strs) == 0 {
return ""
} else if len(strs) == 1 {
return strs[0]
}
// find out the min/max value by alphabetical order
min, max := strs[0], strs[0]
for _, str := range strs[1:] {
if min > str {
min = str
}
if max < str {
max = str
}
}
// find out the common part between min and max
for i := 0; i < len(min) && i < len(max); i++ {
if min[i] != max[i] {
return min[:i]
}
}
return min
}
// copyOptions copies the options.
func copyOptions(opts []string) []string {
if len(opts) == 0 {
return nil
}
acopy := make([]string, len(opts))
copy(acopy, opts)
return acopy
}
// optionsSize returns the byte size of options of mount.
func optionsSize(opts []string) int {
size := 0
for _, opt := range opts {
size += len(opt)
}
return size
}
func mountAt(chdir string, source, target, fstype string, flags uintptr, data string) error {
if chdir == "" {
return unix.Mount(source, target, fstype, flags, data)
}
ch := make(chan error, 1)
go func() {
runtime.LockOSThread()
// Do not unlock this thread.
// If the thread is unlocked go will try to use it for other goroutines.
// However it is not possible to restore the thread state after CLONE_FS.
//
// Once the goroutine exits the thread should eventually be terminated by go.
if err := unix.Unshare(unix.CLONE_FS); err != nil {
ch <- err
return
}
if err := unix.Chdir(chdir); err != nil {
ch <- err
return
}
ch <- unix.Mount(source, target, fstype, flags, data)
}()
return <-ch
}
func (m *Mount) mountWithHelper(helperBinary, typePrefix, target string) error {
// helperBinary: "mount.fuse3"
// target: "/foo/merged"
// m.Type: "fuse3.fuse-overlayfs"
// command: "mount.fuse3 overlay /foo/merged -o lowerdir=/foo/lower2:/foo/lower1,upperdir=/foo/upper,workdir=/foo/work -t fuse-overlayfs"
args := []string{m.Source, target}
for _, o := range m.Options {
args = append(args, "-o", o)
}
args = append(args, "-t", strings.TrimPrefix(m.Type, typePrefix))
infoBeforeMount, err := Lookup(target)
if err != nil {
return err
}
// cmd.CombinedOutput() may intermittently return ECHILD because of our signal handling in shim.
// See #4387 and wait(2).
const retriesOnECHILD = 10
for i := 0; i < retriesOnECHILD; i++ {
cmd := exec.Command(helperBinary, args...)
out, err := cmd.CombinedOutput()
if err == nil {
return nil
}
if !errors.Is(err, unix.ECHILD) {
return fmt.Errorf("mount helper [%s %v] failed: %q: %w", helperBinary, args, string(out), err)
}
// We got ECHILD, we are not sure whether the mount was successful.
// If the mount ID has changed, we are sure we got some new mount, but still not sure it is fully completed.
// So we attempt to unmount the new mount before retrying.
infoAfterMount, err := Lookup(target)
if err != nil {
return err
}
if infoAfterMount.ID != infoBeforeMount.ID {
_ = unmount(target, 0)
}
}
return fmt.Errorf("mount helper [%s %v] failed with ECHILD (retried %d times)", helperBinary, args, retriesOnECHILD)
}

View File

@@ -0,0 +1,245 @@
/*
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 mount
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"reflect"
"testing"
"github.com/containerd/continuity/testutil"
"golang.org/x/sys/unix"
)
func TestLongestCommonPrefix(t *testing.T) {
tcases := []struct {
in []string
expected string
}{
{[]string{}, ""},
{[]string{"foo"}, "foo"},
{[]string{"foo", "bar"}, ""},
{[]string{"foo", "foo"}, "foo"},
{[]string{"foo", "foobar"}, "foo"},
{[]string{"foo", "", "foobar"}, ""},
}
for i, tc := range tcases {
if got := longestCommonPrefix(tc.in); got != tc.expected {
t.Fatalf("[%d case] expected (%s), but got (%s)", i+1, tc.expected, got)
}
}
}
func TestCompactLowerdirOption(t *testing.T) {
tcases := []struct {
opts []string
commondir string
newopts []string
}{
// no lowerdir or only one
{
[]string{"workdir=a"},
"",
[]string{"workdir=a"},
},
{
[]string{"workdir=a", "lowerdir=b"},
"",
[]string{"workdir=a", "lowerdir=b"},
},
// >= 2 lowerdir
{
[]string{"lowerdir=/snapshots/1/fs:/snapshots/10/fs"},
"/snapshots/",
[]string{"lowerdir=1/fs:10/fs"},
},
{
[]string{"lowerdir=/snapshots/1/fs:/snapshots/10/fs:/snapshots/2/fs"},
"/snapshots/",
[]string{"lowerdir=1/fs:10/fs:2/fs"},
},
// if common dir is /
{
[]string{"lowerdir=/snapshots/1/fs:/other_snapshots/1/fs"},
"",
[]string{"lowerdir=/snapshots/1/fs:/other_snapshots/1/fs"},
},
// if common dir is .
{
[]string{"lowerdir=a:aaa"},
"",
[]string{"lowerdir=a:aaa"},
},
}
for i, tc := range tcases {
dir, opts := compactLowerdirOption(tc.opts)
if dir != tc.commondir {
t.Fatalf("[%d case] expected common dir (%s), but got (%s)", i+1, tc.commondir, dir)
}
if !reflect.DeepEqual(opts, tc.newopts) {
t.Fatalf("[%d case] expected options (%v), but got (%v)", i+1, tc.newopts, opts)
}
}
}
func TestFUSEHelper(t *testing.T) {
testutil.RequiresRoot(t)
const fuseoverlayfsBinary = "fuse-overlayfs"
_, err := exec.LookPath(fuseoverlayfsBinary)
if err != nil {
t.Skip("fuse-overlayfs not installed")
}
td := t.TempDir()
for _, dir := range []string{"lower1", "lower2", "upper", "work", "merged"} {
if err := os.Mkdir(filepath.Join(td, dir), 0755); err != nil {
t.Fatal(err)
}
}
opts := 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"))
m := Mount{
Type: "fuse3." + fuseoverlayfsBinary,
Source: "overlay",
Options: []string{opts},
}
dest := filepath.Join(td, "merged")
if err := m.Mount(dest); err != nil {
t.Fatal(err)
}
if err := UnmountAll(dest, 0); err != nil {
t.Fatal(err)
}
}
func TestMountAt(t *testing.T) {
testutil.RequiresRoot(t)
dir1 := t.TempDir()
dir2 := t.TempDir()
defer unix.Unmount(filepath.Join(dir2, "bar"), unix.MNT_DETACH)
if err := os.WriteFile(filepath.Join(dir1, "foo"), []byte("foo"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir2, "bar"), []byte{}, 0644); err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// mount ${dir1}/foo at ${dir2}/bar
// But since we are using `mountAt` we only need to specify the relative path to dir2 as the target mountAt will chdir to there.
if err := mountAt(dir2, filepath.Join(dir1, "foo"), "bar", "none", unix.MS_BIND, ""); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(filepath.Join(dir2, "bar"))
if err != nil {
t.Fatal(err)
}
if string(b) != "foo" {
t.Fatalf("unexpected file content: %s", b)
}
newWD, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if wd != newWD {
t.Fatalf("unexpected working directory: %s", newWD)
}
}
func TestUnmountMounts(t *testing.T) {
testutil.RequiresRoot(t)
target, mounts := setupMounts(t)
if err := UnmountMounts(mounts, target, 0); err != nil {
t.Fatal(err)
}
}
func TestUnmountRecursive(t *testing.T) {
testutil.RequiresRoot(t)
target, _ := setupMounts(t)
if err := UnmountRecursive(target, 0); err != nil {
t.Fatal(err)
}
}
func setupMounts(t *testing.T) (target string, mounts []Mount) {
dir1 := t.TempDir()
dir2 := t.TempDir()
if err := os.Mkdir(filepath.Join(dir1, "foo"), 0755); err != nil {
t.Fatal(err)
}
mounts = append(mounts, Mount{
Type: "bind",
Source: dir1,
Options: []string{
"ro",
"rbind",
},
})
if err := os.WriteFile(filepath.Join(dir2, "bar"), []byte("bar"), 0644); err != nil {
t.Fatal(err)
}
mounts = append(mounts, Mount{
Type: "bind",
Source: dir2,
Target: "foo",
Options: []string{
"ro",
"rbind",
},
})
target = t.TempDir()
if err := All(mounts, target); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(filepath.Join(target, "foo/bar"))
if err != nil {
t.Fatal(err)
}
if string(b) != "bar" {
t.Fatalf("unexpected file content: %s", b)
}
return target, mounts
}

150
core/mount/mount_test.go Normal file
View File

@@ -0,0 +1,150 @@
/*
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 mount
import (
"reflect"
"testing"
// required for `-test.root` flag not to fail
_ "github.com/containerd/continuity/testutil"
)
func TestReadonlyMounts(t *testing.T) {
testCases := []struct {
desc string
input []Mount
expected []Mount
}{
{
desc: "empty slice",
input: []Mount{},
expected: []Mount{},
},
{
desc: "removes `upperdir` and `workdir` from overlay mounts, appends upper layer to lower",
input: []Mount{
{
Type: "overlay",
Source: "overlay",
Options: []string{
"index=off",
"workdir=/path/to/snapshots/4/work",
"upperdir=/path/to/snapshots/4/fs",
"lowerdir=/path/to/snapshots/1/fs",
},
},
{
Type: "overlay",
Source: "overlay",
Options: []string{
"index=on",
"lowerdir=/another/path/to/snapshots/2/fs",
},
},
},
expected: []Mount{
{
Type: "overlay",
Source: "overlay",
Options: []string{
"index=off",
"lowerdir=/path/to/snapshots/4/fs:/path/to/snapshots/1/fs",
},
},
{
Type: "overlay",
Source: "overlay",
Options: []string{
"index=on",
"lowerdir=/another/path/to/snapshots/2/fs",
},
},
},
},
{
desc: "removes `rw` and appends `ro` (once) to other mount types",
input: []Mount{
{
Type: "mount-without-rw",
Source: "",
Options: []string{
"index=off",
"workdir=/path/to/other/snapshots/work",
"upperdir=/path/to/other/snapshots/2",
"lowerdir=/path/to/other/snapshots/1",
},
},
{
Type: "mount-with-rw",
Source: "",
Options: []string{
"an-option=a-value",
"another_opt=/another/value",
"rw",
},
},
{
Type: "mount-with-ro",
Source: "",
Options: []string{
"an-option=a-value",
"another_opt=/another/value",
"ro",
},
},
},
expected: []Mount{
{
Type: "mount-without-rw",
Source: "",
Options: []string{
"index=off",
"workdir=/path/to/other/snapshots/work",
"upperdir=/path/to/other/snapshots/2",
"lowerdir=/path/to/other/snapshots/1",
"ro",
},
},
{
Type: "mount-with-rw",
Source: "",
Options: []string{
"an-option=a-value",
"another_opt=/another/value",
"ro",
},
},
{
Type: "mount-with-ro",
Source: "",
Options: []string{
"an-option=a-value",
"another_opt=/another/value",
"ro",
},
},
},
},
}
for _, tc := range testCases {
if !reflect.DeepEqual(readonlyMounts(tc.input), tc.expected) {
t.Fatalf("incorrectly modified mounts: %s", tc.desc)
}
}
}

71
core/mount/mount_unix.go Normal file
View File

@@ -0,0 +1,71 @@
//go:build !windows && !darwin && !openbsd
/*
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 mount
import (
"os"
"sort"
"github.com/moby/sys/mountinfo"
)
// UnmountRecursive unmounts the target and all mounts underneath, starting
// with the deepest mount first.
func UnmountRecursive(target string, flags int) error {
if target == "" {
return nil
}
target, err := CanonicalizePath(target)
if err != nil {
if os.IsNotExist(err) {
err = nil
}
return err
}
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(target))
if err != nil {
return err
}
targetSet := make(map[string]struct{})
for _, m := range mounts {
targetSet[m.Mountpoint] = struct{}{}
}
var targets []string
for m := range targetSet {
targets = append(targets, m)
}
// Make the deepest mount be first
sort.SliceStable(targets, func(i, j int) bool {
return len(targets[i]) > len(targets[j])
})
for i, target := range targets {
if err := UnmountAll(target, flags); err != nil {
if i == len(targets)-1 { // last mount
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,46 @@
//go:build darwin || openbsd
/*
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 mount
import "errors"
var (
// ErrNotImplementOnUnix is returned for methods that are not implemented
ErrNotImplementOnUnix = errors.New("not implemented under unix")
)
// Mount is not implemented on this platform
func (m *Mount) mount(target string) error {
return ErrNotImplementOnUnix
}
// Unmount is not implemented on this platform
func Unmount(mount string, flags int) error {
return ErrNotImplementOnUnix
}
// UnmountAll is not implemented on this platform
func UnmountAll(mount string, flags int) error {
return ErrNotImplementOnUnix
}
// UnmountRecursive is not implemented on this platform
func UnmountRecursive(mount string, flags int) error {
return ErrNotImplementOnUnix
}

190
core/mount/mount_windows.go Normal file
View File

@@ -0,0 +1,190 @@
/*
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 mount
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio/pkg/bindfilter"
"github.com/Microsoft/hcsshim"
"github.com/containerd/log"
"golang.org/x/sys/windows"
)
const sourceStreamName = "containerd.io-source"
var (
// ErrNotImplementOnWindows is returned when an action is not implemented for windows
ErrNotImplementOnWindows = errors.New("not implemented under windows")
)
// Mount to the provided target.
func (m *Mount) mount(target string) (retErr error) {
if m.Type != "windows-layer" {
return fmt.Errorf("invalid windows mount type: '%s'", m.Type)
}
home, layerID := filepath.Split(m.Source)
parentLayerPaths, err := m.GetParentPaths()
if err != nil {
return err
}
var di = hcsshim.DriverInfo{
HomeDir: home,
}
if err := hcsshim.ActivateLayer(di, layerID); err != nil {
return fmt.Errorf("failed to activate layer %s: %w", m.Source, err)
}
defer func() {
if retErr != nil {
if layerErr := hcsshim.DeactivateLayer(di, layerID); layerErr != nil {
log.G(context.TODO()).WithError(layerErr).Error("failed to deactivate layer during mount failure cleanup")
}
}
}()
if err := hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil {
return fmt.Errorf("failed to prepare layer %s: %w", m.Source, err)
}
defer func() {
if retErr != nil {
if layerErr := hcsshim.UnprepareLayer(di, layerID); layerErr != nil {
log.G(context.TODO()).WithError(layerErr).Error("failed to unprepare layer during mount failure cleanup")
}
}
}()
volume, err := hcsshim.GetLayerMountPath(di, layerID)
if err != nil {
return fmt.Errorf("failed to get volume path for layer %s: %w", m.Source, err)
}
if len(parentLayerPaths) == 0 {
// this is a base layer. It gets mounted without going through WCIFS. We need to mount the Files
// folder, not the actual source, or the client may inadvertently remove metadata files.
volume = filepath.Join(volume, "Files")
if _, err := os.Stat(volume); err != nil {
return fmt.Errorf("no Files folder in layer %s", layerID)
}
}
if err := bindfilter.ApplyFileBinding(target, volume, m.ReadOnly()); err != nil {
return fmt.Errorf("failed to set volume mount path for layer %s: %w", m.Source, err)
}
defer func() {
if retErr != nil {
if bindErr := bindfilter.RemoveFileBinding(target); bindErr != nil {
log.G(context.TODO()).WithError(bindErr).Error("failed to remove binding during mount failure cleanup")
}
}
}()
// Add an Alternate Data Stream to record the layer source.
// See https://docs.microsoft.com/en-au/archive/blogs/askcore/alternate-data-streams-in-ntfs
// for details on Alternate Data Streams.
if err := os.WriteFile(filepath.Clean(target)+":"+sourceStreamName, []byte(m.Source), 0666); err != nil {
return fmt.Errorf("failed to record source for layer %s: %w", m.Source, err)
}
return nil
}
// ParentLayerPathsFlag is the options flag used to represent the JSON encoded
// list of parent layers required to use the layer
const ParentLayerPathsFlag = "parentLayerPaths="
// GetParentPaths of the mount
func (m *Mount) GetParentPaths() ([]string, error) {
var parentLayerPaths []string
for _, option := range m.Options {
if strings.HasPrefix(option, ParentLayerPathsFlag) {
err := json.Unmarshal([]byte(option[len(ParentLayerPathsFlag):]), &parentLayerPaths)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal parent layer paths from mount: %w", err)
}
}
}
return parentLayerPaths, nil
}
// Unmount the mount at the provided path
func Unmount(mount string, flags int) error {
mount = filepath.Clean(mount)
adsFile := mount + ":" + sourceStreamName
var layerPath string
if _, err := os.Lstat(adsFile); err == nil {
layerPathb, err := os.ReadFile(mount + ":" + sourceStreamName)
if err != nil {
return fmt.Errorf("failed to retrieve source for layer %s: %w", mount, err)
}
layerPath = string(layerPathb)
}
if err := bindfilter.RemoveFileBinding(mount); err != nil {
if errors.Is(err, windows.ERROR_INVALID_PARAMETER) || errors.Is(err, windows.ERROR_NOT_FOUND) {
// not a mount point
return nil
}
return fmt.Errorf("removing mount: %w", err)
}
if layerPath != "" {
var (
home, layerID = filepath.Split(layerPath)
di = hcsshim.DriverInfo{
HomeDir: home,
}
)
if err := hcsshim.UnprepareLayer(di, layerID); err != nil {
return fmt.Errorf("failed to unprepare layer %s: %w", mount, err)
}
if err := hcsshim.DeactivateLayer(di, layerID); err != nil {
return fmt.Errorf("failed to deactivate layer %s: %w", mount, err)
}
}
return nil
}
// UnmountAll unmounts from the provided path
func UnmountAll(mount string, flags int) error {
if mount == "" {
// This isn't an error, per the EINVAL handling in the Linux version
return nil
}
if _, err := os.Stat(mount); os.IsNotExist(err) {
return nil
}
return Unmount(mount, flags)
}
// UnmountRecursive unmounts from the provided path
func UnmountRecursive(mount string, flags int) error {
return UnmountAll(mount, flags)
}

23
core/mount/mountinfo.go Normal file
View File

@@ -0,0 +1,23 @@
/*
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 mount
import "github.com/moby/sys/mountinfo"
// Info reveals information about a particular mounted filesystem. This
// struct is populated from the content in the /proc/<pid>/mountinfo file.
type Info = mountinfo.Info

82
core/mount/temp.go Normal file
View File

@@ -0,0 +1,82 @@
/*
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 mount
import (
"context"
"fmt"
"os"
"github.com/containerd/log"
)
var tempMountLocation = getTempDir()
// WithTempMount mounts the provided mounts to a temp dir, and pass the temp dir to f.
// The mounts are valid during the call to the f.
// Finally we will unmount and remove the temp dir regardless of the result of f.
func WithTempMount(ctx context.Context, mounts []Mount, f func(root string) error) (err error) {
root, uerr := os.MkdirTemp(tempMountLocation, "containerd-mount")
if uerr != nil {
return fmt.Errorf("failed to create temp dir: %w", uerr)
}
// We use Remove here instead of RemoveAll.
// The RemoveAll will delete the temp dir and all children it contains.
// When the Unmount fails, RemoveAll will incorrectly delete data from
// the mounted dir. However, if we use Remove, even though we won't
// successfully delete the temp dir and it may leak, we won't loss data
// from the mounted dir.
// For details, please refer to #1868 #1785.
defer func() {
if uerr = os.Remove(root); uerr != nil {
log.G(ctx).WithError(uerr).WithField("dir", root).Error("failed to remove mount temp dir")
}
}()
// We should do defer first, if not we will not do Unmount when only a part of Mounts are failed.
defer func() {
if uerr = UnmountMounts(mounts, root, 0); uerr != nil {
uerr = fmt.Errorf("failed to unmount %s: %w", root, uerr)
if err == nil {
err = uerr
} else {
err = fmt.Errorf("%s: %w", uerr.Error(), err)
}
}
}()
if uerr = All(mounts, root); uerr != nil {
return fmt.Errorf("failed to mount %s: %w", root, uerr)
}
if err := f(root); err != nil {
return fmt.Errorf("mount callback failed on %s: %w", root, err)
}
return nil
}
// WithReadonlyTempMount mounts the provided mounts to a temp dir as readonly,
// and pass the temp dir to f. The mounts are valid during the call to the f.
// Finally we will unmount and remove the temp dir regardless of the result of f.
func WithReadonlyTempMount(ctx context.Context, mounts []Mount, f func(root string) error) (err error) {
return WithTempMount(ctx, readonlyMounts(mounts), f)
}
func getTempDir() string {
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
return xdg
}
return os.TempDir()
}

60
core/mount/temp_unix.go Normal file
View File

@@ -0,0 +1,60 @@
//go:build !windows && !darwin
/*
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 mount
import (
"os"
"sort"
"github.com/moby/sys/mountinfo"
)
// SetTempMountLocation sets the temporary mount location
func SetTempMountLocation(root string) error {
err := os.MkdirAll(root, 0700)
if err != nil {
return err
}
// We need to pass canonicalized path to mountinfo.PrefixFilter in CleanupTempMounts
tempMountLocation, err = CanonicalizePath(root)
return err
}
// CleanupTempMounts all temp mounts and remove the directories
func CleanupTempMounts(flags int) (warnings []error, err error) {
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(tempMountLocation))
if err != nil {
return nil, err
}
// Make the deepest mount be first
sort.Slice(mounts, func(i, j int) bool {
return len(mounts[i].Mountpoint) > len(mounts[j].Mountpoint)
})
for _, mount := range mounts {
if err := UnmountAll(mount.Mountpoint, flags); err != nil {
warnings = append(warnings, err)
continue
}
if err := os.Remove(mount.Mountpoint); err != nil {
warnings = append(warnings, err)
}
}
return warnings, nil
}

View File

@@ -0,0 +1,29 @@
//go:build windows || darwin
/*
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 mount
// SetTempMountLocation sets the temporary mount location
func SetTempMountLocation(root string) error {
return nil
}
// CleanupTempMounts all temp mounts and remove the directories
func CleanupTempMounts(flags int) ([]error, error) {
return nil, nil
}