* [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver
@ 2026-10-26 12:12 Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 1/7] docs/qcow2: add the zoned format feature Niklas Cassel
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Niklas Cassel, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Sam Li
This patch series adds a new extension - zoned format - to the
qcow2 driver, allowing full zoned storage emulation on a qcow2
image file. A user can attach such an image to a guest and have
it appear as a host-managed zoned block device.
The zoned format is opt-in through a new qcow2 header extension
that pins the zone geometry. Behind the extension is a dedicated
zoned metadata region that stores one 8-byte write pointer (WP)
per zone. The extension is gated by an incompatible bit, so an
older qcow2 implementation cannot accidentally open the image.
Each write pointer is routed through the write pointer cache,
a Qcow2Cache object. The write pointer cache is written to disk
after the qcow2 metadata is written, thus guaranteeing that
the write pointer is updated after the corresponding data is
written.
Zone states are in memory. Read-only and offline states are
device-internal events, which are not modelled in qcow2
emulation for simplicity. The other zone states
(closed, empty, full) can be inferred from write pointer
values, persistent across QEMU reboots. The open states are
kept in memory using open zone lists.
To create a qcow2 file with the zoned format:
qemu-img create -f qcow2 zbc.qcow2 \
-o size=768M \
-o zone.size=64M \
-o zone.capacity=64M \
-o zone.conventional_zones=0 \
-o zone.max_open_zones=6 \
-o zone.max_active_zones=8 \
-o zone.mode=host-managed
Then attach it to a guest via the QEMU command line:
-blockdev node-name=drive1,driver=qcow2,file.driver=file,file.filename=zbc.qcow2 \
-device virtio-blk-pci,drive=drive1 \
Tested with blktests, fio, zbd_state (an internal test suite) and
zonefs, comparing a zoned qcow2 image attached via virtio-blk with
zloop, null_blk and scsi_debug (passed/failed/skipped):
+--------------------+----------+---------+-----------+---------+
| Testrun | blktests | fio | zbd_state | zonefs |
+--------------------+----------+---------+-----------+---------+
| zloop | 9/0/10 | 57/0/18 | 29/0/8 | 113/0/0 |
| null_blk | 9/0/10 | 57/0/18 | 29/0/8 | 113/0/0 |
| scsi_debug | 10/0/12 | 61/0/14 | 29/0/8 | 113/0/0 |
| virtio-blk + qcow2 | 9/0/10 | 57/0/18 | 29/0/8 | 113/0/0 |
+--------------------+----------+---------+-----------+---------+
This series depends on the (still not merged) series "block: fix the
zone write granularity and the zone append limit":
https://lore.kernel.org/qemu-devel/20260921142819.2916043-1-cassel@kernel.org/
Git branch which includes the dependency series can be found at:
https://github.com/floatious/qemu/tree/qcow2-v15
Based-on: <20260921142819.2916043-1-cassel@kernel.org>
v14->v15:
- rebase on the series above, which now contains the v14 patches
"block: widen BlockLimits.zone_size to uint64_t" and "virtio-blk: do
not merge writes across a zone boundary", and which moves the zone
append checks to the block layer
- drop zone.max_append_bytes; zonedmeta_offset moves to bytes 24-31 of
the zoned header extension, so images created with v14 are not
compatible
- require a version 3 image, a zone size that is a multiple of the
cluster size, and a zone capacity that is a multiple of the zone
write granularity
- report zone errors with dedicated errnos (new patch) instead of EIO,
EINVAL and EBUSY
- manage the zone resources as in NVMe ZNS (2.1.1.4.1) and ZBC-3, and
fix OPEN ZONE of a full zone, writes that fill a closed zone, writes
from a conventional into a sequential zone, conventional zone and
zone capacity reporting, and a shorter last zone
- validate the zoned header extension, and the write pointer table (new
patch), when opening an image
- account for the zoned metadata in qemu-img check
- report zone errors with their zone status codes in virtio-blk (new
patch)
- extend the iotest with zone errors, zone resources and image checks
Niklas Cassel (3):
block: define errnos for zone protocol errors
qcow2: validate the write pointer table when opening an image
virtio-blk: report zone errors with their zone status codes
Sam Li (4):
docs/qcow2: add the zoned format feature
qcow2: add configurations for zoned format extension
qcow2: add zoned emulation capability
iotests: test the zoned format feature for qcow2 file
MAINTAINERS | 1 +
block/io.c | 13 +-
block/qcow2-cache.c | 11 +-
block/qcow2-refcount.c | 18 +
block/qcow2.c | 1674 ++++++++++++++++-
block/qcow2.h | 86 +-
block/trace-events | 2 +
docs/interop/qcow2.rst | 115 +-
docs/system/qemu-block-drivers.rst.inc | 44 +
hw/block/block.c | 7 +
hw/block/virtio-blk.c | 48 +-
include/block/block-common.h | 16 +
include/block/block-io.h | 6 +
include/block/block_int-common.h | 12 +
include/qemu/osdep.h | 3 +
qapi/block-core.json | 86 +-
tests/qemu-iotests/031.out | 8 +-
tests/qemu-iotests/036.out | 4 +-
tests/qemu-iotests/061.out | 14 +-
tests/qemu-iotests/082.out | 108 ++
tests/qemu-iotests/303.out | 4 +-
tests/qemu-iotests/tests/qcow2-encryption.out | 4 +-
tests/qemu-iotests/tests/zoned-qcow2 | 362 ++++
tests/qemu-iotests/tests/zoned-qcow2.out | 265 +++
24 files changed, 2874 insertions(+), 37 deletions(-)
create mode 100755 tests/qemu-iotests/tests/zoned-qcow2
create mode 100644 tests/qemu-iotests/tests/zoned-qcow2.out
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v15 1/7] docs/qcow2: add the zoned format feature
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 2/7] qcow2: add configurations for zoned format extension Niklas Cassel
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Sam Li, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Niklas Cassel
From: Sam Li <faithilikerun@gmail.com>
Add the specs for the zoned format feature of the qcow2 driver.
The qcow2 file then can emulate real zoned devices, either passed
through by virtio-blk device or NVMe ZNS drive to the guest
given zoned information.
Signed-off-by: Sam Li <faithilikerun@gmail.com>
Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
Reviewed-by: Niklas Cassel <cassel@kernel.org>
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
Changes by Niklas Cassel:
- do not document a zone append size limit, which the driver no longer
stores in the image
---
docs/system/qemu-block-drivers.rst.inc | 44 ++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/docs/system/qemu-block-drivers.rst.inc b/docs/system/qemu-block-drivers.rst.inc
index 675daa72f9..70b9b8d1b4 100644
--- a/docs/system/qemu-block-drivers.rst.inc
+++ b/docs/system/qemu-block-drivers.rst.inc
@@ -172,6 +172,50 @@ This section describes each format and the options that are supported for it.
filename`` to check if the NOCOW flag is set or not (Capital 'C' is
NOCOW flag).
+ .. option:: zone.mode
+
+ If this is set to ``host-managed``, the image is an emulated zoned
+ block device. This option is only valid for emulated zoned device files.
+
+ .. option:: zone.size
+
+ The size of a zone in bytes. The device is divided into zones of this
+ size with the exception of the last zone, which may be smaller.
+ Defaults to ``256 MiB``.
+
+ .. option:: zone.capacity
+
+ The initial capacity value, in bytes, for all zones. The capacity must
+ be less than or equal to zone size. If the last zone is smaller, then
+ its capacity is capped. Defaults to ``zone.size``.
+
+ The zone capacity is per zone and may be different between zones in real
+ devices. QCow2 sets all zones to the same capacity.
+
+ .. option:: zone.conventional_zones
+
+ The number of conventional zones of the zoned device. Defaults to ``0``
+ (all zones are sequential-write-required).
+
+ .. option:: zone.max_active_zones
+
+ The maximum allowed active zones (zones in the implicit open, explicit
+ open, or closed state).
+
+ The max active zones must be less or equal to the number of SWR
+ (sequential write required) zones of the device. Defaults to ``0``,
+ meaning no limit.
+
+ .. option:: zone.max_open_zones
+
+ The maximum allowed open zones (zones in the implicit open or explicit
+ open state). The max open zones must not be larger than the max active
+ zones. Defaults to ``zone.max_active_zones`` if that is set, otherwise
+ ``0`` (no limit).
+
+ If the limits of open zones or active zones are equal to the number of
+ SWR zones, then it is the same as having no limits.
+
.. program:: image-formats
.. option:: qed
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v15 2/7] qcow2: add configurations for zoned format extension
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 1/7] docs/qcow2: add the zoned format feature Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 3/7] block: define errnos for zone protocol errors Niklas Cassel
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Sam Li, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Niklas Cassel
From: Sam Li <faithilikerun@gmail.com>
To configure the zoned format feature on the qcow2 driver, it
requires settings as: the device size, zone model, zone size,
zone capacity, number of conventional zones, limits on zone
resources (max open zones and max active zones).
To create a qcow2 image with zoned format feature, use command like
this:
qemu-img create -f qcow2 zbc.qcow2 -o size=768M \
-o zone.size=64M -o zone.capacity=64M -o zone.conventional_zones=0 \
-o zone.max_open_zones=6 -o zone.max_active_zones=8 \
-o zone.mode=host-managed
Signed-off-by: Sam Li <faithilikerun@gmail.com>
Acked-by: Markus Armbruster <armbru@redhat.com> # QAPI schema
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
Changes by Niklas Cassel:
- validate the zoned extension header before it becomes the driver state,
so that the clamped resource limits are the ones that take effect
- drop the zone append size limit from the header, the QAPI schema and the
format documentation; it described a limit on an operation that the
driver derives from the geometry instead
- do not report a zone write granularity; the block layer already requires
an append to be a multiple of the sector size, and a coarser granularity
belongs to a driver that has a medium with one
- take the zone capacity into account, as bdrv_zone_writable_end(), which
bdrv_zone_is_full() then uses
- require the zone size to be a multiple of the cluster size, so that
a zone never shares a cluster with another zone, and resetting it can
zero its clusters in place
- require version 3 for a zoned image, as the zoned format is an
incompatible feature, which a version 2 header cannot express; refuse
a zoned extension without the incompatible feature bit when opening
---
block/io.c | 13 +-
block/qcow2.c | 363 +++++++++++++++++-
block/qcow2.h | 84 +++-
docs/interop/qcow2.rst | 115 +++++-
hw/block/block.c | 7 +
include/block/block-io.h | 6 +
include/block/block_int-common.h | 12 +
qapi/block-core.json | 86 ++++-
tests/qemu-iotests/031.out | 8 +-
tests/qemu-iotests/036.out | 4 +-
tests/qemu-iotests/061.out | 14 +-
tests/qemu-iotests/082.out | 108 ++++++
tests/qemu-iotests/303.out | 4 +-
tests/qemu-iotests/tests/qcow2-encryption.out | 4 +-
14 files changed, 801 insertions(+), 27 deletions(-)
diff --git a/block/io.c b/block/io.c
index 06b1fc13b9..6e33da7f23 100644
--- a/block/io.c
+++ b/block/io.c
@@ -3400,13 +3400,20 @@ bool bdrv_zone_is_conv(BlockDriverState *bs, uint32_t index)
return BDRV_ZT_IS_CONV(bs->wps->wp[index]);
}
+uint64_t bdrv_zone_writable_end(BlockDriverState *bs, uint32_t index)
+{
+ uint64_t writable = MIN_NON_ZERO(bs->bl.zone_capacity, bs->bl.zone_size);
+ IO_CODE();
+
+ return MIN((uint64_t)index * bs->bl.zone_size + writable,
+ (uint64_t)bs->total_sectors << BDRV_SECTOR_BITS);
+}
+
bool bdrv_zone_is_full(BlockDriverState *bs, uint32_t index)
{
- uint64_t zone_end = MIN((uint64_t)(index + 1) * bs->bl.zone_size,
- (uint64_t)bs->total_sectors << BDRV_SECTOR_BITS);
IO_CODE();
- return bs->wps->wp[index] >= zone_end;
+ return bs->wps->wp[index] >= bdrv_zone_writable_end(bs, index);
}
void *qemu_blockalign(BlockDriverState *bs, size_t size)
diff --git a/block/qcow2.c b/block/qcow2.c
index 0358bc04f3..01215bcf9b 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -73,6 +73,7 @@ typedef struct {
#define QCOW2_EXT_MAGIC_CRYPTO_HEADER 0x0537be77
#define QCOW2_EXT_MAGIC_BITMAPS 0x23852875
#define QCOW2_EXT_MAGIC_DATA_FILE 0x44415441
+#define QCOW2_EXT_MAGIC_ZONED_FORMAT 0x007a6264
static int coroutine_fn
qcow2_co_preadv_compressed(BlockDriverState *bs,
@@ -194,6 +195,90 @@ qcow2_extract_crypto_opts(QemuOpts *opts, const char *fmt, Error **errp)
return cryptoopts_qdict;
}
+/*
+ * Returns true if zone_opt is valid for an image with clusters of
+ * @cluster_size bytes, false otherwise.
+ */
+static bool
+qcow2_check_zone_options(Qcow2ZonedHeaderExtension *zone_opt,
+ uint32_t cluster_size)
+{
+ uint32_t sequential_zones;
+
+ assert(zone_opt != NULL);
+
+ if (zone_opt->zoned != QCOW2_Z_NONE && zone_opt->zoned != QCOW2_Z_HM) {
+ warn_report("Zoned extension header zoned field has unknown "
+ "value %" PRIu8, zone_opt->zoned);
+ return false;
+ }
+
+ if (zone_opt->zone_size == 0) {
+ warn_report("Zoned extension header zone_size field must not be 0");
+ return false;
+ }
+
+ if (!is_power_of_2(zone_opt->zone_size)) {
+ warn_report("Zoned extension header zone_size %" PRIu64
+ "B is not a power of 2", zone_opt->zone_size);
+ return false;
+ }
+
+ /* A zone consists of whole clusters, which it shares with no other zone */
+ if (zone_opt->zone_size % cluster_size) {
+ warn_report("Zoned extension header zone_size %" PRIu64
+ "B is not a multiple of the cluster size %" PRIu32 "B",
+ zone_opt->zone_size, cluster_size);
+ return false;
+ }
+
+ if (zone_opt->nr_zones > QCOW2_MAX_NR_ZONES) {
+ warn_report("Zoned extension header nr_zones %" PRIu32
+ " exceeds maximum %u",
+ zone_opt->nr_zones, QCOW2_MAX_NR_ZONES);
+ return false;
+ }
+
+ if (zone_opt->zone_capacity > zone_opt->zone_size) {
+ warn_report("zone capacity %" PRIu64 "B exceeds zone size "
+ "%" PRIu64 "B", zone_opt->zone_capacity,
+ zone_opt->zone_size);
+ return false;
+ }
+
+ if (zone_opt->conventional_zones >= zone_opt->nr_zones) {
+ warn_report("Conventional_zones %" PRIu32 " exceeds "
+ "nr_zones %" PRIu32 ".",
+ zone_opt->conventional_zones, zone_opt->nr_zones);
+ return false;
+ }
+
+ if (zone_opt->max_active_zones > zone_opt->nr_zones) {
+ warn_report("max_active_zones %" PRIu32 " exceeds nr_zones %" PRIu32
+ ", clamping to nr_zones",
+ zone_opt->max_active_zones, zone_opt->nr_zones);
+ zone_opt->max_active_zones = zone_opt->nr_zones;
+ }
+
+ sequential_zones = zone_opt->nr_zones - zone_opt->conventional_zones;
+ if (zone_opt->max_open_zones > sequential_zones) {
+ warn_report("max_open_zones %" PRIu32 " exceeds the number of SWR "
+ "zones, clamping to %" PRIu32,
+ zone_opt->max_open_zones, sequential_zones);
+ zone_opt->max_open_zones = sequential_zones;
+ }
+ if (zone_opt->max_active_zones != 0 &&
+ zone_opt->max_open_zones > zone_opt->max_active_zones) {
+ warn_report("max_open_zones %" PRIu32 " exceeds max_active_zones "
+ "%" PRIu32 ", clamping to max_active_zones",
+ zone_opt->max_open_zones,
+ zone_opt->max_active_zones);
+ zone_opt->max_open_zones = zone_opt->max_active_zones;
+ }
+
+ return true;
+}
+
/*
* read qcow2 extension and fill bs
* start reading from start_offset
@@ -211,6 +296,7 @@ qcow2_read_extensions(BlockDriverState *bs, uint64_t start_offset,
uint64_t offset;
int ret;
Qcow2BitmapHeaderExt bitmaps_ext;
+ Qcow2ZonedHeaderExtension zoned_ext;
if (need_update_header != NULL) {
*need_update_header = false;
@@ -432,6 +518,96 @@ qcow2_read_extensions(BlockDriverState *bs, uint64_t start_offset,
break;
}
+ case QCOW2_EXT_MAGIC_ZONED_FORMAT:
+ {
+ if (ext.len != sizeof(zoned_ext)) {
+ error_setg(errp, "zoned_ext: unexpected len=%" PRIu32 " "
+ "(expected %zu)", ext.len, sizeof(zoned_ext));
+ return -EINVAL;
+ }
+ ret = bdrv_pread(bs->file, offset, ext.len, &zoned_ext, 0);
+ if (ret < 0) {
+ error_setg_errno(errp, -ret, "zoned_ext: "
+ "Could not read ext header");
+ return ret;
+ }
+
+ zoned_ext.zone_size = be64_to_cpu(zoned_ext.zone_size);
+ zoned_ext.zone_capacity = be64_to_cpu(zoned_ext.zone_capacity);
+ zoned_ext.conventional_zones =
+ be32_to_cpu(zoned_ext.conventional_zones);
+ zoned_ext.nr_zones = be32_to_cpu(zoned_ext.nr_zones);
+ zoned_ext.max_open_zones = be32_to_cpu(zoned_ext.max_open_zones);
+ zoned_ext.max_active_zones =
+ be32_to_cpu(zoned_ext.max_active_zones);
+ /*
+ * A zoned image has the zoned incompatible feature bit set,
+ * which a version 2 image cannot have.
+ */
+ if (zoned_ext.zoned == QCOW2_Z_HM &&
+ !(s->incompatible_features & QCOW2_INCOMPAT_ZONED_FORMAT)) {
+ error_setg(errp, "Zoned extension header without the zoned "
+ "incompatible feature bit");
+ return -EINVAL;
+ }
+
+ /*
+ * Validate the header before it becomes the driver state.
+ * qcow2_check_zone_options() also clamps the zone resource
+ * limits, so the driver has to pick up the checked copy.
+ */
+ if (!qcow2_check_zone_options(&zoned_ext, s->cluster_size)) {
+ error_setg(errp, "Invalid zoned extension header");
+ return -EINVAL;
+ }
+ s->zoned_header = zoned_ext;
+
+ /*
+ * refuse to open broken images: reject untrusted total_sectors
+ * that would overflow when converted to bytes, then verify
+ * nr_zones matches the device size.
+ */
+ if (bs->total_sectors < 0 ||
+ (uint64_t)bs->total_sectors > UINT64_MAX / BDRV_SECTOR_SIZE) {
+ error_setg(errp, "Image size overflows when converted "
+ "to bytes");
+ return -EINVAL;
+ }
+ if (zoned_ext.nr_zones != DIV_ROUND_UP(
+ (uint64_t)bs->total_sectors * BDRV_SECTOR_SIZE,
+ zoned_ext.zone_size)) {
+ error_setg(errp, "Zoned extension header nr_zones field "
+ "is wrong");
+ return -EINVAL;
+ }
+
+ /*
+ * Reject a header that claims more zones than the file can hold.
+ */
+ {
+ int64_t file_length = bdrv_co_getlength(bs->file->bs);
+ uint64_t wp_table_size =
+ (uint64_t)zoned_ext.nr_zones * sizeof(uint64_t);
+
+ if (file_length < 0) {
+ return file_length;
+ }
+ if (zoned_ext.zonedmeta_offset > (uint64_t)file_length ||
+ (uint64_t)file_length - zoned_ext.zonedmeta_offset <
+ wp_table_size) {
+ error_setg(errp, "Zoned metadata region exceeds the "
+ "image file size");
+ return -EINVAL;
+ }
+ }
+
+#ifdef DEBUG_EXT
+ printf("Qcow2: Got zoned format extension: "
+ "offset=%" PRIu64 "\n", offset);
+#endif
+ break;
+ }
+
default:
/* unknown magic - save it in case we need to rewrite the header */
/* If you add a new feature, make sure to also update the fast
@@ -2076,6 +2252,22 @@ static void qcow2_refresh_limits(BlockDriverState *bs, Error **errp)
}
bs->bl.pwrite_zeroes_alignment = s->subcluster_size;
bs->bl.pdiscard_alignment = s->cluster_size;
+
+ switch (s->zoned_header.zoned) {
+ case QCOW2_Z_HM:
+ bs->bl.zoned = BLK_Z_HM;
+ break;
+ case QCOW2_Z_NONE:
+ default:
+ bs->bl.zoned = BLK_Z_NONE;
+ break;
+ }
+
+ bs->bl.nr_zones = s->zoned_header.nr_zones;
+ bs->bl.max_active_zones = s->zoned_header.max_active_zones;
+ bs->bl.max_open_zones = s->zoned_header.max_open_zones;
+ bs->bl.zone_size = s->zoned_header.zone_size;
+ bs->bl.zone_capacity = s->zoned_header.zone_capacity;
}
static int GRAPH_UNLOCKED
@@ -3184,6 +3376,11 @@ int qcow2_update_header(BlockDriverState *bs)
.bit = QCOW2_INCOMPAT_EXTL2_BITNR,
.name = "extended L2 entries",
},
+ {
+ .type = QCOW2_FEAT_TYPE_INCOMPATIBLE,
+ .bit = QCOW2_INCOMPAT_ZONED_FORMAT_BITNR,
+ .name = "zoned format",
+ },
{
.type = QCOW2_FEAT_TYPE_COMPATIBLE,
.bit = QCOW2_COMPAT_LAZY_REFCOUNTS_BITNR,
@@ -3229,6 +3426,29 @@ int qcow2_update_header(BlockDriverState *bs)
buflen -= ret;
}
+ /* Zoned devices header extension */
+ if (s->zoned_header.zoned == QCOW2_Z_HM) {
+ Qcow2ZonedHeaderExtension zoned_header = {
+ .zoned = s->zoned_header.zoned,
+ .zone_size = cpu_to_be64(s->zoned_header.zone_size),
+ .zone_capacity = cpu_to_be64(s->zoned_header.zone_capacity),
+ .conventional_zones =
+ cpu_to_be32(s->zoned_header.conventional_zones),
+ .nr_zones = cpu_to_be32(s->zoned_header.nr_zones),
+ .max_open_zones = cpu_to_be32(s->zoned_header.max_open_zones),
+ .max_active_zones =
+ cpu_to_be32(s->zoned_header.max_active_zones),
+ };
+ ret = header_ext_add(buf, QCOW2_EXT_MAGIC_ZONED_FORMAT,
+ &zoned_header, sizeof(zoned_header),
+ buflen);
+ if (ret < 0) {
+ goto fail;
+ }
+ buf += ret;
+ buflen -= ret;
+ }
+
/* Keep unknown header extensions */
QLIST_FOREACH(uext, &s->unknown_header_ext, next) {
ret = header_ext_add(buf, uext->magic, uext->data, uext->len, buflen);
@@ -3603,6 +3823,8 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
ERRP_GUARD();
BlockdevCreateOptionsQcow2 *qcow2_opts;
QDict *options;
+ Qcow2ZoneCreateOptions *zone_struct;
+ Qcow2ZoneHostManaged *zone_host_managed;
/*
* Open the image file and write a minimal qcow2 header.
@@ -3629,6 +3851,8 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
assert(create_options->driver == BLOCKDEV_DRIVER_QCOW2);
qcow2_opts = &create_options->u.qcow2;
+ zone_struct = qcow2_opts->zone;
+ zone_host_managed = NULL;
bs = bdrv_co_open_blockdev_ref(qcow2_opts->file, errp);
if (bs == NULL) {
@@ -3677,6 +3901,15 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
}
}
+ /* The zoned format is an incompatible feature, which needs version 3 */
+ if (zone_struct && zone_struct->mode == QCOW2_ZONE_MODEL_HOST_MANAGED &&
+ version < 3) {
+ error_setg(errp, "Zoned images are only supported with compatibility "
+ "level 1.1 and above (use version=v3 or greater)");
+ ret = -EINVAL;
+ goto out;
+ }
+
if (!validate_cluster_size(cluster_size, qcow2_opts->extended_l2, errp)) {
ret = -EINVAL;
goto out;
@@ -3842,6 +4075,14 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
header->incompatible_features |=
cpu_to_be64(QCOW2_INCOMPAT_DATA_FILE);
}
+ if (zone_struct && zone_struct->mode == QCOW2_ZONE_MODEL_HOST_MANAGED) {
+ /*
+ * The incompatible bit must be set when the zone model is
+ * host-managed
+ */
+ header->incompatible_features |=
+ cpu_to_be64(QCOW2_INCOMPAT_ZONED_FORMAT);
+ }
if (qcow2_opts->data_file_raw) {
header->autoclear_features |=
cpu_to_be64(QCOW2_AUTOCLEAR_DATA_FILE_RAW);
@@ -3899,10 +4140,9 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
bdrv_graph_co_rdlock();
ret = qcow2_alloc_clusters(blk_bs(blk), 3 * cluster_size);
if (ret < 0) {
- bdrv_graph_co_rdunlock();
error_setg_errno(errp, -ret, "Could not allocate clusters for qcow2 "
"header and refcount table");
- goto out;
+ goto unlock;
} else if (ret != 0) {
error_report("Huh, first cluster in empty image is already in use?");
@@ -3910,11 +4150,71 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
}
/* Set the external data file if necessary */
+ BDRVQcow2State *s = blk_bs(blk)->opaque;
if (data_bs) {
- BDRVQcow2State *s = blk_bs(blk)->opaque;
s->image_data_file = g_strdup(data_bs->filename);
}
+ if (zone_struct && zone_struct->mode == QCOW2_ZONE_MODEL_HOST_MANAGED) {
+ s->zoned_header.zoned = QCOW2_Z_HM;
+ zone_host_managed = &zone_struct->u.host_managed;
+
+ if (zone_host_managed->has_size) {
+ s->zoned_header.zone_size = zone_host_managed->size;
+ } else {
+ s->zoned_header.zone_size = DEFAULT_ZONE_SIZE;
+ }
+
+ if (s->zoned_header.zone_size == 0) {
+ error_setg(errp, "Zoned extension header zone_size field "
+ "can not be 0");
+ s->zoned_header.zoned = QCOW2_Z_NONE;
+ ret = -EINVAL;
+ goto unlock;
+ }
+ s->zoned_header.nr_zones = DIV_ROUND_UP(qcow2_opts->size,
+ s->zoned_header.zone_size);
+
+ if (zone_host_managed->has_capacity) {
+ s->zoned_header.zone_capacity = zone_host_managed->capacity;
+ } else {
+ s->zoned_header.zone_capacity = s->zoned_header.zone_size;
+ }
+
+ if (zone_host_managed->has_conventional_zones) {
+ s->zoned_header.conventional_zones =
+ zone_host_managed->conventional_zones;
+ } else {
+ s->zoned_header.conventional_zones = 0;
+ }
+
+ if (zone_host_managed->has_max_active_zones) {
+ s->zoned_header.max_active_zones =
+ zone_host_managed->max_active_zones;
+ } else {
+ s->zoned_header.max_active_zones = 0;
+ }
+
+ if (zone_host_managed->has_max_open_zones) {
+ s->zoned_header.max_open_zones =
+ zone_host_managed->max_open_zones;
+ } else if (zone_host_managed->has_max_active_zones) {
+ s->zoned_header.max_open_zones =
+ zone_host_managed->max_active_zones;
+ } else {
+ s->zoned_header.max_open_zones = 0;
+ }
+
+ if (!qcow2_check_zone_options(&s->zoned_header, s->cluster_size)) {
+ error_setg(errp, "Invalid zoned device options");
+ s->zoned_header.zoned = QCOW2_Z_NONE;
+ ret = -EINVAL;
+ goto unlock;
+ }
+ } else {
+ s->zoned_header.zoned = QCOW2_Z_NONE;
+ }
+
/* Create a full header (including things like feature table) */
ret = qcow2_update_header(blk_bs(blk));
bdrv_graph_co_rdunlock();
@@ -3988,6 +4288,9 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
}
ret = 0;
+ goto out;
+unlock:
+ bdrv_graph_co_rdunlock();
out:
blk_co_unref(blk);
bdrv_co_unref(bs);
@@ -4066,6 +4369,9 @@ qcow2_co_create_opts(BlockDriver *drv, const char *filename, QemuOpts *opts,
{ BLOCK_OPT_COMPAT_LEVEL, "version" },
{ BLOCK_OPT_DATA_FILE_RAW, "data-file-raw" },
{ BLOCK_OPT_COMPRESSION_TYPE, "compression-type" },
+ { BLOCK_OPT_CONVENTIONAL_ZONES, "zone.conventional-zones" },
+ { BLOCK_OPT_MAX_OPEN_ZONES, "zone.max-open-zones" },
+ { BLOCK_OPT_MAX_ACTIVE_ZONES, "zone.max-active-zones" },
{ NULL, NULL },
};
@@ -5462,6 +5768,27 @@ qcow2_get_specific_info(BlockDriverState *bs, Error **errp)
.data_file_raw = data_file_is_raw(bs),
.compression_type = s->compression_type,
};
+ if (s->zoned_header.zoned == QCOW2_Z_HM) {
+ ImageInfoSpecificQCow2Zoned *z =
+ g_new0(ImageInfoSpecificQCow2Zoned, 1);
+ *z = (ImageInfoSpecificQCow2Zoned){
+ .mode = QCOW2_ZONE_MODEL_HOST_MANAGED,
+ .nr_zones = s->zoned_header.nr_zones,
+ .u.host_managed = {
+ .has_size = true,
+ .size = s->zoned_header.zone_size,
+ .has_capacity = true,
+ .capacity = s->zoned_header.zone_capacity,
+ .has_conventional_zones = true,
+ .conventional_zones = s->zoned_header.conventional_zones,
+ .has_max_open_zones = true,
+ .max_open_zones = s->zoned_header.max_open_zones,
+ .has_max_active_zones = true,
+ .max_active_zones = s->zoned_header.max_active_zones,
+ },
+ };
+ spec_info->u.qcow2.data->zone = z;
+ }
} else {
/* if this assertion fails, this probably means a new version was
* added without having it covered here */
@@ -6285,6 +6612,36 @@ static QemuOptsList qcow2_create_opts = {
.type = QEMU_OPT_BOOL, \
.help = "Assume the external data file already exists and " \
"do not overwrite it" \
+ }, \
+ { \
+ .name = BLOCK_OPT_ZONE_MODEL, \
+ .type = QEMU_OPT_STRING, \
+ .help = "zone model modes, mode choice: host-managed", \
+ }, \
+ { \
+ .name = BLOCK_OPT_ZONE_SIZE, \
+ .type = QEMU_OPT_SIZE, \
+ .help = "zone size", \
+ }, \
+ { \
+ .name = BLOCK_OPT_ZONE_CAPACITY, \
+ .type = QEMU_OPT_SIZE, \
+ .help = "zone capacity", \
+ }, \
+ { \
+ .name = BLOCK_OPT_CONVENTIONAL_ZONES, \
+ .type = QEMU_OPT_NUMBER, \
+ .help = "numbers of conventional zones", \
+ }, \
+ { \
+ .name = BLOCK_OPT_MAX_ACTIVE_ZONES, \
+ .type = QEMU_OPT_NUMBER, \
+ .help = "max active zones", \
+ }, \
+ { \
+ .name = BLOCK_OPT_MAX_OPEN_ZONES, \
+ .type = QEMU_OPT_NUMBER, \
+ .help = "max open zones", \
},
QCOW_COMMON_OPTIONS,
{ /* end of list */ }
diff --git a/block/qcow2.h b/block/qcow2.h
index ba9fe68130..7757b98385 100644
--- a/block/qcow2.h
+++ b/block/qcow2.h
@@ -129,6 +129,11 @@
#define DEFAULT_CLUSTER_SIZE 65536
+#define DEFAULT_ZONE_SIZE (256 * MiB)
+
+#define QCOW2_Z_NONE 0 /* Regular block device */
+#define QCOW2_Z_HM 1 /* Host-managed zoned block device */
+
#define QCOW2_OPT_DATA_FILE "data-file"
#define QCOW2_OPT_LAZY_REFCOUNTS "lazy-refcounts"
#define QCOW2_OPT_DISCARD_REQUEST "pass-discard-request"
@@ -237,6 +242,62 @@ typedef struct Qcow2CryptoHeaderExtension {
uint64_t length;
} QEMU_PACKED Qcow2CryptoHeaderExtension;
+/* Maximum number of zones a zoned image can have. */
+#define QCOW2_MAX_NR_ZONES (1U << 20)
+
+typedef struct Qcow2ZonedHeaderExtension {
+ /* Zoned device attributes */
+ uint8_t zoned;
+ uint8_t reserved[3];
+ uint32_t nr_zones;
+ uint64_t zone_size;
+ uint64_t zone_capacity;
+ uint64_t zonedmeta_offset;
+ uint32_t conventional_zones;
+ uint32_t max_active_zones;
+ uint32_t max_open_zones;
+} QEMU_PACKED Qcow2ZonedHeaderExtension;
+
+typedef struct Qcow2ZoneListEntry {
+ QTAILQ_ENTRY(Qcow2ZoneListEntry) exp_open_zone_entry;
+ QTAILQ_ENTRY(Qcow2ZoneListEntry) imp_open_zone_entry;
+ QTAILQ_ENTRY(Qcow2ZoneListEntry) closed_zone_entry;
+} Qcow2ZoneListEntry;
+
+/*
+ * Per-zone tracking for write pointer (WP) advance. A submitter
+ * reserves an LBA range, performs the data write outside the zone
+ * lock, then waits until the persisted WP catches up to its end
+ * offset before returning success to the guest. A failure aborts
+ * all higher-LBA peers in the same zone.
+ */
+typedef enum Qcow2WPReqState {
+ /* data write submitted, awaiting completion */
+ QCOW2_WP_REQ_INFLIGHT,
+ /* data write completed, persisted WP not yet advanced past lba + len */
+ QCOW2_WP_REQ_PENDING,
+ /* persisted WP covers this range, submitter may ack success */
+ QCOW2_WP_REQ_RESOLVED,
+ /* data write failed, or a lower-LBA peer in the same zone failed */
+ QCOW2_WP_REQ_ABORTED,
+} Qcow2WPReqState;
+
+typedef struct Qcow2WPReq {
+ uint64_t lba; /* start offset of the data write */
+ uint64_t len; /* length in bytes */
+ Qcow2WPReqState state;
+ /* submitter waits for the data write to reach a terminal state */
+ CoQueue wait;
+ QTAILQ_ENTRY(Qcow2WPReq) entry;
+} Qcow2WPReq;
+
+typedef struct Qcow2ZoneWPState {
+ /* INFLIGHT reqs */
+ QTAILQ_HEAD(, Qcow2WPReq) in_flight;
+ /* PENDING reqs, sorted by lba */
+ QTAILQ_HEAD(, Qcow2WPReq) completed_pending;
+} Qcow2ZoneWPState;
+
typedef struct Qcow2UnknownHeaderExtension {
uint32_t magic;
uint32_t len;
@@ -257,17 +318,20 @@ enum {
QCOW2_INCOMPAT_DATA_FILE_BITNR = 2,
QCOW2_INCOMPAT_COMPRESSION_BITNR = 3,
QCOW2_INCOMPAT_EXTL2_BITNR = 4,
+ QCOW2_INCOMPAT_ZONED_FORMAT_BITNR = 5,
QCOW2_INCOMPAT_DIRTY = 1 << QCOW2_INCOMPAT_DIRTY_BITNR,
QCOW2_INCOMPAT_CORRUPT = 1 << QCOW2_INCOMPAT_CORRUPT_BITNR,
QCOW2_INCOMPAT_DATA_FILE = 1 << QCOW2_INCOMPAT_DATA_FILE_BITNR,
QCOW2_INCOMPAT_COMPRESSION = 1 << QCOW2_INCOMPAT_COMPRESSION_BITNR,
QCOW2_INCOMPAT_EXTL2 = 1 << QCOW2_INCOMPAT_EXTL2_BITNR,
+ QCOW2_INCOMPAT_ZONED_FORMAT = 1 << QCOW2_INCOMPAT_ZONED_FORMAT_BITNR,
QCOW2_INCOMPAT_MASK = QCOW2_INCOMPAT_DIRTY
| QCOW2_INCOMPAT_CORRUPT
| QCOW2_INCOMPAT_DATA_FILE
| QCOW2_INCOMPAT_COMPRESSION
- | QCOW2_INCOMPAT_EXTL2,
+ | QCOW2_INCOMPAT_EXTL2
+ | QCOW2_INCOMPAT_ZONED_FORMAT,
};
/* Compatible feature bits */
@@ -426,6 +490,24 @@ typedef struct BDRVQcow2State {
* is to convert the image with the desired compression type set.
*/
Qcow2CompressionType compression_type;
+
+ /* States of zoned device */
+ Qcow2ZonedHeaderExtension zoned_header;
+ QTAILQ_HEAD(, Qcow2ZoneListEntry) exp_open_zones;
+ QTAILQ_HEAD(, Qcow2ZoneListEntry) imp_open_zones;
+ QTAILQ_HEAD(, Qcow2ZoneListEntry) closed_zones;
+ Qcow2ZoneListEntry *zone_list_entries;
+ uint32_t nr_zones_exp_open;
+ uint32_t nr_zones_imp_open;
+ uint32_t nr_zones_closed;
+
+ /* Per-zone in-memory WP tracking for zoned images. */
+ Qcow2ZoneWPState *zone_wp_state;
+ /*
+ * Cache holding the on-disk zonedmeta cluster(s) for zoned image.
+ * WP advances go through this cache.
+ */
+ Qcow2Cache *wp_cache;
} BDRVQcow2State;
typedef struct Qcow2COWRegion {
diff --git a/docs/interop/qcow2.rst b/docs/interop/qcow2.rst
index 5948591107..9385f40ee5 100644
--- a/docs/interop/qcow2.rst
+++ b/docs/interop/qcow2.rst
@@ -128,7 +128,26 @@ the next fields through ``header_length``.
allows subcluster-based allocation. See the
Extended L2 Entries section for more details.
- Bits 5-63: Reserved (set to 0)
+ Bit 5: Zoned extension bit. If this bit is set then
+ the file is an emulated zoned device. The
+ zoned extension must be present.
+ Implementations that do not support zoned
+ emulation cannot open this file because it
+ generally only make sense to interpret the
+ data along with the zone information and
+ write pointers.
+
+ It is unsafe when any qcow2 user without
+ knowing the zoned extension reads or edits
+ a file with the zoned extension. The write
+ pointer tracking can be corrupted when a
+ writer edits a file, like overwriting beyond
+ the write pointer locations. Or a reader tries
+ to access a file without knowing write
+ pointers where the software setup will cause
+ invalid reads.
+
+ Bits 6-63: Reserved (set to 0)
80 - 87: compatible_features
Bitmask of compatible features. An implementation can
@@ -259,6 +278,7 @@ be stored. Each extension has a structure like the following::
0x23852875 - Bitmaps extension
0x0537be77 - Full disk encryption header pointer
0x44415441 - External data file name string
+ 0x007a6264 - Zoned extension
other - Unknown header extension, can be safely
ignored
@@ -344,6 +364,99 @@ The fields of the bitmaps extension are::
Offset into the image file at which the bitmap directory
starts. Must be aligned to a cluster boundary.
+Zoned extension
+---------------
+
+The zoned extension must be present if the incompatible_features bit 5 is set,
+and omitted when it is clear. As incompatible_features only exists from version
+3 on, a zoned image is a version 3 image. The extension contains fields for
+emulating the zoned storage model (https://zonedstorage.io/). Currently only
+the host-managed zone model is supported.
+
+The fields of the zoned extension are::
+
+ Byte 0: zoned
+ The byte represents the zoned model of the device. 0 is for
+ a non-zoned device (all other information in this header
+ is ignored). 1 is for a host-managed device, which only
+ allows for sequential writes within each zone. Other
+ values may be added later, the implementation must refuse
+ to open a device containing an unknown zone model.
+
+ 1 - 3: Reserved, must be zero.
+
+ 4 - 7: nr_zones
+ The number of zones. It is the sum of conventional zones
+ and sequential zones. The maximum value for nr_zones is
+ (2^32 - 1)/8 = 536870911.
+
+ 8 - 15: zone_size
+ Total size of each zone, in bytes. The 64-bit field is to
+ satisfy the virtio-blk zone_size range and emulate a read
+ zoned device, whose maximum zone size can be as large as
+ 2TB.
+
+ The value must be power of 2. Linux currently requires
+ the zone size to be a power of 2 number of LBAs. Qcow2
+ following this is mainly to allow emulating a real
+ ZNS drive configuration.
+
+ The value must also be a multiple of the cluster size, so
+ that each zone consists of whole clusters, which it shares
+ with no other zone. Resetting a zone can then zero its
+ clusters without touching the data of other zones.
+
+ 16 - 23: zone_capacity
+ The number of writable bytes within the zones. The bytes
+ between zone capacity and zone size are unusable: reads
+ will return 0s and writes will fail.
+
+ A zone capacity is always smaller or equal to the zone
+ size. It is for emulating a real ZNS drive configuration,
+ which has the constraint of aligning to some hardware erase
+ block size.
+
+ 24 - 31: zonedmeta_offset
+ The offset of zoned metadata structure in the contained
+ image, in bytes.
+
+ 32 - 35: conventional_zones
+ The number of conventional zones. The conventional zones
+ allow sequential writes and random writes. While the
+ sequential zones only allow sequential writes.
+
+ 36 - 39: max_active_zones
+ The maximum allowed active zones (zones in the implicit
+ open, explicit open, or closed state). It cannot be larger
+ than nr_zones.
+
+ 40 - 43: max_open_zones
+ The maximum allowed open zones (zones in the implicit open
+ or explicit open state). It cannot be larger than the number
+ of SWR zones of the device, nor larger than max_active_zones.
+
+ If the limits of open zones or active zones are equal to
+ the total number of SWR zones, then it's the same as having
+ no limits therefore max open zones and max active zones are
+ set to 0.
+
+The zonedmeta clusters contain a table of the zone write pointers. Each entry
+is a 64-bit value encoding both the zone type and the zone's write pointer::
+
+ Bit 0 - 62: Write pointer offset, in bytes, indicates the starting
+ point of the next write position in that zone.
+ For conventional zones the value is the zone's starting
+ offset.
+
+ 63: Zone type. 0 = SWR; 1 = conventional.
+
+
+Each zone's write pointer in the zonedmeta area is durable on disk only after
+the data it covers is durable. For a write to a SWR zone, the data is written
+back to disk first, then the non-zoned qcow2 L1/L2/refcount metadata is written,
+and finally the write pointers are written. This ordering ensures that after
+a power failure the on-disk write pointer never leads data being written.
+
Full disk encryption header pointer
-----------------------------------
diff --git a/hw/block/block.c b/hw/block/block.c
index 3bb8121e9e..5b2e44351e 100644
--- a/hw/block/block.c
+++ b/hw/block/block.c
@@ -238,6 +238,13 @@ bool blkconf_check_zoned_geometry(BlockConf *conf, Error **errp)
return false;
}
+ if (!QEMU_IS_ALIGNED(bs->bl.zone_capacity, wg)) {
+ error_setg(errp, "zone capacity %" PRIu64 " is not a multiple of the "
+ "zone write granularity %" PRIu32,
+ bs->bl.zone_capacity, wg);
+ return false;
+ }
+
/*
* A write pointer that is not a multiple of the write granularity does not
* fall on a logical block boundary, so the guest can neither read nor write
diff --git a/include/block/block-io.h b/include/block/block-io.h
index f671476f08..deda6cab2b 100644
--- a/include/block/block-io.h
+++ b/include/block/block-io.h
@@ -134,6 +134,12 @@ uint32_t bdrv_zone_index(BlockDriverState *bs, uint64_t offset);
* bdrv_zone_is_full() this does not need the write pointer lock.
*/
bool bdrv_zone_is_conv(BlockDriverState *bs, uint32_t index);
+/*
+ * The offset at which the writable part of a zone ends. That is a zone
+ * capacity beyond the start of the zone, except for a last zone that the
+ * capacity of the device cuts short.
+ */
+uint64_t bdrv_zone_writable_end(BlockDriverState *bs, uint32_t index);
/*
* True when the write pointer of a zone has reached the end of the writable
* part of that zone, so that nothing more can be written to it until it is
diff --git a/include/block/block_int-common.h b/include/block/block_int-common.h
index 0c7746a544..ff9b36d12b 100644
--- a/include/block/block_int-common.h
+++ b/include/block/block_int-common.h
@@ -57,6 +57,12 @@
#define BLOCK_OPT_COMPRESSION_TYPE "compression_type"
#define BLOCK_OPT_EXTL2 "extended_l2"
#define BLOCK_OPT_KEEP_DATA_FILE "keep_data_file"
+#define BLOCK_OPT_ZONE_MODEL "zone.mode"
+#define BLOCK_OPT_ZONE_SIZE "zone.size"
+#define BLOCK_OPT_ZONE_CAPACITY "zone.capacity"
+#define BLOCK_OPT_CONVENTIONAL_ZONES "zone.conventional_zones"
+#define BLOCK_OPT_MAX_ACTIVE_ZONES "zone.max_active_zones"
+#define BLOCK_OPT_MAX_OPEN_ZONES "zone.max_open_zones"
#define BLOCK_PROBE_BUF_SIZE 512
@@ -911,6 +917,12 @@ typedef struct BlockLimits {
*/
uint32_t zone_size_bits;
+ /*
+ * The number of usable logical blocks within the zone, expressed
+ * in bytes. A zone capacity is smaller or equal to the zone size.
+ */
+ uint64_t zone_capacity;
+
/* total number of zones */
uint32_t nr_zones;
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 741c8165c0..ddec1ae87b 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -93,6 +93,9 @@
#
# @compression-type: the image cluster compression method (since 5.1)
#
+# @zone: zoned configuration; only set when the image was created
+# with a zoned model (since 11.1)
+#
# Since: 1.7
##
{ 'struct': 'ImageInfoSpecificQCow2',
@@ -106,7 +109,8 @@
'refcount-bits': 'int',
'*encrypt': 'ImageInfoSpecificQCow2Encryption',
'*bitmaps': ['Qcow2BitmapInfo'],
- 'compression-type': 'Qcow2CompressionType'
+ 'compression-type': 'Qcow2CompressionType',
+ '*zone': 'ImageInfoSpecificQCow2Zoned'
} }
##
@@ -5139,6 +5143,80 @@
{ 'enum': 'Qcow2CompressionType',
'data': [ 'zlib', { 'name': 'zstd', 'if': 'CONFIG_ZSTD' } ] }
+##
+# @Qcow2ZoneModel:
+#
+# Zoned device model used in qcow2 image file
+#
+# @host-managed: The host-managed model only allows sequential write
+# over the device zones.
+#
+# Since: 11.1
+##
+{ 'enum': 'Qcow2ZoneModel',
+ 'data': [ 'host-managed'] }
+
+##
+# @Qcow2ZoneHostManaged:
+#
+# The host-managed zone model. It only allows sequential writes.
+#
+# @size: Total number of bytes within zones (default: 256 MB).
+#
+# @capacity: The usable space within each zone, in bytes. Always
+# smaller than or equal to @size (default: same as @size).
+#
+# @conventional-zones: The number of conventional zones of the
+# zoned device (default: 0).
+#
+# @max-open-zones: The maximal number of open zones. Must be less
+# than or equal to the number of non-conventional zones (i.e.
+# ``size / zone.size - @conventional-zones``), and less than or
+# equal to @max-active-zones (default: 0, meaning no limit).
+#
+# @max-active-zones: The maximal number of zones in the implicit
+# open, explicit open or closed state. It is less than or equal
+# to the number of zones (default: 0, meaning no limit).
+#
+# Since: 11.1
+##
+{ 'struct': 'Qcow2ZoneHostManaged',
+ 'data': { '*size': 'size',
+ '*capacity': 'size',
+ '*conventional-zones': 'uint32',
+ '*max-open-zones': 'uint32',
+ '*max-active-zones': 'uint32' } }
+
+##
+# @Qcow2ZoneCreateOptions:
+#
+# Creation options for zoned qcow2 images.
+#
+# @mode: The zone model used by the image.
+#
+# Since: 11.1
+##
+{ 'union': 'Qcow2ZoneCreateOptions',
+ 'base': { 'mode': 'Qcow2ZoneModel' },
+ 'discriminator': 'mode',
+ 'data': { 'host-managed': 'Qcow2ZoneHostManaged' } }
+
+##
+# @ImageInfoSpecificQCow2Zoned:
+#
+# @mode: The zone model used by the image.
+#
+# @nr-zones: total number of zones in the image, derived at create
+# time from the size and the zone size.
+#
+# Since: 11.1
+##
+{ 'union': 'ImageInfoSpecificQCow2Zoned',
+ 'base': { 'mode': 'Qcow2ZoneModel',
+ 'nr-zones': 'uint32' },
+ 'discriminator': 'mode',
+ 'data': { 'host-managed': 'Qcow2ZoneHostManaged' } }
+
##
# @BlockdevCreateOptionsQcow2:
# Driver specific image creation options for qcow2.
@@ -5180,6 +5258,9 @@
# @compression-type: The image cluster compression method
# (default: zlib, since 5.1)
#
+# @zone: Options for zoned images. If absent, the device is not
+# zoned. (since 11.1)
+#
# Since: 2.12
##
{ 'struct': 'BlockdevCreateOptionsQcow2',
@@ -5196,7 +5277,8 @@
'*preallocation': 'PreallocMode',
'*lazy-refcounts': 'bool',
'*refcount-bits': 'int',
- '*compression-type':'Qcow2CompressionType' } }
+ '*compression-type':'Qcow2CompressionType',
+ '*zone': 'Qcow2ZoneCreateOptions' } }
##
# @BlockdevCreateOptionsQed:
diff --git a/tests/qemu-iotests/031.out b/tests/qemu-iotests/031.out
index 0054c2ed97..02a68b34f7 100644
--- a/tests/qemu-iotests/031.out
+++ b/tests/qemu-iotests/031.out
@@ -117,7 +117,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
Header extension:
@@ -150,7 +150,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
Header extension:
@@ -164,7 +164,7 @@ No errors were found on the image.
magic 0x514649fb
version 3
-backing_file_offset 0x240
+backing_file_offset 0x270
backing_file_size 0x17
cluster_bits 16
size 67108864
@@ -188,7 +188,7 @@ data 'host_device'
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
Header extension:
diff --git a/tests/qemu-iotests/036.out b/tests/qemu-iotests/036.out
index 1fa7cad28d..3ee310bb61 100644
--- a/tests/qemu-iotests/036.out
+++ b/tests/qemu-iotests/036.out
@@ -26,7 +26,7 @@ compatible_features []
autoclear_features [63]
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
@@ -38,7 +38,7 @@ compatible_features []
autoclear_features []
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
*** done
diff --git a/tests/qemu-iotests/061.out b/tests/qemu-iotests/061.out
index 951c6bf3e6..08bc08dd25 100644
--- a/tests/qemu-iotests/061.out
+++ b/tests/qemu-iotests/061.out
@@ -26,7 +26,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
magic 0x514649fb
@@ -84,7 +84,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
magic 0x514649fb
@@ -140,7 +140,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
ERROR cluster 5 refcount=0 reference=1
@@ -195,7 +195,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
magic 0x514649fb
@@ -264,7 +264,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
read 65536/65536 bytes at offset 44040192
@@ -326,7 +326,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
ERROR cluster 5 refcount=0 reference=1
@@ -355,7 +355,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
read 131072/131072 bytes at offset 0
diff --git a/tests/qemu-iotests/082.out b/tests/qemu-iotests/082.out
index e0463815c6..deff62bc55 100644
--- a/tests/qemu-iotests/082.out
+++ b/tests/qemu-iotests/082.out
@@ -72,6 +72,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o ? TEST_DIR/t.qcow2 128M
Supported options:
@@ -99,6 +105,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o cluster_size=4k,help TEST_DIR/t.qcow2 128M
Supported options:
@@ -126,6 +138,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o cluster_size=4k,? TEST_DIR/t.qcow2 128M
Supported options:
@@ -153,6 +171,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o help,cluster_size=4k TEST_DIR/t.qcow2 128M
Supported options:
@@ -180,6 +204,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o ?,cluster_size=4k TEST_DIR/t.qcow2 128M
Supported options:
@@ -207,6 +237,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o cluster_size=4k -o help TEST_DIR/t.qcow2 128M
Supported options:
@@ -234,6 +270,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -o cluster_size=4k -o ? TEST_DIR/t.qcow2 128M
Supported options:
@@ -261,6 +303,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: create -f qcow2 -u -o backing_file=TEST_DIR/t.qcow2,,help -F qcow2 TEST_DIR/t.qcow2 128M
Formatting 'TEST_DIR/t.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=134217728 backing_file=TEST_DIR/t.qcow2,,help backing_fmt=qcow2 lazy_refcounts=off refcount_bits=16
@@ -301,6 +349,12 @@ Supported qcow2 options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
The protocol level may support further options.
Specify the target filename to include those options.
@@ -391,6 +445,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o ? TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -418,6 +478,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o cluster_size=4k,help TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -445,6 +511,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o cluster_size=4k,? TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -472,6 +544,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o help,cluster_size=4k TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -499,6 +577,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o ?,cluster_size=4k TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -526,6 +610,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o cluster_size=4k -o help TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -553,6 +643,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o cluster_size=4k -o ? TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
Supported options:
@@ -580,6 +676,12 @@ Supported options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
Testing: convert -O qcow2 -o backing_fmt=qcow2,backing_file=TEST_DIR/t.qcow2,,help TEST_DIR/t.qcow2 TEST_DIR/t.qcow2.base
qemu-img: Could not open 'TEST_DIR/t.qcow2.base': Could not open backing file: Could not open 'TEST_DIR/t.qcow2,help': No such file or directory
@@ -620,6 +722,12 @@ Supported qcow2 options:
preallocation=<str> - Preallocation mode (allowed values: off, metadata, falloc, full)
refcount_bits=<num> - Width of a reference count entry in bits
size=<size> - Virtual disk size
+ zone.capacity=<size> - zone capacity
+ zone.conventional_zones=<num> - numbers of conventional zones
+ zone.max_active_zones=<num> - max active zones
+ zone.max_open_zones=<num> - max open zones
+ zone.mode=<str> - zone model modes, mode choice: host-managed
+ zone.size=<size> - zone size
The protocol level may support further options.
Specify the target filename to include those options.
diff --git a/tests/qemu-iotests/303.out b/tests/qemu-iotests/303.out
index b3c70827b7..684a481cae 100644
--- a/tests/qemu-iotests/303.out
+++ b/tests/qemu-iotests/303.out
@@ -47,7 +47,7 @@ header_length 112
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
Header extension:
@@ -133,7 +133,7 @@ wrote 1048576/1048576 bytes at offset 7340032
{
"name": "Feature table",
"magic": 1745090647,
- "length": 384,
+ "length": 432,
"data_str": "<binary>"
},
{
diff --git a/tests/qemu-iotests/tests/qcow2-encryption.out b/tests/qemu-iotests/tests/qcow2-encryption.out
index 9b549dc2ab..8be3e57f6e 100644
--- a/tests/qemu-iotests/tests/qcow2-encryption.out
+++ b/tests/qemu-iotests/tests/qcow2-encryption.out
@@ -10,7 +10,7 @@ data <binary>
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
image: TEST_DIR/t.IMGFMT
@@ -24,7 +24,7 @@ No errors were found on the image.
Header extension:
magic 0x6803f857 (Feature table)
-length 384
+length 432
data <binary>
qemu-img: Could not open 'TEST_DIR/t.IMGFMT': Missing CRYPTO header for crypt method 2
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v15 3/7] block: define errnos for zone protocol errors
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 1/7] docs/qcow2: add the zoned format feature Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 2/7] qcow2: add configurations for zoned format extension Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 4/7] qcow2: add zoned emulation capability Niklas Cassel
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Niklas Cassel, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Sam Li
A block driver that emulates zones (e.g. qcow2 with the zoned format
feature) rejects requests that violate the zone protocol, and the device
model has to report each of these to the guest with the matching zone
error (ZAC/ZBC sense data, virtio-blk zone status codes, ...). It can
only do so if each condition comes with an errno of its own, rather than
a generic one such as EIO.
Define one for each condition:
BLK_ZE_UNALIGNED_WRITE ESPIPE write not at the write pointer
BLK_ZE_BOUNDARY ERANGE write past the zone capacity
BLK_ZE_OPEN_RESOURCE ETOOMANYREFS open zone limit reached
BLK_ZE_ACTIVE_RESOURCE EOVERFLOW active zone limit reached
ETOOMANYREFS and EOVERFLOW are what Linux returns for
BLK_STS_ZONE_OPEN_RESOURCE and BLK_STS_ZONE_ACTIVE_RESOURCE, so a host
zoned device used through file-posix already reports the same values.
For the boundary case, ERANGE is used rather than EFBIG, which the host
file system can legitimately return for an image file.
A write that is not at the write pointer gets ESPIPE, which file and
block device I/O never return, rather than EINVAL, which a zoned driver
also uses for invalid requests such as an invalid zone state
transition. So a device model can map each of these errnos to its zone
error without having to know which request failed.
ETOOMANYREFS is not POSIX, so provide a fallback for hosts that lack it.
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
include/block/block-common.h | 16 ++++++++++++++++
include/qemu/osdep.h | 3 +++
2 files changed, 19 insertions(+)
diff --git a/include/block/block-common.h b/include/block/block-common.h
index 3932ff8615..6e4b2eb3ce 100644
--- a/include/block/block-common.h
+++ b/include/block/block-common.h
@@ -125,6 +125,22 @@ typedef struct BlockZoneDescriptor {
BlockZoneState state;
} BlockZoneDescriptor;
+/*
+ * Errors that zoned block drivers report for zone protocol violations, as
+ * positive errno values (drivers return them negated). Each condition has an
+ * errno of its own that zoned block drivers do not return for anything else,
+ * so that device models can report the matching zone error to the guest
+ * whichever request failed. In particular, EINVAL is not used, as it also
+ * reports invalid requests, such as an invalid zone state transition. The
+ * zone resource errors are the errnos Linux uses for BLK_STS_ZONE_OPEN_RESOURCE
+ * and BLK_STS_ZONE_ACTIVE_RESOURCE, so a host zoned device accessed through
+ * file-posix reports the same values.
+ */
+#define BLK_ZE_UNALIGNED_WRITE ESPIPE /* write not at write pointer */
+#define BLK_ZE_BOUNDARY ERANGE /* write past zone capacity */
+#define BLK_ZE_OPEN_RESOURCE ETOOMANYREFS /* open zone limit reached */
+#define BLK_ZE_ACTIVE_RESOURCE EOVERFLOW /* active zone limit reached */
+
/*
* Track write pointers of a zone in bytes.
*/
diff --git a/include/qemu/osdep.h b/include/qemu/osdep.h
index 50c7ce33b6..7925c28ac3 100644
--- a/include/qemu/osdep.h
+++ b/include/qemu/osdep.h
@@ -345,6 +345,9 @@ void QEMU_ERROR("code path is reachable")
#if !defined(ESHUTDOWN)
#define ESHUTDOWN 4099
#endif
+#if !defined(ETOOMANYREFS)
+#define ETOOMANYREFS 4100
+#endif
#define RETRY_ON_EINTR(expr) \
(__extension__ \
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v15 4/7] qcow2: add zoned emulation capability
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
` (2 preceding siblings ...)
2026-10-26 12:12 ` [PATCH v15 3/7] block: define errnos for zone protocol errors Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 5/7] iotests: test the zoned format feature for qcow2 file Niklas Cassel
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Sam Li, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Niklas Cassel
From: Sam Li <faithilikerun@gmail.com>
By adding zone operations and zoned metadata, the zoned emulation
capability enables full emulation support of zoned device using
a qcow2 file. The zoned device metadata includes zone type,
zoned device state and write pointer (WP) of each zone, which is
stored to an array of unsigned integers.
WP accessor (qcow2_rw_wp_at) routes reads and writes of an 8-byte
WP slot through the write pointer cache. The write pointer cache is
written to disk after the qcow2 metadata is written, thus guaranteeing
that the write pointer is updated after the corresponding data is
written. Per-completion cache flush is deferred. The WP cluster reaches
disk on the next flush.
Each zone of a zoned device makes state transitions following
the zone state machine. The zone state machine mainly describes
five states, IMPLICIT OPEN, EXPLICIT OPEN, FULL, EMPTY and CLOSED.
READ ONLY and OFFLINE states will generally be affected by device
internal events. The operations on zones cause corresponding state
changing.
Zoned devices have limits on zone resources, which put constraints on
write operations on zones. It is managed by active zone queues
following LRU policy.
Signed-off-by: Sam Li <faithilikerun@gmail.com>
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
Changes by Niklas Cassel:
- account for the zoned metadata in the refcount check, so that qemu-img
check does not report the clusters it occupies as leaked
- reset and report the last zone of an image whose size is not a multiple
of the zone size, which is shorter than the others
- clamp the write pointer that a zone finish leaves behind to the end of
the writable part of the zone
- drop the zone append size limit, which the geometry now bounds
- report zone errors with the errnos from block-common.h instead of
EIO, EINVAL and EBUSY: BLK_ZE_UNALIGNED_WRITE, BLK_ZE_BOUNDARY,
BLK_ZE_OPEN_RESOURCE and BLK_ZE_ACTIVE_RESOURCE
- manage the zone resources as in the NVMe ZNS specification
(2.1.1.4.1): a write to an empty zone checks the active zone limit
also when the open zone limit is not reached, OPEN ZONE of an empty
zone checks the open zone limit too, and an implicitly open zone is
closed to make room regardless of the active zone limit, as it
remains active
- manage the open zone resources for FINISH ZONE of an empty zone, as
ZBC requires (ZBC-3 4.5.3.2.4)
- check the zone capacity before the zone resources, so that a write
past the zone capacity does not close another zone
- drop the redundant zone capacity check for zone appends
- shorten two comments that were longer than 80 columns
- report the length of a conventional zone as its capacity, as the
whole zone is writable; Linux refuses a disk with a conventional zone
whose capacity is smaller than its length
- refuse a write that starts in a conventional zone and reaches a
sequential zone, without writing any data (ZAC-3 4.7.2)
- remove a closed zone from the closed list when a write fills it, so
that it no longer counts as an active zone
---
block/qcow2-cache.c | 11 +-
block/qcow2-refcount.c | 18 +
block/qcow2.c | 1244 +++++++++++++++++++++++++++++++++++++++-
block/qcow2.h | 2 +
block/trace-events | 2 +
5 files changed, 1271 insertions(+), 6 deletions(-)
diff --git a/block/qcow2-cache.c b/block/qcow2-cache.c
index 23d9588b08..73f2be3641 100644
--- a/block/qcow2-cache.c
+++ b/block/qcow2-cache.c
@@ -275,14 +275,23 @@ int qcow2_cache_set_dependency(BlockDriverState *bs, Qcow2Cache *c,
{
int ret;
+ /*
+ * If the dependency graph is unchanged, nothing to do. This avoids
+ * a synchronous flush on every call below.
+ */
+ if (c->depends == dependency) {
+ return 0;
+ }
+
if (dependency->depends) {
+ /* Flatten any chain on the dependency side as a cycle-breaker. */
ret = qcow2_cache_flush_dependency(bs, dependency);
if (ret < 0) {
return ret;
}
}
- if (c->depends && (c->depends != dependency)) {
+ if (c->depends) {
ret = qcow2_cache_flush_dependency(bs, c);
if (ret < 0) {
return ret;
diff --git a/block/qcow2-refcount.c b/block/qcow2-refcount.c
index df7f6d2f23..0f20fa353e 100644
--- a/block/qcow2-refcount.c
+++ b/block/qcow2-refcount.c
@@ -1239,6 +1239,13 @@ int qcow2_write_caches(BlockDriverState *bs)
}
}
+ if (s->wp_cache) {
+ ret = qcow2_cache_write(bs, s->wp_cache);
+ if (ret < 0) {
+ return ret;
+ }
+ }
+
return 0;
}
@@ -2304,6 +2311,17 @@ calculate_refcounts(BlockDriverState *bs, BdrvCheckResult *res,
}
}
+ /* zoned metadata */
+ if (s->zoned_header.zoned != QCOW2_Z_NONE) {
+ ret = qcow2_inc_refcounts_imrt(bs, res, refcount_table, nb_clusters,
+ s->zoned_header.zonedmeta_offset,
+ (int64_t)s->zoned_header.nr_zones *
+ sizeof(uint64_t));
+ if (ret < 0) {
+ return ret;
+ }
+ }
+
/* bitmaps */
ret = qcow2_check_bitmaps_refcounts(bs, res, refcount_table,
nb_clusters, fix);
diff --git a/block/qcow2.c b/block/qcow2.c
index 01215bcf9b..225ca1775e 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -195,6 +195,331 @@ qcow2_extract_crypto_opts(QemuOpts *opts, const char *fmt, Error **errp)
return cryptoopts_qdict;
}
+#define QCOW2_WP_CONV_BIT (1ULL << 63)
+#define QCOW2_ZT_IS_CONV(wp) ((wp) & QCOW2_WP_CONV_BIT)
+#define QCOW2_GET_WP(wp) ((wp) & ~QCOW2_WP_CONV_BIT)
+
+/*
+ * To emulate a real zoned device, closed, empty and full states are
+ * preserved after a power cycle. The open states are in-memory and will
+ * be lost after closing the device. Read-only and offline states are
+ * device-internal events, which are not considered for simplicity.
+ *
+ * The qcow2 state lock must be held when called.
+ */
+static inline BlockZoneState qcow2_get_zone_state(BlockDriverState *bs,
+ uint32_t index)
+{
+ BDRVQcow2State *s = bs->opaque;
+ Qcow2ZoneListEntry *zone_entry = &s->zone_list_entries[index];
+ uint64_t zone_wp = bs->wps->wp[index];
+ uint64_t zone_start;
+
+ if (QCOW2_ZT_IS_CONV(zone_wp)) {
+ return BLK_ZS_NOT_WP;
+ }
+
+ if (QTAILQ_IN_USE(zone_entry, exp_open_zone_entry)) {
+ return BLK_ZS_EOPEN;
+ }
+ if (QTAILQ_IN_USE(zone_entry, imp_open_zone_entry)) {
+ return BLK_ZS_IOPEN;
+ }
+
+ zone_start = (uint64_t)index * bs->bl.zone_size;
+ if (zone_wp == zone_start) {
+ return BLK_ZS_EMPTY;
+ }
+ if (bdrv_zone_is_full(bs, index)) {
+ return BLK_ZS_FULL;
+ }
+ if (zone_wp > zone_start) {
+ if (!QTAILQ_IN_USE(zone_entry, closed_zone_entry)) {
+ /*
+ * The number of closed zones is not always updated in time when
+ * the device is closed. However, it only matters when doing
+ * zone report. Refresh the count and list of closed zones to
+ * provide correct zone states for zone report.
+ */
+ QTAILQ_INSERT_HEAD(&s->closed_zones, zone_entry, closed_zone_entry);
+ s->nr_zones_closed++;
+ }
+ return BLK_ZS_CLOSED;
+ }
+ return BLK_ZS_NOT_WP;
+}
+
+/* The qcow2 state lock must be held when called */
+static void qcow2_rm_exp_open_zone(BDRVQcow2State *s,
+ uint32_t index)
+{
+ Qcow2ZoneListEntry *zone_entry = &s->zone_list_entries[index];
+
+ QTAILQ_REMOVE(&s->exp_open_zones, zone_entry, exp_open_zone_entry);
+ s->nr_zones_exp_open--;
+}
+
+/* The qcow2 state lock must be held when called */
+static void qcow2_rm_imp_open_zone(BDRVQcow2State *s,
+ int32_t index)
+{
+ Qcow2ZoneListEntry *zone_entry;
+ if (index < 0) {
+ /* Apply LRU when the index is not specified. */
+ zone_entry = QTAILQ_LAST(&s->imp_open_zones);
+ } else {
+ zone_entry = &s->zone_list_entries[index];
+ }
+
+ QTAILQ_REMOVE(&s->imp_open_zones, zone_entry, imp_open_zone_entry);
+ s->nr_zones_imp_open--;
+}
+
+/* The qcow2 state lock must be held when called */
+static void qcow2_rm_open_zone(BDRVQcow2State *s,
+ uint32_t index)
+{
+ Qcow2ZoneListEntry *zone_entry = &s->zone_list_entries[index];
+
+ if (QTAILQ_IN_USE(zone_entry, exp_open_zone_entry)) {
+ qcow2_rm_exp_open_zone(s, index);
+ } else if (QTAILQ_IN_USE(zone_entry, imp_open_zone_entry)) {
+ qcow2_rm_imp_open_zone(s, index);
+ }
+}
+
+/* The qcow2 state lock must be held when called */
+static void qcow2_rm_closed_zone(BDRVQcow2State *s,
+ uint32_t index)
+{
+ Qcow2ZoneListEntry *zone_entry = &s->zone_list_entries[index];
+
+ QTAILQ_REMOVE(&s->closed_zones, zone_entry, closed_zone_entry);
+ s->nr_zones_closed--;
+}
+
+/* The qcow2 state lock must be held when called */
+static void qcow2_do_imp_open_zone(BDRVQcow2State *s,
+ uint32_t index,
+ BlockZoneState zs)
+{
+ Qcow2ZoneListEntry *zone_entry = &s->zone_list_entries[index];
+
+ switch (zs) {
+ case BLK_ZS_EMPTY:
+ break;
+ case BLK_ZS_CLOSED:
+ qcow2_rm_closed_zone(s, index);
+ break;
+ case BLK_ZS_IOPEN:
+ /*
+ * The LRU policy: update the zone that is most recently
+ * used to the head of the zone list
+ */
+ if (zone_entry == QTAILQ_FIRST(&s->imp_open_zones)) {
+ return;
+ }
+ QTAILQ_REMOVE(&s->imp_open_zones, zone_entry, imp_open_zone_entry);
+ s->nr_zones_imp_open--;
+ break;
+ default:
+ return;
+ }
+
+ QTAILQ_INSERT_HEAD(&s->imp_open_zones, zone_entry, imp_open_zone_entry);
+ s->nr_zones_imp_open++;
+}
+
+/* The qcow2 state lock must be held when called */
+static void qcow2_do_exp_open_zone(BDRVQcow2State *s,
+ uint32_t index)
+{
+ Qcow2ZoneListEntry *zone_entry = &s->zone_list_entries[index];
+
+ QTAILQ_INSERT_HEAD(&s->exp_open_zones, zone_entry, exp_open_zone_entry);
+ s->nr_zones_exp_open++;
+}
+
+/*
+ * The list of zones is managed using an LRU policy: the last
+ * zone of the list is always the one that was least recently used
+ * for writing and is chosen as the zone to close to be able to
+ * implicitly open another zone.
+ *
+ * We can only close the open zones. The index is not specified
+ * when it is less than 0.
+ *
+ * The qcow2 state lock must be held when called.
+ */
+static void qcow2_do_close_zone(BlockDriverState *bs,
+ int32_t index,
+ BlockZoneState zs)
+{
+ BDRVQcow2State *s = bs->opaque;
+ Qcow2ZoneListEntry *zone_entry;
+
+ if (index >= 0) {
+ zone_entry = &s->zone_list_entries[index];
+ } else {
+ /* before removal of the last implicitly open zone */
+ zone_entry = QTAILQ_LAST(&s->imp_open_zones);
+ }
+
+ if (zs == BLK_ZS_IOPEN) {
+ assert(zone_entry != NULL);
+ qcow2_rm_imp_open_zone(s, index);
+ QTAILQ_INSERT_HEAD(&s->closed_zones, zone_entry, closed_zone_entry);
+ s->nr_zones_closed++;
+ return;
+ }
+
+ if (index >= 0 && zs == BLK_ZS_EOPEN) {
+ qcow2_rm_exp_open_zone(s, index);
+ /*
+ * The zone state changes when the zone is removed from the list of
+ * open zones (explicitly open -> empty). The closed zone list is
+ * refreshed during get_zone_state().
+ */
+ qcow2_get_zone_state(bs, index);
+ }
+}
+
+/*
+ * Read/Write the new wp value for zone `index` through write pointer
+ * cache. Reads return the value currently held in the cache, which may
+ * be ahead of the on-disk value if the cache hasn't been flushed yet.
+ * Writes update the cache and mark the entry dirty.
+ */
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_rw_wp_at(BlockDriverState *bs, uint64_t *wp,
+ int32_t index, bool is_write)
+{
+ BDRVQcow2State *s = bs->opaque;
+ uint64_t wp_byte_off = sizeof(uint64_t) * index;
+ uint64_t cluster_file_off =
+ s->zoned_header.zonedmeta_offset +
+ (wp_byte_off & ~((uint64_t)s->cluster_size - 1));
+ size_t off_in_cluster = wp_byte_off & (s->cluster_size - 1);
+ void *cluster_buf;
+ uint64_t *slot;
+ int ret;
+
+ assert(s->wp_cache != NULL);
+
+ qemu_co_mutex_lock(&s->lock);
+ ret = qcow2_cache_get(bs, s->wp_cache, cluster_file_off, &cluster_buf);
+ if (ret < 0) {
+ qemu_co_mutex_unlock(&s->lock);
+ error_report("Failed to %s WP slot (zone %d): %s",
+ is_write ? "write" : "read", index, strerror(-ret));
+ return ret;
+ }
+
+ slot = (uint64_t *)((char *)cluster_buf + off_in_cluster);
+ if (is_write) {
+ *slot = cpu_to_be64(*wp);
+ qcow2_cache_entry_mark_dirty(s->wp_cache, cluster_buf);
+ } else {
+ *wp = be64_to_cpu(*slot);
+ }
+ qcow2_cache_put(s->wp_cache, &cluster_buf);
+ qemu_co_mutex_unlock(&s->lock);
+
+ trace_qcow2_wp_tracking(index, *wp >> BDRV_SECTOR_BITS);
+ return 0;
+}
+
+/* The qcow2 state lock must be held when called */
+static bool qcow2_can_activate_zone(BlockDriverState *bs)
+{
+ BDRVQcow2State *s = bs->opaque;
+
+ /* When the max active zone is zero, there is no limit on active zones */
+ if (!s->zoned_header.max_active_zones) {
+ return true;
+ }
+
+ /* Active zones are zones that are open or closed */
+ return s->nr_zones_exp_open + s->nr_zones_imp_open + s->nr_zones_closed
+ < s->zoned_header.max_active_zones;
+}
+
+/*
+ * This function manages open zones under active zones limit, as described in
+ * the NVMe ZNS specification (2.1.1.4.1 Managing resources). It checks if a
+ * zone in state @zs can transition to open state while maintaining max open
+ * and active zone limits, and returns 0 or the zone resource error that
+ * prevents it (-BLK_ZE_OPEN_RESOURCE or -BLK_ZE_ACTIVE_RESOURCE).
+ *
+ * The qcow2 state lock must be held when called.
+ */
+static int qcow2_check_open_zone(BlockDriverState *bs, BlockZoneState zs)
+{
+ BDRVQcow2State *s = bs->opaque;
+
+ /*
+ * An empty zone becomes active when it is opened, whereas a closed zone
+ * already is.
+ */
+ if (zs == BLK_ZS_EMPTY && !qcow2_can_activate_zone(bs)) {
+ return -BLK_ZE_ACTIVE_RESOURCE;
+ }
+
+ /* When the max open zone is zero, there is no limit on open zones */
+ if (!s->zoned_header.max_open_zones) {
+ return 0;
+ }
+
+ /*
+ * The open zones are zones with the states of explicitly and
+ * implicitly open.
+ */
+ if (s->nr_zones_imp_open + s->nr_zones_exp_open <
+ s->zoned_header.max_open_zones) {
+ return 0;
+ }
+
+ /*
+ * Zones are managed one at a time. Thus, the number of implicitly open
+ * zone can never be over the open zone limit. Close only one implicitly
+ * open zone; it remains active, so the number of active zones does not
+ * change.
+ *
+ * Only an implicitly open zone can be evicted to make room. If every
+ * open zone is explicitly open, the open zone limit cannot be satisfied.
+ */
+ if (!s->nr_zones_imp_open) {
+ return -BLK_ZE_OPEN_RESOURCE;
+ }
+ qcow2_do_close_zone(bs, -1, BLK_ZS_IOPEN);
+ trace_qcow2_imp_open_zones(0x23, s->nr_zones_imp_open);
+ return 0;
+}
+
+static inline int coroutine_fn GRAPH_RDLOCK
+qcow2_refresh_zonedmeta(BlockDriverState *bs)
+{
+ int ret;
+ BDRVQcow2State *s = bs->opaque;
+ uint64_t wps_size = s->zoned_header.nr_zones * sizeof(uint64_t);
+ g_autofree uint64_t *temp = NULL;
+
+ QEMU_BUILD_BUG_ON(!BDRV_ZT_IS_CONV(QCOW2_WP_CONV_BIT));
+
+ temp = g_new(uint64_t, s->zoned_header.nr_zones);
+ ret = bdrv_pread(bs->file, s->zoned_header.zonedmeta_offset,
+ wps_size, temp, 0);
+ if (ret < 0) {
+ error_report("Cannot read metadata");
+ return ret;
+ }
+
+ for (uint32_t i = 0; i < s->zoned_header.nr_zones; i++) {
+ bs->wps->wp[i] = be64_to_cpu(temp[i]);
+ }
+ return 0;
+}
+
/*
* Returns true if zone_opt is valid for an image with clusters of
* @cluster_size bytes, false otherwise.
@@ -540,6 +865,9 @@ qcow2_read_extensions(BlockDriverState *bs, uint64_t start_offset,
zoned_ext.max_open_zones = be32_to_cpu(zoned_ext.max_open_zones);
zoned_ext.max_active_zones =
be32_to_cpu(zoned_ext.max_active_zones);
+ zoned_ext.zonedmeta_offset =
+ be64_to_cpu(zoned_ext.zonedmeta_offset);
+
/*
* A zoned image has the zoned incompatible feature bit set,
* which a version 2 image cannot have.
@@ -601,6 +929,38 @@ qcow2_read_extensions(BlockDriverState *bs, uint64_t start_offset,
}
}
+ bs->wps = g_malloc(sizeof(BlockZoneWps)
+ + s->zoned_header.nr_zones * sizeof(uint64_t));
+ ret = qcow2_refresh_zonedmeta(bs);
+ if (ret < 0) {
+ g_free(bs->wps);
+ bs->wps = NULL;
+ return ret;
+ }
+
+ s->zone_list_entries = g_new0(Qcow2ZoneListEntry,
+ zoned_ext.nr_zones);
+ QTAILQ_INIT(&s->exp_open_zones);
+ QTAILQ_INIT(&s->imp_open_zones);
+ QTAILQ_INIT(&s->closed_zones);
+ qemu_co_mutex_init(&bs->wps->colock);
+
+ s->zone_wp_state = g_new0(Qcow2ZoneWPState, zoned_ext.nr_zones);
+ for (uint32_t i = 0; i < zoned_ext.nr_zones; i++) {
+ QTAILQ_INIT(&s->zone_wp_state[i].in_flight);
+ QTAILQ_INIT(&s->zone_wp_state[i].completed_pending);
+ qemu_co_queue_init(&s->zone_wp_state[i].drain);
+ }
+
+ s->wp_cache = qcow2_cache_create(bs,
+ DIV_ROUND_UP(zoned_ext.nr_zones * sizeof(uint64_t),
+ s->cluster_size),
+ s->cluster_size);
+ if (!s->wp_cache) {
+ error_setg(errp, "Could not allocate the write pointer cache");
+ return -ENOMEM;
+ }
+
#ifdef DEBUG_EXT
printf("Qcow2: Got zoned format extension: "
"offset=%" PRIu64 "\n", offset);
@@ -2961,21 +3321,303 @@ static coroutine_fn GRAPH_RDLOCK int qcow2_co_pwritev_task_entry(AioTask *task)
t->l2meta);
}
+/*
+ * Walk the per-zone completed_pending list while the head's LBA equals
+ * the cached persisted-WP, advancing persisted-WP and marking each
+ * covered request RESOLVED. On any advance, the new persisted-WP is
+ * written into the WP cache and depends-on qcow2 L2 table cache is
+ * established so write pointer is updated after the corresponding write
+ * completes.
+ *
+ * Caller holds bs->wps->colock.
+ */
static int coroutine_fn GRAPH_RDLOCK
-qcow2_co_pwritev_part(BlockDriverState *bs, int64_t offset, int64_t bytes,
- QEMUIOVector *qiov, size_t qiov_offset,
- BdrvRequestFlags flags)
+qcow2_wp_greedy_advance_locked(BlockDriverState *bs, uint32_t index)
+{
+ BDRVQcow2State *s = bs->opaque;
+ Qcow2ZoneWPState *zs = &s->zone_wp_state[index];
+ uint64_t wp_byte_off = sizeof(uint64_t) * index;
+ uint64_t cluster_file_off =
+ s->zoned_header.zonedmeta_offset +
+ (wp_byte_off & ~((uint64_t)s->cluster_size - 1));
+ size_t off_in_cluster = wp_byte_off & (s->cluster_size - 1);
+ void *cluster_buf;
+ uint64_t *slot;
+ uint64_t persisted_wp, new_persisted_wp;
+ Qcow2WPReq *head;
+ bool any_advanced = false;
+ int ret;
+
+ qemu_co_mutex_lock(&s->lock);
+ ret = qcow2_cache_get(bs, s->wp_cache, cluster_file_off, &cluster_buf);
+ if (ret < 0) {
+ qemu_co_mutex_unlock(&s->lock);
+ return ret;
+ }
+ slot = (uint64_t *)((char *)cluster_buf + off_in_cluster);
+ persisted_wp = be64_to_cpu(*slot);
+ new_persisted_wp = persisted_wp;
+
+ QTAILQ_FOREACH(head, &zs->completed_pending, entry) {
+ if (head->lba != new_persisted_wp) {
+ break;
+ }
+ new_persisted_wp += head->len;
+ any_advanced = true;
+ }
+
+ if (!any_advanced) {
+ qcow2_cache_put(s->wp_cache, &cluster_buf);
+ qemu_co_mutex_unlock(&s->lock);
+ return 0;
+ }
+
+ *slot = cpu_to_be64(new_persisted_wp);
+ qcow2_cache_entry_mark_dirty(s->wp_cache, cluster_buf);
+ qcow2_cache_put(s->wp_cache, &cluster_buf);
+
+ qcow2_cache_set_dependency(bs, s->wp_cache, s->l2_table_cache);
+ qemu_co_mutex_unlock(&s->lock);
+
+ while (!QTAILQ_EMPTY(&zs->completed_pending)) {
+ head = QTAILQ_FIRST(&zs->completed_pending);
+ if (head->lba >= new_persisted_wp) {
+ break;
+ }
+ QTAILQ_REMOVE(&zs->completed_pending, head, entry);
+ head->state = QCOW2_WP_REQ_RESOLVED;
+ qemu_co_queue_restart_all(&head->wait);
+ }
+
+ return 0;
+}
+
+/*
+ * Insert req into the zone's completed_pending list in ascending LBA
+ * order. Data writes may complete out of order; greedy_advance walks
+ * the list head while head->lba == persisted_wp, so the list must
+ * stay sorted.
+ *
+ * Caller holds bs->wps->colock.
+ */
+static void
+qcow2_wp_insert_pending_locked(Qcow2ZoneWPState *zs, Qcow2WPReq *req)
+{
+ Qcow2WPReq *iter;
+
+ req->state = QCOW2_WP_REQ_PENDING;
+
+ QTAILQ_FOREACH(iter, &zs->completed_pending, entry) {
+ if (iter->lba > req->lba) {
+ QTAILQ_INSERT_BEFORE(iter, req, entry);
+ return;
+ }
+ }
+ QTAILQ_INSERT_TAIL(&zs->completed_pending, req, entry);
+}
+
+/*
+ * Peer requests with lba > failed_lba are marked ABORTED. Survivors
+ * with lba < failed_lba are left untouched. Logical WP is rolled back
+ * to the contiguous extent above persisted-WP that remains covered by
+ * survivor requests.
+ *
+ * Caller holds bs->wps->colock.
+ */
+static void
+qcow2_wp_abort_higher_peers_locked(BlockDriverState *bs, uint32_t index,
+ uint64_t failed_lba)
+{
+ BDRVQcow2State *s = bs->opaque;
+ Qcow2ZoneWPState *zs = &s->zone_wp_state[index];
+ Qcow2WPReq *r, *next;
+ uint64_t logical_wp;
+
+ QTAILQ_FOREACH(r, &zs->in_flight, entry) {
+ if (r->lba > failed_lba && r->state != QCOW2_WP_REQ_ABORTED) {
+ r->state = QCOW2_WP_REQ_ABORTED;
+ qemu_co_queue_restart_all(&r->wait);
+ }
+ }
+ QTAILQ_FOREACH_SAFE(r, &zs->completed_pending, entry, next) {
+ if (r->lba > failed_lba) {
+ r->state = QCOW2_WP_REQ_ABORTED;
+ QTAILQ_REMOVE(&zs->completed_pending, r, entry);
+ qemu_co_queue_restart_all(&r->wait);
+ }
+ }
+
+ /* Rewind logical_wp to the highest survivor end_offset. */
+ logical_wp = failed_lba;
+ QTAILQ_FOREACH(r, &zs->in_flight, entry) {
+ if (r->lba < failed_lba && r->lba + r->len > logical_wp) {
+ logical_wp = r->lba + r->len;
+ }
+ }
+ QTAILQ_FOREACH(r, &zs->completed_pending, entry) {
+ if (r->lba < failed_lba && r->lba + r->len > logical_wp) {
+ logical_wp = r->lba + r->len;
+ }
+ }
+ bs->wps->wp[index] = logical_wp;
+}
+
+/*
+ * If it is an append write request, the offset pointer needs to be updated to
+ * the wp value of that zone after the IO completion. The unique pointer is
+ * passed on to this function to prevent the value being changed in condition of
+ * multiple concurrent writes.
+ *
+ * For host-managed zoned writes, the WP lock is acquired across the WP
+ * check and advance, with the qcow2 state lock nested for the zone
+ * state TAILQ traversals.
+ */
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_co_pwv_part(BlockDriverState *bs, int64_t *offset_ptr, int64_t bytes,
+ QEMUIOVector *qiov, size_t qiov_offset, bool is_append,
+ BdrvRequestFlags flags)
{
BDRVQcow2State *s = bs->opaque;
int offset_in_cluster;
int ret;
unsigned int cur_bytes; /* number of sectors in current iteration */
uint64_t host_offset;
+ int64_t offset = *offset_ptr;
QCowL2Meta *l2meta = NULL;
AioTaskPool *aio = NULL;
+ int64_t start_offset, start_bytes;
+ BlockZoneState zs;
+ int64_t end_zone, end_offset;
+ uint64_t *wp;
+ int64_t zone_size = bs->bl.zone_size;
+ int64_t zone_capacity = bs->bl.zone_capacity;
+ int index = 0;
+ Qcow2WPReq req;
+ Qcow2WPReq *wp_req = NULL;
trace_qcow2_writev_start_req(qemu_coroutine_self(), offset, bytes);
+ start_offset = offset;
+ start_bytes = bytes;
+ if (bs->bl.zoned == BLK_Z_HM) {
+ index = start_offset / zone_size;
+ wp = &bs->wps->wp[index];
+ qemu_co_mutex_lock(&bs->wps->colock);
+ if (QCOW2_ZT_IS_CONV(*wp)) {
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ /*
+ * The conventional zones are the first zones of the image. A write
+ * that starts in one and reaches a sequential zone writes nothing
+ * (ZAC-3 4.7.2: WRITE BOUNDARY VIOLATION).
+ */
+ if (start_offset + start_bytes >
+ s->zoned_header.conventional_zones * zone_size) {
+ error_report("write from a conventional zone into a "
+ "sequential zone with end_offset: 0x%" PRIx64,
+ start_offset + start_bytes);
+ return -BLK_ZE_BOUNDARY;
+ }
+ } else {
+ if (offset != *wp && !is_append) {
+ /* The write offset must be equal to the zone write pointer */
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ error_report("Offset 0x%" PRIx64 " of regular writes must be "
+ "equal to the zone write pointer 0x%" PRIx64 "",
+ offset, *wp);
+ return -BLK_ZE_UNALIGNED_WRITE;
+ }
+
+ if (is_append) {
+ /*
+ * The offset of append write is the write pointer value of
+ * that zone.
+ */
+ start_offset = *wp;
+ }
+
+ end_offset = start_offset + start_bytes;
+
+ /*
+ * Align up (start_offset, zone_size), the start offset is not
+ * necessarily power of two.
+ */
+ end_zone = index * zone_size + zone_capacity;
+ /*
+ * Write cannot exceed the zone capacity. Check this before the
+ * zone resources, which may close another zone.
+ */
+ if (end_offset > end_zone) {
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ error_report("write exceeds zone capacity with end_offset: "
+ "0x%" PRIx64 ", end_zone: 0x%" PRIx64,
+ end_offset / 512, end_zone / 512);
+ return -BLK_ZE_BOUNDARY;
+ }
+
+ qemu_co_mutex_lock(&s->lock);
+
+ /* Only allow writes when there are zone resources left */
+ zs = qcow2_get_zone_state(bs, index);
+ if (zs == BLK_ZS_CLOSED || zs == BLK_ZS_EMPTY) {
+ ret = qcow2_check_open_zone(bs, zs);
+ if (ret < 0) {
+ qemu_co_mutex_unlock(&s->lock);
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ error_report("no more %s zones available",
+ ret == -BLK_ZE_OPEN_RESOURCE ?
+ "open" : "active");
+ return ret;
+ }
+ }
+
+ /*
+ * Real drives change states before it can write to the zone. If
+ * the write fails, the zone state may have changed.
+ *
+ * The zone state transitions to implicit open when the original
+ * state is empty or closed. When the wp reaches the end, the
+ * open states (explicit open, implicit open) become full.
+ */
+ zs = qcow2_get_zone_state(bs, index);
+ if (end_offset == end_zone) {
+ /* Reaching the zone capacity implies full state */
+ if (zs == BLK_ZS_CLOSED) {
+ qcow2_rm_closed_zone(s, index);
+ } else {
+ qcow2_rm_open_zone(s, index);
+ }
+ trace_qcow2_imp_open_zones(0x24,
+ s->nr_zones_imp_open);
+ } else {
+ qcow2_do_imp_open_zone(s, index, zs);
+ trace_qcow2_imp_open_zones(0x24,
+ s->nr_zones_imp_open);
+ }
+
+ qemu_co_mutex_unlock(&s->lock);
+
+ /*
+ * Submission for a zone append write. The logical-WP is updated
+ * while the on-disk WP is not touched.
+ */
+ if (is_append) {
+ *offset_ptr = start_offset;
+ offset = start_offset;
+ }
+
+ wp_req = &req;
+ wp_req->lba = start_offset;
+ wp_req->len = start_bytes;
+ wp_req->state = QCOW2_WP_REQ_INFLIGHT;
+ qemu_co_queue_init(&wp_req->wait);
+ QTAILQ_INSERT_TAIL(&s->zone_wp_state[index].in_flight,
+ wp_req, entry);
+
+ *wp = end_offset;
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ }
+ }
+
while (bytes != 0 && aio_task_pool_status(aio) == 0) {
l2meta = NULL;
@@ -3036,14 +3678,81 @@ fail_nometa:
if (ret == 0) {
ret = aio_task_pool_status(aio);
}
- g_free(aio);
+ g_free(aio);
+ }
+
+ if (wp_req != NULL) {
+ qemu_co_mutex_lock(&bs->wps->colock);
+ QTAILQ_REMOVE(&s->zone_wp_state[index].in_flight, wp_req, entry);
+ if (QTAILQ_EMPTY(&s->zone_wp_state[index].in_flight)) {
+ qemu_co_queue_restart_all(&s->zone_wp_state[index].drain);
+ }
+
+ if (wp_req->state == QCOW2_WP_REQ_ABORTED) {
+ /*
+ * A peer's failure handler aborted us. Whether our data
+ * write itself succeeded or not, reject it.
+ */
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ ret = ret < 0 ? ret : -EIO;
+ goto wp_done;
+ }
+
+ if (ret < 0) {
+ /*
+ * This req's data write failed. Higher-LBA peers (still
+ * in_flight or already completed_pending) are marked ABORTED,
+ * their waiters woken; logical-WP rewinds to the highest
+ * surviving end_offset below this LBA. The zone remains usable.
+ */
+ qcow2_wp_abort_higher_peers_locked(bs, index, wp_req->lba);
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ goto wp_done;
+ }
+
+ qcow2_wp_insert_pending_locked(&s->zone_wp_state[index], wp_req);
+
+ ret = qcow2_wp_greedy_advance_locked(bs, index);
+ if (ret < 0) {
+ if (wp_req->state == QCOW2_WP_REQ_PENDING) {
+ QTAILQ_REMOVE(&s->zone_wp_state[index].completed_pending,
+ wp_req, entry);
+ }
+ qcow2_wp_abort_higher_peers_locked(bs, index, wp_req->lba);
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ goto wp_done;
+ }
+
+ /* Block until this write pointer req is RESOLVED or ABORTED. */
+ while (wp_req->state == QCOW2_WP_REQ_PENDING) {
+ qemu_co_queue_wait(&wp_req->wait, &bs->wps->colock);
+ }
+
+ if (wp_req->state == QCOW2_WP_REQ_ABORTED) {
+ ret = -EIO;
+ } else {
+ assert(wp_req->state == QCOW2_WP_REQ_RESOLVED);
+ ret = 0;
+ }
+ qemu_co_mutex_unlock(&bs->wps->colock);
}
+wp_done:
trace_qcow2_writev_done_req(qemu_coroutine_self(), ret);
return ret;
}
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_co_pwritev_part(BlockDriverState *bs, int64_t offset, int64_t bytes,
+ QEMUIOVector *qiov, size_t qiov_offset,
+ BdrvRequestFlags flags)
+{
+ return qcow2_co_pwv_part(bs, &offset, bytes, qiov, qiov_offset, false,
+ flags);
+}
+
+
static int GRAPH_RDLOCK qcow2_inactivate(BlockDriverState *bs)
{
BDRVQcow2State *s = bs->opaque;
@@ -3072,6 +3781,15 @@ static int GRAPH_RDLOCK qcow2_inactivate(BlockDriverState *bs)
strerror(-ret));
}
+ if (s->wp_cache) {
+ ret = qcow2_cache_flush(bs, s->wp_cache);
+ if (ret) {
+ result = ret;
+ error_report("Failed to flush the WP cache: %s",
+ strerror(-ret));
+ }
+ }
+
/*
* A read-only node cannot resolve an inherited dirty bit here;
* leave it dirty, same as plain read access already does.
@@ -3083,6 +3801,25 @@ static int GRAPH_RDLOCK qcow2_inactivate(BlockDriverState *bs)
return result;
}
+static void qcow2_do_close_all_zone(BDRVQcow2State *s)
+{
+ Qcow2ZoneListEntry *zone_entry, *next;
+
+ QTAILQ_FOREACH_SAFE(zone_entry, &s->imp_open_zones, imp_open_zone_entry,
+ next) {
+ QTAILQ_REMOVE(&s->imp_open_zones, zone_entry, imp_open_zone_entry);
+ s->nr_zones_imp_open--;
+ }
+
+ QTAILQ_FOREACH_SAFE(zone_entry, &s->exp_open_zones, exp_open_zone_entry,
+ next) {
+ QTAILQ_REMOVE(&s->exp_open_zones, zone_entry, exp_open_zone_entry);
+ s->nr_zones_exp_open--;
+ }
+
+ assert(s->nr_zones_imp_open + s->nr_zones_exp_open == 0);
+}
+
static void coroutine_mixed_fn GRAPH_RDLOCK
qcow2_do_close(BlockDriverState *bs, bool close_data_file)
{
@@ -3098,6 +3835,10 @@ qcow2_do_close(BlockDriverState *bs, bool close_data_file)
cache_clean_timer_del_and_wait(bs);
qcow2_cache_destroy(s->l2_table_cache);
qcow2_cache_destroy(s->refcount_block_cache);
+ if (s->wp_cache) {
+ qcow2_cache_destroy(s->wp_cache);
+ s->wp_cache = NULL;
+ }
qcrypto_block_free(s->crypto);
s->crypto = NULL;
@@ -3122,6 +3863,12 @@ qcow2_do_close(BlockDriverState *bs, bool close_data_file)
qcow2_refcount_close(bs);
qcow2_free_snapshots(bs);
+ qcow2_do_close_all_zone(s);
+ g_free(s->zone_list_entries);
+ s->zone_list_entries = NULL;
+ g_free(s->zone_wp_state);
+ s->zone_wp_state = NULL;
+ g_free(bs->wps);
}
static void GRAPH_UNLOCKED qcow2_close(BlockDriverState *bs)
@@ -3438,6 +4185,8 @@ int qcow2_update_header(BlockDriverState *bs)
.max_open_zones = cpu_to_be32(s->zoned_header.max_open_zones),
.max_active_zones =
cpu_to_be32(s->zoned_header.max_active_zones),
+ .zonedmeta_offset =
+ cpu_to_be64(s->zoned_header.zonedmeta_offset),
};
ret = header_ext_add(buf, QCOW2_EXT_MAGIC_ZONED_FORMAT,
&zoned_header, sizeof(zoned_header),
@@ -3846,7 +4595,9 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
int version;
int refcount_order;
uint64_t *refcount_table;
- int ret;
+ uint64_t zoned_meta_size, zoned_clusterlen;
+ int64_t offset;
+ int ret, i;
uint8_t compression_type = QCOW2_COMPRESSION_TYPE_ZLIB;
assert(create_options->driver == BLOCKDEV_DRIVER_QCOW2);
@@ -4211,6 +4962,48 @@ qcow2_co_create(BlockdevCreateOptions *create_options, Error **errp)
ret = -EINVAL;
goto unlock;
}
+
+ uint32_t nrz = s->zoned_header.nr_zones;
+ zoned_meta_size = sizeof(uint64_t) * nrz;
+ g_autofree uint64_t *meta = NULL;
+ meta = g_new0(uint64_t, nrz);
+
+ for (i = 0; i < s->zoned_header.conventional_zones; ++i) {
+ meta[i] = i * s->zoned_header.zone_size;
+ meta[i] |= QCOW2_WP_CONV_BIT;
+ }
+
+ for (; i < nrz; ++i) {
+ meta[i] = i * s->zoned_header.zone_size;
+ }
+
+ offset = qcow2_alloc_clusters(blk_bs(blk), zoned_meta_size);
+ if (offset < 0) {
+ ret = offset;
+ error_setg_errno(errp, -ret, "Could not allocate clusters "
+ "for zoned metadata size");
+ goto unlock;
+ }
+ s->zoned_header.zonedmeta_offset = offset;
+
+ zoned_clusterlen = size_to_clusters(s, zoned_meta_size)
+ * s->cluster_size;
+ ret = qcow2_pre_write_overlap_check(blk_bs(blk), 0, offset,
+ zoned_clusterlen, false);
+ if (ret < 0) {
+ error_setg_errno(errp, -ret, "Overlap check failed for zoned "
+ "metadata");
+ goto unlock;
+ }
+ for (i = 0; i < nrz; ++i) {
+ meta[i] = cpu_to_be64(meta[i]);
+ }
+ ret = bdrv_pwrite(blk_bs(blk)->file, offset, zoned_meta_size, meta, 0);
+ if (ret < 0) {
+ error_setg_errno(errp, -ret, "Could not write zoned metadata "
+ "to disk");
+ goto unlock;
+ }
} else {
s->zoned_header.zoned = QCOW2_Z_NONE;
}
@@ -4615,6 +5408,443 @@ qcow2_co_pdiscard(BlockDriverState *bs, int64_t offset, int64_t bytes)
return ret;
}
+static int coroutine_fn
+qcow2_co_zone_report(BlockDriverState *bs, int64_t offset,
+ unsigned int *nr_zones, BlockZoneDescriptor *zones)
+{
+ BDRVQcow2State *s = bs->opaque;
+ uint64_t zone_size = s->zoned_header.zone_size;
+ uint64_t zone_capacity = s->zoned_header.zone_capacity;
+ int64_t capacity = bs->total_sectors << BDRV_SECTOR_BITS;
+ int64_t size = bs->bl.nr_zones * zone_size;
+ unsigned int nrz, si, i = 0;
+
+ if (offset >= capacity) {
+ error_report("offset %" PRId64 " is equal to or greater than the "
+ "device capacity %" PRId64 "", offset, capacity);
+ return -EINVAL;
+ }
+
+ nrz = ((*nr_zones) < bs->bl.nr_zones) ? (*nr_zones) : bs->bl.nr_zones;
+ si = offset / zone_size; /* Zone size cannot be 0 for zoned device */
+ qemu_co_mutex_lock(&bs->wps->colock);
+ for (; i < nrz; ++i) {
+ if (i + si >= bs->bl.nr_zones) {
+ break;
+ }
+
+ zones[i].start = (si + i) * zone_size;
+
+ /* The last zone can be smaller than the zone size */
+ if ((si + i + 1) == bs->bl.nr_zones && size > capacity) {
+ uint64_t l = zone_size - (size - capacity);
+ zones[i].length = l;
+ zones[i].cap = MIN(zone_capacity, l);
+ } else {
+ zones[i].length = zone_size;
+ zones[i].cap = zone_capacity;
+ }
+
+ uint64_t wp = bs->wps->wp[si + i];
+ if (QCOW2_ZT_IS_CONV(wp)) {
+ zones[i].type = BLK_ZT_CONV;
+ zones[i].state = BLK_ZS_NOT_WP;
+ /* A conventional zone is writable in full */
+ zones[i].cap = zones[i].length;
+ /* Clear masking bits */
+ wp = QCOW2_GET_WP(wp);
+ } else {
+ zones[i].type = BLK_ZT_SWR;
+ qemu_co_mutex_lock(&s->lock);
+ zones[i].state = qcow2_get_zone_state(bs, si + i);
+ qemu_co_mutex_unlock(&s->lock);
+ }
+ zones[i].wp = wp;
+ }
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ *nr_zones = i;
+ return 0;
+}
+
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_open_zone(BlockDriverState *bs, uint32_t index)
+{
+ BDRVQcow2State *s = bs->opaque;
+ int ret;
+
+ qemu_co_mutex_lock(&bs->wps->colock);
+ qemu_co_mutex_lock(&s->lock);
+ BlockZoneState zs = qcow2_get_zone_state(bs, index);
+ trace_qcow2_imp_open_zones(BLK_ZO_OPEN, s->nr_zones_imp_open);
+
+ switch (zs) {
+ case BLK_ZS_EMPTY:
+ ret = qcow2_check_open_zone(bs, zs);
+ if (ret < 0) {
+ goto unlock;
+ }
+ break;
+ case BLK_ZS_IOPEN:
+ qcow2_rm_imp_open_zone(s, index);
+ break;
+ case BLK_ZS_EOPEN:
+ qemu_co_mutex_unlock(&s->lock);
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ return 0;
+ case BLK_ZS_CLOSED:
+ ret = qcow2_check_open_zone(bs, zs);
+ if (ret < 0) {
+ goto unlock;
+ }
+ qcow2_rm_closed_zone(s, index);
+ break;
+ case BLK_ZS_FULL:
+ /*
+ * A full zone has no writable capacity left and holds no zone
+ * resources, so opening it is not a valid transition. It has to be
+ * reset before it can be opened again.
+ */
+ default:
+ ret = -EINVAL;
+ goto unlock;
+ }
+
+ qcow2_do_exp_open_zone(s, index);
+ ret = 0;
+
+unlock:
+ qemu_co_mutex_unlock(&s->lock);
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ return ret;
+}
+
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_close_zone(BlockDriverState *bs, uint32_t index)
+{
+ BDRVQcow2State *s = bs->opaque;
+ int ret;
+
+ qemu_co_mutex_lock(&bs->wps->colock);
+ qemu_co_mutex_lock(&s->lock);
+ BlockZoneState zs = qcow2_get_zone_state(bs, index);
+
+ switch (zs) {
+ case BLK_ZS_EMPTY:
+ break;
+ case BLK_ZS_IOPEN:
+ break;
+ case BLK_ZS_EOPEN:
+ break;
+ case BLK_ZS_CLOSED:
+ /* Closing a closed zone is not an error */
+ ret = 0;
+ goto unlock;
+ case BLK_ZS_FULL:
+ break;
+ default:
+ ret = -EINVAL;
+ goto unlock;
+ }
+ qcow2_do_close_zone(bs, index, zs);
+ ret = 0;
+
+unlock:
+ qemu_co_mutex_unlock(&s->lock);
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ return ret;
+}
+
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_finish_zone(BlockDriverState *bs, uint32_t index)
+{
+ BDRVQcow2State *s = bs->opaque;
+ int ret;
+
+ qemu_co_mutex_lock(&bs->wps->colock);
+ uint64_t *wp = &bs->wps->wp[index];
+
+ /* Wait for any in-flight writes to this zone to drain. */
+ if (s->zone_wp_state) {
+ while (!QTAILQ_EMPTY(&s->zone_wp_state[index].in_flight)) {
+ qemu_co_queue_wait(&s->zone_wp_state[index].drain,
+ &bs->wps->colock);
+ }
+ }
+
+ qemu_co_mutex_lock(&s->lock);
+ BlockZoneState zs = qcow2_get_zone_state(bs, index);
+
+ switch (zs) {
+ case BLK_ZS_EMPTY:
+ /*
+ * An empty or closed zone becomes implicitly open before it becomes
+ * full, which needs zone resources as for a write (ZBC-3 4.5.3.2.4).
+ */
+ ret = qcow2_check_open_zone(bs, zs);
+ if (ret < 0) {
+ goto unlock_state;
+ }
+ break;
+ case BLK_ZS_IOPEN:
+ qcow2_rm_imp_open_zone(s, index);
+ trace_qcow2_imp_open_zones(BLK_ZO_FINISH, s->nr_zones_imp_open);
+ break;
+ case BLK_ZS_EOPEN:
+ qcow2_rm_exp_open_zone(s, index);
+ break;
+ case BLK_ZS_CLOSED:
+ ret = qcow2_check_open_zone(bs, zs);
+ if (ret < 0) {
+ goto unlock_state;
+ }
+ qcow2_rm_closed_zone(s, index);
+ break;
+ case BLK_ZS_FULL:
+ ret = 0;
+ goto unlock_state;
+ default:
+ ret = -EINVAL;
+ goto unlock_state;
+ }
+
+ qemu_co_mutex_unlock(&s->lock);
+
+ uint64_t old_wp = *wp;
+ *wp = bdrv_zone_writable_end(bs, index);
+ ret = qcow2_rw_wp_at(bs, wp, index, true);
+ if (ret < 0) {
+ /* Keep the in-memory write pointer equal to the on-disk one. */
+ *wp = old_wp;
+ goto unlock;
+ }
+
+ /*
+ * Flush the WP cache so the on-disk write pointer reflects the new state
+ * on return.
+ */
+ qemu_co_mutex_lock(&s->lock);
+ ret = qcow2_cache_flush(bs, s->wp_cache);
+ qemu_co_mutex_unlock(&s->lock);
+ goto unlock;
+
+unlock_state:
+ qemu_co_mutex_unlock(&s->lock);
+unlock:
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ return ret;
+}
+
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_reset_zone(BlockDriverState *bs, uint32_t index,
+ int64_t len)
+{
+ BDRVQcow2State *s = bs->opaque;
+ unsigned int nrz = bs->bl.nr_zones;
+ int64_t zone_size = bs->bl.zone_size;
+ int64_t capacity = bs->total_sectors << BDRV_SECTOR_BITS;
+ int64_t zone_start;
+ unsigned int n;
+ int ret = 0;
+ bool any_dirtied = false;
+
+ qemu_co_mutex_lock(&bs->wps->colock);
+ uint64_t *wp = &bs->wps->wp[index];
+ if (len == capacity) {
+ n = nrz;
+ index = 0;
+ wp = &bs->wps->wp[0];
+ } else {
+ /*
+ * A trailing zone that is shorter than the zone size still counts as
+ * a whole zone, otherwise resetting it alone would round down to no
+ * zone at all.
+ */
+ n = DIV_ROUND_UP(len, zone_size);
+ }
+
+ for (unsigned int i = 0; i < n; ++i) {
+ uint64_t *wp_i = (uint64_t *)(wp + i);
+ uint64_t wpi_v = *wp_i;
+ if (QCOW2_ZT_IS_CONV(wpi_v)) {
+ continue;
+ }
+
+ /* Wait for any in-flight writes to this zone to drain. */
+ if (s->zone_wp_state) {
+ while (!QTAILQ_EMPTY(&s->zone_wp_state[index + i].in_flight)) {
+ qemu_co_queue_wait(&s->zone_wp_state[index + i].drain,
+ &bs->wps->colock);
+ }
+ }
+
+ qemu_co_mutex_lock(&s->lock);
+ BlockZoneState zs = qcow2_get_zone_state(bs, index + i);
+ switch (zs) {
+ case BLK_ZS_EMPTY:
+ break;
+ case BLK_ZS_IOPEN:
+ qcow2_rm_imp_open_zone(s, index + i);
+ trace_qcow2_imp_open_zones(BLK_ZO_RESET, s->nr_zones_imp_open);
+ break;
+ case BLK_ZS_EOPEN:
+ qcow2_rm_exp_open_zone(s, index + i);
+ break;
+ case BLK_ZS_CLOSED:
+ qcow2_rm_closed_zone(s, index + i);
+ break;
+ case BLK_ZS_FULL:
+ break;
+ default:
+ ret = -EINVAL;
+ qemu_co_mutex_unlock(&s->lock);
+ goto unlock;
+ }
+ qemu_co_mutex_unlock(&s->lock);
+
+ if (zs == BLK_ZS_EMPTY) {
+ continue;
+ }
+
+ /*
+ * Zero the data extent first. Data write fires before the WP cluster
+ * hits disk. So the wp advance cannot become durable while stale data
+ * is still readable.
+ *
+ * The last zone is shorter than the zone size when the device
+ * capacity is not a multiple of it, so stop at the end of the device.
+ */
+ zone_start = (uint64_t)(index + i) * zone_size;
+ ret = qcow2_co_pwrite_zeroes(bs, zone_start,
+ MIN(zone_size, capacity - zone_start), 0);
+ if (ret < 0) {
+ error_report("Failed to clear zone data at zone %u",
+ index + i);
+ goto unlock;
+ }
+
+ qemu_co_mutex_lock(&s->lock);
+ qcow2_cache_depends_on_flush(s->wp_cache);
+ qemu_co_mutex_unlock(&s->lock);
+
+ uint64_t old_wp = *wp_i;
+ *wp_i = zone_start;
+ ret = qcow2_rw_wp_at(bs, wp_i, index + i, true);
+ if (ret < 0) {
+ /* Keep the in-memory write pointer equal to the on-disk one. */
+ *wp_i = old_wp;
+ goto unlock;
+ }
+ any_dirtied = true;
+ }
+
+ if (any_dirtied) {
+ /* Single flush at the end. */
+ qemu_co_mutex_lock(&s->lock);
+ ret = qcow2_cache_flush(bs, s->wp_cache);
+ qemu_co_mutex_unlock(&s->lock);
+ }
+
+unlock:
+ qemu_co_mutex_unlock(&bs->wps->colock);
+ return ret;
+}
+
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_co_zone_mgmt(BlockDriverState *bs, BlockZoneOp op,
+ int64_t offset, int64_t len)
+{
+ BDRVQcow2State *s = bs->opaque;
+ int ret = 0;
+ int64_t capacity = bs->total_sectors << BDRV_SECTOR_BITS;
+ int64_t zone_size = s->zoned_header.zone_size;
+ int64_t zone_size_mask = zone_size - 1;
+ uint32_t index = offset / zone_size;
+ BlockZoneWps *wps = bs->wps;
+
+ if (offset >= capacity) {
+ error_report("offset %" PRId64 " is equal to or greater than the "
+ "device capacity %" PRId64 "", offset, capacity);
+ return -EINVAL;
+ }
+
+ if (offset & zone_size_mask) {
+ error_report("sector offset %" PRId64 " is not aligned to zone size"
+ " %" PRId64 "", offset / 512, zone_size / 512);
+ return -EINVAL;
+ }
+
+ /* The length must be zone-aligned and within the device capacity. */
+ if ((len < capacity - offset && (len & zone_size_mask)) ||
+ len > capacity - offset) {
+ error_report("number of sectors %" PRId64 " is not aligned to zone"
+ " size %" PRId64 "", len / 512, zone_size / 512);
+ return -EINVAL;
+ }
+
+ qemu_co_mutex_lock(&wps->colock);
+ uint64_t wpv = wps->wp[index];
+ qemu_co_mutex_unlock(&wps->colock);
+
+ if (QCOW2_ZT_IS_CONV(wpv)) {
+ /*
+ * ZONE_RESET_ALL is a global operation that is allowed when the
+ * starting zone is conventional; the zone reset path itself skips
+ * conventional zones.
+ */
+ if (op != BLK_ZO_RESET || len != capacity) {
+ error_report("zone mgmt operation 0x%x is not allowed on "
+ "a conventional zone", op);
+ return -EIO;
+ }
+ }
+
+ switch (op) {
+ case BLK_ZO_OPEN:
+ ret = qcow2_open_zone(bs, index);
+ break;
+ case BLK_ZO_CLOSE:
+ ret = qcow2_close_zone(bs, index);
+ break;
+ case BLK_ZO_FINISH:
+ ret = qcow2_finish_zone(bs, index);
+ break;
+ case BLK_ZO_RESET:
+ ret = qcow2_reset_zone(bs, index, len);
+ break;
+ default:
+ error_report("Unsupported zone op: 0x%x", op);
+ ret = -ENOTSUP;
+ break;
+ }
+ return ret;
+}
+
+static int coroutine_fn GRAPH_RDLOCK
+qcow2_co_zone_append(BlockDriverState *bs, int64_t *offset, QEMUIOVector *qiov,
+ BdrvRequestFlags flags)
+{
+ assert(flags == 0);
+ int64_t capacity = bs->total_sectors << BDRV_SECTOR_BITS;
+ int64_t zone_size_mask = bs->bl.zone_size - 1;
+ int64_t len = 0;
+
+ if (*offset >= capacity) {
+ error_report("*offset %" PRId64 " is equal to or greater than the "
+ "device capacity %" PRId64 "", *offset, capacity);
+ return -ENOSPC;
+ }
+
+ if (*offset & zone_size_mask) {
+ error_report("sector offset %" PRId64 " is not aligned to zone size "
+ "%" PRId64 "", *offset / 512, bs->bl.zone_size / 512);
+ return -EINVAL;
+ }
+
+ len = qiov->size;
+
+ return qcow2_co_pwv_part(bs, offset, len, qiov, 0, true, 0);
+}
+
static int coroutine_fn GRAPH_RDLOCK
qcow2_co_copy_range_from(BlockDriverState *bs,
BdrvChild *src, int64_t src_offset,
@@ -6697,6 +7927,10 @@ BlockDriver bdrv_qcow2 = {
.bdrv_co_pwritev_compressed_part = qcow2_co_pwritev_compressed_part,
.bdrv_make_empty = qcow2_make_empty,
+ .bdrv_co_zone_report = qcow2_co_zone_report,
+ .bdrv_co_zone_mgmt = qcow2_co_zone_mgmt,
+ .bdrv_co_zone_append = qcow2_co_zone_append,
+
.bdrv_snapshot_create = qcow2_snapshot_create,
.bdrv_snapshot_goto = qcow2_snapshot_goto,
.bdrv_snapshot_delete = qcow2_snapshot_delete,
diff --git a/block/qcow2.h b/block/qcow2.h
index 7757b98385..0092eb9e3d 100644
--- a/block/qcow2.h
+++ b/block/qcow2.h
@@ -296,6 +296,8 @@ typedef struct Qcow2ZoneWPState {
QTAILQ_HEAD(, Qcow2WPReq) in_flight;
/* PENDING reqs, sorted by lba */
QTAILQ_HEAD(, Qcow2WPReq) completed_pending;
+ /* zone mgmt ops wait here until in_flight has drained */
+ CoQueue drain;
} Qcow2ZoneWPState;
typedef struct Qcow2UnknownHeaderExtension {
diff --git a/block/trace-events b/block/trace-events
index 950c82d4b8..30a3e303ca 100644
--- a/block/trace-events
+++ b/block/trace-events
@@ -76,6 +76,8 @@ qcow2_writev_data(void *co, uint64_t offset) "co %p offset 0x%" PRIx64
qcow2_pwrite_zeroes_start_req(void *co, int64_t offset, int64_t bytes) "co %p offset 0x%" PRIx64 " bytes %" PRId64
qcow2_pwrite_zeroes(void *co, int64_t offset, int64_t bytes) "co %p offset 0x%" PRIx64 " bytes %" PRId64
qcow2_skip_cow(void *co, uint64_t offset, int nb_clusters) "co %p offset 0x%" PRIx64 " nb_clusters %d"
+qcow2_wp_tracking(int index, uint64_t wp) "wps[%d]: 0x%" PRIx64
+qcow2_imp_open_zones(uint8_t op, int nrz) "nr_imp_open_zones after op 0x%x: %d"
# qcow2-cluster.c
qcow2_alloc_clusters_offset(void *co, uint64_t offset, int bytes) "co %p offset 0x%" PRIx64 " bytes %d"
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v15 5/7] iotests: test the zoned format feature for qcow2 file
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
` (3 preceding siblings ...)
2026-10-26 12:12 ` [PATCH v15 4/7] qcow2: add zoned emulation capability Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 6/7] qcow2: validate the write pointer table when opening an image Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 7/7] virtio-blk: report zone errors with their zone status codes Niklas Cassel
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Sam Li, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Niklas Cassel
From: Sam Li <faithilikerun@gmail.com>
The zoned format feature can be tested by:
$ tests/qemu-iotests/check -qcow2 zoned-qcow2
Signed-off-by: Sam Li <faithilikerun@gmail.com>
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
Changes by Niklas Cassel:
- drop the zone append size limit from the image the test creates
- test, on a second image with zone resource limits, that each zone
error is reported with its own errno, and the zone resource
management of writes, OPEN ZONE and FINISH ZONE
- test that a zoned image needs version 3 and zones of whole clusters,
and that resetting a zone leaves the data of the other zones alone
- test that a closed zone that a write fills no longer counts as an
active zone
- list the test in the qcow2 section of MAINTAINERS
---
MAINTAINERS | 1 +
tests/qemu-iotests/tests/zoned-qcow2 | 322 +++++++++++++++++++++++
tests/qemu-iotests/tests/zoned-qcow2.out | 258 ++++++++++++++++++
3 files changed, 581 insertions(+)
create mode 100755 tests/qemu-iotests/tests/zoned-qcow2
create mode 100644 tests/qemu-iotests/tests/zoned-qcow2.out
diff --git a/MAINTAINERS b/MAINTAINERS
index 4ca1751e30..f870adfb11 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4518,6 +4518,7 @@ L: qemu-block@nongnu.org
S: Supported
F: block/qcow2*
F: docs/interop/qcow2.rst
+F: tests/qemu-iotests/tests/zoned-qcow2*
qcow
M: Kevin Wolf <kwolf@redhat.com>
diff --git a/tests/qemu-iotests/tests/zoned-qcow2 b/tests/qemu-iotests/tests/zoned-qcow2
new file mode 100755
index 0000000000..cc89bd5337
--- /dev/null
+++ b/tests/qemu-iotests/tests/zoned-qcow2
@@ -0,0 +1,322 @@
+#!/usr/bin/env bash
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Test zone management operations for qcow2 file.
+#
+
+seq="$(basename $0)"
+echo "QA output created by $seq"
+status=1 # failure is the default!
+
+file_name="zbc.qcow2"
+file_name2="zbc2.qcow2"
+_cleanup()
+{
+ _cleanup_test_img
+ _rm_test_img "$file_name"
+ _rm_test_img "$file_name2"
+}
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+# get standard environment, filters and checks
+. ../common.rc
+. ../common.filter
+. ../common.qemu
+
+# This test only runs on Linux hosts with qcow2 image files.
+_supported_fmt qcow2
+_supported_proto file
+_supported_os Linux
+
+echo
+echo "=== Initial image setup ==="
+echo
+
+$QEMU_IMG create -f qcow2 $file_name -o size=768M -o zone.size=64M -o \
+zone.capacity=64M -o zone.conventional_zones=0 \
+-o zone.max_open_zones=6 -o zone.max_active_zones=8 -o zone.mode=host-managed
+
+IMG="--image-opts -n driver=qcow2,file.driver=file,file.filename=$file_name"
+# A second image, for cases 5 to 7
+IMG2="--image-opts -n driver=qcow2,file.driver=file,file.filename=$file_name2"
+QEMU_IO_OPTIONS=$QEMU_IO_OPTIONS_NO_FMT
+
+echo
+echo "=== Testing a qcow2 img with zoned format ==="
+echo
+echo "case 1: test zone operations one by one"
+
+echo "(1) report zones[0]:"
+$QEMU_IO $IMG -c "zrp 0 1"
+echo
+echo "report zones[0~9]:"
+$QEMU_IO $IMG -c "zrp 0 10"
+echo
+echo "report zones[-1]:" # zones[-1] dictates the last zone
+$QEMU_IO $IMG -c "zrp 0x2C000000 2" # 0x2C000000 / 512 = 0x160000
+echo
+echo
+echo "(2) open zones[0], zones[1], zones[-1] then close, finish, reset:"
+$QEMU_IO $IMG << EOF
+zo 0 0x4000000
+zrp 0 1
+zo 0x4000000 0x4000000
+zrp 0x4000000 1
+zo 0x2C000000 0x4000000
+zrp 0x2C000000 2
+zc 0 0x4000000
+zrp 0 1
+zc 0x4000000 0x4000000
+zrp 0x4000000 1
+zc 0x2C000000 0x4000000
+zrp 0x2C000000 2
+zf 0 0x4000000
+zrp 0 1
+zf 64M 64M
+zrp 0x4000000 2
+zf 0x2C000000 0x4000000
+zrp 0x2C000000 2
+zrs 0 0x4000000
+zrp 0 1
+zrs 0x4000000 0x4000000
+zrp 0x4000000 1
+zrs 0x2C000000 0x4000000
+zrp 0x2C000000 2
+EOF
+
+echo
+echo "(3) append write with (4k, 8k) data"
+$QEMU_IO $IMG -c "zrp 0 12" # the physical block size of the device is 4096
+echo "Append write zones[0], zones[1] twice"
+$QEMU_IO $IMG << EOF
+zap -p 0 0x1000 0x2000
+zrp 0 1
+zap -p 0 0x1000 0x2000
+zrp 0 1
+zap -p 0x4000000 0x1000 0x2000
+zrp 0x4000000 1
+zap -p 0x4000000 0x1000 0x2000
+zrp 0x4000000 1
+EOF
+
+echo
+echo "Reset all:"
+$QEMU_IO $IMG -c "zrp 0 12" -c "zrs 0 768M" -c "zrp 0 12"
+echo
+echo
+
+echo "case 2: test a sets of ops that works or not"
+echo "(1) append write (4k, 4k) and then write to full"
+$QEMU_IO $IMG << EOF
+zap -p 0 0x1000 0x1000
+zrp 0 1
+zap -p 0 0x1000 0x1ffd000
+zap -p 0 0x1000000 0x1000000
+zrp 0 1
+EOF
+
+echo "Reset zones[0]:"
+$QEMU_IO $IMG -c "zrs 0 64M" -c "zrp 0 1"
+
+echo "(2) write in zones[0], zones[3], zones[8], and then reset all"
+$QEMU_IO $IMG << EOF
+zap -p 0 0x1000 0x1000
+zap -p 0xc000000 0x1000 0x1000
+zap -p 0x20000000 0x1000 0x1000
+zrp 0 12
+zrs 0 768M
+zrp 0 12
+EOF
+
+echo "case 3: test zone resource management"
+echo "(1) write in zones[0], zones[1], zones[2] and then close it"
+$QEMU_IO $IMG << EOF
+zap -p 0 0x1000 0x1000
+zap -p 0x4000000 0x1000 0x1000
+zap -p 0x8000000 0x1000 0x1000
+zrp 0 12
+zc 0 64M
+zc 0x4000000 64M
+zc 0x8000000 64M
+zrp 0 12
+EOF
+
+echo "(2) reset all after 3(1)"
+$QEMU_IO $IMG << EOF
+zrs 0 768M
+zrp 0 12
+EOF
+
+echo
+echo "case 4: WP cache crash consistency under concurrent appends"
+echo "(1) concurrent writes to the same sequential zone (zone 5 @ 320M)"
+# Three concurrent aio_writes at WP, WP+4K, WP+8K. Data writes are
+# permitted to complete out of order
+$QEMU_IO $IMG <<EOF
+aio_write -q -P 0xa1 0x14000000 4k
+aio_write -q -P 0xa2 0x14001000 4k
+aio_write -q -P 0xa3 0x14002000 4k
+aio_flush
+zrp 0x14000000 1
+read -q -P 0xa1 0x14000000 4k
+read -q -P 0xa2 0x14001000 4k
+read -q -P 0xa3 0x14002000 4k
+EOF
+
+echo "(2) concurrent writes to different sequential zones (zones 6, 7, 9)"
+# Spread across three zones. No per-zone interaction; each zone's WP
+# must advance independently.
+$QEMU_IO $IMG <<EOF
+aio_write -q -P 0xb1 0x18000000 4k
+aio_write -q -P 0xb2 0x1C000000 4k
+aio_write -q -P 0xb3 0x24000000 4k
+aio_flush
+zrp 0x18000000 1
+zrp 0x1C000000 1
+zrp 0x24000000 1
+read -q -P 0xb1 0x18000000 4k
+read -q -P 0xb2 0x1C000000 4k
+read -q -P 0xb3 0x24000000 4k
+EOF
+
+echo "(3) reset zones with no in-flight writes (zones 5, 6)"
+# Reset zones 5 and 6 (which we wrote in (1) and (2)). WP should drop
+# back to zone_start; data reads must return zero (unallocated
+# post-reset).
+$QEMU_IO $IMG -c "zrs 0x14000000 64M" -c "zrs 0x18000000 64M" \
+ -c "zrp 0x14000000 1" -c "zrp 0x18000000 1" \
+ -c "read -q -P 0 0x14000000 4k" \
+ -c "read -q -P 0 0x18000000 4k"
+
+echo "(4) BLKRESETALL after concurrent writes"
+# Reset everything first, refill three zones with concurrent writes,
+# then RESETALL. Each zone's WP must return to its start.
+$QEMU_IO $IMG <<EOF
+zrs 0 768M
+aio_write -q -P 0xc1 0x14000000 4k
+aio_write -q -P 0xc2 0x18000000 4k
+aio_write -q -P 0xc3 0x1C000000 4k
+aio_flush
+zrp 0x14000000 1
+zrp 0x18000000 1
+zrp 0x1C000000 1
+zrs 0 768M
+zrp 0x14000000 1
+zrp 0x18000000 1
+zrp 0x1C000000 1
+EOF
+
+echo
+echo "case 5: zone errors are reported with distinct errnos"
+$QEMU_IMG create -f qcow2 $file_name2 -o size=8M -o zone.size=1M -o \
+zone.capacity=1M -o zone.conventional_zones=0 \
+-o zone.max_open_zones=1 -o zone.max_active_zones=2 \
+-o zone.mode=host-managed > /dev/null
+echo "(1) write not at the write pointer of zones[0]"
+echo "(2) write past the capacity of zones[0]"
+echo "(3) open zone limit: zones[0] explicitly open, write zones[1]"
+echo "(4) active zone limit: zones[0] closed, zones[1] open, open zones[2]"
+$QEMU_IO $IMG2 << EOF
+write -q 4k 4k
+write -q 0 1028k
+zo 0 1M
+write -q 1M 4k
+write -q 0 4k
+zc 0 1M
+write -q 1M 4k
+write -q 2M 4k
+zo 2M 1M
+zrp 0 3
+EOF
+
+echo
+echo "case 6: zone resource accounting (max_open_zones=1, max_active_zones=2)"
+echo "(1) an empty zone is not opened beyond the active zone limit"
+$QEMU_IO $IMG2 << EOF
+zrs 0 8M
+write -q 0 4k
+zc 0 1M
+write -q 1M 4k
+zc 1M 1M
+write -q 2M 4k
+zrp 0 3
+EOF
+echo "(2) an explicit open of an empty zone respects the open zone limit"
+$QEMU_IO $IMG2 << EOF
+zrs 0 8M
+zo 0 1M
+zo 1M 1M
+zrp 0 2
+EOF
+echo "(3) at the active zone limit: open a closed zone, then an empty one"
+$QEMU_IO $IMG2 << EOF
+zrs 0 8M
+write -q 0 4k
+write -q 1M 4k
+write -q 4k 4k
+write -q 2M 4k
+zrp 0 3
+EOF
+echo "(4) finish an empty zone, with zones[0] explicitly, then implicitly open"
+$QEMU_IO $IMG2 << EOF
+zrs 0 8M
+zo 0 1M
+zf 1M 1M
+zrs 0 8M
+write -q 0 4k
+zf 1M 1M
+zrp 0 2
+EOF
+echo "(5) a write past the capacity of zones[1] leaves zones[0] open"
+$QEMU_IO $IMG2 << EOF
+zrs 0 8M
+write -q 0 4k
+write -q 1M 1028k
+zrp 0 2
+EOF
+
+echo
+echo "case 7: zones consist of whole clusters, in a version 3 image"
+echo "(1) a zone smaller than a cluster is refused"
+$QEMU_IMG create -f qcow2 $file_name2 -o size=1M -o cluster_size=64K \
+-o zone.size=16K -o zone.capacity=16K -o zone.mode=host-managed
+echo "(2) a version 2 image is refused"
+$QEMU_IMG create -f qcow2 $file_name2 -o compat=0.10 -o size=1M \
+-o zone.size=64K -o zone.capacity=64K -o zone.mode=host-managed
+echo "(3) resetting zones[1] leaves the data of zones[3] alone"
+$QEMU_IMG create -f qcow2 $file_name2 -o size=1M -o cluster_size=16K \
+-o zone.size=16K -o zone.capacity=16K -o zone.conventional_zones=0 \
+-o zone.mode=host-managed > /dev/null
+$QEMU_IO $IMG2 << EOF
+write -q -P 0x5a 48k 4k
+write -q -P 0xa5 16k 4k
+zf 16k 16k
+zrs 16k 16k
+zrp 16k 1
+read -q -P 0 16k 16k
+read -q -P 0x5a 48k 4k
+EOF
+
+echo
+echo "case 8: a closed zone that a write fills is no longer active"
+$QEMU_IMG create -f qcow2 $file_name2 -o size=8M -o zone.size=1M -o \
+zone.capacity=1M -o zone.conventional_zones=0 \
+-o zone.max_open_zones=2 -o zone.max_active_zones=3 \
+-o zone.mode=host-managed > /dev/null
+echo "(1) writing zones[2] closes zones[0], as only two zones can be open"
+echo "(2) filling zones[0] leaves zones[1] and zones[2] active: zones[3] opens"
+$QEMU_IO $IMG2 << EOF
+write -q 0 4k
+write -q 1M 4k
+write -q 2M 4k
+zrp 0 3
+write -q 4k 1020k
+zo 3M 1M
+zrp 0 4
+EOF
+
+# success, all done
+echo "*** done"
+rm -f $seq.full
+status=0
diff --git a/tests/qemu-iotests/tests/zoned-qcow2.out b/tests/qemu-iotests/tests/zoned-qcow2.out
new file mode 100644
index 0000000000..0ac4934a62
--- /dev/null
+++ b/tests/qemu-iotests/tests/zoned-qcow2.out
@@ -0,0 +1,258 @@
+QA output created by zoned-qcow2
+
+=== Initial image setup ===
+
+Formatting 'zbc.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib zone.mode=host-managed zone.size=67108864 zone.capacity=67108864 zone.conventional_zones=0 zone.max_active_zones=8 zone.max_open_zones=6 size=805306368 lazy_refcounts=off refcount_bits=16
+
+=== Testing a qcow2 img with zoned format ===
+
+case 1: test zone operations one by one
+(1) report zones[0]:
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+
+report zones[0~9]:
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+
+report zones[-1]:
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+
+
+(2) open zones[0], zones[1], zones[-1] then close, finish, reset:
+qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:3, [type: 2]
+qemu-io> qemu-io> start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:3, [type: 2]
+qemu-io> qemu-io> start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:3, [type: 2]
+qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x20000, zcond:14, [type: 2]
+qemu-io> qemu-io> start: 0x20000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:14, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x160000, len 0x20000, cap 0x20000, wptr 0x180000, zcond:14, [type: 2]
+qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io>
+(3) append write with (4k, 8k) data
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+Append write zones[0], zones[1] twice
+qemu-io> After zap done, the append sector is 0x0
+qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x18, zcond:2, [type: 2]
+qemu-io> After zap done, the append sector is 0x18
+qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x30, zcond:2, [type: 2]
+qemu-io> After zap done, the append sector is 0x20000
+qemu-io> start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20018, zcond:2, [type: 2]
+qemu-io> After zap done, the append sector is 0x20018
+qemu-io> start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20030, zcond:2, [type: 2]
+qemu-io>
+Reset all:
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x30, zcond:4, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20030, zcond:4, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+
+
+case 2: test a sets of ops that works or not
+(1) append write (4k, 4k) and then write to full
+qemu-io> After zap done, the append sector is 0x0
+qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x10, zcond:2, [type: 2]
+qemu-io> After zap done, the append sector is 0x10
+qemu-io> After zap done, the append sector is 0x10000
+qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x20000, zcond:14, [type: 2]
+qemu-io> Reset zones[0]:
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+(2) write in zones[0], zones[3], zones[8], and then reset all
+qemu-io> After zap done, the append sector is 0x0
+qemu-io> After zap done, the append sector is 0x60000
+qemu-io> After zap done, the append sector is 0x100000
+qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x10, zcond:2, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60010, zcond:2, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100010, zcond:2, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io> case 3: test zone resource management
+(1) write in zones[0], zones[1], zones[2] and then close it
+qemu-io> After zap done, the append sector is 0x0
+qemu-io> After zap done, the append sector is 0x20000
+qemu-io> After zap done, the append sector is 0x40000
+qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x10, zcond:2, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20010, zcond:2, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40010, zcond:2, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io> qemu-io> qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x10, zcond:4, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20010, zcond:4, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40010, zcond:4, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io> (2) reset all after 3(1)
+qemu-io> qemu-io> start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+start: 0x20000, len 0x20000, cap 0x20000, wptr 0x20000, zcond:1, [type: 2]
+start: 0x40000, len 0x20000, cap 0x20000, wptr 0x40000, zcond:1, [type: 2]
+start: 0x60000, len 0x20000, cap 0x20000, wptr 0x60000, zcond:1, [type: 2]
+start: 0x80000, len 0x20000, cap 0x20000, wptr 0x80000, zcond:1, [type: 2]
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+start: 0x100000, len 0x20000, cap 0x20000, wptr 0x100000, zcond:1, [type: 2]
+start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120000, zcond:1, [type: 2]
+start: 0x140000, len 0x20000, cap 0x20000, wptr 0x140000, zcond:1, [type: 2]
+start: 0x160000, len 0x20000, cap 0x20000, wptr 0x160000, zcond:1, [type: 2]
+qemu-io>
+case 4: WP cache crash consistency under concurrent appends
+(1) concurrent writes to the same sequential zone (zone 5 @ 320M)
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0018, zcond:2, [type: 2]
+qemu-io> qemu-io> qemu-io> qemu-io> (2) concurrent writes to different sequential zones (zones 6, 7, 9)
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0008, zcond:2, [type: 2]
+qemu-io> start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0008, zcond:2, [type: 2]
+qemu-io> start: 0x120000, len 0x20000, cap 0x20000, wptr 0x120008, zcond:2, [type: 2]
+qemu-io> qemu-io> qemu-io> qemu-io> (3) reset zones with no in-flight writes (zones 5, 6)
+start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+(4) BLKRESETALL after concurrent writes
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0008, zcond:2, [type: 2]
+qemu-io> start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0008, zcond:2, [type: 2]
+qemu-io> start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0008, zcond:2, [type: 2]
+qemu-io> qemu-io> start: 0xa0000, len 0x20000, cap 0x20000, wptr 0xa0000, zcond:1, [type: 2]
+qemu-io> start: 0xc0000, len 0x20000, cap 0x20000, wptr 0xc0000, zcond:1, [type: 2]
+qemu-io> start: 0xe0000, len 0x20000, cap 0x20000, wptr 0xe0000, zcond:1, [type: 2]
+qemu-io>
+case 5: zone errors are reported with distinct errnos
+(1) write not at the write pointer of zones[0]
+(2) write past the capacity of zones[0]
+(3) open zone limit: zones[0] explicitly open, write zones[1]
+(4) active zone limit: zones[0] closed, zones[1] open, open zones[2]
+qemu-io> qemu-io: Offset 0x1000 of regular writes must be equal to the zone write pointer 0x0
+qemu-io: write failed: Illegal seek
+qemu-io> qemu-io: write exceeds zone capacity with end_offset: 0x808, end_zone: 0x800
+qemu-io: write failed: Numerical result out of range
+qemu-io> qemu-io> qemu-io: no more open zones available
+qemu-io: write failed: Too many references: cannot splice
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io: no more active zones available
+qemu-io: write failed: Value too large for defined data type
+qemu-io> qemu-io: zone open failed: Value too large for defined data type
+qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x8, zcond:4, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x808, zcond:2, [type: 2]
+start: 0x1000, len 0x800, cap 0x800, wptr 0x1000, zcond:1, [type: 2]
+qemu-io>
+case 6: zone resource accounting (max_open_zones=1, max_active_zones=2)
+(1) an empty zone is not opened beyond the active zone limit
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> qemu-io: no more active zones available
+qemu-io: write failed: Value too large for defined data type
+qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x8, zcond:4, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x808, zcond:4, [type: 2]
+start: 0x1000, len 0x800, cap 0x800, wptr 0x1000, zcond:1, [type: 2]
+qemu-io> (2) an explicit open of an empty zone respects the open zone limit
+qemu-io> qemu-io> qemu-io> qemu-io: zone open failed: Too many references: cannot splice
+qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x0, zcond:3, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x800, zcond:1, [type: 2]
+qemu-io> (3) at the active zone limit: open a closed zone, then an empty one
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> qemu-io: no more active zones available
+qemu-io: write failed: Value too large for defined data type
+qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x10, zcond:2, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x808, zcond:4, [type: 2]
+start: 0x1000, len 0x800, cap 0x800, wptr 0x1000, zcond:1, [type: 2]
+qemu-io> (4) finish an empty zone, with zones[0] explicitly, then implicitly open
+qemu-io> qemu-io> qemu-io> qemu-io: zone finish failed: Too many references: cannot splice
+qemu-io> qemu-io> qemu-io> qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x8, zcond:4, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x1000, zcond:14, [type: 2]
+qemu-io> (5) a write past the capacity of zones[1] leaves zones[0] open
+qemu-io> qemu-io> qemu-io> qemu-io: write exceeds zone capacity with end_offset: 0x1008, end_zone: 0x1000
+qemu-io: write failed: Numerical result out of range
+qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x8, zcond:2, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x800, zcond:1, [type: 2]
+qemu-io>
+case 7: zones consist of whole clusters, in a version 3 image
+(1) a zone smaller than a cluster is refused
+Formatting 'zbc2.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib zone.mode=host-managed zone.size=16384 zone.capacity=16384 size=1048576 lazy_refcounts=off refcount_bits=16
+qemu-img: warning: Zoned extension header zone_size 16384B is not a multiple of the cluster size 65536B
+qemu-img: zbc2.qcow2: Invalid zoned device options
+(2) a version 2 image is refused
+Formatting 'zbc2.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib zone.mode=host-managed zone.size=65536 zone.capacity=65536 size=1048576 compat=0.10 lazy_refcounts=off refcount_bits=16
+qemu-img: zbc2.qcow2: Zoned images are only supported with compatibility level 1.1 and above (use version=v3 or greater)
+(3) resetting zones[1] leaves the data of zones[3] alone
+qemu-io> qemu-io> qemu-io> qemu-io> qemu-io> start: 0x20, len 0x20, cap 0x20, wptr 0x20, zcond:1, [type: 2]
+qemu-io> qemu-io> qemu-io>
+case 8: a closed zone that a write fills is no longer active
+(1) writing zones[2] closes zones[0], as only two zones can be open
+(2) filling zones[0] leaves zones[1] and zones[2] active: zones[3] opens
+qemu-io> qemu-io> qemu-io> qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x8, zcond:4, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x808, zcond:2, [type: 2]
+start: 0x1000, len 0x800, cap 0x800, wptr 0x1008, zcond:2, [type: 2]
+qemu-io> qemu-io> qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x800, zcond:14, [type: 2]
+start: 0x800, len 0x800, cap 0x800, wptr 0x808, zcond:4, [type: 2]
+start: 0x1000, len 0x800, cap 0x800, wptr 0x1008, zcond:2, [type: 2]
+start: 0x1800, len 0x800, cap 0x800, wptr 0x1800, zcond:3, [type: 2]
+qemu-io> *** done
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v15 6/7] qcow2: validate the write pointer table when opening an image
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
` (4 preceding siblings ...)
2026-10-26 12:12 ` [PATCH v15 5/7] iotests: test the zoned format feature for qcow2 file Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 7/7] virtio-blk: report zone errors with their zone status codes Niklas Cassel
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Niklas Cassel, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Sam Li
The write pointer table is image content, and qcow2_refresh_zonedmeta()
byte swaps it straight into bs->wps without looking at it. Every other
zoned field is checked before it becomes driver state, but the table that
those fields describe is not.
A write pointer is the offset that an append to its zone is written to.
qcow2_co_pwv_part() takes it as start_offset and only rejects a pointer
that reaches past the end of its zone, so a pointer that sits below the
start of its zone passes and the data is written into an earlier zone
that the guest never asked to write. The write pointer is then stored
back, spreading the damage. A pointer that is not sector aligned is
equally unusable: it is reported to a guest shifted right by
BDRV_SECTOR_BITS, so the guest is told about a position it can never
write to and the zone can only be recovered by resetting it.
Check the table against the geometry the header already declares: the
conventional bit is set for exactly the zones the header calls
conventional, a conventional zone records its own start, and the write
pointer of a sequential zone is sector aligned and within the writable
part of that zone, which is shorter for the last zone of an image whose
capacity is not a multiple of the zone size.
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
block/qcow2.c | 67 ++++++++++++++++++++++++
tests/qemu-iotests/tests/zoned-qcow2 | 40 ++++++++++++++
tests/qemu-iotests/tests/zoned-qcow2.out | 9 +++-
3 files changed, 115 insertions(+), 1 deletion(-)
diff --git a/block/qcow2.c b/block/qcow2.c
index 225ca1775e..8d4ea97172 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -520,6 +520,67 @@ qcow2_refresh_zonedmeta(BlockDriverState *bs)
return 0;
}
+/*
+ * The write pointer table is image content and cannot be trusted. Check it
+ * against the zone geometry, which qcow2_check_zone_options() has already
+ * validated. The write pointer of a zone is the offset that an append to that
+ * zone is written to, so a corrupted table would otherwise misdirect a write:
+ * qcow2_co_pwv_part() only rejects a pointer that reaches beyond the end of
+ * its zone, and one that points below the start of its zone would send the
+ * data to a zone that the guest never asked to write.
+ */
+static bool qcow2_check_zonedmeta(BlockDriverState *bs, Error **errp)
+{
+ BDRVQcow2State *s = bs->opaque;
+ uint32_t conv_zones = s->zoned_header.conventional_zones;
+ uint64_t zone_size = s->zoned_header.zone_size;
+ uint64_t capacity = (uint64_t)bs->total_sectors * BDRV_SECTOR_SIZE;
+
+ for (uint32_t i = 0; i < s->zoned_header.nr_zones; i++) {
+ uint64_t wp = bs->wps->wp[i];
+ uint64_t zone_start = (uint64_t)i * zone_size;
+ bool is_conv = QCOW2_ZT_IS_CONV(wp);
+ uint64_t zone_end;
+
+ if (is_conv != (i < conv_zones)) {
+ error_setg(errp, "Zone %" PRIu32 " contradicts the header, which "
+ "declares the first %" PRIu32 " zones conventional",
+ i, conv_zones);
+ return false;
+ }
+
+ if (is_conv) {
+ if (QCOW2_GET_WP(wp) != zone_start) {
+ error_setg(errp, "Conventional zone %" PRIu32 " has write "
+ "pointer 0x%" PRIx64 " instead of the zone start "
+ "0x%" PRIx64, i, (uint64_t)QCOW2_GET_WP(wp),
+ zone_start);
+ return false;
+ }
+ continue;
+ }
+
+ /* The last zone is shorter when the capacity is not zone aligned */
+ zone_end = zone_start + MIN(s->zoned_header.zone_capacity,
+ capacity - zone_start);
+
+ if (wp < zone_start || wp > zone_end) {
+ error_setg(errp, "Zone %" PRIu32 " has write pointer 0x%" PRIx64
+ " outside its writable range 0x%" PRIx64 "..0x%" PRIx64,
+ i, wp, zone_start, zone_end);
+ return false;
+ }
+
+ if (!QEMU_IS_ALIGNED(wp, BDRV_SECTOR_SIZE)) {
+ error_setg(errp, "Zone %" PRIu32 " has write pointer 0x%" PRIx64
+ " that is not sector aligned", i, wp);
+ return false;
+ }
+ }
+
+ return true;
+}
+
/*
* Returns true if zone_opt is valid for an image with clusters of
* @cluster_size bytes, false otherwise.
@@ -938,6 +999,12 @@ qcow2_read_extensions(BlockDriverState *bs, uint64_t start_offset,
return ret;
}
+ if (!qcow2_check_zonedmeta(bs, errp)) {
+ g_free(bs->wps);
+ bs->wps = NULL;
+ return -EINVAL;
+ }
+
s->zone_list_entries = g_new0(Qcow2ZoneListEntry,
zoned_ext.nr_zones);
QTAILQ_INIT(&s->exp_open_zones);
diff --git a/tests/qemu-iotests/tests/zoned-qcow2 b/tests/qemu-iotests/tests/zoned-qcow2
index cc89bd5337..0d25d47940 100755
--- a/tests/qemu-iotests/tests/zoned-qcow2
+++ b/tests/qemu-iotests/tests/zoned-qcow2
@@ -316,6 +316,46 @@ zo 3M 1M
zrp 0 4
EOF
+echo
+echo "case 9: a corrupted write pointer table is rejected"
+$QEMU_IO $IMG -c "zrs 0 768M"
+
+# Walk the header extensions to find the zoned one, and read the offset of
+# the write pointer table out of it, rather than relying on a fixed layout.
+ext_off=$(peek_file_be "$file_name" 100 4)
+while true; do
+ ext_magic=$(peek_file_be "$file_name" $ext_off 4)
+ ext_len=$(peek_file_be "$file_name" $((ext_off + 4)) 4)
+ if [ "$ext_magic" -eq 0 ]; then
+ echo "no zoned header extension found"
+ break
+ fi
+ if [ "$ext_magic" -eq $((0x007a6264)) ]; then
+ # zoned(1) reserved(3) nr_zones(4) zone_size(8) zone_capacity(8)
+ wps=$(peek_file_be "$file_name" $((ext_off + 8 + 24)) 8)
+ break
+ fi
+ ext_off=$((ext_off + 8 + (ext_len + 7) / 8 * 8))
+done
+
+# Zone 3 starts at 0xc000000. A write pointer below the start of its own zone
+# is the dangerous one: an append would be redirected into an earlier zone.
+poke_file_be "$file_name" $((wps + 8 * 3)) 8 0
+$QEMU_IO $IMG -c "zrp 0 1"
+poke_file_be "$file_name" $((wps + 8 * 3)) 8 $((0xc000000))
+
+poke_file_be "$file_name" $((wps + 8 * 1)) 8 $((0x4000200 + 1))
+$QEMU_IO $IMG -c "zrp 0 1"
+poke_file_be "$file_name" $((wps + 8 * 1)) 8 $((0x4000000))
+
+# Set the conventional bit on a zone the header declares sequential
+poke_file_be "$file_name" $((wps + 8 * 2)) 1 $((0x80))
+$QEMU_IO $IMG -c "zrp 0 1"
+poke_file_be "$file_name" $((wps + 8 * 2)) 1 0
+
+echo "after restoring the table:"
+$QEMU_IO $IMG -c "zrp 0 1"
+
# success, all done
echo "*** done"
rm -f $seq.full
diff --git a/tests/qemu-iotests/tests/zoned-qcow2.out b/tests/qemu-iotests/tests/zoned-qcow2.out
index 0ac4934a62..f34af0102e 100644
--- a/tests/qemu-iotests/tests/zoned-qcow2.out
+++ b/tests/qemu-iotests/tests/zoned-qcow2.out
@@ -255,4 +255,11 @@ qemu-io> qemu-io> qemu-io> start: 0x0, len 0x800, cap 0x800, wptr 0x800, zcond:1
start: 0x800, len 0x800, cap 0x800, wptr 0x808, zcond:4, [type: 2]
start: 0x1000, len 0x800, cap 0x800, wptr 0x1008, zcond:2, [type: 2]
start: 0x1800, len 0x800, cap 0x800, wptr 0x1800, zcond:3, [type: 2]
-qemu-io> *** done
+qemu-io>
+case 9: a corrupted write pointer table is rejected
+qemu-io: can't open: Zone 3 has write pointer 0x0 outside its writable range 0xc000000..0x10000000
+qemu-io: can't open: Zone 1 has write pointer 0x4000201 that is not sector aligned
+qemu-io: can't open: Zone 2 contradicts the header, which declares the first 0 zones conventional
+after restoring the table:
+start: 0x0, len 0x20000, cap 0x20000, wptr 0x0, zcond:1, [type: 2]
+*** done
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v15 7/7] virtio-blk: report zone errors with their zone status codes
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
` (5 preceding siblings ...)
2026-10-26 12:12 ` [PATCH v15 6/7] qcow2: validate the write pointer table when opening an image Niklas Cassel
@ 2026-10-26 12:12 ` Niklas Cassel
6 siblings, 0 replies; 8+ messages in thread
From: Niklas Cassel @ 2026-10-26 12:12 UTC (permalink / raw)
To: qemu-devel
Cc: Niklas Cassel, dlemoal, Stefan Hajnoczi, Pierrick Bouvier,
Michael S. Tsirkin, Hanna Reitz, Markus Armbruster, Kevin Wolf,
Eric Blake, qemu-block, Sam Li
virtio-blk completes a failed write with VIRTIO_BLK_S_IOERR, and a
failed zone append or zone management request with
VIRTIO_BLK_S_ZONE_INVALID_CMD. It never reports the zone status codes
that virtio 1.4 (5.2.6.2) requires for a write off the write pointer
or for a request that exceeds a zone resource limit.
When VIRTIO_BLK_F_ZONED is negotiated, map the zone errors of the block
layer to them:
BLK_ZE_UNALIGNED_WRITE VIRTIO_BLK_S_ZONE_UNALIGNED_WP
BLK_ZE_BOUNDARY VIRTIO_BLK_S_ZONE_INVALID_CMD
BLK_ZE_OPEN_RESOURCE VIRTIO_BLK_S_ZONE_OPEN_RESOURCE
BLK_ZE_ACTIVE_RESOURCE VIRTIO_BLK_S_ZONE_ACTIVE_RESOURCE
Other errors keep their status code. A FINISH that exceeds a zone
resource limit also gets a zone resource status code, which 5.2.6
defines for any zone operation. Zone errors of writes are reported
regardless of the rerror/werror policy, as they are the deterministic
result of the request.
Signed-off-by: Niklas Cassel <cassel@kernel.org>
---
hw/block/virtio-blk.c | 48 +++++++++++++++++++++++++++++++++++++++----
1 file changed, 44 insertions(+), 4 deletions(-)
diff --git a/hw/block/virtio-blk.c b/hw/block/virtio-blk.c
index 536d71a91a..1d35997f7e 100644
--- a/hw/block/virtio-blk.c
+++ b/hw/block/virtio-blk.c
@@ -68,11 +68,44 @@ void virtio_blk_req_complete(VirtIOBlockReq *req, unsigned char status)
virtio_notify(vdev, req->vq);
}
+/*
+ * Map a zone error (see include/block/block-common.h) to its virtio-blk
+ * status, or return VIRTIO_BLK_S_OK if @error is not one.
+ */
+static uint8_t virtio_blk_zone_status(VirtIOBlock *s, int error)
+{
+ if (!virtio_vdev_has_feature(VIRTIO_DEVICE(s), VIRTIO_BLK_F_ZONED)) {
+ return VIRTIO_BLK_S_OK;
+ }
+
+ switch (error) {
+ case BLK_ZE_UNALIGNED_WRITE:
+ return VIRTIO_BLK_S_ZONE_UNALIGNED_WP;
+ case BLK_ZE_BOUNDARY:
+ return VIRTIO_BLK_S_ZONE_INVALID_CMD;
+ case BLK_ZE_OPEN_RESOURCE:
+ return VIRTIO_BLK_S_ZONE_OPEN_RESOURCE;
+ case BLK_ZE_ACTIVE_RESOURCE:
+ return VIRTIO_BLK_S_ZONE_ACTIVE_RESOURCE;
+ default:
+ return VIRTIO_BLK_S_OK;
+ }
+}
+
static int virtio_blk_handle_rw_error(VirtIOBlockReq *req, int error,
bool is_read)
{
VirtIOBlock *s = req->dev;
- BlockErrorAction action = blk_get_error_action(s->blk, is_read, error);
+ uint8_t zone_status = virtio_blk_zone_status(s, error);
+ /*
+ * Zone errors are the deterministic result of the request, so they are
+ * reported whatever the rerror/werror policy: stopping the VM would only
+ * make the request fail the same way again when it is retried on resume,
+ * and ignoring the error would hide the failure from the guest.
+ */
+ BlockErrorAction action = zone_status != VIRTIO_BLK_S_OK ?
+ BLOCK_ERROR_ACTION_REPORT :
+ blk_get_error_action(s->blk, is_read, error);
if (action == BLOCK_ERROR_ACTION_STOP) {
/* Break the link as the next request is going to be parsed from the
@@ -84,7 +117,8 @@ static int virtio_blk_handle_rw_error(VirtIOBlockReq *req, int error,
s->rq = req;
}
} else if (action == BLOCK_ERROR_ACTION_REPORT) {
- virtio_blk_req_complete(req, VIRTIO_BLK_S_IOERR);
+ virtio_blk_req_complete(req, zone_status != VIRTIO_BLK_S_OK ?
+ zone_status : VIRTIO_BLK_S_IOERR);
block_acct_failed(blk_get_stats(s->blk), &req->acct);
g_free(req);
}
@@ -767,7 +801,10 @@ static void virtio_blk_zone_mgmt_complete(void *opaque, int ret)
trace_virtio_blk_zone_mgmt_complete(vdev, req,ret);
if (ret) {
- err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
+ err_status = virtio_blk_zone_status(s, -ret);
+ if (err_status == VIRTIO_BLK_S_OK) {
+ err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
+ }
}
virtio_blk_req_complete(req, err_status);
@@ -827,7 +864,10 @@ static void virtio_blk_zone_append_complete(void *opaque, int ret)
uint8_t err_status = VIRTIO_BLK_S_OK;
if (ret) {
- err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
+ err_status = virtio_blk_zone_status(s, -ret);
+ if (err_status == VIRTIO_BLK_S_OK) {
+ err_status = VIRTIO_BLK_S_ZONE_INVALID_CMD;
+ }
goto out;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-26 12:15 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-26 12:12 [PATCH v15 0/7] Add full zoned storage emulation to the qcow2 driver Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 1/7] docs/qcow2: add the zoned format feature Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 2/7] qcow2: add configurations for zoned format extension Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 3/7] block: define errnos for zone protocol errors Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 4/7] qcow2: add zoned emulation capability Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 5/7] iotests: test the zoned format feature for qcow2 file Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 6/7] qcow2: validate the write pointer table when opening an image Niklas Cassel
2026-10-26 12:12 ` [PATCH v15 7/7] virtio-blk: report zone errors with their zone status codes Niklas Cassel
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.