TenForward

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

LXDのシステムコールインターセプション機能を試す(ちょっと成功編)

昨日失敗した「システムコールインターセプション」機能のお試し。

tenforward.hatenablog.com

昨日追記した

t=2020-03-30T21:42:08+0900 lvl=dbug msg="Handling mknod syscall" audit_architecture=3221225534 container=c1 project=default seccomp_notify_flags=0 seccomp_notify_id=11827333229505280862 syscall_args="&{cMode:8192 cDev:0 cPid:13025 path:/run/systemd/inaccessible/chr}" syscall_number=133

でふと思いつきました。私が試している Plamo Linux 7.1 には systemd はいませんので、これはコンテナ上の systemd のお話だと仮定して、このパスについて調べました。コンテナ上に /run/systemd/inaccessible/chr ディレクトリが存在しなかったので、このディレクトリがないことによるエラーじゃないかと思い、それじゃあということで試しました。(ディレクトリがない理由はわかりません)

images.linuxcontainers.org にある alpine のイメージを使ってコンテナを作ります。

$ lxc launch images:alpine/3.11 a1
a1 を作成中
a1 を起動中                                         
$ lxc shell a1
a1:~# mknod test c 5 1
mknod: test: Operation not permitted

mknodが失敗する所までは同じ。security.syscalls.intercept.mknodtrue に設定してコンテナを再起動すると、特にエラーになることもなく起動します。

$ lxc config set a1 security.syscalls.intercept.mknod true
$ lxc restart a1
$ lxc shell a1

ここでコンテナ内で mknod すると、

a1:~# mknod test c 5 1
a1:~# ls -l
total 0
crw-rw-rw-    0 root     root        5,   1 Mar 31 09:42 test

無事デバイスファイルが作成できました。当たり前ですが、あっさりすぎるほど成功です。

コンテナログにも関連するログが出ていますね。

lxc a1 20200331094221.740 TRACE    seccomp - (path to lxc src)/src/lxc/seccomp.c:lxc_seccomp_load:1267 - Retrieved new seccomp listener fd 7
lxc a1 20200331094221.740 TRACE    attach - (path to lxc src)/src/lxc/attach.c:attach_child_main:810 - Loaded seccomp profile
lxc a1 20200331094221.740 TRACE    commands - (path to lxc src)/src/lxc/commands.c:lxc_cmd_accept:1522 - Accepted new client as fd 14 on command server fd 7
lxc a1 20200331094221.740 TRACE    commands - (path to lxc src)/src/lxc/commands.c:lxc_cmd_rsp_recv:123 - Command "seccomp_notify_add_listener" received response
lxc a1 20200331094221.740 DEBUG    commands - (path to lxc src)/src/lxc/commands.c:lxc_cmd_rsp_recv:156 - Response data length for command "seccomp_notify_add_listener" is 0
lxc a1 20200331094221.740 TRACE    commands - (path to lxc src)/src/lxc/commands.c:lxc_cmd:293 - Opened new command socket connection fd 9 for command "seccomp_notify_add_listener"
lxc a1 20200331094221.740 TRACE    commands - (path to lxc src)/src/lxc/commands.c:lxc_cmd_fd_cleanup:1434 - Closing client fd 14 for command "seccomp_notify_add_listener"

これで解決!と思ったのですが、次のように続けて別のファイルを作ろうとすると、まただんまりになります。

a1:~# mknod test c 5 1  (ちゃんと作成できる)
a1:~# mknod test2 c 0 0 (だんまり)
^C
a1:~# ls -l
total 0
crw-rw-rw-    0 root     root        5,   1 Mar 31 10:04 test

このときのコンテナログはまた

lxc a1 20200331095933.691 ERROR    seccomp - /home/karma/work/PlamoBuild/lxc_git/lxc/src/lxc/seccomp.c:seccomp_notify_handler:1359 - Invalid argument - Failed to read seccomp notification

LXDのログは

t=2020-03-31T19:04:23+0900 lvl=dbug msg="Handling mknod syscall" audit_architecture=3221225534 container=a1 project=default seccomp_notify_flags=0 seccomp_notify_id=16212327395031920484 syscall_args="&{cMode:8630 cDev:1281 cPid:31237 path:test}" syscall_number=133

うーむ、また謎が

オレ、何か mknod について勘違いしてる? 😅(続くかも)

(2020-04-09 追記)

Ubuntu 20.04 LTS の beta で試してみました。LXD は snap で 3.23 が動いていましたが、alpine コンテナで実行すると同じ(最初のmknodはうまくいくが次はうまくいかない)ような感じになりました。

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

このエントリ、ほぼやりかけのことを忘れないための自分用メモです。あまり役に立つことは書いてません(オチがないし)。まあこのブログ全体がそうですが…

みなさん LXD 使ってますか? 私は便利にお仕事で使っています。Plamo LInux 上で CentOS を使うために :-)

もうだいぶ前のリリースになりますが、LXD 3.13 の新機能の中に

というものがあります。

これは Linux カーネルだと 5.0 に対応する機能の初期実装がなされています。kernelnewbies の 5.0 の "9. Security" の項の最初 にあるのがそれだと思います。

つまりseccompの機能ですね。このあたりに解説が、

seccomp でシステムコールをトラップして、ユーザースペースに処理を委ね、その返事を見て処理を進めるかどうか決める、みたいな感じでしょうか。

LXD 3.13のアナウンスに「LXD 3.13 を 5.0 以上のカーネル、最新の libseccomp、liblxc と組み合わせると、ユーザー空間でシステムコールインターセプトして仲介できるようになりました」とあるように、libseccomp は現在リリースされていない機能を含んだ master branch のものを使う必要があるし、LXC は先日リリースされている 4.0.0 が必要になります。

LXD は現在 3.23 で、カーネルも 5.5 になっており、この seccomp の機能を使ってできることは増えているようですが、とりあえずこの 3.13 で実装されたあたりを試してみました。

  • Plamo Linux 7.1
  • kernel 5.5
  • libseccompリポジトリの master ブランチを(2020-03-30時点のもの)
  • LXC 4.0.0(試行錯誤で 2020-03-30 時点の master の状態でも試してます)
  • LXD 3.23

対応の libseccomp を入れた状態で LXC のビルドを行う必要がありました。configure 時に機能の有無を確認するからです。特にこの機能を行うためにシステム共通で行うような LXC/LXD の設定はありません。

LXD で適当なコンテナを作成します。

$ lxc launch ubuntu:18.04 c1
c1 を作成中
c1 を起動中
karma@discovery:~$ lxc shell c1
mesg: ttyname failed: No such device

LXD はデフォルトで非特権コンテナです。一応確認しておきます。

root@c1:~# cat /proc/self/uid_map 
         0     100000      65537

コンテナの root は uid:100000 のユーザーにマッピングされています。ここで、

root@c1:~# mknod test c 5 1
mknod: test: Operation not permitted

当たり前ですが、UID:1000000 の一般ユーザー権限ではこのような mknod の実行は許可されません。

ここで、先に紹介した機能を使うようにコンテナの設定を行います。

$ lxc config set c1 security.syscalls.intercept.mknod true
$ lxc config show c1 | grep syscalls
  security.syscalls.intercept.mknod: "true"

設定されました。コンテナを再起動します。

$ lxc restart c1

再起動はします。ところが…、

$ sudo tail -f /var/log/lxd/c1/lxc.log
lxc c1 20200330121030.523 ERROR    cgfsng - (path to lxc src)/src/lxc/cgroups/cgfsng.c:mkdir_eexist_on_last:1142 - File exists - Failed to create directory "/sys/fs/cgroup/cpuset//lxc.monitor.c1"
lxc c1 20200330121030.524 ERROR    cgfsng - (path to lxc src)/src/lxc/cgroups/cgfsng.c:mkdir_eexist_on_last:1142 - File exists - Failed to create directory "/sys/fs/cgroup/cpuset//lxc.payload.c1"
lxc c1 20200330121030.615 ERROR    seccomp - (path to lxc src)/src/lxc/seccomp.c:seccomp_notify_handler:1359 - Invalid argument - Failed to read seccomp notification
lxc c1 20200330121030.616 ERROR    seccomp - (path to lxc src)/src/lxc/seccomp.c:seccomp_notify_handler:1359 - Invalid argument - Failed to read seccomp notification
lxc c1 20200330121030.616 ERROR    seccomp - (path to lxc src)/src/lxc/seccomp.c:seccomp_notify_handler:1359 - Invalid argument - Failed to read seccomp notification
  : (以下同じエラー)

