Articoli approfonditi sulla tecnologia che plasma il futuro.

Sandbox VM sub-millisecondo con Copy-on-Write

Come il fork di memoria copy-on-write permette sandbox VM che si avviano in meno di un millisecondo, cambiando serverless e isolamento di sicurezza.

Una bolla di sapone che si divide in molte piccole bolle, ognuna contenente una minuscola stanza di vetro

Avviare un container Docker richiede circa 500 millisecondi. Una microVM Firecracker ne richiede circa 125 e un isolate V8 circa 5. Ma una nuova generazione di sandbox leggere, che usa il fork della memoria copy-on-write, può avviare un ambiente di esecuzione isolato in meno di 1 millisecondo, spesso nell'intervallo di 50-200 microsecondi. È abbastanza veloce da creare una sandbox nuova per ogni singola chiamata di funzione.

Non è un semplice miglioramento incrementale. È un cambiamento qualitativo in ciò che la sandboxing può fare. Quando creare una sandbox costa 500ms, le si crea con parsimonia e le si riutilizza. Quando costa 50μs, le si crea per ogni input non fidato, ogni invocazione di plugin, ogni richiesta utente. Il modello di sicurezza passa dall'isolare i tenant all'isolare le singole operazioni.

Cosa Significa Davvero Copy-on-Write

Copy-on-write (CoW) è una tecnica del sistema operativo in cui si crea una 'copia' di una regione di memoria senza copiare effettivamente alcun dato. Sia l'originale sia la copia puntano alle stesse pagine fisiche di memoria, contrassegnate come sola lettura. Sono indistinguibili: vedono gli stessi dati. La copia vera e propria avviene solo quando uno dei due tenta di scrivere su una pagina. A quel punto il kernel intercetta la scrittura, copia solo quella singola pagina e permette la scrittura sulla copia.

La system call fork() di Unix la usa dagli anni '90. Quando si esegue il fork di un processo, il figlio ottiene una copia completa della memoria del padre, ma grazie al CoW non viene copiato effettivamente nessun dato. Se il figlio chiama subito exec() (come accade tipicamente), sostituisce interamente la propria memoria e le pagine CoW vengono semplicemente rilasciate. Il fork era quasi gratuito.

// Classic fork() — copy-on-write in action
pid_t pid = fork();
// At this point:
// - Parent and child share ALL physical memory pages
// - Both have identical virtual address spaces
// - Pages are marked read-only in both
// - Zero data has been copied
if (pid == 0) {
// Child process
// Reading shared_data is free — same physical page
int value = shared_data[0];
// Writing triggers CoW — only THIS page gets copied
shared_data[0] = 42;
// Now child has its own copy of this one page
// Parent's page is unchanged
}

Dal Fork alla Sandbox

L'intuizione alla base delle sandbox CoW è questa: invece di avviare una VM o un container da zero, si pre-avvia un ambiente 'template' (con runtime, librerie e stato iniziale caricati), poi lo si sottopone a fork con CoW per creare copie istantanee. Ogni copia parte esattamente dallo stato in cui si trovava il template, già completamente inizializzata e pronta a eseguire, ma opera nel proprio spazio di memoria isolato.

La differenza di prestazioni è enorme. L'avvio di una VM tradizionale comporta: caricare un kernel, inizializzare l'hardware, montare i filesystem, avviare il sistema di init, caricare il codice applicativo, inizializzare il runtime. Anche con ottimizzazioni spinte (Firecracker ne riduce molto i passaggi), si parla ancora di centinaia di millisecondi di lavoro di inizializzazione.

Il fork CoW salta tutto questo. Il template ha già completato l'inizializzazione. Il fork crea una copia pre-inizializzata in microsecondi. Il 'costo di avvio' è solo la contabilità del kernel per creare un nuovo spazio di indirizzi e duplicare le voci della page table: qualche migliaio di operazioni, indipendentemente da quanta memoria usa il template.

Dove Cambia le Regole del Gioco

Funzioni Serverless

I cold start sono la maledizione del serverless. AWS Lambda impiega 100-500ms per un cold start, e di più con i runtime basati su JVM. È inaccettabile per carichi sensibili alla latenza, e questo costringe gli utenti a mantenere le istanze calde (vanificando lo scopo del serverless) oppure ad accettare una latenza imprevedibile.

Con le sandbox CoW, i cold start scendono sotto il millisecondo. Ogni invocazione può essere un 'cold start', perché in pratica i cold start sono praticamente gratuiti. Niente pool di istanze pronte, nessuno spreco di memoria da istanze inattive, nessuno stato residuo tra un'invocazione e l'altra. Ogni esecuzione di funzione ottiene un ambiente isolato e pulito senza pagare il costo di inizializzazione.

Sistemi di Plugin ed Estensioni

Eseguire plugin non fidati in sicurezza è uno dei problemi più difficili del software. I browser lo hanno risolto per JavaScript con gli isolate V8. Ma per il codice arbitrario (estensioni compilate, linguaggi di scripting, plugin binari) le opzioni di isolamento sono state limitate ai container, troppo lenti per l'isolamento per richiesta, o a WebAssembly, con ecosistema e supporto linguistico limitati.

Le sandbox CoW offrono una via di mezzo: eseguire codice arbitrario in una copia isolata dell'ambiente host, con configurazione e smantellamento in meno di un millisecondo. Il plugin vede un ambiente OS completo (filesystem, rete, librerie), ma le sue modifiche restano confinate: all'uscita dalla sandbox, tutte le modifiche spariscono. È ideale per il codice inviato dagli utenti nei sistemi CI, negli ambienti notebook e negli strumenti di build.

Isolamento di Sicurezza

