Bug #1442
opencall_rcu_thread() leaks one pthread_mutex per loop iteration on platforms where pthread_mutex_init() allocates (e.g. FreeBSD)
0%
Description
I've been chasing a slow but relentless memory leak on FreeBSD: a daemon that links liburcu grows by roughly 1 GiB a day, indefinitely, while its own memory accounting stays completely flat. After quite a bit of digging it turns out the leak lives inside liburcu's call_rcu worker thread, and as far as I can tell it's still there on current master.
In call_rcu_thread() (src/urcu-call-rcu-impl.h), the worker's main loop declares a temporary queue head on the stack every time around and initializes it with cds_wfcq_init() -- but there's no matching cds_wfcq_destroy() anywhere in the file (grep -c cds_wfcq_destroy on that file returns 0).
for (;;) {
struct cds_wfcq_head cbs_tmp_head; /* head WITH a lock */
struct cds_wfcq_tail cbs_tmp_tail;
...
cds_wfcq_init(&cbs_tmp_head, &cbs_tmp_tail); /* pthread_mutex_init() every iteration */
splice_ret = __cds_wfcq_splice_blocking(&cbs_tmp_head, &cbs_tmp_tail,
&crdp->cbs_head, &crdp->cbs_tail);
...
__cds_wfcq_for_each_blocking_safe(&cbs_tmp_head, &cbs_tmp_tail, cbs, cbs_tmp_n) {
rhp = caa_container_of(cbs, struct rcu_head, next);
rhp->func(rhp);
}
/* ... loop repeats, no destroy */
}
The catch is that cds_wfcq_init() does a pthread_mutex_init() on the head's embedded lock. On glibc that's harmless -- pthread_mutex_t is an inline struct, so re-initializing it costs nothing and leaks nothing, which is presumably why nobody noticed. But on FreeBSD pthread_mutex_t is a pointer and pthread_mutex_init() heap-allocates a struct pthread_mutex behind it. Since we never destroy it, every iteration quietly orphans one mutex, and libthr's allocator never gives that memory back to the OS -- so it just grows forever.
What makes it a bit maddening to track down is that this lock is never actually used. The loop only ever touches the temp head through the non-locking __cds_wfcq_* variants, and the head is thread-local anyway (single consumer, wait-free enqueue), so the mutex is pure dead weight -- it exists only to be leaked.
It's a single default worker leaking up to ~100 mutexes/s -- the rate is capped by the loop's poll(NULL, 0, 10) and is basically independent of load, as long as call_rcu() keeps the worker off its futex. On amd64 that's ~128 bytes a pop, so ~1 GiB/day. On a genuinely idle process the worker just parks on the futex and nothing leaks, so it only bites servers that are actually doing RCU reclaim work.
I ran into this while chasing ISC BIND #5528 (https://gitlab.isc.org/isc-projects/bind9/-/work_items/5528): BIND 9.20 started linking liburcu, and on FreeBSD its RSS climbs ~1 GiB/day while jemalloc and BIND's own accounting stay flat -- exactly because the leak is in libthr, not in the application allocator. An LD_PRELOAD shim counting live pthread mutexes showed the count climbing linearly (1206 -> 35814 over ~390 s, ~89/s), all coming from call_rcu_thread. (dladdr mislabels the static function as urcu_memb_defer_exit+0x21a, which sent me the wrong way for a while.)
Since the temp head's lock is never used, the cleanest fix could be just to use the lock-free head type, so no mutex ever gets created:
- struct cds_wfcq_head cbs_tmp_head;
+ struct __cds_wfcq_head cbs_tmp_head;
struct cds_wfcq_tail cbs_tmp_tail;
...
- cds_wfcq_init(&cbs_tmp_head, &cbs_tmp_tail);
+ __cds_wfcq_init(&cbs_tmp_head, &cbs_tmp_tail);
There's a smaller sibling of the same bug in _call_rcu_data_free() : it free() a worker whose cbs_head was created with cds_wfcq_init() but never destroyed -- one mutex per freed worker. Tiny next to the loop, but same root cause; a cds_wfcq_destroy(&crdp->cbs_head, &crdp->cbs_tail) before the free() closes it.
Happy to send this as a proper patch to lttng-dev if that's easier -- just let me know which variant you'd prefer.
I stay available if something not clear or not detailed enough.
Environment details¶
FreeBSD (libthr), amd64. Reproduced on liburcu 0.15.3; I also checked 0.15.4, 0.15.5, 0.15.6 and master and the loop is identical (still no destroy), so an upgrade doesn't help.
MD Updated by Mathieu Desnoyers 2 months ago
- Status changed from New to Feedback
Thanks a lot for the detailed report. Can you have a look at the following commit and let me know if it fixes your issue ?
https://review.lttng.org/c/userspace-rcu/+/18219 Fix: call-rcu leaks mutexes on FreeBSD
MD Updated by Mathieu Desnoyers 2 months ago
Related: https://review.lttng.org/c/userspace-rcu/+/18220 Fix: workqueue leaks mutexes on FreeBSD
MD Updated by Mathieu Desnoyers 2 months ago
related:
remote: https://review.lttng.org/c/userspace-rcu/+/18221 Fix: test_build.c: Test output mismatch [NEW]
remote: https://review.lttng.org/c/userspace-rcu/+/18222 Fix: leaky wfcq tests [NEW]
LG Updated by Laurent Gouhier 2 months ago
Mathieu Desnoyers wrote in #note-1:
Thanks a lot for the detailed report. Can you have a look at the following commit and let me know if it fixes your issue ?
https://review.lttng.org/c/userspace-rcu/+/18219 Fix: call-rcu leaks mutexes on FreeBSD
Yes, thanks a lots