* [PATCH v2] ocfs2: validate in-inode extent list on read
@ 2026-04-23 9:41 ZhengYuan Huang
0 siblings, 0 replies; only message in thread
From: ZhengYuan Huang @ 2026-04-23 9:41 UTC (permalink / raw)
To: mark, jlbec, joseph.qi
Cc: ocfs2-devel, linux-kernel, baijiaju1990, r33s3n6, zzzccc427,
ZhengYuan Huang
[BUG]
A fuzzed OCFS2 image can flip the high byte of
di->id2.i_list.l_next_free_rec. Later
openat("./file0", O_WRONLY|O_CREAT|O_TRUNC, ...) reaches
ocfs2_commit_truncate() and indexes past the in-inode extent array.
KASAN: use-after-free in ocfs2_commit_truncate+0x16ea/0x1aa0 fs/ocfs2/alloc.c:7310
Read of size 1 at addr ffff888023e900c7 by task syz.0.11/360
Call Trace:
__dump_stack lib/dump_stack.c:94 [inline]
dump_stack_lvl+0xbe/0x130 lib/dump_stack.c:120
print_address_description mm/kasan/report.c:378 [inline]
print_report+0xd1/0x650 mm/kasan/report.c:482
kasan_report+0xfb/0x140 mm/kasan/report.c:595
__asan_report_load1_noabort+0x14/0x30 mm/kasan/report_generic.c:378
ocfs2_commit_truncate+0x16ea/0x1aa0 fs/ocfs2/alloc.c:7310
ocfs2_truncate_file+0xcdd/0x13c0 fs/ocfs2/file.c:509
ocfs2_setattr+0xa6d/0x1fd0 fs/ocfs2/file.c:1212
notify_change+0x4b5/0x1030 fs/attr.c:546
do_truncate+0x1d2/0x230 fs/open.c:68
handle_truncate fs/namei.c:3596 [inline]
do_open fs/namei.c:3979 [inline]
path_openat+0x260f/0x2ce0 fs/namei.c:4134
do_filp_open+0x1f6/0x430 fs/namei.c:4161
do_sys_openat2+0x117/0x1c0 fs/open.c:1437
do_sys_open fs/open.c:1452 [inline]
__do_sys_openat fs/open.c:1468 [inline]
__se_sys_openat fs/open.c:1463 [inline]
__x64_sys_openat+0x15b/0x220 fs/open.c:1463
...
[CAUSE]
ocfs2_validate_inode_block() checks dinode header fields, but it does not
validate the in-inode extent list layout for regular extent-bearing
dinodes. A corrupted l_next_free_rec can therefore exceed the fixed
record count. Truncate later uses l_next_free_rec - 1 as the last record
index and reads rec->e_flags from beyond el->l_recs[].
[FIX]
Validate the in-inode extent list when the dinode is read. Reject lists
with an inconsistent l_count, an out-of-range l_next_free_rec, or
malformed used records before they reach truncate and the other extent
consumers that assume a valid in-inode extent list.
Signed-off-by: ZhengYuan Huang <gality369@gmail.com>
---
v2:
- Rewrite the [BUG] and [CAUSE] sections around corrupted l_next_free_rec driving the truncate UAF
- Keep the code change unchanged because the existing read-side validation already blocks this variant
---
fs/ocfs2/inode.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 61 insertions(+), 1 deletion(-)
diff --git a/fs/ocfs2/inode.c b/fs/ocfs2/inode.c
index a510a0eb1adc..7fcd1ee4fd46 100644
--- a/fs/ocfs2/inode.c
+++ b/fs/ocfs2/inode.c
@@ -215,6 +215,61 @@ static int ocfs2_dinode_has_extents(struct ocfs2_dinode *di)
return 1;
}
+static int ocfs2_validate_inode_extent_list(struct super_block *sb,
+ struct buffer_head *bh,
+ struct ocfs2_dinode *di)
+{
+ struct ocfs2_extent_list *el = &di->id2.i_list;
+ u16 count = le16_to_cpu(el->l_count);
+ u16 next_free = le16_to_cpu(el->l_next_free_rec);
+ u16 expected = ocfs2_extent_recs_per_inode_with_xattr(sb, di);
+ u32 prev_end = 0;
+ bool have_prev = false;
+ int i;
+
+ if (count != expected)
+ return ocfs2_error(sb,
+ "Invalid dinode #%llu: extent list count %u (expected %u)\n",
+ (unsigned long long)bh->b_blocknr, count,
+ expected);
+
+ if (next_free > count)
+ return ocfs2_error(sb,
+ "Invalid dinode #%llu: extent list index %u (count %u)\n",
+ (unsigned long long)bh->b_blocknr, next_free,
+ count);
+
+ for (i = 0; i < next_free; i++) {
+ struct ocfs2_extent_rec *rec = &el->l_recs[i];
+ u32 rec_cpos = le32_to_cpu(rec->e_cpos);
+ u32 rec_clusters = ocfs2_rec_clusters(el, rec);
+ u32 rec_end = rec_cpos + rec_clusters;
+
+ if (!rec_clusters) {
+ if (!el->l_tree_depth && i == 0)
+ continue;
+
+ return ocfs2_error(sb,
+ "Invalid dinode #%llu: empty extent record %d at depth %u\n",
+ (unsigned long long)bh->b_blocknr,
+ i, le16_to_cpu(el->l_tree_depth));
+ }
+
+ if (have_prev &&
+ ((el->l_tree_depth && rec_cpos != prev_end) ||
+ (!el->l_tree_depth && rec_cpos < prev_end)))
+ return ocfs2_error(sb,
+ "Invalid dinode #%llu: extent record %d starts at %u after previous end %u\n",
+ (unsigned long long)bh->b_blocknr, i,
+ rec_cpos, prev_end);
+
+ prev_end = rec_end;
+ have_prev = true;
+ }
+
+ return 0;
+}
+
/*
* here's how inodes get read from disk:
* iget5_locked -> find_actor -> OCFS2_FIND_ACTOR
@@ -1552,6 +1607,12 @@ int ocfs2_validate_inode_block(struct super_block *sb,
}
}
+ if (ocfs2_dinode_has_extents(di)) {
+ rc = ocfs2_validate_inode_extent_list(sb, bh, di);
+ if (rc)
+ goto bail;
+ }
+
if ((le16_to_cpu(di->i_dyn_features) & OCFS2_HAS_REFCOUNT_FL) &&
!di->i_refcount_loc) {
rc = ocfs2_error(sb, "Inode #%llu has refcount flag but no i_refcount_loc\n",
@@ -1812,4 +1873,3 @@ const struct ocfs2_caching_operations ocfs2_inode_caching_ops = {
.co_io_lock = ocfs2_inode_cache_io_lock,
.co_io_unlock = ocfs2_inode_cache_io_unlock,
};
-
--
2.43.0
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-04-23 9:41 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-23 9:41 [PATCH v2] ocfs2: validate in-inode extent list on read ZhengYuan Huang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox