Spectre: leggere memoria che la CPU non ha mai davvero letto
Le CPU moderne non aspettano. Quando incontrano un branch condizionale — un
if — non si fermano a calcolare la condizione: il predittore di salto
indovina l'esito e la CPU inizia a eseguire il ramo previsto in
speculazione, in anticipo. Se ha indovinato, ha guadagnato tempo. Se ha
sbagliato, butta via i risultati e riparte dal ramo giusto. Architetturalmente è
come se nulla fosse successo.
Il problema, scoperto nel 2018, è quel quasi. Le istruzioni speculate e poi annullate lasciano un effetto collaterale che l'annullamento non ripulisce: lo stato della cache. Spectre legge quello stato e ci ricava i dati che la speculazione ha toccato.
Il gadget vulnerabile
Il caso canonico, Spectre variante 1 (bounds check bypass):
if (x < array1_size) {
y = array2[array1[x] * 4096];
}
Il controllo x < array1_size sembra proteggere l'accesso. Ma se
array1_size non è in cache, la CPU ci mette centinaia di cicli a
caricarlo. Nell'attesa, il predittore — allenato da noi con tanti x
validi — assume che il ramo si prenda, e specula. Con un x
malevolo e fuori dai limiti, durante la finestra speculativa la
CPU legge davvero array1[x], un byte di memoria arbitraria, e lo usa
come indice in array2.
Il segreto entra nella cache
Il trucco è la moltiplicazione per 4096: la dimensione di una pagina. Il byte
segreto b = array1[x] determina quale pagina di
array2 viene portata in cache dalla lettura speculativa. Ogni valore
possibile di b (0–255) tocca una pagina diversa e distante, così non
c'è prefetch che confonda le acque.
Poi la CPU si accorge che x era fuori limite, annulla tutto, e
y non viene mai scritto architetturalmente. Ma
array2[b*4096] è rimasto in cache.
Flush + Reload: leggere la traccia
Prima dell'attacco svuoti dalla cache tutte le 256 pagine candidate
(clflush). Dopo la speculazione, misuri quanto tempo serve a leggere
la prima parola di ciascuna:
for (i = 0; i < 256; i++) {
addr = &array2[i * 4096];
t0 = rdtscp();
tmp = *addr; // lettura
dt = rdtscp() - t0; // ciclo di misura
if (dt < CACHE_HIT_THRESHOLD)
results[i]++; // questa pagina era in cache -> b == i
}
La pagina che si legge in ~50 cicli invece di ~300 è quella portata in cache
dalla speculazione: il suo indice è il byte segreto. Ripeti la
misura più volte per battere il rumore, avanzi x di un byte, e leggi
memoria a piacere — un byte alla volta, tipicamente a qualche KB/s.
Perché è tanto grave
Spectre non sfrutta un bug del software: sfrutta il comportamento corretto della microarchitettura. Il codice del gadget è privo di errori secondo qualsiasi analisi statica. E soprattutto attraversa confini che consideravamo solidi:
- Sandbox del browser. La versione originale leggeva memoria
del processo del browser da JavaScript, allenando il predittore con codice JS. La
risposta è stata togliere ai siti i timer ad alta risoluzione
(
performance.now()depotenziato,SharedArrayBufferdisabilitato per anni) e introdurre l'isolamento dei siti in processi separati. - Kernel da user space e guest → host nella virtualizzazione, quando il gadget giusto esiste nel codice privilegiato.
Le mitigazioni e il loro prezzo
Non esiste una patch software unica perché la radice è nel silicio. Si combatte su più fronti, e ognuno costa cicli:
- Barriere di speculazione. Su x86
lfencedopo il controllo dei limiti impedisce alla CPU di speculare oltre. Il compilatore può inserirle, ma serializzano l'esecuzione: lento se abusato. - Clamp dell'indice. Invece di affidarsi al ramo, si maschera
l'indice in modo che resti nei limiti anche in speculazione
(
array_index_nospecnel kernel Linux). - Retpoline per la variante 2 (branch target injection): sostituisce le chiamate indirette con un costrutto che intrappola la speculazione in un loop innocuo invece di lasciarla saltare a un target avvelenato.
Il conto complessivo, tra Spectre, Meltdown e i loro discendenti (MDS, L1TF, Retbleed), è stato una perdita di prestazioni misurabile su carichi reali — in certi casi a due cifre percentuali — pagata da ogni datacenter del pianeta. È il caso di studio definitivo del fatto che l'astrazione della CPU è una bugia utile: sotto, il tempo e la cache raccontano tutto.