.. _lab-bpf-pl: =============================================== Zajęcia 6: BPF =============================================== Data: 01.04.2025 :ref:`small_task_5_pl` .. notes:: Od Andrzeja:: = Wstęp - Ostatni dzień na wysłanie pierwszego dużego zadania w terminie - Dziś prezentacja drugiego zadania = BPF - Omówienie materiałów do laboratoriów - Warto pokazać też stronę BPFu (https://ebpf.io/) i fragmenty slajdów z LISA21 (https://www.brendangregg.com/Slides/LISA2021_BPF_Internals.pdf) - Warto zareklamować (darmową) książkę "Learning eBPF" autorstwa Liz Rice (https://isovalent.com/books/learning-ebpf/) = Małe zadanie 4 (też z BPF) Omów krótko treść, zachęć do zrobienia na rozgrzewkę przed drugim dużym zadaniem. = Omówienie Dużego Zadania 2 Omówienie ogólne treści. Uczulenie na konieczność użycia specyficznego pliku .config. Warto zacząć od kompilacji z tym configiem "na sucho". Test używający bpf_simple nie wymaga implementacji nowych funkcji, warto od niego zaczać zacznijcie od tego. Warto omówić /boot/vmlinuz, vmlinux-extracts, symbole w system.map, to wszystko przyda się przy debugowaniu. Pokaż plik bpf.h, bpf_prog_typem, bpf_attach_type Pokaż bpftool, w szczególności: sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h Omów krótko bpf/cgroup.c, warto się na tym trochę wzorować, w szczególności cgroup_current_func_proto Omów krótko bpf/helpers.c (w szczególności bpf_func_proto), tutaj będzie znaczna część rozwiązania. .. tip:: Useful links: - `Introduction to eBPF `_ - `Kernel BPF documentation `_ - `eBPF on Linux `_ -- a bit nicer than the above - `BPF and XDP Reference Guide `_ with technical details .. admonition:: Hands-on For the labs today, you will need to download a prebuild kernel image (unless you have an image for the BPF large task already working):: https://students.mimuw.edu.pl/ZSO/PUBLIC-SO/vmlinuz-6.12.6zsobpf https://students.mimuw.edu.pl/ZSO/PUBLIC-SO/initrd.img-6.12.6zsobpf Boot the QEMU image by using these options apart from your usual ones:: -kernel vmlinuz-6.12.6zsobpf -initrd initrd.img-6.12.6zsobpf -append "root=/dev/sda3" Then, install these dependencies on QEMU:: apt install clang clang-14 llvm pahole bpftool bpftrace bpfcc-tools libbpfcc libbpfcc-dev libbpf-dev Use the superuser account for all the commands today. Wprowadzenie ============ BPF (Berkeley Packet Filter) to technologia pozwalająca na dostarczanie programu filtrującego przez proces użytkownika. W skrócie, BPF umożliwia napisanie krótkich programów (nie będących modułami), które są wykonane w trybie jądra. Najprostszym przykładem programu BPF (dostępnym w "man 2 bpf") jest filtr zliczający pakiety TCP i UDP otrzymanych przez system operacyjny. BPF ma wiele praktycznych zastosowań, związanych między innymi z bezpieczeństwem, śledzeniem i profilowaniem procesów, obsługą interfejsów sieciowych oraz monitorowaniem systemu [1]. Technologia ta zyskuje na popularności np. w 2019 roku Netflix używał domyślnie 15, a Facebook 40 programów BPF w produkcyjnym środowisku [3], co z kolei przekłada się na intensywny rozwój technologii w ostatnich latach - zawartość katalogu linux/kernel/bpf modyfikowało w 2021 prawie 400 commitów. Jedną z niewątpliwych zalet BPF jest jego wysoka wydajność pozwalająca na to, by wykonywać niezbyt skomplikowany program dla każdego pakietu przy 10Gb/s bez wyraźnych opóźnień [5]. Należy jednak pamiętać, że programy BPF nie będą specjalnie szybsze od ich odpowiedników zaimplementowanych w kodzie jądra [6], a najważniejszą cechą programów BPF jest to, że umożliwiają uruchomienie kodu w trybie jądra z procesu użytkownika. Jak łatwo się domyślić operacja taka wymaga szczególnych środków ostrożności, dlatego programy BPF są uruchamiane w piaskownicy (ang. sandbox) po wcześniejszej weryfikacji, o czym więcej za chwilę. Jeśli chodzi o nazewnictwo to skrót BPF pochodzi z publikacji "The BSD Packet Filter" [7] napisanej w 1992. W Linuxie 3.18 dodano extended BPF (eBPF) wspierającego m.in. 64-bitowe rejestry, a wcześniejszą wersję zaczęto nazywać cBPF (classic BPF). W tym momencie najczęściej technologię po prostu nazywa się BPF, chociaż w niektórych miejscach ciągle można spotkać się z użyciem nazwy eBPF [2]. Typy programów BPF ================== Programy BPF mogą być `różnych typów `_, które szczegółowo specyfikuje "enum bpf_prog_type" w pliku ``include/uapi/linux/bpf.h``. Jądro w wersji 6.12 zawiera ponad 30 typów z których niektóre ważniejsze to: - ``BPF_PROG_TYPE_SOCKET_FILTER`` pozwalający na porzucanie lub skracanie pakietów - ``BPF_PROG_TYPE_KPROBE`` pozwalający na instrumentację funkcji - ``BPF_PROG_TYPE_XDP`` pozwala zadecydować o losie pakietu na wczesnym etapie jego obsługi (zanim kosztowne operacje zostaną wykonane), co jest przydatne do ochrony przed atakami DDoS. - ``BPF_PROG_TYPE_CGROUP_*`` pozwalające dodatkowo zarządzać uprawnieniami cgroup .. notes:: Pokaż plik bpf.h a w nim bpf_prog_typem, bpf_attach_type Tworzenie programów BPF ======================= Programy BPF przypominają ASM, ale mają własny zbiór rejestrów i instrukcji. Do dyspozycji programisty jest 11 rejestrów: R0-R9 umożliwiających odczyt i zapis oraz R10 do odczytu adresu ramki (podobnie jak RBP w x86_64). Rejestry są modyfikowane przez liczne instrukcje [8], które umożliwiają między innymi: - operacje arytmetyczne (np. ``BPF_ADD``, ``BPF_MUL``), - skoki i wywołania funkcji (np. ``BPF_JEQ``, ``BPF_JLE``, ``BPF_CALL``), - wczytywanie i zapisywanie wartości (np. ``BPF_LD``, ``BPF_ST``). Jedną z możliwości na napisanie program BPF jest ręczne wykorzystanie struktury "struct bpf_insn" (tak jak w ``samples/bpf/bpf_insn.h``). Ma to jednak oczywiste wady (tak samo jak pisanie programów w ASMie), dlatego istnieją narzędzia umożliwiające pisanie programów BPF w językach programowania takich jak C, C++, Python czy Go, między innymi bcc oraz libbpf. Jako, że programy BPF działają w piaskownicy, nie mogą (`typowo `_) wywoływać dowolnych funkcji jądra i mają ograniczone możliwości interakcji ze światem -- dostępny jest ograniczony zbiór `funkcji pomocniczych `_, które umożliwiają między innymi: - proste wypisywanie (``bpf_trace_printk``), - pobieranie informacji o kontekście (np. `bpf_get_current_uid_gid `_), - komunikację z przestrzenią użytkownika za pomocą różnego rodzaju tablic asocjacyjnych (``bpf_map_*``), - wykonanie operacji specyficznych dla typu programu (np. odrzucenie pakietu), - wywoływanie innych programów BPF (``bpf_tail_call``). Przygotowany program BPF jest po stronie jądra weryfikowany, a następnie kompilowany metodą JIT (just in time compilation) do kodu maszynowego. Kod odpowiedzialny za kompilację dla architektury x86 znajduje się w pliku ``arch/x86/net/bpf_jit_comp.c``. Po skompilowaniu program BPF może zostać uruchomiony. Zazwyczaj, programowi BPF będzie towarzyszył program w przestrzeni użytkownika pośredniczący w komunikacji z nim. Dokumentacja funkcji pomocniczych: `eBPF Docs `_, `manpage `_ Kernel Tracing ============== Linux has multiple facilities for tracing and observability what is happening in it. The most important parts pieces include `tracepoints `_, `ftrace `_, `Kprobes `_. In short, they allow hooking (*placing probes*) at various places. Of special interest are **dynamic** traces, which allow hooking at runtime with virtually no overhead otherwise. The idea of `tracepoints `_ is straightforward: we explicitly place code checking if a probe is connected, and if so, call it with some arguments. Function tracing with *ftrace* is a bit trickier, as we need help from the compiler to put a stub call at each function entry. With `dynamic ftrace `_ on x86, you can notice a call to ``__fentry__`` at almost every function. (Check it yourself with ``objdump --disassemble=vfs_write vmlinux | less``) The function entry hook is also used to place a function exit hook: we just need to replace the return pointer on the stack with a pointer to a specially crafted trampoline. As an extra optimization, the kernel will self-modify and replace these calls with NOPs until they are needed. Kprobes are more powerful, as they allow hooking at individual instructions. In principle, it works by replacing the instruction at question with a breakpoint instruction to redirect the execution flow, then execute the instruction there along with registered probes, and return to the main flow. .. notes:: Show an entrypoint of a function when compiled with FUNCTION_TRACER/DYNAMIC_FTRACE: ``objdump --disassemble=vfs_write vmlinux | less`` Hands-on ======== .. admonition:: Hands-on First, check if you have enabled necessary kernel features with `bpftool `_:: bpftool feature probe ``bpftool`` is developed alongside ``libbpf`` in the main kernel tree. bpftrace -------- ``bpftrace`` is a tool enabling quick hacking a prototyping around BPF probe facilities. .. admonition:: Hands-on You may check a list of all available probes with:: bpftrace -l Go ahead and run your first BPF program with something like:: bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }' Then execute ``sleep`` in another terminal. Keep the trace running, as we will examine it with bpftool:: bpftool prog list will `list currently `_ installed BPF programs. You can see the BPF instructions (after initial translation by the kernel) with:: bpftool prog dump xlated id # or, in this specific case, just: bpftool prog dump xlated name do_nanosleep You cen see what maps are being used with `bpftool map `_:: bpftool map In this case, bpftrace uses ``perf_event_array`` to implement its ``printf``. You may read these events with:: bpftool map event_pipe id libbpf ------ .. important:: When building libbpf out-of-tree, you will need to provide it with information about non-stable functions/structures (such as when you modify the BPF facilities). You may extract these with:: sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h .. admonition:: Hands-on Let's rewrite our probe with C: .. code:: c #define BPF_NO_GLOBAL_DATA #include #include #include SEC("kprobe/do_nanosleep") int handle(void *ctx) { int pid = bpf_get_current_pid_tgid() >> 32; bpf_printk("PID %d is sleeping", pid); return 0; } char LICENSE[] SEC("license") = "GPL"; And compile it with:: clang --target=bpf -g -Og -c example.bpf.c -o example.bpf.o You may use ``llvm-readelf`` and ``llvm-objdump`` to inspect that file. If you have a modern version of bpftool (e.g., compiled in ``linux-6.12.6/tools/bpf/bpftool`` with ``make``), you can just run:: bpftool prog load example.bpf.o /sys/fs/bpf/example autoattach If your version does not support the 'autoattach' option yet, you will have to use libbpf for loading the program. The simplest way is to generate a skeleton file like:: bpftool gen skeleton example.bpf.o name example > example.skel.h And write a loader file like: .. code:: c #include #include "example.skel.h" int main() { struct example *skel; int err = 0; skel = example__open(); if (!skel) goto cleanup; err = example__load(skel); if (err) goto cleanup; err = example__attach(skel); if (err) goto cleanup; pause(); cleanup: example__destroy(skel); return err; } Which may be compiled and executed with:: gcc example.user.c -o example.user -lbpf ./example.user In either way, open the trace printk log with:: bpftool prog tracelog And execute a sleep program in another terminal. Programy BPF od strony jądra ============================ Podstawowa ścieżka uruchomienia programu BPF rozpoczyna się od użycia funkcji ``bpf_prog_load``, która otrzymuje typ programu BPF wraz z listą instrukcji. Funkcja ta powoduje weryfikację oraz załadowanie programu, a następnie zwraca numer deskryptora pliku powiązanego z programem. Deskryptor ten można następnie wykorzystać np. przekazując go jako argument funkcji ``setsocketopt`` lub ioctl z requestem ``PERF_EVENT_IOC_SET_BPF``. Weryfikacja programów BPF -------------------------------------- Ponieważ program BPF jest dostarczany z programu użytkownika, a wykonany w trybie jądra, potrzebna jest dodatkowa weryfikacja poprawności programu, aby zapobiec zarówno nieuprawnionym dostępom do pamięci jak i przypadkowym błędom mogącym zawiesić cały system. Weryfikacja przebiega w dwóch etapach. Pierwszy etap sprawdza między innymi: - Uprawnienia użytkownika (domyślnie tylko użytkownicy z ``CAP_BPF`` mogą ładować programy). - Rozmiar programu (maksymalnie dopuszcza się ``BPF_MAXINSNS`` instrukcji, w naszej wersji to 4096). - Obecność pętli. Od jądra 5.3 dopuszczone są ograniczone pętle (ang. bounded loops) dla których łatwo dowieść własność stopu. - Wywołania funkcji. Generalnie nie można wołać funkcji które nie należą do grupy BPF helpers. - Osiągalność wszystkich instrukcji. Drugi etap jest bardziej skomplikowany. Weryfikator zaczyna od pierwszej instrukcji programu i stara się zbadać wszystkie możliwe przebiegi programu, jednocześnie weryfikując jego stan, zawartość rejestrów oraz operacje które są na nich wykonywane. Do weryfikacji stanu jest użyta struktura ``bpf_reg_state`` dostępna w ``include/linux/bpf_verifier.h`` przechowująca między innymi typ wartości (``bpf_reg_type`` w ``include/linux/bpf.h``). Wartość może mieć typ ``NOT_INIT``, ``SCALAR_VALUE`` lub jeden z typów wskaźnika (np. ``PTR_TO_CTX``, ``PTR_TO_STACK``, ``PTR_TO_PACKET``). Operacje na wskaźnikach mogą zmienić ich typ, np. dodając dwa ``PTR_TO_CTX`` otrzymujemy ``SCALAR_VALUE`` i od tego momentu nie możemy już pamięci spod tej wartości (mogłoby to umożliwić nieuprawniony dostęp do pamięci). Przykłady --------- Implementację funkcji pomocniczych można znaleźć w ``bpf/helpers.c``. Przy implementacji nowego typu programów warto wzorować się na innych, relatywnie prostych jak np. ``bpf/cgroup.c``. .. _small_task_5_pl: Małe zadanie #5 =============== Zaimplementuj program show_bt wyświetlający backtrace dla wywołań funkcji w kodzie jądra wykonanych przez ostatnie 5 sekund. Przykładowo wywołanie ``./show_bt vfs_write`` podczas którego zostaną zapisane dane do pliku (przez inny proces) powinno wyświetlić na stdout backtrace (kodu wywołanego w trybie jądra) dla tego wykonania. Jeśli podczas wykonania programu show_bt funkcja w kodzie jądra zostanie wykonana wiele razy i te wywołania generują różny backtrace, należy wypisać każdy z nich. Do rozwiązania załącz informację dla jakich funkcji program nie działa. Dlaczego? Wskazówka: można wykorzystać bcc, w szczególności pomocna może być funkcja ``attach_kprobe``. Preparing for the Large Assignment ================================== Build your own kernel image that will be able to run examples provided today. You may start from the config provided for the :ref:`z2-ebpf` or the one used to build this image :download:`config-6.12.6zsobpf`. If you want to start from your, config, you need to enable several flags in various places. In ``menuconfig`` visit / and enable at least: - 'General setup' -> 'BPF subsystem': ``CONFIG_BPF=y``, ``CONFIG_BPF_SYSCALL=y``, ``CONFIG_BPF_JIT=y``, and ``CONFIG_BPF_EVENTS=y`` - 'Kernel hacking' -> 'Tracers': ``CONFIG_DYNAMIC_EVENTS=y``, ``CONFIG_KPROBES``, ``CONFIG_FUNCTION_TRACER``, ``CONFIG_DYNAMIC_FTRACE``, ``CONFIG_FPROBE``, ``CONFIG_FTRACE_SYSCALLS``, ``CONFIG_FPROBE_EVENTS``, ``CONFIG_KPROBE_EVENTS`` for examples on this lab (kprobes) - 'General setup': ``CONFIG_IKHEADERS=y`` for ``bcc`` - Under 'Kernel hacking': ``CONFIG_DEBUG_KERNEL`` + ``CONFIG_DEBUG_INFO_BTF`` -- enabling these will likely more than 1GB RAM during build You may attempt to build the examples located at `samples/bpf `_, however this will most likely fail unless you use their reference config. You may find the `cilium guide `_ useful here. Readings and Extra Learning =========================== There is a nice free book by Liz Rice available here: https://isovalent.com/books/learning-ebpf/ There is also a modern tutorial available here: https://github.com/eunomia-bpf/bpf-developer-tutorial Referencje =================== - [1] https://ebpf.io/ - [2] https://www.brendangregg.com/blog/2021-06-15/bpf-internals.html - [3] https://www.brendangregg.com/blog/2019-12-02/bpf-a-new-type-of-software.html - [4] https://www.brendangregg.com/bpf-performance-tools-book.html - [5] https://kinvolk.io/blog/2020/09/performance-benchmark-analysis-of-egress-filtering-on-linux/ - [6] https://pchaigno.github.io/ebpf/2020/09/29/bpf-isnt-just-about-speed.html - [7] https://www.usenix.org/legacy/publications/library/proceedings/sd93/mccanne.pdf - [8] https://www.kernel.org/doc/html/latest/bpf/instruction-set.html - [9] https://github.com/iovisor/bcc/