TenForward

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

Linux 4.14 で導入された Namespaced file capabilities(おまけ)

4.14 で設定した Namespaced file capability を 4.13 以前のカーネルに戻したら

まあ、こんな物好きなことをする人はいないでしょうが、確認してみました。

tenforward.hatenablog.com

の環境のまま、ホスト環境のカーネルを 4.13 に戻して、コンテナを起動し、コンテナ内で一般ユーザーとなって pingping2 コマンドを両方とも実行しました。

$ ./ping www.google.com
-su: ./ping: Numerical result out of range
$ ./ping2 www.google.com
-su: ./ping2: Numerical result out of range

構造体が変わっていたので読み取れないようですね。

Namespaced file capability に設定された id で User Namespace 外から実行してみる

tenforward.hatenablog.com

で、User Namespace 内の root の id が 200000 に設定されていたので、コンテナ内で設定した File capability も 200000 という情報が記録されていました。そこで User Namespace 内の root と同じ id を持つホスト上のユーザーから、file capability を設定したコマンドを実行してみました。

今回は unshare コマンドを使います。

  • ホスト上の uid: 1000 のユーザーを User namespace 内の rootマッピング

ちょっと色々試行錯誤しながら試したので変なことしてるようだったら教えてください。

ホスト上のユーザーは 1000:100 です。ここで unshare します。

$ id -u
1000
$ id -g
100
$ unshare --user --setgroups allow
nobody@borg:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

マッピングがまだできていないので、この時点では User namespace 内では nobody ユーザーになります。

ここで別のシェルからホスト上の rootマッピングを投入します。(Plamo なので gid がちょっと普通と異なるかな)

# echo "0 1000 65534" > /proc/22471/uid_map 
# echo "0 100 65534" > /proc/22471/gid_map

ここで元の User namespace 内のシェルに戻ると、無事 root になりました。

$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

これで他の namespace が作れますので、Network namespace を作ります(これを作らないと file capability 設定しても ping 実行できなかったので)。

$ unshare --net
# ip link set lo up
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever

ここで一般ユーザーになります。ping コマンドをコピーして実行してみます。当然失敗しますね。

$ su karma
$ id -u
1000
$ cd /tmp
$ cp /bin/ping .
$ ls -l ping
-rwxr-xr-x 1 karma users 56,920  32401:02 ping*
$ ./ping 127.0.0.1
ping: socket: Operation not permitted
$ exit

ここで User namespace 内の root で file capability を設定します。

# setcap cap_net_raw+ep /tmp/ping 
# getcap -n /tmp/ping 
/tmp/ping = cap_net_raw+ep [rootid=1000]

id:1000 で Namespaced file capability が設定されています。ここで再度一般ユーザーになります。

# su - karma
$ /tmp/ping -c 1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.012 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.012/0.012/0.012/0.000 ms

User namespace 内で実行できましたね。ここでおもむろに namespace 外のシェルから、id:1000 のユーザーでこの ping を実行してみます。

$ id -u
1000
$ /tmp/ping 127.0.0.1
ping: socket: Operation not permitted

実行はできませんね。

Linux 4.14 で導入された Namespaced file capabilities(2)

User Namespace 外から Namespaced file capability を設定する

前回、

tenforward.hatenablog.com

では、非特権ユーザで起動したコンテナ内の root ユーザーで File capability を設定しました。

ここで、自作コンテナ派の皆様が気になるのは、この capability をコンテナ外から設定するのはどうしたらいいのだろう? というところではないかと思います。コンテナランタイム側からコンテナ内の capability を設定する必要があるかもしれません。

もちろん、これも簡単にできます。先に getcap コマンドに -n オプションを付与しましたが、これと同じオプションが setcap にも存在します。

setcap -n 200000 cap_net_raw+ep ./ping

と言ったように、User Namespace 内の root の uid を指定して capability を指定します。

まず、コンテナ内からアクセスできるように、ホスト環境上で ping コマンドをコンテナの rootfs 内にコピーします。LXC の非特権コンテナは次のようなディレクトリ以下に rootfs が置かれますので、ここにコピーします。ping2 という名前でコピーしました。

$ sudo cp /bin/ping ~/.local/share/lxc/c3/rootfs/home/karma/ping2

次にコピーした ping2 バイナリに対して setcap します。

