[PATCH v4] erofs-utils: fsck: add `--xattr-inode-digest` support

Gao Xiang <[email protected]> Sat, 27 Jun 2026 11:03:39 +0800
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
From: Chengyu Zhu <[email protected]>

Add a new `--xattr-inode-digest` option to verify per-inode digests
embedded by `mkfs.erofs --xattr-inode-digest`.

For each regular inode with non-zero size, the stored digest xattr is
compared against a freshly computed SHA-256 of the inode data:  Any
mismatch is reported as filesystem corruption.

Note that enabling this option also forces extraction.

Signed-off-by: Chengyu Zhu <[email protected]>
Signed-off-by: Gao Xiang <[email protected]>
---
v4:
 - fix a false-positive compiler warning.
 fsck/main.c           | 129 +++++++++++++++++++++++++++++++++++++-----
 include/erofs/xattr.h |   3 +
 lib/xattr.c           |  56 ++++++++++++++++--
 man/fsck.erofs.1      |   4 ++
 4 files changed, 174 insertions(+), 18 deletions(-)

diff --git a/fsck/main.c b/fsck/main.c
index 759ca7df6722..b63fd135d7ad 100644
--- a/fsck/main.c
+++ b/fsck/main.c
@@ -15,9 +15,12 @@
 #include "erofs/xattr.h"
 #include "../lib/compressor.h"
 #include "../lib/liberofs_compress.h"
+#include "../lib/sha256.h"
 
 static int erofsfsck_check_inode(erofs_nid_t pnid, erofs_nid_t nid);
 
