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
./pocRoot 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.