(最初の 2 行は置いといて)このようなエラーが大量に出て、一瞬で GB 単位のログに肥大化します。ここでコンテナ内で mknod やってもエラーにはなりません。しかし当たり前ですがこのエラーのためにコンテナはだんまりです。Invalid argument ですって (-_-;)

$ lxc shell c1
root@c1:~# mknod test c 5 1
(このままだんまり)

コンテナも強制終了する必要があります。

LXC の seccomp.c の 1359 行目は

 1357 »       ret = seccomp_notify_receive(fd, req);
 1358 »       if (ret) {
 1359 »       »       SYSERROR("Failed to read seccomp notification");
 1360 »       »       goto out;
 1361 »       }

こんな感じですね。seccomp_notify_receive()関数がエラーを返してるようです。とりあえずすぐには手に負えない感じなので :-p

LXD が生成する lxc 設定ファイル(lxc.conf)を見てみましょう。それっぽいところだけ抜き出します。

$ sudo cat /var/log/lxd/c1/lxc.conf
lxc.hook.pre-start = /proc/368/exe callhook /var/lib/lxd 28 start
lxc.hook.stop = /usr/bin/lxd callhook /var/lib/lxd 28 stopns
lxc.hook.post-stop = /usr/bin/lxd callhook /var/lib/lxd 28 stop
lxc.seccomp.profile = /var/lib/lxd/security/seccomp/c1
lxc.seccomp.notify.proxy = unix:/var/lib/lxd/seccomp.socket

コンテナ起動、終了時に何か処理してますね。何やってるのかは…知りません。また調べよう。読み込んでる seccomp プロファイルを見ると、

$ sudo cat /var/lib/lxd/security/seccomp/c1
2
blacklist
reject_force_umount  # comment this to allow umount -f;  not recommended
[all]
kexec_load errno 38
open_by_handle_at errno 38
init_module errno 38
finit_module errno 38
delete_module errno 38
seccomp errno 22 [1,2146435072,SCMP_CMP_MASKED_EQ,2146435072]
seccomp errno 22 [1,8,SCMP_CMP_MASKED_EQ,8]
mknod notify [1,8192,SCMP_CMP_MASKED_EQ,61440]
mknod notify [1,24576,SCMP_CMP_MASKED_EQ,61440]
mknodat notify [2,8192,SCMP_CMP_MASKED_EQ,61440]
mknodat notify [2,24576,SCMP_CMP_MASKED_EQ,61440]

最後の方で mknodmknodat を実行した時の処理が書かれていますね。この意味は…わかりません。また調べよう。誰か知ってたら教えてw

でもなんかそれっぽい設定で mknod なんかが呼ばれた時によしなに処理する設定がなされてる風ですね、知らんけど。

Ubuntu 20.04 LTS はこのあたり使えるようにして出てくるのかしら? (いまのところそのようなパッチが適用されてる感じではなさそうだが…)

成功編を書く日は来るのか…

(追記) なんか不審なログが lxd.log に…

=2020-03-30T21:42:08+0900 lvl=dbug msg="Handling mknod syscall" audit_architecture=3221225534 container=c1 project=default seccomp_notify_flags=0 seccomp_notify_id=11827333229505280862 syscall_args="&{cMode:8192 cDev:0 cPid:13025 path:/run/systemd/inaccessible/chr}" syscall_number=133

何この /run/systemd/inaccessible/chr って…。誰だこのパスを設定してるのは…

スマートロック セサミmini その後

(特に検証していない私の勝手な推測が入った記事です)

スマートロック「セサミmin」、導入以来便利に使っていてなくてはならないものになっています。

ここしばらくセサミで鍵を開閉する際のキレが鈍いようで、これはパーツがヤバい予兆かも、と思って昨晩コレを注文したんですよ。

で翌日でかけて帰ると家に入れない!(中から開けてもらいましたが)なんという神タイミング!!金具がこんな風になって壊れてました。

f:id:defiant:20191212012145j:plain

以前、鍵に遊びがあるために、物理的な鍵で閉めた時とセサミで閉めた時の角度が合わなかったとき、それを解決するために、CANDY HOUSEのサポートと UnaStrada さんのアドバイスを元に、サムターン受けにクッションを貼ってなんとか調整しようとしていました。

