- Unter Windows 11 24H2 ließ sich ein Problem reproduzieren, bei dem das Skimmer-Wasserflugzeug verschwindet oder den Spieler direkt nach dem Spawn ungewöhnlich hoch in den Himmel katapultiert. Die Ursache war kein OS-Fehler, sondern ein alter Bug in der internen Datenverarbeitung des Spiels.
- In der
vehicles.ide-Zeile des Skimmer fehlten zwei für Flugzeuge nötige Werte für die Radskalierung, aber CFileLoader::LoadVehicleObject prüfte den Rückgabewert von sscanf nicht und verwendete dadurch unveränderte, nicht initialisierte lokale Variablen.
- In früheren Windows-Umgebungen blieb zufällig der Radskalierungswert
0.7 des unmittelbar vorherigen Fahrzeugs TopFun auf dem Stack liegen, sodass der Skimmer normal wirkte. Unter Windows 11 24H2 änderte sich jedoch die Stack-Nutzung von LeaveCriticalSection, wodurch dieser Zufall wegfiel.
- Die falsche Radskalierung verunreinigte die Federungsberechnung und die Z-Koordinate der Collision Box und pflanzte sich bis in die Berechnung von Spawn-Höhe und Rotorblattgeschwindigkeit fort. Das führte zu fehlerhaften Kamerapositionen, dem Burn-in-Effekt und in Umgebungen mit SilentPatch zu einem hängenden Loop.
- Die Lösung besteht darin, der Skimmer-Zeile in
vehicles.ide -1, 0.7, 0.7, -1 hinzuzufügen oder den nächsten SilentPatch-Hotfix anzuwenden. Validierung von Eingabedaten und der Umgang mit Compiler-Warnungen wirken sich direkt auf langfristige Kompatibilität aus.
Unter Windows 11 24H2 sichtbar gewordene Skimmer-Symptome
- Im Issue-Tracker von SilentPatch wurde nach dem Update auf Windows 11 24H2 gemeldet, dass das Skimmer-Flugzeug vollständig aus dem Spiel verschwunden sei
- Es ließ sich auch per Trainer nicht spawnen und war auch an seinen ursprünglichen Spawnpunkten nicht zu finden
- Reproduzierbar war es sowohl in einem Spiel mit Mods als auch in einer Vanilla-Kopie, auf die nur SilentPatch angewendet war
- Auch in den GTAForums wurde dasselbe Problem seit November 2024 gemeldet; einige Nutzer verdächtigten SilentPatch, doch das Phänomen trat auch in einem völlig ungemoddeten Spiel auf
- Unter Windows 10 22H2 und Windows 11 23H2 spawnte der Skimmer normal, während Nutzer von Windows 11 24H2 denselben Bug erlebten
- Remote-Debugging in einer 24H2-VM zeigte, dass andere Flugzeuge und Boote normal waren und nur der Skimmer verschwand
Anormale Höhe und eine endlose Rotorblatt-Schleife
- Wenn der Skimmer per Skript erzwungen erzeugt und CJ hineingesetzt wird, wird der Spieler auf
1.0287648030984853e+0031 m katapultiert, also rund 10,3 Nonillionen Meter Höhe
- Ist SilentPatch installiert, gerät das Spiel direkt nachdem der Spieler nach oben geschleudert wurde in eine Schleife und friert ein
- Ohne SilentPatch friert das Spiel nicht ein, aber der bekannte Burn-in-Effekt tritt auf, der entsteht, wenn die Kamera an eine Position nahe unendlich bewegt wird
- Die Stelle des Hängers war die Schleife zur Normalisierung des Rotorblattwinkels in
CPlane::PreRender
- Der Wert von
m_fBladeSpeed wuchs auf 3.73340132e+29
- Selbst durch wiederholtes Subtrahieren von
6.2831855 änderte sich der Wert aufgrund der Gleitkommadarstellung nicht, sodass die Schleife nie endete
- Da die Rotorblattgeschwindigkeit aus einem Wert abgeleitet wird, der proportional zur Flughöhe ist, war dies ein Hinweis darauf, dass der Skimmer von Anfang an an einer anormal hohen Position erzeugt wurde
Federungsberechnung verunreinigte die Collision Box
- Die Skript-Erzeugungsfunktion
CCarCtrl::CreateCarForScript addiert zum übergebenen Z-Wert das Ergebnis von GetDistanceFromCentreOfMassToBaseOfModel
- Bei der Prüfung der Collision Box des Skimmer war
bbox.sup.z mit einem unsinnigen Wert wie -4.30747210e+33 verunreinigt
- Die Verfolgung per Data Breakpoint ergab, dass der Wert der Collision Box zum Zeitpunkt des initialen Ladens normal war
- Der anfängliche Wert von
bbox.sup.z betrug -2.21952772
- Später, wenn das Fahrzeug erstmals spawnt, aktualisiert
SetupSuspensionLines die Z-Koordinate der Collision Box unter Berücksichtigung der Federungshöhe
- Das Problem lag in einem der Eingabewerte für die Berechnung der Federungslinien
- In die Berechnung fließen die oberen und unteren Federungsgrenzen aus
handling.cfg sowie die Radskalierung aus vehicles.ide ein
- Die Werte des Skimmer in
handling.cfg unterschieden sich nicht stark von denen anderer Flugzeuge
Die kurze vehicles.ide-Zeile des Skimmer
- Die
vehicles.ide-Definition des Skimmer ist kürzer als die anderer Flugzeuge; die letzten vier Parameter fehlen
- Zwei der fehlenden Werte sind die Skalierung der Vorder- und Hinterräder
- Bei Booten ist es kein Problem, wenn diese Werte fehlen, doch unter den Flugzeugen ist der Skimmer das einzige, das diese Parameter auslässt
- Der Skimmer war in Vice City als Boot definiert und wurde in San Andreas zu einem Flugzeug geändert; dabei wurden offenbar die neu nötigen Parameter nicht ergänzt
- Wenn die fehlenden Parameter wieder eingefügt werden, funktioniert der Skimmer normal
Loader prüfte den Rückgabewert von sscanf nicht
CFileLoader::LoadVehicleObject parst eine Zeile aus vehicles.ide mit sscanf und geht dabei davon aus, dass alle Parameter immer vorhanden sind
- Die Funktion prüft den Rückgabewert von
sscanf nicht und setzt für die meisten der letzten Parameter auch keine Standardwerte
wheelModelID wird nicht initialisiert
- Auch
frontWheelScale und rearWheelScale werden nicht initialisiert
- Nur
wheelUpgradeClass wird mit -1 initialisiert
- Bei Zeilen mit fehlenden Werten wie beim Skimmer bleiben die Variablen für die Radskalierung nicht initialisiert, und diese Werte werden in die Fahrzeugdaten übernommen
- Der SilentPatch-Fix besteht darin, den
sscanf-Aufruf zu kapseln und Standardwerte für die letzten vier Werte bereitzustellen
wheelModelID = -1
frontWheelSize = 0.7f
rearWheelSize = 0.7f
wheelUpgradeClass = -1
- Der Fix-Commit wurde im SilentPatch-Repository übernommen
Warum der Bug 20 Jahre lang verborgen blieb
- San Andreas verwendet eine statisch kompilierte CRT; ein Hotfix auf CRT-Ebene in Windows hat also nicht das Verhalten von
sscanf verändert
- Unter Windows 10 blieb unmittelbar vor dem Parsen des Skimmer an der Position der lokalen Variablen der Wert
0.7 liegen
- Dieser Wert entspricht der Radskalierung von TopFun, das direkt vor dem Skimmer definiert ist
- Die TopFun-Zeile enthält
-1, 0.7, 0.7, -1
vehicles.ide wird der Reihe nach gelesen, und für jede Zeile wird LoadVehicleObject aufgerufen
- Unter Windows 10 wurde diese Stack-Position zwischen den Aufrufen von
LoadVehicleObject nicht überschrieben, sodass der Skimmer zufällig die Radskalierung von TopFun erbte
- Unter Windows 11 24H2 verwendete
LeaveCriticalSection innerhalb von fgets beim Lesen der nächsten Zeile mehr Stack-Speicher; dadurch wurde der zurückgebliebene Wert überschrieben
Windows 11 24H2 war nur der Auslöser
- Die Art, wie interne WinAPI-Funktionen den Stack nutzen, ist kein vertraglich garantiertes Verhalten und kann sich ohne Vorankündigung ändern
- Windows 11 24H2 beseitigte lediglich den zufälligen Restwert auf dem Stack, auf den sich das Spiel verließ; die eigentliche Ursache ist undefiniertes Verhalten im Spiel
- Auch unter Windows 10 wurde die lokale Variable direkt nach der Radskalierung bereits von
LeaveCriticalSection überschrieben, und das Spiel hätte schon vor Jahren auf diesen Bug treffen können
- Da San Andreas auch Windows 98 unterstützte, blieb dieser Bug offenbar über mindestens ein gutes Dutzend Windows-Versionen und mehrere Wine-Releases hinweg zufällig verborgen
- Im offiziellen PC-Patch 1.01 wurde dieser Bug nicht behoben, aber die ursprüngliche Xbox-Version enthielt einen Fix, der den Standardwert
1.0 setzt
- Steam 3.0, newsteam und RGL basieren auf dem Xbox-Codezweig und haben diesen Fix daher geerbt
- Auch die Android-, X360- und PS3-Releases von War Drum Studios sowie die Definitive Edition sind betroffen
Warum SilentPatch 0.7 als Standardwert wählte
- SilentPatch verwendet als Standard-Radskalierung nicht wie Rockstars Xbox-Fix
1.0, sondern 0.7
- Die Entscheidung beruht auf drei Gründen
- In der PC-Version hat der Skimmer bis heute faktisch mit der Radskalierung
0.7 von TopFun funktioniert
- Auch Sea Sparrow und Vortex, andere nicht als Boote klassifizierte Fahrzeuge, die auf dem Wasser treiben, haben eine Radskalierung von
0.7
- Viele Autos im Spiel haben ebenfalls eine Radskalierung von
0.7
So behebt man es selbst
- Die Code-Änderung soll im nächsten SilentPatch-Hotfix enthalten sein
- Wer es sofort reparieren möchte, öffnet im San-Andreas-Verzeichnis
data\vehicles.ide mit dem Editor und ersetzt die Zeile, die mit 460, skimmer beginnt
- Die zu ersetzende Zeile lautet:
460, skimmer, skimmer, plane, SEAPLANE, SKIMMER, null, ignore, 5, 0, 0, -1, 0.7, 0.7, -1
Lehren aus der Kompatibilität alter Spiele
- Dieses Problem war ein einfacher Bug in San Andreas, und die betreffende Funktion war von Anfang an Code, der nicht korrekt funktionieren konnte
- Auch Änderungen am Stack-Layout interner Implementierungen können zu einem Kompatibilitätsproblem werden, wenn eine fehlerhafte Anwendung sich zufällig auf ein bestimmtes Verhalten verlässt
- Ein ähnlicher Fall war Bully: Scholarship Edition, das unter Windows 10 kaputtging, weil es ebenfalls auf falschen Annahmen beruhte und der Fehler durch eine OS-Änderung sichtbar wurde
- Das Grundproblem in San Andreas war das Fehlen von Validierung der Eingabedaten, wodurch eine unvollständige Konfigurationszeile nicht abgefangen wurde
- Dieser Code hat ursprünglich wahrscheinlich Compiler-Warnungen erzeugt; wenn Warnungen ignoriert oder deaktiviert werden, können lange verborgene Bugs zu echten Problemen für Nutzer werden
1 Kommentare
Meinungen auf Hacker News
Das ist ein Niveau, das man sonst von Raymond Chen erwarten würde, und das ist ein riesiges Kompliment.
Schön, dass der Autor noch tiefer nachgegraben und genau herausgefunden hat, warum das passiert.
Persönlich finde ich: Wenn ein Verhalten nicht Teil des Vertrags ist, sollte es randomisiert werden.
Wenn eine Sprache zum Beispiel keine Reihenfolge beim Iterieren über Maps garantiert, sollte sie die Reihenfolge absichtlich randomisieren.
Sonst entsteht fragiler Code, der „funktioniert, bis er eines Tages kaputtgeht“.
-ftrivial-auto-var-init, die nicht initialisierte Variablen mit einem festen oder zufälligen Wert initialisieren.Aber wenn man bei jedem Funktionsaufruf den gesamten Stack-Inhalt randomisieren oder mit Nullen füllen würde, wäre der Performance-Einbruch furchtbar, deshalb macht man das normalerweise nicht.
Es gibt Tools, die so etwas zu Debugging-Zwecken tun, aber in diesem Modus läuft das Programm deutlich langsamer.
Wahrscheinlich ist das auch der Grund, warum die Maintainer des Linux-Kernels so sehr darauf bestehen, den Userspace niemals zu brechen.
Wenn eine API genügend Nutzer hat, ist es egal, was im Vertrag versprochen wurde: Irgendjemand wird sich auf jedes beobachtbare Verhalten des Systems verlassen.
Wenn man Randomisierung verspricht, wird sich jemand auch auf diese Randomisierung verlassen.
Dann kann man auch sie nie wieder entfernen.
Man wird nicht gezwungen, für unnötigen Overhead wie die Initialisierung ungenutzter Variablen zu zahlen.
Bei dem Teil „Compiler-Warnungen nicht ignorieren“ weiß ich nicht, welchen Compiler-Fehler man hier erwarten könnte.
Vielleicht, dass nicht geprüft wurde, ob der Rückgabewert von
scanfzur Anzahl der Argumente passt? Ansonsten wirkt es eher wie ein Fehler in der Datendatei, den der Compiler nicht kennen kann.sscanfnicht prüft.In einem kleinen Beispiel kommt selbst mit
g++ -Wall -Wextra -Wunused-resultkeine Warnung.Da aber die gesamte Zeile mit einem einzigen
sscanf-Aufruf geparst wird, muss die statische Analyse des Compilers annehmen, dass die Werte nun initialisiert sind.Es scheint keine allgemeine statische Analyse zu geben, die diesen Bug erwischen würde.
Man könnte allerdings eine
scanf-spezifische Warnung bauen, die erzwingt, dass vorinitialisierte Werte übergeben oder der Rückgabewert geprüft wird.Solche tiefgehenden technischen Analysen zu lesen, macht immer Freude.
Ich frage mich, ob solche Texte im KI-Zeitalter seltener werden oder nicht.
KI wird sie nicht ersetzen, und die Softwareentwicklungs-Innovationen der letzten über 50 Jahre haben das auch nicht geschafft.
Millionen, vielleicht zig Millionen Entwickler in höheren Programmiersprachen kennen den Unterschied zwischen Stack und Heap nur als vage Theorie aus der Schule, und im Arbeitsalltag müssen sie sich nicht darum kümmern, also interessiert es sie auch nicht.
Mich würde eher interessieren, was sich in dieser Windows-Version an der Implementierung des Sperrens/Entsperrens kritischer Abschnitte geändert hat.
Bin ich der Einzige, den dieser Code stört?
while (this->m_fBladeAngle > 6.2831855) { this->m_fBladeAngle = this->m_fBladeAngle - 6.2831855; }Das wirkt, als hätte jemand keine Lust auf Division gehabt und stattdessen eine
while-Schleife geschrieben, die eventuell unendlich laufen kann.Aber wenn man bedenkt, dass das Parsen von JSON mit
sscanfdie Ladezeit von GTA5 um fünf Minuten verlängern konnte, sind meine Erwartungen nicht allzu hoch.Der Compiler könnte außerdem Wege haben, das weiter zu optimieren.
Es gibt praktisch keine Möglichkeit, dass daraus eine Endlosschleife wird. Underflow ist möglich, aber dafür müsste der Winkel bereits kleiner als
2*pisein, wodurch die Schleife verlassen würde.fmodoffenbar überhaupt nicht.Wer Probleme mit dem Zugriff hat, kann diesen Link verwenden:
https://web.archive.org/web/20250423144746/https://cookieplm...
Weil ich C/C++ kenne, hatte ich schon früh im Blog ungefähr eine Ahnung, was passiert, nämlich ein Problem mit einer nicht initialisierten Variablen.
Es ist erstaunlich, dass eine Sprache erlaubt, Variablen uninitialisiert zu lassen. Dadurch sind unzählige Bugs entstanden, darunter Produktionsbugs, die ich selbst gesehen habe, und oft muss man sich zum Aufspüren auf zusätzliche Compiler-Flags, statische Analysewerkzeuge, Valgrind usw. verlassen.
Neuere Sprachen wählen andere Lösungen, etwa Standard-Nullwerte oder erzwungene Initialisierung vor der Verwendung, und trotzdem kehren die Leute immer wieder zu C/C++ zurück.
Der Abschnitt „All diese Erkenntnisse beweisen, dass der Bug kein Problem von Windows 11 24H2 ist. Dinge wie die Art, wie eine interne WinAPI-Funktion den Stack verwendet, sind kein Vertrag und können sich jederzeit ohne Vorankündigung ändern“ erinnert mich an einen hervorragenden Artikel, den ich früher einmal gelesen habe.
Die Kernaussage war, dass es bei einer hinreichend erfolgreichen API so etwas wie eine private API nicht gibt.