[PATCH v3 2/2] nvme: expose queue information via debugfs

sashiko-bot at kernel.org sashiko-bot at kernel.org
Mon Oct 26 05:55:26 PDT 2026


Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] nvme-tcp: race with queue teardown in nvme_tcp_print_io_queue_info()
- [Medium] nvme: debugfs: nvme_io_queue_info_start() skips queues on resumed reads
- [Low] nvme: debugfs: missing static qualifier on nvme_io_queue_info_seq_ops
--

commit 0f7d7347ae3aacfc41bb3e9a63226f587e2b9ffd
Author: Nilay Shroff <nilay at linux.ibm.com>

nvme: expose queue information via debugfs

This commit adds an io_queue_info debugfs attribute to expose per-queue
information for NVMe controllers using seq_file iteration. For NVMe-TCP,
it outputs each I/O queue's bound CPU and TCP socket source and
destination address and port.

> diff --git a/drivers/nvme/host/debugfs.c b/drivers/nvme/host/debugfs.c
> index 798fec4730f0a..8ea0413f7b753 100644
> --- a/drivers/nvme/host/debugfs.c
> +++ b/drivers/nvme/host/debugfs.c
> @@ -25,6 +25,61 @@ struct nvme_debugfs_ctx {
>  	struct nvme_debugfs_attr *attr;
>  };
>  
> +static void *nvme_io_queue_info_start(struct seq_file *m, loff_t *pos)
> +{
> +	struct nvme_debugfs_ctx *ctx = m->private;
> +	struct nvme_ns *ns = ctx->data;
> +	struct nvme_ctrl *ctrl = ns->ctrl;
> +
> +	/*
> +	 * IO queues starts at offset 1.
> +	 */
> +	return (++*pos < ctrl->queue_count) ? pos : NULL;
> +}

[Severity: Medium]
Will unconditionally pre-incrementing *pos in nvme_io_queue_info_start()
cause queues to be skipped when seq_read_iter() resumes reading on a
subsequent read() call or after a seq_file buffer overflow?

In fs/seq_file.c:seq_read_iter(), the Fill loop advances m->index via
m->op->next() before checking whether the user buffer is full or whether
m->op->show() overflowed the seq_file buffer:

	p = m->op->next(m, p, &m->index);
	...
	if (!p || IS_ERR(p))	// no next record for us
		break;
	if (m->count >= iov_iter_count(iter))
		break;
	err = m->op->show(m, p);
	if (err > 0) {		// ->show() says "skip it"
		m->count = offs;
	} else if (err || seq_has_overflowed(m)) {
		m->count = offs;
		break;
	}

When the loop stops because the seq_file buffer overflowed or the caller's
buffer was filled, m->index has already been incremented by
nvme_io_queue_info_next() to the next unshown queue ID. On the next read,
seq_read_iter() calls nvme_io_queue_info_start(m, &m->index), which
executes ++*pos a second time and skips that queue.

Should nvme_io_queue_info_start() only advance *pos when *pos is 0?

> +
> +static void *nvme_io_queue_info_next(struct seq_file *m, void *v, loff_t *pos)
> +{
> +	struct nvme_debugfs_ctx *ctx = m->private;
> +	struct nvme_ns *ns = ctx->data;
> +	struct nvme_ctrl *ctrl = ns->ctrl;
> +
> +	return (++*pos < ctrl->queue_count) ? pos : NULL;
> +}
> +
> +static void nvme_io_queue_info_stop(struct seq_file *m, void *v)
> +{
> +	/* nothing to be done */
> +}
> +
> +static int nvme_io_queue_info_show(struct seq_file *m, void *v)
> +{
> +	struct nvme_debugfs_ctx *ctx = m->private;
> +	struct nvme_ns *ns = ctx->data;
> +	struct nvme_ctrl *ctrl = ns->ctrl;
> +
> +	if (ctrl->ops->print_io_queue_info)
> +		return ctrl->ops->print_io_queue_info(m, ctrl, *(loff_t *)v);
> +
> +	return 0;
> +}
> +
> +const struct seq_operations nvme_io_queue_info_seq_ops = {
> +	.start = nvme_io_queue_info_start,
> +	.next = nvme_io_queue_info_next,
> +	.stop = nvme_io_queue_info_stop,
> +	.show = nvme_io_queue_info_show
> +};