+static char erofsfsck_nullstr[] = "";
+
 struct erofsfsck_dirstack {
 	erofs_nid_t dirs[PATH_MAX];
 	int top;
@@ -29,6 +32,7 @@ struct erofsfsck_cfg {
 	u64 logical_blocks;
 	char *extract_path;
 	size_t extract_pos;
+	char *digest_xattr_name;
 	mode_t umask;
 	bool superuser;
 	bool corrupted;
@@ -64,6 +68,7 @@ static struct option long_options[] = {
 	{"nid", required_argument, 0, 15},
 	{"path", required_argument, 0, 16},
 	{"no-sbcrc", no_argument, 0, 512},
+	{"xattr-inode-digest", no_argument, 0, 17},
 	{0, 0, 0, 0},
 };
 
@@ -117,6 +122,7 @@ static void usage(int argc, char **argv)
 		" --nid=#                check or extract from the target inode of nid #\n"
 		" --path=X               check or extract from the target inode of path X\n"
 		" --no-sbcrc             bypass the superblock checksum verification\n"
+		" --xattr-inode-digest   verify per-inode digests recorded as extended attributes\n"
 		" --[no-]xattrs          whether to dump extended attributes (default off)\n"
 		"\n"
 		" -a, -A, -y             no-op, for compatibility with fsck of other filesystems\n"
@@ -257,6 +263,10 @@ static int erofsfsck_parse_options_cfg(int argc, char **argv)
 		case 16:
 			fsckcfg.inode_path = optarg;
 			break;
+		case 17:
+			fsckcfg.digest_xattr_name = erofsfsck_nullstr;
+			fsckcfg.check_decomp = true;
+			break;
 		case 512:
 			fsckcfg.nosbcrc = true;
 			break;
@@ -503,7 +513,8 @@ out:
 	return ret;
 }
 
-static int erofs_verify_inode_data(struct erofs_inode *inode, int outfd)
+static int erofs_verify_inode_data(struct erofs_inode *inode, int outfd,
+				   struct sha256_state *digest)
 {
 	struct erofs_map_blocks map = {
 		.buf = __EROFS_BUF_INITIALIZER,
@@ -546,11 +557,24 @@ static int erofs_verify_inode_data(struct erofs_inode *inode, int outfd)
 		if (map.m_la >= inode->i_size || !needdecode)
 			continue;
 
-		if (outfd >= 0 && !(map.m_flags & EROFS_MAP_MAPPED)) {
-			ret = lseek(outfd, map.m_llen, SEEK_CUR);
-			if (ret < 0) {
-				ret = -errno;
-				goto out;
+		if (!(map.m_flags & EROFS_MAP_MAPPED)) {
+			if (digest) {
+				static const char zeros[4096];
+				u64 remain = map.m_llen;
+
+				while (remain > 0) {
+					u64 chunk = remain > sizeof(zeros) ?
+						    sizeof(zeros) : remain;
+					erofs_sha256_process(digest,
+						(const u8 *)zeros, chunk);
+					remain -= chunk;
+				}
+			} else if (outfd >= 0) {
+				ret = lseek(outfd, map.m_llen, SEEK_CUR);
+				if (ret < 0) {
+					ret = -errno;
+					goto out;
+				}
 			}
 			continue;
 		}
@@ -596,6 +620,9 @@ static int erofs_verify_inode_data(struct erofs_inode *inode, int outfd)
 			if (ret)
 				goto out;
 
+			if (digest)
+				erofs_sha256_process(digest,
+					(const u8 *)buffer, map.m_llen);
 			if (outfd >= 0 && write(outfd, buffer, map.m_llen) < 0)
 				goto fail_eio;
 		} else {
@@ -609,6 +636,9 @@ static int erofs_verify_inode_data(struct erofs_inode *inode, int outfd)
 				if (ret)
 					goto out;
 
+				if (digest)
+					erofs_sha256_process(digest,
+						(const u8 *)raw, count);
 				if (outfd >= 0 && write(outfd, raw, count) < 0)
 					goto fail_eio;
 				map.m_llen -= count;
@@ -643,7 +673,7 @@ static inline int erofs_extract_dir(struct erofs_inode *inode)
 	erofs_dbg("create directory %s", fsckcfg.extract_path);
 
 	/* verify data chunk layout */
-	ret = erofs_verify_inode_data(inode, -1);
+	ret = erofs_verify_inode_data(inode, -1, NULL);
 	if (ret)
 		return ret;
 
@@ -739,6 +769,56 @@ static void erofsfsck_hardlink_exit(void)
 	}
 }
 
+static int erofsfsck_verify_file_digest(struct erofs_inode *inode,
+					const u8 *digest)
+{
+	u8 stored[32 + sizeof("sha256:") - 1];
+	int ret;
+
+	ret = __erofs_getxattr(inode, fsckcfg.digest_xattr_name,
+			       (char *)stored, sizeof(stored), true);
+	if (ret == -ENODATA) {
+		erofs_warn("no digest xattr for nid %llu, skipped",
+			   inode->nid | 0ULL);
+		return 0;
+	} else if (ret < 0)
+		return ret;
+
+	if (ret != sizeof(stored) ||
+	    memcmp(stored, "sha256:", sizeof("sha256:") - 1)) {
+		erofs_err("unidentified digest xattr @ nid %llu (size=%d)",
+			  inode->nid | 0ULL, ret);
+		return -EFSCORRUPTED;
+	}
+
+	if (memcmp(digest, stored + sizeof("sha256:") - 1, 32)) {
+		erofs_err("digest MISMATCH @ nid %llu",
+			  inode->nid | 0ULL);
+		return -EFSCORRUPTED;
+	}
+	return 0;
+}
+
+static int erofsfsck_calc_inode_data(struct erofs_inode *inode, int outfd)
+{
+	int ret;
+
+	if (fsckcfg.digest_xattr_name &&
+	    S_ISREG(inode->i_mode) && inode->i_size > 0) {
+		struct sha256_state md;
+		u8 out[32];
+
+		erofs_sha256_init(&md);
+		ret = erofs_verify_inode_data(inode, outfd, &md);
+		erofs_sha256_done(&md, out);
+
+		if (ret)
+			return ret;
+		return erofsfsck_verify_file_digest(inode, out);
+	}
+	return erofs_verify_inode_data(inode, outfd, NULL);
+}
+
 static inline int erofs_extract_file(struct erofs_inode *inode)
 {
 	bool tryagain = true;
@@ -774,8 +854,7 @@ again:
 		return -errno;
 	}
 
-	/* verify data chunk layout */
-	ret = erofs_verify_inode_data(inode, fd);
+	ret = erofsfsck_calc_inode_data(inode, fd);
 	close(fd);
 	return ret;
 }
@@ -791,7 +870,7 @@ static inline int erofs_extract_symlink(struct erofs_inode *inode)
 	erofs_dbg("extract symlink to path: %s", fsckcfg.extract_path);
 
 	/* verify data chunk layout */
-	ret = erofs_verify_inode_data(inode, -1);
+	ret = erofs_verify_inode_data(inode, -1, NULL);
 	if (ret)
 		return ret;
 
@@ -845,7 +924,7 @@ static int erofs_extract_special(struct erofs_inode *inode)
 	erofs_dbg("extract special to path: %s", fsckcfg.extract_path);
 
 	/* verify data chunk layout */
-	ret = erofs_verify_inode_data(inode, -1);
+	ret = erofs_verify_inode_data(inode, -1, NULL);
 	if (ret)
 		return ret;
 
@@ -928,13 +1007,13 @@ static int erofsfsck_dirent_iter(struct erofs_dir_context *ctx)
 
 static int erofsfsck_extract_inode(struct erofs_inode *inode)
 {
-	int ret;
 	char *oldpath;
+	int ret;
 
 	if (!fsckcfg.extract_path || erofs_is_packed_inode(inode)) {
 verify:
 		/* verify data chunk layout */
-		return erofs_verify_inode_data(inode, -1);
+		return erofsfsck_calc_inode_data(inode, -1);
 	}
 
 	oldpath = erofsfsck_hardlink_find(inode->nid);
@@ -968,7 +1047,8 @@ verify:
 			inode->i_mode, inode->nid | 0ULL);
 		goto verify;
 	}
-	if (ret && ret != -ECANCELED)
+
+	if (ret && (ret != -ECANCELED || fsckcfg.digest_xattr_name))
 		return ret;
 
 	/* record nid and old path for hardlink */
@@ -1097,6 +1177,25 @@ int main(int argc, char *argv[])
 		goto exit_put_super;
 	}
 
