Most of the complaints I've seen are about the number of args and the complexity of calling it vs something simple like fork. There are a lot of knobs to turn which you need to be explicit about. That's not even getting into the whole ProcThreadAttributeList and the myriad of options it exposes.
In saying all that I do prefer the `CreateProcess*` APIs on Windows vs the POSIX ones but that might be because I understand the former better.
It’s fair to say the initial surface area of the API is more complex (at first glance). However, fork/exec don’t really scale that well beyond toy examples you see in blogs or CS101. Thing is, if you are using fork/exec, you are likely also calling a few other functions for things like redirecting IO, dealing with exec failing (creating a pipe with O_CLOEXEC, and you need to remember to use _exit instead of exit (or anything else) if exec fails. Plus there is all the complexity of dealing with the child pid, signals, not accidentally creating a zombie, etc. Ohh not to mention that all the pages in the processes memory need to be marked copy-on-write when fork is called, only for that to be reversed when exec is called. I hope you don’t have dozens or hundreds of gigabytes allocated in a multithreaded program, that will cost you. Ohh and of course, you can’t call anything between fork and exec that isn’t signal safe, so you need to be really careful, especially in languages with implicit allocation or exceptions. Ohh bonus points for the fact that it’s ill-formed for multithreaded programs to call fork without exec since that can leave things like locks in inconsistent states.
The way both APIs handle file inheritance is absolutely horrendous, especially since most libcs don’t set the necessary flags. posix_spawn doesn't solve this either, since posix_atfork can open more files in the child, and multithreaded programs can have a TOCTOU bug if another thread opens files between the call to posix_spawn_file_actions and posix_spawn. TBF Windows is actually worse in this regard, since it’s race condition is a little more subtle. Ironically the best to way manage all of this nonsense is to create a child process first thing in main (another binary), which isn’t multithreaded, closes almost all files (uses a whitelist of fds) upfront, and spawns processes on behalf of the parent when requested (IPC). Ideally you would write this in C, minimize usage of libc, and avoid allocating tons of memory.
I would call it a spawn server, based on the "open server" or "doas" pattern, where you have a process that creates children to setuid() (etc.) which then set a private IPC service that open()s files and sends back the open FD to the caller.
In saying all that I do prefer the `CreateProcess*` APIs on Windows vs the POSIX ones but that might be because I understand the former better.