[Devel] [PATCH hci-8.1 v6 6/6] dm-qcow2: truncate unused tail of image on suspend

Alexander Ivanov alexander.ivanov at virtuozzo.com
Thu Oct 22 15:54:31 MSK 2026


>> @@ -1363,6 +1367,8 @@ static void revert_l_entries_update(struct qcow2 *qcow2, struct wb_desc *wbd)
>>   		if (cleared)
>>   			continue; /* no cluster was allocated */
>>   
>> +		/* L1/L2 on disk may point to the cluster */
>> +		wbd->md->status |= MD_LX_REVERTED;
>>   		spin_unlock(&qcow2->md_pages_lock);
>>   		pos &= ~LX_REFCOUNT_EXACTLY_ONE;
>>   
>>   		/*
>>   		 * R1/R2 should be cached, since we was able
>>   		 * to submit cluster allocation.
>>   		 */
>>   		ret = qcow2_handle_r1r2_maps(qcow2, pos, NULL, &r1, &r2, false);
>>   		if (WARN_ON_ONCE(ret <= 0))
>>   			continue;
>>   
>>   		mark_cluster_unused(qcow2, r2.md, r2.index_in_page, pos);
>>   		spin_lock(&qcow2->md_pages_lock);
> [Severity: Medium]
> Can the continue taken on WARN_ON_ONCE(ret <= 0) leave this loop running
> without md_pages_lock?
>
> The lock is dropped just above with spin_unlock(), and it is only retaken
> by the spin_lock() at the bottom of the loop body, which the continue
> skips. qcow2_handle_r1r2_maps() can return a value <= 0 in several cases:
>
>    qcow2_handle_r1r2_maps()
>      __handle_r1r2_maps()
>        calc_refcounters_map() fails with -EIO
>        calc_r2_page_id() returns -EIO or -ENOENT
>        __handle_md_page() returns 0 when the page is not cached and
>          qio is NULL
>      "refblock=%llu, while no snapshots" returns -EIO
>
> After the continue, the next iteration calls get_u64_from_be_page() and
> set_u64_to_be_page() on wbd->md->page without the lock. The caller
> complete_wbd() then does:
>
>    spin_lock_irqsave(&qcow2->md_pages_lock, flags);
>    revert_l_entries_update(qcow2, wbd);
>    clear_writeback_status(qcow2, wbd->md, wbd->ret, &wait_list, &end_list);
>    spin_unlock_irqrestore(&qcow2->md_pages_lock, flags);
>
> So clear_writeback_status() also runs unlocked, and the final unlock is
> unbalanced. The comment says R1/R2 should be cached, but nothing enforces
> that. This patch adds the MD_LX_REVERTED assignment in this same
> unlock window and relies on this error path. Should the lock be retaken
> before the continue?
>
>> @@ -2387,21 +2463,21 @@ static int relocate_refcount_table(struct qcow2 *qcow2, struct qio **qio)
> [ ... ]
>
>> @@ -2412,7 +2488,7 @@ static int relocate_refcount_table(struct qcow2 *qcow2, struct qio **qio)
>>   	for (i = pos; i < r2_end; i += clu_size) {
>>   		ret = qcow2_handle_r1r2_maps(qcow2, i, NULL, &r1, &r2, false);
>>   		if (WARN_ON_ONCE(ret <= 0))
>> -			goto err_free_r2_pages;
>> +			goto err_hdr_written;
>>   		mark_cluster_used(qcow2, r2.md, r2.index_in_page);
>>   	}
> [ ... ]
>
>> +err_hdr_written:
>> +	/* Header on disk points to new R1, refcounts may be inconsistent */
>> +	QC_ERR(qcow2->tgt->ti, "failed to relocate refcount table");
>> +	qcow2->tgt->md_writeback_error = true;
>>   err_free_r2_pages:
>>   	for (i = end; i < r2_end; i += clu_size) {
>>   		ret = handle_md_page(qcow2, i >> PAGE_SHIFT, NULL, &md);
>>   		if (ret <= 0)
>>   			break;
>>   		spin_lock_irq(&qcow2->md_pages_lock);
>>   		qcow2_md_page_erase(qcow2, md);
>>   		spin_unlock_irq(&qcow2->md_pages_lock);
>>   		qcow2_free_md_page(md);
>>   	}
> [Severity: Medium]
> Can this cleanup free R2 md pages that are still linked on
> slow_wb_batch_list?
>
> Earlier in the function, mark_cluster_used() calls
> qcow2_md_make_dirty(qcow2, r2_md, true). That sets MD_DIRTY, does
> list_add_tail(&md->wb_link, &qcow2->slow_wb_batch_list), and may arm
> slow_wb_timer. The R2 pages for the clusters in [end, r2_end) are the
> ones the cleanup loop frees.
>
> If a later WARN_ON_ONCE(ret <= 0) check fails, the code reaches
> err_hdr_written and falls into err_free_r2_pages. There,
> qcow2_md_page_erase() only does rb_erase(&md->node, &qcow2->md_pages).
> qcow2_free_md_page() then frees the page. Neither one does
> list_del(&md->wb_link) or clears MD_DIRTY.
>
> The freed md_page stays on slow_wb_batch_list. The timer callback or the
> worker can later splice that list and read md->status and md->wbd of the
> freed md. Could the dirty pages be unlinked from the writeback list
> before they are freed here? This patch routes more failure paths into
> this shared cleanup.
1, 2: pre-existing, reachable only via WARN paths; this patch neither adds
new jumps into err_free_r2_pages nor sets MD_LX_REVERTED outside the
lock. Will be fixed separately.
>> diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
>> --- a/drivers/md/dm-qcow2-target.c
>> +++ b/drivers/md/dm-qcow2-target.c
>> @@ -170,6 +170,7 @@ static void qcow2_free_md_pages_tree(struct rb_root *root)
>>   void qcow2_flush_deferred_activity(struct qcow2_target *tgt, struct qcow2 *qcow2)
> [ ... ]
>
>> @@ -191,13 +192,17 @@ void qcow2_flush_deferred_activity(struct qcow2_target *tgt, struct qcow2 *qcow2
>>   	     node; node = rb_next(node)) {
>>   		md = rb_entry(node, struct md_page, node);
>>   		/* FIXME: call md_make_dirty() and try once again? */
>> -		if (md->status & MD_WRITEBACK_ERROR) {
>> -			QC_ERR(tgt->ti, "Failed to write dirty pages");
>> -			tgt->md_writeback_error = true;
>> -			break;
>> -		}
>> +		if (md->status & MD_WRITEBACK_ERROR)
>> +			wb_error = true;
>> +		if (md->status & MD_LX_REVERTED)
>> +			tgt->lx_writeback_error = true;
>>   	}
> [Severity: Medium]
> Can this set the sticky tgt->lx_writeback_error from an image that is
> about to be destroyed?
>
> qcow2_flush_deferred_activity() is also called from a non-suspend path:
>
>    qcow2_merge_backward_complete()
>      qcow2_flush_deferred_activity(tgt, qcow2); /* Delayed md pages */
>      qcow2->lower = NULL;
>      ...
>      qcow2_destroy(qcow2);
>
> If a page of the image being removed from the chain is still marked
> MD_LX_REVERTED, the target-wide flag is set. The image and its md pages
> are then freed, but the flag stays set. Nothing clears it.
>
> qcow2_wants_check() now includes the flag, so qcow2_preresume() fails
> with:
>
>    if (qcow2_wants_check(tgt)) {
>            QC_ERR(ti, "image check and target reload are required");
>            return -EIO;
>    }
>
> The surviving image is then refused resume, and
> qcow2_truncate_preallocations() skips the shrink, even though the stale
> page no longer exists. The existing md_writeback_error has the same
> imprecision on this path, but is that intended for the new flag as well?
Intentional, matches md_writeback_error on the same path; it only fires
after a failed L1/L2 writeback during the merge.
>> @@ -1048,11 +1053,34 @@ static void qcow2_truncate_preallocations(struct dm_target *ti)
> [ ... ]
>
>> +	if (tgt->md_writeback_error) {
>> +		QC_INFO(ti, "Skip shrinking to used end after md writeback error");
>> +	} else if (tgt->lx_writeback_error) {
>> +		QC_INFO(ti, "Skip shrinking to used end after reverted L1/L2 writeback");
>> +	} else if (kernel_sets_dirty_bit &&
>> +		   (qcow2->hdr.incompatible_features &
>> +		    INCOMPATIBLE_FEATURES_DIRTY_BIT)) {
>> +		QC_INFO(ti, "Skip shrinking to used end of dirty image");
>> +	} else {
>> +		used_end = qcow2_calc_used_end(qcow2);
>> +		if (used_end < 0)
>> +			QC_ERR(ti, "Can't find used end: %pe", ERR_PTR(used_end));
>> +		else
>> +			end = used_end;
>> +	}
> [Severity: Medium]
> Is this guard enough to protect against truncating below clusters that
> L1/L2 still reference after a crash?
>
> The only protection against stale refcounts is:
>
>    kernel_sets_dirty_bit && (qcow2->hdr.incompatible_features &
>                              INCOMPATIBLE_FEATURES_DIRTY_BIT)
>
> kernel_sets_dirty_bit is a static bool that defaults to false, so this
> branch is inactive by default. In qcow2_check_convert_hdr(), v2 images
> return early:
>
>    if (hdr->version == 2)
>            return 0;
>
>    hdr->incompatible_features = be64_to_cpu(raw_hdr->incompatible_features);
>
> That return comes before incompatible_features is read, so the field
> stays 0 and the guard can never fire for v2 images. The open-time
> dirty-bit check in qcow2_check_convert_hdr() is commented out:
>
>    //	if (!is_ro && kernel_sets_dirty_bit !=
>    //	    !(hdr->incompatible_features & INCOMPATIBLE_FEATURES_DIRTY_BIT))
>    //		return kernel_sets_dirty_bit ? -EUCLEAN : -ENOLCK;
>
> So the kernel does not enforce that the image was checked before an RW
> open.
>
> If refcounts are below real usage after a crash, qcow2_calc_used_end()
> returns an end below clusters that L2 still points to. It is computed
> purely from on-disk R2 entries. qcow2_truncate_safe() then cuts those
> clusters off, even on a plain suspend, remove or table swap with no guest
> writes in the session.
>
> The commit message says find_unused_cluster() already relies on the same
> userspace contract. That path is only reached once a guest write needs a
> new cluster, while this truncation runs on every RW suspend, remove and
> reload.
>
> Should the shrink be skipped or enforced in-kernel for v2 images and for
> the default kernel_sets_dirty_bit setup?
Covered in the commit message: userspace checks the image before
an RW open, including v2 images; find_unused_cluster() relies on the
same contract.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.openvz.org/pipermail/devel/attachments/20261022/b81920ad/attachment-0001.html>


More information about the Devel mailing list