The chroot utility runs a process with the given directory set as the root / directory of its filesystem. There are many use cases for such a tool, e.g. to mess with a different Linux system without booting it, to run glibc software on a musl-based host, or to test examples on a fresh system.
This article will not dwell on security concerns: we’ll assume the software we run is not adversiarial. If secure sandboxing is your primary interest, then further measures beyond chroot are necessary to set up a true container, e.g. namespaces and cgroups (see this blog). Containerization software such as Docker automatically manages all these aspects for you.
Setting up a root directory
The easiest way to setup a sysroot is to download a prebuilt one from a Linux distribution, e.g. the rootfs tarball from Void Linux.
$ mkdir void_sysroot && cd void_syroot
$ wget https://repo-default.voidlinux.org/live/current/void-x86_64-ROOTFS-20250202.tar.xz
$ tar -xpf void-x86_64-ROOTFS-20250202.tar.xz
$ ls
bin dev home lib32 media opt root sbin tmp var
boot etc lib lib64 mnt proc run sys usr
$ cd ..
We should set everything in our sysroot to be owned by the root user.
$ sudo chown -R root:root void_sysroot/*
Now we can simply chroot into a shell.
$ sudo chroot $PWD/void_sysroot /bin/bash
bash-5.3# ls
bin boot dev etc home lib lib32 lib64 media mnt opt proc root run sbin sys tmp usr var
bash-5.3# ldd /usr/bin/ls
linux-vdso.so.1 (0x00007e1ab535d000)
libcap.so.2 => /usr/lib/libcap.so.2 (0x00007e1ab5319000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007e1ab512f000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007e1ab535f000
We are inside a Void Linux sysroot and can run the included dynamic binaries with the included program interpreter and dynamic libraries.
Sharing kernel drivers and hardware
Not everything is going to work yet.
bash-5.3# ls /dev/
bash-5.3# ls /sys/
bash-5.3# ls /proc/
bash-5.3# exit
We have no /dev, /sys, or /proc directories! If we ever need to take advantage of drivers or other system files then we are fresh out of luck. This may be desirable if you don’t want to allow such access, but a problem if you want a fully functional sandbox. We can remedy this by binding the relevant filesystems within the sysroot.
$ sudo mount -t proc none $PWD/void_sysroot/proc
$ sudo mount -t sysfs none $PWD/void_sysroot/sys
$ sudo mount -t tmpfs tmpfs $PWD/void_sysroot/tmp
$ sudo mount -t tmpfs tmpfs $PWD/void_sysroot/run
$ sudo mount --rbind --make-rslave /dev $PWD/void_sysroot/dev
$ sudo chroot $PWD/void_sysroot /bin/bash
bash-5.3# ls /sys/
block bus class dev devices firmware fs hypervisor kernel module power
The combination of --rbind and --make-rslave allows the host to add device nodes to the sysroot, but does not propogate changes to the syroot’s /dev back to the host.
After you finish using your sysroot you should clean up the mount bindings.
bash-5.3# exit
$ umount $PWD/void_sysroot/proc
$ umount $PWD/void_sysroot/sys
$ umount $PWD/void_sysroot/run
$ umount $PWD/void_sysroot/tmp
$ umount -R $PWD/void_sysroot/dev
Network access
Allowing network access is generally as simple as sharing the resolve.conf file from host to guest.
$ cp /etc/resolv.conf $PWD/void_sysroot/etc/resolv.conf
Terminal interfaces
It’s also often useful to share terminfo to allow TUIs to function correctly.
$ mkdir -p $PWD/void_sysroot/usr/share/terminfo
$ cp -r /usr/share/terminfo/* $PWD/void_sysroot/usr/share/terminfo/
Display windows and audio
Even if you install compatible graphics and/or audio libraries within your sysroot, binaries run within a sysroot won’t be able to interact with the host. E.g. if you install libwayland within a sysroot and your host is running a Wayland compositor, you won’t automatically be able to open Wayland windows from a chroot. Sharing the $XDG_RUNTIME_DIR will allow most applications to work.
$ mkdir -p $PWD/fs$XDG_RUNTIME_DIR
$ mount --bind --make-rslave $XDG_RUNTIME_DIR $PWD/fs$XDG_RUNTIME_DIR
Once again we pass --bind and --make-rslave for one way propogation of changes from host to the sysroot.
Full script with cleanup
The following script performs all of the above setup for a given sysroot directory, then cleans up after the chroot shell exits.
# chroot.sh
# usage: ./chroot.sh path_to_sysroot
# set up special Linux system directories
mount -t proc none $1/proc
mount -t sysfs none $1/sys
mount -t tmpfs tmpfs $1/tmp
mount -t tmpfs tmpfs $1/run
mount --rbind --make-rslave /dev $1/dev
# share terminfo
mkdir -p $1/usr/share/terminfo
cp -r /usr/share/terminfo/* $1/usr/share/terminfo/
# share XDG_RUNTIME_DIR
mkdir -p $1$XDG_RUNTIME_DIR
mount --bind --make-rslave $XDG_RUNTIME_DIR $1$XDG_RUNTIME_DIR
# share network config
cp /etc/resolv.conf $1/etc/resolv.conf
chroot $1 /bin/bash
# unmount bindings and filesystems
umount $1$XDG_RUNTIME_DIR
umount $1/proc
umount $1/sys
umount $1/run
umount $1/tmp
umount -R $1/dev