-
Notifications
You must be signed in to change notification settings - Fork 0
Jadro a uzivatelsky rezim
Program, který dělí nulou nebo sáhne za konec pole, dostane SIGFPE nebo SIGSEGV a skončí. Stroj běží dál a ostatní procesy o tom nevědí.
Napiš tu samou chybu do modulu jádra. Stroj se zastaví uprostřed věty - žádné hlášení, jen studený restart a rozpracovaná data všech ostatních v čudu.
Tentýž kód, tatáž chyba, tentýž procesor. Liší se jen stav, ve kterém ho procesor provádí: na x86-64 dva bity v selektoru registru CS, které v praxi mají dvě hodnoty. Rozdíl mezi „spadl program" a „spadl systém" je doslova jeden bit.
Tahle stránka je o té hranici: čím je držená, kudy se přes ni chodí a kde neplatí. Neřeší, jak se sestavuje systémové volání, ani jak se překládají adresy - to je virtuální paměť a stránkování. Předpokládá, že víš, co je proces a že operační systém je správce zdrojů i ochranná vrstva.
Rozšířená představa je, že operační systém někde kontroluje, co proces smí. Nekontroluje - u každé instrukce by musel běžet nějaký kód, a ten by taky musel někdo hlídat.
Tohle je nejdůležitější věc na celé stránce: procesor sám o sobě ví, v jakém režimu běží, a část instrukcí v tom nižším odmítne provést. Není to kontrola, kterou by dělal software. Je to hardware.
Jádro jen nastaví pravidla - tabulky stránek, CR4, tabulku obsluh přerušení - a předá řízení uživatelskému kódu. Od té chvíle hlídá křemík. Zbytek stránky je rozvedení téhle věty.
x86-64 nabízí čtyři úrovně oprávnění, prstence 0 až 3. Nula je jádro, trojka uživatelský kód. Prstence 1 a 2 nepoužívá Linux, Windows ani nikdo jiný, koho by sis nainstaloval.
Stránkování zná jen dvě úrovně. Položka tabulky stránek má jediný bit U/S a prstence 1 a 2 se z jeho pohledu počítají jako privilegované - kód v jedničce by stejně viděl celou paměť jádra. Druhý důvod je přenositelnost: ostatní architektury čtyři prstence nikdy neměly.
Použití pro ně se kdysi našlo (OS/2, Xen), ale skončilo s přechodem na 64 bitů. Ve schématech ty čtyři kroužky zůstaly.
ARM64 to řeší poctivěji: EL0 je aplikace, EL1 jádro, EL2 hypervizor, EL3 firmware - a používají se všechny čtyři.
Zakázané je přesně to, čím by šlo hranici zrušit.
| Instrukce | Co dělá | V prstenci 3 |
|---|---|---|
hlt |
uspí jádro procesoru do dalšího přerušení | #GP |
lgdt, lidt
|
přepíše tabulku deskriptorů nebo obsluh přerušení | #GP |
mov cr3, rax |
vymění kořen tabulek stránek, tedy celý adresní prostor | #GP |
in, out
|
přímé čtení a zápis na I/O port |
#GP podle IOPL a bitmapy v TSS |
#GP je obecná ochranná výjimka, vektor 13, a jádro ji u uživatelského kódu překlopí na SIGSEGV. Ne na SIGILL - ten patří výjimce #UD, tedy instrukci, kterou procesor nezná. Privilegovanou zná, jen ji odmítá.
# jediná privilegovaná instrukce v jinak prázdném programu
printf 'int main(void){ __asm__ volatile("hlt"); }' > /tmp/g.c
gcc -o /tmp/g /tmp/g.c && /tmp/g; echo "navratovy kod: $?"Dostaneš Segmentation fault a kód 139, a stroj se ani nezachvěje. To je celý ten vtip.
Staré návody sahají na porty voláním iopl(3) a dnes je to špatná rada. Chce to schopnost CAP_SYS_RAWIO (viz práva a uživatelé), při zapnutém Secure Bootu neprojde vůbec a správně se píše ovladač.
Přerušení je asynchronní a přichází zvenku: dorazil paket, doběhlo čtení z NVMe, tikl časovač.
Výjimka je synchronní a způsobí ji sama prováděná instrukce - výpadek stránky, dělení nulou, #GP. Obojí má vlastní stránku, přerušení a výjimky.
Systémové volání je jediná dobrovolná cesta: program si o vstup sám řekne instrukcí syscall a jádro do něj vstoupí na jediném předem daném místě - viz systémová volání.
Čtvrtá cesta neexistuje. Když ji někdo najde, nejmenuje se to cesta, ale zranitelnost, a dostane číslo CVE.
Skok mění jediný registr, RIP. Přechod do jádra mění naráz tři věci: úroveň oprávnění, zásobník a to, co je z paměti vidět. Vlastní zásobník je nutnost - na tom procesním by proces jádru přepisoval návratové adresy.
U přerušení a výjimek zásobník přepne hardware sám z pole RSP0 v TSS, dřív než na něj uloží starý stav. U syscall ne - ta instrukce jen změní prstenec, uloží návratovou adresu do RCX a příznaky do R11; zbytek si jádro udělá samo.
Každé vlákno má vlastní jádrový zásobník, na x86-64 velký 16 KiB. Proto přerušení nerozbije stav procesu: uloží se do zásobníku toho vlákna, které běželo, obslouží se a proces pokračuje tam, kde skončil. Kdyby ho vlákna sdílela, nešlo by uprostřed obsluhy přepnout kontext.
Prstenec řeší instrukce, paměť řeší MMU. Stránky jádra mají bit U/S privilegovaný, takže proces v prstenci 3 na ně dostane výpadek stránky, i kdyby znal přesnou adresu - viz virtuální paměť a stránkování. Jádro je přitom namapované v adresním prostoru každého procesu, aby se při vstupu nemusel měnit CR3. Ten kompromis se v roce 2018 vymstil.
Útok na jádro nepotřebuje kód v jádře. Stačí ho přimět, aby skočilo na adresu ve tvé paměti - prstenec se skokem nemění, takže tvoje instrukce poběží v nule.
SMEP (u Intelu od Ivy Bridge, 2012) to ukončí: procesor v prstenci 0 odmítne instrukci ze stránky označené jako uživatelská. SMAP (od Broadwellu, 2014) zakáže jádru z uživatelských stránek i číst a zapisovat, kromě míst, kde si to samo odemkne instrukcemi stac a clac.
grep -o -e ' smep' -e ' smap' /proc/cpuinfo | sort -u # obojí by mělo být vidětKPTI (Kernel Page Table Isolation, v Linuxu od jádra 4.15, leden 2018) ten kompromis ruší: proces má dvě sady tabulek stránek a v té uživatelské není z jádra skoro nic, jen trampolína, která přepne CR3 na tu druhou.
cat /sys/devices/system/cpu/vulnerabilities/meltdown # „Mitigation: PTI" = KPTI běžíMeltdown (CVE-2017-5754, zveřejněný 3. ledna 2018) dělal tohle: uživatelský program přečetl adresu patřící jádru. Procesor přístup nakonec zamítl a poslal výjimku - jenže než to stihl, spekulativně už s hodnotou pracoval a použil ji jako index do vlastního pole. Do architektonicky viditelného stavu se nedostalo nic, do cache ano, a program ji odtud přečetl podle časů přístupu.
Protože jádro má v přímém mapování celou fyzickou paměť, znamenalo to čtení prakticky čehokoli: klíčů, hesel, obsahu cizích procesů. Spectre je jiná rodina - neláme hranici jádra, ale přes špatně předpovězenou větev donutí oběť prozradit vlastní data.
Oprava se jmenuje KPTI, takže se za každý vstup do jádra platí přepnutím CR3, na procesorech bez PCID navíc vyprázdněním TLB. Volání za 50 až 100 ns se dostalo i na 500 ns a zátěže s hodně voláními ztratily procenta až desítky procent.
Poučení: hranice může být architektonicky správně nastavená a přesto téct. Kontrola oprávnění proběhla, jen až potom, co procesor data spekulativně použil.
- července 2024 dostal senzor CrowdStrike Falcon rutinní aktualizaci obsahu - ne novou verzi ovladače, jen konfigurační soubor s pravidly. Interpret uvnitř ovladače, který na Windows běží v režimu jádra, čekal jednadvacet vstupních polí a dostal dvacet. Sáhl mimo a přečetl neplatnou adresu.
V uživatelském režimu by to byl jeden spadlý proces a restart služby. V režimu jádra to byla modrá smrt na zhruba 8,5 milionu strojů podle odhadu Microsoftu: stála letiště, nemocnice i banky. Opravu navíc nešlo rozeslat po síti - stroje padaly dřív, než ji stihly stáhnout.
Poučení: kód v jádře nemá jak spadnout jen trochu. Nemá nad sebou nikoho, kdo by ho zabil a nechal zbytek běžet.
Zařízení s DMA vidí paměť mimo procesor. Řadič disku i síťovka zapisují do fyzické paměti přímo a MMU sedí v procesoru, takže na ně nedosáhne. Proto existuje IOMMU (Intel VT-d, AMD-Vi), která postaví tabulku stránek i před zařízení; o DMA je vstup a výstup.
ls /sys/class/iommu/ # prázdný výstup = zařízení sahají do paměti bez dohleduFirmware a SMM sedí nad jádrem. System Management Mode se spustí na přerušení SMI a přepne procesor do režimu s vlastní pamětí, do které jádro nevidí; pozná to nanejvýš podle toho, že mu z hodin zmizel čas. Ten kód dodal výrobce desky, viz start systému.
Hypervizor sedí pod jádrem. Ve virtuálním stroji je prstenec 0 jen zdání - skutečná nula patří hypervizoru s vlastní úrovní a druhou sadou tabulek stránek; viz virtualizace a kontejnery.
eBPF je řízená mezera. Cizí program běží přímo v jádře, ale jen když projde verifikátorem, který před nahráním dokáže, že skončí a nesáhne mimo povolenou paměť. Je to dnes jediný rozumný způsob, jak jádro rozšířit bez psaní modulu, a zároveň velká plocha útoku - chyba ve verifikátoru je rovnou eskalace na root. Neprivilegovaným uživatelům je nahrávání vypnuté a nech to tak.
Začni kruhovým bufferem jádra a příznakem znečištění. Po pádu je to jediná stopa, kterou máš:
dmesg -T | tail -30 # -T přepíše časy na čitelné
cat /proc/sys/kernel/tainted # 0 je čisté jádro, jinak součet příznakůUžitečné příznaky: 128 (jádro už jednou spadlo), 512 (proběhl WARN), 4096 (modul mimo strom jádra), 8192 (nepodepsaný modul). U 4096 máš podezřelého dřív, než začneš číst výpis.
Oops není panika. Oops je hlášení, že jádro sáhlo někam, kam nemělo: zabije vlákno, které to způsobilo, nastaví příznak 128 a jede dál - se strukturami, které to vlákno nechalo rozdělané, a se zámky, které už neuvolní. Systém po oopsu funguje, ale nevěř mu a restartuj ho.
Panika je konec: oops v obsluze přerušení, oops pod zámkem nebo mrtvý PID 1 s hláškou Attempted to kill init! a jádro se zastaví.
# na serveru: každý oops eskaluj na paniku a po deseti vteřinách restartuj
sysctl -w kernel.panic_on_oops=1 kernel.panic=10Call Trace se čte odshora. RIP: říká, kde to spadlo, Call Trace: jak se tam řízení dostalo. Symbol funkce+0x2a/0x120 je offset 42 bajtů ve funkci dlouhé 288 bajtů; řádky s otazníkem nemusí být skutečné rámce.
Pád v modulu je jiná úloha než pád v jádře. Když je symbol v RIP doplněný názvem modulu v hranatých závorkách, máš oddělitelnou příčinu a šanci, že stačí vyměnit ovladač. Když je RIP v jádře a tainted má 4096, ptej se dál - modul mohl přepsat cizí strukturu a spadnout až později jinde. Nářadí je na nástrojích a diagnostice.
| Co spadlo | Co to udělá se systémem |
|---|---|
| proces v prstenci 3 |
SIGSEGV, spadne jen on |
| privilegovaná instrukce v prstenci 3 |
#GP → SIGSEGV, stroj nic nepozná |
| proces s PID 1 | panika Attempted to kill init!, stroj stojí |
| oops v jádře v kontextu procesu | zabité vlákno, tainted 128, systém běží nespolehlivě |
| oops v obsluze přerušení nebo pod zámkem | eskalace na paniku, stroj stojí |
| DMA od vadného zařízení bez IOMMU | poškozená paměť, kterou MMU nezachytí |
Ochranu dělá hardware, ne jádro. Procesor privilegovanou instrukci odmítne sám, ať si software myslí co chce.
Do jádra vedou tři cesty: přerušení, výjimka, systémové volání. Čtvrtá se jmenuje CVE.
Přechod mění režim, zásobník i viditelnou paměť naráz. Vlastní jádrový zásobník vlákna je důvod, proč přerušení nerozbije proces.
Ten bit sám nestačí. Hranici drží ještě MMU, SMEP, SMAP a od roku 2018 KPTI.
Kód v jádře nemá jak spadnout jen trochu. Jeden vadný konfigurační soubor, 8,5 milionu strojů.
Proti DMA, firmwaru a hypervizoru hranice neplatí. Nad jádrem i pod ním je ještě něco.
- Systémová volání - jediná dobrovolná cesta přes hranici
- Přerušení a výjimky - zbylé dvě cesty a dělení obsluhy na poloviny
- Architektury jádra - spor o to, co ještě dát do prstence 0
- Virtuální paměť a stránkování - jak MMU hranici drží v paměti
- Virtualizace a kontejnery - další úroveň pod prstencem 0
- Nástroje a diagnostika - čím se vyšetřuje pád
Našel jsi chybu nebo něco chybí? Založ issue nebo pošli pull request.
Základy
- Co dělá operační systém
- Režim jádra a uživatelský režim
- Systémová volání
- Přerušení a výjimky
- Architektury jádra
Procesy
Souběh
Paměť
Soubory a zařízení
Systém v provozu
Praxe