Skip to content

Commit

Permalink
rootfs: make pivot_root(2) dance handle initramfs case
Browse files Browse the repository at this point in the history
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 and make that a new root to work around this
problem.

This hack is 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. There are some minor issues with this
trick (the initramfs sticks around at the top of the mount tree, but is
completely masked) but they don't really matter for containers.

[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>
  • Loading branch information
cyphar committed Oct 10, 2024
1 parent 9112335 commit 16b45c8
Showing 1 changed file with 31 additions and 2 deletions.
33 changes: 31 additions & 2 deletions libcontainer/rootfs_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -1079,8 +1079,37 @@ func pivotRoot(rootfs string) error {
return &os.PathError{Op: "fchdir", Path: "fd " + strconv.Itoa(newroot), Err: err}
}

if err := unix.PivotRoot(".", "."); err != nil {
return &os.PathError{Op: "pivot_root", Path: ".", Err: err}
pivotErr := unix.PivotRoot(".", ".")
if errors.Is(pivotErr, unix.EINVAL) {
// If pivot_root(2) failed with -EINVAL, one of the possible reasons is
// that we are in early boot and trying pivot_root on top of the
// initramfs (which isn't allowed because initramfs/rootfs doesn't have
// a parent mount).
//
// Traditionally, users were told to pass --no-pivot-root (which used a
// chroot instead) but this is very insecure (even with the hardenings
// we've put into our chroot() wrapper).
//
// A much better solution is to create a bind-mount of the target and
// chroot into it, resulting in a parented mount that pivot_root(2)
// will accept. One minor issue is that the mount will still exist (and
// in the case of an init system like systemd, this will result in
// wasted memory, so they have to do some hacks to clear the initramfs)
// but the mount is masked in a much more safe way than chroot() so
// this is still much better.
if err := unix.Mount(".", ".", "", unix.MS_BIND|unix.MS_REC, ""); err != nil {
err := &os.PathError{Op: "bind mount over self", Path: rootfs, Err: err}
return fmt.Errorf("error during fallback for failed pivot_root (%w): %w", pivotErr, err)
}
if err := unix.Chroot("."); err != nil {
err := &os.PathError{Op: "chroot into bind-mount", Path: rootfs, Err: err}
return fmt.Errorf("error during fallback for failed pivot_root (%w): %w", pivotErr, err)
}
// Re-try the pivot_root().
pivotErr = unix.PivotRoot(".", ".")
}
if pivotErr != nil {
return &os.PathError{Op: "pivot_root", Path: rootfs, Err: err}
}

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

0 comments on commit 16b45c8

Please sign in to comment.