TenForwardの日記 このページをアンテナに追加 RSSフィード

2016-07-26

[][] ip netns コマンドが意外にきめ細やかにコンテナを作ってくれる

(2016-07-26: 誤記修正しました "setns -> nsenter")

お手軽にシェルスクリプトなんかでコンテナを作る場合の強い味方といえば

  • util-linux の unshare/nsenter コマンド
  • iproute2 の ip netns コマンド

が代表的でしょう。"ip netns" は Network Namespace 作ってコマンド実行するだけの単純なコマンドかと思ったら、中では意外に色々細かくやってくれていることがわかったのでちょっと紹介しておきます。

以下は iproute2 4.2.0 で試しています。

/var/run/netns

まずよく知られているのが /var/run/netns 以下に Namespace 名のファイルを作ってくれることですね。これは、Namespace 内で動いているプロセスがなくなったら Namespace が消滅してくれるのを防ぐためです。だって ip netns add が終了したら、Namespace 内にはプロセスいなくなりますから。

まずはこの辺りで /var/run/netns を MS_SHARED にして、自身を bind mount して、ココをマウントポイントにしています。

   636        while (mount("", NETNS_RUN_DIR, "none", MS_SHARED | MS_REC, NULL)) {
        :(snip)
   644               /* Upgrade NETNS_RUN_DIR to a mount point */
   645               if (mount(NETNS_RUN_DIR, NETNS_RUN_DIR, "none", MS_BIND, NULL)) {
        :(snip)
   651        }
   (ip/ipnetns.cより)

そして

   667        /* Bind the netns last so I can watch for it */
   668        if (mount("/proc/self/ns/net", netns_path, "none", MS_BIND, NULL) < 0) {
   (ip/ipnetns.cより)

ip netns add コマンドのプロセスディレクトリの ns/net ファイルを /var/run/netns 以下に bind mount していますね。このように Namespace を示す特殊なファイルを残したままにしておけば Namespace が消えません。

Namespace と veth 作成

ここで Network Namespace として "netns01" を作成し、ホストとコンテナ間は veth インターフェースを作成します。

# export NETNS="netns01"
# export VETH="veth0"

それでは Network Namespace を作りましょう。

# ip netns add $NETNS
# ip netns list
netns01

簡単ですね。

それでは veth インターフェースを作りましょう。

# ip link add name $VETH-host type veth peer name $VETH-ns
# ip link set $VETH-ns netns $NETNS
# ip addr add 10.10.10.10/24 dev $VETH-host
# ip link set $VETH-host up
# ip netns exec $NETNS ip addr add 10.10.10.11/24 dev $VETH-ns
# ip netns exec $NETNS ip link set $VETH-ns up

ホスト側は "veth-host"、コンテナ側は "veth-ns" という名前のペアを作りました。それぞれアドレスを与えて、インターフェースを up します。

では、この作成した Namespace で bash を実行してコンテナを作成してみましょう。

# ip netns exec $NETNS /bin/bash
# echo $$
11945
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
11: veth0-ns@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ba:aa:99:70:46:ab brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.10.10.11/24 scope global veth0-ns
       valid_lft forever preferred_lft forever
    inet6 fe80::b8aa:99ff:fe70:46ab/64 scope link 
       valid_lft forever preferred_lft forever

"veth0-ns" インターフェースがあって、ちゃんとアドレスが割りあたってますね。まあ、これはそうなるようにコマンド実行していますので当たり前です。

コンテナ内の sysfs

ここで、Namespace 内で /sys 以下を見てみましょう。

# ls /sys/class/net -l
合計 0
lrwxrwxrwx 1 root root 0  7月 26日  20:01 lo -> ../../devices/virtual/net/lo/
lrwxrwxrwx 1 root root 0  7月 26日  20:01 veth0-ns -> ../../devices/virtual/net/veth0-ns/

ちゃんと Namespace 内のインターフェースのみ見えてますね。これは、この辺りの処理ですね。

    66        if (unshare(CLONE_NEWNS) < 0) {
    67               fprintf(stderr, "unshare failed: %s\n", strerror(errno));
    68               return -1;
    69        }
    70        /* Don't let any mounts propagate back to the parent */
    71        if (mount("", "/", "none", MS_SLAVE | MS_REC, NULL)) {
    72               fprintf(stderr, "\"mount --make-rslave /\" failed: %s\n",
    73                      strerror(errno));
    74               return -1;
    75        }
    76        /* Mount a version of /sys that describes the network namespace */
    77        if (umount2("/sys", MNT_DETACH) < 0) {
    78               fprintf(stderr, "umount of /sys failed: %s\n", strerror(errno));
    79               return -1;
    80        }
    81        if (mount(name, "/sys", "sysfs", 0, NULL) < 0) {
    82               fprintf(stderr, "mount of /sys failed: %s\n",strerror(errno));
    83               return -1;
    84        }
    (lib/namespace.cより)
  • 66行目で Mount Namespace を作ってます
  • 71行目で他の Mount Namespace に自身の Mount が伝播しないようにしています
  • 77行目で現在の /sys を umount して
  • 81行目で sysfs を改めて mount します

まあ、当たり前といえば当たり前ですが、ちゃんとここまで面倒見ててくれたんですね、ip netns。

/etc/netns

そして、以上の辺りのソースをつらつらと眺めていたら、気になる関数を見つけました。

    86        /* Setup bind mounts for config files in /etc */
    87        bind_etc(name);
    (lib/namespace.cより)

これを見てみると

    35               snprintf(netns_name, sizeof(netns_name), "%s/%s", etc_netns_path, entry->d_name);
    36               snprintf(etc_name, sizeof(etc_name), "/etc/%s", entry->d_name);
    37               if (mount(netns_name, etc_name, "none", MS_BIND, NULL) < 0) {
    (lib/namespace.cより)

"etc_netns_path" は /etc/netns/[namespace名] が入っていますので、その下のファイルを作成した Namespace 内に bind mount してくれるようです。

早速、ホスト上で /etc/netns/netns01/{hosts,resolv.conf} を作ってみました。

# cat /etc/netns/$NETNS/hosts 
10.10.10.10	host01
10.10.10.11	ns01
# cat /etc/netns/$NETNS/resolv.conf 
nameserver 8.8.8.8

以上を準備してから、再度コンテナを作成します。

# ip netns exec $NETNS /bin/bash
# cat /etc/resolv.conf 
nameserver 8.8.8.8
# cat /etc/hosts
10.10.10.10	host01
10.10.10.11	ns01

見事に作成した Namespace で実行されるコンテナでは準備したファイルが見えていますね。こんなとこまで面倒見てくれてたとは。(ちゃんと man ip-netns に書いてあるやん)

ip netns コマンドのソース、シンプルでコメントもちゃんと書かれてあって読みやすいのでコンテナ作成の勉強になる気がします。

2016-04-25

[][][] cgroup なのか cgroups なのか

割とどーでもいい話(でも気になってた人多いはずw)

以前、第4回のコンテナ勉強会でも質問が出たのですが、cgroup/cgroups という機能の正式な名称は cgroup なのか cgroups なのか、というのはよくわかりませんでした。

私は、英語って単数・複数をきちんと使い分けるし、cgroup は複数のサブシステム・コントローラが存在するので、海外の人は "cgroups" と複数形で使うんだろうなあと思ってました。機能自体を指しているのか、コントローラ群を含めて指しているのかって、曖昧な文脈も多いですし。

しかし、この論争にもついにピリオドが打たれました。カーネル付属文書の cgroup-v2.txt をご覧ください。

"cgroup" stands for "control group" and is never capitalized. The

singular form is used to designate the whole feature and also as a

qualifier as in "cgroup controllers". When explicitly referring to

multiple individual control groups, the plural form "cgroups" is used.

https://www.kernel.org/doc/Documentation/cgroup-v2.txt

超訳

"cgroup" は "control group" を表します。大文字では表記することはありま

せん。単数形は全機能を表したり、"cgroup controllers" のように修飾子と

して全機能を表すために使います。明確に複数の個別の control groups を示

すときに、複数形の "cgroups" を使います。

Tejun Heo 氏は非英語ネイティブと想像しているのできっとその辺りが気になっていたんだ、なのでそこをはっきりさせたんだ、と妄想しています(笑)。

まあ相変わらず cgroup v1 については明確な定義が書かれてるわけではないんですがね (^_^;)

2016-04-14

[][][] 4.5 カーネルで stable となった cgroup の単一階層構造 cgroup v2 の io コントローラ

Control Group v2

以前も少し紹介していましたし、連載でも少し触れましたが、今広く (?) 使われている cgroup は色々問題があって、単一階層構造の cgroup が開発されていました。この辺りは

で紹介しました。

以前は開発中の機能だったため、マウントするときに "__DEVEL__sane_behavior" などというふざけたオプションが必要でした。(^^)

この後も順調に (?) 開発はすすみ、4.5 カーネルのリリースでついにこの機能が stable となったようで、"__DEVEL__" というプレフィックスも不要になりましたし、正式な機能で「まともなふるまい」なんてのはないだろうという話があったのかなかったのか知りませんが、名前も "Control Group v2" という名前になったようです。今までのは "v1" です。

ドキュメントは

にあります。

前に試した時は v1 にあったコントローラ (サブシステム) が全部現れていましたが、正式リリースとなった 4.5 の時点で有効なコントローラは memory, io, pid の 3 つだけのようです。

v1 の問題点のひとつに、コントローラがばらばらに実装されているため、コントローラ間の連携ができないという問題がありました。このため、blkio というブロックデバイスに対する I/O 制限を行うコントローラがあるにもかかわらず、通常のファイル I/O (の書き込み) に対する制限ができませんでした (memory と連携できていないため)。

この辺りは「第4回 コンテナ型仮想化の情報交換会@東京」で @hiro_kamezawa さんにお話頂いたので、詳細をお知りになりたい方はそちらをどうぞ。

v2 では、階層構造が単一となって、この辺りの制限ができるようになりました。昨年の LinuxCon で Heo Tejun 氏が ext2 に対応したという話をされていましたが、4.5 を見てみると ext2, ext4, btrfs に対して制限がかかるようです。

というわけで、この制限が働くのか簡単に試してみました。

kernel は 4.6-rc3、ファイルシステムext2ext4 で試しています。

かなり適当な確認なので、間違いとかあるかもしれませんので、気づいたら優しく教えてください。

v1 のおさらい

v2 を試す前に、まずは v1 の blkio コントローラのおさらいをしておきましょう。direct I/O 以外では制限がかかっていないことも確認してみました。

v1 準備

"test01" cgroup を作成して、/dev/vdb に対する 1MB/sec の読み書きの制限を設定してみました。

  1. cgroup 作成
    # mkdir /sys/fs/cgroup/blkio/test01
  2. プロセスを "test01" へ登録
    # echo $$ > /sys/fs/cgroup/blkio/test01/tasks
  3. /dev/vdbに対する制限を設定
    # ls -l /dev/vdb
    brw-rw---- 1 root disk 254, 16 Apr 13 06:30 /dev/vdb (デバイス番号の確認)
    # echo "254:16 1048576" > /sys/fs/cgroup/blkio/test01/blkio.throttle.read_bps_device (読み込み制限)
    # echo "254:16 1048576" > /sys/fs/cgroup/blkio/test01/blkio.throttle.write_bps_device (書き込み制限)
v1 を使った書き込み制限 (direct I/O)
# dd oflag=direct if=/dev/zero of=/data/testfile bs=4K count=1024
1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 4.00347 s, 1.0 MB/s

実行時に iostat を実行しました。出力の一部。

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vdb             258.59         0.00      1034.34          0       1024
vdb1            258.59         0.00      1034.34          0       1024

書き込みが 1MB/sec に制限されていますね。

v1 を使った読み込み制限 (direct I/O)
# dd iflag=direct if=/data/testfile of=/dev/null bs=4K count=1024
1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 4.0018 s, 1.0 MB/s

同様に iostat 出力の一部。

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vdb             261.22      1044.90         0.00       1024          0
vdb1            261.22      1044.90         0.00       1024          0

読み込みが 1MB/sec に制限されています。

v1 を使った書き込み制限

"oflag=direct" を指定せずに dd を実行してみると、

# dd if=/dev/zero of=/data/testfile bs=4K count=1048576
^C636264+0 レコード入力
636264+0 レコード出力
2606137344 バイト (2.6 GB) コピーされました、 22.6348 秒、 115 MB/秒
# iostat -p vdb 1 | grep "vdb "
  :(略)
vdb             311.00        12.00    315392.00         12     315392
vdb             339.00        12.00    344064.00         12     344064
vdb             456.57        12.12    434424.24         12     430080
vdb             389.53         4.65    351255.81          4     302080
vdb              11.00         0.00      9216.00          0       9216
vdb              39.00         8.00     33688.00          8      33688
vdb              35.00         0.00     31940.00          0      31940
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
  :(略)
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb              84.21         4.21     73675.79          4      69992
vdb             140.43         0.00    126012.77          0     118452
vdb              11.11         4.04      9002.02          4       8912
vdb             507.22        12.37    443455.67         12     430152
vdb             410.10        12.12    376501.01         12     372736
vdb               1.00         0.00       912.00          0        912

制限はかかっていませんね。書き込まれたらしばらく 0 の時間があり、また書き込みが再開されているのがわかります。

v1 を使った読み込み制限

"iflag=direct" を外して実行します。実行前にキャッシュクリアします。

# echo 3 > /proc/sys/vm/drop_caches (キャッシュクリア)
# dd if=/data/testfile of=/dev/null bs=4K count=1024 
1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 4.0036 s, 1.0 MB/s

読み込みは制限がききますね。

Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vdb               8.25      1055.67         0.00       1024          0
vdb1              8.25      1055.67         0.00       1024          0

cgroup v2 の io コントローラ

v2 の準備

v2 は "cgroup2" という名前でマウントします。名前以外は以前試した「まともなふるまい」時代とあまり変わりません。

  1. /sys/fs/cgroup にマウント。
    mount -t cgroup2 cgroup /sys/fs/cgroup/
  2. ルート cgroup の確認。ファイルが 3 つだけです
    # ls /sys/fs/cgroup/
    cgroup.controllers cgroup.procs cgroup.subtree_control
  3. 使えるコントローラを確認
    # cat /sys/fs/cgroup/cgroup.controllers 
    io memory pids
  4. "memory" と "io" をサブ cgroup で使えるようにします
    # cat /sys/fs/cgroup/cgroup.subtree_control (サブ cgroup で使えるコントローラ一覧の確認。デフォルトは空)
    # echo "+memory +io" > /sys/fs/cgroup/cgroup.subtree_control (io と memory を追加)
    # cat /sys/fs/cgroup/cgroup.subtree_control (再度確認)
    io memory (登録されている)
  5. "test01" cgroup 作成
    mkdir /sys/fs/cgroup/test01 (作成)
    # ls /sys/fs/cgroup/test01 (test01ディレクトリの確認)
    cgroup.controllers io.max memory.events memory.stat
    cgroup.events io.stat memory.high memory.swap.current
    cgroup.procs io.weight memory.low memory.swap.max
    cgroup.subtree_control memory.current memory.max
    # cat /sys/fs/cgroup/test01/cgroup.controllers (test01で使えるコントローラの確認)
    io memory
    "test01"でioとmemoryが使えるのが確認できました
v2 で direct I/O 制限

まずは v1 でもちゃんと制限された direct I/O を確認しました。

まずは書き込み。

# dd oflag=direct if=/dev/zero of=/data/testfile bs=4K count=1024
1024+0 レコード入力
1024+0 レコード出力
4194304 バイト (4.2 MB) コピーされました、 4.0041 秒、 1.0 MB/秒
Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vdb             261.22         0.00      1044.90          0       1024
vdb1            261.22         0.00      1044.90          0       1024

読み込み。

#  dd iflag=direct if=/data/testfile of=/dev/null bs=4K count=1024
1024+0 レコード入力
1024+0 レコード出力
4194304 バイト (4.2 MB) コピーされました、 4.00393 秒、 1.0 MB/秒
Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn
vdb             261.22      1044.90         0.00       1024          0
vdb1            261.22      1044.90         0.00       1024          0

v1 と同じく制限されています。

v2 で読み込み制限

少し大きなファイルで確認しました。

# echo 3 > /proc/sys/vm/drop_caches
# dd if=/data/testfile of=/dev/null bs=4K count=1048576
^C4605+0 レコード入力
4604+0 レコード出力
18857984 バイト (19 MB) コピーされました、 18.1077 秒、 1.0 MB/秒

iostat の出力は

# iostat -p vdb 1 | grep "vdb "
vdb              30.83      2378.02     10808.93    1864321    8473984
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               7.07       630.30         0.00        624          0
vdb               8.08      1034.34         0.00       1024          0
vdb               8.00      1024.00         0.00       1024          0
vdb               8.08      1034.34         0.00       1024          0
vdb               8.00      1024.00         0.00       1024          0
vdb               8.08      1034.34         0.00       1024          0
  :(略)

これは v1 と同じように制限がかかります。

v2 で書き込み制限

さて、いよいよハイライト

# dd if=/dev/zero of=/data/testfile bs=4K count=1048576
^C51563+0 レコード入力
51563+0 レコード出力
211202048 バイト (211 MB) コピーされました、 35.7076 秒、 5.9 MB/秒
# iostat -p vdb 1 | grep "vdb "
vdb              28.16      2182.44      9794.36    1888229    8473984
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb               0.00         0.00         0.00          0          0
vdb              36.17       144.68         0.00        136          0
vdb               1.05         0.00       842.11          0        800
vdb               1.01         0.00       517.17          0        512
vdb               1.00         0.00       508.00          0        508
vdb               1.01         0.00       783.84          0        776
vdb               3.00         0.00      1160.00          0       1160
vdb               1.00         0.00      1004.00          0       1004
vdb               1.01         0.00      1034.34          0       1024
vdb               1.01         0.00      1034.34          0       1024
vdb               1.01         0.00      1034.34          0       1024
vdb               1.01         0.00      1034.34          0       1024
vdb               0.99         0.00      1013.86          0       1024
vdb               2.02         0.00      1046.46          0       1036
vdb               2.00         0.00      1024.00          0       1024
vdb               1.01         0.00      1034.34          0       1024

落ち着くまで少し時間があるのと、一瞬 1024 以上の値が出てますが、大体きれいに 1024 (KB) で制限されています。

まとめ

とりあえずディスクへの書き込みがちゃんと制限されているっぽいです (たぶん)。

2016-04-12

[][][] cgroup namespace (2)

前回は /proc/$PID/cgroup ファイルが Namespace を反映した形で記載されているのを見ました。

とりあえずここまで。これだけだとすでにマウントされている cgroupfs はそのままの元のディレクトリ階層で見えるので、/proc/$PID/cgroup だけ見え方が変わっても意味がないような気がしますが、改めて cgroupfs のマウントを試すとエラーになりますし、どうするものなのかちょっとまだ見えてないので、またわかれば続編を書く予定です。

と書きましたが、試したところちゃんと cgroupfs も Namespace ごとに見えるようになったので紹介しておきます。前回何をボケて失敗していたのかわかりませんが、以下のように普通に簡単な処理をやっただけです。

  1. まずは "test01" cgroup を作成します (memory だけ)
    # mkdir -p /sys/fs/cgroup/memory/test01/
  2. 現在のシェルを "test01" の tasks に登録します
    # echo $$ > /sys/fs/cgroup/memory/test01/tasks
  3. namespace内の cgroupfs をホストから見て別のところにマウントするために、LXC コンテナ用のツリーを借ります
    # cd /var/lib/lxc/test01/rootfs/
  4. unshare で chroot を実行します
    # unshare -C chroot $PWD
  5. namespace 内で proc をマウントし、/sys/fs/cgroup を tmpfs でマウントします。これは特に不要な気がしますが
    # mount -n -t proc proc /proc
    # mount -n -t tmpfs none /sys/fs/cgroup/
  6. memory サブシステムを /sys/fs/cgroup/memory にマウントします
    # mkdir /sys/fs/cgroup/memory
    # mount -n -t cgroup -o memory memory /sys/fs/cgroup/memory/

マウントできたので、確認してみます。

# ls /sys/fs/cgroup/memory/
cgroup.clone_children               memory.memsw.failcnt
cgroup.event_control                memory.memsw.limit_in_bytes
cgroup.procs                        memory.memsw.max_usage_in_bytes
memory.failcnt                      memory.memsw.usage_in_bytes
memory.force_empty                  memory.move_charge_at_immigrate
memory.kmem.failcnt                 memory.numa_stat
memory.kmem.limit_in_bytes          memory.oom_control
memory.kmem.max_usage_in_bytes      memory.pressure_level
memory.kmem.slabinfo                memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt             memory.stat
memory.kmem.tcp.limit_in_bytes      memory.swappiness
memory.kmem.tcp.max_usage_in_bytes  memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes      memory.use_hierarchy
memory.kmem.usage_in_bytes          notify_on_release
memory.limit_in_bytes               tasks
memory.max_usage_in_bytes

普通にマウントできていて、"test01" cgroup は存在しませんね。つまりここが cgroupfs のルートなわけです。

では、ここに "test02" cgroup を作ってみましょう。

root@enterprise:~# mkdir /sys/fs/cgroup/memory/test02
root@enterprise:~# ls -d /sys/fs/cgroup/memory/test02
/sys/fs/cgroup/memory/test02/
root@enterprise:~# ls /sys/fs/cgroup/memory/test02
cgroup.clone_children               memory.memsw.failcnt
cgroup.event_control                memory.memsw.limit_in_bytes
cgroup.procs                        memory.memsw.max_usage_in_bytes
  :(略)

/sys/fs/cgroup/memory 直下に test02 というディレクトリができて、中も普通にファイルができています。

"test02" にプロセスを登録して /proc/$PID/cgroup を確認してみましょう。

# echo $$ > /sys/fs/cgroup/memory/test02/tasks
# echo $$
8574
# cat /proc/self/cgroup 
14:debug:/
13:pids:/
12:hugetlb:/
11:net_prio:/
10:perf_event:/
9:net_cls:/
8:freezer:/
7:devices:/
6:memory:/test02
5:blkio:/
4:cpuacct:/
3:cpu:/
2:cpuset:/
1:name=systemd:/

ちゃんと Namespace 内の cgroup ツリーになってますね。

ここで、元のホスト上の Namespace から "test01" と "test02" を確認してみましょう。

$ ls -d /sys/fs/cgroup/memory/test01
/sys/fs/cgroup/memory/test01/
$ ls -d /sys/fs/cgroup/memory/test01/test02
/sys/fs/cgroup/memory/test01/test02/

作成した cgroup namespace の外では "test01" の下に "test02" がありますね。tasks ファイルも確認してみると、

$ cat /sys/fs/cgroup/memory/test01/test02/tasks 
8574

シェルの PID が登録されていますね。

2016-04-06

[][][] cgroup namespace (1)

以前、RHEL6 のころに ns cgroup ってサブシステムが cgroup にありましたが、それとは別のお話。

4.6 カーネルに入る cgroup namespace のお話です (Ubuntu 16.04 のカーネルには入るようです) 。namespace ごとに別の cgroup ツリーが見えるようにするものかな。

軽く試してみました。

準備

(2016-04-12 追記) 今だと 4.6-rc? で試せます

とりあえず cgroup namespace の機能が入ったカーネル

の "2016-03-21/nsroot" というブランチのものを使いました。出来上がったカーネルはこんな

$ uname -r
4.5.0-rc1-plamo64-cgns-00009-g37bbd8c-dirty

(-plamo64 は make menuconfig で指定してる文字列)

cgroup namespace が作れる unshare を含む util-linux

から。こちらも "2016-03-02/cgns" というブランチを元にパッチを作って util-linux-2.27.1 に当てました。

実行

とりあえず /proc/$PID/cgroup ファイルで見える cgroup のパスが Namespace ごとに独立して見えるみたい。この様子を見てみました。

まずは現在のシェルを cgroup に所属させます。memory サブシステムのみ "test01" に所属させてみました。

# echo $$ > /sys/fs/cgroup/memory/test01/tasks

この状態で同じシェル上で /proc/self/cgroup を確認してみましょう。

# cat /proc/self/cgroup 
13:debug:/
12:pids:/
11:hugetlb:/
10:net_prio:/
9:perf_event:/
8:net_cls:/
7:freezer:/
6:devices:/
5:memory:/test01
4:blkio:/
3:cpuacct:/
2:cpu:/
1:cpuset:/

memory は /test01 になっていますね。

ここでパッチを当てた unshare コマンドで新しい cgroup namespace を作成します。-C が cgroup namespace を作るオプションです。

# unshare -C
# cat /proc/self/cgroup
13:debug:/
12:pids:/
11:hugetlb:/
10:net_prio:/
9:perf_event:/
8:net_cls:/
7:freezer:/
6:devices:/
5:memory:/
4:blkio:/
3:cpuacct:/
2:cpu:/
1:cpuset:/
# echo $$
22302

memory の部分が "/test01" でなく、"/" になっていますね。最後に現在のシェルの PID を確認しています。

この PID の cgroup 情報を別のシェル (元の cgroup namespace にいるシェル) から見てみます。

# cat /proc/22302/cgroup 
13:debug:/
12:pids:/
11:hugetlb:/
10:net_prio:/
9:perf_event:/
8:net_cls:/
7:freezer:/
6:devices:/
5:memory:/test01
4:blkio:/
3:cpuacct:/
2:cpu:/
1:cpuset:/

作成した namespace の外から見るとちゃんと "/test01" になっていますね。

/proc/$PID/ns/cgroup ファイルも確認しておきましょう。

# ls -l /proc/1/ns/cgroup 
lrwxrwxrwx 1 root root 0  4月  6日  06:59 /proc/1/ns/cgroup -> cgroup:[4026531835]
# ls -l /proc/22273/ns/cgroup 
lrwxrwxrwx 1 root root 0  4月  6日  07:17 /proc/22302/ns/cgroup -> cgroup:[4026532404]

リンク先が異なりますので違う namespace にいることがわかります。これはこれまでの他の namespace と同じですね。

プロセスが cgroup 間を移動したとき

プロセスを cgroup 間で移動させると面白い見え方をします。まず "test01" と同じ階層に "test02" を作成し、先ほどのプロセスを "test01" から "test02" に移動させます。

# mkdir /sys/fs/cgroup/memory/test02
# echo 22302 > /sys/fs/cgroup/memory/test02/tasks

先ほど unshare して作った namespace 内のシェルから /proc/$PID/cgroup ファイルを見てみると、

# cat /proc/self/cgroup 
13:debug:/
12:pids:/
11:hugetlb:/
10:net_prio:/
9:perf_event:/
8:net_cls:/
7:freezer:/
6:devices:/
5:memory:/../test02
4:blkio:/
3:cpuacct:/
2:cpu:/
1:cpuset:/

相対パスのように見えます。

とりあえずここまで。これだけだとすでにマウントされている cgroupfs はそのままの元のディレクトリ階層で見えるので、/proc/$PID/cgroup だけ見え方が変わっても意味がないような気がしますが、改めて cgroupfs のマウントを試すとエラーになりますし、どうするものなのかちょっとまだ見えてないので、またわかれば続編を書く予定です。

(続くかも?)

2016-01-15

[Linux][Kernel][Security] User Namespace と Overlayfs と CVE-2015-8660

単なるメモです。

aufs-users ML に流れた、Overlayfs と User Namespace(userns) を併せて使った時の脆弱性のお話:

脆弱性の内容は

これに関連する脆弱性として CVE-2015-8660 があるようで、これはすでに修正済みで、上記に書かれた脆弱性もこれで影響なくなるらしい。

aufs で userns は aufs を使った一般ユーザ権限で起動するコンテナ - TenForwardの日記 のあたりの話で、普段から allow_userns=1 で使ってるので、少し興味をそそられたので試してみました。

ただ、上記に書かれている脆弱性ですが、そもそも vanilla kernel だと userns 内の特権ユーザでも overlayfs のマウントはできないので、ここに書かれている userns 内で Overlayfs マウントして悪いことする、ってのは無理なような気がするので、パッチを当てて userns 内で Overlayfs マウントができるようにしている Ubuntu 特有の話に思えるけど、他にできるようにしてるディストリビューションあるのかな?

Plamo でも Ubuntu を真似して、userns 内で overlayfs マウント可能にしてます。こんなパッチ

diff -uNr linux-4.2/fs/overlayfs/super.c linux-4.2-olfs/fs/overlayfs/super.c
--- linux-4.2/fs/overlayfs/super.c	2015-08-31 03:34:09.000000000 +0900
+++ linux-4.2-olfs/fs/overlayfs/super.c	2015-09-02 16:09:24.339937219 +0900
@@ -1097,6 +1097,7 @@
 	.name		= "overlay",
 	.mount		= ovl_mount,
 	.kill_sb	= kill_anon_super,
+	.fs_flags	= FS_USERNS_MOUNT,
 };
 MODULE_ALIAS_FS("overlay");
 

(参考: overlayfs と LXC 非特権コンテナの snapshot によるクローン - TenForwardの日記の「Plamo 5.2の非特権コンテナのclone」の項)

とりあえず aufs-users ML にあったパッチを当てて試してみました。aufs では成功しないけど…

試した環境は

Kernel 4.2.3 の Overlayfs

$ ./UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
Setting uid map in /proc/3749/uid_map
Setting gid map in /proc/3749/gid_map
euid: 65534, egid: 65534
euid: 0, egid: 0
overlayfs
Namespace helper waiting for modification completion
Namespace part completed

成功してるらしいけど、これだけだと何がなんだか謎なので、gdb でステップ実行しながら、Overlayfs マウントして su を chmod したあたりで止めてチェックしてみました。

$ gdb ./UserNamespaceOverlayfsSetuidWriteExec
(gdb) set follow-fork-mode child 
(gdb) b 63
Breakpoint 1 at 0x400fb5: file UserNamespaceOverlayfsSetuidWriteExec.c, line 63.
(gdb) run -- /bin/bash
Starting program: /home/karma/userns/UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
[New process 3236]
Setting uid map in /proc/3236/uid_map
Setting gid map in /proc/3236/gid_map
euid: 0, egid: 0
euid: 0, egid: 0
[Switching to process 3236]

Breakpoint 1, childFunc (arg=0x7fffffffe668)
    at UserNamespaceOverlayfsSetuidWriteExec.c:63
63	  result=chmod("su", 04777);
(gdb) n
64	  if(result) {
(gdb) p result
$1 = 0
(gdb)

ここで止めます。別シェル

$ sudo nsenter -t 3908 -m -U
plamo64:/# ls -l /tmp/x/bin/su
-rwsrwxrwx 1 nobody nogroup 161  1月 15 18:38 /tmp/x/bin/su
plamo64:/# ls -l /tmp/x/over/su
-rwsrwxrwx 1 nobody nogroup 161  1月 15 18:38 /tmp/x/over/su

ここで su のパーミッションが変わっているのがマズいんだと思う。

Kernel 4.4 の Overlayfs

対策済みなカーネルでやるとこんな風に。

$ ./UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
Setting uid map in /proc/4960/uid_map
euid: 65534, egid: 65534
Setting gid map in /proc/4960/gid_map
euid: 0, egid: 0
overlayfs
Mode change failed
Failed to open /proc/4960/cwd/su, error No such file or directory

同じように gdb から起動して chmod のあとで止めてみます。

$ gdb ./UserNamespaceOverlayfsSetuidWriteExec
(gdb) set follow-fork-mode child 
(gdb) b 63
Breakpoint 1 at 0x400ff1: file UserNamespaceOverlayfsSetuidWriteExec.c, line 63.
(gdb) set env TRY_AUFS=1 
(gdb) run -- /bin/bash
Starting program: /home/karma/userns/UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
[New process 3215]
Setting uid map in /proc/3215/uid_map
Setting gid map in /proc/3215/gid_map
euid: 0, egid: 0
euid: 0, egid: 0
aufs
[Switching to process 3215]

Breakpoint 1, childFunc (arg=0x7fffffffe668)
    at UserNamespaceOverlayfsSetuidWriteExec.c:63
63	  result=chmod("su", 04777);
(gdb) n
Failed to open /proc/3215/cwd/su, error Permission denied
64	  if(result) {
(gdb) p result
$1 = -1
(gdb) 

上記で失敗しているのはわかるんだけど、ここで同じく別シェルを開いて一応確認。

$ sudo nsenter -t 3215 -m -U
plamo64:/# ls -l /tmp/x/bin/su
-rwsr-xr-x 1 nobody nogroup 37400  1月 22  2015 /tmp/x/bin/su
plamo64:/# ls -l /tmp/x/over/su
-rwsr-xr-x 1 nobody nogroup 37400  1月 22  2015 /tmp/x/over/su

Kernel 4.2.3 の aufs (allow_userns=1)

allow_userns=1 で実行。

$ TRY_AUFS=1 ./UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
Setting uid map in /proc/3321/uid_map
Setting gid map in /proc/3321/gid_map
euid: 0, egid: 0
euid: 0, egid: 0
aufs
Mode change failed
Failed to open /proc/3321/cwd/su, error Permission denied
$ gdb ./UserNamespaceOverlayfsSetuidWriteExec
(gdb) set follow-fork-mode child 
(gdb) set env TRY_AUFS=1
(gdb) b 63
Breakpoint 1 at 0x400ff1: file UserNamespaceOverlayfsSetuidWriteExec.c, line 63.
(gdb) run -- /bin/bash
Starting program: /home/karma/userns/UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
[New process 3399]
Setting uid map in /proc/3399/uid_map
Setting gid map in /proc/3399/gid_map
euid: 0, egid: 0
euid: 0, egid: 0
aufs
[Switching to process 3399]

Breakpoint 1, childFunc (arg=0x7fffffffe668)
    at UserNamespaceOverlayfsSetuidWriteExec.c:63
63	  result=chmod("su", 04777);
(gdb) n
64	  if(result) {
(gdb) p result
$1 = -1
(gdb) n
65	    fprintf(stderr, "Mode change failed\n");

Kernel 4.2.3 の aufs (allow_userns=0)

allow_userns=0 にすると、そもそも Userns 内のユーザはマウントできないので

$ TRY_AUFS=1 ./UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
Setting uid map in /proc/3245/uid_map
Setting gid map in /proc/3245/gid_map
euid: 0, egid: 0
euid: 0, egid: 0
aufs
Overlay mounting failed: 1 (Operation not permitted)

Kernel 4.4.0 の aufs

4.2.3 と同じような動き。

$ TRY_AUFS=1 ./UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
Setting uid map in /proc/3202/uid_map
Setting gid map in /proc/3202/gid_map
euid: 0, egid: 65534
euid: 0, egid: 0
aufs
Mode change failed
Failed to open /proc/3202/cwd/su, error Permission denied
$ gdb ./UserNamespaceOverlayfsSetuidWriteExec
(gdb) set follow-fork-mode child 
(gdb) b 63
Breakpoint 1 at 0x400ff1: file UserNamespaceOverlayfsSetuidWriteExec.c, line 63.
(gdb) set env TRY_AUFS=1 
(gdb) run -- /bin/bash
Starting program: /home/karma/userns/UserNamespaceOverlayfsSetuidWriteExec -- /bin/bash
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
[New process 3215]
Setting uid map in /proc/3215/uid_map
Setting gid map in /proc/3215/gid_map
euid: 0, egid: 0
euid: 0, egid: 0
aufs
[Switching to process 3215]

Breakpoint 1, childFunc (arg=0x7fffffffe668)
    at UserNamespaceOverlayfsSetuidWriteExec.c:63
63	  result=chmod("su", 04777);
(gdb) n
Failed to open /proc/3215/cwd/su, error Permission denied
64	  if(result) {
(gdb) p result
$1 = -1
(gdb) n
65	    fprintf(stderr, "Mode change failed\n");
(gdb) 

(2016-01-20 追記) http://lwn.net/Articles/671641/ にこの件が載っていますね。

2015-12-02

[DIY] 一日限りの復活(?) 家を建てる日記 番外編 - 年に一度の恒例のお手入れ

実は記事数は少ないものの、このブログの一番のアクセス数を稼いでいるDIYカテゴリ。久々に帰ってきました。

むかーし家を建ててる時の現場の写真を PukiWiki に簡易的なブログ的に記録してたことがありました。その Wiki はもう全く更新していないのですが、その続編みたいな。

最近、mizzy さんの フリーランスが家を建てる日記 が更新されるたびに楽しく拝見してます。人の家でもどんどん出来上がっていくの見るのは楽しいですね。

それに触発されて、年に一回、家の一階のワックスがけをやるのですが、先日それをやった様子を書いておきます。

まずはリビング、ダイニング、キッチンのものを全て別の部屋に移動します。今流行りのミニマリストの家みたいになりました。ウチってこんなに広かったんだ!!

f:id:defiant:20151129125401j:image

これが我が家のワックスがけセット。

f:id:defiant:20151129142737j:image

「床クリーナー」をぬるま湯で薄めて、床を水拭きするわけですね。このクリーナーはワックスを剥離できるので、ワックス表面についた汚れも取れますね。ひどいところは原液を垂らしてゴシゴシすると、表面のワックスが面白いように剥がれて汚れが取れますね。床に直接膝をついて作業してると痛くなってくるので、毛布に乗ってやるわけです。毛布はリビングのテレビとか家具を移動させるときにも使いますよ。

f:id:defiant:20151129150038j:image

定規は床にこべりついたものを剥がすのに使います。剥がれたゴミはコロコロで取ります。定規をヘラのように使うと表面のワックスもはがせます。

この水拭きに数時間かかって、その後のワックスはほんの 30 分ほどで済んでしまいますね。ハイ、出来上がり。

f:id:defiant:20151129194743j:image

2015-09-14

[][][] Linux 4.3 の Process Number Controller (1)

連載書いたり、勉強会で発表したりしているとなかなかブログが更新できませんね。久々の更新です。

これを書いている時点では Linux カーネルの 4.3-rc1 がリリースされていますが、久々に cgroup に新しいコントローラが追加されそうですね。

説明が不要なほどシンプルで分かりやすいです。このブログエントリ読まなくても使えるはず。:-)

軽く使ってみました。大体は上記ドキュメントに沿って試してるだけです。

カーネル

カーネルの config では Cgroup 関連の設定以下に "PIDs cgroup subsystem" という項目が新設されているので、ここを有効にしてカーネルを作成するだけです。

General setup  --->
  Control Group support  --->
    [*] PIDs cgroup subsystem

起動すると /proc/cgroups に pids が現れます。

# cat /proc/cgroups 
#subsys_name	hierarchy	num_cgroups	enabled
cpuset	1	1	1
cpu	2	1	1
cpuacct	3	1	1
blkio	4	1	1
memory	5	1	1
devices	6	1	1
freezer	7	1	1
net_cls	8	1	1
perf_event	9	1	1
net_prio	10	1	1
hugetlb	11	1	1
pids	12	2	1
debug	13	1	1

マウント

まずはマウントしてみましょう。単一階層構造でなく、現在の仕様の cgroup でも使えるようです。

マウントは他のコントローラと同じです。例えば

# mkdir -p /sys/fs/cgroup/pids
# mount -t cgroup -o pids none /sys/fs/cgroup/pids

なかにはどんなファイルがあるかな? ルートは以下のような感じでした。

# ls /sys/fs/cgroup/pids/
cgroup.clone_children  cgroup.sane_behavior  pids.current   tasks
cgroup.procs           notify_on_release     release_agent

"pids.current" が PIDs サブシステム独自のファイルのようですね。

cgroup の作成

グループを作成してみます。ディレクトリを作ることに変わりはありません。

# mkdir /sys/fs/cgroup/pids/test01
# ls /sys/fs/cgroup/pids/test01
cgroup.clone_children  notify_on_release  pids.max
cgroup.procs           pids.current       tasks

"pids.current" と "pids.max" という 2 つのファイルがこのコントローラの独自のファイルのようですね。

制限の設定

ファイル名を見ただけで大体わかると思いますが、それぞれのファイルの役割は以下です。

pids.current
現在のプロセス(タスク)数
pids.max
許可するプロセス(タスク)数の最大値

先に作った "test01" の作成直後、つまり初期値は以下のようになっています。

# cat /sys/fs/cgroup/pids/test01/pids.current 
0
# cat /sys/fs/cgroup/pids/test01/pids.max     
max

"tasks" ファイルに何も追加していないので、当然現在値は 0 です。制限値なしにするには "pids.max" に "max" と書くようで、これが初期値です。

制限値を 2 にしてみましょう。

# echo 2 > /sys/fs/cgroup/pids/test01/pids.max
# cat /sys/fs/cgroup/pids/test01/pids.max 
2

きちんと設定されました。

現在のシェルをグループに追加します。

# echo $$ | tee /sys/fs/cgroup/pids/test01/tasks
5600
# cat /sys/fs/cgroup/pids/test01/pids.current 
2

作成後に現在値を見ると、追加したシェルと実行した cat コマンドで 2 となっていますね。では、ここで 2 つ同時にコマンドを実行してみましょう。

# ( /bin/echo "Here's some processes for you." | cat )
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
Terminated

シェルで 1 消費している状態で 2 つ実行しようとしたので、プロセスを生成できませんでした。

当たり前ですが、ちゃんと制限が効いていますね。

階層構造

上記の "test01" の子グループとして "test02" を作り、"test01" には制限をかけ、"test02" には制限をかけない状態にしてみます。

# mkdir /sys/fs/cgroup/pids/test01/test02
# echo 2 > /sys/fs/cgroup/pids/test01/pids.max 
# cat /sys/fs/cgroup/pids/test01/pids.max        (test01の制限値は2)
2
# cat /sys/fs/cgroup/pids/test01/test02/pids.max (test02は無制限)
max

以下のような設定です。

/sys/fs/cgroup/pids/
└── test01      --> (pids.max = 2)
    └── test02  --> (pids.max = max)

この状態で "test02" で "test01" の制限を超えるプロセスを起動してみます。

# echo $$ > /sys/fs/cgroup/pids/test01/test02/tasks (1 つ追加)
#  ( /bin/echo "Here's some processes for you." | cat )
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
Terminated

起動しませんね。つまり複数の階層がある場合は、上位の階層の制限にひとつでも引っかかればダメということです。

制限を設定した状態でグループのタスクを増やす

"pids.max" を設定した状態で、グループの "tasks" にたくさんプロセスを追加していったらどうなるでしょう?

# echo 2 > /sys/fs/cgroup/pids/test01/pids.max 
# cat /sys/fs/cgroup/pids/test01/pids.max 
2

"test01" に 2 の制限を設定しました。別途起動してあるプロセスを追加していってみましょう。

# echo 5954 > /sys/fs/cgroup/pids/test01/tasks 
# echo 5955 > /sys/fs/cgroup/pids/test01/tasks 
# echo 5956 > /sys/fs/cgroup/pids/test01/tasks
# 

おや、特にエラーにならずに、追加できてしまっています。

# cat /sys/fs/cgroup/pids/test01/tasks        (追加したタスクは全部存在する)
5954
5955
5956
# cat /sys/fs/cgroup/pids/test01/pids.max     (制限値は2に設定されている)
2
# cat /sys/fs/cgroup/pids/test01/pids.current (現在値は3で、制限値より多い)
3

つまり fork() や clone() で新しいプロセスを起動しようとする時にだけ制限が効くということですね。

存在するプロセス数以下の制限値を設定する

では、プロセスが既にグループに存在する状態で、その数以下に "pids.max" を設定してみます。

# echo 2 > /sys/fs/cgroup/pids/test01/pids.max (制限値は2)
# cat /sys/fs/cgroup/pids/test01/tasks 
5968
5970
# cat /sys/fs/cgroup/pids/test01/pids.current  (現在2つのタスクが存在)
2

制限値以下であるふたつのタスクが存在していますが、ここで制限値を 1 に減らしてみましょう。

# echo 1 > /sys/fs/cgroup/pids/test01/pids.max (制限値を1に下げる)
# cat /sys/fs/cgroup/pids/test01/tasks 
5968
5970
# cat /sys/fs/cgroup/pids/test01/pids.current  (タスクは2つのまま)
2

タスクは 2 つのままですね。

まとめ

Linux 4.3-rc1 環境で PIDs コントローラを試してみました。

  • 制限値を設定しておくと、グループ内で fork() や clone() で新たに起動するプロセスを制限できる
  • グループの "tasks" にプロセスを追加したり、制限値を変更したりしても、制限にひっかからない。つまり "pids.max" < "pids.current" となることもある
  • グループに属することのできるタスク数は、そのグループの上位(祖先)のグループの制限を受ける

つづくかも...

2015-07-14

[] シェルスクリプトで書かれた軽量コンテナ MINCS がすばらしい (2)

先に書いた シェルスクリプトで書かれた軽量コンテナ MINCS がすばらしい (1) - TenForwardの日記 は私もびっくりの、このブログを書き始めて以来のはてぶ数に到達しました。MINCS がすばらしいからですね :-)

こないだ書いてからも MINCS はかなり活発に開発が進んでおり、かなり変化していますね。とりあえずもう少しデフォルトの動きを見てみようと思っておっかけたメモです (このエントリはあまり参考にならない気がします ^^;)。

minc コマンドの主要な処理を行っているのは libexec にインストールされる minc-exec です。

minc-exec は最後の行で以下のように unshare コマンドを実行しています。

$IP_NETNS unshare -iumpf $0 $@

つまり unshare の引数に自身 (minc-exec) を指定しており、自身の中に以下のように新しい Namespace で起動したときの処理を記述しています。

if [ $$ -eq 1 ]; then
  :
fi

この中の処理を少し追いましょう。

マウントの伝播を private に設定

systemd は mount propagation を shared にマークしてしまうので、せっかく新しい Mount Namespace を作成しても、Mount Namespace 内のマウントが他の Namespace に伝播してしまいます。これではコンテナにならないので、まずはこれを private にしています。

mount --make-rprivate /

overlayfs マウント

次に libexec/minc-coat を呼び、overlayfs でマウントを行います。minc-coat の該当部分を見ると、

mount -t overlay -o upperdir=$UD,lowerdir=$BASEDIR,workdir=$WD overlayfs $RD

こんな感じにマウントしています。コンテナ用のディレクトリはあらかじめ mktemp -d /tmp/minc$$-XXXXXX のように作られており、この下に upperdir, workdir, コンテナ用root を作成します。

特にオプションを指定せずに実行すると以下のようなマウントを実行します (minc1149-aEYmn0 は mktemp -d /tmp/minc$$-XXXXXX で作成したディレクトリ)。

mount -t overlay -o upperdir=/tmp/minc1149-aEYmnO/storage,lowerdir=/,workdir=/tmp/minc1149-aEYmnO/work overlayfs /tmp/minc1149-aEYmnO/root

つまり

  • overlayfs の upperdir は "/tmp/minc$$-XXXXXX/storage"
  • overlayfs の lowerdir は /
  • overlayfs の workdir は "/tmp/minc$$-XXXXXX/work"
  • overlayfs をマウントするディレクトリは "/tmp/minc$$-XXXXXX/root"

という感じになります。

ホスト名の設定

コンテナ名が指定されていたり、コンテナ内にコンテナ名を指定するファイルがあれば設定します。もちろん UTS Namespace が作成されているので、ホストには影響ありません。

  if [ "$MINC_UTSNAME" ]; then
    hostname $MINC_UTSNAME
    echo $MINC_UTSNAME > $MINC_TMPDIR/utsname
  elif [ -f $MINC_TMPDIR/utsname ]; then
    hostname `cat $MINC_TMPDIR/utsname`
  fi

/dev

"--usedev" オプションを指定しなければコンテナ内の /dev (/tmp/minc$$-XXXXXX/root/dev) は tmpfs でマウントされます。

そしてコンテナの /dev/pts はコンテナ専用に独立した /dev/pts としてマウントされます。ホストの /dev/pts を bind mount したりするとホストとコンテナの devpts が共通化されてしまい、コンテナから出力したらホストに出たりしてまずいので。この辺りはカーネル付属の filesytems/devpts.txt をどうぞ。

mkdir $RD/dev/pts
mount devpts -t devpts -onoexec,nosuid,gid=5,mode=0620,newinstance,ptmxmode=0666 $RD/dev/pts
ln -s /dev/pts/ptmx $RD/dev/ptmx

あとは適当に必要なデバイスファイルを bind mount します。

/proc

この時点ではホストの /proc は見えていますので、これを読み込み専用でマウントします。そしてコンテナ用の /proc をマウントします。以下のような処理を行っています。

mount -t proc -o ro,nosuid,nodev,noexec proc /proc
mount -t proc -o rw,nosuid,nodev,noexec,relatime proc $RD/proc

以上を実行する前の状態だと /proc は以下のような状態になっており、ホストのプロセスの状態が見えています (プロセスがたくさんあることからわかります)。

# ls -1F /proc
1/
10/
1019/
11/
12/
1200/
1201/
125/
13/
132/
134/
135/
137/
139/
  :()

処理後は /proc も以下のようになります。この例は、処理途中の様子を見るためにbashを実行しているので PID 1 の minc-exec の他に bashlsプロセスが存在しています。

# ls -1F  /proc
1/
36/
50/
  :()

この後に /proc 以下のセキュリティ的にヤバそうなファイルや /sys を bind mount しています。さきほどの 2 行の proc のマウントで /proc は読み込み専用でマウントしていましたので、bind mount するとこれらは読み込み専用になります。うまく考えられてますね。:-)

/ の変更

この後いよいよ pivot_root で / を変更します。

cd $RD
mkdir -p .orig
pivot_root . .orig
grepumount -e "^/\.orig/"

"pivot_root . .orig" で /tmp/minc$$-XXXXXX/root を新しい / にして、元を .orig にマウントします。この pivot_root 直後の状態は .orig 以下のものがたくさんマウントされた状態です。

# cat /proc/mounts 
/dev/mapper/vivid--vg-root /.orig ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
udev /.orig/dev devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
devpts /.orig/dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
tmpfs /.orig/dev/shm tmpfs rw,nosuid,nodev 0 0
hugetlbfs /.orig/dev/hugepages hugetlbfs rw,relatime 0 0
mqueue /.orig/dev/mqueue mqueue rw,relatime 0 0
tmpfs /.orig/run tmpfs rw,nosuid,noexec,relatime,size=101696k,mode=755 0 0
  :()
sysfs /.orig/sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
securityfs /.orig/sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /.orig/sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0
cgroup /.orig/sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
  :()
pstore /.orig/sys/fs/pstore pstore rw,nosuid,nodev,noexec,relatime 0 0
fusectl /.orig/sys/fs/fuse/connections fusectl rw,relatime 0 0
debugfs /.orig/sys/kernel/debug debugfs rw,relatime 0 0
proc /.orig/proc proc rw,nosuid,nodev,noexec,relatime 0 0
systemd-1 /.orig/proc/sys/fs/binfmt_misc autofs rw,relatime,fd=35,pgrp=0,timeout=300,minproto=5,maxproto=5,direct 0 0
/dev/vda1 /.orig/boot ext2 rw,relatime 0 0
overlayfs / overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1718-shaYgx/storage,workdir=/tmp/minc1718-shaYgx/work 0 0
tmpfs /dev tmpfs rw,relatime 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
proc /.orig/proc proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

この状態で .orig をアンマウントしたいところですが、実はそれは以下のように一部失敗します。overlayfs の lowerdir を元の / にしているからですね。

# cut -f 2 -d " " < /proc/mounts | grep -e "^/\.orig/" | sort -r | xargs umount
umount: /.orig/proc/sys/fs/binfmt_misc: not mounted
umount: /.orig/proc: target is busy
        (In some cases useful info about processes that
         use the device is found by lsof(8) or fuser(1).)
# cat /proc/mounts
/dev/mapper/vivid--vg-root /.orig ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
proc /.orig/proc proc rw,nosuid,nodev,noexec,relatime 0 0
systemd-1 /.orig/proc/sys/fs/binfmt_misc autofs rw,relatime,fd=35,pgrp=0,timeout=300,minproto=5,maxproto=5,direct 0 0
overlayfs / overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1718-shaYgx/storage,workdir=/tmp/minc1718-shaYgx/work 0 0
tmpfs /dev tmpfs rw,relatime 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

/.orig とかマウントされたままです。umount はできませんでしたので、.orig 内に移動して再度 pivot_root します。

cd /.orig/
pivot_root . dev/

この2度目の pivot_root 直後はこんなです。

# cat /proc/mounts 
/dev/mapper/vivid--vg-root / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
  :()
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
  :()
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
  :()
/dev/vda1 /boot ext2 rw,relatime 0 0
overlayfs /dev overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1814-ESDwdP/storage,workdir=/tmp/minc1814-ESDwdP/work 0 0
tmpfs /dev/dev tmpfs rw,relatime 0 0
devpts /dev/dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/dev/mqueue mqueue rw,relatime 0 0
proc /proc proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /dev/sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

この状態で /dev は元の / がマウントされていて、他に余計なものが見えない状態ですので chroot /dev して、ここを / にします。

RD=dev/
  :
exec $MINC_DEBUG_PREFIX chroot $OPT $RD $@

これで minc コマンドで指定されたコマンドが Namespace 内でコンテナ用の新しい / 以下で実行されますね。

と、ほとんど私が複数回 pivot_root 行われているけど、それぞれどういう意味だ? とおっかけた個人メモ的なわかりづらいエントリになってしまいました。

minc --debug /bin/bash

とやって実際どういうコマンドが実行されているのか観察したり、実際にやってることをおっかけたりすると良いと思いますよ。

2015-07-07

[][] aufs を使った一般ユーザ権限で起動するコンテナ

LXC ではコンテナのクローンを行う際に色々なストレージバックエンドの特徴を生かしたスナップショットクローンを行えます。この辺りは 連載の第 19 回〜 22 回 辺りで詳しく解説しています。

今まで、非特権LXCコンテナでストレージバックエンドの特徴を生かしたスナップショットクローンは、btrfs か overlayfs でしか行えませんでした。

先日、私の送ったパッチで aufs を使ったスナップショットクローンが一般ユーザ権限でもできるようになりました。

前者は liblxc 側の変更で lxc-clone や lxc-start 側で関係するパッチ、後者は lxc-start-ephemeral コマンドに関係するパッチです。

これで一般ユーザでも aufs を使った非特権クローンとコンテナが起動できるようなりました。簡単に紹介しておきます。

Plamo 5.3.1 に aufs パッチを当てた 4.1.1 カーネルを使っています。

$ lsb_release -d
Description:	Plamo Linux release 5.3.1
$ uname -r
4.1.1-plamo64-aufs

最近の aufs では allow_userns というモジュールオプションを Y にすると User Namespace 内の特権ユーザが aufs をマウントできるようになります。マニュアルにも記載があります。

私も手元では以下のように設定しています。

$ cat /etc/modprobe.d/aufs.conf 
options aufs allow_userns=1

こんな一般ユーザで操作しています。

$ id
uid=1000(karma) gid=100(users) groups=100(users),26(audio),28(dialout),29(video),32(cdrom),36(kvm),38(pulse),39(pulse-access),44(mlocate),47(libvirt),60(docker),1000(sudo)

まず、普通に dir バックエンドを使ったコンテナを作成します。以下がその config の rootfs の設定。

$ grep lxc.rootfs ~/.local/share/lxc/ct01/config 
lxc.rootfs = /home/karma/.local/share/lxc/ct01/rootfs

このコンテナのクローンを作成します。

$ lxc-clone -o ct01 -n aufs01 -s -B aufs
Created container aufs01 as snapshot of ct01
$ lxc-ls -f
NAME    STATE    IPV4  IPV6  GROUPS  AUTOSTART  
----------------------------------------------
aufs01  STOPPED  -     -     -       NO         
ct01    STOPPED  -     -     -       NO         

クローンは成功しています。

$ grep lxc.rootfs ~/.local/share/lxc/aufs01/config
lxc.rootfs = aufs:/home/karma/.local/share/lxc/ct01/rootfs:/home/karma/.local/share/lxc/aufs01/delta0

こんな感じに aufs を使うコンテナのルートファイルシステムの定義がされています。

起動してみます。Plamo では cgmanager とか systemd-logind とかないので、起動前には自分で一般ユーザ権限の cgroup を作成して、現在のシェルの PID を登録してから起動しています。

$ lxc-start -n aufs01 -d
$ lxc-ls -f
NAME    STATE    IPV4          IPV6  GROUPS  AUTOSTART  
------------------------------------------------------
aufs01  RUNNING  10.0.100.179  -     -       NO         
ct01    STOPPED  -             -     -       NO         

無事起動しましたね。

なぜ今 aufs ?

以上が動きと機能の紹介でした。でも、overlayfs がカーネルにマージされた今、なぜ aufs なのか? ってところですが...

色々なファイルシステムを扱う場合は特権が必要となります。例え User Namespace を使った Namespace 内の特権であっても、ファイルシステムをマウントできなかったりします。

User Namespace 内の特権ユーザがファイルシステムをマウントするには、カーネル内でファイルシステムフラグに "FS_USERNS_MOUNT" というフラグが指定されている必要があります。(参考: overlayfs と LXC 非特権コンテナの snapshot によるクローン - TenForwardの日記)

このフラグ、4.1.1 カーネルで指定されているファイルシステムを調べてみると

$ find fs/ -type f | xargs grep FS_USERNS_MOUNT
fs/devpts/inode.c:	.fs_flags	= FS_USERNS_MOUNT | FS_USERNS_DEV_MOUNT,
fs/proc/root.c:	.fs_flags	= FS_USERNS_MOUNT,
fs/ramfs/inode.c:	.fs_flags	= FS_USERNS_MOUNT,
fs/sysfs/mount.c:	.fs_flags	= FS_USERNS_MOUNT,
fs/namespace.c:		if (!(type->fs_flags & FS_USERNS_MOUNT)) {

最後の (fs/namespace.c) は関係ないので devpts, proc, ramfs, sysfs だけです。この辺りはマウントできないとそもそも非特権のシステムコンテナが起動できなかったりするので指定されていて当然という気がしますね。つまり普通のファイルシステムはこれがそもそも指定されていないわけです。

overlayfs もこのフラグは指定されていません。なのに LXC で非特権 overlayfs がサポートされているのはなぜか? という話ですが、これは Ubuntuカーネルパッチが当たっているからです。パッチがあたっていないバニラカーネルだと当然これは失敗します。

$ lxc-clone -o ct01 -n overlay01 -s -B overlayfs
clone failed

私の使っている 4.1.1 の overlayfs には特に何の変更も加えていないので、このように失敗します。

一方、aufs はカーネルにはマージされていないものの、最新の aufs が使える環境では素の aufs で User Namespace 内の特権ユーザが aufs をマウントできるわけです。この辺りからでしょうか。

まあ、パッチの量の大小の差はあるにせよ、overlayfs も aufs もバニラカーネルパッチを当てないと非特権コンテナでは使えないわけで、なら aufs の非特権コンテナサポートを追加することで少しでも非特権コンテナのサポートする範囲が広がれば良いかなと思ってパッチを作りました。

あ、ちなみに Ubuntuカーネルに当たっている aufs は古いバージョンなので、allow_userns は使えない模様です。Ubuntu は aufs 止めちゃうみたいですものね。

ま、LXC の overlayfs 周りとかは以前もパッチ送ったことがあってコードをよく知ってたので、非特権 aufs 対応はさほど難しくないって分かってたから作ったんですけどね。(^_^;)