- Sous Windows 11 24H2, un problème a pu être reproduit où l’hydravion Skimmer disparaissait ou projetait le joueur anormalement haut dans le ciel juste après son apparition, et la cause n’était pas l’OS mais un ancien bug interne de traitement des données du jeu
- Dans la ligne
vehicles.ide du Skimmer, il manquait les deux valeurs d’échelle de roue nécessaires à l’avion, mais CFileLoader::LoadVehicleObject ne vérifiait pas la valeur de retour de sscanf et utilisait donc telle quelle une variable locale non initialisée
- Sur les anciennes versions de Windows, la valeur d’échelle de roue
0.7 du véhicule précédent, TopFun, restait par hasard sur la pile, ce qui donnait l’impression que le Skimmer fonctionnait normalement, mais sous Windows 11 24H2, la consommation de pile de LeaveCriticalSection a changé et ce hasard favorable a disparu
- La mauvaise échelle de roue a corrompu le calcul de la suspension ainsi que la coordonnée Z de la boîte de collision, puis s’est propagée jusqu’à la hauteur d’apparition et au calcul de la vitesse des pales, provoquant une anomalie de position de caméra, l’effet de burn-in et l’arrêt de la boucle dans un environnement SilentPatch
- La correction consiste à ajouter
-1, 0.7, 0.7, -1 à la ligne du Skimmer dans vehicles.ide ou à appliquer le prochain hotfix de SilentPatch, et la validation des données d’entrée ainsi que la gestion des avertissements de compilation ont un impact direct sur la compatibilité à long terme
Symptômes du Skimmer révélés sous Windows 11 24H2
- Le tracker d’issues de SilentPatch a reçu un signalement indiquant qu’après la mise à jour vers Windows 11 24H2, l’avion Skimmer avait complètement disparu du jeu
- Il n’apparaissait ni via un trainer, ni à son point d’apparition d’origine
- Le problème se reproduisait aussi bien sur une version modifiée du jeu que sur une copie vanilla avec uniquement SilentPatch appliqué
- Sur GTAForums, le même problème a aussi été signalé à partir de novembre 2024, et si certains utilisateurs soupçonnaient SilentPatch, le même phénomène apparaissait aussi sur une version totalement sans mods
- Sous Windows 10 22H2 et Windows 11 23H2, le Skimmer apparaissait normalement, tandis que les utilisateurs de Windows 11 24H2 rencontraient tous le même bug
- Le débogage à distance sur une machine virtuelle 24H2 a montré que les autres avions et bateaux fonctionnaient normalement, et que seul le Skimmer disparaissait
Altitude anormale et boucle de pales sans fin
- Si l’on crée de force un Skimmer par script et qu’on y fait monter CJ, le joueur est projeté à
1.0287648030984853e+0031 m, soit environ 10,3 nonillions de mètres d’altitude
- Quand SilentPatch est installé, le jeu se fige dans une boucle juste après avoir propulsé le joueur vers le haut
- Sans SilentPatch, le jeu ne se bloque pas, mais on voit apparaître le célèbre effet de burn-in produit quand la caméra part vers une position proche de l’infini
- Le point de blocage se trouvait dans la boucle de normalisation de l’angle des pales du rotor de
CPlane::PreRender
- La valeur de
m_fBladeSpeed montait jusqu’à 3.73340132e+29
- Même en soustrayant
6.2831855 de façon répétée, la valeur ne changeait plus en représentation flottante, donc la boucle ne se terminait jamais
- Comme la vitesse des pales dérive d’une valeur proportionnelle à l’altitude de l’avion, cela indiquait que le Skimmer était créé dès le départ à une hauteur anormalement élevée
Le calcul de suspension qui a corrompu la boîte de collision
- La fonction de création par script
CCarCtrl::CreateCarForScript ajoute au Z transmis le résultat de GetDistanceFromCentreOfMassToBaseOfModel
- En examinant la boîte de collision du Skimmer, on a constaté que
bbox.sup.z était corrompu par une valeur absurde comme -4.30747210e+33
- Un suivi via point d’arrêt sur données a montré qu’au chargement initial, la valeur de la boîte de collision était normale
- La valeur initiale de
bbox.sup.z était -2.21952772
- Ensuite, lors de la première apparition du véhicule,
SetupSuspensionLines mettait à jour la coordonnée Z de la boîte de collision en tenant compte de la hauteur de suspension
- Le problème venait de l’une des valeurs d’entrée utilisées dans le calcul des lignes de suspension
- Le calcul utilisait les limites haute et basse de suspension définies dans
handling.cfg ainsi que l’échelle de roue définie dans vehicles.ide
- Les valeurs du Skimmer dans
handling.cfg n’étaient pas très différentes de celles des autres avions
La ligne vehicles.ide trop courte du Skimmer
- La définition du Skimmer dans
vehicles.ide est plus courte que celle des autres avions, et les quatre derniers paramètres y sont absents
- Parmi les valeurs manquantes, deux correspondent à l’échelle des roues avant et arrière
- Pour les bateaux, l’absence de ces valeurs ne pose pas de problème, mais le Skimmer est le seul avion à omettre ces paramètres
- Il semble que le Skimmer ait été défini comme bateau dans Vice City puis transformé en avion dans San Andreas, sans qu’on ajoute les nouveaux paramètres devenus nécessaires
- En réinsérant les paramètres manquants, le Skimmer fonctionne normalement
Le loader qui ne vérifiait pas la valeur de retour de sscanf
CFileLoader::LoadVehicleObject parse une ligne de vehicles.ide avec sscanf en supposant que tous les paramètres sont toujours présents
- Cette fonction ne vérifie pas la valeur de retour de
sscanf et n’assigne pas non plus de valeur par défaut à la plupart des derniers paramètres
wheelModelID n’est pas initialisé
frontWheelScale et rearWheelScale ne sont pas non plus initialisés
- Seul
wheelUpgradeClass est initialisé à -1
- Dans une ligne incomplète comme celle du Skimmer, les variables d’échelle de roue restent non initialisées, et leur contenu est propagé aux données du véhicule
- La correction de SilentPatch consiste à encapsuler l’appel à
sscanf pour fournir des valeurs par défaut aux quatre dernières valeurs
wheelModelID = -1
frontWheelSize = 0.7f
rearWheelSize = 0.7f
wheelUpgradeClass = -1
- Le commit de correction a été intégré au dépôt SilentPatch
Pourquoi ce bug est resté caché pendant 20 ans
- San Andreas utilise une CRT compilée statiquement, donc un hotfix de la CRT de Windows n’a pas modifié le comportement de
sscanf
- Sous Windows 10, la valeur
0.7 restait en place à l’emplacement de la variable locale juste avant le parsing du Skimmer
- Cette valeur correspond à l’échelle de roue du TopFun, défini juste avant le Skimmer
- La ligne du TopFun contient
-1, 0.7, 0.7, -1
vehicles.ide est lu dans l’ordre, et LoadVehicleObject est appelé pour chaque ligne
- Sous Windows 10, cet emplacement de pile n’était pas écrasé entre deux appels à
LoadVehicleObject, si bien que le Skimmer héritait par hasard de l’échelle de roue du TopFun
- Sous Windows 11 24H2,
LeaveCriticalSection à l’intérieur de fgets utilisait davantage d’espace de pile lors de la lecture de la ligne suivante, ce qui écrasait la valeur résiduelle
Windows 11 24H2 n’a été qu’un déclencheur
- La manière dont les fonctions WinAPI internes utilisent la pile ne relève pas d’un comportement contractuel et peut changer sans préavis
- Windows 11 24H2 n’a fait que supprimer la valeur résiduelle de pile sur laquelle le jeu s’appuyait par hasard ; la véritable cause est le comportement indéfini du jeu
- Même sous Windows 10, la variable locale juste après l’échelle de roue était déjà écrasée par
LeaveCriticalSection, et le jeu pouvait théoriquement rencontrer ce bug depuis des années
- San Andreas prenait en charge Windows 98, ce qui signifie que ce bug n’est simplement pas apparu, par hasard, sur au moins une dizaine de versions de Windows et plusieurs versions de Wine
- Le patch PC officiel 1.01 n’a pas corrigé ce bug, mais la version Xbox d’origine incluait bien une correction qui attribuait la valeur par défaut
1.0
- Steam 3.0, newsteam et RGL héritent de cette correction car ils reposent sur la branche de code Xbox
- Les versions Android de War Drum Studios, X360, PS3 ainsi que la Definitive Edition sont elles aussi concernées
Pourquoi SilentPatch a choisi 0.7 comme valeur par défaut
- SilentPatch utilise
0.7 comme échelle de roue par défaut, et non 1.0 comme dans la correction Xbox de Rockstar
- Ce choix repose sur trois arguments
- Sur la version PC, le Skimmer a dans les faits toujours fonctionné jusqu’ici avec l’échelle de roue
0.7 du TopFun
- Sea Sparrow et Vortex, autres véhicules non bateaux flottant sur l’eau, utilisent eux aussi une échelle de roue de
0.7
- De nombreuses voitures du jeu utilisent également une échelle de roue de
0.7
Comment corriger le problème soi-même
- La correction de code sera incluse dans le prochain hotfix de SilentPatch
- Pour corriger immédiatement le problème, il suffit d’ouvrir
data\vehicles.ide dans le répertoire de San Andreas avec le Bloc-notes et de remplacer la ligne qui commence par 460, skimmer
- La ligne à remplacer est la suivante
460, skimmer, skimmer, plane, SEAPLANE, SKIMMER, null, ignore, 5, 0, 0, -1, 0.7, 0.7, -1
Ce que ce bug enseigne sur la compatibilité des vieux jeux
- Ce problème n’était qu’un simple bug de San Andreas, et la fonction concernée était dès l’origine incapable de se comporter correctement
- Même un changement de disposition de pile dans une implémentation interne peut devenir un problème de compatibilité lorsqu’une application boguée dépend accidentellement d’un comportement précis
- Un cas similaire est celui de Bully: Scholarship Edition, cassé sous Windows 10 après avoir reposé sur de mauvaises hypothèses jusqu’à ce qu’un changement d’OS fasse apparaître le problème
- Le problème fondamental de San Andreas était l’absence de validation des données d’entrée, qui n’a pas permis de rejeter une ligne de configuration incomplète
- Ce code a probablement émis des avertissements de compilation à l’origine, et ignorer ou désactiver ces avertissements peut transformer, à long terme, des bugs cachés en problèmes visibles pour les utilisateurs
1 commentaires
Avis sur Hacker News
C’est le genre d’article qu’on s’attendrait à lire sous la plume de Raymond Chen, et c’est un énorme compliment.
C’est réjouissant de voir qu’il est allé encore plus loin pour déterminer exactement pourquoi.
Personnellement, je pense que tout comportement non couvert par le contrat devrait être aléatorisé.
Par exemple, si un langage ne garantit pas l’ordre d’itération d’une map, il devrait délibérément rendre cet ordre aléatoire.
Sinon, on obtient du code fragile qui « marche très bien jusqu’au jour où il casse ».
-ftrivial-auto-var-init, qui initialisent les variables non initialisées avec une valeur donnée ou aléatoire.Mais aléatoriser ou remplir de zéros tout le contenu de la pile à chaque appel de fonction aurait un impact catastrophique sur les performances, donc ce n’est généralement pas fait.
Il existe des outils qui font cela à des fins de débogage, mais dans ce mode, les programmes s’exécutent beaucoup plus lentement.
C’est probablement aussi pour cela que les mainteneurs du noyau Linux insistent pour ne jamais casser l’espace utilisateur.
Avec suffisamment d’utilisateurs d’une API, peu importe ce qui est promis dans le contrat : quelqu’un finira par dépendre de chaque comportement observable du système.
Si vous promettez de l’aléatoire, quelqu’un dépendra aussi de cet aléatoire.
Et alors vous ne pourrez plus jamais le supprimer.
On n’est pas forcé de payer un surcoût inutile comme l’initialisation de variables qu’on n’utilise pas.
Sur la partie « ne pas ignorer les avertissements du compilateur », je ne vois pas trop quelle erreur de compilation on pourrait attendre ici.
Peut-être le fait de ne pas vérifier que la valeur de retour de
scanfcorrespond au nombre d’arguments ? À part ça, cela ressemble plutôt à une erreur dans un fichier de données que le compilateur ne peut pas connaître.sscanf.Sur un petit exemple, même avec
g++ -Wall -Wextra -Wunused-result, aucun avertissement n’apparaît.Mais comme toute la ligne est analysée par un seul appel à
sscanf, l’analyse statique du compilateur ne peut que supposer que les valeurs sont désormais initialisées.Je ne vois pas de méthode générale d’analyse statique pour détecter ce bug.
En revanche, on pourrait créer un avertissement spécifique à
scanf, imposant de passer des valeurs préalablement initialisées ou de vérifier la valeur de retour.C’est toujours un plaisir de lire ce genre d’analyse technique approfondie.
Je me demande si ce type d’article deviendra plus rare à l’ère de l’IA.
L’IA ne les remplacera pas, pas plus que les innovations des plus de 50 dernières années en développement logiciel ne l’ont fait.
Des centaines de milliers, peut-être des dizaines de millions de développeurs de langages de programmation de haut niveau ne connaissent la différence entre pile et tas que comme une théorie vaguement apprise à l’école, et n’ont ni besoin ni envie de s’en soucier dans leur travail quotidien.
Je suis plus curieux de savoir ce qui a changé dans cette version de Windows au niveau de l’implémentation du verrouillage/déverrouillage des sections critiques.
Je suis le seul que ce code dérange ?
while (this->m_fBladeAngle > 6.2831855) { this->m_fBladeAngle = this->m_fBladeAngle - 6.2831855; }On dirait qu’ils ont utilisé une boucle
whilequi peut potentiellement devenir infinie juste pour éviter une division.Mais quand on voit qu’analyser du JSON avec
sscanfa pu allonger le chargement de GTA5 de cinq minutes, mes attentes ne sont pas très élevées.Le compilateur peut aussi avoir des techniques pour optimiser encore davantage cela.
En pratique, il n’y a quasiment aucun moyen que cela devienne une boucle infinie. Un underflow est possible, mais il faudrait alors que l’angle soit déjà inférieur à
2*pi, donc la boucle se terminerait.fmod.Pour ceux qui ont des problèmes d’accès, utilisez ce lien :
https://web.archive.org/web/20250423144746/https://cookieplm...
Comme je connais C/C++, j’ai deviné dès le début du billet à peu près ce qui se passait, à savoir un problème de variable non initialisée.
C’est étonnant qu’un langage permette de laisser des variables non initialisées. Cela a causé d’innombrables bugs, y compris des bugs de production que j’ai vus directement, et il faut souvent s’appuyer sur des flags de compilation supplémentaires, des outils d’analyse statique, Valgrind, etc. pour les attraper.
Même si les langages plus récents choisissent d’autres solutions, comme une valeur zéro par défaut ou l’obligation d’initialiser avant utilisation, les gens reviennent toujours à C/C++.
Le passage « Toutes ces découvertes prouvent que le bug n’est pas un problème de Windows 11 24H2. Des choses comme la manière dont les fonctions internes de WinAPI utilisent la pile ne font pas partie d’un contrat et peuvent changer à tout moment sans préavis » me rappelle un excellent article que j’avais lu.
L’idée générale était qu’une API qui réussit suffisamment n’a pas vraiment d’API privée.