Иногда люди воспринимают вещи слишком буквально
Nov 24, 2008 · CommentsПрограммированиеWow64
На днях свалился в inbox вопрос:
The documentation for QueueUserApc states “an APC cannot be queued from a 64-bit process to a 32-bit process or vice versa.”
However on Vista Ultimate x64, we _can _queue a user APC from a 32-bit process to a 64-bit process. In our test application the 64-bit recipient crashes, we haven’t explored why just yet, but the point is that the operation should not succeed and the 64-bit application’s APC queue should never have the APC inserted.
Is it an OS bug?
Документация к функции QueueUserApc утверждает, что асинхронный вызов процедуры (APC) не может быть запрошен из 64-х битного процесса для выполнения в 32-х битном и наоборот.
Однако на Vista Ultimate x64 мы можем выполнить асинхронный вызов пользовательского режима из 32-х битного процесса в 64-х битном процессе. В нашем тестовом приложении 64-х битный получатель падает и мы еще не выяснили почему, но дело в том, что операция (QueueUserApc) не должна завершаться успешно и асинхронный вызов не должен вставляться в очередь вызовов 64-х разрядного процесса.
Это баг ОС?
Вопрос примечателен тем, что это уже третье письмо на эту тему за последние два месяца, хотя функция QueueUserApc далеко не самая широко используемая функция Win32. Не иначе какой настойчивый клиент эту функцию мучает. :-)
Для начала разберемся, что такое асинхронные вызовы процедур (APC). Те кто знает могут смело пропустить этот абзац. Windows NT поддерживает специальное состояние заблокированного потока - “alertable wait”. Чтобы переключиться в это состояние поток может использовать “Ex” версии “WaitFor” функций или функции асинхронного ввода-ввода. Это состояние примечательно тем, что поток может быть в любой момент разбужен, при этом управление будет передано по произвольному (заранее согласованному) адресу. Эта возможность используется ядром для уведомления приложения о завершении операции ввода-вывода. К примеру, если в NtWriteFile передан не нулевой ApcRoutine, то по завершении операции ядро вызовет ApcRoutine именно с помощью механизма асинхронных вызовов.
Функция QueueUserApc позволяет воспользоваться этим механизмом в своих корыстных целях. Эта функция ставит запрос на выполнение APC процедуры в определённом потоке. Процедура будет вызвана (возможно, вместе с другими процедурами) в контексте целевого потока, как только тот переключиться в состояние alertable wait. Небольшая, по важная тонкость заключается в том, что адрес процедуры может быть любым. Функция никак не гарантирует его правильность. Если адрес процедуры неверен, то целевой поток, скорее всего, упадет с кодом access violation. При этом поток, вызвавший QueueUserApc, продолжит работу, как ни в чем не бывало.
Далее, поскольку 32-х и 64-х битные процессы используют разную ширину адреса, то с вызовом 64-х разрядных процедур из 32-х битного кода возникают вполне понятные сложности. 32-х битный код просто не имеет возможности передать 64-х битный адрес в QueueUserApc. Передать 32-х адрес возможно, но возникают другие сложности. Например, как отличить адрес 32-х битного кода от адреса 64-х битного также расположенного ниже границы 4GB.
Полезная ссылка для тех, кто интересуется деталями: http://www.nynaeve.net/?p=202
Вернемся теперь к оригинальному вопросу. Формально, тот факт, что QueueUserApc позволяет поставить вызов в очередь потока с другой разрядностью, можно назвать ошибкой. Хотя на самом деле это скорее ошибка в документации, которая должна была бы утверждать, что " что асинхронный вызов процедуры (APC) не следует запрашивать из 64-х битного процесса для выполнения в 32-х битном и наоборот." Небольшая разница, но какой эффект.
Дело в том, что QueueUserApc просто не имеет возможности проверить разрядность целевого потока во всех случаях. Чтобы это было возможно процесс целевого потока должен быть доступен с уровнем доступа PROCESS_QUERY_INFORMATION или PROCESS_QUERY_LIMITED_INFORMATION. Проверить соответствие разрядности можно в ядре, но там мешает тот факт, что система как раз должна уметь вызывать 32-х битные APC из 64-х разрядного кода. В результате, разработчики Wow64 решили, что овчинка не стоит выделки, и такая проверка не была реализована. Текущая реализация этой функции такова, что вызов процедуры в потоке другой разрядного практически гарантированно приводит к падению целевого потока.
Позвольте, скажете вы, но как же так? Получается, что 32-х разрядно приложение может запросто нарушить работу 64-х разрядных приложений? Это же дыра? На самом деле эта дыра не чуть не больше чем в случае, когда разрядности потоков совпадают. Как я уже говорил, вызывающий поток может передать любой в том числе не корректный адрес, что немедленно приводит к краху целевого потока. Вызывающий поток должен иметь уровень доступа THREAD_SET_CONTEXT к целевому потоку, что означает, что вызывающая сторона и так имеет полный контроль над целевом потоком.