$ sudo setcap -n 200000 cap_net_raw+ep ~/.local/share/lxc/c3/rootfs/home/karma/ping2 (-nオプションでroot uidを指定)
$ sudo getcap -n ~/.local/share/lxc/c3/rootfs/home/karma/ping2
/home/karma/.local/share/lxc/c3/rootfs/home/karma/ping2 = cap_net_raw+ep [rootid=200000]
$ sudo getfattr -d -m "security" ~/.local/share/lxc/c3/rootfs/home/karma/ping2
getfattr: Removing leading '/' from absolute path names
# file: home/karma/.local/share/lxc/c3/rootfs/home/karma/ping2
security.capability=0sAQAAAwAgAAAAAAAAAAAAAAAAAABADQMA

設定されましたね。

再度コンテナ内に入ってみましょう。

$ lxc-attach c3
# su - karma
$ ls -l ./ping*
-rwxr-xr-x 1 karma  users   56,920  2月 21日  16:30 ./ping*
-rwxr-xr-x 1 nobody nogroup 56,920  2月 21日  16:39 ./ping2*

ホスト側の権限でコピーしていますので、ファイルの所有権が nobody:nogroup になっていますね。

$ sudo getcap -n ./ping2
./ping2 = cap_net_raw+ep [rootid=200000]

capability を確認すると、設定されているのがわかります。では、実行してみましょう。

$ ./ping2 -c 1 www.google.com
PING www.google.com (216.58.196.228) 56(84) bytes of data.
64 bytes from kix06s01-in-f228.1e100.net (216.58.196.228): icmp_seq=1 ttl=49 time=1.22 ms

--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.226/1.226/1.226/0.000 ms

実行できました。

Namespace 外からでも問題なく設定できますね。

kernel のデータ構造の変更

ここからは素人なので間違ってる可能性あります。詳しくはご自分でどうぞ。

4.13 までは include/uapi/linux/capability.h を見ると、以下のような構造体がありました。

#define VFS_CAP_FLAGS_EFFECTIVE 0x000001

  : (snip)

#define VFS_CAP_REVISION_2 0x02000000

struct vfs_cap_data {
    __le32 magic_etc;            /* Little endian */
    struct {
        __le32 permitted;    /* Little endian */
        __le32 inheritable;  /* Little endian */
    } data[VFS_CAP_U32];
};

https://elixir.bootlin.com/linux/v4.13/source/include/uapi/linux/capability.h#L53 付近)

この permittedinheritable に必要な capability を設定するわけですね。effective セットは file capability の場合はオンオフだったと思いますので、magic_etc に設定します。

effective セットを設定する場合、これ(4.13)までは、

struct vfs_cap_data xattr;

xattr.magic_etc = VFS_CAP_REVISION_2 | VFS_CAP_FLAGS_EFFECTIVE;

のように設定したようです。

4.14 以降で Namespaced file capability が導入されたあとは、

#define VFS_CAP_REVISION_2  0x02000000
#define VFS_CAP_U32_2           2
#define XATTR_CAPS_SZ_2         (sizeof(__le32)*(1 + 2*VFS_CAP_U32_2))

#define VFS_CAP_REVISION_3 0x03000000
#define VFS_CAP_U32_3           2
#define XATTR_CAPS_SZ_3         (sizeof(__le32)*(2 + 2*VFS_CAP_U32_3))

#define XATTR_CAPS_SZ           XATTR_CAPS_SZ_3
#define VFS_CAP_U32             VFS_CAP_U32_3
#define VFS_CAP_REVISION   VFS_CAP_REVISION_3

struct vfs_cap_data {
    __le32 magic_etc;            /* Little endian */
    struct {
        __le32 permitted;    /* Little endian */
        __le32 inheritable;  /* Little endian */
    } data[VFS_CAP_U32];
};

/*
 * same as vfs_cap_data but with a rootid at the end
 */
struct vfs_ns_cap_data {
    __le32 magic_etc;
    struct {
        __le32 permitted;    /* Little endian */
        __le32 inheritable;  /* Little endian */
    } data[VFS_CAP_U32];
    __le32 rootid;
};

https://elixir.bootlin.com/linux/v4.14/source/include/uapi/linux/capability.h#L64 付近)

というように struct vfs_ns_cap_data という、vfs_cap_data の最後に rootid が追加された構造体が追加されています。そう、これが setcapgetcap で追加された -n オプションの正体ですね(たぶん)。

それと同時に、従来の Namespaced されていない file capability なのか、Namespaced な file capability なのかは、magic_etc に指定する file capability の Revision で指定します。

