RubyKaigiも近付いたしeBPFの機運を高めようとしている。
タイトルですが、そういうことができます。
このドキュメントにも「どうすればできる」と言うところが書いておらず、最終的にカーネルのサンプルを眺めることになる。
下準備
まず、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の神)の記事に詳しい。
こう言う構造体にパックされる。
struct ebpf_insn { u8 code; /* オペコード */ u8 dst_reg:4; /* ディスティネーションレジスタ */ u8 src_reg:4; /* ソースレジスタ */ s16 off; /* オフセット */ s32 imm; /* 即値 */ };
定数は bpf_common.h と bpf.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のプログラミング練習に良いかもしれない。