Hlavní navigace

Záplatování jádra za běhu: kGraft to zvládne bez výpadku

Petr Krčmář

Linuxový server se musí kvůli software restartovat v jediném případě: když je potřeba vyměnit jádro. S technologií kGraft už ale nutně neplatí ani to. Dokáže za plného provozu vyměnit jednu funkci za druhou.

Živé záplatování jádra umožňuje zjednodušit správu velkých serverů a opravit nejvážnější problémy za běhu, říká Jiří Kosina z pražské pobočky společnosti SUSE. Máme tu velkou skupinu jaderných vývojářů, kteří se zabývají ovladači, plánovačem a podobně, řekl na začátku při představování Jiří Kosina. Nejnovější věc, na které tu teď pracujeme, je patchování linuxového jádra za běhu. Často se prý lidé vývojářů ptají, k čemu je něco takového vůbec potřeba, když reboot nic nestojí. Děláme to hlavně proto, protože to po nás chtěli zákazníci. Reboot celého datacentra taky není zadarmo, stojí to obrovské náklady.

Velká datacentra mají přesně naplánované údržbové cykly, většinou v řádech měsíců. Každý neočekávaný restart systémů přináší další náklady. Když přijde bezpečnostní problém nebo se objeví problém se stabilitou, který by jim to mohl shodit, je pro ně hodně drahé operativně naplánovat restart celého datacentra. Firmy proto chtějí mít možnost zabezpečit jádro okamžitě a pak mají čas v bezpečném stavu počkat do dalšího okna pro plánovanou údržbu.

Jiří Kosina, SUSE

Tohle je ovšem jen nepřímý důsledek toho, že nabootovat enterprise server může trvat třeba tři čtvrtě hodiny. Když máte šestnáct terabajtů RAM, bude jen její inicializace biosem trvat klidně čtyřicet minut. I z tohoto důvodu se to nedá udělat rychleji, vždycky vás to bude stát čas, během kterého server nepracuje, vysvětlil Kosina.

kGraft nepotřebuje přestávku

Vývojáři SUSE vytvořili pro záplatování za běhu vlastní technologii kGraft, kterou už komerčně nabízejí a zákazníci ji mají reálně nasazenou. Trochu jsme tu porušili naši běžnou filosofii, kdy obvykle nejprve všechno posíláme do upstreamu Linusovi. kGraft je ale hodně nová věc, která je poměrně rozsáhlá. Nejprve jsme chtěli mít nějaká reálná nasazení a uživatele a teď to celé po kouskách posíláme do jádra. V podstatě jsme s tím hotovi. Ve zdrojových souborech jádra je už volba CONFIG_LIVEPATCH, která zapíná patřičné API pro aktualizaci funkcí jádra za běhu.

Příklad serveru, který nechcete restartovat

Patch pro živé záplatování jádra má zvláštní formát a SUSE v této formě neposkytuje zdaleka všechny záplaty. Není to možné, protože to není v našich silách, je to hromada ruční práce. Proto se záplaty omezují na bezpečnostní problémy, chyby způsobující nestabilitu nebo potenciálně poškozující data. Pokud tedy víme, že v jádře je chyba umožňující získat roota nebo je někdo schopen úmyslně poškodit data, vytvoříme live patch. V této formě se tak například nedistribuují nové ovladače pro hardware.

Vývojáře prý na začátku překvapilo, že jádro nebylo nutné nijak zvlášť upravovat. Vytvořili jsme celou novou infrastrukturu, ale do stávajících kódů jsme zasahovali poměrně málo. Později se objevily okrajové případy, kdy bylo potřeba upravovat zdrojové kódy více. Například záplatování plánovače je velmi zajímavé, nebo jsme doplnili možnost opravy samotného kGraft. Taky je to hodně komplikované, ale jde to.

Výhodou kGraft je fakt, že se jádro během záplatování vůbec nezastaví. Možná jste slyšeli o Ksplice, který ale funguje jinak. Pošle všem procesorům přerušení, všechny se sejdou v nekonečné smyčce, pak se vymění jádro a zase se spustí. Nedojde sice k restartování systému, ale je tam potenciál pro výpadek v řádu stovek milisekund, kdy jádro nemůže vůbec nic dělat. My jsme zvolili jiný přístup, kdy se činnost jádra vůbec nezastaví a kód se postupně přesune do nového stavu.