+	if (fsckcfg.digest_xattr_name == erofsfsck_nullstr) {
+		fsckcfg.digest_xattr_name =
+			erofs_xattr_get_ishare_prefix(&g_sbi);
+		if (IS_ERR(fsckcfg.digest_xattr_name)) {
+			err = PTR_ERR(fsckcfg.digest_xattr_name);
+			erofs_err("failed to get ishare prefix: %s",
+				  erofs_strerror(err));
+			goto exit_put_super;
+		}
+
+		if (!fsckcfg.digest_xattr_name) {
+			erofs_err("image has no inode digest xattrs (was --xattr-inode-digest used during mkfs?)");
+			err = -ENODATA;
+			goto exit_put_super;
+		}
+		erofs_info("verifying digests using xattr \"%s\"",
+			   fsckcfg.digest_xattr_name);
+	}
+
 	if (fsckcfg.extract_path)
 		erofsfsck_hardlink_init();
 
@@ -1177,6 +1276,8 @@ exit_hardlink:
 	if (fsckcfg.extract_path)
 		erofsfsck_hardlink_exit();
 exit_put_super:
+	if (fsckcfg.digest_xattr_name != erofsfsck_nullstr)
+		free(fsckcfg.digest_xattr_name);
 	erofs_put_super(&g_sbi);
 exit_dev_close:
 	erofs_dev_close(&g_sbi);
