Engine & platform

Debuggen von Speicherfehlern: Wer hat '2' in meinen Stapel geschrieben?!

TAUTVYDAS ŽILYS / UNITY TECHNOLOGIESContributor
Apr 25, 2016|16 Min.
Debuggen von Speicherfehlern: Wer hat '2' in meinen Stapel geschrieben?!
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.
Hallo, mein Name ist Tautvydas und ich bin Softwareentwickler bei Unity und arbeite im Windows-Team. Ich möchte Ihnen eine Geschichte über die Fehlersuche in einem schwer fassbaren Speicherfehler erzählen.

Vor einigen Wochen erhielten wir einen Fehlerbericht von einem Kunden, der uns mitteilte, dass sein Spiel bei Verwendung des IL2CPP-Skript-Backends abstürzt. Die QA hat den Fehler überprüft und ihn mir zur Behebung zugewiesen. Das Projekt war ziemlich groß (wenn auch bei weitem nicht das größte); es dauerte 40 Minuten, um es auf meinem Rechner zu erstellen. Die Anweisungen im Fehlerbericht lauteten: "Spielen Sie das Spiel für 5-10 Minuten, bis es abstürzt". Nachdem ich die Anweisungen befolgt hatte, konnte ich einen Absturz beobachten. Ich habe WinDbg gestartet und war bereit, die Sache festzunageln. Leider war der Stack-Trace gefälscht:

0:049> k
# Child-SP           RetAddr           Call Site
00 00000022`e25feb10 00000000`00000010 0x00007ffa`00000102

0:050> u 0x00007ffa`00000102 L10
00007ffa`00000102 ??         ???
                          ^ Memory access error in 'u 0x00007ffa`00000102 l10'

Offensichtlich hat er versucht, eine ungültige Speicheradresse auszuführen. Obwohl der Stacktrace beschädigt war, hatte ich gehofft, dass nur ein Teil des gesamten Stacks beschädigt wurde und dass ich ihn rekonstruieren kann, wenn ich mir den Speicherinhalt nach dem Stackpointer-Register ansehe. Das brachte mich auf eine Idee, wo ich als nächstes suchen sollte:

