Patch für CPropertyPage Bug der MFC71.DLL/MFC71U.DLL

Wer keinen neuen Build bezüglich des dieses Problemes Bug in der MFC71.DLL bzgl. CPropertySheet/CPropertyPage machen kann oder auch nicht seinen Sourcecode nach entsprechenden Konstruktoren durchsuchen kann und auf die ausgelieferten Versionen von Microsoft angewiesen ist, kann auch einen kleinen Patch durchführen.

MFC71.DLL aus SP1 Version 7.10.6030
Größe 1.060.864 Bytes, Dienstag, 11. Juli 2006, 18:43:32
Patch der Adressen
0x0008E9F0 von 0x89 0x38 -> 0x90 0x90
0x0008EA09 von 0x89 0x38 -> 0x90 0x90

MFC71U.DLL aus SP1 Version 7.10.6030
Größe 1.056.768 Bytes, Dienstag, 11. Juli 2006, 19:02:30
Patch der Adressen
0x000A1ACC von 0x89 0x38 -> 0x90 0x90
0x000A1AE5 von 0x89 0x38 -> 0x90 0x90

Gesucht werden kann auch nach der Sequenz 0x8B 0x46 0x74 0x89 0x38. Diese Sequenz kommt in der MFC.DLL genau zweimal vor.

Es entspricht dem folgenden Code:

8B 46 74 mov eax,dword ptr [esi+74h]
89 38 .. mov dword ptr [eax],edi

Weggepatch wird in diesem Falle einfach das Laden der Adresse (zweites mov Statement) und durch zwei NOP ersetzt.
Wer die entsprechende PDB Datei hat sieht im Disassembly hier das Statement
m_psp.dwSize = sizeof(m_psp);
Der Bug befindet sich in der Datei dlgprop.cpp in den Zeilen 184 und 189!

Anwendung auf eigene Gefahr!

Bug in der MFC71.DLL bzgl. CPropertySheet/CPropertyPage

Ich habe ein Projekt, dass einen eigenen MFC Build verwendet. Dieser basiert zu 99,999% auf dem MFC71 SP1 Build aus VC-2003.NET. Das dies ein eigener Build ist spielt für den Bug allerdings keine Rolle, allerdings evtl. für die Lösung.

Zu den Rahmenbedingungen. Man baut eine EXE/DLL mit folgenden Eigenschaften:

  • _WIN32_IE< =0x0500.
  • Verwendet die DLL-Version der MFC (Use MFC in a Shared DLL)
  • Man verwendet CPropertyPage und CProprtySheet als Wizard.
  • Man verwendet Header-Titel und Header-Subtitel und damit einen der beiden nachfolgenden Konstruktoren:
    CPropertyPage(UINT nIDTemplate, UINT nIDCaption, UINT nIDHeaderTitle, UINT nIDHeaderSubTitle = 0, DWORD dwSize = sizeof(PROPSHEETPAGE));
    CPropertyPage(LPCTSTR lpszTemplateName, UINT nIDCaption, UINT nIDHeaderTitle, UINT nIDHeaderSubTitle = 0, DWORD dwSize = sizeof(PROPSHEETPAGE));

Die MFC71.DLL ist so gebaut, dass sie mit allen 3 Versionen der PROPSHEETPAGE Struktur klar kommen soll. Es gibt drei Versionen, die mit folgenden _WIN32_IE Definitionen unterschieden werden:

  • für WIN32_IE<0x0400, die aller erste Version die mit Windows 95 kam. Größe 40 Bytes.
  • für WIN32_IE>=0x0400 && _WIN32_WINNT<=0x0500, hier wurden Titel und Subtitel hinzugefügt (pszHeaderTitle, pszHeaderSubTitle). Größe 48 Bytes.
  • und zuletzt _WIN32_WINNT>=0x0501, hier kam noch ein Activation-Context dazu (hActCtx). Größe 52 Bytes.

Was passiert nun:

  • Einer der genannten Konstruktoren für die CPropertyPage wird aufgerufen.
  • Der letzte Parameter sizeof(PROPERTYPAGE) zeigt nun der DLL an welche Version das aufrufende Programm verwendet, damit die MFC71.DLL die entsprechenden Größen bereitstellt.
  • AllocPSP wird entsprechend aufgerufen und dynamisch diese PROPERTYPAGE allokiert.

Bis hierher ist alles prima, aber leider passiert es un, dass diese vordefinierte Größe in der Funktion CommonConstruct(LPCTSTR lpszTemplateName, UINT nIDCaption, UINT nIDHeaderTitle, UINT nIDHeaderSubTitle) überschrieben wird (Zeile 184+189 in dlgprop.cpp). Das passiert mit dem Statement:
m_psp.dwSize = sizeof(m_psp);
D.h. die PROPERTYPAGE Struktur ist mit weniger als 40 oder 48 Bytes allokiert. Im dwSize Member steht aber nun 52.
In der Funktion BuildPropPageArray() wird nun aus den einzelnen PROPERTYPAGE’s ein Array gebildet. Dabei werden jeweils dwSize-Bytes (also 52) aus den entsprechenden allokierten Bereichen kopiert. Aber Achtung, diese sind kleiner allokiert (40 oder 48 Bytes)
In der Debug Version ist das oft nicht tragisch, denn da kommen hinten dran noch einige Füllbytes. Auch in der Release Version kracht es nur manchmal. Genau nur dann, wenn einer der allokierten PROPERTYPAGE Strukturen genau auf mit einer allokierten Speicherseite endet und das 49ste Byte der Struktur eben nicht allokiert ist und die Speicherseite exakt hier endet.

