
Tailscale ajuda a achar bug de 16 anos no SQLite
Entre o fim do ano passado e o início deste ano, a Tailscale enfrentou meses de instabilidade no plano de controle. A causa era um bug profundo no SQLite, responsável por 19 incidentes separados de corrupção de banco de dados em seis meses.
Arquitetura e impacto
O plano de controle da Tailscale é dividido internamente em shards de coordenação. Cada tailnet fica em um shard, que usa um banco SQLite acessado exclusivamente por um único processo Go, seguindo o padrão recomendado de single-writer. A empresa utiliza SQLite como banco principal desde 2022 justamente por ser uma tecnologia considerada estável e amplamente testada.
Os incidentes começaram quando um pipeline de backup detectou corrupção em um banco. As corrupções não tinham padrão claro: não estavam ligadas a um shard específico, cliente, horário ou nível de carga. Durante os reparos, o processo do shard era interrompido, impedindo que novos dispositivos entrassem na rede e bloqueando temporariamente o console web e a API. Dispositivos já conectados continuavam funcionando, mas sem aprender mudanças na rede. Como os bancos guardam apenas metadados de configuração, não houve exposição de chaves privadas ou tráfego.
Investigação
A Tailscale não conseguiu reproduzir o bug sinteticamente e passou a coletar telemetria forense em produção. A empresa contratou suporte profissional dos desenvolvedores do SQLite, e juntos descartaram várias hipóteses: locks POSIX quebrados no close(), gerenciamento incorreto de memória ou uso indevido de múltiplas threads.
Para acelerar a recuperação, a equipe criou um pipeline de log de transações. Como o SQLite é single-writer com transações serializáveis, o histórico era linear e determinístico. Ao tentar reproduzir backups a partir desse log, surgiram pistas importantes: dados gravados e confirmados por uma transação ficavam invisíveis para transações posteriores, sem gerar erro.
O bug WAL-Reset
Os desenvolvedores do SQLite criaram um shim de debug na camada de sistema de arquivos virtual, chamado tmstmpvfs, para rastrear operações de checkpoint. Com esses logs, identificaram uma corrida rara entre um checkpoint e uma transação de escrita. Se uma escrita ocorria em um momento específico do checkpoint, o processo assumia incorretamente que algumas páginas já haviam sido copiadas do WAL para o arquivo principal. Essas páginas nunca eram gravadas, causando perda permanente de dados e corrupção.
O problema foi chamado de “WAL-Reset bug” e estava presente no SQLite havia pelo menos 16 anos. A Tailscale era mais suscetível porque controlava manualmente os checkpoints e os executava de forma muito agressiva.
Correção e falso alarme
A correção veio no SQLite 3.52.0, mas o rollout causou alertas de corrupção em 13 bancos. Não era corrupção real: uma otimização na conversão de texto para ponto flutuante alterou sutilmente o arredondamento e afetou índices em colunas geradas, fazendo o integrity_check reportar erro. A versão foi retirada e substituída pela 3.51.3, que continha apenas a correção da corrida. A Tailscale também reduziu a precisão de timestamps para segundos inteiros, e o SQLite 3.53.0 adicionou um recurso de auto-cura para índices de expressão.
Confirmação
Para confirmar que o bug realmente ocorria em produção, a Tailscale instrumentou o driver para registrar quando uma escrita e um reset de WAL coincidiam. O alerta demorou dois meses para disparar, mas comprovou que as condições exatas da corrida aconteciam. Desde então, a empresa passou mais quatro meses sem incidentes de banco.
Lição
A conclusão da equipe é que usar tecnologia estável de forma não padrão cria risco operacional. A configuração era documentada e suportada, mas o controle manual e agressivo dos checkpoints saiu do caminho comum e expôs um bug raríssimo. O episódio levou a melhorias em backup, recuperação, monitoramento e financiamento de ferramentas open-source para diagnóstico no SQLite.