So what’s the case you might ask? Well as I wrote my article about how to write a Winlogon Notification Packages, I often noticed in the C examples I found on the net, that those examples made use of SafeTerminateProcess so I wondered what this call stands for and why they used it.
After some research I found an article which was from Dr Dobbs written somewhere around 1999 which explains the difficulties around TerminateProcess()
Just to mention a few of them:
- 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().
- 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.
So what we can do now? Simple make a call to SafeTerminateProcess instead!
safetp.h — Declaration for SafeTerminateProcess()
/* Header file for SafeTerminateProcess */ #ifndef _SAFETP_H__ #define _SAFETP_H__ BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode); #endif /* End of File */
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 80×86 processors) when ExitProcess() finishes.
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.
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.
/*
Safely terminate a process by creating a remote thread
in the process that calls ExitProcess
*/
#define STRICT
#include
BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode)
{
DWORD dwTID, dwCode, dwErr = 0;
HANDLE hProcessDup = INVALID_HANDLE_VALUE;
HANDLE hRT = NULL;
HINSTANCE hKernel = GetModuleHandle("Kernel32");
BOOL bSuccess = FALSE;
BOOL bDup = DuplicateHandle(GetCurrentProcess(),
hProcess,
GetCurrentProcess(),
&hProcessDup,
PROCESS_ALL_ACCESS,
FALSE,
0);
// Detect the special case where the process is
// already dead...
if ( GetExitCodeProcess((bDup) ? hProcessDup : hProcess, &dwCode) &&
(dwCode == STILL_ACTIVE) )
{
FARPROC pfnExitProc;
pfnExitProc = GetProcAddress(hKernel, "ExitProcess");
hRT = CreateRemoteThread((bDup) ? hProcessDup : hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)pfnExitProc,
(PVOID)uExitCode, 0, &dwTID);
if ( hRT == NULL )
dwErr = GetLastError();
}
else
{
dwErr = ERROR_PROCESS_ABORTED;
}
if ( hRT )
{
// Must wait process to terminate to
// guarantee that it has exited...
WaitForSingleObject((bDup) ? hProcessDup : hProcess,
INFINITE);
CloseHandle(hRT);
bSuccess = TRUE;
}
if ( bDup )
CloseHandle(hProcessDup);
if ( !bSuccess )
SetLastError(dwErr);
return bSuccess;
}
/* End of File */
So simple lets translate the above code to Delphi and make a small program to check if our translation works.
unit Main;
interface
uses
Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
edtProcHandle: TEdit;
edtExitResult: TEdit;
btnTerminate: TButton;
btnRunProcess: TButton;
procedure btnTerminateClick(Sender: TObject);
procedure btnRunProcessClick(Sender: TObject);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
RemoteProcHandle: cardinal;
function SafeTerminateProcess(const hProcess: cardinal;
uExitCode: cardinal): boolean;
procedure StartApp(const App, Parameters, CurDir: string);
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
uses
JwaWindows,
JWsclToken,
JwsclSid,
JwsclKnownSid,
JwsclTypes,
JwsclAcl,
JwsclConstants,
JwsclLsa,
JwsclExceptions,
JwsclStrings;
// Get remote process handle from process id
function GetProcessHandleFromID(ID: DWORD): THandle;
begin
Result := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_QUERY_INFORMATION or
PROCESS_VM_OPERATION or PROCESS_VM_WRITE or PROCESS_VM_READ, False, ID);
end;
// creates a process and waits for input idle
procedure TMainForm.StartApp(const App, Parameters, CurDir: string);
var
StartupInfo: TStartupInfo;
ProcInfo: TProcessInformation;
pEnv: Pointer;
pCurDir, pCmdLine: PChar;
begin
ZeroMemory(@StartupInfo, sizeof(StartupInfo));
StartupInfo.cb := SizeOf(StartupInfo);
StartupInfo.lpDesktop := 'winsta0\default';
CreateEnvironmentBlock(@pEnv, 0, True);
try
if Length(Parameters) > 0 then
pCmdLine := PChar('"' + App + '" ' + Parameters)
else
pCmdLine := PChar('"' + App + '" ');
pCurDir := nil;
if Length(CurDir) > 0 then
pCurDir := PChar(CurDir);
if not CreateProcess(PChar(App), pCmdLine, nil, nil, True,
CREATE_NEW_CONSOLE or CREATE_UNICODE_ENVIRONMENT, pEnv, pCurDir,
StartupInfo, ProcInfo) then
raiseLastOsError;
finally
DestroyEnvironmentBlock(pEnv);
end;
// remote process handle
RemoteProcHandle := GetProcessHandleFromID(ProcInfo.dwProcessId);
//wait for 60 secs
//WaitForSingleObject(ProcInfo.hProcess, 60 * 1000);
//wait until process can receive user input
WaitForInputIdle(ProcInfo.hProcess, 60 * 1000);
CloseHandle(ProcInfo.hProcess);
CloseHandle(ProcInfo.hThread);
end;
// SafeTerminateProcess implementation
function TMainForm.SafeTerminateProcess(const hProcess: cardinal;
uExitCode: cardinal): boolean;
var
dwTID, dwCode, dwErr: DWORD;
hProcessDup: cardinal;
bDup: BOOL;
hrt: cardinal;
hKernel: HMODULE;
bSuccess: BOOL;
FARPROC: Pointer;
begin
dwTID := 0;
dwCode := 0;
dwErr := 0;
hProcessDup := INVALID_HANDLE_VALUE;
hrt := NULL;
bSuccess := False;
if (Win32Platform = VER_PLATFORM_WIN32_NT) then
begin
bDup := DuplicateHandle(GetCurrentProcess(), hProcess,
GetCurrentProcess(), @hProcessDup, PROCESS_ALL_ACCESS, False, 0);
// Detect the special case where the process is
// already dead...
if (GetExitCodeProcess(hProcessDup, dwCode)) then
begin
hKernel := GetModuleHandle('Kernel32');
FARPROC := GetProcAddress(hKernel, 'ExitProcess');
hRT := CreateRemoteThread(hProcessDup, nil, 0, Pointer(FARPROC),
@uExitCode, 0, @dwTID);
if (hRT = NULL) then
dwErr := GetLastError()
else
dwErr := ERROR_PROCESS_ABORTED;
if (hrt <> Null) then
WaitForSingleObject(hProcessDup,
INFINITE);
CloseHandle(hRT);
bSuccess := True;
if (bDup) then
CloseHandle(hProcessDup);
if not (bSuccess) then
SetLastError(dwErr);
Result := bSuccess;
end;
end;
end;
procedure TMainForm.btnRunProcessClick(Sender: TObject);
begin
StartApp('c:\windows\system32\calc.exe', '', '');
edtProcHandle.Text := IntToStr(RemoteProcHandle);
end;
procedure TMainForm.btnTerminateClick(Sender: TObject);
begin
JwEnablePrivilege(SE_DEBUG_NAME, pst_Enable);
case SafeTerminateProcess(RemoteProcHandle, 0) of
True: edtExitResult.Text := 'Safely terminated!';
False: edtExitResult.Text := 'Something went wrong';
end;
end;
end.
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: 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. 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.
Also, 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.
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.
Download
[download id="6" Format="4"]
References
[1] Microsoft Knowledge Base Article #Q178893, HOWTO: Terminate an Application “Cleanly” in Win32,
[2] A Safer Alternative to TerminateProcess()

