Случайные сбои в Windows 10 64bit с подклассами ATL

С самого начала: с 1 марта 2017 года эта ошибка подтверждена Microsoft. Прочитайте комментарии в конце.

Краткое описание:

У меня случайные сбои в больших приложениях, использующих MFC, ATL. Во всех таких случаях после использования подклассов ATL для окна при простых действиях с окном (перемещение, изменение размера, установка фокуса, рисование и т. Д.) Происходит сбой при случайном адресе выполнения.

Сначала это выглядело как дикий указатель или повреждение кучи, но я сузил весь сценарий до очень простого приложения, использующего чистый ATL и только Windows API.

Требования / мои использованные сценарии:

  • Приложение было создано с VS 2015 Enterprise Update 3.
  • Программа должна быть скомпилирована как 32-битная.
  • Тестовое приложение использует CRT в качестве общей DLL.
  • Приложение работает под Windows 10 Build 14393.693 64bit (но у нас есть репродукции под Windows 8.1 и Windows Server 2012 R2, все 64bit)
  • atlthunk.dll имеет версию 10.0.14393.0

Что делает приложение:

Он просто создает рамочное окно и пытается создать множество статических окон с помощью Windows API.
После создания статического окна это окно разделяется на подклассы с помощью метода ATL CWindowImpl :: SubclassWindow.
После операции подкласса отправляется простое оконное сообщение.

Что просходит:

Не при каждом запуске, но очень часто происходит сбой приложения при отправке сообщения в подклассовое окно.
В окне 257 (или другом кратном 256 + 1) подкласс каким-то образом завершается неудачей. Созданный блок ATL недействителен. Кажется, что сохраненный адрес выполнения новой функции подкласса неверен.
Отправка любого сообщения в окно вызывает сбой.
CallCack всегда одинаков. Последний видимый и известный адрес в стеке вызовов находится в atlthunk.dll

atlthunk.dll!AtlThunk_Call(unsigned int,unsigned int,unsigned int,long) Unknown
atlthunk.dll!AtlThunk_0x00(struct HWND__ *,unsigned int,unsigned int,long)  Unknown
user32.dll!__InternalCallWinProc@20()   Unknown
user32.dll!UserCallWinProcCheckWow()    Unknown
user32.dll!SendMessageWorker()  Unknown
user32.dll!SendMessageW()   Unknown
CrashAtlThunk.exe!WindowCheck() Line 52 C++

Выданное исключение в отладчике показано как:

Exception thrown at 0x0BF67000 in CrashAtlThunk.exe:
0xC0000005: Access violation executing location 0x0BF67000.

или другой образец

Exception thrown at 0x2D75E06D in CrashAtlThunk.exe:
0xC0000005: Access violation executing location 0x2D75E06D.

Что я знаю о atlthunk.dll:

Atlthunk.dll, кажется, является лишь частью 64-битной ОС. Я нашел это на системах Win 8.1 и Win 10.

Если доступен файл atlthunk.dll (все машины с Windows 10), эта DLL заботится о thunking. Если DLL отсутствует, thunking выполняется стандартным способом: выделяет блок в куче, помечает его как исполняемый, добавляет некоторую загрузку и оператор jump.

Если DLL присутствует. Он содержит 256 предопределенных слотов для подклассов. Если сделано 256 подклассов, DLL повторно загружает себя в память и использует следующие 256 доступных слотов в DLL.

Насколько я вижу, atlthunk.dll принадлежит Windows 10 и не подлежит обмену или распространению.

Вещи проверены:

  • Антивирусная система была включена или включена, без изменений
  • Защита выполнения данных не имеет значения. (/ NXCOMPAT: НЕТ, а EXE определяется как исключение в настройках системы, тоже вылетает)
  • Дополнительные вызовы вызовов FlushInstructionCache или Sleep после подкласса не имеют никакого эффекта.
  • Целостность кучи здесь не проблема, я перепроверил ее несколькими инструментами.
  • и еще тысячи (возможно, я уже забыл, что я проверял) …;)

Воспроизводимость:

Проблема как-то воспроизводима. Он не падает все время, он падает случайно. У меня есть машина, где код вылетает при каждом третьем выполнении.

Я могу воспроизвести его на двух настольных станциях с i7-4770 и i7-6700.

На другие машины это никак не влияет (работает всегда на ноутбуке i3-3217 или на рабочем столе с i7-870)

О образце:

