On Mon, Jun 29, 2026 at 7:40 PM Stefan Hajnoczi <[email protected]> wrote:
>
> On Tue, Jun 23, 2026 at 08:48:29PM +0200, Sam Li wrote:
> > +/*
> > + * Read/Write the new wp value for zone `index` through write pointe
>
> pointe -> pointer
>
> > +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;
> > + int i = 0;
> > + int si;
>
> Should i and si be unsigned int since nr_zones is an unsigned int?
>
> > +static int coroutine_fn GRAPH_RDLOCK
> > +qcow2_reset_zone(BlockDriverState *bs, uint32_t index,
> > + int64_t len)
> > +{
> > + BDRVQcow2State *s = bs->opaque;
> > + int nrz = bs->bl.nr_zones;
> > + int64_t zone_size = bs->bl.zone_size;
> > + int n, ret = 0;
> > + bool any_dirtied = false;
>
> nr_zones is an unsigned int. Variables holding numbers of zones like
> nrz, i, and n should be unsigned int, too. That way they behave
> correctly over their full range instead of behaving unexpectedly when
> they become negative (e.g. in a for loop with i < n).
Makes sense.
>
> > +
> > + qemu_co_mutex_lock(&bs->wps->colock);
> > + uint64_t *wp = &bs->wps->wp[index];
> > + if (len == bs->total_sectors << BDRV_SECTOR_BITS) {
> > + n = nrz;
> > + index = 0;
> > + wp = &bs->wps->wp[0];
> > + } else {
> > + n = len / zone_size;
> > + }
> > +
> > + for (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;
> > + }
> > +
> > + /* Reject if any write is in flight for this zone. */
> > + if (s->zone_wp_state &&
> > + !QTAILQ_EMPTY(&s->zone_wp_state[index + i].in_flight)) {
> > + ret = -EBUSY;
> > + goto unlock;
> > + }
>
> This is a coroutine function and it should yield to wait instead of
> returning -EBUSY. If -EBUSY is propagated to the guest that would result
> in unexpected behavior - I think real devices don't do this and the
> guest will not be prepared to handle -EBUSY.
You are right. It should yield until drain, then proceed. A zoned
null_blk does this.
>
> > +
> > + 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. Date write fires before the WP
> > cluster
>
> Date -> Data
>
> > + * hits disk. So the wp advance cannot become durable while stale
> > data
> > + * is still readable.
> > + */
> > + ret = qcow2_co_pwrite_zeroes(bs, (uint64_t)(index + i) * zone_size,
> > + zone_size, 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);
> > +
> > + *wp_i = (uint64_t)(index + i) * zone_size;
> > + ret = qcow2_rw_wp_at(bs, wp_i, index + i, true);
> > + if (ret < 0) {
> > + goto unlock;
>
> *wp_i has been modified but the operation will fail. The write pointers
> in memory and the write pointers in the qcow2 cache and on disk are now
> inconsistent. I expect the failure semantics to be that the write
> pointers in memory are consistent with the on-disk write pointers,
> although I haven't checked specs to see if they say anything about this.
Right, on wp-write failure, the in-memory wp needs to be stored so RAM
stays consistent with disk.
>
> > + }
> > + 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)
>
> Indentation is off here.
>
> > +{
> > + 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;
>
> It would be safer to check that this qcow2 image supports zone emulation
> before getting deeper into this function. zone_size could be 0 here if
> zone emulation is not enabled and it would cause a divide-by-zero
> exception.
>
> All block driver zone callbacks in qcow2 should perform this check just
> in case an emulated device exposes a code path where the guest can
> perform a zoned operation on a non-zoned BlockDriverState.
> Alternatively, you could add a generic check in bdrv_co_zone_mgmt() and
> similar functions.
I'll add this check to bdrv_co_zone_mgmt() to prevent the leak.
>
> > + 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;
> > + }
> > +
> > + if (((offset + len) < capacity && len & zone_size_mask) ||
>
> Here is how the capacity check is expressed without an integer overflow
> in block/block-backend.c:blk_check_byte_request():
>
> if (offset > len || len - offset < bytes) {
> ^^^^^^^^^^^^^^^^^^^^
> return -EIO;
> }
>
> It would be preferrable to use that form to avoid integer overflow.
Nice approach!
>
> > + offset + len > capacity) {
> > + 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 iov_len = 0;
> > + int64_t len = 0;
> > +
> > + if (*offset >= capacity) {
> > + error_report("*offset %" PRId64 " is equal to or greater than the"
> > + "device capacity %" PRId64 "", *offset, capacity);
> > + return -EINVAL;
>
> I'm not sure whether this should be -ENOSPC or -EINVAL. Did you look
> into what is most appropriate? pwrite(2) on Linux returns ENOSPC when
> writing beyond the end of the block device.
>
> This also applies to other places in this patch that check if offset >=
> capacity.
Ah, it should be -ENOSPC. That's also what a zoned null_blk device
returns when writing at/beyond device capacity.
>
> > + }
> > +
> > + /* offset + len should not pass the end of that zone starting from
> > offset */
> > + if (*offset & zone_size_mask) {
>
> The comment doesn't match the check that is performed here.
Will fix it.
Thanks for reviewing!
Sam