Pular para o conteúdo
Engenharia de Software

Integer Overflow Explicado: O Bug Mais

Um número grande demais destruiu um foguete de 370 milhões de dólares. Integer overflow é o tipo de bug que parece simples mas mata sistemas em produção até hoje.

TL;DR

Integer Overflow Explicado: O Bug Mais. Um número grande demais destruiu um foguete de 370 milhões de dólares. Integer overflow é o tipo de bug que parece simples mas mata sistemas em produção até hoje.

Se eu te perguntar qual é o bug mais perigoso da programação, provavelmente você vai pensar em SQL injection, ou talvez XSS. Mas tem um bug mais antigo, mais simples e que já causou mais estrago do que qualquer vulnerabilidade web: integer overflow.

Integer overflow destruiu um foguete de 370 milhões. Causa vulnerabilidades de segurança em kernels de sistemas operacionais até hoje. E o mais irônico: é um bug que qualquer dev entende em 5 minutos quando explicado direito. Mas a maioria nunca para pra pensar nele quando está escrevendo código.

O que é integer overflow

Computadores armazenam números inteiros em espaços fixos de memória. Um inteiro de 8 bits armazena valores de 0 a 255 (sem sinal) ou de -128 a 127 (com sinal). Um inteiro de 16 bits vai de 0 a 65535 (sem sinal) ou de -32768 a 32767 (com sinal). E assim por diante.

Integer overflow acontece quando você tenta colocar um número que não cabe naquele espaço. É como um odômetro de carro com 5 dígitos: quando chega em 99999 e soma mais 1, volta pra 00000. O número 'dá a volta'.

plaintext
Inteiro de 8 bits sem sinal (uint8):
Máximo: 255 (binário: 11111111)
255 + 1 = ???

  11111111  (255)
+ 00000001  (1)
----------
 100000000  (256 em 9 bits)

Mas só cabem 8 bits!
Resultado: 00000000 = 0

255 + 1 = 0  ← overflow!

Pra inteiros com sinal, é pior. O bit mais significativo indica o sinal. Quando um número positivo grande estoura, ele vira negativo. 127 + 1 num int8 com sinal dá -128. Um número que deveria ser muito grande de repente fica extremamente negativo. Imagina isso acontecendo num cálculo de velocidade de foguete.

O caso Ariane 5 em detalhe

Já mencionei o Ariane 5 no artigo sobre os 10 bugs que mudaram a história da tecnologia, mas aqui eu quero ir fundo no mecanismo técnico do que aconteceu.

O sistema de referência inercial (SRI) do Ariane 5 calculava a velocidade horizontal do foguete usando ponto flutuante de 64 bits. Em determinado momento, esse valor precisava ser convertido para inteiro de 16 bits para ser enviado a outro componente do sistema. O código fazia um cast direto, sem verificar se o valor cabia no tipo destino.

No Ariane 4, essa conversão nunca dava problema porque o foguete era mais lento — a velocidade horizontal nunca ultrapassava 32767. O Ariane 5 era mais potente. 37 segundos após o lançamento, a velocidade horizontal ultrapassou 32767. O cast falhou. O SRI gerou uma exceção. O sistema de backup tentou assumir — mas rodava o mesmo código e deu a mesma exceção.

Com os dois SRIs fora, o sistema de navegação recebeu dados de diagnóstico em vez de dados de navegação. Interpretou esses dados como se fossem coordenadas e mandou os motores corrigirem a trajetória violentamente. O foguete girou fora de controle e se autodestruiu.

ada
-- Código real do SRI do Ariane 5 (Ada)
-- A variável BV era velocidade horizontal (float 64)
-- L_M_BV_32 era a versão inteira de 16 bits

pragma Suppress(Numeric_Error);
-- ^ Essa linha desabilitava a proteção de overflow!
-- Foi removida de 4 variáveis por "otimização"
-- Mas mantida em 3 outras

L_M_BV_32 := TDB.T_ENTIER_16S(BV);
-- BV > 32767.0 → exceção Operand_Error
-- Handler de exceção: desliga o SRI inteiro
-- SRI primário e backup: mesmo código → mesmo crash

O detalhe que mata

A proteção de overflow existia no código original do Ariane 4.

Foi removida em 4 variáveis por motivo de 'otimização de performance'.

Uma dessas 4 variáveis era justamente a velocidade horizontal.

Os engenheiros calcularam que o valor nunca excederia o limite — baseado nos parâmetros do Ariane 4.

Overflow em C e C++: undefined behavior

Em C e C++, overflow de inteiro com sinal (signed integer) é oficialmente undefined behavior. Isso significa que o compilador pode fazer literalmente qualquer coisa. Pode dar a volta no número, pode manter o valor máximo, pode formatar seu disco rígido. Qualquer coisa é 'válida' segundo o padrão da linguagem.

Na prática, o que acontece com mais frequência é que o compilador otimiza baseado na premissa de que overflow nunca acontece. Isso gera bugs sutis e difíceis de reproduzir.

c
// Em C, esse código é uma armadilha:
int check_add(int a, int b) {
    if (a + b < a) {  // tentativa de detectar overflow
        return -1;    // overflow detectado!
    }
    return a + b;
}

// O compilador com otimização (-O2) pensa assim:
// "overflow de signed int é undefined behavior"
// "logo, a + b NUNCA é menor que a" (premissa do compilador)
// "logo, essa condição é sempre falsa"
// "logo, posso remover o if inteiro"

// Resultado com -O2: a proteção de overflow é removida!
// A função NUNCA retorna -1, mesmo com overflow.

É por isso que em C, se você precisa detectar overflow, tem que checar antes de fazer a operação, não depois. Ou usar unsigned integers, onde overflow é bem definido (dá a volta). Ou usar funções de safe math como __builtin_add_overflow do GCC.

Overflow em Java: definido mas traiçoeiro

Java define que overflow de inteiro dá a volta. Não é undefined behavior como em C — o resultado é previsível. Mas 'previsível' não significa 'correto'. Se você está calculando o preço total de um carrinho de compras e o resultado estoura, o valor vira negativo. Previsível. Mas seu cliente vai pagar -R$ 500? Não.

java
// Java: overflow é definido (wraps around)
int max = Integer.MAX_VALUE; // 2147483647
int resultado = max + 1;     // -2147483648 (!)

// Math.addExact lança exceção em overflow
try {
    int seguro = Math.addExact(max, 1);
} catch (ArithmeticException e) {
    // overflow detectado!
}

// Desde Java 8: use Math.addExact, multiplyExact, etc.
// para operações que PRECISAM ser corretas

JavaScript: sem overflow clássico, mas com surpresas

JavaScript não tem tipo inteiro separado. Todos os números são IEEE 754 float de 64 bits (double). Isso significa que JavaScript não tem integer overflow clássico. Mas tem outro problema: perda de precisão para números grandes.

javascript
// JavaScript: todos os números são float64
9007199254740992 + 1 === 9007199254740992 // true (!)
// Acima de 2^53, JS perde precisão em inteiros

// Number.MAX_SAFE_INTEGER = 9007199254740991
// Acima disso, operações com inteiros ficam erradas

// Solução: BigInt (desde ES2020)
const grande = 9007199254740992n + 1n; // 9007199254740993n
// BigInt nunca perde precisão e não tem overflow
// Mas não dá pra misturar com Number sem cast explícito

// CUIDADO: muitas APIs retornam Number, não BigInt
// IDs de banco com mais de 53 bits: use string, não number

Python: o paraíso dos inteiros

Python é a exceção feliz. Inteiros em Python têm tamanho arbitrário — crescem conforme necessário. Não existe overflow. Você pode calcular 2 elevado a 1 milhão e Python vai alocar memória suficiente pra guardar o resultado. Sem problemas, sem erros, sem surpresas.

python
# Python: inteiros crescem automaticamente
x = 2 ** 1000
print(len(str(x)))  # 302 dígitos, sem overflow

# Isso nunca estoura em Python:
resultado = 999999999999999999 * 999999999999999999
# Funciona perfeitamente

# MAS: se você usa numpy, volta a ter overflow!
import numpy as np
np.int32(2147483647) + np.int32(1)  # -2147483648
# numpy usa tipos fixos por performance
# Sempre use Python puro para cálculos onde overflow importa

Rust: overflow é erro em debug, wrap em release

Rust tem uma abordagem interessante. Em modo debug, overflow de inteiro causa panic — o programa para com erro. Em modo release, faz wrap (dá a volta) por motivos de performance. E se você precisa de um comportamento específico, tem métodos explícitos.

rust
// Rust: abordagem explícita para overflow
let x: u8 = 255;

// Em debug: panic! Em release: wraps to 0
let y = x + 1;

// Métodos explícitos — sem ambiguidade:
let a = x.wrapping_add(1);   // 0 (wrap)
let b = x.saturating_add(1); // 255 (satura no máximo)
let c = x.checked_add(1);    // None (retorna Option)
let d = x.overflowing_add(1); // (0, true) — valor + flag

