1 pontos por GN⁺ 2025-04-24 | 1 comentários | Compartilhar no WhatsApp
  • No Windows 11 24H2, foi reproduzido um problema em que o hidroavião Skimmer desaparecia ou o jogador era lançado para uma altitude anormalmente alta logo após o spawn, e a causa não era o sistema operacional, mas um bug antigo de tratamento de dados dentro do jogo
  • A linha do Skimmer em vehicles.ide não tinha os 2 valores de escala das rodas exigidos para aviões, mas CFileLoader::LoadVehicleObject não verificava o valor de retorno de sscanf e usava variáveis locais não inicializadas como estavam
  • Em ambientes Windows antigos, o valor de escala de roda 0.7 do veículo TopFun anterior permanecia por acaso na pilha, fazendo o Skimmer parecer normal, mas no Windows 11 24H2 esse acaso se desfez quando o uso de pilha de LeaveCriticalSection mudou
  • A escala incorreta das rodas contaminou o cálculo da suspensão e a coordenada Z da caixa de colisão, e isso se propagou até o cálculo da altura de criação e da velocidade das hélices, resultando em posição anormal da câmera, efeito de burn-in e travamento em loop no ambiente com SilentPatch
  • A correção é adicionar -1, 0.7, 0.7, -1 à linha do Skimmer em vehicles.ide ou aplicar o próximo hotfix do SilentPatch; validação de dados de entrada e gestão de avisos de compilação afetam diretamente a compatibilidade de longo prazo

Sintoma do Skimmer revelado no Windows 11 24H2

  • No rastreador de issues do SilentPatch, surgiu um relato de que, após a atualização para o Windows 11 24H2, o avião Skimmer havia desaparecido completamente do jogo
    • Não aparecia nem com trainer, nem podia ser encontrado no local de spawn original
    • O problema ocorria tanto em jogos com mods quanto em uma cópia vanilla com apenas o SilentPatch aplicado
  • Nos fóruns do GTAForums, o mesmo problema também foi relatado desde novembro de 2024, e embora alguns usuários suspeitassem do SilentPatch, o mesmo comportamento aparecia até em uma instalação totalmente sem mods
  • No Windows 10 22H2 e no Windows 11 23H2, o Skimmer surgia normalmente, enquanto usuários do Windows 11 24H2 enfrentavam o bug
  • A depuração remota em uma máquina virtual 24H2 mostrou que outros aviões e barcos funcionavam normalmente, e apenas o Skimmer desaparecia

Altitude anormal e loop infinito das hélices

  • Ao criar o Skimmer à força por script e colocar CJ dentro dele, o jogador era lançado para 1.0287648030984853e+0031 m, cerca de 10,3 nonilhões de metros de altura
  • Com o SilentPatch instalado, o jogo entrava em loop e travava logo após lançar o jogador para cima
  • Sem o SilentPatch, o jogo não travava, mas aparecia o famoso efeito de burn-in que ocorre quando a câmera vai para uma posição próxima do infinito
  • O ponto do travamento estava no loop de normalização do ângulo das pás do rotor em CPlane::PreRender
    • O valor de m_fBladeSpeed crescia até 3.73340132e+29
    • Mesmo subtraindo 6.2831855 repetidamente, o valor não mudava na representação de ponto flutuante, então o loop nunca terminava
  • Como a velocidade das hélices deriva de um valor proporcional à altitude do avião, isso indicava que o Skimmer já estava sendo criado em uma posição absurdamente alta desde o início

Cálculo da suspensão que contaminou a caixa de colisão

  • A função de criação por script CCarCtrl::CreateCarForScript soma ao Z recebido o resultado de GetDistanceFromCentreOfMassToBaseOfModel
  • Ao inspecionar a caixa de colisão do Skimmer, bbox.sup.z estava contaminado com um valor absurdo como -4.30747210e+33
  • O rastreamento com breakpoint de dados mostrou que, no carregamento inicial, o valor da caixa de colisão era normal
    • O bbox.sup.z inicial era -2.21952772
    • Depois, quando o veículo era criado pela primeira vez, SetupSuspensionLines atualizava a coordenada Z da caixa de colisão refletindo a altura da suspensão
  • O problema estava em um dos valores de entrada usados no cálculo das linhas de suspensão
    • O cálculo usa os limites superior e inferior da suspensão em handling.cfg e a escala das rodas em vehicles.ide
    • Os valores do Skimmer em handling.cfg não eram muito diferentes dos de outros aviões

