Linux のコンテナ仮想化を構成する要素の一つに、カーネルの Namespace (名前空間) という機能がある。 これは、プロセスが動作する際のリソースを Namespace という単位で隔離して扱うための仕組み。 以下のとおり、隔離する対象によって Namespace は色々とある。
今回は、その中でもマウント情報を隔離するための Mount Namespace を扱ってみる。 具体的には、unshare(1) と unshare(2) を使ってマウント情報が隔離される様子を観察してみる。 unshare(2) というのは Namespace を操作するためのシステムコールで、unshare(1) は同名のシステムコールを利用したコマンドラインツールになっている。
使った環境は次のとおり。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=20.04 DISTRIB_CODENAME=focal DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS" $ uname -rm 5.4.0-91-generic aarch64 $ unshare --version unshare from util-linux 2.34 $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.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.
もくじ
- もくじ
- 下準備
- unshare(1) を使って Mount Namespace を操作する
- unshare(2) を使って Mount Namespace を操作する
- マウントのプロパゲーションについて
- まとめ
下準備
下準備として、mount(1) と unshare(1) を使うために util-linux をインストールする。 同様に、unshare(2) を使ったコードをビルドするために build-essential をインストールする。
$ sudo apt-get update $ sudo apt-get -y install util-linux build-essential
unshare(1) を使って Mount Namespace を操作する
まずは unshare(1) を使って Mount Namespace を試してみよう。
現在のプロセスの Mount Namespace の識別子は procfs から取得できる。
具体的には /proc/self/ns/mnt
を読めば良い。
$ file /proc/self/ns/mnt
/proc/self/ns/mnt: symbolic link to mnt:[4026531840]
上記で右端にあるブラケットに囲まれた数字は、現在のプロセス、つまりシェルが所属する Mount Namespace を表している。
今回は /tmp/mntpoint
というディレクトリを例にして説明していこう。
このディレクトリに tmpfs をマウントして、マウントされたのが見えるか否かで動作確認していく。
まずはマウント先としてディレクトリを作っておこう。
$ mkdir -p /tmp/mntpoint
作ったディレクトリに動作確認用のファイルを適当に作っておく。
$ echo "Hello, World" > /tmp/mntpoint/greet.txt $ ls /tmp/mntpoint greet.txt $ cat /tmp/mntpoint/greet.txt Hello, World
続いては肝心の unshare(1) を使って Mount Namespace を作る。
このとき --mount
オプションを付与することで Mount Namespace を作ることを指定しよう。
末尾には bash
を指定してシェルを起動する。
$ sudo unshare --mount bash
上記を実行したら、先ほどと同じように /proc/self/ns/mnt
にアクセスしてみよう。
すると、識別子が変わっていることがわかる。
つまり、先ほどとは異なる Mount Namespace にプロセスが所属している。
# file /proc/self/ns/mnt /proc/self/ns/mnt: symbolic link to mnt:[4026532128]
この時点では、まだ /tmp/mntpoint
以下には何もしていないのでファイルが見える。
# ls /tmp/mntpoint/
greet.txt
ここでおもむろに tmpfs を上記のディレクトリにマウントしてみよう。
# mount -t tmpfs tmpfs /tmp/mntpoint
すると、当然だけどマウントされてファイルは見えなくなる。
# mount | grep /tmp/mntpoint tmpfs on /tmp/mntpoint type tmpfs (rw,relatime) # ls /tmp/mntpoint/| wc -l 0
マウントできたことが確認できたら元のシェルに戻ろう。
# exit
元のシェルに戻ると Mount Namespace も元に戻っていることがわかる。
$ file /proc/self/ns/mnt
/proc/self/ns/mnt: symbolic link to mnt:[4026531840]
確認すると /tmp/mntpoint
には何もマウントされておらず、ファイルがそのままある。
$ mount | grep /tmp/mntpoint
$ ls /tmp/mntpoint/
greet.txt
$ cat /tmp/mntpoint/greet.txt
Hello, World
これこそ Mount Namespace の効果を示している。 つまり、異なる Namespace 上で実施したマウントの操作は、別の Namespace には影響を与えない。
unshare(2) を使って Mount Namespace を操作する
続いては、システムコールを直接使って Mount Namespace を操作してみよう。
早速だけど以下にサンプルコードを示す。
unshare(2) で Mount Namespace を作るには引数に CLONE_NEWNS
を指定して呼び出せば良い。
その上で、mount(2) を使って /tmp/mntpoint
に tmpfs をマウントしている。
最終的にやるのは execvp(3) を使って bash を起動すること。
#define _GNU_SOURCE #include <sched.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/mount.h> int main(int argc, char *argv[]) { // 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, "Can not change root filesystem propagation: %s\n", strerror(errno)); exit(EXIT_FAILURE); } // tmpfs をマウントする char mount_dst[] = "/tmp/mntpoint"; if (mount("tmpfs", mount_dst, "tmpfs", MS_NOSUID | MS_NODEV, NULL) != 0) { fprintf(stderr, "Failed to mount %s: %s\n", mount_dst, 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 -Wall example.c
できたバイナリを実行・・・する前に、一旦 Mount Namespace の識別子を確認しておこう。
$ file /proc/self/ns/mnt
/proc/self/ns/mnt: symbolic link to mnt:[4026531840]
改めて、できたバイナリをスーパーユーザの権限で実行する。
$ sudo ./a.out
するとシェルが起動するので、また Mount Namespace の識別子を確認する。 ちゃんと先ほどと異なっている。
# file /proc/self/ns/mnt /proc/self/ns/mnt: symbolic link to mnt:[4026532128]
ソースコードの中でマウントしているので /tmp/mntpoint
に tmpfs がマウントされてファイルが見えなくなっている。
さっきとはマウントするときのオプションがちょっと違ってるけど、まあそこは良いか。
# mount | grep /tmp/mntpoint tmpfs on /tmp/mntpoint type tmpfs (rw,nosuid,nodev,relatime) # ls /tmp/mntpoint/ | wc -l 0
ひとしきり満足したら元のシェルに戻ろう。
# exit
もちろん、別の Mount Namespace での出来事なので、元のシェルにはマウントされていない。
$ mount | grep /tmp/mntpoint $ ls /tmp/mntpoint/ | wc -l 1
マウントのプロパゲーションについて
ところで、先ほどのソースコードに次のような処理が入っていることを不思議に感じたと思う。
// ルートディレクトリから再帰的にマウントのプロパゲーションを無効にする if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) != 0) { fprintf(stderr, "cannot change root filesystem propagation: %s\n", strerror(errno)); exit(EXIT_FAILURE); }
これはマウントのプロパゲーションという機能を抑制するために入れている。 というのも、プロパゲーションの設定によっては Mount Namespace が別れていても、操作が別の Namespace まで伝搬してしまうのだ。
Linux カーネルのデフォルトの設定では伝搬しない (PRIVATE) ようになっている。 しかし、systemd(1) は起動時に伝搬する (SHARED) ように設定を書きかえてしまう。 この振る舞いは以下のドキュメントにも記述がある。
実際に確認してみよう。
/proc/self/mountinfo
を見ると、マウントポイントごとにプロパゲーションのフラグが確認できる。
以下はルートファイルシステムの設定。
$ cat /proc/self/mountinfo | grep ' / / ' 32 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw
上記で中ほどにある shared:1
というのが、伝搬する (SHARED) 設定になっていることを示している。
これを伝搬しない (PRIVATE) に設定するには、次のように mount(1) に --make-private
オプションをつけて実行する。
$ sudo mount --make-private /
もう一度 /proc/self/mountinfo
を見ると、shared:1
だった部分が -
になった。
$ cat /proc/self/mountinfo | grep ' / / ' 32 1 8:1 / / rw,relatime - ext4 /dev/sda1 rw
この状況であればマウントの操作は伝搬しないので、先ほどの処理は不要になる。 試しに、処理を除外したソースコードを用意して試してみよう。
#define _GNU_SOURCE #include <sched.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/mount.h> int main(int argc, char *argv[]) { // Mount Namespace を作成する if (unshare(CLONE_NEWNS) != 0) { fprintf(stderr, "Failed to create a new mount namespace: %s\n", strerror(errno)); exit(EXIT_FAILURE); } // mount(2) で tmpfs をマウントする char mount_dst[] = "/tmp/mntpoint"; if (mount("tmpfs", mount_dst, "tmpfs", MS_NOSUID | MS_NODEV, NULL) != 0) { fprintf(stderr, "Failed to mount %s: %s\n", mount_dst, 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 -Wall example.c $ sudo ./a.out
起動したシェルでは、もちろん tmpfs のマウントが確認できる。
# mount | grep /tmp/mntpoint tmpfs on /tmp/mntpoint type tmpfs (rw,nosuid,nodev,relatime) # ls /tmp/mntpoint | wc -l 0
シェルを抜けて戻ると、マウントは見えなくなっている。 ちゃんと伝搬しないことが確認できた。
# exit $ mount | grep /tmp/mntpoint $ ls /tmp/mntpoint/ greet.txt
ちなみに、mount(1) に --make-shared
を指定すると、プロパゲーションの振る舞いを元に戻せる。
$ sudo mount --make-shared / $ cat /proc/self/mountinfo | grep ' / / ' 32 1 8:1 / / rw,relatime shared:327 - ext4 /dev/sda1 rw
この状況だと、マウントの操作が Namespace を越えて伝搬してしまう。 実際に試してみよう。 先ほどコンパイルしたバイナリを実行する。
$ sudo ./a.out
起動したシェルでは tmpfs のマウントが確認できる。 ここまでは変わらない。
# mount | grep /tmp/mntpoint tmpfs on /tmp/mntpoint type tmpfs (rw,nosuid,nodev,relatime) # ls /tmp/mntpoint | wc -l 0
しかし、シェルから抜けた後で確認しても、マウントが維持されている。 これはつまり、マウントの操作が Namespace を越えて伝搬してしまった、ということ。
# exit $ mount | grep /tmp/mntpoint tmpfs on /tmp/mntpoint type tmpfs (rw,nosuid,nodev,relatime) $ ls /tmp/mntpoint/ | wc -l 0
ところで、unshare(1) を使うときはプロパゲーションについて特に意識する必要がなかったことを思い出してほしい。 これは、unshare(1) が最初に示したソースコードと同等の処理を実行しているため。 つまり、デフォルトでルートディレクトリのプロパゲーションを明示的に無効にしている。
まとめ
今回は Mount Namespace を使ってマウントの情報をプロセスごとに隔離してみた。 ただし、プロパゲーションの設定によっては操作が伝搬してしまう点に注意する必要がある。