Murder Mystery: GCC builds failing after sbuild refactoring
linux.it
linux.it
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.
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.
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.
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.
It's possible to split it out, but then everyone complains about the result. We thought exceptions were going to fix the error code problem, but then we collectively decided we hate them. OSes and static analyzers and instrumentation tools like valgrind/asan/msan have done a really good job of turning NULL dereference into a built-in assertion everywhere, but people won't use them.
Even Rust, which has probably come closest to a genuine solution, has done so only at the cost of immense complexity and somewhat fugly syntax. No free lunch.
I think what's notable here isn't the bug itself, which is pretty routine. It's the fact that simple bugs can be buried so deeply that they get exposed only in a six-hour-long test rig on one particular Linux distribution's backend.
If a syscalls returns an fd, and on error it returns a negative value, why not define it as a bitfield with one bit a flag? Then in C there'd at least be a field name you could grep for, or grep for it not being there.
This is actually a discussion that's been going on in the systems programming community for decades. There's a cultural aversion to the runtime costs that's always outweighed any hygiene advantages.
The sign bit has much more defined semantics regardless. You can sign extend it. It's guaranteed to be the MSB, rather than dependent on how your compiler does struct layout and every ABI will agree on where it lives. You don't have to worry about a mismatch between the language semantics of the payload and how any particular ISA treats the "integer". Etc.
But which of those semantics do we need for conveying which of two states something is in?
If the Linux kernel recommended a more rich type for expressing the result of a syscall, compilers would find a way to express that portably.
Or they wouldn't recommend it. Or compilers wouldn't make it portable. Or libc implementors won't use it. Or lots of other possibilities. And we keep living in the world where we kill a process (often init) when we forget to check a syscall result, and pass fd-or-error somewhere that takes an fd, because those things are both integers.
Haskell Maybe
Scala Option
Java Optional
These things have existed for over a decade (multiple, in the case of Haskell).
And as I said I think the jury is very much still out on that one. Again, we tried to do this with exceptions in the early 90's and whiffed badly. Doing it at the validation/assertion/fuzzing/coverage layer with instrumentation tools turns out to be really effective in practice but the uptake remains really low.
Today the mindscape points to Rust's Option as the saviour, which is why I listed it. But as you point out, all these other environments had it much earlier. And... kinda failed. So I'm not hopeful.
In-band sentinels like NULLs and error codes are (1) clear and obvious, (2) near-zero-overhead and (3) extremely useful. Everything else falls down on at least one of those requirements.
Huh?
I suppose maybe the Java one is dubious(?), but the others were very happily adopted by the ecosystem over sentinel values.
enum Process {
Invalid,
Terminated,
Pid(libc::pid_t),
}
There would be no way you could pass instances of the first two enum variants to kill(). Rust is of course not the only language where you can express something like this; e.g. in Scala2: sealed trait Process
object Process (
case object Invalid extends Process
case object Terminated extends Process
case class Pid(pid: Int) extends Process
}
Does D have algebraic data types? Only allowing enums to be represented by integers is a language weakness.Some observations unrelated to the bug.
1. Nice to see some details about the Debian build server implementation. I have always found it rather obscure. Compared to that OpenSUSE's OBS is refreshingly open. Not only can you just use their cloud service for free, but you can also rather easily set up your own instance.
2. Nice to see that the Debian build infra gets updated. I am a bit suprised that Perl code gets introduced in 2024. Isn't that a bit out of fashion for a reason: It's typically very hard to read code written by others. (As said unrelated. It did not cause the bug in TFA.)
Most people who want to rewrite things in a language that's more fashionable don't have the experience maintaining working systems over 20+ years, like Debian does
Except maybe arithmetic gotos designed to minimize the number of functions. Those are straight evil and I'm glad no modern language supports them.
On another note, I have done a lot of things with Perl and know it well, and I agree that it can be written in a very unreadable way, which is of course enabled by its many esoteric features and uncommon flexibility in both syntax and methodology. It is simultaneously a high-level scripting language and a low-level system tool. Indeed, I have caught myself writing “clever” code only to come back years later to regret it. Instead of “TMTOWTDI”it is has turned into “TTMWTDI” (there’s too many ways to do it).
Could that be survivor bias? If that old Fortran was hard to work with, maybe it would have been rewritten, and left the set of “oldest code” in the process.
From your experience maintaining 40+ year old codebases, is it possible that improving the Perl codebase is the best choice in a certain situation?
Or should we always be "surprised" that Perl exists? (honest question)
In other words, I don't see the connection between "this code is hard to maintain" and "I am surprised that it exists" / "it should be rewritten".
Example:
* Prolog generated the worlds most functional 1 liner code, as it is bewildering for hope-and-poke programmers
* Pythons ease of use combined with out-of-band library packages became a minefield of compatibility, security, and structural problems (i.e. became the modern BASIC)
* NodeJS was based on a poorly designed clown JavaScript toy language, so naturally became a circus
* Modern C++ template libraries became unusable as complexity and feature creep spiraled out of control to meet everyone's pet use-case
* Rust had a massive inrush of users that don't understand why llvm compilers are dangerous in some use-cases. But considering 99% of devs are in application space, they will unlikely ever need to understand why C has a different use-case.
While "Goto" could just be considered a euphemism for tail-recursion in some languages. We have to remember the feature is there for a reason, but most amateurs are not careful enough to use it in the proper edge-case.
I really hope Julia becomes more popular, as it is the first fun language I've seen in years that kind of balances ease of use with efficient parallelism.
wrote a lot of RISC Assembly at one time... where stacks were finite, and thus one knew evil well... lol =3
The llvm abstraction behavior is usually fine in multitasking application spaces, but can cause obscure intermittent failure modes if concurrent register states are externally dependent on the architecture explicitly avoiding contention. I would recommend having a look at zynq DMA kernel module memory mapped to hardware io examples.
It seems rather trivial, Best of luck =3
There are binary optimizers and linkers that can still thrash Assembly objects in unpredictable ways.
Best of luck =3
Replacing key infrastructure is hard, particularly when it's working well and the cost of replacement is high, especially so when it's volunteer effort. It was cutting edge back when Perl was all the rage, but while Perl might no longer be the fashionable choice, tools written in Perl continue to work just as they always have. I found it dense and impenetrable even 20 years back, and I used to have the whole thing printed out on fan-fold paper from a dot matrix; it was about 3/4" thick and covered in annotations! Understanding several thousand lines of obscure regexes is a cautionary tale in how to write unmaintainable Perl, and was one of the motivations to clean it up, refactor it into meaningful functions, and document what it was doing so that it could be maintained with a little less effort.
>If pid is less than -1, then sig is sent to every process in the process group whose ID is -pid.
This is just awful IMO. Any time you build an API that has significant behavior boundary on input value (rather than input type) you have probably made a mistake.
I assume this is from the 60s though and therefore impossible to change.
Has an outright replacement for sbuild itself been considered? Given the ubiquity of modern CI systems, it seems quite possible to replace it entirely. I would have thought GitLab runners or Jenkins or any other standard CI system would work well. While I once enjoyed writing Perl, sbuild is not a great piece of code, and the problems it was trying to solve now have better and more widely-used solutions which don't require ongoing maintenance of an even older piece of Perl code.