0:049> dps @rsp L200
...............
00000022`e25febd8  00007ffa`b1fdc65c ucrtbased!heap_alloc_dbg+0x1c [d:\th\minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp @ 447]
00000022`e25febe0  00000000`00000004
00000022`e25febe8  00000022`00000001
00000022`e25febf0  00000022`00000000
00000022`e25febf8  00000000`00000000
00000022`e25fec00  00000022`e25fec30
00000022`e25fec08  00007ffa`99b3d3ab UnityPlayer!std::_Vector_alloc<std::_Vec_base_types<il2cpp::os::PollRequest,std::allocator<il2cpp::os::PollRequest> > >::_Get_data+0x2b [ c:\program files (x86)\microsoft visual studio 14.0\vc\include\vector @ 642]
00000022`e25fec10  00000022`e25ff458
00000022`e25fec18  cccccccc`cccccccc
00000022`e25fec20  cccccccc`cccccccc
00000022`e25fec28  00007ffa`b1fdf54c ucrtbased!_calloc_dbg+0x6c [d:\th\minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp @ 511]
00000022`e25fec30  00000000`00000010
00000022`e25fec38  00007ffa`00000001
...............
00000022`e25fec58  00000000`00000010
00000022`e25fec60  00000022`e25feca0
00000022`e25fec68  00007ffa`b1fdb69e ucrtbased!calloc+0x2e [d:\th\minkernel\crts\ucrt\src\appcrt\heap\calloc.cpp @ 25]
00000022`e25fec70  00000000`00000001
00000022`e25fec78  00000000`00000010
00000022`e25fec80  cccccccc`00000001
00000022`e25fec88  00000000`00000000
00000022`e25fec90  00000022`00000000
00000022`e25fec98  cccccccc`cccccccc
00000022`e25feca0  00000022`e25ff3f0
00000022`e25feca8  00007ffa`99b3b646 UnityPlayer!il2cpp::os::SocketImpl::Poll+0x66 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\win32\socketimpl.cpp @ 1429]
00000022`e25fecb0  00000000`00000001
00000022`e25fecb8  00000000`00000010
...............
00000022`e25ff3f0  00000022`e25ff420
00000022`e25ff3f8  00007ffa`99c1caf4 UnityPlayer!il2cpp::os::Socket::Poll+0x44 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\socket.cpp @ 324]
00000022`e25ff400  00000022`e25ff458
00000022`e25ff408  cccccccc`ffffffff
00000022`e25ff410  00000022`e25ff5b4
00000022`e25ff418  00000022`e25ff594
00000022`e25ff420  00000022`e25ff7e0
00000022`e25ff428  00007ffa`99b585f8 UnityPlayer!il2cpp::vm::SocketPollingThread::RunLoop+0x268 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 452]
00000022`e25ff430  00000022`e25ff458
00000022`e25ff438  00000000`ffffffff
...............
00000022`e25ff7d8  00000022`e25ff6b8
00000022`e25ff7e0  00000022`e25ff870
00000022`e25ff7e8  00007ffa`99b58d2c UnityPlayer!il2cpp::vm::SocketPollingThreadEntryPoint+0xec [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 524]
00000022`e25ff7f0  00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff7f8  00007ffa`99b57700 UnityPlayer!il2cpp::vm::FreeThreadHandle [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 488]
00000022`e25ff800  00000000`0000106c
00000022`e25ff808  cccccccc`cccccccc
00000022`e25ff810  00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff818  000001c4`1705f5c0
00000022`e25ff820  cccccccc`0000106c
...............
00000022`e25ff860  00005eaa`e9a6af86
00000022`e25ff868  cccccccc`cccccccc
00000022`e25ff870  00000022`e25ff8d0
00000022`e25ff878  00007ffa`99c63b52 UnityPlayer!il2cpp::os::Thread::RunWrapper+0xd2 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\thread.cpp @ 106]
00000022`e25ff880  00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff888  00000000`00000018
00000022`e25ff890  cccccccc`cccccccc
...............
00000022`e25ff8a8  000001c4`15508c90
00000022`e25ff8b0  cccccccc`00000002
00000022`e25ff8b8  00007ffa`99b58c40 UnityPlayer!il2cpp::vm::SocketPollingThreadEntryPoint [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 494]
00000022`e25ff8c0  00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff8c8  000001c4`155a5890
00000022`e25ff8d0  00000022`e25ff920
00000022`e25ff8d8  00007ffa`99c19a14 UnityPlayer!il2cpp::os::ThreadStartWrapper+0x54 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\win32\threadimpl.cpp @ 31]
00000022`e25ff8e0  000001c4`155a5890
...............
00000022`e25ff900  cccccccc`cccccccc
00000022`e25ff908  00007ffa`99c63a80 UnityPlayer!il2cpp::os::Thread::RunWrapper [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\thread.cpp @ 80]
00000022`e25ff910  000001c4`155a5890
...............
00000022`e25ff940  000001c4`1e0801b0
00000022`e25ff948  00007ffa`e6858102 KERNEL32!BaseThreadInitThunk+0x22
00000022`e25ff950  000001c4`1e0801b0
00000022`e25ff958  00000000`00000000
00000022`e25ff960  00000000`00000000
00000022`e25ff968  00000000`00000000
00000022`e25ff970  00007ffa`99c199c0 UnityPlayer!il2cpp::os::ThreadStartWrapper [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\win32\threadimpl.cpp @ 26]
00000022`e25ff978  00007ffa`e926c5b4 ntdll!RtlUserThreadStart+0x34
00000022`e25ff980  00007ffa`e68580e0 KERNEL32!BaseThreadInitThunk

Hier ist ein grober rekonstruierter Stacktrace:

00000022`e25febd8  00007ffa`b1fdc65c ucrtbased!heap_alloc_dbg+0x1c [...\appcrt\heap\debug_heap.cpp @ 447]
00000022`e25fec28  00007ffa`b1fdf54c ucrtbased!_calloc_dbg+0x6c [...\appcrt\heap\debug_heap.cpp @ 511]
00000022`e25fec68 00007ffa`b1fdb69e  ucrtbased!calloc+0x2e [...\appcrt\heap\calloc.cpp @ 25]
00000022`e25feca8  00007ffa`99b3b646 UnityPlayer!il2cpp::os::SocketImpl::Poll+0x66 [...\libil2cpp\os\win32\socketimpl.cpp @ 1429]
00000022`e25ff3f8  00007ffa`99c1caf4 UnityPlayer!il2cpp::os::Socket::Poll+0x44 [...\libil2cpp\os\socket.cpp @ 324]
00000022`e25ff428  00007ffa`99b585f8 UnityPlayer!il2cpp::vm::SocketPollingThread::RunLoop+0x268 [...\libil2cpp\vm\threadpool.cpp @ 452]
00000022`e25ff7e8  00007ffa`99b58d2c UnityPlayer!il2cpp::vm::SocketPollingThreadEntryPoint+0xec [...\libil2cpp\vm\threadpool.cpp @ 524]
00000022`e25ff878  00007ffa`99c63b52 UnityPlayer!il2cpp::os::Thread::RunWrapper+0xd2 [...\libil2cpp\os\thread.cpp @ 106]
00000022`e25ff8d8  00007ffa`99c19a14 UnityPlayer!il2cpp::os::ThreadStartWrapper+0x54 [...\libil2cpp\os\win32\threadimpl.cpp @ 31]
00000022`e25ff948  00007ffa`e6858102 KERNEL32!BaseThreadInitThunk+0x22
00000022`e25ff978  00007ffa`e926c5b4 ntdll!RtlUserThreadStart+0x34

Gut, jetzt wusste ich, welcher Thread abstürzte: Es war der Socket-Polling-Thread der IL2CPP-Laufzeit. Seine Aufgabe ist es, anderen Threads mitzuteilen, wann ihre Sockets bereit sind, Daten zu senden oder zu empfangen. Das funktioniert so: Es gibt eine FIFO-Warteschlange, in die Socket Polling-Anfragen von anderen Threads gestellt werden. Der Socket Polling-Thread nimmt diese Anfragen dann eine nach der anderen aus der Warteschlange, ruft die Funktion select() auf und wenn select() ein Ergebnis liefert, stellt er einen Callback, der in der ursprünglichen Anfrage enthalten war, in die Warteschlange des Thread Pools.

Jemand hat also den Stapel schwer beschädigt. Um die Suche einzugrenzen, habe ich beschlossen, die meisten Stack Frames in diesem Thread mit "Stack Sentinels" zu kennzeichnen. Hier sehen Sie, wie mein Stack Sentinel definiert wurde:

Stapelwächter

Wenn er konstruiert ist, würde er den Puffer mit "0xDD" füllen. Wenn es zerstört wird, prüft es, ob sich diese Werte nicht geändert haben. Das hat unglaublich gut funktioniert: Das Spiel ist nicht mehr abgestürzt! Sie hat stattdessen behauptet:

Jemand schrieb 2

Jemand hatte das Gemächt meines Wächters angefasst - und es war definitiv kein Freund. Ich habe dies noch ein paar Mal ausgeführt und das Ergebnis war dasselbe: jedes Mal wurde zuerst der Wert "2" in den Puffer geschrieben. Als ich mir die Speicheransicht ansah, bemerkte ich, dass mir das, was ich sah, bekannt vorkam:

Ansicht Speicher

Dies sind genau die gleichen Werte, die wir in der allerersten beschädigten Stapelverfolgung gesehen haben. Mir wurde klar, dass die Ursache des Absturzes auch für die Beschädigung des Stack-Sentinels verantwortlich war. Zuerst dachte ich, dass es sich um eine Art Pufferüberlauf handelt und jemand außerhalb der Grenzen seiner lokalen Variablen schreibt. Also begann ich, diese Stack-Sentinels viel aggressiver zu platzieren: vor fast jedem Funktionsaufruf, den der Thread machte. Die Fehler schienen jedoch zu zufälligen Zeiten aufzutreten, und ich konnte mit dieser Methode nicht herausfinden, was die Ursache dafür war.

