Destroying all child processes (and grandchildren) when the parent exits


Today's Little Program launches a child process and then just hangs around. If you terminate the parent process, then all the children (and grandchildren and great-grandchildren, you get the idea) are also terminated.

The tool for this is the Job Object. Specifically, we mark the job as "kill on job close" which causes all processes in the job to be terminated when the last handle to the job is closed.

We must therefore be careful not to allow this handle to be inherited, because that would create another handle that needs to be closed before the job is terminated. And of course we need to be careful not to close the handle unless we really do want to terminate the job.

#define STRICT
#include <windows.h>

BOOL CreateProcessInJob(
    HANDLE hJob,
    LPCTSTR lpApplicationName,
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCTSTR lpCurrentDirectory,
    LPSTARTUPINFO lpStartupInfo,
    LPPROCESS_INFORMATION ppi)
{
    BOOL fRc = CreateProcess(
        lpApplicationName,
        lpCommandLine,
        lpProcessAttributes,
        lpThreadAttributes,
        bInheritHandles,
        dwCreationFlags | CREATE_SUSPENDED,
        lpEnvironment,
        lpCurrentDirectory,
        lpStartupInfo,
        ppi);
    if (fRc) {
        fRc = AssignProcessToJobObject(hJob, ppi->hProcess);
        if (fRc && !(dwCreationFlags & CREATE_SUSPENDED)) {
            fRc = ResumeThread(ppi->hThread) != (DWORD)-1;
        }
        if (!fRc) {
            TerminateProcess(ppi->hProcess, 0);
            CloseHandle(ppi->hProcess);
            CloseHandle(ppi->hThread);
            ppi->hProcess = ppi->hThread = nullptr;
        }
    }
    return fRc;
}

The Create­Process­In­Job function simply creates a process suspended, adds it to a job, and then resumes the process if the original caller asked for a running process.

Let's take it for a spin.

int __cdecl main(int, char **)
{
 HANDLE hJob = CreateJobObject(nullptr, nullptr);

 JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = { };
 info.BasicLimitInformation.LimitFlags =
                    JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
 SetInformationJobObject(hJob,
       JobObjectExtendedLimitInformation,
       &info, sizeof(info));
 STARTUPINFO si = { sizeof(si) };
 PROCESS_INFORMATION pi;
 char szCommandLine[] = "taskmgr.exe";
 if (CreateProcessInJob(hJob,
     "C:\\Windows\\System32\\taskmgr.exe",
     szCommandLine,
     nullptr, nullptr, FALSE, 0,
     nullptr, nullptr, &si, &pi)) {
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
 }
 Sleep(30 * 1000);
 CloseHandle(hJob);
 return 0;
}

After creating the job object, we set the "kill children on close" flag on the job. Then we launch Task Manager into the job and give you 30 seconds to do your business. After that, it's "Time's up, you lose!" and Task Manager and any processes you launched from Task Manager will go away.