tenforward.hatenablog.com

このあと、金具に力がかかるのか一度金具が今回と同じように破損しました。そして、↑のブログに書いたクッションよりはもう少し柔らかめの隙間テープを片側に貼って(もう片側は↑のブログの通りのちょっと硬めのやつ)運用していました。ちなみにこの時点で、鍵とアプリの閉めた位置の角度のズレはうまく解決できずあきらめ状態でした。

これでも力がかかるのか、片側の金具が少し開いてきていました。↑の回の写真でもちょっと片側の金具が開いた感じになってるのがわかるかと思いますし、今回取り外して金具単体で撮ってみたのが次の写真です。わかりづらいですが、よく見るとセサミに取り付ける部分とサムターン受けパーツを取り付ける部分は直角であるはずですが、少し傾いているのがわかるかと(わからんかw)。

f:id:defiant:20191212001652j:plain

今回、この開いた側の金具が折れたのかと思ったら、開いた金具が反対側になるので逆側に変な力が加わったのか、逆側がボキッと折れてしまっていたわけです。根元の部分が一番弱い箇所で力がかかるのか前回もここから曲がってきて破損しました。

これは、うちの鍵に遊びがあるのと、セサミ自体も上下左右に遊びがあるために、サムターン受けの隅っこの方でサムターンの隅っこを掴んで回すような形になることがあり、力がかかるのかと思います。こんな感じ(ちょっと大げさに書いてますし、勝手な推測です)。

f:id:defiant:20191212010225j:plain

さらには、サムターンを垂直で「開」、水平で「閉」にせずに、いずれもちょっと行き過ぎた位置で位置設定をしているため、手で開閉した場合、上の写真のようにセサミのサムターンがちょっと行き過ぎた位置で止まってしまい、「開」の位置なのに更に手で開けてしまおうとしたり、「閉」の位置なのにさらに手で閉めようとしてしまったりして、変な力が加わっていることも考えられます(私は注意しているけど家族みんながそんなに注意してセサミを扱っているわけではない)。(←セサミのサムターンの角度から、今開いているのか閉まってるのかの判別がしにくいんです)

というわけで、セサミの作り上、

  • 鍵のサムターンに遊びがある場合は鍵で閉めた場合とセサミで閉めた場合のサムターン位置に差が出てしまう

というこれまでにあった問題以外にも、

  • 鍵にも遊びがある上に、セサミにも上下左右方向に遊びがあるためにサムターン受けパーツに力がかかってしまい根元で折れる

という問題があるので、鍵に遊びがある場合はちょっと気をつけた方が良さそうです。

Qurio はウェブで見る限りはサムターン受けに遊びがなさそうな気がしますので、こういう問題は起こらないのかな?

結局以前 CANDY HOUSE さんに作っていただいた、結局使っていなかったアダプターを付けることにしました。↓参照。

tenforward.hatenablog.com

このアダプターは上下方向の遊びがなく、サムターン受けの上下方向の位置が固定されるため、サムターン受けが上下に動くことによってサムターン受けに力がかかることがないかなと思ったからです。これでも本来のサムターン受けは遊びがあって動くのですが、変に力が加わることはない程度しか動かないかなと。

このアダプターをつけても、鍵で閉めた場合とアプリで閉めた場合の角度の差は埋められなかったので、鍵で閉めた場合とセサミで閉めた場合の位置の差については完全にあきらめることに。

f:id:defiant:20191212002157j:plain

要望

ついでに要望を書いておこう。以前も書いたのですが、

  • 開閉位置の設定で、「手動で鍵で閉めた場合」と「アプリで閉めた場合」の位置をそれぞれ設定できるようにしてほしい

これで鍵とアプリの位置の差は吸収できると思います。ついでに書いておきますが、

  • Android 版アプリの場合、アプリを起動してから開閉ができるようになるまでの時間がかかりすぎ。逆に iOS 版はアプリ起動から開閉できるようになるまでの時間が早い気がします。これくらいの時間になれば…

子供はこの時間が待ちきれなくてアプリでの開閉を止めましたので。アプリ自体あまりアップデートしてないけど改良してるのかな? あとは、

  • 今回破損した金具、いかにも根元に力がかかって弱そうなのでもう少し構造的にどうにかならないのか?

って思います。

