今回は、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 を新しく用意する 1。
ROOTFS
変数をそのまま使い続けたいので、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 をコマンドラインツールとシステムコールから使って、プロセスのルートファイルシステムを変更してみた。