diff --git a/src/runtime/virtcontainers/mount.go b/src/runtime/virtcontainers/mount.go index 5e75826199..243c13f330 100644 --- a/src/runtime/virtcontainers/mount.go +++ b/src/runtime/virtcontainers/mount.go @@ -6,29 +6,22 @@ package virtcontainers import ( - "context" "fmt" "os" "path/filepath" "strings" "syscall" - merr "github.com/hashicorp/go-multierror" volume "github.com/kata-containers/kata-containers/src/runtime/pkg/direct-volume" - "github.com/kata-containers/kata-containers/src/runtime/pkg/katautils/katatrace" "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/utils" "github.com/pkg/errors" "github.com/sirupsen/logrus" - otelLabel "go.opentelemetry.io/otel/attribute" ) // DefaultShmSize is the default shm size to be used in case host // IPC is used. const DefaultShmSize = 65536 * 1024 -// Sadly golang/sys doesn't have UmountNoFollow although it's there since Linux 2.6.34 -const UmountNoFollow = 0x8 - const ( rootfsDir = "rootfs" lowerDir = "lowerdir" @@ -50,13 +43,6 @@ func mountLogger() *logrus.Entry { return virtLog.WithField("subsystem", "mount") } -var propagationTypes = map[string]uintptr{ - "shared": syscall.MS_SHARED, - "private": syscall.MS_PRIVATE, - "slave": syscall.MS_SLAVE, - "ubind": syscall.MS_UNBINDABLE, -} - func isSystemMount(m string) bool { for _, p := range systemMountPrefixes { if m == p || strings.HasPrefix(m, p+"/") { @@ -248,83 +234,6 @@ func evalMountPath(source, destination string) (string, string, error) { return absSource, destination, nil } -// bindMount bind mounts a source in to a destination. This will -// do some bookkeeping: -// * evaluate all symlinks -// * ensure the source exists -// * recursively create the destination -// pgtypes stands for propagation types, which are shared, private, slave, and ubind. -func bindMount(ctx context.Context, source, destination string, readonly bool, pgtypes string) error { - span, _ := katatrace.Trace(ctx, nil, "bindMount", mountTracingTags) - defer span.End() - span.SetAttributes(otelLabel.String("source", source), otelLabel.String("destination", destination)) - - absSource, destination, err := evalMountPath(source, destination) - if err != nil { - return err - } - span.SetAttributes(otelLabel.String("source_after_eval", absSource)) - - if err := syscall.Mount(absSource, destination, "bind", syscall.MS_BIND, ""); err != nil { - return fmt.Errorf("Could not bind mount %v to %v: %v", absSource, destination, err) - } - - if pgtype, exist := propagationTypes[pgtypes]; exist { - if err := syscall.Mount("none", destination, "", pgtype, ""); err != nil { - return fmt.Errorf("Could not make mount point %v %s: %v", destination, pgtypes, err) - } - } else { - return fmt.Errorf("Wrong propagation type %s", pgtypes) - } - - // For readonly bind mounts, we need to remount with the readonly flag. - // This is needed as only very recent versions of libmount/util-linux support "bind,ro" - if readonly { - return syscall.Mount(absSource, destination, "bind", uintptr(syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY), "") - } - - return nil -} - -// An existing mount may be remounted by specifying `MS_REMOUNT` in -// mountflags. -// This allows you to change the mountflags of an existing mount. -// The mountflags should match the values used in the original mount() call, -// except for those parameters that you are trying to change. -func remount(ctx context.Context, mountflags uintptr, src string) error { - span, _ := katatrace.Trace(ctx, nil, "remount", mountTracingTags) - defer span.End() - span.SetAttributes(otelLabel.String("source", src)) - - absSrc, err := filepath.EvalSymlinks(src) - if err != nil { - return fmt.Errorf("Could not resolve symlink for %s", src) - } - span.SetAttributes(otelLabel.String("source_after_eval", absSrc)) - - if err := syscall.Mount(absSrc, absSrc, "", syscall.MS_REMOUNT|mountflags, ""); err != nil { - return fmt.Errorf("remount %s failed: %v", absSrc, err) - } - - return nil -} - -// remount a mount point as readonly -func remountRo(ctx context.Context, src string) error { - return remount(ctx, syscall.MS_BIND|syscall.MS_RDONLY, src) -} - -// bindMountContainerRootfs bind mounts a container rootfs into a 9pfs shared -// directory between the guest and the host. -func bindMountContainerRootfs(ctx context.Context, shareDir, cid, cRootFs string, readonly bool) error { - span, _ := katatrace.Trace(ctx, nil, "bindMountContainerRootfs", mountTracingTags) - defer span.End() - - rootfsDest := filepath.Join(shareDir, cid, rootfsDir) - - return bindMount(ctx, cRootFs, rootfsDest, readonly, "private") -} - // Mount describes a container mount. // nolint: govet type Mount struct { @@ -372,96 +281,6 @@ func isSymlink(path string) bool { return stat.Mode()&os.ModeSymlink != 0 } -func bindUnmountContainerShareDir(ctx context.Context, sharedDir, cID, target string) error { - destDir := filepath.Join(sharedDir, cID, target) - if isSymlink(filepath.Join(sharedDir, cID)) || isSymlink(destDir) { - mountLogger().WithField("container", cID).Warnf("container dir is a symlink, malicious guest?") - return nil - } - - err := syscall.Unmount(destDir, syscall.MNT_DETACH|UmountNoFollow) - if err == syscall.ENOENT { - mountLogger().WithError(err).WithField("share-dir", destDir).Warn() - return nil - } - if err := syscall.Rmdir(destDir); err != nil { - mountLogger().WithError(err).WithField("share-dir", destDir).Warn("Could not remove container share dir") - } - - return err -} - -func bindUnmountContainerRootfs(ctx context.Context, sharedDir, cID string) error { - span, _ := katatrace.Trace(ctx, nil, "bindUnmountContainerRootfs", mountTracingTags) - defer span.End() - span.SetAttributes(otelLabel.String("shared-dir", sharedDir), otelLabel.String("container-id", cID)) - return bindUnmountContainerShareDir(ctx, sharedDir, cID, rootfsDir) -} - -func bindUnmountContainerSnapshotDir(ctx context.Context, sharedDir, cID string) error { - span, _ := katatrace.Trace(ctx, nil, "bindUnmountContainerSnapshotDir", mountTracingTags) - defer span.End() - span.SetAttributes(otelLabel.String("shared-dir", sharedDir), otelLabel.String("container-id", cID)) - return bindUnmountContainerShareDir(ctx, sharedDir, cID, snapshotDir) -} - -func getVirtiofsDaemonForNydus(sandbox *Sandbox) (VirtiofsDaemon, error) { - var virtiofsDaemon VirtiofsDaemon - switch sandbox.GetHypervisorType() { - case string(QemuHypervisor): - virtiofsDaemon = sandbox.hypervisor.(*qemu).virtiofsDaemon - case string(ClhHypervisor): - virtiofsDaemon = sandbox.hypervisor.(*cloudHypervisor).virtiofsDaemon - default: - return nil, errNydusdNotSupport - } - return virtiofsDaemon, nil -} - -func nydusContainerCleanup(ctx context.Context, sharedDir string, c *Container) error { - sandbox := c.sandbox - virtiofsDaemon, err := getVirtiofsDaemonForNydus(sandbox) - if err != nil { - return err - } - if err := virtiofsDaemon.Umount(rafsMountPath(c.id)); err != nil { - return errors.Wrap(err, "umount rafs failed") - } - if err := bindUnmountContainerSnapshotDir(ctx, sharedDir, c.id); err != nil { - return errors.Wrap(err, "umount snapshotdir err") - } - destDir := filepath.Join(sharedDir, c.id, c.rootfsSuffix) - if err := syscall.Rmdir(destDir); err != nil { - return errors.Wrap(err, "remove container rootfs err") - } - return nil -} - -func bindUnmountAllRootfs(ctx context.Context, sharedDir string, sandbox *Sandbox) error { - span, ctx := katatrace.Trace(ctx, nil, "bindUnmountAllRootfs", mountTracingTags) - defer span.End() - span.SetAttributes(otelLabel.String("shared-dir", sharedDir), otelLabel.String("sandbox-id", sandbox.id)) - - var errors *merr.Error - for _, c := range sandbox.containers { - if isSymlink(filepath.Join(sharedDir, c.id)) { - mountLogger().WithField("container", c.id).Warnf("container dir is a symlink, malicious guest?") - continue - } - c.unmountHostMounts(ctx) - if c.state.Fstype == "" { - // even if error found, don't break out of loop until all mounts attempted - // to be unmounted, and collect all errors - if c.rootFs.Type == NydusRootFSType { - errors = merr.Append(errors, nydusContainerCleanup(ctx, sharedDir, c)) - } else { - errors = merr.Append(errors, bindUnmountContainerRootfs(ctx, sharedDir, c.id)) - } - } - } - return errors.ErrorOrNil() -} - const ( dockerVolumePrefix = "/var/lib/docker/volumes" dockerVolumeSuffix = "_data" diff --git a/src/runtime/virtcontainers/mount_darwin.go b/src/runtime/virtcontainers/mount_darwin.go new file mode 100644 index 0000000000..90af2a3aa3 --- /dev/null +++ b/src/runtime/virtcontainers/mount_darwin.go @@ -0,0 +1,12 @@ +// Copyright (c) 2023 Apple Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package virtcontainers + +import "context" + +func nydusContainerCleanup(ctx context.Context, sharedDir string, c *Container) error { + return nil +} diff --git a/src/runtime/virtcontainers/mount_linux.go b/src/runtime/virtcontainers/mount_linux.go new file mode 100644 index 0000000000..be76a93a69 --- /dev/null +++ b/src/runtime/virtcontainers/mount_linux.go @@ -0,0 +1,195 @@ +// Copyright (c) 2017 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package virtcontainers + +import ( + "context" + "fmt" + "path/filepath" + "syscall" + + merr "github.com/hashicorp/go-multierror" + "github.com/kata-containers/kata-containers/src/runtime/pkg/katautils/katatrace" + "github.com/pkg/errors" + otelLabel "go.opentelemetry.io/otel/attribute" +) + +// Sadly golang/sys doesn't have UmountNoFollow although it's there since Linux 2.6.34 +const UmountNoFollow = 0x8 + +var propagationTypes = map[string]uintptr{ + "shared": syscall.MS_SHARED, + "private": syscall.MS_PRIVATE, + "slave": syscall.MS_SLAVE, + "ubind": syscall.MS_UNBINDABLE, +} + +// bindMount bind mounts a source in to a destination. This will +// do some bookkeeping: +// * evaluate all symlinks +// * ensure the source exists +// * recursively create the destination +// pgtypes stands for propagation types, which are shared, private, slave, and ubind. +func bindMount(ctx context.Context, source, destination string, readonly bool, pgtypes string) error { + span, _ := katatrace.Trace(ctx, nil, "bindMount", mountTracingTags) + defer span.End() + span.SetAttributes(otelLabel.String("source", source), otelLabel.String("destination", destination)) + + absSource, destination, err := evalMountPath(source, destination) + if err != nil { + return err + } + span.SetAttributes(otelLabel.String("source_after_eval", absSource)) + + if err := syscall.Mount(absSource, destination, "bind", syscall.MS_BIND, ""); err != nil { + return fmt.Errorf("Could not bind mount %v to %v: %v", absSource, destination, err) + } + + if pgtype, exist := propagationTypes[pgtypes]; exist { + if err := syscall.Mount("none", destination, "", pgtype, ""); err != nil { + return fmt.Errorf("Could not make mount point %v %s: %v", destination, pgtypes, err) + } + } else { + return fmt.Errorf("Wrong propagation type %s", pgtypes) + } + + // For readonly bind mounts, we need to remount with the readonly flag. + // This is needed as only very recent versions of libmount/util-linux support "bind,ro" + if readonly { + return syscall.Mount(absSource, destination, "bind", uintptr(syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY), "") + } + + return nil +} + +// An existing mount may be remounted by specifying `MS_REMOUNT` in +// mountflags. +// This allows you to change the mountflags of an existing mount. +// The mountflags should match the values used in the original mount() call, +// except for those parameters that you are trying to change. +func remount(ctx context.Context, mountflags uintptr, src string) error { + span, _ := katatrace.Trace(ctx, nil, "remount", mountTracingTags) + defer span.End() + span.SetAttributes(otelLabel.String("source", src)) + + absSrc, err := filepath.EvalSymlinks(src) + if err != nil { + return fmt.Errorf("Could not resolve symlink for %s", src) + } + span.SetAttributes(otelLabel.String("source_after_eval", absSrc)) + + if err := syscall.Mount(absSrc, absSrc, "", syscall.MS_REMOUNT|mountflags, ""); err != nil { + return fmt.Errorf("remount %s failed: %v", absSrc, err) + } + + return nil +} + +// remount a mount point as readonly +func remountRo(ctx context.Context, src string) error { + return remount(ctx, syscall.MS_BIND|syscall.MS_RDONLY, src) +} + +// bindMountContainerRootfs bind mounts a container rootfs into a 9pfs shared +// directory between the guest and the host. +func bindMountContainerRootfs(ctx context.Context, shareDir, cid, cRootFs string, readonly bool) error { + span, _ := katatrace.Trace(ctx, nil, "bindMountContainerRootfs", mountTracingTags) + defer span.End() + + rootfsDest := filepath.Join(shareDir, cid, rootfsDir) + + return bindMount(ctx, cRootFs, rootfsDest, readonly, "private") +} + +func bindUnmountContainerShareDir(ctx context.Context, sharedDir, cID, target string) error { + destDir := filepath.Join(sharedDir, cID, target) + if isSymlink(filepath.Join(sharedDir, cID)) || isSymlink(destDir) { + mountLogger().WithField("container", cID).Warnf("container dir is a symlink, malicious guest?") + return nil + } + + err := syscall.Unmount(destDir, syscall.MNT_DETACH|UmountNoFollow) + if err == syscall.ENOENT { + mountLogger().WithError(err).WithField("share-dir", destDir).Warn() + return nil + } + if err := syscall.Rmdir(destDir); err != nil { + mountLogger().WithError(err).WithField("share-dir", destDir).Warn("Could not remove container share dir") + } + + return err +} + +func bindUnmountContainerRootfs(ctx context.Context, sharedDir, cID string) error { + span, _ := katatrace.Trace(ctx, nil, "bindUnmountContainerRootfs", mountTracingTags) + defer span.End() + span.SetAttributes(otelLabel.String("shared-dir", sharedDir), otelLabel.String("container-id", cID)) + return bindUnmountContainerShareDir(ctx, sharedDir, cID, rootfsDir) +} + +func bindUnmountContainerSnapshotDir(ctx context.Context, sharedDir, cID string) error { + span, _ := katatrace.Trace(ctx, nil, "bindUnmountContainerSnapshotDir", mountTracingTags) + defer span.End() + span.SetAttributes(otelLabel.String("shared-dir", sharedDir), otelLabel.String("container-id", cID)) + return bindUnmountContainerShareDir(ctx, sharedDir, cID, snapshotDir) +} + +func getVirtiofsDaemonForNydus(sandbox *Sandbox) (VirtiofsDaemon, error) { + var virtiofsDaemon VirtiofsDaemon + switch sandbox.GetHypervisorType() { + case string(QemuHypervisor): + virtiofsDaemon = sandbox.hypervisor.(*qemu).virtiofsDaemon + case string(ClhHypervisor): + virtiofsDaemon = sandbox.hypervisor.(*cloudHypervisor).virtiofsDaemon + default: + return nil, errNydusdNotSupport + } + return virtiofsDaemon, nil +} + +func nydusContainerCleanup(ctx context.Context, sharedDir string, c *Container) error { + sandbox := c.sandbox + virtiofsDaemon, err := getVirtiofsDaemonForNydus(sandbox) + if err != nil { + return err + } + if err := virtiofsDaemon.Umount(rafsMountPath(c.id)); err != nil { + return errors.Wrap(err, "umount rafs failed") + } + if err := bindUnmountContainerSnapshotDir(ctx, sharedDir, c.id); err != nil { + return errors.Wrap(err, "umount snapshotdir err") + } + destDir := filepath.Join(sharedDir, c.id, c.rootfsSuffix) + if err := syscall.Rmdir(destDir); err != nil { + return errors.Wrap(err, "remove container rootfs err") + } + return nil +} + +func bindUnmountAllRootfs(ctx context.Context, sharedDir string, sandbox *Sandbox) error { + span, ctx := katatrace.Trace(ctx, nil, "bindUnmountAllRootfs", mountTracingTags) + defer span.End() + span.SetAttributes(otelLabel.String("shared-dir", sharedDir), otelLabel.String("sandbox-id", sandbox.id)) + + var errors *merr.Error + for _, c := range sandbox.containers { + if isSymlink(filepath.Join(sharedDir, c.id)) { + mountLogger().WithField("container", c.id).Warnf("container dir is a symlink, malicious guest?") + continue + } + c.unmountHostMounts(ctx) + if c.state.Fstype == "" { + // even if error found, don't break out of loop until all mounts attempted + // to be unmounted, and collect all errors + if c.rootFs.Type == NydusRootFSType { + errors = merr.Append(errors, nydusContainerCleanup(ctx, sharedDir, c)) + } else { + errors = merr.Append(errors, bindUnmountContainerRootfs(ctx, sharedDir, c.id)) + } + } + } + return errors.ErrorOrNil() +} diff --git a/src/runtime/virtcontainers/mount_linux_test.go b/src/runtime/virtcontainers/mount_linux_test.go new file mode 100644 index 0000000000..a34f7c28f3 --- /dev/null +++ b/src/runtime/virtcontainers/mount_linux_test.go @@ -0,0 +1,321 @@ +// Copyright (c) 2017 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package virtcontainers + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" + + ktu "github.com/kata-containers/kata-containers/src/runtime/pkg/katatestutils" +) + +func TestIsEphemeralStorage(t *testing.T) { + assert := assert.New(t) + if tc.NotValid(ktu.NeedRoot()) { + t.Skip(ktu.TestDisabledNeedRoot) + } + + dir, err := os.MkdirTemp(testDir, "foo") + assert.NoError(err) + defer os.RemoveAll(dir) + + sampleEphePath := filepath.Join(dir, K8sEmptyDir, "tmp-volume") + err = os.MkdirAll(sampleEphePath, testDirMode) + assert.Nil(err) + + err = syscall.Mount("tmpfs", sampleEphePath, "tmpfs", 0, "") + assert.NoError(err) + defer syscall.Unmount(sampleEphePath, 0) + + isEphe := IsEphemeralStorage(sampleEphePath) + assert.True(isEphe) + + isHostEmptyDir := Isk8sHostEmptyDir(sampleEphePath) + assert.False(isHostEmptyDir) + + sampleEphePath = "/var/lib/kubelet/pods/366c3a75-4869-11e8-b479-507b9ddd5ce4/volumes/cache-volume" + isEphe = IsEphemeralStorage(sampleEphePath) + assert.False(isEphe) + + isHostEmptyDir = Isk8sHostEmptyDir(sampleEphePath) + assert.False(isHostEmptyDir) +} + +func TestGetDeviceForPathBindMount(t *testing.T) { + assert := assert.New(t) + + if tc.NotValid(ktu.NeedRoot()) { + t.Skip(ktu.TestDisabledNeedRoot) + } + + source := filepath.Join(testDir, "testDeviceDirSrc") + dest := filepath.Join(testDir, "testDeviceDirDest") + syscall.Unmount(dest, 0) + os.Remove(source) + os.Remove(dest) + + err := os.MkdirAll(source, mountPerm) + assert.NoError(err) + + defer os.Remove(source) + + err = os.MkdirAll(dest, mountPerm) + assert.NoError(err) + + defer os.Remove(dest) + + err = bindMount(context.Background(), source, dest, false, "private") + assert.NoError(err) + + defer syscall.Unmount(dest, syscall.MNT_DETACH) + + destFile := filepath.Join(dest, "test") + _, err = os.Create(destFile) + assert.NoError(err) + + defer os.Remove(destFile) + + sourceDev, _ := getDeviceForPath(source) + destDev, _ := getDeviceForPath(destFile) + + assert.Equal(sourceDev, destDev) +} + +func TestBindMountInvalidSourceSymlink(t *testing.T) { + source := filepath.Join(testDir, "fooFile") + os.Remove(source) + + err := bindMount(context.Background(), source, "", false, "private") + assert.Error(t, err) +} + +func TestBindMountFailingMount(t *testing.T) { + source := filepath.Join(testDir, "fooLink") + fakeSource := filepath.Join(testDir, "fooFile") + os.Remove(source) + os.Remove(fakeSource) + assert := assert.New(t) + + _, err := os.OpenFile(fakeSource, os.O_CREATE, mountPerm) + assert.NoError(err) + + err = os.Symlink(fakeSource, source) + assert.NoError(err) + + err = bindMount(context.Background(), source, "", false, "private") + assert.Error(err) +} + +func cleanupFooMount() { + dest := filepath.Join(testDir, "fooDirDest") + + syscall.Unmount(dest, 0) +} + +func TestBindMountSuccessful(t *testing.T) { + assert := assert.New(t) + if tc.NotValid(ktu.NeedRoot()) { + t.Skip(ktu.TestDisabledNeedRoot) + } + + source := filepath.Join(testDir, "fooDirSrc") + dest := filepath.Join(testDir, "fooDirDest") + t.Cleanup(cleanupFooMount) + + err := os.MkdirAll(source, mountPerm) + assert.NoError(err) + + err = os.MkdirAll(dest, mountPerm) + assert.NoError(err) + + err = bindMount(context.Background(), source, dest, false, "private") + assert.NoError(err) +} + +func TestBindMountReadonlySuccessful(t *testing.T) { + assert := assert.New(t) + if tc.NotValid(ktu.NeedRoot()) { + t.Skip(ktu.TestDisabledNeedRoot) + } + + source := filepath.Join(testDir, "fooDirSrc") + dest := filepath.Join(testDir, "fooDirDest") + t.Cleanup(cleanupFooMount) + + err := os.MkdirAll(source, mountPerm) + assert.NoError(err) + + err = os.MkdirAll(dest, mountPerm) + assert.NoError(err) + + err = bindMount(context.Background(), source, dest, true, "private") + assert.NoError(err) + + // should not be able to create file in read-only mount + destFile := filepath.Join(dest, "foo") + _, err = os.OpenFile(destFile, os.O_CREATE, mountPerm) + assert.Error(err) +} + +func TestBindMountInvalidPgtypes(t *testing.T) { + assert := assert.New(t) + if tc.NotValid(ktu.NeedRoot()) { + t.Skip(ktu.TestDisabledNeedRoot) + } + + source := filepath.Join(testDir, "fooDirSrc") + dest := filepath.Join(testDir, "fooDirDest") + t.Cleanup(cleanupFooMount) + + err := os.MkdirAll(source, mountPerm) + assert.NoError(err) + + err = os.MkdirAll(dest, mountPerm) + assert.NoError(err) + + err = bindMount(context.Background(), source, dest, false, "foo") + expectedErr := fmt.Sprintf("Wrong propagation type %s", "foo") + assert.EqualError(err, expectedErr) +} + +// TestBindUnmountContainerRootfsENOENTNotError tests that if a file +// or directory attempting to be unmounted doesn't exist, then it +// is not considered an error +func TestBindUnmountContainerRootfsENOENTNotError(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("Test disabled as requires root user") + } + testMnt := "/tmp/test_mount" + sID := "sandIDTest" + cID := "contIDTest" + assert := assert.New(t) + + // Check to make sure the file doesn't exist + testPath := filepath.Join(testMnt, sID, cID, rootfsDir) + if _, err := os.Stat(testPath); !os.IsNotExist(err) { + assert.NoError(os.Remove(testPath)) + } + + err := bindUnmountContainerRootfs(context.Background(), filepath.Join(testMnt, sID), cID) + assert.NoError(err) +} + +func TestBindUnmountContainerRootfsRemoveRootfsDest(t *testing.T) { + assert := assert.New(t) + if tc.NotValid(ktu.NeedRoot()) { + t.Skip(ktu.TestDisabledNeedRoot) + } + + sID := "sandIDTestRemoveRootfsDest" + cID := "contIDTestRemoveRootfsDest" + + testPath := filepath.Join(testDir, sID, cID, rootfsDir) + syscall.Unmount(testPath, 0) + os.Remove(testPath) + + err := os.MkdirAll(testPath, mountPerm) + assert.NoError(err) + defer os.RemoveAll(filepath.Join(testDir, sID)) + + bindUnmountContainerRootfs(context.Background(), filepath.Join(testDir, sID), cID) + + if _, err := os.Stat(testPath); err == nil { + t.Fatal("empty rootfs dest should be removed") + } else if !os.IsNotExist(err) { + t.Fatal(err) + } +} + +func TestIsHostDevice(t *testing.T) { + assert := assert.New(t) + tests := []struct { + mnt string + expected bool + }{ + {"/dev", true}, + {"/dev/zero", true}, + {"/dev/block", true}, + {"/mnt/dev/block", false}, + } + + for _, test := range tests { + result := isHostDevice(test.mnt) + assert.Equal(result, test.expected) + } +} + +func TestMajorMinorNumber(t *testing.T) { + assert := assert.New(t) + devices := []string{"/dev/zero", "/dev/net/tun"} + + for _, device := range devices { + cmdStr := fmt.Sprintf("ls -l %s | awk '{print $5$6}'", device) + cmd := exec.Command("sh", "-c", cmdStr) + output, err := cmd.Output() + assert.NoError(err) + + data := bytes.Split(output, []byte(",")) + assert.False(len(data) < 2) + + majorStr := strings.TrimSpace(string(data[0])) + minorStr := strings.TrimSpace(string(data[1])) + + majorNo, err := strconv.Atoi(majorStr) + assert.NoError(err) + + minorNo, err := strconv.Atoi(minorStr) + assert.NoError(err) + + stat := syscall.Stat_t{} + err = syscall.Stat(device, &stat) + assert.NoError(err) + + // Get major and minor numbers for the device itself. Note the use of stat.Rdev instead of Dev. + major := major(uint64(stat.Rdev)) + minor := minor(uint64(stat.Rdev)) + + assert.Equal(minor, minorNo) + assert.Equal(major, majorNo) + } +} + +func TestGetDeviceForPathValidMount(t *testing.T) { + assert := assert.New(t) + dev, err := getDeviceForPath("/proc") + assert.NoError(err) + + expected := "/proc" + + assert.Equal(dev.mountPoint, expected) +} + +func TestIsDeviceMapper(t *testing.T) { + assert := assert.New(t) + + // known major, minor for /dev/tty + major := 5 + minor := 0 + + isDM, err := isDeviceMapper(major, minor) + assert.NoError(err) + assert.False(isDM) + + // fake the block device format + blockFormatTemplate = "/sys/dev/char/%d:%d" + isDM, err = isDeviceMapper(major, minor) + assert.NoError(err) + assert.True(isDM) +} diff --git a/src/runtime/virtcontainers/mount_test.go b/src/runtime/virtcontainers/mount_test.go index 056fa2c140..6d91d22a7b 100644 --- a/src/runtime/virtcontainers/mount_test.go +++ b/src/runtime/virtcontainers/mount_test.go @@ -6,15 +6,9 @@ package virtcontainers import ( - "bytes" - "context" "fmt" "os" - "os/exec" "path/filepath" - "strconv" - "strings" - "syscall" "testing" ktu "github.com/kata-containers/kata-containers/src/runtime/pkg/katatestutils" @@ -55,24 +49,6 @@ func TestIsSystemMount(t *testing.T) { } } -func TestIsHostDevice(t *testing.T) { - assert := assert.New(t) - tests := []struct { - mnt string - expected bool - }{ - {"/dev", true}, - {"/dev/zero", true}, - {"/dev/block", true}, - {"/mnt/dev/block", false}, - } - - for _, test := range tests { - result := isHostDevice(test.mnt) - assert.Equal(result, test.expected) - } -} - func TestIsHostDeviceCreateFile(t *testing.T) { assert := assert.New(t) if tc.NotValid(ktu.NeedRoot()) { @@ -89,41 +65,6 @@ func TestIsHostDeviceCreateFile(t *testing.T) { assert.NoError(os.Remove(path)) } -func TestMajorMinorNumber(t *testing.T) { - assert := assert.New(t) - devices := []string{"/dev/zero", "/dev/net/tun"} - - for _, device := range devices { - cmdStr := fmt.Sprintf("ls -l %s | awk '{print $5$6}'", device) - cmd := exec.Command("sh", "-c", cmdStr) - output, err := cmd.Output() - assert.NoError(err) - - data := bytes.Split(output, []byte(",")) - assert.False(len(data) < 2) - - majorStr := strings.TrimSpace(string(data[0])) - minorStr := strings.TrimSpace(string(data[1])) - - majorNo, err := strconv.Atoi(majorStr) - assert.NoError(err) - - minorNo, err := strconv.Atoi(minorStr) - assert.NoError(err) - - stat := syscall.Stat_t{} - err = syscall.Stat(device, &stat) - assert.NoError(err) - - // Get major and minor numbers for the device itself. Note the use of stat.Rdev instead of Dev. - major := major(stat.Rdev) - minor := minor(stat.Rdev) - - assert.Equal(minor, minorNo) - assert.Equal(major, majorNo) - } -} - func TestGetDeviceForPathRoot(t *testing.T) { assert := assert.New(t) dev, err := getDeviceForPath("/") @@ -134,16 +75,6 @@ func TestGetDeviceForPathRoot(t *testing.T) { assert.Equal(dev.mountPoint, expected) } -func TestGetDeviceForPathValidMount(t *testing.T) { - assert := assert.New(t) - dev, err := getDeviceForPath("/proc") - assert.NoError(err) - - expected := "/proc" - - assert.Equal(dev.mountPoint, expected) -} - func TestGetDeviceForPathEmptyPath(t *testing.T) { assert := assert.New(t) _, err := getDeviceForPath("") @@ -165,64 +96,6 @@ func TestGetDeviceForPath(t *testing.T) { assert.Error(err) } -func TestGetDeviceForPathBindMount(t *testing.T) { - assert := assert.New(t) - - if tc.NotValid(ktu.NeedRoot()) { - t.Skip(ktu.TestDisabledNeedRoot) - } - - source := filepath.Join(testDir, "testDeviceDirSrc") - dest := filepath.Join(testDir, "testDeviceDirDest") - syscall.Unmount(dest, 0) - os.Remove(source) - os.Remove(dest) - - err := os.MkdirAll(source, mountPerm) - assert.NoError(err) - - defer os.Remove(source) - - err = os.MkdirAll(dest, mountPerm) - assert.NoError(err) - - defer os.Remove(dest) - - err = bindMount(context.Background(), source, dest, false, "private") - assert.NoError(err) - - defer syscall.Unmount(dest, syscall.MNT_DETACH) - - destFile := filepath.Join(dest, "test") - _, err = os.Create(destFile) - assert.NoError(err) - - defer os.Remove(destFile) - - sourceDev, _ := getDeviceForPath(source) - destDev, _ := getDeviceForPath(destFile) - - assert.Equal(sourceDev, destDev) -} - -func TestIsDeviceMapper(t *testing.T) { - assert := assert.New(t) - - // known major, minor for /dev/tty - major := 5 - minor := 0 - - isDM, err := isDeviceMapper(major, minor) - assert.NoError(err) - assert.False(isDM) - - // fake the block device format - blockFormatTemplate = "/sys/dev/char/%d:%d" - isDM, err = isDeviceMapper(major, minor) - assert.NoError(err) - assert.True(isDM) -} - func TestIsDockerVolume(t *testing.T) { assert := assert.New(t) path := "/var/lib/docker/volumes/00da1347c7cf4f15db35f/_data" @@ -234,38 +107,6 @@ func TestIsDockerVolume(t *testing.T) { assert.False(isDockerVolume) } -func TestIsEphemeralStorage(t *testing.T) { - assert := assert.New(t) - if tc.NotValid(ktu.NeedRoot()) { - t.Skip(ktu.TestDisabledNeedRoot) - } - - dir, err := os.MkdirTemp(testDir, "foo") - assert.NoError(err) - defer os.RemoveAll(dir) - - sampleEphePath := filepath.Join(dir, K8sEmptyDir, "tmp-volume") - err = os.MkdirAll(sampleEphePath, testDirMode) - assert.Nil(err) - - err = syscall.Mount("tmpfs", sampleEphePath, "tmpfs", 0, "") - assert.NoError(err) - defer syscall.Unmount(sampleEphePath, 0) - - isEphe := IsEphemeralStorage(sampleEphePath) - assert.True(isEphe) - - isHostEmptyDir := Isk8sHostEmptyDir(sampleEphePath) - assert.False(isHostEmptyDir) - - sampleEphePath = "/var/lib/kubelet/pods/366c3a75-4869-11e8-b479-507b9ddd5ce4/volumes/cache-volume" - isEphe = IsEphemeralStorage(sampleEphePath) - assert.False(isEphe) - - isHostEmptyDir = Isk8sHostEmptyDir(sampleEphePath) - assert.False(isHostEmptyDir) -} - func TestIsEmtpyDir(t *testing.T) { assert := assert.New(t) path := "/var/lib/kubelet/pods/5f0861a0-a987-4a3a-bb0f-1058ddb9678f/volumes/kubernetes.io~empty-dir/foobar" @@ -355,148 +196,3 @@ func TestIsWatchable(t *testing.T) { result = isWatchableMount(configs) assert.False(result) } - -func TestBindMountInvalidSourceSymlink(t *testing.T) { - source := filepath.Join(testDir, "fooFile") - os.Remove(source) - - err := bindMount(context.Background(), source, "", false, "private") - assert.Error(t, err) -} - -func TestBindMountFailingMount(t *testing.T) { - source := filepath.Join(testDir, "fooLink") - fakeSource := filepath.Join(testDir, "fooFile") - os.Remove(source) - os.Remove(fakeSource) - assert := assert.New(t) - - _, err := os.OpenFile(fakeSource, os.O_CREATE, mountPerm) - assert.NoError(err) - - err = os.Symlink(fakeSource, source) - assert.NoError(err) - - err = bindMount(context.Background(), source, "", false, "private") - assert.Error(err) -} - -func cleanupFooMount() { - dest := filepath.Join(testDir, "fooDirDest") - - syscall.Unmount(dest, 0) -} - -func TestBindMountSuccessful(t *testing.T) { - assert := assert.New(t) - if tc.NotValid(ktu.NeedRoot()) { - t.Skip(testDisabledAsNonRoot) - } - - source := filepath.Join(testDir, "fooDirSrc") - dest := filepath.Join(testDir, "fooDirDest") - t.Cleanup(cleanupFooMount) - - err := os.MkdirAll(source, mountPerm) - assert.NoError(err) - - err = os.MkdirAll(dest, mountPerm) - assert.NoError(err) - - err = bindMount(context.Background(), source, dest, false, "private") - assert.NoError(err) -} - -func TestBindMountReadonlySuccessful(t *testing.T) { - assert := assert.New(t) - if tc.NotValid(ktu.NeedRoot()) { - t.Skip(testDisabledAsNonRoot) - } - - source := filepath.Join(testDir, "fooDirSrc") - dest := filepath.Join(testDir, "fooDirDest") - t.Cleanup(cleanupFooMount) - - err := os.MkdirAll(source, mountPerm) - assert.NoError(err) - - err = os.MkdirAll(dest, mountPerm) - assert.NoError(err) - - err = bindMount(context.Background(), source, dest, true, "private") - assert.NoError(err) - - // should not be able to create file in read-only mount - destFile := filepath.Join(dest, "foo") - _, err = os.OpenFile(destFile, os.O_CREATE, mountPerm) - assert.Error(err) -} - -func TestBindMountInvalidPgtypes(t *testing.T) { - assert := assert.New(t) - if tc.NotValid(ktu.NeedRoot()) { - t.Skip(testDisabledAsNonRoot) - } - - source := filepath.Join(testDir, "fooDirSrc") - dest := filepath.Join(testDir, "fooDirDest") - t.Cleanup(cleanupFooMount) - - err := os.MkdirAll(source, mountPerm) - assert.NoError(err) - - err = os.MkdirAll(dest, mountPerm) - assert.NoError(err) - - err = bindMount(context.Background(), source, dest, false, "foo") - expectedErr := fmt.Sprintf("Wrong propagation type %s", "foo") - assert.EqualError(err, expectedErr) -} - -// TestBindUnmountContainerRootfsENOENTNotError tests that if a file -// or directory attempting to be unmounted doesn't exist, then it -// is not considered an error -func TestBindUnmountContainerRootfsENOENTNotError(t *testing.T) { - if os.Getuid() != 0 { - t.Skip("Test disabled as requires root user") - } - testMnt := "/tmp/test_mount" - sID := "sandIDTest" - cID := "contIDTest" - assert := assert.New(t) - - // Check to make sure the file doesn't exist - testPath := filepath.Join(testMnt, sID, cID, rootfsDir) - if _, err := os.Stat(testPath); !os.IsNotExist(err) { - assert.NoError(os.Remove(testPath)) - } - - err := bindUnmountContainerRootfs(context.Background(), filepath.Join(testMnt, sID), cID) - assert.NoError(err) -} - -func TestBindUnmountContainerRootfsRemoveRootfsDest(t *testing.T) { - assert := assert.New(t) - if tc.NotValid(ktu.NeedRoot()) { - t.Skip(ktu.TestDisabledNeedRoot) - } - - sID := "sandIDTestRemoveRootfsDest" - cID := "contIDTestRemoveRootfsDest" - - testPath := filepath.Join(testDir, sID, cID, rootfsDir) - syscall.Unmount(testPath, 0) - os.Remove(testPath) - - err := os.MkdirAll(testPath, mountPerm) - assert.NoError(err) - defer os.RemoveAll(filepath.Join(testDir, sID)) - - bindUnmountContainerRootfs(context.Background(), filepath.Join(testDir, sID), cID) - - if _, err := os.Stat(testPath); err == nil { - t.Fatal("empty rootfs dest should be removed") - } else if !os.IsNotExist(err) { - t.Fatal(err) - } -}