* Re: xfs: XLOG_WAS_CONT_TRANS on a transaction with no buffered regions reads ri_buf[-1] - null deref Oops during mount log recovery
[not found] ` <c30-xfs-wascont-rideref-20261107@qq.com>
@ 2026-11-07 12:00 ` Guikun Yang
2026-11-08 4:00 ` Guikun Yang
` (3 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Guikun Yang @ 2026-11-07 12:00 UTC (permalink / raw)
To: Carlos Maiolino; +Cc: Darrick J. Wong, linux-xfs, linux-kernel
Hi,
While continuing on the same recovery path I found a second issue in
the log-replay handling of the primary superblock buffer - this one is
an unvalidated install of logged geometry, and it is a different
mechanism from the flag-sequence gap reported above.
## 5. Recovery installs the logged superblock without validating its
geometry fields
xlog_recover_do_primary_sb_buffer() (fs/xfs/xfs_buf_item_recover.c)
recovers the buffer, then installs the on-disk superblock into the
in-core one with no validation:
error = xlog_recover_do_reg_buffer(mp, item, bp, buf_f, current_lsn);
...
xfs_sb_from_disk(&mp->m_sb, dsb);
The only checks afterwards are that the AG count and rtgroup count did
not shrink. Everything else in the freshly installed superblock is
used as-is. The immediately following consumers trust the geometry:
error = xfs_update_last_ag_size(mp, orig_agcount);
which reaches __xfs_agino_range() (fs/xfs/libxfs/xfs_ag.c:172) and
shifts by the freshly installed sb_inopblog:
UBSAN: shift-out-of-bounds in fs/xfs/libxfs/xfs_ag.c:172:11
shift exponent 40 is too large for 32-bit type 'unsigned int'
__xfs_agino_range.cold
xfs_update_last_ag_size
xlog_recover_buf_commit_pass2
Kernel panic - not syncing: UBSAN: panic_on_warn set ...
The same mount-time read path validates these fields through
xfs_sb_read_verify()/xfs_validate_sb(); the recovery path bypasses
that entirely - the same on-disk structure gets two different levels
of trust depending on which path reads it.
## Reproducer
The base image carries a real dirty log from a normal crash. The
logged primary-superblock buffer item (XFS_LI_BUF, blf_blkno == 0,
XFS_BLFT_SB_BUF) is located, one byte in its data region is changed -
sb_inopblog, data offset 123, from 3 to 40 - and the v2 log record CRC
is recomputed. The unmodified image recovers and mounts cleanly.
Reproduced 3/3 with panic_on_warn=1. The code in mainline as of
2026-11-07 is identical.
Other geometry fields are installed through the same unvalidated
xfs_sb_from_disk() call and feed the same consumers: sb_agblocks,
sb_dblocks and sb_agcount go into __xfs_ag_block_count(),
sb_inopblock into the inode geometry, sb_blocklog into every shift and
mask derived from it. I did not chase each variant; the mechanism is
the single unvalidated install.
## Suggested fix
Validate the recovered superblock before installing it - the existing
xfs_validate_sb() (or at least its geometry subset) covers exactly the
fields these consumers use:
xfs_sb_from_disk(&mp->m_sb, dsb);
+ error = xfs_validate_sb(mp, &mp->m_sb, true);
+ if (error) { ... return -EFSCORRUPTED; }
The reproducer (log scanner that finds the SB buffer item, flips
sb_inopblog and recomputes the CRC, ready-made image, serial log) is
available on request.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: xfs: XLOG_WAS_CONT_TRANS on a transaction with no buffered regions reads ri_buf[-1] - null deref Oops during mount log recovery
[not found] ` <c30-xfs-wascont-rideref-20261107@qq.com>
2026-11-07 12:00 ` Guikun Yang
@ 2026-11-08 4:00 ` Guikun Yang
2026-11-09 2:00 ` Guikun Yang
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Guikun Yang @ 2026-11-08 4:00 UTC (permalink / raw)
To: Carlos Maiolino; +Cc: Darrick J. Wong, linux-xfs, linux-kernel
Hi,
One more from the same recovery path - this time in the inode item
handling, a different asymmetry from the two reported above.
## 6. ilf_len is used as the buffer read length without requiring
whole filesystem blocks
xlog_recover_inode_commit_pass2() passes the logged ilf_len (a basic
block count) straight to xfs_buf_read()
(fs/xfs/xfs_inode_item_recover.c:351):
error = xfs_buf_read(mp->m_ddev_targp, in_f->ilf_blkno, in_f->ilf_len,
0, &bp, &xfs_inode_buf_ops);
Nothing requires ilf_len to cover whole filesystem blocks. The
verifier, however, computes the number of inodes from the buffer
length rounded up (fs/xfs/libxfs/xfs_inode_buf.c):
ni = XFS_BB_TO_FSB(mp, bp->b_length) * mp->m_sb.sb_inopblock;
for (i = 0; i < ni; i++) {
dip = xfs_buf_offset(bp, (i << mp->m_sb.sb_inodelog));
unlinked_ino = be32_to_cpu(dip->di_next_unlinked);
...
With a 512-byte block size on the log and a 4k filesystem block,
ilf_len = 1 reads a single 512-byte buffer (kmalloc-512), the rounding
produces 1 FSB * 8 inodes, and the loop walks 4096 bytes - reading
past the slab from the second inode on:
BUG: KASAN: slab-out-of-bounds in xfs_inode_buf_verify+...
Read of size 2 ...
xfs_inode_buf_verify -> xfs_buf_read -> xlog_recover_inode_commit_pass2
Allocated by: kmalloc-512 (the buffer from xfs_buf_read)
The readahead path passes ilf_len through unbounded as well
(fs/xfs/xfs_inode_item_recover.c:36,41).
9232301894 ("xfs: bound inode fork length against the fork size during
log recovery") added bounds for the logged fork regions in this same
function, but the buffer read length itself is still unchecked - the
same log record carries both fields, one is now bounded and the other
is not.
## Reproducer
The dirty-log image from a normal crash is modified in five bytes:
ilf_len in the inode log item from 32 to 1, plus the four CRC bytes of
the log record. ilf_blkno and ilf_boffset keep their original values.
The unmodified image recovers and mounts cleanly; the modified one
hits the KASAN report above at mount(2), 3/3.
## Suggested fix
The verifier is the defence layer for every caller, so bounding the
walk by what the buffer actually holds seems most robust:
ni = XFS_BB_TO_FSB(mp, bp->b_length) * mp->m_sb.sb_inopblock;
becomes something like
ni = XFS_BB_TO_FSB(mp, min(bp->b_length, XFS_FSB_TO_BB(mp, 1))) * ...
or equivalently computing ni from the buffer length divided by the
inode size instead of rounding up to a filesystem block. Rejecting
non-block-aligned ilf_len before xfs_buf_read() would also work but
leaves the verifier as-is for other callers.
The reproducer (log scanner, image builder, ready-made image, serial
log) is available on request.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: xfs: XLOG_WAS_CONT_TRANS on a transaction with no buffered regions reads ri_buf[-1] - null deref Oops during mount log recovery
[not found] ` <c30-xfs-wascont-rideref-20261107@qq.com>
2026-11-07 12:00 ` Guikun Yang
2026-11-08 4:00 ` Guikun Yang
@ 2026-11-09 2:00 ` Guikun Yang
2026-11-09 8:00 ` Guikun Yang
2026-11-09 12:00 ` Guikun Yang
4 siblings, 0 replies; 7+ messages in thread
From: Guikun Yang @ 2026-11-09 2:00 UTC (permalink / raw)
To: Carlos Maiolino; +Cc: Darrick J. Wong, linux-xfs, linux-kernel
Hi,
Another one from the region-collection stage that runs before any of
the item handlers - this one does not involve any item type at all.
## 7. ilf_size is read from a region smaller than the format header
xlog_recover_add_to_trans() allocates the region buffer with the
attacker-controlled oh_len and then reads the inode log format from
it (fs/xfs/xfs_log_recover.c):
ptr = xlog_kvmalloc(len);
memcpy(ptr, dp, len);
in_f = (struct xfs_inode_log_format *)ptr;
...
if (item->ri_total == 0) {
if (in_f->ilf_size == 0 ||
in_f->ilf_size > XLOG_MAX_REGIONS_IN_ITEM) {
in_f->ilf_size is at offset 2 of struct xfs_inode_log_format, so the
read needs len >= 4 - nothing checks that. len is the raw oh_len from
the operation header.
Reachability is clean: this branch only requires the item list to be
non-empty with no regions collected yet, which is exactly the state
left by a complete 16-byte transaction header (len ==
sizeof(struct xfs_trans_header) makes xlog_recover_add_item() hang an
empty item). A normal transaction header followed by a region
operation with oh_len = 1 reaches the read with a 1-byte allocation:
BUG: KASAN: slab-out-of-bounds in xlog_recover_add_to_trans+0x799/0x830
Read of size 2 at addr ...
xlog_recover_add_to_trans
xlog_recovery_process_trans
xlog_recover_process_ophdr
xlog_recover_process_data
...
xfs_fs_fill_super
The read is of size 2 - in_f->ilf_size itself.
## Reproducer
Same shape as the previous reports: a real dirty-log image, the region
operation header's oh_len changed from 0x14 to 0x01 plus the four CRC
bytes of the log record. The unmodified image recovers and mounts
cleanly; the modified one hits the KASAN report at mount(2), 3/3.
Unchanged in mainline as of 2026-11-08.
## Suggested fix
Check the region length before interpreting it as a log format - the
header magic is the only field this stage may assume:
if (len < sizeof(struct xfs_inode_log_format_32) /* or 4 */) {
xfs_warn(...);
return -EFSCORRUPTED;
}
before the ilf_size read; the later xfs_inode_item_format_convert()
already handles short legacy formats properly, so the check belongs at
this stage.
The reproducer (image builder, ready-made image, serial log) is
available on request.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: xfs: XLOG_WAS_CONT_TRANS on a transaction with no buffered regions reads ri_buf[-1] - null deref Oops during mount log recovery
[not found] ` <c30-xfs-wascont-rideref-20261107@qq.com>
` (2 preceding siblings ...)
2026-11-09 2:00 ` Guikun Yang
@ 2026-11-09 8:00 ` Guikun Yang
2026-11-09 12:00 ` Guikun Yang
4 siblings, 0 replies; 7+ messages in thread
From: Guikun Yang @ 2026-11-09 8:00 UTC (permalink / raw)
To: Carlos Maiolino; +Cc: Darrick J. Wong, linux-xfs, linux-kernel
Hi,
One more from the buffer item handling, again a different mechanism:
the number of set-bit runs in the buffer data map drives an index into
ri_buf[] that is never checked against the number of regions the log
item actually carries.
## 8. xlog_recover_do_inode_buffer() indexes ri_buf[] by bitmap runs,
unchecked against ri_total
The inode-buffer recovery walks di_next_unlinked fields and skips
forward through the buffer data map run by run
(fs/xfs/xfs_buf_item_recover.c:625):
while (next_unlinked_offset >= (reg_buf_offset + reg_buf_bytes)) {
...
bit = xfs_next_bit(buf_f->blf_data_map, buf_f->blf_map_size, bit);
if (bit == -1)
return 0;
nbits = xfs_contig_bits(buf_f->blf_data_map, buf_f->blf_map_size, bit);
...
item_index++;
}
...
ASSERT(item->ri_buf[item_index].iov_base != NULL);
...
logged_nextp = item->ri_buf[item_index].iov_base + ...;
item_index is advanced once per set-bit run in blf_data_map, but
ri_buf[] has ri_total entries, where ri_total comes from blf_size -
the number of regions actually present in the log item. Nothing ties
the bitmap to blf_size: a data map with more runs than blf_size claims
regions walks item_index past the end of ri_buf[] and the ASSERTs are
the only guards, which compile away on non-DEBUG kernels.
The sibling path xlog_recover_do_reg_buffer() validates the same
bitmap against the same ri_buf[] array for the non-inode case and
clamps nbits to the region length - the asymmetry between the two
sisters in the same file is the whole bug.
## Reproducer
All field values are legal; nothing is forged out of range. A buffer
log item is turned into an inode-buffer type with blf_flags = 1,
blf_len = 8, blf_size = 2 (two regions), and a data map of 0x55555555
(alternating bits). The alternating map makes every inode skip a run:
item_index reaches 1 after i=0 and 3 after i=1, past ri_total = 2.
The blf_blkno is aimed at the zeroed area of the device so that
xlog_recover_get_buf_lsn() returns -1 and the item is replayed
immediately.
BUG: KASAN: slab-out-of-bounds in xlog_recover_do_inode_buffer+0x5a4/0x620
Read of size 8 at addr ...
xlog_recover_do_inode_buffer -> xlog_recover_buf_commit_pass2
-> ... -> xfs_log_mount -> xfs_mountfs
Kernel panic - not syncing: KASAN: panic_on_warn set ...
The read of size 8 is ri_buf[item_index].iov_base itself. 3/3 at
mount(2); the unmodified image mounts cleanly.
## Suggested fix
Two guards, mirroring do_reg_buffer():
item_index++;
+ if (XFS_IS_CORRUPT(mp, item_index >= item->ri_total))
+ return -EFSCORRUPTED;
plus clamping nbits against item->ri_buf[item_index].iov_len the way
do_reg_buffer() does, so the region copy cannot run past the region.
Unchanged in mainline as of 2026-11-09 (the function is byte-identical
to the tree this was tested on).
The reproducer (image builder, ready-made image, serial log) is
available on request.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: xfs: XLOG_WAS_CONT_TRANS on a transaction with no buffered regions reads ri_buf[-1] - null deref Oops during mount log recovery
[not found] ` <c30-xfs-wascont-rideref-20261107@qq.com>
` (3 preceding siblings ...)
2026-11-09 8:00 ` Guikun Yang
@ 2026-11-09 12:00 ` Guikun Yang
4 siblings, 0 replies; 7+ messages in thread
From: Guikun Yang @ 2026-11-09 12:00 UTC (permalink / raw)
To: Carlos Maiolino; +Cc: Darrick J. Wong, linux-xfs, linux-kernel
Hi,
One correction and a related finding in the same function the
previous mail discussed.
## Correction to item 8
Item 8 said that xlog_recover_do_reg_buffer() validates the same
bitmap against the same ri_buf[] array. That holds for the
destination side only. On the source side this function is just as
unchecked as do_inode_buffer() - which is what the finding below is
about. Both functions need the same two guards, not one.
## 9. xlog_recover_do_reg_buffer() indexes ri_buf[] by bitmap runs
without checking against ri_total
The loop advances the source index once per set-bit run and only
asserts about it (fs/xfs/xfs_buf_item_recover.c:464):
i = 1; /* 0 is the buf format structure */
while (1) {
bit = xfs_next_bit(buf_f->blf_data_map, buf_f->blf_map_size, bit);
if (bit == -1)
break;
nbits = xfs_contig_bits(...);
ASSERT(nbits > 0);
ASSERT(item->ri_buf[i].iov_base != NULL); /* no-op */
ASSERT(item->ri_buf[i].iov_len % XFS_BLF_CHUNK == 0);
/* destination side: validated for real since CVE-2026-80536 */
if (XFS_IS_CORRUPT(mp, BBTOB(bp->b_length) <
((uint)bit << XFS_BLF_SHIFT) + (nbits << XFS_BLF_SHIFT)))
return -EFSCORRUPTED;
if (item->ri_buf[i].iov_len < (nbits << XFS_BLF_SHIFT))
nbits = item->ri_buf[i].iov_len >> XFS_BLF_SHIFT;
memcpy(..., item->ri_buf[i].iov_base, nbits << XFS_BLF_SHIFT);
i++;
}
ASSERT(i == item->ri_total); /* still a no-op in HEAD */
The only bound on i is that trailing ASSERT. The number of
iterations is driven entirely by the dirty bitmap, which has no
relationship to ri_total (blf_size). The commit that added the
destination check says so in its own message:
"The destination offset (bit/nbits) and the buffer size (from
blf_len) are both attacker-controlled ... yet the only thing
bounding the copy is an ASSERT(), which compiles away on
production kernels."
The same argument applies to the source index that was left in
place.
## Reproducer
Five field changes on a real dirty-log image, all values legal except
the ones being forged: blf_flags 0x9000 -> 0 (regular buffer), blf_len
1 -> 8 (4 KiB, so the destination check passes), blf_blkno -> the
zeroed area of the device, blf_data_map 0x7 -> 0x55555555 (16 runs),
blf_size left at 2 (ri_total = 2). With 16 runs the index reaches 16
and ri_buf[2..15] are read out of bounds:
BUG: KASAN: slab-out-of-bounds in xlog_recover_do_reg_buffer+0x676/0x740
Read of size 8 ... by task init/1
xlog_recover_do_reg_buffer -> xlog_recover_buf_commit_pass2
-> xlog_recover_process_data -> xlog_do_recovery_pass -> xfs_log_mount
3/3 at mount(2); the unmodified image mounts cleanly. The function is
byte-identical to mainline as of 2026-11-09.
## Suggested fix
The same guard as item 8, applied to both functions:
i++;
+ if (XFS_IS_CORRUPT(mp, i >= item->ri_total))
+ return -EFSCORRUPTED;
or better, check before the ri_buf[i] use at the top of each
iteration. With the destination check, the nbits clamp and this
index check in place, the ASSERTs can go away entirely.
The reproducer (image builder, ready-made image, serial log) is
available on request.
Thanks,
Guikun Yang
^ permalink raw reply [flat|nested] 7+ messages in thread