Skip to content

Commit a69d9d3

Browse files
committed
rootfs: make pivot_root(2) dance handle initramfs case
While pivot_root(2) normally refuses to pivot a mount if you are running with / as initramfs (because initramfs doesn't have a parent mount), you can create a bind-mount of / and make that your new root to work around this problem. This does use chroot(2), but this is only done temporarily to set current->fs->root to the new mount. Once pivot_root(2) finishes, the chroot(2) and / are gone. Variants of this hack are fairly well known and is used all over the place (see [1,2]) but until now we have forced users to have a far less secure configuration with --no-pivot. This is a slightly modified version that uses the container rootfs as the temporary spot for the / clone -- this allows runc to continue working with read-only image-based OS images. [1]: containers/bubblewrap#592 (comment) [2]: https://aconz2.github.io/2024/07/29/container-from-initramfs.html Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
1 parent e37371e commit a69d9d3

File tree

1 file changed

+49
-10
lines changed

1 file changed

+49
-10
lines changed

libcontainer/rootfs_linux.go

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,19 +1068,58 @@ func pivotRoot(rootfs string) error {
10681068
}
10691069
defer unix.Close(oldroot) //nolint: errcheck
10701070

1071-
newroot, err := unix.Open(rootfs, unix.O_DIRECTORY|unix.O_RDONLY, 0)
1072-
if err != nil {
1073-
return &os.PathError{Op: "open", Path: rootfs, Err: err}
1074-
}
1075-
defer unix.Close(newroot) //nolint: errcheck
1076-
10771071
// Change to the new root so that the pivot_root actually acts on it.
1078-
if err := unix.Fchdir(newroot); err != nil {
1079-
return &os.PathError{Op: "fchdir", Path: "fd " + strconv.Itoa(newroot), Err: err}
1072+
if err := os.Chdir(rootfs); err != nil {
1073+
return err
10801074
}
10811075

1082-
if err := unix.PivotRoot(".", "."); err != nil {
1083-
return &os.PathError{Op: "pivot_root", Path: ".", Err: err}
1076+
pivotErr := unix.PivotRoot(".", ".")
1077+
if errors.Is(pivotErr, unix.EINVAL) {
1078+
// If pivot_root(2) failed with -EINVAL, one of the possible reasons is
1079+
// that we are in early boot and trying pivot_root on top of the
1080+
// initramfs (which isn't allowed because initramfs/rootfs doesn't have
1081+
// a parent mount).
1082+
//
1083+
// Traditionally, users were told to pass --no-pivot-root (which used a
1084+
// chroot instead) but this is very insecure (even with the hardenings
1085+
// we've put into our chroot() wrapper).
1086+
//
1087+
// A much better solution is to create a bind-mount clone of / (which
1088+
// would have a parent) and then chroot into that clone so that we are
1089+
// properly rooted within a mount that has a parent mount. Then we can
1090+
// retry the pivot_root().
1091+
1092+
// Clone / on top of . to create a version of / that has a parent and
1093+
// so can be pivot-rooted.
1094+
if err := unix.Mount("/", ".", "", unix.MS_BIND|unix.MS_REC, ""); err != nil {
1095+
err := &os.PathError{Op: "make clone of / mount", Path: rootfs, Err: err}
1096+
return fmt.Errorf("error during fallback for failed pivot_root (%w): %w", pivotErr, err)
1097+
}
1098+
// Switch to the cloned mount. We have to use the full path here
1099+
// because we need to get the kernel to move us into the new mount
1100+
// (chdir(".") will keep us in the old non-cloned / mount).
1101+
if err := os.Chdir(rootfs); err != nil {
1102+
return fmt.Errorf("error during fallback for failed pivot_root (%w): switch to cloned mount: %w", pivotErr, err)
1103+
}
1104+
// Move the cloned mount to /.
1105+
if err := unix.Mount(".", "/", "", unix.MS_MOVE, ""); err != nil {
1106+
err := &os.PathError{Op: "move / clone mount to /", Path: rootfs, Err: err}
1107+
return fmt.Errorf("error during fallback for failed pivot_root (%w): %w", pivotErr, err)
1108+
}
1109+
// Update current->fs->root to be the cloned / (to be pivot_root'd).
1110+
if err := unix.Chroot("."); err != nil {
1111+
err := &os.PathError{Op: "chroot into cloned /", Path: rootfs, Err: err}
1112+
return fmt.Errorf("error during fallback for failed pivot_root (%w): %w", pivotErr, err)
1113+
}
1114+
1115+
// Go back to the container rootfs and retry pivot_root.
1116+
if err := os.Chdir(rootfs); err != nil {
1117+
return fmt.Errorf("error during fallback for failed pivot_root (%w): %w", pivotErr, err)
1118+
}
1119+
pivotErr = unix.PivotRoot(".", ".")
1120+
}
1121+
if pivotErr != nil {
1122+
return &os.PathError{Op: "pivot_root", Path: rootfs, Err: pivotErr}
10841123
}
10851124

10861125
// Currently our "." is oldroot (according to the current kernel code).

0 commit comments

Comments
 (0)