DEV Community

Arseny Zinchenko
Arseny Zinchenko

Posted on • Originally published at rtfm.co.ua on

What is: chroot – the system call and utility in Linux

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):


$ 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
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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

$ mkdir -p /tmp/chroot/{1,2,3,4}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

$ gcc chroot_example.c -o chroot_example
Enter fullscreen mode Exit fullscreen mode

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

$ 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
Enter fullscreen mode Exit fullscreen mode

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

SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
  return ksys_chroot(filename);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

$ 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
Enter fullscreen mode Exit fullscreen mode

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

$ cd /tmp/
$ mkdir changed_root
Enter fullscreen mode Exit fullscreen mode

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

$ 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 +++
Enter fullscreen mode Exit fullscreen mode

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::

[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
Enter fullscreen mode Exit fullscreen mode

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

[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
Enter fullscreen mode Exit fullscreen mode

Try again:

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

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:

[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)
Enter fullscreen mode Exit fullscreen mode

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

[setevoy@setevoy-arch-work /tmp]  $ mkdir changed_root/usr/lib changed_root/lib64
Enter fullscreen mode Exit fullscreen mode

And copy libs files:

[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
Enter fullscreen mode Exit fullscreen mode

Run the chroot again:

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

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

bash-5.0# pwd
/
Enter fullscreen mode Exit fullscreen mode

But obviously – no other external utils will work here:

bash-5.0# ls -l
bash: ls: command not found
Enter fullscreen mode Exit fullscreen mode

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

[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/
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

See also

Similar posts

Top comments (3)

Collapse
 
biros profile image
Boris Jamot ✊ /

Back to the root😉

Collapse
 
theodesp profile image
Theofanis Despoudis

Classic. This should be on every developer’s knowledge list

Collapse
 
setevoy profile image
Arseny Zinchenko

Agreed)
The topic I hope to write about will be cgroups.
Just need to find some time to do it :-|