chroot ~Alpine LinuxのDockerイメージでglibcで動くプログラム(bash)を動かしてみる~

はじめに

小ネタです。chrootの動作を見るのに手を動かしてみた内容を書いたのが今回の記事です。

chrootはプロセスのルートディレクトリを変更するコマンド、関数、システムコールです。動かしてみてchrootの雰囲気がわかることが目標です。

Alpine LinuxのDockerイメージでglibcで動くプログラム(bash)を動かしてみる

Alpine Linuxはmusl libcが使われています。これからそこへglibcをインストールしてそれに依存しているbashを動かすという意味のないことをやります。何かプログラムを動かす時に、現在の環境を汚したくない時に使えるのではないでしょうか*1

ではさっそく、Alpineのコンテナの中に入ります*2

$ docker run -it alpine /bin/sh
/ #

chrootに必要なディレクリトリを準備します。

/ # mkdir /root/newroot
/ # cd /root/newroot
~/newroot # mkdir -p usr/bin/ bin dev proc root etc
~/newroot # ls
bin   dev   etc   proc  root  usr

glibcの展開に必要なツールをインストールします。

~/newroot # apk add libarchive-tools
(1/7) Installing libacl (2.2.53-r0)
(2/7) Installing libbz2 (1.0.8-r1)
...
OK: 8 MiB in 21 packages

glibcをダウンロードして展開します。

~/newroot # wget https://www.archlinux.org/packages/core/x86_64/glibc/download/ -O - | bsdtar xfJ -
Connecting to www.archlinux.org (138.201.81.199:443)
...
written to stdout
~/newroot # ls usr/lib/
Mcrt1.o                  ld-linux-x86-64.so.2     libc.a                   libdl.so.2               libmvec.so               libnss_dns-2.30.so       libpthread.a             libthread_db-1.0.so
Scrt1.o                  libBrokenLocale-2.30.so  libc.so                  libg.a                   libmvec.so.1             libnss_dns.so            libpthread.so            libthread_db.so
audit                    libBrokenLocale.a        libc.so.6                libm-2.30.a              libmvec_nonshared.a      libnss_dns.so.2          libpthread.so.0          libthread_db.so.1
crt1.o                   libBrokenLocale.so       libc_nonshared.a         libm-2.30.so             libnsl-2.30.so           libnss_files-2.30.so     libresolv-2.30.so        libutil-2.30.so
crti.o                   libBrokenLocale.so.1     libcrypt-2.30.so         libm.a                   libnsl.so.1              libnss_files.so          libresolv.a              libutil.a
crtn.o                   libSegFault.so           libcrypt.a               libm.so                  libnss_compat-2.30.so    libnss_files.so.2        libresolv.so             libutil.so
gconv                    libanl-2.30.so           libcrypt.so              libm.so.6                libnss_compat.so         libnss_hesiod-2.30.so    libresolv.so.2           libutil.so.1
gcrt1.o                  libanl.a                 libcrypt.so.1            libmcheck.a              libnss_compat.so.2       libnss_hesiod.so         librt-2.30.so            locale
getconf                  libanl.so                libdl-2.30.so            libmemusage.so           libnss_db-2.30.so        libnss_hesiod.so.2       librt.a                  rcrt1.o
grcrt1.o                 libanl.so.1              libdl.a                  libmvec-2.30.so          libnss_db.so             libpcprofile.so          librt.so                 systemd
ld-2.30.so               libc-2.30.so             libdl.so                 libmvec.a                libnss_db.so.2           libpthread-2.30.so       librt.so.1               tmpfiles.d

chrootに必要なファイルを準備します。

~/newroot # cp /bin/busybox bin/
~/newroot # cp -r /lib lib
~/newroot # ln -s /bin/busybox bin/sh
~/newroot # ln -s /bin/busybox bin/ln
~/newroot # cp /etc/resolv.conf etc/

chrootを実行します。

~/newroot # chroot .
/ #

ここでchrootを実行した「今動かしているシェル」のルートが変わります。今までの「必要なディレクトリを用意」などはchrootをしてルートディレクトリが/root/newrootに変わったあとのシェルにとって必要なディレクトリという意味でした。

lsなど必要なコマンドはbusyboxを使います。busyboxはmusl libcに依存しています。

/ # ls usr/bin/
/bin/sh: ls: not found
/ # ln -s /bin/busybox /bin/ls
/ # ls
bin   dev   etc   lib   proc  root  usr   var

上のディレクトリへcdしてみます。

/ # cd ..
/ #

いけません。ちゃんとrootディレクトリが変わっています。

ではglibcに依存しているbashを入れてみます。bash実行に必要なシンボリックリンクを作って、いったんexitchrootから抜けます。

/ # ln -s /usr/lib /lib64
/ # exit

bash本体と依存するライブラリを取ってきます。

~/newroot # wget https://www.archlinux.org/packages/core/x86_64/bash/download/ -O - | bsdtar xfJ -
~/newroot # wget https://www.archlinux.org/packages/core/x86_64/readline/download/ -O - | bsdtar xfJ -
~/newroot # wget https://www.archlinux.org/packages/core/x86_64/ncurses/download/ -O - | bsdtar xfJ -

chrootbashを指定して実行してみます。

~/newroot # chroot . /usr/bin/bash
[I have no name!@eeb2b9133148 /]#

bashが動きました*3。ちなみにapk add bashbashをインストールするのと違うことは、bashの動作に必要なライブラリなどの配置をapkがやってくれるのに対して、今回は自分で用意したところですね。

chroot システムコール

chrootのシステムコールを追ってみると、currentが持つfsのルートを変更するというものなようです。currentの型はタスク構造体です。現在のプロセスを表しています。そのルートを変えたということです。

set_fs_root(current->fs, &path);
/*
 * Replace the fs->{rootmnt,root} with {mnt,dentry}. Put the old values.
 * It can block.
 */
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は単純にプロセスのルートを変えるだけ
  • chrootをしたあとは、そこで動くプログラムが依存するライブラリやディレクトリやファイルなどがあってそいつらを用意する必要がある
  • chrootしたディレクトリを削除すれば元通りになって元の環境を汚さない感じがいいなと思った
  • Linuxの仕組みがよくわかっていないので、調べるの大変だなと思うけど、人生の暇つぶしにとても良い

*1:Dockerもchrootと同じことをしているのでDockerを使えばいいということですねw

*2:さっそくchroot(pivot_root)をします。

*3:I have no nameと出る。なんだろう。調べるのは次の機会にしますw