Diese sehr üble Konstellation machte es mir extrem schwer diesen Fehler zu finden. Ich hasse es einfach optimierten Code zu debuggen 😉

Fix:

  • Keinen der obigen Konstruktoren verwenden und die Member m_strHeaderTitle und m_strHeaderSubTitle selbst füllen.
  • oder wie ich einen eigenen MFC Build erzeugen und verwenden. Die entsprechenden Code Zeilen 184+189 aus der dlgprop.cpp entfernen.

Hinweis:
Crashdumps zeigen meistens im Stacktrace die Funktionen BuildPropPageArray und memcpy!

BTW:
Dieser Bug betrifft sowohl die MFC71.DLL im RTM und im SP1.
In der MFC 8.0 des VS-2005 ist dieser Bug behoben.

Notwendigkeit von Manifesten für DLL’s mit VC-2005

Durch eine Anfrage in meiner Lieblingsgruppe microsoft.public.de.vc bin ich auf folgende interessante Frage gestoßen: Benötigt eine DLL die mit VC-2005 erzeugt wurde ein Manifest?

Nun wie viele Antworten im Leben lässt sich dies nicht eindeutig mit Ja oder Nein beantworten.

Nein! Es wird kein Manifest benötigt, wenn die CRT statisch gelinkt wird. In diesem Fall wird auch kein Manifest benötigt zumindest nicht für die CRT, was nicht heißt, dass nicht auch andere Assemblies per Manifest angebunden werden müssen.

Nein! Wenn die EXE-Datei bereits über ein Manifest verfügt und eine CRT-DLL mit gleichem Namen lädt, hier also z.B. die MSVCR80.DLL. In diesem Fall kann man sich ein Manifest sparen. Es kann aber hier spannend werden, weil die entsprechende DLL natürlich so nicht beeinflussen kann dir 8.0 CRT als SP1 oder RTM zu nutzen. Aber vermutlich ist das sowieso egal.

Ja! Die DLL benötigt ein Manifest, wenn ein beliebiges Programm die DLL nutzt. Zum Beispiel eines, das die CRT statisch linkt, oder das mit einer älteren VC Version erzeugt wurde. In diesem Fall muss die DLL zwingend ein Manifest haben. Und jetzt wird es noch strenger. Wird die DLL per LoadLibrary geladen, dann muss dieses Manifest sogar embedded sein! 🙄
Externe Manifeste bei DLLs werden nur beim impliziten Laden berücksichtigt. Wird eine DLL mit LoadLibrary geladen, dann werden nur eingebette Manifeste berücksichtigt.

❗ Falle: Hat die DLL kein Manifest wird immer garantiert, dass die CRT Version von EXE und DLL passen. Man kann also gefahrlos Speicher in der DLL allozieren und in der EXE freigeben und umgekehrt. Oder eben auch CRT Objekte austauschen. Nutzt die DLL ein eigenes Manifest besteht die Möglichekit, dass EXE und DLL eine unterschiedliche CRT verwenden! Und noch spannender wird es wenn eine EXE mit CRTx eine DLL1 benutzt die per Manifest CRTy verwendet und diese nun eine DLL ohne Manifest lädt… Ja es wird die CRT der EXE verwendet…

Ja ja. Wir haben dank der Manifeste nun keine DLL Hölle mehr. Wir haben eine Manifest Hölle 😀

Meine Empfehlung daher:

  1. Wenn es eigenständige kleine Module sind, dann statisch gegen die CRT linken.
  2. Ansonsten immer ein Manifest einbetten, wenn die DLL eigenständig genutzt wird!
  3. Wird die DLL im Kontext einer bestimmten EXE(s) Datei verwendet, sollte nur die EXE(s) ein  Manifest haben.
  4. In jedem Fall darauf achten, dass gleiche CRT Versionen verwendet werden. RTM, SP1 und irgendwann wahrscheinlich SP2 werden hier Konflikte möglich machen.
  5. MFC Extension DLLs müssen zwingend mit der selben MFC Version (RTM oder SP1) verwendet werden!

Stoff zum weiterlesen:
Isolated Applications and Side-by-side Assemblies

Besonders lesenswert hier der Beitrag von Nicola Dudar:
How to Debug ‚The System cannot Execute the specified program‘ message

Button + Accelerator + ShowWindow(SW_HIDE) – EnableWindow(FALSE) = Falle

Da hat man einen Multifunktionalen Dialog. Einer der Schalter in dem Dialog heißt Delete. Und das D ist als Accelerator mit einem & versehen. Gemäß einer internen Rechteverwaltung haben manche Nutzer nicht das Recht diesen Button zu benutzen. Der Programmierer (nicht ich ;-)) hat in diesem Fall einfach den Schalter mit ShowWindow(SW_HIDE) verborgen. Ein weiterer Test ob die Rechte wirklich gegeben sind entfiel im OnBtDelete Handler.

Nun stellte sich aber heraus, dass es manche Nutzer geschafft haben, dennoch Einträge zu löschen.

Nun der Grund ist einfach. Solange der Button nicht mit EnableWindow(FALSE) auch disabled wird, kann man mit ALT+D, also Drücken der ALT-Taste und des Accelerators diesen Schalter auslösen.

Jo. So einfach hat man ein Userinterface gebastelt, mit dem man sich hereinlegen kann.