[PATCH 1/1] fsck.erofs: add --verify-digest for per-file SHA-256 verification
ChengyuZhu6
hudson at cyzhu.com
Thu Apr 9 12:27:53 AEST 2026
From: Chengyu Zhu <hudsonzhu at tencent.com>
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 <hudsonzhu at tencent.com>
---
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
More information about the Linux-erofs
mailing list