enum ProcessHandle {
Pid(pid_t),
Invalid,
Terminated
}
There is no way to pass a Terminated or Invalid process handle to kill, since there is no numeric identifier for it anymore. And, any code that wants to get a PID from a ProcessHandle has to explicitly handle non-PIDs - making it impossible to forget to deal with these special cases.Depending on the layout and arch, -2 could well represent Terminated
Rust enums are a Rust concept. Two identical enums will have different memory layouts on different machines, Rust versions, etc. Even if this was solved, you’re still passing the enum across a boundary. Both sides need to agree on the layout of the enum in memory.
To illustrate the problem, imagine we had two variants: `Pid(NonZeroUsize), LaunchTheNukes`
This can be laid out in memory with a single `usize`: the 0 value becomes the discriminate for `LaunchTheNukes`.
Thus, if you pass `0` (I.e null) across the boundary the other side will interpret it as `LaunchTheNukes`.
In short, if you naively change the layout of an enum and don’t recompile everything that uses the enum (i.e the kernel) then your -2 value could change from being interpreted as `Pid(-2)` in the application to `LaunchTheNukes` in the kernel.
It gets more complex with larger enums and more discriminators, but the general point is that the concepts of enums, type safety and such doesn’t really exist across boundaries like syscalls, because it doesn’t exist in hardware / at the machine level.
The solution for this particular problem is cgroups. The build system just wasn't using it.
cgroups also allows killing a group of processes race-free, but Linux only got this feature (cgroup.kill) in 2021.[1] Before then even systemd would just walk the cgroup PID list and send a signal to each individual process, which was inherently racy. If this was done from PID 1--init--this could maybe be done correctly by ensuring zombie's weren't reaped--preventing PID recycling--while walking the PID list. OTOH, Linux supports a prctl, PR_SET_CHILD_SUBREAPER, to change a reaper, so perhaps even systemd could never guaranteed to not accidentally shoot down an innocent process.
[1] Merged https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux... I'm not sure what the subsequent release history looks like, but I wouldn't be surprised if some popular LTS distro versions lack this capability.
Also FWIW, you could freeze a cgroup atomically in the very earliest versions and deconstruct it at leisure. You just didn't do it by sending signals.
If you can architect a system that never experiences handle collision then quite trivially you can architect an equivalent system that never experiences PID reuse.
And yes, of course it would be trivially easy to do the same in Linux. Except for that whole part where it would break all the existing code relying on POSIX semantics.
The filesystem API offers a solution for that. The process API doesn't (maybe with procfd; I haven't tried that API yet since it's so new).
What I'm saying is that instead of writing code like:
for fname in someglob:
if os.path.exists(fname):
os.unlink(fname)
You should instead write for fname in someglob:
os.unlink(fname)
and handle the errors.The fix was to check if the PID variable was set to one of the sentinel values, and not call kill() at all if that's what they are.
What do you mean? I admit there are cases where it's hard to avoid TOCTOU, but I can't think of any where it's impossible.
Ok then.
Ownership matters for more than just memory. And the "just use GC" answer means deadlocks and leaks galore. If you limit yourself to killing processes you own, you will never have a problem.
Think about it - if you aren't init, what exactly is too limiting about "kill the processes or process groups of the children you created yourself"? As a rule, most grandchildren won't bother to enter a new process group, and if they do their immediate parent should be advanced enough to take responsibility for forwarding fatal signals.
For a while the limit has been around 4 million or something like that, instead of the traditional 32k. Which of course broke a bunch of stuff that assumed "char pid[5];" is good enough. Fun stuff.
The kernel offers up this (here abbreviated) example which you could use for all accesses to /proc/<pid> when you have an open pidfd to ensure that the two refer to the same process:
static int pidfd_metadata_fd(pid_t pid, int pidfd) {
char path[100];
snprintf(path, sizeof(path), "/proc/%d", pid);
int procfd = open(path, O_DIRECTORY | O_RDONLY | O_CLOEXEC);
if (sys_pidfd_send_signal(pidfd, 0, NULL, 0) < 0 && errno != EPERM) {
close(procfd);
procfd = -1;
}
return procfd;
}
And these days you even can poll for pidfds (readable = exited) and you can even use P_PIDFD with waitid. So the very basic process management cycle can be done through pidfds nowadays.> as far as I know you still can't implement something like pstree without just reading files from `/proc` which is shit.
It's also slow! Tools like top/htop/ps -aux etc. use a surprising amount of CPU because they have to do a trillion trips to the kernel and back. Compare this to the win32 equivalent where, iirc, there is just one or two trips to the kernel to acquire a snapshot and you walk that in user-space instead.
char path[100];
snprintf(path, sizeof(path), "/proc/%d", pid);
You're assuming the pid will fit in 93 bytes! The exact same mistake as `char pid[5];`Jokes aside, sometimes userspace deserves to be broken, and `char pid[5]` is IMO one of those cases. The fact that we treat integers as a scarce reusable resource is bad, and the fact that we now have a second-order process identifier to cover that up is worse.