검증기가 거부한 이유를 한 줄씩 읽는다
이 실습은 VM 에서 돕니다
우분투 24.04 VM 한 대입니다. clang 과 libbpf 헤더를 설치하느라 처음 뜨는 데 2분 남짓 걸립니다. BPF 프로그램을 커널에 올리는 일은 CAP_BPF 가 필요해서 실습 파드에서는 할 수 없습니다.
목표
커널에 이미 올라가 있는 BPF 프로그램과 맵을 bpftool 로 읽습니다. 작은 C 프로그램을
BPF 바이트코드로 컴파일해 BTF 를 들여다보고, 올리고, tracepoint 에 붙이고, 맵의
숫자가 실제로 오르는 것을 봅니다. 그다음 검증기(verifier)가 거부하는 프로그램을 올려
그 기록을 한 줄씩 읽고 고쳐서 다시 올립니다.
왜 중요한가
BPF 프로그램은 커널 안에서 돕니다. 잘못된 포인터 하나가 커널을 멈출 수 있으니,
커널은 올리기 전에 검증기로 모든 경로를 따라가 봅니다. 검증기가 거부하면 프로그램은
아예 올라가지 않고, 남는 것은 레지스터 상태가 적힌 긴 기록뿐입니다. 이 기록을 읽을 줄
모르면 eBPF 를 쓰는 도구(Cilium, Falco, Pixie)의 문제를 조사할 수 없습니다. 그리고
bpftool 은 그 도구들이 무엇을 어디에 올려 두었는지 보여 주는 거의 유일한 창입니다.
"올렸다" 와 "붙였다" 는 다른 일이라는 것도 여기서 배웁니다. 프로그램과 맵이 커널에 있어도 훅에 붙지 않으면 아무 일도 일어나지 않습니다. 그런 상태는 바깥에서 보기에 정상과 구별되지 않습니다.
단계
- 지금 올라가 있는 BPF 프로그램 목록을 JSON 으로
/root/kx/tool/01-progs.json에 저장하세요. /root/kx/tool/count.bpf.c를 BTF 를 담아/root/kx/tool/count.bpf.o로 컴파일하세요.- 오브젝트의 BTF 를
/root/kx/tool/03-btf.txt에 풀어 저장하세요. - 프로그램을 올리고 tracepoint 에 붙여 링크를
/sys/fs/bpf/kx/count아래에 핀하세요. - 맵
exec_count의 내용을 JSON 으로/root/kx/tool/05-map.json에 저장하세요. broken.bpf.c를 컴파일해 올려 보고, 검증기의 기록을/root/kx/tool/06-verifier.log에 저장하세요.broken.bpf.c를 고쳐 다시 올리고 붙여 링크를/sys/fs/bpf/kx/fixed아래에 핀하세요.- 검증기 기록을 읽은 내용을
/root/kx/tool/08-report.md에 정리하세요.
참고
- 컴파일:
clang -O2 -g -target bpf -I/usr/include/x86_64-linux-gnu -c 소스 -o 오브젝트.-g가 있어야 BTF 가 생기고,-I가 없으면asm/types.h를 못 찾습니다. bpftool prog loadall <오브젝트> <핀 디렉터리> autoattach는 올리면서 붙이고, 붙은 링크를 그 디렉터리에 핀합니다.autoattach를 빼면 올리기만 합니다.- 올라간 것은
bpftool prog show, 붙은 것은bpftool link show, 맵은bpftool map show·bpftool map dump name <맵>으로 봅니다. 뒤에-j를 붙이면 JSON 입니다. - 흔한 실수: 검증기 기록의 마지막 줄만 읽는 것. 원인은 대개 그보다 몇 줄 위의 레지스터 상태에 있습니다.
이미 올라가 있는 것
bpftool prog show 의 결과를 JSON 으로 /root/kx/tool/01-progs.json 에 저장하세요. 이 VM 에는 아무것도 올리지 않았는데도 프로그램이 몇 개 있습니다.
-j(또는 --json)를 붙이면 JSON 배열로 나옵니다. 각 원소의 type 과 name 을 보면 누가 올렸는지 짐작할 수 있습니다. systemd 는 cgroup 에 장치 접근 규칙 같은 프로그램을 붙입니다.
BPF 바이트코드로 컴파일한다
/root/kx/tool/count.bpf.c 를 clang -target bpf 로 컴파일해 /root/kx/tool/count.bpf.o 를 만드세요. 오브젝트에 .BTF 섹션이 있어야 합니다.
BTF 는 디버그 정보에서 만들어집니다. -g 를 빼면 .BTF 섹션이 없고, 그러면 bpftool 이 맵의 구조를 모르고 libbpf 도 맵 정의를 못 읽습니다. asm/types.h 를 못 찾는다는 오류가 나면 참고의 -I 를 보세요. 확인은 readelf -S 로 합니다.
오브젝트가 들고 있는 타입
bpftool btf dump file 로 count.bpf.o 의 BTF 를 풀어 /root/kx/tool/03-btf.txt 에 저장하세요. 함수 count_exec 와 맵 exec_count 가 보여야 합니다.
BTF 는 타입 정보를 담은 작은 표입니다. 프로그램 함수는 FUNC, 전역 변수(맵 정의 포함)는 VAR 로 나옵니다. 이 정보가 있어서 bpftool 이 맵 값을 숫자로 풀어 보여 줄 수 있습니다.
올리고, 붙이고, 핀한다
bpftool prog loadall 로 count.bpf.o 를 올리면서 tracepoint 에 붙이고, 링크를 /sys/fs/bpf/kx/count 아래에 핀하세요. 채점기는 /bin/true 를 실행해 맵 exec_count 의 값이 실제로 오르는지 봅니다.
loadall 뒤에 autoattach 를 붙여야 섹션 이름(tracepoint/sched/sched_process_exec)을 보고 붙여 줍니다. 빼면 프로그램과 맵은 커널에 있지만 아무 사건도 오지 않습니다. 붙었는지는 bpftool link show 에서 확인합니다. 핀한 링크가 있어 bpftool 이 끝나도 프로그램이 남습니다.
맵을 읽는다
bpftool map dump 로 맵 exec_count 의 내용을 JSON 으로 /root/kx/tool/05-map.json 에 저장하세요. 값이 0 보다 커야 합니다.
맵은 이름이나 id 로 고릅니다. BTF 가 있으면 JSON 에 원시 바이트(key·value)와 함께 풀어 쓴 값(formatted)이 나옵니다. 저장하기 전에 명령을 몇 번 실행해 숫자가 오르는 것을 보세요.
검증기가 거부한다
/root/kx/tool/broken.bpf.c 를 컴파일해 bpftool prog load 로 올려 보세요. 실패할 것입니다. 그때 나온 검증기 기록 전체를 /root/kx/tool/06-verifier.log 에 저장하세요.
bpftool 은 검증기 기록을 표준 오류로 냅니다. 2> 로 파일에 받으세요. 기록은 명령 하나마다 레지스터의 상태를 적고, 거부한 줄에서 끝납니다. bpf_map_lookup_elem 을 부른 직후 R0 의 상태가 무엇이라고 적혀 있는지 보세요.
고쳐서 다시 올린다
broken.bpf.c 를 고쳐 검증기를 통과하게 하고, 다시 컴파일해 bpftool prog loadall … autoattach 로 올리면서 붙여 링크를 /sys/fs/bpf/kx/fixed 아래에 핀하세요. 프로그램 이름(broken_exec)과 맵 이름(broken_count)은 그대로 두세요. 채점기는 맵 값이 오르는지 봅니다.
검증기는 bpf_map_lookup_elem 이 NULL 을 돌려줄 수 있다고 봅니다(키가 없을 수 있으니까). 그 값을 쓰기 전에 NULL 이 아닌지 확인하는 분기가 있으면, 그 아래에서는 R0 를 안전한 포인터로 다룹니다. 증가를 빼서 통과시키면 맵이 오르지 않아 채점기가 알아챕니다.
검증기 기록을 풀어 쓴다
/root/kx/tool/08-report.md 에 ## 거부된 줄·## R0 의 상태·## 고친 방법 세 절을 쓰세요. 6단계 기록에서 거부를 알린 줄(invalid mem access 가 들어간 줄)을 그대로 인용하고, map_value_or_null 이 무슨 뜻인지 설명해야 합니다.
기록에서 call bpf_map_lookup_elem 줄 바로 뒤의 R0_w= 를 보세요. 그 상태 그대로 메모리를 건드린 것이 거부 이유입니다. 고친 뒤에는 분기 아래에서 R0 가 map_value 로 바뀝니다. 셋째 절에는 올리기와 붙이기의 차이도 적으세요.