Finding the “second bug” in glibc’s condition variable
probablydance.com
probablydance.com
But, my takeaway from this is that there’s a glibc bug which causes deadlocks in correct code, and a proposed fix (even if partial) has been available for years, and glibc still hasn’t shipped a version with the fix so it’s up to distros and users to patch the broken glibc code.
Reminds me of how a glibc update broke GNU m4 and GNU didn’t ship a fix to m4 for years, so it was up to distros and users to patch the latest release of GNU m4 to make it compile with the latest version of GNU glibc.
And then a couple of years later GNU shipped another version of glibc which broke m4.
GNU doesn’t strike me as an organisation which places a lot of value on software quality and reliability.
This stems from a misunderstanding of what GNU is. Ideally all projects under the GNU umbrella would work towards a unified operating system, but that's sadly not what it's like. The GNU project is not much of a project in the common sense of the word, nor is it much of an organisation. This is one of the many reasons why https://gnu.tools exists, a subset of GNU with common goals and values, including collaborative project management.
But I wish that the projects in the GNU toolchain would at the very least collaborate. The combination of glibc + gcc + autotools + m4 makes such a core part of any GNU system that you'd hope that GNU cares to keep them mutually compatible. So when they release an update to glibc which breaks m4 which makes it impossible to compile the current version of the GNU toolchain using the current version of the GNU toolchain, that's extremely disappointing.
GNU can never succeed at it because it can never compromise and maybe that's fine. It's a shame that it s considered a failure when they accomplished so much in the OS scaffolding part.
This stupidity surfaces everywhere; here is a Go bug report for it:
> https://github.com/golang/go/issues/21083
The irony that they proposed a fix in 2017 but never got around to actually implementing it...
That's not surprising, because their main purpose is to promote the GPL and "free as in freedom" software. I've heard theories that their software is deliberately overly complex and uniquely different from other implementations in order to make it easier to find GPL violations. In other words, they're optimising for something other than quality.
I've inspected the glibc condition variable implementatoin (while looking at what may be a slightly different bug, or it could be another manifestation of this one; not sure at this point since it was years ago) and the first thing I noticed was that it is disturbingly complex.
You're conflating the FSF and the GNU project.
This was on a 32-core machine with lots of background compilation going on at the same time (i.e. with a high load).
It could be a coincidence or an unrelated bug, but it's also possible that the author's patch might not fix the issue completely.
I couldn’t diagnose or debug some intermittent faults in poor multi-threaded code (written by someone who struggles to think concurrently). I threw the code away and wrote it properly from scratch and that got rid of the data or race conditions causing the intermittent fault. I have had to fix multi-threaded code many many times because I am better than most at properly fixing it.
But for the record, the author has since found the issue, and in this case it indeed was a bug in the first patch that was inadvertently fixed by a later patch in the series:
https://probablydance.com/2022/09/17/finding-the-second-bug-...
static void st_masterlock_acquire(st_masterlock * m)
{
pthread_mutex_lock(&m->lock);
while (m->busy) {
m->waiters ++;
custom_condvar_wait(&m->is_free, &m->lock);
m->waiters --;
}
m->busy = 1;
pthread_mutex_unlock(&m->lock);
}
How is this bug not more common with such a simple invocation?Also, I do wonder why they roll their own mutex using condition variables. Most people probably would use a mutex and thus wouldn't hit the condition variable at all. Condition variables are more niche than mutex.
> It is possible to use a plain POSIX mutex as master lock, but fairness is awful.
https://discuss.ocaml.org/t/is-there-a-known-recent-linux-lo...
The glibc project should be incredibly embarrassed by this, assuming there's anyone even still paying attention there.
Why pthreads included condition variables but not semaphores, I don't know. Maybe because POSIX already had them?
(Personally, I like events, but sometimes I want ones that don't reset - as in, the event is only ever flipped once in its lifetime, from "not ready" to "ready", but any number of consumers can check if it's ready or wait for it to be ready. These can't be directly implemented with POSIX semaphores. But then, I didn't know until just now that fast POSIX semaphore implementations even existed – there's my Apple bias.)
These are the semaphores I'm familiar with, which are plain counting semaphores you can also implement trivially using condition variables. The condition variable is a lower-level concept which pairs an OS-level signaling object and a mutex, it places no limits on what the condition may be, unlike a counting semaphore.
Personally I find it more odd that they'd bother with sem_t when they already have pthread_cond_t. Perhaps there's an optimization opportunity in sem_t when the condition is specialized to involving just an integer counter, justifying the specialized interface?
WRT waking the condition variable multiple times, properly handling spurious wakeups is kind of central to the proper use of them. That's why the condition test is always part of a loop, e.g.:
pthread_mutex_lock(&lock);
while (!condition_satisfied(shared_state))
pthread_cond_wait(&cond, &lock);
{ do things with condition satisfied and lock held }
I always found condition variables to be nicely composable and ergonomic when writing pthread programs in C. But they come up short when you start wanting a file descriptor event to trigger the condition wakeup. In that case you end up doing silly things like spawning a thread just for the purpose of monitoring for fd events and turning them into a pthread_cond_signal() on the appropriate pthread_cond_t. AIUI the Windows WaitForMultipleObjects() interface does this better, so I hear, but I've never really dealt with Windows.Unfortunately it is hard to implement select/wfmo for fast-pathed edge triggred primitives because of the race that can cause lost wakeups. Futex had a futexfd feature for polling but it was impossible to use correctly and was removed.
No.
What's ridiculous is neglecting to fix a known correctness bug in a core synchronization primitive causing random deadlocks for over two years.
That commit claims to fix ordering guarantees required by the clarified spec, but in doing so has introduced real deadlock bugs.
The linked discussion at [1] is telling. It seems glibc's nptl is prioritizing playing games with optimizations over correctness, even before [0] landed.
To suggest all that's required is someone showing up with a patch is to completely ignore the fact that there's zero shortage of correct pthread_cond_t implementations all over the place. This is a maintenance failure, not a lack of code.
[0] https://github.com/bminor/glibc/commit/ed19993b5b0d05d62cc88...
I also very much do not think that there are plenty of correct pthread_cond_t, far from it. It is possible that the musl one might be close to drop in replacement (although IIRC, musl at some point had a different thread cancellation implementation than glibc).
Surely code to operate condition variables correctly is in the public domain, that does not involve mitigations or stolen signals.
I'm sure the BSDs have posix cond vars implementations, but they won't use futexes but some BSD specific signalling primitive, and will likely have different scalability profiles than the one in gilibc, which has been tuned mostly for the needs of Red Hat, Oracle and other big Linux vendors.
In my experience, both writing and teaching concurrency, there's a very strong sentiment of "I'll just use another CV/lock/etc." whenever people encounter a bug and try to fix it, which seldom fully solves the problem but usually makes it even more subtle and compounds the number of resulting states.
One of my tricks to increase chances for race conditions and deadlocks when testing is to use a modified sampling profiler to randomly delay and pause threads, but with such a long sequence of steps as this bug requires, that would've been unlikely to help. Yet Murphy's Law says that it will happen in practice...
Trying to fix this in userspace is just about tuning to try to suppress the underlying issues, and recovering cleanly when things break.
IPC is just really hard, we struggle with exactly this in Zephyr too (though we have mostly managed to avoid this particular kind of mistake as in an RTOS the kernel/API boundary is thinner), at a much smaller scale. Add NUMA and cache-locality-concern to the mix and things get harder still.
I suspect there may be additional concerns, but I'm not a glibc expert and have not needed to mess with it much.
Also, does musl libc [0] have less bugs, on average?
The author remarks that this is in part because musl is conservative, and identifies barriers in musl that can be removed. But I think musl should get credits for being conservative here. Also in benchmarks, redundant barriers showed no performance impact.
https://people.mpi-sws.org/~viktor/papers/asplos2021-vsync.p...
[0]: https://en.wikipedia.org/wiki/Java_memory_model
[1]: https://docs.oracle.com/javase/specs/jls/se18/html/jls-17.ht...
So, pick a formal model and use it - though note that formal methods tend to add significant extra effort to use, and may or may not pay off depednig on your domain and desired outcomes.