1 puntos por GN⁺ 2025-04-24 | 1 comentarios | Compartir por WhatsApp
  • En Windows 11 24H2 se reprodujo un problema en el que el hidroavión Skimmer desaparece o el jugador sale disparado a una altura anormalmente grande justo después de que aparezca; la causa no era el sistema operativo, sino un viejo bug de procesamiento de datos dentro del juego
  • En la línea del Skimmer en vehicles.ide faltaban los 2 valores de escala de ruedas necesarios para los aviones, pero CFileLoader::LoadVehicleObject no comprobaba el valor de retorno de sscanf y usaba tal cual variables locales sin inicializar
  • En entornos antiguos de Windows, el valor de escala de ruedas 0.7 del vehículo anterior, TopFun, quedaba por casualidad en la pila y hacía que el Skimmer pareciera funcionar con normalidad, pero en Windows 11 24H2 cambió el uso de pila de LeaveCriticalSection y esa casualidad dejó de sostenerse
  • La escala de ruedas incorrecta contaminó los cálculos de suspensión y la coordenada Z de la caja de colisión, y luego se propagó al cálculo de altura de creación y velocidad de las aspas, causando anomalías en la posición de la cámara, el efecto de burn-in y, en entornos con SilentPatch, un bucle infinito
  • La solución es agregar -1, 0.7, 0.7, -1 a la línea del Skimmer en vehicles.ide o aplicar el próximo hotfix de SilentPatch; esto muestra que la validación de datos de entrada y la gestión de advertencias de compilación afectan directamente la compatibilidad a largo plazo

Síntomas del Skimmer expuestos en Windows 11 24H2

  • En el issue tracker de SilentPatch apareció un reporte de que, después de actualizar a Windows 11 24H2, el avión Skimmer desaparecía por completo del juego
    • No aparecía ni con trainers, ni podía encontrarse en sus puntos de spawn originales
    • Se reproducía tanto en partidas con mods como en una copia vanilla con solo SilentPatch aplicado
  • En GTAForums también se reportaba el mismo problema desde noviembre de 2024, y aunque algunos usuarios sospechaban de SilentPatch, el mismo fenómeno ocurría en juegos completamente sin mods
  • En Windows 10 22H2 y Windows 11 23H2 el Skimmer aparecía con normalidad, mientras que los usuarios de Windows 11 24H2 sufrían el mismo bug
  • Al depurarlo de forma remota en una máquina virtual con 24H2, otros aviones y botes funcionaban bien, y el estado era que solo desaparecía el Skimmer

Altura anormal y bucle interminable de aspas

  • Al crear el Skimmer por la fuerza mediante un script y subir a CJ, el jugador salía disparado hasta 1.0287648030984853e+0031 m, aproximadamente 10.3 nonillion metros de altura
  • Si SilentPatch estaba instalado, el juego entraba en un bucle y se congelaba justo después de lanzar al jugador hacia arriba
  • Sin SilentPatch, el juego no se congelaba, pero aparecía el famoso efecto de burn-in que ocurre cuando la cámara se mueve a una posición cercana al infinito
  • El punto de congelamiento estaba en el bucle de normalización del ángulo de las aspas del rotor en CPlane::PreRender
    • El valor de m_fBladeSpeed crecía hasta 3.73340132e+29
    • Aunque se restara 6.2831855 repetidamente, el valor no cambiaba por la representación de punto flotante, así que el bucle nunca terminaba
  • Como la velocidad de las aspas se deriva de un valor proporcional a la altura del avión, esto daba la pista de que el Skimmer se estaba creando desde el inicio en una posición anormalmente alta

Cálculo de suspensión que contaminó la caja de colisión

  • La función de creación por script CCarCtrl::CreateCarForScript suma al valor Z recibido el resultado de GetDistanceFromCentreOfMassToBaseOfModel
  • Al revisar la caja de colisión del Skimmer, bbox.sup.z estaba contaminado con un valor absurdo como -4.30747210e+33
  • Al rastrearlo con un punto de interrupción de datos, el valor de la caja de colisión durante la carga inicial era normal
    • El bbox.sup.z inicial era -2.21952772
    • Luego, cuando el vehículo aparecía por primera vez, SetupSuspensionLines actualizaba la coordenada Z de la caja de colisión reflejando la altura de la suspensión
  • El problema estaba en uno de los valores de entrada usados en el cálculo de las líneas de suspensión
    • El cálculo usa los límites superior e inferior de suspensión de handling.cfg y la escala de ruedas de vehicles.ide
    • Los valores de handling.cfg del Skimmer no eran muy distintos de los de otros aviones