先のコードと対応する例だと、

struct vfs_ns_cap_data xattr;

xattr.magic_etc = VFS_CAP_REVISION_3 | VFS_CAP_FLAGS_EFFECTIVE;

となるわけですね(たぶん)。

(つづく)

tenforward.hatenablog.com

Linux 4.14 で導入された Namespaced file capabilities(1)

Hosting Casual Talks #5

本題に入る前に、このブログエントリのネタを発表してきた勉強会の感想を。

ホスティング業界に関わるエンジニアが カジュアルに 情報交換をするという、全員発表型の勉強会 "Hosting Casual Talks #5" に参加してきました。飲みながら技術について情報交換できる カジュアルな 勉強会ではありますが、内容が全く カジュアルじゃない!!

connpass.com

ひとりあたり10 or 15分の LT サイズの時間割り当てなのですが、皆さんちゃんと解決したい問題があり、仮説を立て、検証して、考察すると言ったストーリー性のあるスライドになっていたり、問題を解決するためにコード書きました、こういうの作りましたというような、とても LT サイズに収まるものではなく、全員持ち時間はどこへやら、ガッツリと 6 時間、ガチな内容がひたすら続くなかなか内容の濃い勉強会になりました。

で、そこで真面目に 10 分サイズで軽めに作ったネタがコレでした。一発芸でオチもなく、Linux カーネルの機能を紹介してきました。次々とすごいガチな発表がされるので、こんなネタで発表するのが徐々に辛くなって…😅


4.14 kernel はいくつかコンテナ関連の機能が追加されているのですが、その中のひとつである "Namespaced file capabilities" の紹介です。

"Namespaced file capabilities" 機能についてはこのあたりが詳しいです。

コミットはこちら

File Capability

File Capability については色々と参考になる情報がありますので、そちらをどうぞ。

ここでは File Capability の説明あるあるの ping コマンドに cap_net_raw を与えて色々試してみます。

File Capability お試し

まずは普通に。ここでは Linux 4.13.16 上の Plamo Linux 7.0 で試しています。libcap は 2.26。

通常は setuid されている ping をコピーすると setuid は落とされて ping は一般ユーザーでは実行できません。

$ id -u
1000 (一般ユーザーです)
$ ls -l /bin/ping
-rwsr-sr-x 1 root root 56,920  2月 12日  20:31 /bin/ping* (setuid されています)
$ cp /bin/ping . (コピーする)
$ ls -l ./ping   (setuid は落ちています)
-rwxr-xr-x 1 karma users 56,920  3月 23日  23:35 ./ping*
$ ./ping www.google.com (setuid されていないので権限がなく実行できません)
ping: socket: Operation not permitted

ここで file capability を設定すると一般ユーザーでも限定的な権限が与えられ、実行できるようになります。

$ sudo setcap cap_net_raw+ep ./ping (cap_net_raw を与えます)
$ sudo getcap -n ./ping (cap_net_raw が設定されています)
./ping = cap_net_raw+ep
$ sudo getfattr -d -m "security" ./ping (拡張属性領域の確認。File Capability はここに保存されています)
# file: ping
security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=

これで一般ユーザーでも ping が実行できます。

$ ./ping -c 1 www.google.com
PING www.google.com(kix05s01-in-x04.1e100.net (2404:6800:400a:808::2004)) 56 data bytes
64 bytes from kix05s01-in-x04.1e100.net (2404:6800:400a:808::2004): icmp_seq=1 ttl=51 time=7.08 ms

--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 7.087/7.087/7.087/0.000 ms

コンテナ内で File Capability を設定してみる(4.13.16 kernel)

これと同じ操作を一般ユーザー権限で実行しているコンテナ内で行ってみます。次のように root が uid: 200000 のユーザーにマッピングされているコンテナがあります。

karma@plamo71:~$ lxc-info c3
Name:           c3
State:          RUNNING
PID:            20569
IP:             10.0.3.122
CPU use:        0.17 seconds
BlkIO use:      12.45 MiB
Memory use:     16.59 MiB
KMem use:       1.89 MiB
Link:           vethBXNXMY
 TX bytes:      1.55 KiB
 RX bytes:      9.86 KiB
 Total bytes:   11.41 KiB
karma@plamo71:~$ sudo cat /proc/20569/{u,g}id_map
         0     200000      65536
         0     200000      65536
karma@plamo71:~$ ps auxf 20569
  : (snip)
