CUBE SUGAR CONTAINER

技術系のこと書きます。

pivot_root について

今回は、Linux でプロセスのルートファイルシステムの場所を変更する機能の pivot_root について扱う。 プロセスのルートファイルシステムを変更するのは、古典的な chroot を使っても実現できる。 ただ、chroot は隔離したはずのルートファイルシステムから脱出できてしまう事象、いわゆる脱獄が起こりやすい仕様になっている。 そのため、Docker などの一般的な Linux コンテナの実装では pivot_root がデフォルトで使われている。 今回は、そんな pivot_root をコマンドラインツールとしての pivot_root(8) と、システムコールとしての pivot_root(2) で触ってみる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.4.0-109-generic aarch64
$ pivot_root --version
pivot_root from util-linux 2.34
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.7) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

もくじ

下準備

下準備として、コマンドラインツールとしての pivot_root(8) が入っている util-linux をインストールしておく。 また、システムコールとしての pivot_root(2) を含む C のソースコードをビルドするために build-essential もインストールする。

$ sudo apt-get -y install util-linux build-essential

pivot_root(8) の動作を試す

まずはコマンドラインツールとしての pivot_root(8) から使ってみよう。

pivot_root を利用するには、変更先となるルートファイルシステムが必要になるので、用意する。 今回は mktemp(1) を使ってテンポラリディレクトリを作って利用する。

$ export ROOTFS=$(mktemp -d)
$ echo $ROOTFS
/tmp/tmp.pMdpBMx0Uu

ルートファイルシステムにはプロセスが利用するプログラムとライブラリの一式が必要になる。 そこで、必要そうなコマンドラインツール本体と、ダイナミックリンクされているライブラリをコピーしておく。 コピーするツールについてはお好みで。

$ COPY_CMDS=ls,cat,rm,head,mkdir,mount,umount,df
$ IFS=","
$ for CMD in ${COPY_CMDS}
do
  cp -avL --parents $(which ${CMD}) ${ROOTFS}
  ldd $(which ${CMD}) | grep -o "/lib.*\.[0-9]\+" | xargs -I {} cp -avL --parents {} ${ROOTFS}
done

上記を実行すると、コマンドラインツールと依存しているライブラリがテンポラリディレクトリ以下にコピーされる。

$ find $ROOTFS
/tmp/tmp.pMdpBMx0Uu
/tmp/tmp.pMdpBMx0Uu/usr
/tmp/tmp.pMdpBMx0Uu/usr/bin
/tmp/tmp.pMdpBMx0Uu/usr/bin/df
/tmp/tmp.pMdpBMx0Uu/usr/bin/cat
/tmp/tmp.pMdpBMx0Uu/usr/bin/mount
/tmp/tmp.pMdpBMx0Uu/usr/bin/ls
/tmp/tmp.pMdpBMx0Uu/usr/bin/head
/tmp/tmp.pMdpBMx0Uu/usr/bin/rm
/tmp/tmp.pMdpBMx0Uu/usr/bin/umount
/tmp/tmp.pMdpBMx0Uu/usr/bin/mkdir
/tmp/tmp.pMdpBMx0Uu/lib
/tmp/tmp.pMdpBMx0Uu/lib/ld-linux-aarch64.so.1
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libblkid.so.1
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libpcre2-8.so.0
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libmount.so.1
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libc.so.6
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libpthread.so.0
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libdl.so.2
/tmp/tmp.pMdpBMx0Uu/lib/aarch64-linux-gnu/libselinux.so.1

続いて、unshare(8) を使って Mount Namespace を新しく用意する 1ROOTFS 変数をそのまま使い続けたいので、sudo(8) するときに -E オプションを指定して環境変数を引き継いでおく。

$ sudo -E unshare --mount

ROOTFS ディレクトリをバインドマウントすることでマウントポイントにしておく。 これは pivot_root で新しいルートファイルシステムにする場所はマウントポイントである必要があるため。

# mount --bind ${ROOTFS} ${ROOTFS}

続いて、古いルートファイルシステムをマウントする場所としてディレクトリを作っておく。 これは pivot_root を実行するときに、新しいルートファイルシステムの他に古いルートファイルシステムをマウントする場所も指定する必要があるため 2

# mkdir -p ${ROOTFS}/.old-root

満を持して pivot_root(8) を実行する。

# pivot_root ${ROOTFS} ${ROOTFS}/.old-root

これで、ルートファイルシステムが先ほどテンポラリディレクトリで作ったディレクトリに切り替わった。

# ls /
lib  usr

ただし、この時点ではまだカレントワーキングディレクトリとして古いルートファイルシステムが見えてしまっている。 なので、カレントワーキングディレクトリをルートファイルシステムに変更しておこう。

# cd /

また、この時点ではまだ先ほど指定した /.old-root に古いルートファイルシステムが残っている。

# ls /.old-root
bin  boot  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var

なので、アンマウントしたい。 そのために、まずは proc ファイルシステムをマウントする。 これは、どうやら umount(8) が proc ファイルシステムを見て動作しているようなので必要になる。