La línea corta del Skimmer en vehicles.ide

  • La definición del Skimmer en vehicles.ide es más corta que la de otros aviones y le faltan los últimos 4 parámetros
  • 2 de los valores faltantes son las escalas de ruedas delantera y trasera
  • En los botes no hay problema si estos valores no existen, pero el Skimmer es el único avión que omite esos parámetros
  • El Skimmer parece haber sido definido como bote en Vice City y luego cambiado a avión en San Andreas, sin que se agregaran los nuevos parámetros necesarios
  • Al volver a agregar los parámetros faltantes, el Skimmer funciona correctamente

Un loader que no comprobaba el valor de retorno de sscanf

  • CFileLoader::LoadVehicleObject parsea una línea de vehicles.ide con sscanf asumiendo que todos los parámetros siempre existen
  • Esta función no comprueba el valor de retorno de sscanf y tampoco asigna valores por defecto a la mayoría de los parámetros finales
    • wheelModelID no se inicializa
    • frontWheelScale y rearWheelScale tampoco se inicializan
    • Solo wheelUpgradeClass se inicializa con -1
  • En una línea con valores faltantes, como la del Skimmer, las variables de escala de ruedas quedan sin inicializar, y ese valor se propaga a los datos del vehículo
  • La corrección de SilentPatch consiste en envolver la llamada a sscanf y proporcionar valores por defecto para los últimos 4 valores
    • wheelModelID = -1
    • frontWheelSize = 0.7f
    • rearWheelSize = 0.7f
    • wheelUpgradeClass = -1
  • El commit de corrección quedó reflejado en el repositorio de SilentPatch

Por qué permaneció oculto durante 20 años

  • San Andreas usa un CRT compilado de forma estática, así que no fue un hotfix a nivel del CRT de Windows lo que cambió el comportamiento de sscanf
  • En Windows 10, justo antes de parsear el Skimmer, quedaba un valor 0.7 en la posición de las variables locales
    • Ese valor coincidía con la escala de ruedas de TopFun, definido justo antes del Skimmer
    • La línea de TopFun contiene -1, 0.7, 0.7, -1
  • vehicles.ide se lee en orden, y se llama a LoadVehicleObject por cada línea
  • En Windows 10, esa posición de la pila no se sobrescribía entre llamadas a LoadVehicleObject, por lo que el Skimmer heredaba por casualidad la escala de ruedas de TopFun
  • En Windows 11 24H2, durante la lectura de la siguiente línea, LeaveCriticalSection dentro de fgets usó más espacio de pila, y como resultado se sobrescribió el valor que había quedado

Windows 11 24H2 solo fue el detonante

  • La forma en que una función interna de WinAPI usa la pila no es un comportamiento garantizado por contrato y puede cambiar sin aviso previo
  • Windows 11 24H2 simplemente eliminó el valor residual de pila del que dependía el juego por casualidad; la causa real era el comportamiento indefinido del juego
  • Incluso en Windows 10, la variable local inmediatamente posterior a la escala de ruedas ya era sobrescrita por LeaveCriticalSection, y el juego estaba en condiciones de encontrarse con este bug desde hacía años
  • Como San Andreas también era compatible con Windows 98, este bug no llegó a manifestarse por casualidad en al menos una decena de versiones de Windows y varias versiones de Wine
  • El parche oficial 1.01 para PC no corrigió este bug, pero el lanzamiento original para Xbox sí incluía una corrección que asignaba el valor por defecto 1.0
    • Steam 3.0, newsteam y RGL se basan en la rama de código de Xbox, así que heredaron esta corrección
    • Los lanzamientos de War Drum Studios para Android, X360 y PS3, así como Definitive Edition, también están afectados

Por qué SilentPatch eligió 0.7 como valor por defecto

  • SilentPatch usa 0.7 como escala de ruedas por defecto, no 1.0 como la corrección de Rockstar para Xbox
  • La elección se basa en tres razones
    • En la versión de PC, el Skimmer hasta ahora funcionó en la práctica con 0.7, la escala de ruedas de TopFun
    • Otros vehículos no-bote que flotan sobre el agua, como Sea Sparrow y Vortex, también tienen escala de ruedas 0.7
    • Muchos autos del juego también tienen escala de ruedas 0.7

Cómo corregirlo manualmente

  • La corrección de código se incluirá en el próximo hotfix de SilentPatch
  • Para arreglarlo de inmediato, hay que abrir data\vehicles.ide en el directorio de San Andreas con el Bloc de notas y reemplazar la línea que empieza con 460, skimmer
  • La línea de reemplazo es la siguiente
460, 	skimmer,	skimmer, 	plane,		SEAPLANE,	SKIMMER,	null,	ignore,		5,	0,	0,		-1, 0.7, 0.7,		-1

Lecciones para la compatibilidad de juegos antiguos

  • Este problema era un bug simple de San Andreas, y esa función era código que desde el principio no podía funcionar correctamente
  • Incluso un cambio en el layout de la pila de una implementación interna puede convertirse en un problema de compatibilidad si una aplicación con bugs depende por casualidad de cierto comportamiento
  • Un caso similar es Bully: Scholarship Edition, que se rompió en Windows 10 por depender de una suposición incorrecta que quedó expuesta con un cambio del sistema operativo
  • El problema de fondo de San Andreas era la falta de validación de datos de entrada, que no filtraba una línea de configuración incompleta
  • Es probable que este código originalmente emitiera una advertencia de compilación, y si las advertencias se ignoran o se desactivan, bugs que permanecieron ocultos durante mucho tiempo pueden convertirse en problemas reales para los usuarios

