* [PATCH] ata: libata-scsi: reject to-device transfers for simulated commands
@ 2026-10-27 9:08 Zhao Gongyi
2026-10-27 21:22 ` Niklas Cassel
2026-10-28 11:39 ` [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer Zhao Gongyi
0 siblings, 2 replies; 5+ messages in thread
From: Zhao Gongyi @ 2026-10-27 9:08 UTC (permalink / raw)
To: dlemoal, cassel; +Cc: linux-ide, linux-kernel, Zhao Gongyi
ata_scsi_rbuf_fill() emulates commands that only return data to the
initiator (INQUIRY, MODE SENSE, READ CAPACITY, REPORT LUNS and
MAINTENANCE IN). It builds the reply in a kernel buffer and then copies
it into the request's scatterlist without looking at the transfer
direction.
The scatterlist is built from the user buffer by the passthrough paths
(bsg/SG_IO), which derive the data direction from the transfer lengths
supplied by userspace. A request can therefore carry a data-in command
while declaring a to-device transfer. The buffer is then pinned for
reading only. In the observed crash, an anonymous VMA covered the
buffer address, but its page was not populated; the read-only pin
resolved to the shared zero page. Copying the reply into that
scatterlist attempted to write to a read-only kernel page:
BUG: unable to handle page fault for address: ffff888005140038
#PF: supervisor write access in kernel mode
#PF: error_code(0x0003) - permissions violation
PGD 7d00067 P4D 7d00067 PUD 7d01067 PMD 80000000050001a1
RIP: 0010:memcpy_orig+0x68/0x140
Call Trace:
sg_copy_buffer+0x1e2/0x250
ata_scsi_rbuf_fill+0xe6/0x280
__ata_scsi_queuecmd+0x12e4/0x1580
scsi_queue_rq+0x1ff7/0x33d0
blk_execute_rq+0x1c5/0x390
scsi_bsg_sg_io_fn+0x3bc/0xb00
bsg_ioctl+0x3ef/0x550
The faulting address falls inside the kernel rodata mapping and matches
empty_zero_page (Kernel rodata 05000000-05dc2fff, empty_zero_page at
physical 0x5140000), which confirms the reply copy attempted to write
to the shared read-only zero page.
Even when the pinned page happens to be writable the reply silently
corrupts the user buffer, since it was not mapped for writing. Reject
to-device transfers for these commands with an ILLEGAL REQUEST sense
instead of copying the reply into them. This is the same class of
problem as commit 5da5231bb478 ("libata: if T_LENGTH is zero, dma
direction should be DMA_NONE"), which rejects inconsistent data
directions for ATA pass-through commands.
Reproduced on 7.3.0-rc6 with the syzkaller C reproducer against an
ata_piix disk (/dev/bsg/0:0:0:0): the unpatched kernel panics on every
attempt (4/4), while with this change the request completes with CHECK
CONDITION and the reproducer survives 5/5 runs.
Standard-length CDBs behave the same way. A correctly directed 6-byte
INQUIRY still completes normally, while a 6-byte INQUIRY and a 10-byte
READ CAPACITY(10) declared as to-device are rejected with CHECK
CONDITION; on the unpatched kernel those same requests silently write
the reply into the caller's page (the transfer has to be page aligned to
take the zero-copy mapping path, otherwise it bounces through a kernel
buffer and nothing is visible to the caller).
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com>
---
drivers/ata/libata-scsi.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c
index b3666519b..8b65b896e 100644
--- a/drivers/ata/libata-scsi.c
+++ b/drivers/ata/libata-scsi.c
@@ -2009,6 +2009,12 @@ static void ata_scsi_rbuf_fill(struct ata_device *dev, struct scsi_cmnd *cmd,
unsigned long flags;
unsigned int len;
+ /* A data-out request may contain pages pinned without write access. */
+ if (cmd->sc_data_direction == DMA_TO_DEVICE) {
+ ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, 0x24, 0x0);
+ return;
+ }
+
spin_lock_irqsave(&ata_scsi_rbuf_lock, flags);
memset(ata_scsi_rbuf, 0, ATA_SCSI_RBUF_SIZE);
--
2.39.5 (Apple Git-154)
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: [PATCH] ata: libata-scsi: reject to-device transfers for simulated commands 2026-10-27 9:08 [PATCH] ata: libata-scsi: reject to-device transfers for simulated commands Zhao Gongyi @ 2026-10-27 21:22 ` Niklas Cassel 2026-10-28 11:39 ` [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer Zhao Gongyi 1 sibling, 0 replies; 5+ messages in thread From: Niklas Cassel @ 2026-10-27 21:22 UTC (permalink / raw) To: Zhao Gongyi; +Cc: dlemoal, linux-ide, linux-kernel Hello Zhao, On Tue, Oct 27, 2026 at 05:08:09PM +0800, Zhao Gongyi wrote: > ata_scsi_rbuf_fill() emulates commands that only return data to the > initiator (INQUIRY, MODE SENSE, READ CAPACITY, REPORT LUNS and > MAINTENANCE IN). It builds the reply in a kernel buffer and then copies > it into the request's scatterlist without looking at the transfer > direction. > > The scatterlist is built from the user buffer by the passthrough paths > (bsg/SG_IO), which derive the data direction from the transfer lengths > supplied by userspace. A request can therefore carry a data-in command > while declaring a to-device transfer. The buffer is then pinned for > reading only. In the observed crash, an anonymous VMA covered the > buffer address, but its page was not populated; the read-only pin > resolved to the shared zero page. Copying the reply into that > scatterlist attempted to write to a read-only kernel page: > > BUG: unable to handle page fault for address: ffff888005140038 > #PF: supervisor write access in kernel mode > #PF: error_code(0x0003) - permissions violation > PGD 7d00067 P4D 7d00067 PUD 7d01067 PMD 80000000050001a1 > RIP: 0010:memcpy_orig+0x68/0x140 > Call Trace: > sg_copy_buffer+0x1e2/0x250 > ata_scsi_rbuf_fill+0xe6/0x280 > __ata_scsi_queuecmd+0x12e4/0x1580 > scsi_queue_rq+0x1ff7/0x33d0 > blk_execute_rq+0x1c5/0x390 > scsi_bsg_sg_io_fn+0x3bc/0xb00 > bsg_ioctl+0x3ef/0x550 > > The faulting address falls inside the kernel rodata mapping and matches > empty_zero_page (Kernel rodata 05000000-05dc2fff, empty_zero_page at > physical 0x5140000), which confirms the reply copy attempted to write > to the shared read-only zero page. > > Even when the pinned page happens to be writable the reply silently > corrupts the user buffer, since it was not mapped for writing. Reject > to-device transfers for these commands with an ILLEGAL REQUEST sense > instead of copying the reply into them. This is the same class of > problem as commit 5da5231bb478 ("libata: if T_LENGTH is zero, dma > direction should be DMA_NONE"), which rejects inconsistent data > directions for ATA pass-through commands. > > Reproduced on 7.3.0-rc6 with the syzkaller C reproducer against an > ata_piix disk (/dev/bsg/0:0:0:0): the unpatched kernel panics on every > attempt (4/4), while with this change the request completes with CHECK > CONDITION and the reproducer survives 5/5 runs. > > Standard-length CDBs behave the same way. A correctly directed 6-byte > INQUIRY still completes normally, while a 6-byte INQUIRY and a 10-byte > READ CAPACITY(10) declared as to-device are rejected with CHECK > CONDITION; on the unpatched kernel those same requests silently write > the reply into the caller's page (the transfer has to be page aligned to > take the zero-copy mapping path, otherwise it bounces through a kernel > buffer and nothing is visible to the caller). > > Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") > Cc: stable@vger.kernel.org > Assisted-by: LLM The Assisted-by tag is deprecated since commit 1e035297ffda ("docs: remove Assisted-by references"). > Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com> > --- > drivers/ata/libata-scsi.c | 6 ++++++ > 1 file changed, 6 insertions(+) > > diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c > index b3666519b..8b65b896e 100644 > --- a/drivers/ata/libata-scsi.c > +++ b/drivers/ata/libata-scsi.c > @@ -2009,6 +2009,12 @@ static void ata_scsi_rbuf_fill(struct ata_device *dev, struct scsi_cmnd *cmd, > unsigned long flags; > unsigned int len; > > + /* A data-out request may contain pages pinned without write access. */ > + if (cmd->sc_data_direction == DMA_TO_DEVICE) { > + ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, 0x24, 0x0); > + return; > + } > + This patch does not compile on mainline. Please rebase on latest Linus' tree. The fixed call will be: ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, INVALID_FIELD_IN_CDB); Your patch seems to only patch simulated commands (ata_scsi_rbuf_fill()), I think you need to patch translated commands too (ata_scsi_translate()), something like: diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c index 5cde90f..6fba9da 100644 --- a/drivers/ata/libata-scsi.c +++ b/drivers/ata/libata-scsi.c @@ -2048,6 +2048,19 @@ static int ata_scsi_translate(struct ata_device *dev, struct scsi_cmnd *cmd, if (xlat_func(qc)) goto done; + /* + * A data-out request may contain pages pinned without write access, so + * it must not be issued as a device-to-host transfer. Reject it before + * issuing, so that a completion handler never rewrites the buffer. + */ + if (unlikely(cmd->sc_data_direction == DMA_TO_DEVICE && + ata_is_data(qc->tf.protocol) && + !(qc->tf.flags & ATA_TFLAG_WRITE))) { + ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, + INVALID_FIELD_IN_CDB); + goto done; + } + return ata_scsi_qc_issue(ap, qc); Rejecting before issuing is important because the ZBC IN and GET PHYSICAL ELEMENT STATUS completions rewrite the buffer even when the drive aborts the command. Kind regards, Niklas ^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer 2026-10-27 9:08 [PATCH] ata: libata-scsi: reject to-device transfers for simulated commands Zhao Gongyi 2026-10-27 21:22 ` Niklas Cassel @ 2026-10-28 11:39 ` Zhao Gongyi 2026-10-28 13:59 ` Damien Le Moal 1 sibling, 1 reply; 5+ messages in thread From: Zhao Gongyi @ 2026-10-28 11:39 UTC (permalink / raw) To: Damien Le Moal, Niklas Cassel Cc: linux-ide, linux-kernel, Zhao Gongyi, stable ata_scsi_rbuf_fill() copies simulated data-in replies to the SCSI scatterlist without checking the request direction. SG_IO can supply a data-in CDB with DMA_TO_DEVICE, in which case the pages may have been pinned without write access. In the reported case, copying the reply into the shared zero page caused a kernel page fault. Translated commands can reach the same kind of mismatch. In particular, the ZBC IN and GET PHYSICAL ELEMENT STATUS completion handlers rewrite the receive buffer, including when the device aborts the command. Checking the direction only in ata_scsi_rbuf_fill() leaves those paths unprotected. Reject DMA_TO_DEVICE requests in the simulated-command path. After translation, reject a DMA_TO_DEVICE request whose ATA taskfile performs a data-in transfer, before issuing it to the device. Return ILLEGAL REQUEST / INVALID FIELD IN CDB for both cases. The original reproducer panicked in ata_scsi_rbuf_fill() on all four runs with the unpatched kernel. With this patch applied, it completed without a crash. A diagnostic run confirmed CHECK CONDITION with ILLEGAL REQUEST / INVALID FIELD IN CDB. Built an x86_64 defconfig kernel from the current Linus tree and booted it in QEMU with an ata_piix disk. Normal INQUIRY and READ(10) SG_IO requests succeeded. Each command declared as SG_DXFER_TO_DEV returned CHECK CONDITION with ILLEGAL REQUEST / INVALID FIELD IN CDB. No kernel warnings or oopses appeared. ZBC IN and GET PHYSICAL ELEMENT STATUS were checked by code inspection; the test disk does not support them. Suggested-by: Niklas Cassel <cassel@kernel.org> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Link: https://lore.kernel.org/r/20261027090809.94126-1-zhaogongyi@bytedance.com Cc: stable@vger.kernel.org Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com> --- Changes since v1: - Rebase on Linus' tree and use INVALID_FIELD_IN_CDB with the current ata_scsi_set_sense() signature. - Reject mismatched translated data-in commands before issuing them, as suggested by Niklas Cassel. - Remove the deprecated Assisted-by trailer. drivers/ata/libata-scsi.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c index a61fb0331..a5170a36a 100644 --- a/drivers/ata/libata-scsi.c +++ b/drivers/ata/libata-scsi.c @@ -2048,6 +2048,19 @@ static int ata_scsi_translate(struct ata_device *dev, struct scsi_cmnd *cmd, if (xlat_func(qc)) goto done; + /* + * A data-out request may contain pages pinned without write access. + * Reject a translated data-in command before issuing it, since its + * completion handler may write to the buffer even if the device fails. + */ + if (unlikely(cmd->sc_data_direction == DMA_TO_DEVICE && + ata_is_data(qc->tf.protocol) && + !(qc->tf.flags & ATA_TFLAG_WRITE))) { + ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, + INVALID_FIELD_IN_CDB); + goto done; + } + return ata_scsi_qc_issue(ap, qc); done: @@ -2079,6 +2092,13 @@ static void ata_scsi_rbuf_fill(struct ata_device *dev, struct scsi_cmnd *cmd, unsigned long flags; unsigned int len; + /* A data-out request may contain pages pinned without write access. */ + if (cmd->sc_data_direction == DMA_TO_DEVICE) { + ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, + INVALID_FIELD_IN_CDB); + return; + } + spin_lock_irqsave(&ata_scsi_rbuf_lock, flags); memset(ata_scsi_rbuf, 0, ATA_SCSI_RBUF_SIZE); -- 2.39.5 (Apple Git-154) ^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer 2026-10-28 11:39 ` [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer Zhao Gongyi @ 2026-10-28 13:59 ` Damien Le Moal 2026-10-29 4:37 ` Zhao Gongyi 0 siblings, 1 reply; 5+ messages in thread From: Damien Le Moal @ 2026-10-28 13:59 UTC (permalink / raw) To: Zhao Gongyi, Niklas Cassel; +Cc: linux-ide, linux-kernel, stable On 2026/10/28 18:39, Zhao Gongyi wrote: > ata_scsi_rbuf_fill() copies simulated data-in replies to the SCSI > scatterlist without checking the request direction. SG_IO can supply a > data-in CDB with DMA_TO_DEVICE, in which case the pages may have been > pinned without write access. In the reported case, copying the reply > into the shared zero page caused a kernel page fault. Sure that can happen if the user sets up the SG_IO incorrectly. > Translated commands can reach the same kind of mismatch. In particular, No they cannot, as long as the kernel is correct for internally issued commands. But t is not clear here if you are still talking about commands passed through SG_IO. > the ZBC IN and GET PHYSICAL ELEMENT STATUS completion handlers rewrite > the receive buffer, including when the device aborts the command. > Checking the direction only in ata_scsi_rbuf_fill() leaves those paths > unprotected. > > Reject DMA_TO_DEVICE requests in the simulated-command path. After > translation, reject a DMA_TO_DEVICE request whose ATA taskfile performs > a data-in transfer, before issuing it to the device. Return ILLEGAL > REQUEST / INVALID FIELD IN CDB for both cases. Why is this being checked here and not in the SCSI layer (sg driver and bsg)? It seems sensible to check this there. > The original reproducer panicked in ata_scsi_rbuf_fill() on all four > runs with the unpatched kernel. With this patch applied, it completed > without a crash. A diagnostic run confirmed CHECK CONDITION with > ILLEGAL REQUEST / INVALID FIELD IN CDB. > > Built an x86_64 defconfig kernel from the current Linus tree and booted it > in QEMU with an ata_piix disk. Normal INQUIRY and READ(10) SG_IO requests > succeeded. Each command declared as SG_DXFER_TO_DEV returned CHECK > CONDITION with ILLEGAL REQUEST / INVALID FIELD IN CDB. No kernel > warnings or oopses appeared. ZBC IN and GET PHYSICAL ELEMENT STATUS > were checked by code inspection; the test disk does not support them. Test details are not needed in the commit message. > > Suggested-by: Niklas Cassel <cassel@kernel.org> > Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") > Link: https://lore.kernel.org/r/20261027090809.94126-1-zhaogongyi@bytedance.com > Cc: stable@vger.kernel.org > Signed-off-by: Zhao Gongyi <zhaogongyi@bytedance.com> > --- > Changes since v1: > - Rebase on Linus' tree and use INVALID_FIELD_IN_CDB with the current > ata_scsi_set_sense() signature. > - Reject mismatched translated data-in commands before issuing them, > as suggested by Niklas Cassel. > - Remove the deprecated Assisted-by trailer. > > drivers/ata/libata-scsi.c | 20 ++++++++++++++++++++ > 1 file changed, 20 insertions(+) > > diff --git a/drivers/ata/libata-scsi.c b/drivers/ata/libata-scsi.c > index a61fb0331..a5170a36a 100644 > --- a/drivers/ata/libata-scsi.c > +++ b/drivers/ata/libata-scsi.c > @@ -2048,6 +2048,19 @@ static int ata_scsi_translate(struct ata_device *dev, struct scsi_cmnd *cmd, > if (xlat_func(qc)) > goto done; > > + /* > + * A data-out request may contain pages pinned without write access. > + * Reject a translated data-in command before issuing it, since its > + * completion handler may write to the buffer even if the device fails. > + */ > + if (unlikely(cmd->sc_data_direction == DMA_TO_DEVICE && > + ata_is_data(qc->tf.protocol) && > + !(qc->tf.flags & ATA_TFLAG_WRITE))) { > + ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, > + INVALID_FIELD_IN_CDB); The CDB may be perfectly correct. The error is in the SG_IO descriptor setup, NOT in the CDB. So returning invalid field in CDB seems very misleading. This is not something that SAT defines as far as I know, so we need to find a sense code that is more informative. Please have a loo at include/scsi/scsi_sense.h to see if something better exist. > + goto done; > + } > + > return ata_scsi_qc_issue(ap, qc); > > done: > @@ -2079,6 +2092,13 @@ static void ata_scsi_rbuf_fill(struct ata_device *dev, struct scsi_cmnd *cmd, > unsigned long flags; > unsigned int len; > > + /* A data-out request may contain pages pinned without write access. */ > + if (cmd->sc_data_direction == DMA_TO_DEVICE) { > + ata_scsi_set_sense(dev, cmd, ILLEGAL_REQUEST, > + INVALID_FIELD_IN_CDB); > + return; > + } > + > spin_lock_irqsave(&ata_scsi_rbuf_lock, flags); > > memset(ata_scsi_rbuf, 0, ATA_SCSI_RBUF_SIZE); -- Damien Le Moal Western Digital Research ^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer 2026-10-28 13:59 ` Damien Le Moal @ 2026-10-29 4:37 ` Zhao Gongyi 0 siblings, 0 replies; 5+ messages in thread From: Zhao Gongyi @ 2026-10-29 4:37 UTC (permalink / raw) To: Damien Le Moal, Niklas Cassel; +Cc: linux-ide, linux-kernel, stable Hi Damien, Thanks for the review. On 10/28/26 9:59 PM, Damien Le Moal wrote: > On 2026/10/28 18:39, Zhao Gongyi wrote: > > No they cannot, as long as the kernel is correct for internally issued commands. > But t is not clear here if you are still talking about commands passed through > SG_IO. > Yes, by "translated commands" I meant commands submitted through SG_IO and subsequently translated by libata, not commands issued internally by the kernel. I will make that explicit and narrow the description to the affected ZBC IN and GET PHYSICAL ELEMENT STATUS paths. > > Why is this being checked here and not in the SCSI layer (sg driver and bsg)? It > seems sensible to check this there. > I checked the SG and BSG submission paths. For data-direction purposes, they derive the request direction from the SG_IO descriptor and otherwise treat the CDB as opaque. I could not find a generic helper that derives the transfer direction of arbitrary standard, service-action, ATA pass-through, and vendor-specific CDBs. For simulated commands, libata itself performs the CPU-side write. For the ZBC IN and GET PHYSICAL ELEMENT STATUS translation paths, the actual ATA data direction is known after xlat_func() has built qc->tf, and their completion handlers may modify the receive buffer. This is why I currently think that checking these paths in libata is the smallest fix suitable for stable. A broader SG/BSG direction-validation policy may also be useful, but would likely be a separate change. > > Test details are not needed in the commit message. > Agreed. I will remove the detailed validation results from the commit message and keep them below the --- separator. > > The CDB may be perfectly correct. The error is in the SG_IO descriptor setup, > NOT in the CDB. So returning invalid field in CDB seems very misleading. > This is not something that SAT defines as far as I know, so we need to find a > sense code that is more informative. Please have a loo at > include/scsi/scsi_sense.h to see if something better exist. > Agreed. INVALID FIELD IN CDB is misleading here. I propose returning ABORTED COMMAND / DATA PHASE ERROR (4b/00) instead. Would this approach be acceptable? If so, I will send a v3 with the clarified description, the shorter commit message, and the updated sense data. Thanks, Gongyi ^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-29 4:37 UTC | newest] Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-27 9:08 [PATCH] ata: libata-scsi: reject to-device transfers for simulated commands Zhao Gongyi 2026-10-27 21:22 ` Niklas Cassel 2026-10-28 11:39 ` [PATCH v2] ata: libata-scsi: reject data-in commands with a data-out buffer Zhao Gongyi 2026-10-28 13:59 ` Damien Le Moal 2026-10-29 4:37 ` Zhao Gongyi
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®