Ich wusste, dass der Speicher immer beschädigt wird, wenn einer meiner Sentinels in Reichweite ist. Ich musste das Ding, das es verdirbt, irgendwie auf frischer Tat ertappen. Ich habe mir überlegt, den Stack-Sentinel-Speicher für die Dauer der Lebensdauer des Stack-Sentinels schreibgeschützt zu machen: Ich würde VirtualProtect() im Konstruktor aufrufen, um die Seiten als schreibgeschützt zu kennzeichnen, und es im Destruktor erneut aufrufen, um sie schreibbar zu machen:

Geschützter Wächter

Zu meiner Überraschung war es immer noch korrumpiert! Und die Meldung im Debug-Protokoll lautete:

Der Speicher wurde bei 0xd046ffeea8 beschädigt. Sie war schreibgeschützt, als sie beschädigt wurde.
CrashingGame.exe hat einen Haltepunkt ausgelöst.

Das war für mich ein Warnsignal. Jemand hatte den Speicher beschädigt, entweder während der Speicher schreibgeschützt war oder kurz bevor ich ihn auf schreibgeschützt setzte. Da ich keine Zugriffsverletzungen erhielt, nahm ich an, dass es sich um letzteres handelte. Also änderte ich den Code, um zu prüfen, ob sich der Speicherinhalt direkt nach dem Setzen meiner magischen Werte geändert hatte:

Kontrolle direkt nach der Einstellung

Meine Theorie hat sich bestätigt:

Der Speicher wurde bei 0x79b3bfea78 beschädigt.
CrashingGame.exe hat einen Haltepunkt ausgelöst.

An diesem Punkt dachte ich: "Nun, es muss ein anderer Faden sein, der meinen Stapel verdirbt. Es MUSS sein. Richtig? RICHTIG?". Die einzige Möglichkeit, wie ich bei der Untersuchung dieses Problems vorgehen konnte, war die Verwendung von Daten- (Speicher-) Haltepunkten, um den Übeltäter zu erwischen. Leider können Sie auf x86 nur vier Speicherplätze gleichzeitig überwachen, d.h. ich kann höchstens 32 Byte überwachen, während der Bereich, der beschädigt wurde, 16 KB groß war. Ich musste irgendwie herausfinden, wo ich die Haltepunkte setzen sollte. Ich begann, Korruptionsmuster zu beobachten. Zunächst sah es so aus, als ob sie zufällig wären, aber das war nur eine Illusion aufgrund der Natur von ASLR: Jedes Mal, wenn ich das Spiel neu startete, wurde der Stack an einer zufälligen Speicheradresse platziert, so dass der Ort der Beschädigung natürlich unterschiedlich war. Als mir das klar wurde, habe ich aufgehört, das Spiel jedes Mal neu zu starten, wenn der Speicher beschädigt wurde, und es einfach weiter ausgeführt. Dabei stellte ich fest, dass die Adresse des beschädigten Speichers bei einer bestimmten Debugging-Sitzung immer konstant war. Mit anderen Worten, wenn es einmal beschädigt wurde, wird es immer an genau der gleichen Speicheradresse beschädigt, solange ich das Programm nicht beende:

Der Speicher wurde bei 0x90445febd8 beschädigt.
CrashingGame.exe hat einen Haltepunkt ausgelöst.
Der Speicher wurde bei 0x90445febd8 beschädigt.
CrashingGame.exe hat einen Haltepunkt ausgelöst.

Ich setzte einen Daten-Breakpoint an dieser Speicheradresse und beobachtete, wie er immer wieder abbrach, wenn ich ihn auf den magischen Wert 0xDD setzte. Ich dachte mir schon, dass dies eine Weile dauern würde, aber Visual Studio erlaubt es mir tatsächlich, eine Bedingung für diesen Haltepunkt festzulegen: nur dann zu unterbrechen, wenn der Wert dieser Speicheradresse 2 ist:

