← Wszystkie notatki

Nasze własne badaniaDziałaj

Pytaj listę, nie pole: nasz use-after-free w kernelu Linuksa

Fuzzing znalazł use-after-free w kodzie, który przechowuje skojarzenia bezpieczeństwa IPsec. Poprawka jest nasza i siedzi dziś w każdej utrzymywanej gałęzi stable. Sam crash jest tu najmniej ciekawy. Ciekawe jest pytanie, które zadawał kod.

Opublikowano
Tematy
kernel Linux · CVE · fuzzing · use-after-free · zgłaszanie podatności
0crashy xfrm_state w 7,1 mln wykonań fuzzera po nałożeniu poprawki

Tym razem to nasze. Fuzzing kodu, który przechowuje skojarzenia bezpieczeństwa IPsec, wykrył use-after-free. Napisaliśmy poprawkę i dziś jest ona w każdej utrzymywanej gałęzi stable Linuksa. Piszemy o tym, bo kształt tego błędu jest bardziej pouczający niż sam crash.

Pytanie, które zadawał kod

Kiedy kernel usuwa stan IPsec, musi zdjąć obiekt z czterech list haszujących. Przy dwóch z nich kod decydował o zdjęciu na podstawie wartości: czy stan ma numer sekwencyjny, czy ma SPI. To zastępnik pytania, które naprawdę ma znaczenie: czy obiekt w ogóle jest na liście.

Zwykle zastępnik i prawda się zgadzają. Przy współbieżności potrafią się rozjechać, a wtedy ścieżka usuwania albo pomija zdjęcie z listy, którego potrzebowała, albo wykonuje takie, którego nie powinna. Drugie usunięcie tego samego obiektu pisze wtedy przez wskaźnik zatruty za pierwszym razem. Ta sama funkcja zadawała już właściwe pytanie przy dwóch nowszych listach. Starszych nikt nie wyrównał.

Przed i po, dwie listy z warunkiem
przed:
if (x->km.seq)
        hlist_del_rcu(&x->byseq);
if (x->id.spi)
        hlist_del_rcu(&x->byspi);

po:
if (!hlist_unhashed(&x->byseq))
        hlist_del_init_rcu(&x->byseq);
if (!hlist_unhashed(&x->byspi))
        hlist_del_init_rcu(&x->byspi);

Dwie zmiany i każda robi co innego. Pytanie listy zamiast pola sprawia, że decyzja śledzi rzeczywistość. Ponowna inicjalizacja węzła przy usuwaniu sprawia, że drugie usunięcie jest nieszkodliwym no-opem, a nie zapisem przez zatruty wskaźnik.

Pole to deklaracja o stanie

To ten sam błąd, o którym ciągle piszemy przy systemach agentowych, tylko warstwę niżej. Pole mówiące, że obiekt ma SPI, to deklaracja o tym, gdzie obiekt jest. Lista to fakt. Kod, który ufa deklaracji, działa, dopóki coś nie zmieni jednego bez drugiego, a współbieżność jest dokładnie tym czymś.

Agent, który ufa opisowi narzędzia zamiast temu, co narzędzie robi. Kontrola uprawnień, która ufa etykiecie zamiast zasobowi. Ścieżka usuwania, która ufa liczbie zamiast liście. Różne warstwy, jeden nawyk. Zaufanie to powierzchnia ataku, także zaufanie, jakim funkcja darzy własną księgowość.

Czego poprawka celowo nie robi

Nie tropi jednego konkretnego przeplotu, który zwalnia obiekt dwa razy. Sprawia, że usuwanie jest idempotentne, a warunek prawdziwy, więc skutek znika niezależnie od tego, która ścieżka dotrze pierwsza. Maintainerzy przyjęli ją właśnie na takich warunkach. Łatka na ten jeden wyścig, który akurat złapaliśmy, byłaby węższa i zostawiłaby miejsce następnemu.

Część, która nigdy nie trafia do CVE

Pierwsza wersja się nie nakładała. Poszła przez webowego klienta poczty, który wyciął z diffa wszystkie tabulatory, i nie miała tagu Fixes:. Maintainer poprosił o tag, opiekun gałęzi stable odpisał listem szablonowym z odnośnikiem do zasad stable, a ponowna wysyłka przez git send-email została przyjęta następnego ranka.

Ten tag to nie formalność. To mechanizm, który przenosi poprawkę do starszych gałęzi. Bez niego łatka wylądowałaby w mainline i na tym by się skończyło. Każda poprawka dalej w łańcuchu, wymieniona w kronice, istnieje dlatego, że jedna linia wskazała commity, które wprowadziły błąd.

Gdzie kończy błąd w kernelu

Crash znaleziony na maszynach wirtualnych w chmurze trafia do advisory przemysłowego sterownika i do długoterminowych kerneli korporacyjnych, które większość ludzi uważa za zamrożone. Dla firmy, która kupuje sprzęt, wniosek jest praktyczny: Twoja ekspozycja to nie wersja Twojego kernela. To każde urządzenie z Linuksem, którego nie budowałeś, łatane w rytmie dostawcy, a nie upstreamu.