diff --git a/include/erofs/xattr.h b/include/erofs/xattr.h
index 235688649592..5fe3e91a4054 100644
--- a/include/erofs/xattr.h
+++ b/include/erofs/xattr.h
@@ -35,9 +35,12 @@ int erofs_load_shared_xattrs_from_path(struct erofs_sb_info *sbi, const char *pa
 int erofs_xattr_insert_name_prefix(const char *prefix);
 int erofs_xattr_set_ishare_prefix(struct erofs_sb_info *sbi,
 				  const char *prefix);
+char *erofs_xattr_get_ishare_prefix(struct erofs_sb_info *sbi);
 void erofs_xattr_cleanup_name_prefixes(void);
 int erofs_xattr_flush_name_prefixes(struct erofs_importer *im, bool plain);
 int erofs_xattr_prefixes_init(struct erofs_sb_info *sbi);
+int __erofs_getxattr(struct erofs_inode *vi, const char *name,
+		     char *buffer, size_t buffer_size, bool hidden);
 int erofs_setxattr(struct erofs_inode *inode, int index, const char *name,
 		   const void *value, size_t size);
 int erofs_vfs_setxattr(struct erofs_inode *inode, const char *name,
diff --git a/lib/xattr.c b/lib/xattr.c
index 1891ac3f23ef..051fdd846146 100644
--- a/lib/xattr.c
+++ b/lib/xattr.c
@@ -1410,8 +1410,8 @@ static int erofs_xattr_iter_shared(struct erofs_xattr_iter *it,
 	return ret;
 }
 
-int erofs_getxattr(struct erofs_inode *vi, const char *name, char *buffer,
-		   size_t buffer_size)
+int __erofs_getxattr(struct erofs_inode *vi, const char *name,
+		     char *buffer, size_t buffer_size, bool hidden)
 {
 	int ret;
 	unsigned int prefix, prefixlen;
@@ -1424,8 +1424,12 @@ int erofs_getxattr(struct erofs_inode *vi, const char *name, char *buffer,
 	if (ret)
 		return ret;
 
-	if (!erofs_xattr_prefix_matches(name, &prefix, &prefixlen))
-		return -ENODATA;
+	if (!erofs_xattr_prefix_matches(name, &prefix, &prefixlen)) {
+		if (!hidden)
+			return -ENODATA;
+		prefixlen = 0;
+		prefix = 0;
+	}
 	it.index = prefix;
 	it.name = name + prefixlen;
 	it.len = strlen(it.name);
@@ -1445,6 +1449,12 @@ int erofs_getxattr(struct erofs_inode *vi, const char *name, char *buffer,
 	return ret ? ret : it.buffer_ofs;
 }
 
+int erofs_getxattr(struct erofs_inode *vi, const char *name,
+		   char *buffer, size_t buffer_size)
+{
+	return __erofs_getxattr(vi, name, buffer, buffer_size, false);
+}
+
 int erofs_listxattr(struct erofs_inode *vi, char *buffer, size_t buffer_size)
 {
 	int ret;
@@ -1515,6 +1525,44 @@ int erofs_xattr_set_ishare_prefix(struct erofs_sb_info *sbi,
 	return 0;
 }
 
+char *erofs_xattr_get_ishare_prefix(struct erofs_sb_info *sbi)
+{
+	struct erofs_xattr_prefix_item *pf = NULL;
+	unsigned int idx, base_index;
+	size_t base_len, infix_len;
+	char *name;
+
+	if (!erofs_sb_has_ishare_xattrs(sbi))
+		return NULL;
+
+	if (sbi->ishare_xattr_prefix_id & EROFS_XATTR_LONG_PREFIX) {
+		idx = sbi->ishare_xattr_prefix_id & EROFS_XATTR_LONG_PREFIX_MASK;
+		if (idx >= sbi->xattr_prefix_count)
+			return NULL;
+
+		pf = &sbi->xattr_prefixes[idx];
+		base_index = pf->prefix->base_index;
+		infix_len = pf->infix_len;
+	} else {
+		base_index = sbi->ishare_xattr_prefix_id &
+			EROFS_XATTR_LONG_PREFIX_MASK;
+		infix_len = 0;
+	}
+	if (base_index >= ARRAY_SIZE(xattr_types))
+		return ERR_PTR(-EFSCORRUPTED);
+
+	base_len = xattr_types[base_index].prefix_len;
+	name = malloc(base_len + infix_len + 1);
+	if (!name)
+		return ERR_PTR(-ENOMEM);
+
+	memcpy(name, xattr_types[base_index].prefix, base_len);
+	if (infix_len)
+		memcpy(name + base_len, pf->prefix->infix, infix_len);
+	name[base_len + infix_len] = '\0';
+	return name;
+}
+
 void erofs_xattr_cleanup_name_prefixes(void)
 {
 	struct ea_type_node *tnode, *n;
diff --git a/man/fsck.erofs.1 b/man/fsck.erofs.1
index 0f698da3b9b7..b2d35a7ded70 100644
--- a/man/fsck.erofs.1
+++ b/man/fsck.erofs.1
@@ -48,6 +48,10 @@ Specify the target inode by its path for checking or extraction. If both
 .BI "--[no-]xattrs"
 Whether to dump extended attributes during extraction (default off).
 .TP
+.B "\-\-xattr-inode-digest"
+Verify per-inode digests recorded as extended attributes during image
+creation with \fBmkfs.erofs \-\-xattr-inode-digest\fR.
+.TP
 \fB\-h\fR, \fB\-\-help\fR
 Display help string and exit.
 .TP
-- 
2.43.5