Quando si elabora input non fidato (analizzare un PDF caricato, renderizzare HTML fornito dall'utente, eseguire una query sul database), eseguire l'operazione in una sandbox isolata limita il raggio d'azione di qualsiasi exploit. Se il parser PDF ha un buffer overflow, l'attaccante ottiene il controllo di una sandbox usa e getta che sta per essere distrutta, non del server applicativo.

Questo approccio, cioè isolare ogni operazione non fidata in un processo separato, è stato impraticabile con la sandboxing tradizionale, perché l'overhead superava il tempo di elaborazione. Se analizzare un PDF richiede 10ms, spendere 500ms per creare un container non ha senso. Ma spendere 100μs per creare una sandbox CoW è banalmente economico.

I Dettagli Implementativi

Costruire un sistema pratico di sandbox CoW richiede di risolvere diversi problemi oltre alla semplice chiamata a fork().

  • Contabilità della memoria. Il CoW rende ambiguo l'uso della memoria. Se un template usa 1 GB e si esegue il fork di 100 copie che modificano ciascuna 10 MB, l'uso fisico di memoria è di circa 2 GB (1 GB condiviso + 100 × 10 MB unici), non 100 GB. Il kernel tiene traccia delle pagine condivise e private, ma ottenere un uso accurato per sandbox richiede di analizzare /proc/[pid]/smaps.
  • Isolamento del filesystem. Il CoW gestisce la memoria, ma le scritture sul filesystem richiedono un isolamento separato. I filesystem overlay (overlayfs) offrono una semantica CoW per i file: la sandbox vede il filesystem del template, ma le scritture finiscono in un livello separato. All'uscita dalla sandbox, l'overlay viene scartato.
  • Isolamento di rete. Ogni sandbox ha bisogno del proprio network namespace per evitare interferenze. I namespace Linux lo garantiscono, ma creare network namespace ha un overhead misurabile. Alcuni sistemi riutilizzano un pool di namespace pre-creati.
  • Limiti di risorse. Una sandbox che alloca memoria senza limiti o consuma CPU illimitata è un vettore di denial-of-service. I cgroup forniscono limiti di risorse (memoria, CPU, I/O), ma creare e distruggere cgroup aggiunge overhead. Anche qui, il pooling aiuta.
  • Pulizia deterministica. Quando una sandbox termina, tutte le sue risorse (memoria, file descriptor, connessioni di rete, oggetti IPC) devono essere ripulite in modo affidabile. I PID namespace aiutano: si termina il processo init del namespace e tutti i discendenti vengono uccisi.

CoW contro Sandbox WebAssembly

WebAssembly (Wasm) è l'altra grande tecnologia di sandboxing leggero. Vale la pena confrontare le due, perché fanno compromessi fondamentalmente diversi.

Le sandbox Wasm eseguono il codice in una macchina virtuale memory-safe con un modello di memoria lineare. La sandbox non può accedere a nulla al di fuori della propria memoria lineare: niente filesystem, niente rete, nessuna system call (a meno che non vengano fornite esplicitamente tramite WASI). È estremamente sicuro ma restrittivo: il codice esistente va ricompilato in Wasm, e non tutti i linguaggi compilano bene in Wasm.

Le sandbox CoW eseguono codice nativo in un ambiente OS isolato. La sandbox ha accesso a un'interfaccia OS completa (eventualmente limitata da filtri seccomp), può eseguire qualsiasi binario e usa le normali librerie di sistema. È meno restrittivo ma anche meno sicuro: il confine di isolamento è il modello a processi del sistema operativo, che ha una superficie di attacco più ampia della VM minimale di Wasm.

Scegli Wasm quando: controlli il codice da isolare, il tuo carico di lavoro compila pulitamente in Wasm e ti serve il massimo isolamento possibile. Scegli le sandbox CoW quando: devi eseguire binari arbitrari già esistenti, il carico di lavoro richiede capacità a livello OS (filesystem, rete, processi figli) e vuoi privilegiare la compatibilità rispetto a una superficie di attacco minima.

La Trappola: il Fork nei Programmi Multithread

C'è un noto problema con fork(): copia solo il thread chiamante. Se il padre ha 20 thread, il figlio ne ottiene uno. I mutex tenuti dagli altri 19 thread risultano ancora bloccati nella memoria del figlio, ma i thread che li detenevano non esistono più. Il figlio andrà in deadlock la prima volta che proverà ad acquisirne uno.

I sistemi di sandbox CoW aggirano il problema assicurandosi che il processo template sia single-thread nel momento del fork. In pratica: si inizializza tutto nel template (caricamento delle librerie, configurazione del runtime, preparazione dello stato iniziale), poi si fermano tutti i thread tranne quello principale, si esegue il fork e ogni figlio ricrea i thread quando serve. Il costo di inizializzazione si paga una sola volta; il fork evita il rischio legato ai thread.

Alcuni approcci più recenti usano userfaultfd o gestori di page fault personalizzati per implementare una semantica simile al CoW senza affidarsi affatto a fork(). Evitano il problema del multithreading, ma aggiungono complessità e richiedono un coordinamento più profondo a livello di kernel.

Cosa Tenere d'Occhio

Il sandboxing sub-millisecondo è ancora agli inizi, ma i building block sono solidi (fork, namespace, cgroup e overlayfs sono tutti maturi). I sistemi costruiti sopra, per il serverless, la CI/CD e l'esecuzione sicura del codice, stanno dimostrando che l'isolamento per singola operazione è praticabile su larga scala. Man mano che questi strumenti maturano, l'idea che il sandboxing sia costoso diventerà obsoleta quanto l'idea che la garbage collection sia troppo lenta per le applicazioni in tempo reale. L'overhead sta scomparendo, e i benefici di sicurezza del 'sandbox everything' diventano difficili da ignorare.