All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 0/6] Support for Zoned UFS
       [not found] <CGME20261020001647epcms2p52cd58173164a43e252c33a3eb5935dc8@epcms2p5>
@ 2026-10-20  0:16 ` Daejun Park
  2026-10-20  0:17   ` [PATCH v3 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
                     ` (6 more replies)
  0 siblings, 7 replies; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:16 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,
	stanleyjhu@google.com, Keoseong Park, qemu-stable@nongnu.org,
	Daejun Park

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 v3:
- 1/6: fail a missing PRDT with OCS MISMATCH DATA BUFFER SIZE instead of
  INVALID COMMAND TABLE ATTRIBUTES.
- 3/6, retitled from "Limit data transfers to the transfer length of
  the CDB": also fail a request whose PRDT is shorter than the transfer
  length, with the same OCS before it reaches scsi-hd, as proposed in
  reply to Stanley [1]. ufs_fit_data_buffer() in ufs.c fits the PRDT;
  ufs-lu passes it the parsed transfer length. The HID qtest wrote eight
  blocks from a one-block buffer and now has eight blocks.
- 5/6: reject WRITE SAME(10/16) on zoned logical units with INVALID
  COMMAND OPERATION CODE and clear its bits in the Logical Block
  Provisioning VPD page (Stanley). Change the zone of a write only once
  scsi-hd has accepted the write: in v2, a write that scsi-hd refused,
  for example for its WRPROTECT field, could still open its zone or close
  another one. A write with a short PRDT changes no zone either; REPORT
  ZONES and the ZBC VPD page follow the same PRDT rule.
- 4/6, 6/6: documentation and tests follow.
- Rebased on current master.
- Link to v2: https://lore.kernel.org/qemu-devel/20261016092423epcms2p83f80a8dd8f7c1ddc3a94541e0771c306@epcms2p8

[1] https://lore.kernel.org/qemu-devel/20261019013307epcms2p14049cf5797cbe5332877ddeb950f18bc@epcms2p1

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: Check the PRDT size against 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                 |  930 ++++++++++++++++++++++++++++-
 hw/ufs/trace-events         |    7 +
 hw/ufs/ufs.c                |   45 +-
 hw/ufs/ufs.h                |   46 ++
 include/block/ufs.h         |   27 +
 tests/qtest/ufs-test.c      | 1368 ++++++++++++++++++++++++++++++++++++++++++-
 7 files changed, 2470 insertions(+), 8 deletions(-)
---
base-commit: a19f59a72d96056d9a8f48cf6c77f5787491e414
change-id: 20261016-zoned-ufs-0a6c741446a9

Best regards,
-- 
Daejun Park <daejun7.park@samsung.com>


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v3 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
@ 2026-10-20  0:17   ` Daejun Park
  2026-10-20  0:18   ` [PATCH v3 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10) Daejun Park
                     ` (5 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:17 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,
	stanleyjhu@google.com, Keoseong Park, 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 MISMATCH DATA BUFFER SIZE before it reaches scsi-hd:
the PRDT describes less data than the command moves. 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 ++++++++++++++++++++----
 hw/ufs/ufs.c           | 10 ++++++++--
 hw/ufs/ufs.h           |  2 ++
 tests/qtest/ufs-test.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 75 insertions(+), 6 deletions(-)

diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index 5905c49fac..fb72416fe1 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 with OCS MISMATCH DATA BUFFER
+     * SIZE, as virtio-scsi fails a buffer that 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_MISMATCH_DATA_BUF;
+    }
+
     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/hw/ufs/ufs.c b/hw/ufs/ufs.c
index f008af7467..dda615da7c 100644
--- a/hw/ufs/ufs.c
+++ b/hw/ufs/ufs.c
@@ -2451,10 +2451,16 @@ void ufs_complete_req(UfsRequest *req, UfsReqResult req_result)
     UfsHc *u = req->hc;
     assert(req->state == UFS_REQUEST_RUNNING);
 
-    if (req_result == UFS_REQUEST_SUCCESS) {
+    switch (req_result) {
+    case UFS_REQUEST_SUCCESS:
         req->utrd.header.dword_2 = cpu_to_le32(UFS_OCS_SUCCESS);
-    } else {
+        break;
+    case UFS_REQUEST_MISMATCH_DATA_BUF:
+        req->utrd.header.dword_2 = cpu_to_le32(UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+        break;
+    default:
         req->utrd.header.dword_2 = cpu_to_le32(UFS_OCS_INVALID_CMD_TABLE_ATTR);
+        break;
     }
 
     req->state = UFS_REQUEST_COMPLETE;
diff --git a/hw/ufs/ufs.h b/hw/ufs/ufs.h
index 265a43faaa..adf74b6833 100644
--- a/hw/ufs/ufs.h
+++ b/hw/ufs/ufs.h
@@ -47,6 +47,8 @@ typedef enum UfsReqResult {
     UFS_REQUEST_SUCCESS = 0,
     UFS_REQUEST_FAIL = 1,
     UFS_REQUEST_NO_COMPLETE = 2,
+    /* The PRDT describes less data than the command moves */
+    UFS_REQUEST_MISMATCH_DATA_BUF = 3,
 } UfsReqResult;
 
 #define UFS_INVALID_SLOT (-1)
diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index 9a9c5ca834..ea7b9aa62a 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_MISMATCH_DATA_BUF_SIZE);
+
+    ocs = ufs_send_scsi_command(ufs, test_lun, write_cdb, NULL, 0, NULL, 0,
+                                &rsp_upiu);
+    g_assert_cmpuint(ocs, ==, UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+
+    /* 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] 10+ messages in thread

* [PATCH v3 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10)
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
  2026-10-20  0:17   ` [PATCH v3 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
@ 2026-10-20  0:18   ` Daejun Park
  2026-10-20  0:19   ` [PATCH v3 3/6] hw/ufs: Check the PRDT size against the transfer length of the CDB Daejun Park
                     ` (4 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:18 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,
	stanleyjhu@google.com, Keoseong Park, 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 fb72416fe1..1c4d34c1c0 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 ea7b9aa62a..3c9e9e4032 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] 10+ messages in thread

* [PATCH v3 3/6] hw/ufs: Check the PRDT size against the transfer length of the CDB
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
  2026-10-20  0:17   ` [PATCH v3 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
  2026-10-20  0:18   ` [PATCH v3 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10) Daejun Park
@ 2026-10-20  0:19   ` Daejun Park
  2026-10-20 12:24     ` Stanley Jhu
  2026-10-20  0:19   ` [PATCH v3 4/6] docs/system: Document zoned UFS logical units Daejun Park
                     ` (3 subsequent siblings)
  6 siblings, 1 reply; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:19 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,
	stanleyjhu@google.com, Keoseong Park, 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.

When the PRDT is smaller than the transfer length, scsi-hd moved what it
holds and the request completed without an error, for example a
two-block WRITE(10) with a 4 KiB PRDT. Such a request now fails with OCS
MISMATCH DATA BUFFER SIZE, as one without a PRDT does. As in
virtio-scsi, the PRDT is compared with the transfer length of the CDB,
which for commands that return data is the allocation length, and before
scsi-hd checks the CDB.

The new ufs_fit_data_buffer() in ufs.c, where the SG list is built from
the PRDT, cuts a longer list and reports a shorter one; ufs-lu passes it
the transfer length that scsi_req_new() parsed. The HID qtest wrote
eight blocks from a one-block buffer, so give it eight blocks. The new
long-prdt and short-prdt qtests 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            |  20 ++++---
 hw/ufs/trace-events    |   1 +
 hw/ufs/ufs.c           |  35 ++++++++++++
 hw/ufs/ufs.h           |   1 +
 tests/qtest/ufs-test.c | 141 ++++++++++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 189 insertions(+), 9 deletions(-)

diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index 1c4d34c1c0..0c0537cfee 100644
--- a/hw/ufs/lu.c
+++ b/hw/ufs/lu.c
@@ -412,15 +412,19 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
                             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 with OCS MISMATCH DATA BUFFER
-     * SIZE, as virtio-scsi fails a buffer that 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.
+     * scsi-hd moves as many bytes as the SG list holds, and without an SG
+     * list it would call the transfer_data callback, which ufs-lu does not
+     * set. A UFS device moves what the CDB asks for, so fit the PRDT to the
+     * transfer length, and fail a request whose PRDT describes less, a
+     * missing PRDT included, with OCS MISMATCH DATA BUFFER SIZE, as
+     * virtio-scsi fails a buffer that does not match the command. The
+     * response reports the rest of the Expected Data Transfer Length as a
+     * residual. A request that reports a pending unit attention 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) {
+    if (!scsi_req->sense_len &&
+        !ufs_fit_data_buffer(req, scsi_req->cmd.xfer)) {
         scsi_req_unref(scsi_req);
         return UFS_REQUEST_MISMATCH_DATA_BUF;
     }
diff --git a/hw/ufs/trace-events b/hw/ufs/trace-events
index 5e5a54a3fb..db9d172713 100644
--- a/hw/ufs/trace-events
+++ b/hw/ufs/trace-events
@@ -22,6 +22,7 @@ ufs_hce_reset(void) "HCE 1 -> 0 reset: cancelling BHs, resetting MCQ and request
 ufs_err_dma_read_utrd(uint32_t slot, uint64_t addr) "failed to read utrd. UTRLDBR slot %"PRIu32", UTRD dma addr %"PRIu64""
 ufs_err_dma_read_req_upiu(uint32_t slot, uint64_t addr) "failed to read req upiu. UTRLDBR slot %"PRIu32", request upiu addr %"PRIu64""
 ufs_err_dma_read_prdt(uint32_t slot, uint64_t addr) "failed to read prdt. UTRLDBR slot %"PRIu32", prdt addr %"PRIu64""
+ufs_err_mismatch_data_buf(uint32_t slot, uint8_t task_tag, uint64_t xfer, uint64_t prdt_size) "slot %"PRIu32", task tag %"PRIu8", transfer length %"PRIu64", PRDT %"PRIu64""
 ufs_err_dma_read_sq(uint8_t sqid, uint64_t addr) "failed to read sq entry. sqid %"PRIu8", hwaddr %"PRIu64""
 ufs_err_dma_write_utrd(uint32_t slot, uint64_t addr) "failed to write utrd. UTRLDBR slot %"PRIu32", UTRD dma addr %"PRIu64""
 ufs_err_dma_write_rsp_upiu(uint32_t slot, uint64_t addr) "failed to write rsp upiu. UTRLDBR slot %"PRIu32", response upiu addr %"PRIu64""
diff --git a/hw/ufs/ufs.c b/hw/ufs/ufs.c
index dda615da7c..cd28f97a12 100644
--- a/hw/ufs/ufs.c
+++ b/hw/ufs/ufs.c
@@ -227,6 +227,41 @@ static MemTxResult ufs_dma_read_prdt(UfsRequest *req)
     return MEMTX_OK;
 }
 
+/*
+ * Fit the SG list built from the PRDT to the len bytes that the command of
+ * req moves: keep only the first len bytes of a longer list. Return false if
+ * the PRDT describes fewer than len bytes.
+ */
+bool ufs_fit_data_buffer(UfsRequest *req, uint64_t len)
+{
+    QEMUSGList *prdt_sg = req->sg;
+    uint64_t prdt_size = prdt_sg ? prdt_sg->size : 0;
+    QEMUSGList *sg;
+    int i;
+
+    if (prdt_size < len) {
+        trace_ufs_err_mismatch_data_buf(req->slot,
+                                        req->req_upiu.header.task_tag, len,
+                                        prdt_size);
+        return false;
+    }
+    if (prdt_size == len) {
+        return true;
+    }
+
+    sg = g_new0(QEMUSGList, 1);
+    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;
+    return true;
+}
+
 static MemTxResult ufs_dma_read_upiu(UfsRequest *req)
 {
     MemTxResult ret;
diff --git a/hw/ufs/ufs.h b/hw/ufs/ufs.h
index adf74b6833..15b3a352f3 100644
--- a/hw/ufs/ufs.h
+++ b/hw/ufs/ufs.h
@@ -306,6 +306,7 @@ void ufs_build_upiu_header(UfsRequest *req, uint8_t trans_type, uint8_t flags,
                            uint16_t data_segment_length);
 void ufs_build_query_response(UfsRequest *req);
 void ufs_complete_req(UfsRequest *req, UfsReqResult req_result);
+bool ufs_fit_data_buffer(UfsRequest *req, uint64_t len);
 void ufs_wb_update_avail_buffer(UfsHc *u);
 void ufs_init_wlu(UfsLu *wlu, uint8_t wlun);
 bool ufs_realize(UfsHc *u, DeviceState *dev, AddressSpace *dma_as,
diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index 3c9e9e4032..e8993e8c24 100644
--- a/tests/qtest/ufs-test.c
+++ b/tests/qtest/ufs-test.c
@@ -859,6 +859,142 @@ 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_short_prdt(void *obj, void *data, QGuestAllocator *alloc)
+{
+    QUfs *ufs = obj;
+    const uint8_t request_sense_cdb[UFS_CDB_SIZE] = {
+        REQUEST_SENSE,
+    };
+    /* Two blocks at LBA 2, and one */
+    const uint8_t write_2_cdb[UFS_CDB_SIZE] = {
+        WRITE_10, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00,
+    };
+    const uint8_t read_2_cdb[UFS_CDB_SIZE] = {
+        READ_10, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00,
+    };
+    const uint8_t write_1_cdb[UFS_CDB_SIZE] = {
+        WRITE_10, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00,
+    };
+    const uint8_t read_1_cdb[UFS_CDB_SIZE] = {
+        READ_10, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00,
+    };
+    /* Allocation length 36 */
+    const uint8_t inquiry_cdb[UFS_CDB_SIZE] = {
+        INQUIRY, 0x00, 0x00, 0x00, 0x24, 0x00,
+    };
+    uint8_t buf[4096];
+    uint8_t zero_buf[4096] = { 0 };
+    uint8_t write_buf[4096];
+    UtpUpiuRsp 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);
+    memset(write_buf, 0x5a, sizeof(write_buf));
+
+    /* A PRDT that describes less than the command moves fails */
+    ocs = ufs_send_scsi_command(ufs, 1, write_2_cdb, write_buf,
+                                sizeof(write_buf), NULL, 0, &rsp_upiu);
+    g_assert_cmpuint(ocs, ==, UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+    ocs = ufs_send_scsi_command(ufs, 1, read_2_cdb, NULL, 0, buf, sizeof(buf),
+                                &rsp_upiu);
+    g_assert_cmpuint(ocs, ==, UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+    ocs = ufs_send_scsi_command(ufs, 1, inquiry_cdb, NULL, 0, buf, 16,
+                                &rsp_upiu);
+    g_assert_cmpuint(ocs, ==, UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+
+    /* The failed WRITE wrote nothing */
+    ocs = ufs_send_scsi_command(ufs, 1, read_1_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_cmpmem(buf, sizeof(buf), zero_buf, sizeof(zero_buf));
+
+    /* A matching PRDT still works */
+    ocs = ufs_send_scsi_command(ufs, 1, write_1_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);
+    ocs = ufs_send_scsi_command(ufs, 1, read_1_cdb, NULL, 0, buf, sizeof(buf),
+                                &rsp_upiu);
+    g_assert_cmpuint(ocs, ==, UFS_OCS_SUCCESS);
+    g_assert_cmpmem(buf, sizeof(buf), write_buf, sizeof(write_buf));
+
+    ufs_exit(ufs, alloc);
+}
+
 static void ufstest_mcq_cq_wraparound(void *obj, void *data,
                                       QGuestAllocator *alloc)
 {
@@ -1577,7 +1713,8 @@ static void ufstest_hid(void *obj, void *data, QGuestAllocator *alloc)
         /* WRITE(10) to LBA 0, transfer length 8 sectors = 1 fragment */
         WRITE_10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00
     };
-    uint8_t write_buf[4096];
+    /* The PRDT must hold the eight blocks that the WRITE(10) moves */
+    uint8_t write_buf[8 * 4096];
     uint8_t read_buf[4096];
     int i;
 
@@ -1812,6 +1949,8 @@ 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("short-prdt", "ufs", ufstest_short_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] 10+ messages in thread

* [PATCH v3 4/6] docs/system: Document zoned UFS logical units
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
                     ` (2 preceding siblings ...)
  2026-10-20  0:19   ` [PATCH v3 3/6] hw/ufs: Check the PRDT size against the transfer length of the CDB Daejun Park
@ 2026-10-20  0:19   ` Daejun Park
  2026-10-20  0:21   ` [PATCH v3 5/6] hw/ufs: Support for Zoned UFS Daejun Park
                     ` (2 subsequent siblings)
  6 siblings, 0 replies; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:19 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,
	stanleyjhu@google.com, Keoseong Park, 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..a76fdbe76e 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 and WRITE AND VERIFY 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 WRITE SAME or 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] 10+ messages in thread

* [PATCH v3 5/6] hw/ufs: Support for Zoned UFS
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
                     ` (3 preceding siblings ...)
  2026-10-20  0:19   ` [PATCH v3 4/6] docs/system: Document zoned UFS logical units Daejun Park
@ 2026-10-20  0:21   ` Daejun Park
  2026-10-20 12:24     ` Stanley Jhu
  2026-10-20  0:22   ` [PATCH v3 6/6] tests/qtest: Add tests " Daejun Park
  2026-10-22  3:09   ` [PATCH v3 0/6] Support " Jeuk Kim
  6 siblings, 1 reply; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:21 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,
	stanleyjhu@google.com, Keoseong Park, 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 and WRITE AND VERIFY 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 zone of a write is opened and its write pointer moved once scsi-hd
has accepted the write, before any data moves, 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). The write pointer moves back if scsi-hd then
fails or drops the write, unless the zone changed since, for example
because a later write to it started. A write that passes the zone checks
but is refused before it starts changes no zone: the host controller
fails one whose PRDT is shorter than the write with OCS MISMATCH DATA
BUFFER SIZE, and scsi-hd fails one with a WRPROTECT field with INVALID
FIELD IN CDB. Neither keeps the write pointer of an earlier write to
that zone from moving back. 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, and so does WRITE SAME, which Linux
does not send to UFS devices (no_write_same) and which scsi-hd completes
without writing when NDOB is set. READ CAPACITY(16) reports no logical
block provisioning, the Block Limits VPD page reports no UNMAP limits,
and the Logical Block Provisioning VPD page clears LBPU, LBPWS and
LBPWS10. 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.

REPORT ZONES and the ZBC device characteristics page, which the logical
unit answers itself, follow the PRDT rule of the commands that reach
scsi-hd: a PRDT shorter than the allocation length fails with OCS
MISMATCH DATA BUFFER SIZE.

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         | 903 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 hw/ufs/trace-events |   6 +
 hw/ufs/ufs.h        |  43 +++
 include/block/ufs.h |  27 ++
 4 files changed, 971 insertions(+), 8 deletions(-)

diff --git a/hw/ufs/lu.c b/hw/ufs/lu.c
index 0c0537cfee..4fe1247547 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,307 @@ 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;
+    }
+}
+
+static UfsZoneState *ufs_find_implicit_open_zone(UfsLu *lu)
+{
+    uint32_t i;
+
+    for (i = 0; i < lu->zone_info.nr_zones; i++) {
+        if (lu->zone_array[i].cond == UFS_ZONE_COND_IMPLICIT_OPEN) {
+            return &lu->zone_array[i];
+        }
+    }
+
+    return NULL;
+}
+
+/* Close one implicitly open zone; false if there is none */
+static bool ufs_close_implicit_open_zone(UfsLu *lu)
+{
+    UfsZoneState *zone = ufs_find_implicit_open_zone(lu);
+    uint64_t seq;
+
+    if (!zone) {
+        return false;
+    }
+
+    /*
+     * Closing moves no write pointer: keep seq, so that a write in flight to
+     * this zone can still be undone.
+     */
+    seq = zone->seq;
+    ufs_assign_zone_cond(lu, zone, UFS_ZONE_COND_CLOSED);
+    zone->seq = seq;
+    return true;
+}
+
+/*
+ * 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;
+    }
+}
+
+/* Whether ufs_open_zone() would succeed, without changing anything */
+static bool ufs_can_open_zone(UfsLu *lu, UfsZoneState *zone)
+{
+    if (zone->cond != UFS_ZONE_COND_EMPTY &&
+        zone->cond != UFS_ZONE_COND_CLOSED) {
+        return true;
+    }
+
+    return !lu->zone_info.max_open ||
+           lu->zone_info.nr_open < lu->zone_info.max_open ||
+           ufs_find_implicit_open_zone(lu);
+}
+
+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;
+    }
+}
+
+/*
+ * Open the zone of a write that passed ufs_zbc_check_write() and move its
+ * write pointer, once scsi-hd has accepted the write and before any data
+ * moves. A write that scsi-hd rejects outright, for example one with a
+ * WRPROTECT field, thus leaves the zones alone. The write pointer moves
+ * before the write completes, so that queued writes to one zone pass the
+ * check in submission order.
+ */
+static void ufs_zbc_start_write(UfsLu *lu, UfsRequest *req)
+{
+    UfsZoneState *zone = req->zone_write;
+
+    if (!zone) {
+        return;
+    }
+    req->zone_write = NULL;
+
+    req->zone_cond_before = zone->cond;
+    req->zone_wp_before = zone->wp;
+
+    /*
+     * ufs_zbc_check_write() found that the zone can be opened. What ran
+     * since, scsi_req_new(), ufs_fit_data_buffer() and scsi_req_enqueue()
+     * of this request, changes no zone and does not run the event loop, so
+     * no other request completed in between.
+     */
+    if (!ufs_open_zone(lu, zone, UFS_ZONE_COND_IMPLICIT_OPEN)) {
+        g_assert_not_reached();
+    }
+    zone->wp += req->zone_write_blocks;
+    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;
+}
+
+/*
+ * If scsi-hd fails or drops a started write (an aborted or purged request,
+ * an I/O error), move the write pointer back, unless the zone changed since:
+ * a later write started, 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, and since WRITE SAME
+ * is, clear its bits in the latter. 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 and WRITE SAME are 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, LBPWS and LBPWS10 */
+        if (len > 5) {
+            buf[5] &= ~0xe0;
+        }
+        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 +474,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 +485,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 +504,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 +513,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 +543,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 +652,458 @@ 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 otherwise no more than the smaller of
+ * the allocation length and the whole report.
+ */
+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:
+        *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:
+        *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, leave it
+ * for ufs_zbc_start_write(). A write that reaches beyond the capacity or has
+ * no blocks goes to scsi-hd unchecked: scsi-hd fails the former and
+ * completes the latter 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 if (!ufs_can_open_zone(lu, zone)) {
+        *sense = SENSE_CODE(INSUFFICIENT_ZONE_RESOURCES);
+    } else {
+        req->zone_write = zone;
+        req->zone_write_blocks = len;
+        return true;
+    }
+
+    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: {
+        uint32_t alloc_len = ldl_be_p(&cdb[10]);
+        uint64_t report_len = REPORT_ZONES_HEADER_SIZE +
+                              (uint64_t)REPORT_ZONES_DESC_SIZE *
+                              lu->zone_info.nr_zones;
+        uint32_t outbuf_len;
+
+        /* As for the commands that reach scsi-hd, the PRDT must hold it */
+        if (!ufs_fit_data_buffer(req, alloc_len)) {
+            return UFS_REQUEST_MISMATCH_DATA_BUF;
+        }
+
+        /* Size the buffer by what can be returned, not by alloc_len alone */
+        outbuf_len = MIN(alloc_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;
+        }
+        if (!ufs_fit_data_buffer(req, lduw_be_p(&cdb[3]))) {
+            return UFS_REQUEST_MISMATCH_DATA_BUF;
+        }
+        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;
+    case WRITE_SAME_10:
+    case WRITE_SAME_16:
+        /*
+         * Not a UFS command (Linux sets no_write_same for UFS), and scsi-hd
+         * completes one with NDOB set without writing anything.
+         */
+        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;
@@ -387,6 +1169,17 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
     SCSIRequest *scsi_req;
     uint32_t len;
 
+    req->zone_write = NULL;
+    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
@@ -413,13 +1206,13 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
 
     /*
      * scsi-hd moves as many bytes as the SG list holds, and without an SG
-     * list it would call the transfer_data callback, which ufs-lu does not
-     * set. A UFS device moves what the CDB asks for, so fit the PRDT to the
-     * transfer length, and fail a request whose PRDT describes less, a
-     * missing PRDT included, with OCS MISMATCH DATA BUFFER SIZE, as
-     * virtio-scsi fails a buffer that does not match the command. The
-     * response reports the rest of the Expected Data Transfer Length as a
-     * residual. A request that reports a pending unit attention moves no
+     * list it would call the transfer_data callback, which only handles the
+     * INQUIRY data of a zoned LU. A UFS device moves what the CDB asks for,
+     * so fit the PRDT to the transfer length, and fail a request whose PRDT
+     * describes less, a missing PRDT included, with OCS MISMATCH DATA BUFFER
+     * SIZE, as virtio-scsi fails a buffer that does not match the command.
+     * The response reports the rest of the Expected Data Transfer Length as
+     * a residual. A request that reports a pending unit attention moves no
      * data; its sense is already set, so let it through and the guest sees
      * the UA.
      */
@@ -431,8 +1224,13 @@ static UfsReqResult ufs_process_scsi_cmd(UfsLu *lu, UfsRequest *req)
 
     req->sreq = scsi_req;
 
+    /*
+     * 0 means scsi-hd completed the command already, for example because it
+     * rejected it: only a write that it accepted starts.
+     */
     len = scsi_req_enqueue(scsi_req);
     if (len) {
+        ufs_zbc_start_write(lu, req);
         scsi_req_continue(scsi_req);
     }
 
@@ -442,6 +1240,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)
@@ -474,12 +1276,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;
@@ -527,6 +1388,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;
@@ -543,6 +1408,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;
@@ -566,12 +1432,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)
@@ -579,6 +1463,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 db9d172713..c5c148be91 100644
--- a/hw/ufs/trace-events
+++ b/hw/ufs/trace-events
@@ -59,3 +59,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 15b3a352f3..f6583a5db7 100644
--- a/hw/ufs/ufs.h
+++ b/hw/ufs/ufs.h
@@ -17,6 +17,28 @@
 #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, and by
+     * FINISH ZONE of a full zone
+     */
+    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
@@ -66,6 +88,16 @@ typedef struct UfsRequest {
     QEMUSGList *sg;
     uint32_t data_len;
 
+    /* write to a zoned LU that passed the zone checks, not yet started */
+    UfsZoneState *zone_write;
+    uint32_t zone_write_blocks;
+
+    /* write started on 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;
@@ -80,6 +112,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;
@@ -88,6 +126,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] 10+ messages in thread

* [PATCH v3 6/6] tests/qtest: Add tests for Zoned UFS
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
                     ` (4 preceding siblings ...)
  2026-10-20  0:21   ` [PATCH v3 5/6] hw/ufs: Support for Zoned UFS Daejun Park
@ 2026-10-20  0:22   ` Daejun Park
  2026-10-22  3:09   ` [PATCH v3 0/6] Support " Jeuk Kim
  6 siblings, 0 replies; 10+ messages in thread
From: Daejun Park @ 2026-10-20  0:22 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,
	stanleyjhu@google.com, Keoseong Park, 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
writes at LBA 0, to the last block of zone 2 and to the first block of
zone 3 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 neither READ CAPACITY(16) nor the Block Limits and Logical Block
Provisioning VPD pages offer UNMAP and that the latter offers no WRITE
SAME, the caching mode page, REPORT ZONES, and INQUIRY requests without
a PRDT for the standard data, which scsi-hd builds, and for the ZBC
page.

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 that a REPORT ZONES allocation length
larger than the data buffer fails with OCS MISMATCH DATA BUFFER SIZE.

zufs-write-cmds checks that WRITE(6) and WRITE(10) move the write
pointer, that WRITE SAME(10) and WRITE SAME(16) are rejected, 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, in the meantime, scsi-hd rejected another write to its zone
or a write to another zone closed its zone implicitly, but not once a
later write to its zone has started, 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, and UNMAP. It also checks that writes
refused before they start change no zone: a write without a PRDT leaves
the write pointer alone; one with a shorter PRDT and one with a
WRPROTECT field, which scsi-hd rejects, leave the write pointer and the
condition of a closed or an explicitly open zone alone; the latter
closes no implicitly open zone when all open resources are in use, and
a read that follows does not start it.

Signed-off-by: Daejun Park <daejun7.park@samsung.com>
---
 tests/qtest/ufs-test.c | 1136 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 1136 insertions(+)

diff --git a/tests/qtest/ufs-test.c b/tests/qtest/ufs-test.c
index e8993e8c24..e24caa2efc 100644
--- a/tests/qtest/ufs-test.c
+++ b/tests/qtest/ufs-test.c
@@ -1899,6 +1899,1118 @@ 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 writes at LBA 0, to
+ * the last block of zone 2 and to the first block of zone 3 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"
+/* The last block of zone 2 */
+#define EIO_ZONE2_SECTOR ((3 * ZONE_SIZE - 4096) / 512)
+/* The first block of zone 3 */
+#define EIO_ZONE3_SECTOR (3 * 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;
+}
+
+/* Condition and write pointer of the zone at zone_start */
+static uint8_t ufs_zufs_zone(QUfs *ufs, uint8_t lun, 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, 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 uint64_t ufs_zufs_write_pointer(QUfs *ufs, uint64_t zone_start)
+{
+    uint64_t wp;
+
+    ufs_zufs_zone(ufs, ZONED_TEST_LUN, zone_start, &wp);
+    return wp;
+}
+
+static uint8_t ufs_zufs_zone_cond(QUfs *ufs, uint64_t zone_start)
+{
+    uint64_t wp;
+
+    return ufs_zufs_zone(ufs, ZONED_TEST_LUN, zone_start, &wp);
+}
+
+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);
+    /* LBPU, LBPWS and LBPWS10 */
+    g_assert_cmpuint(buf[5] & 0xe0, ==, 0);
+
+    /*
+     * An INQUIRY without a PRDT fails like on a regular LU, also for the page
+     * that ufs-lu answers itself
+     */
+    g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, inquiry_cdb,
+                                           NULL, 0, NULL, 0, &rsp_upiu), ==,
+                     UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+    g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN,
+                                           inquiry_vpd_b6_cdb, NULL, 0, NULL,
+                                           0, &rsp_upiu), ==,
+                     UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+
+    /* 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 fails like a short PRDT */
+    ufs_zufs_build_report_zones(cdb, 0x00, 1024 * 1024);
+    g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, cdb, NULL, 0,
+                                           read_buf, 256, &rsp_upiu), ==,
+                     UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+
+    /* 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 at the write pointer: INVALID COMMAND OPERATION CODE */
+    memset(cdb, 0, sizeof(cdb));
+    cdb[0] = WRITE_SAME_16;
+    stq_be_p(&cdb[2], 1);
+    stl_be_p(&cdb[10], 2);
+    ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+    ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x20, 0x00);
+    memset(cdb, 0, sizeof(cdb));
+    cdb[0] = WRITE_SAME_10;
+    stl_be_p(&cdb[2], 1);
+    stw_be_p(&cdb[7], 2);
+    ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf), NULL, 0, &rsp_upiu);
+    ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x20, 0x00);
+    g_assert_cmpuint(ufs_zufs_write_pointer(ufs, 0), ==, 1);
+
+    /* 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;
+}
+
+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,
+                   zone3 = 3 * ZONE_BLOCKS;
+    uint8_t buf[4096];
+    uint8_t write_buf[4096];
+    uint8_t fill_buf[8 * 4096];
+    uint8_t cdb[UFS_CDB_SIZE];
+    UtpUpiuRsp rsp_upiu;
+    enum UtpOcsCodes ocs;
+    uint64_t wp, lba;
+    uint32_t blocks;
+    int slot, slot2;
+
+    ufs_init(ufs, alloc);
+    g_assert_true(ufs->support_mcq);
+    memset(write_buf, 0x5a, sizeof(write_buf));
+    memset(fill_buf, 0x3c, sizeof(fill_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_zone(ufs, EIO_ZONED_TEST_LUN, 0, &wp), ==, 0x2);
+    g_assert_cmpuint(wp, ==, 1);
+
+    /* A write that scsi-hd rejects leaves this one undoable */
+    ufs_zufs_build_write16(cdb, 1, 1);
+    cdb[1] = 0x20;
+    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);
+    ufs_zufs_assert_sense(&rsp_upiu, ILLEGAL_REQUEST, 0x24, 0x00);
+
+    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_zone(ufs, EIO_ZONED_TEST_LUN, 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_zone(ufs, EIO_ZONED_TEST_LUN, 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.
+     */
+    for (lba = zone2; lba < zone2 + ZONE_BLOCKS - 1; lba += blocks) {
+        blocks = MIN(8, zone2 + ZONE_BLOCKS - 1 - lba);
+        ufs_zufs_build_write16(cdb, lba, blocks);
+        ocs = ufs_send_scsi_command(ufs, EIO_ZONED_TEST_LUN, cdb, fill_buf,
+                                    blocks * 4096, 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_zone(ufs, EIO_ZONED_TEST_LUN, zone2, &wp),
+                     ==, 0x2);
+    g_assert_cmpuint(wp, ==, zone2 + ZONE_BLOCKS - 1);
+
+    qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "break write_aio B");
+    ufs_zufs_build_write16(cdb, zone2 + ZONE_BLOCKS - 1, 1);
+    slot = ufs_zufs_send_write_async(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+                                     sizeof(write_buf));
+    g_assert_cmpuint(ufs_zufs_zone(ufs, EIO_ZONED_TEST_LUN, 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_zone(ufs, EIO_ZONED_TEST_LUN, zone2, &wp),
+                     ==, 0xe);
+
+    /*
+     * A write to zone 3 fails after a later write to the zone started: the
+     * write pointer is not moved back.
+     */
+    qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "break write_aio C");
+    ufs_zufs_build_write16(cdb, zone3, 1);
+    slot = ufs_zufs_send_write_async(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+                                     sizeof(write_buf));
+    qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "break write_aio D");
+    ufs_zufs_build_write16(cdb, zone3 + 1, 1);
+    slot2 = ufs_zufs_send_write_async(ufs, EIO_ZONED_TEST_LUN, cdb, write_buf,
+                                      sizeof(write_buf));
+    qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "resume C");
+    g_assert_cmpuint(ufs_zufs_wait_async(ufs, slot, &rsp_upiu), ==,
+                     CHECK_CONDITION);
+    g_assert_cmpuint(ufs_zufs_zone(ufs, EIO_ZONED_TEST_LUN, zone3, &wp),
+                     ==, 0x2);
+    g_assert_cmpuint(wp, ==, zone3 + 2);
+    qtest_qemu_io(qts, EIO_ZONED_TEST_DRIVE, "resume D");
+    g_assert_cmpuint(ufs_zufs_wait_async(ufs, slot2, &rsp_upiu), ==, GOOD);
+    g_assert_cmpuint(ufs_zufs_zone(ufs, EIO_ZONED_TEST_LUN, zone3, &wp),
+                     ==, 0x2);
+    g_assert_cmpuint(wp, ==, zone3 + 2);
+
+    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 write_buf2[2 * 4096] = { 0 };
+    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, TEST_IMAGE_SIZE / 4096 - 1, 2);
+    ocs = ufs_send_scsi_command(ufs, ODD_ZONED_TEST_LUN, cdb, write_buf2,
+                                sizeof(write_buf2), 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 fill_buf[8 * 4096];
+    uint8_t cdb[UFS_CDB_SIZE];
+    UtpUpiuRsp rsp_upiu;
+    const uint64_t zone4 = 4 * ZONE_BLOCKS, zone5 = 5 * ZONE_BLOCKS,
+                   zone6 = 6 * ZONE_BLOCKS, zone7 = 7 * ZONE_BLOCKS;
+    uint64_t lba;
+    uint32_t blocks;
+    int i;
+
+    ufs_init(ufs, alloc);
+    ufs_zufs_clear_unit_attention(ufs);
+    memset(write_buf, 0x96, sizeof(write_buf));
+    memset(fill_buf, 0x69, sizeof(fill_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, or with one shorter than the transfer, 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_MISMATCH_DATA_BUF_SIZE);
+    ufs_zufs_build_write16(cdb, zone5, 2);
+    g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, cdb, write_buf,
+                                           sizeof(write_buf), NULL, 0,
+                                           &rsp_upiu), ==,
+                     UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+    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);
+
+    /*
+     * A write whose PRDT is shorter than the write changes no zone: a closed
+     * zone stays closed, an explicitly open one stays open.
+     */
+    ufs_zufs_build_write16(cdb, zone6, 1);
+    g_assert_cmpuint(ufs_zufs_send(ufs, cdb, write_buf, sizeof(write_buf),
+                                   NULL, 0, &rsp_upiu), ==, GOOD);
+    ufs_zufs_build_zbc_out(cdb, UFS_ZBC_CLOSE_ZONE, zone6, false);
+    g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+                     GOOD);
+    ufs_zufs_build_write16(cdb, zone6 + 1, 2);
+    g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, cdb, write_buf,
+                                           sizeof(write_buf), NULL, 0,
+                                           &rsp_upiu), ==,
+                     UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+    g_assert_cmpuint(ufs_zufs_zone_cond(ufs, zone6), ==, 0x4);
+    g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone6), ==, zone6 + 1);
+    g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x02), ==, 0);
+
+    ufs_zufs_build_zbc_out(cdb, UFS_ZBC_OPEN_ZONE, zone7, false);
+    g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, NULL, 0, &rsp_upiu), ==,
+                     GOOD);
+    ufs_zufs_build_write16(cdb, zone7, ZONE_BLOCKS);
+    g_assert_cmpuint(ufs_send_scsi_command(ufs, ZONED_TEST_LUN, cdb, write_buf,
+                                           sizeof(write_buf), NULL, 0,
+                                           &rsp_upiu), ==,
+                     UFS_OCS_MISMATCH_DATA_BUF_SIZE);
+    g_assert_cmpuint(ufs_zufs_zone_cond(ufs, zone7), ==, 0x3);
+    g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone7), ==, zone7);
+
+    /*
+     * Nor does a write that scsi-hd rejects, here for its WRPROTECT field:
+     * the closed zone stays closed, the explicitly open one that the write
+     * would fill stays open, and no implicitly open zone is closed to make
+     * room for the zone of the write.
+     */
+    ufs_zufs_build_write16(cdb, zone6 + 1, 1);
+    cdb[1] = 0x20;
+    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 read that follows does not start the rejected write either */
+    memset(cdb, 0, sizeof(cdb));
+    cdb[0] = READ_16;
+    stq_be_p(&cdb[2], zone6);
+    stl_be_p(&cdb[10], 1);
+    g_assert_cmpuint(ufs_zufs_send(ufs, cdb, NULL, 0, fill_buf, 4096,
+                                   &rsp_upiu), ==, GOOD);
+    g_assert_cmpuint(ufs_zufs_zone_cond(ufs, zone6), ==, 0x4);
+    g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone6), ==, zone6 + 1);
+    g_assert_cmpuint(ufs_zufs_count_zones(ufs, 0x02), ==, 0);
+
+    for (lba = zone7; lba < zone7 + ZONE_BLOCKS - 1; lba += blocks) {
+        blocks = MIN(8, zone7 + ZONE_BLOCKS - 1 - lba);
+        ufs_zufs_build_write16(cdb, lba, blocks);
+        g_assert_cmpuint(ufs_zufs_send(ufs, cdb, fill_buf, blocks * 4096, NULL,
+                                       0, &rsp_upiu), ==, GOOD);
+    }
+    ufs_zufs_build_write16(cdb, zone7 + ZONE_BLOCKS - 1, 1);
+    cdb[1] = 0x20;
+    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_zone_cond(ufs, zone7), ==, 0x3);
+    g_assert_cmpuint(ufs_zufs_write_pointer(ufs, zone7), ==,
+                     zone7 + ZONE_BLOCKS - 1);
+
+    for (i = 0; i < MAX_OPEN_ZONE - 1; i++) {
+        ufs_zufs_build_write16(cdb, (8 + 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_write16(cdb, (8 + i) * ZONE_BLOCKS, 1);
+    cdb[1] = 0x20;
+    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_count_zones(ufs, 0x02), ==, MAX_OPEN_ZONE - 1);
+    g_assert_cmpuint(ufs_zufs_zone_cond(ufs, (8 + i) * ZONE_BLOCKS), ==, 0x1);
+
+    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,"
+                           "file.inject-error.2.event=none,"
+                           "file.inject-error.2.iotype=write,"
+                           "file.inject-error.2.errno=5,"
+                           "file.inject-error.2.sector=%d,"
+                           "file.inject-error.2.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_ZONE3_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;
@@ -1921,6 +3033,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) });
@@ -1962,6 +3080,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] 10+ messages in thread

* Re: [PATCH v3 3/6] hw/ufs: Check the PRDT size against the transfer length of the CDB
  2026-10-20  0:19   ` [PATCH v3 3/6] hw/ufs: Check the PRDT size against the transfer length of the CDB Daejun Park
@ 2026-10-20 12:24     ` Stanley Jhu
  0 siblings, 0 replies; 10+ messages in thread
From: Stanley Jhu @ 2026-10-20 12:24 UTC (permalink / raw)
  To: qemu-devel, Daejun Park
  Cc: Jeuk Kim, farosas, lvivier, pbonzini, kwolf, hreitz, qemu-block,
	pierrick.bouvier, Keoseong Park

Reviewed-by: Stanley Jhu <stanleyjhu@google.com>


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3 5/6] hw/ufs: Support for Zoned UFS
  2026-10-20  0:21   ` [PATCH v3 5/6] hw/ufs: Support for Zoned UFS Daejun Park
@ 2026-10-20 12:24     ` Stanley Jhu
  0 siblings, 0 replies; 10+ messages in thread
From: Stanley Jhu @ 2026-10-20 12:24 UTC (permalink / raw)
  To: qemu-devel, Daejun Park
  Cc: Jeuk Kim, farosas, lvivier, pbonzini, kwolf, hreitz, qemu-block,
	pierrick.bouvier, Keoseong Park

Reviewed-by: Stanley Jhu <stanleyjhu@google.com>


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH v3 0/6] Support for Zoned UFS
  2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
                     ` (5 preceding siblings ...)
  2026-10-20  0:22   ` [PATCH v3 6/6] tests/qtest: Add tests " Daejun Park
@ 2026-10-22  3:09   ` Jeuk Kim
  6 siblings, 0 replies; 10+ messages in thread
From: Jeuk Kim @ 2026-10-22  3:09 UTC (permalink / raw)
  To: qemu-devel
  Cc: Jeuk Kim, farosas, lvivier, pbonzini, kwolf, hreitz, qemu-block,
	pierrick.bouvier, stanleyjhu, Keoseong Park, qemu-stable,
	Daejun Park, j-young.choi

On Tue, 20 Oct 2026 09:16:47 +0900, Daejun Park <daejun7.park@samsung.com> wrote:
> Support for Zoned UFS
> 
> 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.
> 
> [...]

Reviewed-by: Jeuk Kim <jeuk20.kim@samsung.com>

-- 
Jeuk Kim <jeuk20.kim@samsung.com>


^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-10-22  3:10 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <CGME20261020001647epcms2p52cd58173164a43e252c33a3eb5935dc8@epcms2p5>
2026-10-20  0:16 ` [PATCH v3 0/6] Support for Zoned UFS Daejun Park
2026-10-20  0:17   ` [PATCH v3 1/6] hw/ufs: Fail SCSI commands that move data without a PRDT Daejun Park
2026-10-20  0:18   ` [PATCH v3 2/6] hw/ufs: Pass SYNCHRONIZE CACHE(16) to scsi-hd as SYNCHRONIZE CACHE(10) Daejun Park
2026-10-20  0:19   ` [PATCH v3 3/6] hw/ufs: Check the PRDT size against the transfer length of the CDB Daejun Park
2026-10-20 12:24     ` Stanley Jhu
2026-10-20  0:19   ` [PATCH v3 4/6] docs/system: Document zoned UFS logical units Daejun Park
2026-10-20  0:21   ` [PATCH v3 5/6] hw/ufs: Support for Zoned UFS Daejun Park
2026-10-20 12:24     ` Stanley Jhu
2026-10-20  0:22   ` [PATCH v3 6/6] tests/qtest: Add tests " Daejun Park
2026-10-22  3:09   ` [PATCH v3 0/6] Support " Jeuk Kim

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.