ICMP sockets

連載の第42回

ファイルケーパビリティは安全のためにコピーすると設定が外れます。

なんてことを書いてますが、同僚から「Arch Linux でやるとなぜか ping が実行できてしまう」という情報が。確かに Arch でやると実行できてしまいます。

$ lsb_release -d
Description:    Arch Linux
$ cp /usr/bin/ping .
$ getcap ./ping 
$ ./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.025 ms

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

strace とかで見てみるとどうも SOCK_RAW でなく SOCK_DGRAM を使ってる模様。どうやらこれらしい。hayajo さんからも同じ情報を頂いて自信を持って、これを手がかりにググってみると、

icmp(7) にも記載が。えー、2.6.39 から?

$ man icmp
    :(snip)
       ping_group_range (two integers; default: see below; since Linux 2.6.39)
              Range of the group IDs (minimum and maximum  group  IDs,  inclu‐
              sive) that are allowed to create ICMP Echo sockets.  The default
              is "1 0", which means no group is allowed to  create  ICMP  Echo
              sockets.

いやー、Linux 奥が深いですね。こんなの知りませんでした。Arch を確認してみると確かに設定されています。

$ sysctl -a 2>/dev/null | grep ping_group_range 
net.ipv4.ping_group_range = 0    2147483647

Plamo だと

$ /sbin/sysctl -a 2>/dev/null | grep ping
net.ipv4.ping_group_range = 1    0

Arch の値はやりすぎ感あるのでとりあえず 0〜65534 とすると、

$ sudo sysctl -w net.ipv4.ping_group_range="0 65534"
net.ipv4.ping_group_range = 0 65534
$ /sbin/sysctl -n net.ipv4.ping_group_range
0  65534

設定できたので、

$ cp /bin/ping .
$ /sbin/getcap ./ping 
$ ./ping -c1 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) バイトのデータ
64 バイト応答 送信元 127.0.0.1: icmp_seq=1 ttl=64 時間=0.013ミリ秒

--- 127.0.0.1 ping 統計 ---
送信パケット数 1, 受信パケット数 1, パケット損失 0%, 時間 0ミリ秒
rtt 最小/平均/最大/mdev = 0.013/0.013/0.013/0.000ミリ秒

実行できました。

ちなみに「安全のためにコピーしたら外れる」とか書きましたが、cp コマンドでも CAP_SETFCAP ケーパビリティがあれば --preserve=xattr とかやればファイルケーパビリティ保存したままコピーされますね。

(補足・2019-11-29) Twitter で systemd がそのように設定していると教えていただきました。ありがとうございます。

        * This release enables unprivileged programs (i.e. requiring neither
          setuid nor file capabilities) to send ICMP Echo (i.e. ping) requests
          by turning on the "net.ipv4.ping_group_range" sysctl of the Linux
          kernel for the whole UNIX group range, i.e. all processes. This
          change should be reasonably safe, as the kernel support for it was
          specifically implemented to allow safe access to ICMP Echo for
          processes lacking any privileges. If this is not desirable, it can be
          disabled again by setting the parameter to "1 0".

ケーパビリティバウンディングセット

今更感たっぷりな話題ですが、今頃ケーパビリティについて調べてます。これまで何度も調べてはよくわからなくて挫折を繰り返してるとこです。

ケーパビリティについては

udzura.hatenablog.jp

が詳しいのですが、「ケーパビリティバウンディングセット」の部分だけはちょっと良くわからんのですよね…(偉そうにスミマセン)。

そして、udzura さんのブログも Ambient については記載がまだありませんので、Ambient についてはこちら、

nojima.hatenablog.com

現時点では日本語ではここしかありません(勉強させてもらいました)。

そういうわけで、ケーパビリティバウンディングセットについて調べていてもなんかよくわからんので調べながらメモ。メモなので脈絡なかったり、急になんの前提もなく何かが出てくるかもしれません。

基本はマニュアル

ただでもややこしいのに英語だとなあ、という場合は日本語訳あるけど古いんですよね。ただ、このケーパビリティバウンディングセットの部分は順序が変わっていたりするものの文章の訳としてはそのまま使える部分がほとんどなので、英語苦手な私には参考になります。Ambient ケーパビリティの説明がない頃のやつなんですよね。