Co zrobić

  • Przejdź na pierwsze wydanie ze swojej gałęzi stable, które ma poprawkę. Kronika je wymienia. Gałęzie długoterminowe dostały ją tygodnie po mainline, więc sprawdź, zamiast zakładać.
  • Jeśli nie możesz jeszcze łatać, przyjrzyj się nieuprzywilejowanym przestrzeniom nazw użytkownika. Niska ocena wymaganych uprawnień opiera się właśnie na nich. Hosty, które ich nie potrzebują, tracą tę ścieżkę.
  • Zapytaj dostawców urządzeń, na jakim są etapie. Sterowniki, firewalle i macierze działają na kernelach, których nie widzisz. Jeden dostawca przemysłowy oznaczył to jako podatne i nie ma jeszcze poprawionego firmware.
  • Jeśli przeglądasz kod kernela lub systemowy, szukaj warunków-zastępników. Każdy warunek, który sprawdza wartość zamiast przynależności, własności czy stanu, warto zastąpić sprawdzeniem samej rzeczy.

E/01Co jest potwierdzone
  • CVE-2026-46116, nadany przez CNA kernela Linux, opublikowany 28 maja 2026. Commit z poprawką 14acf9652e56, "xfrm: defensively unhash xfrm_state lists in __xfrm_state_delete", napisany, zgłoszony i przetestowany przez Michała Kosiorka z ArkLink. Przyjęty do drzewa ipsec przez jego maintainera 30 kwietnia 2026.
  • Znaleziony syzkallerem (746545b8) na Linuksie 6.12.47 zbudowanym z KASAN i KCOV, na 32 maszynach QEMU/KVM na AWS c5.metal. Dziewięć unikalnych sygnatur crashy w około dziewięć godzin, wszystkie w cyklu życia xfrm_state. Główna to 8-bajtowy zapis slab-use-after-free w __xfrm_state_delete, osiągany ze sprzątania przestrzeni nazw sieci.
  • Rozmiar poprawki: 6 linii dodanych i 6 usuniętych w net/xfrm/xfrm_state.c. Ten sam harness na załatanym kernelu przez 1 h 16 min dał 7,1 mln wykonań na 32 maszynach i zero trafień use-after-free lub out-of-bounds w xfrm_state, przy wciąż aktywnie alokowanym i zwalnianym slabie xfrm_state.
  • Wydania z poprawką: mainline 7.1-rc3 (10 maja 2026); stable 7.0.7, 6.18.30 i 6.12.88 (14 maja); 6.6.140 (17 maja); 6.1.176 i 5.15.210 (19 czerwca); 5.10.261 (24 lipca). 12 maja poprawka nie nałożyła się automatycznie na 6.6 i starsze i została przeniesiona ręcznie.
  • Rekord CVE oznacza jako podatny każdy kernel od 2.6.19 do tych wydań, zgodnie z dwoma tagami Fixes: w poprawce.
  • Ocena CNA kernela: CVSS 3.1 bazowo 7.8, AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. Uzasadnienie PR:L: xfrm sprawdza CAP_NET_ADMIN względem przestrzeni nazw użytkownika danej przestrzeni sieciowej, a tę nieuprzywilejowany użytkownik uzyska przez unshare -Urn. Red Hat ocenia na 7.0 z AC:H i klasyfikuje jako Important. Status w NVD na 7 października 2026: Undergoing Analysis.
  • Trzy etykiety słabości dla jednego błędu: CWE-416 od NVD, CWE-763 od Red Hat, CWE-20 od Siemensa.
  • Dalej w łańcuchu: rekord NVD wymienia 23 errata Red Hat, w tym kernel czasu rzeczywistego dla RHEL 7 Extended Lifecycle Support. Siemens SSA-019113 V1.1 (8 września 2026) wymienia jako podatny podsystem GNU/Linux w firmware V3.1.6 sterownika SIMATIC S7-1500 CPU 1518(F)-4 PN/DP MFP, w advisory obejmującym 432 CVE.
E/02Czego jeszcze nie wiemy
  • Czy da się to wykorzystać do czegoś więcej niż crash. Exploita nie budowaliśmy. C:H i I:H w ocenie CNA kernela opisują, na co prymityw mógłby pozwolić, a nie coś, co zademonstrowano, a AC:H od Red Hat oznacza odmienne zdanie co do niezawodności wyścigu.
  • Czy wyścig jest osiągalny w każdym kernelu, który rekord oznacza jako podatny. Zakres wynika z tagów Fixes:. Odtworzyliśmy go tylko na 6.12.47, a tę samą ścieżkę kodu potwierdziliśmy w mainline i drzewie ipsec.
  • Nie ma opublikowanego minimalnego, samodzielnego reproducera. Dowodem jest harness fuzzera i liczba crashy przed poprawką i po niej.
  • Żadna z ocen nie uwzględnia hostów z wyłączonymi nieuprzywilejowanymi przestrzeniami nazw użytkownika, gdzie uzasadnienie PR:L nie działa. Nie mierzyliśmy, jak częsta jest taka konfiguracja.
  • Siemens nie podaje w advisory poprawionego firmware dla tego CVE ani daty jego wydania.