Для простоты я использую обработчик SEH, чтобы поймать ошибку. Если вы отлаживаете приложение, отладчик покажет упомянутый выше стек вызовов.
Программу можно запустить с целым числом в командной строке. В этом случае программа запускается снова с уменьшенным на 1. счетчиком. Так что, если вы запустите CrashAtlThunk 100, она запустит приложение 100 раз. В случае ошибки обработчик SEH обнаружит ошибку и отобразит текст «Crash» в окне сообщения. Если приложение работает без ошибок, приложение отображает «Успешно» в окне сообщения.
Если приложение запускается без параметра, оно выполняется только один раз.

Вопросы:

  • Кто-нибудь еще может это сделать?
  • Кто-нибудь видел подобные эффекты?
  • Кто-нибудь знает или может представить причину этого?
  • Кто-нибудь знает, как обойти эту проблему?

Заметки:

2017-01-20 Открыта поддержка в Microsoft.

Код

// CrashAtlThunk.cpp : Defines the entry point for the application.
//

// Windows Header Files:
#include <windows.h>

// C RunTime Header Files
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>

#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS      // some CString constructors will be explicit

#include <atlbase.h>
#include <atlstr.h>
#include <atlwin.h>// Global Variables:
HINSTANCE hInst;                                // current instance

const int NUM_WINDOWS = 1000;

//------------------------------------------------------
//    The problematic code
//        After the 256th subclass the application randomly crashes.

class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
virtual BOOL ProcessWindowMessage(_In_ HWND hWnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam, _Inout_ LRESULT& lResult, _In_ DWORD dwMsgMapID) override
{
return FALSE;
}
};

