What is: chroot – system call and utility in Linux

By | 03/23/2019

chroot() was added to the Version 7 Unix in 1979 and used for filesystem isolation.

In fact, it’s the predecessor of the whole current containerization idea, just now there are namespaces and cgroups are used while earlier chroot was used to create an environment which is isolated from a host and can be used for testing purposes, for example.

Also, the ch and root is an “abbreviation” from the change and root (of a filesystem).

Linux file system tree

The directories tree in Linux usually looks like next (see also Filesystem Hierarchy Standard):

[simterm]

$ tree -d -L 1 /
/
├── bin -> usr/bin
├── boot
├── data
├── dev
├── etc
├── home
├── lib -> usr/lib
├── lib64 -> usr/lib
├── lost+found
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin -> usr/bin
├── srv
├── sys
├── tmp
├── usr
└── var

[/simterm]

chroot() allows to create a nested filesystem tree which can be demonstrated with the next picture:

Below we will take a closer look at the chroot() with some C code example, and on the chroot utility and its usage in an operating system.

chroot() – the Linux system call

So, chroot is intended to limit access to a filesystem by changing its root.

I.e. instead of directories structure like this:

[simterm]

$ tree -d -L 1 /
/
├── bin -> usr/bin
├── boot
├── data
...
├── tmp
├── usr
└── var

[/simterm]

A process will see the only those which are limited at the top-level by a parameter passed to the chroot().

Let’s create the next directories to be used for example:

[simterm]

$ mkdir -p /tmp/chroot/{1,2,3,4}

[/simterm]

And let’s write the next code in C:

#include <stdio.h>
#include <unistd.h>
#include <dirent.h>

int main(void) {

    // check path before chroot()
    char t_cwd[PATH_MAX];
    getcwd(t_cwd, sizeof(t_cwd));
    printf("Current dir before chroot(): %s\n", t_cwd);

    // do chroot()
    chdir("/tmp/chroot/");
    if (chroot("/tmp/chroot/") != 0) {
        perror("chroot /tmp/chroot/");
        return 1;
    }

    // check path path after chroot()
    char a_cwd[PATH_MAX];
    getcwd(a_cwd, sizeof(a_cwd));
    printf("Current dir after chroot(): %s\n", a_cwd);

    // point dr struct to the "root"
    struct dirent *de;
    DIR *dr = opendir("/");  
  
    // run readdir() and list "root"'s content
    while ((de = readdir(dr)) != NULL)  
        printf("%s\n", de->d_name);  

    // try to open /etc/passwd from a "host" filesystem
    FILE *f;
    f = fopen("/etc/passwd", "r");

    if (f == NULL) {
        perror("/etc/passwd");
        return 1;
    } else {
        char buf[100];
        while (fgets(buf, sizeof(buf), f)) {
             printf("%s", buf);
        }
    }

    return 0;
}

Here it will:

  • check the current path before calling chroot()
  • call the chroot()
  • check current path again
  • get the “root” content
  • try to open the /etc/passwd file which is present on a “real” filesystem

Build it:

[simterm]

$ gcc chroot_example.c -o chroot_example

[/simterm]

And run to check (with sudo as chroot() can be used by root only):

[simterm]

$ sudo ./chroot_example 
Current dir before chroot(): /home/setevoy/Scripts/C
Current dir after chroot(): /
.
..
4
3
2
1
/etc/passwd: No such file or directory

[/simterm]

The chroot()itself is defined in the kernel’s open.c file:

SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
  return ksys_chroot(filename);
}

And will return the ksys_chroot():

int ksys_chroot(const char __user *filename)
{
  struct path path;
  int error;
  unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
retry:
  error = user_path_at(AT_FDCWD, filename, lookup_flags, &path);
  if (error)
    goto out;

  error = inode_permission(path.dentry->d_inode, MAY_EXEC | MAY_CHDIR);
  if (error)
    goto dput_and_out;

  error = -EPERM;
  if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
    goto dput_and_out;
  error = security_path_chroot(&path);
  if (error)
    goto dput_and_out;

  set_fs_root(current->fs, &path);
  error = 0;
dput_and_out:
  path_put(&path);
  if (retry_estale(error, lookup_flags)) {
    lookup_flags |= LOOKUP_REVAL;
    goto retry;
  }
out:
  return error;
}

Which in its turn will call the set_fs_root() for a process:

void set_fs_root(struct fs_struct *fs, const struct path *path)
{
  struct path old_root;

  path_get(path);
  spin_lock(&fs->lock);
  write_seqcount_begin(&fs->seq);
  old_root = fs->root;
  fs->root = *path;
  write_seqcount_end(&fs->seq);
  spin_unlock(&fs->lock);
  if (old_root.dentry)
    path_put(&old_root);
}

