[PATCH v2] f2fs: verify inode data-block ownership at mount
From: Haoran Wang
Date: Mon Nov 09 2026 - 04:29:36 EST
At mount time, walk the NAT to enumerate live inode blocks and claim the
direct data blocks referenced by each regular inode's i_addr region in a
bitmap. If two inodes claim the same data block, fail the mount with
-EFSCORRUPTED, since at least one of them is forged or corrupted.
The check is NAT-driven, so blocks unreachable from the NAT - such as
stale images left by earlier mkfs runs on untrimmed disks - are ignored
and cannot trigger false positives.
Reported-by: kernel test robot <yi1.lai@xxxxxxxxx>
Closes: https://lore.kernel.org/oe-lkp/202610262143.a92547a9-lkp@xxxxxxxxx
Signed-off-by: Haoran Wang <haoranwangsec@xxxxxxxxx>
---
Changes since v1: fix the false positive reported by the kernel test
robot: v1 scanned the whole main area with sb_bread and trusted stale
inode blocks left on disk by earlier mkfs runs, failing the mount.
Rework the scan to walk the NAT so only NAT-reachable inodes are checked.
fs/f2fs/f2fs.h | 1 +
fs/f2fs/node.c | 98 +++++++++++++++++++++++++++++++++++++++++++++++
fs/f2fs/super.c | 5 ++
3 files changed, 104 insertions(+)
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -4000,6 +4000,7 @@
bool f2fs_need_dentry_mark(struct f2fs_sb_info *sbi, nid_t nid);
bool f2fs_is_checkpointed_node(struct f2fs_sb_info *sbi, nid_t nid);
bool f2fs_need_inode_block_update(struct f2fs_sb_info *sbi, nid_t ino);
+int f2fs_verify_inode_data_ownership(struct f2fs_sb_info *sbi);
int f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid,
struct node_info *ni, bool checkpoint_context);
pgoff_t f2fs_get_next_page_offset(struct dnode_of_data *dn, pgoff_t pgofs);
--- a/fs/f2fs/node.c
+++ b/fs/f2fs/node.c
@@ -3455,6 +3455,111 @@
return 0;
}
+
+static int f2fs_claim_data_addr(unsigned long *claimed, block_t main_b,
+ block_t total, block_t blkaddr)
+{
+ if (blkaddr == NULL_ADDR || blkaddr == NEW_ADDR)
+ return 0;
+ if (blkaddr < main_b || blkaddr >= total)
+ return 0;
+ if (test_and_set_bit(blkaddr - main_b, claimed))
+ return -EFSCORRUPTED;
+ return 0;
+}
+
+/*
+ * Verify that no data block in the main area is claimed by more than one
+ * regular inode. Walk the NAT (the authoritative index of live node
+ * blocks) instead of scanning the raw main area, so stale node blocks
+ * left on disk by previous filesystem generations are ignored. Only
+ * inodes reachable from the NAT are checked; inline-data files carry no
+ * separate data block and are out of scope.
+ */
+int f2fs_verify_inode_data_ownership(struct f2fs_sb_info *sbi)
+{
+ struct f2fs_nm_info *nm_i = NM_I(sbi);
+ struct f2fs_super_block *raw = F2FS_RAW_SUPER(sbi);
+ block_t main_b = le32_to_cpu(raw->main_blkaddr);
+ block_t total = le64_to_cpu(raw->block_count);
+ unsigned long *claimed;
+ struct f2fs_node *fn;
+ struct f2fs_inode *ri;
+ struct folio *folio;
+ unsigned long *bitmap_bits;
+ nid_t nid;
+ int err = 0;
+
+ bitmap_bits = kvzalloc_objs(*bitmap_bits,
+ BITS_TO_LONGS(total - main_b),
+ GFP_KERNEL);
+ if (!bitmap_bits)
+ return -ENOMEM;
+ claimed = bitmap_bits;
+
+ /* skip the f2fs-internal metadata inodes (NODE_INO, META_INO):
+ * their NAT entries are not user data and trip
+ * "inconsistent nat entry" on every lookup.
+ */
+ for (nid = F2FS_ROOT_INO(sbi); nid < nm_i->max_nid; nid++) {
+ struct node_info ni;
+ int base, end, i;
+
+ /* only live inode blocks: NAT entry valid and nid == ino */
+ if (f2fs_get_node_info(sbi, nid, &ni, false))
+ continue;
+ if (ni.blk_addr == NULL_ADDR || ni.blk_addr == NEW_ADDR)
+ continue;
+ if (ni.ino != nid)
+ continue;
+
+ folio = f2fs_get_node_folio(sbi, nid, NODE_TYPE_INODE);
+ if (IS_ERR(folio)) {
+ err = PTR_ERR(folio);
+ if (err == -ENOENT)
+ err = 0; /* node gone, skip */
+ else
+ break;
+ continue;
+ }
+ fn = F2FS_NODE(folio);
+ ri = &fn->i;
+ if (!S_ISREG(le16_to_cpu(ri->i_mode)))
+ goto next;
+ if (ri->i_inline & (F2FS_INLINE_DATA | F2FS_INLINE_DENTRY))
+ goto next;
+ if (le32_to_cpu(ri->i_flags) & F2FS_COMPR_FL)
+ goto next;
+ base = (ri->i_inline & F2FS_EXTRA_ATTR)
+ ? le16_to_cpu(ri->i_extra_isize) / 4 : 0;
+ end = DEF_ADDRS_PER_INODE;
+ if (ri->i_inline & F2FS_INLINE_XATTR) {
+ if (f2fs_sb_has_flexible_inline_xattr(sbi) &&
+ (ri->i_inline & F2FS_EXTRA_ATTR))
+ end -= le16_to_cpu(ri->i_inline_xattr_size);
+ else
+ end -= DEFAULT_INLINE_XATTR_ADDRS;
+ }
+ if (base > end || end > DEF_ADDRS_PER_INODE)
+ goto next;
+ for (i = base; i < end; i++) {
+ err = f2fs_claim_data_addr(claimed, main_b, total,
+ le32_to_cpu(ri->i_addr[i]));
+ if (err) {
+ f2fs_err(sbi, "inode %u claims data block %u already owned by another inode",
+ nid, le32_to_cpu(ri->i_addr[i]));
+ f2fs_folio_put(folio, true);
+ goto out;
+ }
+ }
+next:
+ f2fs_folio_put(folio, true);
+ }
+out:
+ kvfree(claimed);
+ return err;
+}
+
int f2fs_build_node_manager(struct f2fs_sb_info *sbi)
{
int err;
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -5372,6 +5372,12 @@
if (err)
goto free_root_inode;
+ err = f2fs_verify_inode_data_ownership(sbi);
+ if (err) {
+ f2fs_err(sbi, "data block referenced by multiple inodes");
+ goto free_root_inode;
+ }
+
err = f2fs_register_sysfs(sbi);
if (err)
goto free_compress_inode;