[PATCH v2] ocfs2: validate in-inode extent list on read

ZhengYuan Huang <[email protected]>
Newsgroups dev.linux.lists.ocfs2-devel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
[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 <[email protected]>
---
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.