Eu gosto muito da abordagem do Rust. Em vez de fingir que overflow não existe (C) ou silenciosamente dar a volta (Java), Rust te obriga a tomar uma decisão explícita sobre o que fazer quando o número não cabe. Cada método (wrapping, saturating, checked, overflowing) comunica claramente a intenção.

Como detectar overflow no seu código

Esperar o bug aparecer em produção não é estratégia. Existem ferramentas que detectam overflow antes que ele cause problema.

Overflow como vetor de ataque

Integer overflow não é só um bug funcional — é um dos vetores de ataque mais explorados em segurança. Quando um atacante controla o input que causa overflow, ele pode manipular o comportamento do programa de formas devastadoras.

Um cenário clássico: o código calcula o tamanho de um buffer como 'número_de_itens * tamanho_de_cada_item'. Se número_de_itens for grande o suficiente pra causar overflow, o resultado do cálculo é um número pequeno. O programa aloca um buffer pequeno. Depois tenta escrever todos os itens nesse buffer pequeno. Buffer overflow. O atacante controla o que é escrito além do buffer — e pode executar código arbitrário.

c
// Vulnerabilidade real (padrão CVE recorrente)
void process_items(unsigned int count) {
    // count = 1073741824 (2^30)
    // sizeof(struct item) = 16
    // 1073741824 * 16 = 17179869184 (> 2^32)
    // Com uint32: overflow! Resultado: 0
    
    size_t total = count * sizeof(struct item);
    // total = 0 (!)
    
    char *buf = malloc(total);
    // malloc(0) retorna ponteiro válido
    
    for (unsigned int i = 0; i < count; i++) {
        memcpy(buf + i * sizeof(struct item),
               &items[i], sizeof(struct item));
        // Escrevendo MUITO além do buffer alocado
        // → heap overflow → RCE (Remote Code Execution)
    }
}

CVEs famosos causados por integer overflow

A lista de CVEs causados por integer overflow é absurdamente longa. Kernels de Linux e Windows, OpenSSH, libpng, zlib, ffmpeg — basicamente qualquer software escrito em C que faz cálculos com tamanhos de buffers já teve alguma variante desse bug.

  1. CVE-2014-1266 (goto fail): não é overflow puro, mas a família de bugs de validação em C que o overflow habita
  2. CVE-2021-21220 (Chrome V8): integer overflow no JIT compiler do V8 permitia execução de código via página web
  3. CVE-2016-0728 (Linux Kernel): overflow no reference counter do keyring permitia escalação de privilégio
  4. CVE-2019-14287 (sudo): overflow em user ID permitia rodar comando como root quando não deveria
  5. CVE-2023-4863 (libwebp): heap overflow via imagem WebP maliciosa, afetou Chrome, Firefox, Signal e mais

Como prevenir overflow no seu código

Guia prático anti-overflow

  • Em C/C++: use safe math functions (__builtin_add_overflow, SafeInt) em vez de operadores diretos
  • Em Java: use Math.addExact / Math.multiplyExact pra operações que precisam ser corretas
  • Em JavaScript: use BigInt quando os valores podem ultrapassar Number.MAX_SAFE_INTEGER
  • Em qualquer linguagem: valide inputs antes de usar em cálculos — rejeite valores absurdamente grandes
  • Rode UBSan/ASan nos testes do CI — detectam overflow automaticamente sem mudar o código
  • Em cálculos de tamanho de buffer: verifique overflow ANTES do malloc, não depois
  • Prefira tipos maiores quando a performance aceita: use int64 em vez de int32 quando possível
  • Em Rust: use checked_add/saturating_add — faça a decisão explícita

A ironia do overflow

O integer overflow é talvez o bug mais democrático da computação. Não importa se você é um estagiário ou um engenheiro da ESA com 30 anos de experiência. O bug é o mesmo. A aritmética é a mesma. Um número grande demais num espaço pequeno demais. Simples assim.

O que diferencia o dev que evita overflow do dev que é pego de surpresa não é inteligência — é hábito. É ter no instinto que toda multiplicação pode estourar. Que todo cast pra tipo menor pode perder dados. Que todo input do usuário pode ser absurdamente grande. Quando esse instinto vira automático, overflow para de ser ameaça e vira checklist.

Se esse tema de bugs históricos te interessou, o artigo sobre os 10 bugs que mudaram a história da tecnologia traz o contexto completo de cada desastre. E pra entender como a indústria tenta prevenir esses bugs em software de missão crítica, o artigo sobre code review pra sistemas críticos mostra as técnicas de revisão que a aviação e a medicina usam.