Bedingter Daten-Haltepunkt

Eine Minute später war dieser Haltepunkt endlich erreicht. Ich bin an diesem Punkt angelangt, nachdem ich 3 Tage mit der Fehlersuche verbracht habe. Das sollte mein Triumph werden. "Endlich habe ich Sie festgenagelt!", verkündete ich. Zumindest dachte ich das so optimistisch:

Bei der Zuweisung beschädigt

Ich schaute ungläubig auf den Debugger, während sich mein Kopf mit mehr Fragen als Antworten füllte: "Was? Wie ist das überhaupt möglich? Bin ich verrückt geworden?". Ich beschloss, mir die Demontage anzusehen:

Beschädigt bei der Disassemblierung der Zuweisung

Und tatsächlich, er hat diesen Speicherplatz verändert. Aber es wurde 0xDD geschrieben, nicht 0x02! Nachdem ich mir das Speicherfenster angesehen hatte, war die gesamte Region bereits beschädigt:

RAX-Speicher

Als ich kurz davor war, mit dem Kopf gegen die Wand zu schlagen, rief ich meinen Kollegen an und bat ihn zu prüfen, ob ich etwas Offensichtliches übersehen hatte. Wir haben uns den Debugging-Code gemeinsam angesehen und konnten nichts finden, was auch nur im Entferntesten eine solche Seltsamkeit verursachen könnte. Ich bin dann einen Schritt zurück getreten und habe versucht, mir vorzustellen, woran es liegen könnte, dass der Debugger nicht mehr funktioniert, weil er denkt, dass der Code den Wert "2" setzt. Ich habe mir die folgende hypothetische Kette von Ereignissen ausgedacht:

1. mov byte ptr [rax], 0DDh ändert die Speicherstelle, die CPU unterbricht die Ausführung, damit der Debugger den Programmzustand überprüfen kann
2. Das Gedächtnis wird durch etwas beschädigt
3. Der Debugger untersucht die Speicheradresse, findet darin eine "2" und denkt, dass sich das geändert hat.

Also... was kann den Speicherinhalt verändern, während das Programm von einem Debugger eingefroren wird? Soweit ich weiß, sind 2 Szenarien möglich: Entweder ist es ein anderer Prozess, der das tut, oder der Betriebssystemkern. Um eines dieser Probleme zu untersuchen, ist ein herkömmlicher Debugger nicht geeignet. Betreten Sie das Land der Kernel-Fehlersuche.

Überraschenderweise ist die Einrichtung des Kernel-Debugging unter Windows extrem einfach. Sie benötigen 2 Rechner: den, auf dem der Debugger läuft, und den, auf dem Sie debuggen werden. Öffnen Sie eine erweiterte Eingabeaufforderung auf dem Rechner, auf dem Sie das Debugging durchführen werden, und geben Sie Folgendes ein:

Kernel-Debugger einschalten

Host IP ist die IP-Adresse des Rechners, auf dem der Debugger läuft. Er verwendet den angegebenen Port für die Verbindung zum Debugger. Der Wert kann zwischen 49152 und 65535 liegen. Nachdem Sie beim zweiten Befehl die Eingabetaste gedrückt haben, wird Ihnen ein geheimer Schlüssel mitgeteilt (in der Abbildung abgeschnitten), der als Passwort dient, wenn Sie den Debugger anschließen. Nachdem Sie diese Schritte durchgeführt haben, starten Sie neu.

Öffnen Sie auf dem anderen Computer WinDbg, klicken Sie auf Datei -> Kernel-Debug und geben Sie Port und Schlüssel ein.

Kernel-Debugger anhängen

Wenn alles gut geht, können Sie die Ausführung unterbrechen, indem Sie auf Debug -> Break drücken. Wenn das funktioniert, friert der "debugee" Computer ein. Geben Sie "g" ein, um die Ausführung fortzusetzen.