A linha curta do Skimmer em vehicles.ide

  • A definição do Skimmer em vehicles.ide é mais curta que a dos outros aviões, e faltam os 4 últimos parâmetros
  • Entre os valores ausentes, 2 são a escala das rodas dianteiras e traseiras
  • Em barcos, a ausência desses valores não causa problema, mas o Skimmer é o único avião que omite esses parâmetros
  • Parece que o Skimmer era definido como barco em Vice City e passou a ser avião em San Andreas, sem que os novos parâmetros necessários fossem adicionados
  • Ao recolocar os parâmetros ausentes, o Skimmer volta a funcionar normalmente

O loader que não verificava o retorno de sscanf

  • CFileLoader::LoadVehicleObject faz o parse de uma linha de vehicles.ide com sscanf, assumindo que todos os parâmetros sempre existem
  • A função não verifica o valor de retorno de sscanf e também não define valores padrão para a maior parte dos últimos parâmetros
    • wheelModelID fica não inicializado
    • frontWheelScale e rearWheelScale também ficam não inicializados
    • Apenas wheelUpgradeClass é inicializado com -1
  • Em linhas com valores faltando, como a do Skimmer, as variáveis de escala das rodas permanecem não inicializadas, e esse valor se propaga para os dados do veículo
  • A correção do SilentPatch envolve encapsular a chamada de sscanf e fornecer valores padrão para os 4 últimos campos
    • wheelModelID = -1
    • frontWheelSize = 0.7f
    • rearWheelSize = 0.7f
    • wheelUpgradeClass = -1
  • O commit da correção foi aplicado no repositório do SilentPatch

Por que isso ficou escondido por 20 anos

  • San Andreas usa CRT compilada estaticamente, então não foi um hotfix no nível da CRT do Windows que mudou o comportamento do sscanf
  • No Windows 10, o valor 0.7 permanecia na posição da variável local imediatamente antes do parse do Skimmer
    • Esse valor coincide com a escala de roda do TopFun, definido logo antes do Skimmer
    • A linha do TopFun contém -1, 0.7, 0.7, -1
  • vehicles.ide é lido em ordem, e LoadVehicleObject é chamado para cada linha
  • No Windows 10, essa posição da pilha não era sobrescrita entre as chamadas de LoadVehicleObject, então o Skimmer acabava herdando por acaso a escala de roda do TopFun
  • No Windows 11 24H2, durante a leitura da linha seguinte, LeaveCriticalSection dentro de fgets passou a usar mais espaço de pilha, e o valor remanescente foi sobrescrito

O Windows 11 24H2 foi apenas o gatilho

  • A forma como funções internas da WinAPI usam a pilha não é um comportamento contratual e pode mudar sem aviso prévio
  • O Windows 11 24H2 apenas eliminou o valor residual de pilha do qual o jogo dependia por acaso; a causa real era o comportamento indefinido do próprio jogo
  • Mesmo no Windows 10, a variável local logo após a escala das rodas já era sobrescrita por LeaveCriticalSection, e o jogo já podia encontrar esse bug anos atrás
  • Como San Andreas também suportava Windows 98, esse bug simplesmente não apareceu por acaso em pelo menos uma dezena de versões do Windows e várias versões do Wine
  • O patch oficial 1.01 para PC não corrige esse bug, mas o lançamento original de Xbox incluía uma correção que definia o padrão como 1.0
    • Steam 3.0, newsteam e RGL herdam essa correção por serem baseados no branch de código do Xbox
    • As versões Android, X360 e PS3 da War Drum Studios, além da Definitive Edition, também não são afetadas