[Severity: Low]
Should nvme_io_queue_info_seq_ops be marked static since it is only used
within drivers/nvme/host/debugfs.c?

[ ... ]

> diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
> index 7190eb10c2481..a59169629d56b 100644
> --- a/drivers/nvme/host/tcp.c
> +++ b/drivers/nvme/host/tcp.c
> @@ -2691,6 +2691,59 @@ static void nvme_tcp_free_ctrl(struct nvme_ctrl *nctrl)
>  	kfree(ctrl);
>  }
>  
> +static int nvme_tcp_print_io_queue_info(struct seq_file *m,
> +		struct nvme_ctrl *ctrl, int qid)
> +{
> +	int cpu;
> +	struct sockaddr_storage src, dst;
> +	struct nvme_tcp_ctrl *tctrl = to_tcp_ctrl(ctrl);
> +	struct nvme_tcp_queue *queue = &tctrl->queues[qid];
> +	int ret = -EINVAL;
> +
> +	if (!qid || qid >= ctrl->queue_count)
> +		return -EINVAL;
> +
> +	if (!test_bit(NVME_TCP_Q_LIVE, &queue->flags))
> +		return 0;
> +
> +	mutex_lock(&queue->queue_lock);
> +	if (!queue->sock)
> +		goto unlock;
> +
> +	ret = kernel_getsockname(queue->sock, (struct sockaddr *)&src);
> +	if (ret <= 0)
> +		goto unlock;
> +
> +	ret = kernel_getpeername(queue->sock, (struct sockaddr *)&dst);
> +	if (ret <= 0)
> +		goto unlock;

[Severity: High]
Can nvme_tcp_print_io_queue_info() race with queue teardown during
controller reset or error recovery?

Holding a reference on struct nvme_ns keeps the namespace and controller
allocated, but does not prevent nvme_reset_ctrl_work() or
nvme_tcp_error_recovery_work() from running nvme_tcp_teardown_io_queues()
while io_queue_info is being read.

In nvme_tcp_stop_queue_nowait(), NVME_TCP_Q_LIVE is cleared under
queue->queue_lock while queue->sock remains non-NULL:

	mutex_lock(&queue->queue_lock);
	if (test_and_clear_bit(NVME_TCP_Q_LIVE, &queue->flags))
		__nvme_tcp_stop_queue(queue);
	/* Stopping the queue will disable TLS */
	queue->tls_enabled = false;
	mutex_unlock(&queue->queue_lock);

Shortly after, nvme_tcp_free_io_queues() -> nvme_tcp_free_queue() runs
without holding queue->queue_lock, frees queue->sock before clearing the
pointer, and destroys queue->queue_lock:

	__fput_sync(queue->sock->file);
	queue->sock = NULL;
	memalloc_noio_restore(noio_flag);

	kfree(queue->pdu);
	mutex_destroy(&queue->send_mutex);
	mutex_destroy(&queue->queue_lock);
	mutex_destroy(&queue->pf_cache_lock);

If nvme_tcp_print_io_queue_info() checks test_bit(NVME_TCP_Q_LIVE,
&queue->flags) just before teardown, it can acquire queue->queue_lock
after nvme_tcp_stop_queue_nowait() unlocks it, see queue->sock != NULL
because NVME_TCP_Q_LIVE is not re-checked under the lock, and dereference
queue->sock in kernel_getsockname() and kernel_getpeername() concurrently
with __fput_sync(queue->sock->file), or touch queue->queue_lock while
nvme_tcp_free_queue() destroys it (and reconnect re-initializes it in
nvme_tcp_alloc_queue()).

In addition, if !queue->sock is taken under queue->queue_lock (where ret
is still -EINVAL) or if kernel_getpeername() returns -ENOTCONN on a
shut-down socket, returning a negative error code from the seq_file show
callback aborts the entire seq_read_iter() read instead of skipping the
stopped queue.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261026123411.1988949-4-nilay@linux.ibm.com?part=2



More information about the Linux-nvme mailing list