Ich habe das Spiel gestartet und gewartet, bis es einmal abbricht, damit ich die Adresse herausfinden kann, an der der Speicher beschädigt wird:

Der Speicher wurde bei 0x49d05fedd8 beschädigt.
CrashingGame.exe hat einen Haltepunkt ausgelöst.

Nun gut, da ich nun die Adresse kannte, an der ich einen Datenhaltepunkt setzen musste, musste ich meinen Kernel-Debugger so konfigurieren, dass er ihn auch tatsächlich setzt:

kd> !process 0 0
PROCESS ffffe00167228080
    SessionId: 1  Cid: 26b8    Peb: 49cceca000  ParentCid: 03d8
    DirBase: 1ae5e3000  ObjectTable: ffffc00186220d80  HandleCount: 
    Image: CrashingGame.exe

kd> .process /i ffffe00167228080
You need to continue execution (press 'g' ) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff801`7534beb0 cc              int     3
kd> .process
Implicit process is now ffffe001`66e9e080
kd> .reload /f
kd> ba w 1 0x00000049D05FEDD8 ".if (@@c++(*(char*)0x00000049D05FEDD8 == 2)) { k } .else { gc }"

Nach einiger Zeit wurde der Haltepunkt tatsächlich erreicht...

 # Child-SP          RetAddr           Call Site
00 ffffd000`23c1e980 fffff801`7527dc64 nt!IopCompleteRequest+0xef
01 ffffd000`23c1ea70 fffff801`75349953 nt!KiDeliverApc+0x134
02 ffffd000`23c1eb00 00007ffd`7e08b4bd nt!KiApcInterrupt+0xc3
03 00000049`d05fad50 cccccccc`cccccccc UnityPlayer!StackSentinel::StackSentinel+0x4d [...\libil2cpp\utils\memory.cpp @ 21]

Also gut, was ist hier los?! Der Sentinel setzt fröhlich seine magischen Werte, dann gibt es einen Hardware-Interrupt, der dann eine Abschlussroutine aufruft, die eine "2" in meinen Stack schreibt. Wow. Okay, aus irgendeinem Grund korrumpiert der Windows-Kernel meinen Speicher. Aber warum?

Zuerst dachte ich, dass dies daran liegt, dass wir eine Windows API aufrufen und ihr ungültige Argumente übergeben. Ich bin also den gesamten Code des Socket-Polling-Threads noch einmal durchgegangen und habe festgestellt, dass der einzige Systemaufruf, den wir dort aufgerufen haben, die Funktion select() war. Ich ging zu MSDN und verbrachte eine Stunde damit, die Dokumentation zu select() erneut zu lesen und zu überprüfen, ob wir alles richtig gemacht hatten. Soweit ich das beurteilen kann, gibt es nicht wirklich viel, was man damit falsch machen könnte, und es gab definitiv keine Stelle in der Dokumentation, in der es hieß : "Wenn Sie diesen Parameter übergeben, schreiben wir 2 in Ihren Stack". Es schien, als ob wir alles richtig machen würden.

Nachdem mir nichts mehr einfiel, beschloss ich, die select() -Funktion mit einem Debugger zu testen, ihre Disassemblierung zu durchlaufen und herauszufinden, wie sie funktioniert. Ich habe ein paar Stunden gebraucht, aber ich habe es geschafft. Es scheint, dass die Funktion select() ein Wrapper für die Funktion WSPSelect() ist, die ungefähr so aussieht:

auto completionEvent = TlsGetValue(MSAFD_SockTlsSlot);

/* setting up some state
*/

IO_STATUS_BLOCK statusBlock;
auto result = NtDeviceIoControlFile(networkDeviceHandle, completionEvent, nullptr, nullptr, &statusBlock, 0x12024,
    buffer, bufferLength, buffer, bufferLength);

if (result == STATUS_PENDING)
    WaitForSingleObjectEx(completionEvent, INFINITE, TRUE);

/* convert result and return it
...
*/