Por que o SilentPatch escolheu 0.7 como padrão

  • O SilentPatch usa 0.7 como escala padrão das rodas, e não 1.0 como na correção da Rockstar para Xbox
  • A escolha se baseia em três pontos
    • No PC, o Skimmer vinha funcionando na prática com a escala 0.7 do TopFun até agora
    • Outros veículos não-barco que flutuam na água, como Sea Sparrow e Vortex, também usam escala de roda 0.7
    • Muitos carros do jogo também usam escala de roda 0.7

Como corrigir manualmente

  • A correção no código será incluída no próximo hotfix do SilentPatch
  • Para corrigir imediatamente, basta abrir data\vehicles.ide no diretório de San Andreas com o Bloco de Notas e substituir a linha que começa com 460, skimmer
  • A linha a ser substituída é a seguinte
460, 	skimmer,	skimmer, 	plane,		SEAPLANE,	SKIMMER,	null,	ignore,		5,	0,	0,		-1, 0.7, 0.7,		-1

A lição deixada pela compatibilidade com jogos antigos

  • Esse problema era um bug simples de San Andreas, e aquela função nunca poderia ter funcionado corretamente desde o início
  • Mudanças no layout de pilha da implementação interna também podem virar problemas de compatibilidade quando um aplicativo com bug depende por acaso de um comportamento específico
  • Um caso parecido foi Bully: Scholarship Edition, quebrado no Windows 10 por depender de suposições erradas até que mudanças no sistema operacional expuseram o problema
  • O problema fundamental de San Andreas era a falta de validação dos dados de entrada, que não conseguia rejeitar linhas de configuração incompletas
  • Esse código provavelmente já emitia avisos de compilação originalmente, e ignorar ou desativar esses avisos pode fazer com que bugs escondidos por muito tempo se transformem em problemas reais para usuários

