diff options
Diffstat (limited to 'runsc/container')
-rw-r--r-- | runsc/container/BUILD | 2 | ||||
-rw-r--r-- | runsc/container/container.go | 75 | ||||
-rw-r--r-- | runsc/container/container_test.go | 165 | ||||
-rw-r--r-- | runsc/container/fs.go | 287 | ||||
-rw-r--r-- | runsc/container/fs_test.go | 158 |
5 files changed, 196 insertions, 491 deletions
diff --git a/runsc/container/BUILD b/runsc/container/BUILD index 3b25ff79a..2936b7cdf 100644 --- a/runsc/container/BUILD +++ b/runsc/container/BUILD @@ -6,7 +6,6 @@ go_library( name = "container", srcs = [ "container.go", - "fs.go", "hook.go", "status.go", ], @@ -34,7 +33,6 @@ go_test( srcs = [ "console_test.go", "container_test.go", - "fs_test.go", "multi_container_test.go", "shared_volume_test.go", ], diff --git a/runsc/container/container.go b/runsc/container/container.go index 6f092a5ce..fdcf8d7b7 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -281,18 +281,6 @@ func Create(id string, spec *specs.Spec, conf *boot.Config, bundleDir, consoleSo if specutils.ShouldCreateSandbox(spec) { log.Debugf("Creating new sandbox for container %q", id) - // Setup rootfs and mounts. It returns a new mount list with destination - // paths resolved. Since the spec for the root container is read from disk, - // Write the new spec to a new file that will be used by the sandbox. - cleanMounts, err := setupFS(spec, conf, bundleDir) - if err != nil { - return nil, fmt.Errorf("setup mounts: %v", err) - } - spec.Mounts = cleanMounts - if err := specutils.WriteCleanSpec(bundleDir, spec); err != nil { - return nil, fmt.Errorf("writing clean spec: %v", err) - } - // Create and join cgroup before processes are created to ensure they are // part of the cgroup from the start (and all tneir children processes). cg, err := cgroup.New(spec) @@ -306,14 +294,14 @@ func Create(id string, spec *specs.Spec, conf *boot.Config, bundleDir, consoleSo } } if err := runInCgroup(cg, func() error { - ioFiles, err := c.createGoferProcess(spec, conf, bundleDir) + ioFiles, specFile, err := c.createGoferProcess(spec, conf, bundleDir) if err != nil { return err } // Start a new sandbox for this container. Any errors after this point // must destroy the container. - c.Sandbox, err = sandbox.New(id, spec, conf, bundleDir, consoleSocket, userLog, ioFiles, cg) + c.Sandbox, err = sandbox.New(id, spec, conf, bundleDir, consoleSocket, userLog, ioFiles, specFile, cg) return err }); err != nil { return nil, err @@ -387,26 +375,22 @@ func (c *Container) Start(conf *boot.Config) error { return err } } else { - // Setup rootfs and mounts. It returns a new mount list with destination - // paths resolved. Replace the original spec with new mount list and start - // container. - cleanMounts, err := setupFS(c.Spec, conf, c.BundleDir) - if err != nil { - return fmt.Errorf("setup mounts: %v", err) - } - c.Spec.Mounts = cleanMounts - if err := specutils.WriteCleanSpec(c.BundleDir, c.Spec); err != nil { - return fmt.Errorf("writing clean spec: %v", err) - } - // Join cgroup to strt gofer process to ensure it's part of the cgroup from // the start (and all tneir children processes). if err := runInCgroup(c.Sandbox.Cgroup, func() error { // Create the gofer process. - ioFiles, err := c.createGoferProcess(c.Spec, conf, c.BundleDir) + ioFiles, mountsFile, err := c.createGoferProcess(c.Spec, conf, c.BundleDir) if err != nil { return err } + defer mountsFile.Close() + + cleanMounts, err := specutils.ReadMounts(mountsFile) + if err != nil { + return fmt.Errorf("reading mounts file: %v", err) + } + c.Spec.Mounts = cleanMounts + return c.Sandbox.StartContainer(c.Spec, conf, c.ID, ioFiles) }); err != nil { return err @@ -665,12 +649,6 @@ func (c *Container) Destroy() error { errs = append(errs, err.Error()) } - if err := destroyFS(c.Spec); err != nil { - err = fmt.Errorf("destroying container fs: %v", err) - log.Warningf("%v", err) - errs = append(errs, err.Error()) - } - if err := os.RemoveAll(c.Root); err != nil && !os.IsNotExist(err) { err = fmt.Errorf("deleting container root directory %q: %v", c.Root, err) log.Warningf("%v", err) @@ -787,7 +765,7 @@ func (c *Container) waitForStopped() error { return backoff.Retry(op, b) } -func (c *Container) createGoferProcess(spec *specs.Spec, conf *boot.Config, bundleDir string) ([]*os.File, error) { +func (c *Container) createGoferProcess(spec *specs.Spec, conf *boot.Config, bundleDir string) ([]*os.File, *os.File, error) { // Start with the general config flags. args := conf.ToFlags() @@ -800,7 +778,7 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *boot.Config, bund if conf.LogFilename != "" { logFile, err := os.OpenFile(conf.LogFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - return nil, fmt.Errorf("opening log file %q: %v", conf.LogFilename, err) + return nil, nil, fmt.Errorf("opening log file %q: %v", conf.LogFilename, err) } defer logFile.Close() goferEnds = append(goferEnds, logFile) @@ -811,7 +789,7 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *boot.Config, bund if conf.DebugLog != "" { debugLogFile, err := specutils.DebugLogFile(conf.DebugLog, "gofer") if err != nil { - return nil, fmt.Errorf("opening debug log file in %q: %v", conf.DebugLog, err) + return nil, nil, fmt.Errorf("opening debug log file in %q: %v", conf.DebugLog, err) } defer debugLogFile.Close() goferEnds = append(goferEnds, debugLogFile) @@ -825,30 +803,39 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *boot.Config, bund } // Open the spec file to donate to the sandbox. - specFile, err := specutils.OpenCleanSpec(bundleDir) + specFile, err := specutils.OpenSpec(bundleDir) if err != nil { - return nil, fmt.Errorf("opening spec file: %v", err) + return nil, nil, fmt.Errorf("opening spec file: %v", err) } defer specFile.Close() goferEnds = append(goferEnds, specFile) args = append(args, "--spec-fd="+strconv.Itoa(nextFD)) nextFD++ + // Create pipe that allows gofer to send mount list to sandbox after all paths + // have been resolved. + mountsSand, mountsGofer, err := os.Pipe() + if err != nil { + return nil, nil, err + } + defer mountsGofer.Close() + goferEnds = append(goferEnds, mountsGofer) + args = append(args, fmt.Sprintf("--mounts-fd=%d", nextFD)) + nextFD++ + // Add root mount and then add any other additional mounts. mountCount := 1 - - // Add additional mounts. for _, m := range spec.Mounts { if specutils.Is9PMount(m) { mountCount++ } } - sandEnds := make([]*os.File, 0, mountCount) + sandEnds := make([]*os.File, 0, mountCount) for i := 0; i < mountCount; i++ { fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0) if err != nil { - return nil, err + return nil, nil, err } sandEnds = append(sandEnds, os.NewFile(uintptr(fds[0]), "sandbox IO FD")) @@ -884,12 +871,12 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *boot.Config, bund // Start the gofer in the given namespace. log.Debugf("Starting gofer: %s %v", binPath, args) if err := specutils.StartInNS(cmd, nss); err != nil { - return nil, err + return nil, nil, err } log.Infof("Gofer started, PID: %d", cmd.Process.Pid) c.GoferPid = cmd.Process.Pid c.goferIsChild = true - return sandEnds, nil + return sandEnds, mountsSand, nil } // changeStatus transitions from one status to another ensuring that the diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 06a25de6d..f17155175 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -1594,6 +1594,171 @@ func TestCreateWorkingDir(t *testing.T) { } } +// TestMountPropagation verifies that mount propagates to slave but not to +// private mounts. +func TestMountPropagation(t *testing.T) { + // Setup dir structure: + // - src: is mounted as shared and is used as source for both private and + // slave mounts + // - dir: will be bind mounted inside src and should propagate to slave + tmpDir, err := ioutil.TempDir(testutil.TmpDir(), "mount") + if err != nil { + t.Fatalf("ioutil.TempDir() failed: %v", err) + } + src := filepath.Join(tmpDir, "src") + srcMnt := filepath.Join(src, "mnt") + dir := filepath.Join(tmpDir, "dir") + for _, path := range []string{src, srcMnt, dir} { + if err := os.MkdirAll(path, 0777); err != nil { + t.Fatalf("MkdirAll(%q): %v", path, err) + } + } + dirFile := filepath.Join(dir, "file") + f, err := os.Create(dirFile) + if err != nil { + t.Fatalf("os.Create(%q): %v", dirFile, err) + } + f.Close() + + // Setup src as a shared mount. + if err := syscall.Mount(src, src, "bind", syscall.MS_BIND, ""); err != nil { + t.Fatalf("mount(%q, %q, MS_BIND): %v", dir, srcMnt, err) + } + if err := syscall.Mount("", src, "", syscall.MS_SHARED, ""); err != nil { + t.Fatalf("mount(%q, MS_SHARED): %v", srcMnt, err) + } + + spec := testutil.NewSpecWithArgs("sleep", "1000") + + priv := filepath.Join(tmpDir, "priv") + slave := filepath.Join(tmpDir, "slave") + spec.Mounts = []specs.Mount{ + { + Source: src, + Destination: priv, + Type: "bind", + Options: []string{"private"}, + }, + { + Source: src, + Destination: slave, + Type: "bind", + Options: []string{"slave"}, + }, + } + + conf := testutil.TestConfig() + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + cont, err := Create(testutil.UniqueContainerID(), spec, conf, bundleDir, "", "", "") + if err != nil { + t.Fatalf("creating container: %v", err) + } + defer cont.Destroy() + + if err := cont.Start(conf); err != nil { + t.Fatalf("starting container: %v", err) + } + + // After the container is started, mount dir inside source and check what + // happens to both destinations. + if err := syscall.Mount(dir, srcMnt, "bind", syscall.MS_BIND, ""); err != nil { + t.Fatalf("mount(%q, %q, MS_BIND): %v", dir, srcMnt, err) + } + + // Check that mount didn't propagate to private mount. + privFile := filepath.Join(priv, "mnt", "file") + args := &control.ExecArgs{ + Filename: "/usr/bin/test", + Argv: []string{"test", "!", "-f", privFile}, + } + if ws, err := cont.executeSync(args); err != nil || ws != 0 { + t.Fatalf("exec: test ! -f %q, ws: %v, err: %v", privFile, ws, err) + } + + // Check that mount propagated to slave mount. + slaveFile := filepath.Join(slave, "mnt", "file") + args = &control.ExecArgs{ + Filename: "/usr/bin/test", + Argv: []string{"test", "-f", slaveFile}, + } + if ws, err := cont.executeSync(args); err != nil || ws != 0 { + t.Fatalf("exec: test -f %q, ws: %v, err: %v", privFile, ws, err) + } +} + +func TestMountSymlink(t *testing.T) { + for _, conf := range configs(overlay) { + t.Logf("Running test with conf: %+v", conf) + + dir, err := ioutil.TempDir(testutil.TmpDir(), "mount-symlink") + if err != nil { + t.Fatalf("ioutil.TempDir() failed: %v", err) + } + + source := path.Join(dir, "source") + target := path.Join(dir, "target") + for _, path := range []string{source, target} { + if err := os.MkdirAll(path, 0777); err != nil { + t.Fatalf("os.MkdirAll(): %v", err) + } + } + f, err := os.Create(path.Join(source, "file")) + if err != nil { + t.Fatalf("os.Create(): %v", err) + } + f.Close() + + link := path.Join(dir, "link") + if err := os.Symlink(target, link); err != nil { + t.Fatalf("os.Symlink(%q, %q): %v", target, link, err) + } + + spec := testutil.NewSpecWithArgs("/bin/sleep", "1000") + + // Mount to a symlink to ensure the mount code will follow it and mount + // at the symlink target. + spec.Mounts = append(spec.Mounts, specs.Mount{ + Type: "bind", + Destination: link, + Source: source, + }) + + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + cont, err := Create(testutil.UniqueContainerID(), spec, conf, bundleDir, "", "", "") + if err != nil { + t.Fatalf("creating container: %v", err) + } + defer cont.Destroy() + + if err := cont.Start(conf); err != nil { + t.Fatalf("starting container: %v", err) + } + + // Check that symlink was resolved and mount was created where the symlink + // is pointing to. + file := path.Join(target, "file") + args := &control.ExecArgs{ + Filename: "/usr/bin/test", + Argv: []string{"test", "-f", file}, + } + if ws, err := cont.executeSync(args); err != nil || ws != 0 { + t.Fatalf("exec: test -f %q, ws: %v, err: %v", file, ws, err) + } + } +} + // executeSync synchronously executes a new process. func (cont *Container) executeSync(args *control.ExecArgs) (syscall.WaitStatus, error) { pid, err := cont.Execute(args) diff --git a/runsc/container/fs.go b/runsc/container/fs.go deleted file mode 100644 index 998160487..000000000 --- a/runsc/container/fs.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2018 Google LLC -// -// 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 container - -import ( - "bufio" - "fmt" - "os" - "path/filepath" - "strings" - "syscall" - - specs "github.com/opencontainers/runtime-spec/specs-go" - "gvisor.googlesource.com/gvisor/pkg/log" - "gvisor.googlesource.com/gvisor/runsc/boot" - "gvisor.googlesource.com/gvisor/runsc/specutils" -) - -type mapping struct { - set bool - val uint32 -} - -var optionsMap = map[string]mapping{ - "acl": {set: true, val: syscall.MS_POSIXACL}, - "async": {set: false, val: syscall.MS_SYNCHRONOUS}, - "atime": {set: false, val: syscall.MS_NOATIME}, - "bind": {set: true, val: syscall.MS_BIND}, - "defaults": {set: true, val: 0}, - "dev": {set: false, val: syscall.MS_NODEV}, - "diratime": {set: false, val: syscall.MS_NODIRATIME}, - "dirsync": {set: true, val: syscall.MS_DIRSYNC}, - "exec": {set: false, val: syscall.MS_NOEXEC}, - "iversion": {set: true, val: syscall.MS_I_VERSION}, - "loud": {set: false, val: syscall.MS_SILENT}, - "mand": {set: true, val: syscall.MS_MANDLOCK}, - "noacl": {set: false, val: syscall.MS_POSIXACL}, - "noatime": {set: true, val: syscall.MS_NOATIME}, - "nodev": {set: true, val: syscall.MS_NODEV}, - "nodiratime": {set: true, val: syscall.MS_NODIRATIME}, - "noexec": {set: true, val: syscall.MS_NOEXEC}, - "noiversion": {set: false, val: syscall.MS_I_VERSION}, - "nomand": {set: false, val: syscall.MS_MANDLOCK}, - "norelatime": {set: false, val: syscall.MS_RELATIME}, - "nostrictatime": {set: false, val: syscall.MS_STRICTATIME}, - "nosuid": {set: true, val: syscall.MS_NOSUID}, - "private": {set: true, val: syscall.MS_PRIVATE}, - "rbind": {set: true, val: syscall.MS_BIND | syscall.MS_REC}, - "relatime": {set: true, val: syscall.MS_RELATIME}, - "remount": {set: true, val: syscall.MS_REMOUNT}, - "ro": {set: true, val: syscall.MS_RDONLY}, - "rprivate": {set: true, val: syscall.MS_PRIVATE | syscall.MS_REC}, - "rw": {set: false, val: syscall.MS_RDONLY}, - "silent": {set: true, val: syscall.MS_SILENT}, - "strictatime": {set: true, val: syscall.MS_STRICTATIME}, - "suid": {set: false, val: syscall.MS_NOSUID}, - "sync": {set: true, val: syscall.MS_SYNCHRONOUS}, -} - -// setupFS creates the container directory structure under 'spec.Root.Path'. -// This allows the gofer serving the containers to be chroot under this -// directory to create an extra layer to security in case the gofer gets -// compromised. -// Returns list of mounts equivalent to 'spec.Mounts' with all destination paths -// cleaned and with symlinks resolved. -func setupFS(spec *specs.Spec, conf *boot.Config, bundleDir string) ([]specs.Mount, error) { - rv := make([]specs.Mount, 0, len(spec.Mounts)) - for _, m := range spec.Mounts { - if m.Type != "bind" || !specutils.IsSupportedDevMount(m) { - rv = append(rv, m) - continue - } - - // It's possible that 'm.Destination' follows symlinks inside the - // container. - dst, err := resolveSymlinks(spec.Root.Path, m.Destination) - if err != nil { - return nil, fmt.Errorf("resolving symlinks to %q: %v", m.Destination, err) - } - - flags := optionsToFlags(m.Options) - flags |= syscall.MS_BIND - log.Infof("Mounting src: %q, dst: %q, flags: %#x", m.Source, dst, flags) - if err := specutils.Mount(m.Source, dst, m.Type, flags); err != nil { - return nil, fmt.Errorf("mounting %v: %v", m, err) - } - - // Make the mount a slave, so that for recursive bind mount, umount won't - // propagate to the source. - flags = syscall.MS_SLAVE | syscall.MS_REC - if err := syscall.Mount("", dst, "", uintptr(flags), ""); err != nil { - return nil, fmt.Errorf("mount rslave dst: %q, flags: %#x, err: %v", dst, flags, err) - } - - cpy := m - relDst, err := filepath.Rel(spec.Root.Path, dst) - if err != nil { - panic(fmt.Sprintf("%q could not be made relative to %q: %v", dst, spec.Root.Path, err)) - } - cpy.Destination = filepath.Join("/", relDst) - rv = append(rv, cpy) - } - - if spec.Process.Cwd != "" { - dst, err := resolveSymlinks(spec.Root.Path, spec.Process.Cwd) - if err != nil { - return nil, fmt.Errorf("resolving symlinks to %q: %v", spec.Process.Cwd, err) - } - if err := os.MkdirAll(dst, 0755); err != nil { - return nil, err - } - } - - // If root is read only, check if it needs to be remounted as readonly. - if spec.Root.Readonly { - isMountPoint, readonly, err := mountInfo(spec.Root.Path) - if err != nil { - return nil, err - } - if readonly { - return rv, nil - } - if !isMountPoint { - // Readonly root is not a mount point nor read-only. Can't do much other - // than just logging a warning. The gofer will prevent files to be open - // in write mode. - log.Warningf("Mount where root is located is not read-only and cannot be changed: %q", spec.Root.Path) - return rv, nil - } - - // If root is a mount point but not read-only, we can change mount options - // to make it read-only for extra safety. - log.Infof("Remounting root as readonly: %q", spec.Root.Path) - flags := uintptr(syscall.MS_BIND | syscall.MS_REMOUNT | syscall.MS_RDONLY | syscall.MS_REC) - src := spec.Root.Path - if err := syscall.Mount(src, src, "bind", flags, ""); err != nil { - return nil, fmt.Errorf("remounting root as read-only with source: %q, target: %q, flags: %#x, err: %v", spec.Root.Path, spec.Root.Path, flags, err) - } - } - return rv, nil -} - -// mountInfo returns whether the path is a mount point and whether the mount -// that path belongs to is read-only. -func mountInfo(path string) (bool, bool, error) { - // Mounts are listed by their real paths. - realPath, err := filepath.EvalSymlinks(path) - if err != nil { - return false, false, err - } - f, err := os.Open("/proc/mounts") - if err != nil { - return false, false, err - } - scanner := bufio.NewScanner(f) - - var mountPoint string - var readonly bool - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, " ") - if len(parts) < 4 { - return false, false, fmt.Errorf("invalid /proc/mounts line format %q", line) - } - mp := parts[1] - opts := strings.Split(parts[3], ",") - - // Find the closest submount to the path. - if strings.Contains(realPath, mp) && len(mp) > len(mountPoint) { - mountPoint = mp - readonly = specutils.ContainsStr(opts, "ro") - } - } - if err := scanner.Err(); err != nil { - return false, false, err - } - return mountPoint == realPath, readonly, nil -} - -// destroyFS unmounts mounts done by runsc under `spec.Root.Path`. This -// recovers the container rootfs into the original state. -func destroyFS(spec *specs.Spec) error { - for _, m := range spec.Mounts { - if m.Type != "bind" || !specutils.IsSupportedDevMount(m) { - continue - } - - // It's possible that 'm.Destination' follows symlinks inside the - // container. - dst, err := resolveSymlinks(spec.Root.Path, m.Destination) - if err != nil { - return err - } - - flags := syscall.MNT_DETACH - log.Infof("Unmounting dst: %q, flags: %#x", dst, flags) - // Do not return error if dst is not a mountpoint. - // Based on http://man7.org/linux/man-pages/man2/umount.2.html - // For kernel version 2.6+ and MNT_DETACH flag, EINVAL means - // the dst is not a mount point. - if err := syscall.Unmount(dst, flags); err != nil && - !os.IsNotExist(err) && err != syscall.EINVAL { - return err - } - } - return nil -} - -// resolveSymlinks walks 'rel' having 'root' as the root directory. If there are -// symlinks, they are evaluated relative to 'root' to ensure the end result is -// the same as if the process was running inside the container. -func resolveSymlinks(root, rel string) (string, error) { - return resolveSymlinksImpl(root, root, rel, 255) -} - -func resolveSymlinksImpl(root, base, rel string, followCount uint) (string, error) { - if followCount == 0 { - return "", fmt.Errorf("too many symlinks to follow, path: %q", filepath.Join(base, rel)) - } - - rel = filepath.Clean(rel) - for _, name := range strings.Split(rel, string(filepath.Separator)) { - if name == "" { - continue - } - // Note that Join() resolves things like ".." and returns a clean path. - path := filepath.Join(base, name) - if !strings.HasPrefix(path, root) { - // One cannot '..' their way out of root. - path = root - continue - } - fi, err := os.Lstat(path) - if err != nil { - if !os.IsNotExist(err) { - return "", err - } - // Not found means there is no symlink to check. Just keep walking dirs. - base = path - continue - } - if fi.Mode()&os.ModeSymlink != 0 { - link, err := os.Readlink(path) - if err != nil { - return "", err - } - if filepath.IsAbs(link) { - base = root - } - base, err = resolveSymlinksImpl(root, base, link, followCount-1) - if err != nil { - return "", err - } - continue - } - base = path - } - return base, nil -} - -func optionsToFlags(opts []string) uint32 { - var rv uint32 - for _, opt := range opts { - if m, ok := optionsMap[opt]; ok { - if m.set { - rv |= m.val - } else { - rv ^= m.val - } - } else { - log.Warningf("Ignoring mount option %q", opt) - } - } - return rv -} diff --git a/runsc/container/fs_test.go b/runsc/container/fs_test.go deleted file mode 100644 index 87cdb078e..000000000 --- a/runsc/container/fs_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2018 Google LLC -// -// 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 container - -import ( - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "testing" - - "gvisor.googlesource.com/gvisor/runsc/test/testutil" -) - -type dir struct { - rel string - link string -} - -func construct(root string, dirs []dir) error { - for _, d := range dirs { - p := path.Join(root, d.rel) - if d.link == "" { - if err := os.MkdirAll(p, 0755); err != nil { - return fmt.Errorf("error creating dir: %v", err) - } - } else { - if err := os.MkdirAll(path.Dir(p), 0755); err != nil { - return fmt.Errorf("error creating dir: %v", err) - } - if err := os.Symlink(d.link, p); err != nil { - return fmt.Errorf("error creating symlink: %v", err) - } - } - } - return nil -} - -func TestResolveSymlinks(t *testing.T) { - root, err := ioutil.TempDir(testutil.TmpDir(), "root") - if err != nil { - t.Fatal("ioutil.TempDir() failed:", err) - } - dirs := []dir{ - {"dir1/dir11/dir111/dir1111", ""}, // Just a boring dir - {"dir1/lnk12", "dir11"}, // Link to sibling - {"dir1/lnk13", "./dir11"}, // Link to sibling through self - {"dir1/lnk14", "../dir1/dir11"}, // Link to sibling through parent - {"dir1/dir15/lnk151", ".."}, // Link to parent - {"dir1/lnk16", "dir11/dir111"}, // Link to child - {"dir1/lnk17", "."}, // Link to self - {"dir1/lnk18", "lnk13"}, // Link to link - {"lnk2", "dir1/lnk13"}, // Link to link to link - {"dir3/dir21/lnk211", "../.."}, // Link to root relative - {"dir3/lnk22", "/"}, // Link to root absolute - {"dir3/lnk23", "/dir1"}, // Link to dir absolute - {"dir3/lnk24", "/dir1/lnk12"}, // Link to link absolute - {"lnk5", "../../.."}, // Link outside root - } - if err := construct(root, dirs); err != nil { - t.Fatal("construct failed:", err) - } - - tests := []struct { - name string - rel string - want string - compareHost bool - }{ - {name: "root", rel: "/", want: "/", compareHost: true}, - {name: "basic dir", rel: "/dir1/dir11/dir111", want: "/dir1/dir11/dir111", compareHost: true}, - {name: "dot 1", rel: "/dir1/dir11/./dir111", want: "/dir1/dir11/dir111", compareHost: true}, - {name: "dot 2", rel: "/dir1/././dir11/./././././dir111/.", want: "/dir1/dir11/dir111", compareHost: true}, - {name: "dotdot 1", rel: "/dir1/dir11/../dir15", want: "/dir1/dir15", compareHost: true}, - {name: "dotdot 2", rel: "/dir1/dir11/dir1111/../..", want: "/dir1", compareHost: true}, - - {name: "link sibling", rel: "/dir1/lnk12", want: "/dir1/dir11", compareHost: true}, - {name: "link sibling + dir", rel: "/dir1/lnk12/dir111", want: "/dir1/dir11/dir111", compareHost: true}, - {name: "link sibling through self", rel: "/dir1/lnk13", want: "/dir1/dir11", compareHost: true}, - {name: "link sibling through parent", rel: "/dir1/lnk14", want: "/dir1/dir11", compareHost: true}, - - {name: "link parent", rel: "/dir1/dir15/lnk151", want: "/dir1", compareHost: true}, - {name: "link parent + dir", rel: "/dir1/dir15/lnk151/dir11", want: "/dir1/dir11", compareHost: true}, - {name: "link child", rel: "/dir1/lnk16", want: "/dir1/dir11/dir111", compareHost: true}, - {name: "link child + dir", rel: "/dir1/lnk16/dir1111", want: "/dir1/dir11/dir111/dir1111", compareHost: true}, - {name: "link self", rel: "/dir1/lnk17", want: "/dir1", compareHost: true}, - {name: "link self + dir", rel: "/dir1/lnk17/dir11", want: "/dir1/dir11", compareHost: true}, - - {name: "link^2", rel: "/dir1/lnk18", want: "/dir1/dir11", compareHost: true}, - {name: "link^2 + dir", rel: "/dir1/lnk18/dir111", want: "/dir1/dir11/dir111", compareHost: true}, - {name: "link^3", rel: "/lnk2", want: "/dir1/dir11", compareHost: true}, - {name: "link^3 + dir", rel: "/lnk2/dir111", want: "/dir1/dir11/dir111", compareHost: true}, - - {name: "link abs", rel: "/dir3/lnk23", want: "/dir1"}, - {name: "link abs + dir", rel: "/dir3/lnk23/dir11", want: "/dir1/dir11"}, - {name: "link^2 abs", rel: "/dir3/lnk24", want: "/dir1/dir11"}, - {name: "link^2 abs + dir", rel: "/dir3/lnk24/dir111", want: "/dir1/dir11/dir111"}, - - {name: "root link rel", rel: "/dir3/dir21/lnk211", want: "/", compareHost: true}, - {name: "root link abs", rel: "/dir3/lnk22", want: "/"}, - {name: "root contain link", rel: "/lnk5/dir1", want: "/dir1"}, - {name: "root contain dotdot", rel: "/dir1/dir11/../../../../../../../..", want: "/"}, - - {name: "crazy", rel: "/dir3/dir21/lnk211/dir3/lnk22/dir1/dir11/../../lnk5/dir3/../dir3/lnk24/dir111/dir1111/..", want: "/dir1/dir11/dir111"}, - } - for _, tst := range tests { - t.Run(tst.name, func(t *testing.T) { - got, err := resolveSymlinks(root, tst.rel) - if err != nil { - t.Errorf("resolveSymlinks(root, %q) failed: %v", tst.rel, err) - } - want := path.Join(root, tst.want) - if got != want { - t.Errorf("resolveSymlinks(root, %q) got: %q, want: %q", tst.rel, got, want) - } - if tst.compareHost { - // Check that host got to the same end result. - host, err := filepath.EvalSymlinks(path.Join(root, tst.rel)) - if err != nil { - t.Errorf("path.EvalSymlinks(root, %q) failed: %v", tst.rel, err) - } - if host != got { - t.Errorf("resolveSymlinks(root, %q) got: %q, want: %q", tst.rel, host, got) - } - } - }) - } -} - -func TestResolveSymlinksLoop(t *testing.T) { - root, err := ioutil.TempDir(testutil.TmpDir(), "root") - if err != nil { - t.Fatal("ioutil.TempDir() failed:", err) - } - dirs := []dir{ - {"loop1", "loop2"}, - {"loop2", "loop1"}, - } - if err := construct(root, dirs); err != nil { - t.Fatal("construct failed:", err) - } - if _, err := resolveSymlinks(root, "loop1"); err == nil { - t.Errorf("resolveSymlinks() should have failed") - } -} |