Wichtig ist hier der Aufruf von NtDeviceIoControlFile(), die Tatsache, dass die lokale Variable statusBlock als Out-Parameter übergeben wird, und schließlich die Tatsache, dass auf die Signalisierung des Ereignisses mit einem alertable wait gewartet wird. So weit so gut: Sie ruft eine Kernel-Funktion auf, die STATUS_PENDING zurückgibt, wenn sie die Anfrage nicht sofort abschließen kann. In diesem Fall wartet WSPSelect(), bis das Ereignis gesetzt wird. Sobald NtDeviceIoControlFile() fertig ist, schreibt es das Ergebnis in die Variable statusBlock und setzt dann das Ereignis. Die Wartezeit wird beendet und dann kehrt WSPSelect() zurück.

IO_STATUS_BLOCK struct sieht wie folgt aus:

typedef struct _IO_STATUS_BLOCK
{
    union
    {
        NTSTATUS Status;
        PVOID    Pointer;
    };
    ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

Bei 64-Bit ist diese Struktur 16 Byte lang. Es ist mir aufgefallen, dass diese Struktur meinem Muster der Speicherbeschädigung zu entsprechen scheint: die ersten 4 Bytes werden beschädigt (NTSTATUS ist 4 Bytes lang), dann werden 4 Bytes übersprungen (Padding/Leerzeichen für PVOID) und schließlich werden 8 weitere beschädigt. Wenn das tatsächlich in meinen Speicher geschrieben wurde, dann würden die ersten vier Bytes den Ergebnisstatus enthalten. Die ersten 4 Korruptionsbytes waren immer 0x00000102. Und das ist zufällig der Fehlercode für... STATUS_TIMEOUT! Das wäre eine gute Theorie, wenn WSPSelect() nicht auf die Fertigstellung von NtDeviceIOControlFile() warten würde. Aber es war so.

Nachdem ich herausgefunden hatte, wie die select()-Funktion funktionierte, beschloss ich, mir das Gesamtbild der Funktionsweise des Socket Polling Threads anzusehen. Und dann traf es mich wie eine Tonne Ziegelsteine.

Wenn ein anderer Thread einen Socket zur Verarbeitung durch den Socket-Polling-Thread schiebt, ruft der Socket-Polling-Thread select() für diese Funktion auf. Da select() ein blockierender Aufruf ist, muss select() irgendwie unterbrochen werden, wenn ein anderer Socket in die Socket-Polling-Thread-Warteschlange gestellt wird, damit der neue Socket so schnell wie möglich verarbeitet wird. Wie kann man die Funktion select() unterbrechen? Offensichtlich haben wir QueueUserAPC() verwendet, um eine asynchrone Prozedur auszuführen, während select() blockiert war... und das hat eine Exception ausgelöst! Dadurch wurde der Stack abgerollt, wir führten weiteren Code aus und irgendwann in der Zukunft würde der Kernel die Arbeit abschließen und das Ergebnis in die lokale Variable statusBlock schreiben (die zu diesem Zeitpunkt nicht mehr existierte). Wenn es zufällig auf eine Rücksprungadresse auf dem Stack trifft, stürzen wir ab.

Die Lösung war ziemlich einfach: Anstatt QueueUserAPC() zu verwenden, erstellen wir jetzt einen Loopback-Socket, an den wir jedes Mal ein Byte senden, wenn wir select() unterbrechen müssen. Dieser Pfad wird schon seit geraumer Zeit auf POSIX-Plattformen verwendet und wird nun auch unter Windows eingesetzt. Die Korrektur dieses Fehlers wurde in Unity 5.3.4p1 ausgeliefert.

Dies ist einer dieser Fehler, die Sie nachts wachhalten. Ich habe 5 Tage gebraucht, um ihn zu lösen, und er ist wahrscheinlich einer der schwierigsten Fehler, die ich je untersuchen und beheben musste. Lektion gelernt, Leute: Werfen Sie keine Ausnahmen aus asynchronen Prozeduren, wenn Sie sich innerhalb eines Systemaufrufs befinden!