Technical Root Cause Analysis
1. Relevant Subsystems, Functions, and Files
2. Background and Mechanism
BH Workqueues and Context Borrowing
In Linux kernels supporting Bottom Half (BH) workqueues, work items queued on system_bh_wq (such as TCP Small Queues work tcp_tsq_workfn) are serviced in softirq context via tasklet_action() -> workqueue_softirq_action() -> bh_worker().
Whenever bottom halves are enabled (for example, in read_unlock_bh(&sk->sk_callback_lock) inside rds_tcp_write_space()), pending softirqs can be processed synchronously in the context of the running thread. In this trace, the running thread is kworker/u8:15 executing rds_send_worker(). Prior to servicing the softirq, rds_send_worker() had already acquired 6 locks (e.g., workqueue tracking maps, RDS connection mutexes/spinlocks, socket locks).
Lockdep Tracking in process_one_work()
When bh_worker() invokes process_one_work(worker, work) to run tcp_tsq_workfn, process_one_work() performs lock debugging checks introduced for BH workqueues (commit c35aea39d1e1):
/* kernel/workqueue.c:3466 */
rcu_start_depth = rcu_preempt_depth();
lockdep_start_depth = lockdep_depth(current);
...
worker->current_func(work);
...
if (unlikely((worker->task && in_atomic()) ||
lockdep_depth(current) != lockdep_start_depth ||
rcu_preempt_depth() != rcu_start_depth)) {
pr_err("BUG: workqueue leaked atomic, lock or RCU: %s[%d]\n"
" preempt=0x%08x lock=%d->%d RCU=%d->%d workfn=%ps\n",
current->comm, task_pid_nr(current), preempt_count(),
lockdep_start_depth, lockdep_depth(current),
rcu_start_depth, rcu_preempt_depth(),
worker->current_func);
debug_show_held_locks(current);
dump_stack();
}
The Discrepancy (lock=6->0)
- Before
worker->current_func(work) is called, debug_locks is 1. Since current already holds 6 locks from the outer rds_send_worker() context, lockdep_depth(current) evaluates to 6. Thus:
$$\text{lockdep_start_depth} = 6$$
- While
tcp_tsq_workfn() is running (or due to an external condition such as an unhandled lockdep error, warning, or kernel taint/softlockup), debug_locks_off() is called, setting debug_locks = 0.
- In
include/linux/lockdep.h:
#define lockdep_depth(tsk) (debug_locks ? (tsk)->lockdep_depth : 0)
When debug_locks is 0, lockdep_depth(current) unconditionally returns 0.
- When
process_one_work() checks whether locks were leaked:
lockdep_depth(current) != lockdep_start_depth // evaluates to 0 != 6 -> true
Because process_one_work() does not verify whether debug_locks is still active, it mistakes the global disabling of lockdep for an unlock imbalance (lock=6->0), prints the false-positive splat:
BUG: workqueue leaked atomic, lock or RCU: kworker/u8:15[1533]
preempt=0x00000000 lock=6->0 RCU=2->2 workfn=tcp_tsq_workfn
and subsequently calls debug_show_held_locks(current), which notes that lockdep was turned off:
INFO: lockdep is turned off.
3. Invariant Violated
process_one_work() assumes that lockdep_depth(current) accurately reflects the number of held locks across the invocation of worker->current_func(). This invariant is violated when debug_locks transitions from 1 to 0 during execution, which collapses lockdep_depth() to 0 and triggers a spurious leak report.