From: sashiko-bot@kernel.org
To: "M Nikhil" <nikh1092@linux.ibm.com>,
linux-s390@vger.kernel.org, linux-scsi@vger.kernel.org
Cc: "Heiko Carstens" <hca@linux.ibm.com>,
"Vasily Gorbik" <gor@linux.ibm.com>,
"Alexander Gordeev" <agordeev@linux.ibm.com>,
"Christian Borntraeger" <borntraeger@linux.ibm.com>
Subject: Re: [PATCH v14 2/2] zfcp: Use DMA streaming API to map I/O
Date: Wed, 28 Oct 2026 05:45:35 +0000 [thread overview]
Message-ID: <sashiko-outbox-187541@kernel.org> (raw)
In-Reply-To: <20261028051446.3163392-3-nikh1092@linux.ibm.com>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] scsi: zfcp: premature DMA unmap and UAF on adapter shutdown in SCSI EH
- [Critical] scsi: zfcp: use-after-free and DMA leak on host reset with ERP_FAILED
- [High] scsi: zfcp: stale QDIO SBALE DMA address on abort and status read error paths
- [High] scsi: zfcp: race between zfcp_fsf_reqid_check() and zfcp_scsi_forget_cmnds() on restored req->data
--
commit 62d32e0c324a9864b32b7819374bcb22949814d1
Author: M Nikhil <nikh1092@linux.ibm.com>
zfcp: Use DMA streaming API to map I/O
This commit converts zFCP I/O buffer mappings (QTCB, SRB, CT/ELS, and SCSI
I/O) from direct physical address translations to the DMA streaming API using
dma_map_sgtable() and dma_unmap_sgtable(). It also updates the SCSI error
handling and QDIO submission paths to manage DMA mapping lifetimes and SBAL
cleanup.
> diff --git a/drivers/s390/scsi/zfcp_fsf.c b/drivers/s390/scsi/zfcp_fsf.c
> index 12b3d2d0a2c87..05f3b226b1634 100644
> --- a/drivers/s390/scsi/zfcp_fsf.c
> +++ b/drivers/s390/scsi/zfcp_fsf.c
[ ... ]
> @@ -954,17 +1066,29 @@ int zfcp_fsf_status_read(struct zfcp_qdio *qdio)
> memset(sr_buf, 0, sizeof(*sr_buf));
> req->data = sr_buf;
>
> - zfcp_qdio_fill_next(qdio, &req->qdio_req, sr_buf, sizeof(*sr_buf));
> + sg_init_one(&req->metadata_sg, sr_buf, sizeof(*sr_buf));
> + req->metadata_sgt.sgl = &req->metadata_sg;
> + req->metadata_sgt.orig_nents = 1;
> +
> + retval = zfcp_fsf_req_map_sg(req, &req->metadata_sgt, DMA_FROM_DEVICE);
> + if (unlikely(retval))
> + goto failed_dma_map;
> +
> + zfcp_qdio_fill_next(qdio, &req->qdio_req,
> + sg_dma_address(req->metadata_sgt.sgl),
> + sg_dma_len(req->metadata_sgt.sgl));
> zfcp_qdio_set_sbale_last(qdio, &req->qdio_req);
>
> retval = zfcp_fsf_req_send(req);
> if (retval)
> goto failed_req_send;
> /* NOTE: DO NOT TOUCH req PAST THIS POINT! */
>
> goto out;
>
> failed_req_send:
> + zfcp_fsf_req_unmap_metadata(req);
> +failed_dma_map:
> req->data = NULL;
> mempool_free(virt_to_page(sr_buf), adapter->pool.sr_data);
> failed_buf:
> zfcp_dbf_hba_fsf_uss("fssr__1", req);
> zfcp_fsf_req_free(req);
> out:
> spin_unlock_irq(&qdio->req_q_lock);
> return retval;
> }
[ ... ]
> @@ -1076,6 +1200,7 @@ struct zfcp_fsf_req *zfcp_fsf_abort_fcp_cmnd(struct scsi_cmnd *scmnd)
> }
>
> out_error_free:
> + zfcp_fsf_req_unmap_metadata(req);
> zfcp_fsf_req_free(req);
> req = NULL;
> out:
[Severity: High]
Can the error paths in zfcp_fsf_abort_fcp_cmnd() and zfcp_fsf_status_read()
leave stale DMA addresses in the QDIO request queue SBALs?
In zfcp_fsf_abort_fcp_cmnd(), zfcp_fsf_req_create() allocates and maps
req->qtcb and calls zfcp_qdio_req_init() with has_qtcb = true, which writes
element[0] and element[1] (with qtcb_dma_handle and qtcb_len) of
qdio->req_q[qdio->req_q_idx]:
drivers/s390/scsi/zfcp_qdio.h:zfcp_qdio_req_init() {
...
q_req->sbale_curr = 1;
sbale->eflags = 0;
sbale->sflags = SBAL_SFLAGS0_COMMAND | sbtype;
if (unlikely(!has_qtcb))
return;
sbale++;
sbale->addr = u64_to_dma64(qtcb_dma_handle);
sbale->length = qtcb_len;
}
If the LUN is blocked (!(atomic_read(&zfcp_sdev->status) &
ZFCP_STATUS_COMMON_UNBLOCKED)), zfcp_fsf_abort_fcp_cmnd() jumps to
out_error_free and unmaps and frees req->qtcb without calling
zfcp_qdio_zero_sbals() and without advancing qdio->req_q_idx.
If the next caller to use qdio->req_q[qdio->req_q_idx] is
zfcp_fsf_status_read(), its zfcp_fsf_req_create() calls zfcp_qdio_req_init()
with has_qtcb = false, which sets q_req->sbale_curr = 1, initializes
element[0], and returns early without clearing element[1].
zfcp_fsf_status_read() then calls zfcp_qdio_fill_next(), which increments
q_req->sbale_curr to 2 and writes the status read buffer DMA address into
element[2]. The SRB SBAL is then submitted to the adapter with element[1]
still holding the unmapped and freed QTCB DMA address.
Similarly, in zfcp_fsf_status_read(), if mempool_alloc() (goto failed_buf) or
zfcp_fsf_req_map_sg() (goto failed_dma_map) fails after zfcp_fsf_req_create()
initialized element[0], zfcp_qdio_zero_sbals() is not called before freeing
req. Should both out_error_free in zfcp_fsf_abort_fcp_cmnd() and failed_buf
in zfcp_fsf_status_read() call zfcp_qdio_zero_sbals()?
> diff --git a/drivers/s390/scsi/zfcp_scsi.c b/drivers/s390/scsi/zfcp_scsi.c
> index ea4b968352f4d..99ea8c499bd1a 100644
> --- a/drivers/s390/scsi/zfcp_scsi.c
> +++ b/drivers/s390/scsi/zfcp_scsi.c
> @@ -163,6 +163,70 @@ static int zfcp_scsi_sdev_init(struct scsi_device *sdev)
[ ... ]
> +static int zfcp_scsi_abort_failed(struct zfcp_adapter *adapter,
> + struct scsi_cmnd *scpnt, u64 old_reqid)
> +{
> + struct zfcp_fsf_req *old_req;
> + unsigned long flags;
> +
> + write_lock_irqsave(&adapter->abort_lock, flags);
> + old_req = zfcp_reqlist_find(adapter->req_list, old_reqid);
> + if (old_req) {
> + old_req->data = scpnt;
> + write_unlock_irqrestore(&adapter->abort_lock, flags);
> + return FAILED;
> + }
> + write_unlock_irqrestore(&adapter->abort_lock, flags);
> +
> + zfcp_scsi_unmap_cmnd(adapter, scpnt);
> + return FAILED;
> +}
[Severity: High]
When zfcp_scsi_abort_failed() restores old_req->data = scpnt so that a later
LUN or target reset will unmap and detach scpnt in zfcp_scsi_forget_cmnds(),
can a concurrent completion in zfcp_fsf_reqid_check() race with
zfcp_scsi_forget_cmnds()?
On the QDIO completion path, zfcp_fsf_reqid_check() removes fsf_req from
adapter->req_list before acquiring adapter->abort_lock in
zfcp_fsf_fcp_cmnd_handler():
drivers/s390/scsi/zfcp_fsf.c:zfcp_fsf_reqid_check() {
...
fsf_req = zfcp_reqlist_find_rm(adapter->req_list, req_id);
...
zfcp_fsf_req_complete(fsf_req);
-> zfcp_fsf_fcp_cmnd_handler(fsf_req)
read_lock_irqsave(&req->adapter->abort_lock, flags);
}
If CPU1 removes old_req in zfcp_reqlist_find_rm() just before CPU2 runs
zfcp_scsi_forget_cmnds() at the end of zfcp_scsi_task_mgmt_function():
drivers/s390/scsi/zfcp_scsi.c:zfcp_scsi_forget_cmnds() {
...
write_lock_irqsave(&adapter->abort_lock, flags);
zfcp_reqlist_apply_for_all(adapter->req_list, zfcp_scsi_forget_cmnd,
&filter);
write_unlock_irqrestore(&adapter->abort_lock, flags);
}
CPU2 will not find old_req in adapter->req_list, so zfcp_scsi_forget_cmnd()
never unmaps scpnt or clears old_req->data. zfcp_scsi_task_mgmt_function()
then returns SUCCESS to scsi_eh, which either reuses scpnt for scsi_eh_tur()
(where scsi_eh_prep_cmnd() zeroes scpnt->sdb and sets scpnt->sc_data_direction
= DMA_NONE) or finishes and frees scpnt.
When CPU1 subsequently enters zfcp_fsf_fcp_cmnd_handler() and acquires
adapter->abort_lock, it sees req->data == scpnt still non-NULL, skips
unmapping the zeroed scpnt->sdb (leaking the DMA mapping), and calls
scsi_done(scpnt) on a command that is either mid-TUR or already finished.
> @@ -198,18 +284,57 @@ static int zfcp_scsi_eh_abort_handler(struct scsi_cmnd *scpnt)
> zfcp_erp_wait(adapter);
> ret = fc_block_scsi_eh(scpnt);
> if (ret) {
> + /*
> + * fc_block_scsi_eh returned FAST_IO_FAIL because
> + * fast_io_fail_tmo fired. This may originate from a
> + * per-port reopen (fc_timeout_fail_rport_io ->
> + * zfcp_scsi_terminate_rport_io ->
> + * zfcp_erp_port_forced_reopen) that closes the port
> + * asynchronously without shutting QDIO down or
> + * calling zfcp_fsf_req_dismiss_all().
> + *
> + * Only unmap scpnt's DMA buffers if QDIO is already
> + * down. That is the exact precondition enforced by
> + * zfcp_fsf_req_dismiss_all() (BUG_ON QDIOUP) and
> + * guarantees the hardware is no longer using them.
> + *
> + * If QDIO is still up the hardware may still be
> + * writing into the buffers; we must not unmap now.
> + * Return FAILED so scsi_eh escalates instead of
> + * finishing scpnt. zfcp_scsi_abort_failed() hands
> + * the unmap back to the request's completion or
> + * dismissal, or to the TMF path (forget_cmnds), so
> + * the mapping is not leaked.
> + */
> zfcp_dbf_scsi_abort("abrt_bl", scpnt, NULL);
> - return ret;
> + if (atomic_read(&adapter->status) &
> + ZFCP_STATUS_ADAPTER_QDIOUP)
> + return zfcp_scsi_abort_failed(adapter, scpnt,
> + old_reqid);
> + retval = ret; /* FAST_IO_FAIL, QDIO is down */
> + goto out_unmap;
> }
> if (!(atomic_read(&adapter->status) &
> ZFCP_STATUS_COMMON_RUNNING)) {
> + /*
> + * Adapter is gone; ERP shut down QDIO and dismissed
> + * all requests before clearing RUNNING. Safe to
> + * unmap now.
> + */
> zfcp_dbf_scsi_abort("abrt_ru", scpnt, NULL);
> - return SUCCESS;
> + retval = SUCCESS;
> + goto out_unmap;
> }
> }
[ ... ]
> @@ -305,6 +479,21 @@ static int zfcp_scsi_task_mgmt_function(struct scsi_device *sdev, u8 tm_flags)
> ret = fc_block_rport(rport);
> if (ret) {
> zfcp_dbf_scsi_devreset("fiof", sdev, tm_flags, NULL);
> + /*
> + * QDIO still up: the FCP channel may still be using
> + * the buffers of outstanding commands. Do not let
> + * scsi_eh finish them; escalate to host reset instead.
> + */
> + if (atomic_read(&adapter->status) &
> + ZFCP_STATUS_ADAPTER_QDIOUP)
> + return FAILED;
> + /*
> + * QDIO down: the hardware is done with the buffers.
> + * Detach and unmap the commands so that a later
> + * completion or dismissal of their requests cannot
> + * touch a finished scpnt.
> + */
> + zfcp_scsi_forget_cmnds(zfcp_sdev, tm_flags);
> return ret;
> }
>
> if (!(atomic_read(&adapter->status) &
> ZFCP_STATUS_COMMON_RUNNING)) {
> zfcp_dbf_scsi_devreset("nres", sdev, tm_flags, NULL);
> return SUCCESS;
> }
[Severity: Critical]
Is it safe to assume that QDIO has been shut down and all requests have been
dismissed whenever ZFCP_STATUS_COMMON_RUNNING is clear?
Looking at zfcp_erp_adapter_shutdown() -> zfcp_erp_adapter_reopen() in
drivers/s390/scsi/zfcp_erp.c:
drivers/s390/scsi/zfcp_erp.c:zfcp_erp_adapter_shutdown() {
int flags = ZFCP_STATUS_COMMON_RUNNING | ZFCP_STATUS_COMMON_ERP_FAILED;
zfcp_erp_adapter_reopen(adapter, clear | flags, dbftag);
-> zfcp_erp_adapter_block(adapter, clear | flags);
-> zfcp_scsi_schedule_rports_block(adapter);
-> write_lock_irqsave(&adapter->erp_lock, ...);
-> zfcp_erp_action_enqueue(..., ZFCP_ERP_ACTION_REOPEN_ADAPTER, ...);
}
zfcp_erp_adapter_block() clears ZFCP_STATUS_COMMON_RUNNING synchronously
before the ERP action is enqueued, while zfcp_qdio_close() and
zfcp_fsf_req_dismiss_all() run asynchronously later on the ERP thread in
zfcp_erp_adapter_strategy_close().
This appears to cause a few problems in the error handlers:
1. In zfcp_scsi_eh_abort_handler() (at "abrt_ru"), if
zfcp_erp_adapter_shutdown() runs concurrently (for example, between
zfcp_erp_adapter_block() and zfcp_erp_action_enqueue() so zfcp_erp_wait()
returns immediately, or right after zfcp_erp_wait() returns),
ZFCP_STATUS_COMMON_RUNNING is already clear while QDIO is still active.
Jumping to out_unmap will unmap scpnt's DMA buffers and return SUCCESS
while the FCP channel may still be performing DMA.
2. If zfcp_scsi_eh_abort_handler() fails and zfcp_scsi_abort_failed()
restores old_req->data = scpnt, scsi_eh escalates to
zfcp_scsi_task_mgmt_function(). At "nres", observing
!(atomic_read(&adapter->status) & ZFCP_STATUS_COMMON_RUNNING) returns
SUCCESS without checking ZFCP_STATUS_ADAPTER_QDIOUP and without calling
zfcp_scsi_forget_cmnds(). scsi_eh then finishes scpnt while old_req is
still on adapter->req_list with old_req->data == scpnt. When
zfcp_fsf_req_dismiss_all() or normal completion later runs
zfcp_fsf_fcp_cmnd_handler(), it will unmap and call scsi_done() on the
already-finished scpnt.
3. In zfcp_qdio_close() in drivers/s390/scsi/zfcp_qdio.c,
ZFCP_STATUS_ADAPTER_QDIOUP is cleared before calling tasklet_disable(),
qdio_stop_irq(), and qdio_shutdown(). Can the lockless checks of
ZFCP_STATUS_ADAPTER_QDIOUP in "abrt_bl" and "fiof" observe QDIOUP cleared
and unmap scpnt before qdio_shutdown() has actually stopped hardware DMA?
[Severity: Critical]
In addition, when zfcp_scsi_eh_abort_handler() (at "abrt_bl" or "abrt_ar")
and zfcp_scsi_task_mgmt_function() (at "fiof" or "reqf") return FAILED
because ZFCP_STATUS_ADAPTER_QDIOUP is still set while old_req->data == scpnt
remains on adapter->req_list, what happens if scsi_eh escalates to
zfcp_scsi_eh_host_reset_handler() after a link-down event?
When a local link-down event occurs (zfcp_fsf_status_read_link_down() ->
zfcp_fsf_link_down_info_eval(), or FSF_PROT_LINK_DOWN in
zfcp_fsf_protstatus_eval()), zfcp_erp_set_adapter_status(adapter,
ZFCP_STATUS_COMMON_ERP_FAILED) sets ZFCP_STATUS_COMMON_ERP_FAILED on
adapter->status while leaving ZFCP_STATUS_ADAPTER_QDIOUP set.
When fast_io_fail_tmo fires on the blocked rport and both abort and
LUN/target reset return FAILED because QDIOUP is set, scsi_eh calls
zfcp_scsi_eh_host_reset_handler():
drivers/s390/scsi/zfcp_scsi.c:zfcp_scsi_eh_host_reset_handler() {
...
zfcp_erp_adapter_reopen(adapter, 0, "schrh_1");
zfcp_erp_wait(adapter);
fc_ret = fc_block_scsi_eh(scpnt);
if (fc_ret)
ret = fc_ret;
...
return ret;
}
Because zfcp_erp_adapter_reopen() is called with clear = 0,
zfcp_erp_action_enqueue() -> zfcp_erp_handle_failed() sees
ZFCP_STATUS_COMMON_ERP_FAILED still set on adapter->status and suppresses
ZFCP_ERP_ACTION_REOPEN_ADAPTER (need = 0). As a result, zfcp_qdio_close() and
zfcp_fsf_req_dismiss_all() are never executed, QDIO stays up, and old_req
remains in adapter->req_list with old_req->data == scpnt.
zfcp_scsi_eh_host_reset_handler() then returns FAST_IO_FAIL (or SUCCESS) to
scsi_eh_host_reset(), which finishes and frees scpnt. When the link later
comes back up (FSF_STATUS_READ_LINK_UP) or the adapter is shut down,
zfcp_fsf_req_dismiss_all() will invoke zfcp_fsf_fcp_cmnd_handler(old_req)
with req->data pointing to the freed scpnt, causing a use-after-free in
zfcp_fsf_req_unmap_sg() and scsi_done().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028051446.3163392-1-nikh1092@linux.ibm.com?part=2
prev parent reply other threads:[~2026-10-28 5:45 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 5:14 [PATCH v14 0/2] zfcp: Use DMA streaming API to map I/O M Nikhil
2026-10-28 5:14 ` [PATCH v14 1/2] zfcp: Replace sg-list with sg_table in zfcp_fsf_ct_els structure M Nikhil
2026-10-28 5:20 ` sashiko-bot
2026-10-28 5:14 ` [PATCH v14 2/2] zfcp: Use DMA streaming API to map I/O M Nikhil
2026-10-28 5:45 ` sashiko-bot [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-187541@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=agordeev@linux.ibm.com \
--cc=borntraeger@linux.ibm.com \
--cc=gor@linux.ibm.com \
--cc=hca@linux.ibm.com \
--cc=linux-s390@vger.kernel.org \
--cc=linux-scsi@vger.kernel.org \
--cc=nikh1092@linux.ibm.com \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox