Skip to content
cvekernelext4null-pointer-dereference

Linux kernel ext4 NULL pointer dereference

3 min read

Overview

Mounting a crafted image and then removing a directory can cause a NULL-pointer dereference. It can be triggered not only locally (mounting an ext4 image in a local shell) but also remotely (mounting a corrupted USB or other storage carrying a crafted ext4 image).

Target

Linux Kernel 5.3.11 Ext4 filesystem. The bug affects ext4_empty_dir in fs/ext4/namei.c in the Linux kernel through 5.3.12.

Bug type: NULL-Pointer-Dereference.

Reproduce

gcc -o poc poc_2019_19037.c
mkdir mnt
mount poc_2019_19037.img ./mnt
cp poc ./mnt/
cd mnt
./poc

Root cause

fs/ext4/namei.c:2848 (link)

/*
 * routine to check that the specified directory is empty (for rmdir)
 */
bool ext4_empty_dir(struct inode *inode)
{
	unsigned int offset;
	struct buffer_head *bh;
	struct ext4_dir_entry_2 *de, *de1;
	struct super_block *sb;
 
	if (ext4_has_inline_data(inode)) {
		int has_inline_data = 1;
		int ret;
 
		ret = empty_inline_dir(inode, &has_inline_data);
		if (has_inline_data)
			return ret;
	}
 
	sb = inode->i_sb;
	if (inode->i_size < EXT4_DIR_REC_LEN(1) + EXT4_DIR_REC_LEN(2)) {
		EXT4_ERROR_INODE(inode, "invalid size");
		return true;
	}
	/* The first directory block must not be a hole,
	 * so treat it as DIRENT_HTREE
	 */
	bh = ext4_read_dirblock(inode, 0, DIRENT_HTREE);
	if (IS_ERR(bh))
		return true;
 
	de = (struct ext4_dir_entry_2 *) bh->b_data;
	de1 = ext4_next_entry(de, sb->s_blocksize);
	if (le32_to_cpu(de->inode) != inode->i_ino ||
			le32_to_cpu(de1->inode) == 0 ||
			strcmp(".", de->name) || strcmp("..", de1->name)) {
		ext4_warning_inode(inode, "directory missing '.' and/or '..'");
		brelse(bh);
		return true;
	}
	offset = ext4_rec_len_from_disk(de->rec_len, sb->s_blocksize) +
		 ext4_rec_len_from_disk(de1->rec_len, sb->s_blocksize);
	de = ext4_next_entry(de1, sb->s_blocksize);
	while (offset < inode->i_size) {
[1]		if ((void *) de >= (void *) (bh->b_data+sb->s_blocksize)) {
			unsigned int lblock;
			brelse(bh);
			lblock = offset >> EXT4_BLOCK_SIZE_BITS(sb);
[2]			bh = ext4_read_dirblock(inode, lblock, EITHER);
			if (bh == NULL) {
				offset += sb->s_blocksize;
				continue;
			}
			if (IS_ERR(bh))
				return true;
			de = (struct ext4_dir_entry_2 *) bh->b_data;
		}
		if (ext4_check_dir_entry(inode, NULL, de, bh,
					 bh->b_data, bh->b_size, offset)) {
			de = (struct ext4_dir_entry_2 *)(bh->b_data +
							 sb->s_blocksize);
			offset = (offset | (sb->s_blocksize - 1)) + 1;
			continue;
		}
		if (le32_to_cpu(de->inode)) {
			brelse(bh);
			return false;
		}
		offset += ext4_rec_len_from_disk(de->rec_len, sb->s_blocksize);
		de = ext4_next_entry(de, sb->s_blocksize);
	}
	brelse(bh);
	return true;
}

In loop [1] (bh->b_data+sb->s_blocksize), the [2] function return value is 0, so the local variable bh can be NULL.

Debugger / KASAN

Debugger view — $rbp (struct buffer_head *bh, a local variable) appears to be 0:

─────────────────────────────────────────────────────────── arguments ────
__asan_load8 (
   long unsigned int var_0 = 0x0000000000000028 → 0x0000000000000028
)
─────────────────────────────────────────────────────────── trace ────
[#0] 0xffffffff81439f63 → ext4_empty_dir(inode=0xffff8880676ba5a8)
[#1] 0xffffffff8143b946 → ext4_rmdir(dir=0xffff8880676ba110, dentry=0xffff88806758e300)
[#2] 0xffffffff81306b24 → vfs_rmdir(dir=0xffff8880676ba110, dentry=0xffff88806758e300)
[#3] 0xffffffff8130f98a → do_rmdir(dfd=0xffffff9c, pathname=0x7ffd254e9672 "foo/bar")
[#4] 0xffffffff81003dee → do_syscall_64(nr=0x54, regs=0xffff8880670f7f58)
[#5] 0xffffffff8260008c → entry_SYSCALL_64()

KASAN log (trimmed to the relevant frames):

[  177.407772] ==================================================================
[  177.407772] BUG: KASAN: null-ptr-deref in ext4_empty_dir+0x298/0x410
[  177.407772] Read of size 8 at addr 0000000000000028 by task 161/200
[  177.407772] CPU: 0 PID: 200 Comm: 161 Not tainted 5.3.11 #1
[  177.407772] Call Trace:
[  177.407772]  dump_stack+0x76/0xab
[  177.407772]  kasan_report+0xe/0x20
[  177.407772]  ext4_empty_dir+0x298/0x410
[  177.407772]  ext4_rmdir+0x236/0x7b0
[  177.407772]  vfs_rmdir+0xf4/0x1e0
[  177.407772]  do_rmdir+0x24a/0x2a0
[  177.407772]  do_syscall_64+0x5e/0x190
[  177.407772]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[  177.407772] ==================================================================
[  177.454308] BUG: kernel NULL pointer dereference, address: 0000000000000028
[  177.454988] Oops: 0000 [#1] SMP KASAN NOPTI
[  177.454988] RIP: 0010:ext4_empty_dir+0x29d/0x410
[  177.488637] ---[ end trace 4ce4c1c3e9b58473 ]---

A crafted image can force ext4_read_dirblock(inode, 0, EITHER) to return 0, which can be dangerous in other functions too.

Details

CVSS 5.5 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H), CWE-476, published 2019-11-21, last modified 2024-11-21.

Attribution

Found as part of the Best of the Best (BoB) 8th bobfuzzer team project — a five-person team that ported the JANUS file-system fuzzer. This is team work, not sole authorship.

References