Bug in CPropertyPage und „Certified for Vista“

Der von mir gemeldete Bug Bug in der MFC71.DLL bzgl. CPropertySheet/CPropertyPage führt dazu, dass ein Programm, dass den genannten Voraussetzungen entspricht, bei den Test für „Certified for Vista“ durchfällt!

Der Application Verifier führt bei den Basic Test einen Page Boundary Check durch. Dabei werden alle Allokationen so durchgeführt, dass sie immer am Ende einer Memory Page erfolgen. Jeder Zugriff über die Grenze hinweg führt dann zu einem Crash.

Im realen Leben ist der Crash wirklich selten, aber mit dem Application Verifier kracht es sofort. Das Problem ist, dass dieser Basic Test Bestandteil von „Certified for Vista“ ist.

Die Folge man rasselt mit 100% Sicherheit durch!

Bug Report hier:
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=270493

Dämlicherweise wurde auch dieser Bug von Microsoft als „Gelöst (Nicht reproduzierbar)“ markiert. Da kann man wirklich nurmit dem Kopf schütteln.

Und ein kleines Demo findet ihr hier:
Testprogramm für CPropertyPage Bug in MFC 7.1

Probleme bei der CTime Serialisierung mit MFC 7.1 und MFC 8.0

Der operator<< und operator>> in MFC 7.1 serialisiert grundsätzlich nur 32bit Werte des CTime. Das wurde offensichtlich gemacht um rückwärtskompatibel zu sein zu time_t obwohl mittlerweile ja intern __time64_t verwendet wird.
Gleichzeitig wurde Serialize64 eingeführt.
Man muss also in VS2003 mit MFC 7.1 Serialize64 verwenden um alle 64bit zu speichern, also den komplettem Wertebereich von CTime.

In VS2005 mit der MFC 8.0 ging man jetzt einen Schritt weiter. Die Operatoren operator<< und operator>> sollten auch 64bit verarbeiten können. Man wollte 64bit speichern und 32bit Daten beim Einlesen erkennen wenn es eben aus alten Dateien stammt und nicht mit der neuen MFC geschrieben wurde.

Dazu wird bei Speicherung von 64bit CTime über den operator<< zuerst ein DWORD gespeichert mit dem Wert cTime64Mark (der Wert ist INT_MIN+10). Danach der 64bit Wert.
Beim Lesen wird zuerst ein DWORD gelesen. Ist dieses nicht cTime64Mark (also INT_MIN+10), dann wird dieser Wert als 32bit CTime Wert gelesen.
Ansonsten werden die 64bit in den CTime Wert gelesen.

operator>> bläht also auch die Ausgabe um weitere 32bit auf. Ein 64bit CTime Wert wird in der MFC 8.0 als 32+64bit Wert gespeichert, also 4Bytes mehr.

Speicherst man mit MFC 7.1 und dem operator<<, dann kannst man die Daten in MFC 7.1 und MFC 8.0 mit dem operator>> lesen, denn der Wert wird wegen des fehlenden Leadin-Bytes als 32bit Wert in der MFC 8.0 erkannt.
Speicherst man aber mit MFC 8.0 und dem operator<< dann ist das für MFC 8.0 kompatibel. Und kannst es in MFC 8.0 mit dem operator>> lesen. Allerdings speichert – wie schon gesagt – MFC 8.0 zuerst ein DWORD (das Leadin für 64bit) und dann den 64bit Wert.
Das versteht nun MFC 7.1 gar nicht, denn das liest immer nur ein DWORD mit dem operator>>.

❗ Ein Problem liegt also im Schreiben unter MFC 8.0 mit dem operator<< und im Lesen der Daten mit einem Programm das die MFC 7.1 verwendet und den operator>>!

❓ Was kann man machen um komplett kompatibel zu sein?

  • Würde in beiden Programmen Serialize64 verwendet gibt es kein Problem!
    Wichtig Serialize64 verwendet kein Leadin weder unter MFC 7.1 noch unter MFC 8.0!
  • Kompatibel zu beiden Versionen von MFC 7.1 und MFC 8.0 ist man auch wenn in der MFC 7.1 und in der MFC 8.0 immer nur ein DWORD gespeichert wird (das kann ich aber wirklich nicht mehr empfehelen). Unter MFC 8.0 muss das manuell gemacht werden.
  • Wenn man also Code-kompatibel (nicht binärkomaptibel) für alle MFC Versionen ab 7.0 bleiben will sollte man immer Serialize64 verwenden was einem auch ein kompakteres Format beim Speichern der Daten garantiert.

Eigentümlich ist, dass dies nicht als Breaking Change dokumentiert ist.
Oder ich habe es (wie so manches) einfach überlesen, aber hier steht es nicht:
ATL and MFC Changes: ATL 7.1 and MFC 7.1
ATL and MFC Changes: ATL 7.0 and MFC 7.0

PS: Ich habe CTime Aufgrund der vielen Unzulänglichkeiten nie verwendet. Mein Favorit ist seit eh und je COleDateTime bzw. DATE und die dazugehörige COleDateTimeSpan Klasse. Diese ist wirklich einfach in der Kalkulation und war durch das Design nie von einem begrenzten Wertebereich betroffen.

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