[PATCH 1/1] fsck.erofs: add --verify-digest for per-file SHA-256 verification

ChengyuZhu6 <[email protected]>
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
From: Chengyu Zhu <[email protected]>

Add a new --verify-digest option to fsck.erofs that verifies per-file
SHA-256 digests embedded by mkfs.erofs --xattr-inode-digest. For each
regular file, the stored digest xattr is compared against a freshly
computed SHA-256 of the decompressed file data. Any mismatch is
reported as filesystem corruption.

Signed-off-by: Chengyu Zhu <[email protected]>
---
 fsck/main.c           | 160 ++++++++++++++++++++++++++++++++++++++----
 include/erofs/xattr.h |   1 +
 lib/sha256.h          |   4 +-
 lib/xattr.c           |  33 +++++++++
 man/fsck.erofs.1      |   7 ++
 5 files changed, 190 insertions(+), 15 deletions(-)

diff --git a/fsck/main.c b/fsck/main.c
index 21ada19..efc92c5 100644
--- a/fsck/main.c
+++ b/fsck/main.c
@@ -15,6 +15,7 @@
 #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);
 
@@ -42,6 +43,8 @@ struct erofsfsck_cfg {
 	erofs_nid_t nid;
 	const char *inode_path;
 	bool nosbcrc;
+	bool verify_digest;
+	char *digest_xattr_name;
 };
 static struct erofsfsck_cfg fsckcfg;
 
@@ -64,6 +67,7 @@ static struct option long_options[] = {
 	{"nid", required_argument, 0, 15},
 	{"path", required_argument, 0, 16},
 	{"no-sbcrc", no_argument, 0, 512},
+	{"verify-digest", no_argument, 0, 17},
 	{0, 0, 0, 0},
 };
 
@@ -117,6 +121,8 @@ 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"
+		" --verify-digest        verify per-file SHA-256 digests embedded by\n"
+		"                        mkfs.erofs --xattr-inode-digest\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.verify_digest = true;
+			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,7 +769,8 @@ static void erofsfsck_hardlink_exit(void)
 	}
 }
 
-static inline int erofs_extract_file(struct erofs_inode *inode)
+static inline int erofs_extract_file(struct erofs_inode *inode,
+				     struct sha256_state *digest)
 {
 	bool tryagain = true;
 	int ret, fd;
@@ -775,7 +806,7 @@ again:
 	}
 
 	/* verify data chunk layout */
-	ret = erofs_verify_inode_data(inode, fd);
+	ret = erofs_verify_inode_data(inode, fd, digest);
 	close(fd);
 	return ret;
 }
@@ -790,7 +821,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;
 
@@ -844,7 +875,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;
 
@@ -925,15 +956,81 @@ static int erofsfsck_dirent_iter(struct erofs_dir_context *ctx)
 	return ret;
 }
 
+static int erofsfsck_compute_digest(struct erofs_inode *inode,
+				    int fd, u8 *out)
+{
+	struct sha256_state md;
+	int ret;
+
+	erofs_sha256_init(&md);
+	ret = erofs_verify_inode_data(inode, fd, &md);
+	if (!ret)
+		erofs_sha256_done(&md, out);
+	return ret;
+}
+
+static int erofsfsck_verify_file_digest(struct erofs_inode *inode,
+					const u8 *computed_digest)
+{
+	u8 stored[32 + EROFS_SHA256_PREFIX_LEN];
+	u8 computed[32];
+	const u8 *digest = computed_digest;
+	int ret;
+
+	ret = erofs_getxattr(inode, fsckcfg.digest_xattr_name,
+			     (char *)stored, sizeof(stored));
+	if (ret == -ENODATA) {
+		erofs_warn("no digest xattr for nid %llu, skipped",
+			   inode->nid | 0ULL);
+		return 0;
+	}
+	if (ret < 0)
+		return ret;
+	if (ret != sizeof(stored) ||
+	    strncmp((const char *)stored, EROFS_SHA256_PREFIX, EROFS_SHA256_PREFIX_LEN)) {
+		erofs_err("malformed digest xattr @ nid %llu (size=%d)",
+			  inode->nid | 0ULL, ret);
+		return -EFSCORRUPTED;
+	}
+
+	if (!digest) {
+		ret = erofsfsck_compute_digest(inode, -1, computed);
+		if (ret)
+			return ret;
+		digest = computed;
+	}
+
+	if (memcmp(digest, stored + EROFS_SHA256_PREFIX_LEN, 32)) {
+		erofs_err("digest MISMATCH @ nid %llu",
+			  inode->nid | 0ULL);
+		return -EFSCORRUPTED;
+	}
+	return 0;
+}
+
 static int erofsfsck_extract_inode(struct erofs_inode *inode)
 {
+	u8 computed_digest[32];
+	bool has_digest = false;
 	int ret;
 	char *oldpath;
 
 	if (!fsckcfg.extract_path || erofs_is_packed_inode(inode)) {
 verify:
 		/* verify data chunk layout */
-		return erofs_verify_inode_data(inode, -1);
+		if (fsckcfg.verify_digest &&
+		    S_ISREG(inode->i_mode) && inode->i_size > 0) {
+			ret = erofsfsck_compute_digest(inode, -1,
+						       computed_digest);
+			if (ret)
+				return ret;
+			has_digest = true;
+		} else {
+			ret = erofs_verify_inode_data(inode, -1, NULL);
+			if (ret)
+				return ret;
+		}
+		goto check_digest;
 	}
 
 	oldpath = erofsfsck_hardlink_find(inode->nid);
@@ -950,9 +1047,21 @@ verify:
 	case S_IFDIR:
 		ret = erofs_extract_dir(inode);
 		break;
-	case S_IFREG:
-		ret = erofs_extract_file(inode);
+	case S_IFREG: {
+		struct sha256_state md;
+
+		if (fsckcfg.verify_digest && inode->i_size > 0) {
+			erofs_sha256_init(&md);
+			ret = erofs_extract_file(inode, &md);
+			if (!ret) {
+				erofs_sha256_done(&md, computed_digest);
+				has_digest = true;
+			}
+		} else {
+			ret = erofs_extract_file(inode, NULL);
+		}
 		break;
+	}
 	case S_IFLNK:
 		ret = erofs_extract_symlink(inode);
 		break;
@@ -969,11 +1078,21 @@ verify:
 	}
 	if (ret && ret != -ECANCELED)
 		return ret;