# mkdir /proc
# mount -t proc proc /proc
# df -Th
Filesystem     Type      Size  Used Avail Use% Mounted on
/dev/sda1      ext4      4.7G  2.9G  1.8G  62% /.old-root
udev           devtmpfs  452M     0  452M   0% /.old-root/dev
tmpfs          tmpfs     485M     0  485M   0% /.old-root/dev/shm
tmpfs          tmpfs      97M  1.1M   96M   2% /.old-root/run
tmpfs          tmpfs     5.0M     0  5.0M   0% /.old-root/run/lock
tmpfs          tmpfs      97M     0   97M   0% /.old-root/run/user/1000
tmpfs          tmpfs     485M     0  485M   0% /.old-root/sys/fs/cgroup
/dev/loop0     squashfs  128K  128K     0 100% /.old-root/snap/bare/5
/dev/loop1     squashfs   58M   58M     0 100% /.old-root/snap/core20/1437
/dev/loop2     squashfs   58M   58M     0 100% /.old-root/snap/core20/1408
/dev/loop3     squashfs   62M   62M     0 100% /.old-root/snap/lxd/22761
/dev/loop4     squashfs   62M   62M     0 100% /.old-root/snap/lxd/22530
/dev/loop6     squashfs   39M   39M     0 100% /.old-root/snap/snapd/15541
/dev/loop5     squashfs  896K  896K     0 100% /.old-root/snap/multipass-sshfs/147
/dev/loop7     squashfs   38M   38M     0 100% /.old-root/snap/snapd/15183
/dev/sda15     vfat       98M  290K   98M   1% /.old-root/boot/efi

これで、古いルートファイルシステムがアンマウントできる。

# umount -l /.old-root

これで最低限必要な作業は一通り終わった。 マウント状況を確認すると、だいぶシンプルになっている。

# df -Th
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sda1      ext4  4.7G  2.9G  1.8G  62% /
# cat /proc/self/mountinfo
822 713 8:1 /tmp/tmp.IXvzlsgIm8 / rw,relatime - ext4 /dev/sda1 rw
823 822 0:5 / /proc rw,relatime - proc proc rw

あとは、この状態だと /dev がなかったりするのでマウントしたり。

# mkdir /dev
# mount -t devtmpfs devtmpfs /dev

pivot_root(2) の動作を試す

続いてはシステムコールとしての pivot_root(2) を使う。

先ほどと同じように、あらかじめテンポラリディレクトリに必要なコマンドラインツールとライブラリ一式をコピーしておく。 今回は bash もコピーする。

$ export ROOTFS=$(mktemp -d)
$ COPY_CMDS=bash,ls,cat,rm,head,mkdir,mount,umount,df
$ IFS=","
$ for CMD in ${COPY_CMDS}
do
  cp -avL --parents $(which ${CMD}) ${ROOTFS}
  ldd $(which ${CMD}) | grep -o "/lib.*\.[0-9]\+" | xargs -I {} cp -avL --parents {} ${ROOTFS}
done

先ほどはコマンドラインで操作していた、以降の処理はライブラリ関数やシステムコールで実行する。 以下に、そのサンプルコードを示す。 肝心の pivot_root(2) を呼び出しているのは syscall(SYS_pivot_root, argv[1], put_old_path) のところ。 pivot_root(2) は libc のラッパー関数がないので、直接 syscall(2) を使って呼び出す必要がある。

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/syscall.h>

int main(int argc, char *argv[]) {
    // 引数の長さをチェックする
    if (argc < 2) {
        fprintf(stderr, "Please specify the path to change root\n");
        exit(EXIT_FAILURE);
    }

    // unshare(2) で Mount Namespace を作成する
    if (unshare(CLONE_NEWNS) != 0) {
        fprintf(stderr, "Failed to create a new mount namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // ルートディレクトリから再帰的にマウントのプロパゲーションを無効にする
    if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) {
        fprintf(stderr, "Failed to change root filesystem propagation: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // new_root に使うディレクトリを bind mount する
    if (mount(argv[1], argv[1], NULL, MS_BIND, NULL) != 0) {
        fprintf(stderr, "Failed to bind mount new_root directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // put_old に使うパスを求める
    char put_old_path[256];
    if (sprintf(put_old_path, "%s/.old-root", argv[1]) < 0) {
        fprintf(stderr, "Failed to sprintf: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // put_old に使うディレクトリを作る
    if (mkdir(put_old_path, 0777) < 0) {
        fprintf(stderr, "Failed to make directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // pivot_root(2) したいディレクトリにカレントワーキングディレクトリを変更する
    if (chdir(argv[1]) != 0) {
        fprintf(stderr, "Failed to change directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // pivot_root(2) を呼び出す
    // libc のラッパー関数がないので syscall(2) で呼び出す
    if (syscall(SYS_pivot_root, argv[1], put_old_path) < 0) {
        fprintf(stderr, "Can not pivot_root: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // put_old を umount2(2) で Lazy Detach する
    if (umount2("/.old-root", MNT_DETACH) < 0) {
        fprintf(stderr, "Can not umount: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // 古いファイルシステムをマウントしていた場所は不要なので削除しておく
    if (rmdir("/.old-root") < 0) {
        fprintf(stderr, "Failed to remove directory: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記のソースコードをビルドする。

$ gcc --std=c11 --static -Wall pivot_root.c

上記でビルドしたバイナリを、テンポラリディレクトリを引数にして実行する。

$ sudo ./a.out ${ROOTFS}

実行すると、先ほどコマンドラインで実行した状態と同じになる 3

# ls /
lib  usr
# df -Th
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/sda1      ext4  4.7G  2.9G  1.8G  62% /
# cat /proc/self/mountinfo
822 713 8:1 /tmp/tmp.CtMCVVvCU4 / rw,relatime - ext4 /dev/sda1 rw
714 822 0:5 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw

いじょう。

まとめ

今回は pivot_root をコマンドラインツールとシステムコールから使って、プロセスのルートファイルシステムを変更してみた。


  1. 古い unshare(8) ではマウントのプロパゲーションが生じるかもしれない。必要なら $ sudo mount --make-private / を実行しておく

  2. すぐにアンマウントするなら、新しいルートファイルシステムと古いルートファイルシステムに同じ場所を指定しても良いらしい

  3. /proc や /dev はマウントしていない