From: Yifan Zhao <stopire@gmail.com>
To: linux-erofs@lists.ozlabs.org
Cc: xiang@kernel.org
Subject: [PATCH v2 4/4] erofs-utils: mkfs: support dm-persistent file deltas
Date: Mon, 21 Sep 2026 21:35:35 +0800 [thread overview]
Message-ID: <20260921133535.2381246-4-stopire@gmail.com> (raw)
In-Reply-To: <20260921133535.2381246-1-stopire@gmail.com>
Parse and validate version 1 dm-persistent snapshot exception stores,
then convert exceptions into sorted DATA extents. Inherit ranges
without exceptions and clip the last update to the target file size.
Require the source chunk size to match --chunksize, independently of the
parent chunk geometry.
Assisted-by: Codex:gpt-6-astra
Signed-off-by: Yifan Zhao <stopire@gmail.com>
---
lib/Makefile.am | 3 +-
lib/deltas/dm_persistent.c | 199 +++++++++++++++++++++++++++++++++++
lib/liberofs_dm_persistent.h | 9 ++
man/mkfs.erofs.1 | 8 ++
mkfs/main.c | 2 +
5 files changed, 220 insertions(+), 1 deletion(-)
create mode 100644 lib/deltas/dm_persistent.c
create mode 100644 lib/liberofs_dm_persistent.h
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 7f86409..7d7411e 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -24,6 +24,7 @@ noinst_HEADERS = $(top_srcdir)/include/erofs_fs.h \
$(top_srcdir)/lib/liberofs_cache.h \
$(top_srcdir)/lib/liberofs_chunk.h \
$(top_srcdir)/lib/liberofs_compress.h \
+ $(top_srcdir)/lib/liberofs_dm_persistent.h \
$(top_srcdir)/lib/liberofs_file_delta.h \
$(top_srcdir)/lib/liberofs_fragments.h \
$(top_srcdir)/lib/liberofs_private.h \
@@ -46,7 +47,7 @@ liberofs_la_SOURCES = config.c io.c cache.c super.c inode.c xattr.c exclude.c \
fragments.c dedupe.c uuid_unparse.c uuid.c tar.c \
block_list.c rebuild.c diskbuf.c bitops.c dedupe_ext.c \
vmdk.c metabox.c global.c importer.c base64.c \
- file_delta.c
+ file_delta.c deltas/dm_persistent.c
liberofs_la_CFLAGS = -Wall ${libuuid_CFLAGS} -I$(top_srcdir)/include
liberofs_la_LIBADD = $(LIBEROFS_LIBS)
diff --git a/lib/deltas/dm_persistent.c b/lib/deltas/dm_persistent.c
new file mode 100644
index 0000000..d09bd89
--- /dev/null
+++ b/lib/deltas/dm_persistent.c
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: GPL-2.0+ OR MIT
+#include <config.h>
+#include "erofs/print.h"
+#include "liberofs_dm_persistent.h"
+
+#define EROFS_DM_SNAP_MAGIC 0x70416e53U
+#define EROFS_DM_DISK_VERSION 1U
+#define EROFS_DM_SECTOR_SIZE 512U
+
+struct erofs_dm_disk_header {
+ __le32 magic;
+ __le32 valid;
+ __le32 version;
+ __le32 chunk_size;
+} __packed;
+
+struct erofs_dm_disk_exception {
+ __le64 old_chunk;
+ __le64 new_chunk;
+} __packed;
+
+struct erofs_dm_exception {
+ erofs_off_t offset, source_offset;
+};
+
+/* Temporary sorting storage, released before the parser returns. */
+struct erofs_dm_exceptions {
+ struct erofs_dm_exception *entries;
+ size_t count, capacity;
+};
+
+static int erofs_dm_pread(struct erofs_vfile *vf, erofs_off_t size,
+ void *buf, size_t len, erofs_off_t offset)
+{
+ const struct erofs_delta_extent source = { .vf = vf, .length = size };
+ int ret;
+
+ ret = erofs_delta_read(&source, offset, buf, len);
+ return ret == -EINVAL || ret == -EIO ? -EFSCORRUPTED : ret;
+}
+
+static int erofs_dm_exception_cmp(const void *a, const void *b)
+{
+ const struct erofs_dm_exception *ea = a, *eb = b;
+
+ return (ea->offset > eb->offset) - (ea->offset < eb->offset);
+}
+
+static int erofs_dm_add_exception(struct erofs_dm_exceptions *exceptions,
+ u64 max_chunks, erofs_off_t offset,
+ erofs_off_t source_offset)
+{
+ struct erofs_dm_exception *p;
+ size_t capacity;
+
+ if (exceptions->count >= max_chunks)
+ return -EINVAL;
+ if (exceptions->count == exceptions->capacity) {
+ capacity = exceptions->capacity ? exceptions->capacity * 2 : 64;
+ if (capacity < exceptions->capacity)
+ return -E2BIG;
+ if (capacity > max_chunks)
+ capacity = max_chunks;
+ if (capacity > SIZE_MAX / sizeof(*p))
+ return -E2BIG;
+ p = realloc(exceptions->entries, capacity * sizeof(*p));
+ if (!p)
+ return -ENOMEM;
+ exceptions->entries = p;
+ exceptions->capacity = capacity;
+ }
+ exceptions->entries[exceptions->count++] = (struct erofs_dm_exception) {
+ .offset = offset,
+ .source_offset = source_offset,
+ };
+ return 0;
+}
+
+static int erofs_dm_parse_exceptions(struct erofs_vfile *vf,
+ erofs_off_t source_size,
+ erofs_off_t target_size,
+ erofs_off_t chunk_bytes,
+ struct list_head *extents)
+{
+ /* Read metadata in bounded batches, independently of the dm chunk size. */
+ struct erofs_dm_disk_exception buf[256];
+ struct erofs_dm_exceptions exceptions = {};
+ u64 per_area = chunk_bytes / sizeof(buf[0]), stride = per_area + 1;
+ u64 source_chunks = source_size / chunk_bytes;
+ u64 max_chunks = target_size / chunk_bytes + !!(target_size % chunk_bytes);
+ erofs_off_t metadata_chunk = 1, metadata_offset, offset, source_offset;
+ u64 i, j, nr;
+ size_t n;
+ int ret;
+
+ for (;;) {
+ /* Chunk 0 is the header; metadata areas start at 1 + n * stride. */
+ if (metadata_chunk >= source_chunks) {
+ ret = -EFSCORRUPTED;
+ goto out;
+ }
+ metadata_offset = metadata_chunk * chunk_bytes;
+ for (i = 0; i < per_area; i += nr) {
+ nr = min_t(u64, per_area - i, ARRAY_SIZE(buf));
+ ret = erofs_dm_pread(vf, source_size, buf, nr * sizeof(buf[0]),
+ metadata_offset + i * sizeof(buf[0]));
+ if (ret)
+ goto out;
+ for (j = 0; j < nr; ++j) {
+ u64 old_chunk = le64_to_cpu(buf[j].old_chunk);
+ u64 new_chunk = le64_to_cpu(buf[j].new_chunk);
+
+ /* The first zero new_chunk terminates the exception table. */
+ if (!new_chunk)
+ goto terminated;
+ if (new_chunk % stride == 1 ||
+ new_chunk >= source_chunks) {
+ ret = -EFSCORRUPTED;
+ goto out;
+ }
+ if (old_chunk >= max_chunks) {
+ ret = -EINVAL;
+ goto out;
+ }
+ offset = old_chunk * chunk_bytes;
+ source_offset = new_chunk * chunk_bytes;
+ ret = erofs_dm_add_exception(&exceptions, max_chunks,
+ offset, source_offset);
+ if (ret)
+ goto out;
+ }
+ }
+ if (check_add_overflow(metadata_chunk, stride, &metadata_chunk)) {
+ ret = -EFSCORRUPTED;
+ goto out;
+ }
+ }
+terminated:
+ /* Exceptions are stored in commit order, not logical file order. */
+ if (exceptions.count > 1)
+ qsort(exceptions.entries, exceptions.count,
+ sizeof(*exceptions.entries), erofs_dm_exception_cmp);
+ for (n = 0; n < exceptions.count; ++n) {
+ const struct erofs_dm_exception *e = &exceptions.entries[n];
+
+ if (n && exceptions.entries[n - 1].offset == e->offset) {
+ ret = -EINVAL;
+ goto out;
+ }
+ ret = erofs_delta_add(extents, false, e->offset,
+ min_t(erofs_off_t, chunk_bytes, target_size - e->offset),
+ vf, e->source_offset);
+ if (ret)
+ goto out;
+ }
+ ret = 0;
+out:
+ free(exceptions.entries);
+ return ret;
+}
+
+static int erofs_dm_persistent_parse(struct erofs_vfile *vf,
+ erofs_off_t source_size,
+ erofs_off_t target_size,
+ erofs_off_t expected_unit,
+ struct list_head *extents)
+{
+ struct erofs_dm_disk_header header;
+ erofs_off_t chunk_bytes;
+ u32 sectors;
+ int ret;
+
+ BUILD_BUG_ON(sizeof(header) != 16);
+ BUILD_BUG_ON(sizeof(struct erofs_dm_disk_exception) != 16);
+ ret = erofs_dm_pread(vf, source_size, &header, sizeof(header), 0);
+ if (ret)
+ return ret;
+ if (le32_to_cpu(header.magic) != EROFS_DM_SNAP_MAGIC ||
+ le32_to_cpu(header.version) != EROFS_DM_DISK_VERSION)
+ return -EFSCORRUPTED;
+ if (le32_to_cpu(header.valid) != 1)
+ return -EINVAL;
+ sectors = le32_to_cpu(header.chunk_size);
+ if (!sectors || (sectors & (sectors - 1)) || sectors > (INT_MAX >> 9))
+ return -EFSCORRUPTED;
+ chunk_bytes = (erofs_off_t)sectors * EROFS_DM_SECTOR_SIZE;
+ if (chunk_bytes != expected_unit) {
+ erofs_err("dm-persistent chunk size %llu does not match %llu",
+ chunk_bytes | 0ULL, expected_unit | 0ULL);
+ return -EINVAL;
+ }
+ return erofs_dm_parse_exceptions(vf, source_size, target_size,
+ chunk_bytes, extents);
+}
+
+const struct erofs_delta_spec_ops erofs_file_delta_dm_persistent_ops = {
+ .name = "dm-persistent",
+ .parse = erofs_dm_persistent_parse,
+};
diff --git a/lib/liberofs_dm_persistent.h b/lib/liberofs_dm_persistent.h
new file mode 100644
index 0000000..190468b
--- /dev/null
+++ b/lib/liberofs_dm_persistent.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0+ OR MIT */
+#ifndef __EROFS_LIB_LIBEROFS_DM_PERSISTENT_H
+#define __EROFS_LIB_LIBEROFS_DM_PERSISTENT_H
+
+#include "liberofs_file_delta.h"
+
+extern const struct erofs_delta_spec_ops erofs_file_delta_dm_persistent_ops;
+
+#endif
diff --git a/man/mkfs.erofs.1 b/man/mkfs.erofs.1
index b599232..217abfa 100644
--- a/man/mkfs.erofs.1
+++ b/man/mkfs.erofs.1
@@ -357,6 +357,14 @@ unsupported.
Changed data is stored in the output image. The output requires the parent as
external device 1, followed by the parent's existing external devices in order.
\fISOURCE\fR is needed only during the build.
+.RS
+.TP
+.B dm-persistent
+\fISOURCE\fR must be a valid Linux dm-snapshot persistent v1 exception store,
+provided as a regular file or block device and unchanged throughout the build.
+Its chunk size must match \fB\-\-chunksize\fR. Ranges without exceptions
+inherit the parent contents.
+.RE
.TP
.BI "\-\-max-extent-bytes=" #
Specify maximum decompressed extent size in bytes. The default is unlimited.
diff --git a/mkfs/main.c b/mkfs/main.c
index 199762a..1328492 100644
--- a/mkfs/main.c
+++ b/mkfs/main.c
@@ -26,6 +26,7 @@
#include "erofs/compress_hints.h"
#include "../lib/compressor.h"
#include "../lib/liberofs_chunk.h"
+#include "../lib/liberofs_dm_persistent.h"
#include "../lib/liberofs_file_delta.h"
#include "../lib/liberofs_gzran.h"
#include "../lib/liberofs_metabox.h"
@@ -333,6 +334,7 @@ static unsigned int rebuild_src_count;
static LIST_HEAD(rebuild_src_list);
static LIST_HEAD(file_delta_specs);
static const struct erofs_delta_spec_ops * const file_delta_backends[] = {
+ &erofs_file_delta_dm_persistent_ops,
NULL,
};
static u8 fixeduuid[16];
--
2.55.0
prev parent reply other threads:[~2026-09-21 13:35 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-21 13:35 [PATCH v2 1/4] erofs-utils: lib: fix short write retries Yifan Zhao
2026-09-21 13:35 ` [PATCH v2 2/4] erofs-utils: lib: fix large chunk index writes Yifan Zhao
2026-09-21 13:35 ` [PATCH v2 3/4] erofs-utils: mkfs: add file-delta framework Yifan Zhao
2026-09-21 13:35 ` Yifan Zhao [this message]
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=20260921133535.2381246-4-stopire@gmail.com \
--to=stopire@gmail.com \
--cc=linux-erofs@lists.ozlabs.org \
--cc=xiang@kernel.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.