You can find good syscalls description here>> and here>>>.


chroot– the Linux utility

To create an isolated space in Linux you can use chroot utility:

[simterm]

$ which chroot
/usr/bin/chroot
$ file /usr/bin/chroot
/usr/bin/chroot: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f3861107940247a67dbbf6343fa5ff1c1c70305c, stripped

[/simterm]

Let’s create a catalog for our “jail” (FreeBSD’s jail is an advanced successor of UNIX’s chroot) with an isolated filesystem:

[simterm]

$ cd /tmp/
$ mkdir changed_root

[/simterm]

Actually, the chroot utility will call the same chroot()system call – let’s check it with the strace:

[simterm]

$ sudo strace -e trace=chroot chroot changed_root/
chroot("changed_root/")                 = 0
chroot: failed to run command ‘/bin/bash’: No such file or directory
+++ exited with 127 +++

[/simterm]

The ‘/bin/bash’: No such file or directory error is caused by the fact that in this new environment there is no /bin directory and the bash executable.

Similarly, such an error will be returned if try to call any other program::

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ which ls
/usr/bin/ls
[setevoy@setevoy-arch-work /tmp]  $ sudo chroot changed_root /usr/bin/ls
chroot: failed to run command ‘/usr/bin/ls’: No such file or directory

[/simterm]

Let’s fix it – create /bin directory in our /tmp/changed_root and copy bashfile from a “host ” inside of this “container”:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ mkdir changed_root/bin
[setevoy@setevoy-arch-work /tmp]  $ cp /bin/bash changed_root/bin
[setevoy@setevoy-arch-work /tmp]  $ file changed_root/bin/bash 
changed_root/bin/bash: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=357034d1736cd97d2c8f8347045250dbd0de998e, stripped

[/simterm]

Try again:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ sudo chroot changed_root /bin/bash
chroot: failed to run command ‘/bin/bash’: No such file or directory

[/simterm]

Okay.

But now it’s caused because there are no necessary libs – chroot just can’t tell about this.

Check the bash‘s dependencies with the ldd:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ ldd /bin/bash
        linux-vdso.so.1 (0x00007ffe37f16000)
        libreadline.so.8 => /usr/lib/libreadline.so.8 (0x00007f39b13d2000)
        libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f39b13cd000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f39b1209000)
        libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007f39b119a000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f39b153f000)

[/simterm]

Create two more catalogs –  /lib and /lib64 in our new working directory:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ mkdir changed_root/usr/lib changed_root/lib64

[/simterm]

And copy libs files:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ cp /usr/lib/libreadline.so.8 changed_root/usr/lib/
[setevoy@setevoy-arch-work /tmp]  $ cp /usr/lib/libdl.so.2 changed_root/usr/lib/
[setevoy@setevoy-arch-work /tmp]  $ cp /usr/lib/libc.so.6 changed_root/usr/lib/
[setevoy@setevoy-arch-work /tmp]  $ cp /usr/lib/libncursesw.so.6 changed_root/usr/lib/
[setevoy@setevoy-arch-work /tmp]  $ cp /lib64/ld-linux-x86-64.so.2 changed_root/lib64

[/simterm]

Run the chroot again:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ sudo chroot changed_root/
bash-5.0#

[/simterm]

Now we have bash running here and all its built-in functions:

[simterm]

bash-5.0# pwd
/

[/simterm]

But obviously – no other external utils will work here:

[simterm]

bash-5.0# ls -l
bash: ls: command not found

[/simterm]

And this can be fixed in the same way as we did it for the bash:

[simterm]

[setevoy@setevoy-arch-work /tmp]  $ which ls
/usr/bin/ls
[setevoy@setevoy-arch-work /tmp]  $ cp /usr/bin/ls changed_root/bin/
[setevoy@setevoy-arch-work /tmp]  $ ldd /usr/bin/ls
        linux-vdso.so.1 (0x00007ffdebbf5000)
        libcap.so.2 => /usr/lib/libcap.so.2 (0x00007fa5b147d000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007fa5b12b9000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fa5b14d8000)
[setevoy@setevoy-arch-work /tmp]  $ cp /usr/lib/libcap.so.2 changed_root/usr/lib/

[/simterm]

Other libs already are copied so let’s run ls again:

[simterm]

bash-5.0# /bin/ls -l /
total 0
drwxr-xr-x 2 1000 1000  80 Mar 22 11:45 bin
drwxr-xr-x 2 1000 1000 120 Mar 22 11:37 lib
drwxr-xr-x 2 1000 1000  60 Mar 22 11:38 lib64
drwxr-xr-x 3 1000 1000  60 Mar 22 11:39 usr

[/simterm]

See also