Build and Test a Small chroot Environment Safely
You will run a command with its filesystem root changed to a directory you control, verify what the command can see, and remove the test environment without touching the host filesystem. Allow about twenty minutes. You need GNU coreutils 9.4 or a compatible installation, a shell, and elevated privileges for the chroot operation itself. The examples use a temporary directory and do not alter a service.
The route
Jump straight to the step you need, or tick off Done means at the end.
Safety boundary
chroot changes the apparent root directory for a process. It is not a complete security sandbox. A process may retain capabilities, access resources outside the filesystem boundary through other mechanisms, or escape a poorly designed setup. Do not use this guide as a substitute for containers, a dedicated virtual machine or a security policy.
1. Check the installed command
First confirm which implementation you have and read its local contract. These are ordinary, read-only commands:
$ command -v chroot
/usr/sbin/chroot
$ chroot --version
chroot (GNU coreutils) 9.4
$ dpkg-query -W -f='${Package} ${Version}\n' coreutils
coreutils 9.4-3ubuntu6.3
Your package revision can differ. The installed manual describes four options: supplementary groups with --groups, an effective user and group with --userspec, retaining the current working directory with --skip-chdir, and the usual help and version options.
Checkpoint
The command must be GNU chroot, and you should know whether the account running it has the privilege required by your system. A normal unprivileged attempt commonly ends with exit status 125 because the directory change is refused.
2. Create a deliberately small root directory
Make a temporary directory that will become the new root. This changes state only below /tmp and does not need root:
$ ROOT_DIR="$(mktemp -d /tmp/chroot-demo.XXXXXX)"
$ mkdir -p "$ROOT_DIR/bin"
$ cp --preserve=mode /bin/sh "$ROOT_DIR/bin/sh"
$ printf 'root directory: %s\n' "$ROOT_DIR"
root directory: /tmp/chroot-demo.A1b2C3
The shell binary is present, but its shared libraries are not. On a dynamically linked host, /bin/sh will therefore fail inside this root. That is useful here: it demonstrates that chroot does not copy or magically provide host files. A usable root needs the command, its loader, shared libraries, device nodes and any required configuration, all arranged below the new root.
Do not copy an arbitrary host command into a test root and assume it is self-contained. Check dependencies with your normal package or ELF inspection tools, and keep untrusted input out of the commands you run as root.
3. Run a command below the new root
When the root directory contains a runnable command, invoke chroot with the new root first, then an absolute command path. The following is the basic shape:
$ sudo chroot "$ROOT_DIR" /bin/sh -c 'printf "inside root: %s\n" "$PWD"'
inside root: /
sudo is the elevated part. The command after the new root is run in the changed filesystem view, and the default behaviour also changes its working directory to /. If the shell is dynamically linked, this example instead reports a loader or library error until you populate those dependencies. That is a setup failure, not evidence that the root change did not occur.
Use a complete, known-good root for a real test. For example, a distribution bootstrap tool can create one, but the exact files and package commands depend on the distribution and are outside this coreutils interface. Do not improvise by copying sensitive host files such as password databases, private keys or service credentials.
Checkpoint
A successful smoke test prints a line from the child and returns status 0. Capture the status when diagnosing a failure:
$ sudo chroot "$ROOT_DIR" /bin/sh -c 'printf "child-ok\n"'
$ status=$?
$ printf 'chroot status: %s\n' "$status"
chroot status: 0
4. Keep the current directory when you need it
By default, chroot changes the child's working directory to /. The --skip-chdir option suppresses that step:
$ sudo chroot --skip-chdir "$ROOT_DIR" /bin/sh -c 'printf "working directory: %s\n" "$PWD"'
working directory: /home/andy/src/manpages/prompts/generated/chroot-8
The displayed directory must still be meaningful from inside the new root. A current directory outside the new filesystem can be confusing, and a child may fail when it tries to use a relative path there. Use this option only when retaining the caller's directory is intentional. Otherwise, accept the safer and more predictable default of starting at /.
5. Limit the child identity and groups
After the root is ready, you can ask chroot to run the command with a specified user and group. Names and numeric IDs are accepted by the installed manual; numeric values avoid depending on files such as /etc/passwd inside the new root:
$ sudo chroot --userspec=65534:65534 "$ROOT_DIR" /bin/sh -c 'id -u; id -g'
65534
65534
This example assumes that the root contains a working shell and the id program, so its exact output depends on how the root was built. The user and group change is security-sensitive: verify the selected IDs before running a command, and do not treat a non-root identity as proof that the process has no useful capabilities.
Supplementary groups are separate from the primary user and group. If the child must have a precise group set, supply a comma-separated list with --groups=G_LIST and verify the result from inside the child. Do not add host groups merely to make a failing command work.
6. Understand failures and exit statuses
The installed command reserves status 125 for a failure of chroot itself. Status 126 means the command was found but could not be invoked, and 127 means it could not be found. If chroot starts the command, the child's own exit status is returned:
$ sudo chroot "$ROOT_DIR" /bin/does-not-exist
chroot: failed to run command '/bin/does-not-exist': No such file or directory
$ printf 'status: %s\n' "$?"
status: 127
Do not confuse a missing command with a missing shared library. Both can produce a "No such file or directory" message, because the kernel also needs the interpreter named by the executable. Inspect the root's command and its dependencies before changing permissions or copying more files.
A failed command can leave a partial output file if it was writing somewhere below the new root. Use a temporary destination and rename it only after a successful status, just as you would for any conversion or build step.
7. Remove the test root
When no process is using the temporary root, inspect it and remove exactly the directory you created:
$ find "$ROOT_DIR" -maxdepth 2 -print
$ rm -rf -- "$ROOT_DIR"
$ test ! -e "$ROOT_DIR" && echo 'temporary root removed'
temporary root removed
Warning
rm -rf is irreversible. Check the variable with printf '%s\n' "$ROOT_DIR" before running it, and never substitute a broad path such as /, your home directory or an unset variable. There is no recovery command for files removed this way; restore them from a backup if you deleted the wrong target. If a process still has the directory as its root or current directory, stop that process first and investigate rather than forcing cleanup.
Done means
- You confirmed the installed GNU coreutils version and the local
chrootoptions. - The test root was created below a specific temporary path, with no host credentials copied into it.
- A command ran with an absolute path below the new root, and its status was checked.
- You treated
--skip-chdir,--userspecand--groupsas deliberate choices rather than defaults. - You interpreted 125, 126 and 127 as different failure classes.
- The temporary root was removed only after its exact path was checked.