A Safer Alternative to TerminateProcess()
Andrew Tucker
The simplest tasks can sometimes be difficult to get right for all possible situations, especially when you're dealing with software. Killing an external process on Win32 is just such a beast. Although TerminateProcess() is available, it has serious drawbacks that make it dangerous to use as a general-purpose solution. Other approaches, such as sending a specific message to the application's main window, work only in limited situations and have problems of their own. Luckily, there is an alternative that uses only documented Win32 APIs and doesn't have any serious drawbacks. This article will explain the problems with the obvious solutions mentioned, and present an implementation of a different approach that avoids these problems.
Killing Processes
You can cleanly kill a Win32 process whose source code you don't control; the typical strategy for this requires three steps. You start by using EnumWindows() and GetWindowThreadProcessId() to find all top-level windows owned by the process, and then sending them each a WM_CLOSE message. You can then call WaitForSingleObject() to wait for the process to die. If the wait times out, then the process isn't responding and you can forcibly kill it with a call to TerminateProcess(). For a more in-depth discussion of this strategy, and an implementation for Win32 and Win16, see [1].
The problem with this strategy is that any application that does not have a console control handler or a message loop will not be able to react to the WM_CLOSE and will be killed via TerminateProcess(). Other situations, such as deadlock in the message processing thread and using too small a timeout value in the call to WaitForSingleObject(), can cause TerminateProcess() to be used as well. It is generally a bad idea to call TerminateProcess() because the system does not shut down the process in an orderly fashion. Any DLLs used by the process will not receive the DLL_PROCESS_DETACH event, disk buffers are not properly flushed, and memory shared with other processes can be left in an inconsistent state. Windows CE is an exception to this rule, and I'll discuss its unique behavior towards the end of this article.
The panacea I am seeking is a technique that doesn't require sending window messages and allows the process to clean up properly during its demise. The solution is SafeTerminateProcess(), whose interface and implementation are in safetp.h (Listing 1) and safetp.c (Listing 2).
SafeTerminateProcess()
SafeTerminateProcess() takes advantage of the fact that Win32's ExitProcess() has a function signature compatible with that of a thread entry point. By "compatible," I mean that the parameters of both function prototypes are the same type, but they have different return types. This lets me launch ExitProcess() in any process, using Win32's CreateRemoteThread(), causing that process to perform an orderly shutdown. The fact that ExitProcess() has a void return type while a thread function is expected to return a DWORD is not a problem. This just means that the exit code of the remote thread will be whatever happened to be in the return value register (EAX on 80x86 processors) when ExitProcess() finishes. Since I don't use the return value, this detail doesn't matter. A similar approach is used in [2] to allocate memory in another process.
SafeTerminateProcess() has the same signature as TerminateProcess() and starts out by using DuplicateHandle() to ensure that it will have rights to create a thread in the remote process. Handles returned from CreateProcess() always have full privileges, but if the handle was obtained via OpenProcess() or some other method, it may not have the required access. If the Win32 API provided a way to query a handle's current privilege level, I could avoid duplicating it unnecessarily, but this feature is a currently only a figment of imagination.
SafeTerminateProcess() checks to make sure that the process is still running (no point in shooting a dead horse), and sets an appropriate GetLastError() value if it is not. If it is alive, I call CreateRemoteThread() with ExitProcess() as the entry point and pass SafeTerminateProcess()'s uExitCode parameter as the thread parameter. If the call fails for some reason, SafeTerminateProcess() saves the GetLastError() value so it can use it before returning.
Even though it seems redundant to use GetProcAddress() to retrieve a pointer to ExitProcess(), it is necessary. The reason is that a Win32 executable links to DLL functions via an indirect pointer called a thunk. The thunks are located in different areas of memory for each process, and the operating system loader fills them in with real addresses when the code initially starts up and before the process starts running. If you were to pass the address of ExitProcess() directly like this:
hRT = CreateRemoteThread((bDup) ? hProcessDup : hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)ExitProcess, (PVOID)uExitCode, 0, &dwTID);
you would be passing the address of the thunk in the calling process, which would be meaningless to CreateRemoteThread()'s target process and most likely cause an access violation. By explicitly grabbing the function's location with GetProcAddress() I am getting the actual location in memory instead of the address of the thunk entry. Since kernel32.dll (the module that exports ExitProcess()) is at the same location in every process, it's perfectly fine to directly use the function's address.
If the call to CreateRemoteThread() succeeded, SafeTerminateProcess() then uses WaitForSingleObject() to pause until the thread exits, ensuring that the remote process has now perished. The function then closes the handles it created. If the function is returning FALSE, I also restore the GetLastError() value I saved so that the caller can examine it to find out what went wrong.
Testing SafeTerminateProcess()
stptest.c (Listing 3), shows an example program that uses SafeTerminateProcess(). It simply creates a process that loads a DLL, pauses for a second, kills the process with SafeTerminateProcess(), and checks to see if the return values were what was expected. The code for sample.exe is in sample.c (Listing 4), and that for sampdll.dll is in sampdll.c (Listing 5).
If you run the sample under a debugger or DBWin32 [4] to view the calls to OutputDebugString() you will see that the DLL outputs code indicating that its DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, and DLL_PROCESS_DETACH events fire in that order. Changing stptest.c (Listing 3) to use TerminateProcess() instead of SafeTerminateProcess() will cause only the DLL_PROCESS_ATTACH to fire since no remote thread is created and it has the documented behavior of not firing the DLL_PROCESS_DETACH event. Using TerminateProcess() also results in the return value test for the process failing intermittently. The problem here is that TerminateProcess() has the undocumented behavior of not guaranteeing that the process has actually exited before it returns you must manually wait on the process handle or use the less efficient approach of looping until GetExitCodeProcess() doesn't return STILL_ACTIVE.
Another interesting note is that the CreateRemoteThread() call caused a DLL_THREAD_ATTACH event but no corresponding DLL_THREAD_DETACH event. This is the expected and documented behavior. The only time the DLL_THREAD_DETACH event occurs is when a thread exits while the process is still running threads terminated in normal process shutdown sequence don't fire this event.
Alas, SafeTerminateProcess() isn't perfect. Its problems, however, stem from its lack of portability and a special case situation rather than the corruption issues TerminateProcess() suffers from. SafeTerminateProcess() will work only on NT since neither Win95, Win98, or WinCE support CreateRemoteThread(). WinCE suffers from the additional problem of not supporting the ExitProcess() API function.
WinCE has some other interesting quirks relating to shutting down processes. Unlike TerminateProcess() on NT and Win9x, the WinCE version fires the DLL_PROCESS_DETACH event for all DLLs in the target process. This lets you simulate the missing ExitProcess() API function like this:
void WINAPI ExitProcess(UINT uExitCode) { TerminateProcess(GetCurrentProcess(), uExitCode); }
with the caveat that I/O buffers are not guaranteed to be flushed. Even with this drawback, it can be extremely useful if you're trying to port existing code to WinCE or maintain a set of sources that runs on all Win32 platforms.
There is a special case that turns SafeTerminateProcess() and WinCE's TerminateProcess() feature of firing the DLL_PROCESS_ATTACH event into a liability instead of a benefit. It is a fairly well-known, but officially undocumented, fact that Win32 serializes calls to DllMain() and several Win32 functions (e.g., WaitForSingleObject(), CreateProcess(), etc.) with a per-process critical section. This means that if you try to call any of these functions while inside a call to DllMain() the process will deadlock (see [3] for all the gory details). If a process is deadlocked in this manner, SafeTerminateProcess() will deadlock as well because the remote thread's call to ExitProcess() will never be able to acquire the critical section necessary to call DllMain() with DLL_PROCESS_ATTACH. The remote thread will never exit and thus SafeTerminateProcess() will never return from the WaitForSingleObject() call on the remote thread handle.
For applications that are deadlocked in this way, using the less attractive TerminateProcess() is the only option on NT and Win9x. Under CE, however, there's no way to kill a process that's deadlocked in DllMain(). It'll just remain as a ghost in the background until you reboot the device.
Next time you feel the need (or have the requirement) to hunt down and destroy other processes on a NT system, take SafeTerminateProcess() with you. Using it to replace TerminateProcess() in the strategy outlined in [1] is as good as it currently gets.
References
[1] Microsoft Knowledge Base Article #Q178893, HOWTO: Terminate an Application "Cleanly" in Win32, http://support.microsoft.com/support/kb/articles/q178/8/93.asp.
[2] Advanced Windows 3rd ed., Jeffrey Richter, Chapter 18: "Breaking Through Process Boundary Walls."
[3] Microsoft Systems Journal, "Under The Hood," Matt Pietrek, January 1996 (available on MSDN).
[4] Available at http://www.halcyon.com/ast/swdev.htm.
Andrew Tucker works on development tools for Windows CE at BSQUARE Corporation. The company, located in Bellevue, Washington, specializes in Windows CE and offers consulting services, integration products and services, and software applications for PC Companions. Andrew is also pursuing a masters degree in Computer Science at the University of Washington. He can be reached at [email protected].
Get Source Code