EN
en direct

Tailscale débusque dans SQLite un bug de seize ans qui corrompait ses bases en silence

En six mois, Tailscale a subi dix-neuf incidents de corruption causés par une course de données vieille de seize ans dans le mécanisme de WAL-reset de SQLite. Si vous self-hébergez des applications SQLite, vérifiez la version embarquée et ne sortez pas du chemin balisé.

Un meuble ancien à casiers de bibliothèque dans une salle d’archives sombre, un tiroir entrouvert laissant dépasser une fiche cartonnée de travers, un filet de lumière ambre filtrant par l’interstice.

12 août 2026. Tailscale publie le récit d’une enquête qui a duré six mois. Dix-neuf incidents de corruption de base, un bug présent depuis seize ans, et une correction livrée deux fois — dont une retirée dans la foulée. Le coupable ne se trouvait pas chez Tailscale, mais au cœur de SQLite, la base de données « ennuyeuse » sur laquelle repose une large partie du web et de votre homelab.

L’histoire vaut la peine d’être lue en entier, parce qu’elle change ce que « fiable » veut dire. SQLite est partout — dans vos téléphones, vos navigateurs, et surtout dans les applications que vous self-hébergez : Immich, Paperless-ngx, Gitea, Vaultwarden, Nextcloud. Un bug de ce calibre, dormant depuis seize ans, rappelle que même la technologie la plus testée peut cacher une faille que seule une charge inhabituelle finit par réveiller.

Une panne que personne ne savait expliquer

Le plan de contrôle de Tailscale est découpé en « shards », des serveurs de coordination indépendants. Chaque shard porte une base SQLite unique, accédée par un unique processus Go — exactement le modèle à écrivain unique pour lequel SQLite est conçu. Depuis 2022, l’équipe sauvegarde chaque base en prenant un instantané complet toutes les quelques minutes, puis en l’envoyant vers un bucket S3.

En août 2025, un pipeline de lecture de ces sauvegardes signale une erreur. Un PRAGMA integrity_check confirme : la base est corrompue. La corruption est « possible » en théorie, mais rarissime en pratique. L’équipe répare, enquête, ne trouve rien — puis l’incident se reproduit. Et encore. Et encore. Dix-neuf fois en six mois.

Ce qui rendait le bug insaisissable, c’est son absence totale de régularité. Aucun facteur commun entre les incidents : ni shard, ni client, ni fonctionnalité, ni heure, ni niveau de charge. Parfois quelques heures entre deux pannes, parfois six semaines de calme trompeur — dont une pause entre octobre et décembre, avant un retour en guise de cadeau de Noël. Impossible de reproduire le bug de façon synthétique : il a fallu déployer de la télémétrie passive en production pour le prendre en flagrant délit.

Le mécanisme : une course entre checkpoint et écriture

Pour comprendre le bug, il faut savoir comment SQLite écrit. Une base est découpée en « pages ». En mode WAL (Write-Ahead Logging), les nouvelles pages ne vont pas directement dans le fichier principal : elles sont d’abord écrites dans le WAL, un journal. Périodiquement, ces pages sont recopiées du WAL vers le fichier principal : c’est le checkpoint.

Tailscale faisait une chose inhabituelle : l’équipe pilotait le checkpoint manuellement, à un rythme très agressif, pour produire des sauvegardes rapides et cohérentes. Ce choix, documenté et supporté, les a pourtant sortis du « chemin bien balisé » — et c’est précisément ce qui les a rendus vulnérables.

Les mainteneurs de SQLite ont construit un outil de débogage sur mesure, le shim tmstmpvfs, enveloppant la couche de fichiers virtuels pour tracer chaque écriture. Tailscale l’a financé et déployé en production. Au premier incident suivant, les logs ont parlé : une course de données rare entre un checkpoint et une transaction d’écriture.

Concrètement, si une écriture survient à un moment précis du checkpoint, le processus se persuade que certaines pages ont déjà été recopiées du WAL vers la base — alors qu’elles ne l’ont pas été. Ces pages ne sont jamais écrites, et leurs données sont perdues définitivement, sans la moindre erreur levée. Pire, d’autres pages qui référencent les pages manquantes — un index, par exemple — sont bien écrites, ce qui rend la base incohérente. Les mainteneurs l’ont baptisé le « WAL-Reset bug » et estiment qu’il dormait dans le code depuis au moins seize ans.

Le correctif, puis un faux positif

La correction est arrivée avec SQLite 3.52.0 : un simple contrôle supplémentaire dans la fonction de checkpoint, qui détecte si le WAL a été réinitialisé par un autre thread entre-temps. Tailscale a déployé prudemment, d’abord sur des shards canaris.

