Possible regression: ext4 lsetxattr returns ENOSPC on malformed image since v7.3-rc1

From: Hengyu Liang

Date: Wed Sep 02 2026 - 12:23:01 EST


Hi,

We found a stable ext4 behavior change: with the same malformed ext4 image,
a large lsetxattr() succeeds through v7.2, but returns ENOSPC on v7.3-rc1.

The minimized syzlang reproducer is attached as repro.prog.

The relevant operations are:

syz_mount_image$ext4(..., "./file2", ..., 0x42f, <embedded image>)

open("./file2",
O_APPEND|O_EXCL|O_NONBLOCK|O_SYNC|O_TRUNC, 0)

lsetxattr$trusted_overlay_upper("./file1",
"trusted.overlay.upper",
<65079-byte value>, 65079, 0)

Observed results:

v6.18 through v7.2:

syz_mount_image: success
open: success
lsetxattr: success

v7.3-rc1:

syz_mount_image: success
open: success
lsetxattr: -1, errno=ENOSPC (28)

The behavior is stable across repeated runs.

The embedded filesystem is malformed. Running e2fsck -fn reports multiple
inode, directory-entry, and symlink inconsistencies, so it is not obvious
whether the old success result or the new ENOSPC result should be considered
correct.

One detail appears potentially relevant.

The filesystem has:

s_last_orphan = 15

and inode 15 is the root-level /file1 used by the lsetxattr() operation.
It is an inline-data inode with:

i_extra_isize = 32

while the mount options request:

debug_want_extra_isize=104

During mount-time orphan cleanup, /file1 can therefore be processed through:

ext4_orphan_cleanup()
-> ext4_process_orphan()
-> ext4_truncate()
-> ext4_inline_data_truncate()
-> ext4_mark_inode_dirty()

The v7.2 to v7.3-rc1 range contains:

7461c60b9c6a
ext4: skip extra isize expansion during mount to prevent deadlock

This commit skips extra-isize expansion while the superblock is not active,
and its commit message specifically mentions the
ext4_process_orphan() -> ext4_truncate() -> ext4_mark_inode_dirty()
path.

This seems like a plausible source of the behavior change, since it could
leave the inode in a different state before the later lsetxattr() call.
However, I have not established that this commit is the direct or only
cause.

Neither behavior is obviously correct to me: the older kernels allow the
operation to succeed on a malformed filesystem, while v7.3-rc1 returns
ENOSPC rather than a corruption-related error. Therefore, I am not sure what
the expected behavior should be, but this seems to indicate a potential bug
in how ext4 handles this malformed state.

The reproducer can be run with syz-execprog, for example:

syz-execprog -os=linux -arch=amd64 \
-executor=syz-executor \
-sandbox=none -procs=1 -threaded=0 -repeat=1 \
repro.prog

Thanks,
Hengyu
syz_mount_image$ext4(&(0x7f0000000040)='ext4\x00', &(0x7f00000001c0)='./file2\x00', 0x404, &(0x7f0000000280)={[{@nogrpid}, {@jqfmt_vfsv0}, {@debug_want_extra_isize={'debug_want_extra_isize', 0x3d, 0x68}}, {@debug}, {@nombcache}, {@quota}, {@nolazytime}]}, 0x3, 0x42f, &(0x7f0000000940)="$eJzs289rHFUcAPDvzCat/WViqT+aVo1WMfgjadJae/CiKHhQEPRQjzFJS+y2kSaCLUGjSD1Kwbt4FPwLPOlF1JPgVe9SKJJLq6eV2Z1Jdje7aZJustX9fGCS92be8t53Z97ue/N2AuhZw9mfJGJ/RPweEQO1bGOB4dq/W8uLU38vL04lUam89VdSLXdzeXGqKFq8bl+R6YtIP0viSIt65y9fOT9ZLs9cyvNjCxfeH5u/fOW52QuT52bOzVycOH365InxF05NPN+ROLO4bg59NHf08GvvXHtj6sy1d3/+Ninib4qjQ4bXO/hkpdLh6rrrQF066etiQ9iUUq2bRn+1/w9EKVZP3kC8+mlXGwdsq0qlUnmg/eGlCvA/lkS3WwB0R/FFn81/i22Hhh53hRsv1SZAWdy38q12pC/SvEx/0/y2k4Yj4szSP19lW2zPfQgAgAbfZ+OfZ1uN/9Kovy90b76GMhgR90XEwYg4FRGHIuL+iGrZByPioU3W37xIsnb8k17fUmAblI3/XszXthrHf8XoLwZLee5ANf7+5OxseeZ4/p6MRP/uLD++Th0/vPLbF+2O1Y//si2rvxgL5u243re78TXTkwuTdxJzvRufRAz1tYo/WVkJSCLicEQMbbGO2ae/Odru2O3jX0cH1pkqX0c8VTv/S9EUfyFZf31y7J4ozxwfK66KtX759eqb7eq/o/g7IDv/e1te/yvxDyb167Xzm6/j6h+ft53TbPX635W83bDvw8mFhUvjEbuS12uNrt8/0VRuYrV8Fv/Isdb9/2CsvhNHIiK7iB+OiEci4tG87Y9FxOMRcWyd+H96+Yn3th7/9srin97U+V9N7IrmPa0TpfM/ftdQ6eBm4s/O/8lqaiTfs5HPv420a2tXMwAAAPz3pBGxP5J0dCWdpqOjtd/wH4q9aXlufuGZs3MfXJyuPSMwGP1pcadroO5+6Hg+rS/yE035E/l94y9Le6r50am58nS3g4cet69N/8/8Wep264Bt53kt6F36P/Qu/R96l/4PvatF/9/TjXYAO6/V9//HXWgHsPOa+r9lP+gh5v/Qu/R/6F36P/Sk+T1x+4fkJSTWJCK9K5ohsU2Jbn8yAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdMa/AQAA//9QOObV")
open(&(0x7f0000000000)='./file2\x00', 0x101e80, 0x0)
lsetxattr$trusted_overlay_upper(&(0x7f0000000100)='./file1\x00', &(0x7f00000000c0), &(0x7f0000000040)=ANY=[], 0xfe37, 0x0)