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


Предусловия

Проблема может возникать при выполнении следующих условий:
  • Код приложения не определяет макросы _WIN32_IE или _WIN32_WINNT, или их значения меньше чем 0x0300 и 0x0501 соответственно.
И
  • Либо MFC используется в виде статической библиотеки и приложение явно или неявно использует CThreadLocal<AFX_MODULE_THREAD_STATE>::GetData(). Например:
    AFX_MODULE_STATE* moduleState = _AFX_CMDTARGET_GETSTATE();
    AFX_MODULE_THREAD_STATE* threadState = moduleState->m_thread;
    в данном случае CThreadLocal<>::GetData() вызывается неявно через CThreadLocal<>::operator TYPE*().
  • либо (вне зависимости от того используется MFC как статическая библиотека или как DLL) код приложения использует члены m_nLastStatus или m_pLastStatus класса 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, не остается.
Автор: Павел Кузнецов    Оценить