マウントプロパゲーション(2)〜 private mount 〜
昨日のエントリの続きです。引き続き完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。
それでは今日はprivateを見てみましょう。privateに設定したマウントのサブマウントは、他のマウントには反映されません。
sharedのときと同じディレクトリで試してみましょう。
# tree . . ├── bind └── orig └── tmp 3 directories, 0 files
まだ何もマウントが行われていない状態です。ここで、sharedと同様にorig
をbind
にバインドマウントしてみましょう。
# mount --bind orig bind (バインドマウント) # grep bind /proc/self/mountinfo (マウント情報の確認) 613 30 253:0 /root/test/orig /root/test/bind rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw (sharedでマウントされている) # mount --make-private bind (privateに設定) # grep bind /proc/self/mountinfo 613 30 253:0 /root/test/orig /root/test/bind rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw (sharedの文字が消えている)
バインドマウントした状態ではsharedになっていたマウントで、--make-private
でprivateに設定した後はshared
の文字が消えています。
この状態でbind
配下のtmp
ディレクトリにtmpfsをマウントします。
# tree . . ├── bind │ └── tmp └── orig └── tmp 4 directories, 0 files (バインドマウントされているので両方にtmpが存在する) # mount -t tmpfs tmpfs bind/tmp (tmpfsマウント) # egrep "bind.*tmpfs" /proc/self/mountinfo 627 613 0:74 / /root/test/bind/tmp rw,relatime - tmpfs tmpfs rw (マウントされている)
bind/tmp
にtmpfsがマウントされました。このtmpfs上にファイルを置いてみましょう。
# touch bind/tmp/testfile # tree . . ├── bind │ └── tmp │ └── testfile └── orig └── tmp 4 directories, 1 file
bind
以下には作成したファイルが見えていますが、orig
以下ではファイルが見えません。つまりtmpfsはバインドマウント先のbind
配下でだけ有効で、バインドマウント元のorig
では反映されていないことがわかります。
sharedのときと同様にorig
以下で行ったマウントがbind
で行われないことも確認おきましょう。
# mkdir orig/tmp2 (orig以下にtmp2ディレクトリを作成) # tree . . ├── bind │ ├── tmp │ │ └── testfile │ └── tmp2 └── orig ├── tmp └── tmp2 6 directories, 1 file # mount --make-private / (orig側のマウントをprivateに設定) # mount -t tmpfs tmpfs orig/tmp2 (tmp2にtmpfsをマウント) # touch orig/tmp2/testfile2 (ファイルをtmpfs上に置く) # tree . . ├── bind │ ├── tmp │ │ └── testfile │ └── tmp2 └── orig ├── tmp └── tmp2 └── testfile2 6 directories, 2 files (orig側のtmpfsマウントはbind側には反映されていない)
上のようにorig
側でのtmpfsマウントがbind
側では反映されていないことがわかります。つまりtmpfsはバインドマウント元のorig
配下でだけ有効で、バインドマウント先のbind
には反映されていないことがわかります。
このようにprivateに設定したマウントのサブマウントは、他のマウント配下には反映されません。
マウントプロパゲーション(1)〜 shared mount 〜
完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。
2001 年に 2.4 kernel でバインド(bind)マウントが、2002 年の 2.4.19 で mount namespace が導入されました。ただ、ユースケースによっては mount namespace が行う分離は徹底しているので、もう少しマウントが共有されても良いシーンがあったようで、shared マウントなどのマウントプロパゲーションの考え方が 2.6.15(2006 年 1 月)で導入されました。
そのマウントプロパゲーションについてまとめていきます。
説明中の「サブマウント」とは、あるマウントポイントがあったとして、そのツリー配下で行われるマウント処理のことを指すとします。例えば、ホストで/dev/sda1
というデバイスをルート/
にマウントします。その配下の/home
に/dev/sda2
をマウントしたりしますが、この/home
をサブマウントと呼ぶことにします。
プロパゲーションの設定 | 説明 |
---|---|
shared | マウント元のサブマウントはマウント先にも反映される。マウント先配下のサブマウントはマウント元にも反映される |
private | あるマウント配下でのサブマウントは他のマウント配下には公開されない |
slave | マウント元のサブマウントはマウント先にも反映されるが、マウント先のサブマウントはマウント元には反映されない |
unbindable | privateと同様でさらにこのマウントをバインドマウントできない |
上のそれぞれについて、配下のマウントに再帰的に設定を反映させることができます。
プロパゲーションの設定 | 説明 |
---|---|
rshared | 配下で行われるマウントすべてを shared にする |
rprivate | 配下で行われるマウントすべてを private にする |
rslave | 配下で行われるマウントすべてを slave にする |
runbindable | 配下で行われるマウントすべてを unbindable にする |
では、バインドマウントを使ってマウントプロパゲーションの説明をしていきましょう。
shared
まずはsharedです。sharedは、複数のマウント配下で行ったマウントがお互いに伝播します。
まず、バインドマウント用にorig
とbind
というディレクトリをふたつ作ります。そしてバインドマウントを行います。バインドマウント元ディレクトリorig
内にtmp
というディレクトリを作りました。
# mkdir orig bind # mount --bind orig bind (origをbindにバインドマウント) # mkdir orig/tmp (orig内にtmpというディレクトリを作成)
orig
がbind
にもバインドマウントされているので、次のように両方のディレクトリにtmp
ディレクトリが見えます。
# tree . . ├── bind │ └── tmp └── orig └── tmp 4 directories, 0 files
ここでbind
をsharedマウントとしてみましょう。sharedにするにはmountコマンドに--make-shared
を指定します。
# mount --make-shared bind # grep bind /proc/self/mountinfo (マウント情報の確認) 518 30 253:0 /root/test/orig /root/test/bind rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw (shared指定されている)
sharedになっていることを確認するために/proc/self/mountinfo
を確認したところ、きちんと"shared"という文字がみえます。ここでshared指定されたbind
以下でマウント操作を行ってみます。ここではtmp
ディレクトリにtmpfsをマウントしてみましょう。
# mount -t tmpfs tmpfs bind/tmp (bind以下のtmpにtmpfsマウント) # egrep "bind.*tmpfs" /proc/self/mountinfo (マウントされたのを確認) 546 518 0:49 / /root/test/bind/tmp rw,relatime shared:253 - tmpfs tmpfs rw
tmpfsがマウントされていますので、このtmpfs上にファイルを置いてみましょう。そしてorig
とbind
の配下がどうなっているのかを確認してみます。
# touch bind/tmp/testfile (tmpfs上にファイルを置く) # tree . . ├── bind │ └── tmp │ └── testfile └── orig └── tmp └── testfile 4 directories, 2 files
バインドマウント先のbind
内でマウントしたtmpfsがバインドマウント元でもマウントされた状態になっています。
この逆もやってみましょう。orig
ディレクトリはマウントポイントではありませんので、ここではorig
ディレクトリが所属するマウントのマウントポイントであるルート(/
)でsharedを指定します。ここは環境によって違いますので、実際に試す場合は適宜変更してください。
# mount --make-shared /
そしてorig
ディレクトリ以下にtmp2
ディレクトリを作成しtmpfsをマウントし、ファイルを置きます。
# mkdir orig/tmp2 # mount -t tmpfs tmpfs orig/tmp2/ (tmp2にtmpfsをマウント) # touch orig/tmp2/testfile2 (tmpfs上にファイルを置く) # tree . . ├── bind │ ├── tmp │ │ └── testfile │ └── tmp2 │ └── testfile2 └── orig ├── tmp │ └── testfile └── tmp2 └── testfile2 6 directories, 4 files
上のように、orig
以下で行ったマウントがきちんとbind
以下でも見えています。
このようにsharedに設定したマウント配下で行ったマウントは、他のマウント配下にも反映されます。
unshare コマンドがマウントプロパゲーションをまともに扱うような変更を入れていた
全国10万人の unshare でコンテナを作るのが好きなコンテナマニアの方々が systemd のおかげで余計な処理をさせられてたのですが、いつの間にか unshare(1)
コマンド側であるべき姿に戻す処理が追加されていたというお話です。
結論
回りくどい話が嫌いな方も多いかと思うので、最初に結論を簡潔にまとめときます。ここだけ読めば結論はわかります。
前提として、
- kernel デフォルトではマウントプロパゲーションは
private
- 8 年ほど前に
systemd
で起動時にshared
にするように処理が追加されてる - このため
unshare --mount
で新たに Mount Namespace を作って、その中でマウント処理を行ってもホスト側でもそのマウントが見えるようになっていた
というような状態でした。ところが unshare --mount
を実行したら当然 Namespace 内のマウントは他から見えないはずと思っていたユーザーが多く、直感的でないため、
- unshare コマンドのデフォルトで Mount Namespace を作る際に
private
を指定するような変更が加わっていた
ということです。つまりホスト側で systemd がマウントプロパゲーションを shared
に設定しても、unshare --mount
して Namespace を作ったら、ちゃんと private
になって他の Namespace からはマウントは見えないよ、となっています。
回りくどい話が嫌いな人はここまで読めば大丈夫です。
詳細
もう 8 年とか前の話なのですが、systemd が加えたこの変更、
kernel ではマウント時のプロパゲーションのデフォルトは private のはずですが、なぜかいつしか shared でマウントされるようになっててハマった人がいるのは、この変更のせいでした。
これは /proc/self/mountinfo
なんかを見るとわかります。
$ grep '/ / ' /proc/self/mountinfo 30 1 252:2 / / rw,relatime shared:1 - ext4 /dev/vda2 rw
shared
の文字が見えますね。ちなみにこのような制御を行わないシンプルな sysvinit を採用している Plamo Linux だと private
でマウントされています。
$ grep '/ / ' /proc/self/mountinfo 22 1 259:6 / / rw,relatime - ext4 /dev/nvme0n1p6 rw
この変更のせいで、unshare --mount
と新たに Mount Namespace を作って、Namespace 内でマウントしたにも関わらず、別の Namespace(unshareする前の Namespace)でマウントが共有されてしまっていてハマったりしたものです。例えば、
私の連載でも後日補足を入れる羽目になってます。
で、まあこの辺りを色々あって見直していたら、イマドキは systemd 環境で Mount Namespace を新たに作ってもマウントが共有されなくなっています。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04 LTS Release: 20.04 Codename: focal $ grep vda2 /proc/self/mountinfo 30 1 252:2 / / rw,relatime shared:1 - ext4 /dev/vda2 rw (←確かにshared) $ sudo unshare --mount /bin/bash # grep vda2 /proc/self/mountinfo 437 436 252:2 / / rw,relatime - ext4 /dev/vda2 rw (←sharedの文字が消えてる!)
このように private になってる感じです。試しにこの Namespace でマウントしてみると、
# losetup /dev/loop6 test.img # mount /dev/loop6 /mnt # grep loop6 /proc/self/mountinfo 478 437 7:6 / /mnt rw,relatime - ext4 /dev/loop6 rw
マウントされていますが、ホスト環境で見ると、
$ grep loop6 /proc/self/mountinfo
$
と見えません。つまり private になってるってことですね。
誰が変わってしまったんだろう?と調べてみると、コレでした!util-linux のコミットです。
After "unshare --mount" users assume that mount operations within the new namespaces are unshared (invisible for the rest of the system).
Unfortunately, this is not true and the behavior depends on the current mount propagation setting. The kernel default is "private", but for example systemd based distros use "shared". The solution is to use (for example) "mount --make-private" after unshare(1).
I have been requested many times to provide less fragile and more unified unshared mount setting by default to make things user friendly.
The patch forces unshare(1) to explicitly use MS_REC|MS_PRIVATE for all tree by default.
(超訳)
"unshare --mount" なんてマニアックなコマンドを使うヤツは当然新しい Namespace ではマウントは共有されないと思ってるよね。
不幸なことにそんなことはなくて、今どんなマウントプロパゲーションの設定になってるかによるんだよね。カーネルのデフォルトが "private" なのに、クソsystemdのヤツが "shared" に設定してる。このクソ設定に対抗するには unshare のあとに "mount --make-private" って実行するしかないよね。
オレは何度もデフォルトでこんなマニアックなユーザーがわかりやすいようにちゃんとした共有されないマウント設定をすべきって頼まれてきた。
このパッチでソレに応えて unshare では明確に "MS_REC|MS_PRIVATE" を使うようにしたぜ。
man unshare
にもその辺りの記載があります。
unshare since util-linux version 2.27 automatically sets propagation to private in a new mount namespace to make sure that the new namespace is really unshared. It's possible to disable this feature with option --propagation unchanged. Note that private is the kernel default.
ここにあるように --propagation unchanged
を指定して unshare
を実行すると、
$ sudo unshare --mount --propagation unchanged /bin/bash # grep vda2 /proc/self/mountinfo 438 436 252:2 / / rw,relatime shared:1 - ext4 /dev/vda2 rw
確かにホスト環境と同じように shared
でマウントされたままです。この状態で Namespace 内でマウントすると、
# mount /dev/loop6 /mnt # grep loop6 /proc/self/mountinfo 479 438 7:6 / /mnt rw,relatime shared:350 - ext4 /dev/loop6 rw
と shared
です。ホスト側でも
$ grep loop6 /proc/self/mountinfo 480 30 7:6 / /mnt rw,relatime shared:350 - ext4 /dev/loop6 rw
とマウントが見えます。
(関連する Issue)
LXDのシステムコールインターセプション機能を試す(マウント編)(2)
先のエントリーでは、コンテナ内から ext4 ファイルシステムのマウントができたのですが、実は security.syscalls.intercept.mount.shift
という設定が効いていないようで、そこで時間切れでした(どこがうまく行ってないか書いてませんでしたが)。
そこで Ubuntu 20.04 もリリースされたことですし、もう一度試してみましょう。
前回のおさらい
前回と同様に /dev/vdb
というディスクが存在し、パーティションをひとつだけ作成し、ext4
で mkfs
しています。テストのために事前にホスト側でマウントし、testfile
というファイルをひとつだけ作ってあります。
$ ls -l /mnt/ total 16 drwx------ 2 root root 16384 Apr 29 07:38 lost+found -rw-r--r-- 1 root root 0 Apr 29 07:56 testfile karma@focal:~$ sudo umount /mnt
コンテナをひとつ作成して起動します。
$ lxc launch images:alpine/3.11 c1 Creating c1 Starting c1
デバイス(/dev/vdb1
)をコンテナに割り当てます。
$ lxc config device add c1 vdb1 unix-block path=/dev/vdb1
Device vdb1 added to c1
システムコールインターセプションの設定を行います。shiftfs
を有効にして、ext4
をマウントできるように許可します。
$ lxc config set c1 security.syscalls.intercept.mount true $ lxc config set c1 security.syscalls.intercept.mount.shift true $ lxc config set c1 security.syscalls.intercept.mount.allowed ext4 $ lxc restart c1 $ lxc config show c1 architecture: x86_64 config: : (snip) security.syscalls.intercept.mount: "true" security.syscalls.intercept.mount.allowed: ext4 security.syscalls.intercept.mount.shift: "true" : (snip) devices: vdb1: path: /dev/vdb1 type: unix-block : (snip)
設定されているのが確認できましたので、マウントしてみます。
$ lxc exec c1 -- mount /dev/vdb1 /mnt $ lxc exec c1 -- ls -lh /mnt total 16K drwx------ 2 nobody nobody 16.0K Apr 29 07:38 lost+found -rw-r--r-- 1 nobody nobody 0 Apr 29 07:56 testfile
ここまでが前回のおさらいです。
ShiftFS おさらい
マウントはできたのですが、ファイルシステム中のファイルの所有権がホストでは root:root
だったのが、nobody:nobody
となります。
これは User Namespace を使って、ホスト上のユーザーとコンテナ内のユーザーをマッピングしており、マッピングしている範囲外の id はコンテナ内では扱えず、その場合は id が /proc/sys/kernel/overflow{u,g}id
の値になります。つまり nobody:nobody
になるわけです。
$ cat /proc/sys/kernel/overflow{u,g}id 65534 65534
コンテナ内でイメージなどをマウントするには、ホスト上のユーザーが所有する uid/gid はコンテナ内で扱えませんので、このあたりを解決するのに ShiftFS というファイルシステムが開発されており、Ubuntu カーネルにはこの機能のパッチが適用されています。この辺りは
をどうぞ。
LXD で ShiftFS を使えるように設定
ここで、最初のコンテナに対して行った設定をもう一度見てみましょう。うまく働いてないなという security.syscalls.intercept.mount.shift
はマウントの際に ShiftFS を使って uid/gid をコンテナ内の uid/gid に合わせる設定です。
しかし、先の例では nobody:nobody
になってしまっていたので、この ShiftFS が効いていないことがわかります。
lxd の情報を確認してみると、
$ lxc info | grep shiftfs shiftfs: "false"
このように有効になっていないようです。
まずはホスト上で ShiftFS が有効になっているかを確認してみましょう。
$ lsmod | grep shiftfs
$
さすがにデフォルトでは shiftfs のカーネルモジュールはロードされないようです。このモジュールをロードして lxd を再起動してみましょう。lxd は snap パッケージで提供されているので、snap.lxd.daemon
を再起動します。
$ sudo modprobe -v shiftfs insmod /lib/modules/5.4.0-26-generic/kernel/fs/shiftfs.ko $ lsmod | grep shiftfs shiftfs 28672 0 $ sudo systemctl restart snap.lxd.daemon
これでどうでしょう?
$ lxc info | grep shiftfs shiftfs: "false"
おや?ダメですね…
調べてみると、snap の lxd の方に shiftfs が有効であると教えてあげないとダメなようです。
$ sudo snap set lxd shiftfs.enable=true $ sudo systemctl reload snap.lxd.daemon
これでいけるかな?
$ lxc info | grep shiftfs shiftfs: "true"
有効になりました!! コンテナを再度作成してみましょう(再作成しなくてもよさそうな気がしましたが、lxd で ShiftFS が有効になったあとにコンテナを再作成しないとうまくいきませんでした)。
$ lxc launch images:alpine/3.11 c1 Creating c1 Starting c1 $ lxc config device add c1 vdb1 unix-block path=/dev/vdb1 Device vdb1 added to c1 $ lxc config set c1 security.syscalls.intercept.mount true $ lxc config set c1 security.syscalls.intercept.mount.shift true $ lxc config set c1 security.syscalls.intercept.mount.allowed ext4 $ lxc restart c1 $ lxc exec c1 -- mount /dev/vdb1 /mnt $ lxc exec c1 -- ls -l /mnt total 16 drwx------ 2 root root 16384 Apr 29 07:38 lost+found -rw-r--r-- 1 root root 0 Apr 29 07:56 testfile
無事に root:root
の所有になっており、ShiftFS が効いていることがわかります。ちなみにホスト側で見ると
$ ls -l /mnt total 16 drwx------ 2 root root 16384 Apr 29 07:38 lost+found -rw-r--r-- 1 root root 0 Apr 29 07:56 testfile
こちらの id も変わってません。
システムコンテナとして、少しでもVMやベアメタル上でOSを使うのに近づけようとする努力には本当に頭が下がりますね。
(追記)
snap set lxd shiftfs.enable=true
を実行する前に作成したコンテナで ShiftFS を使うには、次のようにすると良いようです(私は未確認)。
$ lxc config set c1 security.privileged true ("c1" のとこはコンテナ名入れてね) $ lxc restart c1 $ lxc config unset c1 security.privileged true $ lxc restart c1
LXDのシステムコールインターセプション機能を試す(マウント編)
前回、LXD のシステムコールインターセプション機能がうまく動いたので調子に乗って引き続き試します。
この機能には色々できることがあって今後も追加されそうな気がしますが、とりあえずは見た目でパッとわかりやすいファイルシステムのマウントを試してみましょう。
お試し環境は Ubuntu 20.04 LTS のベータ版です。
ホスト環境に 1GB のディスクを追加しています(/dev/vdb
)。これは別に不要なのですが、そのディスク上に /dev/vdb1
というパーティションをひとつ作成します。
$ sudo fdisk -l /dev/vdb [sudo] password for karma: Disk /dev/vdb: 1 GiB, 1073741824 bytes, 2097152 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x74374d12 Device Boot Start End Sectors Size Id Type /dev/vdb1 2048 2097151 2095104 1023M 83 Linux
まずはコンテナにこのブロックデバイスを追加する必要があります。でないとコンテナ内でデバイスを指定して mount コマンドを実行できません。
$ lxc config device add c1 vdb unix-block path=/dev/vdb Device vdb added to c1 $ lxc config device add c1 vdb1 unix-block path=/dev/vdb1 Device vdb1 added to c1
パーティションを切ってるので上のふたつを追加しました。/dev/vdb1
をコンテナ内でマウントするだけなら上記の vdb1
だけを追加すれば良いと思います。ホスト上と同じように fdisk
が実行できるようにするために vdb
も登録しています。
$ lxc config show c1 :(snip) devices: vdb: path: /dev/vdb type: unix-block vdb1: path: /dev/vdb1 type: unix-block :(snip)
このように追加されています。コンテナを起動すると次のようにデバイスファイルが作成されているのがわかります。fdisk でもちゃんとパーティションが見えています。
$ lxc shell c1 mesg: ttyname failed: No such device root@c1:~# cat /proc/self/{u,g}id_map (非特権であることを確認) 0 1000000 1000000000 0 1000000 1000000000 root@c1:~# ls -l /dev/vdb* brw-rw---- 1 root root 252, 16 Apr 12 13:16 /dev/vdb brw-rw---- 1 root root 252, 17 Apr 12 13:16 /dev/vdb1 root@c1:~# fdisk -l /dev/vdb Disk /dev/vdb: 1 GiB, 1073741824 bytes, 2097152 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x74374d12 Device Boot Start End Sectors Size Id Type /dev/vdb1 2048 2097151 2095104 1023M 83 Linux
これだけでは mount できません。非特権コンテナなので当たり前です。
oot@c1:~# mount /dev/vdb1 /mnt mount: /mnt: permission denied.
ここで mount ができるように設定しましょう。
$ lxc config set c1 security.syscalls.intercept.mount true $ lxc config set c1 security.syscalls.intercept.mount.shift true $ lxc config set c1 security.syscalls.intercept.mount.allowed ext4 $ lxc config show c1 :(snip) security.syscalls.intercept.mount: "true" security.syscalls.intercept.mount.allowed: ext4 security.syscalls.intercept.mount.shift: "true" :(snip)
コンテナを再起動します。
$ lxc restart c1 $ lxc shell c1 mesg: ttyname failed: No such device root@c1:~# mount /dev/vdb1 /mnt root@c1:~# df -h /mnt Filesystem Size Used Avail Use% Mounted on /dev/vdb1 991M 2.6M 922M 1% /mnt
mount できました。
ここで security.syscalls.intercept.mount.shift: "true"
という設定だけが効いてないようなのですが、今日は力尽きました。引き続き調べます。
(つづく)