Samuel Arendt

O PHP 8.5.5 corrigiu resultados errados no JIT

O PHP 8.5.5 corrigiu um bug em que o JIT podia produzir resultados aritméticos errados. O processo não precisava cair, emitir warning ou deixar uma exceção no monitoramento. O número retornado podia simplesmente estar incorreto.

O changelog oficial registra a correção do GH-20838 em OPcache. O relato verificado no repositório do PHP reproduziu o problema com opcache.jit=1255: alguns cálculos que entregavam valores plausíveis passaram a devolver um número próximo de zero. Com 1251, o mesmo programa produziu os valores esperados.

Esse alcance precisa acompanhar qualquer recomendação. O issue documenta uma configuração específica e um caminho de execução concreto. Ele não demonstra que toda aplicação PHP 8.5 calculou faturas ou fretes errados. Também não reduz a gravidade para quem usava a combinação afetada. O perfil da falha é o ponto mais útil para uma equipe: corrupção silenciosa pede controles diferentes dos usados para crash.

Primeiro descubra se o JIT estava ativo

Desde o PHP 8.4, o valor padrão de opcache.jit é disable. O buffer tem 64 MB por padrão, mas isso não ativa o compilador sozinho. Antes do 8.4, a desativação padrão acontecia pelo buffer zerado. Em ambos os modelos, instalações podem sobrescrever a configuração no php.ini, em imagens de container ou em arquivos específicos do SAPI.

Consultar a versão do PHP não basta. A equipe precisa olhar a configuração efetiva de cada processo relevante. CLI e FPM podem carregar arquivos diferentes; jobs, workers e servidores web podem ainda usar imagens distintas. O inventário deve responder qual versão roda, qual SAPI executa a carga, se o JIT está ativo e qual valor de opcache.jit foi aplicado.

Esse levantamento reduz dois erros comuns. O primeiro é mobilizar uma aplicação cujo JIT nunca esteve ligado. O segundo é atualizar o container web e esquecer workers antigos que executam cálculos em outra imagem.

Quem encontrar PHP 8.5 com JIT habilitado deve atualizar para o patch mais recente disponível da série, em vez de mirar especificamente o 8.5.5 meses depois. A versão 8.5.5 é onde a correção apareceu; patches posteriores a incorporam e acumulam outras correções.

Monitoramento de erro não detecta um número válido

Falhas tradicionais deixam sinais úteis. Um segfault reinicia processo, um erro fatal aumenta taxa de respostas 500, uma exceção chega ao tracker. Um valor numericamente válido passa por essas camadas.

Imagine uma função que retorna float e recebe entradas dentro do domínio esperado. O tipo continua correto, a resposta serializa e a request termina com status 200. Métricas de disponibilidade ficam verdes. O defeito aparece apenas quando existe uma relação conhecida entre entrada e saída.

Por isso, invariantes de domínio merecem espaço próximo ao cálculo. Uma porcentagem pode ter limites, uma soma de parcelas pode reconciliar com um total e uma conversão pode ser comparada com vetores conhecidos. Nem todo valor tem uma regra simples, mas fluxos financeiros e de medição costumam oferecer alguns pontos de controle.

Essas verificações não precisam virar milhares de asserts na produção. Um conjunto pequeno de cálculos críticos, executado no pipeline com a mesma configuração de runtime, já detecta uma classe que o teste do código-fonte sozinho pode perder.

Um teste diferencial encurta a investigação

Para este tipo de incidente, eu acrescentaria um teste temporário que roda os mesmos vetores com JIT ligado e desligado. A versão interpretada funciona como comparação, sem ser tratada como uma implementação independente perfeita. Divergência em operações determinísticas exige investigação.

O ambiente precisa aquecer os caminhos do JIT. O compilador por tracing observa trechos quentes e gera código nativo conforme seus gatilhos. Um teste que chama a função uma vez pode nunca reproduzir a condição de produção. Use as opções reais, repita o fluxo e preserve as entradas que causaram diferença.

Também evitaria copiar o caso enorme do issue para todo projeto. Ele serve para confirmar a falha no runtime antigo. O teste permanente deve representar as operações do produto: cálculo de preço, rateio, pontuação, limite ou conversão usados de verdade.

Depois da atualização, três evidências fecham melhor o trabalho: o runtime efetivo reporta o patch novo; os vetores produzem o resultado esperado com a configuração de produção; os ambientes que executam o código foram reiniciados ou reciclados. Atualizar um arquivo de imagem sem substituir os processos mantém o binário antigo atendendo tráfego.

Desligar o JIT pode ser mitigação, com medida

Se a atualização imediata estiver bloqueada, desativar o JIT reduz a exposição ao caminho afetado. A decisão pode alterar desempenho e precisa ser testada na carga real. Para muitas aplicações web, banco, rede e serialização dominam o tempo; para workloads computacionais, a diferença pode importar.

O manual do PHP oferece disable, que impede ativação em runtime, e off, que permite ativá-lo depois. Em uma mitigação de incidente, uma configuração inequívoca é preferível. O rollout deve confirmar o valor efetivo nos processos, não apenas a intenção no repositório.

Desativar também não substitui o patch. A versão 8.5.5 corrigiu outros problemas de JIT, incluindo loop infinito em um acesso polimórfico a propriedade indefinida e um use-after-free. Manter o runtime atualizado remove correções conhecidas da lista de riscos pendentes.

O GH-20838 não pede pânico em todo projeto PHP. Pede uma pergunta verificável: nossa produção executou PHP 8.5 vulnerável com JIT ativo nessa família de configuração? Se a resposta for sim, a investigação precisa procurar resultados, não apenas erros.

Referências

#Operações, #Performance, #Qualidade de Software