Miksi Rust ytimen kehitykseen
Käyttöjärjestelmät yhdistetään yleensä C:hen.
Tämä yhteys on ymmärrettävä. C on pieni, ennustettava, lähellä konetta ja historiallisesti hallitseva kieli ydintyössä. Se antaa ohjelmoijalle suoran pääsyn muistiin, rekistereihin, layouteihin ja kutsukonventioihin.
Juuri näitä ydin tarvitsee.
Ne ovat myös juuri niitä asioita, jotka tekevät ytimistä vaikeita turvata.
EriX on kirjoitettu pääosin Rustilla, koska projekti rakentuu yhden keskeisen ajatuksen ympärille:
Auktoriteetin pitää olla eksplisiittistä.
Tämä koskee kyvykkyyksiä, käynnistyksen handoff-rakenteita, IPC-viestejä, muistiobjekteja ja laitepääsyä. Se koskee myös toteutuskieltä. Rust ei tee ytimen kehityksestä automaattisesti turvallista, mutta se antaa koodikannalle tavan tehdä omistajuus, mutaatio, lifetimet ja turvattomuus näkyviksi.
Tällä näkyvyydellä on väliä.
Ydin on epämukava paikka muistivirheille⌗
Muistivirheet ovat huonoja kaikkialla.
Ytimessä ne ovat pahempia.
Use-after-free tavallisessa sovelluksessa voi korruptoida sen sovelluksen. Use-after-free ytimessä voi korruptoida schedulerin tilaa, kyvykkyystaulukoita, osoiteavaruuksia tai laitemappauksia.
Rajavirhe käyttäjätilan parserissa voi kaataa yhden prosessin. Rajavirhe käynnistyksen parserissa voi vaikuttaa siihen, mitä koodia kone suorittaa seuraavaksi.
Huono osoitin sovelluksessa on paikallinen bugi. Huono osoitin ytimessä voi olla auktoriteettibugi.
Siksi muistiturvallisuus ei ole EriXille kosmeettinen huoli. Ydin kuuluu luotettuun laskentapohjaan. Bootloader kuuluu luotettuun laskentapohjaan. Useat parseri- ja ABI-kirjastot ovat TCB:tä tai TCB:n vieressä.
Jos nämä komponentit käsittelevät muistia väärin, eristysmalli voi pettää ennen kuin yksikään kyvykkyystarkistus ehtii auttaa.
Mitä Rust antaa ytimelle⌗
Rust antaa ytimelle useita hyödyllisiä oletuksia.
Se antaa rajatarkistetut slicet tarkistamattomien osoitin-pituus-parien sijaan tavallisessa koodissa.
Se antaa omistajuussäännöt, jotka tekevät vahingossa syntyvästä aliasoinnista vaikeampaa.
Se antaa lifetimet, joilla voidaan ilmaista lainattuja näkymiä käynnistyskuviin, ELF-binääreihin, handoff-blobeihin ja IPC-puskureihin ilman että väitetään niiden omistavan taustalla olevan tallennustilan.
Se antaa Result-tyypin ja enumit tavallisiksi työkaluiksi virhepolkujen
eksplisiittiseen esittämiseen.
Se antaa tyyppijärjestelmän, joka on tarpeeksi vahva erottamaan monia tiloja, joita C-koodi usein esittäisi kommentteina, flageina tai konventioina.
Mikään tästä ei poista ytimen suunnittelun tarvetta.
Rust ei päätä, mitä kyvykkyys tarkoittaa. Se ei päätä, minkä palvelun pitäisi saada laite-endpoint. Se ei todista IPC-protokollaa oikeaksi. Se ei estä jokaista kilpatilannetta schedulerissa.
Mutta se muuttaa oletuksen.
C:ssä oletus on, että melkein mikä tahansa funktio voi tehdä tarkistamatonta osoitinaritmetiikkaa, aliasoida muuttuvaa muistia, tulkita bytejä rakenteiksi ja ylivuotaa hiljaisesti, ellei ohjelmoija ole kurinalainen kaikkialla.
Rustissa tavallinen koodi ei voi tehdä näitä asioita kulkematta unsafe-rajan
tai eksplisiittisesti tarkistetun operaation kautta.
Se on käytännöllinen tietoturvaero.
no_std on normaali ympäristö⌗
Ydin-Rust ei ole tavallista sovellus-Rustia.
EriX-ytimen crate on #![no_std]. Se ei suoriteta käyttöjärjestelmän päällä. Se
on käyttöjärjestelmän perusta.
Monet EriX-kirjastot ovat myös no_std, esimerkiksi:
lib-bootimglib-elflib-handofflib-ipclib-capabilib-bootstraplib-consolelib-devicelib-driverlib-time
Useat näistä crateista käyttävät myös #![deny(unsafe_code)].
Tämä yhdistelmä on tärkeä. Se tarkoittaa, että samoja kirjastoja voidaan käyttää bootissa, ytimessä ja freestanding-palveluissa riippumatta host-OS:n palveluista. Se tarkoittaa myös, että parseri- ja ABI-koodi voidaan usein kirjoittaa ilman raw-osoittimia.
Tämä on yksi Rustin vahvimmista sopivuuksista EriXiin.
Projektissa on paljon pieniä crateja, joiden tehtävä ei ole koskea suoraan rautaan. Niiden tehtävä on parsia, validoida, enkoodata, dekoodata ja hylätä. Nämä ovat juuri tehtäviä, joissa turvallinen Rust on arvokasta.
Turvalliset parserit ovat tietoturvarajoja⌗
EriX kohtelee parsereita tietoturvarajoina.
lib-bootimg parsii ja varmentaa boot.img:n. Sen lukupolku on osa bootin
luottamusketjua. Se validoi rakenteen, kohdistuksen, rajat, hashit ja
allekirjoitukset ennen kuin bootloader luottaa kuvaan.
lib-elf parsii ELF64-binäärit ennen kuin bootloader käyttää lataussegmenttejä.
lib-handoff validoi versioidut handoff-rakenteet boot-vaiheiden välillä sekä
ytimen ja rootd:n välillä.
lib-capabi määrittelee kyvykkyystyypit, oikeudet, slot-vakiot,
siirtodeskriptorit ja käynnistyksen validointiapurit.
Nämä cratet ovat tarkoituksella tylsiä.
Ne käyttävät byte-sliceja. Ne käyttävät tarkistettua aritmetiikkaa. Ne palauttavat rakenteisia virheitä. Ne hylkäävät tukemattomat versiot, väärät koot, väärät offsetit ja virheelliset taulukot.
Ne eivät tee I/O:ta. Ne eivät omista runtime-auktoriteettia. Ne eivät arvaa.
Tämä rakenne olisi mahdollinen C:llä, mutta se vaatisi jatkuvaa kurinalaisuutta:
- jokainen osoitin pitää tarkistaa
- jokainen offset pitää rajatarkistaa
- jokainen kokonaislukuoperaatio pitää auditoida ylivuodon varalta
- jokainen palautettu näkymä pitää sitoa käsin input-puskurin lifetimeen
Rust tekee näiden sääntöjen ylläpitämisestä halvempaa.
Esimerkiksi parsittu boot-kuva voi lainata alkuperäisestä byte-slicesta. Validoitu ELF-lataussegmentti voi paljastaa lainattuja bytejä uuden tarkistamattoman osoittimen valmistamisen sijaan. Handoff-parseri voi palauttaa iteraattorin, joka tarkistaa jokaisen tietueen alueen ennen sen parsimista.
Kieli ei todista parseria semanttisesti täydelliseksi.
Se kuitenkin poistaa suuren luokan vahingossa syntyviä muistivirheitä parserin normaalilta polulta.
unsafe ei katoa⌗
Ytimen kehitys ei voi olla kokonaan turvallista Rustia.
Rautarajapinta ei ole type-safe. CPU-rekisterit, sivutaulut, keskeytyskuvaajat, port I/O, MMIO, DMA-puskurit ja bootin sisääntulo-osoittimet eivät saavu ystävällisinä Rust-arvoina.
Jossain kohdassa ytimen pitää sanoa:
Tiedän, mitä tämä raw-osoite tarkoittaa.
Tämä väite on unsafe.
EriXissä unsafe on paikoissa, joissa kone pakottaa sen:
- bootin sisääntulokoodi muuntaa raw handoff -osoittimen ja pituuden byte-sliceksi
- x86_64-syscall-glue käyttää inline assemblya ja kiinteää rekisteri-ABI:a
- context switch käyttää arkkitehtuurikohtaista assemblya
- GDT, IDT, TSS ja syscall-alustus koskevat CPU:n kuvaajatilaan
- sivutaulujen käsittely käyttää volatile-lukuja ja -kirjoituksia
- serial- ja legacy-laitepolut käyttävät port I/O:ta
- MMIO- ja DMA-ajurit käyttävät volatile-accessoreita
- muutamat yksisäikeiset runtimet käyttävät
UnsafeCell-rakennetta dokumentoitujen staattisten puskurien takana
Tämä lista ei ole Rustin epäonnistuminen.
Se on rehellinen raja kielen ja koneen välillä.
Tavoite ei ole, ettei unsafe-koodia olisi missään. Tavoite on pitää unsafe
pienenä, paikallisena, dokumentoituna ja turvallisten rajapintojen ympäröimänä.
Unsafe-rajat pitäisi pitää kapeina⌗
Tärkeä kysymys ei ole:
Sisältääkö koodikanta
unsafe-koodia?
Tärkeä kysymys on:
Missä
unsafeon, ja mikä invariantti tekee siitä pätevän?
EriXissä raw syscall -silta ipc-syscall_x86_64:ssä on hyvä esimerkki.
Crate tarjoaa turvallisia wrapper-funktioita kuten ipc_call, ipc_recv,
ipc_reply, query_local_cap ja drop_local_cap. Sisäisesti siinä on yksi raw
assembly -raja, joka mapittaa argumentit x86_64-syscallin rekisteri-ABI:in ja
palauttaa raw-rekisteriarvot.
Wrapper ei itse dereferoi käyttäjäosoittimia. Se välittää osoitin- ja pituusarvot ytimelle, jossa kyvykkyys-, endpoint- ja viestipituustarkistukset tapahtuvat.
Tämä on oikea muoto:
- raw assembly on eristetty
- ABI on dokumentoitu
- julkinen API on tyypitetty
- semanttinen validointi jää ytimeen
Ytimen context switch -raja noudattaa samaa mallia. Rekisterien tallentamiseen
ja palauttamiseen on arkkitehtuurikohtaista assemblya, mutta schedulerin muu
logiikka voi toimia KernelContext-rakenteella ja ContextSwitcher-traitilla.
Host-testit voivat käyttää pehmeää switcheriä, joka mallintaa tilan siirtoa
hyppäämättä oikeasti CPU:lla.
Tämä jako on tärkeä, koska se pitää suurimman osan scheduler-logiikasta testattavana ilman etuoikeutettujen käskyjen suorittamista.
Rautaan pääsy on edelleen rautaan pääsyä⌗
Rust ei tee MMIO:sta turvallista itsestään.
Jos ajuri kirjoittaa väärän arvon väärään rekisteriin, Rust ei pelasta laitetta. Jos DMA-puskuri kuvataan väärin, rauta voi silti kirjoittaa muistiin. Jos sivutaulumerkintä on väärä, CPU toteuttaa väärän mappauksen erittäin tehokkaasti.
EriX käsittelee tämän kahdella tavalla.
Ensiksi rauta-auktoriteetti tehdään eksplisiittiseksi. Ajurin ei pitäisi saada laajaa koneauktoriteettia. Sen pitäisi saada vain tarvitsemansa device frame, I/O-alue, keskeytyslinja tai endpoint-perhe.
Toiseksi rautaan koskeva unsafe-koodi pidetään lähellä rautarajaa.
Esimerkiksi drv-virtio-block sisältää volatile MMIO- ja DMA-apureita. Se on
odotettua lohkoajurille. Tärkeää on, että tämä koodi suoritetaan käyttäjätilan
ajurissa, jolla on kapea laiteauktoriteetti, ei suuressa monoliittisessa
ytimessä täydellä koneauktoriteetilla.
Rust auttaa ajurin sisällä, mutta EriX nojaa edelleen arkkitehtuuriin:
- eksplisiittiset laitekyvykkyydet
- tyypitetty laitemuisti kuten
CAP_TYPE_DEVICE_FRAME - kapeat kernel-control-endpointit
- käyttäjätilan ajurieristys
- fail-closed-käynnistysvalidointi
Kieli ja OS-suunnittelu vahvistavat toisiaan.
Kumpikaan ei korvaa toista.
Rust sopii kyvykkyysajatteluun⌗
Rustin omistajuusmalli ei ole sama asia kuin kyvykkyysjärjestelmä.
Rust-borrow ei ole EriX-kyvykkyys. Rust-tyyppi ei ole ytimen pakottamaa auktoriteettia. Kääntäjä ei voi tietää, pitäisikö prosessin saada mapata frame tai lähettää endpointiin.
Silti Rust ja kyvykkyydet sopivat hyvin yhteen.
Molemmat rohkaisevat eksplisiittisyyteen.
EriXissä komponentti voi toimia vain, kun sillä on oikea kyvykkyys oikeilla oikeuksilla. Rustissa koodi voi mutatoida dataa vain, kun sillä on oikeanlainen pääsy siihen dataan.
Tämä samankaltaisuus on hyödyllinen sekä kulttuurisesti että mekaanisesti.
Se rohkaisee API:ja, joissa auktoriteetti välitetään arvona eikä löydetä globaalista tilasta. Se rohkaisee funktioita kertomaan, mitä ne tarvitsevat. Se rohkaisee käynnistyskoodia validoimaan oikeasti vastaanotetun kyvykkyysbundleen sen sijaan, että slot-numero oletettaisiin luvaksi.
lib-capabi on esimerkki tästä tyylistä. Se ei pakota auktoriteettia runtimessa,
mutta määrittelee auktoriteetin yhteisen kielen: kyvykkyystyyppien ID:t,
oikeusflagit, slot-vakiot, siirtodeskriptorit, rooliapurit ja
käynnistysvalidoinnin.
Koska se on no_std ja deny(unsafe_code), tätä auktoriteettisanastoa voidaan
käyttää laajasti ilman, että jokaisesta kuluttajasta tulee raw-muistin kuluttaja.
Vertailu C:hen⌗
C antaa ytimelle maksimaalisen kontrollin pienellä abstraktiokustannuksella.
Se on sen vahvuus.
Se on myös sen heikkous.
C:ssä kyvykkyystaulukkoa voidaan indeksoida rajojen ulkopuolelta. Viestipuskuri voidaan kopioida väärällä pituudella. Levyllä oleva pakattu rakenne voidaan lukea kohdistamattoman osoittimen kautta. Osoitin voi elää pidempään kuin sen kuvaama tallennustila. Kokonaislukuylivuoto voi muuttua liian pieneksi allokaatioksi. Funktio voi vahingossa mutatoida tilaa aliasin kautta, jota kutsuja ei odottanut.
Kaikki nämä voidaan välttää C:ssä.
Ne vältetään katselmoinnilla, konventioilla, staattisella analyysillä, sanitizereilla, fuzzauksella ja kurinalaisuudella.
Nämä työkalut ovat tärkeitä. EriX tarvitsisi edelleen katselmointia, testejä ja fuzzausta, vaikka se olisi kirjoitettu kokonaan Rustilla.
Ero on siinä, että Rust siirtää monet näistä tarkistuksista kielen oletusmalliin.
Kun EriX parsii boot-kuvaa, käy läpi siirtotaulukkoa, rakentaa IPC-viestiä tai
validoi handoff-envelopeä, tavallinen representaatio on slice, enum, iteraattori,
tarkistettu kokonaislukuoperaatio tai rakenteinen Result.
C:ssä tavallinen representaatio olisi usein osoitin, pituus, cast ja lupaus.
Lupaukset eivät ole arvottomia.
Mutta ytimet keräävät lupauksia nopeasti.
Rust vähentää sitä, kuinka moneen lupaukseen täytyy luottaa.
Rust ei poista suunnitteluvirheitä⌗
Rust ei ole tietoturvatodistus.
Se ei estä väärän kyvykkyyden myöntämistä.
Se ei estä endpointia saamasta liian laajoja oikeuksia.
Se ei päätä, pitäisikö rootd:n, procd:n vai deviced:n omistaa
politiikkapäätös.
Se ei automaattisesti saa parseria hylkäämään jokaista semanttisesti virheellistä tiedostoa.
Se ei poista sivukanavia.
Se ei tee DMA:sta koherenttia.
Se ei tee keskeytysjärjestyksestä yksinkertaista.
Se ei tee schedulerista reilua.
Siksi EriX korostaa edelleen arkkitehtuuria:
- pieni TCB
- eksplisiittiset kyvykkyydet
- kapeat endpointit
- deterministinen käynnistys
- fail-closed-käyttäytyminen
- clean-room-riippuvuudet
- käyttäjätilan palvelut
- roolikohtainen ajuriauktoriteetti
Rust parantaa toteutusalustaa. Se ei korvaa tietoturvamallia.
Panic ei ole virhestrategia⌗
Ydinkoodi tarvitsee selkeän vikamallin.
EriXissä ulkoisesta inputista tulevat palautettavat virheet pitäisi palauttaa virheinä. Virheellistä handoff-dataa, tukemattomia versioita, virheellisiä siirtodeskriptoreita, vääriä endpoint-tyyppejä ja puuttuvaa auktoriteettia ei pitäisi korjata hiljaisesti.
Niiden pitäisi epäonnistua eksplisiittisesti.
Panic varataan sisäisten invarianttien rikkomuksiin, ei tavalliseen input-validointiin.
Tämä on tärkeää, koska Rust tekee panicista helpon, mutta ydin ei voi käsitellä panicia normaalina kontrollivirran mekanismina. TCB:ssä oleva parseri ei saisi panicata virheellisestä inputista. Boot-polku ei saisi vahingossa unwindata firmware-tilan läpi. Runtime-palvelu ei saisi käsitellä hyökkääjän hallitsemaa inputia syynä siirtyä määrittelemättömään palautuspolkuun.
Hyödyllinen Rust-malli on:
- validoi ennen luottamista
- palauta
Result - pidä virhevariantit eksplisiittisinä
- varaa panic mahdottomiin sisäisiin tiloihin
- epäonnistu suljetusti, kun tietoturvatila on epäselvä
Tämä malli näkyy EriXin parseri- ja handoff-crateissa.
Testaushyödyt⌗
Rust auttaa myös testausta.
EriX-ydin on no_std-crate, mutta suuri osa sen logiikasta voidaan silti testata
hostissa #[cfg(test)]-tuella.
Tämä on hyödyllistä koodille, jonka ei pitäisi vaatia VM-boottia vain puhtaan invariantin validoimiseksi:
- parserien rajatarkistukset
- handoff-validointi
- kyvykkyysdeskriptorien parsiminen
- oikeusmaskien logiikka
- slot-politiikkataulukot
- schedulerin tilasiirtymät
- pehmeät context switch -mallit
Arkkitehtuurikohtaiset osat tarvitsevat edelleen integraatiotestejä. Inline assembly, sivutaulujen manipulointi, keskeytysten toimitus ja käyttäjätilaan siirtymät täytyy testata ympäristössä, jossa ne oikeasti suoritetaan.
Mutta Rust helpottaa pitämään puhtaan logiikan puhtaana.
Se on arvokasta mikroytimessä, koska monet komponentit voidaan typistää pieniksi deterministisiksi tilakoneiksi eksplisiittisten viestien ja kyvykkyyksien ympärille.
Miksi tämä merkitsee EriXille⌗
EriX ei käytä Rustia siksi, että Rust on muodikas.
Se käyttää Rustia, koska projektin tavoitteet sopivat kielen vahvuuksiin:
- muistiturvattomuuden vähentäminen luotetussa koodissa
- omistajuuden ja mutaation tekeminen näkyväksi
- raw-rautapääsyn eristäminen kapeiden rajojen taakse
- parserikoodin pitäminen turvallisena, deterministisenä ja auditoitavana
- protokolla- ja auktoriteettidatan ilmaiseminen tyypitetyillä rakenteilla
no_std-koodin jakaminen bootin, ytimen ja käyttäjätilan palvelujen välillä- monien bugien tekeminen alun perin vaikeammiksi kirjoittaa
Jäljelle jäävä unsafe-koodi on edelleen vakavaa.
Se täytyy katselmoida yhtä huolellisesti kuin C katselmoitaisiin. Erona on, että EriX voi keskittää tarkastelun niihin kohtiin, joissa kone todella pakottaa raw-kontrollin: sisääntulokoodi, CPU-alustus, sivutaulut, syscallit, context switchit, MMIO, DMA ja port I/O.
Se on käytännöllinen arvo.
Rust ei tee ytimestä turvallista.
Se saa unsafe-osat erottumaan.
Katse eteenpäin⌗
Seuraava askel on seurata suoritusta ensimmäisestä firmwaren lataamasta koodista ytimeen.
Tällä polulla monet aiempien kirjoitusten ideat kohtaavat: TCB, Rust-rajat, validoidut parserit, allekirjoitetut boot-kuvat, handoff-rakenteet ja ensimmäinen koneauktoriteetin siirto eksplisiittisiksi ydinobjekteiksi.
Seuraava kirjoitus käy läpi EriXin boot-prosessin firmwaresta ytimeen: UEFI-lähtökohdan, bootloaderin handoff-mallin ja sen, miksi ytimeen siirrytään higher-half-suoritettavana.