La base di calcolo fidata: perché la dimensione conta
Le discussioni sulla sicurezza spesso si concentrano su singoli bug:
- un buffer overflow
- un problema di confused deputy
- un parser non controllato
- un percorso di escalation dei privilegi
Quei bug contano, ma sono sintomi di una domanda più profonda:
Quanto codice deve essere corretto perché il sistema rimanga sicuro?
Quel codice è la base di calcolo fidata, di solito abbreviata in TCB.
La dimensione e la forma della TCB determinano quanto codice deve essere considerato fidato, auditato, testato e compreso. Un sistema può avere astrazioni forti sulla carta, ma se queste astrazioni dipendono da una grande quantità di codice privilegiato che si comporta perfettamente, l’argomento di sicurezza diventa molto più debole.
Questo articolo spiega cos’è la TCB, perché la sua dimensione influenza la superficie di attacco e come EriX cerca di mantenere il codice fidato piccolo ed esplicito.
Cos’è la TCB?⌗
La base di calcolo fidata è l’insieme dei componenti la cui correttezza è necessaria perché le proprietà di sicurezza del sistema restino valide.
Se un componente della TCB viene compromesso, le garanzie di sicurezza del sistema possono non essere più vere.
In un sistema operativo, la TCB spesso include:
- il bootloader
- il kernel
- servizi di sistema privilegiati
- logica di autenticazione e autorizzazione
- parser per dati di boot fidati
- codice di verifica crittografica
- codice che distribuisce o trasferisce autorità
La TCB esatta dipende dal design del sistema.
In un kernel monolitico tradizionale, molta parte del kernel fa solitamente parte della TCB perché molti sottosistemi girano con pieno privilegio di kernel. Un bug in un driver, filesystem o stack di rete può diventare una compromissione del kernel.
In un sistema microkernel, la TCB del kernel può essere più piccola, ma il sistema fidato totale include ancora i componenti che distribuiscono autorità e applicano politica.
Questa distinzione conta.
I microkernel riducono la quantità di codice che gira con piena autorità sulla macchina. Non rendono automaticamente non fidati tutti i servizi in spazio utente.
Fidato non significa sicuro⌗
La parola “fidato” è facile da fraintendere.
Un componente fidato non è un componente che sappiamo corretto.
È un componente da cui il sistema dipende perché sia corretto.
È una definizione meno comoda, ma è quella utile.
Se un servizio è fidato per distribuire capacità, un bug in quel servizio può concedere autorità in modo errato. Se un parser è fidato per validare un eseguibile prima del boot, un bug del parser può indebolire la catena di boot. Se un handler syscall del kernel è fidato per validare diritti di endpoint, un controllo mancante può rompere l’isolamento.
La fiducia non è lode.
La fiducia è rischio.
L’obiettivo quindi non è etichettare quanto più codice possibile come fidato. L’obiettivo è rendere l’insieme fidato il più piccolo, stretto e auditabile possibile.
Perché la dimensione conta⌗
La dimensione della TCB conta per diverse ragioni.
1. Più codice significa più bug⌗
Tutto il software ha bug.
Man mano che cresce la quantità di codice fidato, cresce anche la probabilità di bug rilevanti per la sicurezza. Questo è particolarmente vero per codice che:
- fa parsing di input non fidato
- gestisce memoria
- gestisce concorrenza
- interpreta permessi
- traduce un modello di autorità in un altro
I sistemi operativi contengono tutti questi pattern.
Ridurre la dimensione della TCB non elimina i bug, ma riduce la quantità di codice in cui un bug può compromettere l’intero sistema.
2. Più interfacce significano più superficie di attacco⌗
La superficie di attacco non riguarda solo le righe di codice.
Riguarda anche i punti di ingresso.
Ogni interfaccia verso codice fidato è un luogo in cui un attaccante può fornire input:
- argomenti syscall
- messaggi IPC
- metadati dell’immagine di boot
- header ELF
- strutture di filesystem
- descrittori di dispositivi
- eventi di interrupt
- tabelle fornite dal firmware
Ogni interfaccia richiede validazione.
Un piccolo componente fidato con un’interfaccia mal progettata può essere ancora pericoloso. Ma quando aumenta il numero di interfacce fidate, aumenta anche il carico di validazione.
3. Più stato rende il ragionamento più difficile⌗
I fallimenti di sicurezza spesso non avvengono perché manca un singolo controllo isolato, ma perché lo stato cambia in un ordine inatteso.
Per esempio:
- una capacità viene copiata prima che i diritti siano ridotti
- un servizio parte prima che il suo bundle di startup sia completamente validato
- uno slot locale obsoleto viene trattato come prova di autorità
- un dispositivo viene considerato presente prima che la scoperta sia completa
- un processo conserva autorità dopo un percorso di lancio fallito
Più stato mutabile fidato ha un sistema, più è difficile dimostrare che ogni transizione preservi le invarianti previste.
Per questo EriX enfatizza startup deterministico, record di trasferimento espliciti e comportamento fail-closed.
4. Più privilegio significa raggio d’impatto maggiore⌗
Lo stesso bug ha conseguenze diverse a seconda di dove si verifica.
Un bug di parsing in uno strumento non privilegiato può far cadere quello strumento.
Un bug di parsing nel bootloader può compromettere l’intero sistema prima che il kernel parta.
Un bug in un driver in spazio utente con accesso solo a un intervallo I/O specifico è serio, ma è diverso da un bug in un driver che gira dentro il kernel con piena autorità sulla macchina.
Ridurre la TCB significa anche ridurre il raggio d’impatto dei singoli bug.
Il kernel è solo parte della TCB⌗
È tentante dire:
La TCB è il kernel.
Di solito è troppo semplice.
Il kernel è centrale, ma un sistema sicuro dipende anche dal codice che prepara il kernel, avvia il primo task in spazio utente, definisce formati di autorità e distribuisce capacità.
In EriX, il kernel è esplicitamente nella TCB.
Possiede tabelle di capacità dello spazio kernel, stato di scheduling e risorse della macchina. Valida l’handoff dal bootloader al kernel, crea il task root, gestisce oggetti fondamentali del kernel ed espone entry point trap, syscall e interrupt specifici dell’architettura.
Se il kernel non applica controlli di capacità, diritti degli endpoint, confini degli spazi di indirizzamento o durate di vita degli oggetti, il modello di isolamento del sistema fallisce.
Ma il kernel non è tutta la storia.
Anche il codice di boot è fidato⌗
Il bootloader gira prima del kernel.
Questo lo rende critico per la sicurezza.
In EriX, il bootloader è responsabile di caricare e verificare un boot.img
firmato, fare parsing delle immagini del kernel e dei servizi, costruire una
struttura di handoff deterministica e trasferire il controllo al kernel.
Questo colloca il bootloader nella TCB.
Durante il boot possiede autorità fornita dal firmware e controlla il salto finale nel kernel. Deve trattare il supporto di boot come non fidato, validare struttura e integrità crittografica dell’immagine, rifiutare binari ELF malformati e fallire chiuso in caso di ambiguità.
Se il bootloader accetta un’immagine manomessa o costruisce un handoff incoerente, il kernel può iniziare da una base compromessa.
Per questo il codice di boot deve essere piccolo, rigoroso e noioso.
I parser possono essere confini della TCB⌗
I parser sono spesso sottovalutati nella sicurezza dei sistemi.
Sono esattamente il punto in cui byte non fidati diventano struttura fidata.
EriX tratta diversi crate di parsing e ABI come componenti TCB o adiacenti alla TCB:
lib-bootimgfa parsing e verifica struttura, hash e firme diboot.imglib-elfvalida binari ELF64 prima che il bootloader si fidi dei segmenti di caricamentolib-handoffvalida strutture di handoff versionate tra fasi di bootlib-ipcdefinisce e valida layout di messaggi IPClib-capabidefinisce tipi di capacità, diritti, costanti di slot e descrittori di trasferimento
Queste librerie possono non possedere capacità runtime da sole.
Questo non le rende irrilevanti per la TCB.
Se lib-bootimg accetta un’immagine di boot modificata, il bootloader può
fidarsi di codice che dovrebbe rifiutare. Se lib-elf accetta un eseguibile
malformato, la catena di boot può caricare byte sbagliati o fidarsi di intervalli
di segmento invalidi. Se lib-ipc decodifica male un messaggio, un’operazione
può essere interpretata in modo errato. Se lib-capabi definisce una policy di
ruolo troppo ampia, un servizio può ricevere autorità che non dovrebbe mai avere.
Anche codice puro può essere codice fidato.
La proprietà importante è che queste librerie sono strette. Non fanno I/O, non possiedono politica di sistema ed evitano autorità ambientale. Il loro compito è fare parsing, validare e rifiutare.
I servizi in spazio utente possono essere componenti fidati⌗
Spostare la politica fuori dal kernel non la fa sparire.
La sposta in servizi in spazio utente, dove può essere isolata, vincolata e auditata separatamente.
In EriX, rootd è la prima autorità in spazio utente che porta politica. Valida
l’handoff dal kernel a root, fa parsing della configurazione di boot, esegue il
DAG di startup e trasferisce capacità di minimo privilegio ai servizi richiesti.
rootd è ad alto privilegio.
Ma non è il kernel.
Questa distinzione è importante. rootd è fidato per la politica iniziale e la
distribuzione di capacità, ma non implementa oggetti kernel né possiede
direttamente autorità sulla macchina. Il suo compito è distribuire autorità
secondo contratti di startup espliciti.
Anche altri servizi si trovano dentro confini fidati specifici:
procdè fidato per l’orchestrazione del ciclo di vita dei processidevicedè fidato per la politica dei driver e la consegna di capacità drivervfsdè fidato come confine del namespace pubblico del filesystem- i provider filesystem privati sono fidati solo per il loro ruolo di provider
Questo non rende quei servizi poco importanti.
Rende la loro autorità più stretta dell’autorità completa del kernel.
Superficie di attacco in un design monolitico⌗
In un kernel monolitico, molti sottosistemi condividono un unico spazio di indirizzamento privilegiato.
Questo può rendere semplici i percorsi veloci, ma crea anche una superficie di attacco ampia.
Una vulnerabilità in qualunque sottosistema nel kernel può diventare una vulnerabilità del kernel:
- metadati filesystem malformati
- gestione difettosa di pacchetti di rete
- codice driver insicuro
- descrittori hardware inattesi
- race condition nello stato condiviso del kernel
All’attaccante serve un solo percorso verso codice privilegiato.
Questo non significa che i kernel monolitici non possano essere sicuri. Possono essere ingegnerizzati, irrobustiti, fuzzati, sandboxati e auditati estesamente.
Ma l’architettura parte con una grande superficie privilegiata.
L’argomento di sicurezza deve quindi spiegare come quella superficie sia controllata.
Superficie di attacco in un design microkernel⌗
Un microkernel cambia la forma della superficie di attacco.
Il kernel espone ancora interfacce critiche:
- syscall
- consegna IPC
- scheduling
- operazioni sugli spazi di indirizzamento
- operazioni sulle capacità
- gestione degli interrupt
Quelle interfacce devono essere corrette.
Ma i servizi di livello superiore non girano automaticamente con pieno privilegio di kernel. Se un provider filesystem gestisce media malformati, l’obiettivo è che il bug resti dentro l’autorità di quel provider. Se un driver fallisce, l’obiettivo è che fallisca solo con l’autorità di dispositivo che ha ricevuto esplicitamente.
Questo trasforma una grande superficie privilegiata in diverse superfici di autorità più piccole.
Non è automaticamente più semplice.
Funziona solo se i confini sono rigorosi e l’autorità che li attraversa è stretta.
Come EriX minimizza la TCB⌗
EriX riduce la dimensione della TCB e la superficie di attacco tramite diverse scelte di design.
1. Un kernel minimo nella politica⌗
Il kernel EriX è responsabile dei meccanismi, non della politica di sistema.
Gestisce:
- oggetti fondamentali del kernel
- semantica delle capacità
- scheduling ed esecuzione dei task
- primitive degli spazi di indirizzamento
- IPC e dispatch degli endpoint
- entry point di interrupt ed eccezioni
Non possiede:
- politica di startup dei servizi
- politica di orchestrazione dei processi
- politica del namespace filesystem
- politica di attivazione dei driver
- politica di allocazione memoria ad alto livello
Questo mantiene il kernel concentrato sull’applicazione dell’isolamento invece che sulla forma dell’intero sistema.
2. Capacità esplicite invece di autorità ambientale⌗
EriX modella l’autorità tramite capacità.
Un componente può agire solo se possiede una capacità con i diritti richiesti. Il kernel valida i riferimenti di capacità a ogni uso, applica i diritti degli endpoint al dispatch syscall e tratta i trasferimenti come eventi espliciti.
Questo evita di basarsi su nomi globali o numeri di slot convenzionali come permesso.
Conoscere un numero di slot non è autorità.
Possedere la capacità corretta nel CSpace locale è autorità.
Questa distinzione è centrale per ridurre la TCB: il codice fidato non deve inferire permessi dallo stato globale quando l’autorità è portata esplicitamente.
3. Endpoint di controllo del kernel stretti⌗
I design precedenti spesso concentrano il controllo dietro interfacce privilegiate ampie.
EriX va nella direzione opposta.
Il runtime attuale usa famiglie strette di endpoint di controllo del kernel per compiti specifici:
- tempo
- interrupt
- hotplug
- configurazione PCI
- console e framebuffer
- I/O COM1
- I/O i8042
- retyping della memoria
- mapping VSpace
- risoluzione dei fault del pager
- controllo dei processi
- letture ACPI
Il boot runtime normale non dà ai servizi un ampio endpoint root per tutte le operazioni kernel.
Invece, ogni servizio riceve la famiglia di endpoint specifica di cui ha
bisogno. timed riceve il controllo del tempo. irqd riceve il controllo degli
interrupt. drv-serial riceve I/O specifico per COM1. drv-i8042 riceve I/O
specifico per i8042. drv-acpi riceve autorità di lettura ACPI.
Questo restringe sia l’interfaccia fidata sia il danno causato da uso improprio.
4. Contratti di startup esatti⌗
EriX tratta il trasferimento di capacità di startup come un contratto, non come un suggerimento.
Le envelope di startup descrivono quali capacità un servizio deve ricevere, dove devono apparire e quali diritti devono portare.
I servizi validano gli slot locali reali ricevuti prima di dichiararsi pronti. I trasferimenti di endpoint sono controllati per slot sorgente, slot destinazione, diritti e tipo di endpoint atteso. Trasferimenti sconosciuti, errati o extra sono rifiutati.
Questo evita un bug di autorità comune:
“Uno slot è occupato, quindi deve essere la cosa giusta.”
In EriX, l’occupazione di uno slot da sola non prova autorità.
La capacità deve corrispondere alla forma di autorità dichiarata.
5. Creazione di processi a fasi⌗
La creazione di processi è ad alto rischio perché combina esecuzione, spazi di indirizzamento, endpoint e autorità iniziale.
EriX instrada la creazione di processi runtime attraverso creazione staged di figli invece di una vecchia operazione diretta.
Il flusso staged è esplicito:
- creare un figlio staged
- ricevere un grant di installazione e un alias di endpoint del figlio
- installare il bundle dichiarato di capacità di startup
- avviare il processo solo quando la popolazione è completa
Il kernel nega l’avvio del processo mentre grant di installazione vivi puntano ancora a quello stage figlio.
Questo impedisce a processi parzialmente popolati di diventare eseguibili con uno stato di autorità ambiguo.
6. Autorità driver specifica per ruolo⌗
I driver sono una grande fonte di rischio nei sistemi operativi.
EriX non tratta tutto il controllo hardware come un unico permesso ampio.
L’autorità driver è specifica per ruolo:
drv-serialriceve autorità I/O solo COM1drv-i8042riceve autorità I/O solo i8042drv-acpiriceve autorità di lettura ACPIprobedriceve autorità di lettura configurazione PCIdrv-virtio-blockriceve un frame dispositivo validato invece di autorità memoria generale
deviced gestisce la politica dei driver, ma non consegna semplicemente a ogni
driver una capacità generica di controllo dispositivo. Usa installazione
esplicita di capacità di startup e superfici di autorità specifiche per ruolo.
Questo limita l’impatto dei bug driver sulla TCB.
7. La memoria dispositivo è un tipo di capacità separato⌗
La memoria dispositivo è pericolosa perché può influenzare direttamente lo stato hardware.
EriX distingue la memoria dispositivo dalla RAM ordinaria con
CAP_TYPE_DEVICE_FRAME.
Il percorso storage può derivare un frame MMIO basato su BAR per deviced, e
deviced può installare solo quel frame dispositivo derivato nel bundle di
startup di un driver.
Quel frame dispositivo non è trattato come memoria allocabile ordinaria.
Questo conta perché confondere memoria dispositivo con frame normali allargherebbe il modello di autorità e renderebbe più difficile ragionare sulla sicurezza della memoria.
8. Dipendenze clean-room⌗
Le dipendenze possono allargare la TCB silenziosamente.
Se il codice fidato dipende da una libreria esterna general-purpose, il sistema può ereditare:
- percorsi di codice di cui non ha bisogno
- assunzioni che non corrispondono all’OS
- comportamento di parsing troppo permissivo
- rischio di aggiornamento e supply chain
EriX evita crate di terze parti e implementa le sue librerie critiche dentro il progetto.
Questo aumenta il lavoro di implementazione, ma mantiene visibile il confine fidato. Quando un parser, crate ABI o helper crittografico fa parte del percorso di boot o di autorità, fa parte della superficie di review del sistema.
9. Comportamento fail-closed⌗
Una TCB piccola non basta se i fallimenti sono ambigui.
EriX cerca di rendere i fallimenti sensibili alla sicurezza espliciti e terminali:
- handoff di boot malformato ferma il boot
- versioni non supportate vengono rifiutate
- autorità di startup invalida impedisce la prontezza
- tipi endpoint errati falliscono la validazione
- fallimento di un servizio richiesto ferma la progressione
- fallimenti dopo start attivano teardown fail-closed
Fallire chiuso è importante perché le euristiche di recupero diventano spesso politica nascosta.
La politica nascosta espande il comportamento fidato del sistema.
Più piccolo non significa banale⌗
Ridurre la TCB non rende facile il design del sistema.
Spesso lo rende più esplicito.
Invece di mettere tutta la logica in un solo spazio di indirizzamento privilegiato, il sistema deve definire:
- quale componente possiede ogni decisione
- quale autorità riceve ogni componente
- come viene trasferita l’autorità
- come vengono riportati i fallimenti
- come viene ripulito uno startup parziale
- come capacità obsolete vengono invalidate o eliminate
È più lavoro all’inizio.
Ma produce un sistema in cui l’argomento di sicurezza è più facile da ispezionare.
La domanda diventa:
Quale componente deve essere fidato per questa proprietà specifica?
È una domanda migliore di:
L’intero kernel è corretto?
Una vista pratica della TCB di EriX⌗
Una vista pratica della TCB di EriX è stratificata.
Alla base c’è la catena di boot:
- bootloader
- verifica dell’immagine di boot
- percorso read/verify di
lib-bootimg - parsing ELF
- validazione handoff
Poi il kernel:
- oggetti capacità
- enforcement di CSpace e VSpace
- IPC e diritti endpoint
- scheduling e gestione trap
- consegna interrupt
Poi il primo spazio utente fidato:
rootdper politica di startup e distribuzione di capacitàprocdper ciclo di vita dei processidevicedper politica driver- librerie ABI e di validazione selezionate condivise tra servizi
Poi servizi fidati più stretti:
- mediazione del namespace filesystem in
vfsd - provider backend privati
- servizi di input, console, logging, blocchi, tempo e interrupt
Non tutti questi componenti sono ugualmente privilegiati.
È questo il punto.
EriX cerca di evitare un unico mondo fidato piatto. Ogni componente dovrebbe essere fidato solo per il suo ruolo documentato e solo con le capacità che ha ricevuto esplicitamente.
Guardando avanti⌗
La discussione sulla TCB porta naturalmente al linguaggio di implementazione.
Se il codice fidato deve essere piccolo, esplicito e auditabile, allora la
sicurezza della memoria conta. Contano anche i confini unsafe, la correttezza
dei parser, il layout dei dati e la disciplina necessaria lavorando vicino
all’hardware.
Il prossimo articolo esaminerà perché EriX è scritto principalmente in Rust, che cosa Rust risolve e non risolve per lo sviluppo del kernel, e come si confronta con l’approccio tradizionale in C alla programmazione di sistema.