========================= Zadanie 2: filtr sygnałów ========================= Data ogłoszenia: 30.03.2021 Termin oddania: 04.05.2021 (ostateczny 18.05.2021) Materiały dodatkowe =================== - :download:`z2-tests.tar` (ostatnia modyfikacja: 27.04.2021) Wprowadzenie ============ Debugowanie programów w systemach unixopodobnych odbywa się przez użycie interfejsu ptrace. Możemy za jego pomocą zatrzymać proces, mieć dostęp do jego przestrzeni adresowej i rejestrów, a także przechwytywać sygnały przesyłane do programu. Przechwytywanie sygnałów jest ważne, gdyż są one używane do realizacji breakpointów — debugger może ustawić breakpoint sprzętowy (przez ustawienie specjalnych rejestrów debugowania) bądź programowy (przez wstawienie instrukcji breakpoint w kod programu), a napotkanie go w trakcie wykonania spowoduje dostarczenie przez jądro sygnału ``SIGTRAP`` do programu. Sygnał ten zostanie przechwycony przez debugger, który przekaże wtedy kontrolę programiście. Specjalnym rodzajem breakpointów są breakpointy warunkowe, czyli takie, które powinny zatrzymać program tylko jeśli zachodzi jakiś warunek (np. dana zmienna ma wartość ≥ 13). Klasyczna implementacja takich breakpointów jest dość nieefektywna — jedyne breakpointy obsługiwane przez sprzęt są bezwarunkowe, więc realizacja warunkowych breakpointów polega na dostarczeniu sygnału do debuggera za każdym razem, ewaluacji warunku w debuggerze, po czym wznowienia programu jeśli warunek nie zachodzi. Wymaga to dwóch przełączeń kontekstu na każde trafienie breakpointa. Na szczęście współczesne wersje jądra mają podsystem eBPF pozwalający na ładowanie prostych fragmentów kodu i wykonywanie ich w jądrze. Można użyć tego mechanizmu do ewaluacji warunku breakpointa bezpośrednio w jądrze, bez konieczności wołania debuggera za każdym razem. Zadanie ======= Dodać do syscalla ``ptrace`` możliwość wpięcia do śledzonego procesu filtra sygnałów — programu eBPF, który będzie wykonany przy dostarczaniu sygnału spowodowanego wyjątkiem procesora do procesu i zadecyduje, czy faktycznie należy dostarczyć sygnał, czy też go zignorować (tak, że nigdy nie zostanie dostarczony do debuggera, a program będzie kontynuował wykonanie). Tak zainstalowany program eBPF powinien mieć następujące możliwości: - dostęp (tylko do odczytu) do struktury siginfo opisującej sygnał - dostęp (do odczytu i zapisu) do rejestrów procesu - dostęp (do odczytu i zapisu) do przestrzeni adresowej procesu Ustalenia techniczne ==================== Należy dodać nowy typ programów eBPF: ``BPF_PROG_TYPE_SIGFILTER`` (mający numer o 1 większy od ``BPF_PROG_TYPE_SK_LOOKUP``). Należy dodać nowy typ podpięcia programów eBPF: ``BPF_SIGFILTER`` (mający numer o 1 większy od ``BPF_XDP``). Należy dodać dwie nowe podfunkcje do syscalla ``ptrace``: - ``PTRACE_SET_SIGFILTER`` (wartość ``0x420f``): parametr ``data`` jest (przerzutowanym na wskaźnik) deskryptorem pliku wskazującym na program eBPF typu ``BPF_PROG_TYPE_SIGFILTER``. Ustawia filtr sygnałów podanego procesu na podany program. Podany proces musi już być śledzony przez wywołujący proces. Jeśli podany proces ma już ustawiony filtr sygnałów, należy go zastąpić podanym. Zwraca 0 lub kod błędu. - ``PTRACE_UNSET_SIGFILTER`` (wartość ``0x4210``): usuwa filtr sygnałów z podanego procesu. Podany proces musi już być śledzony przez wywołujący proces. Zwraca 0 lub kod błędu. Gdy proces śledzący przestanie śledzić dany proces, należy automatycznie usunąć jego filtr sygnałów. Nowo utworzone procesy nie powinny mieć filtra sygnałów — nawet procesy utworzone przez ``clone`` przez proces z aktywnym filtrem sygnałów. Filtr sygnałów powinien być wykonywany przez jądro podczas dostarczania do procesu sygnałów wynikających z wyjątku procesora (``SIGTRAP``, ``SIGSEGV``, ``SIGFPE``, ``SIGILL``, ...). Dotyczy to tylko sygnałów faktycznie spowodowanych wyjątkiem — nie należy wykonywać filtra sygnałów w przypadku, gdy np. użytkownik ręcznie wyśle sygnał ``SIGTRAP`` przez syscall ``kill``. Podczas dostarczania sygnału dotyczącego wyjątku procesora, który normalnie zostałby dostarczony do procesu śledzącego, należy wywołać program wpięty jako filtr sygnałów dla danego procesu. Jeśli program ten zwróci wynik inny niż 0, należy zignorować sygnał (nie dostarczać go do procesu śledzącego, wznowić wykonanie procesu śledzonego). Jeśli program zwróci wynik 0 (bądź jego wykonanie zakończy się błędem), należy normalnie dostarczyć sygnał. Filtr sygnałów powinien zostać wywołany ze strukturą ``siginfo`` jako swoim kontekstem. Powinna to być 32-bitowa struktura jeśli proces śledzący (który zainstalował filtr) jest 32-bitowy, bądź 64-bitowa struktura jeśli proces jest 64-bitowy. Należy dodać 3 nowe funkcje dostępne dla nowego typu programów (i tylko dla nich): ``int bpf_getregset (unsigned type, unsigned long offset, void *ptr, unsigned long size)`` Czyta blok danych z podanego regsetu pod podany wskaźnik (analogicznie do ``PTRACE_GETREGSET``, choć bez struktury ``iovec``). Zwraca 0 dla udanego odczytu, bądź kod błędu. ``int bpf_setregset (unsigned type, unsigned long offset, const void *ptr, unsigned long size)`` Zapisuje blok danych do podanego regsetu (analogicznie do ``PTRACE_SETREGSET``, choć bez struktury ``iovec``). Zwraca 0 dla udanego zapisu, bądź kod błędu. ``int bpf_copy_to_user (void __user *uptr, const void *ptr, unsigned long size)`` Wrapper na ``copy_to_user``, analogiczny do istniejącej funkcji ``bpf_copy_from_user``. Zwraca 0 bądź ``-EFAULT`` w przypadku błędu. Powyższe funkcje powinny mieć kolejne numery po ``bpf_sock_from_file``. Nowy typ programów powinien mieć dostęp do następujących funkcji (i żadnych innych): - bazowy wspólny zbiór funkcji - ``bpf_copy_from_user`` - powyższe 3 funkcje Zasady oceniania ================ Za zadanie można uzyskać do 10 punktów. Na ocenę zadania składają się dwie części: - wynik testów (od 0 do 10 punktów) - ocena kodu rozwiązania (od 0 do -10 punktów) Najczęstsze błędy - spis oznaczeń w USOSwebie ============================================= 1. Uruchomienie testów wymaga uprawnień roota (-0.0) 2. Patch niezgodny ze specyfikacją (np. wymagane ``-p2``, bądź inny zestaw opcji niż podany w wymaganiach) (-0.1) 3. Warning w ``ptrace_check_attach`` podczas wykonania ``ptrace`` (-0.5) 4. Wyciek pamięci przy ``bpf_getregset`` (-0.3) 5. Wskaźnik `void*` w ``task_struct`` zamiast użycia adekwatnego typu (-0.0) 6. Trzymanie deskryptora w ``task_struct`` zamiast ``struct bpf_prog*`` (-0.5) 7. Brak zwalniania istniejącego eBPF przy wielokrotnym wykonaniu ``PTRACE_SET_SIGFILTER`` (-0.3) 8. CPU pinning przy uruchamianiu eBPF (-0.3) 9. Potencjalne user-after-free: brak czyszczenia wskaźnika ``struct bpf_prog*`` w procesie potomnym (-0.5) 10. Brak konwersji przekazywanej struktury dla trybu 32-bit (-0.3) 11. Nieprawidłowy limit przy walidacji uprawnień dostępu do pamięci (-0.2) 12. Przechowywanie informacji w ``task_struct`` o trybie (32/64-bit) przy wyłączonym ``CONFIG_COMPAT`` (-0.0) 13. Budowanie kodu w przypadku gdy BPF nie jest włączony w konfiguracji jądra (-0.1) 14. Wyciek: brak gwarancji zwolnienia programu eBPF przy zakończeniu procesu śledzącego (-0.3) 15. Problemy z lockami (w sytuacji gdy proces śledzący kończy pracę przed procesem śledzonym) (-0.5) Forma rozwiązania ================= Jako rozwiązanie należy wysłać paczkę zawierającą: - patcha na jądro w wersji 5.11.2, w jednym z następujących formatów: - patch wygenerowaniy przez diffa z opcjami ``-uprN`` nakładający się przez ``patch -p1`` - ``git format-patch`` - krótki opis rozwiązania Rozwiązania prosimy nadsyłać na adres ``p.zuk@mimuw.edu.pl`` z kopią do ``mwk@mimuw.edu.pl``. Wskazówki ========= Dobrym miejscem do wpięcia się w proces dostarczania odpowiednich sygnałów jest funkcja ``force_sig_info_to_task``.