void WindowCheck()
{
HWND ahwnd[NUM_WINDOWS];
CMyWindow subclass[_countof(ahwnd)];

HWND hwndFrame;
ATLVERIFY(hwndFrame = ::CreateWindow(_T("Static"), _T("Frame"), SS_SIMPLE, 0, 0, 10, 10, NULL, NULL, hInst, NULL));

for (int i = 0; i<_countof(ahwnd); ++i)
{
ATLVERIFY(ahwnd[i] = ::CreateWindow(_T("Static"), _T("DummyWindow"), SS_SIMPLE|WS_CHILD, 0, 0, 10, 10, hwndFrame, NULL, hInst, NULL));
if (ahwnd[i])
{
subclass[i].SubclassWindow(ahwnd[i]);
ATLVERIFY(SendMessage(ahwnd[i], WM_GETTEXTLENGTH, 0, 0)!=0);
}
}
for (int i = 0; i<_countof(ahwnd); ++i)
{
if (ahwnd[i])
::DestroyWindow(ahwnd[i]);
}
::DestroyWindow(hwndFrame);
}
//------------------------------------------------------

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR    lpCmdLine,
_In_ int       nCmdShow)
{
hInst = hInstance;

int iCount = _tcstol(lpCmdLine, nullptr, 10);

__try
{
WindowCheck();
if (iCount==0)
{
::MessageBox(NULL, _T("Succeeded"), _T("CrashAtlThunk"), MB_OK|MB_ICONINFORMATION);
}
else
{
TCHAR szFileName[_MAX_PATH];
TCHAR szCount[16];
_itot_s(--iCount, szCount, 10);
::GetModuleFileName(NULL, szFileName, _countof(szFileName));
::ShellExecute(NULL, _T("open"), szFileName, szCount, nullptr, SW_SHOW);
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
::MessageBox(NULL, _T("Crash"), _T("CrashAtlThunk"), MB_OK|MB_ICONWARNING);
return FALSE;
}

return 0;
}

Комментарий после ответа от Евгения (24 февраля 2017 г.):

Я не хочу менять свой первоначальный вопрос, но я хочу добавить дополнительную информацию о том, как получить это в 100% Repro.

1, изменить основную функцию на

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR    lpCmdLine,
_In_ int       nCmdShow)
{
// Get the load address of ATLTHUNK.DLL
// HMODULE hMod = LoadLibrary(_T("atlThunk.dll"));

// Now allocate a page at the prefered start address
void* pMem = VirtualAlloc(reinterpret_cast<void*>(0x0f370000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
DWORD dwLastError = ::GetLastError();

hInst = hInstance;

WindowCheck();

return 0;
}
  1. Раскомментируйте вызов LoadLibrary. Компиляция.

  2. Запустите программу один раз и остановитесь в отладчике. Обратите внимание на адрес, куда была загружена библиотека (hMod).

  3. Остановите программу. Теперь прокомментируйте вызов библиотеки снова и измените VirtualAlloc вызов адреса предыдущего значения hMod, это предпочтительный адрес загрузки в этом окне сеанса.

  4. Перекомпилируйте и запустите. CRASH!

Спасибо Евгению.

До сих пор. Microsoft все еще расследует это. У них есть дампы и весь код. Но у меня нет окончательного ответа. Факт в том, что у нас есть фатальная ошибка в некоторых 64-битных ОС Windows.

В настоящее время я сделал следующие изменения, чтобы обойти это

  1. Откройте atlstdthunk.h VS-2015.

  2. Полностью раскомментируйте блок #ifdef, который определяет USE_ATL_THUNK2. Строки кода от 25 до 27.

  3. Перекомпилируйте вашу программу.

Это позволяет использовать старый механизм громкоговорителей, хорошо известный по VC-2010, VC-2013 … и это работает без сбоев для меня. Пока не задействованы другие уже скомпилированные библиотеки, которые могут каким-либо образом создавать подклассы или использовать 256 окон через ATL.

Комментарий (1 марта 2017 г.):

  • Microsoft подтвердила, что это ошибка. Это должно быть исправлено в Windows 10 RS2.
  • Mircrosoft соглашается с тем, что редактирование заголовков в atlstdthunk.h является обходным решением проблемы.

На самом деле это говорит. Пока не будет стабильного патча, я никогда не смогу снова использовать обычный ATL thunking, потому что я никогда не буду знать, какие версии Window в мире будут использовать мою программу. Потому что Windows 8 и Windows 8.1 и Windows 10 до RS2 пострадают от этой ошибки.

Заключительный комментарий (9 марта 2017 г.):

  • Постройки с VS-2017 тоже пострадали, различий между VS-2015 и VS-2017 нет
  • Microsoft решила, что в этом случае не будет исправлений для более старых ОС.
  • Ни Windows 8.1, ни Windows Server 2012 RC2, ни другие сборки Windows 10 не получат исправления для устранения этой проблемы.
  • Проблема в том, чтобы быть редким, а влияние для нашей компании — маленьким. Также решение с нашей стороны простое. Другие сообщения об этой ошибке не известны.
  • Дело закрыто.

Мой совет всем программистам: замените файл atlstdthunk.h в вашей версии Visual Studio VS-2015, VS-2017 (см. Выше). Я не понимаю Microsoft. Эта ошибка является серьезной проблемой в ATL thunking. Это может поразить каждого программиста, который использует большее количество окон и / или подклассов.

Мы знаем только об исправлении в Windows 10 RS2. Так что все старые ОС затронуты! Поэтому я рекомендую отключить использование atlthunk.dll, закомментировав определение, указанное выше.

8

Решение

Это ошибка внутри atlthunk.dll. Когда он загружается сам второй раз и дальше это происходит вручную через вызов MapViewOfFile. В этом случае не каждый адрес относительно базы модуля должным образом изменяется (когда DLL, загруженная LoadLibarary / LoadLibraryEx, вызывает системный загрузчик, делает это автоматически). Тогда если первый время DLL был загружен на предпочтительный базовый адрес все работает нормально, так как неизмененные адреса указывают на похожий код или данные. Но если нет, вы получите сбой, когда 257-е подклассовое окно обрабатывает сообщения.

Начиная с Vista у нас есть функция «рандомизации макета адресного пространства», это объясняет, почему ваш код зависает случайно. Чтобы каждый раз происходил сбой, вам необходимо обнаружить базовый адрес atlthunk.dll в вашей ОС (он отличается в разных версиях ОС) и выполнить резервирование адресного пространства одной страницы памяти по этому адресу с помощью вызова VirtualAlloc до первого подкласса. Чтобы найти базовый адрес, вы можете использовать dumpbin /headers atlthunk.dll Команда или анализировать PE заголовки вручную.

Мой тест показывает, что в Windows 10 build 14393.693 версия x32 подвержена уязвимости, а x64 — нет. На сервере 2012R2 с последними обновлениями затронуты обе версии (x32 и x64).

Кстати, код atlthunk.dll содержит примерно в 10 раз больше инструкций ЦП на вызов thunk, чем предыдущая реализация. Это может быть не очень значительным, но это замедляет обработку сообщений.

4

Другие решения

Других решений пока нет …

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector