Se sei arrivato fin qui probabilmente hai letto anche gli altri articoli e hai capito il senso e la direzione di questo progetto. Se non lo hai fatto, te lo spiego al volo.
Questo e' un blog personale. Volutamente scritto in italiano, perche' mi piace la mia lingua e mi rilassa scriverla. Lo scopo e' divulgativo: smontare cose complesse e spiegarle in modo familiare, senza fuffa, con un occhio alla matematica che e' fondamentale e che adoro. Passeremo anche a temi piu' complessi, ma un passo alla volta. Cosi' quando ci si arriva, tutti capiscono perfettamente almeno il concetto.
// Apri Una Pagina, Perdi Tutto
Sezione 00. L'antefattoLa iena mi urla dall'altra stanza. "Il computer si e' rotto." Le dico di chiudere la tab. "L'ho fatto. Si e' chiuso tutto." Pausa. "Anche le altre tab. Anche la musica."
Arrivo zoppicando col braccio nel gesso. Schermo nero. Il renderer e' morto. Le chiedo che sito aveva aperto. "Un blog di ricette. Biscotti al miele." Classico. Mi siedo, apro il terminale con una mano sola, guardo i crash log. Il processo di rendering e' andato in segfault. In un parser CSS. Per un font. Un font. Come dire che ti sei rotto una gamba inciampando in un granello di polvere.
Controllo la versione di Chrome. 144.0.qualcosa. Non aggiornato. "Ma quando e' l'ultima volta che hai aggiornato Chrome?" La iena mi guarda come quando Panna sente un rumore e inclina la testa. "Aggiornato cosa?" Ecco. Siamo messi cosi'.
Il punto e' che un blog di ricette non crasha Chrome. Ma i blog di ricette sono pieni zeppi di banner pubblicitari. E i banner caricano codice da network pubblicitari di terze parti. E dentro quel codice qualcuno puo' infilare un CSS costruito ad arte. La iena cercava i biscotti al miele. Col miele delle sue api, ovviamente. I biscotti al miele le hanno crashato il browser. E su un Chrome non aggiornato, un crash e' solo l'antipasto.
La patch esisteva gia'. Google l'aveva rilasciata due settimane prima. CVE-2026-2441. CVSS 8.8. Use-after-free nel rendering CSS. Exploitata in the wild. Il primo zero-day di Chrome del 2026. Tutto per colpa di un font. Ma la iena non aggiorna niente. Mai. Per lei "aggiornamento" e' quando le galline cambiano il piumaggio.
Nell'articolo su Spectre il processore mentiva. Qui e' il browser che si fa fregare. La' il canale era la cache, qui e' la memoria heap. Ma il concetto e' lo stesso: una cosa che sembra innocua (un'istruzione, un font CSS) si trasforma in un'arma.
Chromium e' open source. Il commit del fix e' pubblico. Il proof-of-concept gira su GitHub. E io ho una mano sola e troppo tempo libero. Combinazione pericolosa.
Smontiamolo.
// Il CSS Che Nessuno Usa
Sezione 01. @font-feature-valuesOk, partiamo dal pezzo di CSS che ha causato tutto 'sto casino. Nella specifica CSS Fonts Level 4 c'e' una regola che il 99.9% degli sviluppatori web non ha mai visto e non vedra' mai: @font-feature-values. Serve a dare nomi leggibili alle feature OpenType dei font. Se non sai cosa vuol dire, tranquillo, non lo sa neanche chi ha scritto il bug.
@styleset {
fancy-ligatures: 1;
old-style-nums: 3;
swash-caps: 7;
}
}
Sembra innocuo, no? Un mapping nome → numero. Chrome lo parsa, crea un oggetto CSSOM chiamato CSSFontFeatureValuesRule, e dentro ci mette una CSSFontFeatureValuesMap. Che e' un wrapper attorno a una HashMap in C++. La struttura dati piu' comune del mondo. Tipo, la prima cosa che studi al secondo anno di informatica.
E il bug e' proprio li'. Nella roba che danno per scontata.
Perche' JavaScript puo' accedere a questa mappa tramite il DOM: rule.styleset. Puo' iterarla con entries(), for...of, o forEach. Puo' modificarla con set() e delete().
E puo' fare le due cose contemporaneamente. Si'. Hai capito bene.
Regola aurea della programmazione: non modificare mai una collezione mentre ci stai iterando sopra. E' la prima cosa che insegnano nei corsi di informatica. E' l'ultima cosa che Chrome controllava in questo codice. L'ironia scrive da sola.
// Dentro la HashMap
Sezione 02. Come funziona una tabella hash in C++Per capire il bug devi capire la struttura dati. Prometto che e' veloce. Una HashMap (in Chromium si chiama WTF::HashMap, e si', WTF sta per "Web Template Framework", non per quello che stai pensando) e' un array di bucket. Ogni chiave viene hashata, l'hash determina in quale bucket finisce la coppia chiave-valore.
Il punto critico e' il rehash. Quando la mappa supera un certo rapporto di carico (circa il 75% dei bucket occupati), alloca un nuovo array piu' grande, ricopia tutti gli elementi, e libera il vecchio array.
Fin qui tutto normale. Tutte le HashMap lo fanno. Il casino succede quando qualcun altro ha ancora un puntatore al vecchio array. Tipo, l'iteratore che JavaScript sta usando per leggere la mappa.
Quando il load factor supera la soglia, il vecchio storage viene deallocato
// Il Puntatore Morto
Sezione 03. Il cuore della vulnerabilita'Ok, qui entriamo nel vivo. Questo e' il codice reale di Chromium, la versione vulnerabile prima della patch. Le righe evidenziate sono quelle che contano:
Vedi il problema? Riga 17: const FontFeatureAliases* aliases. Riga 42: const FontFeatureAliases* aliases_. Puntatore raw allo storage interno della HashMap. Non una copia. Non un riferimento contato. Un puntatore nudo. Tipo camminare sul tetto senza parapetto. Di notte. Col ghiaccio.
Quando JavaScript chiama map.entries(), Blink crea questa IterationSource e salva il puntatore allo storage attuale. Fin qui tutto bene. Il puntatore funziona, tutti contenti.
Ma se tra un .next() e l'altro JavaScript chiama map.set() o map.delete()... beh.
const iter = map.entries()
Blink salva aliases_ = &storage. Il puntatore e' valido.
iter.next() → legge aliases_->begin(). Funziona. Il puntatore e' ancora vivo.
map.delete(key); map.set("x", [1,2,3])
La HashMap si rialloca. free(old_storage). Il puntatore aliases_ ora punta a memoria liberata.
iter.next() → legge aliases_->... → USE-AFTER-FREE.
Il puntatore e' morto. La memoria sotto potrebbe contenere qualsiasi cosa.
Quattro passi. Dal CSS al crash. E dal crash, potenzialmente, a code execution. La iena cercava ricette di biscotti. Qualcun altro poteva prenderle il computer.
// Anatomia di un Use-After-Free
Sezione 04. Cosa succede quando leggi memoria mortaLa iena a questo punto ha chiesto "ma non puo' semplicemente crashare e basta?". Bella domanda. No. E il motivo e' una delle cose piu' insidiose della gestione della memoria in C++.
Quando liberi un blocco di memoria con free(), quel blocco non scompare. Resta li'. L'allocatore lo segna come "disponibile" e lo rimette nel suo pool. Il contenuto non viene azzerato. Costerebbe troppo farlo ogni volta.
Quindi il tuo puntatore punta ancora allo stesso indirizzo. Ma ora quell'indirizzo puo' essere stato riassegnato a qualcun altro. E quando leggi attraverso il puntatore morto, stai leggendo i dati di qualcun altro. E' come tornare a casa tua dopo uno sfratto. La porta si apre, ma dentro ci abita qualcun altro.
storage HashMap
bucket 0
bucket 1
bucket 2
FREED
freed
freed
freed
DATI ATTACCANTE
controllato
controllato
controllato
Questo e' il cuore dell'exploit. L'attaccante non scrive in memoria direttamente. Sarebbe troppo facile da bloccare. Invece libera la memoria per conto di Chrome (forzando il rehash), poi la rioccupa con i suoi dati (creando oggetti della stessa dimensione). Quando Chrome rilegge attraverso il puntatore morto, legge i dati dell'attaccante. Pensando siano i suoi.
La differenza con Spectre: nell'articolo su Spectre il processore leggeva memoria che non doveva toccare e lasciava tracce nella cache. Qui il browser legge la sua stessa memoria, ma il contenuto e' stato sostituito da un attaccante. La' era un bug di architettura hardware, qui e' un bug di gestione della memoria software. Ma il risultato e' lo stesso: dati controllati dall'attaccante finiscono dove non dovrebbero.
// Il Proof of Concept
Sezione 05. HTML + CSS + JS = crashBasta teoria. Vediamo il PoC pubblicato su GitHub. La vulnerabilita' l'ha scoperta Shaheen Fazim, il PoC l'ha scritto un ricercatore che si firma huseyinstif. Usa tre strategie diverse per triggerare il bug. Le smontiamo una per una.
Strategia 1: entries() + mutazione
La piu' diretta e la piu' brutale. Crei un iteratore con map.entries(), poi tra un .next() e l'altro cancelli chiavi e ne aggiungi di nuove. Ogni set() con abbastanza elementi forza il rehash. Tre iterazioni e Chrome va giu'.
const rule = sheet.cssRules[0];
const map = rule.styleset;
// 2. Crea iteratore. Blink salva il puntatore raw
const iterator = map.entries();
// 3. Itera e muta contemporaneamente
while (true) {
const result = iterator.next(); // ← legge tramite puntatore
if (result.done) break;
map.delete(result.value[0]); // ← muta la HashMap
// Forza rehash: 512 inserimenti superano il load factor
for (let i = 0; i < 512; i++) {
map.set("spray_" + i, [i]); // ← rehash → free(old) → UAF
}
}
Strategia 2: for...of
Stessa logica, path diverso dentro Blink. Alcune versioni di Chromium usano un codice leggermente diverso per for...of rispetto a entries(), ma il puntatore raw e' lo stesso. Cinque iterazioni questa volta. Un po' piu' lento, stesso risultato.
Strategia 3: requestAnimationFrame
Questa e' la piu' cattiva. L'iteratore viene avanzato dentro un ciclo requestAnimationFrame, forzando il layout recalc tra un frame e l'altro con document.body.offsetWidth. Questo obbliga il CSS engine a rielaborare le regole, amplificando le possibilita' di corruzione. Piu' lenta (circa 10 iterazioni), ma molto piu' difficile da rilevare perche' sembra una normale operazione di rendering.
Heap grooming: il PoC non si limita a triggerare il crash. E' piu' furbo di cosi'. Crea 50 regole @font-feature-values aggiuntive prima dell'attacco. Servono a riempire l'heap con oggetti della stessa dimensione, cosi' quando lo storage originale viene liberato, la memoria viene riusata da un oggetto controllato. In pratica, l'attaccante prepara il terreno prima di sparare. Questo trasforma un crash casuale in una corruzione di memoria prevedibile. La differenza tra un fulmine e un cecchino.
// Da Crash a Code Execution
Sezione 06. L'escalation"Ok, crasha. E allora?" La iena ha ragione per una volta. Un crash non e' un exploit. Un crash e' fastidioso, chiudi e riapri. Un exploit e' pericoloso, qualcuno ti entra nel computer. La distanza tra i due e' la differenza tra un ubriaco che sbatte contro un muro e un ladro che trova la porta aperta.
Ecco come un attaccante percorre quella distanza:
Passo 1: Trigger. L'iteratore legge attraverso il puntatore morto. Chrome non crasha immediatamente (non sempre, almeno). Dipende da cosa trova a quell'indirizzo.
Passo 2: Heap spray. L'attaccante crea centinaia di oggetti JavaScript della stessa dimensione del vecchio storage HashMap. L'allocatore riusa la memoria freed. Ora il puntatore morto punta a dati controllati dall'attaccante. E Chrome non ha idea che e' successo.
Passo 3: Type confusion. Chrome legge quei dati pensando siano una HashMap entry (chiave, valore, hash). Ma sono dati costruiti dall'attaccante. Se il "valore" e' un puntatore falso, Chrome lo seguira'. Se la "chiave" e' un indirizzo calcolato, Chrome lo dereferenziera'. Tipo dare in mano a qualcuno una mappa dove "casa" punta al covo dei ladri.
Passo 4: Code execution. Con un indirizzo controllato, l'attaccante puo' redirigere l'esecuzione verso il suo codice. Dentro la sandbox di Chrome, per ora. Ma la sandbox ha i suoi bug. E chi fa questo di mestiere li conosce.
La sandbox come ultima difesa: anche con code execution, l'attaccante e' dentro la sandbox di Chrome. Per uscire serve un secondo bug (una sandbox escape, tipo la Mojo IPC o un bug kernel). Ma la storia insegna che le sandbox escape si trovano. NSO Group le concatenava cosi': un bug nel renderer + un bug nella sandbox + un bug nel kernel = compromissione totale da un link WhatsApp. Un link. Ti viene da ridere, se non fosse terrificante.
// Il Fix: Una Riga
Sezione 07. Commit 63f3cb48E ora la parte che mi fa sempre ridere (e piangere). Il fix. Il commit 63f3cb48. Ecco il diff reale:
Vedi le righe rosse e verdi? const FontFeatureAliases* aliases diventa const FontFeatureAliases aliases. Un asterisco. Hanno tolto un *. Da puntatore a copia. L'ho fatto vedere alla iena. "Tutto 'sto casino per un asterisco?" Si'. Benvenuta nella sicurezza informatica.
E guarda il commento che i dev di Chromium hanno lasciato nel codice fixato:
Righe 40-45: "Create a copy to keep the iterator from becoming invalid if there are modifications to the aliases HashMap while iterating." E poi un TODO: "Implement live/stable iteration... avoiding taking a copy here." Tradotto: sappiamo che la deep copy e' un cerotto, ma per ora funziona.
Con la deep copy, l'iteratore lavora sulla sua copia personale della mappa. Se JavaScript muta l'originale, il rehash avviene sull'originale. La copia dell'iteratore resta intatta. Nessun puntatore morto. Nessun UAF. Fine del problema.
snapshot della mappa
puo' riallocarsi liberamente
Pero' la deep copy ha un costo: memoria e tempo. Ogni volta che JavaScript crea un iteratore, Chrome clona l'intera mappa. Per una mappa con 8 entry e' niente. Per una con migliaia, e' un overhead reale. Il commento nel bug tracker dice che c'e' "remaining work". Probabilmente un refactor piu' elegante con WeakPtr o una generazione di versione per invalidare l'iteratore senza copiare.
Ma quando il tuo zero-day e' attivamente exploitato in the wild, non ti metti a fare il refactor elegante. Copi e patchi. Domani pensi al bello.
| Aspetto | Prima (vulnerabile) | Dopo (fix) |
|---|---|---|
| Tipo | const FontFeatureAliases* |
const FontFeatureAliases |
| Semantica | Puntatore raw → storage condiviso | Deep copy → storage privato |
| Rehash | Invalida il puntatore → dangling | Non tocca la copia → safe |
| Costo | Zero (nessuna copia) | O(n) per entry nella mappa |
| Sicurezza | Use-After-Free | Safe |
// La Cronologia
Sezione 08. Dal report alla patchDue giorni. Da segnalazione a fix rilasciato. Dico, due giorni. Google quando vuole non scherza.
| Data | Evento |
|---|---|
| 11 Feb 2026 | Shaheen Fazim segnala la vulnerabilita' a Google |
| 13 Feb 2026 | Chrome 145.0.7632.75 rilasciato con il fix. Patch out-of-band. |
| 17 Feb 2026 | CISA aggiunge CVE-2026-2441 al catalogo KEV (Known Exploited Vulnerabilities) |
| 20 Feb 2026 | PoC pubblicato su GitHub |
| 27 Feb 2026 | Questo articolo. Un pirata con un braccio rotto lo smonta. |
Google ha confermato che l'exploit era "in the wild" prima della patch. Qualcuno la usava gia'. Chi? Non si sa. Non lo dicono mai. Ma il pattern e' quello classico: zero-day nel renderer, CSS/font come vettore, nessuna interazione utente. Tipico di operazioni di sorveglianza mirata. NSO Group, Intellexa, e compagnia bella costruiscono catene di exploit esattamente cosi'. Roba da milioni di dollari, per spiare giornalisti e dissidenti. Con un font CSS.
// Perche' Succede Sempre
Sezione 09. Il problema strutturaleA questo punto ti starai chiedendo: ma e' un caso sfortunato? No. CVE-2026-2441 non e' un caso isolato. E' un pattern. Chrome ha avuto dozzine di UAF negli ultimi anni. Guarda il grafico qui sotto e capisci perche'. Blink e' scritto in C++. E il C++ non ha garbage collector.
In un linguaggio con garbage collector (Java, Go, Python), la memoria viene liberata solo quando nessuno la referenzia piu'. Nessun dangling pointer. Nessun UAF. Il costo e' performance. Il GC deve tracciare tutte le referenze. Ma almeno non ti spari nei piedi.
In C++, il programmatore decide quando liberare. E' piu' veloce, ma ogni free() e' una promessa: "giuro che nessun altro ha un puntatore a questa memoria". Se sbagli, hai un UAF. E in un codebase di milioni di righe, qualcuno sbaglia sempre.
Google lo sa. Ha investito in MiraclePtr (puntatori che controllano se la memoria e' stata liberata), in Oilpan (garbage collector per il DOM), nella migrazione a Rust per i componenti nuovi. Ma Blink ha milioni di righe di C++ legacy. La migrazione e' lenta. E nel frattempo, un asterisco di troppo in un parser CSS per font diventa uno zero-day. La vita.
Il paradosso: la feature CSS piu' inutile del mondo (@font-feature-values, che quasi nessuno usa) ha prodotto lo zero-day piu' pericoloso del 2026 (finora). Piu' codice c'e', piu' superficie di attacco c'e'. E Chrome ha molto, molto codice. Tipo, piu' righe di codice del Signore degli Anelli ha parole. Di parecchio.
// Come Difendersi
Sezione 10. La praticaLa risposta breve, quella che ho dato alla iena: aggiorna Chrome. La versione 145.0.7632.75 o successiva contiene il fix. Fine. Vai. Fallo adesso. Ti aspetto.
La risposta lunga, per chi non si accontenta e vuole capire i layer di protezione:
| Layer | Protezione | Cosa ferma |
|---|---|---|
| Aggiornamento | Chrome auto-update | Il bug stesso (deep copy) |
| Sandbox | Processo renderer isolato | Code execution resta confinata |
| Site Isolation | Ogni sito in un processo separato | Un sito malevolo non legge i dati di un altro |
| ASLR | Indirizzi di memoria randomizzati | L'attaccante non sa dove puntare |
| MiraclePtr | Smart pointer che rileva UAF | Crash controllato invece di exploitation |
| V8 Sandbox | Isolamento del motore JavaScript | Limita la primitiva di heap spray |
Il punto: nessun layer e' perfetto da solo. La sicurezza moderna funziona per accumulo, tipo le cipolle di Shrek. Ogni layer alza il costo dell'exploit. Un UAF nel CSS da solo costa poco. Un UAF + sandbox escape + kernel exploit costa milioni. E quei milioni sono il budget di NSO Group, non del ragazzino nel seminterrato. Se non sei un dissidente politico o un giornalista investigativo, probabilmente non ti cercano con catene da tre bug. Ma aggiorna Chrome lo stesso. Dai.
// Il Font Non E' Innocuo
Sezione 11. EpilogoRicapitoliamo il viaggio, perche' e' stato un bel giro. Una regola CSS che nessuno usa (@font-feature-values) crea una mappa interna. La mappa usa una HashMap. La HashMap ha un iteratore con un puntatore raw. JavaScript puo' mutare la mappa durante l'iterazione. La HashMap si rialloca. Il puntatore diventa dangling. Chrome legge memoria liberata. Un attaccante riempie quella memoria con i suoi dati. Chrome li esegue.
Tutto questo succede quando apri una pagina web. Zero click. Nessun download. Nessun popup. Nessun avviso. Un font.
La mattina dopo le ho aggiornato Chrome io. 145.0.qualcosa. "Fatto, sei a posto." La iena ha annuito, con la stessa convinzione con cui annuisce quando le spiego la differenza tra HTTP e HTTPS. Cioe' zero. Poi e' tornata a scrollare TikTok.
Panna mi ha guardato dal divano. Ho la netta impressione che Panna capisca i dangling pointer meglio di quanto lasci credere. Ha quell'aria. Quella di chi sa che il puntatore e' morto ma non te lo dice perche' vuole vedere come va a finire.
Dopo un po' ha alzato gli occhi dal telefono. "Ma tu guadagni qualcosa a smontare 'ste cose?" Pausa. "Perche' le mie galline almeno le uova le fanno. Tu stai li' a smontare cose che la gente normale non sa neanche che esistono."
"Appunto. Per quello qualcuno le deve smontare."
"Si' va bene. Pero' i biscotti al miele li cerco da un altro sito."
Almeno quello l'ha capito.
E il browser esegue il codice di qualcun altro."
CVE-2026-2441 ci ricorda che la superficie di attacco di un browser moderno non e' dove pensi. Non e' nel JavaScript. Non e' nell'HTML. E' in una funzione CSS per tipografi, in un puntatore nudo in un file che nessuno legge, in un rehash che nessuno ha previsto. Il codice piu' pericoloso e' quello che sembra innocuo.
Signal Pirate