syzbot
BUG: spinlock already unlocked in corrupted
Status:
upstream: reported C repro on 2026/11/05 11:09
Labels:
prio:low
[Documentation on labels]
Reported-by: syzbot+b29ed72382b0e218323a@syzkaller.appspotmail.com
First crash: 6h39m, last: 6h39m
✨ AI-generated root cause summary:
In drivers/block/nbd.c, nbd_add_socket() retrieves a user-supplied socket fd via nbd_get_socket() and invokes nbd_reclassify_socket(sock) to assign NBD-specific lockdep classes to the socket. When CONFIG_DEBUG_LOCK_ALLOC is enabled, nbd_reclassify_socket() performs an unsynchronized check sock_allow_reclassification(sk) and calls sock_lock_init_class_and_name(sk, ...), which is an initialization macro that not only sets lockdep keys but also resets sk->sk_lock.owned to 0, reinitializes sk->sk_lock.wq, and calls spin_lock_init(&sk->sk_lock.slock). Because userspace retains the socket fd, concurrent socket operations (such as inet_shutdown() calling lock_sock() or release_sock()) can acquire sk->sk_lock.slock immediately after the check; spin_lock_init() then reinitializes the active lock to the unlocked state, causing debug_spin_unlock() to trigger a "BUG: spinlock already unlocked" kernel panic.
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
1. Involved Components and Functions
-
Files:
-
Structures and Fields:
struct socket: Represents the BSD socket; contains struct sock *sk.
struct sock: Represents the network-layer socket; contains socket_lock_t sk_lock.
socket_lock_t: Contains spinlock slock, user ownership flag owned, waitqueue wq, and lockdep map dep_map.
2. Technical Mechanism and Trigger Sequence
-
User Socket Ingestion into NBD:
In drivers/block/nbd.c, when userspace issues an ioctl such as NBD_SET_SOCK or sends a netlink connect message, nbd_add_socket() resolves the user-supplied file descriptor into a struct socket * via nbd_get_socket(nbd, arg, &err). Unlike kernel-internal sockets (e.g., in NVMe-oF or SunRPC, which are allocated via sock_create_kern() and never exposed to userspace), the NBD socket is an active, open file descriptor owned by userspace. Userspace threads can freely issue concurrent syscalls (e.g., shutdown(), send(), recv()) or attach the same socket fd across multiple NBD devices.
-
Lock Reclassification Mechanism:
To prevent lockdep from flagging circular dependencies between filesystem/block memory reclaim (fs_reclaim) and socket locks (sk_lock), nbd_add_socket() invokes nbd_reclassify_socket(sock). Under #ifdef CONFIG_DEBUG_LOCK_ALLOC, this function checks whether the socket can be reclassified:
static inline bool sock_allow_reclassification(const struct sock *csk)
{
struct sock *sk = (struct sock *)csk;
return !sock_owned_by_user_nocheck(sk) &&
!spin_is_locked(&sk->sk_lock.slock);
}
This check performs an unsynchronized read of sk->sk_lock.owned and sk->sk_lock.slock without acquiring either lock. Immediately after this condition evaluates to true, a concurrent thread can lock the socket.
-
Reinitialization of an Active Lock:
nbd_reclassify_socket() then invokes sock_lock_init_class_and_name():
#define sock_lock_init_class_and_name(sk, sname, skey, name, key) \
do { \
sk_owner_set(sk, THIS_MODULE); \
sk->sk_lock.owned = 0; \
init_waitqueue_head(&sk->sk_lock.wq); \
spin_lock_init(&(sk)->sk_lock.slock); \
debug_check_no_locks_freed((void *)&(sk)->sk_lock, \
sizeof((sk)->sk_lock)); \
lockdep_set_class_and_name(&(sk)->sk_lock.slock, \
(skey), (sname)); \
lockdep_init_map(&(sk)->sk_lock.dep_map, (name), (key), 0); \
} while (0)
Although called to update lockdep class annotations, sock_lock_init_class_and_name() was originally written for fresh socket initialization in sock_lock_init() (net/core/sock.c). Consequently, it unconditionally resets:
sk->sk_lock.slock via spin_lock_init() (resetting the raw spinlock to 0 and its owner to unowned).
sk->sk_lock.owned = 0 (resetting the user-space ownership flag).
sk->sk_lock.wq via init_waitqueue_head() (reinitializing the waitqueue head).
-
The Race Condition with inet_shutdown:
3. Configuration Guard
nbd_reclassify_socket() is explicitly guarded by #ifdef CONFIG_DEBUG_LOCK_ALLOC in drivers/block/nbd.c:
#ifdef CONFIG_DEBUG_LOCK_ALLOC
static void nbd_reclassify_socket(struct socket *sock) { ... }
#else
static inline void nbd_reclassify_socket(struct socket *sock) {}
#endif
In builds without CONFIG_DEBUG_LOCK_ALLOC (standard production kernels), nbd_reclassify_socket() compiles to an empty inline stub, so reclassification and destructive lock reinitialization do not occur.
Sample crash report:
Warning: Permanently added '10.128.0.74' (ED25519) to the list of known hosts.
[*] Starting reproducer for BUG: spinlock already unlocked in inet_shutdown...
[+] Detected 2 online CPUs.
[+] open /dev/nbd0 successful (fd=3).
[+] open /dev/nbd1 successful (fd=3).
[+] open /dev/nbd2 successful (fd=3).
[+] open /dev/nbd3 successful (fd=3).
[+] open /dev/nbd4 successful (fd=3).
[+] open /dev/nbd5 successful (fd=3).
[+] open /dev/nbd6 successful (fd=3).
[+] open /dev/nbd7 successful (fd=3).
[+] open /dev/nbd8 successful (fd=3).
[+] open /dev/nbd9 successful (fd=3).
[+] open /dev/nbd10 successful (fd=3).
[+] All 11 NBD devices initialized and verified.
[+] Server socket created.
[+] setsockopt(SO_REUSEADDR) successful.
[+] Server socket bound to 127.0.0.1.
[+] getsockname successful (port=43427).
[+] Server socket listening.
[+] Client socket created.
[+] Client socket connected.
[+] Accepted client connection on server.
[+] Server listening socket closed.
[+] Accepted peer socket closed (peer EOF).
[+] setsockopt(SOL_TCP, TCP_REPAIR, TCP_REPAIR_ON) successful.
[+] Shared TCP socket ready (fd=4).
[+] Started 5 reclassifier threads on devices 0..4.
[+] Started 6 runner threads on devices 5..10.
[+] Started disconnector thread.
[*] Racing NBD_SET_SOCK against NBD_DO_IT (recv_work) and inet_shutdown...
* Struck through repros no longer work on HEAD.