1 comentarios

 
GN⁺ 2025-04-24
Opiniones de Hacker News
  • Este tipo de artículo es del nivel que uno esperaría de Raymond Chen, y eso es un gran elogio.
    Me alegra que hayan profundizado aún más para descubrir exactamente por qué pasaba.

  • Personalmente, creo que si es un comportamiento no incluido en el contrato, debería aleatorizarse.
    Por ejemplo, si un lenguaje no garantiza el orden de recorrido de un mapa, debería aleatorizarlo deliberadamente.
    De lo contrario, terminas con código frágil que “funciona bien hasta que un día se rompe”.

    • Hay varias opciones de compilador, como -ftrivial-auto-var-init, que inicializan variables no inicializadas con un valor específico o aleatorio.
      Pero si en cada llamada a función se aleatorizara o se llenara con ceros todo el contenido de la pila, la caída de rendimiento sería terrible, así que normalmente no se hace.
    • Este nivel de aleatorización es demasiado costoso.
      Hay herramientas que hacen esto con fines de depuración, pero en ese modo el programa corre mucho más lento.
    • Desde el punto de vista del contrato, también está esta lección del texto original: “Es una lección interesante sobre compatibilidad. Si una aplicación tiene un bug y depende accidentalmente de un comportamiento específico, incluso un cambio en la disposición de la pila de una implementación interna puede tener impacto en la compatibilidad”.
      Probablemente por eso los mantenedores del kernel de Linux insisten tanto en no romper nunca el espacio de usuario.
    • No. Hay que recordar https://www.hyrumslaw.com/.
      Si hay suficientes usuarios de una API, no importa qué prometía el contrato: alguien terminará dependiendo de todo comportamiento observable del sistema.
      Si prometes aleatorización, alguien también dependerá de esa aleatorización.
      Entonces tampoco podrás eliminarla nunca.
    • Podría decirse que una de las ventajas de lenguajes como C es que solo pagas el costo de las funciones que eliges usar.
      No te obligan a pagar overhead innecesario, como inicializar variables que no usas.
  • Sobre la parte de “no ignores las advertencias del compilador”, no sé qué error de compilador se podría esperar aquí.
    ¿Tal vez no verificar que el valor de retorno de scanf coincida con la cantidad de argumentos? Aparte de eso, parece un error en el archivo de datos que el compilador no podría conocer.

    • Probando con g++ 11.4, no hay ninguna advertencia predeterminada aunque no se verifique el valor de retorno de sscanf.
      En un ejemplo pequeño, ni siquiera aparece una advertencia con g++ -Wall -Wextra -Wunused-result.
    • Acceder a memoria no inicializada es comportamiento indefinido, así que un sanitizer lo habría detectado.
    • Buen punto. Al leerlo, pensé vagamente que una advertencia de “uso de memoria no inicializada” lo detectaría.
      Pero como se parsea toda la línea con una sola llamada a sscanf, el análisis estático del compilador no tiene más opción que asumir que los valores ya quedaron inicializados.
      No parece haber una forma general de análisis estático que detecte este bug.
      Aunque sí podría crearse una advertencia específica para scanf, que obligue a pasar valores ya inicializados o a verificar el valor de retorno.
  • Siempre es un placer leer este tipo de análisis técnico profundo.
    Me pregunto si en la era de la IA estos textos se volverán más raros o no.

    • No creo que se vuelvan más raros. Siempre habrá ingenieros de primer nivel que investiguen a fondo.
      La IA no los va a reemplazar, y tampoco lo lograron más de 50 años de innovaciones en desarrollo de software.
      Millones, quizá decenas de millones, de desarrolladores de lenguajes de programación de alto nivel solo conocen la diferencia entre stack y heap como una teoría que vieron de forma vaga en la escuela, y como no necesitan preocuparse por eso en el trabajo diario, tampoco les interesa.
    • El ingeniero de software promedio puede estar pasando de artesano a algo más parecido a un técnico, pero este tipo de textos parece surgir justamente de un estilo artesanal.
  • Me da más curiosidad saber qué cambió en esta versión de Windows en la implementación de bloqueo/desbloqueo de secciones críticas.

    • Parece que aumentó el tamaño de pila usado o la zona de protección de la pila.
  • ¿Soy el único al que le molesta este código?
    while (this->m_fBladeAngle > 6.2831855) { this->m_fBladeAngle = this->m_fBladeAngle - 6.2831855; }
    Da la impresión de que usaron un bucle while que incluso podría volverse infinito solo por no querer hacer una división.

    • Quiero creer que los desarrolladores de GTA hicieron este hack porque en entornos como PlayStation 2 era más rápido que una división de punto flotante.
      Pero viendo que podían aumentar en 5 minutos la carga de GTA5 por parsear JSON con sscanf, no tengo demasiadas expectativas.
    • Creo que es muy probable que haya sido por rendimiento. Una resta es más barata que una división de punto flotante.
      El compilador también podría tener formas de optimizar esto aún más.
      En la práctica, no hay forma de que esto se convierta en un bucle infinito. Puede haber underflow, pero para eso el ángulo ya tendría que ser menor que 2*pi, así que saldría del bucle.
    • Es poco probable, pero si el valor es pequeño, este bucle podría ser más rápido que una división.
    • Realmente es así. Parece que el autor no conocía fmod en absoluto.
  • Quienes tengan problemas de acceso pueden usar este enlace:
    https://web.archive.org/web/20250423144746/https://cookieplm...

  • Como sé C/C++, desde el inicio del blog más o menos imaginé qué estaba pasando: un problema de variable no inicializada.
    Es sorprendente que exista un lenguaje que permita dejar variables sin inicializar. Esto ha provocado innumerables bugs, incluidos bugs en producción que vi personalmente, y para detectarlos muchas veces hay que depender de flags adicionales del compilador, herramientas de análisis estático, Valgrind, etc.
    Aunque los lenguajes más modernos adoptan otras soluciones, como valores cero por defecto o exigir inicialización antes de usar, la gente sigue volviendo a C/C++.

  • La parte que dice “Todo este hallazgo demuestra que el bug no es un problema de Windows 11 24H2. Cosas como la forma en que una función interna de WinAPI usa la pila no forman parte del contrato y pueden cambiar en cualquier momento sin aviso previo” me recordó un excelente artículo que leí hace tiempo.
    La idea central era que, para una API suficientemente exitosa, no existe tal cosa como una API privada.

    • Estaría bueno que encontraras ese artículo y compartieras el enlace. Me interesa el razonamiento.
    • Creo que hay una viñeta de XKCD relacionada con esto.