200000   20569  0.0  0.0   4500  1600 pts/1    Ss+  18:38   0:00  \_ init [3]
200000   20772  0.0  0.1 172404  2488 ?        Ssl  18:38   0:00      \_ /sbin/rsyslogd -i /run/rsyslogd.pid
  : (snip)

このコンテナ内に入り、一般ユーザー権限で ping をコピーしてみます。当然実行はできません。

$ lxc-attach c3
# su - karma
$ id
uid=1000(karma) gid=100(users) groups=100(users),10(wheel),26(audio),28(dialout),29(video),32(cdrom),36(kvm),38(pulse),39(pulse-access),44(mlocate),47(libvirt),50(input)
$ uname -a
Linux c3 4.13.16-plamo64 #1 SMP PREEMPT Wed Dec 5 15:00:28 JST 2018 x86_64 GNU/Linux
$ cp /bin/ping .
$ ls -l ./ping 
-rwxr-xr-x 1 karma users 56,920  2月 20日  19:02 ./ping*
$ ./ping -c 1 www.google.com
ping: socket: Operation not permitted

ここで capability を設定してみましょう。

$ sudo setcap cap_net_raw+ep ./ping 
Failed to set capabilities on file `./ping' (Operation not permitted)
usage: setcap [-q] [-v] [-n <rootid>] (-r|-|<caps>) <filename> [ ... (-r|-|<capsN>) <filenameN> ]

 Note <filename> must be a regular (non-symlink) file.

エラーで setcap コマンドが実行できません。

4.14 以降のカーネルの コンテナ内で File Capability を設定してみる(4.20.10 kernel)

勉強会のときは 5.0.0 カーネルで試したデモを披露しましたが、ここでは 4.20.10 で試した結果です。

ちなみに 4.14 より前のカーネルを試そうと思って Ubuntu 16.04 の 4.4 kernel なら試せる!と思ったりすると、普通にこの機能はバックポートされていますのでご注意ください。つまりUbuntu 16.04 の 4.4 kernel でもこの機能を試せます。(←ハマった人)(2019-03-24 追記)

$ uname -a
Linux plamo71 4.20.10-plamo64 #5 SMP PREEMPT Mon Feb 18 16:16:45 JST 2019 x86_64 GNU/Linux
$ lxc-start c3
$ lxc-ls -f c3
NAME STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
c3   RUNNING 0         -      10.0.3.122 -    true         
$ lxc-info c3
Name:           c3
State:          RUNNING
PID:            4817
IP:             10.0.3.122
CPU use:        0.17 seconds
BlkIO use:      12.55 MiB
Memory use:     16.97 MiB
KMem use:       2.16 MiB
Link:           vethH3LNCN
 TX bytes:      1.28 KiB
 RX bytes:      9.31 KiB
 Total bytes:   10.59 KiB
$ sudo cat /proc/4817/{u,g}id_map
         0     200000      65536
         0     200000      65536
$ ps auxf | less
  : (snip)
200000    4817  0.0  0.0   4500  1632 pts/1    Ss+  16:24   0:00  \_ init [3]
200000    5019  0.0  0.1 172404  2472 ?        Ssl  16:24   0:00      \_ /sbin/rsyslogd -i /run/rsyslogd.pid
  : (snip)

カーネルのバージョンが違う以外は同じコンテナを実行しています。

$ lxc-attach c3
# su - karma
$ id
uid=1000(karma) gid=100(users) groups=100(users),10(wheel),26(audio),28(dialout),29(video),32(cdrom),36(kvm),38(pulse),39(pulse-access),44(mlocate),47(libvirt),50(input)
$ uname -a
Linux c3 4.20.10-plamo64 #5 SMP PREEMPT Mon Feb 18 16:16:45 JST 2019 x86_64 GNU/Linux
$ cp /bin/ping .
$ ls -l ./ping
-rwxr-xr-x 1 karma users 56,920  2月 21日  16:30 ./ping*
$ ./ping www.google.com
ping: socket: Operation not permitted

当然ですが、ここまでは 4.13 カーネルのときと同じです。ここで 4.13 では失敗した setcap を試してみましょう。

$ sudo setcap cap_net_raw+ep ./ping
$ sudo getcap -n ./ping 
./ping = cap_net_raw+ep [rootid=200000]
$ sudo getfattr -d -m "security" ./ping 
# file: ping
security.capability=0sAQAAAwAgAAAAAAAAAAAAAAAAAABADQMA

4.13 では失敗した setcap が成功しています。ここで getcap-n を与えている所がミソで、これは結果のうち [rootid=200000] を表示させるために使っています。この 200000 は、コンテナの rootマッピングされている uid ですね。

つまり user namespace 内で setcap を実行すると、user namespace 内の root の uid の情報が設定されます。そして、その user namespace 内でのみ、その File capability が有効になり、権限が与えられた状態でコマンドが実行できます。

$ ./ping -c 1 www.google.com
PING www.google.com (216.58.197.4) 56(84) bytes of data.
64 bytes from kix06s02-in-f4.1e100.net (216.58.197.4): icmp_seq=1 ttl=48 time=1.30 ms

--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.303/1.303/1.303/0.000 ms

問題なく実行できていますね。

上記のファイルをコンテナの外で確認してみましょう。

$ id
uid=1000(karma) gid=100(users) groups=100(users),10(wheel),26(audio),28(dialout),29(video),32(cdrom),36(kvm),38(pulse),39(pulse-access),44(mlocate),47(libvirt),50(input)
$ sudo getcap -n ~/.local/share/lxc/c3/rootfs/home/karma/ping
/home/karma/.local/share/lxc/c3/rootfs/home/karma/ping = cap_net_raw+ep [rootid=200000]
$ ~/.local/share/lxc/c3/rootfs/home/karma/ping -c 1 www.google.com
ping: socket: Operation not permitted

コンテナ外で確認しても getcap の情報は変わりません。そしてコンテナ(user namespace)外から実行すると、設定したはずの capability は有効ではなく、ping は実行できません。

(つづく)

tenforward.hatenablog.com

Linux 4.14 で導入された cpuset の cgroup v2 mode

(2019-02-23 少し追記しています)

4.14 kernel から cgroup の cpuset コントローラーに cpuset_v2_mode というオプションが使えるようになりました。

しばらく見ないうちに cpuset コントローラーをマウントするとファイルがたくさん増えていますが、このオプションを設定すると、従来からある cpuset.{cpus,mems} ファイルの動きが変わるようです。そしていつの間にか増えている cpuset.effective_{cpus,mems} ファイルと合わせて使って cpuset の制限を行うようです。

このオプションを設定したときの動きが、cgroup v2 の cpuset の動きになるようです(v2 の cpuset は現時点ではまだマージされていません)。

現在使える cpu や memory が effective で表示されており、それと cpuset.{cpus,mems} のマスクを取って、使える cpu, memory を導き出すようですね。

従来の cpuset(v1 mode)

# mount -t tmpfs cgroup /sys/fs/cgroup/
# mkdir /sys/fs/cgroup/cpuset
# mount -t cgroup -o cpuset cgroup /sys/fs/cgroup/cpuset/
# cd /sys/fs/cgroup/cpuset/
# mkdir test01
# cd test01
# echo "0-1" > cpuset.cpus
# echo "0" > cpuset.mems

この状態で test01 cgroup 内のファイルは、

# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0-1
cpuset.effective_cpus: 0-1

となっています。ここでおもむろに cpu をひとつオフラインにします。cpu1 です。

# echo 0 > /sys/devices/system/cpu/cpu1/online

すると、

# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0
cpuset.effective_cpus: 0

cpuset.cpuscpuset.effective_cpus0 になります。

ここで再度 cpu1 をオンラインにすると、

echo 1 > /sys/devices/system/cpu/cpu1/online
# cat /sys/devices/system/cpu/cpu1/online
1
# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0
cpuset.effective_cpus: 0

特に変化はありません。

cpuset_v2_mode 付きマウント(v2 mode)

マウントオプションとして cpuset_v2_mode を与えます。

# mount -t cgroup -o cpuset,cpuset_v2_mode cgroup /sys/fs/cgroup/cpuset/
# cat /proc/self/mounts | grep cpuset
cgroup /sys/fs/cgroup/cpuset cgroup rw,relatime,cpuset,cpuset_v2_mode 0 0

この状態では、さきほどと特に変化はありません。

# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0-1
cpuset.effective_cpus: 0-1

ここで cpu1 をオフラインにします。

# echo 0 > /sys/devices/system/cpu/cpu1/online

すると、

# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0-1
cpuset.effective_cpus: 0

cpuset.cpus はそのままで実際の cpuset.effective_cpus のみ値が変わります。

ここまで来ると動きは予想できますね。再度 cpu1 をオンラインにしてみます。

# echo 1 > /sys/devices/system/cpu/cpu1/online
# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0-1
cpuset.effective_cpus: 0-1

cgroup作成後にCPUが増えた時(2019-02-23 追記)

cgroup作成後にcpuが減って、また戻った場合の動きは見ましたが、cgroup作成後にonline の cpu が増えた場合にどうなるのかが気になったので試してみました。

# echo 0 > /sys/devices/system/cpu/cpu2/online
# mkdir test01
# cd test01
# cat cpuset.cpus

# cat cpuset.effective_cpus 
0-1
# echo "0-1" > cpuset.cpus 
# echo "0" > cpuset.mems 
# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0-1
cpuset.effective_cpus: 0-1
# echo 1 > /sys/devices/system/cpu/cpu2/online
# for f in $(ls cpuset.*cpus); do echo -n "$f: "; cat $f; done
cpuset.cpus: 0-1
cpuset.effective_cpus: 0-1

上の例のように CPU をひとつ(cpu2) offline にしておいて、cgroup を作成後、cpu2 を online にしても cpuset.effective_cpus の値は変化しません。

まとめ

CPU/メモリーの online, offline を切り替えると:

  • v1 mode: cpuset.{cpus,mems}cpuset.effective_{cpus,mems} の値も変化するが、その後再度状態が変わってもそのまま
  • v2 mode: cpuset.{cpus,mems} の値は変化しない。cpuset.effective_{cpus,mems} の値は実際に合わせて変化する

ただし、cpuset.effective_* の復元は cgroup 作成時の値まで。

おまけ(2019-02-23 追記)

ちなみにこのオプションが使えるようになったコミットは次のものですが、これはオプションの追加と、そのオプションが指定されているときの条件分岐を変更しているだけですので、処理自体はこの前にあらかじめ実装されているようですね。

Plamo 7.0 上で非特権な systemd コンテナを起動する

Plamo 7.0 では cgroupfs_mount パッケージと lxc パッケージを提供していますが、非特権コンテナを起動するための調整は特に行っていませんでした。

cgroupfs_mount パッケージは、Plamo 7.0 リリース直後は Ubuntu 14.04 あたりで cgroupfs をマウントするのに使われていた cgroup-lite パッケージを元にした cgroupfs-mount をそのまま入れていました。これはシェルスクリプトで、システム上でサポートされているコントローラに対応したディレクトリを /sys/fs/cgroup に作成し、それぞれコントローラをオプションに指定してマウントするだけのシンプルなスクリプトです。

これで非特権コンテナは起動しますが、コンテナ内の systemd がちゃんと起動しません。今回はコンテナは Ubuntu bionic のイメージを使っています。

誰得?な記事ですが、非 systemd 環境でコンテナを起動する場合は参考になるかもしれません(←やっぱり誰得😂)

非特権コンテナを起動する準備

非特権の lxc コンテナを起動するためには、連載の第17回 で書いた方法と特に変わっていません。しかし一部の設定項目名が変わったりしています。

まずはコンテナを起動したいユーザが使えるサブIDを登録します。ここでは plamo ユーザーとします。

$ sudo usermod -v 200000-299999 -w 200000-299999 plamo

記事では、システム上で使う 65536 個のサブIDを使えるようにしていましたが、今の実装ではちょうどだと「足りない」とエラーが出るようです(詳しい原因は調べてません)。これは LXD のときも同じです。ですので実際に使う以上に余裕を持ってサブIDを確保しておきましょう。

非特権コンテナで veth インターフェースを使うために /etc/lxc/lxc-usernet ファイルにエントリを作成します。これはエディタで直接編集。

$ cat /etc/lxc/lxc-usernet 
plamo   veth    lxcbr0  10

個人用の lxc 設定をコピーします。そして非特権コンテナ用のIDマッピングの設定を追加します。

$ mkdir -p ~/.config/lxc
$ cp /etc/lxc/default.conf ~/.config/lxc/
$ (編集)
$ cat ~/.config/lxc/default.conf
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx

# これ以降が追加した行
lxc.idmap = u 0 200000 65536
lxc.idmap = g 0 200000 65536

記事では lxc.id_map でしたが、現在のバージョンである 3.x 系では lxc.idmap になっていますので注意が必要です。

pam_cgfs

記事の時点では、ユーザーが書き込み可能な cgroup をシェルスクリプトで作成し、各 cgroup に PID を登録していました。

今では lxc に pam_cgfs という PAM モジュールが付属していますので、これを使うとログイン時にユーザー権限の cgroup を作成してくれます。

/etc/pam.d/system-session ファイルの最後に次のように追加します。-c オプションの後にコントローラー名をカンマ区切りで指定するか、all を指定します。all だとシステム上で使えるすべてのコントローラーにユーザー用の cgroup を使います。

session  optional    pam_cgfs.so -c all

一部のコントローラーだけ必要であれば、次のように書きます。Ubuntu なんかでは次のように書かれています。

session  optional    pam_cgfs.so -c freezer,memory,name=systemd

これで freezermemoryコントローラー用の cgroup 配下と、systemd が作る systemd という cgroup 配下にユーザー用の cgroup を作ります。

Plamo の場合、ssh でアクセスしたときは system-session ファイルは使いませんので、/etc/pam.d/sshd ファイルの末尾に上記の行を追加してください。

これでログインすると、こんな感じにユーザー用の cgroup が作られ、ログインしたシェルが登録されます。

$ cat /proc/self/cgroup 
14:rdma:/user/plamo/0
13:pids:/user/plamo/0
12:hugetlb:/user/plamo/0
11:net_prio:/user/plamo/0
10:perf_event:/user/plamo/0
9:net_cls:/user/plamo/0
8:freezer:/user/plamo/0
7:devices:/user/plamo/0
6:memory:/user/plamo/0
5:blkio:/user/plamo/0
4:cpuacct:/user/plamo/0
3:cpu:/user/plamo/0
2:cpuset:/user/plamo/0
1:name=systemd:/user/plamo/0

各コントローラー用の cgroup 配下に /user/ ディレクトリを挟んでユーザー名のディレクトリ(= cgroup)が作られ、さらにはセッションID名のcgroupができています。自身のシェルプロセスの /proc/self/cgroup ファイルを見ると、上記のようにそのディレクトリにプロセスが登録されているのがわかります。

これで準備は OK のはずなので、Ubuntu コンテナを作成し、起動してみます。

$ lxc-create -t download c2 -- -d ubuntu -r bionic -a amd64
$ lxc-start c2
$ lxc-attach c2
$ lxc-ls -f
NAME STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
c2   RUNNING 0         -      -    -    true         

起動しましたが、待っていてもアドレスは割り振られません。中で確認してみます。

$ lxc-attach c2
root@c2:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.2  76408  5408 ?        Ss   11:15   0:00 /sbin/init
root         7  0.0  0.1  21444  3328 pts/2    Ss   11:16   0:00 /bin/bash
root        10  0.0  0.1  37336  2856 pts/2    R+   11:16   0:00 ps aux

というように init とシェルしかプロセスがありません。

どうやら systemd でホスト側に systemd が作成した cgroup が必要のようです(ちゃんと調べたわけではないですが)。

ホスト側で必要な systemd 用 cgroup を作成

そこで systemd が作る cgroup を自分で作ってみます。

$ sudo mkdir -p /sys/fs/cgroup/systemd/user.slice/user-$(id -u plamo).slice
$ sudo chown $(id -u plamo):$(id -g plamo) /sys/fs/cgroup/systemd/user.slice/user-$(id -u plamo).slice
$ sudo chown $(id -u plamo):$(id -g plamo) /sys/fs/cgroup/systemd/user.slice/user-$(id -u plamo).slice/{cgroup.procs,tasks}

各コントローラー用には次のようなスクリプトをでっち上げます。

UNPRIV_USERS="plamo"
mkdir -p /sys/fs/cgroup/systemd
mount -t cgroup -o none,name=systemd cgroup /sys/fs/cgroup/systemd
mkdir -p /sys/fs/cgroup/systemd/user.slice
for u in $UNPRIV_USERS
do
    USER_CG="/sys/fs/cgroup/systemd/user.slice/user-$(id -u $u).slice"
    mkdir -p $USER_CG
    chown $(id -u $u):$(id -g $u) $USER_CG
    chown $(id -u $u):$(id -g $u) $USER_CG/{cgroup.procs,tasks}
done

for sys in $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups); do
    mkdir -p $sys
    if ! mountpoint -q $sys; then
        if ! mount -n -t cgroup -o $sys cgroup $sys; then
            rmdir $sys || true
        else
            echo 1 > /sys/fs/cgroup/$sys/cgroup.clone_children
            for u in $UNPRIV_USERS
            do
                USER_CG="$sys/user.slice/user-$(id -u $u).slice"
                mkdir -p $USER_CG
                chown $(id -u $u):$(id -g $u) $USER_CG
                chown $(id -u $u):$(id -g $u) $USER_CG/{cgroup.procs,tasks}
                echo 1 > $USER_CG/cgroup.clone_children
                if [ $sys = "cpuset" ]; then
                    chown $(id -u $u):$(id -g $u) $USER_CG/cpuset.{cpus,mems}
                    echo 0 > $USER_CG/cpuset.cpus
                    echo 0 > $USER_CG/cpuset.mems
                fi
            done
        fi
    fi
done

つまり各 cgroup 配下に user.slice/user-$(id).slice という cgroup を作って、ユーザー権限を与えるわけですね。

次のようにディレクトリができます。

$ find /sys/fs/cgroup/memory/ | grep user.slice
/sys/fs/cgroup/memory/user.slice
/sys/fs/cgroup/memory/user.slice/memory.memsw.usage_in_bytes
/sys/fs/cgroup/memory/user.slice/memory.use_hierarchy
  :(snip)
/sys/fs/cgroup/memory/user.slice/user-1000.slice
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.memsw.usage_in_bytes
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.use_hierarchy
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.kmem.tcp.max_usage_in_bytes
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.kmem.slabinfo

これでコンテナを起動させてみます。

$ lxc-start c2
$ lxc-ls -f
NAME STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
c2   RUNNING 0         -      10.0.3.223 -    true
$ lxc-attach c2
root@c2:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.8  0.4  77040  8312 ?        Ss   11:58   0:00 /sbin/init
root        17  0.0  0.4  78312  9700 ?        Ss   11:58   0:00 /lib/systemd/systemd-journald
root        26  0.0  0.1  42104  3408 ?        Ss   11:58   0:00 /lib/systemd/systemd-udevd
systemd+    31  0.0  0.2  80028  5148 ?        Ss   11:58   0:00 /lib/systemd/systemd-networkd
systemd+    50  0.0  0.2  70616  5128 ?        Ss   11:58   0:00 /lib/systemd/systemd-resolved
root        54  0.0  0.1  31292  3036 ?        Ss   11:58   0:00 /usr/sbin/cron -f
root        55  0.6  0.8 170360 16912 ?        Ssl  11:58   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
root        56  0.0  0.2  62008  5508 ?        Ss   11:58   0:00 /lib/systemd/systemd-logind
message+    57  0.0  0.2  49928  4196 ?        Ss   11:58   0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
syslog      58  0.0  0.1 193400  4040 ?        Ssl  11:58   0:00 /usr/sbin/rsyslogd -n
root        59  0.0  0.1  61816  2956 ?        Ss   11:58   0:00 /lib/systemd/systemd-hostnamed
root        63  0.0  0.1  15956  2392 pts/3    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/3 115200,38400,9600 vt220
root        64  0.0  0.1  15956  2324 pts/1    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/1 115200,38400,9600 vt220
root        65  0.0  0.1  15956  2308 pts/1    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud console 115200,38400,9600 linux
root        66  0.0  0.1  15956  2264 pts/2    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/2 115200,38400,9600 vt220
root        67  0.0  0.1  15956  2296 pts/0    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/0 115200,38400,9600 vt220
root        70  0.0  0.1  21444  3264 pts/2    Ss   11:58   0:00 /bin/bash
root        73  0.0  0.1  37336  2996 pts/2    R+   11:58   0:00 ps aux

ちゃんと起動していますね。pidsdevicessystemd cgroup 配下に色々と systemd が cgroup を作っているので、この辺りがホストにも必要だったということでしょう。

Plamo 7.x の現時点の最新の cgroupfs_mount パッケージ 1.8 では、上のような処理を反映させたスクリプトにしてありますので、これをインストールして、/etc/sysconfig/cgroupfs-mount ファイル内で非特権コンテナを起動したいユーザーを指定すれば OK です。

$ cat /etc/sysconfig/cgroupfs-mount 
# List of non-root users who want to create owned cgroups
UNPRIV_USERS="plamo"

まとめ

以上のようにダミーで systemd が作る cgroup ツリーを作成すれば、systemd 採用のコンテナは起動しました。

今回はコンテナとして Ubuntu bionic を使っていますが、もう少し古い systemd だと /sys/fs/cgroup/systemd という cgroup ツリーさえあれば起動していた気がします。今後も systemd の変化とともに動きが変わるかもしれません。