Existuje také ještě varianta zvaná Kexec, která ale dělá ve skutečnosti něco jiného – nechá nabootovat nové jádro místo starého. Ušetříte tím sice čas inicializace hardware, ale popadají vám všechny procesy, soubory i TCP spojení. Měníte prostě celé jádro se vším všudy.

Příprava je nutná

Jak tedy provést živou úpravu kódu tak, aby se nic nerozbilo? Používáme tam několik zajímavých triků. Celé jádro je zkompilované pomocí GCC s parametrem -pg, který vytvoří profilovací kód. Na začátek každé funkce překladač přidá skok do profilovací funkce, od které se očekává, že bude měřit, kolikrát se daná funkce použila, jak dlouho trval její běh a podobně. V jádře se ale tato vlastnost nepoužívá, takže vývojáři využijí přidané volání a „ukradnou si ho“ pro sebe. Na tohle místo jsme schopni v případě potřeby přidat přesměrování na opravenou verzi funkce, popisuje základní princip Kosina.

Jiří Kosina, SUSE

Při startu jádra se pak všechna přidaná volání upraví tak, že obsahují jen instrukci NOP . Ta nic nedělá (NOP = no operation) a jádro běží normálním způsobem. Všechny jaderné funkce mají na začátku pět bajtů, které se ale při provádění přeskočí a nijak běh neovlivňují. Takto připravené místo je pak možné kdykoliv později použít pro vložení instrukceJUMP následované adresou nové funkce. Tímhle trikem přesměrujeme jádro na jinou funkci a zbytek té původní už se pak nikdy neprovede.

V současné době je záplatování za běhu podporováno na platformě x86, pracuje se také na S/390, PowerPC a ARM64. Přidávat další architektury není problém, většina kódu je společná, musíme jen naučit GCC přidávat správně prology funkcí a to je vlastně všechno.

Upravujeme jádro za plného provozu

Jak pak probíhá samotné přesměrování? Zásadní je, že změna musí být atomická a nikdy nesmí dojít k tomu, že by byla funkce rozbitá. V každou chvíli totiž může být jádrem zavolána a musí proběhnout bez zastavení a podle očekávání. Prolog funkce musí mít pět bajtů (instrukce JUMP plus adresa), ale platforma x86 dovoluje atomicky vyměnit pouze čtyři bajty. My se ale nesmíme dostat do situace, kdy máme už čtyři bajty vyměněné a pátý ještě ne a jádro nám do takto rozpracované funkce skočí.

Proto se použije další zajímavý trik, který umožňuje chytře atomicky vyměnit všechny bajty „najednou“. Když chceme začít záplatovat, naplníme si připravené místo pěti jednobajtovými instrukcemi INT 3, což je softwarový breakpoint. Pokud by v kteroukoliv chvíli procesor chtěl funkci vykonávat, narazí na začátku na instrukce NOP nebo INT 3. Breakpoint v tomto případě zpracovává samo jádro, které ví o probíhajícím záplatování a proto rovnou skočí zpět za pětibajtový prolog. Chováme se k tomu tedy tak, jako by tam žádný breakpoint nebyl, a funkce normálně proběhne.

Postupná výměna adresy v prologu

Pět breakpointů je postupně odzadu vyměněno za adresu nové funkce. V jakoukoliv chvíli je před adresou stále alespoň jeden breakpoint, takže přepis může trvat libovolně dlouho a funkce je stále vykonavatelná v původní podobě. Teprve až v posledním kroku nahradíme první breakpoint instrukcí JUMP a tím dokončíme přesměrování ze staré podoby funkce na novou. Při tomto postupu mají vývojáři vždy jistotu, že procesor nenarazí na nekonzistentní data, na kterých by havaroval.

Upravená funkce tak nakonec začíná skokem do kódu kGraft, který se musí rozhodnout, kam bude skok pokračovat. O zmíněných pět bajtů se dělíme ještě s funkcí ftrace, která dovoluje jádru zapínat různé mechanismy pro sledování chování jednotlivých funkcí. Proto musí být mezi novou a starou podobu funkce vložena logika, která rozhoduje, co bude po skoku následovat. Pokud je to náš skok kvůli upravené funkci, následuje další skok na novou podobu, která opět začíná pěti nepoužívanými bajty. To pro případ, kdy by bylo nutné někdy později funkci znovu opravit. Je možné buď znovu zasáhnout do volání původní funkce nebo jednotlivé skoky řetězit.

Průběh funkce před a po opravě

