* Forwarded: [PATCH] ext4: add bounds check in xattr_find_entry() to prevent use-after-free
2026-02-23 0:12 [syzbot] [ext4?] KASAN: use-after-free Read in xattr_find_entry (2) syzbot
@ 2026-02-24 8:36 ` syzbot
2026-02-24 8:52 ` syzbot
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: syzbot @ 2026-02-24 8:36 UTC (permalink / raw)
To: linux-kernel, syzkaller-bugs
For archival purposes, forwarding an incoming command email to
linux-kernel@vger.kernel.org, syzkaller-bugs@googlegroups.com.
***
Subject: [PATCH] ext4: add bounds check in xattr_find_entry() to prevent use-after-free
Author: kartikey406@gmail.com
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
xattr_find_entry() receives an 'end' pointer to mark the boundary of
the valid xattr region, but never uses it to validate entries during
iteration. The IS_LAST_ENTRY() check dereferences the entry pointer
(reading 4 bytes) without first verifying that the entry is within
bounds. On a corrupted filesystem, this allows the loop to walk past
the valid buffer into freed memory, triggering a use-after-free.
This is observed when mounting a crafted ext4 image where inline xattr
entries in the inode body are corrupted. During path lookup, the ACL
permission check calls ext4_get_acl() -> ext4_xattr_ibody_get() ->
xattr_find_entry(), which iterates over the corrupted inline xattr
entries and reads from a freed page.
Fix this by adding a bounds check against 'end' before each entry
is accessed in the iteration loop, and validating that the next entry
also falls within bounds. Return -EFSCORRUPTED if the xattr entries
overrun the valid region.
Reported-by: syzbot+fb32afec111a7d61b939@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=fb32afec111a7d61b939
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
fs/ext4/xattr.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 7bf9ba19a89d..5080ec44228a 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -652,6 +652,13 @@ ext4_xattr_ibody_get(struct inode *inode, int name_index, const char *name,
header = IHDR(inode, raw_inode);
end = ITAIL(inode, raw_inode);
entry = IFIRST(header);
+
+ if ((void *)entry + sizeof(__u32) > end) {
+ EXT4_ERROR_INODE(inode, "inline xattr region overflow");
+ error = -EFSCORRUPTED;
+ goto cleanup;
+ }
+
error = xattr_find_entry(inode, &entry, end, name_index, name, 0);
if (error)
goto cleanup;
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* Forwarded: [PATCH] ext4: add bounds check in xattr_find_entry() to prevent use-after-free
2026-02-23 0:12 [syzbot] [ext4?] KASAN: use-after-free Read in xattr_find_entry (2) syzbot
2026-02-24 8:36 ` Forwarded: [PATCH] ext4: add bounds check in xattr_find_entry() to prevent use-after-free syzbot
@ 2026-02-24 8:52 ` syzbot
2026-03-26 14:50 ` Forwarded: [PATCH] ext4: fix bounds check in check_xattrs() to account for IS_LAST_ENTRY() read syzbot
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: syzbot @ 2026-02-24 8:52 UTC (permalink / raw)
To: linux-kernel, syzkaller-bugs
For archival purposes, forwarding an incoming command email to
linux-kernel@vger.kernel.org, syzkaller-bugs@googlegroups.com.
***
Subject: [PATCH] ext4: add bounds check in xattr_find_entry() to prevent use-after-free
Author: kartikey406@gmail.com
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
xattr_find_entry() receives an 'end' pointer to mark the boundary of
the valid xattr region but never uses it to validate entries during
iteration. The IS_LAST_ENTRY() macro dereferences the entry pointer
by casting it to __u32 and reading 4 bytes, without first verifying
that the entry falls within bounds.
On a corrupted filesystem, inline xattr entries in the inode body can
have a bogus e_name_len field. EXT4_XATTR_NEXT() uses e_name_len to
compute the next entry offset, which can jump past the valid xattr
region into freed memory. The subsequent IS_LAST_ENTRY() call on this
out-of-bounds pointer triggers a use-after-free read.
Fix this by:
1. Checking that the entry pointer is within bounds before each
IS_LAST_ENTRY() dereference in the loop condition.
2. Validating that the next entry computed via EXT4_XATTR_NEXT()
also falls within bounds before advancing the loop.
Return -EFSCORRUPTED if entries overrun the valid xattr region.
Reported-by: syzbot+fb32afec111a7d61b939@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=fb32afec111a7d61b939
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
fs/ext4/xattr.c | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 7bf9ba19a89d..f38eef93e3f8 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -333,6 +333,12 @@ xattr_find_entry(struct inode *inode, struct ext4_xattr_entry **pentry,
name_len = strlen(name);
for (entry = *pentry; !IS_LAST_ENTRY(entry); entry = next) {
next = EXT4_XATTR_NEXT(entry);
+ if ((void *)next + sizeof(__u32) > end) {
+ EXT4_ERROR_INODE(inode, "corrupted xattr entry: e_name_len=%u",
+ entry->e_name_len);
+ return -EFSCORRUPTED;
+ }
+
if ((void *) next >= end) {
EXT4_ERROR_INODE(inode, "corrupted xattr entries");
return -EFSCORRUPTED;
@@ -652,6 +658,13 @@ ext4_xattr_ibody_get(struct inode *inode, int name_index, const char *name,
header = IHDR(inode, raw_inode);
end = ITAIL(inode, raw_inode);
entry = IFIRST(header);
+
+ if ((void *)entry + sizeof(__u32) > end) {
+ EXT4_ERROR_INODE(inode, "inline xattr region overflow");
+ error = -EFSCORRUPTED;
+ goto cleanup;
+ }
+
error = xattr_find_entry(inode, &entry, end, name_index, name, 0);
if (error)
goto cleanup;
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread* Forwarded: [PATCH] ext4: fix bounds check in check_xattrs() to account for IS_LAST_ENTRY() read
2026-02-23 0:12 [syzbot] [ext4?] KASAN: use-after-free Read in xattr_find_entry (2) syzbot
2026-02-24 8:36 ` Forwarded: [PATCH] ext4: add bounds check in xattr_find_entry() to prevent use-after-free syzbot
2026-02-24 8:52 ` syzbot
@ 2026-03-26 14:50 ` syzbot
2026-03-27 13:28 ` Forwarded: [PATCH] ext4: add debug printk to trace xattr validation path syzbot
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: syzbot @ 2026-03-26 14:50 UTC (permalink / raw)
To: linux-kernel, syzkaller-bugs
For archival purposes, forwarding an incoming command email to
linux-kernel@vger.kernel.org, syzkaller-bugs@googlegroups.com.
***
Subject: [PATCH] ext4: fix bounds check in check_xattrs() to account for IS_LAST_ENTRY() read
Author: kartikey406@gmail.com
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
The bounds check for the next xattr entry in check_xattrs() uses
(void *)next >= end, which allows next to point within sizeof(u32)
bytes of end. On the next iteration, IS_LAST_ENTRY() reads 4 bytes
from next via *(__u32 *)(entry), which can overrun the valid xattr
region and access freed memory.
This is triggered when mounting a corrupted ext4 filesystem where the
inode's i_extra_isize leaves insufficient space for inline xattr
entries. The ACL permission check calls ext4_get_acl() ->
ext4_xattr_ibody_get() -> xattr_find_entry(), which reads past the
inode buffer.
Fix this by changing the check to (void *)next + sizeof(u32) > end,
ensuring there is always enough space for the IS_LAST_ENTRY() read
on the subsequent iteration.
Reported-by: syzbot+fb32afec111a7d61b939@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=fb32afec111a7d61b939
Link: https://lore.kernel.org/all/20260224231429.31361-1-kartikey406@gmail.com/T/ [v1]
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
v2: Move the fix to check_xattrs() as suggested by Ted Ts'o, instead
of adding a separate check in ext4_xattr_ibody_get().
---
fs/ext4/xattr.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 7bf9ba19a89d..c6205b405efe 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -226,7 +226,7 @@ check_xattrs(struct inode *inode, struct buffer_head *bh,
/* Find the end of the names list */
while (!IS_LAST_ENTRY(e)) {
struct ext4_xattr_entry *next = EXT4_XATTR_NEXT(e);
- if ((void *)next >= end) {
+ if ((void *)next + sizeof(u32) > end) {
err_str = "e_name out of bounds";
goto errout;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Forwarded: [PATCH] ext4: add debug printk to trace xattr validation path
2026-02-23 0:12 [syzbot] [ext4?] KASAN: use-after-free Read in xattr_find_entry (2) syzbot
` (2 preceding siblings ...)
2026-03-26 14:50 ` Forwarded: [PATCH] ext4: fix bounds check in check_xattrs() to account for IS_LAST_ENTRY() read syzbot
@ 2026-03-27 13:28 ` syzbot
2026-03-30 1:43 ` Forwarded: [PATCH] loop: block loop reconfiguration of offset/sizelimit on mounted device syzbot
2026-03-31 1:04 ` Forwarded: [PATCH] loop: block changing lo_offset/lo_sizelimit " syzbot
5 siblings, 0 replies; 7+ messages in thread
From: syzbot @ 2026-03-27 13:28 UTC (permalink / raw)
To: linux-kernel, syzkaller-bugs
For archival purposes, forwarding an incoming command email to
linux-kernel@vger.kernel.org, syzkaller-bugs@googlegroups.com.
***
Subject: [PATCH] ext4: add debug printk to trace xattr validation path
Author: kartikey406@gmail.com
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
Add temporary printk statements to trace the inline xattr validation
code path for debugging syzbot use-after-free in xattr_find_entry().
This helps determine whether __xattr_check_inode() is being called
before ext4_xattr_ibody_get() accesses inline xattr entries, and
what the IFIRST/ITAIL gap values are at each stage.
Not for upstream submission - debug only.
Reported-by: syzbot+fb32afec111a7d61b939@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=fb32afec111a7d61b939
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
fs/ext4/inode.c | 4 ++++
fs/ext4/xattr.c | 8 ++++++++
2 files changed, 12 insertions(+)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 396dc3a5d16b..af3a6992bf20 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -5331,11 +5331,15 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino,
if (EXT4_INODE_SIZE(inode->i_sb) > EXT4_GOOD_OLD_INODE_SIZE) {
if (ei->i_extra_isize == 0) {
+ printk("DEBUG: inode %lu: i_extra_isize == 0, skipping xattr check\n",
+ inode->i_ino);
/* The extra space is currently unused. Use it. */
BUILD_BUG_ON(sizeof(struct ext4_inode) & 3);
ei->i_extra_isize = sizeof(struct ext4_inode) -
EXT4_GOOD_OLD_INODE_SIZE;
} else {
+ printk("DEBUG: inode %lu: calling ext4_iget_extra_inode\n",
+ inode->i_ino);
ret = ext4_iget_extra_inode(inode, raw_inode, ei);
if (ret)
goto bad_inode;
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 7bf9ba19a89d..abc27521a3a8 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -316,6 +316,9 @@ int
__xattr_check_inode(struct inode *inode, struct ext4_xattr_ibody_header *header,
void *end, const char *function, unsigned int line)
{
+ printk("DEBUG: inode %lu: __xattr_check_inode called, IFIRST=%px end=%px gap=%ld\n",
+ inode->i_ino, IFIRST(header), end,
+ (long)(end - (void *)IFIRST(header)));
return check_xattrs(inode, NULL, IFIRST(header), end, IFIRST(header),
function, line);
}
@@ -645,6 +648,8 @@ ext4_xattr_ibody_get(struct inode *inode, int name_index, const char *name,
if (!ext4_test_inode_state(inode, EXT4_STATE_XATTR))
return -ENODATA;
+ printk("DEBUG: inode %lu: ext4_xattr_ibody_get called, EXT4_STATE_XATTR is set\n",
+ inode->i_ino);
error = ext4_get_inode_loc(inode, &iloc);
if (error)
return error;
@@ -652,6 +657,9 @@ ext4_xattr_ibody_get(struct inode *inode, int name_index, const char *name,
header = IHDR(inode, raw_inode);
end = ITAIL(inode, raw_inode);
entry = IFIRST(header);
+ printk("DEBUG: inode %lu: ibody_get IFIRST=%px end=%px gap=%ld\n",
+ inode->i_ino, entry, end,
+ (long)(end - (void *)entry));
error = xattr_find_entry(inode, &entry, end, name_index, name, 0);
if (error)
goto cleanup;
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Forwarded: [PATCH] loop: block loop reconfiguration of offset/sizelimit on mounted device
2026-02-23 0:12 [syzbot] [ext4?] KASAN: use-after-free Read in xattr_find_entry (2) syzbot
` (3 preceding siblings ...)
2026-03-27 13:28 ` Forwarded: [PATCH] ext4: add debug printk to trace xattr validation path syzbot
@ 2026-03-30 1:43 ` syzbot
2026-03-31 1:04 ` Forwarded: [PATCH] loop: block changing lo_offset/lo_sizelimit " syzbot
5 siblings, 0 replies; 7+ messages in thread
From: syzbot @ 2026-03-30 1:43 UTC (permalink / raw)
To: linux-kernel, syzkaller-bugs
For archival purposes, forwarding an incoming command email to
linux-kernel@vger.kernel.org, syzkaller-bugs@googlegroups.com.
***
Subject: [PATCH] loop: block loop reconfiguration of offset/sizelimit on mounted device
Author: kartikey406@gmail.com
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
LOOP_SET_STATUS{64} allows changing lo_offset and lo_sizelimit while
a filesystem is mounted on the loop device. This effectively mutates
the data visible to the mounted filesystem, which is equivalent to
writing directly to the block device.
When bdev_allow_write_mounted is false, direct writes to a mounted
block device are blocked via bdev_writes_blocked(). However,
LOOP_SET_STATUS{64} bypasses this protection because it modifies
the loop configuration through an ioctl rather than opening the
block device for writing.
Fix this by checking bdev_writes_blocked() before allowing changes
to lo_offset or lo_sizelimit. If the loop device has writes blocked
(indicating a filesystem is mounted with write protection), return
-EBUSY. Other loop status fields that do not affect the visible
data can still be changed while mounted.
Export bdev_writes_blocked() so it can be used from the loop driver.
Suggested-by: Theodore Ts'o <tytso@mit.edu>
Reported-by: syzbot+fb32afec111a7d61b939@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=fb32afec111a7d61b939
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
block/bdev.c | 4 +++-
drivers/block/loop.c | 12 ++++++++++++
include/linux/blkdev.h | 1 +
3 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/block/bdev.c b/block/bdev.c
index ed022f8c48c7..96520fac7b2f 100644
--- a/block/bdev.c
+++ b/block/bdev.c
@@ -860,10 +860,12 @@ void blkdev_put_no_open(struct block_device *bdev)
put_device(&bdev->bd_device);
}
-static bool bdev_writes_blocked(struct block_device *bdev)
+bool bdev_writes_blocked(struct block_device *bdev)
{
return bdev->bd_writers < 0;
}
+EXPORT_SYMBOL_GPL(bdev_writes_blocked);
+
static void bdev_block_writes(struct block_device *bdev)
{
diff --git a/drivers/block/loop.c b/drivers/block/loop.c
index 0000913f7efc..3f3a29abad1f 100644
--- a/drivers/block/loop.c
+++ b/drivers/block/loop.c
@@ -1239,6 +1239,18 @@ loop_set_status(struct loop_device *lo, const struct loop_info64 *info)
goto out_unlock;
}
+ /*
+ * Changing lo_offset or lo_sizelimit on a mounted device is
+ * equivalent to modifying the block device contents, block
+ * this if writes are blocked on the device.
+ */
+ if ((lo->lo_offset != info->lo_offset ||
+ lo->lo_sizelimit != info->lo_sizelimit) &&
+ bdev_writes_blocked(lo->lo_device)) {
+ err = -EBUSY;
+ goto out_unlock;
+ }
+
if (lo->lo_offset != info->lo_offset ||
lo->lo_sizelimit != info->lo_sizelimit) {
size_changed = true;
diff --git a/include/linux/blkdev.h b/include/linux/blkdev.h
index d463b9b5a0a5..6b908e9dd035 100644
--- a/include/linux/blkdev.h
+++ b/include/linux/blkdev.h
@@ -820,6 +820,7 @@ static inline bool bdev_read_only(struct block_device *bdev)
return bdev_test_flag(bdev, BD_READ_ONLY) || get_disk_ro(bdev->bd_disk);
}
+bool bdev_writes_blocked(struct block_device *bdev);
bool set_capacity_and_notify(struct gendisk *disk, sector_t size);
void disk_force_media_change(struct gendisk *disk);
void bdev_mark_dead(struct block_device *bdev, bool surprise);
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Forwarded: [PATCH] loop: block changing lo_offset/lo_sizelimit on mounted device
2026-02-23 0:12 [syzbot] [ext4?] KASAN: use-after-free Read in xattr_find_entry (2) syzbot
` (4 preceding siblings ...)
2026-03-30 1:43 ` Forwarded: [PATCH] loop: block loop reconfiguration of offset/sizelimit on mounted device syzbot
@ 2026-03-31 1:04 ` syzbot
5 siblings, 0 replies; 7+ messages in thread
From: syzbot @ 2026-03-31 1:04 UTC (permalink / raw)
To: linux-kernel, syzkaller-bugs
For archival purposes, forwarding an incoming command email to
linux-kernel@vger.kernel.org, syzkaller-bugs@googlegroups.com.
***
Subject: [PATCH] loop: block changing lo_offset/lo_sizelimit on mounted device
Author: kartikey406@gmail.com
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
LOOP_SET_STATUS{64} allows changing lo_offset and shrinking
lo_sizelimit while a filesystem is mounted on the loop device.
This effectively mutates the data visible to the mounted filesystem,
which is equivalent to writing directly to the block device.
When CONFIG_BLK_DEV_WRITE_MOUNTED is disabled, direct writes to a
mounted block device are blocked. However, LOOP_SET_STATUS{64}
bypasses this protection because it modifies the loop configuration
through an ioctl rather than opening the block device for writing.
Fix this by checking bdev_writes_blocked() before allowing changes
to lo_offset or shrinking lo_sizelimit. If the loop device has
writes blocked, return -EBUSY. Increasing lo_sizelimit is still
allowed since growing the device is harmless and has legitimate
use cases such as online resize.
Move bdev_writes_blocked() from block/bdev.c to
include/linux/blk_types.h as a static inline function so it can
be used from the loop driver without exporting a symbol.
Reported-by: syzbot+fb32afec111a7d61b939@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=fb32afec111a7d61b939
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
v2:
- Use #ifndef CONFIG_BLK_DEV_WRITE_MOUNTED instead of exporting
bdev_writes_blocked(), as suggested by Ted Ts'o
- Move bdev_writes_blocked() to include/linux/blk_types.h as
static inline instead of exporting from block/bdev.c, as
suggested by Christoph Hellwig
- Allow increasing lo_sizelimit since growing the device is
harmless, as pointed out by Christoph Hellwig
- Remove spurious empty line
---
block/bdev.c | 5 -----
drivers/block/loop.c | 16 ++++++++++++++++
include/linux/blk_types.h | 5 +++++
3 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/block/bdev.c b/block/bdev.c
index ed022f8c48c7..e0bace1a6c27 100644
--- a/block/bdev.c
+++ b/block/bdev.c
@@ -860,11 +860,6 @@ void blkdev_put_no_open(struct block_device *bdev)
put_device(&bdev->bd_device);
}
-static bool bdev_writes_blocked(struct block_device *bdev)
-{
- return bdev->bd_writers < 0;
-}
-
static void bdev_block_writes(struct block_device *bdev)
{
bdev->bd_writers--;
diff --git a/drivers/block/loop.c b/drivers/block/loop.c
index 0000913f7efc..34bbbf3bcb36 100644
--- a/drivers/block/loop.c
+++ b/drivers/block/loop.c
@@ -1239,6 +1239,22 @@ loop_set_status(struct loop_device *lo, const struct loop_info64 *info)
goto out_unlock;
}
+#ifndef CONFIG_BLK_DEV_WRITE_MOUNTED
+ /*
+ * Changing lo_offset or shrinking lo_sizelimit on a mounted
+ * device is equivalent to modifying the block device contents.
+ * Block this if writes to the device are blocked.
+ */
+ if ((lo->lo_offset != info->lo_offset ||
+ (info->lo_sizelimit &&
+ (lo->lo_sizelimit == 0 ||
+ info->lo_sizelimit < lo->lo_sizelimit))) &&
+ bdev_writes_blocked(lo->lo_device)) {
+ err = -EBUSY;
+ goto out_unlock;
+ }
+#endif
+
if (lo->lo_offset != info->lo_offset ||
lo->lo_sizelimit != info->lo_sizelimit) {
size_changed = true;
diff --git a/include/linux/blk_types.h b/include/linux/blk_types.h
index 8808ee76e73c..82ece8737b85 100644
--- a/include/linux/blk_types.h
+++ b/include/linux/blk_types.h
@@ -84,6 +84,11 @@ struct block_device {
#define bdev_whole(_bdev) \
((_bdev)->bd_disk->part0)
+static inline bool bdev_writes_blocked(struct block_device *bdev)
+{
+ return bdev->bd_writers < 0;
+}
+
#define dev_to_bdev(device) \
container_of((device), struct block_device, bd_device)
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread