| From c6ad7fd16657ebd34a87a97d9588195aae87597d Mon Sep 17 00:00:00 2001 |
| From: Chao Yu <chao@kernel.org> |
| Date: Wed, 14 Sep 2022 19:51:51 +0800 |
| Subject: f2fs: fix to do sanity check on summary info |
| |
| From: Chao Yu <chao@kernel.org> |
| |
| commit c6ad7fd16657ebd34a87a97d9588195aae87597d upstream. |
| |
| As Wenqing Liu reported in bugzilla: |
| |
| https://bugzilla.kernel.org/show_bug.cgi?id=216456 |
| |
| BUG: KASAN: use-after-free in recover_data+0x63ae/0x6ae0 [f2fs] |
| Read of size 4 at addr ffff8881464dcd80 by task mount/1013 |
| |
| CPU: 3 PID: 1013 Comm: mount Tainted: G W 6.0.0-rc4 #1 |
| Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.15.0-1 04/01/2014 |
| Call Trace: |
| dump_stack_lvl+0x45/0x5e |
| print_report.cold+0xf3/0x68d |
| kasan_report+0xa8/0x130 |
| recover_data+0x63ae/0x6ae0 [f2fs] |
| f2fs_recover_fsync_data+0x120d/0x1fc0 [f2fs] |
| f2fs_fill_super+0x4665/0x61e0 [f2fs] |
| mount_bdev+0x2cf/0x3b0 |
| legacy_get_tree+0xed/0x1d0 |
| vfs_get_tree+0x81/0x2b0 |
| path_mount+0x47e/0x19d0 |
| do_mount+0xce/0xf0 |
| __x64_sys_mount+0x12c/0x1a0 |
| do_syscall_64+0x38/0x90 |
| entry_SYSCALL_64_after_hwframe+0x63/0xcd |
| |
| The root cause is: in fuzzed image, SSA table is corrupted: ofs_in_node |
| is larger than ADDRS_PER_PAGE(), result in out-of-range access on 4k-size |
| page. |
| |
| - recover_data |
| - do_recover_data |
| - check_index_in_prev_nodes |
| - f2fs_data_blkaddr |
| |
| This patch adds sanity check on summary info in recovery and GC flow |
| in where the flows rely on them. |
| |
| After patch: |
| [ 29.310883] F2FS-fs (loop0): Inconsistent ofs_in_node:65286 in summary, ino:0, nid:6, max:1018 |
| |
| Cc: stable@vger.kernel.org |
| Reported-by: Wenqing Liu <wenqingliu0120@gmail.com> |
| Signed-off-by: Chao Yu <chao@kernel.org> |
| Signed-off-by: Jaegeuk Kim <jaegeuk@kernel.org> |
| Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| --- |
| fs/f2fs/gc.c | 10 +++++++++- |
| fs/f2fs/recovery.c | 15 ++++++++++++--- |
| 2 files changed, 21 insertions(+), 4 deletions(-) |
| |
| --- a/fs/f2fs/gc.c |
| +++ b/fs/f2fs/gc.c |
| @@ -977,7 +977,7 @@ static bool is_alive(struct f2fs_sb_info |
| { |
| struct page *node_page; |
| nid_t nid; |
| - unsigned int ofs_in_node; |
| + unsigned int ofs_in_node, max_addrs; |
| block_t source_blkaddr; |
| |
| nid = le32_to_cpu(sum->nid); |
| @@ -1003,6 +1003,14 @@ static bool is_alive(struct f2fs_sb_info |
| return false; |
| } |
| |
| + max_addrs = IS_INODE(node_page) ? DEF_ADDRS_PER_INODE : |
| + DEF_ADDRS_PER_BLOCK; |
| + if (ofs_in_node >= max_addrs) { |
| + f2fs_err(sbi, "Inconsistent ofs_in_node:%u in summary, ino:%u, nid:%u, max:%u", |
| + ofs_in_node, dni->ino, dni->nid, max_addrs); |
| + return false; |
| + } |
| + |
| *nofs = ofs_of_node(node_page); |
| source_blkaddr = data_blkaddr(NULL, node_page, ofs_in_node); |
| f2fs_put_page(node_page, 1); |
| --- a/fs/f2fs/recovery.c |
| +++ b/fs/f2fs/recovery.c |
| @@ -437,7 +437,7 @@ static int check_index_in_prev_nodes(str |
| struct dnode_of_data tdn = *dn; |
| nid_t ino, nid; |
| struct inode *inode; |
| - unsigned int offset; |
| + unsigned int offset, ofs_in_node, max_addrs; |
| block_t bidx; |
| int i; |
| |
| @@ -463,15 +463,24 @@ static int check_index_in_prev_nodes(str |
| got_it: |
| /* Use the locked dnode page and inode */ |
| nid = le32_to_cpu(sum.nid); |
| + ofs_in_node = le16_to_cpu(sum.ofs_in_node); |
| + |
| + max_addrs = ADDRS_PER_PAGE(dn->node_page, dn->inode); |
| + if (ofs_in_node >= max_addrs) { |
| + f2fs_err(sbi, "Inconsistent ofs_in_node:%u in summary, ino:%lu, nid:%u, max:%u", |
| + ofs_in_node, dn->inode->i_ino, nid, max_addrs); |
| + return -EFSCORRUPTED; |
| + } |
| + |
| if (dn->inode->i_ino == nid) { |
| tdn.nid = nid; |
| if (!dn->inode_page_locked) |
| lock_page(dn->inode_page); |
| tdn.node_page = dn->inode_page; |
| - tdn.ofs_in_node = le16_to_cpu(sum.ofs_in_node); |
| + tdn.ofs_in_node = ofs_in_node; |
| goto truncate_out; |
| } else if (dn->nid == nid) { |
| - tdn.ofs_in_node = le16_to_cpu(sum.ofs_in_node); |
| + tdn.ofs_in_node = ofs_in_node; |
| goto truncate_out; |
| } |
| |