* [PATCH v2 0/6] Support for Zoned UFS
[not found] <CGME20261016092423epcms2p83f80a8dd8f7c1ddc3a94541e0771c306@epcms2p8>
@ 2026-10-16 9:24 ` Daejun Park
2026-10-16 9:25 ` [PATCH v2 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
` (5 more replies)
0 siblings, 6 replies; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:24 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park, qemu-stable@nongnu.org
This series lets a ufs-lu be a host-managed zoned block device (ZBC)
with zoned=on, so that the zoned storage stack, for example f2fs on
host-managed zones, can be tested on UFS without zoned UFS hardware.
Patches 1-3 fix the command path that the zoned code relies on and can
be merged first; patch 4 documents zoned logical units, patch 5 adds
them and patch 6 the tests.
Not modelled: persistent zone state, conventional and gap zones, and the
zoned UFS descriptor bits.
Tested with the UFS qtests (also under ASan and UBSan) and with Linux
7.3-rc7 and android17-6.18 guests, legacy doorbell and MCQ: fio
--zonemode=zbd with verification, blkzone, and f2fs on android17-6.18.
Changes in v2:
- Drop the hw/scsi patch: scsi-hd stays TYPE_DISK and ufs-lu reports
TYPE_ZBC itself. With v1's TYPE_ZBC scsi-hd, MODE SENSE failed and the
guest never sent a flush.
- New patches 1-3: a crash on commands without a PRDT, SYNCHRONIZE
CACHE(16), and PRDTs longer than the CDB transfer length.
- Fix v1's REPORT ZONES buffer overflow, NULL zone dereference past the
last zone, division by zero with zoned.zone_size=0, and its zone state
handling.
- Move the write pointer back when a write fails or is aborted, reject
UNMAP, drop zoned.zone_capacity.
- Add trace events, tests and documentation.
- Jeuk's Reviewed-by on v1 is not carried over, since both patches
changed substantially.
- Link to v1: https://lore.kernel.org/qemu-devel/20231208060902epcms2p3810b29fefbddaf16a7f3f2758cf218ba@epcms2p3
---
Daejun Park (6):
hw/ufs: Fail SCSI commands that move data without a PRDT
hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10)
hw/ufs: Limit data transfers to the transfer length of the CDB
docs/system: Document zoned UFS logical units
hw/ufs: Support for Zoned UFS
tests/qtest: Add tests for Zoned UFS
docs/system/devices/ufs.rst | 55 +++
hw/ufs/lu.c | 886 ++++++++++++++++++++++++++++++++-
hw/ufs/trace-events | 6 +
hw/ufs/ufs.h | 36 ++
include/block/ufs.h | 27 +
tests/qtest/ufs-test.c | 1146 +++++++++++++++++++++++++++++++++++++++++++
6 files changed, 2151 insertions(+), 5 deletions(-)
---
base-commit: 7e8d3c7fc460fe7cd28a3e423d7466fa292bc269
change-id: 20261016-zoned-ufs-0a6c741446a9
Best regards,
--
Daejun Park <daejun7.park@samsung.com>
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
@ 2026-10-16 9:25 ` Daejun Park
2026-10-16 9:27 ` [PATCH v2 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10) Daejun Park
` (4 subsequent siblings)
5 siblings, 0 replies; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:25 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
qemu-stable@nongnu.org, Daejun Park
A command whose CDB transfers data, sent with a PRDT length of 0,
reaches scsi-hd without an SG list. The SCSI layer then calls the
transfer_data callback of the bus, which ufs_scsi_info does not set, and
QEMU dies with SIGSEGV. A guest triggers this with, for example, an
INQUIRY whose PRDT is empty; the new no-prdt qtest crashed QEMU before
this change.
Check the transfer direction that scsi_req_new() parsed, and fail such a
command with OCS INVALID COMMAND TABLE ATTRIBUTES before it reaches
scsi-hd. virtio-scsi likewise fails a command that has no matching
buffer (VIRTIO_SCSI_S_OVERRUN). A request that only reports a pending
unit attention moves no data and goes through, so that the guest still
sees the unit attention.
Handing scsi-hd an empty SG list instead is not enough: READ and WRITE
then complete synchronously in dma_blk_io() and trip the assertion in
scsi_dma_complete().
Fixes: 2a8b36a4967d ("hw/ufs: Support for UFS logical unit")
Cc: qemu-stable@nongnu.org
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
hw/ufs/lu.c | 24 ++++++++++++++++++++----
tests/qtest/ufs-test.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index 5905c49fac..1853284acf 100644
--- a/hw/ufs/lu.c
+++ b/hw/ufs/lu.c
@@ -382,6 +382,8 @@ static UfsReqResult ufs_emulate_scsi_cmd(UfsLu *lu, UfsRequest *req)
static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
{
uint8_t task_tag = req->req_upiu.header.task_tag;
+ SCSIRequest *scsi_req;
+ uint32_t len;
/*
* Each ufs-lu has its own independent virtual SCSI bus. Therefore, we can't
@@ -393,12 +395,26 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
return ufs_emulate_scsi_cmd(lu, req);
}
- SCSIRequest *scsi_req =
- scsi_req_new(lu->scsi_dev, task_tag, lu->lun, req->req_upiu.sc.cdb,
- UFS_CDB_SIZE, req);
+ scsi_req = scsi_req_new(lu->scsi_dev, task_tag, lu->lun,
+ req->req_upiu.sc.cdb, UFS_CDB_SIZE, req);
+
+ /*
+ * A command that moves data needs a PRDT. Without one there is no SG
+ * list, and the SCSI layer would call the transfer_data callback, which
+ * ufs-lu does not set. Fail the request, as virtio-scsi does when the
+ * buffer does not match the command. A request that reports a pending
+ * unit attention instead moves no data; its sense is already set, so let
+ * it through and the guest sees the UA.
+ */
+ if (scsi_req->cmd.mode != SCSI_XFER_NONE && !req->sg &&
+ !scsi_req->sense_len) {
+ scsi_req_unref(scsi_req);
+ return UFS_REQUEST_FAIL;
+ }
+
req->sreq = scsi_req;
- uint32_t len = scsi_req_enqueue(scsi_req);
+ len = scsi_req_enqueue(scsi_req);
if (len) {
scsi_req_continue(scsi_req);
}
diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index 9a9c5ca834..63f805419e 100644
--- a/tests/qtest/ufs-test.c
+++ b/tests/qtest/ufs-test.c
@@ -770,6 +770,50 @@ static void ufstest_read_write(void *obj, void *data, QGuestAllocator *alloc)
ufs_exit(ufs, alloc);
}
+static void ufstest_no_prdt(void *obj, void *data, QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t buf[4096];
+ const uint8_t inquiry_cdb[UFS_CDB_SIZE] = {
+ /* INQUIRY, allocation length 36 */
+ INQUIRY, 0x00, 0x00, 0x00, 0x24
+ };
+ const uint8_t write_cdb[UFS_CDB_SIZE] = {
+ /* WRITE(10) to LBA 0, transfer length 1 */
+ WRITE_10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00
+ };
+ enum UtpOcsCodes ocs;
+ UtpUpiuRsp rsp_upiu;
+ const int test_lun = 1;
+
+ ufs_init(ufs, alloc);
+
+ /* A pending unit attention is reported even without a PRDT */
+ ocs = ufs_send_scsi_command(ufs, test_lun, write_cdb, NULL, 0, NULL, 0,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, CHECK_CONDITION);
+ g_assert_cmpuint(rsp_upiu.sr.sense_data[2] & 0xf, ==, UNIT_ATTENTION);
+
+ /* Commands that move data but come without a PRDT fail */
+ ocs = ufs_send_scsi_command(ufs, test_lun, inquiry_cdb, NULL, 0, NULL, 0,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_INVALID_CMD_TABLE_ATTR);
+
+ ocs = ufs_send_scsi_command(ufs, test_lun, write_cdb, NULL, 0, NULL, 0,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_INVALID_CMD_TABLE_ATTR);
+
+ /* The logical unit keeps working */
+ ocs = ufs_send_scsi_command(ufs, test_lun, inquiry_cdb, NULL, 0, buf, 0x24,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(buf[0] & 0x1f, ==, TYPE_DISK);
+
+ ufs_exit(ufs, alloc);
+}
+
static void ufstest_mcq_cq_wraparound(void *obj, void *data,
QGuestAllocator *alloc)
{
@@ -1721,6 +1765,7 @@ static void ufs_register_nodes(void)
qos_add_test("init", "ufs", ufstest_init, NULL);
qos_add_test("legacy-read-write", "ufs", ufstest_read_write, &io_test_opts);
qos_add_test("mcq-read-write", "ufs", ufstest_read_write, &mcq_test_opts);
+ qos_add_test("no-prdt", "ufs", ufstest_no_prdt, &io_test_opts);
qos_add_test("mcq-cq-wraparound", "ufs", ufstest_mcq_cq_wraparound,
&mcq_test_opts);
qos_add_test("query-flag", "ufs", ufstest_query_flag_request,
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10)
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
2026-10-16 9:25 ` [PATCH v2 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
@ 2026-10-16 9:27 ` Daejun Park
2026-10-16 9:31 ` [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB Daejun Park
` (3 subsequent siblings)
5 siblings, 0 replies; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:27 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
scsi-hd implements SYNCHRONIZE CACHE(10) only and fails the 16-byte
variant with INVALID COMMAND OPERATION CODE. Linux sends SYNCHRONIZE
CACHE(16) to zoned disks (sd_zbc sets use_16_for_sync), so once a
logical unit can be zoned, every flush from such a guest fails. With
the zoned logical units that a later commit adds and without this
change, a Linux 7.3-rc7 guest logged
sd 2:0:0:1: [sdb] tag#9 Add. Sense: Invalid command operation code
critical target error, dev sdb, sector 0 op 0x1:(WRITE) flags 0x800
and fsync() on the disk failed with EREMOTEIO.
scsi-hd flushes the whole medium for SYNCHRONIZE CACHE(10) and does not
look at its LBA range. Hand the 16-byte command to scsi-hd as the
10-byte one and keep the IMMED bit. This keeps the UFS change inside
hw/ufs; if scsi-disk learns SYNCHRONIZE CACHE(16), the translation can
go.
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
hw/ufs/lu.c | 17 +++++++++++++++--
tests/qtest/ufs-test.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index 1853284acf..b812e1bef8 100644
--- a/hw/ufs/lu.c
+++ b/hw/ufs/lu.c
@@ -382,6 +382,8 @@ static UfsReqResult ufs_emulate_scsi_cmd(UfsLu *lu, UfsRequest *req)
static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
{
uint8_t task_tag = req->req_upiu.header.task_tag;
+ uint8_t *cdb = req->req_upiu.sc.cdb;
+ uint8_t sync_cache_10[UFS_CDB_SIZE] = { SYNCHRONIZE_CACHE };
SCSIRequest *scsi_req;
uint32_t len;
@@ -395,8 +397,19 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
return ufs_emulate_scsi_cmd(lu, req);
}
- scsi_req = scsi_req_new(lu->scsi_dev, task_tag, lu->lun,
- req->req_upiu.sc.cdb, UFS_CDB_SIZE, req);
+ /*
+ * scsi-hd implements SYNCHRONIZE CACHE(10) only, while Linux sends the
+ * 16-byte variant to zoned disks. scsi-hd flushes the whole medium for
+ * SYNCHRONIZE CACHE(10) whatever its LBA range, so pass the 16-byte
+ * command on as the 10-byte one, keeping IMMED.
+ */
+ if (cdb[0] == SYNCHRONIZE_CACHE_16) {
+ sync_cache_10[1] = cdb[1] & 0x02;
+ cdb = sync_cache_10;
+ }
+
+ scsi_req = scsi_req_new(lu->scsi_dev, task_tag, lu->lun, cdb, UFS_CDB_SIZE,
+ req);
/*
* A command that moves data needs a PRDT. Without one there is no SG
diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index 63f805419e..594b91706b 100644
--- a/tests/qtest/ufs-test.c
+++ b/tests/qtest/ufs-test.c
@@ -814,6 +814,51 @@ static void ufstest_no_prdt(void *obj, void *data, QGuestAllocator *alloc)
ufs_exit(ufs, alloc);
}
+static void ufstest_sync_cache(void *obj, void *data, QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t buf[4096];
+ const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+ REQUEST_SENSE,
+ };
+ const uint8_t sync_cache_10_cdb[UFS_CDB_SIZE] = {
+ SYNCHRONIZE_CACHE,
+ };
+ const uint8_t sync_cache_16_cdb[UFS_CDB_SIZE] = {
+ SYNCHRONIZE_CACHE_16,
+ };
+ const uint8_t sync_cache_16_immed_cdb[UFS_CDB_SIZE] = {
+ SYNCHRONIZE_CACHE_16, 0x02 /* IMMED */
+ };
+ enum UtpOcsCodes ocs;
+ UtpUpiuRsp rsp_upiu;
+ const int test_lun = 1;
+
+ ufs_init(ufs, alloc);
+
+ /* Clear Unit Attention */
+ ocs = ufs_send_scsi_command(ufs, test_lun, request_sense_cdb, NULL, 0,
+ buf, sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+
+ ocs = ufs_send_scsi_command(ufs, test_lun, sync_cache_10_cdb, NULL, 0,
+ NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+
+ ocs = ufs_send_scsi_command(ufs, test_lun, sync_cache_16_cdb, NULL, 0,
+ NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+
+ ocs = ufs_send_scsi_command(ufs, test_lun, sync_cache_16_immed_cdb, NULL,
+ 0, NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+
+ ufs_exit(ufs, alloc);
+}
+
static void ufstest_mcq_cq_wraparound(void *obj, void *data,
QGuestAllocator *alloc)
{
@@ -1766,6 +1811,7 @@ static void ufs_register_nodes(void)
qos_add_test("legacy-read-write", "ufs", ufstest_read_write, &io_test_opts);
qos_add_test("mcq-read-write", "ufs", ufstest_read_write, &mcq_test_opts);
qos_add_test("no-prdt", "ufs", ufstest_no_prdt, &io_test_opts);
+ qos_add_test("sync-cache", "ufs", ufstest_sync_cache, &io_test_opts);
qos_add_test("mcq-cq-wraparound", "ufs", ufstest_mcq_cq_wraparound,
&mcq_test_opts);
qos_add_test("query-flag", "ufs", ufstest_query_flag_request,
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
2026-10-16 9:25 ` [PATCH v2 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
2026-10-16 9:27 ` [PATCH v2 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10) Daejun Park
@ 2026-10-16 9:31 ` Daejun Park
2026-10-16 11:54 ` Stanley Jhu
2026-10-16 9:32 ` [PATCH v2 4/6] docs/system: Document zoned UFS logical units Daejun Park
` (2 subsequent siblings)
5 siblings, 1 reply; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:31 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
scsi-hd moves as many bytes as the SG list of a request holds, and
ufs-lu builds that list from the whole PRDT. When the PRDT is larger
than the transfer length in the CDB, a WRITE writes more blocks than
the CDB names and a READ reads more. A one-block WRITE(10) at LBA 0
with an 8 KiB PRDT also wrote LBA 1, and its response reported no
residual. A UFS device transfers what the CDB asks for and reports the
rest of the Expected Data Transfer Length as a residual.
The commands that scsi-hd emulates had their residual computed against
the PRDT too: an INQUIRY with an allocation length of 36, a 4 KiB PRDT
and an Expected Data Transfer Length of 4096 reported an overflow of
4294959176 bytes. It now reports an underflow of 4060.
Cut the SG list to the transfer length of the parsed command before
the request reaches scsi-hd. The new long-prdt qtest failed before this
change.
Fixes: 2a8b36a4967d ("hw/ufs: Support for UFS logical unit")
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
hw/ufs/lu.c | 28 ++++++++++++++++++++
tests/qtest/ufs-test.c | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 99 insertions(+)
diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index b812e1bef8..bebe4eaac2 100644
--- a/hw/ufs/lu.c
+++ b/hw/ufs/lu.c
@@ -379,6 +379,24 @@ static UfsReqResult ufs_emulate_scsi_cmd(UfsLu *lu, UfsRequest *req)
return UFS_REQUEST_SUCCESS;
}
+/* Keep only the first len bytes of the SG list built from the PRDT */
+static void ufs_limit_sg_list(UfsRequest *req, uint64_t len)
+{
+ QEMUSGList *prdt_sg = req->sg;
+ QEMUSGList *sg = g_new0(QEMUSGList, 1);
+ int i;
+
+ qemu_sglist_init(sg, prdt_sg->dev, prdt_sg->nsg, prdt_sg->as);
+ for (i = 0; i < prdt_sg->nsg && sg->size < len; i++) {
+ qemu_sglist_add(sg, prdt_sg->sg[i].base,
+ MIN(prdt_sg->sg[i].len, len - sg->size));
+ }
+ qemu_sglist_destroy(prdt_sg);
+ g_free(prdt_sg);
+ req->sg = sg;
+ req->data_len = sg->size;
+}
+
static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
{
uint8_t task_tag = req->req_upiu.header.task_tag;
@@ -425,6 +443,16 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
return UFS_REQUEST_FAIL;
}
+ /*
+ * scsi-hd moves as many bytes as the SG list holds, while a UFS device
+ * transfers what the CDB asks for. Cut a longer SG list to the transfer
+ * length; the response reports the rest of the Expected Data Transfer
+ * Length as a residual.
+ */
+ if (req->sg && req->sg->size > scsi_req->cmd.xfer) {
+ ufs_limit_sg_list(req, scsi_req->cmd.xfer);
+ }
+
req->sreq = scsi_req;
len = scsi_req_enqueue(scsi_req);
diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index 594b91706b..4e0c2aee5f 100644
--- a/tests/qtest/ufs-test.c
+++ b/tests/qtest/ufs-test.c
@@ -859,6 +859,76 @@ static void ufstest_sync_cache(void *obj, void *data, QGuestAllocator *alloc)
ufs_exit(ufs, alloc);
}
+static void ufstest_long_prdt(void *obj, void *data, QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+ REQUEST_SENSE,
+ };
+ /* One block at LBA 0 and at LBA 1 */
+ const uint8_t write_lba0_cdb[UFS_CDB_SIZE] = {
+ WRITE_10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ };
+ const uint8_t read_lba0_cdb[UFS_CDB_SIZE] = {
+ READ_10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ };
+ const uint8_t read_lba1_cdb[UFS_CDB_SIZE] = {
+ READ_10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,
+ };
+ /* Allocation length 36 */
+ const uint8_t inquiry_cdb[UFS_CDB_SIZE] = {
+ INQUIRY, 0x00, 0x00, 0x00, 0x24, 0x00,
+ };
+ uint8_t buf[2 * 4096];
+ uint8_t zero_buf[4096] = { 0 };
+ uint8_t write_buf[2 * 4096];
+ UtpUpiuRsp rsp_upiu, write_rsp_upiu;
+ enum UtpOcsCodes ocs;
+
+ ufs_init(ufs, alloc);
+ ocs = ufs_send_scsi_command(ufs, 1, request_sense_cdb, NULL, 0, buf, 4096,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+
+ /* A one-block WRITE with a two-block PRDT writes one block */
+ memset(write_buf, 0xa5, sizeof(write_buf));
+ ocs = ufs_send_scsi_command(ufs, 1, write_lba0_cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &write_rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(write_rsp_upiu.header.scsi_status, ==, GOOD);
+
+ ocs = ufs_send_scsi_command(ufs, 1, read_lba1_cdb, NULL, 0, buf, 4096,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpmem(buf, 4096, zero_buf, sizeof(zero_buf));
+
+ g_assert_cmpuint(write_rsp_upiu.header.flags, ==, UFS_UPIU_FLAG_UNDERFLOW);
+ g_assert_cmpuint(be32_to_cpu(write_rsp_upiu.sr.residual_transfer_count),
+ ==, 4096);
+
+ /* A one-block READ with a two-block PRDT reads one block */
+ ocs = ufs_send_scsi_command(ufs, 1, read_lba0_cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(rsp_upiu.header.flags, ==, UFS_UPIU_FLAG_UNDERFLOW);
+ g_assert_cmpuint(be32_to_cpu(rsp_upiu.sr.residual_transfer_count), ==,
+ 4096);
+ g_assert_cmpmem(buf, 4096, write_buf, 4096);
+
+ /* A 36-byte INQUIRY with a 4 KiB PRDT reports the rest as a residual */
+ ocs = ufs_send_scsi_command(ufs, 1, inquiry_cdb, NULL, 0, buf, 4096,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(rsp_upiu.header.flags, ==, UFS_UPIU_FLAG_UNDERFLOW);
+ g_assert_cmpuint(be32_to_cpu(rsp_upiu.sr.residual_transfer_count), ==,
+ 4096 - 36);
+
+ ufs_exit(ufs, alloc);
+}
+
static void ufstest_mcq_cq_wraparound(void *obj, void *data,
QGuestAllocator *alloc)
{
@@ -1812,6 +1882,7 @@ static void ufs_register_nodes(void)
qos_add_test("mcq-read-write", "ufs", ufstest_read_write, &mcq_test_opts);
qos_add_test("no-prdt", "ufs", ufstest_no_prdt, &io_test_opts);
qos_add_test("sync-cache", "ufs", ufstest_sync_cache, &io_test_opts);
+ qos_add_test("long-prdt", "ufs", ufstest_long_prdt, &io_test_opts);
qos_add_test("mcq-cq-wraparound", "ufs", ufstest_mcq_cq_wraparound,
&mcq_test_opts);
qos_add_test("query-flag", "ufs", ufstest_query_flag_request,
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 4/6] docs/system: Document zoned UFS logical units
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
` (2 preceding siblings ...)
2026-10-16 9:31 ` [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB Daejun Park
@ 2026-10-16 9:32 ` Daejun Park
2026-10-16 9:33 ` [PATCH v2 5/6] hw/ufs: Support for Zoned UFS Daejun Park
2026-10-16 9:35 ` [PATCH v2 6/6] tests/qtest: Add tests " Daejun Park
5 siblings, 0 replies; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:32 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
Describe the zoned, zoned.zone_size and zoned.max_open properties of
ufs-lu with an example, what a zoned logical unit emulates and what it
rejects, that reads above the write pointer return what the drive
holds, that zone state is not persistent and that the drive cannot be
resized.
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
docs/system/devices/ufs.rst | 55 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/docs/system/devices/ufs.rst b/docs/system/devices/ufs.rst
index d3482e9299..f719b2f145 100644
--- a/docs/system/devices/ufs.rst
+++ b/docs/system/devices/ufs.rst
@@ -74,6 +74,61 @@ Logical Unit Properties
Logical unit number, 0 to 31. A host controller accepts up to 32
logical units.
+``zoned`` (default off)
+ Present the logical unit as a host-managed zoned block device (ZBC).
+ All zones are sequential write required zones whose capacity is their
+ size. The drive must be writable.
+
+``zoned.zone_size`` (default 128M)
+ Zone size in bytes: a multiple of the 4 KiB logical block size, below
+ 16 TiB and not larger than the drive. A logical unit has at most
+ 67108862 zones. Linux also requires a power of two. The last zone is
+ smaller when the capacity is not a multiple of the zone size.
+
+``zoned.max_open`` (default 6)
+ Maximum number of open zones; 0 means no limit. When the limit is
+ reached, a write to another zone closes an implicitly open zone, and
+ fails with INSUFFICIENT ZONE RESOURCES if every open zone was opened
+ explicitly. f2fs needs at least as many as its active logs, 6 by
+ default.
+
+Zoned Logical Units
+===================
+
+The following adds a regular logical unit and a zoned one with 64 MiB
+zones:
+
+.. code-block:: console
+
+ -drive file=lu0.img,if=none,id=lu0,format=raw \
+ -drive file=lu1.img,if=none,id=lu1,format=raw \
+ -device ufs,id=ufs0,serial=0001 \
+ -device ufs-lu,drive=lu0,bus=ufs0,lun=0 \
+ -device ufs-lu,drive=lu1,bus=ufs0,lun=1,zoned=on,zoned.zone_size=64M
+
+A Linux guest needs ``CONFIG_BLK_DEV_ZONED``. f2fs keeps its metadata on
+a regular logical unit, for example ``mkfs.f2fs -m -c /dev/sdb /dev/sda``
+with the zoned logical unit as ``/dev/sdb``.
+
+The zoned logical unit reports the ZBC device type in its INQUIRY data,
+handles REPORT ZONES and the OPEN, CLOSE, FINISH and RESET WRITE POINTER
+zone operations, and checks every WRITE, WRITE AND VERIFY and WRITE SAME
+command against the write pointer of its zone. Zone operations act on
+one zone or, with the ALL bit, on every zone; a ZONE COUNT above 1 is
+rejected. The logical unit does not support UNMAP and reports no logical
+block provisioning. All other commands, including reads, MODE SENSE and
+SYNCHRONIZE CACHE, behave as on a regular logical unit. FORMAT UNIT
+changes neither data nor zones.
+
+Reads are not restricted by the write pointer: reads above it, including
+reads of a zone after RESET WRITE POINTER, return whatever the drive
+holds. Zone conditions and write pointers are kept in memory only: after
+QEMU restarts every zone is empty again while the drive keeps its data,
+so the write pointers no longer match what a file system wrote. The zone
+layout is set when the logical unit is created; resizing its drive is
+not supported. Zone conditions and write pointers, open zones included,
+are kept across a host controller reset and a system reset.
+
Limitations
===========
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 5/6] hw/ufs: Support for Zoned UFS
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
` (3 preceding siblings ...)
2026-10-16 9:32 ` [PATCH v2 4/6] docs/system: Document zoned UFS logical units Daejun Park
@ 2026-10-16 9:33 ` Daejun Park
2026-10-16 11:54 ` Stanley Jhu
2026-10-16 9:35 ` [PATCH v2 6/6] tests/qtest: Add tests " Daejun Park
5 siblings, 1 reply; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:33 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
Add a "zoned" property to ufs-lu. A zoned logical unit is a host-managed
zoned block device made of sequential write required zones, with the
zone size and the maximum number of open zones set by zoned.zone_size
and zoned.max_open. max_open defaults to 6 because f2fs refuses a zoned
device with fewer open zones than its active logs, 6 by default.
The logical unit keeps the condition and the write pointer of each zone
in memory and emulates REPORT ZONES and the OPEN, CLOSE, FINISH and
RESET WRITE POINTER zone operations, with or without the ALL bit. Every
WRITE, WRITE AND VERIFY and WRITE SAME of at least one block that lies
within the capacity is checked against its zone before it reaches
scsi-hd: a write crossing the end of the zone fails with WRITE BOUNDARY
VIOLATION, a write to a full zone with INVALID FIELD IN CDB, and a write
that does not start at the write pointer with UNALIGNED WRITE COMMAND.
When max_open zones are open, a write to another zone closes an
implicitly open zone, and fails with INSUFFICIENT ZONE RESOURCES only if
all open zones were opened explicitly. The zone checks, the open zone
accounting and the sense codes follow the ZBC emulation of Linux
scsi_debug; unlike it, a ZONE COUNT above 1 is rejected and RESET WRITE
POINTER does not zero the zone.
The write pointer moves when a write is submitted, so that queued writes
to one zone pass the check in submission order: with MCQ, the Android
common kernel queues several writes to a zone on one submission queue
(BLK_FEAT_ORDERED_HWQ). It moves back if scsi-hd fails or drops that
write, unless the zone changed since, for example because a later write
to it was accepted. A pending unit attention is reported before any zone
command or zone check, as scsi-hd reports it before most commands,
INQUIRY and REPORT LUNS excepted.
UNMAP fails with INVALID COMMAND OPERATION CODE, since deallocation
would bypass the write pointers: READ CAPACITY(16) reports no logical
block provisioning, and the Block Limits and Logical Block Provisioning
VPD pages do not advertise UNMAP. Reads and the remaining commands go to
scsi-hd. Reads above the write pointer are not restricted and return
what the drive holds, also after RESET WRITE POINTER.
The wrapped scsi-hd stays a direct-access (TYPE_DISK) device. INQUIRY
data of a zoned logical unit reaches the guest through a transfer_data
callback, which sets the peripheral device type to TYPE_ZBC and adds the
ZBC device characteristics page (B6h) to the list of supported VPD
pages. The logical unit emulates that page, reporting unrestricted reads
(URSWRZ), which Linux requires for host-managed devices, and the maximum
number of open zones.
Setting the scsi-hd device type to TYPE_ZBC instead takes the rest of
the command set off the disk path. scsi-disk serves mode pages for
TYPE_DISK and TYPE_ROM only, so MODE SENSE fails, the guest assumes a
write-through cache and never sends SYNCHRONIZE CACHE or FUA writes.
With a Linux 7.3-rc7 guest, a zoned logical unit built that way logged
"Assuming drive cache: write through" and no flush reached QEMU on
fsync. With this patch the guest logs "Write cache: enabled, read cache:
enabled, supports DPO and FUA" and the flush arrives.
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
hw/ufs/lu.c | 829 +++++++++++++++++++++++++++++++++++++++++++++++++++-
hw/ufs/trace-events | 6 +
hw/ufs/ufs.h | 36 +++
include/block/ufs.h | 27 ++
4 files changed, 893 insertions(+), 5 deletions(-)
diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index bebe4eaac2..19bab236a2 100644
--- a/hw/ufs/lu.c
+++ b/hw/ufs/lu.c
@@ -20,6 +20,9 @@
#include "ufs.h"
#define SCSI_COMMAND_FAIL (-1)
+#define REPORT_ZONES_HEADER_SIZE 64
+#define REPORT_ZONES_DESC_SIZE 64
+#define ZBC_VPD_B6_LEN 64
static void ufs_build_upiu_sense_data(UfsRequest *req, uint8_t *sense,
uint32_t sense_len)
@@ -58,6 +61,249 @@ static void ufs_build_scsi_response_upiu(UfsRequest *req, uint8_t *sense,
status, data_segment_length);
}
+static inline uint32_t ufs_zone_idx(UfsLu *lu, uint64_t lba)
+{
+ return lba / lu->zone_info.zone_size;
+}
+
+static inline UfsZoneState *ufs_get_zone_by_lba(UfsLu *lu, uint64_t lba)
+{
+ if (lba >= be64_to_cpu(lu->unit_desc.logical_block_count)) {
+ return NULL;
+ }
+
+ return &lu->zone_array[ufs_zone_idx(lu, lba)];
+}
+
+/* The last zone may be smaller than the zone size */
+static inline uint64_t ufs_zone_end(UfsZoneState *zone)
+{
+ return zone->start + zone->size;
+}
+
+static void ufs_assign_zone_cond(UfsLu *lu, UfsZoneState *zone,
+ UfsZoneCond new_cond)
+{
+ switch (zone->cond) {
+ case UFS_ZONE_COND_IMPLICIT_OPEN:
+ case UFS_ZONE_COND_EXPLICIT_OPEN:
+ lu->zone_info.nr_open--;
+ break;
+ default:
+ break;
+ }
+
+ zone->cond = new_cond;
+ zone->seq++;
+
+ switch (new_cond) {
+ case UFS_ZONE_COND_EMPTY:
+ zone->wp = zone->start;
+ break;
+ case UFS_ZONE_COND_IMPLICIT_OPEN:
+ case UFS_ZONE_COND_EXPLICIT_OPEN:
+ lu->zone_info.nr_open++;
+ break;
+ case UFS_ZONE_COND_FULL:
+ zone->wp = ufs_zone_end(zone);
+ break;
+ default:
+ break;
+ }
+}
+
+/* Close one implicitly open zone; false if there is none */
+static bool ufs_close_implicit_open_zone(UfsLu *lu)
+{
+ uint32_t i;
+
+ for (i = 0; i < lu->zone_info.nr_zones; i++) {
+ UfsZoneState *zone = &lu->zone_array[i];
+
+ if (zone->cond == UFS_ZONE_COND_IMPLICIT_OPEN) {
+ uint64_t seq = zone->seq;
+
+ /*
+ * Closing moves no write pointer: keep seq, so that a write in
+ * flight to this zone can still be undone.
+ */
+ ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_CLOSED);
+ zone->seq = seq;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Open an empty or closed zone. When all open zone resources are in use, an
+ * implicitly open zone is closed to make room; if every open zone was opened
+ * explicitly, the zone cannot be opened. A max_open of 0 means no limit.
+ */
+static bool ufs_open_zone(UfsLu *lu, UfsZoneState *zone, UfsZoneCond open_cond)
+{
+ switch (zone->cond) {
+ case UFS_ZONE_COND_EMPTY:
+ case UFS_ZONE_COND_CLOSED:
+ if (lu->zone_info.max_open &&
+ lu->zone_info.nr_open >= lu->zone_info.max_open &&
+ !ufs_close_implicit_open_zone(lu)) {
+ return false;
+ }
+ ufs_assign_zone_cond(lu, zone, open_cond);
+ return true;
+ case UFS_ZONE_COND_IMPLICIT_OPEN:
+ if (open_cond == UFS_ZONE_COND_EXPLICIT_OPEN) {
+ ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_EXPLICIT_OPEN);
+ }
+ return true;
+ default:
+ /* Explicitly open and full zones stay as they are */
+ return true;
+ }
+}
+
+static void ufs_close_zone(UfsLu *lu, UfsZoneState *zone)
+{
+ if (zone->cond == UFS_ZONE_COND_IMPLICIT_OPEN ||
+ zone->cond == UFS_ZONE_COND_EXPLICIT_OPEN) {
+ ufs_assign_zone_cond(lu, zone, zone->wp == zone->start ?
+ UFS_ZONE_COND_EMPTY :
+ UFS_ZONE_COND_CLOSED);
+ }
+}
+
+static void ufs_finish_zone(UfsLu *lu, UfsZoneState *zone, bool all)
+{
+ switch (zone->cond) {
+ case UFS_ZONE_COND_EMPTY:
+ /* FINISH ZONE with the ALL bit leaves empty zones alone */
+ if (all) {
+ break;
+ }
+ /* fallthrough */
+ case UFS_ZONE_COND_IMPLICIT_OPEN:
+ case UFS_ZONE_COND_EXPLICIT_OPEN:
+ case UFS_ZONE_COND_CLOSED:
+ ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_FULL);
+ break;
+ case UFS_ZONE_COND_FULL:
+ /* An in-flight write that filled the zone must not undo this */
+ zone->seq++;
+ break;
+ default:
+ break;
+ }
+}
+
+static void ufs_reset_write_pointer_zone(UfsLu *lu, UfsZoneState *zone)
+{
+ switch (zone->cond) {
+ case UFS_ZONE_COND_IMPLICIT_OPEN:
+ case UFS_ZONE_COND_EXPLICIT_OPEN:
+ case UFS_ZONE_COND_CLOSED:
+ case UFS_ZONE_COND_FULL:
+ ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_EMPTY);
+ break;
+ default:
+ break;
+ }
+}
+
+/*
+ * The write pointer moves when a write is accepted, so that queued writes to
+ * one zone pass the check in submission order. If scsi-hd then fails or
+ * drops the write (an aborted or purged request, an I/O error), move the
+ * write pointer back, unless the zone changed since: a later write was
+ * accepted, or a zone operation changed it.
+ */
+static void ufs_zbc_write_undo(UfsLu *lu, UfsRequest *req)
+{
+ UfsZoneState *zone = req->zone;
+
+ req->zone = NULL;
+ if (!zone || zone->seq != req->zone_seq) {
+ return;
+ }
+
+ trace_ufs_zbc_write_undone(lu->lun, zone->start, req->zone_wp_before);
+
+ if (zone->cond == UFS_ZONE_COND_FULL) {
+ /*
+ * The write filled the zone, which gave up its open resource, and
+ * another zone may hold it by now. So the zone comes back closed or
+ * empty, also if it was open explicitly before the write, or if an
+ * OPEN ZONE found it full in the meantime.
+ */
+ ufs_assign_zone_cond(lu, zone, req->zone_wp_before == zone->start ?
+ UFS_ZONE_COND_EMPTY :
+ UFS_ZONE_COND_CLOSED);
+ } else if (req->zone_wp_before == zone->start &&
+ req->zone_cond_before != UFS_ZONE_COND_EXPLICIT_OPEN) {
+ /* The write opened the zone implicitly */
+ ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_EMPTY);
+ }
+ zone->wp = req->zone_wp_before;
+ zone->seq++;
+}
+
+/*
+ * The wrapped scsi-hd is a direct-access (TYPE_DISK) device, so every
+ * command that a zoned LU forwards to it, including MODE SENSE, takes the
+ * regular disk path. Only the identification differs: a zoned LU reports
+ * itself as a host-managed zoned block device. Set the peripheral device
+ * type of the INQUIRY data that scsi-hd built to TYPE_ZBC, and add the ZBC
+ * device characteristics VPD page (emulated in ufs_emulate_zbc_cmd()) to the
+ * list of supported VPD pages. Since UNMAP is rejected, also hide it in the
+ * Block Limits and Logical Block Provisioning VPD pages. scsi-hd pads its
+ * data with zeros up to the allocation length, and len is never larger
+ * than that.
+ */
+static void ufs_zoned_fixup_inquiry(const uint8_t *cdb, uint8_t *buf,
+ uint32_t len)
+{
+ uint8_t page_len;
+
+ if (len == 0) {
+ return;
+ }
+
+ buf[0] = TYPE_ZBC;
+
+ if (!(cdb[1] & 0x1) || len < 4) {
+ return;
+ }
+
+ /* UNMAP is rejected, see ufs_emulate_zbc_cmd() */
+ if (cdb[2] == 0xb0) {
+ /* Block Limits: maximum UNMAP LBA and block descriptor counts */
+ if (len > 20) {
+ memset(&buf[20], 0, MIN(len, 28) - 20);
+ }
+ return;
+ }
+ if (cdb[2] == 0xb2) {
+ /* Logical Block Provisioning: clear LBPU */
+ if (len > 5) {
+ buf[5] &= ~0x80;
+ }
+ return;
+ }
+ if (cdb[2] != 0x00) {
+ return;
+ }
+
+ /* Supported VPD pages, in ascending order: 0xb6 goes last */
+ page_len = buf[3];
+ if (page_len < 255 && !memchr(&buf[4], 0xb6, MIN(page_len, len - 4))) {
+ buf[3] = page_len + 1;
+ if (4 + page_len < len) {
+ buf[4 + page_len] = 0xb6;
+ }
+ }
+}
+
#define UFS_GROUP_NUMBER_MASK 0x1F
#define UFS_WB_GROUP_NUMBER_DEFAULT 0x00 /* 00000b */
#define UFS_WB_GROUP_NUMBER_PINNED 0x18 /* 11000b */
@@ -170,6 +416,7 @@ static void ufs_hid_count_fragments(UfsRequest *req)
static void ufs_scsi_command_complete(SCSIRequest *scsi_req, size_t resid)
{
UfsRequest *req = scsi_req->hba_private;
+ UfsLu *lu = container_of(scsi_req->bus, UfsLu, bus);
int16_t status = scsi_req->status;
uint32_t transfered_len = scsi_req->cmd.xfer - resid;
@@ -180,6 +427,9 @@ static void ufs_scsi_command_complete(SCSIRequest *scsi_req, size_t resid)
if (status == GOOD) {
ufs_wb_process_write_req(req, transfered_len);
ufs_hid_count_fragments(req);
+ req->zone = NULL;
+ } else {
+ ufs_zbc_write_undo(lu, req);
}
ufs_build_scsi_response_upiu(req, scsi_req->sense, scsi_req->sense_len,
@@ -196,6 +446,7 @@ static void ufs_scsi_command_cancelled(SCSIRequest *scsi_req)
UfsRequest *req = scsi_req->hba_private;
assert(req != NULL);
+ ufs_zbc_write_undo(container_of(scsi_req->bus, UfsLu, bus), req);
req->sreq = NULL;
scsi_req->hba_private = NULL;
scsi_req_unref(scsi_req);
@@ -204,9 +455,29 @@ static void ufs_scsi_command_cancelled(SCSIRequest *scsi_req)
static QEMUSGList *ufs_get_sg_list(SCSIRequest *scsi_req)
{
UfsRequest *req = scsi_req->hba_private;
+ UfsLu *lu = container_of(scsi_req->bus, UfsLu, bus);
+
+ /* INQUIRY data of a zoned LU goes through ufs_scsi_transfer_data() */
+ if (lu->zone_enabled && scsi_req->cmd.buf[0] == INQUIRY) {
+ return NULL;
+ }
+
return req->sg;
}
+static void ufs_scsi_transfer_data(SCSIRequest *scsi_req, uint32_t len)
+{
+ UfsRequest *req = scsi_req->hba_private;
+ uint8_t *buf = scsi_req_get_buf(scsi_req);
+
+ /* Only INQUIRY of a zoned LU has no SG list, see ufs_get_sg_list() */
+ assert(scsi_req->cmd.buf[0] == INQUIRY && req->sg);
+
+ ufs_zoned_fixup_inquiry(scsi_req->cmd.buf, buf, len);
+ dma_buf_read(buf, len, NULL, req->sg, MEMTXATTRS_UNSPECIFIED);
+ scsi_req_continue(scsi_req);
+}
+
static const struct SCSIBusInfo ufs_scsi_info = {
.tcq = true,
.max_target = 0,
@@ -214,6 +485,7 @@ static const struct SCSIBusInfo ufs_scsi_info = {
.max_channel = 0,
.get_sg_list = ufs_get_sg_list,
+ .transfer_data = ufs_scsi_transfer_data,
.complete = ufs_scsi_command_complete,
.cancel = ufs_scsi_command_cancelled,
};
@@ -322,6 +594,453 @@ static int ufs_emulate_wlun_inquiry(UfsRequest *req, uint8_t *outbuf,
return SCSI_INQUIRY_LEN;
}
+/* ZBC device characteristics VPD page */
+static int ufs_emulate_zbc_vpd_page(UfsLu *lu, uint8_t *outbuf)
+{
+ memset(outbuf, 0, ZBC_VPD_B6_LEN);
+ outbuf[0] = TYPE_ZBC;
+ outbuf[1] = 0xb6;
+ stw_be_p(&outbuf[2], ZBC_VPD_B6_LEN - 4);
+
+ /* URSWRZ: reads are not restricted by the write pointer */
+ outbuf[4] = 0x01;
+ /* optimal number of open sequential write preferred zones */
+ stl_be_p(&outbuf[8], 0xffffffff);
+ /* optimal number of non-seq. written seq. write preferred zones */
+ stl_be_p(&outbuf[12], 0xffffffff);
+ /* maximum number of open sequential write required zones */
+ stl_be_p(&outbuf[16],
+ lu->zone_info.max_open ? lu->zone_info.max_open : 0xffffffff);
+ /* zone alignment method 0 and starting LBA granularity 0: zone length */
+
+ return ZBC_VPD_B6_LEN;
+}
+
+/* ZBC additional sense codes */
+static const struct SCSISense sense_code_UNALIGNED_WRITE = {
+ .key = ILLEGAL_REQUEST, .asc = 0x21, .ascq = 0x04
+};
+
+static const struct SCSISense sense_code_WRITE_BOUNDARY_VIOLATION = {
+ .key = ILLEGAL_REQUEST, .asc = 0x21, .ascq = 0x05
+};
+
+static const struct SCSISense sense_code_INSUFFICIENT_ZONE_RESOURCES = {
+ .key = DATA_PROTECT, .asc = 0x55, .ascq = 0x0e
+};
+
+static const struct SCSISense sense_code_INSUFFICIENT_RESOURCES = {
+ .key = ILLEGAL_REQUEST, .asc = 0x55, .ascq = 0x03
+};
+
+/* REPORT ZONES reporting options */
+static bool ufs_zbc_report_option_valid(uint8_t rep_opts)
+{
+ switch (rep_opts) {
+ case 0x00 ... 0x07:
+ case 0x10:
+ case 0x11:
+ case 0x3e:
+ case 0x3f:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool ufs_zbc_report_option_match(UfsZoneState *zone, uint8_t rep_opts)
+{
+ switch (rep_opts) {
+ case 0x00: /* all zones */
+ case 0x3e: /* all zones except gap zones */
+ return true;
+ case 0x01:
+ return zone->cond == UFS_ZONE_COND_EMPTY;
+ case 0x02:
+ return zone->cond == UFS_ZONE_COND_IMPLICIT_OPEN;
+ case 0x03:
+ return zone->cond == UFS_ZONE_COND_EXPLICIT_OPEN;
+ case 0x04:
+ return zone->cond == UFS_ZONE_COND_CLOSED;
+ case 0x05:
+ return zone->cond == UFS_ZONE_COND_FULL;
+ default:
+ /*
+ * Read-only, offline, reset write pointer recommended, non-sequential
+ * write resources active and not write pointer zones: none exist.
+ */
+ return false;
+ }
+}
+
+/*
+ * outbuf holds at least the header and at most what the guest can take: the
+ * smaller of the allocation length and its data buffer.
+ */
+static bool ufs_zbc_report_zones(UfsLu *lu, UfsRequest *req, uint8_t *outbuf,
+ uint32_t outbuf_len, uint32_t *len,
+ SCSISense *sense)
+{
+ uint8_t *cdb = req->req_upiu.sc.cdb;
+ uint64_t nr_blocks = be64_to_cpu(lu->unit_desc.logical_block_count);
+ uint64_t zone_start_lba = ldq_be_p(&cdb[2]);
+ uint32_t alloc_len = ldl_be_p(&cdb[10]);
+ uint8_t rep_opts = cdb[14] & 0x3f;
+ bool partial = cdb[14] & 0x80;
+ uint32_t limit, max_descs, nr_descs = 0;
+ uint32_t offset = REPORT_ZONES_HEADER_SIZE;
+ uint32_t i;
+
+ trace_ufs_zbc_report_zones(lu->lun, zone_start_lba, alloc_len, rep_opts);
+
+ *len = 0;
+
+ if ((cdb[1] & 0x1f) != UFS_ZBC_REPORT_ZONES ||
+ !ufs_zbc_report_option_valid(rep_opts)) {
+ *sense = SENSE_CODE(INVALID_FIELD);
+ return false;
+ }
+
+ if (zone_start_lba >= nr_blocks) {
+ *sense = SENSE_CODE(LBA_OUT_OF_RANGE);
+ return false;
+ }
+
+ if (alloc_len == 0) {
+ return true;
+ }
+
+ assert(outbuf_len >= REPORT_ZONES_HEADER_SIZE);
+ limit = MIN(alloc_len, outbuf_len);
+ max_descs = limit > REPORT_ZONES_HEADER_SIZE ?
+ (limit - REPORT_ZONES_HEADER_SIZE) / REPORT_ZONES_DESC_SIZE :
+ 0;
+
+ for (i = ufs_zone_idx(lu, zone_start_lba); i < lu->zone_info.nr_zones;
+ i++) {
+ UfsZoneState *zone = &lu->zone_array[i];
+
+ if (!ufs_zbc_report_option_match(zone, rep_opts)) {
+ continue;
+ }
+
+ if (nr_descs < max_descs) {
+ outbuf[offset] = zone->type;
+ outbuf[offset + 1] = zone->cond << 4;
+ stq_be_p(&outbuf[offset + 8], zone->size);
+ stq_be_p(&outbuf[offset + 16], zone->start);
+ stq_be_p(&outbuf[offset + 24], zone->wp);
+ offset += REPORT_ZONES_DESC_SIZE;
+ } else if (partial) {
+ break;
+ }
+
+ nr_descs++;
+ }
+
+ /* Zone list length and maximum LBA */
+ stl_be_p(&outbuf[0], nr_descs * REPORT_ZONES_DESC_SIZE);
+ stq_be_p(&outbuf[8], nr_blocks - 1);
+
+ *len = MIN(offset, limit);
+ return true;
+}
+
+/*
+ * OPEN ZONE with the ALL bit opens every closed zone, closing implicitly open
+ * zones as needed. It fails without changing anything if the closed and the
+ * explicitly open zones together exceed the open zone limit.
+ */
+static bool ufs_open_all_zones(UfsLu *lu)
+{
+ uint32_t i, nr_explicit = 0, nr_closed = 0;
+
+ for (i = 0; i < lu->zone_info.nr_zones; i++) {
+ nr_explicit += lu->zone_array[i].cond == UFS_ZONE_COND_EXPLICIT_OPEN;
+ nr_closed += lu->zone_array[i].cond == UFS_ZONE_COND_CLOSED;
+ }
+
+ if (lu->zone_info.max_open &&
+ nr_explicit + nr_closed > lu->zone_info.max_open) {
+ return false;
+ }
+
+ for (i = 0; i < lu->zone_info.nr_zones; i++) {
+ if (lu->zone_array[i].cond == UFS_ZONE_COND_CLOSED) {
+ ufs_assign_zone_cond(lu, &lu->zone_array[i],
+ UFS_ZONE_COND_EXPLICIT_OPEN);
+ }
+ }
+
+ while (lu->zone_info.max_open &&
+ lu->zone_info.nr_open > lu->zone_info.max_open) {
+ if (!ufs_close_implicit_open_zone(lu)) {
+ break;
+ }
+ }
+
+ return true;
+}
+
+static int ufs_zbc_zone_op(UfsLu *lu, UfsRequest *req, SCSISense *sense)
+{
+ uint8_t *cdb = req->req_upiu.sc.cdb;
+ uint8_t action = cdb[1] & 0x1f;
+ uint64_t zone_id = ldq_be_p(&cdb[2]);
+ uint16_t zone_count = lduw_be_p(&cdb[12]);
+ bool all = cdb[14] & 0x01;
+ UfsZoneState *zone;
+ uint32_t i;
+
+ trace_ufs_zbc_zone_op(lu->lun, action, zone_id, all);
+
+ switch (action) {
+ case UFS_ZBC_CLOSE_ZONE:
+ case UFS_ZBC_FINISH_ZONE:
+ case UFS_ZBC_OPEN_ZONE:
+ case UFS_ZBC_RESET_WRITE_POINTER:
+ break;
+ default:
+ *sense = SENSE_CODE(INVALID_FIELD);
+ return SCSI_COMMAND_FAIL;
+ }
+
+ if (all) {
+ if (action == UFS_ZBC_OPEN_ZONE) {
+ if (!ufs_open_all_zones(lu)) {
+ *sense = SENSE_CODE(INSUFFICIENT_ZONE_RESOURCES);
+ return SCSI_COMMAND_FAIL;
+ }
+ return 0;
+ }
+
+ for (i = 0; i < lu->zone_info.nr_zones; i++) {
+ zone = &lu->zone_array[i];
+ if (action == UFS_ZBC_CLOSE_ZONE) {
+ ufs_close_zone(lu, zone);
+ } else if (action == UFS_ZBC_FINISH_ZONE) {
+ ufs_finish_zone(lu, zone, true);
+ } else {
+ ufs_reset_write_pointer_zone(lu, zone);
+ }
+ }
+ return 0;
+ }
+
+ /* Operations on a range of zones are not supported */
+ if (zone_count > 1) {
+ *sense = SENSE_CODE(INVALID_FIELD);
+ return SCSI_COMMAND_FAIL;
+ }
+
+ zone = ufs_get_zone_by_lba(lu, zone_id);
+ if (!zone) {
+ *sense = SENSE_CODE(LBA_OUT_OF_RANGE);
+ return SCSI_COMMAND_FAIL;
+ }
+
+ if (zone_id != zone->start) {
+ *sense = SENSE_CODE(INVALID_FIELD);
+ return SCSI_COMMAND_FAIL;
+ }
+
+ switch (action) {
+ case UFS_ZBC_CLOSE_ZONE:
+ ufs_close_zone(lu, zone);
+ break;
+ case UFS_ZBC_FINISH_ZONE:
+ ufs_finish_zone(lu, zone, false);
+ break;
+ case UFS_ZBC_OPEN_ZONE:
+ if (!ufs_open_zone(lu, zone, UFS_ZONE_COND_EXPLICIT_OPEN)) {
+ *sense = SENSE_CODE(INSUFFICIENT_ZONE_RESOURCES);
+ return SCSI_COMMAND_FAIL;
+ }
+ break;
+ case UFS_ZBC_RESET_WRITE_POINTER:
+ ufs_reset_write_pointer_zone(lu, zone);
+ break;
+ }
+
+ return 0;
+}
+
+/* LBA and number of blocks of a command that writes the medium */
+static bool ufs_zbc_write_range(const uint8_t *cdb, uint64_t *lba,
+ uint32_t *nr_blocks)
+{
+ switch (cdb[0]) {
+ case WRITE_6:
+ *lba = ldl_be_p(&cdb[0]) & 0x1fffff;
+ *nr_blocks = cdb[4] ? cdb[4] : 256;
+ return true;
+ case WRITE_10:
+ case WRITE_VERIFY_10:
+ case WRITE_SAME_10:
+ *lba = (uint32_t)ldl_be_p(&cdb[2]);
+ *nr_blocks = lduw_be_p(&cdb[7]);
+ return true;
+ case WRITE_12:
+ case WRITE_VERIFY_12:
+ *lba = (uint32_t)ldl_be_p(&cdb[2]);
+ *nr_blocks = ldl_be_p(&cdb[6]);
+ return true;
+ case WRITE_16:
+ case WRITE_VERIFY_16:
+ case WRITE_SAME_16:
+ *lba = ldq_be_p(&cdb[2]);
+ *nr_blocks = ldl_be_p(&cdb[10]);
+ return true;
+ default:
+ return false;
+ }
+}
+
+/*
+ * Check a write against the zone it targets and, if it may proceed, move the
+ * write pointer, remembering enough in req to undo that if scsi-hd does not
+ * complete the write. A write that reaches beyond the capacity or has no
+ * blocks goes to scsi-hd unchecked: scsi-hd fails the former and a
+ * zero-length WRITE SAME, and completes other zero-length writes without
+ * writing anything.
+ */
+static bool ufs_zbc_check_write(UfsLu *lu, UfsRequest *req, SCSISense *sense)
+{
+ const uint8_t *cdb = req->req_upiu.sc.cdb;
+ uint64_t nr_blocks = be64_to_cpu(lu->unit_desc.logical_block_count);
+ uint64_t lba;
+ uint32_t len;
+ UfsZoneState *zone;
+
+ if (!ufs_zbc_write_range(cdb, &lba, &len)) {
+ return true;
+ }
+
+ if (len == 0 || lba >= nr_blocks || len > nr_blocks - lba) {
+ return true;
+ }
+
+ zone = ufs_get_zone_by_lba(lu, lba);
+
+ if (lba + len > ufs_zone_end(zone)) {
+ *sense = SENSE_CODE(WRITE_BOUNDARY_VIOLATION);
+ } else if (zone->cond == UFS_ZONE_COND_FULL) {
+ *sense = SENSE_CODE(INVALID_FIELD);
+ } else if (lba != zone->wp) {
+ *sense = SENSE_CODE(UNALIGNED_WRITE);
+ } else {
+ req->zone_cond_before = zone->cond;
+ req->zone_wp_before = zone->wp;
+
+ if (ufs_open_zone(lu, zone, UFS_ZONE_COND_IMPLICIT_OPEN)) {
+ zone->wp += len;
+ zone->seq++;
+ if (zone->wp == ufs_zone_end(zone)) {
+ ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_FULL);
+ }
+ req->zone = zone;
+ req->zone_seq = zone->seq;
+ return true;
+ }
+ *sense = SENSE_CODE(INSUFFICIENT_ZONE_RESOURCES);
+ }
+
+ trace_ufs_zbc_write_rejected(lu->lun, lba, len, zone->wp, zone->cond,
+ sense->asc, sense->ascq);
+ return false;
+}
+
+/*
+ * Commands that a zoned LU handles itself. Everything else, including READ,
+ * MODE SENSE, writes that passed ufs_zbc_check_write() and the INQUIRY data
+ * that ufs_zoned_fixup_inquiry() adjusts, goes to the wrapped scsi-hd
+ * (UFS_REQUEST_NO_COMPLETE).
+ */
+static UfsReqResult ufs_emulate_zbc_cmd(UfsLu *lu, UfsRequest *req)
+{
+ g_autofree uint8_t *outbuf = NULL;
+ uint8_t *cdb = req->req_upiu.sc.cdb;
+ uint8_t sense_buf[UFS_SENSE_SIZE];
+ uint8_t scsi_status = GOOD;
+ uint32_t len = 0, xfer_len;
+ SCSISense sense;
+ bool ok;
+
+ /*
+ * Let scsi-hd report a pending unit attention first, as it does for
+ * every command it does not exempt (INQUIRY and REPORT LUNS among them),
+ * so that nothing here, including the write pointer of a write, changes
+ * before the guest has seen it.
+ */
+ if (cdb[0] != INQUIRY &&
+ (lu->scsi_dev->unit_attention.key == UNIT_ATTENTION ||
+ lu->bus.unit_attention.key == UNIT_ATTENTION)) {
+ return UFS_REQUEST_NO_COMPLETE;
+ }
+
+ switch (cdb[0]) {
+ case UFS_ZBC_IN: {
+ /* Size the buffer by what can be returned, not by the PRDT alone */
+ uint64_t report_len = REPORT_ZONES_HEADER_SIZE +
+ (uint64_t)REPORT_ZONES_DESC_SIZE *
+ lu->zone_info.nr_zones;
+ uint32_t outbuf_len = MIN(MIN(ldl_be_p(&cdb[10]), req->data_len),
+ report_len);
+
+ outbuf_len = MAX(outbuf_len, REPORT_ZONES_HEADER_SIZE);
+ outbuf = g_try_malloc0(outbuf_len);
+ if (!outbuf) {
+ sense = SENSE_CODE(INSUFFICIENT_RESOURCES);
+ ok = false;
+ break;
+ }
+ ok = ufs_zbc_report_zones(lu, req, outbuf, outbuf_len, &len, &sense);
+ break;
+ }
+ case UFS_ZBC_OUT:
+ ok = ufs_zbc_zone_op(lu, req, &sense) == 0;
+ break;
+ case INQUIRY:
+ /* Only the ZBC device characteristics VPD page is emulated here */
+ if (!(cdb[1] & 0x1) || cdb[2] != 0xb6) {
+ return UFS_REQUEST_NO_COMPLETE;
+ }
+ outbuf = g_malloc0(ZBC_VPD_B6_LEN);
+ len = MIN(ufs_emulate_zbc_vpd_page(lu, outbuf), lduw_be_p(&cdb[3]));
+ ok = true;
+ break;
+ case UNMAP:
+ /* Deallocating blocks would bypass the write pointers */
+ sense = SENSE_CODE(INVALID_OPCODE);
+ ok = false;
+ break;
+ default:
+ if (ufs_zbc_check_write(lu, req, &sense)) {
+ return UFS_REQUEST_NO_COMPLETE;
+ }
+ ok = false;
+ break;
+ }
+
+ if (!ok) {
+ scsi_build_sense(sense_buf, sense);
+ scsi_status = CHECK_CONDITION;
+ len = 0;
+ }
+
+ xfer_len = MIN(len, req->data_len);
+ if (xfer_len > 0 &&
+ dma_buf_read(outbuf, xfer_len, NULL, req->sg,
+ MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
+ return UFS_REQUEST_FAIL;
+ }
+
+ ufs_build_scsi_response_upiu(req, sense_buf, sizeof(sense_buf), xfer_len,
+ scsi_status);
+
+ return UFS_REQUEST_SUCCESS;
+}
+
static UfsReqResult ufs_emulate_scsi_cmd(UfsLu *lu, UfsRequest *req)
{
uint8_t lun = lu->lun;
@@ -405,6 +1124,16 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
SCSIRequest *scsi_req;
uint32_t len;
+ req->zone = NULL;
+
+ if (lu->zone_enabled) {
+ UfsReqResult result = ufs_emulate_zbc_cmd(lu, req);
+ /* UFS_REQUEST_NO_COMPLETE continues command processing */
+ if (result != UFS_REQUEST_NO_COMPLETE) {
+ return result;
+ }
+ }
+
/*
* Each ufs-lu has its own independent virtual SCSI bus. Therefore, we can't
* use scsi_target_emulate_report_luns() which gets all lu information over
@@ -432,14 +1161,15 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
/*
* A command that moves data needs a PRDT. Without one there is no SG
* list, and the SCSI layer would call the transfer_data callback, which
- * ufs-lu does not set. Fail the request, as virtio-scsi does when the
- * buffer does not match the command. A request that reports a pending
- * unit attention instead moves no data; its sense is already set, so let
- * it through and the guest sees the UA.
+ * only handles the INQUIRY data of a zoned LU. Fail the request, as
+ * virtio-scsi does when the buffer does not match the command. A request
+ * that reports a pending unit attention instead moves no data; its sense
+ * is already set, so let it through and the guest sees the UA.
*/
if (scsi_req->cmd.mode != SCSI_XFER_NONE && !req->sg &&
!scsi_req->sense_len) {
scsi_req_unref(scsi_req);
+ ufs_zbc_write_undo(lu, req);
return UFS_REQUEST_FAIL;
}
@@ -466,6 +1196,10 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
static const Property ufs_lu_props[] = {
DEFINE_PROP_DRIVE("drive", UfsLu, conf.blk),
DEFINE_PROP_UINT8("lun", UfsLu, lun, 0),
+ DEFINE_PROP_BOOL("zoned", UfsLu, params.zoned, false),
+ DEFINE_PROP_SIZE("zoned.zone_size", UfsLu, params.zone_size,
+ UFS_DEFAULT_ZONE_SIZE),
+ DEFINE_PROP_UINT32("zoned.max_open", UfsLu, params.zone_max_open, 6),
};
static bool ufs_add_lu(UfsHc *u, UfsLu *lu, Error **errp)
@@ -498,12 +1232,71 @@ void ufs_init_wlu(UfsLu *wlu, uint8_t wlun)
wlu->scsi_op = &ufs_emulate_scsi_cmd;
}
+static bool ufs_lu_init_zoned(UfsLu *lu, Error **errp)
+{
+ uint64_t nr_blocks = be64_to_cpu(lu->unit_desc.logical_block_count);
+ uint64_t zone_size = lu->params.zone_size;
+ uint64_t nr_zones, start = 0;
+ UfsZoneState *zone;
+ uint32_t i;
+
+ if (!zone_size || zone_size % UFS_BLOCK_SIZE ||
+ zone_size / UFS_BLOCK_SIZE > UINT32_MAX) {
+ error_setg(errp,
+ "zoned.zone_size must be a non-zero multiple of %d bytes "
+ "below 16 TiB",
+ UFS_BLOCK_SIZE);
+ return false;
+ }
+
+ zone_size /= UFS_BLOCK_SIZE;
+ if (zone_size > nr_blocks) {
+ error_setg(errp, "zoned.zone_size is larger than the drive");
+ return false;
+ }
+
+ /* The REPORT ZONES zone list length must fit in 32 bits */
+ nr_zones = DIV_ROUND_UP(nr_blocks, zone_size);
+ if (nr_zones > (UINT32_MAX - REPORT_ZONES_HEADER_SIZE) /
+ REPORT_ZONES_DESC_SIZE) {
+ error_setg(errp, "too many zones (%" PRIu64 "), raise zoned.zone_size",
+ nr_zones);
+ return false;
+ }
+
+ lu->zone_array = g_try_new0(UfsZoneState, nr_zones);
+ if (!lu->zone_array) {
+ error_setg(errp, "cannot allocate the state of %" PRIu64 " zones",
+ nr_zones);
+ return false;
+ }
+
+ lu->zone_info.zone_size = zone_size;
+ lu->zone_info.nr_zones = nr_zones;
+ lu->zone_info.nr_open = 0;
+ lu->zone_info.max_open = lu->params.zone_max_open;
+
+ for (i = 0, zone = lu->zone_array; i < nr_zones; i++, zone++) {
+ /* SEQUENTIAL WRITE REQUIRED */
+ zone->type = 0x2;
+ zone->cond = UFS_ZONE_COND_EMPTY;
+ zone->start = start;
+ zone->size = MIN(zone_size, nr_blocks - start);
+ zone->wp = start;
+ start += zone->size;
+ }
+
+ lu->zone_enabled = true;
+ return true;
+}
+
static void ufs_init_lu(UfsLu *lu)
{
BlockBackend *blk = lu->conf.blk;
int64_t brdv_len = blk_getlength(blk);
memset(&lu->unit_desc, 0, sizeof(lu->unit_desc));
+ lu->zone_enabled = false;
lu->unit_desc.length = sizeof(UnitDescriptor);
lu->unit_desc.descriptor_idn = UFS_QUERY_DESC_IDN_UNIT;
lu->unit_desc.lu_enable = 0x01;
@@ -551,6 +1344,10 @@ static void ufs_init_scsi_device(UfsLu *lu, BlockBackend *blk, Error **errp)
qdev_prop_set_uint32(scsi_dev, "logical_block_size", UFS_BLOCK_SIZE);
qdev_prop_set_uint32(scsi_dev, "scsi-id", 0);
qdev_prop_set_uint32(scsi_dev, "lun", lu->lun);
+ if (lu->params.zoned) {
+ /* Deallocating blocks would bypass the write pointers */
+ qdev_prop_set_uint32(scsi_dev, "discard_granularity", 0);
+ }
if (!qdev_prop_set_drive_err(scsi_dev, "drive", blk, errp)) {
object_unparent(OBJECT(scsi_dev));
return;
@@ -567,6 +1364,7 @@ static void ufs_init_scsi_device(UfsLu *lu, BlockBackend *blk, Error **errp)
static void ufs_lu_realize(DeviceState *dev, Error **errp)
{
+ ERRP_GUARD();
UfsLu *lu = DO_UPCAST(UfsLu, qdev, dev);
BusState *s = qdev_get_parent_bus(dev);
UfsHc *u = UFS_BUS(s)->hc;
@@ -590,12 +1388,30 @@ static void ufs_lu_realize(DeviceState *dev, Error **errp)
return;
}
+ if (lu->params.zoned && !blk_supports_write_perm(blk)) {
+ error_setg(errp, "a zoned logical unit needs a writable drive");
+ return;
+ }
+
ufs_init_lu(lu);
- if (!ufs_add_lu(u, lu, errp)) {
+ if (lu->params.zoned && !ufs_lu_init_zoned(lu, errp)) {
return;
}
+ if (!ufs_add_lu(u, lu, errp)) {
+ goto fail;
+ }
+
ufs_init_scsi_device(lu, blk, errp);
+ if (*errp) {
+ goto fail;
+ }
+ return;
+
+fail:
+ lu->zone_enabled = false;
+ g_free(lu->zone_array);
+ lu->zone_array = NULL;
}
static void ufs_lu_unrealize(DeviceState *dev)
@@ -603,6 +1419,9 @@ static void ufs_lu_unrealize(DeviceState *dev)
UfsLu *lu = DO_UPCAST(UfsLu, qdev, dev);
lu->scsi_dev = NULL;
+ lu->zone_enabled = false;
+ g_free(lu->zone_array);
+ lu->zone_array = NULL;
}
static void ufs_lu_class_init(ObjectClass *oc, const void *data)
diff --git a/hw/ufs/trace-events b/hw/ufs/trace-events
index 5e5a54a3fb..1977a9c9ad 100644
--- a/hw/ufs/trace-events
+++ b/hw/ufs/trace-events
@@ -58,3 +58,9 @@ ufs_err_mcq_delete_cq_sq_not_deleted(uint8_t sqid, uint8_t cqid) "mcq sq %"PRIu8
# HID (Host Initiated Defragmentation)
ufs_hid_defrag_operation(uint8_t op, uint8_t state) "HID defrag operation 0x%"PRIx8", new state 0x%"PRIx8""
ufs_hid_defrag_progress(uint32_t remaining, uint8_t progress) "HID defrag remaining %"PRIu32", progress %"PRIu8"%%"
+
+# lu.c (zoned logical units)
+ufs_zbc_report_zones(uint8_t lun, uint64_t lba, uint32_t alloc_len, uint8_t options) "lun 0x%"PRIx8", lba %"PRIu64", alloc_len %"PRIu32", options 0x%"PRIx8""
+ufs_zbc_zone_op(uint8_t lun, uint8_t action, uint64_t zone_id, bool all) "lun 0x%"PRIx8", action 0x%"PRIx8", zone id %"PRIu64", all %d"
+ufs_zbc_write_rejected(uint8_t lun, uint64_t lba, uint32_t len, uint64_t wp, uint8_t cond, uint8_t asc, uint8_t ascq) "lun 0x%"PRIx8", lba %"PRIu64", len %"PRIu32", wp %"PRIu64", cond 0x%"PRIx8", asc 0x%"PRIx8", ascq 0x%"PRIx8""
+ufs_zbc_write_undone(uint8_t lun, uint64_t zone_start, uint64_t wp) "lun 0x%"PRIx8", zone start %"PRIu64", wp back to %"PRIu64""
diff --git a/hw/ufs/ufs.h b/hw/ufs/ufs.h
index 265a43faaa..4819e2522f 100644
--- a/hw/ufs/ufs.h
+++ b/hw/ufs/ufs.h
@@ -17,6 +17,25 @@
#include "scsi/constants.h"
#include "system/dma.h"
+typedef struct UfsZoneState {
+ uint8_t type;
+ UfsZoneCond cond;
+ uint64_t start;
+ uint64_t size;
+ uint64_t wp;
+ /* bumped by every change of cond or wp, except an implicit close */
+ uint64_t seq;
+} UfsZoneState;
+
+typedef struct UfsZoneInfo {
+ uint32_t zone_size;
+ uint32_t nr_zones;
+ uint32_t nr_open;
+ uint32_t max_open;
+} UfsZoneInfo;
+
+#define UFS_DEFAULT_ZONE_SIZE (128 * MiB)
+
#define UFS_MAX_LUS 32
#define UFS_MAX_MCQ_QNUM 32
#define UFS_BLOCK_SIZE_SHIFT 12
@@ -64,6 +83,12 @@ typedef struct UfsRequest {
QEMUSGList *sg;
uint32_t data_len;
+ /* write accepted by a zoned LU, undone if scsi-hd does not complete it */
+ UfsZoneState *zone;
+ UfsZoneCond zone_cond_before;
+ uint64_t zone_wp_before;
+ uint64_t zone_seq;
+
/* for MCQ */
struct UfsSq *sq;
struct UfsCqEntry cqe;
@@ -78,6 +103,12 @@ static inline bool ufs_mcq_req(UfsRequest *req)
struct UfsLu;
typedef UfsReqResult (*UfsScsiOp)(struct UfsLu *, UfsRequest *);
+typedef struct UfsLuParams {
+ bool zoned;
+ uint64_t zone_size;
+ uint32_t zone_max_open;
+} UfsLuParams;
+
typedef struct UfsLu {
DeviceState qdev;
uint8_t lun;
@@ -86,6 +117,11 @@ typedef struct UfsLu {
SCSIDevice *scsi_dev;
BlockConf conf;
UfsScsiOp scsi_op;
+ /* For Zoned UFS */
+ UfsLuParams params;
+ bool zone_enabled;
+ UfsZoneInfo zone_info;
+ UfsZoneState *zone_array;
} UfsLu;
typedef struct UfsParams {
diff --git a/include/block/ufs.h b/include/block/ufs.h
index 4a614fbe52..a0bbe873d6 100644
--- a/include/block/ufs.h
+++ b/include/block/ufs.h
@@ -708,6 +708,33 @@ enum {
UFS_MASK_OCS = 0xFF,
};
+/* ZBC zone conditions, table 26 of zbcr05 */
+typedef enum UfsZoneCond {
+ UFS_ZONE_COND_NOT_WP = 0x0,
+ UFS_ZONE_COND_EMPTY = 0x1,
+ UFS_ZONE_COND_IMPLICIT_OPEN = 0x2,
+ UFS_ZONE_COND_EXPLICIT_OPEN = 0x3,
+ UFS_ZONE_COND_CLOSED = 0x4,
+ UFS_ZONE_COND_READ_ONLY = 0xd,
+ UFS_ZONE_COND_FULL = 0xe,
+ UFS_ZONE_COND_OFFLINE = 0xf,
+} UfsZoneCond;
+
+/* ZBC IN and ZBC OUT operation codes and their service actions */
+#define UFS_ZBC_OUT 0x94
+#define UFS_ZBC_IN 0x95
+
+enum {
+ UFS_ZBC_REPORT_ZONES = 0x00,
+};
+
+enum {
+ UFS_ZBC_CLOSE_ZONE = 0x01,
+ UFS_ZBC_FINISH_ZONE = 0x02,
+ UFS_ZBC_OPEN_ZONE = 0x03,
+ UFS_ZBC_RESET_WRITE_POINTER = 0x04,
+};
+
/*
* struct UfshcdSgEntry - UFSHCI PRD Entry
* @addr: Physical address; DW-0 and DW-1.
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 6/6] tests/qtest: Add tests for Zoned UFS
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
` (4 preceding siblings ...)
2026-10-16 9:33 ` [PATCH v2 5/6] hw/ufs: Support for Zoned UFS Daejun Park
@ 2026-10-16 9:35 ` Daejun Park
5 siblings, 0 replies; 12+ messages in thread
From: Daejun Park @ 2026-10-16 9:35 UTC (permalink / raw)
To: qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
Add ten qtests for zoned logical units. They use four zoned logical
units: one with 2 MiB zones, one whose capacity is not a multiple of its
3 MiB zones, one on blkdebug with a single open zone, where the first
write at LBA 0 and the first write at the start of zone 2 fail with an
I/O error, and one on null-co that is larger than 8 TiB.
zufs-init checks the INQUIRY device type, the supported VPD pages with
full and short allocation lengths, the ZBC device characteristics page,
that UNMAP is advertised nowhere, the caching mode page, REPORT ZONES,
and an INQUIRY without a PRDT.
zufs-write, also run with MCQ as zufs-mcq-write, checks write pointer
handling, SYNCHRONIZE CACHE(16), resets of open zones, finishing an
empty zone, writing to more zones than max_open allows, LBAs past the
end of the logical unit, and a REPORT ZONES allocation length larger
than the data buffer. Without the bound on that length, QEMU writes past
its own buffer; a debug build without sanitizers aborted with "double
free or corruption".
zufs-write-cmds checks that WRITE(6), WRITE(10) and WRITE SAME(16) move
the write pointer, a zero length WRITE SAME, the sense codes for writes
crossing a zone and writes to a full zone, and that a WRITE(10) at an
LBA above 2^31 is checked against the zone it addresses.
zufs-open-zones checks explicit opens up to the limit, closing an
implicitly open zone to make room, OPEN ALL with and without enough
resources, FINISH ALL and RESET ALL.
zufs-unit-attention checks that a zone operation is not performed while
a unit attention is pending. zufs-io-error checks that a write failed
by the drive moves the write pointer back.
zufs-write-in-flight, with MCQ, holds writes in flight with a blkdebug
breakpoint. It checks that a failed write moves the write pointer back
also when a write to another zone closed its zone implicitly in the
meantime, and that a zone stays full when FINISH ZONE completed while a
write that filled it was in flight and then failed.
zufs-report-zones checks a start LBA inside a zone, PARTIAL, the
reporting options 3Eh, 06h and 3Fh, and the smaller last zone.
zufs-more-cmds checks WRITE(12), WRITE AND VERIFY(16), CLOSE ALL,
finishing a closed zone, ZONE COUNT with and without the ALL bit, zone
IDs that do not start a zone, a write without a PRDT, and UNMAP.
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
tests/qtest/ufs-test.c | 984 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 984 insertions(+)
diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index 4e0c2aee5f..0b3e764b19 100644
--- a/tests/qtest/ufs-test.c
+++ b/tests/qtest/ufs-test.c
@@ -1832,6 +1832,966 @@ static void ufstest_hid(void *obj, void *data, QGuestAllocator *alloc)
ufs_exit(ufs, alloc);
}
+#define ZONE_SIZE (2 * 1024 * 1024)
+#define ZONE_BLOCKS (ZONE_SIZE / 4096)
+#define MAX_OPEN_ZONE 6
+#define NUM_ZONES (TEST_IMAGE_SIZE / ZONE_SIZE)
+#define REPORT_ZONES_DESC_SIZE 64
+#define ZONED_TEST_LUN 2
+/* A second zoned LU whose capacity is not a multiple of the zone size */
+#define ODD_ZONED_TEST_LUN 3
+#define ODD_ZONE_SIZE (3 * 1024 * 1024)
+#define ODD_NUM_ZONES DIV_ROUND_UP(TEST_IMAGE_SIZE, ODD_ZONE_SIZE)
+/*
+ * A zoned LU on blkdebug with one open zone: the first write at LBA 0 and the
+ * first write at the start of zone 2 fail with an I/O error, and a breakpoint
+ * can hold a write in flight. It is a -drive with an id because
+ * qtest_qemu_io() needs a BlockBackend name: for a node name, x-qemu-io
+ * attaches a temporary BlockBackend, and detaching it drains the node, which
+ * never returns while a breakpoint holds a write.
+ */
+#define EIO_ZONED_TEST_LUN 4
+#define EIO_ZONED_TEST_DRIVE "drv4"
+#define EIO_ZONE2_SECTOR (2 * ZONE_SIZE / 512)
+/* A zoned LU of more than 8 TiB with 128 MiB zones, for LBAs of 2^31 and up */
+#define BIG_ZONED_TEST_LUN 5
+#define BIG_ZONED_TEST_SIZE ((8ULL << 40) + 128 * 1024 * 1024)
+
+static void ufs_zufs_build_write16(uint8_t *cdb, uint64_t lba, uint32_t blocks)
+{
+ memset(cdb, 0, UFS_CDB_SIZE);
+ cdb[0] = WRITE_16;
+ stq_be_p(&cdb[2], lba);
+ stl_be_p(&cdb[10], blocks);
+}
+
+static void ufs_zufs_build_zbc_out(uint8_t *cdb, uint8_t action, uint64_t lba,
+ bool all)
+{
+ memset(cdb, 0, UFS_CDB_SIZE);
+ cdb[0] = UFS_ZBC_OUT;
+ cdb[1] = action;
+ stq_be_p(&cdb[2], lba);
+ cdb[14] = all ? 0x01 : 0x00;
+}
+
+static void ufs_zufs_build_report_zones(uint8_t *cdb, uint8_t rep_opts,
+ uint32_t alloc_len)
+{
+ memset(cdb, 0, UFS_CDB_SIZE);
+ cdb[0] = UFS_ZBC_IN;
+ cdb[1] = UFS_ZBC_REPORT_ZONES;
+ stl_be_p(&cdb[10], alloc_len);
+ cdb[14] = rep_opts;
+}
+
+static void ufs_zufs_clear_unit_attention(QUfs *ufs)
+{
+ const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+ REQUEST_SENSE,
+ };
+ uint8_t buf[4096];
+ UtpUpiuRsp rsp_upiu;
+ enum UtpOcsCodes ocs;
+
+ ocs = ufs_send_scsi_command(ufs, ZONED_TEST_LUN, request_sense_cdb, NULL, 0,
+ buf, sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, CHECK_CONDITION);
+}
+
+static uint8_t ufs_zufs_send(QUfs *ufs, const uint8_t *cdb,
+ const uint8_t *data_in, size_t data_in_len,
+ uint8_t *data_out, size_t data_out_len,
+ UtpUpiuRsp *rsp_upiu)
+{
+ enum UtpOcsCodes ocs;
+
+ ocs = ufs_send_scsi_command(ufs, ZONED_TEST_LUN, cdb, data_in, data_in_len,
+ data_out, data_out_len, rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ return rsp_upiu->header.scsi_status;
+}
+
+static void ufs_zufs_assert_sense(UtpUpiuRsp *rsp_upiu, uint8_t key,
+ uint8_t asc, uint8_t ascq)
+{
+ g_assert_cmpuint(rsp_upiu->header.scsi_status, ==, CHECK_CONDITION);
+ g_assert_cmpuint(rsp_upiu->sr.sense_data[2] & 0xf, ==, key);
+ g_assert_cmpuint(rsp_upiu->sr.sense_data[12], ==, asc);
+ g_assert_cmpuint(rsp_upiu->sr.sense_data[13], ==, ascq);
+}
+
+static uint32_t ufs_zufs_count_zones(QUfs *ufs, uint8_t rep_opts)
+{
+ uint8_t buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+
+ ufs_zufs_build_report_zones(cdb, rep_opts, sizeof(buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ return ldl_be_p(&buf[0]) / REPORT_ZONES_DESC_SIZE;
+}
+
+static uint64_t ufs_zufs_write_pointer(QUfs *ufs, uint64_t zone_start)
+{
+ uint8_t buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(buf));
+ stq_be_p(&cdb[2], zone_start);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 16]), ==, zone_start);
+ return ldq_be_p(&buf[64 + 24]);
+}
+
+static void ufstest_zufs_init(void *obj, void *data, QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t buf[4096] = { 0 };
+ const uint8_t inquiry_cdb[UFS_CDB_SIZE] = {
+ INQUIRY, 0x00, 0x00, 0x00, 0x60,
+ };
+ const uint8_t inquiry_vpd_00_cdb[UFS_CDB_SIZE] = {
+ INQUIRY, 0x01 /* EVPD */, 0x00 /* page code */, 0x00, 0x40,
+ };
+ const uint8_t inquiry_vpd_b6_cdb[UFS_CDB_SIZE] = {
+ INQUIRY, 0x01 /* EVPD */, 0xb6 /* page code */, 0x00, 0x40,
+ };
+ const uint8_t mode_sense_caching_cdb[UFS_CDB_SIZE] = {
+ MODE_SENSE_10, 0x08 /* DBD */, MODE_PAGE_CACHING, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff,
+ };
+ const uint8_t read_capacity_cdb[UFS_CDB_SIZE] = {
+ SERVICE_ACTION_IN_16, SAI_READ_CAPACITY_16, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20,
+ };
+ uint8_t report_zones_cdb[UFS_CDB_SIZE];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ uint32_t zone_list_len;
+ uint8_t vpd_00_len;
+
+ ufs_init(ufs, alloc);
+ ufs_zufs_clear_unit_attention(ufs);
+
+ /* Standard INQUIRY: host-managed zoned block device */
+ g_assert_cmpuint(ufs_zufs_send(ufs, inquiry_cdb, NULL, 0, buf, 0x60,
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[0] & 0x1f, ==, TYPE_ZBC);
+
+ /* Supported VPD pages include the ZBC device characteristics page */
+ g_assert_cmpuint(ufs_zufs_send(ufs, inquiry_vpd_00_cdb, NULL, 0, buf, 0x40,
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[0], ==, TYPE_ZBC);
+ g_assert_nonnull(memchr(&buf[4], 0xb6, buf[3]));
+ g_assert_cmpuint(buf[4 + buf[3] - 1], ==, 0xb6);
+ vpd_00_len = buf[3];
+
+ /* ZBC device characteristics VPD page */
+ g_assert_cmpuint(ufs_zufs_send(ufs, inquiry_vpd_b6_cdb, NULL, 0, buf, 0x40,
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[0], ==, TYPE_ZBC);
+ g_assert_cmpuint(buf[1], ==, 0xb6);
+ g_assert_cmpuint(lduw_be_p(&buf[2]), ==, 0x3c);
+ /* URSWRZ */
+ g_assert_cmpuint(buf[4] & 0x1, ==, 1);
+ g_assert_cmpuint(ldl_be_p(&buf[16]), ==, MAX_OPEN_ZONE);
+
+ /* Allocation length 4: only the header, whose page length counts 0xb6 */
+ memset(cdb, 0, sizeof(cdb));
+ memcpy(cdb, inquiry_vpd_00_cdb, 5);
+ cdb[4] = 4;
+ memset(buf, 0, sizeof(buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[0], ==, TYPE_ZBC);
+ g_assert_cmpuint(buf[3], ==, vpd_00_len);
+ g_assert_cmpuint(buf[4], ==, 0);
+
+ /* Allocation length of the scsi-hd list: 0xb6 does not fit */
+ cdb[4] = 4 + vpd_00_len - 1;
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[3], ==, vpd_00_len);
+ g_assert_cmpuint(buf[4 + vpd_00_len - 2], !=, 0xb6);
+ g_assert_cmpuint(buf[4 + vpd_00_len - 2], !=, 0);
+ g_assert_cmpuint(buf[4 + vpd_00_len - 1], ==, 0);
+
+ /* UNMAP is not advertised in the Block Limits and LBP pages */
+ cdb[2] = 0xb0;
+ cdb[4] = 0x40;
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[0], ==, TYPE_ZBC);
+ g_assert_cmpuint(ldl_be_p(&buf[20]), ==, 0);
+ g_assert_cmpuint(ldl_be_p(&buf[24]), ==, 0);
+ cdb[2] = 0xb2;
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[5] & 0x80, ==, 0);
+
+ /* An INQUIRY without a PRDT fails like on a regular LU */
+ g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, inquiry_cdb,
+ NULL, 0, NULL, 0, &rsp_upiu), ==,
+ UFS_OCS_INVALID_CMD_TABLE_ATTR);
+
+ /* No logical block provisioning: the guest gets no UNMAP */
+ g_assert_cmpuint(ufs_zufs_send(ufs, read_capacity_cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[14] & 0x80, ==, 0);
+
+ /* MODE SENSE takes the scsi-hd disk path: caching page, WCE and DPOFUA */
+ g_assert_cmpuint(ufs_zufs_send(ufs, mode_sense_caching_cdb, NULL, 0, buf,
+ 0xff, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(buf[3] & 0x10, ==, 0x10);
+ g_assert_cmpuint(buf[8] & 0x3f, ==, MODE_PAGE_CACHING);
+ g_assert_cmpuint(buf[10] & 0x04, ==, 0x04);
+
+ /* Report zones */
+ ufs_zufs_build_report_zones(report_zones_cdb, 0x00, sizeof(buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, report_zones_cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu), ==, GOOD);
+ zone_list_len = ldl_be_p(&buf[0]);
+ g_assert_cmpuint(zone_list_len, ==, REPORT_ZONES_DESC_SIZE * NUM_ZONES);
+ g_assert_cmpuint(ldq_be_p(&buf[8]), ==, TEST_IMAGE_SIZE / 4096 - 1);
+ /* First zone: sequential write required, empty */
+ g_assert_cmpuint(buf[64] & 0xf, ==, 0x2);
+ g_assert_cmpuint(buf[65] >> 4, ==, 0x1);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 8]), ==, ZONE_BLOCKS);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_write(void *obj, void *data, QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t read_buf[4096] = { 0 };
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ int i;
+
+ ufs_init(ufs, alloc);
+ ufs_zufs_clear_unit_attention(ufs);
+ memset(write_buf, 0xab, sizeof(write_buf));
+
+ /* Write at the write pointer of zone 0 */
+ ufs_zufs_build_write16(cdb, 0, 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+
+ /* Unaligned write error */
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, CHECK_CONDITION);
+ g_assert_cmpint(rsp_upiu.sr.sense_data[12], ==, 0x21);
+ g_assert_cmpint(rsp_upiu.sr.sense_data[13], ==, 0x04);
+
+ /* Linux flushes zoned disks with SYNCHRONIZE CACHE(16) */
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = SYNCHRONIZE_CACHE_16;
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+
+ /* Zone 0 is implicitly open */
+ ufs_zufs_build_report_zones(cdb, 0x02, sizeof(read_buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, read_buf,
+ sizeof(read_buf), &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&read_buf[0]), ==, REPORT_ZONES_DESC_SIZE);
+ g_assert_cmpuint(ldq_be_p(&read_buf[64 + 24]), ==, 1);
+
+ /* Resetting an open zone empties it */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_RESET_WRITE_POINTER, 0, false);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ ufs_zufs_build_write16(cdb, 0, 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+
+ /*
+ * Writing to one zone more than MAX_OPEN_ZONE closes an implicitly open
+ * zone instead of failing.
+ */
+ for (i = 1; i <= MAX_OPEN_ZONE; i++) {
+ ufs_zufs_build_write16(cdb, (uint64_t)i * ZONE_BLOCKS, 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ }
+ ufs_zufs_build_report_zones(cdb, 0x04, sizeof(read_buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, read_buf,
+ sizeof(read_buf), &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&read_buf[0]), ==, REPORT_ZONES_DESC_SIZE);
+
+ /* Finishing an empty zone makes it full */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE,
+ (uint64_t)(NUM_ZONES - 1) * ZONE_BLOCKS, false);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ ufs_zufs_build_report_zones(cdb, 0x05, sizeof(read_buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, read_buf,
+ sizeof(read_buf), &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&read_buf[0]), ==, REPORT_ZONES_DESC_SIZE);
+
+ /* Write and zone management beyond the last LBA: LBA OUT OF RANGE */
+ ufs_zufs_build_write16(cdb, TEST_IMAGE_SIZE / 4096, 1);
+ ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x00);
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, TEST_IMAGE_SIZE / 4096,
+ false);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x00);
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_RESET_WRITE_POINTER,
+ TEST_IMAGE_SIZE / 4096, false);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x00);
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(read_buf));
+ stq_be_p(&cdb[2], TEST_IMAGE_SIZE / 4096);
+ ufs_zufs_send(ufs, cdb, NULL, 0, read_buf, sizeof(read_buf), &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x00);
+
+ /* Unknown reporting option and zone operation: INVALID FIELD IN CDB */
+ ufs_zufs_build_report_zones(cdb, 0x08, sizeof(read_buf));
+ ufs_zufs_send(ufs, cdb, NULL, 0, read_buf, sizeof(read_buf), &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+ ufs_zufs_build_zbc_out(cdb, 0x05, 0, false);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+
+ /*
+ * An allocation length above the data buffer stays in bounds: QEMU must
+ * not fill its buffer beyond the data buffer size.
+ */
+ ufs_zufs_build_report_zones(cdb, 0x00, 1024 * 1024);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, read_buf, 256,
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&read_buf[0]), ==,
+ REPORT_ZONES_DESC_SIZE * NUM_ZONES);
+
+ /* Reset all zones */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_RESET_WRITE_POINTER, 0, true);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ ufs_zufs_build_report_zones(cdb, 0x01, sizeof(read_buf));
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, read_buf,
+ sizeof(read_buf), &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&read_buf[0]), ==,
+ REPORT_ZONES_DESC_SIZE * NUM_ZONES);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_write_cmds(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t read_buf[4096];
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ const uint64_t zone1 = ZONE_BLOCKS, zone2 = 2 * ZONE_BLOCKS,
+ zone3 = 3 * ZONE_BLOCKS;
+
+ ufs_init(ufs, alloc);
+ ufs_zufs_clear_unit_attention(ufs);
+ memset(write_buf, 0xcd, sizeof(write_buf));
+
+ /* WRITE(10) and WRITE(6) move the write pointer like WRITE(16) */
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = WRITE_10;
+ stl_be_p(&cdb[2], 0);
+ stw_be_p(&cdb[7], 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, 0), ==, 1);
+
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = WRITE_6;
+ cdb[1] = (zone3 >> 16) & 0x1f;
+ cdb[2] = (zone3 >> 8) & 0xff;
+ cdb[3] = zone3 & 0xff;
+ cdb[4] = 1;
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone3), ==, zone3 + 1);
+
+ /* WRITE SAME(16) of two blocks at the write pointer */
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = WRITE_SAME_16;
+ stq_be_p(&cdb[2], 1);
+ stl_be_p(&cdb[10], 2);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, 0), ==, 3);
+
+ /* scsi-hd reports WSNZ=1 and fails a zero length WRITE SAME */
+ stq_be_p(&cdb[2], 3);
+ stl_be_p(&cdb[10], 0);
+ ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, 0), ==, 3);
+
+ /* A write crossing the end of its zone: WRITE BOUNDARY VIOLATION */
+ ufs_zufs_build_write16(cdb, zone1, ZONE_BLOCKS + 1);
+ ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x05);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone1), ==, zone1);
+
+ /* A write to a full zone: INVALID FIELD IN CDB */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, zone2, false);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ ufs_zufs_build_write16(cdb, zone2, 1);
+ ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+
+ /* A WRITE(10) at an LBA above 2^31 is checked like a WRITE(16) */
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = REQUEST_SENSE;
+ g_assert_cmpuint(ufs_send_scsi_command(ufs, BIG_ZONED_TEST_LUN, cdb, NULL,
+ 0, read_buf, sizeof(read_buf),
+ &rsp_upiu), ==, UFS_OCS_SUCCESS);
+ cdb[0] = WRITE_10;
+ stl_be_p(&cdb[2], 0x80000001);
+ stw_be_p(&cdb[7], 1);
+ g_assert_cmpuint(ufs_send_scsi_command(ufs, BIG_ZONED_TEST_LUN, cdb,
+ write_buf, sizeof(write_buf), NULL,
+ 0, &rsp_upiu), ==, UFS_OCS_SUCCESS);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x04);
+ /* ... and one at the zone start moves that zone's write pointer */
+ stl_be_p(&cdb[2], 0x80000000);
+ g_assert_cmpuint(ufs_send_scsi_command(ufs, BIG_ZONED_TEST_LUN, cdb,
+ write_buf, sizeof(write_buf), NULL,
+ 0, &rsp_upiu), ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(read_buf));
+ stq_be_p(&cdb[2], 0x80000000);
+ g_assert_cmpuint(ufs_send_scsi_command(ufs, BIG_ZONED_TEST_LUN, cdb, NULL,
+ 0, read_buf, sizeof(read_buf),
+ &rsp_upiu), ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(ldq_be_p(&read_buf[64 + 16]), ==, 0x80000000);
+ g_assert_cmpuint(ldq_be_p(&read_buf[64 + 24]), ==, 0x80000001);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_open_zones(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ int i;
+
+ ufs_init(ufs, alloc);
+ ufs_zufs_clear_unit_attention(ufs);
+ memset(write_buf, 0xef, sizeof(write_buf));
+
+ /* Open MAX_OPEN_ZONE zones explicitly */
+ for (i = 0; i < MAX_OPEN_ZONE; i++) {
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_OPEN_ZONE,
+ (uint64_t)i * ZONE_BLOCKS, false);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu),
+ ==, GOOD);
+ }
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x03), ==, MAX_OPEN_ZONE);
+
+ /* No implicitly open zone to close: INSUFFICIENT ZONE RESOURCES */
+ ufs_zufs_build_write16(cdb, (uint64_t)MAX_OPEN_ZONE * ZONE_BLOCKS, 1);
+ ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, DATA_PROTECT, 0x55, 0x0e);
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_OPEN_ZONE,
+ (uint64_t)MAX_OPEN_ZONE * ZONE_BLOCKS, false);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, DATA_PROTECT, 0x55, 0x0e);
+
+ /* Closing an open zone with nothing written empties it */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_CLOSE_ZONE, 0, false);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x01), ==, NUM_ZONES - 5);
+
+ /*
+ * Fill the freed resource with an implicitly open zone, then write to
+ * one more zone: the implicitly open zone is closed.
+ */
+ ufs_zufs_build_write16(cdb, (uint64_t)MAX_OPEN_ZONE * ZONE_BLOCKS, 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ ufs_zufs_build_write16(cdb, (uint64_t)(MAX_OPEN_ZONE + 1) * ZONE_BLOCKS,
+ 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x04), ==, 1);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x02), ==, 1);
+
+ /* OPEN ALL: 5 explicit + 1 closed fit, the implicit one is closed */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_OPEN_ZONE, 0, true);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x03), ==, MAX_OPEN_ZONE);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x04), ==, 1);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x02), ==, 0);
+
+ /* OPEN ALL again: 6 explicit + 1 closed do not fit, nothing changes */
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, DATA_PROTECT, 0x55, 0x0e);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x04), ==, 1);
+
+ /* FINISH ALL fills open and closed zones, empty ones stay empty */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, 0, true);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x05), ==, MAX_OPEN_ZONE + 1);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x01), ==,
+ NUM_ZONES - MAX_OPEN_ZONE - 1);
+
+ /* RESET ALL */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_RESET_WRITE_POINTER, 0, true);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x01), ==, NUM_ZONES);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_unit_attention(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+
+ ufs_init(ufs, alloc);
+ memset(write_buf, 0x5a, sizeof(write_buf));
+
+ /*
+ * After the reset in ufs_init(), the first command that scsi-hd does not
+ * exempt gets UNIT ATTENTION and is not performed, zone commands too:
+ * the zone stays empty and the write pointer stays put, so that the
+ * guest can retry.
+ */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, 0, false);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, CHECK_CONDITION);
+ g_assert_cmpuint(rsp_upiu.sr.sense_data[2] & 0xf, ==, UNIT_ATTENTION);
+ ufs_zufs_build_write16(cdb, 0, 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, 0), ==, 1);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_io_error(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+ REQUEST_SENSE,
+ };
+ uint8_t buf[4096];
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ enum UtpOcsCodes ocs;
+
+ ufs_init(ufs, alloc);
+ memset(write_buf, 0xa5, sizeof(write_buf));
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, request_sense_cdb,
+ NULL, 0, buf, sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+
+ /* scsi-hd fails the first write at LBA 0: the write pointer moves back */
+ ufs_zufs_build_write16(cdb, 0, 1);
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, CHECK_CONDITION);
+
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(buf));
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(buf[64 + 1] >> 4, ==, 0x1);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 24]), ==, 0);
+
+ /* The retry succeeds */
+ ufs_zufs_build_write16(cdb, 0, 1);
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(buf));
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 24]), ==, 1);
+
+ ufs_exit(ufs, alloc);
+}
+
+/* Submit a SCSI command that writes data_in through MCQ, without waiting */
+static int ufs_zufs_send_write_async(QUfs *ufs, uint8_t lun,
+ const uint8_t *cdb,
+ const uint8_t *data_in,
+ size_t data_in_len)
+{
+ QTestState *qts = ufs->dev.bus->qts;
+ int cmd_desc_slot = alloc_cmd_desc_slot(ufs);
+ uint64_t req_upiu_addr =
+ ufs->cmd_desc_addr + cmd_desc_slot * UTP_COMMAND_DESCRIPTOR_SIZE;
+ UfshcdSgEntry entry = { 0 };
+ UtpUpiuReq req_upiu = { 0 };
+ UtpTransferReqDesc utrd;
+
+ g_assert_true(ufs->support_mcq);
+ g_assert_cmpuint(data_in_len, <=, PRD_ENTRY_DATA_SIZE);
+ qtest_memwrite(qts, ufs->data_buffer_addr, data_in, data_in_len);
+ entry.addr = cpu_to_le64(ufs->data_buffer_addr);
+ entry.size = cpu_to_le32(data_in_len - 1);
+ qtest_memwrite(qts, req_upiu_addr + UTP_PRDT_UPIU_OFFSET, &entry,
+ sizeof(entry));
+
+ req_upiu.header.trans_type = UFS_UPIU_TRANSACTION_COMMAND;
+ req_upiu.header.flags = UFS_UPIU_CMD_FLAGS_WRITE;
+ req_upiu.header.lun = lun;
+ req_upiu.header.task_tag = cmd_desc_slot;
+ req_upiu.sc.exp_data_transfer_len = cpu_to_be32(data_in_len);
+ memcpy(req_upiu.sc.cdb, cdb, UFS_CDB_SIZE);
+ qtest_memwrite(qts, req_upiu_addr, &req_upiu, sizeof(req_upiu));
+
+ utrd = ufs_build_req_utrd(req_upiu_addr, UFS_UTP_HOST_TO_DEVICE, 1);
+ __ufs_send_transfer_request_mcq_async(ufs, lun, &utrd);
+ return cmd_desc_slot;
+}
+
+/* Wait for the request of ufs_zufs_send_write_async(); returns its status */
+static uint8_t ufs_zufs_wait_async(QUfs *ufs, int cmd_desc_slot,
+ UtpUpiuRsp *rsp_upiu)
+{
+ uint64_t rsp_upiu_addr = ufs->cmd_desc_addr +
+ cmd_desc_slot * UTP_COMMAND_DESCRIPTOR_SIZE +
+ UTP_RESPONSE_UPIU_OFFSET;
+ uint64_t end_time;
+ UfsCqEntry cqe;
+ int n_cqe;
+
+ end_time = g_get_monotonic_time() + TIMEOUT_SECONDS * G_TIME_SPAN_SECOND;
+ do {
+ qtest_clock_step(ufs->dev.bus->qts, 100);
+ n_cqe = ufs_mcq_poll_cq(ufs, &cqe, 1);
+ } while (!n_cqe && g_get_monotonic_time() < end_time);
+ g_assert_cmpint(n_cqe, ==, 1);
+ g_assert_cmpuint(cqe.status, ==, UFS_OCS_SUCCESS);
+ ufs_wreg(ufs, A_IS, FIELD_DP32(0, IS, CQES, 1));
+
+ qtest_memread(ufs->dev.bus->qts, rsp_upiu_addr, rsp_upiu,
+ sizeof(*rsp_upiu));
+ g_assert_cmpuint(rsp_upiu->header.task_tag, ==, cmd_desc_slot);
+ release_cmd_desc_slot(ufs, cmd_desc_slot);
+ return rsp_upiu->header.scsi_status;
+}
+
+/* Condition and write pointer of the zone at zone_start of the EIO LU */
+static uint8_t ufs_zufs_eio_zone(QUfs *ufs, uint64_t zone_start, uint64_t *wp)
+{
+ uint8_t buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ enum UtpOcsCodes ocs;
+
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(buf));
+ stq_be_p(&cdb[2], zone_start);
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 16]), ==, zone_start);
+ *wp = ldq_be_p(&buf[64 + 24]);
+ return buf[64 + 1] >> 4;
+}
+
+static void ufstest_zufs_write_in_flight(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ QTestState *qts = ufs->dev.bus->qts;
+ const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+ REQUEST_SENSE,
+ };
+ const uint64_t zone1 = ZONE_BLOCKS, zone2 = 2 * ZONE_BLOCKS;
+ uint8_t buf[4096];
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ enum UtpOcsCodes ocs;
+ uint64_t wp;
+ int slot;
+
+ ufs_init(ufs, alloc);
+ g_assert_true(ufs->support_mcq);
+ memset(write_buf, 0x5a, sizeof(write_buf));
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, request_sense_cdb,
+ NULL, 0, buf, sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+
+ /*
+ * A write to zone 1 takes the only open resource and closes zone 0,
+ * which a write in flight opened. That write fails and still moves the
+ * write pointer back.
+ */
+ qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "break write_aio A");
+ ufs_zufs_build_write16(cdb, 0, 1);
+ slot = ufs_zufs_send_write_async(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf));
+ g_assert_cmpuint(ufs_zufs_eio_zone(ufs, 0, &wp), ==, 0x2);
+ g_assert_cmpuint(wp, ==, 1);
+
+ ufs_zufs_build_write16(cdb, zone1, 1);
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_eio_zone(ufs, 0, &wp), ==, 0x4);
+ g_assert_cmpuint(wp, ==, 1);
+
+ qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "resume A");
+ g_assert_cmpuint(ufs_zufs_wait_async(ufs, slot, &rsp_upiu), ==,
+ CHECK_CONDITION);
+ g_assert_cmpuint(ufs_zufs_eio_zone(ufs, 0, &wp), ==, 0x1);
+ g_assert_cmpuint(wp, ==, 0);
+
+ ufs_zufs_build_write16(cdb, 0, 1);
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+
+ /*
+ * FINISH ZONE completes while a write that filled zone 2 is in flight.
+ * When the write fails, the zone stays full.
+ */
+ qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "break write_aio B");
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = WRITE_SAME_16;
+ stq_be_p(&cdb[2], zone2);
+ stl_be_p(&cdb[10], ZONE_BLOCKS);
+ slot = ufs_zufs_send_write_async(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf));
+ g_assert_cmpuint(ufs_zufs_eio_zone(ufs, zone2, &wp), ==, 0xe);
+
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, zone2, false);
+ ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, NULL, 0, NULL, 0,
+ &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+
+ qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "resume B");
+ g_assert_cmpuint(ufs_zufs_wait_async(ufs, slot, &rsp_upiu), ==,
+ CHECK_CONDITION);
+ g_assert_cmpuint(ufs_zufs_eio_zone(ufs, zone2, &wp), ==, 0xe);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_report_zones(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+ REQUEST_SENSE,
+ };
+ uint8_t buf[4096];
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ uint64_t last_start;
+ enum UtpOcsCodes ocs;
+
+ ufs_init(ufs, alloc);
+ ufs_zufs_clear_unit_attention(ufs);
+ memset(write_buf, 0x3c, sizeof(write_buf));
+
+ /* A start LBA inside a zone reports from that zone on */
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(buf));
+ stq_be_p(&cdb[2], 2 * ZONE_BLOCKS + 5);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&buf[0]), ==,
+ (NUM_ZONES - 2) * REPORT_ZONES_DESC_SIZE);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 16]), ==, 2 * ZONE_BLOCKS);
+
+ /* PARTIAL: the zone list length counts only the returned zones */
+ ufs_zufs_build_report_zones(cdb, 0x80, 64 + 3 * REPORT_ZONES_DESC_SIZE);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&buf[0]), ==, 3 * REPORT_ZONES_DESC_SIZE);
+ ufs_zufs_build_report_zones(cdb, 0x00, 64 + 3 * REPORT_ZONES_DESC_SIZE);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, buf, sizeof(buf),
+ &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&buf[0]), ==, NUM_ZONES * REPORT_ZONES_DESC_SIZE);
+
+ /* All zones except gap zones; no read-only or conventional zones */
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x3e), ==, NUM_ZONES);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x06), ==, 0);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x3f), ==, 0);
+
+ /* A capacity that is not a multiple of the zone size: a smaller zone */
+ ocs = ufs_send_scsi_command(ufs, ODD_ZONED_TEST_LUN, request_sense_cdb,
+ NULL, 0, buf, sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ last_start = (uint64_t)(ODD_NUM_ZONES - 1) * (ODD_ZONE_SIZE / 4096);
+ ufs_zufs_build_report_zones(cdb, 0x00, sizeof(buf));
+ stq_be_p(&cdb[2], last_start);
+ ocs = ufs_send_scsi_command(ufs, ODD_ZONED_TEST_LUN, cdb, NULL, 0, buf,
+ sizeof(buf), &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ g_assert_cmpuint(ldl_be_p(&buf[0]), ==, REPORT_ZONES_DESC_SIZE);
+ g_assert_cmpuint(ldq_be_p(&buf[64 + 8]), ==,
+ TEST_IMAGE_SIZE / 4096 - last_start);
+
+ /* A write crossing the end of that zone and of the logical unit */
+ ufs_zufs_build_write16(cdb, last_start, 1);
+ ocs = ufs_send_scsi_command(ufs, ODD_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ g_assert_cmpuint(rsp_upiu.header.scsi_status, ==, GOOD);
+ ufs_zufs_build_write16(cdb, last_start + 1,
+ TEST_IMAGE_SIZE / 4096 - last_start);
+ ocs = ufs_send_scsi_command(ufs, ODD_ZONED_TEST_LUN, cdb, write_buf,
+ sizeof(write_buf), NULL, 0, &rsp_upiu);
+ g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x21, 0x00);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void ufstest_zufs_more_cmds(void *obj, void *data,
+ QGuestAllocator *alloc)
+{
+ QUfs *ufs = obj;
+ uint8_t write_buf[4096];
+ uint8_t cdb[UFS_CDB_SIZE];
+ UtpUpiuRsp rsp_upiu;
+ const uint64_t zone4 = 4 * ZONE_BLOCKS, zone5 = 5 * ZONE_BLOCKS;
+
+ ufs_init(ufs, alloc);
+ ufs_zufs_clear_unit_attention(ufs);
+ memset(write_buf, 0x96, sizeof(write_buf));
+
+ /* WRITE(12) and WRITE AND VERIFY(16) move the write pointer */
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = WRITE_12;
+ stl_be_p(&cdb[2], zone4);
+ stl_be_p(&cdb[6], 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = WRITE_VERIFY_16;
+ stq_be_p(&cdb[2], zone4 + 1);
+ stl_be_p(&cdb[10], 1);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+ NULL, 0, &rsp_upiu), ==, GOOD);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone4), ==, zone4 + 2);
+
+ /* CLOSE ALL closes it, FINISH ZONE fills the closed zone */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_CLOSE_ZONE, 0, true);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x04), ==, 1);
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, zone4, false);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x05), ==, 1);
+
+ /* A zone operation on a range of zones is not supported */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_FINISH_ZONE, zone5, false);
+ stw_be_p(&cdb[12], 2);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+
+ /* With the ALL bit, ZONE COUNT does not matter */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_CLOSE_ZONE, 0, true);
+ stw_be_p(&cdb[12], 2);
+ g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+ GOOD);
+
+ /* A zone ID that is not the start of a zone */
+ ufs_zufs_build_zbc_out(cdb, UFS_ZBC_RESET_WRITE_POINTER, zone5 + 1, false);
+ ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+
+ /* A write without a PRDT fails and leaves the write pointer alone */
+ ufs_zufs_build_write16(cdb, zone5, 1);
+ g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, cdb, NULL, 0,
+ NULL, 0, &rsp_upiu), ==,
+ UFS_OCS_INVALID_CMD_TABLE_ATTR);
+ g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone5), ==, zone5);
+ g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x02), ==, 0);
+
+ /* UNMAP would bypass the write pointers */
+ memset(cdb, 0, sizeof(cdb));
+ cdb[0] = UNMAP;
+ stw_be_p(&cdb[7], 24);
+ memset(write_buf, 0, 24);
+ stw_be_p(&write_buf[0], 22);
+ stw_be_p(&write_buf[2], 16);
+ stq_be_p(&write_buf[8], zone4);
+ stl_be_p(&write_buf[16], 1);
+ ufs_zufs_send(ufs, cdb, write_buf, 24, NULL, 0, &rsp_upiu);
+ ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x20, 0x00);
+
+ ufs_exit(ufs, alloc);
+}
+
+static void *ufs_zufs_test_setup(GString *cmd_line, void *arg)
+{
+ char *tmp_path = drive_create();
+ char *odd_path = drive_create();
+ char *eio_path = drive_create();
+
+ g_string_append_printf(cmd_line,
+ " -blockdev file,filename=%s,node-name=drv2 "
+ "-device ufs-lu,bus=ufs0,drive=drv2,lun=%d,"
+ "zoned=on,zoned.max_open=%d,zoned.zone_size=%d "
+ "-blockdev file,filename=%s,node-name=drv3 "
+ "-device ufs-lu,bus=ufs0,drive=drv3,lun=%d,"
+ "zoned=on,zoned.zone_size=%d "
+ "-drive if=none,id=%s,driver=raw,"
+ "file.driver=blkdebug,file.image.filename=%s,"
+ "file.inject-error.0.event=none,"
+ "file.inject-error.0.iotype=write,"
+ "file.inject-error.0.errno=5,"
+ "file.inject-error.0.sector=0,"
+ "file.inject-error.0.once=on,"
+ "file.inject-error.1.event=none,"
+ "file.inject-error.1.iotype=write,"
+ "file.inject-error.1.errno=5,"
+ "file.inject-error.1.sector=%d,"
+ "file.inject-error.1.once=on "
+ "-device ufs-lu,bus=ufs0,drive=%s,lun=%d,"
+ "zoned=on,zoned.zone_size=%d,zoned.max_open=1 "
+ "-blockdev null-co,node-name=drv5,read-zeroes=on,"
+ "size=%" PRIu64 " "
+ "-device ufs-lu,bus=ufs0,drive=drv5,lun=%d,"
+ "zoned=on ",
+ tmp_path, ZONED_TEST_LUN, MAX_OPEN_ZONE, ZONE_SIZE,
+ odd_path, ODD_ZONED_TEST_LUN, ODD_ZONE_SIZE,
+ EIO_ZONED_TEST_DRIVE, eio_path, EIO_ZONE2_SECTOR,
+ EIO_ZONED_TEST_DRIVE, EIO_ZONED_TEST_LUN, ZONE_SIZE,
+ (uint64_t)BIG_ZONED_TEST_SIZE, BIG_ZONED_TEST_LUN);
+
+ return arg;
+}
+
static void ufs_register_nodes(void)
{
const char *arch;
@@ -1854,6 +2814,12 @@ static void ufs_register_nodes(void)
"mcq=false,nutrs=32,nutmrs=8,"
"wb-max-size=1024,"
"wb-min-size=256" };
+ QOSGraphTestOptions zufs_test_opts = { .before = ufs_zufs_test_setup,
+ .edge.extra_device_opts =
+ "mcq=false,nutrs=32,nutmrs=8" };
+ QOSGraphTestOptions zufs_mcq_test_opts = { .before = ufs_zufs_test_setup,
+ .edge.extra_device_opts =
+ "mcq=true,mcq-maxq=1" };
QOSGraphTestOptions acpi_eject_test_opts = { .subprocess = true };
add_qpci_address(&edge_opts, &(QPCIAddress){ .devfn = QPCI_DEVFN(4, 0) });
@@ -1894,6 +2860,24 @@ static void ufs_register_nodes(void)
qos_add_test("wb-init", "ufs", ufstest_wb_init, &wb_test_opts);
qos_add_test("wb-read-write", "ufs", ufstest_wb_read_write, &wb_test_opts);
qos_add_test("hid", "ufs", ufstest_hid, &io_test_opts);
+ qos_add_test("zufs-init", "ufs", ufstest_zufs_init, &zufs_test_opts);
+ qos_add_test("zufs-write", "ufs", ufstest_zufs_write, &zufs_test_opts);
+ qos_add_test("zufs-mcq-write", "ufs", ufstest_zufs_write,
+ &zufs_mcq_test_opts);
+ qos_add_test("zufs-write-cmds", "ufs", ufstest_zufs_write_cmds,
+ &zufs_test_opts);
+ qos_add_test("zufs-open-zones", "ufs", ufstest_zufs_open_zones,
+ &zufs_test_opts);
+ qos_add_test("zufs-unit-attention", "ufs", ufstest_zufs_unit_attention,
+ &zufs_test_opts);
+ qos_add_test("zufs-io-error", "ufs", ufstest_zufs_io_error,
+ &zufs_test_opts);
+ qos_add_test("zufs-write-in-flight", "ufs", ufstest_zufs_write_in_flight,
+ &zufs_mcq_test_opts);
+ qos_add_test("zufs-report-zones", "ufs", ufstest_zufs_report_zones,
+ &zufs_test_opts);
+ qos_add_test("zufs-more-cmds", "ufs", ufstest_zufs_more_cmds,
+ &zufs_test_opts);
}
libqos_init(ufs_register_nodes);
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB
2026-10-16 9:31 ` [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB Daejun Park
@ 2026-10-16 11:54 ` Stanley Jhu
2026-10-19 1:33 ` Daejun Park
0 siblings, 1 reply; 12+ messages in thread
From: Stanley Jhu @ 2026-10-16 11:54 UTC (permalink / raw)
To: qemu-devel, Daejun Park
Cc: Jeuk Kim, farosas, lvivier, pbonzini, kwolf, hreitz, qemu-block,
pierrick.bouvier
On Fri, 16 Oct 2026 18:31:09 +0900, Daejun Park wrote:
> + if (req->sg && req->sg->size > scsi_req->cmd.xfer) {
> + ufs_limit_sg_list(req, scsi_req->cmd.xfer);
> + }
This handles a PRDT that is larger than the CDB. Does a PRDT that is
smaller than the CDB need to be handled as well? If so, could that be
done on the host controller side instead of in the LU?
Stanley
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 5/6] hw/ufs: Support for Zoned UFS
2026-10-16 9:33 ` [PATCH v2 5/6] hw/ufs: Support for Zoned UFS Daejun Park
@ 2026-10-16 11:54 ` Stanley Jhu
2026-10-19 1:32 ` Daejun Park
0 siblings, 1 reply; 12+ messages in thread
From: Stanley Jhu @ 2026-10-16 11:54 UTC (permalink / raw)
To: qemu-devel, Daejun Park
Cc: Jeuk Kim, farosas, lvivier, pbonzini, kwolf, hreitz, qemu-block,
pierrick.bouvier
On Fri, 16 Oct 2026 18:33:43 +0900, Daejun Park wrote:
> + case WRITE_SAME_10:
As far as I can see, JESD220H does not list WRITE SAME among the UFS
commands. Would it make sense to reject WRITE SAME(10/16) on a zoned
LU?
Stanley
^ permalink raw reply [flat|nested] 12+ messages in thread
* RE:(2) [PATCH v2 5/6] hw/ufs: Support for Zoned UFS
2026-10-16 11:54 ` Stanley Jhu
@ 2026-10-19 1:32 ` Daejun Park
0 siblings, 0 replies; 12+ messages in thread
From: Daejun Park @ 2026-10-19 1:32 UTC (permalink / raw)
To: Stanley Jhu, qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
On Fri, 16 Oct 2026 19:54:36 +0800, Stanley Jhu wrote:
> On Fri, 16 Oct 2026 18:33:43 +0900, Daejun Park wrote:
>> + case WRITE_SAME_10:
>
> As far as I can see, JESD220H does not list WRITE SAME among the UFS
> commands. Would it make sense to reject WRITE SAME(10/16) on a zoned
> LU?
Thanks, agreed. Linux ufshcd sets no_write_same on every UFS logical
unit, so Linux guests do not depend on it.
v3 will fail WRITE SAME(10) and WRITE SAME(16) on a zoned LU with
INVALID COMMAND OPERATION CODE, as it does UNMAP. This also removes a
case where the write pointer runs ahead of the data: scsi-hd completes
a WRITE SAME with NDOB=1 with GOOD without writing anything, after the
zone check has moved the write pointer.
Regular LUs keep passing WRITE SAME to scsi-hd as today; if they
should reject it too, I can send that as a separate patch.
Daejun
^ permalink raw reply [flat|nested] 12+ messages in thread
* RE:(2) [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB
2026-10-16 11:54 ` Stanley Jhu
@ 2026-10-19 1:33 ` Daejun Park
2026-10-19 8:03 ` Stanley Jhu
0 siblings, 1 reply; 12+ messages in thread
From: Daejun Park @ 2026-10-19 1:33 UTC (permalink / raw)
To: Stanley Jhu, qemu-devel@nongnu.org
Cc: Jeuk Kim, farosas@suse.de, lvivier@redhat.com,
pbonzini@redhat.com, kwolf@redhat.com, hreitz@redhat.com,
qemu-block@nongnu.org, pierrick.bouvier@oss.qualcomm.com,
Daejun Park
On Fri, 16 Oct 2026 19:54:34 +0800, Stanley Jhu wrote:
> On Fri, 16 Oct 2026 18:31:09 +0900, Daejun Park wrote:
>> + if (req->sg && req->sg->size > scsi_req->cmd.xfer) {
>> + ufs_limit_sg_list(req, scsi_req->cmd.xfer);
>> + }
>
> This handles a PRDT that is larger than the CDB. Does a PRDT that is
> smaller than the CDB need to be handled as well? If so, could that be
> done on the host controller side instead of in the LU?
Thanks, yes. Today scsi-hd moves only what the SG list holds and
completes with GOOD, and on a zoned LU the write pointer moves past
blocks that were not written.
UFSHCI reports a data buffer that is too small with OCS MISMATCH DATA
BUFFER SIZE, so the status belongs to ufs.c. ufs.c does not parse the
CDB, though; the LU does, in scsi_req_new(). In v3 the LU would pass
the transfer length to ufs.c, and ufs.c would fail the request with
that OCS when the PRDT is smaller, before scsi-hd sees it and with the
write pointer unchanged. Would that split work for you?
Daejun
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB
2026-10-19 1:33 ` Daejun Park
@ 2026-10-19 8:03 ` Stanley Jhu
0 siblings, 0 replies; 12+ messages in thread
From: Stanley Jhu @ 2026-10-19 8:03 UTC (permalink / raw)
To: Daejun Park, qemu-devel
Cc: Jeuk Kim, farosas, lvivier, pbonzini, kwolf, hreitz, qemu-block,
pierrick.bouvier
On Mon, 19 Oct 2026 10:33:07 +0900, Daejun Park wrote:
> UFSHCI reports a data buffer that is too small with OCS MISMATCH DATA
> BUFFER SIZE, so the status belongs to ufs.c. ufs.c does not parse the
> CDB, though; the LU does, in scsi_req_new(). In v3 the LU would pass
> the transfer length to ufs.c, and ufs.c would fail the request with
> that OCS when the PRDT is smaller, before scsi-hd sees it and with the
> write pointer unchanged. Would that split work for you?
Yes, that split sounds good to me. Thanks.
Stanley
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-10-19 8:04 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <CGME20261016092423epcms2p83f80a8dd8f7c1ddc3a94541e0771c306@epcms2p8>
2026-10-16 9:24 ` [PATCH v2 0/6] Support for Zoned UFS Daejun Park
2026-10-16 9:25 ` [PATCH v2 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
2026-10-16 9:27 ` [PATCH v2 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10) Daejun Park
2026-10-16 9:31 ` [PATCH v2 3/6] hw/ufs: Limit data transfers to the transfer length of the CDB Daejun Park
2026-10-16 11:54 ` Stanley Jhu
2026-10-19 1:33 ` Daejun Park
2026-10-19 8:03 ` Stanley Jhu
2026-10-16 9:32 ` [PATCH v2 4/6] docs/system: Document zoned UFS logical units Daejun Park
2026-10-16 9:33 ` [PATCH v2 5/6] hw/ufs: Support for Zoned UFS Daejun Park
2026-10-16 11:54 ` Stanley Jhu
2026-10-19 1:32 ` Daejun Park
2026-10-16 9:35 ` [PATCH v2 6/6] tests/qtest: Add tests " Daejun Park
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.