ublk: consequences of a dead server are only failing on the last /dev/ublkcN release
Christian Brauner <[email protected]>
| Newsgroups | org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-block |
|---|---|
| Message-ID | <20260918-eisbrecher-toilette-worum-eb11f777c563@brauner> |
Hi,
Chris scanned a new series of mine for me and he reported some
interesting behavior that I dug into.
A ublk request that was handed to the server is only failed once the
last reference to /dev/ublkcN is dropped. So holding the file open keeps
the request alive.
This causes any waiter to be stuck in D state and blocks STOP_DEV.
I'm attaching two reproducers:
(1) ublk_inherited_fd_test.c the general case, no vfork involved
(2) vfork_ublk_test.c the holder of the reference is the waiter itself
Re (1):
A single-threaded ublk server fork()s a helper. The helper inherits the
fdtable including /dev/ublkcN. Now another process opens /dev/ublkbN and
writes 4 kb then fsync()s. The servers gets a WRITE and _exit()s without
completing it - say it crashes:
- The writer sleeps in folio_wait_writeback() in D state.
- STOP_DEV issued from another process never returns.
The control command runs on an io-wq worker of that process, which
calls ublk_stop_dev() -> del_gendisk() -> bdev_mark_dead() ->
filemap_write_and_wait_range() and ends up waiting on the page that
won't be written back.
Said process can't exit and io_wq_put_and_exit() waits for the
worker.
This can only be undone if the helper is SIGKILLed:
# ublk0 up, server 119
# helper 123 forked by the server, holds the inherited fds
# WRITER fsync
# server 119 died with the WRITE in hand
# 2s after the server died:
# writer pid 124, state D:
# folio_wait_bit_common
# folio_wait_writeback
# __filemap_fdatawait_range
# file_write_and_wait_range
# blkdev_fsync
# do_fsync
# STOP_DEV from pid 125 has not returned after 5s:
# STOP_DEV issuer thread 126:
# folio_wait_bit_common
# folio_wait_writeback
# __filemap_fdatawait_range
# filemap_write_and_wait_range
# bdev_mark_dead
# blk_report_disk_dead
# __del_gendisk
# del_gendisk
# ublk_stop_dev_unlocked.part.0
# ublk_ctrl_uring_cmd
# io_uring_cmd
# io_wq_submit_work
# io_worker_handle_work
# io_wq_worker
# killing helper 123
# WRITER fsync returned -1 errno 5
# STOP_DEV returned after the helper died
What happens to a device after a server crash depends on whoever else
holds /dev/ublkcN open: A forked helper, a process that got the fd via
SCM_RIGHTS, or a vfork child.
I'm not sure whether that's intended semantics but it surely has
confusion potential and should probably be at least documented.
I suppose failing outstanding requests on server crash is intentionally
not done for recover reasons or whatever. But maybe STOP_DEV should be
made to work.
Re (2):
vfork() makes it really ugly. Say a single-threaded ublk server vfork()s
a child. The child opens /dev/ublkbN, writes some stuff and closes the
fd. Since close(2) runs bdev_release() -> sync_blockdev() it waits for
the write.
The write gets dispatched to the server's task work. But the server
sleeps in wait_for_vfork_done() until the child execs or exits.
But the child cannot exec or exit because close(2) doesn't return.
ublk makes this more hairy though. Even a SIGKILL sent to the server aka
the parent won't help. The vfork()ed child hangs on the close(2).
So that stays in D state forever with an undeletable device:
server, after SIGKILL: gone
child:
State: D (disk sleep)
folio_wait_bit_common
folio_wait_writeback
__filemap_fdatawait_range
filemap_write_and_wait_range
bdev_release
blkdev_release
__fput
fput_close_sync
__x64_sys_close
Unrelated, but also a potential issue:
The commit 7fc4da6a304b ("ublk: scan partition in async way") made
partition scans run from a workqueue that holds disk->open_mutex while
the server serves its reads and START_DEV returns before it's finished.
Any server that treats START_DEV's return as a signal that the disk is
ready and doesn't serve requests hangs every open() of /dev/ublkbN on
uninterruptible on open_mutex. You can combine that with (1) and (2).
Thanks,
Christian
--
ublk_inherited_fd_test.c
(text/x-csrc, 14.4 KB)
// SPDX-License-Identifier: GPL-2.0
/*
* A ublk server hands a request to userspace and dies while a plain
* fork()ed helper still holds the inherited /dev/ublkcN descriptor.
*
* ublk only fails a request that was handed to the server from
* ublk_ch_release_work_fn(), i.e. when the last reference to the char
* device is dropped. As long as the helper lives:
*
* - the writer whose page is in that request sleeps in
* folio_wait_writeback() in D state,
* - STOP_DEV from another process wedges its io-wq worker in
* ublk_stop_dev() -> del_gendisk() behind the same request.
*
* Killing the helper releases the char device, the request fails with
* -EIO and both waiters continue. No request timeout applies to a
* privileged device (ublk_timeout() only acts on UBLK_F_UNPRIVILEGED_DEV).
*
* Steps, all reported on stdout:
* 1. server: ADD_DEV, SET_PARAMS, open /dev/ublkcN, FETCH_REQ x depth
* 2. monitor: START_DEV, wait for the partition scan, open the disk
* 3. server: fork() a helper that sleeps (inherits every fd)
* 4. writer: open /dev/ublkbN, write 4 KiB, fsync()
* 5. server: receives the WRITE and _exit()s without completing it
* 6. monitor: writer is in D state; STOP_DEV wedges
* 7. monitor: SIGKILL the helper; fsync() returns -EIO, STOP_DEV returns
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <liburing.h>
#include <linux/ublk_cmd.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define QD 4
#define DEV_SIZE (4UL << 20)
#define IO_BUF_BYTES (64U << 10)
#define UD_GO 1000
struct ctrl {
struct io_uring ring;
int fd;
};
static double now(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
static int ctrl_open(struct ctrl *c)
{
c->fd = open("/dev/ublk-control", O_RDWR);
if (c->fd < 0)
return -errno;
return io_uring_queue_init(8, &c->ring, IORING_SETUP_SQE128);
}
static int ctrl_cmd(struct ctrl *c, __u32 cmd_op, int dev_id, void *addr,
__u16 len, __u64 data0)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&c->ring);
struct ublksrv_ctrl_cmd *cmd;
struct io_uring_cqe *cqe;
int ret;
memset(sqe, 0, 128);
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd = c->fd;
sqe->cmd_op = cmd_op;
cmd = (struct ublksrv_ctrl_cmd *)sqe->cmd;
cmd->dev_id = dev_id;
cmd->queue_id = -1;
cmd->addr = (__u64)(uintptr_t)addr;
cmd->len = len;
cmd->data[0] = data0;
ret = io_uring_submit(&c->ring);
if (ret < 0)
return ret;
ret = io_uring_wait_cqe(&c->ring, &cqe);
if (ret < 0)
return ret;
ret = cqe->res;
io_uring_cqe_seen(&c->ring, cqe);
return ret;
}
static void queue_io_cmd(struct io_uring *ring, int cfd, __u32 cmd_op, int tag,
void *buf, int result)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
struct ublksrv_io_cmd *cmd;
memset(sqe, 0, 128);
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd = cfd;
sqe->cmd_op = cmd_op;
cmd = (struct ublksrv_io_cmd *)sqe->cmd;
cmd->q_id = 0;
cmd->tag = tag;
cmd->addr = (__u64)(uintptr_t)buf;
cmd->result = result;
sqe->user_data = tag;
}
static int server(int to_mon, int from_mon)
{
struct ublksrv_ctrl_dev_info info = {
.nr_hw_queues = 1,
.queue_depth = QD,
.max_io_buf_bytes = IO_BUF_BYTES,
.dev_id = -1,
};
struct ublk_params params = {
.types = UBLK_PARAM_TYPE_BASIC,
.basic = {
.logical_bs_shift = 9,
.physical_bs_shift = 12,
.io_opt_shift = 12,
.io_min_shift = 9,
.max_sectors = IO_BUF_BYTES >> 9,
.dev_sectors = DEV_SIZE >> 9,
},
};
struct io_uring ring;
struct ctrl c;
void *bufs[QD], *desc;
char *ram, cpath[64], go;
size_t desc_size, cmd_buf_size;
int ret, dev_id, cfd = -1, i, armed = 0;
ret = ctrl_open(&c);
if (ret < 0) {
dprintf(to_mon, "FAIL ctrl_open %d\n", ret);
return 1;
}
ret = ctrl_cmd(&c, UBLK_U_CMD_ADD_DEV, -1, &info, sizeof(info), 0);
if (ret < 0) {
dprintf(to_mon, "FAIL ADD_DEV %d\n", ret);
return 1;
}
dev_id = info.dev_id;
desc_size = info.io_desc_size ?: sizeof(struct ublksrv_io_desc);
params.len = sizeof(params);
ret = ctrl_cmd(&c, UBLK_U_CMD_SET_PARAMS, dev_id, ¶ms, sizeof(params), 0);
if (ret < 0) {
dprintf(to_mon, "FAIL SET_PARAMS %d\n", ret);
return 1;
}
snprintf(cpath, sizeof(cpath), "/dev/ublkc%d", dev_id);
for (i = 0; i < 200 && cfd < 0; i++) {
cfd = open(cpath, O_RDWR);
if (cfd < 0)
usleep(10000);
}
if (cfd < 0) {
dprintf(to_mon, "FAIL open %s %d\n", cpath, -errno);
return 1;
}
cmd_buf_size = (QD * desc_size + 4095) & ~4095UL;
desc = mmap(NULL, cmd_buf_size, PROT_READ, MAP_SHARED | MAP_POPULATE,
cfd, UBLKSRV_CMD_BUF_OFFSET);
if (desc == MAP_FAILED) {
dprintf(to_mon, "FAIL mmap desc %d\n", -errno);
return 1;
}
for (i = 0; i < QD; i++) {
if (posix_memalign(&bufs[i], 4096, IO_BUF_BYTES)) {
dprintf(to_mon, "FAIL memalign\n");
return 1;
}
}
ram = calloc(1, DEV_SIZE);
if (!ram) {
dprintf(to_mon, "FAIL alloc\n");
return 1;
}
ret = io_uring_queue_init(32, &ring, IORING_SETUP_SQE128);
if (ret < 0) {
dprintf(to_mon, "FAIL io ring %d\n", ret);
return 1;
}
for (i = 0; i < QD; i++)
queue_io_cmd(&ring, cfd, UBLK_U_IO_FETCH_REQ, i, bufs[i], 0);
ret = io_uring_submit(&ring);
if (ret != QD) {
dprintf(to_mon, "FAIL FETCH submit %d\n", ret);
return 1;
}
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, from_mon, &go, 1, 0);
sqe->user_data = UD_GO;
}
dprintf(to_mon, "READY %d\n", dev_id);
for (;;) {
struct __kernel_timespec ts = { .tv_nsec = 100 * 1000 * 1000 };
struct io_uring_cqe *cqe;
io_uring_submit(&ring);
ret = io_uring_wait_cqe_timeout(&ring, &cqe, &ts);
while (ret == 0) {
__u64 ud = cqe->user_data;
int res = cqe->res;
io_uring_cqe_seen(&ring, cqe);
if (ud == UD_GO) {
pid_t helper = fork();
if (helper == 0) {
/* An ordinary helper: inherits every fd, idles. */
for (;;)
pause();
}
dprintf(to_mon, "HELPER %d\n", helper);
armed = 1;
} else if (res == UBLK_IO_RES_OK) {
const struct ublksrv_io_desc *iod = desc + ud * desc_size;
__u8 op = iod->op_flags & 0xff;
__u64 off = iod->start_sector << 9;
__u32 len = iod->nr_sectors << 9;
int result = len;
dprintf(to_mon, "IO op=%u sector=%llu n=%u\n", op,
(unsigned long long)iod->start_sector,
iod->nr_sectors);
if (armed && op == UBLK_IO_OP_WRITE) {
/* The crash: die with the request in hand. */
dprintf(to_mon, "DYING with the write in hand\n");
_exit(1);
}
if (off + len > DEV_SIZE) {
result = -EIO;
} else if (op == UBLK_IO_OP_READ) {
memcpy(bufs[ud], ram + off, len);
} else if (op == UBLK_IO_OP_WRITE) {
memcpy(ram + off, bufs[ud], len);
} else if (op == UBLK_IO_OP_FLUSH) {
result = 0;
} else if (op == UBLK_IO_OP_DISCARD ||
op == UBLK_IO_OP_WRITE_ZEROES) {
memset(ram + off, 0, len);
} else {
result = -EOPNOTSUPP;
}
queue_io_cmd(&ring, cfd, UBLK_U_IO_COMMIT_AND_FETCH_REQ,
ud, bufs[ud], result);
} else {
dprintf(to_mon, "DEAD %d\n", res);
return 1;
}
ret = io_uring_peek_cqe(&ring, &cqe);
}
}
return 0;
}
static int writer(int to_mon, const char *bpath)
{
static char buf[4096] __attribute__((aligned(4096)));
int fd, ret, err;
fd = open(bpath, O_RDWR);
if (fd < 0) {
dprintf(to_mon, "WRITER open failed %d\n", -errno);
return 1;
}
memset(buf, 'u', sizeof(buf));
if (write(fd, buf, sizeof(buf)) != sizeof(buf)) {
dprintf(to_mon, "WRITER write failed %d\n", -errno);
return 1;
}
dprintf(to_mon, "WRITER fsync\n");
ret = fsync(fd);
err = errno;
dprintf(to_mon, "WRITER fsync returned %d errno %d\n", ret, ret ? err : 0);
close(fd);
return 0;
}
/* Read one line from @fd with a deadline. Returns 0 on EOF, -1 on timeout. */
static int read_line(int fd, char *buf, size_t len, double deadline)
{
size_t n = 0;
while (n + 1 < len) {
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int ms = (int)((deadline - now()) * 1000);
ssize_t r;
if (ms <= 0 || poll(&pfd, 1, ms) <= 0)
return -1;
r = read(fd, buf + n, 1);
if (r <= 0)
return 0;
if (buf[n++] == '\n')
break;
}
buf[n] = 0;
return 1;
}
static void cat(const char *path, const char *prefix)
{
char buf[4096];
ssize_t n;
int fd = open(path, O_RDONLY);
if (fd < 0)
return;
n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n <= 0)
return;
buf[n] = 0;
for (char *p = buf, *e; p && *p; p = e) {
e = strchr(p, '\n');
if (e)
*e++ = 0;
printf("# %s%s\n", prefix, p);
}
}
static int task_state(pid_t pid)
{
char path[64], line[256];
int st = 0;
FILE *f;
snprintf(path, sizeof(path), "/proc/%d/status", pid);
f = fopen(path, "r");
if (!f)
return 0;
while (fgets(line, sizeof(line), f)) {
if (!strncmp(line, "State:", 6)) {
st = line[7];
break;
}
}
fclose(f);
return st;
}
static void dump_task(pid_t pid, const char *what)
{
char path[64];
printf("# %s pid %d, state %c:\n", what, pid, task_state(pid) ?: '?');
snprintf(path, sizeof(path), "/proc/%d/wchan", pid);
cat(path, "wchan: ");
snprintf(path, sizeof(path), "/proc/%d/stack", pid);
cat(path, "");
}
static void dump_threads(pid_t pid, const char *what)
{
char path[64];
int tid;
/* io-wq workers are threads of the issuing task. */
for (tid = pid + 1; tid < pid + 64; tid++) {
snprintf(path, sizeof(path), "/proc/%d/task/%d/stack", pid, tid);
if (!access(path, F_OK)) {
printf("# %s thread %d:\n", what, tid);
cat(path, "");
}
}
}
int main(int argc, char **argv)
{
int a[2], b[2], dev_id = -1, status, ret, i, fd, scan_reads = 0;
int reproduced = 0, err = -1;
char line[128], bpath[64];
pid_t srv, helper = 0, wr = 0, stopper = 0;
double deadline;
struct ctrl mc;
setvbuf(stdout, NULL, _IONBF, 0);
if (access("/dev/ublk-control", F_OK)) {
printf("not ok 1 # no /dev/ublk-control\n");
return 1;
}
if (pipe(a) || pipe(b)) {
printf("not ok 1 # pipe: %m\n");
return 1;
}
srv = fork();
if (srv == 0) {
close(a[0]);
close(b[1]);
_exit(server(a[1], b[0]));
}
close(b[0]);
deadline = now() + 10;
if (read_line(a[0], line, sizeof(line), deadline) <= 0 ||
sscanf(line, "READY %d", &dev_id) != 1) {
printf("not ok 1 # server setup: %s", line);
goto out;
}
if (ctrl_open(&mc) < 0) {
printf("not ok 1 # ctrl_open: %m\n");
goto out;
}
ret = ctrl_cmd(&mc, UBLK_U_CMD_START_DEV, dev_id, NULL, 0, srv);
if (ret < 0) {
printf("not ok 1 # START_DEV %d\n", ret);
goto out;
}
snprintf(bpath, sizeof(bpath), "/dev/ublkb%d", dev_id);
for (i = 0; i < 200 && access(bpath, F_OK); i++)
usleep(10000);
/* Let the asynchronous partition scan finish before anything else. */
deadline = now() + 10;
while (!scan_reads) {
unsigned op;
if (read_line(a[0], line, sizeof(line), deadline) <= 0) {
printf("not ok 1 # no partition scan read: %s", line);
goto out;
}
if (sscanf(line, "IO op=%u", &op) == 1 && op == UBLK_IO_OP_READ)
scan_reads++;
}
for (i = 0; i < 100; i++) {
fd = open(bpath, O_RDONLY);
if (fd >= 0 || errno != ENXIO)
break;
usleep(100000);
}
if (fd < 0) {
printf("not ok 1 # open %s: %m\n", bpath);
goto out;
}
close(fd);
printf("# ublk%d up, server %d\n", dev_id, srv);
/* 3. the server forks its helper */
if (write(b[1], "g", 1) != 1)
goto out;
deadline = now() + 10;
for (;;) {
if (read_line(a[0], line, sizeof(line), deadline) <= 0) {
printf("not ok 1 # no helper: %s", line);
goto out;
}
if (sscanf(line, "HELPER %d", &helper) == 1)
break;
}
printf("# helper %d forked by the server, holds the inherited fds\n", helper);
/* 4. + 5. the writer and the crash */
wr = fork();
if (wr == 0) {
close(b[1]);
_exit(writer(a[1], bpath));
}
close(a[1]);
deadline = now() + 10;
for (;;) {
if (read_line(a[0], line, sizeof(line), deadline) <= 0) {
printf("not ok 1 # waiting for the crash: %s", line);
goto out;
}
if (!strncmp(line, "DYING", 5)) {
printf("# server %d died with the WRITE in hand\n", srv);
break;
}
if (!strncmp(line, "WRITER", 6))
printf("# %s", line);
}
waitpid(srv, &status, 0);
srv = 0;
/* 6. the writer is stuck, STOP_DEV wedges */
usleep(2000000);
printf("# 2s after the server died:\n");
dump_task(wr, "writer");
if (task_state(wr) == 'D')
reproduced = 1;
stopper = fork();
if (stopper == 0) {
struct ctrl sc;
if (ctrl_open(&sc) == 0)
ctrl_cmd(&sc, UBLK_U_CMD_STOP_DEV, dev_id, NULL, 0, 0);
_exit(0);
}
deadline = now() + 5;
while (now() < deadline && waitpid(stopper, &status, WNOHANG) != stopper)
usleep(100000);
if (task_state(stopper)) {
printf("# STOP_DEV from pid %d has not returned after 5s:\n", stopper);
dump_task(stopper, "STOP_DEV issuer");
dump_threads(stopper, "STOP_DEV issuer");
} else {
printf("# STOP_DEV returned\n");
stopper = 0;
}
/* 7. drop the helper's reference */
printf("# killing helper %d\n", helper);
kill(helper, SIGKILL);
waitpid(helper, &status, 0);
helper = 0;
deadline = now() + 10;
for (;;) {
int r;
if (read_line(a[0], line, sizeof(line), deadline) <= 0) {
printf("# writer still stuck 10s after the helper died\n");
dump_task(wr, "writer");
break;
}
if (sscanf(line, "WRITER fsync returned %d errno %d", &r, &err) == 2) {
printf("# %s", line);
break;
}
}
if (stopper) {
deadline = now() + 10;
while (now() < deadline && waitpid(stopper, &status, WNOHANG) != stopper)
usleep(100000);
printf("# STOP_DEV %s after the helper died\n",
task_state(stopper) ? "still stuck" : "returned");
if (task_state(stopper))
kill(stopper, SIGKILL);
stopper = 0;
}
if (reproduced && err == EIO)
printf("ok 1 request handed to a dead server is only failed when the inherited char dev fd goes away\n");
else if (reproduced)
printf("not ok 1 # writer stuck (reproduced) but did not get -EIO after the helper died\n");
else
printf("not ok 1 # writer was not stuck 2s after the server died\n");
out:
if (srv > 0)
kill(srv, SIGKILL);
if (helper > 0)
kill(helper, SIGKILL);
if (wr > 0) {
deadline = now() + 5;
while (now() < deadline && waitpid(wr, &status, WNOHANG) != wr)
usleep(100000);
}
if (dev_id >= 0) {
pid_t del = fork();
if (del == 0) {
struct ctrl dc;
if (ctrl_open(&dc) == 0)
ctrl_cmd(&dc, UBLK_U_CMD_DEL_DEV, dev_id, NULL, 0, 0);
_exit(0);
}
deadline = now() + 10;
while (now() < deadline && waitpid(del, &status, WNOHANG) != del)
usleep(100000);
if (task_state(del)) {
printf("# DEL_DEV stuck too\n");
kill(del, SIGKILL);
}
}
return !reproduced;
}
vfork_ublk_test.c
(text/x-csrc, 17.1 KB)
// SPDX-License-Identifier: GPL-2.0
/*
* A single-threaded ublk server vforks a child that opens the block
* device the server serves, dirties a page and then either execs (the
* file is O_CLOEXEC), closes the file or exits.
*
* The final release of the block device runs sync_blockdev() and the
* write can only be served once the parent runs again.
*
* exec: the release runs from close_cloexec_files() in exec.
* close: the release runs from close(2), synchronous since v6.6.
* exit: the release runs from exit_files(), after exit_mm() woke
* the parent.
*
* A monitor process issues START_DEV, waits for the asynchronous
* partition scan (its reads are served by the server and it holds the
* disk's open_mutex, so the server must not vfork before it is done),
* and kills a server that is stuck in vfork.
*/
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <liburing.h>
#include <linux/ublk_cmd.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define QD 4
#define DEV_SIZE (4UL << 20)
#define IO_BUF_BYTES (64U << 10)
#define HANG_SECS 15
#define STACK_SIZE (256 * 1024)
#define UD_GO 1000
enum mode { MODE_EXEC, MODE_CLOSE, MODE_EXIT };
static const char *mode_name[] = { "exec", "close", "exit" };
struct ctrl {
struct io_uring ring;
int fd;
};
static double now(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec + ts.tv_nsec / 1e9;
}
static int ctrl_open(struct ctrl *c)
{
c->fd = open("/dev/ublk-control", O_RDWR);
if (c->fd < 0)
return -errno;
return io_uring_queue_init(8, &c->ring, IORING_SETUP_SQE128);
}
static int ctrl_cmd(struct ctrl *c, __u32 cmd_op, int dev_id, void *addr,
__u16 len, __u64 data0)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&c->ring);
struct ublksrv_ctrl_cmd *cmd;
struct io_uring_cqe *cqe;
int ret;
memset(sqe, 0, 128);
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd = c->fd;
sqe->cmd_op = cmd_op;
cmd = (struct ublksrv_ctrl_cmd *)sqe->cmd;
cmd->dev_id = dev_id;
cmd->queue_id = -1;
cmd->addr = (__u64)(uintptr_t)addr;
cmd->len = len;
cmd->data[0] = data0;
ret = io_uring_submit(&c->ring);
if (ret < 0)
return ret;
ret = io_uring_wait_cqe(&c->ring, &cqe);
if (ret < 0)
return ret;
ret = cqe->res;
io_uring_cqe_seen(&c->ring, cqe);
return ret;
}
static void queue_io_cmd(struct io_uring *ring, int cfd, __u32 cmd_op, int tag,
void *buf, int result)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
struct ublksrv_io_cmd *cmd;
memset(sqe, 0, 128);
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd = cfd;
sqe->cmd_op = cmd_op;
cmd = (struct ublksrv_io_cmd *)sqe->cmd;
cmd->q_id = 0;
cmd->tag = tag;
cmd->addr = (__u64)(uintptr_t)buf;
cmd->result = result;
sqe->user_data = tag;
}
struct child_args {
const char *self;
const char *bdev;
enum mode mode;
int to_mon;
};
static char child_buf[4096] __attribute__((aligned(4096)));
static void child_say(int fd, const char *s)
{
if (write(fd, s, strlen(s)) < 0)
_exit(20);
}
/* Runs on its own stack in the parent's mm. Only syscalls, never returns. */
static int child_fn(void *arg)
{
struct child_args *a = arg;
char *const argv[] = { (char *)a->self, "--child", NULL };
int flags = O_RDWR | (a->mode == MODE_EXEC ? O_CLOEXEC : 0);
int fd;
fd = open(a->bdev, flags);
if (fd < 0)
_exit(11);
child_say(a->to_mon, "CHILD opened\n");
memset(child_buf, 'u', sizeof(child_buf));
if (write(fd, child_buf, sizeof(child_buf)) != sizeof(child_buf))
_exit(12);
child_say(a->to_mon, "CHILD wrote\n");
switch (a->mode) {
case MODE_EXEC:
child_say(a->to_mon, "CHILD exec\n");
execv(a->self, argv);
_exit(13);
case MODE_CLOSE:
child_say(a->to_mon, "CHILD close\n");
if (close(fd))
_exit(14);
child_say(a->to_mon, "CHILD closed\n");
_exit(0);
case MODE_EXIT:
child_say(a->to_mon, "CHILD exit\n");
_exit(0);
}
_exit(15);
}
static int server(enum mode mode, int to_mon, int from_mon, const char *self)
{
struct ublksrv_ctrl_dev_info info = {
.nr_hw_queues = 1,
.queue_depth = QD,
.max_io_buf_bytes = IO_BUF_BYTES,
.dev_id = -1,
};
struct ublk_params params = {
.types = UBLK_PARAM_TYPE_BASIC,
.basic = {
.logical_bs_shift = 9,
.physical_bs_shift = 12,
.io_opt_shift = 12,
.io_min_shift = 9,
.max_sectors = IO_BUF_BYTES >> 9,
.dev_sectors = DEV_SIZE >> 9,
},
};
struct child_args args = { .self = self, .mode = mode, .to_mon = to_mon };
struct io_uring ring;
struct ctrl c;
void *bufs[QD], *desc, *stack;
char *ram, cpath[64], bpath[64], go;
size_t desc_size, cmd_buf_size;
int ret, dev_id, cfd = -1, i, status;
pid_t child = 0;
double t0 = 0, t_vfork = 0;
ret = ctrl_open(&c);
if (ret < 0) {
dprintf(to_mon, "FAIL ctrl_open %d\n", ret);
return 1;
}
ret = ctrl_cmd(&c, UBLK_U_CMD_ADD_DEV, -1, &info, sizeof(info), 0);
if (ret < 0) {
dprintf(to_mon, "FAIL ADD_DEV %d\n", ret);
return 1;
}
dev_id = info.dev_id;
desc_size = info.io_desc_size ?: sizeof(struct ublksrv_io_desc);
params.len = sizeof(params);
ret = ctrl_cmd(&c, UBLK_U_CMD_SET_PARAMS, dev_id, ¶ms, sizeof(params), 0);
if (ret < 0) {
dprintf(to_mon, "FAIL SET_PARAMS %d\n", ret);
return 1;
}
snprintf(cpath, sizeof(cpath), "/dev/ublkc%d", dev_id);
snprintf(bpath, sizeof(bpath), "/dev/ublkb%d", dev_id);
args.bdev = bpath;
for (i = 0; i < 200 && cfd < 0; i++) {
cfd = open(cpath, O_RDWR);
if (cfd < 0)
usleep(10000);
}
if (cfd < 0) {
dprintf(to_mon, "FAIL open %s %d\n", cpath, -errno);
return 1;
}
cmd_buf_size = (QD * desc_size + 4095) & ~4095UL;
desc = mmap(NULL, cmd_buf_size, PROT_READ, MAP_SHARED | MAP_POPULATE,
cfd, UBLKSRV_CMD_BUF_OFFSET);
if (desc == MAP_FAILED) {
dprintf(to_mon, "FAIL mmap desc %d\n", -errno);
return 1;
}
for (i = 0; i < QD; i++) {
if (posix_memalign(&bufs[i], 4096, IO_BUF_BYTES)) {
dprintf(to_mon, "FAIL memalign\n");
return 1;
}
}
ram = calloc(1, DEV_SIZE);
stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (!ram || stack == MAP_FAILED) {
dprintf(to_mon, "FAIL alloc\n");
return 1;
}
ret = io_uring_queue_init(32, &ring, IORING_SETUP_SQE128);
if (ret < 0) {
dprintf(to_mon, "FAIL io ring %d\n", ret);
return 1;
}
for (i = 0; i < QD; i++)
queue_io_cmd(&ring, cfd, UBLK_U_IO_FETCH_REQ, i, bufs[i], 0);
ret = io_uring_submit(&ring);
if (ret != QD) {
dprintf(to_mon, "FAIL FETCH submit %d\n", ret);
return 1;
}
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, from_mon, &go, 1, 0);
sqe->user_data = UD_GO;
}
/* The monitor issues START_DEV now and tells us when to go. */
dprintf(to_mon, "READY %d\n", dev_id);
for (;;) {
struct __kernel_timespec ts = { .tv_nsec = 100 * 1000 * 1000 };
struct io_uring_cqe *cqe;
io_uring_submit(&ring);
ret = io_uring_wait_cqe_timeout(&ring, &cqe, &ts);
while (ret == 0) {
__u64 ud = cqe->user_data;
int res = cqe->res;
io_uring_cqe_seen(&ring, cqe);
if (ud == UD_GO) {
dprintf(to_mon, "GO\n");
t0 = now();
child = clone(child_fn, (char *)stack + STACK_SIZE,
CLONE_VM | CLONE_VFORK | SIGCHLD, &args);
t_vfork = now() - t0;
dprintf(to_mon, "VFORK %.2f %d\n", t_vfork, child);
if (child < 0)
return 1;
} else if (res == UBLK_IO_RES_OK) {
const struct ublksrv_io_desc *iod = desc + ud * desc_size;
__u8 op = iod->op_flags & 0xff;
__u64 off = iod->start_sector << 9;
__u32 len = iod->nr_sectors << 9;
int result = len;
dprintf(to_mon, "IO op=%u sector=%llu n=%u\n", op,
(unsigned long long)iod->start_sector,
iod->nr_sectors);
if (off + len > DEV_SIZE) {
result = -EIO;
} else if (op == UBLK_IO_OP_READ) {
memcpy(bufs[ud], ram + off, len);
} else if (op == UBLK_IO_OP_WRITE) {
memcpy(ram + off, bufs[ud], len);
} else if (op == UBLK_IO_OP_FLUSH) {
result = 0;
} else if (op == UBLK_IO_OP_DISCARD ||
op == UBLK_IO_OP_WRITE_ZEROES) {
memset(ram + off, 0, len);
} else {
result = -EOPNOTSUPP;
}
queue_io_cmd(&ring, cfd, UBLK_U_IO_COMMIT_AND_FETCH_REQ,
ud, bufs[ud], result);
} else {
/* -ENODEV: the device is going away. */
dprintf(to_mon, "DEAD %d\n", res);
return 1;
}
ret = io_uring_peek_cqe(&ring, &cqe);
}
if (child > 0 && waitpid(child, &status, WNOHANG) == child) {
dprintf(to_mon, "DONE 0x%x %.2f %.2f\n", status,
t_vfork, now() - t0);
break;
}
}
return 0;
}
/* Read one line from @fd with a deadline. Returns 0 on EOF, -1 on timeout. */
static int read_line(int fd, char *buf, size_t len, double deadline)
{
size_t n = 0;
while (n + 1 < len) {
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int ms = (int)((deadline - now()) * 1000);
ssize_t r;
if (ms <= 0 || poll(&pfd, 1, ms) <= 0)
return -1;
r = read(fd, buf + n, 1);
if (r <= 0)
return 0;
if (buf[n++] == '\n')
break;
}
buf[n] = 0;
return 1;
}
static void cat(const char *path, const char *prefix)
{
char buf[4096];
ssize_t n;
int fd = open(path, O_RDONLY);
if (fd < 0)
return;
n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n <= 0)
return;
buf[n] = 0;
for (char *p = buf, *e; p && *p; p = e) {
e = strchr(p, '\n');
if (e)
*e++ = 0;
printf("# %s%s\n", prefix, p);
}
}
static void dump_task(pid_t pid, const char *what)
{
char path[64], line[256];
FILE *f;
snprintf(path, sizeof(path), "/proc/%d/status", pid);
f = fopen(path, "r");
if (!f)
return;
printf("# %s pid %d:\n", what, pid);
while (fgets(line, sizeof(line), f)) {
if (!strncmp(line, "Name:", 5) || !strncmp(line, "State:", 6))
printf("# %s", line);
}
fclose(f);
snprintf(path, sizeof(path), "/proc/%d/wchan", pid);
cat(path, "wchan: ");
snprintf(path, sizeof(path), "/proc/%d/stack", pid);
cat(path, "");
}
static pid_t children[8];
static int nr_children;
static void dump_family(pid_t srv)
{
struct dirent *d;
DIR *dir;
nr_children = 0;
dump_task(srv, "server");
dir = opendir("/proc");
if (!dir)
return;
while ((d = readdir(dir))) {
char path[64], line[256];
pid_t pid = atoi(d->d_name), ppid = 0;
FILE *f;
if (pid <= 0)
continue;
snprintf(path, sizeof(path), "/proc/%d/status", pid);
f = fopen(path, "r");
if (!f)
continue;
while (fgets(line, sizeof(line), f))
if (sscanf(line, "PPid: %d", &ppid) == 1)
break;
fclose(f);
if (ppid == srv) {
dump_task(pid, "child");
if (nr_children < 8)
children[nr_children++] = pid;
}
}
closedir(dir);
}
/* Is @pid still around and in D state? */
static int task_state(pid_t pid)
{
char path[64], line[256];
int st = 0;
FILE *f;
snprintf(path, sizeof(path), "/proc/%d/status", pid);
f = fopen(path, "r");
if (!f)
return 0;
while (fgets(line, sizeof(line), f)) {
if (!strncmp(line, "State:", 6)) {
st = line[7];
break;
}
}
fclose(f);
return st;
}
/* After the server was killed: does the stuck child get going again? */
static void watch_children(const char *mode)
{
double deadline = now() + 10;
int i;
for (i = 0; i < nr_children; i++) {
int st;
while ((st = task_state(children[i])) == 'D' && now() < deadline)
usleep(100000);
if (!st)
printf("# %s: child %d is gone after the kill\n", mode, children[i]);
else if (st == 'D')
printf("# %s: child %d still in D state 10s after the kill\n",
mode, children[i]);
else
printf("# %s: child %d left D state after the kill (%c)\n",
mode, children[i], st);
if (st == 'D')
dump_task(children[i], "child");
}
}
/* STOP_DEV + DEL_DEV can block on a dead device, so do it in a helper. */
static void cleanup_dev(int dev_id)
{
double deadline;
int status;
pid_t pid;
pid = fork();
if (pid == 0) {
struct ctrl mc;
if (ctrl_open(&mc) == 0) {
ctrl_cmd(&mc, UBLK_U_CMD_STOP_DEV, dev_id, NULL, 0, 0);
ctrl_cmd(&mc, UBLK_U_CMD_DEL_DEV, dev_id, NULL, 0, 0);
}
_exit(0);
}
deadline = now() + 10;
while (now() < deadline) {
if (waitpid(pid, &status, WNOHANG) == pid)
return;
usleep(100000);
}
printf("# cleanup of ublk%d stuck, abandoning it\n", dev_id);
dump_task(pid, "cleanup helper");
kill(pid, SIGKILL);
}
static int run_mode(enum mode mode, int nr, const char *self)
{
int a[2], b[2], dev_id = -1, status, ret, i, fd, rc = 1, scan_reads = 0;
char line[128], bpath[64];
double deadline, t_vfork = -1, t_done = -1;
pid_t srv;
struct ctrl mc;
if (pipe(a) || pipe(b)) {
printf("not ok %d %s # pipe: %m\n", nr, mode_name[mode]);
return 1;
}
srv = fork();
if (srv < 0) {
printf("not ok %d %s # fork: %m\n", nr, mode_name[mode]);
return 1;
}
if (srv == 0) {
close(a[0]);
close(b[1]);
_exit(server(mode, a[1], b[0], self));
}
close(a[1]);
close(b[0]);
deadline = now() + 10;
if (read_line(a[0], line, sizeof(line), deadline) <= 0 ||
sscanf(line, "READY %d", &dev_id) != 1) {
printf("not ok %d %s # server setup: %s", nr, mode_name[mode], line);
goto out_kill;
}
ret = ctrl_open(&mc);
if (ret < 0) {
printf("not ok %d %s # monitor ctrl_open %d\n", nr, mode_name[mode], ret);
goto out_kill;
}
ret = ctrl_cmd(&mc, UBLK_U_CMD_START_DEV, dev_id, NULL, 0, srv);
if (ret < 0) {
printf("not ok %d %s # START_DEV %d\n", nr, mode_name[mode], ret);
goto out_kill;
}
snprintf(bpath, sizeof(bpath), "/dev/ublkb%d", dev_id);
for (i = 0; i < 200 && access(bpath, F_OK); i++)
usleep(10000);
if (access(bpath, F_OK)) {
printf("not ok %d %s # %s missing\n", nr, mode_name[mode], bpath);
goto out_kill;
}
/*
* The partition scan runs from a workqueue and holds the disk's
* open_mutex while the server serves its reads. Wait for its first
* read, then open the device ourselves: that blocks until the scan
* is done, and only then may the server vfork.
*/
deadline = now() + 10;
while (!scan_reads) {
unsigned op;
if (read_line(a[0], line, sizeof(line), deadline) <= 0) {
printf("not ok %d %s # no partition scan read: %s", nr,
mode_name[mode], line);
goto out_kill;
}
if (sscanf(line, "IO op=%u", &op) == 1 && op == UBLK_IO_OP_READ)
scan_reads++;
}
for (i = 0; i < 100; i++) {
fd = open(bpath, O_RDONLY);
if (fd >= 0 || errno != ENXIO)
break;
usleep(100000);
}
if (fd < 0) {
printf("not ok %d %s # open %s: %m\n", nr, mode_name[mode], bpath);
goto out_kill;
}
close(fd);
deadline = now() + HANG_SECS;
if (write(b[1], "g", 1) != 1)
goto out_kill;
for (;;) {
unsigned st;
int pid;
ret = read_line(a[0], line, sizeof(line), deadline);
if (ret < 0) {
printf("# %s: no progress for %ds\n", mode_name[mode], HANG_SECS);
dump_family(srv);
printf("not ok %d %s # server %d stuck in vfork(), child's block device release waits for it\n",
nr, mode_name[mode], srv);
goto out_kill;
}
if (ret == 0) {
printf("not ok %d %s # server died\n", nr, mode_name[mode]);
goto out_kill;
}
if (!strncmp(line, "IO ", 3))
continue;
if (sscanf(line, "VFORK %lf %d", &t_vfork, &pid) == 2) {
printf("# %s: vfork() returned after %.2fs\n", mode_name[mode], t_vfork);
continue;
}
if (sscanf(line, "DONE 0x%x %lf %lf", &st, &t_vfork, &t_done) == 3) {
printf("# %s: child reaped after %.2fs, status 0x%x\n",
mode_name[mode], t_done, st);
if (WIFEXITED(st) && WEXITSTATUS(st) == 0 && t_vfork < HANG_SECS / 3) {
printf("ok %d %s\n", nr, mode_name[mode]);
rc = 0;
} else {
printf("not ok %d %s # status 0x%x vfork %.2fs\n",
nr, mode_name[mode], st, t_vfork);
}
break;
}
printf("# %s: %s", mode_name[mode], line);
if (!strncmp(line, "FAIL", 4) || !strncmp(line, "DEAD", 4)) {
printf("not ok %d %s # %s", nr, mode_name[mode], line);
goto out_kill;
}
}
waitpid(srv, &status, 0);
srv = 0;
out_kill:
if (srv > 0) {
printf("# %s: killing server %d\n", mode_name[mode], srv);
kill(srv, SIGKILL);
deadline = now() + 10;
while (waitpid(srv, &status, WNOHANG) != srv) {
if (now() > deadline) {
printf("# %s: server %d does not die\n", mode_name[mode], srv);
dump_family(srv);
break;
}
usleep(100000);
}
printf("# %s: server %d reaped\n", mode_name[mode], srv);
watch_children(mode_name[mode]);
}
if (dev_id >= 0)
cleanup_dev(dev_id);
close(a[0]);
close(b[1]);
return rc;
}
int main(int argc, char **argv)
{
int rc = 0;
if (argc > 1 && !strcmp(argv[1], "--child"))
return 0;
setvbuf(stdout, NULL, _IONBF, 0);
if (access("/dev/ublk-control", F_OK)) {
printf("not ok 1 # no /dev/ublk-control\n");
return 1;
}
if (argc > 1) {
for (int m = 0; m < 3; m++)
if (!strcmp(argv[1], mode_name[m]))
return run_mode(m, 1, argv[0]);
return 1;
}
rc |= run_mode(MODE_EXEC, 1, argv[0]);
rc |= run_mode(MODE_CLOSE, 2, argv[0]);
rc |= run_mode(MODE_EXIT, 3, argv[0]);
{
struct dirent *d;
DIR *dir = opendir("/proc");
while (dir && (d = readdir(dir))) {
pid_t pid = atoi(d->d_name);
char path[64], comm[64] = "";
FILE *f;
if (pid <= 0 || pid == getpid())
continue;
snprintf(path, sizeof(path), "/proc/%d/comm", pid);
f = fopen(path, "r");
if (!f)
continue;
if (fgets(comm, sizeof(comm), f) && !strncmp(comm, "vfork_ublk_test", 15))
dump_task(pid, "leftover");
fclose(f);
}
if (dir)
closedir(dir);
}
return rc;
}