From: ZhengYuan Huang <gality369@gmail.com>
To: tytso@mit.edu, adilger.kernel@dilger.ca
Cc: linux-ext4@vger.kernel.org, linux-kernel@vger.kernel.org,
baijiaju1990@gmail.com, r33s3n6@gmail.com, zzzccc427@gmail.com,
ZhengYuan Huang <gality369@gmail.com>,
stable@vger.kernel.org
Subject: [PATCH] ext4: xattr: fix size_t underflow in ext4_xattr_set_entry
Date: Tue, 17 Mar 2026 18:28:10 +0800 [thread overview]
Message-ID: <20260317102810.2984100-1-gality369@gmail.com> (raw)
[BUG]
KASAN reports an out-of-bounds read with an astronomically large access
size when ext4_xattr_set_entry() deletes a corrupted ibody xattr entry:
BUG: KASAN: out-of-bounds in ext4_xattr_set_entry+0x11a3/0x1f40 fs/ext4/xattr.c:1756
Read of size 18446744073709551600 at addr ffff8880179737cc
Call Trace:
...
ext4_xattr_set_entry+0x11a3/0x1f40 fs/ext4/xattr.c:1756
ext4_xattr_ibody_set+0x396/0x5a0 fs/ext4/xattr.c:2268
ext4_destroy_inline_data_nolock+0x25e/0x560 fs/ext4/inline.c:463
ext4_convert_inline_data_nolock+0x186/0xa80 fs/ext4/inline.c:1105
ext4_try_add_inline_entry+0x58e/0x960 fs/ext4/inline.c:1224
ext4_add_entry+0x6d2/0xce0 fs/ext4/namei.c:2389
ext4_add_nondir+0x9c/0x280 fs/ext4/namei.c:2784
ext4_create+0x380/0x500 fs/ext4/namei.c:2830
...
The access size 18446744073709551600 equals (size_t)(-16), the result of
a pointer arithmetic underflow.
The bug is reproducible on next-20260313 with our dynamic metadata
fuzzing tool that corrupts ext4 metadata at runtime.
[CAUSE]
When deleting an ibody xattr entry (i->value == NULL), the code computes
the memmove length as:
(void *)last - (void *)here + sizeof(__u32)
where `last` is first the result of walking the xattr entry list to its
terminating IS_LAST_ENTRY, and then stepped back by EXT4_XATTR_LEN(name_len):
last = ENTRY((void *)last - size);
Consider a corrupted ibody xattr list where a spurious IS_LAST_ENTRY (four
zero bytes) has been planted before the real target entry (e.g.,
system.data used for inline data). When ext4_xattr_ibody_find() calls
xattr_find_entry() to locate the entry, the walk stops at the spurious
terminator. xattr_find_entry() returns -ENODATA and sets s->here to that
terminator position; s->not_found is set to -ENODATA accordingly.
Back in ext4_xattr_set_entry(), the entry-walking for loop starts from
s->first and also stops at the same spurious IS_LAST_ENTRY, so
last == here after the loop. Because the "remove old value" block is
guarded by (!s->not_found), it is skipped. The delete-name path
(if (!i->value)) is not guarded by s->not_found at all, so it runs
unconditionally. It then executes:
last = ENTRY((void *)last - size); /* last = here - 20 */
which places last before here. The subsequent memmove() computes its
length as:
(here - 20) - here + sizeof(__u32) = -16
which wraps to (size_t)(-16) = 18446744073709551600 as an unsigned type,
causing memmove() to attempt an enormous read.
The load-time check_xattrs() call also stops at the first IS_LAST_ENTRY
it encounters. A spurious terminator inserted before the real entries
silently truncates the range that check_xattrs() examines, so entries
beyond it are never validated at load time.
[FIX]
After adjusting `last`, check that it has not moved before `here`. If it
has, the xattr entry list is corrupted; reject it with -EFSCORRUPTED.
After this fix, the code will detect the corrupted entry and reject it with
-EFSCORRUPTED, preventing the out-of-bounds access.
Cc: stable@vger.kernel.org
Signed-off-by: ZhengYuan Huang <gality369@gmail.com>
---
It might also be worth considering whether check_xattrs() needs a similar fix.
I'm not deeply familiar with the ext4 codebase, so I'd appreciate any guidance
from the maintainers and would be happy to update and resend the patch if needed.
---
fs/ext4/xattr.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index ce7253b3f549..b8f1102972f0 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -1753,6 +1753,11 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
size_t size = EXT4_XATTR_LEN(name_len);
last = ENTRY((void *)last - size);
+ if ((void *)last < (void *)here) {
+ EXT4_ERROR_INODE(inode, "corrupted xattr entries: last before here");
+ ret = -EFSCORRUPTED;
+ goto out;
+ }
memmove(here, (void *)here + size,
(void *)last - (void *)here + sizeof(__u32));
memset(last, 0, size);
--
2.43.0
reply other threads:[~2026-03-17 10:28 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260317102810.2984100-1-gality369@gmail.com \
--to=gality369@gmail.com \
--cc=adilger.kernel@dilger.ca \
--cc=baijiaju1990@gmail.com \
--cc=linux-ext4@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=r33s3n6@gmail.com \
--cc=stable@vger.kernel.org \
--cc=tytso@mit.edu \
--cc=zzzccc427@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox