1

I read the linux documentation on initramfs and the source code of switch_root.

The documentation says:

When switching another root device, initrd would pivot_root and then umount the ramdisk. But initramfs is rootfs: you can neither pivot_root rootfs, nor unmount it. Instead delete everything out of rootfs to free up the space (find -xdev / -exec rm '{}' ';'), overmount rootfs with the new root (cd /newmount; mount --move . /; chroot .), attach stdin/stdout/stderr to the new /dev/console, and exec the new init.

And switch_root really does so:

if (chdir(newroot)) {
    warn(_("failed to change directory to %s"), newroot);
    return -1;
}

...

if (mount(newroot, "/", NULL, MS_MOVE, NULL) < 0) {
    close(cfd);
    warn(_("failed to mount moving %s to /"), newroot);
    return -1;
}

...

if (chroot(".")) {
    close(cfd);
    warn(_("failed to change root"));
    return -1;
}

Why do we need to move the mount point over /?
Why isn't chrooting to new_root sufficient?

2
  • Maybe because of chroot break: 1 2 3(pdf)
    – A.B
    Apr 28, 2020 at 16:43
  • @A.B Thanks, it really prevents it. Apr 28, 2020 at 18:00

2 Answers 2

2

Edit: edited thanks to @timothy-baldwin.

Mounting new_root over / changes the root directory of the mount namespace, chrooting without overmounting / will cause the system to be in a chroot environment (where the root directory does not match the root directory of the mount namespace).

This will cause some problems, for example:

1. Creating user namespaces is not allowed when inside chroot.

According to man 2 unshare, unshareing user namespace will fail with EPERM when in chroot environment.

EPERM (since Linux 3.9)
      CLONE_NEWUSER was specified in flags and the caller is in a  chroot  environment
      (i.e., the  caller's root directory does not match the root directory of the
      mount namespace in which it resides).
$ unshare -U
unshare: unshare failed: Operation not permitted

2. Entering a mount namespace will set the root directory to the namespace's root directory

Entering a mount namespace sets the root directory of the process to the root directory of the mount namespace, so doing setns to our mount namespace will set our root directory to the rootfs directory.

$ nsenter -m/proc/self/ns/mnt /bin/sh
$ ls -ld /new_root
new_root

I can see new_root directory which is outside my chroot.

Mounting over / does not really prevent escaping the chroot

The root user can umount this directory, re-entering its mount namespace (setns) and see the rootfs:

#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>

int main() {
    int ns = open("/proc/self/ns/mnt", O_RDONLY);
    if (ns == -1) {
        perror("open");
        goto out;
    }

    if (umount2("/", MNT_DETACH)) {
        perror("umount2");
        goto out;
    }

    if (setns(ns, CLONE_NEWNS)) {
        perror("setns");
        goto out;
    }

    char *a[] = { "/bin/sh", NULL };
    char *e[] = { NULL };
    execve(a[0], a, e);

    perror("execve");

out:
    return 1;
}
$ gcc -o main main.c
$ unshare -m ./main
/ # ls -d new_root
new_root
/ # mount -t proc proc /proc
/ # cat /proc/mounts
none / rootfs rw 0 0
proc /proc proc rw,relatime 0 0

Mounting new_root over / is necessary in order to prevent chroot escapes.

Created a minimal initramfs and replaces switch_root binary with this shell script to get a shell:

#!/bin/sh

exec /bin/sh

Also changed /bin/sh inside the initramfs to a statically linked busybox.

Compiled the following code and linked it statically:

#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open(".", O_RDONLY | O_CLOEXEC);
    if (fd < 0) {
        perror("open");
        goto out0;
    }

    if (chroot("tmp")) {
        perror("chroot");
        goto out1;
    }

    if (fchdir(fd)) {
        perror("fchdir");
        goto out1;
    }

    if (chdir("..")) {
        perror("chdir");
        goto out1;
    }

    char *const argvp[] = { "sh", NULL };
    char *const envp[] = { NULL };
    execve("bin/sh", argvp, envp);

    perror("execve");

out1:
    close(fd);

out0:
    return  1;

}

Places in my real root filesystem's root directory as /escape.

Rebooted and got a shell just before the switch_root takes place.

Without overmounting root

$ mount --move proc new_root/proc
$ mount --move dev new_root/dev
$ mount --move sys new_root/sys
$ mount --move run new_root/run
$ exec chroot new_root
$ ./escape
$ ls -d new_root
new_root

I escaped the chroot.

With overmounting root

$ mount --move proc new_root/proc
$ mount --move dev new_root/dev
$ mount --move sys new_root/sys
$ mount --move run new_root/run
$ cd new_root
$ mount --move . /
$ exec chroot .
$ ./escape
$ ls -d new_root
ls: cannot access 'new_root': No such file or directory

I cannot escape the chroot.

5
1

Not overmounting rootfs breaks user and mount namespaces:

  • The setns system call will set the callers root directory to root directory of the mount namespace, undoing chroot.
  • If the process root directory is not the root directory of it's mount namespace creating a user namespace by an unprivileged process is forbidden.
2
  • Did you mean setns syscall? And what do you by "unprivileged process is forbidden"? May 2, 2020 at 10:54
  • @IdanYadgar I've added the missing words. May 2, 2020 at 11:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .