SQLite aveva un bug nascosto da 16 anni: Tailscale lo ha trovato

Dopo 19 corruzioni in 6 mesi, Tailscale e gli sviluppatori SQLite hanno isolato il WAL-Reset bug: un bug raro presente nel DBMS SQL da ben 16 anni (corretto in SQLite 3.51.3).

SQLite è uno dei database più diffusi in assoluto: gira dentro browser, smartphone, applicazioni desktop, dispositivi embedded e servizi cloud, spesso senza che l’utente ne sia consapevole. Proprio per questo sorprende che una race condition introdotta con il supporto al Write-Ahead Logging (WAL), disponibile dalla versione 3.7.0 del 2010, sia riuscita a restare nascosta per quasi 16 anni.

A portarla alla luce è stata Tailscale, dopo una serie di episodi di corruzione apparentemente inspiegabili nei database usati internamente. Il problema non compariva sotto carichi estremi né richiedeva configurazioni anomale: scattava solo quando un checkpoint e una nuova scrittura sul WAL si sovrapponevano in una finestra temporale estremamente stretta. In quelle condizioni SQLite poteva considerare alcune pagine già trasferite nel database principale quando, in realtà, risiedevano ancora soltanto nel WAL.

Il difetto, oggi noto come WAL-Reset bug, ha provocato 19 incidenti in circa 6 mesi nei sistemi Tailscale ed è risultato particolarmente difficile da riprodurre in laboratorio.

L’indagine, condotta insieme agli sviluppatori di SQLite, ha richiesto nuovi strumenti di diagnostica e ha infine portato alla correzione inclusa in SQLite 3.51.3. La vicenda è interessante perché mostra quanto possano essere insidiose le race condition nei componenti più maturi: il codice può funzionare correttamente per miliardi di operazioni e fallire soltanto quando due eventi si incrociano nell’ordine sbagliato.

Come funziona il WAL di SQLite e dove risiedeva il problema

Per capire il problema basta ricordare il principio alla base del WAL: quando un’applicazione modifica il database, SQLite non aggiorna subito le pagine nel file principale. Le nuove versioni finiscono prima nel file -wal; in lettura è possibile continuare ad accedere al database mentre il writer aggiunge nuove modifiche.

Periodicamente interviene un checkpoint che copia le pagine dal WAL al database principale. Quando tutte le modifiche utili sono state trasferite e nessun lettore dipende più dal vecchio contenuto, SQLite può riutilizzare il WAL ripartendo dall’inizio. È proprio durante questa fase di reset che si nascondeva il difetto.

Per ben 16 anni, spiega Tailscale, non ci si è accorti che in una finestra temporale estremamente stretta, un secondo checkpoint poteva non accorgersi dell’accaduto: SQLite finiva così per conservare nell’indice WAL un’indicazione errata sul numero di pagine già trasferite nel database principale.

Al checkpoint successivo alcune pagine appena scritte potevano quindi essere saltate: SQLite le considerava già copiate, mentre in realtà esistevano soltanto nel WAL. Quando quel contenuto veniva poi riutilizzato, le modifiche risultavano perse.

SQLite organizza dati e indici in pagine collegate tra loro; se alcune pagine raggiungono il file principale e altre no, il database può ritrovarsi con strutture che non rappresentano più uno stato transazionale coerente. Da qui ne consegue un fenomeno di corruzione del database.

Perché Tailscale ha scoperto un bug così raro

SQLite utilizza normalmente checkpoint automatici e, con la configurazione predefinita, interviene quando il WAL raggiunge circa 1.000 pagine. Tailscale gestiva invece il checkpointing in modo più aggressivo: così, una race condition quasi impossibile da osservare in un’applicazione comune diventava abbastanza probabile da provocare 19 incidenti in pochi mesi.

Il caso Tailscale resta comunque notevole. Non tanto perché SQLite si sia dimostrato fragile, ma per il motivo opposto: un difetto presente dal 2010 ha richiesto miliardi di operazioni, checkpoint particolarmente frequenti e una strumentazione costruita appositamente prima di poter essere isolato con certezza.

Ti consigliamo anche

Link copiato negli appunti