結局、マニュアルをすみずみまで読まないとダメということですね。すみません、つまみ食いしてました。man だから入門書のように書くわけにはいかないけど、機能は全部書かないといけない。そのあたりのバランスを取っている結果か、ひとつの機能についてあっちとこっちに書いてある(ときには別の man page にある)なんてことがあるんだなあと思ったりしてます。

まずはマニュアルの"Thread capability sets"の項にある"Bounding"の説明。

Bounding (per-thread since Linux 2.6.25)
       The capability bounding set is a mechanism that can be used to
       limit the capabilities that are gained during execve(2).

            :(略)

       For more details on the capability bounding set, see below.

まずここには

ケーパビリティバウンディングセットは execve(2) の間に得られるケーパビリティを制限するのに使えるメカニズムです。

とあります。ふむふむ、execve(2)の際に使う機能機能なんだな…

で、ケーパビリティあるあるで、execve(2) の際のケーパビリティ決定のアルゴリズム。これがなければ始まらないという感じですね。

P'(ambient)     = (file is privileged) ? 0 : P(ambient)

P'(permitted)   = (P(inheritable) & F(inheritable)) |
                  (F(permitted) & P(bounding)) | P'(ambient)

P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

P'(inheritable) = P(inheritable)    [i.e., unchanged]

P'(bounding)    = P(bounding)       [i.e., unchanged]

関係するのはP'(permitted)を決定している所です。

詳しくはこっちを見ろと書かれている "Capability bounding set" の項を見ると、

Capability bounding set

    :(略)

    * During an execve(2), the capability bounding set is ANDed with the
      file permitted capability set, and the result of this operation is
      assigned to the thread's permitted capability set.  The capability
      bounding set thus places a limit on the permitted capabilities that
      may be granted by an executable file.

ここは日本語訳と特に変化してないので、そっちを参照:

    * execve(2) 実行時に、ケーパビリティバウンディングセットと ファイルの許
      可ケーパビリティセットの論理和 (AND) を取ったものが、 そのスレッドの許
      可ケーパビリティセットに割り当てられる。つまり、ケーパビリティバウンディ
      ングセットは、実行ファイルが認めている許可ケーパビリティに対して制限を
      課す働きをする。

つまりファイルケーパビリティで設定されているケーパビリティであってもバウンディングセットで許可されていなければファイルケーパビリティの設定が無効になるということです。

ところが、さきほどのアルゴリズムを見ると、この制限したセット (F(permitted) & P(bounding)) は inheritable の条件である (P(inheritable) & F(inheritable)) との OR ですので、バウンディングセットで制限したとしてもプロセスとファイルの inheritable で許可されていれば、制限できないことになります。これは man の後の段落に書かれています。

Note that the bounding set masks the file permitted capabilities, but
not the inheritable capabilities.  If a thread maintains a capability
in its inheritable set that is not in its bounding set, then it can
still gain that capability in its permitted set by executing a file
that has the capability in its inheritable set.

日本語訳:

バウンディングセットがマスクを行うのは、継承可能ケーパビリティではなく、
ファイルの許可ケーパビリティのマスクを行う点に注意すること。あるスレッ
ドの継承可能セットにそのスレッドのバウンディングセットに 存在しないケー
パビリティが含まれている場合、そのスレッドは、継承可能セットに含まれ
るケーパビリティを持つファイルを実行することにより、許可セットに含ま
れるケーパビリティも獲得できるということである。

あくまでバウンディングセットはファイルケーパビリティの permitted を制限するだけで、inheritable で許可されれば、permitted に追加できてしまいます。これじゃあ制限できないのでは? と思いますが、ここで man の次の項目を見てみましょう。

* (Since Linux 2.6.25) The capability bounding set acts as a limiting
  superset for the capabilities that a thread can add to its inheri‐
  table set using capset(2).  This means that if a capability is not
  in the bounding set, then a thread can't add this capability to its
  inheritable set, even if it was in its permitted capabilities, and
  thereby cannot have this capability preserved in its permitted set
  when it execve(2)s a file that has the capability in its inherita‐
  ble set.

これも日本語訳

