VC71, AFX_MODULE_THREAD_STATE, m_pLastStatus
От: Павел Кузнецов  
Дата: 21.05.04 21:44
Оценка: 59 (5)
#Имя: FAQ.mfc.AFX_MODULE_THREAD_STATE
Вчера обнаружил весьма своеобразную проблему с AFX_MODULE_THREAD_STATE.


Предусловия

Проблема может возникать при выполнении следующих условий:
И

В будущем эта же самая проблема может возникать, если структуры TTTOOLINFOA и TTTOOLINFOW в следующих версиях <commctl.h> приобретут новые члены, возможно "включающиеся" соответствующими макросами, и пользователь начнет использовать новую версию PlatformSDK.


Симптомы

Непосредственно наблюдавшееся проявление заключалось в том, что приложение "падало" в функции CWnd::CancelToolTips(BOOL bKeys) при обращении к AFX_MODULE_THREAD_STATE::m_pLastStatus (...\atlmfc\src\mfc\wincore.cpp):
  1062  // check for active control bar fly-by status
  1063  CControlBar* pLastStatus = pModuleThreadState->m_pLastStatus;
  1064  if (bKeys && pLastStatus != NULL && GetKeyState(VK_LBUTTON) >= 0)
  1065    pLastStatus->SetStatusText(static_cast<INT_PTR>(-1));

Также я полагаю, что эта же проблема может проявляться в похожих "падениях" или "стрельбе по памяти" в других местах при доступе к AFX_MODULE_THREAD_STATE::m_pLastStatus:
  ...\atlmfc\src\mfc\barcore.cpp(203):  if (pModuleThreadState->m_pLastStatus == this)
  ...\atlmfc\src\mfc\barcore.cpp(205):  pModuleThreadState->m_pLastStatus = NULL;
  ...\atlmfc\src\mfc\barcore.cpp(336):  pModuleThreadState->m_pLastStatus = NULL;
  ...\atlmfc\src\mfc\barcore.cpp(350):  pModuleThreadState->m_pLastStatus = this;
  ...\atlmfc\src\mfc\barcore.cpp(576):  if (pModuleThreadState->m_pLastStatus == this)
  ...\atlmfc\src\mfc\barcore.cpp(579):  ASSERT(pModuleThreadState->m_pLastStatus == NULL);

или AFX_MODULE_THREAD_STATE::m_nLastStatus:
  ...\atlmfc\src\mfc\afxstate.cpp(149): m_nLastStatus = static_cast<INT_PTR>(-1);
  ...\atlmfc\src\mfc\barcore.cpp(206):  pModuleThreadState->m_nLastStatus = static_cast<INT_PTR>(-1);
  ...\atlmfc\src\mfc\barcore.cpp(279):  pModuleThreadState->m_nLastStatus = static_cast<INT_PTR>(-1);
  ...\atlmfc\src\mfc\barcore.cpp(291):  pModuleThreadState->m_nLastStatus = static_cast<INT_PTR>(-1);
  ...\atlmfc\src\mfc\barcore.cpp(298):  if (pModuleThreadState->m_nLastStatus == static_cast<INT_PTR>(-1))
  ...\atlmfc\src\mfc\barcore.cpp(348):  if (!(m_nStateFlags & statusSet) || pModuleThreadState->m_nLastStatus != nHit)
  ...\atlmfc\src\mfc\barcore.cpp(394):  nHit = pModuleThreadState->m_nLastStatus;
  ...\atlmfc\src\mfc\barcore.cpp(416):  else if (nHit != pModuleThreadState->m_nLastStatus)
  ...\atlmfc\src\mfc\barcore.cpp(420):  pModuleThreadState->m_nLastStatus = nHit;



Причина

Т.к. AFX_MODULE_THREAD_STATE включает TOOLINFO m_lastInfo по значению, и sizeof(TOOLINFO) зависит от макросов _WIN32_IE и _WIN32_WINNT, очевидно, что при несовпадении значений этих макро-определений с теми, которые использовались при компиляции MFC, могут начинаться сюрпризы. В частности, любой непосредственный доступ к членам m_nLastStatus и m_pLastStatus класса AFX_MODULE_THREAD_STATE из кода приложения может приводить к получению неверных значений и/или порче памяти, т.к. эти члены будут расположены по другим смещениям, чем в версии AFX_MODULE_THREAD_STATE, которую "видит" MFC.

