If you look around, you often see people take a lock around their calls to the CreateÂProcess
function. Why do they do that? Isn’t the CreateÂProcess
function thread safe?
Yes, the CreateÂProcess
function is thread safe. But thinking about thread safety is the right thing.
The issue is with inheritable handles.
When you ask for handles to be inherited, the CreateÂProcess
inherits all the handles in the process that are marked as inheritable. If you have multiple threads creating processes, you run into trouble if each thread wants a different set of handles to be inherited. The two threads each create their respective inheritable handles, and as a result, the handles get inherited into both processes.
Prior to Windows Vista, the standard workaround was to use a mutex so that only one thread at a time can go through the steps of
- Creating inheritable handles.
- Calling
CreateÂProcess
withbInheritHandles = true
. - Closing the inheritable handles created in step 1.
Windows Vista introduced the PROC_
, which I discussed some time ago. This addresses the concurrency problem by allowing each call to THREAD_
ATTRIBUTE_
LIST
CreateÂProcess
to specify a custom list of handles to be inherited. That way, you can have two threads calling CreateÂProcess
at the same time without interfering with each other’s inherited handles. You don’t need a mutex any more.
There’s still a problem with this, though: It requires everybody to be playing the same game.
In order for a handle to be inherited, you not only have to put it in the PROC_
, but you also must make the handle inheritable. This means that if another thread is not on board with the THREAD_
ATTRIBUTE_
LIST
PROC_
trick and does a straight THREAD_
ATTRIBUTE_
LIST
CreateÂProcess
with bInheritHandles = true
, it will inadvertently inherit your handles.
A colleague of mine came up with a sneaky trick for addressing this new problem: Create a dummy parent process and put the inheritable handles in there.
Here’s the basic idea:
Preparation
- Create a process suspended. This process will never run. It is just a container for handles.
Call this process the “helper” process. This process will end up helping us, despite the process not actually doing anything!
Process creation: Do this each time you need to create a process with specific inherited handles.
- Create all the handles as non-inheritable. This ensures they don’t accidentally get inherited if another thread (not written by you) calls
CreateÂProcess
. - Use
DuplicateÂHandle
to duplicate the handles you want to inherit into the helper process, withbInheritHandles = true
. - Add those handles to a
PROC_
.THREAD_
ATTRIBUTE_
LIST
- Add the handle of the helper process as a
PROC_
, so that it acts as the nominal parent process. Specifically, it is the source of inherited handles.THREAD_
ATTRIBUTE_
PARENT_
PROCESS
- Call
CreateÂProcess
with this attribute list. - Use
DuplicateÂHandle
withDUPLICATE_
to close the handles you injected into the helper process.CLOSE_
SOURCE
Cleanup
- Terminate the helper process.
This technique works because the handles are never marked as inheritable in the main process. Therefore, they can never be accidentally inherited. The only place the handles are marked inheritable is in the helper process. Since the helper process is always suspended, there’s no way that anybody in the helper process can call CreateÂProcess
. The only way somebody can accidentally inherit the handles is if they accidentally get a handle to your helper process, which would be quite an accident.
You probably want to put the helper process in a “terminate on close” job, so that the cleanup occurs automatically when your process terminates. That way, you don’t leak helper processes if your main process crashes before it can clean up properly.
“There’s still a problem with this, though: It requires everybody to be playing the same game.”
Didn’t the previous workaround also require that, in that everybody has to use the same mutex?
The previous solution required everybody to use the same mutex, which can be difficult to coordinate among multiple components. The revised solution requires everybody to follow the same steps, but they don’t need to share a mutex. (By analogy: The previous solution avoided collisions by requiring everybody to share the same car. The revised solution allows everybody to have their own car, as long as they remember to use their turn signals. The sneaky trick gives everybody their own private road.)
Raymond, would really appreciate your take on this: https://devblogs.microsoft.com/oldnewthing/20200221-00/?p=103466#comment-136245
No opinion. I’m relying on Malcolm’s information. (Though (1) I would expect there to be contention on the shared event, and (2) you don’t appear to be disagreeing with the article.)
Hmm I wonder what takes up more memory, the CREATE_SUSPENDED large binary or the crimped 4kb binary with a tiny stack that does GetCommandLine -> OpenProcess-> WaitForSingleObject -> ExitProcess.
My guess is that it only uses up some address space, as I would expect the memory manager to swap this suspended process out ASAP if there is memory pressure.