Wie RPC Context Handles nicht funktionieren
Bei επτ€σ schreibt gerade Hinz und Kunz irgendwelches RPC-Gedöns und so mancher beginnt zu begreifen, was für eine Power hinter dieser eigentlich uralten Technologie steckt, insbesondere hinter Context Handles. Dummerweise scheint nur jeder, inklusive meiner bescheidenen Wenigkeit, am Anfang immer den gleichen Denkfehler dabei zu begehen, weshalb ich hier 'mal drüber schreibe, nicht zuletzt weil ich heute im Code eines prominenten Kollegen genau diesen Denkfehler, den kein geringerer als LarryO bei mir hier vor gut einem Jahr beseitigt hat, wiedergefunden habe. (Wenn im folgenden von "einem RPC" die Rede ist, dann meine ich damit "einen Remote Procedure Call", nicht RPC als Technologie.)
Was'n das überhaupt, so'n Context Handle?
Ein Context Handle repräsentiert State, der in einem RPC-Server angelegt wird und über mehrere Aufrufe des Clients hinweg verwendet wird um letztendlich im Server dann wieder abgebaut zu werden. Typischerweise ruft zu dem Zweck der Client einen RPC auf, der ihm ein opakes HANDLE zurückgibt, das er sich dann merkt und in nachfolgenden Aufrufen verwendet, bis er genug davon hat und einen RPC aufruft, der den State explizit wieder freigibt. Dieses Pattern findet man im Windows API überall: Um eine Datei zu öffnen, rufe ich CreateFile auf und bekomme ein File Handle zurück, das kann ich dann irgendwelchen anderen APIs zufüttern (WriteFile, ReadFile, etc...) um es schließlich mit CloseHandle zu schließen. Genauso erhalte ich mit RegOpenKeyEx/RegCreateKeyEx ein Handle auf einen Registry Key das ich verwenden kann um Values auszulesen (RegQueryValueEx), Subkeys anzulegen (RegCreateKeyEx), etc... und wenn ich fertig bin, mache ich einen RegCloseKey.
Was dabei unter der Haube im RPC Server passiert, wenn ein Client solch ein Context Handle anfordert, ist dem Server überlassen. Zumeist legt er sich aber irgendeine Datenstruktur auf seinem Heap an, deren Adresse er dann dem serverseitigen Context Handle zuweist und die der serverseitige Funktionsaufruf dann als [out]-Parameter an die RPC-runtime zurückgibt. Auf Clientseite erhält dann der Client sein clientseitiges Context Handle von dem RPC zurück, das er in nachfolgenden Aufrufen dann verwenden kann. Wann immer nun ein Client mit diesem clientseitigen Context Handle einen RPC aufruft, wird im Server der entsprechende RPC mit diesem Handle Value, den der Server zuvor an die RPC-runtime zurückgegeben hatte, aufgerufen. Der Server castet dann einfach diesen Wert in einen Pointer auf die zuvor angelegte Datenstruktur und kann dann mit dem State weiterarbeiten. Das schöne ist aber, daß die RPC-runtime verhindert, daß ein böswilliger Client einfach Garbage als Context Handle schicken kann um somit den Server zu crashen und weitherhin, daß verlorengegangene Clients zu einem rundown des serverseitigen Context Handles führen.
Was'n ein rundown?
Der rundown verhindert, daß Clients, die ja über das Context Handle zumeist Speicher in einem Server anlegen, ein Speicherleck im Server verursachen, wenn sie vergessen, dieses Context Handle mit dem dafür vorgesehenen RPC wieder zu schließen. Auch dadurch daß ein Client einfach crasht oder das Netzwerk zwischen dem Client und dem Server runterfällt, bevor er zum Schließen des Context Handles kommt, könnte es zu dem Speicherleck kommen, das der rundown verhindert.
Wenn man also ein RPC-Interface schreibt, das Context Handles an den Client ausgibt, muß man zwangsweise eine Rundown-Methode für die Context Handles implementieren, die in aller Regel nichts anderes tut, als das, was die Methode zum regulären Schließen des Context Handles durch den Client auch tun würde. Diese Rundown-Methode ruft dann die RPC-runtime immer dann auf, wenn ein Client-Prozeß beendet wurde (oder nicht mehr erreichbar ist), der noch austehende Context Handles hat, und zwar genau einmal pro ausstehendem Context Handle.
Und um welchen Denkfehler geht es jetzt?
Der Denkfehler bei alldem besteht nun darin, anzunehmen, daß exakt der Wert, den der Server seinem serverseitigen Context Handle bei der Erzeugung zuweist, dann auch beim Client auftaucht. So hat der liebe Kollege in seinem Server für den Fehlerfall dem Context Handle den Wert INVALID_HANDLE_VALUE (das ist (HANDLE)-1 oder (void *)-1 oder auf 32-bit Systemen der Wert 0xFFFFFFFF) zugewiesen und ist steif und fest davon ausgegangen, daß exakt dieser Wert auch dem clientseitigen Context Handle von der RPC runtime zugewiesen wird, so daß der Client anhand von INVALID_HANDLE_VALUE erkennen kann, daß im Server ein Fehler aufgetreten ist. Was aber stattdessen in diesem Fall tatsächlich im Client ankommt, ist ein gültiger Wert, der weder NULL ist, noch INVALID_HANDLE_VALUE, denn der einzige Wert, der einem Context Handle im Server zugewiesen werden kann, der dann auch identisch im Client erscheint, ist NULL. Und aus dem Grund hat auch ein gültiges Context Handle auf Clientseite einen Non-NULL-Wert und keinen Non-INVALID_HANDLE_VALUE-Wert. Das bedeutet, daß die RPC runtime ein serverseitiges Context Handle das nicht den Wert NULL (also beispielsweise den Wert INVALID_HANDLE_VALUE) hat, immer als gültiges Context Handle betrachtet und für dieses im Bedarfsfall auch den rundown aufruft. Im Umkehrschluß bedeutet daß, daß man immer nur mit dem Wert NULL einen Fehler anzeigen sollte, nicht aber mit dem Wert INVALID_HANDLE_VALUE. Und was für eine Koinzidenz: Weist man im Server einem Context Handle den Wert NULL zu, wird dafür im Bedarfsfall auch kein rundown aufgerufen.
Mein Denkfehler in dem Zusammenhang vor einem Jahr war ein wenig anders: Ich konnte damals nicht begreifen, daß ein 64-bittiger RPC-Server ein 64-bittiges Context Handle an den Client ausgeben kann, und der dann über ein 32-bittiges Context Handle (das ja dann nach meiner Logik den truncateden Wert des 64-bittigen Context Handles beinhalten mußte) korrekt den State im Server bei nachfolgenden RPCs ansprechen kann. Aber das war eben ein Denkfehler, denn der Wert den der Client als Context Handle sieht, hat außer bei NULL überhaupt nichts zu tun mit dem Wert, den der Server ausgegeben hat - außer für die RPC runtime selber, die die Zuordnung zwischen beiden auf magische Weise herstellt.
Trackback address for this post
No feedback yet
Comments are closed for this post.