Про то, как отцепиться и ничего не порушить

Главная проблема с хуками состоит не в том, как зацепиться, а в том, как отцепиться и ничего не порушить. К примеру, приходит недавно письмо:

We recently saw an AV in stress where our vectored exception handler was called after our dll was unloaded. After investigating the issue, it seems like removing the vectored exception handler does not wait for all users of that exception handler to finish (and does not even remove the exception handler from list for future users if there is one current user). So, there seems to be no way to synchronize removing the exception handler and the dll unloading – any synchronization within the exception handler is useless since the exception thread may be about to call the exception handler.

Недавно мы наблюдали падение приложения во время стрессового тестирования, вызванное тем, что векторный обработчик исключения был вызван после того, как наша DLL была выгружена из памяти. В процессе расследования выяснилось, что, похоже, снятие векторного обработчика исключения не ожидает, пока все пользователи этого обработчика закончат работу (и даже не удаляет обработчик для будущих пользователей, если обработчик в данный момент используется). Так, похоже, что не существует способа синхронизировать снятие векторного обработчика и выгрузку DLL – любая синхронизация в пределах обработчика исключения бесполезна, поскольку поток, в котором произошло исключение, может только готовится вызвать обработчик исключения.

Действительно, векторные обработчики исключений представляют собой не что иное, как функцию (или несколько функций), которая вызываются диспетчером исключений до или после SEH части обработки исключения. Диспетчер поддерживает список указателей на обработчики, регистрируемые с помощью функций AddVectoredExceptionHandler и AddVectoredContinueHandler. Диспетчер обеспечивает минимальную целостность списка на случай, если тот читается или обновляется из нескольких потоков одновременно, и только. Даже если список был обновлен в процессе обработки исключения, приложение должно обработать эту ситуацию корректно.

Можно представить себе ситуацию, когда диспетчер заботится о транзакционной целостности списка обработчиков. Каждому исключению будет предъявлена та копия списка, которая существовала на момент начала обработки, а функции снятия обработчиков ждать завершение обработки всех исключений, «увидевших» данный обработчик. Такой дизайн имеет как минимум две серьезные проблемы:

  1. Исключение, по определению, происходит в исключительной ситуации. В этот момент, например, нельзя выделить память, так как свободной памяти в системе может и не быть. Получается, что отдельную копию списка обработчиков хранить негде. Нет, можно конечно извернуться с версионностью указателей, но это уже какой-то Oracle получается. :-)

  2. Синхронизация доступа к списку чревата блокировками, особенно в случае вложенных исключений.

Что характерно, похожим проблемам подвержены и другие разновидности хуков, так или иначе внедряющиеся в код, который они не контролируют. Скажем, популярный ныне сплайсинг (AKA splicing), когда точка входа в функцию заменяется на вызов перехватчика, а подмененные инструкции копируются в отдельный буфер, дабы сохранить возможность продолжать выполнение оригинальной функции. Корректно снять такой перехватчик – та еще задача, поскольку не известно, сколько потоков выполняют код перехватчика в данный момент.

А вот кто придумает решение для оригинальной проблемы с выгрузкой векторного обработчика?

comments powered by Disqus