Not a kernel guy

… in the Windows kernel team

Sunday, July 15, 2007

Опасная функция ExitProcess.

Как вы думаете, какая их двух функций опаснее: ExitProcess или TerminateProcess? Ответ, конечно, зависит от определения того, что считать более безопасным. Однако если задать этот вопрос нескольким людям, большинство автоматически укажет на TerminateProcess. Почему? Потому что TerminateProcess в отличие от ExitProcess не делает попыток освободить занятые ресурсы. К примеру, программа не сможет записать несохранённые данные на диск, тем самым нарушив их целостность.

В реальности функция ExitProcess ни чем не безопаснее, чем TerminateProcess, а может и ещё хуже, так как внушает чувство ложной безопасности. Рассмотрим алгоритм работы ExitProcess. Помимо закрытия всех открытых описателей (handles) объектов и установки состояния объектов завершаемого процесса и принадлежащих ему потоков в состояние «signaled», ExitProcess делает две важные вещи:

  • Завершает все потоки процесса, кроме потока, вызвавшего ExitProcess;
  • Оповещает все загруженные DLL, вызывая DllMain c кодом DLL_PROCESS_DETACH.

В случае если один из потоков выполнял код, защищённый критической секцией, в момент своего завершения, то критическая секция может остаться захваченной навсегда. Если один из обработчиков DLL_PROCESS_DETACH попытается захватить эту секцию, то он будет заблокирован на ней навсегда. (Это пример из статьи в MSDN, посвященной ExitProcess).

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

И наконец в общем случае вы не можете быть уверены какие DLL загружены в ваш процесс, так как:

  1. Набор библиотек может отличаться на разных версиях операционной системы;
  2. Набор библиотек может отличаться в зависимости он версий сторонних библиотек, которые использует ваше приложение;
  3. Другие приложения могут внедрять свой код в ваш процесс, например с помощью SetWindowsHookEx или CreateRemoteThread. В этом случае, кстати, вероятность того, что внедряемая DLL синхронизирует доступ к глобальным данным с помощью критических секций стремится к 100%.

Т.е. это означает, что вызывая ExitProcess, вы выполняете заранее неизвестный код, обладающий неизвестным поведением.

Единственный способ сделать вызов ExitProcess безопасным – подготовиться к вызову этой функции должным образом: корректно завершить все потоки и вызвать ExitProcess из единственного оставшегося потока. Если в процессе подготовки что-то пошло не так, то имеет смысл вызвать TerminateProcess вместо ExitProcess, так как хуже все равно не будет – состояние программы уже не определено. Следует также помнить, что CRT функции exit, _exit и прочие, а также выход из функции main в конце концов приводят в вызову ExitProcess. Т.е. они подвержены тем же самым проблемам, что и ExitProcess.

Posted at 10:56 pm •

RSS feed | Trackback URI

12 Comments »

Trackback by Зеркало: Not a kernel guy — July 15, 2007 @ 10:59 pm

Опасная функция ExitProcess….

Как вы думаете, какая их двух функций безопаснее: ExitProcess или TerminateProce…

 
Comment by zhmur — July 16, 2007 @ 12:46 am

Не догнал что имеется ввиду под критической секцией. Если EnterCriticalSection, то это user mode синхропримитив и должен корректно отваливаться при завершении процесса. Или имеется ввиду CreateMutex? Тогда это кернел косяк однако, обработать синхропримитивы зависшие на умерших процессах не должно быть так сложно.

 
Comment by AlexWWolf — July 16, 2007 @ 1:55 am

Я вот только одного не понимаю: а как тогда не рушатся приложения, которые завершили поток функции main, но у которых продолжают работать порожденные потоки? Ситуация, возможно, не очень типичная, но я вроде никаких глюков в таких случаях не встречал….

 
Comment by Vladimir Scherbina — July 16, 2007 @ 3:33 am

Идеально конечно же через PostThreadMessage нотифицировать потокам о завершении, ждать пока хендлы потоков засигналятся и только тогда завершать процесс.

 
Comment by Not a kernel guy — July 16, 2007 @ 8:16 am

 

Если EnterCriticalSection, то это user mode синхропримитив и должен корректно отваливаться при завершении процесса. Или имеется ввиду CreateMutex?

Я имею ввиду CriticalSection и все остальные примитивы кроме Mutex.

void access_global_data()
{
    // 1. thread_a захватывает секцию cs
    // 4. thread_b пытается захватить секцию cs, которую никогда
    //    не освободит thread_a
    EnterCriticalSection(&cs);

    // thread_b вызывает ExitProcess
    ...
    LeaveCriticalSection(&cs);
}

BOOL WINAPI DllMain(
    HINSTANCE instance,
    DWORD reason,
    LPVOID reserved
    )
{
    switch (reason)
    {
    case DLL_PROCESS_DETACH:
        // 3. ExitProcess уведомляет о завершении процесса каждую из
        //    загруженных библиотек DllMain(DLL_PROCESS_DETACH)
        access_global_data();
        break;
    }

    return TRUE;
}

void thread_a()
{
    access_global_data();
}

void thread_b()
{
    // 2. thread_b пытается завершить процесс. ExitProcess убивает thread_a.
    ExitProcess(0);
}
 
Comment by Not a kernel guy — July 16, 2007 @ 8:37 am

 

Я вот только одного не понимаю: а как тогда не рушатся приложения, которые завершили поток функции main, но у которых продолжают работать порожденные потоки? Ситуация, возможно, не очень типичная, но я вроде никаких глюков в таких случаях не встречал….

Они, видимо, не пользуются стандартной C runtime library. Или завершают основной поток минуя runtime library.

 
Comment by Vladimir Scherbina — July 16, 2007 @ 8:55 am

>>Они, видимо, не пользуются стандартной C runtime library. Или завершают основной поток минуя runtime library.

 
Comment by max404 — July 16, 2007 @ 10:19 am

Может я чего-то не понимаю, но мне кажется в первом предложении ошибка, должно быть “какая из 2-х функций опаснее”…

 
Comment by Not a kernel guy — July 16, 2007 @ 10:31 am

 

Может я чего-то не понимаю, но мне кажется в первом предложении ошибка, должно быть “какая из 2-х функций опаснее”…

Точно. Поправил.

 
Comment by Not a kernel guy — July 16, 2007 @ 3:40 pm

 

Если EnterCriticalSection, то это user mode синхропримитив и должен корректно отваливаться при завершении процесса. Или имеется ввиду CreateMutex?

Я имею ввиду CriticalSection и все остальные примитивы кроме Mutex.

Хе-хе. Всё оказалось даже интереснее. Специально для этого случая в EnterCriticalSection добавили проверку. Расскажу об этом подробнее отдельным постом.

 
Comment by Konstantin — July 17, 2007 @ 9:49 am

http://blogs.msdn.com/oldnewthing/archive/2007/05/03/2383346.aspx

Тут вроде товарищ Чен написал, что после вызова ExitProcess все вызовы EnterCriticalSection срабатывают.

 

[...] История про ExitProcess получила несколько неожиданное продолжение. Оказалось, что для того, чтобы избежать блокировки на критической секции, захваченной другим потоком во время ExitProcess, в функцию EnterCriticalSection был добавлен код, обрабатывающий эту ситуацию. Начиная с Windows XP EnterCriticalSection проверяет захвачена ли секция и, в случае если захвачена, сверяет идентификатор текущего потока с идентификатором захватчика. А вот дальнейшее поведение зависит от версии операционной системы. [...]

 

Your Comment (smaller | larger)

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Powered by WordPress