Den betrodda beräkningsbasen: varför storlek spelar roll
Säkerhetsdiskussioner fokuserar ofta på enskilda buggar:
- ett buffertspill
- ett confused-deputy-fel
- en okontrollerad parser
- en väg för privilegieeskalering
De buggarna spelar roll, men de är symptom på en djupare fråga:
Hur mycket kod måste vara korrekt för att systemet ska förbli säkert?
Den koden är den betrodda beräkningsbasen, vanligtvis förkortad TCB.
TCB:ns storlek och form avgör hur mycket kod som måste betros, granskas, testas och förstås. Ett system kan ha starka abstraktioner på papperet, men om de abstraktionerna beror på att en stor mängd privilegierad kod beter sig perfekt blir säkerhetsargumentet mycket svagare.
Det här inlägget förklarar vad TCB är, varför dess storlek påverkar attackytan och hur EriX försöker hålla betrodd kod liten och explicit.
Vad är TCB?⌗
Den betrodda beräkningsbasen är mängden komponenter vars korrekthet krävs för att systemets säkerhetsegenskaper ska hålla.
Om en komponent i TCB komprometteras kan systemets säkerhetsgarantier sluta gälla.
I ett operativsystem omfattar TCB ofta:
- bootloadern
- kärnan
- privilegierade systemtjänster
- autentiserings- och auktoriseringslogik
- parsers för betrodd bootdata
- kryptografisk verifieringskod
- kod som distribuerar eller överför auktoritet
Den exakta TCB:n beror på systemets design.
I en traditionell monolitisk kärna ingår vanligtvis stora delar av kärnan i TCB, eftersom många delsystem körs med fulla kärnprivilegier. En bugg i en drivrutin, ett filsystem eller en nätverksstack kan bli en kärnkompromettering.
I ett mikrokärnesystem kan kärnans TCB vara mindre, men det totala betrodda systemet omfattar fortfarande komponenterna som distribuerar auktoritet och genomdriver policy.
Den skillnaden spelar roll.
Mikrokärnor minskar mängden kod som körs med full maskinauktoritet. De gör inte alla tjänster i användarrymden obetrodda automatiskt.
Betrodd betyder inte säker⌗
Ordet “betrodd” är lätt att missförstå.
En betrodd komponent är inte en komponent som vi vet är korrekt.
Det är en komponent som systemet är beroende av att den är korrekt.
Det är en mindre bekväm definition, men den användbara.
Om en tjänst är betrodd för att distribuera kapabiliteter kan en bugg i tjänsten ge auktoritet felaktigt. Om en parser är betrodd för att validera en körbar fil före boot kan en parserbugg underminera bootkedjan. Om en syscall-hanterare i kärnan är betrodd för att validera endpoint-rättigheter kan en saknad kontroll bryta isoleringen.
Förtroende är inte beröm.
Förtroende är risk.
Målet är därför inte att märka så mycket kod som möjligt som betrodd. Målet är att göra den betrodda mängden så liten, smal och granskningsbar som möjligt.
Varför storlek spelar roll⌗
TCB:ns storlek spelar roll av flera skäl.
1. Mer kod betyder fler buggar⌗
All programvara har buggar.
När mängden betrodd kod växer växer också sannolikheten för säkerhetsrelevanta buggar. Det gäller särskilt kod som:
- parsar obetrodd indata
- hanterar minne
- hanterar samtidighet
- tolkar behörigheter
- översätter en auktoritetsmodell till en annan
Operativsystem innehåller alla dessa mönster.
Att minska TCB:ns storlek tar inte bort buggar, men minskar mängden kod där en bugg kan kompromettera hela systemet.
2. Fler gränssnitt betyder större attackyta⌗
Attackyta handlar inte bara om kodrader.
Det handlar också om ingångspunkter.
Varje gränssnitt in i betrodd kod är en plats där en angripare kan lämna indata:
- syscall-argument
- IPC-meddelanden
- metadata i bootavbildningar
- ELF-huvuden
- filsystemsstrukturer
- enhetsdeskriptorer
- avbrottshändelser
- tabeller från firmware
Varje gränssnitt behöver validering.
En liten betrodd komponent med ett dåligt designat gränssnitt kan fortfarande vara farlig. Men när antalet betrodda gränssnitt växer växer valideringsbördan med det.
3. Mer tillstånd gör resonemang svårare⌗
Säkerhetsfel uppstår ofta inte för att en enskild kontroll saknas, utan för att tillstånd ändras i oväntad ordning.
Till exempel:
- en kapabilitet kopieras innan rättigheter reduceras
- en tjänst startar innan dess startup-bundle är helt validerad
- ett gammalt lokalt slot behandlas som bevis på auktoritet
- en enhet anses närvarande innan upptäckten är klar
- en process behåller auktoritet efter en misslyckad startväg
Ju mer betrott muterbart tillstånd ett system har, desto svårare blir det att bevisa att varje övergång bevarar avsedda invariants.
Det är därför EriX betonar deterministisk startup, explicita överföringsposter och fail-closed-beteende.
4. Mer privilegium betyder större skadezon⌗
Samma bugg får olika konsekvenser beroende på var den sker.
En parserbugg i ett oprivilegierat verktyg kan krascha verktyget.
En parserbugg i bootloadern kan kompromettera hela systemet innan kärnan startar.
En bugg i en användarrymdsdriver som bara har åtkomst till ett specifikt I/O- intervall är allvarlig, men den är annorlunda än en bugg i en drivrutin som körs inne i kärnan med full maskinauktoritet.
Att minska TCB handlar delvis om att minska skadezonen för enskilda buggar.
Kärnan är bara en del av TCB⌗
Det är frestande att säga:
TCB är kärnan.
Det är oftast för enkelt.
Kärnan är central, men ett säkert system beror också på koden som förbereder kärnan, startar den första användarrymdsuppgiften, definierar auktoritetsformat och distribuerar kapabiliteter.
I EriX är kärnan uttryckligen i TCB.
Den äger kapabilitetstabeller i kärnrymden, schemaläggningstillstånd och maskinresurser. Den validerar handoff från bootloader till kernel, skapar root-uppgiften, hanterar grundläggande kärnobjekt och exponerar arkitekturspecifika ingångspunkter för trap, syscall och avbrott.
Om kärnan inte genomdriver kapabilitetskontroller, endpoint-rättigheter, adressrymdsgränser eller objektlivstider fallerar systemets isoleringsmodell.
Men kärnan är inte hela berättelsen.
Bootkod är också betrodd⌗
Bootloadern körs före kärnan.
Det gör den säkerhetskritisk.
I EriX ansvarar bootloadern för att ladda och verifiera en signerad boot.img,
parsa kärn- och tjänsteavbildningar, bygga en deterministisk handoff-struktur
och överföra kontrollen till kärnan.
Det placerar bootloadern i TCB.
Den har firmwaretilldelad auktoritet under boot och kontrollerar det sista hoppet in i kärnan. Den måste behandla bootmediet som obetrott, validera avbildningens struktur och kryptografiska integritet, avvisa felformade ELF-binärer och misslyckas stängt vid oklarhet.
Om bootloadern accepterar en manipulerad avbildning eller bygger en inkonsekvent handoff kan kärnan börja från en komprometterad grund.
Därför måste bootkod vara liten, strikt och tråkig.
Parsers kan vara TCB-gränser⌗
Parsers underskattas ofta i systemsäkerhet.
De sitter precis där obetrodda bytes blir betrodd struktur.
EriX behandlar flera parser- och ABI-crates som TCB- eller TCB-nära komponenter:
lib-bootimgparsar och verifierarboot.img-struktur, hashar och signaturerlib-elfvaliderar ELF64-binärer innan bootloadern litar på laddsegmentlib-handoffvaliderar versionerade handoff-strukturer mellan bootsteglib-ipcdefinierar och validerar IPC-meddelandelayouterlib-capabidefinierar kapabilitetstyper, rättigheter, slotkonstanter och överföringsdeskriptorer
Dessa bibliotek behöver inte själva ha runtime-kapabiliteter.
Det gör dem inte irrelevanta för TCB.
Om lib-bootimg accepterar en ändrad bootavbildning kan bootloadern lita på kod
som borde avvisas. Om lib-elf accepterar en felformad körbar fil kan bootkedjan
ladda fel bytes eller lita på ogiltiga segmentintervall. Om lib-ipc avkodar
ett meddelande fel kan en operation tolkas fel. Om lib-capabi definierar en
för bred rollpolicy kan en tjänst få auktoritet den aldrig borde ha.
Ren kod kan fortfarande vara betrodd kod.
Den viktiga egenskapen är att dessa bibliotek är smala. De gör ingen I/O, äger ingen systempolicy och undviker ambient auktoritet. Deras jobb är att parsa, validera och avvisa.
Tjänster i användarrymden kan vara betrodda komponenter⌗
Att flytta policy ut ur kärnan gör inte att policyn försvinner.
Den flyttas till tjänster i användarrymden där den kan isoleras, begränsas och granskas separat.
I EriX är rootd den första policybärande auktoriteten i användarrymden. Den
validerar handoff från kernel till root, parsar bootkonfiguration, kör startup-
DAG:en och överför kapabiliteter med minsta privilegium till nödvändiga tjänster.
rootd är högprivilegierad.
Men den är inte kärnan.
Den skillnaden är viktig. rootd är betrodd för tidig policy och
kapabilitetsdistribution, men implementerar inte kärnobjekt och äger inte
maskinauktoritet direkt. Dess jobb är att distribuera auktoritet enligt
explicita startup-kontrakt.
Andra tjänster finns också inom specifika betrodda gränser:
procdär betrodd för processlivscykelorkestreringdevicedär betrodd för drivrutinspolicy och leverans av drivrutinskapabilitetvfsdär betrodd som gräns för det publika filsystemsnamnrymden- privata filsystemsproviders är betrodda endast för sin providerroll
Det gör inte dessa tjänster oviktiga.
Det gör deras auktoritet smalare än full kärnauktoritet.
Attackyta i en monolitisk design⌗
I en monolitisk kärna delar många delsystem ett enda privilegierat adressutrymme.
Det kan göra snabba vägar enkla, men skapar också en bred attackyta.
En sårbarhet i vilket delsystem som helst i kärnan kan bli en kärnsårbarhet:
- felformad filsystemsmetadata
- buggig nätpakethantering
- osäker drivrutinskod
- oväntade hårdvarudeskriptorer
- race conditions i delat kärntillstånd
Angriparen behöver bara en väg in i privilegierad kod.
Det betyder inte att monolitiska kärnor inte kan vara säkra. De kan konstrueras, härdas, fuzzas, sandboxas och granskas omfattande.
Men arkitekturen börjar med en stor privilegierad yta.
Säkerhetsargumentet måste sedan förklara hur den stora ytan kontrolleras.
Attackyta i en mikrokärnedesign⌗
En mikrokärna ändrar attackytans form.
Kärnan exponerar fortfarande kritiska gränssnitt:
- syscalls
- IPC-leverans
- schemaläggning
- adressrymdsoperationer
- kapabilitetsoperationer
- avbrottshantering
Dessa gränssnitt måste vara korrekta.
Men högre nivåers tjänster körs inte automatiskt med fulla kärnprivilegier. Om en filesystemprovider hanterar felformade medier är målet att buggen stannar inom providerauktoriteten. Om en drivrutin misslyckas är målet att den misslyckas endast med den enhetsauktoritet den explicit fick.
Detta gör en stor privilegierad yta till flera mindre auktoritetsytor.
Det är inte automatiskt enklare.
Det fungerar bara om gränserna är strikta och auktoriteten som passerar dem är smal.
Hur EriX minimerar TCB⌗
EriX minskar TCB-storlek och attackyta genom flera designval.
1. En policyminimal kärna⌗
EriX-kärnan ansvarar för mekanismer, inte systempolicy.
Den hanterar:
- grundläggande kärnobjekt
- kapabilitetssemantik
- schemaläggning och task-exekvering
- adressrymdsprimitiver
- IPC och endpoint-dispatch
- ingångspunkter för avbrott och undantag
Den äger inte:
- policy för tjänstestart
- processorkestreringspolicy
- filsystemsnamnrymdspolicy
- drivrutinsaktiveringspolicy
- högnivåpolicy för minnesallokering
Det håller kärnan fokuserad på isolering i stället för att bestämma hela systemets form.
2. Explicita kapabiliteter i stället för ambient auktoritet⌗
EriX modellerar auktoritet genom kapabiliteter.
En komponent kan bara agera om den har en kapabilitet med rätt rättigheter. Kärnan validerar kapabilitetsreferenser vid användning, genomdriver endpoint-rättigheter vid syscall-dispatch och behandlar överföringar som explicita händelser.
Detta undviker globala namn eller konventionella slotnummer som behörighet.
Att känna till ett slotnummer är inte auktoritet.
Att ha rätt kapabilitet i lokal CSpace är auktoritet.
Den skillnaden är central för att minska TCB: betrodd kod behöver inte härleda behörighet ur globalt tillstånd när auktoriteten bärs explicit.
3. Smala kernel-control-endpoints⌗
Äldre operativsystemdesigner koncentrerar ofta kontroll bakom breda privilegierade gränssnitt.
EriX går åt andra hållet.
Nuvarande runtime använder smala familjer av kernel-control-endpoints för specifika jobb:
- tid
- avbrott
- hotplug
- PCI-konfiguration
- konsol och framebuffer
- COM1-I/O
- i8042-I/O
- memory retyping
- VSpace-mappning
- pager-felupplösning
- processkontroll
- ACPI-läsningar
Normal runtime-boot ger inte tjänster ett brett root-endpoint för alla kärnoperationer.
I stället får en tjänst den endpoint-familj den behöver. timed får
tidskontroll. irqd får avbrottskontroll. drv-serial får COM1-specifik I/O.
drv-i8042 får i8042-specifik I/O. drv-acpi får ACPI-läsauktoritet.
Det smalnar både det betrodda gränssnittet och skadan från missbruk.
4. Exakta startup-kontrakt⌗
EriX behandlar startup-överföring av kapabiliteter som ett kontrakt, inte ett förslag.
Startup-envelopes beskriver vilka kapabiliteter en tjänst ska få, var de ska synas och vilka rättigheter de ska bära.
Tjänster validerar de faktiska lokala slot de fick innan de deklarerar sig redo. Endpoint-överföringar kontrolleras efter källslot, destinationsslot, rättigheter och förväntad endpoint-typ. Okända, felaktiga eller extra överföringar avvisas.
Detta förhindrar en vanlig auktoritetsbugg:
“Ett slot är upptaget, alltså måste det vara rätt sak.”
I EriX är slotockupation i sig inget bevis på auktoritet.
Kapabiliteten måste matcha den deklarerade auktoritetsformen.
5. Stegvis processkapande⌗
Processkapande är en riskfylld operation eftersom den kombinerar exekvering, adressrymder, endpoints och initial auktoritet.
EriX styr runtime-processkapande genom stegvis child creation i stället för en äldre direkt skapa-operation.
Flödet är explicit:
- skapa ett staged child
- ta emot install grant och child endpoint-alias
- installera deklarerad startup-kapabilitetsbundle
- starta processen först när populationen är klar
Kärnan nekar processstart medan levande install grants fortfarande pekar på det child-staget.
Det hindrar delvis populerade processer från att bli körbara med tvetydigt auktoritetstillstånd.
6. Rollspecifik drivrutinsauktoritet⌗
Drivrutiner är en stor källa till operativsystemsrisk.
EriX behandlar inte all hårdvarukontroll som en bred behörighet.
Drivrutinsauktoritet är rollspecifik:
drv-serialfår endast COM1-I/O-auktoritetdrv-i8042får endast i8042-I/O-auktoritetdrv-acpifår ACPI-läsauktoritetprobedfår PCI-konfigurationsläsauktoritetdrv-virtio-blockfår en validerad device frame i stället för generell minnesauktoritet
deviced hanterar drivrutinspolicy, men ger inte varje drivrutin en generisk
device-control-kapabilitet. Den använder explicit installation av
startup-kapabiliteter och rollspecifika auktoritetsytor.
Det begränsar TCB-effekten av drivrutinsbuggar.
7. Enhetsminne är en separat kapabilitetstyp⌗
Enhetsminne är farligt eftersom det kan påverka hårdvarutillstånd direkt.
EriX skiljer enhetsminne från vanligt RAM med CAP_TYPE_DEVICE_FRAME.
Lagringsvägen kan härleda en BAR-stödd MMIO-frame för deviced, och deviced
kan installera endast den härledda enhetsframen i en drivrutins startup-bundle.
Den enhetsframen behandlas inte som vanligt allokerbart minne.
Det spelar roll eftersom sammanblandning av enhetsminne och normala frames skulle bredda auktoritetsmodellen och göra resonemang om minnessäkerhet svårare.
8. Clean-room-beroenden⌗
Beroenden kan förstora TCB tyst.
Om betrodd kod beror på ett generellt externt bibliotek kan systemet ärva:
- kodvägar det inte behöver
- antaganden som inte matchar operativsystemet
- parserbeteende som är för tillåtande
- uppdaterings- och leveranskedjerisk
EriX undviker tredjepartscrates och implementerar sina kritiska bibliotek inom projektet.
Det ökar implementationsarbetet, men håller den betrodda gränsen synlig. När en parser, ABI-crate eller kryptografisk hjälpare är del av boot- eller auktoritetsvägen är den del av systemets granskningsyta.
9. Fail-closed-beteende⌗
En liten TCB räcker inte om fel är tvetydiga.
EriX försöker göra säkerhetskänsliga fel explicita och terminala:
- felformad boot-handoff stoppar boot
- versioner som inte stöds avvisas
- ogiltig startup-auktoritet hindrar readiness
- fel endpoint-typer misslyckas vid validering
- fel i en nödvändig tjänst stoppar progression
- fel efter start utlöser fail-closed-teardown
Att misslyckas stängt är viktigt eftersom återhämtningsheuristik ofta blir dold policy.
Dold policy utvidgar systemets betrodda beteende.
Mindre betyder inte trivialt⌗
Att minska TCB gör inte systemdesign lätt.
Det gör den ofta mer explicit.
I stället för att placera all logik i ett privilegierat adressutrymme måste systemet definiera:
- vilken komponent som äger varje beslut
- vilken auktoritet varje komponent får
- hur auktoritet överförs
- hur fel rapporteras
- hur delvis startup städas upp
- hur gamla kapabiliteter ogiltigförklaras eller släpps
Det är mer arbete i början.
Men det producerar ett system där säkerhetsargumentet är lättare att granska.
Frågan blir:
Vilken komponent måste vara betrodd för denna specifika egenskap?
Det är en bättre fråga än:
Är hela kärnan korrekt?
En praktisk TCB-bild av EriX⌗
En praktisk bild av EriX TCB är lagerindelad.
Längst ned finns bootkedjan:
- bootloader
- verifiering av bootavbildning
- read/verify-vägen i
lib-bootimg - ELF-parsning
- handoff-validering
Sedan kärnan:
- kapabilitetsobjekt
- CSpace- och VSpace-genomdrivning
- IPC och endpoint-rättigheter
- schemaläggning och trap-hantering
- avbrottsleverans
Sedan tidigt betrott användarrymme:
rootdför startpolicy och kapabilitetsdistributionprocdför processlivscykeldevicedför drivrutinspolicy- utvalda ABI- och valideringsbibliotek delade mellan tjänster
Sedan smalare betrodda tjänster:
- filsystemsnamnrymdsmediering i
vfsd - privata backend-providers
- tjänster för input, konsol, loggning, block, tid och avbrott
Alla dessa komponenter är inte lika privilegierade.
Det är poängen.
EriX försöker undvika en platt betrodd värld. Varje komponent bör vara betrodd endast för sin dokumenterade roll och endast med de kapabiliteter den uttryckligen fick.
Framåt⌗
TCB-diskussionen leder naturligt till implementationsspråket.
Om betrodd kod måste vara liten, explicit och granskningsbar spelar
minnessäkerhet roll. Det gör även unsafe-gränser, parserkorrekthet,
datalayout och disciplinen som krävs nära hårdvaran.
Nästa inlägg undersöker varför EriX främst är skrivet i Rust, vad Rust löser och inte löser för kärnutveckling, och hur det jämförs med den traditionella C-metoden för systemprogrammering.