What is: chroot – системный вызов и утилита в Linux

Автор: | 23/03/2019

chroot() был добавлен в Version 7 Unix в 1979 году и используется для изоляции файловой системы.

По сути, является предшественником вообще всей идеи нынешней контейнеризации, только в современных системах используются namespaces и cgroups, а раньше применяли chroot для создания изолированного от хоста рабочего окружения, которое могло использоваться для тестирования.

Собственно, ch и root и является “аббревиатурой” от change и rootизменить корень (файловой системы).

Дерево файловой системы Linux

Структура каталогов в Linux как правило выглядит следующим образом (см. 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() позволяет создать “вложенное” дерево системы, которое можно проиллюстрировать так:

Ниже рассмотрим сам системный вызов chroot() на примере кода на Си, и утилиту chroot и её применение в операционной системе.

chroot() – системный вызов

Задача chroot – ограничить доступ к файловой системе, изменив корневую файловую систему для процесса.

Вместо всего дерева каталогов:

[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().

Создадим каталог для примеров:

[simterm]

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

[/simterm]

И используем такой код:

#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;
}

Тут мы:

  • проверяем путь к текущему каталогу до вызова chroot()
  • выполняем chroot()
  • проверяем текущий путь ещё раз
  • получаем содержимое “корневого” каталога
  • и пробуем открыть файл /etc/passwd, который существует в “реальной” системе

Собираем:

[simterm]

$ gcc chroot_example.c -o chroot_example

[/simterm]

Проверяем:

[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]

Сам системный вызов chroot() определяется в open.c:

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

И возвращает 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;
}

Которая в свою очередь и вызывает set_fs_root() для процесса:

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);
}

Хорошое описание по структуре системных вызовов есть тут>> и тут>>>.


Утилита chroot

Для создания изолированного пространства в Linux можно использовать утилиту chroot:

[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]

Создадим каталог для нашей “клетки” (jail во FreeBSD является именно “продвинутым” развитием chroot) с изолированной файловой системой:

[simterm]

$ cd /tmp/
$ mkdir changed_root

[/simterm]

Собственно утилита chroot и выполнит системный вызов chroot() – проверяем с 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]

Ошибка ‘/bin/bash’: No such file or directory вызвана тем, что в новом окружении, очевидно, нет каталога /bin и исполняемого файла bash.

Аналогично будет выдана ошибка на любую другую программу:

[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]

Попробуем исправить её – создадим каталог /bin в нашем /tmp/changed_root, и скопируем исполняемый файл bash с “хоста” в этот “контейнер”:

[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]

Пробуем ещё раз:

[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.

Но сейчас ошибка вызвана уже тем, что нет необходимых библиотек, хотя утилита chroot просто не умеет об этом нормально сообщить, и выводит то же самое сообщение.

Проверяем зависимости bash:

[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]

Создаём каталоги /lib и /lib64 в нашем новом корневом каталоге:

[simterm]

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

[/simterm]

И копируем библиотеки:

[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]

Пробуем ещё раз:

[simterm]

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

[/simterm]

Тут у нас будут работать встроенные функции самого bash:

[simterm]

bash-5.0# pwd
/

[/simterm]

Но, очевидно, что не будут работать внешние утилиты:

[simterm]

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

[/simterm]

Что исправляется тем же образом, что мы применяли для запуска 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]

Остальные файлы библиотек у нас уже есть – проверяем работу ls теперь:

[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]

Ссылки по теме