Root Cause Analysis
1. Overview of the Locking Inversion
The reported circular locking warning flags an inverted dependency cycle connecting memory management locks (&mm->mmap_lock and &hugetlbfs_i_mmap_rwsem_key), direct memory reclaim (fs_reclaim), the block layer request queue, and network socket locks (sk_lock):
dup_mmap()
down_write(&mm->mmap_lock)
down_write_nested(&mm->mmap_lock/1)
down_write(&hugetlbfs_i_mmap_rwsem_key)
allocate_file_region_entries() -> kmalloc() [GFP_KERNEL]
fs_reclaim
blk_alloc_queue() -> &q->q_usage_counter(io)
elevator_change() -> &q->elevator_lock
blk_mq_quiesce_queue() -> set->srcu
nbd_queue_rq() -> &cmd->lock
nbd_handle_cmd() -> &nsock->tx_lock
nbd_mark_nsock_dead() -> kernel_sock_shutdown()
inet_shutdown() -> lock_sock (sk_lock-AF_INET6)
[sendmsg() -> copy_from_user() -> __might_fault()]
might_lock_read(&mm->mmap_lock)
In Linux memory management, the locking rules dictate:
mm->mmap_lock and hugetlbfs_i_mmap_rwsem_key are acquired prior to GFP_KERNEL memory allocations (which enter fs_reclaim). This ordering is intentional; as documented in fs/hugetlbfs/inode.c, hugetlbfs is non-reclaimable and uses a dedicated lockdep class (hugetlbfs_i_mmap_rwsem_key) specifically because allocations (huge_pmd_share, reservation tracking via allocate_file_region_entries) occur while holding mapping->i_mmap_rwsem.
- Consequently,
fs_reclaim must never depend on mm->mmap_lock. Any lock acquired in the reclaim or block I/O writeback path cannot depend on user-space page faults or mm->mmap_lock.
The defect resides in drivers/block/nbd.c, where NBD violates this invariant through improper socket lifecycle and lockdep management:
-
Failure to enforce lockdep reclassification:
In nbd_add_socket() (drivers/block/nbd.c:1277), NBD receives an open socket file descriptor passed from userspace via sockfd_lookup(), and attempts to reclassify the socket lockdep class using nbd_reclassify_socket():
static void nbd_reclassify_socket(struct socket *sock)
{
struct sock *sk = sock->sk;
if (!sock_allow_reclassification(sk))
return;
...
}
If the socket is concurrently in use or locked when the netlink/ioctl command executes, sock_allow_reclassification(sk) returns false. Unlike other kernel network storage drivers (e.g., CIFS, NVMe-oF, SunRPC) which create their sockets via sock_create_kern() and assert successful reclassification, NBD silently returns without reclassifying the socket. The socket retains the generic lockdep class (here sk_lock-AF_INET6).
-
Shared user-space socket lifecycle and lock_sock -> mmap_lock dependency:
Because the socket originates from user space (sockfd_lookup()), the user process retains its file descriptor. If userspace invokes sendmsg() or recvmsg() on the socket, inet_sendmsg() acquires lock_sock(sk) and calls copy_from_user(), which invokes __might_fault() and records an acquisition of &mm->mmap_lock under lock_sock.
-
Reclaim / block I/O path acquiring lock_sock under nsock->tx_lock:
When dirty pages or anonymous pages are swapped or flushed during memory pressure (fs_reclaim), the block layer issues requests to the NBD device via nbd_queue_rq(). In nbd_queue_rq(), nbd_handle_cmd() acquires mutex_lock(&nsock->tx_lock).
If a transmission error occurs, or if a socket shutdown is triggered via sock_shutdown() / nbd_clear_sock():
mutex_lock(&nsock->tx_lock);
nbd_mark_nsock_dead(nbd, nsock, 0);
mutex_unlock(&nsock->tx_lock);
nbd_mark_nsock_dead() invokes kernel_sock_shutdown(nsock->sock, SHUT_RDWR), calling inet_shutdown(), which acquires lock_sock(sk).
This creates the inverted chain:
fs_reclaim $\rightarrow$ &nsock->tx_lock $\rightarrow$ lock_sock $\rightarrow$ &mm->mmap_lock. When a process holds mm->mmap_lock (or hugetlbfs_i_mmap_rwsem_key) and triggers fs_reclaim, the cycle is complete.