- 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
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”.
-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.
Hay herramientas que hacen esto con fines de depuración, pero en ese modo el programa corre mucho más lento.
Probablemente por eso los mantenedores del kernel de Linux insisten tanto en no romper nunca el espacio de usuario.
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.
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
scanfcoincida con la cantidad de argumentos? Aparte de eso, parece un error en el archivo de datos que el compilador no podría conocer.sscanf.En un ejemplo pequeño, ni siquiera aparece una advertencia con
g++ -Wall -Wextra -Wunused-result.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.
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.
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.
¿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
whileque incluso podría volverse infinito solo por no querer hacer una división.Pero viendo que podían aumentar en 5 minutos la carga de GTA5 por parsear JSON con
sscanf, no tengo demasiadas expectativas.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.fmoden 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.