ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

cgroup v2の、BPFによるデバイスアクセス制御を試す

RubyKaigiも近付いたしeBPFの機運を高めようとしている。

タイトルですが、そういうことができます。

www.kernel.org

このドキュメントにも「どうすればできる」と言うところが書いておらず、最終的にカーネルのサンプルを眺めることになる。

elixir.bootlin.com

elixir.bootlin.com

下準備

まず、cgroup v2が有効なカーネルを使う必要がある。いったん。Ubuntu Groovy(5.8.0-53-generic)を使う。

その上で、cgroup v1のdevicesコントローラを一応umountしておいいた。必要なのかはわからないが...。

$ sudo umount -l /sys/fs/cgroup/devices

その上で cgroup v2のグループを作成。

$ sudo mkdir /sys/fs/cgroup/unified/test-bpf-based-device-cgroup/
$ ls /sys/fs/cgroup/unified/test-bpf-based-device-cgroup/
cgroup.controllers  cgroup.freeze     cgroup.max.descendants  cgroup.stat             cgroup.threads  cpu.pressure  io.pressure
cgroup.events       cgroup.max.depth  cgroup.procs            cgroup.subtree_control  cgroup.type     cpu.stat      memory.pressure

最小限のBPFプログラムを書く

以下のようなBPFプログラム dev_cgroup.c を書く。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("cgroup/dev")
int bpf_prog1(struct bpf_cgroup_dev_ctx *ctx)
{
  // /dev/urandom Device type: 1,9
  if (ctx->major == 1 && ctx->minor == 9)
    return 0; // NG
  return 1; // OK
}

char _license[] SEC("license") = "GPL";

-target bpf でこんな感じでビルド。

$ clang -O1 -g -c -target bpf dev_cgroup.c -o dev_cgroup.o

objdumpしてみるとeBPFのopcodeがわかる。今回はCのソースも非常に単純なので、なんとなく読めるかもしれない。

$ llvm-objdump -xS dev_cgroup.o

dev_cgroup.o:   file format elf64-bpf

architecture: bpfel
start address: 0x0000000000000000

Program Header:

Dynamic Section:
Sections:
Idx Name             Size     VMA              Type
  0                  00000000 0000000000000000
  1 .strtab          000000c9 0000000000000000
  2 .text            00000000 0000000000000000 TEXT
  3 cgroup/dev       00000038 0000000000000000 TEXT
  4 license          00000004 0000000000000000 DATA
...

Disassembly of section cgroup/dev:

0000000000000000 <bpf_prog1>:
;   if (ctx->major == 1 && ctx->minor == 9)
       0:       61 12 04 00 00 00 00 00 r2 = *(u32 *)(r1 + 4)
       1:       55 02 03 00 01 00 00 00 if r2 != 1 goto +3 <LBB0_2>
       2:       b7 00 00 00 00 00 00 00 r0 = 0
;   if (ctx->major == 1 && ctx->minor == 9)
       3:       61 11 08 00 00 00 00 00 r1 = *(u32 *)(r1 + 8)
       4:       15 01 01 00 09 00 00 00 if r1 == 9 goto +1 <LBB0_3>

0000000000000028 <LBB0_2>:
       5:       b7 00 00 00 01 00 00 00 r0 = 1

0000000000000030 <LBB0_3>:
; }
       6:       95 00 00 00 00 00 00 00 exit

具体的なopcodeの詳細や仕様はmmisonoさん(eBPFの神)の記事に詳しい。

www.atmarkit.co.jp

こう言う構造体にパックされる。

    struct ebpf_insn {
        u8    code;       /* オペコード */
        u8    dst_reg:4;  /* ディスティネーションレジスタ */
        u8    src_reg:4;  /* ソースレジスタ */
        s16   off;        /* オフセット */
        s32   imm;        /* 即値 */
    };

定数は bpf_common.hbpf.h にある。

例えば 55 02 03 00 01 00 00 00 は以下のように読める*1。リトルエンディアンである。

55 ... BPF_JMP|BPF_JNE (1桁目が操作、2桁目が操作のモード)
0 ... コピー先レジスタ、今回は無視
2 ... 参照元レジスタ R2
03 00 ... オフセット 3
01 00 00 00 ... 値 1 。今回は比較対象

一応 objdump -S の結果でこの命令がこの操作に相当する、と言う翻訳は出力される。

