From: sashiko-bot@kernel.org
To: "Yehyeong Lee" <yhlee@isslab.korea.ac.kr>
Cc: linux-scsi@vger.kernel.org
Subject: Re: [PATCH v4] scsi: libiscsi_tcp: check that a read got the data the target claimed
Date: Wed, 07 Oct 2026 07:21:47 +0000 [thread overview]
Message-ID: <sashiko-outbox-162712@kernel.org> (raw)
In-Reply-To: <20261007070207.666738-1-yhlee@isslab.korea.ac.kr>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] iscsi: integer underflow bypasses payload validation via OVERFLOW flag
- [Critical] scsi: iscsi_tcp_check_data_in bypasses length validation on out-of-band RECOVERED_ERROR
- [High] iscsi: connection drop on target failure due to missing response field check
--
commit 2fd3a9439d4f7bea0212ce97dd1b8ede74211f4a
Author: Yehyeong Lee <yhlee@isslab.korea.ac.kr>
scsi: libiscsi_tcp: check that a read got the data the target claimed
A target can finish a read without sending the data. iscsi_tcp_data_in()
bounds each Data-In PDU against the command buffer but never adds them up,
so a completion that declares no underflow is believed and the command
ends with DID_OK. The pages keep whatever they held.
Over a 1 MiB pread of a file that had never been read, 999424 bytes came
back as the contents of an unrelated file the same process had written
earlier. pread() returned 1048576 and errno was 0.
> diff --git a/drivers/scsi/libiscsi_tcp.c b/drivers/scsi/libiscsi_tcp.c
> index d35f93451ee9b..599bfed79847b 100644
> --- a/drivers/scsi/libiscsi_tcp.c
> +++ b/drivers/scsi/libiscsi_tcp.c
> @@ -397,6 +397,62 @@ void iscsi_tcp_hdr_recv_prep(struct iscsi_tcp_conn *tcp_conn)
> }
> EXPORT_SYMBOL_GPL(iscsi_tcp_hdr_recv_prep);
>
> +/**
> + * iscsi_tcp_check_data_in - verify a read received the data the target claimed
> + * @task: scsi command task
> + * @status: SCSI status of the PDU carrying the status
> + * @flags: flags of the PDU carrying the status
> + * @residual: residual count of that PDU
> + * @sense: sense data segment of that PDU, if it has one
> + * @senselen: length of @sense
> + *
> + * Check the payload only for a completion that reports the whole buffer as
> + * transferred. That is SAM_STAT_GOOD, and also CHECK_CONDITION with a sense
> + * key of RECOVERED_ERROR, which sd_done() counts as fully transferred. Any
> + * other status means the command failed and the data may legitimately be
> + * absent.
> + *
> + * ISCSI_FLAG_CMD_UNDERFLOW and ISCSI_FLAG_DATA_UNDERFLOW share a value, so the
> + * Data-In and SCSI Response paths both use this.
> + */
> +static int iscsi_tcp_check_data_in(struct iscsi_task *task, u8 status,
> + u32 flags, u32 residual,
> + const void *sense, u32 senselen)
> +{
> + struct iscsi_tcp_task *tcp_task = task->dd_data;
> + struct scsi_cmnd *sc = task->sc;
> + unsigned int expected;
> +
> + if (!sc || sc->sc_data_direction == DMA_TO_DEVICE)
> + return 0;
> +
> + if (status != SAM_STAT_GOOD) {
[Severity: High]
Does this code need to verify the iSCSI response field before checking status?
According to RFC 3720, if an iSCSI SCSI Response PDU has a non-zero response
field (indicating an iSCSI-level failure like Target Failure), the cmd_status
field is undefined and typically set to 0.
If response is non-zero and cmd_status is 0 (SAM_STAT_GOOD), this function
will enforce data_in_bytes == expected.
Because the command failed at the transport layer, no data was transferred
(data_in_bytes = 0). iscsi_tcp_check_data_in() will incorrectly return
ISCSI_ERR_DATALEN.
This error propagates up and causes the TCP connection to be entirely torn
down, instead of simply failing the individual SCSI command gracefully as the
pre-patch code did.
> + struct scsi_sense_hdr sshdr;
> + const u8 *s = sense;
> + u16 len;
> +
> + if (status != SAM_STAT_CHECK_CONDITION || !s || senselen < 2)
> + return 0;
[Severity: Critical]
Does this bypass the length validation for out-of-band RECOVERED_ERROR sense
data?
If a target responds to a read command with a short transfer and
SAM_STAT_CHECK_CONDITION, but omits inline sense data (datalen = 0), this
code sees !s and returns 0, bypassing the new tcp_task->data_in_bytes ==
expected payload validation.
The SCSI midlayer error handler will then issue REQUEST SENSE. If the target
replies with RECOVERED_ERROR, sd_done() will force good_bytes =
scsi_bufflen(SCpnt) and the midlayer will successfully complete the command.
This can lead to silent data corruption and kernel memory leaks, which matches
the exact issue this patch aimed to fix.
> + len = get_unaligned_be16(s);
> + if (len + 2U > senselen)
> + return 0;
> + if (!scsi_normalize_sense(s + 2, len, &sshdr) ||
> + sshdr.sense_key != RECOVERED_ERROR)
> + return 0;
> + }
> +
> + expected = sc->sdb.length;
> + if (flags & ISCSI_FLAG_DATA_UNDERFLOW) {
> + if (residual > expected)
> + return 0;
[Severity: Critical]
Can this early return allow an integer underflow that bypasses the payload
validation?
If the target sets ISCSI_FLAG_DATA_UNDERFLOW, this unconditionally returns 0
if residual > expected, bypassing the new payload length check.
The code assumes the SCSI midlayer will safely reject out-of-bounds residuals.
However, in the preexisting iscsi_scsi_cmd_rsp() and iscsi_data_in_rsp(), the
bounds check res_count <= scsi_bufflen(sc) is bypassed using a short-circuit
operator if ISCSI_FLAG_CMD_OVERFLOW is also set (which shares the exact same
bit value 0x04 as ISCSI_FLAG_DATA_OVERFLOW):
drivers/scsi/libiscsi.c:iscsi_scsi_cmd_rsp() {
...
if (res_count > 0 &&
(rhdr->flags & ISCSI_FLAG_CMD_OVERFLOW ||
res_count <= scsi_bufflen(sc)))
/* write side for bidi or uni-io set_resid */
scsi_set_resid(sc, res_count);
...
}
If a target sends a PDU with BOTH the Underflow and Overflow flags set, along
with a residual_count larger than the buffer, scsi_set_resid(sc, residual)
is executed with the malicious count.
In scsi_finish_command(), the calculation good_bytes -= scsi_get_resid(cmd)
suffers an integer underflow, wrapping good_bytes to an enormous positive
value.
scsi_io_completion() then passes this value to blk_update_request(), which
caps it to the request size and completes the entire buffer as a success.
This completely bypasses the patch's protection.
> + expected -= residual;
> + }
> + if (tcp_task->data_in_bytes == expected)
> + return 0;
> +
> + return ISCSI_ERR_DATALEN;
> +}
> +
> /*
> * Handle incoming reply to any other type of command
> */
> @@ -410,8 +466,30 @@ iscsi_tcp_data_recv_done(struct iscsi_tcp_conn *tcp_conn,
> if (!iscsi_tcp_dgst_verify(tcp_conn, segment))
> return ISCSI_ERR_DATA_DGST;
>
> - rc = iscsi_complete_pdu(conn, tcp_conn->in.hdr,
> - conn->data, tcp_conn->in.datalen);
> + if ((tcp_conn->in.hdr->opcode & ISCSI_OPCODE_MASK) ==
> + ISCSI_OP_SCSI_CMD_RSP) {
> + struct iscsi_scsi_rsp *rsp =
> + (struct iscsi_scsi_rsp *)tcp_conn->in.hdr;
> + struct iscsi_task *task;
> +
> + spin_lock(&conn->session->back_lock);
> + task = iscsi_itt_to_ctask(conn, rsp->itt);
> + if (!task) {
> + spin_unlock(&conn->session->back_lock);
> + return ISCSI_ERR_BAD_ITT;
> + }
> + rc = iscsi_tcp_check_data_in(task, rsp->cmd_status, rsp->flags,
> + be32_to_cpu(rsp->residual_count),
> + conn->data, tcp_conn->in.datalen);
[Severity: High]
Does this code need to pass or verify rsp->response before invoking the
payload length checks?
As mentioned above, if the iSCSI response field indicates a transport failure,
cmd_status is undefined. This caller passes rsp->cmd_status without verifying
the response field first.
[ ... ]
> @@ -783,6 +873,37 @@ iscsi_tcp_hdr_dissect(struct iscsi_conn *conn, struct iscsi_hdr *hdr)
> break;
> }
>
> + if (opcode == ISCSI_OP_SCSI_CMD_RSP) {
> + struct iscsi_scsi_rsp *rsp = (struct iscsi_scsi_rsp *)hdr;
> +
> + spin_lock(&conn->session->back_lock);
> + task = iscsi_itt_to_ctask(conn, hdr->itt);
> + if (!task) {
> + spin_unlock(&conn->session->back_lock);
> + return ISCSI_ERR_BAD_ITT;
> + }
> + if (tcp_conn->in.datalen) {
> + /*
> + * The sense is in the data segment, so the
> + * check has to wait for it.
> + */
> + spin_unlock(&conn->session->back_lock);
> + iscsi_tcp_data_recv_prep(tcp_conn);
> + return 0;
> + }
> + rc = iscsi_tcp_check_data_in(task, rsp->cmd_status,
> + rsp->flags,
> + be32_to_cpu(rsp->residual_count),
> + NULL, 0);
[Severity: High]
Is it safe to invoke this validation using the potentially undefined
cmd_status field without verifying if rsp->response is
ISCSI_STATUS_CMD_COMPLETED?
Like the previous caller, this omits checking the response field, leading to
the same connection teardown issue on target failure.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261007070207.666738-1-yhlee@isslab.korea.ac.kr?part=1
prev parent reply other threads:[~2026-10-07 7:21 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-07 7:02 [PATCH v4] scsi: libiscsi_tcp: check that a read got the data the target claimed Yehyeong Lee
2026-10-07 7:21 ` 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-162712@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=linux-scsi@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=yhlee@isslab.korea.ac.kr \
/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