[PATCH] nbd: fix race between nbd_pending_cmd_work and socket teardown
Weisson <[email protected]> Fri, 31 Jul 2026 18:30:51 +0800
| Newsgroups | org.kernel.vger.linux-block,org.kernel.vger.linux-kernel,org.kernel.vger.stable |
|---|---|
| Message-ID | <[email protected]> |
Hi,
This patch fixes a NULL pointer dereference in nbd_pending_cmd_work()
that occurs when socket teardown races with the partial-send worker.
The bug was reported by Peiyang He via syzkaller fuzzing and exists
since commit 8337b029f788 ("nbd: fix partial sending") introduced the
partial-send worker mechanism.
The race sequence:
CPU 0 (submit path) CPU 1 (disconnect path)
───────────────────── ─────────────────────────
nbd_send_cmd() interrupted
was_interrupted() && sent > 0
nbd_sched_pending_work():
nsock->pending = req
schedule_work(&nsock->work)
return BLK_STS_OK
nbd-client -d
sock_shutdown()
┌──────────────────────┐ mutex_lock(&nsock->tx_lock)
│ async gap: work is │ nbd_mark_nsock_dead():
│ queued but kworker │ nsock->dead = true
│ hasn't run yet │ nsock->pending = NULL ← cleared
└──────────────────────┘ mutex_unlock(&nsock->tx_lock)
kworker scheduled
nbd_pending_cmd_work():
req = nsock->pending ← NULL
blk_mq_rq_to_pdu(NULL) ← accesses 0 + 0xf8
*** NULL pointer dereference ***
The root cause is that schedule_work() only enqueues the work item;
actual execution depends on kworker scheduling. Between enqueue and
execution, disconnect can synchronously clear nsock->pending.
The fix establishes single ownership: once nbd_sched_pending_work()
schedules the worker, only the worker may clear nsock->pending and
terminate the request. Socket teardown (nbd_mark_nsock_dead) only
sets nsock->dead without touching the pending request. The worker
checks nsock->dead after each send attempt and completes the request
with BLK_STS_IOERR.
Reproduction:
The natural race window is microseconds wide, so a kprobe is used to
inject a busy-wait at nbd_pending_cmd_work() entry to widen it.
1. Build the kprobe delay module (must use mdelay, not msleep --
kprobe pre-handlers run with preemption disabled):
/* nbd-delay-repro.c */
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
static int delay_ms = 30;
module_param(delay_ms, int, 0644);
static struct kprobe kp;
static int __kprobes handler_pre(struct kprobe *p, struct pt_regs *regs)
{
if (delay_ms > 0)
mdelay(delay_ms);
return 0;
}
static int __init init(void)
{
kp.symbol_name = "nbd_pending_cmd_work";
kp.pre_handler = handler_pre;
return register_kprobe(&kp);
}
static void __exit exit(void)
{
unregister_kprobe(&kp);
}
module_init(init);
module_exit(exit);
MODULE_LICENSE("GPL");
Build with:
make -C /lib/modules/$(uname -r)/build M=$PWD modules
2. Setup environment (forces partial send by filling TCP buffer):
modprobe nbd
insmod nbd-delay-repro.ko delay_ms=30
sysctl -w kernel.panic_on_oops=0
sysctl -w net.ipv4.tcp_wmem='1024 2048 4096'
tc qdisc add dev lo root netem delay 20ms
dd if=/dev/zero of=/tmp/nbd.img bs=1M count=512 status=none
nbd-server 10823 /tmp/nbd.img &
sleep 2
3. Trigger (repeat until crash, typically 1-5 iterations):
nbd-client 127.0.0.1 10823 /dev/nbd1
echo none > /sys/block/nbd1/queue/scheduler
# Writer: 1MB O_DIRECT writes with SIGALRM every 500us
/tmp/nbd-repro-writer 1048576 500 /dev/nbd1 &
WP=$!
sleep 0.05
dmesg -C
nbd-client -d /dev/nbd1
sleep 0.2
kill -9 $WP 2>/dev/null; wait $WP 2>/dev/null
dmesg | grep 'null pointer'
The writer source (compile with gcc -O2 -o /tmp/nbd-repro-writer):
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
void h(int s) {}
int main(int ac, char **av) {
int bs = ac>1 ? atoi(av[1]) : 1048576;
int us = ac>2 ? atoi(av[2]) : 500;
char *buf; posix_memalign((void**)&buf, 4096, bs);
memset(buf, 0xab, bs); signal(SIGALRM, h);
int fd = -1;
while (1) {
if (fd<0) { fd=open(av[3]?av[3]:"/dev/nbd1",
O_WRONLY|O_DIRECT);
if (fd<0) { usleep(100000); continue; } }
ualarm(us, 0);
ssize_t n = write(fd, buf, bs);
if (n<0 && errno!=EINTR && errno!=EAGAIN)
{ close(fd); fd=-1; }
}
}
Key parameters explained:
- "none" scheduler: ensures nbd_queue_rq runs in process context
via __blk_mq_issue_directly, making SIGALRM visible to
sk_stream_wait_memory (kworker threads mask all signals)
- tcp_wmem='1024 2048 4096': tiny send buffer fills immediately
- netem delay 20ms: delays ACKs, keeps buffer full
- mdelay(30) kprobe: widens the async gap to 30ms so disconnect
reliably clears nsock->pending before the worker reads it
4. Expected oops (without fix):
BUG: kernel NULL pointer dereference, address: 00000000000000f8
Oops: Oops: 0000 [#1] SMP NOPTI
Workqueue: events nbd_pending_cmd_work [nbd]
RIP: 0010:nbd_pending_cmd_work+0x22/0x110 [nbd]
CR2: 00000000000000f8
5. With fix applied: 300 iterations, zero crashes, zero
lockdep/WARNING/scheduling-while-atomic reports.
Please review.
Thanks,
Weisson
From 6d985e412da840d5fdf6e64d9a544f2a5f271ec2 Mon Sep 17 00:00:00 2001
From: Weisson <[email protected]>
Date: Fri, 31 Jul 2026 16:30:00 +0800
Subject: [PATCH] nbd: fix race between nbd_pending_cmd_work and socket
teardown
nbd_pending_cmd_work() dereferences nsock->pending without any
synchronization. If nbd_mark_nsock_dead() clears nsock->pending
concurrently, the worker hits a NULL pointer dereference:
BUG: kernel NULL pointer dereference, address: 00000000000000f8
Workqueue: events nbd_pending_cmd_work [nbd]
RIP: 0010:nbd_pending_cmd_work+0x22/0x110 [nbd]
The worker reads nsock->pending (NULL) and immediately passes it to
blk_mq_rq_to_pdu(), which computes (req + 1). With req == NULL this
accesses address 0 + sizeof(struct request) = 0xf8, triggering the
page fault.
The race sequence is:
1. nbd_send_cmd() is interrupted with sent > 0, calls
nbd_sched_pending_work() which sets nsock->pending = req and
calls schedule_work(). The originating thread returns
BLK_STS_OK immediately -- it no longer owns the request.
2. Before the kworker picks up the work item, userspace issues a
disconnect (nbd-client -d). sock_shutdown() takes tx_lock and
calls nbd_mark_nsock_dead() which sets nsock->pending = NULL.
3. The kworker finally runs nbd_pending_cmd_work(), reads the now-
NULL nsock->pending, and passes it to blk_mq_rq_to_pdu() which
dereferences NULL + 0xf8.
Fix this by establishing single ownership: once nbd_sched_pending_work()
schedules the worker, only the worker may clear nsock->pending, release
the config_refs, and terminate the request. Socket teardown
(nbd_mark_nsock_dead) only marks the connection dead but does not touch
the pending request owned by the worker.
The worker checks nsock->dead after each send attempt, and if set,
completes the request with BLK_STS_IOERR. On deadline expiry, the
worker also marks the socket dead since the TCP stream contains an
incomplete NBD message and cannot be reused.
Additionally, nbd_reconnect_socket() now skips nsock slots that still
have a pending request, preventing a new TCP connection from inheriting
stale partial-send state.
Signed-off-by: Weisson <[email protected]>
---
drivers/block/nbd.c | 34 +++++++++++++++++++++++++++++++---
1 file changed, 31 insertions(+), 3 deletions(-)
diff --git a/drivers/block/nbd.c b/drivers/block/nbd.c
index 8f10762e90ef..7a462a626306 100644
--- a/drivers/block/nbd.c
+++ b/drivers/block/nbd.c
@@ -327,8 +327,8 @@ static void nbd_mark_nsock_dead(struct nbd_device *nbd, struct nbd_sock *nsock,
}
}
nsock->dead = true;
- nsock->pending = NULL;
- nsock->sent = 0;
+ if (!nsock->pending)
+ nsock->sent = 0;
}
static int nbd_set_size(struct nbd_device *nbd, loff_t bytesize, loff_t blksize)
@@ -793,8 +793,10 @@ static blk_status_t nbd_send_cmd(struct nbd_device *nbd, struct nbd_cmd *cmd,
*
* We must run from pending work function.
* */
- if (test_bit(NBD_CMD_PARTIAL_SEND, &cmd->flags))
+ if (test_bit(NBD_CMD_PARTIAL_SEND, &cmd->flags)) {
+ nbd_mark_nsock_dead(nbd, nsock, 1);
return BLK_STS_OK;
+ }
/* retry on a different socket */
dev_err_ratelimited(disk_to_dev(nbd->disk),
@@ -826,8 +828,18 @@ static void nbd_pending_cmd_work(struct work_struct *work)
if (!nsock->pending)
break;
+ if (nsock->dead)
+ goto dead;
+
/* don't bother timeout handler for partial sending */
if (READ_ONCE(jiffies) + msecs_to_jiffies(wait_ms) >= deadline) {
+ /*
+ * The socket contains a partially transmitted request
+ * and cannot be reused for another NBD request.
+ */
+ nbd_mark_nsock_dead(nbd, nsock, 1);
+ nsock->pending = NULL;
+ nsock->sent = 0;
cmd->status = BLK_STS_IOERR;
blk_mq_complete_request(req);
break;
@@ -840,6 +852,18 @@ static void nbd_pending_cmd_work(struct work_struct *work)
out:
mutex_unlock(&cmd->lock);
nbd_config_put(nbd);
+ return;
+
+ /* Complete the request here; nbd_clear_req() will not handle it. */
+dead:
+ nsock->pending = NULL;
+ nsock->sent = 0;
+ mutex_unlock(&nsock->tx_lock);
+ clear_bit(NBD_CMD_PARTIAL_SEND, &cmd->flags);
+ cmd->status = BLK_STS_IOERR;
+ mutex_unlock(&cmd->lock);
+ blk_mq_complete_request(req);
+ nbd_config_put(nbd);
}
static int nbd_read_reply(struct nbd_device *nbd, struct socket *sock,
@@ -1376,6 +1400,10 @@ static int nbd_reconnect_socket(struct nbd_device *nbd, unsigned long arg)
mutex_unlock(&nsock->tx_lock);
continue;
}
+ if (nsock->pending) {
+ mutex_unlock(&nsock->tx_lock);
+ continue;
+ }
sk_set_memalloc(sock->sk);
if (nbd->tag_set.timeout)
sock->sk->sk_sndtimeo = nbd->tag_set.timeout;
--
2.47.3