| 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 | |
| } |