Comments (15)
  1. skSdnW says:

    The problem with job objects is when a parent process outside your control (Add/Remove programs etc) has already put you in a job. One way out of this is to spawn a copy of yourself detached from the job (Assuming the job is OK with detaching). Nested job support came too late and the implementation is not optimal…

  2. Eugene says:

    1) What if parent process is terminated after it called CreateProcess, but before AssignProcessToJobObject?

    2) What if parent process was put into a job (e.g. by UAC, run as, debugger etc)?

  3. Brian_EE says:

    I know it's "just a Little Program", but hard coding the path to taskmanager instead of using SHGetSpecialFolder(,,CSIDL_SYSTEM,) seems to go against the grain of what you often complain about in your blog…

    [Had-coded paths is one of the explicit things that Little Programs are allowed to do because of their nature. The point is to let the idea shine through. -Raymond]
  4. skSdnW says:

    @Eugene 1) Then you end up with a never got started half-initialized process that sticks around until logoff. If the design/job usage allows for it then you can add yourself to the job before spawning children…

  5. Joshua says:

    The fact that #2 is so common in Windows now makes use of jobs an iffy idea to use. Nevermind the fact that that the use of jobs in these places are to solve the problems about children that outlive their parents in the first place. There really needs to be a way to wait for the entire process tree to terminate that can be written recursively (that is, starting such a wait can be done while somebody is waiting for you), and this backported as far back as Windows 7 (Windows XP didn't use jobs all over the place so the application can use them at will).

  6. Joker_vD says:

    "2) What if parent process was put into a job (e.g. by UAC, run as, debugger etc)?"

    Then there will be nested jobs. Duh. Really, Microsoft engineers are not *that* stupid and/or ignorant.

    But yeah, this method is as straight-forward as performing tonsillectomy through the anus, honestly.

  7. q says:

    @Joker_vD

    By your inference they *were* "that stupid and/or ignorant", because nested jobs were introduced only in Windows 8.

    MSDN:

    Windows 7, Windows Server 2008 R2, Windows XP with SP3, Windows Server 2008, Windows Vista, and Windows Server 2003:  The process must not already be assigned to a job; if it is, the function fails with ERROR_ACCESS_DENIED. This behavior changed starting in Windows 8 and Windows Server 2012.

    Try to know what you are talking about before snarking.

    And this method would have been just fine, only if not for this little issue of nested jobs.

  8. So, basically, this article is suggesting a method that is not guaranteed to work on versions of Windows that matter? Sad…

    [But it most likely works well enough down to Windows 2000. Or do you need to support NT 4.0 too? -Raymond]
  9. Aqua says:

    Another vote for the 'doesn't actually work' camp.

    I wanted to use this a while ago but as others have pointed out, Program Compatibility Assistant, Terminal Services, etc. all use job objects under the hood, resulting in rather poor compatibility. It seems to also depend on whether a program is started from a CMD prompt or by Explorer (presumably in one or the other case the parent process is already attached to a job).

    This was on Windows XP SP3 and Windows Server 2003 R2, mind. I can only imagine that problems are even worse on NT 6.. :-(

  10. Joshua says:

    [But it most likely works well enough down to Windows 2000. Or do you need to support NT 4.0 too? -Raymond]

    Works on 2000. Works on XP. Breaks on Vista. Breaks on 7. Works on 8. Works on 8.1.

    Probably what's getting to people is breaks on 7. This is due to high likelihood of the current process being in a job sooner or later but lack of nested jobs.

    [You can probably salvage it on Vista and Windows 7 by asking that the child process break away from the parent process's job. -Raymond]
  11. Joshua says:

    [You can probably salvage it on Vista and Windows 7 by asking that the child process break away from the parent process's job. -Raymond]

    As skSdnW pointed out, you need to add yourself to the job to avoid a race condition.

  12. Gabe says:

    Aqua: On my Win7 box, the only job objects are ones used for this purpose: low-integrity IE or Word processes that will exit as soon as their parent is terminated. I just made a remote desktop session to a Server 2008R2 machine, and it has no job objects. In my experience this is a perfectly cromulent technique.

  13. Myria says:

    Another problem with the job implementation is that there is no way to atomically create a child process and put it into a job.  There is always some window between CreateProcessW and AssignProcessToJobObject within which if you are terminated, you will have an unterminated suspended zombie child.

    A future version of Windows ought to let you put a job object handle into a process attribute list for STARTUPINFOEXW.

    Regarding the note with SHGetSpecialFolderW…  The biggest problem with SHGetSpecialFolderW are the out-of-memory errors you get for your 32-bit users because of the truly massive shell32.dll you have to load in order to call SHGetSpecialFolderW.  With the type of application I write, the 12 MB of address space consumed by shell32.dll is severely expensive for one API call.  It's the last one in shell32.dll we're trying to get rid of, somehow…

  14. Joshua says:

    @Myria: That one's a good candidate for invoking out-of-process.

  15. Bruno says:

    A child process can escape the job even when normal break away is forbidden by creating a grandchildren process that's actually the child of explorer.exe or another third party process.

Comments are closed.

Skip to main content