+	if (ret == -ECANCELED && fsckcfg.verify_digest)
+		return ret;
 
 	/* record nid and old path for hardlink */
 	if (inode->i_nlink > 1 && !S_ISDIR(inode->i_mode))
 		ret = erofsfsck_hardlink_insert(inode->nid,
 						fsckcfg.extract_path);
+	if (ret)
+		return ret;
+
+check_digest:
+	if (fsckcfg.verify_digest &&
+	    S_ISREG(inode->i_mode) && inode->i_size > 0)
+		ret = erofsfsck_verify_file_digest(inode,
+						   has_digest ? computed_digest : NULL);
 	return ret;
 }
 
@@ -1096,6 +1215,18 @@ int main(int argc, char *argv[])
 		goto exit_put_super;
 	}
 
+	if (fsckcfg.verify_digest) {
+		fsckcfg.digest_xattr_name =
+			erofs_xattr_build_ishare_name(&g_sbi);
+		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();
 
@@ -1176,6 +1307,7 @@ exit_hardlink:
 	if (fsckcfg.extract_path)
 		erofsfsck_hardlink_exit();
 exit_put_super:
+	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 2356886..4fbf871 100644
--- a/include/erofs/xattr.h
+++ b/include/erofs/xattr.h
@@ -35,6 +35,7 @@ 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_build_ishare_name(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);
diff --git a/lib/sha256.h b/lib/sha256.h
index 6bcf03c..a9fca0e 100644
--- a/lib/sha256.h
+++ b/lib/sha256.h
@@ -4,6 +4,9 @@
 
 #include "erofs/defs.h"
 
+#define EROFS_SHA256_PREFIX	"sha256:"
+#define EROFS_SHA256_PREFIX_LEN	(sizeof(EROFS_SHA256_PREFIX) - 1)
+
 #if defined(HAVE_OPENSSL) && defined(HAVE_OPENSSL_EVP_H)
 #include <openssl/evp.h>
 struct sha256_state {
@@ -22,7 +25,6 @@ void erofs_sha256_init(struct sha256_state *md);
 int erofs_sha256_process(struct sha256_state *md,
 		const unsigned char *in, unsigned long inlen);
 int erofs_sha256_done(struct sha256_state *md, unsigned char *out);
-
 void erofs_sha256(const unsigned char *in, unsigned long in_size,
 		  unsigned char out[32]);
 
diff --git a/lib/xattr.c b/lib/xattr.c
index 3cf86e8..f78072e 100644
--- a/lib/xattr.c
+++ b/lib/xattr.c
@@ -1515,6 +1515,39 @@ int erofs_xattr_set_ishare_prefix(struct erofs_sb_info *sbi,
 	return 0;
 }
 
+char *erofs_xattr_build_ishare_name(struct erofs_sb_info *sbi)
+{
+	struct erofs_xattr_prefix_item *pf;
+	unsigned int idx, base_index;
+	const char *base;
+	size_t base_len, total;
+	char *name;
+
+	if (!erofs_sb_has_ishare_xattrs(sbi))
+		return NULL;
+
+	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;
+	if (!base_index || base_index >= ARRAY_SIZE(xattr_types))
+		return NULL;
+
+	base = xattr_types[base_index].prefix;
+	base_len = xattr_types[base_index].prefix_len;
+	total = base_len + pf->infix_len + 1;
+	name = malloc(total);
+	if (!name)
+		return NULL;
+
+	memcpy(name, base, base_len);
+	memcpy(name + base_len, pf->prefix->infix, pf->infix_len);
+	name[total - 1] = '\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 0f698da..c704aae 100644
--- a/man/fsck.erofs.1
+++ b/man/fsck.erofs.1
@@ -37,6 +37,13 @@ Optionally extract contents of the \fIIMAGE\fR to \fIdirectory\fR.
 .B "--no-sbcrc"
 Bypass the on-disk superblock checksum verification.
 .TP
+.B "--verify-digest"
+Verify the per-file SHA-256 digests that were embedded during image creation
+with \fBmkfs.erofs --xattr-inode-digest\fR. For each regular file, the stored
+digest is compared against a freshly computed SHA-256 of the (decompressed)
+file data. Any digest mismatch or digest-related verification error causes
+the filesystem to be reported as corrupted.
+.TP
 .BI "\-\-nid=" #
 Specify the target inode by its NID for checking or extraction.
 The default is the root inode.
-- 
2.51.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.