TT Lab
블로그 시작하기
배우기 러닝패스 코스

리눅스 커널과 eBPF

검증기가 거부한 이유를 한 줄씩 읽는다

TT Lab 에서 이어서 보기

이 실습은 VM 에서 돕니다

우분투 24.04 VM 한 대입니다. clang 과 libbpf 헤더를 설치하느라 처음 뜨는 데 2분 남짓 걸립니다. BPF 프로그램을 커널에 올리는 일은 CAP_BPF 가 필요해서 실습 파드에서는 할 수 없습니다.

목표

커널에 이미 올라가 있는 BPF 프로그램과 맵을 bpftool 로 읽습니다. 작은 C 프로그램을 BPF 바이트코드로 컴파일해 BTF 를 들여다보고, 올리고, tracepoint 에 붙이고, 맵의 숫자가 실제로 오르는 것을 봅니다. 그다음 검증기(verifier)가 거부하는 프로그램을 올려 그 기록을 한 줄씩 읽고 고쳐서 다시 올립니다.

왜 중요한가

BPF 프로그램은 커널 안에서 돕니다. 잘못된 포인터 하나가 커널을 멈출 수 있으니, 커널은 올리기 전에 검증기로 모든 경로를 따라가 봅니다. 검증기가 거부하면 프로그램은 아예 올라가지 않고, 남는 것은 레지스터 상태가 적힌 긴 기록뿐입니다. 이 기록을 읽을 줄 모르면 eBPF 를 쓰는 도구(Cilium, Falco, Pixie)의 문제를 조사할 수 없습니다. 그리고 bpftool 은 그 도구들이 무엇을 어디에 올려 두었는지 보여 주는 거의 유일한 창입니다.

"올렸다" 와 "붙였다" 는 다른 일이라는 것도 여기서 배웁니다. 프로그램과 맵이 커널에 있어도 훅에 붙지 않으면 아무 일도 일어나지 않습니다. 그런 상태는 바깥에서 보기에 정상과 구별되지 않습니다.

단계

  1. 지금 올라가 있는 BPF 프로그램 목록을 JSON 으로 /root/kx/tool/01-progs.json 에 저장하세요.
  2. /root/kx/tool/count.bpf.c 를 BTF 를 담아 /root/kx/tool/count.bpf.o 로 컴파일하세요.
  3. 오브젝트의 BTF 를 /root/kx/tool/03-btf.txt 에 풀어 저장하세요.
  4. 프로그램을 올리고 tracepoint 에 붙여 링크를 /sys/fs/bpf/kx/count 아래에 핀하세요.
  5. 맵 exec_count 의 내용을 JSON 으로 /root/kx/tool/05-map.json 에 저장하세요.
  6. broken.bpf.c 를 컴파일해 올려 보고, 검증기의 기록을 /root/kx/tool/06-verifier.log 에 저장하세요.
  7. broken.bpf.c 를 고쳐 다시 올리고 붙여 링크를 /sys/fs/bpf/kx/fixed 아래에 핀하세요.
  8. 검증기 기록을 읽은 내용을 /root/kx/tool/08-report.md 에 정리하세요.

참고

이미 올라가 있는 것

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 로 바뀝니다. 셋째 절에는 올리기와 붙이기의 차이도 적으세요.