From: Haoran Wang <haoranwangsec@gmail.com>
To: jaegeuk@kernel.org, chao@kernel.org
Cc: linux-f2fs-devel@lists.linux.dev, linux-kernel@vger.kernel.org,
yi1.lai@intel.com, haoranwangsec@gmail.com
Subject: [PATCH v2] f2fs: verify inode data-block ownership at mount
Date: Mon, 09 Nov 2026 17:16:01 +0800 [thread overview]
Message-ID: <20261109171601.75996-1-haoranwangsec@gmail.com> (raw)
In-Reply-To: <6ad2359d.0083e9c2.97e0f.79de@mx.google.com>
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@intel.com>
Closes: https://lore.kernel.org/oe-lkp/202610262143.a92547a9-lkp@intel.com
Signed-off-by: Haoran Wang <haoranwangsec@gmail.com>
---
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;
prev parent reply other threads:[~2026-11-09 9:16 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-16 14:32 [PATCH] " Haoran Wang
2026-10-27 2:14 ` kernel test robot
2026-11-09 9:16 ` Haoran Wang [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=20261109171601.75996-1-haoranwangsec@gmail.com \
--to=haoranwangsec@gmail.com \
--cc=chao@kernel.org \
--cc=jaegeuk@kernel.org \
--cc=linux-f2fs-devel@lists.linux.dev \
--cc=linux-kernel@vger.kernel.org \
--cc=yi1.lai@intel.com \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®