1 comentários

 
GN⁺ 2025-04-24
Opiniões no Hacker News
  • Este é o tipo de texto que eu esperaria do Raymond Chen, e isso é um enorme elogio.
    É bom ver que eles foram mais fundo e descobriram exatamente o porquê.

  • Pessoalmente, acho que qualquer comportamento não incluído no contrato deveria ser randomizado.
    Por exemplo, se uma linguagem não garante a ordem de iteração de um mapa, ela deveria randomizar essa ordem de propósito.
    Caso contrário, surgem códigos frágeis que “funcionam bem até que, um dia, quebram”.

    • Existem várias opções de compilador, como -ftrivial-auto-var-init, que inicializam variáveis não inicializadas com um valor específico ou aleatório.
      Mas randomizar ou zerar todo o conteúdo da pilha a cada chamada de função causaria uma queda de desempenho terrível, então isso normalmente não é feito.
    • Esse nível de randomização é caro demais.
      Há ferramentas que fazem isso para fins de depuração, mas, nesse modo, o programa roda muito mais devagar.
    • Do ponto de vista de contrato, há também esta lição do texto original: “É uma lição interessante de compatibilidade. Se um aplicativo tem um bug e depende sem querer de um comportamento específico, até uma mudança no layout da pilha de uma implementação interna pode ter impacto de compatibilidade”.
      Talvez seja por isso que os mantenedores do kernel Linux insistem tanto em nunca quebrar o espaço de usuário.
    • Não. É preciso lembrar de https://www.hyrumslaw.com/.
      Se houver usuários suficientes de uma API, não importa o que o contrato prometeu: alguém vai depender de todo comportamento observável do sistema.
      Se você prometer randomização, alguém vai depender até dessa randomização.
      Então ela também não poderá mais ser removida para sempre.
    • Uma das vantagens de linguagens como C pode ser vista como pagar custo apenas pelos recursos que você escolhe usar.
      Você não é obrigado a pagar por overhead desnecessário, como inicializar variáveis que não usa.
  • Na parte sobre “não ignore avisos de compilação”, não sei que erro de compilador seria esperado aqui.
    Talvez algo como não verificar se o valor de retorno de scanf bate com o número de argumentos? Fora isso, parece um erro no arquivo de dados que o compilador não teria como saber.

    • Testando com g++ 11.4, não há aviso padrão mesmo sem verificar o retorno de sscanf.
      Num exemplo pequeno, mesmo usando g++ -Wall -Wextra -Wunused-result, não aparece aviso.
    • Acessar memória não inicializada é comportamento indefinido, então um sanitizador teria detectado.
    • Bom ponto. Ao ler, eu tinha pensado vagamente que um aviso de “uso de memória não inicializada” pegaria isso.
      Mas, como a linha inteira é analisada por uma única chamada a sscanf, a análise estática do compilador não tem opção a não ser assumir que os valores agora foram inicializados.
      Não parece haver um método geral de análise estática para detectar esse bug.
      Ainda assim, talvez desse para criar um aviso específico para scanf, obrigando a passar valores previamente inicializados ou a verificar o valor de retorno.
  • É sempre um prazer ler esse tipo de análise técnica profunda.
    Fico curioso para saber se textos assim vão ficar mais raros na era da IA.

    • Não acho que vão ficar mais raros. Sempre haverá engenheiros de ponta que investigam a fundo.
      A IA não vai substituí-los, assim como mais de 50 anos de inovações no desenvolvimento de software também não substituíram.
      Milhões, talvez dezenas de milhões, de desenvolvedores de linguagens de alto nível conhecem a diferença entre stack e heap só como uma teoria aprendida vagamente na escola, e não se importam porque não precisam pensar nisso no trabalho do dia a dia.
    • O engenheiro de software comum pode estar se deslocando de artesão para algo mais próximo de técnico, mas textos assim parecem vir justamente de um estilo artesanal.
  • Fiquei mais curioso para saber o que mudou na implementação de bloqueio/desbloqueio de seções críticas nessa versão do Windows.

    • Parece que aumentou o tamanho da pilha usado ou a região de proteção da pilha.
  • Só eu acho este código incômodo?
    while (this->m_fBladeAngle > 6.2831855) { this->m_fBladeAngle = this->m_fBladeAngle - 6.2831855; }
    Parece que usaram um loop while, que pode até virar loop infinito, só para evitar fazer uma divisão.

    • Quero acreditar que os desenvolvedores de GTA fizeram esse hack porque era mais rápido que divisão de ponto flutuante em ambientes como o PlayStation 2.
      Mas, considerando que eles conseguiram aumentar o carregamento do GTA5 em 5 minutos analisando JSON com sscanf, minhas expectativas não são tão altas.
    • Acho bem provável que tenha sido por desempenho. Subtração é mais barata que divisão de ponto flutuante.
      O compilador também pode ter técnicas para otimizar melhor isso.
      Na prática, quase não há como isso virar loop infinito. Underflow é possível, mas, para isso, o ângulo já teria de ser menor que 2*pi, então o loop terminaria.
    • É pouco provável, mas, se o valor for pequeno, esse loop talvez seja mais rápido que uma divisão.
    • É isso mesmo. Parece que o autor simplesmente não conhecia fmod.
  • Quem estiver com problema de acesso pode usar este link:
    https://web.archive.org/web/20250423144746/https://cookieplm...

  • Por conhecer C/C++, eu já suspeitei mais ou menos do que estava acontecendo desde o começo do blog: um problema de variável não inicializada.
    É impressionante que exista uma linguagem que permita deixar variáveis sem inicializar. Isso já causou incontáveis bugs, incluindo bugs de produção que vi pessoalmente, e muitas vezes é preciso depender de flags extras do compilador, ferramentas de análise estática, Valgrind etc. para pegá-los.
    Mesmo linguagens mais modernas adotando outras soluções, como usar valor zero por padrão ou exigir inicialização antes do uso, as pessoas continuam voltando para C/C++.

  • A parte “Todas essas descobertas provam que o bug não é um problema do Windows 11 24H2. Coisas como o modo como uma função interna da WinAPI usa a pilha não fazem parte do contrato e podem mudar a qualquer momento sem aviso prévio” me lembrou um ótimo texto que li há algum tempo.
    A ideia principal era que, em uma API bem-sucedida o bastante, não existe algo como API privada.

    • Seria bom se você encontrasse esse texto e mandasse o link. Fiquei curioso com o argumento.
    • Pelo que sei, há uma tirinha do XKCD relacionada a isso.