Zadanie 2: filtr sygnałów

Data ogłoszenia: 30.03.2021

Termin oddania: 04.05.2021 (ostateczny 18.05.2021)

Materiały dodatkowe

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.