Но проблема даже хуже, т.к. даже если пользовательский код не содержит обращений к упомянутым членам класса AFX_MODULE_THREAD_STATE, данные могут быть запорчены прямо изнутри кода MFC. Чтобы это произошло, достаточно, чтобы приложение было прилинковано к MFC статически, и чтобы пользовательский код содержал вызов CThreadLocal<AFX_MODULE_THREAD_STATE>::GetData() явно или неявно, через члены operator TYPE*() или operator->() шаблона CThreadLocal<>.

Когда компилятор встречает подобный вызов в коде пользователя, инстанциируются члены GetData() и CreateObject() класса CThreadLocal<AFX_MODULE_THREAD_STATE>. Будучи инстанциирована из пользовательского кода со значениями _WIN32_IE или _WIN32_WINNT отличающимися от тех, что были использованы при компиляции MFC, CreateObject() будет создавать объекты AFX_MODULE_THREAD_STATE другого размера, нежели это ожидается MFC.

Т.к. в приложении может быть только один экземпляр одного и того же члена специализации шаблона класса, компилятор вполне может выберать специализацию, порожденную из пользовательского кода. Более того, это даже более вероятно чем то, что будет выбрана специализация, содержащаяся в коде MFC.

В свою очередь это приведет к тому, что CNoTrackObject::operator new(size_t nSize) будет вызван с nSize < sizeof(AFX_MODULE_THREAD_STATE), ожидаемого MFC. Конструктор AFX_MODULE_THREAD_STATE полагается на то, что CNoTrackObject::operator new заполнит память нулями, поэтому членm_pLastStatus останется неинициализированным. Если и _WIN32_IE < 0x0300 и _WIN32_WINNT < 0x0501, то конструкторAFX_MODULE_THREAD_STATE осуществит запись за пределами выделенного блока при инициализации члена m_nLastStatus.


Решение

Собственно говоря, текущий дизайн класса AFX_MODULE_THREAD_STATE можно считать дефектным, т.к. он слишком слабо устойчив к смене версии PlatformSDK и значений макросов _WIN32_IE и _WIN32_WINNT, используемых приложением. Так что настоящим решением был бы редизайн класса AFX_MODULE_THREAD_STATE так, чтобы члены не были открыты, и, например, член m_lastInfo хранился бы на куче.

Т.к. это не в наших силах, лучшее, что можно сделать сегодня, — не трогать член AFX_MODULE_STATE::m_thread, а вместо этого пользоваться функцией AfxGetModuleThreadState(). В этом случае члены GetData() и CreateObject() класса CThreadLocal<AFX_MODULE_THREAD_STATE> из пользовательского кода инстанциироваться не будут.

Если же приложение содержит обращения к членам m_nLastStatus или m_pLastStatus, то ничего, кроме как следить за синхронностью значений _WIN32_IE и _WIN32_WINNT с теми, что использовались для сборки MFC, не остается.
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Re: VC71, AFX_MODULE_THREAD_STATE, m_pLastStatus
От: SchweinDeBurg Россия http://zarezky.spb.ru/
Дата: 22.05.04 09:23
Оценка:
Здравствуйте, Павел Кузнецов, Вы писали:

ПК>Вчера обнаружил весьма своеобразную проблему с AFX_MODULE_THREAD_STATE.


Спасибо за инфу, Павел! Как я понял, ты 7-ку терзал (или 7.1)... интересно, в 6-ке та же проблема есть? ИМХО _WIN32_IE вряд ли кто-то определит меньше чем 0x0300, а вот _WIN32_WINNT — это уже хуже. Хотя MS, небось, отмажется тем, что AFX_MODULE_STATE и AFX_MODULE_THREAD_STATE — это "внутренние" структуры и нечего туда лазать.

P.S.
Увидев в форуме MFC пост от "Павел Кузнецов" в первый момент подумал, что у меня Янус заглючил.
[ posted via RSDN@Home 1.1.2 stable ]
- Искренне ваш, Поросенок Пафнутий ~ ICQ#116846877
In Windows, there’s always a catch… © Paul DiLascia
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.