* (Linux 2.6.25 以降) ケーパビリティバウンディングセットは、スレッドが
  capset(2) により自身の継承可能セットに追加可能なケーパビリティの母集団
  を制限する役割を持つ。 スレッドに許可されたケーパビリティであっても、
  バウンディングセットに含まれていなければ、スレッドはそのケーパビリティ
  は自身の継承可能セットに追加できず、その結果、継承可能セットにそのケー
  パビリティを含むファイルを execve(2) する場合、そのケーパビリティを許
  可セットに持ち続けることができない、ということである。

プロセス(スレッド)は自身のケーパビリティを操作できます。しかし、バウンディングセットに含まれていないケーパビリティは自身の inheritable に追加できないということです。なので、そこから execve(2) してプログラムを実行する場合は、バウンディングセットに含まれていないケーパビリティはそれ以上持ち続けられない(継承できない)ということです。

つまり元々 inheritable にセットされていないけど、自身に capset(2) を使ってケーパビリティを設定しようと思っても、バウンディングセットに含まれていなければ inheritable に追加できません。結果的に execve(2) してもバウンディングセットに含まれていないケーパビリティを持てないということです。

と思って読んでたら、マニュアルの「ケーパビリティバウンディングセット」の最後に書いてあるやん。

Removing a capability from the bounding set does not remove it from
the thread's inheritable set.  However it does prevent the capability
from being added back into the thread's inheritable set in the
future.
バウンディングセットからケーパビリティを削除しても、スレッドの継承可
能セットからはそのケーパビリティは削除されない。しかしながら、バウン
ディングセットからの削除により、この先そのケーパビリティをスレッドの
継承可能セットに追加することはできなくなる。

capset(2)

ここで capset(2) を使ってのケーパビリティの追加を見てみましょう。同じくマニュアルの CAP_SETPCAP の解説、

CAP_SETPCAP
       If file capabilities are supported (i.e., since Linux 2.6.24):
       add any capability from the calling thread's bounding set to
       its inheritable set; drop capabilities from the bounding set
       (via prctl(2) PR_CAPBSET_DROP); make changes to the securebits
       flags.

          :(略)

日本語訳:

ファイルケーパビリティがサポートされている場合(Linux 2.6.24以降): 呼
び出し元スレッドのバウンディングセットの任意のケーパビリティを 自身の
継承可能ケーパビリティセットに追加できる。 (prctl(2) PR_CAPBSET_DROP
を使って) バウンディングセットからケーパビリティを削除できる。
securebits フラグを変更できる。

こはちょっとカーネルを追ってみました(5.3 kernel)。細かい所は(理解してないので :-p)すっとばしますが、まず capset システムコールkernel/capability.c にあります。その後は

sys_capset (kernel/capability.c)
  security_capset (security/security.c)
    cap_capset (security/commoncap.c) <- call_int_hook マクロ経由

という風に呼ばれ、実際の処理は security/commoncap.c 内の cap_capset 関数で行われているようです。

まずは CAP_SETPCAP を持っていない場合のチェックです。このチェックは cap_inh_is_capped 関数で行っており、 CAP_SETPCAP ケーパビリティを持っていれば 0 を、持っていなければ 1 を返します。

int cap_capset(struct cred *new,
               const struct cred *old,
               const kernel_cap_t *effective,
               const kernel_cap_t *inheritable,
               const kernel_cap_t *permitted)
{
        if (cap_inh_is_capped() &&
            !cap_issubset(*inheritable,
                          cap_combine(old->cap_inheritable,
                                      old->cap_permitted)))
                /* incapable of using this inheritable set */
                return -EPERM;

新たに設定しようとしている inheritable が、現在の "inheritable | permitted" のサブセットであるかどうかをチェックし、そうでなければエラーとなります。

先のチェックが CAP_SETPCAP がないときのお話でしたので、次は CAP_SETPCAP を持っている場合です。

        if (!cap_issubset(*inheritable,
                          cap_combine(old->cap_inheritable,
                                      old->cap_bset)))
                /* no new pI capabilities outside bounding set */
                return -EPERM;

新たに設定しようとしている inheritable が、現在の "inheritable | bset" (bset=バウンディングセット) のサブセットであるかどうかをチェックし、そうでなければエラーとなります。先に引用したマニュアルの「スレッドに許可されたケーパビリティであっても、バウンディングセットに含まれていなければ、スレッドはそのケーパビリティは自身の継承可能セットに追加できず」というのがこの部分でしょうか。

次は新たな permitted のチェックは、

        /* verify restrictions on target's new Permitted set */
        if (!cap_issubset(*permitted, old->cap_permitted))
                return -EPERM;

シンプルに現在の permitted のサブセットであれば OK です。

新しい effective のチェックは、

        /* verify the _new_Effective_ is a subset of the _new_Permitted_ */
        if (!cap_issubset(*effective, *permitted))
                return -EPERM;

新しい permitted のサブセットであれば OK です。

ここまでくれば新たなケーパビリティセットを設定できるということですので、次の 3 行で新たなケーパビリティとして設定しています。

        new->cap_effective   = *effective;
        new->cap_inheritable = *inheritable;
        new->cap_permitted   = *permitted;

最後に Ambient ケーパビリティです。

        /*
         * Mask off ambient bits that are no longer both permitted and
         * inheritable.
         */
        new->cap_ambient = cap_intersect(new->cap_ambient,
                                         cap_intersect(*permitted,
                                                       *inheritable));
        if (WARN_ON(!cap_ambient_invariant_ok(new)))
                return -EINVAL;
        return 0;
}

