SYZKALLER BUG: messing with a mounted file system via loop ioctls (was: Re: [PATCH] ext4: add bounds check in ext4_xattr_ibody_get() to) prevent out-of-bounds access
From: Theodore Tso
Date: Fri Mar 27 2026 - 12:46:35 EST
On Fri, Mar 27, 2026 at 08:02:30PM +0530, Deepanshu Kartikey wrote:
>
> Thank you for the review. I tried moving the fix to check_xattrs()
> as you suggested, but the syzbot reproducer still crashes. I added
> printk statements to trace the code path and found the root cause.
>
> The issue is that __xattr_check_inode() runs once during ext4_iget(),
> but ext4_xattr_ibody_get() re-reads the inode from disk via
> ext4_get_inode_loc() on every call. The reproducer exploits this by
> shrinking the loop device after mount, causing the re-read to return
> corrupted data.
I consider this more a defect in Syzkaller than in ext4. Syzkaller
already unsets CONFIG_BLK_DEV_WRITE_MOUNTED because modifying a
mounted file system is not considered a valid security concern.
Unfortunately, the syzkaller fuzzer which corrupts a mounted file
system by messing with a loop device using a privileged ioctl bypasses
!CONFIG_DEV_BLK_WRITE_MOUNTED.
I'll accept a change to add a to check_xattrs(), which will protect
against static corruptions, but adding an extra check to a hotpath
(this would slow down SELinux, which aggressively needs to read the
file's sid) to protect against a corruption issue which requires root
privs is not something I'm interested in.
My priority is improving ext4 --- not reducing the syzkaller reports
against ext4 to zero, since there are false positives like this. For
people who care about reducing the syzkaller report gamification, my
suggestion is to either extend CONFIG_BLK_DEV_WRITE_MOUNTED to prevent
these loop device reconfiguration while a file system is mounted on
that loop device, or to fix syzkaller to not create fuzzers like this.
Cheers,
- Ted
>
> Here is the relevant debug output showing the sequence:
>
> 1) Inode 12 is loaded, __xattr_check_inode passes with gap=92:
>
> DEBUG: inode 12: calling ext4_iget_extra_inode
> DEBUG: inode 12: __xattr_check_inode called,
> IFIRST=ffff88805b9f9fa4 end=ffff88805b9fa000 gap=92
>
> 2) The loop device is then shrunk:
>
> loop0: detected capacity change from 1024 to 64
>
> 3) First access after shrink, inode re-read still looks okay:
>
> DEBUG: inode 12: ibody_get
> IFIRST=ffff88806edbcfa4 end=ffff88806edbd000 gap=92
>
> 4) Second access, re-read returns corrupted data with gap=-4:
>
> DEBUG: inode 12: ibody_get
> IFIRST=ffff88806edbd004 end=ffff88806edbd000 gap=-4
>
> 5) Crash follows immediately in xattr_find_entry().
>
> Note that IFIRST and end are at different addresses between steps
> 1 and 4 - ext4_get_inode_loc() returned a different buffer_head
> with corrupted i_extra_isize, pushing IFIRST 4 bytes past end.
> The initial __xattr_check_inode() validation cannot protect
> against this because it validated the original buffer, not the
> corrupted re-read.
>
> I think we need both fixes:
>
> 1) The bounds fix in check_xattrs() as you suggested, changing
> (void *)next >= end to (void *)next + sizeof(u32) > end
>
> 2) A bounds check in ext4_xattr_ibody_get() before calling
> xattr_find_entry(), to catch corrupted re-reads
>
> Should I submit a v2 with both fixes as a patch series?
>
> Thanks,
> Deepanshu Kartikey