1package chrootarchive // import "github.com/docker/docker/pkg/chrootarchive"
2
3import (
4	"fmt"
5	"io/ioutil"
6	"os"
7	"path/filepath"
8
9	"github.com/docker/docker/pkg/mount"
10	rsystem "github.com/opencontainers/runc/libcontainer/system"
11	"golang.org/x/sys/unix"
12)
13
14// chroot on linux uses pivot_root instead of chroot
15// pivot_root takes a new root and an old root.
16// Old root must be a sub-dir of new root, it is where the current rootfs will reside after the call to pivot_root.
17// New root is where the new rootfs is set to.
18// Old root is removed after the call to pivot_root so it is no longer available under the new root.
19// This is similar to how libcontainer sets up a container's rootfs
20func chroot(path string) (err error) {
21	// if the engine is running in a user namespace we need to use actual chroot
22	if rsystem.RunningInUserNS() {
23		return realChroot(path)
24	}
25	if err := unix.Unshare(unix.CLONE_NEWNS); err != nil {
26		return fmt.Errorf("Error creating mount namespace before pivot: %v", err)
27	}
28
29	// Make everything in new ns slave.
30	// Don't use `private` here as this could race where the mountns gets a
31	//   reference to a mount and an unmount from the host does not propagate,
32	//   which could potentially cause transient errors for other operations,
33	//   even though this should be relatively small window here `slave` should
34	//   not cause any problems.
35	if err := mount.MakeRSlave("/"); err != nil {
36		return err
37	}
38
39	if mounted, _ := mount.Mounted(path); !mounted {
40		if err := mount.Mount(path, path, "bind", "rbind,rw"); err != nil {
41			return realChroot(path)
42		}
43	}
44
45	// setup oldRoot for pivot_root
46	pivotDir, err := ioutil.TempDir(path, ".pivot_root")
47	if err != nil {
48		return fmt.Errorf("Error setting up pivot dir: %v", err)
49	}
50
51	var mounted bool
52	defer func() {
53		if mounted {
54			// make sure pivotDir is not mounted before we try to remove it
55			if errCleanup := unix.Unmount(pivotDir, unix.MNT_DETACH); errCleanup != nil {
56				if err == nil {
57					err = errCleanup
58				}
59				return
60			}
61		}
62
63		errCleanup := os.Remove(pivotDir)
64		// pivotDir doesn't exist if pivot_root failed and chroot+chdir was successful
65		// because we already cleaned it up on failed pivot_root
66		if errCleanup != nil && !os.IsNotExist(errCleanup) {
67			errCleanup = fmt.Errorf("Error cleaning up after pivot: %v", errCleanup)
68			if err == nil {
69				err = errCleanup
70			}
71		}
72	}()
73
74	if err := unix.PivotRoot(path, pivotDir); err != nil {
75		// If pivot fails, fall back to the normal chroot after cleaning up temp dir
76		if err := os.Remove(pivotDir); err != nil {
77			return fmt.Errorf("Error cleaning up after failed pivot: %v", err)
78		}
79		return realChroot(path)
80	}
81	mounted = true
82
83	// This is the new path for where the old root (prior to the pivot) has been moved to
84	// This dir contains the rootfs of the caller, which we need to remove so it is not visible during extraction
85	pivotDir = filepath.Join("/", filepath.Base(pivotDir))
86
87	if err := unix.Chdir("/"); err != nil {
88		return fmt.Errorf("Error changing to new root: %v", err)
89	}
90
91	// Make the pivotDir (where the old root lives) private so it can be unmounted without propagating to the host
92	if err := unix.Mount("", pivotDir, "", unix.MS_PRIVATE|unix.MS_REC, ""); err != nil {
93		return fmt.Errorf("Error making old root private after pivot: %v", err)
94	}
95
96	// Now unmount the old root so it's no longer visible from the new root
97	if err := unix.Unmount(pivotDir, unix.MNT_DETACH); err != nil {
98		return fmt.Errorf("Error while unmounting old root after pivot: %v", err)
99	}
100	mounted = false
101
102	return nil
103}
104
105func realChroot(path string) error {
106	if err := unix.Chroot(path); err != nil {
107		return fmt.Errorf("Error after fallback to chroot: %v", err)
108	}
109	if err := unix.Chdir("/"); err != nil {
110		return fmt.Errorf("Error changing to new root after chroot: %v", err)
111	}
112	return nil
113}
114