Operační systémy
Obsah Soubory
markdown

Jadro-a-uzivatelsky-rezim.md

13 kB 182 řádků Změněno Zobrazit na GitHubu Stáhnout
markdown
# Režim jádra a uživatelský režim 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í](Systemova-volani), ani jak se překládají adresy - to je [virtuální paměť a stránkování](Virtualni-pamet-a-strankovani). Předpokládá, že víš, co je proces a že [operační systém je správce zdrojů i ochranná vrstva](Co-dela-operacni-system). ## Ochranu nedělá software 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. ## Čtyři prstence, z toho dva použité 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. ## Co uživatelský kód nesmí 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á. ```bash# jediná privilegovaná instrukce v jinak prázdném programuprintf 'int main(void){ __asm__ volatile("hlt"); }' > /tmp/g.cgcc -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é](Prava-a-uzivatele)), při zapnutém Secure Bootu neprojde vůbec a správně se píše ovladač. ## Tři cesty do jádra a žádná čtvrtá **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](Preruseni-a-vyjimky). **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í](Systemova-volani). Čtvrtá cesta neexistuje. Když ji někdo najde, nejmenuje se to cesta, ale zranitelnost, a dostane číslo CVE. ## Přechod není skok 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](Vlakna) sdílela, nešlo by uprostřed obsluhy [přepnout kontext](Prepinani-kontextu). ## Co drží hranici kromě toho bitu ### Oddělený adresní prostor 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í](Virtualni-pamet-a-strankovani). 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. ### SMEP a SMAP Ú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`. ```bashgrep -o -e ' smep' -e ' smap' /proc/cpuinfo | sort -u   # obojí by mělo být vidět``` ### KPTI KPTI (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. ```bashcat /sys/devices/system/cpu/vulnerabilities/meltdown   # „Mitigation: PTI" = KPTI běží``` ## Co doopravdy selhává ### Meltdown a Spectre, 2018 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. ### CrowdStrike, červenec 2024 19. č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. ## Kde ta hranice přestává platit **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](Vstup-a-vystup). ```bashls /sys/class/iommu/    # prázdný výstup = zařízení sahají do paměti bez dohledu``` **Firmware 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](Start-systemu). **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](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. ## Diagnostika **Začni kruhovým bufferem jádra a příznakem znečištění.** Po pádu je to jediná stopa, kterou máš: ```bashdmesg -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í. ```bash# na serveru: každý oops eskaluj na paniku a po deseti vteřinách restartujsysctl -w kernel.panic_on_oops=1 kernel.panic=10``` **`Call 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](Nastroje-a-diagnostika). | 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í | ## Co si odnést **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. ## Kam dál - **[Systémová volání](Systemova-volani)** - jediná dobrovolná cesta přes hranici- **[Přerušení a výjimky](Preruseni-a-vyjimky)** - zbylé dvě cesty a dělení obsluhy na poloviny- **[Architektury jádra](Architektury-jadra)** - spor o to, co ještě dát do prstence 0- **[Virtuální paměť a stránkování](Virtualni-pamet-a-strankovani)** - jak MMU hranici drží v paměti- **[Virtualizace a kontejnery](Virtualizace-a-kontejnery)** - další úroveň pod prstencem 0- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - čím se vyšetřuje pád