Le moniteur de sauvegarde est alors passé au rouge : treize bases signalées comme corrompues. Fausse alerte. Ces bases n’avaient rien — elles subissaient un second bug, celui des index d’expression périmés. Tailscale stockait des horodatages haute précision en texte, convertis en flottants dans une colonne générée ; la version 3.52.0 avait, au passage, modifié subtilement l’arrondi des conversions texte-vers-flottant. Résultat : des index dont les valeurs ne correspondaient plus à la colonne, signalés à tort comme de la corruption.

La leçon est double. SQLite a retiré la version 3.52.0 et publié 3.51.3, qui ne contient que le correctif du WAL-Reset. Puis la 3.53.0 a ajouté un mécanisme d’auto-réparation des index. De son côté, Tailscale a réduit la précision de ses horodatages à la seconde entière — une conversion texte-vers-entier sans ambiguïté. La preuve finale est venue deux mois plus tard : une alerte a confirmé que les conditions exactes du bug se produisaient bel et bien en production, sans provoquer de corruption. Depuis, quatre mois sans incident.

La pipeline de reprise qui a trahi le bug

Pendant la chasse, il fallait aussi faire tourner la plateforme. Tailscale a construit une pipeline de journalisation des transactions : chaque instruction SQL modifiant une base était diffusée vers un fichier de log séparé. Comme SQLite est mono-écrivain avec des transactions sérialisables, cet historique était linéaire et déterministe — impossible dans une base multi-écrivains comme PostgreSQL. Rejouer ces transactions sur la dernière sauvegarde saine permettait de reconstruire la base à son état le plus récent, en contournant la corruption.

La pipeline a fait mieux que restaurer : elle a fourni l’indice décisif. Dans deux incidents, les logs ne se rejouaient pas proprement, et des données écrites par une transaction devenaient invisibles pour les suivantes. Une écriture s’était évaporée sans lever d’erreur — l’anomalie qui a fini par pointer vers la course du checkpoint.

Pourquoi votre homelab est concerné

Vous ne pilotez probablement pas vos checkpoints à la main, et vous ne faites pas des millions d’écritures par minute. La probabilité que vous déclenchiez le WAL-Reset bug est infime : c’est une course de données rare, que SQLite a dû provoquer artificiellement en test pour la voir. Mais l’article de Tailscale contient trois enseignements qui vous concernent directement.

D’abord, la version. La plupart des applications self-hostées embarquent SQLite en statique, via leur image Docker. Vous ne « mettez pas à jour SQLite » : vous mettez à jour l’application. Vérifier la version embarquée n’est pas un luxe :

bash
sqlite3 --version
# 3.51.3 minimum pour le correctif WAL-Reset ; 3.53.0 ajoute l’auto-réparation d’index

Ensuite, la configuration par défaut. Le message de fond de l’enquête tient en une phrase : faire tourner une technologie ennuyeuse de façon non standard est un risque. Le mode WAL, le checkpoint automatique et l’écrivain unique sont massivement testés ; en sortir, même avec des options documentées, vous place sur un chemin que peu de monde emprunte.

Enfin, la vérification d’intégrité. Tailscale n’aurait jamais vu la corruption sans un moniteur qui exécute PRAGMA integrity_check sur ses sauvegardes en continu :

bash
sqlite3 /chemin/vers/ma-base.db "PRAGMA integrity_check;"
# attendu : « ok »

Une sauvegarde corrompue mais jamais restaurée est une fausse sécurité. Testez vos restaurations, pas seulement vos sauvegardes.

Verdict

Si vous self-hébergez des applications SQLite, deux actions suffisent : gardez vos images à jour pour embarquer SQLite ≥ 3.51.3, et ne touchez pas aux réglages de checkpoint ou de WAL sans raison documentée. Ajoutez un integrity_check régulier sur vos sauvegardes — il coûte quelques secondes par base.

Si vous exploitez SQLite en production avec un contrôle manuel des checkpoints — le profil exact de Tailscale —, passez en 3.51.3 ou plus immédiatement, et remettez en question ce choix de conception : le gain de vitesse se paie en risque de corruption silencieuse.

Le signal de fond : la fiabilité de SQLite n’est pas remise en cause, elle est au contraire renforcée par cette chasse. Mais elle repose sur un contrat — configurations standard, écrivain unique, vérifications d’intégrité. Sortez du contrat, et c’est vous qui devenez le testeur.

Références

Le brief cyber, chaque mardi

Les failles qui comptent, les correctifs à appliquer, en dix minutes de lecture.

Pas de spam. Désinscription en un clic.
à lire ensuite

Sur le même sujet

HCCF soumet à l’ICANN sa candidature pour le domaine .self dédié au self-hosting

La Human-Centered Computing Foundation a déposé auprès de l’ICANN sa candidature pour le TLD .self, un espace de noms pensé pour les projets auto-hébergés et centrés sur l’utilisateur. Derrière le symbole, la vraie question est de savoir ce qu’un nom de domaine peut réellement garantir — et si .self servira les auto-hébergeurs ou les désignera comme cibles.

← Retour au fil

Tapez au moins deux caractères.

naviguer ouvrir esc fermer