Jeden modul vládne všem

Jádro je možné záplatovat po jednotlivých funkcích, ale z praktických důvodů se zákazníkům distribuuje vždy jedna velká kumulativní záplata ve formě běžného modulu .ko. Ten poslední vždycky obsahuje všechno, takže s poslední verzí vždycky měníme všechny už záplatované funkce. Vůbec to nevadí, protože doba záplatování podle Kosiny nezávisí na tom, kolik funkcí se upravuje. Jedna velká úprava je pro nás výhodnější, protože vždycky víme, v jakém stavu se systém nachází. V opačném případě by si uživatel náhodně aplikoval různé změny a množiny záplat a my bychom nebyli schopni takový systém podporovat.

Tvorba záplaty je z velké části ruční prací, což má podle Jiřího Kosiny výhodu zejména v tom, že vývojář může skutečně celý kód projít a zkontrolovat. Většinou je to celá skupina funkcí, protože se většinou ukáže, že kvůli jedné chybě je toho potřeba vyměnit víc. Problém je, že tyto funkce jsou na sobě závislé a je potřeba opět udržet konzistenci. Může se stát, že nová verze funkce má jiný počet parametrů, což se musí zohlednit i v ostatních funkcích. Musí se tedy vždy zajistit, že se mezi sebou nemíchají různé generace funkcí. Stará volá starou nebo už nová novou, nikdy ne křížem.

Jádro tedy navíc hlídá, v jakém stavu je rozpracované záplatování a pouští jednotlivé procesy vstupující do jádra buď starou cestou nebo už novou. Říkáme tomu, že hlídáme, v jakém vesmíru ten daný proces je. Jestli je ve starém nebo už v novém. Podle tohohle kontextu ho pak na rozhraní uživatelského a jaderného prostoru pouštíme do správných funkcí. Postupně se tedy jednotlivé procesy označují podle toho, zda už jejich funkce byly záplatovány nebo ne. Až jsou všechny procesy označené, můžeme prohlásit, že záplatování jádra skončilo a vše už běží na novém kódu. Jádro tak vlastně postupně „dopluje“ do migrovaného stavu.

Rozhodování na vstupu do jádra

Tento proces zatím ovšem neumí upravovat datové struktury v jádře. Umíme jen vyměnit kód, ale nedokážeme to ve chvíli, kdy se zároveň mění data. Museli bychom vyhledat všechny instance těchto dat v paměti, což není možné, a zároveň všechen kód, který s nimi pracuje. Naštěstí se to u bezpečnostních záplat nestává příliš často. Za rok a půl jsme dělali několik stovek záplat a stalo se nám jednou, že jsme museli zasahovat do dat. Podle Kosiny má ale i tohle několik způsobů řešení a vývojáři nich pracují. Není to velký problém, ale chtěli bychom ho do budoucna vyřešit.

Oprava za jízdy

Výhodou technologie kGraft je, že skutečně ani na okamžik nezastavuje činnost opravovaného serveru. Nevýhodou je, že záplaty je nutné vytvářet ručně. Podle Kosiny to ale v praxi není problém, protože to v SUSE zvládá jediný vývojář. Nejsložitější je obvykle pochopit, co daná úprava dělá, což nemusí být triviální. Ale daří se nám vydávat opravy ve stejnou chvíli, kdy je distribuujeme ve standardní podobě. Uživatelé si tak mohou vybrat. Vývojáři zároveň podporují až dvacet různých verzí jádra v různých produktech. Uživatelé nechtějí restartovat servery, takže jim nemůžeme říct, že podporujeme jen nejnovější jádro a oni mají restartovat a vyměnit si ho.

S během záplatovaných funkcí je samozřejmě spojena také nějaká režie. Je to několik instrukcí na funkci navíc, což může být hodně, ale také nemusí. Pokud se funkce volá velmi sporadicky, nemá její záplata na výkon systému žádný vliv. Pokud se ale naopak používá velmi často, začne se zdržení projevovat. Dokud ale není funkce nijak záplatovaná, její úprava se nijak neprojevuje. Jen zabírá v paměti o pět bajtů více, takže jádro je mírně nabobtnalé.

Celkem prý vývojáři v SUSE za rok a půl vydali téměř tisíc takových záplat. Jejich množství ale rozhodně není stabilní, někdy se neděje nic a pak někdo objeví chybu a ostatní se na ni vrhnou a objeví hromadu dalších.

Našli jste v článku chybu?