Nový kernel jsem vyzkoušel proti fyzickému disku bez restartu serveru
Cizí patch do ovladače sd bez hlavičky a bez stopy na internetu cílil na jádro, které nikdy neexistovalo. Popisuju, jak jsem ho posoudil, doportoval na 6.8 a vyzkoušel proti fyzickému disku v QEMU, aniž bych restartoval server.
Disk s 528bajtovými sektory nejde v Linuxu použít. Blokové vrstvě nesedí velikost bloku, která není mocninou dvou, takže zařízení skončí s nulovou velikostí a nic přes něj neproteče. Když se firmware odemknout nedaří, zbývá obejít ho v ovladači: číst a zapisovat celé 528bajtové sektory a hostu ukazovat prvních 512 bajtů z každého.
Někdo takový patch napsal a mně se dostal do ruky. Nedalo se ale poznat, kdo ho psal ani jestli ho někdy někdo přeložil. Tenhle text je o dvou věcech, které z toho vzešly a hodí se i jinde: jak posoudit cizí kernelový patch neznámého původu a jak nový kernel vyzkoušet proti fyzickému disku, aniž bych restartoval stroj, ve kterém ten disk sedí.
Proč jsem tu byl a co dalšího jsem na těch discích zkoušel, popisuju v článku Velikost sektoru zamkl výrobce disku, ne značka na štítku.
Co jsem dostal a co o tom šlo zjistit
Dva soubory. Unified diff proti drivers/scsi/sd.c
a drivers/scsi/sd.h, 605 přidaných řádků a 6 odebraných, plus
pythoní skript, který ten patch přenáší na jiné verze jádra. Cílová cesta
v tom skriptu je patches/kernel/9999-wvg-sd-528-translation.patch,
což je konvence Proxmoxu. To byla jediná stopa k prostředí, ze kterého to
pochází.
Jinak nic. Patch nemá hlavičku, chybí Signed-off-by, autor,
copyright i řádek SPDX. Jeho identifikátory jsem hledal na internetu a nenašel
ani jeden, ani v gitu jádra, ani na LKML, ani na GitHubu. Časová razítka v diffu
ukazují na únor a duben 2026.
Kód přitom působí kompetentně. Převody dělá přes DIV_ROUND_UP,
ne bitovým posunem, což je u 528 nutnost, protože to mocnina dvojky není.
Odkládací buffery bere z předalokovaného mempoolu, takže se v cestě I/O nic
nealokuje. Má ošetřené chyby a stropy na velikost požadavku i hloubku fronty.
A správně nuluje protection_type, protože těch 16 bajtů navíc není
ochrana T10 PI, ale metadata diskového pole.
Dobře napsaný kód ale není důkaz, že se ten kód někdy překládal.
Kontext, který v jádře neexistuje
Aplikace na čistou 6.8 skončila takhle:
sd.c 10 ze 13 hunku sedi, 3 selhaly (#1, #9, #10)
sd.h 3 ze 3 hunku sedi
Deset hunků prošlo hlavně proto, že patch toleruje posun.
Zajímavé byly ty tři, které neprošly. Hunk 9 očekává v kontextových řádcích
tohle:
if (!scsi_device_online(sdp))
goto out;
lim = kmalloc(sizeof(*lim), GFP_KERNEL);
if (!lim)
goto out;
A hunk 10 pak sahá na lim->max_dev_sectors, tedy přes ukazatel.
Stáhl jsem si originály sd.c pro tagy v6.8 až v6.14 a spočítal
výskyty:
| Jádro | lim = kmalloc | lim->max_dev_sectors | lim.max_dev_sectors | struct queue_limits lim; |
|---|---|---|---|---|
| 6.8 až 6.10 | 0 | 0 | 0 | 0 |
| 6.11 až 6.14 | 0 | 0 | 1 | 4 |
Do 6.10 včetně tam žádné queue_limits nejsou vůbec. Od 6.11 už
ano, ale jako lokální proměnná na zásobníku, ke které se přistupuje tečkou, ne
šipkou, a nikde se nealokuje. Patch tedy cílí na strom, který v upstreamu nikdy
nebyl.
Přiložený rebase skript to nezachránil, protože spadl hned na první kontrole:
ERROR: sd_revalidate_disk: expected one function definition, found 0
Dohromady to znamená kód, který nikdy neprošel překladem. Kdyby ho autor zkompiloval, na tenhle rozpor by narazil při prvním pokusu. Není to důkaz, že je patch špatný. Je to důkaz, že je neověřený, a to jsou dvě různé věci.
Doportování na 6.8
Tři místa a tři různé opravy:
| Místo | Co bylo potřeba |
|---|---|
sd_disable_advanced_block_ops() | brala lim a sahala na lim->max_discard_sectors; přepsáno na blk_queue_max_discard_sectors(q, 0) a blk_queue_max_write_zeroes_sectors(q, 0), upraveno i volající místo |
| hunk 9 | volání sd_528_limit_queue_depth() přesunuto před buffer = kmalloc(SD_BUF_SIZE, ...) |
| hunk 10 | lim->max_dev_sectors na q->limits.max_dev_sectors, totéž u max_segments |
Past, na kterou jsem naletěl dvakrát
Skript, který ty tři hunky dopisuje, si musí umět ověřit, že už doběhl. Napsal jsem to takhle:
if "sd_528_effective_max_sectors" in text:
return "uz aplikovano"
Vypadá to nevinně a je to špatně. Ten symbol totiž do souboru přichází už
s hunkem 1, který prošel obyčejným patchem o krok dřív. Kontrola
tedy hlásila hotovo i ve chvíli, kdy hunk 10 hotový nebyl, a já pak dlouho
hledal, proč se překlad chová, jako by mé úpravy vůbec nebyly v souboru.
Napodruhé jsem na to naletěl znovu, protože jsem tu podmínku opravil jinde
a tady zapomněl.
Správně se kontroluje symbol, který přijde jedině s tím
hunkem, který zrovna aplikuju. U hunku 10 je to emu_cap. Obecné
pravidlo z toho je krátké: u skriptovaného patchování musí být příznak
„už aplikováno“ vázaný na tu jednu změnu, ne na cokoli, co s ní přišlo do
souboru.
Testovat nový kernel na stroji, který se nesmí restartovat
Ovladač sd je v tom systému zabudovaný napevno, ne jako modul,
takže ho nejde vyměnit za běhu. A hostitel se restartovat nesměl. To vypadá
jako slepá ulička, ale není: fyzický disk se dá do virtuálky pustit celý.
qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 -nographic -no-reboot \
-kernel /usr/src/k/linux-6.8/arch/x86/boot/bzImage \
-initrd /tmp/qinitrd.gz \
-append "console=ttyS0 sd_mod.emulate_512_from_fat_sectors=1" \
-device virtio-scsi-pci,id=scsi0 \
-drive file=/dev/sg2,if=none,id=d0,format=raw \
-device scsi-generic,drive=d0,bus=scsi0.0
Podstatné je scsi-generic. Příkazy SCSI z virtuálky jdou rovnou
na fyzické zařízení, takže testovaný ovladač mluví se skutečným diskem, ne
s emulací. Hostitelské jádro do toho nemluví a běží dál. Initramfs stačí
postavit z busyboxu, není v něm potřeba nic než shell a pár nástrojů.
Tenhle postup se hodí i mimo tenhle případ. Kdykoli potřebujete zkusit ovladač proti konkrétnímu kusu hardwaru na stroji, který nemůžete odstavit, je to nejlevnější cesta.
Výsledky
Nejdřív kontrolní běh s vypnutou emulací, aby bylo vidět, že měřím to, co si myslím:
emulate_512_from_fat_sectors=0
sda: velikost=0 physical_bs=4224 dd -> 0 records
emulate_512_from_fat_sectors=1
sd 0:0:0:0: [sda] Attached SCSI disk
sda: 3030911576 bloku logical_bs=512 physical_bs=512 -> 1445 GB
dd -> 1+0 records in/out
Pak zápis a zpětné čtení na třech místech, která se od sebe liší tím, jak padnou do vnitřního dělení:
| Test | Výsledek |
|---|---|
| zarovnaný zápis 32 kB na LBA 1000 | otisk sedí |
| nezarovnaný zápis 17 sektorů na LBA 2049 | sedí |
| 1 MB přes hranici 64kB chunku na LBA 10240 | otisk sedí |
Otisky sedící samy se sebou ale nedokazují tolik, kolik to vypadá. Kdyby emulace ukládala data soustavně o kus vedle, čtení touž cestou by se s ní zmýlilo stejně. Proto ten poslední krok, který mě na celé práci potěšil nejvíc: data zapsaná ve virtuálce přes emulované 512bajtové zařízení jsem přečetl z hostitele nativně jako 528bajtový sektor.
sg_raw -r 528 -o out.bin /dev/sg2 28 00 00 00 03 e8 00 00 01 00
emulace v QEMU: 3b 0f a4 56 af f2 0e 24 c8 c9 87 4b a2 e2 6f 29
nativne na disku: 3b 0f a4 56 af f2 0e 24 c8 c9 87 4b a2 e2 6f 29
25 24 02 a8 c3 21 4d d3 96 c4 7f 4b b9 50 4a 53
metadata[512:528]: same nuly
Bajty jsou fyzicky na správném místě ve správných sektorech a 16 bajtů metadat je vynulovaných. Disk po všech testech hlásí zdraví v pořádku, nula vadných sektorů a velikost bloku beze změny 528 bajtů.
Slepá ulička: shim v userspace
Než jsem se pustil do jádra, zkusil jsem to jednodušeji. Plugin pro nbdkit, který totéž přepočítávání dělá nad síťovým blokovým zařízením, se napíše za odpoledne a nepotřebuje překládat kernel.
První verze běžela 4,7 MB/s. Přepočítávala bajt po bajtu v Pythonu, což byla hloupost, kterou jsem si měl uvědomit dřív, než jsem to spustil. Po přepsání na práci s řezy vyskočila na 118 MB/s. To už je použitelné číslo, jenže nativní přístup k témuž disku dává kolem 390 MB/s, takže by se za pohodlí platilo dvěma třetinami výkonu. Plus vrstva navíc v cestě dat, kterou by bylo potřeba provozovat a hlídat. Zamítl jsem to.
Co to stojí a co jsem neudělal
Cena emulace je 16 bajtů z každého sektoru: 1,55 TB místo 1,60 TB na disk, tedy kolem 388 GB ze všech osmi dohromady.
Co se tím naopak nezmění, je rychlost linku – a stojí za to říct proč,
protože se to nabízí jako první nápad. Řadič nabízí 12 Gb/s
(maximum_linkrate hlásí 12.0 Gbit), ale disk se domluví na 6 a tam
zůstane. Ptal jsem se přímo jeho: stránka 19h/01h „Phy control and discover“
vrací u obou phy v bajtu 33 hodnotu 0xaa, tedy hardwarové maximum
6 Gb/s. Strop nesedí v kabeláži ani v řadiči, ale ve firmwaru
disku, a jádro s ním nezmůže nic.
Druhý port by nepomohl taky ne, i když disky jsou dual-port. V SAS se rychlost vyjednává na každé phy zvlášť, takže druhá linka přidá redundanci a součet propustnosti – ne vyšší rychlost jednoho spoje. Rozepsal jsem to zvlášť v rozboru rychlosti linku.
A hlavně: ostrá data na tom zatím nemám a mít nebudu, dokud
neproběhne pořádné testování. Testy výše byly krátké a záměrně mířené
na hranice dělení, ne na výdrž. Než na tom něco poběží, chce to fio
s verifikací přes několik hodin, badblocks -w na celý disk, test
se souborovým systémem včetně kontroly po odpojení a odpověď na otázku, co
udělá výpadek napájení uprostřed zápisu. Šest set řádků v cestě DMA se
neprojeví hláškou, ale tichým poškozením dat.
Emulace navíc zahazuje metadata, ze kterých si diskové pole počítá vlastní kontrolu integrity. Disk pak už pole konzistentní nepřipadá. Pro mě to nevadí, používám ho jako obyčejné blokové zařízení, ale zpátky do pole by ten disk už neměl co dělat.
Kód a podklady
Všechno, na čem tenhle text stojí, je ve veřejném repozitáři st1600fm0013-528b-sectors – skripty, zálohy firmwaru i rozbory, které se do článku nevešly.
kernel-patch/ORIGIN_CZ.md– odkud patch je a jak jsem si ho ověřilkernel-patch/port_to_68.py– doportování na API 6.8kernel-patch/RESULTS_CZ.md– výsledky měřenítools/sector528_shim.py– zamítnutý shim v userspace
Zdroje
drivers/scsi/sd.cve v6.8 – strom, proti kterému se patch ověřoval- QEMU: block drivers – dokumentace k obrazům a průchozím zařízením
- nbdkit(1) – rámec, ve kterém vznikl zamítnutý shim
Dál k tématu
-
Velikost sektoru zamkl výrobce disku, ne značka na štítku
Osm SAS SSD z vyřazeného pole hlásí v Linuxu nulovou velikost. Rozebral jsem firmware, našel sedm bajtů, které za to můžou, a doportoval patch do jádra. Pak jsem pustil jednu nedestruktivní sondu a zjistil, že u jiného disku stačí jeden příkaz.
-
Zámek velikosti sektoru sedí v datovém záznamu, ne v kódu firmwaru
Osm SAS SSD z vyřazeného pole hlásí v Linuxu nulovou velikost. Než jsem začal obcházet firmware v ovladači, rozebral jsem tři dumpy SPI flash a našel místo, které o velikosti sektoru rozhoduje. Je to datový záznam, ne kus kódu.
-
Linux load average – Chápeme hodnoty příkazu uptime
Řešili jste někdy co vlastně znamenají 3 čísla na konci příkazu uptime ? load average: 0,10, 0,09, 0,11 Tato čísla obecně vyjadřují průměrné zatížení linuxového systému za období 1, 5 a 15 minut a nezahrnují žádné procesy, čekající doby i/o wait na discích nebo síti.…
-
Test rychlosti internetu (speedtest) z linuxového bashe
Řešili jste někdy otázku, jak změřit rychlost na linuxovém serveru bez GUI ? Dříve jsem to řešil stahováním 1GB souboru , ale to se pro větší rychlosti nehodí hlavně kvůli nedostatečné šířce pásma cílového serveru, tudíž přesné změření rychlosti nepřipadá v úvahu. Proto jsem…
Diskuse
Zatím tu nikdo nediskutuje.
Napsat příspěvek
Diskutovat můžete i bez účtu. S registrací se ale příspěvek zveřejní hned a nemusíte pokaždé vyplňovat jméno. Účet už máte? Přihlaste se.