Kprobes pozwalają wstrzyknąć kod w dowolne miejsce jądra, ale robisz to, pisząc moduł kernela. Jeden błąd i masz kernel panic. eBPF rozwiązuje ten problem: ładujesz mały program do jądra, a weryfikator gwarantuje, że nie wykona niczego niebezpiecznego. To dziś podstawa nowoczesnej obserwowalności, sieci i bezpieczeństwa w Linuksie.

Czym jest eBPF?

eBPF (extended Berkeley Packet Filter) to maszyna wirtualna wewnątrz jądra. Pozwala uruchamiać niewielkie programy w przestrzeni jądra, podpięte pod zdarzenia, bez rekompilacji kernela i bez restartu. Z grubsza działa tak:

  1. Piszesz program w C kompilowanym do bytecode’u BPF albo w wyższym języku przez bpftrace.
  2. Ładujesz go wywołaniem systemowym bpf().
  3. Weryfikator sprawdza go przed uruchomieniem: brak nieograniczonych pętli, brak dostępu poza zakresem pamięci, gwarancja zakończenia, brak wycieku wskaźników jądra do userspace.
  4. JIT kompiluje bytecode do natywnego kodu maszynowego, więc narzut jest minimalny.
  5. Program podpina się pod hook: kprobe, tracepoint, XDP, gniazdo sieciowe, LSM, perf event, cgroup czy uprobe.

To właśnie weryfikator odróżnia eBPF od modułu jądra. Moduł może wszystko, łącznie z wywaleniem systemu. Program eBPF, który nie przejdzie weryfikacji, po prostu się nie załaduje.

Mapy: komunikacja z userspace

Program eBPF nie działa w próżni. Wymienia dane z przestrzenią użytkownika przez mapy (BPF maps): tablice haszujące, tablice, ring buffery. Program w jądrze zapisuje, narzędzie w userspace czyta. Tak powstają liczniki, histogramy i strumienie zdarzeń.

CO-RE i BTF: przenośność

Klasyczny problem kprobes to niestabilny ABI: nazwy i układ struktur jądra zmieniają się między wersjami. eBPF rozwiązuje to przez CO-RE (Compile Once, Run Everywhere) i BTF (BPF Type Format). Kernel z CONFIG_DEBUG_INFO_BTF=y udostępnia własne typy w /sys/kernel/btf/vmlinux, a program eBPF dopasowuje się do nich w czasie ładowania. Jeden binarny program działa na wielu wersjach jądra.

Wymagania

Większość współczesnych dystrybucji ma eBPF włączone. Kluczowe opcje:

CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_DEBUG_INFO_BTF=y    # dla CO-RE

Sprawdzisz tak:

grep -E 'CONFIG_BPF_SYSCALL|CONFIG_DEBUG_INFO_BTF' /boot/config-$(uname -r)
ls /sys/kernel/btf/vmlinux   # istnieje, jeśli BTF jest dostępny

Do pracy przydadzą się pakiety bpftrace, bpfcc-tools (zestaw narzędzi BCC) i bpftool.

bpftrace, najszybsze wejście

bpftrace to język jednolinijkowców do eBPF. Nie musisz pisać ani kompilować kodu w C. Przykład, pokaż każdy uruchamiany proces w systemie:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
    printf("%s -> %s\n", comm, str(args->filename));
}'

Użyliśmy tracepointa, a nie kprobe. Tracepointy to stabilny interfejs. W przeciwieństwie do nazw funkcji jądra nie zmieniają się przy każdej wersji, więc skrypt jest przenośny.

Policz wywołania systemowe per program przez 10 sekund:

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# Ctrl+C kończy i wypisuje posortowane liczniki

Histogram opóźnień odczytu z dysku:

sudo bpftrace -e '
tracepoint:block:block_rq_issue { @start[args->dev, args->sector] = nsecs; }
tracepoint:block:block_rq_complete /@start[args->dev, args->sector]/ {
    @us = hist((nsecs - @start[args->dev, args->sector]) / 1000);
    delete(@start[args->dev, args->sector]);
}'

Gotowe narzędzia BCC

Pakiet bpfcc-tools to kilkadziesiąt gotowych programów eBPF, których nie musisz pisać sam:

sudo execsnoop-bpfcc      # każdy nowy proces (jak exec w czasie rzeczywistym)
sudo opensnoop-bpfcc      # kto otwiera jakie pliki
sudo tcpconnect-bpfcc     # nowe połączenia TCP wychodzące
sudo biolatency-bpfcc     # histogram opóźnień we/wy bloku
sudo tcpretrans-bpfcc     # retransmisje TCP (diagnoza problemów sieci)

To samo co strace czy tcpdump, ale dla całego systemu naraz i z pomijalnym narzutem. Jeśli interesuje Cię szersze profilowanie, zobacz 5 narzędzi do diagnostyki wydajności.

Sieć: XDP

XDP (eXpress Data Path) to program eBPF podpięty najwcześniej jak się da, w sterowniku karty sieciowej, zanim jądro zbuduje sk_buff. Dla każdego pakietu zwraca werdykt: XDP_PASS, XDP_DROP albo XDP_TX. Dzięki temu osiąga miliony pakietów na sekundę, dlatego stoi za rozwiązaniami anty-DDoS (Cloudflare, Katran od Meta) i siecią kontenerów (Cilium).

Podejrzyj, czy na interfejsie wisi program XDP:

ip link show dev eth0        # szukaj "xdp" w wyjściu
sudo bpftool net show        # programy sieciowe (XDP, tc) per interfejs

Gotowe filtry znajdziesz w projekcie xdp-tools, a tc z eBPF pozwala robić to samo nieco dalej w stosie sieciowym, z dostępem do pełnego pakietu.

Bezpieczeństwo runtime

Skoro eBPF widzi każde wywołanie systemowe, każde połączenie i każde uruchomienie procesu, to idealny fundament pod wykrywanie ataków:

  • Falco: reguły wykrywające podejrzane zachowania (shell w kontenerze, zapis do /etc/passwd, odczyt /etc/shadow).
  • Tetragon (Cilium) i Tracee (Aqua): obserwacja i egzekwowanie polityk na poziomie jądra.
  • LSM-BPF (CONFIG_BPF_LSM=y): pozwala pisać moduły Linux Security Module jako programy eBPF. To programowalny AppArmor, decydujesz w jądrze, czy dana operacja ma prawo się wykonać.

W odróżnieniu od logów z userspace, ataku trudniej tu uniknąć, bo zdarzenie jest przechwytywane w jądrze, w momencie wykonania.

eBPF to też powierzchnia ataku

Skoro program eBPF działa w jądrze, błąd w samym mechanizmie oznacza eskalację uprawnień. Weryfikator to kawał skomplikowanego kodu i sam bywał źródłem podatności, choćby CVE-2021-3490 i seria LPE przez błędy śledzenia zakresów wartości. Dwa wnioski praktyczne:

  • Wyłącz nieuprzywilejowany eBPF. Ustaw kernel.unprivileged_bpf_disabled = 1 (lub 2) oraz net.core.bpf_jit_harden = 2. W konfiguracji jądra włącz CONFIG_BPF_UNPRIV_DEFAULT_OFF=y. Dzięki temu bpf() wymaga CAP_BPF/roota.
  • Pamiętaj o rootkitach eBPF. Programy typu kprobe czy fmod_ret potrafią ukrywać pliki, procesy i połączenia, co robią rodziny złośliwego oprogramowania jak BPFDoor czy Symbiote.

Czy Twój kernel jest poprawnie utwardzony pod kątem eBPF (i nie tylko), sprawdzisz w Kernel Security Checker. Zakładki konfiguracji, sysctl i CVE pokrywają wszystkie powyższe ustawienia.

eBPF vs strace vs ftrace

strace ftrace eBPF
Zakres jeden proces jądro jądro + sieć + userspace
Narzut wysoki niski bardzo niski
Programowalność brak mała pełna
Przenośność wysoka średnia wysoka (CO-RE)
Bezpieczeństwo bezpieczny bezpieczny weryfikator BPF

strace jest świetny do jednego procesu, ftrace i kprobes do jądra, ale to eBPF łączy wszystko: obserwowalność, sieć i bezpieczeństwo, z narzutem, na który możesz sobie pozwolić na produkcji. Jeśli masz uruchomić tylko jedną rzecz, zacznij od bpftrace. Kilka jednolinijkowców i widzisz, co naprawdę robi Twój system.