> Linuxový server se musí kvůli software restartovat v jediném případě: když je potřeba vyměnit jádro.
Neni pravda. Glibc, PAM, OpenSSL, atd atd.
Patchovani jadra za behu je fajn, ale dneska se malokdy patchuje jenom jadro.
Jinak dik za info, je fajn vedet, ze na tom porad nekdo dela...
Ne, není to vůbec divný, protože sshd běží jako daemon, na kterýho se připojuješ a po připojení vytvoří klientovi další proces. Pokud přes ssh session vypneš běžným způsobem ssh, killneš daemona, ale proces pro uživatele běží tak dlouho, dokud se neodpojíš. Pak už se samozřejmě znova nepřipojíš. A když patchneš a restartneš ssh, nový připojení poběží na nové verzi, ale světe div se, to starý připojení se nijak magicky nepřepne a běží pořád na staré verzi. Takže to není zrovna skvělá ukázka patchování běžícího procesu za běhu.
Patchovani to neni, ale resi to cely problem velmi elegantne. Proste stare spojeni bezi sice na starych verzich, ale vsechny nove uz na novych. S posledni starou session zmizi i stara verze. Sluzba ma nepreruseny chod a i za behu stare si lze vyzkouset ze ty nove funguji taky. Bohuzel to jiz dnes neni standard, naopak vznikaji offline aktualizacni mechanismy, protoze to je predsi jednodussi, nez se starat o to aby to update mohl probehnout online. A nelze si nepripomenout systemd, ktery pri online aktualizaci je schopen jit totalne do kopru, protoze mu zmizi nejaky zivotne dulezity symlink.
To by už nemalo byť pravdou
KernelCare Is Another Alternative To Canonical's Ubuntu Live Kernel Patching
Written by Michael Larabel in Proprietary Software on 21 October 2016
KernelCare isn't limited to just Ubuntu 16.04 but also works with Ubuntu 14.04 and other distributions such as CentOS/RHEL, Debian, and other enterprise Linux distributions.
Another big difference to Canonical's Livepatch is that KernelCare does support rollback functionality while Canonical doesn't appear to support it at this time. KernelCare can also handle custom patches, 32-bit support, and they share they plan to soon begin offering livepatching support for glibc, OpenSSL, and QEMU.
https://www.phoronix.com/scan.php?page=news_item&px=KernelCare-Ubuntu-Alternative
A kCraft má jednoduchý rollback, v podstate prepísaním JMP na INT 3, len ten PAM to nebol spomenutý, ale keď vedia glibc, tak nie je "veľmi ťažké" robiť update akejkoľvek knižnice, a ak ide QEMU, tak ide updateovať akékoľvek aplikácia vrátne SSH.
Kerene,lom sa začalo preto, lebo tam sa reálne objaví najviac problémov, a ak ide kernel, dá sa to použiť na knižnice a potom na programy...
Mno, na hrani hezky ... bych chtel videt admina, kterej bude riskovat patch naprosto cehokoli na necem, co se nemuze restartovat a tim padem to nesmi zbuchnout.
Bych tak nejka cekal, ze specielne ty "velky" datacentra to maj zarizeny tak, ze vypadek(a tudiz i restart) cehokoli neni problem.
[...] jejichž funkci přebere jiný stroj v dané chvíli [...]
podle mne je to ekonomicka otazka. Jestlize je ten stroj (s temi TB pameti, jak se pise v clanku) , tak tenhle stroj neco stoji a kvuli rebootu by musel mit provozovatel 2 takove drahe zarizeni. V takovych pripadech bych se asi taky rozhodl pro patchovani zabehu.
Hlavně ty služby na tom serveru asi něco stojí a provozovatel jich MUSÍ mít víc, aby se měly kam přehodit. Spíš je problém v tom, že ne každá služba se dá provozovat takovým způsobem, aby se přepla bez výpadku. U nějakých webíků to moc nevadí, pokud 10 vteřin nepojedou, ale jsou i takové služby, jejichž shození/nahození není tak triviální, je třeba shodit a nahodit více komponent v určitém pořadí, je třeba vyřešit konzistenci...
Je mi mnohem bližší filozofie, že se celá služba rovnou vytváří s tím, že s výpadkem se počítá. A že žádné bestie s mnoha TB RAM vůbec nejsou potřeba, všechno běží na komoditním hardware.
Pak tyhle problémy odpadají už z principu.
Ale třeba existují aplikace, kde to takhle z nějakého důvodu nejde a já jsem rád, že s nimi nepřijdu do kontaktu.
Redirekce se dela pres trampolinu, ktera teprve rozhoduje, jestli uz je vporadku zavolat novy kod, nebo jestli je jeste potreba z konzistencnich duvodu (nez patchovani skonci) volat kod stary. To je prave to rozhodovani o "verzi vesmiru", ve ktere se dany kontext nachazi. A to je to misto, kde lze udelat atomicky switch (zjednodusene se v tu chvili jedna uz o jednobitovy flag).
Pokud se nemění počet a typ parametrů, není potřeba na volání sahat. Pokud jo, je asi jednodušší napsat novou funkci, natáhnout do paměti a postupně na ni přehodit funkce, co volají tu původní... Původní pak není potřeba měnit (samozřejmě za předpokladu, že interně nepoužívá statickou proměnnou apod.).
Za prvé: Windows má tohleto "už sto let", nevím jak pro jádro, ale pro user space knihovny (user32.dll, kernel32.dll, …) určitě. Používají to některé opravy přicházející skrze Windows Update. Na začátek funkce se přidá sedm bajtů, dva pro "jmp near", a pět pro INT3. Těch pět může patchovací mechanismus v klídku neatomicky přepsat na "jmp far" a později atomicky přepsat "jmp near" na "nop nop". Odskok, jak už tu bylo zmíněno, může být někam do trampolíny, která rozhodne, jestli se bude volat nová varianta funkce nebo původní. Může také přepnout všechny patchovane funkce najednou.
Za druhé: Atomicky vyměnit osm bajtů na x86 samozřejmě lze. Předchází se tak ABA problému u lock-free datových struktur. Ale pro volání kódu (a tedy patchovani kódu za běhu) je to nepoužitelné.
Marek
Za prvé: Windows má tohleto "už sto let", nevím jak pro jádro, ale pro user space knihovny (user32.dll, kernel32.dll, …) určitě. Používají to některé opravy přicházející skrze Windows Update</strongi>
O windows vim jen z doslechu, ale co jsem slysel, tak ten mechanismus (tak jak ho popisujete, tzn. s tim docela peknym trikem pres short jmp tesne pred funkci) tam kdysi meli, ale nepouzivali ho, a ted uz to tam snad ani negeneruji. Ale je to zarucena zprava od agentury JPP.
Za druhé: Atomicky vyměnit osm bajtů na x86 samozřejmě lze. Předchází se tak ABA problému u lock-free datových struktur. Ale pro volání kódu (a tedy patchovani kódu za běhu) je to nepoužitelné.
Mate pravdu, je to tam napsano nepresne, a pri kontrolnim cteni textu jsem si toho nevsiml. Ve strucnosti jde o to, ze vymenu nopu za (far)jmp+adresa nelze udelat atomicky, a pro kod je potreba stejne prepisovani pres INT3 delat kvuli CPU (i kdyby byl short), ktere muze byt zrovna dany kus uz fetchnuty do pipeline resp. I$ (kde se koherence a-la MESI neprovadi).
Prilezitostne poslu Petrovi Krcmarovi nejaky navrh na reformulaci, diky za pripominku.
Upřesnění: Windows mají na začátku funkcí "dvoubajtový nop" v podobě mov edi, edi a pět vyhrazených bajtů před funkcí, které by tam s nejvyšší pravděpodobností byly tak jako tak kvůli zarovnání. Mám k dispozici Windows 7 32 bit a tento pattern tam stále je (minimálně v user-space kernel32.dll, kernelbase32.dll a ntdll.dll). Marek.
Oprava mého předchozího příspěvku: Windows má na začátku patchovatelné funkce instrukci mov edi, edi (která se chová jako dvoubajtový nop). A pět volných bajtů před funkcí.
Těch pět bajtů mohou přepsat na skok typu "far jmp" někam do trampolíny a potom atomicky přepsat ten "fancy nop" na skok typu "near jmp" na ten plnohodnotný "far jmp".
Windows má tohleto už „sto let“ možná pro pár věcí, ten zbytek vynucuje restart OS, protože zamykání souborů. Vyměnit za běhu systémové knihovny a použít je prostým restartem aplikací prostě nejde, musí se otočit celý systém a ještě čekat na rozkopírování souborů před vypnutím a při startu OS. Takže jakýkoliv výpadek serveru při update je ještě delší než by být musel. Prostě super! :)
Postup patchování:
- nahradit NOPy za INT3
- nahradit INT3 od konce za instrukci skoku
Má velký problém v případě, že přijde přerušení v momentě, kdy procesor zpracovává NOPy. Po návratu z přerušení pak bude pokračovat v polovině instrukce skoku.
Jak je zaručeno, že GCC vygeneruje 5x NOP a ne jeden pětibajtový NOP? Přepis části této instrukce na INT3 by mohlo způsobit pád.
Všechny běžně používané procesory umožňují atomicky zapsat 8 bytů - a určitě všechny používané v serverech.
Přišly by mi lepší dvě řešní:
a) namísto 5x NOP použít pětibajtový NOP a výměnu provést atomicky (pomocí cmpxchg8b; instrukce musí být v jedné cache line)
b) pokud cmpxchg8b není podporované (nějaká 80486 nebo Pentium či alternativy) a přesto patchování za běhu chcete, pak namísto 5x NOP použít skok s offsetem 0, a offset za běhu přepsat (způsobí flush pipeline a instrukce skoku musí být v jedné cache line)
Obě řešení předpokládají možnost atomického zápisu tím, že zapisovaný kód je v jedné cache line - to bych ale předpokládal, že bude vždy (funkce jsou běžně zarovnány na 16B). Nebo je právě tohle důvod, proč bylo zvoleno řešení s přepisem po bytech?
Má velký problém v případě, že přijde přerušení v momentě, kdy procesor zpracovává NOPy. Po návratu z přerušení pak bude pokračovat v polovině instrukce skoku.
Jak je zaručeno, že GCC vygeneruje ...
Bohužel jste narazil na to, že popis procesu výměny instrukcí byl pro účely přednášky/článku samozřejmě ochuzen, kvůli zjednodušení, o spoustu a spoustu detailů.
- NOPy negeneruje GCC, kernel si je do prologu funkcí přidává při bootu sám na místo, kam nechal původně gcc vygenerovat profilovací call (který se nepoužije).
- kernel používá (na x86_64) pětipajtový atomický nop ( ASM_NOP5_ATOMIC)
- dělá se vždy atomicky replace posledních čtyř bajtů za situace, kdy už je v prvním předem připraven INT3 bajtů opcode,
- pak se změní první bajt z INT3 na první bajt nového opcode
- mezitím se vždy dělá "magie", která zaručí, že CPU se "sesynchronizuje s realitou" (ve smyslu I$ a prefetchnutych instrukci), a to přes IRET-to-self. Důležité je to především po vložení INT3 do prvního bajtu, ale dělá se i mezi ostatními fázemi. Dle explicitního vyjádření Intelu jsou tyto ostatní synchronizace kromě té po vložení INT3 nadbytečné na Intel CPU, ale u AMD jsou pravděpodobně potřeba, proto je tam raději máme vždy.
namísto 5x NOP použít pětibajtový NOP a výměnu provést atomicky (pomocí cmpxchg8b; instrukce musí být v jedné cache line)
Tímhle ten procesor sesypete IMO asi celkem spolehlivě, protože vůbec nevíte, v jakém stavu byla I$ a kolik toho bylo prefetchnuto.
pak namísto 5x NOP použít skok s offsetem 0, a offset za běhu přepsat (způsobí flush pipeline a instrukce skoku musí být v jedné cache line)
Stejný argument jako výše -- výměna je atomická ve smyslu, že CPU + memory controller zajistí, že při vykonávání instrukci pro čtení z paměti je vždy vidět konzistentní hodnota. Vzhledem k I$ a pipeline toho atomicita příliš nezaručuje. Pokud v prvním bajtu pětibajtového NOPu nebudete mít INT3, tak se CPU může při takovéto výměně dostat do náhodného stavu.
Tím, že se ten breakpoint dá na začátek pětibajtového nopu (a tudíž CPU trapne korektně za kterékoliv situace) se zajistím bezproblémová výměna jak prvního, tak zbylých bajtů.
Díky za vysvětlení. V článku jsem se chytnul dvou věcí, které se mi nezdály:
- 5x NOP (namísto pětibytového nopu)
- "maximální atomický zápis 4 bytů"
- no a práce s patchováním na 0xcc a pak ošetřováním interruptu mi přišla zbytečně komplikovaná a potenciálně problematická, protože INT3 může být použit i jinak (při debugování)
Ad "NOPy negeneruje GCC" - znáte přepínač gcc -pg -mnop-mcount? Pak dostanete 5B NOPy už přímo z GCC, čímž si můžete ušetřit práci - pokud tedy současné chování není úmyslné.
Předpokládám, že tedy patchování probíhá následovně:
- přepis prvního byte na 0xCC
- IPI na všechny CPU, s tím, že musím počkat, než bude všude přijato (pro synchronizaci instrukčního dekodéru)
- pokud infrastruktura neumožňuje IPI sám sobě, provedu IRET ručně (jak píšete)
- přepsání offsetu
- znovu sync (kvůli AMD)
- přepsání 0xCC -> 0xE9
- znovu sync (kvůli AMD)
Je to takhle?
Byl jsem si celkem jistý, že instrukční dekodér vždy přečte zarovnaný 16B blok atomicky, proto by nemělo použití atomických operací při zarovnaném začátku vadit - ale můžu se pléct; i kdyby to takhle měl Intel, pořád jsou i další výrobci x86 procesorů.
Popsaný postup je tedy nakonec asi neprůstřelný.
Možná bych ale čekal mnohem jednodušší řešení - nový kernel slinkovat s .text na jinou adresu, a jen změnit entry pointy na nový kernel (to nejde udělat atomicky, ale v současném řešení to atomické taky není). Ostatní sekce (.data, .bss) musí být stejné, jinak by různé části kódu (nová a stará) používaly jiné adresy. Možná je nějaký zřejmý důvod, proč to nejde (např. některé adresy v kernelu jsou pevně dané). Co jsem přehlédl?
Díky
potenciálně problematická, protože INT3 může být použit i jinak (při debugování)
To máte pravdu, ale je to uděláno tak, že int3 exception handler může snadno detekovat zda-li se jedná o trap z debugovacího int3, nebo int3 kvůli live výměně instrukcí (protože víme, kde zrovna patchujeme), a podle toho se zachovat.
Ad "NOPy negeneruje GCC" - znáte přepínač gcc -pg -mnop-mcount? Pak dostanete 5B NOPy už přímo z GCC, čímž si můžete ušetřit práci - pokud tedy současné chování není úmyslné.
Ano, gcc má několik různých způsobů, jak vygenerovat NOPy do prologu. Je v tom docela nepořádek, některé z těch options jsou pouze pro některé architektury (např. ta kterou zmiňujete Vy pouze pro x86), nejsou kompatibilní s některými jinýmu důležitými options (např. opět Vámi zmiňovaná nemůže být použita společně s -fPIC). Mezi další (většinou opět arch-specific, ale pro jiné architektury) patří např. -mhotpatch, -mprofile-kernel, atd.
BTW pouze -pg nestačí, protože ten bohužel ten profilovací kód vygeneruje až za prolog, což už je dost pozdě. Je potřeba -mfentry.
V současné době se snažíme do gcc procpat obecnou option, která bude nezávislá na architektuře i čemkoliv ostatním, a bude generovat do prologu funkce potřebné (volitelné) množství NOPů.
I tak to ale budeme používat nadále jen pro rezervaci místa, a kernel si to bude při bootu přepatchovávat, protože se tam vždy dávají ideální / optimální NOPy pro dané CPU.
Předpokládám, že tedy patchování probíhá následovně:
Ano, popsal jsem to správně. Viz také changelog a komentáře commitu, kterým jsem tohle do kernelu přidával:
http://git.kernel.org/linus/fd4363fff3d96795d3feb1b3fb48ce590f186bdd
Možná bych ale čekal mnohem jednodušší řešení - nový kernel slinkovat s .text na jinou adresu, a jen změnit entry pointy na nový kernel
Tomu asi ne úplně přesně rozumím -- máte na mysli nahrát komplet celý .text celého vyměněného kernelu na nějaké jiné místo v paměti, a přesměrovat IDT/GDT, exception tables, apod?
Děkuji za upřesnění.
<i>Tomu asi ne úplně přesně rozumím -- máte na mysli nahrát komplet celý .text celého vyměněného kernelu na nějaké jiné místo v paměti, a přesměrovat IDT/GDT, exception tables, apod?</i>
Ano, tak jsem to myslel - byla by to první věc, kterou bych zkusil, kdybych měl za úkol vyměnit kernel za běhu. Pokud je možné vyměnit entry pointy s kGraft, pak by to mělo být možné i při výměně celé .text sekce. Ale dost možná bych narazil na nepřekonatelné problémy, které jsou mnohým zřejmé; linuxový kernel moc neznám. Nebo je řešení s kGraft jednoduše lepší.