From: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
To: andrey.zhadchenko@virtuozzo.com
Cc: devel@openvz.org
Subject: [Devel] [PATCH hci-8.1 v11 10/10] dm-qcow2: truncate unused tail of image on suspend
Date: Mon, 9 Nov 2026 15:01:43 +0100 [thread overview]
Message-ID: <20261109140143.1408038-11-alexander.ivanov@virtuozzo.com> (raw)
In-Reply-To: <20261109140143.1408038-1-alexander.ivanov@virtuozzo.com>
Discard punches holes in place of freed clusters, so they hold no
physical extents, but it doesn't change the file size. On postsuspend
we truncate the image file only down to file_preallocated_area_start,
i.e. we drop preallocations only. So if clusters at the end of the
file are freed (refcount dropped to zero), the file size stays the
same and the image never shrinks.
Add qcow2_calc_used_end(), which scans refcount blocks backward from
file_preallocated_area_start and returns the end of the last cluster
with non-zero refcount, clamped to file_preallocated_area_start since
the last cluster may be partial. Refcount ranges with no allocated
refblock are skipped at once.
The scan walks R1 entries normal I/O may never touch, so it validates
them itself instead of letting calc_r2_page_id() WARN on a corrupted
one. Metadata pages are taken from md cache or read synchronously.
This is called on suspended device after md writeback, so R1 and R2
can't change under us. A failed relocation of the reftable leaves
file_preallocated_area_start beyond the space covered by the reftable,
so the scan starts at most from its end.
Truncate the file to the used end. Fall back to the old behaviour on
scan error and after a failed R1/R2 writeback, which leaves refcounts
on disk stale.
Refcounts may also be below real usage after a crash, since both QEMU
with lazy_refcounts and this driver write L2 before refcounts. The scan
would then cut off clusters L2 still points to, even in a session
without guest writes. With kernel_sets_dirty_bit such an image has the
dirty bit set at open, and the disabled dirty bit check lets it be
opened RW, so skip the scan for it.
In the default mode userspace sets the dirty bit on every RW open, so
the bit doesn't mean the image is inconsistent. There the scan is safe
only because userspace checks the image before an RW open. A v2 image
has no dirty bit, so the opened_dirty guard never applies to it, and
it is covered only by that check too.
The scan trusts refcounts, as the rest of the driver does. Only a walk
over all L2 tables would catch a zeroed or misplaced R1 entry, so
corrupted refcounts are left to the same image check as stale ones.
Close and reload to RO go through postsuspend too. On remove,
__dm_destroy() calls the presuspend and postsuspend hooks if the live
table isn't suspended yet. It neither stops the queue nor waits for
I/O, but the device is idle there, because dm_lock_for_deletion()
refuses to remove an open device.
On table swap, do_resume() suspends the old (RW) table before
dm_swap_table(). So the file is shrunk in these cases as well.
https://virtuozzo.atlassian.net/browse/VSTOR-148171
Feature: dm-qcow2: block device over QCOW2 files driver
Signed-off-by: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
---
drivers/md/dm-ploop-target.c | 2 +-
drivers/md/dm-qcow2-map.c | 63 ++++++++++++++++++++++++++++++++++--
drivers/md/dm-qcow2-target.c | 21 +++++++++---
drivers/md/dm-qcow2.h | 1 +
4 files changed, 80 insertions(+), 7 deletions(-)
diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
index d8279c79ef56..b869c92bbe72 100644
--- a/drivers/md/dm-ploop-target.c
+++ b/drivers/md/dm-ploop-target.c
@@ -447,7 +447,7 @@ static struct ploop_worker *ploop_worker_create(struct ploop *ploop,
*
* FIXME: We must implement metadata reload on resume. After that we can
* implement preallocation truncation on suspend (see dm-qcow2 as it already
- * implements that in qcow2_parse_metadata and qcow2_truncate_preallocations).
+ * implements that in qcow2_parse_metadata and qcow2_truncate_unused_tail).
*/
static int ploop_ctr(struct dm_target *ti, unsigned int argc, char **argv)
{
diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
index 3bcaceab1a1d..621bc9b18e26 100644
--- a/drivers/md/dm-qcow2-map.c
+++ b/drivers/md/dm-qcow2-map.c
@@ -1090,6 +1090,11 @@ static int calc_refcounters_map(struct qcow2 *qcow2, loff_t pos,
return -EIO;
}
+static bool r1_entry_is_valid(struct qcow2 *qcow2, u64 entry)
+{
+ return !(entry & R1_RESERVED_ZERO_MASK) && CLU_OFF(qcow2, entry) == 0;
+}
+
static int calc_r2_page_id(struct qcow2 *qcow2, struct qcow2_map_item *r1,
struct qcow2_map_item *r2)
{
@@ -1097,8 +1102,7 @@ static int calc_r2_page_id(struct qcow2 *qcow2, struct qcow2_map_item *r1,
u32 bits = qcow2->refblock_bits;
loff_t pos;
- if (WARN_ON_ONCE((entry & R1_RESERVED_ZERO_MASK) ||
- (CLU_OFF(qcow2, entry) != 0)))
+ if (WARN_ON_ONCE(!r1_entry_is_valid(qcow2, entry)))
return -EIO;
/* The corresponding refcount block has not yet been allocated */
@@ -2168,6 +2172,61 @@ int qcow2_truncate_safe(struct file *file, loff_t new_len)
return do_ftruncate(file, new_len, 0);
}
+/*
+ * Return the end of the last cluster with non-zero refcount. Must be
+ * called on a suspended device after md writeback.
+ */
+loff_t qcow2_calc_used_end(struct qcow2 *qcow2)
+{
+ u32 clu_size = qcow2->clu_size;
+ struct qcow2_map_item r1, r2;
+ struct md_page *r1_md = NULL;
+ loff_t pos;
+ s32 i;
+ int ret;
+
+ pos = min(qcow2->file_preallocated_area_start,
+ qcow2->reftable_max_file_size);
+ pos = round_down(pos - 1, clu_size);
+ while (pos >= 0) {
+ if (calc_refcounters_map(qcow2, pos, &r1, &r2) < 0)
+ return -EIO;
+
+ if (!r1_md || r1_md->id != r1.page_id) {
+ ret = qcow2_md_page_read_sync(qcow2, r1.page_id, &r1_md);
+ if (ret)
+ return ret;
+ }
+ r1.md = r1_md;
+
+ if (!r1_entry_is_valid(qcow2, get_u64_from_be_page(r1_md->page,
+ r1.index_in_page)))
+ return -EIO;
+
+ ret = calc_r2_page_id(qcow2, &r1, &r2);
+ if (ret == -ENOENT) {
+ pos = (loff_t)r1.index * qcow2->refblock_entries * clu_size;
+ pos -= clu_size;
+ continue;
+ }
+
+ if (ret < 0)
+ return ret;
+
+ ret = qcow2_md_page_read_sync(qcow2, r2.page_id, &r2.md);
+ if (ret)
+ return ret;
+
+ for (i = r2.index_in_page; i >= 0; i--, pos -= clu_size) {
+ if (get_r2_entry(qcow2, r2.md, i))
+ return min_t(loff_t, pos + clu_size,
+ qcow2->file_preallocated_area_start);
+ }
+ }
+
+ return -EIO;
+}
+
static int truncate_prealloc_safe(struct qcow2 *qcow2, loff_t len, const char *func)
{
loff_t prealloc_len, max_prealloc_len = qcow2->reftable_max_file_size;
diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
index e28c47d4e728..30b6bb5c98f4 100644
--- a/drivers/md/dm-qcow2-target.c
+++ b/drivers/md/dm-qcow2-target.c
@@ -1061,21 +1061,34 @@ static void qcow2_dtr(struct dm_target *ti)
qcow2_tgt_destroy(tgt);
}
-static void qcow2_truncate_preallocations(struct dm_target *ti)
+static void qcow2_truncate_unused_tail(struct dm_target *ti)
{
struct qcow2_target *tgt = to_qcow2_target(ti);
struct qcow2 *qcow2 = top_qcow2_protected(ti);
- loff_t end = qcow2->file_preallocated_area_start;
+ loff_t used_end, end = qcow2->file_preallocated_area_start;
int ret;
if (!(dm_table_get_mode(ti->table) & FMODE_WRITE))
return;
+
+ if (tgt->md_writeback_error) {
+ QC_INFO(ti, "Skip shrinking to used end after md writeback error");
+ } else if (kernel_sets_dirty_bit && qcow2->opened_dirty) {
+ 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;
+ }
+
if (end == qcow2->file_size)
return;
ret = qcow2_truncate_safe(qcow2->file, end);
if (ret) {
- QC_ERR(ti, "Can't truncate preallocations");
+ QC_ERR(ti, "Can't truncate unused tail");
tgt->truncate_error = true;
return;
}
@@ -1156,7 +1169,7 @@ static void qcow2_postsuspend(struct dm_target *ti)
int ret;
qcow2_flush_deferred_activity_all(to_qcow2_target(ti));
- qcow2_truncate_preallocations(ti);
+ qcow2_truncate_unused_tail(ti);
if (dm_table_get_mode(ti->table) & FMODE_WRITE) {
ret = qcow2_set_image_file_features(qcow2, false);
diff --git a/drivers/md/dm-qcow2.h b/drivers/md/dm-qcow2.h
index ebab22d505e8..ed427a0a7699 100644
--- a/drivers/md/dm-qcow2.h
+++ b/drivers/md/dm-qcow2.h
@@ -416,6 +416,7 @@ void qcow2_ref_dec(struct qcow2_target *tgt, u8 ref_index);
int qcow2_inflight_ref_switch(struct qcow2_target *tgt);
void qcow2_flush_deferred_activity(struct qcow2_target *tgt, struct qcow2 *qcow2);
int qcow2_truncate_safe(struct file *file, loff_t new_len);
+loff_t qcow2_calc_used_end(struct qcow2 *qcow2);
void qcow2_merge_backward_work(struct work_struct *work);
void qcow2_merge_backward_cancel(struct qcow2_target *tgt);
--
2.53.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
next prev parent reply other threads:[~2026-11-09 14:03 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-09 14:01 [Devel] [PATCH hci-8.1 v11 00/10] " Alexander Ivanov
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 01/10] dm-qcow2: add qcow2_md_page_read_sync() helper Alexander Ivanov
2026-11-09 14:06 ` no-reply
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 02/10] dm-qcow2: drop duplicate prefix from QC_ERR/QC_INFO messages Alexander Ivanov
2026-11-09 14:05 ` no-reply
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 03/10] dm-qcow2: extend the file for a new refblock in allocate_cluster() Alexander Ivanov
2026-11-09 14:06 ` no-reply
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 04/10] dm-qcow2: complete wbd before ending the data qio Alexander Ivanov
2026-11-09 14:09 ` no-reply
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 05/10] dm-qcow2: flush lower md before clearing dirty bit on merge failure Alexander Ivanov
2026-11-09 14:08 ` no-reply
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 06/10] dm-qcow2: keep the dirty bit of an image that needs check Alexander Ivanov
2026-11-09 14:11 ` no-reply
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 07/10] dm-qcow2: flag the image for check if reftable relocation fails late Alexander Ivanov
2026-11-09 14:11 ` no-reply
2026-11-09 15:42 ` Konstantin Khorenko
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 08/10] dm-qcow2: leak clusters allocated for a failed L1/L2 writeback Alexander Ivanov
2026-11-09 14:14 ` no-reply
2026-11-09 14:36 ` Alexander Ivanov
2026-11-09 15:42 ` Konstantin Khorenko
2026-11-09 14:01 ` [Devel] [PATCH hci-8.1 v11 09/10] dm-qcow2: retry R1/R2 writeback that failed with ENOSPC Alexander Ivanov
2026-11-09 14:16 ` no-reply
2026-11-09 14:39 ` Alexander Ivanov
2026-11-09 15:42 ` Konstantin Khorenko
2026-11-09 14:01 ` Alexander Ivanov [this message]
2026-11-09 14:18 ` [Devel] [PATCH hci-8.1 v11 10/10] dm-qcow2: truncate unused tail of image on suspend no-reply
2026-11-09 14:47 ` Alexander Ivanov
2026-11-09 15:42 ` Konstantin Khorenko
2026-11-09 15:42 ` [Devel] [PATCH hci-8.1 v11 00/10] " Konstantin Khorenko
2026-11-09 15:54 ` Konstantin Khorenko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261109140143.1408038-11-alexander.ivanov@virtuozzo.com \
--to=alexander.ivanov@virtuozzo.com \
--cc=andrey.zhadchenko@virtuozzo.com \
--cc=devel@openvz.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.