新たに決定したケーパビリティセットのうち permitted と inherited の両方で有効になっていないケーパビリティは ambient としては許可されない(マニュアルのAmbientの説明部分参照)ので、permitted & inherited の値と AND を取ります。(new->cap_ambient はどこで設定されてるん? <力尽きたので教えて)

マニュアルの "Capability bounding set" の残りの部分で現在のカーネルに相当する部分は日本語訳で OK です(わざわざ転載する必要もないけど説明として引用しておきます)。

カーネルのバージョンにより、ケーパビリティバウンディングセットはシス
テム共通の属性の場合と、プロセス単位の属性の場合がある。

Linux 2.6.25 以降のケーパビリティバウンディングセット

Linux 2.6.25 以降では、「ケーパビリティバウンディングセット」はスレッ
ド単位の属性である (システム共通のケーパビリティバウンディングセットは
もはや存在しない)。

バウンディングセットは fork(2) 時にはスレッドの親プロセスから継承され、
execve(2) の前後では保持される。

スレッドが CAP_SETPCAP ケーパビリティを持っている場合、そのスレッドは
prctl(2) の PR_CAPBSET_DROP 操作を使って自身のケーパビリティバウンディ
ングセットからケーパビリティを削除することができる。いったんケーパビ
リティをバウンディングセットから削除してしまうと、スレッドはそのケー
パビリティを再度セットすることはできない。 prctl(2) の PR_CAPBSET_READ
操作を使うことで、スレッドがあるケーパビリティが自身のバウンディングセッ
トに含まれているかを知ることができる。

バウンディングセットからのケーパビリティの削除がサポートされるのは、
カーネルのコンパイル時にファイルケーパビリティが有効になっている場合
だけである。Linux 2.6.33 より前のカーネルでは、ファイルケーパビリティ
は 設定オプション CONFIG_SECURITY_FILE_CAPABILITIES で切り替えられる追
加の機能であった。Linux 2.6.33 以降では、この設定オプションは削除され、
ファイルケーパビリティは常にカーネルに組込まれるようになった。ファイ
ルケーパビリティがカーネルにコンパイル時に組み込まれている場合、(全て
のプロセスの先祖である) init プロセスはバウンディングセットで全てのケー
パビリティが セットされた状態で開始する。ファイルケーパビリティが有効
になっていない場合には、 init はバウンディングセットで CAP_SETPCAP 以
外の全てのケーパビリティがセットされた状態で開始する。このようになっ
ているのは、 CAP_SETPCAP ケーパビリティがファイルケーパビリティがサポー
トされていない場合には 違った意味を持つからである。

バウンディングセットからケーパビリティを削除しても、スレッドの継承可
能セットからはそのケーパビリティは削除されない。しかしながら、バウン
ディングセットからの削除により、この先そのケーパビリティをスレッドの
継承可能セットに追加すること はできなくなる。

まとめ

ケーパビリティバウンディングセットは、

  • ファイルケーパビリティに設定されている permitted セットを制限する。しかし、プロセスとファイルの inherited で許可されていれば制限が効かない
  • capset(2) で、スレッドのケーパビリティバウンディングセットで設定されているケーパビリティしか、inherited に追加できない

と理解しました。(間違ってたら優しく教えてください)