Filehandles, files, directories
chroot#
Change the root directory of the current process and every child it later spawns.
After a successful chroot, any pathname that begins with / is
resolved relative to the directory named by FILENAME instead of the
real filesystem root. The process loses the ability to name files
outside that subtree through absolute paths. The kernel enforces this
for the current process and for every descendant — there is no
per-thread scope and no way to undo it from inside the jail.
chroot does not change the current working directory. If the
working directory was outside the new root when the call succeeded, it
stays outside, and relative paths can still escape. Always follow a
successful chroot with chdir to / (which now means
inside the jail) before doing anything else.
The call is restricted to the superuser. An unprivileged process gets
EPERM and $! is set accordingly.
Synopsis#
chroot FILENAME
chroot
What you get back#
1 on success, 0 on failure. On failure $! holds
the errno from the underlying chroot(2) system call — commonly
EPERM (not root), ENOENT (path does not exist), or ENOTDIR
(path is not a directory).
Always check the return value. A silent failure leaves the process running outside the intended jail, which is usually worse than the code path that made the call in the first place.
chroot "/var/empty"
or die "chroot: $!";
chdir "/"
or die "chdir after chroot: $!";
Global state it touches#
Examples#
The canonical safe sequence — change root, then change working directory to the new root, then drop privileges:
chroot "/srv/jail" or die "chroot: $!";
chdir "/" or die "chdir: $!";
$) = $(= 65534; # drop to 'nobody' gids
$> = $< = 65534; # drop to 'nobody' uids
Open files and load shared libraries before chroot, not after.
The new root typically has neither /etc/resolv.conf nor the dynamic
linker’s search paths:
open my $log, ">>", "/var/log/app.log" or die $!;
my $cfg = read_config("/etc/app.conf");
chroot "/srv/jail" or die "chroot: $!";
chdir "/" or die "chdir: $!";
# $log and $cfg are still usable inside the jail.
Checking failure modes separately — distinguishing “not root” from “bad path” matters for diagnostics:
use Errno qw(EPERM ENOENT ENOTDIR);
unless (chroot $path) {
die "chroot: need root privileges\n" if $! == EPERM;
die "chroot: $path does not exist\n" if $! == ENOENT;
die "chroot: $path is not a directory\n" if $! == ENOTDIR;
die "chroot: $!";
}
Default-argument form reads the path from $_:
$_ = "/srv/jail";
chroot or die "chroot: $!";
Edge cases#
Not root: fails with
EPERM. There is no capability-based relaxation in stock Linux unless the process holdsCAP_SYS_CHROOT.Relative path:
chroot "jail"resolves against the current working directory at the moment of the call. Prefer absolute paths so the behavior does not depend on the caller’scwd.Symlinks in
FILENAME: resolved before the chroot takes effect. Symlinks inside the new root that point to absolute paths will resolve relative to the new root, not the real root — this is the intended jailing behavior, not a bug.Open file descriptors survive: any handle opened before the call remains usable afterward. This is how well-behaved daemons keep logs, config, and listening sockets across the transition.
No escape from inside: a process cannot
chrootits way out. The only way to leave a chroot is for a privileged process outside it to reap and replace it.Working directory outside the new root: the kernel allows the call, but relative paths and
..traversal from there can still reach files outside the jail. The mandatorychdir "/"afterchrootcloses that gap.Threads:
chrooton Linux is per-process (strictly, per fs struct). Underuse threadsall interpreter threads share the same root after the call — there is no per-thread jail.Does not affect
$0,%ENV, or open sockets. Only path resolution changes.
Differences from upstream#
Fully compatible with upstream Perl 5.42.
See also#
chdir— the mandatory follow-up after a successfulchroot; without it the working directory may still point outside the new rootfork— children inherit the chroot; combine withchrootto run untrusted code in a restricted subtreeexec— runs the replacement program inside the current root, so the target binary and its dynamic libraries must exist inside the jailopen— opening files by absolute path afterchrootlooks up names inside the jail; pre-open anything you need from outside before the call$_— the default path whenchrootis called with no argument$!— holds theerrnofromchroot(2)on failure