The supported design is to prevent breakaway and unauthorized job-handle distribution, retain a controller job handle with query and termination rights, terminate through that handle, and establish closure by polling ActiveProcesses == 0; however, public Win32 documentation does not specify the complete PROC_THREAD_ATTRIBUTE_JOB_LIST rights/rollback contract or resolve the contradictory TerminateJobObject process-handle wording.
Creation-time assignment
PROC_THREAD_ATTRIBUTE_JOB_LIST is supported on Windows 10/Windows Server 2016 and later. It accepts an ordered array of job handles, but its documentation does not state:
- The required access mask on each job handle.
- Any additional process-handle rights or privileges.
- Whether assignment is transactional across multiple jobs.
- Rollback behavior if assignment fails partway through.
- What happens if the creator terminates while
CreateProcessWis in progress.
Therefore, those properties should not be treated as supported guarantees. The attribute value must remain valid until the attribute list is deleted, and CreateProcessW requires STARTUPINFOEX plus EXTENDED_STARTUPINFO_PRESENT.^1^ ^2^
Nested jobs require Windows 8/Windows Server 2012 or later. A valid hierarchy requires each child job to contain a subset of its parent’s processes; explicit assignments must proceed root-to-leaf. Nested relationships cannot be formed where the relevant jobs have UI limits. Under Terminal Services, assigned processes must be in the same session as the job.^3^ ^4^
Conservative fallback: suspended creation and explicit assignment
The documented explicit-assignment rights are:
- Job handle:
JOB_OBJECT_ASSIGN_PROCESS. - Process handle:
PROCESS_SET_QUOTA | PROCESS_TERMINATE.^4^
Use CREATE_SUSPENDED, assign root-to-leaf, and resume only after every assignment succeeds. This avoids intentionally resuming an unassigned initial thread, but the documentation supplied does not elevate suspension into an atomic creator-death or rollback guarantee.
Required failure handling:
- Zero-initialize
PROCESS_INFORMATION. - If
CreateProcessWfails, saveGetLastError; do not use its handles. - If any assignment fails, save the error, terminate the still-suspended process, wait for its process handle, and close both returned handles.
- If
ResumeThreadreturns(DWORD)-1, save the error, terminate and wait for the process, then close both handles. - Check
TerminateProcessand wait results; handleWAIT_FAILEDwithGetLastError.
Assignment itself can terminate the process when a job’s exhausted time limit or active-process limit prevents admission.^4^
TerminateJobObject authorization
The unambiguous contract is that hJob requires JOB_OBJECT_TERMINATE. Terminating a nested job terminates processes in that job and all child jobs.^5^ ^3^
The same page also says that “the handle for each process” must have PROCESS_TERMINATE, although no process handles are parameters. Public documentation does not explain whose handles these are or how the caller could supply them for descendants. Consequently:
- Do not claim that caller-retained per-process handles are required.
- Do not claim that
JOB_OBJECT_TERMINATEconclusively overrides that sentence. - Treat this as a documentation/API-contract ambiguity requiring Microsoft confirmation.
By comparison, direct TerminateProcess requires PROCESS_TERMINATE, while waiting on a process requires SYNCHRONIZE.^6^
Establishing complete in-scope closure
After TerminateJobObject succeeds:
- Repeatedly query
JobObjectBasicAccountingInformationusing a job handle withJOB_OBJECT_QUERY. - Continue until
ActiveProcesses == 0. - Treat that as closure only for processes actually associated with the queried job hierarchy.^7^ ^8^
It does not prove closure of processes that broke away, were never admitted, or escaped through an uncontrolled creation path.
JobObjectBasicProcessIdList can enumerate current members, including child-job members, but PIDs are not stable identities. If complete handles can reliably be captured, retain process handles with SYNCHRONIZE and wait for them; process handles become signaled at termination and remain valid until closed.^9^ ^10^
Those waits are supplemental: completion-port process notifications are generally not guaranteed, and notification PIDs may already have exited or been reused unless an open process handle was retained.^11^
Preventing admission, escape, and owner-loss gaps
- Set neither
JOB_OBJECT_LIMIT_BREAKAWAY_OKnorJOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK. Otherwise, some descendants can leave the monitored hierarchy.^12^ - Do not give untrusted code job handles carrying
JOB_OBJECT_ASSIGN_PROCESS,JOB_OBJECT_TERMINATE, or other sensitive rights through inheritance, brokers capable ofDuplicateHandle, or IPC handle transfer. - Serialize all controller-authorized admissions against shutdown. The public contract says assignment fails if the job or a parent job is terminating, but does not document a general permanent “sealed after termination” state.^4^
-
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEacts only when the last job handle closes. A sole controller handle therefore supports controller-crash cleanup. If any inherited, duplicated, broker-held, or independently retained handle remains open, controller loss will not trigger it. An independent supervisor is needed only when another owner must survive the controller or when other handles may exist.^12^
If the design depends on atomic JOB_LIST rollback, creator-death behavior, or a definitive interpretation of the TerminateJobObject PROCESS_TERMINATE sentence, open an official Microsoft developer support case for Windows/Win32 API contract clarification. Also submit documentation feedback on both relevant Learn pages so the missing and contradictory contracts can be corrected.
References
- UpdateProcThreadAttribute function (processthreadsapi.h)
- CreateProcessW function (processthreadsapi.h)
- Nested Jobs
- AssignProcessToJobObject function (jobapi2.h)
- TerminateJobObject function (jobapi2.h)
- Process Security and Access Rights
- QueryInformationJobObject function (jobapi2.h)
- JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h)
- JOBOBJECT_BASIC_PROCESS_ID_LIST structure (winnt.h)
- Process Handles and Identifiers
- JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h)
- Job Objects