BPFでデバイスアクセス制限

このプログラムをロードするには、カーネル内のサンプルを参考にして bpf_prog_load()/bpf_prog_attach() を使う。 load_dev_cgroup.c:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>

#define DEV_CGROUP_PROG "./dev_cgroup.o"
#define TEST_CGROUP "/sys/fs/cgroup/unified/test-bpf-based-device-cgroup/"

int main(int argc, char **argv)
{
  struct bpf_object *obj;
  int error = -1;
  int prog_fd, cgroup_fd;

  if (bpf_prog_load(DEV_CGROUP_PROG, BPF_PROG_TYPE_CGROUP_DEVICE,
                    &obj, &prog_fd)) {
    printf("Failed to load DEV_CGROUP program\n");
    goto out;
  }

  cgroup_fd = open(TEST_CGROUP, O_RDONLY);
  if (cgroup_fd < 0) {
    printf("Failed to open test cgroup\n");
    goto out;
  }

  if (bpf_prog_attach(prog_fd, cgroup_fd, BPF_CGROUP_DEVICE, 0)) {
    printf("Failed to attach DEV_CGROUP program");
    goto out;
  }
  
  error = 0;
  
 out:
  return error;
}

bpf_prog_load() でBPFプログラムをカーネルにロードし、bpf_prog_attach()で、今回はcgroupを表すfdに紐づける。

ビルドして起動する。

$ gcc load_dev_cgroup.c -o load_dev_cgroup -l bpf
$ sudo ./load_dev_cgroup 

bpftoolを使えばロードされているのがわかる。

$ sudo bpftool prog 
119: cgroup_skb  tag 6deef7357e7b4530  gpl
        loaded_at 2021-06-02T15:28:41+0000  uid 0
        xlated 64B  jited 66B  memlock 4096B
120: cgroup_skb  tag 6deef7357e7b4530  gpl
        loaded_at 2021-06-02T15:28:41+0000  uid 0
        xlated 64B  jited 66B  memlock 4096B
121: cgroup_skb  tag 6deef7357e7b4530  gpl
        loaded_at 2021-06-02T15:28:41+0000  uid 0
        xlated 64B  jited 66B  memlock 4096B
122: cgroup_skb  tag 6deef7357e7b4530  gpl
        loaded_at 2021-06-02T15:28:41+0000  uid 0
        xlated 64B  jited 66B  memlock 4096B
123: cgroup_skb  tag 6deef7357e7b4530  gpl
        loaded_at 2021-06-02T15:28:41+0000  uid 0
        xlated 64B  jited 66B  memlock 4096B
124: cgroup_skb  tag 6deef7357e7b4530  gpl
        loaded_at 2021-06-02T15:28:41+0000  uid 0
        xlated 64B  jited 66B  memlock 4096B
160: cgroup_device  name bpf_prog1  tag 3ea6c33a1948b0f6  gpl
        loaded_at 2021-06-03T15:43:04+0000  uid 0
        xlated 56B  jited 60B  memlock 4096B
        btf_id 25

動作確認

当該cgroupに現在のシェルプロセスを紐づける。

# 通常はアクセスできる
$ head -c 10 /dev/urandom | od
0000000 102321 125234 173165 163517 167231
0000012

# 当該cgroupに所属したらアクセス不可になる
$ echo $$ | sudo tee /sys/fs/cgroup/unified/test-bpf-based-device-cgroup/cgroup.procs 
1838
$ head -c 10 /dev/urandom | od
head: cannot open '/dev/urandom' for reading: Operation not permitted
0000000

# はずれると戻る。
$ echo $$ | sudo tee /sys/fs/cgroup/unified/cgroup.procs 
1838
$ head -c 10 /dev/urandom | od
0000000 123444 177161 045424 105463 167632
0000012

アンロードするには

当該cgroupを削除する。

$ sudo rmdir /sys/fs/cgroup/unified/test-bpf-based-device-cgroup/

cgroup v2 のデバイスアクセス制御を試した。確認が容易で、BPFのコードもシンプルで良いので、BPFのプログラミング練習に良いかもしれない。

*1:Rubyでアンパックするなら、 [%w(55 02 03 00 01 00 00 00).join].pack('H*').unpack('c c s i').tap{|(c, r, o, i)| break [c, r>>2, r&0b11, o, i]} #=> [85, 0, 2, 3, 1] こう言う感じ。