dotcloud/docker

View on GitHub
internal/mounttree/switchroot_linux.go

Summary

Maintainability
A
2 hrs
Test Coverage
package mounttree // import "github.com/docker/docker/internal/mounttree"

import (
    "fmt"
    "os"
    "path/filepath"

    "github.com/moby/sys/mount"
    "github.com/moby/sys/mountinfo"
    "golang.org/x/sys/unix"
)

// SwitchRoot changes path to be the root of the mount tree and changes the
// current working directory to the new root.
//
// This function bind-mounts onto path; it is the caller's responsibility to set
// the desired propagation mode of path's parent mount beforehand to prevent
// unwanted propagation into different mount namespaces.
func SwitchRoot(path string) error {
    if mounted, _ := mountinfo.Mounted(path); !mounted {
        if err := mount.Mount(path, path, "bind", "rbind,rw"); err != nil {
            return realChroot(path)
        }
    }

    // setup oldRoot for pivot_root
    pivotDir, err := os.MkdirTemp(path, ".pivot_root")
    if err != nil {
        return fmt.Errorf("Error setting up pivot dir: %v", err)
    }

    var mounted bool
    defer func() {
        if mounted {
            // make sure pivotDir is not mounted before we try to remove it
            if errCleanup := unix.Unmount(pivotDir, unix.MNT_DETACH); errCleanup != nil {
                if err == nil {
                    err = errCleanup
                }
                return
            }
        }

        errCleanup := os.Remove(pivotDir)
        // pivotDir doesn't exist if pivot_root failed and chroot+chdir was successful
        // because we already cleaned it up on failed pivot_root
        if errCleanup != nil && !os.IsNotExist(errCleanup) {
            errCleanup = fmt.Errorf("Error cleaning up after pivot: %v", errCleanup)
            if err == nil {
                err = errCleanup
            }
        }
    }()

    if err := unix.PivotRoot(path, pivotDir); err != nil {
        // If pivot fails, fall back to the normal chroot after cleaning up temp dir
        if err := os.Remove(pivotDir); err != nil {
            return fmt.Errorf("Error cleaning up after failed pivot: %v", err)
        }
        return realChroot(path)
    }
    mounted = true

    // This is the new path for where the old root (prior to the pivot) has been moved to
    // This dir contains the rootfs of the caller, which we need to remove so it is not visible during extraction
    pivotDir = filepath.Join("/", filepath.Base(pivotDir))

    if err := unix.Chdir("/"); err != nil {
        return fmt.Errorf("Error changing to new root: %v", err)
    }

    // Make the pivotDir (where the old root lives) private so it can be unmounted without propagating to the host
    if err := unix.Mount("", pivotDir, "", unix.MS_PRIVATE|unix.MS_REC, ""); err != nil {
        return fmt.Errorf("Error making old root private after pivot: %v", err)
    }

    // Now unmount the old root so it's no longer visible from the new root
    if err := unix.Unmount(pivotDir, unix.MNT_DETACH); err != nil {
        return fmt.Errorf("Error while unmounting old root after pivot: %v", err)
    }
    mounted = false

    return nil
}

func realChroot(path string) error {
    if err := unix.Chroot(path); err != nil {
        return fmt.Errorf("Error after fallback to chroot: %v", err)
    }
    if err := unix.Chdir("/"); err != nil {
        return fmt.Errorf("Error changing to new root after chroot: %v", err)
    }
    return nil
}