Dem DLL-Lader auf die Sprünge helfen...
Derzeit entstehen bei επτ€σ neue DLLs wie nix Gutes. Das ist einerseits etwas Vorteilhaftes, weil dadurch weniger dieser Riesenklötze von Monolithen wie in der Vergangenheit entstehen, andererseits geht schnell aber 'mal der Überblick verloren. So beispielsweise bei der Frage, ob eine DLL auch wirklich an der virtuellen Adresse geladen werden kann, an der sie geladen werden soll. Weil kein Mensch das unmöglich von Hand überprüfen kann, habe ich am vergangenen Wochenende hierzu ein Tool geschrieben, das man einfach in den Buildprozess integrieren kann und das dem Buildmaster nicht nur sagen kann, ob jemand einfach nur geschlampt hat und sein Rebasing vergessen hat, sondern auch, ob es trotz aller guten Absichten bei eigentlich sauber ge-rebaseten (wie schreibt man das eigentlich?) DLLs irgendwelche Überlappungen in ihrem virtuellen Adreßraum gibt. Das Tool kann man hier runterladen und es wird so angewendet:
prefload dir <-e=excludefile> <-v> wildcards
also beispielsweise so:
prefload d:\devstudio\mysoftwareproduct\release *.dll *.ocx
Im Beispiel durchsucht es alle Dateien mit den Endungen dll und ocx unterhalb des Verzeichnisses d:\devstudio\mysoftwareproduct\release und stellt dabei potentielle Ladekonflikte fest. Wenn ein derartiger Ladekonflikt gefunden wird, wird er nach stdout geschrieben:
hornet.dll (10000000-10013000) has a conflict with ant.dll(10000000-10008000)
Und wenn irgendein solcher Ladekonflikt auftritt, dann kehrt das Tool auch mit einem Exitcode ungleich 0 zum Betriebssystem zurueck, was natürlich die Verwendung in einem batchbasierten System wie dem daily build besonders geschmeidig macht.
Der Clou ist natürlich, daß man mit dem Tool, für das ich mir gar nicht erst die Mühe eines UNICODE- oder x64-Builds gemacht habe, auch x64-Binaries ohne Probleme untersuchen kann. Möglich macht's der simple Aufbau von Windows PE-files. Man öffnet einfach ein PE-File mit plain-vanilla CreateFile und bildet es in ein memory mapped file ab. Ab da hangelt man sich anhand der Strukturen aus winnt.h an der Datei entlang um an die entscheidenden Informationen zu gelangen. Piece o' cake.
Was ich aber in keinster Weise weiß: Muß man das für DLLs, die aus managed code bestehen, eigentlich auch tun? Eigentlich schon, oder? DEI to the rescue!
Und das nächste Mal: Wozu überhaupt dieses gestörte Rebasing, tut doch schließlich auch so, oder!?!
Wieviel .NET steckt in Vista Build 5342?
Der Build ist zwar schon etwas älter, aber eben nun der Vollständigkeit halber:
Von 7385 DLL und Exe-Dateien in diesem Build linken 428 gegen die .NET-runtime. Das sind 5,80% und damit 0,06 Prozent mehr als im Build 5308.
Running under least privilege
Irgendwie sind wir, so scheint's, immer noch nicht im 21. Jahrhundert angekommen. Oder wie erkläre ich mir sonst, daß es immer noch namhafte Software gibt, die nicht mit least Privileges läuft?
Heute habe ich mir Visual Assist installiert. Brav also als lokaler Administrator eingeloggt, das Setup ausgeführt, den Key eingegeben, gefreut. Dann ausgeloggt und als unterprivilegerter User gestartet, msdev abgefeuert - keine Spur von Visual Assist! Prominente Registry Keys in HKCU aus dem Hive des Administratoren in den Userhive gemergt: Aha, Visual Assist zuckt und fragt wieder nach dem Key, aber nach der Keyeingabe fehlt wieder jede Spur von Visual Assist. Schade, daß ein ansonsten so gutes Produkt an dieser Stelle so erbärmlich patzt. Das wäre ja, als ob επτ€σ seine Logfiles ins Program Files Directories schriebe... ähem, OK, hüstel, ...lassen wir das.
Zu guter Letzt die FAQ des Herstellers gesucht und schnell fündig geworden: Irgendwie stellen die Knilche das beinahe auch noch als Feature dar, daß man Visual Assist auch als Nichtadministrator betreiben kann (wenn man die NTFS-Rechte für das Installationsverzeichnis aufbohrt und VA für jeden User dahin oder woandershin neu installiert). So schnell werden aus Tätern Opfer...
What's wrong with this code? Antwort
Link: http://mcblogs.craalse.de/sku?title=what_s_wrong_with_this_code&more=1&c=1&tb=1&pb=1
So jetzt die Antwort. Mein tendenziöses Source-Code-Quoting ließ es dem DEI ja schon wie Schuppen von den Haaren fallen. Hier noch mal der Stein des Anstoßes:
BOOL NetState_ThreadControl( DWORD dwCtrlCode, PNETSTATE& pNetState ) { /// ... switch ( dwCtrlCode ) { case NETSTATE_CTRL_STARTPOLLING: pNetState->hPollingThread = _beginthreadex( NULL, 0, NetState_PollingThread, .... break; case NETSTATE_CTRL_SUSPENDPOLLING: SuspendThread( pNetState->hPollingThread ); break; case NETSTATE_CTRL_STOPPOLLING: TerminateThread( pNetState->hPollingThread, 0); CloseHandle( pNetState->hPollingThread ); pNetState->hPollingThread = NULL; break; } return TRUE; }
Halten wir mal folgende Fakten fest:
- Der Code verwendet die C-tuntime von Microsoft, das erkennt man an der Verwendung von _beginthreadex.
- Derselbe Thread der dergestalt erzeugt wird, wird entweder suspendet oder terminiert in den
NETSTATE_CTRL_SUSPENDPOLLING oder NETSTATE_CTRL_STOPPOLLING case-branches.
Und damit wäre das Kardinalproblem schon genannt: Einen Thread der mit Methoden der C-runtime erzeugt wurde, darf man niemals mit TerminateThread terminieren, weil dadurch die per-Thread-Datenstrukturen der Runtime nicht freigegeben werden können. Auch Datenstrukturen des Threads, die andere DLls für ihn in DllMain anlegen, werden nicht mehr abgebaut, sein Stack bleibt bestehen (also in der Regel 1MByte im weiteren Leben des Prozesses nicht mehr nutzbarer virtueller Adressraum). Natürlich werden auch die Destruktoren der Objekte auf dem Stack nicht aufgerufen, wär ja noch schöner! Schlimmer noch als diese Speicherlecks, die man beim Shutdown eines Prozesses vielleicht noch verkraften könnte, wiegt aber der Umstand, dass sowohl TerminateThread als auch SuspendThread einen kompletten Prozess mit Leichtigkeit in einen Deadlock bringen können. Denn wenn der zu suspendierende oder terminierende Thread gerade der Owner einer Critical Section ist, wenn auf den Thread diese Aktion ausgelöst wird, dann ist diese Critical Section für andere Threads verloren. Das kann passieren, wenn der fragliche Thread gerade mitten in einer Speicherallokation mit new oder malloc ist und er dann durch diese APIs nicht mehr den lock des Memory Manager freigeben kann. Will nun ein anderer Thread Speicher allozieren, wartet er darauf bis zum Sanktnimmerleinstag. Die Threads, und womöglich der ganze Prozeß, sind dann im Deadlock.
Wann kann man nun die beiden APIs legal aufrufen? Eigentlich nur wenn man gegen keine C-runtime linkt, wenn man in den Threads keine Aufrufe ausser in kernel32.dll und kein LoadLibrary/LoadLibraryEx macht und wenn man keinen State auf seinem Thread hat (Can you say destructors? Can you say __try-__finally?). Warum die Beschränkung auf kernel32.dll? Weil MS seit W2K System-DLLs ausliefert, die per Delayload andere DLLs nachladen. Das bedeutet, daß harmlose API-Aufrufe zum Nachladen einer DLL führen können. Dann hat der fragliche Thread den Loader-Lock, wenn ein anderer ihn nun suspendet oder terminiert. Das hat wiederum zur Folge, daß kein anderer Thread mehr eine DLL laden kann, bzw. ein derartiger Versuch eben deadlockt. Natürlich kann ein anderer Thread auch kein derartiges API mehr aufrufen, das per Delayload andere DLLs nachlädt.
Und ja, warum der zweite formale Parameter ausgerechnet eine PNETSTATE-Referenz sein muß, ist mir auch nicht wirklich klar...
Auch warum der TeminateThread dem Thread einen im allgemeinen als legal angesehenen Wert wie 0 unterschiebt, damit ein anderer ja nicht erkennt, daß der Thread eines gewaltsamen Todes gestorben ist, entzieht sich meinem an Defekten so reichen Intellekt dann vollends. Wie wär's mit 0xdeadbeef?
What's wrong with this code?
So, heute war's mir langweilig und ich habe mal den unlängst geschriebenen Code von anderen Leuten in meinem Dunstkreis inspiziert. Denn man gibt sich bei επτ€σ zwar gerne weltmännisch offen und trägt die eine oder andere Tugend wie eine Fahne vor sich her, aber die Realität sieht dann in aller Regel a bissal anders aus. Wie früher beim Unternehmensmotto ("Where people make..."), so heute auch bei Code Reviews. Und deswegen muß die Lücke eben am frühen Samstagabend notdürftig geschlossen werden. Denn nach nicht mal 5 Minuten bin ich über ein ziemlich grauenvolles Artefakt gestossen:
BOOL NetState_ThreadControl( DWORD dwCtrlCode, PNETSTATE& pNetState ) { /// ... switch ( dwCtrlCode ) { case NETSTATE_CTRL_STARTPOLLING: pNetState->hPollingThread = _beginthreadex( NULL, 0, NetState_PollingThread, .... break; case NETSTATE_CTRL_SUSPENDPOLLING: SuspendThread( pNetState->hPollingThread ); break; case NETSTATE_CTRL_STOPPOLLING: TerminateThread( pNetState->hPollingThread, 0); CloseHandle( pNetState->hPollingThread ); pNetState->hPollingThread = NULL; break; } return TRUE; }
Wo ist hier das große Problem, das den Einsatz dieses Stückchen Code zum Glücksspiel werden läßt? Kleiner Hinweis: Es geht nicht um fehlende Prüfungen von Rückgabewerten, error propagation im Allgemeinen. Los DEI, KIH, Schmitti und OTE und wer noch, was stinkt hier unübersehbar zum Himmel?