[PATCH 2/6] nvme-fc: fix iterator over rport->ctrl_list in nvme_fc_match_disconn_ls()
Guixin Liu
kanie at linux.alibaba.com
Fri Nov 6 02:49:30 PST 2026
在 2026/11/6 15:38, Hannes Reinecke 写道:
> On 11/6/26 2:58 AM, Guixin Liu wrote:
>> Commit b71cbcf7d170e ("nvme-fc: don't hold rport lock when putting ctrl")
>> switched the loop to list_for_each_entry_safe() to allow dropping the
>> rport lock for the nvme_fc_ctrl_put() of a non-matching controller.
>> But the safe iterator only protects the current entry within one
>> iteration: the next entry is cached before the body runs, and while
>> the lock is dropped that entry can run through its own final put into
>> nvme_fc_ctrl_free(), which removes it from the list and frees it.
>> The next iteration then dereferences freed memory.
>>
>> The safe variant is not needed here at all. The reference the loop
>> takes on the current entry pins it on the list until the iteration
>> moves on, so a plain list_for_each_entry() tolerates the lock drop:
>> while the lock is held the current entry cannot be unlinked, and once
>> the lock is dropped the cached ->next of the pinned entry still points
>> at a valid list member or the head.
>>
>> Switch back to the plain iterator.
>>
>> Fixes: b71cbcf7d170e ("nvme-fc: don't hold rport lock when putting ctrl")
>> Cc: stable at vger.kernel.org
>> Signed-off-by: Guixin Liu <kanie at linux.alibaba.com>
>>
>> ---
>>
>> Reproduced under KASAN in a VM through fcloop with three or more
>> controllers on one remote port, a tbkas force-reconnect racing
>> concurrent controller disconnects. The lock-drop window was widened
>> with a test-only mdelay(200) inserted at the same place in both the
>> buggy and the fixed version so the iterator is the only variable; the
>> stock microsecond window did not trigger in 400 iterations of the
>> same workload.
>>
>> BUG: KASAN: slab-use-after-free in nvme_fc_match_disconn_ls
>> Read of size 8 by task kworker
>> Workqueue: events nvme_fc_handle_ls_rqst_work
>> Call Trace:
>> nvme_fc_match_disconn_ls
>> nvme_fc_ls_disconnect_assoc
>> nvme_fc_handle_ls_rqst_work
>> process_one_work
>>
>> The object was allocated by nvme_fc_alloc_ctrl() on connect and freed
>> by nvme_free_ctrl() on the concurrent teardown of the cached next
>> entry. With the plain iterator the identical amplified sequence
>> reports nothing and survives; the read hits the pinned current entry,
>> which cannot be freed while the loop holds its reference.
>> ---
>> drivers/nvme/host/fc.c | 4 ++--
>> 1 file changed, 2 insertions(+), 2 deletions(-)
>>
>> diff --git a/drivers/nvme/host/fc.c b/drivers/nvme/host/fc.c
>> index 0b9ff50d26ff..edf192055618 100644
>> --- a/drivers/nvme/host/fc.c
>> +++ b/drivers/nvme/host/fc.c
>> @@ -1470,14 +1470,14 @@ nvme_fc_match_disconn_ls(struct nvme_fc_rport *rport,
>> {
>> struct fcnvme_ls_disconnect_assoc_rqst *rqst =
>> &lsop->rqstbuf->rq_dis_assoc;
>> - struct nvme_fc_ctrl *ctrl, *tmp, *ret = NULL;
>> + struct nvme_fc_ctrl *ctrl, *ret = NULL;
>> struct nvmefc_ls_rcv_op *oldls = NULL;
>> u64 association_id = be64_to_cpu(rqst->associd.association_id);
>> unsigned long flags;
>> spin_lock_irqsave(&rport->lock, flags);
>> - list_for_each_entry_safe(ctrl, tmp, &rport->ctrl_list, ctrl_list) {
>> + list_for_each_entry(ctrl, &rport->ctrl_list, ctrl_list) {
>> if (!nvme_fc_ctrl_get(ctrl))
>> continue;
>> spin_lock(&ctrl->lock);
>
> Not sure here. The implication of the function is that the any controller might've been deleted during each loop iteration
> (disconnect might be triggered from both sides, either host
> _or_ controller).
> So while it's true that a controller might not get deleted while
> a reference is held, that other process might drop a reference
> to the controller while the first process is executing the loop.
> Then the call to 'nvme_fc_ctrl_put()' at the end of the loop
> will be the last reference, the controller gets deleted,
> and the ->next element becomes invalid causing the loop
> to crash.
You are right, Hannes, the issue still exists.
>
> So, no.
>
> And the issue you've described should've caused the
> nvme_fc_ctrl_get() at the start of the loop to return
> 'false'; why didn't it trigger here?
The UAF happens earlier, inside the "for" loop,
list_for_each_entry_safe() expands to "pos = n, n = list_next_entry(n)",
so at that point it already dereferences the next pointer of that ctrl
that has been freed. That why KASAN reports "Read of size 8 by task kworker"
in nvme_fc_match_disconn_ls().
By the time execution reaches nvmet_fc_ctrl_get(), the memory state is
event less clear.
I will change a v2 fix, thanks.
Best Regards,
Guixin Liu
>
>
> Cheers,
>
> Hannes
More information about the Linux-nvme
mailing list