Project

General

Profile

Actions

Bug #1442

open
LG

call_rcu_thread() leaks one pthread_mutex per loop iteration on platforms where pthread_mutex_init() allocates (e.g. FreeBSD)

Bug #1442: call_rcu_thread() leaks one pthread_mutex per loop iteration on platforms where pthread_mutex_init() allocates (e.g. FreeBSD)

Added by Laurent Gouhier 2 months ago. Updated 2 months ago.

Status:
Feedback
Priority:
Normal
Assignee:
-
Target version:
-
Start date:
07/03/2026
Due date:
% Done:

0%

Estimated time:

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 Actions #1

  • 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 Actions #2

Related: https://review.lttng.org/c/userspace-rcu/+/18220 Fix: workqueue leaks mutexes on FreeBSD

MD Updated by Mathieu Desnoyers 2 months ago Actions #3

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 Actions #4

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

Actions

Also available in: PDF Atom