TenForward

技術ブログ。はてなダイアリーから移転しました

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 が加えたこの変更、

github.com

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)でマウントが共有されてしまっていてハマったりしたものです。例えば、

kernhack.hatenablog.com

私の連載でも後日補足を入れる羽目になってます。

gihyo.jp


で、まあこの辺りを色々あって見直していたら、イマドキは 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 のコミットです。

github.com

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)

github.com

LXDのシステムコールインターセプション機能を試す(マウント編)(2)

先のエントリーでは、コンテナ内から ext4 ファイルシステムのマウントができたのですが、実は security.syscalls.intercept.mount.shift という設定が効いていないようで、そこで時間切れでした(どこがうまく行ってないか書いてませんでしたが)。

tenforward.hatenablog.com

そこで Ubuntu 20.04 もリリースされたことですし、もう一度試してみましょう。

前回のおさらい

前回と同様に /dev/vdb というディスクが存在し、パーティションをひとつだけ作成し、ext4mkfs しています。テストのために事前にホスト側でマウントし、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 カーネルにはこの機能のパッチが適用されています。この辺りは

tenforward.hatenablog.com

tenforward.hatenablog.com

をどうぞ。

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 のシステムコールインターセプション機能がうまく動いたので調子に乗って引き続き試します。

tenforward.hatenablog.com

この機能には色々できることがあって今後も追加されそうな気がしますが、とりあえずは見た目でパッとわかりやすいファイルシステムのマウントを試してみましょう。

お試し環境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" という設定だけが効いてないようなのですが、今日は力尽きました。引き続き調べます。

(つづく)

LXDのシステムコールインターセプション機能を試す(解決編)

3 回シリーズになるとは思ってなかったこのシリーズ、ついに解決編です。

tenforward.hatenablog.com

1 度目の mknod は成功するものの、なぜか続けて mknod を実行すると失敗してしまうという謎のトラブル。ふと、LXC のコミットログを眺めていると seccomp 関連の修正があり、見てみると簡単な修正ですが、コミットログと修正内容を見てピンと来ました。

それがこれです。

github.com

新しいカーネルではバッファーをゼロクリアしておく必要があるとのこと。これだと 1 度実行して成功しても、そのバッファに前のデータが残っていたらエラーになりそう!ということで試すとバッチリ成功しました。

これだけだとなんなので、今回のシステムコールインターセプション機能についてまとめておきましょう。

システムコールインターセプション機能

5.0 kernel で Add seccomp trap to userspace. なる機能が追加されました。解説は次にあります。

これは、本来カーネル側で権限のチェックを行い実行の可否を判断するシステムコールの実行を、seccomp を使ってシステムコールをトラップし、ユーザースペースに処理を委ね、その応答を見て処理を進めるかどうかを決めるというもののようです。

この機能が LXD に入ったのは 3.13 と少し前で、その時のアナウンスには

LXD 3.13 を 5.0 以上のカーネル、最新の libseccomp、liblxc と組み合わせると、ユーザー空間でシステムコールインターセプトして仲介できるようになりました

とあります。

お試し

そういうわけで、先の失敗編エントリーのころからは少し環境を変えています。

  • Plamo Linux 7.1
  • kernel 5.6
  • libseccomp は 2020-03-30 時点の master branch を取得しビルド
  • LXC 4.0.1 + 先のパッチ
  • LXD 4.0.0

これで Alpine Linux コンテナを作成しました。

$ lxc launch images:alpine/3.11 a1
a1 を作成中
a1 を起動中
$ lxc shell a1
a1:~# whoami
root
a1:~# cat /proc/self/uid_map /proc/self/gid_map (非特権コンテナで動作していることを確認)
         0     100000      65537
         0     100000      65537
a1:~# mknod test c 5 1
mknod: test: Operation not permitted

上のように root ユーザーではありますが、ID のマッピングを見ると、ホスト環境では uid/gid: 100000 の一般ユーザー権限で User Namespace を使って動作していることがわかります。

当然一般ユーザーでは mknod する権限はありませんので失敗します。

ここで LXD の設定 security.syscalls.intercept.mknodtrue に設定して同じ操作を試してみましょう。

$ lxc config set a1 security.syscalls.intercept.mknod=true
$ lxc config show a1
architecture: x86_64
config:
  image.architecture: amd64
  image.description: Alpine 3.11 amd64 (20200402_13:00)
  image.os: Alpine
  image.release: "3.11"
  image.serial: "20200402_13:00"
  image.type: squashfs
  security.syscalls.intercept.mknod: "true"
    :(略)

このように設定が true になりました。コンテナに設定を反映させるために再起動します。

$ lxc restart a1
$ lxc shell a1
a1:~# mknod test c 5 1
a1:~# mknod test2 c 0 0
a1:~# ls -l
total 0
crw-rw-rw-    0 root     root        5,   1 Apr 10 11:53 test
crw-rw-rw-    0 root     root        0,   0 Apr 10 11:53 test2

このように、LXC にパッチを適用する前は失敗していた 2 つ目の mknod も無事成功し、キャラクターデバイスがふたつ作成できました。

ちなみにどんなデバイスでも作成できるわけではありません。LXD 側でその辺りは制御されており、問題のないデバイスのみが作成できます。作成できるデバイスのリストは公式文書にあります。

リストにないデバイスファイルは次のように失敗します。

a1:~# mknod test3 c 5 2
mknod: test3: Operation not permitted

このあたりの機能、もう少し遊んでみるつもりです。

Linux 5.6 で追加された Time Namespace(1)

Linux 5.6 の情報をなんとなく調べてたらコンテナ関連の機能増えてました。lwn.net で適当な記事を絞りきれなかったのでとりあえずそれっぽいコミットをメモ代わりに

このあたりもかな

とりあえず 5.6 kernel で起動してみました。

$ uname -r
5.6.0-plamo64
$ ls -l /proc/self/ns
合計 0
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 net -> net:[4026532008]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 pid -> pid:[4026531836]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 time -> time:[4026531834]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 time_for_children -> time_for_children:[4026531834]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 user -> user:[4026531837]
lrwxrwxrwx 1 karma users 0  4月  1日  11:42 uts -> uts:[4026531838]

増えてますね!!

(2020-04-07 追記)

id:udzura さんが試してくれてます❗リリースされている util-linux ではダメですので、試したい人はリポジトリから持ってきましょう。

(2020-04-09 追記)

udzura さんによるブログエントリもリリースされました!これでこのテーマでのこのブログでの(2)はないかも?w

udzura.hatenablog.jp