The Unix process API is unreliable and unsafe (2021)
catern.com
catern.com
For example, in 1.1.4 the author talks about why containers are not a solution giving three distinct reasons. But if we change our perspective a little bit, none of the three reasons are blocking. The first is that it's not easy; but `docker run` or `podman run` is easy. Even systemd units start with separate control groups to allow you to terminate everything at once. The second reason was about gdb; when was the last time you used gdb in production? If you are using gdb someone is interactively using the computer and can be relied upon to clean up processes manually. The third reason is that containers are more heavyweight, but there's no need to make every subprocess a separate container: if multiple processes should be managed as a single unit (including the case when we'd want to terminate a whole group of processes) they should run in the same container.
So with a slight change of perspective we find the problem easily solved. It had trade offs but it works well enough in practice that only very few purists have a problem with it. Not to diss on the author—I think this type of perfectionist thinking is illuminating in terms of API design—but pragmatically it's a solved problem.
[1] fcntl locks permit querying the PID of the lock holder, so you don't need to write the PID to the file, providing a solution to the PID file race and loaded gun dilemmas. (There's still a race, but the same race exists with Linux containers, and both can be resolved in similar manner--query PID, send SIGSTOP, verify PID association, send SIGKILL or SIGCONT.)
[2] One of the crucial behaviors, that the kernel atomically sends SIGHUP to all processes in the group if the controlling process terminates, isn't guaranteed by POSIX, but it's the behavior on all Unix I've tried--AIX, FreeBSD, macOS, Linux, NetBSD, OpenBSD, and Solaris.
>The first is that it's not easy; but `docker run` or `podman run` is easy.
I was referring to easy use from a full fledged programming language. When you start a subprocess in your programming language of choice, do you always run it in a container? I seriously doubt it, and the reason for that is because it's hard.
>The second reason was about gdb
No, the second reason was about user namespaces, which break many things including ptrace, which in turn breaks gdb, as just one example. There's lots of useful tracing and monitoring software which makes occasional use of ptrace.
>if multiple processes should be managed as a single unit (including the case when we'd want to terminate a whole group of processes) they should run in the same container
Yes, that's true. And indeed, scripting use cases often have that characteristic, where everything can be terminated at once at the end. You can compare this to missile-style garbage collection: Just never free your memory/processes. Unfortunately, long-lived applications both need to free their memory over time and need to clean up their processes over time.
I use a (standard) library to start subprocesses in my favourite programming languages.
For running a container, I'd also use a library.
Seems about equally hard, no?
My experience has been that _literally_every_ advanced cloud architecture can be simplified to:
- Host w/1 administration IP on a vlan w/ admin services on that IP, then N customer/service IPs w/ customer+service apps bound to them (11.12.13.14:5432 for customer A postgres, 25.26.27.28:5432 for customer B postgres, whatever)
- For each IP+service combo, configure the service to use its own directory of storage. Back each storage according to what it needs; the DB is backed by block RAM > SSD partitions > HDD partitions > SMB nonsense.
- For each Host+customer+service triple, write a python (complex)/shell (simple) deployment.py/sh and health script.py/sh to handle 95% of monthly deployment+maintenance needs.
Done. Scale to MxN HostsxServices across M IPs by N services over N customers.
unshare -U --map-user=$(id -u) --map-group=$(id -g) -f -p sh -c '{ sleep inf &} &'
The author doesn't share this example because "user namespaces introduce a number of quirks, such as breaking gdb (by breaking ptrace), so they also can't be used by most users". I disagree that the container/unshare approach can't be used by most users. So strace works just fine on my machine and gdb is able to attach and print a backtrace. Now it's true that gdb itself prints out a warning when I do this: warning: Target and debugger are in different PID namespaces; thread lists and other data are likely unreliable. Connect to gdbserver inside the container.
So:- user namespaces do solve the "kill all child processes" problem
- strace still works
- gdb from outside the namespace isn't fully supported
So if you wanted to debug a child subprocess with gdb you'd presumably have to invoke gdb via unshare so it shared the same pid namespace (not tested).
> unshare -U --map-user=$(id -u) --map-group=$(id -g) -f -p sh -c '{ sleep inf &} &'
When I run:
> unshare -U "--map-user=$(id -u)" "--map-group=$(id -g)" -f -p sh -c 'command sleep infinity'
then Ctrl+C has no effect.
Sending SIGTERM to `unshare` has no effect.
Sending SIGKILL to `unshare` reparents the inner process `sleep infinity` to my computer's top-level systemd, where it continues to run (process leak).
So not sure how that ensures "child processes are killed".
You need at least the `--kill-child` flag:
> unshare -Ufp --kill-child -- bash -c "command sleep infinity"
See https://unix.stackexchange.com/questions/393210/why-does-uns...
I added the `--kill-child` flag to unshare because Linux did not offer a reliable way to kill child processes when pressing the "Cancel build" button in my CI pipeline.
With the above, SIGKILL against `unshare` will reliably tear it down and everything below it.
But Ctrl+C still has no effect, and SIGTERM against `unshare` still has no effect. So I agree with the post author that the Linux process API is unreliable. This stuff should be easy.
You can find the text in https://jmmv.dev/2019/11/wait-for-process-group.html and the two extra linked posts in the last paragraph, one providing a solution for Linux and the other for macOS. It isn’t pretty nor easy, but it’s doable to various degrees of correctness.
https://www.microsoft.com/en-us/research/uploads/prod/2019/0...
It's very much worth a read.
Windows has a much better process API, except for CreateProcess() (the less said about which the better).
One thing I generally do when I have a multi-process program (one that starts multiple worker processes, say), is to have a pipe with the write end only in the parent process and whose read end the children include in their I/O event loops. That way when the parent exits the children find out and then they too exit. The parent will still try to signal them, but say the parent gets `SIGKILL`ed: the children find out and they exit.
One can approximate pidfd in multi-processed programs on OSes that lack it, but that's about it. pidfd needs to be first-class.
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.
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.
That's a sensible strategy. Do you know if this design pattern has a name, of creating this "clean" process image base for forks via ipc?
https://lwn.net/Articles/785430/
https://www.microsoft.com/en-us/research/uploads/prod/2019/0...
My specific complaint about CreateProcess() is everything to do with how the child process is bootstrapped and how arguments get passed (as a single string! that the child has to parse like it were a shell! gaaaa!!!).
[0]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/p...
- You can't control every call to open, and thus enforce O_CLOEXEC on all files (by default)
- Because of this, most files will be leaked into the child, this is likely not desirable, especially since some distros have low open file limits for processes
- posix_spawn (the replacement for fork/exec) allows you to specify a list of file actions to perform, including opening and closing, this seems like a solution at first glance
- However, there is a TOCTOU race here, as you need to first make a list of file actions with posix_spawn_file_actions and then call posix_spawn. Note that every file you want to close needs to have it's own file action, this means you need to determine all the files that are open and manually add each one. This alone introduces the problem of determining all open files in your process.
- In a multi-threaded program it is possible for another thread to open a file between the calls to posix_spawn_file_actions and before posix_spawn, thus creating the potential for files to leak into the child.
- Even in a single threaded program, it is possible for posix_spawn to to invoke functions established with pthread_atfork, and atfork handlers are allowed to call signal-safe functions, including but not limited to open. Implementations aren't required to call atfork handlers, and modern glibc doesn't, but this is by far no guarantee.
- Therefore, my argument is that posix_spawn cannot be used to create a process with a guaranteed minimal and clean state, and so you are back to square-one with fork/exec.
The defaults for working with these APIs are just completely wrong, and very hard to get correct. The issues with fork/exec are numerous and nuanced, and most people simply aren't aware of the issues or don't care. There is a specific song and dance that needs to be performed when using fork/exec and usually you want to hide all of that behind a library function... which will look something similar to CreateProcess.... sure you might use the builder pattern to make it look nicer, but you really don't want the fork/exec split.
Here are few other issues with fork/exec, non-exhaustive:
- Only signal-safe functions can be invoked between fork and exec. This means you need to be super careful with any stdlib code you invoke between these two (or better yet, just don't).
- Multithreaded programs cannot call fork without exec. period. The state of objects such as mutexes and condition variables will be inconsistent. This is implied by the above, but I wanted to specifically call this out.
- Detecting if exec failed instead of the program requires using an extra pipe marked with CLOEXEC, I have seen too much code using a magic exit code (which is wrong)
- Cleaning up the state of the child process and not accidentally creating a zombie is a bit tricky and there are some race conditions to be aware of. pidfd is not a solution if you need to support older kernels, although helps tremendously.
- Interaction with signals is a bit messy.
- When fork is called, all pages will be marked as copy-on-write, this can be slow for processes with lots of memory allocated, and is completely redundant if your goal is to call exec. If other threads exist and are writing to memory, the pages they touch will be copied unnecessarily.
- Like I harped on earlier, files are inherited by default, not the other way around. You should be required to manually list the fds that you want the child to inherit (likely stdin, stderr and stdout only for 99% of cases).
- Distinguishing exec failure from exceeding but the process failing requires a CLOEXEC pipe
- If exec fails, _exit must be called! you cannot terminate the child in any way that might run destructors, of invoke callbacks/handlers as these can perform I/O and would thus be observable.
CreateProcess is just much better, and the whole "it takes 12 parameters how awful" argument against it is 100% a non-issue. It isn't 1960 anymore, it's okay to have a function with a name longer than 6 letters and more than 3 parameters.
> - You can't control every call to open, and thus enforce O_CLOEXEC on all files (by default)
Eh, you can if you open-code everything that might not call `pipe2(2)`, or `accept4(2)`, etc. It's not great, but it is possible. You can also LD_PRELOAD a shim to make everything do that -- also not great, but possible.
You can also do the spawn server thing, which solves the problem, though you need to spawn the spawn server early, and if not, well, yeah.
You can also close (on the child side of vfork()) all the FDs you don't know. That mostly works, unless you are running in a context where you need to keep some FDs you don't know about.
We don't have a time machine. We only have these workarounds. It's not all bad.
io_uring_spawn is a WIP. https://lpc.events/event/16/contributions/1213/
Except ... on AIX at some time, they randomised pid usage. So it was now far more likely!
The reason they randomised pid usage was because the random number routine used the pid+time+something else!
https://vmssoftware.com/about/news/2022-07-14-openvms-v92-fo...
But I expect it would be several thousand dollars. I doubt they really want hobbyists as paying customers because the cost of servicing them is likely to greatly outweigh the revenue potential.
I think the author knows this, but you don't have to start a full-blown container if all you want is to solve the article's stated problem of process leaks. Become a new pid NS: point 1, the subprocess.run criticism is fixed (it just works); point 2, I don't believe a pid NS requires either root or a user NS; and all that remains is point 3. It doesn't require you to start a separate init, you can be the init, i.e., whatever your top-level service is. IIRC, the only two requirements is handling SIGTERM (which you should probably already be doing) and reaping reparented orphans who then die. But also dumb-init is available? The article notes using a separate init, too: "This init process will do nothing but increase the load on the system, and it will prevent us from directly monitoring the started processes." and … no? dumb-init, in a container I have here that's run for >2 weeks, has used < 20 ms of CPU time. RSS of 522 KiB. You'll be fine. I'm not sure how it "will prevent us from directly monitoring the started processes" — it would live above you in the process tree. You'd monitor the started process the same way you would any started process.
Edit: ah, crap, I've got it wrong. A new PID NS requires root (or user NSes); being a subreaper, I think, maybe does not. But I'm not sure being a subreaper is sufficient; you want the subtree reaped on the subtree root's death.
(I'm also not sure that the subreaper approach is sufficient: if the subreaper itself dies, the processes leak.)
It's just as flawed a solution as the other flawed solutions. We can accept the subreaper being bug-free as a requirement for this workaround to be working, but we can't prevent it from being sigkilled.
Doooooooo it
But containers aren't jails, pid and uid remapping is just remapping.
A huge problem container has to drop capabilities on the honor system. In the default docker mode, running as root, anyone who can launch a container can read from any block device if they don't drop the mknod capability as an example.
Actually a privileged container can update the bios or even load arbitrary kernel modules in the host context or change kernel parameters as it is a shared kernel.
I tried to get the docker folks to add a conf option disallow privileged container but they refused.
You can run in user mode now but most people want persistence and other features that don't allow for that.
The important point is if you assume containers are a security feature you are going to have a bad time. Jails were bad enough and containers are just one step up from chroots as far as security go.
namespace isolation is the main benefit of containers.
Selinux and apparmor are far more appropriate than containers for the security concerns. While I don't personally like selinux, apparmor profiles are pretty easy to write.
Containers make the first point of the OP far worse by adding way more pid namespaces.
Um, what?
What do you mean by "impersonate" here? What does a process that does not impersonate the user look like? Do you just mean "executables that run as the user"?
When you log in, and a shell is started that runs as you, is that shell impersonating the user?
When you execute commands, as yourself, those commands run with your credentials. Because you ran them. Isn't that, like, the point?
Related:
https://en.wikipedia.org/wiki/Ambient_authority
A command running inside a virtual machine, maybe?
The collections of Cgroups and Namespaces (and for all that they are different APIs, you almost never use one without the other, so perhaps it's bet to refer to the whole group of them as "Containers" or "Containment" to differentiate it from Docker-style containers) is complex and flexible for a reason, even if an absurd proportion of the common cases can be solved with a reasonable set of defaults of them.
Take the venerable `cowsay` for example. Currently, running `cowsay` (as with any other program) can cryptolocker your hard drive, delete all your files, reach out to arbitrary servers on the internet, etc. But how much access to your system does it actually need to do its job? Well, mostly... STDIN, and STDOUT, really.
Yes, actually doing this is complicated. Another reply has linked to some broad info about object-capability security, but here's a good introduction to the subject: http://habitatchronicles.com/2017/05/what-are-capabilities/
...and this paper is an excellent deep dive into a capability system: https://mumble.net/~jar/pubs/secureos/secureos.html
Tl'dr two functions "dispatch" that calls systemd-run and "wrap" that takes a command, a memory limit, and a cpu limit.
Use the C utility supervise to start your processes; for Python, you can use its associated Python library."
C utility written in 1999. Last updated in 2001. I'm still using it everyday, not always with multilog and svscan.
Largely because of Docker and Kubernetes, many think of a container as all of the following:
1. A cgroup + [all or nearly all of the] unshare-able namespaces
2. A writable, disposable overlay on top of an immutable "image", which may be lazily downloaded and extracted
3. A resource managed by a userspace daemon managed by a userspace utility over a socket
4. Optionally, a seccomp-bpf filter or apparmor profile or something
But there's a whole useful spectrum between a vanilla process and a Docker container like that. Lots of points on that spectrum still feel highly container-ized but aren't really much more heavyweight than a vanilla process.
Beyond that, in the point about PID namespaces, the author should mention that there are ultra-light-weight init implementations that are barely a factor in overhead.
If you have to rely on systemd then arguably is a systemd solution, not a Unix or Linux one.
However we usually cope with that by closing all unknown/unexpected file descriptors after fork and before exec. Linux even has a system call to make that easier: https://man7.org/linux/man-pages/man2/close_range.2.html
Not exactly. The problem is that you have to be able to set it atomically from the creation of the file descriptor. Setting it after creation is subject to a race condition where a fork occurs in the interim. There's no portable way to do that, and people often ignore O_CLOEXEC even when there's a platform-dependent way to pass it. (How often do you see dup3() called, for example? And how often do you see higher-level languages and libraries expose this and force callers to make a conscious decision?)
> However we usually cope with that by closing all unknown/unexpected file descriptors after fork and before exec.
You can't really do that portably (well, maybe unless you want to call close() billions of times). And even if/when you can do that, you run into the reverse problem, where you might close descriptors that were supposed to be duplicated into the subprocess but that you didn't know about. (One example is when a user performs redirect inside a shell like 2>&3 and wants it to work inside a descendant process - you don't want to just randomly close FDs you don't recognize.)
Or you have to accept that when spawning a new process, you have to know which FDs you want to leak.
But doing it portably? Lol yeah, forget it. Posix is a farce.
accept4, dup3, pipe2, F_DUPFD_CLOEXEC, MSG_CMSG_CLOEXEC, and SOCK_CLOEXEC are already in the draft for POSIX Issue 8. Most of these have already been in the BSDs and Solaris for a few years, now, at least. macOS is the major exception.
Also in the draft are close-on-fork analogs: F_DUPFD_CLOFORK, FD_CLOFORK, MSG_CMSG_CLOFORK, O_CLOFORK, and SOCK_CLOFORK.
We are fairly thorough with using dup3, pipe2 etc in our own code, on platforms that support it. Of course hard to control other people's code.
This is not true on all systems. For example, on ilumos we have fdwalk(3C) and in particular closefrom(3C). These allow you to dup2() your intended descriptors to a contiguous block starting at fd 3 (assuming you keep the stdio descriptors at 0-2) and then close everything from one past the last descriptor you intend the child to inherit.
- on fork, you are by definition implicitly cooperating with the parent as you are the same software image as the parent; in the case of transitive forks you can be expected, I think, to continue cooperating.
- on exec, you move from implicit cooperation to the need for explicit cooperation, through some kind of interface contract; e.g., the BASH_XTRACEFD environment variable. It is incumbent upon you to hide the good silverware before making the exec transition, as exec is a trust boundary; like when you sell your house, you don't get to control what the new owner of the process does with it, except through kernel-enforced mechanisms like privileges and resource controls and so on.
(rm -f /run/lock/temp && exec 5<> /run/lock/temp && echo test123 > /run/lock/temp && perl -e 'system("bash -c \"cat /dev/fd/5\"")')
it needs to work fine, and this shouldn't require any particular cooperation between Perl and Bash.FDs need to be inherited correctly... just like environment variables already are.
[1] It's literally a small, trivial loop in the original Bourne shell source code. In modern shells job control and other niceties require additional bookkeeping, but those aren't necessarily implicated in your example. I don't have a URL at hand for that source file, but I did find it last year or the year before, so it shouldn't be too hard to find if you're curious.
For the purposes of the autorunner, I use approach 1.1.3 (“always write down the pid of every process you start, or otherwise coordinate between A and B”) and leave it to the user to figure out what happens if the child process misbehaves with relation to any processes it starts.
However, I want to point out that approach 1.1.4 (“A should run B inside a container”) is easier to do than one might expect, and I'd like to plug one of my favourite utilities—Bubblewrap[4]. The Bubblewrap documentation says “[y]ou are unlikely to use it directly from the commandline, although that is possible” but I have built some amazing little tools from it.
Try the following invocation:
bwrap --ro-bind / / --proc /proc --unshare-pid ps
This launches `ps` in a PID namespace with a new `/proc` (since `ps` will read from the host proc otherwise) and the root filesystem mounted readonly. Any procesesses within the PID namespace should have been created by the immediate command that `bwrap` launched. There are also flags `--die-with-parent` and `--as-pid-1` which can further reduce runtime overhead. If you really need a supervisor process, this can be as simple as a `/bin/sh` script that `kill TERM --timeout 1000 KILL` in a loop on everything it sees in `ps`.)As you can see, there's a lot you can do with this tool with significantly lower overhead than using Docker. It has been my goal for some time to extract some of the functionality of Bubblewrap into a Zsh extension to allow accessing these mechanisms with even lower overhead. I think the creation of namespaces is a missing primitive in Linux shells, and being able to quickly construct namespaced environments allows for a style of safe, robust, simple shell scripting. e.g., if you create a mount namespace to run your script, you can actually be looser about parameterising file locations (since the namespace can ensure everything is exactly where you want it to be.)
[1] https://fosstodon.org/@dontusethsicode/110019380909461936
[2] http://eradman.com/entrproject/
Now if this is a problem and the application does not need address space isolation. clone() with the CLONE_VM flag will get rid of this problem, and clone() is nice in the fact you can get more granularity/control. This control basically ranging between most of the continuum between thread and separate process depending on your needs.
So whatever Linux does better, or any other UNIX for that matter, might be a pool of surprises somewhere else.
https://cr.yp.to/docs/selfpipe.html
FWIW a shell is basically two alternating, non-overlapping event loops:
- a select() loop on the input terminal FD for getting keystrokes (e.g. GNU readline)
- the waitpid(-1) loop for running code, i.e. get the next process that exited
It never actually does both at the same time -- it doesn't wait for processes and stream input simultaneously, which is awkward without the self-pipe trick.
---
Regarding adversarial processes, yes you need something like Linux cgroups to solve that problem. In traditional Unix, a process that can run arbitrary code can always escape your attempts to kill it.
IIRC you can start a Linux process in a freezer cgroup, and stop everything in the cgroup. I recall reading the docs for an HPC platform that does that, and I'm sure Docker does it in some way too.
---
I'd be interested in where `supervise` is used in production ... it seems like there is a bigger story behind this article!
(copy of lobste.rs comment)
Couldn’t you use a signalfd to receive the SIGCHLD, and listen to both the terminal and signalfd at the same time?
https://lobste.rs/s/om32da/unix_process_api_is_unreliable_un...
As far as your question, shells don't use signalfd() because it's Linux-only!
Most shells predate not just signalfd(), but Linux itself :) I wouldn't use it in a newer shell for portability.
If it ever came up, I probably I would use the self-pipe trick.
BTW I think there was some argument awhile ago that signalfd() is actually quite bad in the presence of threads, but a shell doesn't have threads so that doesn't matter.
In bash, try “set -o notify” (same as “set -b”). This will turn on immediate notification of terminations of subjobs.
So it’s not that bash lacks support for it, it’s that bash wants to be compatible with old Unix shells, but offers other behavior as an option.
The argument you bring here can also be advanced in favour of Windows 95 running stable enough, can't it?
> pidfd is a great solution to the third and fourth problems
So why talking about them? Is it just for proving the superiority of the author's own utility "supervise"?
1. It's easy for processes to leak
illumos has contracts[1][2][3], which were developed as part of the Service Management Facility[4]. They are another process grouping abstraction that allows SMF to track trees of processes, whether they daemonise or not. They allow for tracking or ignoring certain events (e.g., a fatal signal sent from outside the contract, a process within the contract that aborts and dumps core, if a particular process, or all processes, terminate in some way) and for doing certain kinds of automatic cleanup (e.g., terminate all processes in the contract when the process that owns the contract is terminated). These are managed by the kernel, so they are effectively inescapable even when not held correctly by a user process.
2. It's impossible to prevent malicious processes leaks
This is not really true for us either. Between contracts, and resource controls[5], and privileges[6], I expect one would be able to limit the malicious or accidental escape of processes from supervision or any run-away resource consumption caused by, say, a fork bomb.
3. Processes have global, reusable IDs
This is true on some level, but in practice I think that's just part of UNIX and when you have solved 1-2 and 4 in other ways, it's not actually that bad. If you want to kill everything in a contract you own, even without knowing the full list of pids, you can do that with ct_ctl_abandon(3CONTRACT)[7] which takes a contract file descriptor. The termination action (which could be to tear down all of the processes) will take effect then.
4. Process exit is communicated through signals
This is not entirely true, in the sense that they are by default for classic UNIX applications -- but they need not be. We have forkx(2)[8], which has the FORK_NOSIGCHLD and FORK_WAITPID flags. These request that SIGCHLD is not posted for process termination, and that the classic UNIX wait(2) family of calls will not receive notification or reap children. You can use these from a library, and then manage your own waitid(2) or waitpid(3C) calls on the specific process IDs you are responsible for reaping. You can also use contracts to receive notifications of events about processes within the contract coming and going.
[1]: https://illumos.org/man/5/contract
[2]: https://illumos.org/man/3LIB/libcontract
[3]: https://illumos.org/man/3CONTRACT/
[4]: https://illumos.org/man/7/smf
[5]: https://illumos.org/man/7/resource_controls
[6]: https://illumos.org/man/7/privileges
Just like the information you’re providing if you can’t be bothered to setup an ssl certificate for a page
And for many, maybe most, applications, leaking a child process isn't really a problem.
The APIs are inherently unsafe, this is because once you are in a situation where you have leaked a process, you can't safely kill it without risking other processes. In some situations this could even be abused by an attacker to cause other problems.
So yes, in the most boring of applications where you have a user in front of a computer, there's no need for the reliability of processes having lifetimes bounded by the references held to those processes over their life. Likewise, there's probably no massive need for safety because if a process gets killed unintentionally, the user can fix it. But just because there exist circumstances where neither reliability nor safety are important, doesn't mean that the APIs aren't inherently unreliable and unsafe.
Furthermore, yes, linux has solutions for half of these problems, FreeBSD has solutions for other parts of these problems, Solaris has solution for other problems. The article calls out "the UNIX APIs" for a reason. The original APIs are as bad as the article says, and they have to be worked around with OS specific non-portable APIs. Which are, by definition, not "standard UNIX APIs" anymore.