* [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert
@ 2026-09-10 0:16 syzbot
2026-09-10 5:06 ` Dave Chinner
0 siblings, 1 reply; 5+ messages in thread
From: syzbot @ 2026-09-10 0:16 UTC (permalink / raw)
To: cem, linux-kernel, linux-xfs, syzkaller-bugs
Hello,
syzbot found the following issue on:
HEAD commit: 654ae5d73c05 Merge tag 'drm-fixes-2026-09-05' of https://g..
git tree: upstream
console output: https://syzkaller.appspot.com/x/log.txt?x=16691125580000
kernel config: https://syzkaller.appspot.com/x/.config?x=19560cab9a915237
dashboard link: https://syzkaller.appspot.com/bug?extid=a4fde844548510369112
compiler: gcc (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44
Unfortunately, I don't have any reproducer for this issue yet.
Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/bb30b9c473e7/disk-654ae5d7.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/fcb222028f47/vmlinux-654ae5d7.xz
kernel image: https://storage.googleapis.com/syzbot-assets/5cfc3b5a7eef/bzImage-654ae5d7.xz
IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+a4fde844548510369112@syzkaller.appspotmail.com
XFS (loop0): Please unmount the filesystem and rectify the problem(s).
==================================================================
BUG: KASAN: slab-use-after-free in instrument_atomic_write include/linux/instrumented.h:97 [inline]
BUG: KASAN: slab-use-after-free in set_bit include/asm-generic/bitops/instrumented-atomic.h:28 [inline]
BUG: KASAN: slab-use-after-free in xlog_cil_ail_insert+0x55b/0xd00 fs/xfs/xfs_log_cil.c:889
Write of size 8 at addr ffff888028e06d60 by task kworker/1:1H/1998
CPU: 1 UID: 0 PID: 1998 Comm: kworker/1:1H Not tainted syzkaller #0 PREEMPT(full)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 07/16/2026
Workqueue: xfs-log/loop0 xlog_ioend_work
Call Trace:
<TASK>
__dump_stack lib/dump_stack.c:94 [inline]
dump_stack_lvl+0x100/0x190 lib/dump_stack.c:120
print_address_description mm/kasan/report.c:378 [inline]
print_report+0x13d/0x4b0 mm/kasan/report.c:482
kasan_report+0xdf/0x1c0 mm/kasan/report.c:595
check_region_inline mm/kasan/generic.c:186 [inline]
kasan_check_range+0x10f/0x1e0 mm/kasan/generic.c:200
instrument_atomic_write include/linux/instrumented.h:97 [inline]
set_bit include/asm-generic/bitops/instrumented-atomic.h:28 [inline]
xlog_cil_ail_insert+0x55b/0xd00 fs/xfs/xfs_log_cil.c:889
xlog_cil_committed+0xd7/0x6f0 fs/xfs/xfs_log_cil.c:995
xlog_cil_process_committed+0x123/0x1f0 fs/xfs/xfs_log_cil.c:1025
xlog_state_shutdown_callbacks+0x1ef/0x3a0 fs/xfs/xfs_log.c:438
xlog_force_shutdown+0x21b/0x490 fs/xfs/xfs_log.c:3408
xlog_ioend_work+0xec/0x110 fs/xfs/xfs_log.c:1205
process_one_work+0xac7/0x1b10 kernel/workqueue.c:3396
process_scheduled_works kernel/workqueue.c:3479 [inline]
worker_thread+0x5ef/0xe50 kernel/workqueue.c:3560
kthread+0x373/0x450 kernel/kthread.c:436
ret_from_fork+0x730/0xd60 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
</TASK>
Allocated by task 9369:
kasan_save_stack+0x30/0x50 mm/kasan/common.c:57
kasan_save_track+0x14/0x30 mm/kasan/common.c:78
unpoison_slab_object mm/kasan/common.c:340 [inline]
__kasan_slab_alloc+0x89/0x90 mm/kasan/common.c:366
kasan_slab_alloc include/linux/kasan.h:253 [inline]
slab_post_alloc_hook mm/slub.c:4683 [inline]
slab_alloc_node mm/slub.c:4996 [inline]
kmem_cache_alloc_noprof+0x269/0x6a0 mm/slub.c:5010
xfs_buf_item_init+0x8a/0x660 fs/xfs/xfs_buf_item.c:872
_xfs_trans_bjoin+0x3f/0x140 fs/xfs/xfs_trans_buf.c:75
xfs_trans_get_buf_map+0x2be/0x750 fs/xfs/xfs_trans_buf.c:162
xfs_trans_get_buf fs/xfs/xfs_trans.h:189 [inline]
xfs_dquot_disk_alloc+0x62a/0xbc0 fs/xfs/xfs_dquot.c:397
xfs_qm_dqread+0x4e7/0x5d0 fs/xfs/xfs_dquot.c:720
xfs_qm_dqget+0xee/0x3c0 fs/xfs/xfs_dquot.c:923
xfs_qm_quotacheck_dqadjust+0xae/0x5a0 fs/xfs/xfs_qm.c:1260
xfs_qm_dqusage_adjust+0x524/0x6f0 fs/xfs/xfs_qm.c:1403
xfs_iwalk_ag_recs+0x523/0x870 fs/xfs/xfs_iwalk.c:209
xfs_iwalk_run_callbacks+0x1f7/0x580 fs/xfs/xfs_iwalk.c:370
xfs_iwalk_ag+0x81e/0xa30 fs/xfs/xfs_iwalk.c:473
xfs_iwalk_ag_work+0x117/0x1f0 fs/xfs/xfs_iwalk.c:620
xfs_pwork_work+0x82/0x170 fs/xfs/xfs_pwork.c:47
process_one_work+0xac7/0x1b10 kernel/workqueue.c:3396
process_scheduled_works kernel/workqueue.c:3479 [inline]
worker_thread+0x5ef/0xe50 kernel/workqueue.c:3560
kthread+0x373/0x450 kernel/kthread.c:436
ret_from_fork+0x730/0xd60 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Freed by task 13586:
kasan_save_stack+0x30/0x50 mm/kasan/common.c:57
kasan_save_track+0x14/0x30 mm/kasan/common.c:78
kasan_save_free_info+0x3b/0x70 mm/kasan/generic.c:584
poison_slab_object mm/kasan/common.c:253 [inline]
__kasan_slab_free+0x5f/0x80 mm/kasan/common.c:285
kasan_slab_free include/linux/kasan.h:235 [inline]
slab_free_hook mm/slub.c:2748 [inline]
slab_free mm/slub.c:6499 [inline]
kmem_cache_free+0x127/0x6c0 mm/slub.c:6626
xfs_buf_ioend+0x351/0x1aa0 fs/xfs/xfs_buf.c:1167
xfs_buf_iowait+0xfa/0x470 fs/xfs/xfs_buf.c:1336
xfs_buf_delwri_submit+0x3f8/0x600 fs/xfs/xfs_buf.c:2051
xfs_qm_quotacheck+0x2f9/0x880 fs/xfs/xfs_qm.c:1536
xfs_qm_mount_quotas+0x17f/0x6d0 fs/xfs/xfs_qm.c:1645
xfs_mountfs+0x1d04/0x1fd0 fs/xfs/xfs_mount.c:1190
xfs_fs_fill_super+0x1523/0x1f30 fs/xfs/xfs_super.c:1987
get_tree_bdev_flags+0x38c/0x620 fs/super.c:1887
vfs_get_tree+0x92/0x320 fs/super.c:1947
fc_mount fs/namespace.c:1198 [inline]
do_new_mount_fc fs/namespace.c:3772 [inline]
do_new_mount fs/namespace.c:3848 [inline]
path_mount+0x7d0/0x24c0 fs/namespace.c:4168
do_mount fs/namespace.c:4181 [inline]
__do_sys_mount fs/namespace.c:4397 [inline]
__se_sys_mount fs/namespace.c:4374 [inline]
__x64_sys_mount+0x293/0x310 fs/namespace.c:4374
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x123/0x790 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
The buggy address belongs to the object at ffff888028e06d20
which belongs to the cache xfs_buf_item of size 272
The buggy address is located 64 bytes inside of
freed 272-byte region [ffff888028e06d20, ffff888028e06e30)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x28e06
flags: 0xfff00000000000(node=0|zone=1|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 00fff00000000000 ffff88802031a500 dead000000000100 dead000000000122
raw: 0000000000000000 00000002000c000c 00000000f5000000 0000000000000000
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 0, migratetype Unmovable, gfp_mask 0xd2c40(GFP_NOFS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 9561, tgid 9561 (kworker/u8:12), ts 351126059615
set_page_owner include/linux/page_owner.h:33 [inline]
post_alloc_hook+0xfd/0x120 mm/page_alloc.c:1871
prep_new_page mm/page_alloc.c:1879 [inline]
get_page_from_freelist+0x110d/0x35d0 mm/page_alloc.c:3943
__alloc_frozen_pages_noprof+0x2eb/0x3300 mm/page_alloc.c:5436
alloc_slab_page mm/slub.c:3347 [inline]
allocate_slab mm/slub.c:3462 [inline]
new_slab+0xa9/0x6e0 mm/slub.c:3513
refill_objects+0xe3/0x410 mm/slub.c:7410
refill_sheaf mm/slub.c:2885 [inline]
__pcs_replace_empty_main+0x35e/0x650 mm/slub.c:4774
alloc_from_pcs mm/slub.c:4850 [inline]
slab_alloc_node mm/slub.c:4984 [inline]
kmem_cache_alloc_noprof+0x4c4/0x6a0 mm/slub.c:5010
xfs_buf_item_init+0x8a/0x660 fs/xfs/xfs_buf_item.c:872
_xfs_trans_bjoin+0x3f/0x140 fs/xfs/xfs_trans_buf.c:75
xfs_trans_read_buf_map+0x54f/0x8b0 fs/xfs/xfs_trans_buf.c:294
xfs_trans_read_buf fs/xfs/xfs_trans.h:212 [inline]
xfs_alloc_read_agfl+0x23b/0x390 fs/xfs/libxfs/xfs_alloc.c:885
xfs_alloc_fix_freelist+0xac5/0x10d0 fs/xfs/libxfs/xfs_alloc.c:2961
xfs_alloc_vextent_prepare_ag+0x7a/0x680 fs/xfs/libxfs/xfs_alloc.c:3566
xfs_alloc_vextent_iterate_ags.constprop.0+0x1b5/0x9f0 fs/xfs/libxfs/xfs_alloc.c:3750
xfs_alloc_vextent_start_ag+0x2cb/0x7f0 fs/xfs/libxfs/xfs_alloc.c:3839
xfs_bmap_btalloc_best_length fs/xfs/libxfs/xfs_bmap.c:3617 [inline]
xfs_bmap_btalloc+0x1113/0x18a0 fs/xfs/libxfs/xfs_bmap.c:3661
page last free pid 12 tgid 12 ts 350823962674 stack trace:
reset_page_owner include/linux/page_owner.h:26 [inline]
__free_pages_prepare mm/page_alloc.c:1418 [inline]
__free_frozen_pages+0x79f/0x10f0 mm/page_alloc.c:2962
__folio_put+0x3b4/0x5f0 mm/folio.c:105
folio_put_refs include/linux/mm.h:2180 [inline]
gup_put_folio+0x1cb/0x270 mm/gup.c:114
unpin_user_pages mm/gup.c:422 [inline]
unpin_user_pages+0x179/0x200 mm/gup.c:401
io_free_region+0xd6/0x340 io_uring/memmap.c:115
io_rings_free io_uring/io_uring.c:2071 [inline]
io_ring_ctx_free io_uring/io_uring.c:2180 [inline]
io_ring_exit_work+0x9de/0xd59 io_uring/io_uring.c:2410
process_one_work+0xac7/0x1b10 kernel/workqueue.c:3396
process_scheduled_works kernel/workqueue.c:3479 [inline]
worker_thread+0x5ef/0xe50 kernel/workqueue.c:3560
kthread+0x373/0x450 kernel/kthread.c:436
ret_from_fork+0x730/0xd60 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Memory state around the buggy address:
ffff888028e06c00: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff888028e06c80: fb fb fb fb fb fb fb fb fb fb fb fb fc fc fc fc
>ffff888028e06d00: fc fc fc fc fa fb fb fb fb fb fb fb fb fb fb fb
^
ffff888028e06d80: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff888028e06e00: fb fb fb fb fb fb fc fc fc fc fc fc fc fc fa fb
==================================================================
---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.
syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.
If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title
If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)
If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report
If you want to undo deduplication, reply with:
#syz undup
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert
2026-09-10 0:16 [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert syzbot
@ 2026-09-10 5:06 ` Dave Chinner
2026-09-10 15:47 ` Jeffin Philip
0 siblings, 1 reply; 5+ messages in thread
From: Dave Chinner @ 2026-09-10 5:06 UTC (permalink / raw)
To: syzbot; +Cc: cem, linux-kernel, linux-xfs, syzkaller-bugs
On Wed, Sep 09, 2026 at 05:16:23PM -0700, syzbot wrote:
> Hello,
>
> syzbot found the following issue on:
>
> HEAD commit: 654ae5d73c05 Merge tag 'drm-fixes-2026-09-05' of https://g..
> git tree: upstream
> console output: https://syzkaller.appspot.com/x/log.txt?x=16691125580000
> kernel config: https://syzkaller.appspot.com/x/.config?x=19560cab9a915237
> dashboard link: https://syzkaller.appspot.com/bug?extid=a4fde844548510369112
> compiler: gcc (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44
>
> Unfortunately, I don't have any reproducer for this issue yet.
>
> Downloadable assets:
> disk image: https://storage.googleapis.com/syzbot-assets/bb30b9c473e7/disk-654ae5d7.raw.xz
> vmlinux: https://storage.googleapis.com/syzbot-assets/fcb222028f47/vmlinux-654ae5d7.xz
> kernel image: https://storage.googleapis.com/syzbot-assets/5cfc3b5a7eef/bzImage-654ae5d7.xz
>
> IMPORTANT: if you fix the issue, please add the following tag to the commit:
> Reported-by: syzbot+a4fde844548510369112@syzkaller.appspotmail.com
>
> XFS (loop0): Please unmount the filesystem and rectify the problem(s).
> ==================================================================
> BUG: KASAN: slab-use-after-free in instrument_atomic_write include/linux/instrumented.h:97 [inline]
> BUG: KASAN: slab-use-after-free in set_bit include/asm-generic/bitops/instrumented-atomic.h:28 [inline]
> BUG: KASAN: slab-use-after-free in xlog_cil_ail_insert+0x55b/0xd00 fs/xfs/xfs_log_cil.c:889
> Write of size 8 at addr ffff888028e06d60 by task kworker/1:1H/1998
>
> CPU: 1 UID: 0 PID: 1998 Comm: kworker/1:1H Not tainted syzkaller #0 PREEMPT(full)
> Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 07/16/2026
> Workqueue: xfs-log/loop0 xlog_ioend_work
> Call Trace:
> <TASK>
> __dump_stack lib/dump_stack.c:94 [inline]
> dump_stack_lvl+0x100/0x190 lib/dump_stack.c:120
> print_address_description mm/kasan/report.c:378 [inline]
> print_report+0x13d/0x4b0 mm/kasan/report.c:482
> kasan_report+0xdf/0x1c0 mm/kasan/report.c:595
> check_region_inline mm/kasan/generic.c:186 [inline]
> kasan_check_range+0x10f/0x1e0 mm/kasan/generic.c:200
> instrument_atomic_write include/linux/instrumented.h:97 [inline]
> set_bit include/asm-generic/bitops/instrumented-atomic.h:28 [inline]
> xlog_cil_ail_insert+0x55b/0xd00 fs/xfs/xfs_log_cil.c:889
> xlog_cil_committed+0xd7/0x6f0 fs/xfs/xfs_log_cil.c:995
> xlog_cil_process_committed+0x123/0x1f0 fs/xfs/xfs_log_cil.c:1025
> xlog_state_shutdown_callbacks+0x1ef/0x3a0 fs/xfs/xfs_log.c:438
> xlog_force_shutdown+0x21b/0x490 fs/xfs/xfs_log.c:3408
> xlog_ioend_work+0xec/0x110 fs/xfs/xfs_log.c:1205
Buffer is pinned, so the CIL hold a reference to the BLI that
guarantees the existence of both the BLI and the underlying buffer.
However, there is a shutdown in progress....
> Allocated by task 9369:
> kasan_save_stack+0x30/0x50 mm/kasan/common.c:57
> kasan_save_track+0x14/0x30 mm/kasan/common.c:78
> unpoison_slab_object mm/kasan/common.c:340 [inline]
> __kasan_slab_alloc+0x89/0x90 mm/kasan/common.c:366
> kasan_slab_alloc include/linux/kasan.h:253 [inline]
> slab_post_alloc_hook mm/slub.c:4683 [inline]
> slab_alloc_node mm/slub.c:4996 [inline]
> kmem_cache_alloc_noprof+0x269/0x6a0 mm/slub.c:5010
> xfs_buf_item_init+0x8a/0x660 fs/xfs/xfs_buf_item.c:872
> _xfs_trans_bjoin+0x3f/0x140 fs/xfs/xfs_trans_buf.c:75
> xfs_trans_get_buf_map+0x2be/0x750 fs/xfs/xfs_trans_buf.c:162
> xfs_trans_get_buf fs/xfs/xfs_trans.h:189 [inline]
> xfs_dquot_disk_alloc+0x62a/0xbc0 fs/xfs/xfs_dquot.c:397
> xfs_qm_dqread+0x4e7/0x5d0 fs/xfs/xfs_dquot.c:720
> xfs_qm_dqget+0xee/0x3c0 fs/xfs/xfs_dquot.c:923
> xfs_qm_quotacheck_dqadjust+0xae/0x5a0 fs/xfs/xfs_qm.c:1260
Buffer allocated and logged from quotacheck.
> Freed by task 13586:
> kasan_save_stack+0x30/0x50 mm/kasan/common.c:57
> kasan_save_track+0x14/0x30 mm/kasan/common.c:78
> kasan_save_free_info+0x3b/0x70 mm/kasan/generic.c:584
> poison_slab_object mm/kasan/common.c:253 [inline]
> __kasan_slab_free+0x5f/0x80 mm/kasan/common.c:285
> kasan_slab_free include/linux/kasan.h:235 [inline]
> slab_free_hook mm/slub.c:2748 [inline]
> slab_free mm/slub.c:6499 [inline]
> kmem_cache_free+0x127/0x6c0 mm/slub.c:6626
> xfs_buf_ioend+0x351/0x1aa0 fs/xfs/xfs_buf.c:1167
> xfs_buf_iowait+0xfa/0x470 fs/xfs/xfs_buf.c:1336
> xfs_buf_delwri_submit+0x3f8/0x600 fs/xfs/xfs_buf.c:2051
> xfs_qm_quotacheck+0x2f9/0x880 fs/xfs/xfs_qm.c:1536
And shutdown has occurred during dirty buffer writeback from
quotacheck.
This -normally- blocks on pinned buffers, allowing the buffer to
move from the CIL to the AIL, releasing it's reference to the BLI
and leaving it to be release by IO completion.
However, the shutdown path through xfs_buf_submit() on shutdown does
not wait on pinned buffers - it simply errors them out, resulting
in xfs_buf_ioend() being called on a buffer with an attached BLI.
This then frees the BLI despite it still having an active reference
(xfs_buf_item_done -> xfs_buf_item_relse -> xfs_buf_item_free)
because it assumes buffers running IO completion have no references
to the BLI.
This is yet another manifestation of the the "AIL doesn't hold a
log item reference" problem; xfs_buf_item_done() assumes there are
no references because the buffer is locked, has no pins and so it
owns the attached BLI and can free it.
Hence when the CIL (correctly!) tries to access the BLI it still
holds a reference to, it finds that it has been freed and triggers
the UAF.
The short term fix is to slap a bandaid on xfs_buf_submit() to wait
for unpin before erroring out a pinned buffer on shutdown. That,
however, doesn't fix the underlying lack of full lifecycle reference
counts for log items.
The long term fix is have the AIL require a reference count to be
held on log items it tracks, and only have that refcount removed
when the item is removed from the AIL. That's a much bigger rework
of the code.
FYI, I've attached two document below - the design for
generic full lifecycle log item reference counting, and the design
for converting the inode log item to be dynamic similar to the buf
log item so we don't end up with millions of ILIs that are never
used left sitting around in memory until it's owner inode is
reclaimed.
-Dave.
--
Dave Chinner
dgc@kernel.org
$ cat log-item-recount.rst
.. SPDX-License-Identifier: GPL-2.0
======================================
XFS Log Item Reference Counting Design
======================================
Goal: Eliminate the "zero refcount but in AIL means alive" state by
adding an AIL reference to log items. Lift the refcount from the
type-specific level (bli_refcount, efi_refcount, etc.) to the generic
xfs_log_item level using a lockref. This allows the AIL to take and
drop log item references directly without needing type-specific
methods, and provides lock-free log item lifecycle management via
lockref_get_not_dead() and cmpxchg-based owner detachment.
Current State: Type-Specific Refcounts
======================================
Every log item type that persists in the AIL has its own refcount:
- BLI: bli_refcount (atomic_t in xfs_buf_log_item)
- EFI: efi_refcount (atomic_t in xfs_efi_log_item)
- RUI: rui_refcount (atomic_t in xfs_rui_log_item)
- CUI: cui_refcount (atomic_t in xfs_cui_log_item)
- BUI: bui_refcount (atomic_t in xfs_bui_log_item)
- ATTRI: attri_refcount (atomic_t in xfs_attri_log_item)
- XMI: xmi_refcount (atomic_t in xfs_xmi_log_item)
All use the same pattern: atomic_dec_and_test to detect the last
reference, then remove from AIL and free. The intent items initialise
to 2 (AIL + done item), the BLI increments/decrements dynamically
(transaction + CIL pin).
Lifting the refcount to xfs_log_item unifies this, eliminates
per-type boilerplate, and enables the AIL to manage references
generically.
AIL removal is always performed by type-specific code (never by
generic AIL infrastructure), and the AIL lock is always dropped
before the type-specific caller frees the item. This means the type-
specific code can drop the AIL reference after AIL removal and handle
freeing without holding the AIL lock.
Generic Infrastructure: lockref on xfs_log_item
===============================================
Add a struct lockref li_ref to struct xfs_log_item.
Using lockref gives us:
lockref_get_not_dead():
Atomic "get if alive" that returns false if the item is dead
(count < 0). Replaces the current assumption that an existing
log item pointer always means the item is usable.
lockref_mark_dead():
Marks the item dead so no further gets succeed. Called when the
last reference is dropped and the item is about to be freed.
lockref_put_or_lock():
Drops the reference; if it was the last one, returns with the
embedded spinlock held. The caller can then perform final cleanup
(detach from owner, mark dead) atomically under that lock before
freeing.
Embedded spinlock:
Serialises the final teardown against concurrent lookups without
needing external per-object locks.
Generic helpers:
xfs_log_item_get(lip):
lockref_get — caller knows the item is alive.
xfs_log_item_put(lip):
lockref_put_or_lock — returns true if last ref. Caller holds the
lockref spinlock on true return and must mark dead + free.
xfs_log_item_get_safe(lip):
lockref_get_not_dead — returns bool. Used for lookups where the
item may be dying.
These do NOT free the item — freeing is type-specific.
xfs_trans_ail_delete() Return Value
===================================
xfs_trans_ail_delete() currently returns void. Change it to return
bool: true if the item was removed from the AIL, false if it was not
in the AIL (shutdown case).
Callers use this to decide whether to drop the AIL reference:
.. code-block:: c
bool removed = xfs_trans_ail_delete(lip, shutdown_type);
if (removed && xfs_log_item_put(lip)) {
lockref_mark_dead(...)
type_specific_detach_and_free(item);
}
For callers that use xfs_ail_delete_one() directly (batch operations
under the AIL lock), the AIL reference drop must be deferred until
after the AIL lock is released. These callers already collect items
and process them after the lock is dropped (e.g. xfs_iflush_ail_updates
collects into a list, then xfs_iflush_finish processes after the lock).
The ref drop and potential free can be added to the post-lock
processing.
Log Item Lookup and Detachment via lockref + cmpxchg
====================================================
For log items attached to owner objects (BLI → buffer, inode item →
inode), the lockref enables lock-free lifecycle management:
Lookup (joining an existing item to a transaction):
.. code-block:: c
item = READ_ONCE(owner->item_pointer);
if (item && lockref_get_not_dead(&item->li_ref))
return item; /* existing item is alive, reuse it */
/* No item or item is dead — allocate a new one */
The load of the owner's item pointer must use ``READ_ONCE()`` to
ensure the pointer is loaded exactly once. The lookup can race
with a concurrent teardown that clears the pointer via ``cmpxchg``.
Without ``READ_ONCE()``, the compiler may reload the pointer after
the NULL check, potentially reading NULL on the second load and
passing it to ``lockref_get_not_dead()``. All type-specific get
wrappers (``xfs_inode_item_get()``, ``xfs_buf_item_get()``, etc.)
must use ``READ_ONCE()`` for the initial pointer load.
If lockref_get_not_dead returns false, the existing item is dead
(being torn down). We allocate a new item and attach it, knowing the
dead one is about to be freed.
Memory ordering consideration: further investigation is needed
into whether ``smp_load_acquire()`` / ``smp_store_release()`` is
required for the owner pointer load in ``_get()`` and the owner
pointer store during attachment. An unlocked lookup that races
with a concurrent ``xfs_trans_ijoin()`` creating and attaching a
new log item could potentially see the new pointer but read
incompletely initialised structure fields if the stores that
initialise the log item are reordered past the store that
publishes the pointer. ``smp_store_release()`` on the attachment
side and ``smp_load_acquire()`` on the lookup side would ensure
that all initialisation is visible before the pointer becomes
visible. Whether this is actually necessary depends on whether
all lookup callers hold locks that provide the needed barriers,
or whether truly unlocked lookups (e.g. ``xfs_inode_clean()``)
can race with initial attachment.
Attachment (new item to owner):
.. code-block:: c
cmpxchg(&owner->item_pointer, old_item, new_item)
where old_item is the dead item (or NULL). If the cmpxchg fails,
another thread already attached a new item — retry the lookup.
Detachment (dying item from owner):
When the last reference is dropped via lockref_put_or_lock(), it
returns with the lockref spinlock held. Under that lock:
a. lockref_mark_dead() — prevents any new gets
b. cmpxchg(&owner->item_pointer, item, NULL)
If the cmpxchg fails, a concurrent join already replaced the
pointer with a new item — the dead item is effectively detached.
c. Drop the lockref spinlock
d. Free the item
The mark_dead + cmpxchg under the lockref spinlock ensures that once
an item is being torn down, no concurrent lookup can get a reference
to it.
Brief coexistence of old and new items:
There is a brief window where a dead item and a new item both exist
for the same owner. This is safe because:
- The dead item is no longer reachable via the owner pointer
- The dead item's remaining teardown operates on the item itself
- The CIL/AIL track items by direct pointer, not via owner lookups
- The new item has its own independent lifecycle
Not all item types have owner objects with pointer-based attachment.
Inode items and dquot items are embedded in their owner structures
rather than being separately allocated and pointer-linked. These
types need different detachment strategies — see their individual
sections below.
AIL Reference Management
========================
The AIL takes a log item reference when an item is first inserted
(XFS_LI_IN_AIL transitions 0 → 1, detected by test_and_set_bit
returning 0 in xfs_trans_ail_update_bulk). Moving an item within
the AIL does not change the refcount.
The AIL reference is dropped by type-specific code after removing
the item from the AIL. The AIL removal (xfs_ail_delete_one /
xfs_trans_ail_delete) clears XFS_LI_IN_AIL but does not drop the
reference — the caller does that after the AIL lock is released.
The AIL cannot take references until the corresponding item type
supports dropping them. During the conversion, temporary scaffolding
in the AIL insert path conditionally takes references only for item
types that have been converted. Once all types are converted, the
scaffolding is replaced with an unconditional reference on insert.
Buffer Log Item (BLI) Modifications
===================================
See bli-lifecycle-analysis.rst for the full BLI lifecycle analysis.
Owner object: struct xfs_buf, via bp->b_log_item pointer.
Current refcount semantics:
- bli_refcount initialised to 0 at allocation
- Transaction join: +1
- CIL pin: +1
- Transaction release (iop_committing): -1
- CIL unpin: -1
- Refcount 0 + IN_AIL = alive (unusual state)
- IO completion frees at refcount 0
Changes needed:
a. Replace bli_refcount with li_ref (lockref).
b. xfs_buf_item_init(): use lockref_get_not_dead() to test whether
an existing bp->b_log_item is still alive. If dead, allocate a
new BLI and attach via cmpxchg.
c. xfs_buf_item_relse(): use lockref_put_or_lock, mark_dead, and
cmpxchg to detach from bp->b_log_item atomically. Remove the
ASSERT(xfs_buf_islocked) — buffer lock no longer needed for BLI
lifecycle.
d. xfs_buf_item_put(): remove the IN_AIL check at refcount 0.
With AIL reference, refcount cannot be 0 while IN_AIL.
e. xfs_buf_item_release() (iop_committing): the dirty+IN_AIL case
at refcount 0 can no longer occur. Simplify accordingly.
f. xfs_buf_item_done(): after xfs_trans_ail_delete, drop the AIL
ref via xfs_log_item_put. If last ref, mark dead + detach + free.
g. xfs_buf_item_finish_stale(): same restructuring as done().
h. xfs_buf_item_unpin(): after dropping pin ref, BLI has refcount
>= 1 if in AIL (AIL ref). Update comments.
Updated BLI refcount trace:
0 → 1 (trans join) → 2 (CIL pin) → 3 (AIL insert) →
2 (trans commit) → 1 (CIL unpin) → 0 (AIL remove at IO completion)
→ mark dead → detach from buffer → free
Inode Log Item Modifications
============================
See (future) inode-item-lifecycle-analysis.rst for detailed analysis.
Owner object: struct xfs_inode, via ip->i_itemp pointer. The inode
item is separately allocated but has complex interactions:
- Push model flushes inode to cluster buffer, not direct IO
- Batch AIL delete in xfs_iflush_ail_updates under AIL lock
- IO completion handling via xfs_buf_inode_iodone and b_li_list
Needs its own detailed lifecycle analysis and modification plan
similar to the BLI analysis.
Dquot Log Item Modifications
============================
See (future) dquot-item-lifecycle-analysis.rst for detailed analysis.
Owner object: struct xfs_dquot, with qli_item embedded (not a pointer).
Embedded items cannot use cmpxchg-based detachment — need different
strategy.
Needs its own detailed lifecycle analysis and modification plan
similar to the BLI analysis.
Intent Item Modifications (EFI, RUI, CUI, BUI, ATTRI, XMI)
==========================================================
See intent-item-lifecycle-analysis.rst for the detailed lifecycle
analysis of intent items, including the intent/done item pairing,
the implicit CIL reference model, and ordering considerations.
RT variants (RTEFI, RTRUI, RTCUI) follow the same pattern as their
non-RT counterparts.
This section describes how to convert intent and intent done items
to use the same generic reference counting model as all other log
item types: explicit references taken and dropped at well-defined
lifecycle points via the standard iop callbacks.
Intent Item References
----------------------
The intent item is created with a single reference: the transaction
reference. It then follows the standard log item lifecycle:
**Creation:**
refcount = 1 (transaction reference)
**Transaction commit into CIL:**
iop_pin: +1 (CIL pin reference), refcount becomes 2.
iop_committing: -1 (transaction reference), refcount becomes 1.
**Transaction abort (before CIL insertion):**
iop_release: -1 (transaction reference), refcount becomes 0. Item freed.
**CIL checkpoint completion (normal):**
AIL insert: +1 (AIL reference), refcount becomes 2.
iop_unpin: -1 (CIL pin reference), refcount becomes 1.
Intent item is now in the AIL with refcount 1 (AIL ref only),
awaiting the done item to remove it.
**CIL checkpoint abort:**
iop_unpin(abort): -1 (CIL pin reference), refcount becomes 0. Item freed.
Item was never inserted into AIL, no AIL ref exists.
Hold Reference for the Intent Done Item
---------------------------------------
Between committing the intent and creating the done item, there is
a window where a shutdown could race and free the intent (via CIL
abort → iop_unpin dropping to 0). The caller must hold a reference
to the intent across this window to ensure it remains alive until
the done item is created.
The sequence is:
1. Caller creates the intent item (refcount 1, transaction ref).
2. Caller takes a "hold" reference on the intent before committing
the transaction via ``xfs_log_item_get(intent)``, bringing the
refcount to 2.
3. Transaction commits. The standard CIL path runs:
``iop_pin: +1`` brings refcount to 3, then
``iop_committing: -1`` brings refcount to 2.
The intent now has: hold ref + CIL pin ref.
4. Caller creates the done item in a new transaction, gifting the
hold reference to the done item. The caller no longer owns that
reference — the done item is now responsible for it.
5. If an error occurs between step 2 and step 4 (e.g. shutdown),
the caller still holds the hold reference and must drop it.
The CIL abort will drop the pin ref via iop_unpin(abort). The
caller's drop of the hold ref will then free the intent (or
vice versa — the refcount handles either ordering).
Intent Done Item References
---------------------------
The done item follows the same standard lifecycle as any other log
item, plus it holds the gifted reference to the paired intent item.
**Creation:**
refcount = 1 (transaction reference).
The done item holds the gifted reference to the intent item.
**Transaction commit into CIL:**
iop_pin: +1 (CIL pin reference), refcount becomes 2.
iop_committing: -1 (transaction reference), refcount becomes 1.
**Transaction abort (before CIL insertion):**
iop_release: -1 (transaction reference), refcount becomes 0.
On last ref, the done item must: remove the intent item from the AIL
(if present), drop the gifted reference to the intent item, and free
itself.
**CIL checkpoint completion (normal):**
Done items have XFS_ITEM_RELEASE_WHEN_COMMITTED, so they are NOT
inserted into the AIL. Instead, iop_unpin: -1 (CIL pin reference),
refcount becomes 0. On last ref, the done item must: remove the intent
item from the AIL, drop the gifted reference to the intent item, and
free itself. The intent item's refcount after the done item completes:
before it was 1 (AIL ref only, CIL pin already dropped), the done item
removes from AIL dropping the AIL ref to 0 and freeing the intent.
**CIL checkpoint abort:**
iop_unpin(abort): -1 (CIL pin reference), refcount becomes 0.
Same cleanup as normal completion — remove intent from AIL, drop
gifted ref, free self. The intent may or may not be in the AIL
depending on whether its own checkpoint completed before the abort.
Normal Operation Refcount Trace
-------------------------------
Intent item:
1 (create) → 2 (hold ref) → 3 (CIL pin) → 2 (committing drops
trans ref) → 3 (AIL insert) → 2 (CIL unpin) → 1 (done item
removes from AIL, drops AIL ref) → 0 (done item drops hold ref)
→ free
Done item:
1 (create) → 2 (CIL pin) → 1 (committing drops trans ref) →
0 (CIL unpin, last ref) → cleanup intent → free self
Shutdown Race Refcount Traces
-----------------------------
Shutdown before done item is created:
Intent: 1 (create) → 2 (hold ref) → 3 (CIL pin) → 2 (committing)
Shutdown occurs. CIL abort: iop_unpin(abort) → 1 (hold ref).
Caller drops hold ref → 0 → free.
(Intent was never in AIL — no AIL ref to drop.)
Shutdown after done item is created, intent in AIL:
Intent in AIL with refcount 1 (AIL ref). Done item committed.
Shutdown aborts done item's CIL context.
Done item iop_unpin(abort) drops CIL pin → refcount 0.
Done item cleanup: removes intent from AIL, drops AIL ref on
intent → intent refcount 0 → intent freed.
Done item freed.
Shutdown after done item committed, before intent checkpoint completes:
Intent: CIL pin ref + hold ref (gifted to done) = refcount 2.
Done item committed, its checkpoint completes (or is aborted).
Done item cleanup: intent not yet in AIL (no AIL ref), drops
hold ref → intent refcount 1 (CIL pin only).
Intent's CIL abort: iop_unpin(abort) → refcount 0 → free.
Whiteout Refcount Trace
-----------------------
When the whiteout optimisation fires, the reconciled refcount model
handles it cleanly with no special cases:
**Intent item committed into CIL:**
1 (create) then 2 (hold ref for done) then 3 (iop_pin) then
2 (iop_committing drops trans ref).
Intent is in CIL with: CIL pin ref + hold ref.
**Done item committed into same checkpoint:**
xlog_cil_process_intents detects the pairing, marks the intent as a
whiteout, and calls the done item's iop_release which drops the hold
ref on the intent bringing its refcount to 1 (CIL pin ref only). The
done item is freed.
**Checkpoint push:**
The whiteout intent is moved to the whiteout list (not written to the
journal). After the commit record is written, xlog_cil_cleanup_whiteouts
calls iop_unpin(abort) which drops the CIL pin ref, bringing refcount
to 0 and freeing the intent.
No AIL reference is involved because the intent is never inserted
into the AIL. The entire lifecycle stays within the CIL scope, using
only the CIL pin reference and the hold reference (gifted to and
consumed by the done item).
Comparison with Current Model
-----------------------------
Current:
- Intent created with refcount 2 (implicit CIL + done refs)
- No iop_pin, no iop_committing, no iop_release
- Release function combines ref drop + AIL removal
- Implicit reference passing
Reconciled:
- Intent created with refcount 1 (transaction ref)
- Explicit hold ref taken before commit for done item
- Standard iop_pin/iop_unpin for CIL references
- Standard iop_committing/iop_release for transaction references
- AIL ref taken on insert, dropped on removal
- Done item explicitly removes intent from AIL and drops refs
- All references explicitly taken and dropped at defined points
- No implicit reference passing
ICREATE Item Modifications
==========================
Simple single-use items with no refcount. Freed by iop_release at
commit time (XFS_ITEM_RELEASE_WHEN_COMMITTED). May not need lockref
complexity — assess whether a simple refcount or even no refcount is
sufficient.
Quotaoff Item Modifications
===========================
Simple single-use items. Similar to ICREATE — assess whether lockref
is needed or if a simpler approach suffices.
Implementation Ordering
=======================
The implementation is ordered so that generic infrastructure is added
first, each item type is converted independently, and the AIL
behaviour change is the final step.
Generic infrastructure:
1. Add li_ref (lockref) to xfs_log_item, add helpers, init in
xfs_log_item_init(). No behaviour change.
2. Change xfs_trans_ail_delete() to return bool. No behaviour change.
Per-type conversion (order does not matter):
3. BLI: replace bli_refcount with li_ref, convert init/get/put/
relse, add get-not-dead lookup, add cmpxchg detachment.
4. Intent items: replace type-specific refcounts with li_ref.
5. Inode items: full analysis needed, then convert.
6. Dquot items: full analysis needed, then convert.
7. ICREATE/Quotaoff: assess and convert if needed.
AIL reference (requires all types converted):
8. For each converted type: add AIL ref drop to removal path, then
add conditional ref-get on AIL insert (type-checked scaffolding).
9. Once all types done: replace scaffolding with unconditional
ref-get on AIL insert.
10. Remove dead code: "refcount 0 + IN_AIL" checks, related asserts
and comments.
EOF
$ cat ili-dynamic-lifecycle-model.rst
.. SPDX-License-Identifier: GPL-2.0
==============================================
XFS Inode Log Item Dynamic Lifecycle Model
==============================================
This document describes the conversion of the inode log item (ILI)
from the current "allocate once, use forever" model to a dynamic
reference-counted lifecycle using the lockref on ``xfs_log_item``.
The key insight is that the ``ili_lock`` spinlock currently protecting
flush/dirty state interactions can be replaced by the lockref's
embedded spinlock, unifying state serialisation with reference
counting.
See ``inode-item-lifecycle-analysis.rst`` for the complete analysis
of the current ILI access patterns that this design must support.
Design Principles
=================
1. **Replace ili_lock with the lockref spinlock.** The lockref
embedded in ``li_ref`` contains a spinlock that serialises
reference count changes. All state changes currently protected
by ``ili_lock`` (fields, flush_lsn, commit_seq, datasync_seq)
will be serialised by the lockref's spinlock instead. This
means state changes are inherently atomic with respect to
reference count transitions — particularly the critical case
where IO completion drops the last reference while also clearing
flush state.
2. **Every accessor must hold a reference or prove liveness.** The
current code assumes ``ip->i_itemp`` is stable once set. With
dynamic lifecycle, every context that uses the ILI must either
hold a reference (guaranteeing the ILI stays alive) or use
``lockref_get_not_dead()`` to conditionally acquire one.
A context that already holds a reference does not need to prove
liveness — the reference IS the proof. For example, a
transaction context that has joined the inode holds a transaction
reference to the ILI, so all subsequent accesses within that
transaction (``xfs_trans_log_inode()``, ``iop_precommit``, etc.)
can use the ILI directly without ``get_not_dead()`` checks.
Contexts that do NOT hold a reference — such as
``xfs_iflush_cluster()`` iterating inodes under ILOCK — must
use ``lockref_get_not_dead()`` after taking the ILOCK. The
ILOCK serialises against concurrent ILI creation (a new
transaction dirtying the inode) and concurrent ILI teardown
(IO completion marking the inode clean and dropping the last
reference). But ILOCK alone does not guarantee the ILI is
alive — a racing IO completion on a different CPU can drop
the last reference without holding ILOCK. The
``lockref_get_not_dead()`` call resolves the race: it either
acquires a reference (ILI is alive, safe to use) or returns
false (ILI is dying/dead, skip this inode).
3. **CIL and AIL residency hold references.** The CIL pin takes a
reference (via ``iop_pin``) and drops it (via ``iop_unpin``).
The AIL takes a reference on first insertion and drops it on
removal. These replace the implicit lifetime guarantees
currently provided by ``i_pincount`` (for CIL) and the
"IN_AIL prevents reclaim" assert (for AIL).
The AIL reference is particularly important for writeback. The
inode must be in the AIL for the entire duration of IO — from
the point ``iop_push`` triggers writeback through to IO
completion removing it from the AIL. The AIL reference
guarantees the ILI remains alive across this entire window.
This means IO completion code (``xfs_buf_inode_iodone()``,
``xfs_iflush_ail_updates()``, ``xfs_iflush_finish()``) can
directly access the ILI without needing to prove liveness —
being under IO is proof that the AIL holds a reference. The
AIL reference is only dropped by IO completion itself, after
it has finished accessing the ILI and removed it from the AIL.
4. **Buffer attachment is NOT a reference.** The ILI holds a
reference to the cluster buffer (``li_buf``, ``xfs_buf_hold``),
not the other way around. The ILI's presence on ``b_li_list``
does not require a separate reference because the entire time
the ILI is attached to a buffer, it is already covered by one
of the "inode is dirty" keep-alive references:
- Precommit attaches the ILI to the buffer. At this point the
transaction reference is held.
- The transaction commits, taking a CIL pin reference before
dropping the transaction reference.
- CIL checkpoint completion inserts into the AIL (taking an AIL
reference) before dropping the CIL pin reference.
- The AIL pushes the inode, triggering writeback. The AIL
reference is held for the entire duration of IO.
- IO completion removes the ILI from the AIL and detaches it
from the buffer — both in the same ``xfs_buf_inode_iodone()``
call. The AIL reference is dropped after all ILI access is
complete, including the buffer detach in ``xfs_iflush_finish()``.
There is no window where the ILI is on the buffer list but not
covered by at least one of the transaction, CIL pin, or AIL
references. The AIL reference in particular spans the entire
writeback IO, and IO completion performs AIL removal and buffer
detach sequentially — the AIL reference is not dropped until
after ``xfs_iflush_finish()`` has finished accessing the ILI.
If the inode is re-dirtied during writeback, a new transaction
reference and CIL pin reference are added. When the original IO
completes, the inode is still dirty (``ili_fields != 0``), so
``xfs_iflush_finish()`` does NOT detach from the buffer. The
AIL entry is not removed either (``flush_lsn != li_lsn`` because
the inode was relogged). The ILI stays on the buffer list,
covered by the new AIL reference from the relog.
5. **The inode ``i_itemp`` pointer is the owner attachment.**
Attachment and detachment use atomic ``cmpxchg`` operations on
``ip->i_itemp`` and can safely run concurrently without any
external serialisation. See the "Log Item Lookup and Detachment
via lockref + cmpxchg" section in ``log-item-refcount-design.rst``
for the full model.
Long-Term Reference Holders
===========================
The ILI has three types of long-term reference holders. Each
reference has a well-defined acquisition and release point, and
the references chain together so the ILI always has at least one
reference from the time it is first dirtied until it is fully
clean and removed from the AIL.
Transaction reference
Acquired: ``xfs_trans_ijoin()`` — either by creating a new ILI
(born with refcount 1) or by ``lockref_get()`` on an existing one.
Released: ``iop_committing`` (dirty items) or ``iop_release``
(clean items / abort). The transaction reference is always the
first reference acquired and one of the first released.
CIL pin reference
Acquired: ``iop_pin`` — ``lockref_get()``.
Released: ``iop_unpin`` — ``lockref_put_or_lock()``.
The CIL pin reference replaces the current ``i_pincount`` for
ILI lifetime purposes. ``i_pincount`` remains for preventing
inode writeback while pinned, but is no longer load-bearing for
ILI liveness.
AIL reference
Acquired: on first AIL insertion (``test_and_set_bit(IN_AIL)``
returns 0 → ``lockref_get()``). Moving within AIL does not
change the refcount.
Released: after AIL removal — the caller drops via
``lockref_put_or_lock()`` after the AIL lock is released.
Note: there is no buffer attachment reference. As described in
design principle #4, the ILI's presence on the buffer's
``b_li_list`` is always covered by one of the above three
references. The ILI holds a reference to the buffer (via
``xfs_buf_hold``), not the reverse.
Note: the transaction reference and CIL pin reference overlap
briefly during commit. The sequence is: ``iop_pin`` (+1) then
``iop_committing`` (-1), so the total refcount does not drop
during the commit window.
Reference Continuity Across Transaction Rolls
----------------------------------------------
When a transaction is rolled (via ``xfs_trans_roll_inode()`` or
``xfs_defer_trans_roll()``), the old transaction is committed
and the inode is re-joined to a new transaction. The commit
drops the old transaction reference (via ``iop_committing``),
and the re-join must acquire a new one. There is a window
between these two events where the ILI must remain alive.
The current code implicitly relies on the CIL pin reference
(taken by ``iop_pin`` before ``iop_committing`` runs) to keep
the ILI alive across this window. However, depending on an
implicit side-effect of CIL pinning for ILI liveness is fragile
— it couples the reference model to CIL implementation details
and makes the lifecycle harder to reason about.
Instead, we make the reference handoff explicit by taking a
"hold" reference before the roll and transferring it to the new
transaction context via a new ``xfs_trans_irejoin()`` helper.
``xfs_defer_save_resources()`` and ``xfs_trans_roll_inode()``
currently log the inode (ensuring it is dirty) before committing.
At this save point, we take an explicit hold reference:
.. code-block:: c
/* In xfs_defer_save_resources, for each held inode: */
xfs_trans_log_inode(tp, ip, XFS_ILOG_CORE);
xfs_log_item_get(&ili->ili_item); /* hold ref for rejoin */
dres->dr_ip[dres->dr_inos++] = ip;
/* In xfs_trans_roll_inode: */
xfs_trans_log_inode(*tpp, ip, XFS_ILOG_CORE);
xfs_log_item_get(&ip->i_itemp->ili_item); /* hold ref for rejoin */
The hold reference keeps the ILI alive across the commit. The
commit sequence is:
1. ``iop_pin``: +1 (CIL pin ref)
2. ``iop_committing``: -1 (drops trans ref)
3. Refcount = hold ref + CIL pin ref (at minimum)
After the commit, the inode is re-joined to the new transaction
using ``xfs_trans_irejoin()`` instead of ``xfs_trans_ijoin()``:
.. code-block:: c
/*
* Re-join an inode to a new transaction after a roll. The caller
* holds a reference to the ILI that is transferred to the new
* transaction as its transaction reference. Unlike xfs_trans_ijoin(),
* this does not need to look up or allocate the ILI — it is known
* to exist and be alive because the caller holds a reference.
*/
void
xfs_trans_irejoin(
struct xfs_trans *tp,
struct xfs_inode *ip)
{
struct xfs_inode_log_item *iip = ip->i_itemp;
xfs_assert_ilocked(ip, XFS_ILOCK_EXCL);
ASSERT(iip != NULL);
ASSERT(iip->ili_lock_flags == 0);
/* Caller's hold reference becomes the transaction reference */
iip->ili_dirty_flags = 0;
xfs_trans_add_item(tp, &iip->ili_item);
}
``xfs_trans_irejoin()`` does not call ``lockref_get()`` — the
caller's hold reference IS the new transaction reference. This
makes the reference transfer explicit and avoids any dependency
on CIL pin timing.
The rejoin is done unconditionally, regardless of whether the
roll succeeded or failed. ``xfs_trans_roll()`` always returns a
new transaction even on failure, and the caller cancels that
transaction on error. The cancel calls ``iop_release`` on all
joined items, which drops the transaction reference (i.e. the
transferred hold reference). This means no special error
handling is needed for the hold reference — it is always
consumed by either a successful commit or an error cancel of
the new transaction.
``xfs_defer_restore_resources()`` already re-joins
unconditionally (regardless of roll error). It calls
``xfs_trans_irejoin()`` instead of ``xfs_trans_ijoin()`` for
saved inodes:
.. code-block:: c
/* In xfs_defer_restore_resources: */
for (i = 0; i < dres->dr_inos; i++)
xfs_trans_irejoin(tp, dres->dr_ip[i]);
``xfs_trans_roll_inode()`` is updated to also rejoin
unconditionally, matching the ``xfs_defer_trans_roll()`` pattern:
.. code-block:: c
int
xfs_trans_roll_inode(
struct xfs_trans **tpp,
struct xfs_inode *ip)
{
int error;
xfs_trans_log_inode(*tpp, ip, XFS_ILOG_CORE);
xfs_log_item_get(&ip->i_itemp->ili_item);
error = xfs_trans_roll(tpp);
xfs_trans_irejoin(*tpp, ip);
return error;
}
On success, the caller continues using the new transaction with
the inode joined. On failure, the caller cancels the new
transaction, which releases all items including the re-joined
inode — the hold reference is dropped by ``iop_release`` during
the cancel.
ILI Get/Put Wrappers
====================
The ILI needs type-specific get/put wrappers that follow the
generic log item lookup model described in the "Log Item Lookup
and Detachment via lockref + cmpxchg" section of
``log-item-refcount-design.rst``.
``xfs_inode_item_get(ip)`` loads ``ip->i_itemp``, checks it for
NULL, and if non-NULL calls ``lockref_get_not_dead()`` to
conditionally acquire a reference. It returns the ILI pointer on
success or NULL if there is no ILI or the ILI is dying. The
caller must ensure the inode itself remains valid (VFS ref, ILOCK,
or other structural guarantee).
``xfs_inode_item_put(iip)`` drops a reference via
``xfs_log_item_put()``. If this is the last reference, the
lockref spinlock is held on return and the caller must detach
from the inode, mark dead, and free.
Access Pattern Classification
=============================
Every current access to the ILI falls into one of these categories,
each with a different reference management strategy:
1. Existence check optimisation (NULL check, no dereference)
2. State read under lockref spinlock
3. Context-guaranteed stable access (caller prevents races)
4. Transaction-based modification (holds ILOCK)
5. IO completion state modification (no ILOCK)
6. Flush state modification (holds ILOCK + IFLUSHING)
7. Abort/shutdown state modification
8. Structural queries (AIL ordering)
The following sections describe each pattern and how it maps to
the new lifecycle model.
Pattern 1: Existence Check Optimisation
---------------------------------------
These contexts check ``ip->i_itemp != NULL`` to determine whether
a more expensive operation is needed. They do not dereference the
ILI pointer — they use the NULL/non-NULL result purely as a
fast-path filter to avoid unnecessary work on clean inodes.
Current callers:
- ``xfs_file_fsync()``: checks ``ip->i_itemp`` then calls
``xfs_fsync_flush_log()``
- ``xfs_log_force_inode()``: checks ``ip->i_itemp`` then reads
``ili_commit_seq``
These checks do not need a reference. The ``ip->i_itemp`` NULL
check is exactly the same situation as the initial pointer load
in the ``_get()`` wrapper — we do not call
``lockref_get_not_dead()`` if the pointer is NULL. The check is
a valid existence optimisation and can be left unchanged.
It is only once the caller proceeds to dereference the ILI
pointer (e.g. to read ``ili_commit_seq``) that an existence
guarantee is needed. That happens inside the called function
(e.g. ``xfs_fsync_flush_log()``), which must use the ``_get()``
wrapper to acquire a reference before accessing ILI fields.
See Pattern 2.
Pattern 2: State Read Under Lockref Spinlock
--------------------------------------------
These contexts read multiple related fields that must be
consistent with each other, currently using ``ili_lock`` to
serialise.
Current callers:
- ``xfs_fsync_flush_log()``: reads either ``ili_datasync_seq`` or
``ili_commit_seq`` under ``ili_lock``
- ``xfs_bmbt_to_iomap()``: reads ``ili_datasync_seq`` under
``ili_lock`` to set IOMAP_F_DIRTY
- ``xfs_iunpin()``: reads ``ili_commit_seq`` under ``ili_lock``
Converted pattern:
.. code-block:: c
/* xfs_bmbt_to_iomap example */
iip = xfs_inode_item_get(ip);
if (iip) {
spin_lock(&iip->ili_item.li_ref.lock);
if (iip->ili_datasync_seq)
iomap->flags |= IOMAP_F_DIRTY;
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_inode_item_put(iip);
}
Note: ``xfs_iunpin()`` is called with ILOCK held and the inode is
dirty (it was just logged in a transaction), so the ILI is
guaranteed to be alive. It can use ``lockref_get()`` (unconditional)
rather than ``lockref_get_not_dead()``. However, using the wrapper
is still correct and clearer.
Pattern 3: Context-Guaranteed Stable Access
-------------------------------------------
These contexts access ``ip->i_itemp`` and dereference it to read
ILI state, but do so in a context where the caller has guaranteed
that ``ip->i_itemp`` cannot change during the access. No reference
is needed because the caller's context prevents both concurrent
creation and concurrent removal of the ILI.
The key example is ``xfs_inode_clean()``, which checks
``!ip->i_itemp || !(ip->i_itemp->ili_fields & XFS_ILOG_ALL)``.
This dereferences ``ip->i_itemp`` to read ``ili_fields`` without
taking a lock or reference. This is safe because every caller
provides two guarantees:
a. **No concurrent creation.** The caller holds ILOCK (shared or
exclusive), which prevents any new ``xfs_trans_ijoin()`` from
allocating and attaching a new ILI. If ``ip->i_itemp`` is NULL,
it will remain NULL for the duration of the access.
b. **No concurrent removal.** The caller holds ``XFS_IFLUSHING``
on the inode, which prevents IO completion from running
``xfs_iflush_finish()`` and tearing down the ILI. If
``ip->i_itemp`` is non-NULL, it will remain valid for the
duration of the access.
Together, these two guarantees ensure ``ip->i_itemp`` is stable:
it is either NULL (and stays NULL) or points to a valid, live ILI
(and stays that way). No ``_get()`` wrapper or lockref operation
is needed.
Current callers and their guarantees:
``xfs_iflush_cluster()`` (``xfs_inode.c``):
Holds ILOCK_SHARED on each inode in the cluster. Sets
``XFS_IFLUSHING`` on each inode before checking
``xfs_inode_clean()``. ILOCK prevents new modifications from
creating an ILI, and IFLUSHING prevents IO completion from
removing one.
``xfs_reclaim_inode()`` (``xfs_icache.c``):
Holds ILOCK_EXCL and has set ``XFS_IFLUSHING``. At this point
the inode has been checked for pincount == 0 (so no CIL
activity), and IFLUSHING prevents writeback races. If the
inode is clean, reclaim proceeds; if dirty, reclaim is
deferred.
``xfs_reclaim_inode()`` assert (``xfs_icache.c``):
The ``ASSERT(xfs_inode_clean(ip))`` after reclaim processing
runs when the inode is being freed — no concurrent access is
possible.
This pattern can remain unchanged with dynamic ILI lifecycle.
The ILOCK + IFLUSHING combination that callers already provide
is exactly the guarantee needed to ensure ``ip->i_itemp``
stability without reference counting.
Pattern 4: Transaction-Based Modification
-----------------------------------------
Transaction operations hold ILOCK_EXCL on the inode, providing
structural serialisation against other transactions and against
inode reclaim. The ILI is either created or referenced at
``xfs_trans_ijoin()`` time and released at commit/abort.
Current callers:
- ``xfs_trans_ijoin()``: lazy-allocates ILI, sets lock_flags
- ``xfs_trans_log_inode()``: accumulates dirty_flags
- ``xfs_inode_item_precommit()``: merges dirty state under ili_lock,
attaches cluster buffer
- ``xfs_inode_item_committing()``: sets commit_seq under ili_lock,
releases ILOCK
- ``xfs_inode_item_release()``: releases ILOCK (clean item / abort)
- ``xfs_ifree()``: clears owner change flags under ili_lock
Converted pattern for ``xfs_trans_ijoin()``:
.. code-block:: c
iip = xfs_inode_item_get(ip); /* try existing */
if (!iip) {
xfs_inode_item_init(ip, mp); /* allocates with refcount 1 */
iip = ip->i_itemp;
}
/* iip now has transaction reference */
The transaction reference is acquired in ``xfs_trans_ijoin()`` (either
via the new allocation giving refcount 1, or via
``lockref_get_not_dead()`` on the existing ILI). It is released by
``iop_committing`` or ``iop_release``.
For ``xfs_inode_item_precommit()``, the lockref spinlock replaces
``ili_lock`` directly:
.. code-block:: c
spin_lock(&iip->ili_item.li_ref.lock);
if (!iip->ili_item.li_buf) {
spin_unlock(&iip->ili_item.li_ref.lock);
/* read cluster buffer */
spin_lock(&iip->ili_item.li_ref.lock);
iip->ili_item.li_buf = bp;
...
}
iip->ili_fields |= (flags | iip->ili_last_fields);
spin_unlock(&iip->ili_item.li_ref.lock);
No additional reference is taken when attaching the buffer.
The buffer attachment is covered by the transaction reference
(currently held) which chains into the CIL pin and AIL
references. See design principle #4.
For ``xfs_inode_item_committing()``:
.. code-block:: c
spin_lock(&iip->ili_item.li_ref.lock);
iip->ili_commit_seq = seq;
if (iip->ili_dirty_flags & ...)
iip->ili_datasync_seq = seq;
spin_unlock(&iip->ili_item.li_ref.lock);
iip->ili_dirty_flags = 0;
xfs_inode_item_release(lip);
/* drop transaction reference */
if (xfs_inode_item_put(iip))
xfs_inode_item_teardown(iip);
For ``xfs_ifree()``:
.. code-block:: c
/* ILOCK_EXCL held, ILI guaranteed alive */
spin_lock(&iip->ili_item.li_ref.lock);
iip->ili_fields &= ~(XFS_ILOG_AOWNER | XFS_ILOG_DOWNER);
spin_unlock(&iip->ili_item.li_ref.lock);
Pattern 5: IO Completion State Modification
-------------------------------------------
IO completion runs in softirq context without ILOCK. This is the
critical path where the lockref unification pays off: the state
cleanup and potential last-reference drop are atomic.
Current callers:
- ``xfs_iflush_finish()``: under ili_lock, checks ili_fields,
detaches from buffer if clean, clears last_fields/flush_lsn/
FLUSHING
- ``xfs_iflush_ail_updates()``: batch AIL delete under AIL lock
- ``xfs_buf_inode_iodone()``: orchestrates the above
The IO completion path in ``xfs_iflush_finish()`` currently does:
1. Lock ili_lock
2. Check if inode is clean (ili_fields == 0)
3. If clean: detach from buffer (li_buf = NULL, list_del)
4. Clear ili_last_fields, ili_flush_lsn, LI_FLUSHING
5. Unlock ili_lock
6. Clear XFS_IFLUSHING on inode
7. If detached: drop ILI's buffer reference
Converted pattern:
.. code-block:: c
spin_lock(&iip->ili_item.li_ref.lock);
ASSERT(iip->ili_item.li_buf == bp);
if (!iip->ili_fields) {
iip->ili_item.li_buf = NULL;
list_del_init(&iip->ili_item.li_bio_list);
drop_buffer = true;
}
iip->ili_last_fields = 0;
iip->ili_flush_lsn = 0;
clear_bit(XFS_LI_FLUSHING, &iip->ili_item.li_flags);
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_iflags_clear(iip->ili_inode, XFS_IFLUSHING);
if (drop_buffer)
xfs_buf_rele(bp);
No ILI reference is dropped here — the buffer detach only drops
the ILI's reference to the buffer, not a reference to the ILI.
The ILI's liveness during ``xfs_iflush_finish()`` is guaranteed
by the AIL reference (see design principle #3).
AIL reference handling in ``xfs_iflush_ail_updates()``:
This is a batch operation under the AIL lock. The AIL reference
is the last reference to the ILI in the normal clean-inode case
(transaction ref and CIL pin ref have long since been released).
Dropping it is the point where the ILI may reach refcount 0 and
be torn down.
However, ``xfs_iflush_ail_updates()`` runs BEFORE
``xfs_iflush_finish()`` in ``xfs_buf_inode_iodone()``. If the
AIL ref drop triggers teardown, ``xfs_iflush_finish()`` would
access freed memory when it iterates the same list.
Resolution: the AIL reference drop must be deferred until after
``xfs_iflush_finish()`` has completed all ILI access. The
sequence in ``xfs_buf_inode_iodone()`` becomes:
1. ``xfs_iflush_ail_updates()``: remove items from AIL, but do
NOT drop the AIL reference yet. Items that were removed are
flagged (e.g. via a bit or a separate list).
2. ``xfs_iflush_finish()``: detach clean inodes from buffer,
clear flush state. All ILI access is now complete.
3. Drop AIL references for items removed in step 1. This is where
the last reference may drop and teardown occurs.
.. code-block:: c
/* In xfs_buf_inode_iodone: */
xfs_iflush_ail_updates(ailp, &ail_updates);
/* items removed from AIL but AIL refs not yet dropped */
list_splice_tail(&ail_updates, &flushed_inodes);
xfs_iflush_finish(bp, &flushed_inodes);
/* all ILI state access complete */
/* now safe to drop AIL references */
list_for_each_entry_safe(lip, n, &ail_updates, li_bio_list) {
if (was_removed_from_ail(lip)) {
if (xfs_log_item_put(lip))
xfs_inode_item_teardown(INODE_ITEM(lip));
}
}
If the inode was re-dirtied during writeback, the inode will have
been relogged: ``flush_lsn != li_lsn``, so it is NOT removed from
the AIL. ``xfs_iflush_finish()`` sees ``ili_fields != 0`` and does
NOT detach from the buffer. The ILI remains alive with the AIL
reference from the relog. No ref drop occurs.
If the inode is clean but was NOT removed from the AIL (e.g.
``flush_lsn != li_lsn`` due to a concurrent AIL move), the AIL
ref is still held. ``xfs_iflush_finish()`` detaches from buffer
and drops the ILI's buffer reference, but the ILI stays alive
via the AIL ref until a subsequent IO completion removes it.
Pattern 6: Flush State Modification
------------------------------------
The flush path (``xfs_iflush()``) runs with ILOCK_EXCL or
ILOCK_SHARED + IFLUSHING. It copies inode data to the buffer and
then atomically moves ``ili_fields → ili_last_fields``.
Current code:
.. code-block:: c
spin_lock(&iip->ili_lock);
iip->ili_last_fields = iip->ili_fields;
iip->ili_fields = 0;
set_bit(XFS_LI_FLUSHING, &iip->ili_item.li_flags);
spin_unlock(&iip->ili_lock);
xfs_trans_ail_copy_lsn(mp->m_ail, &iip->ili_flush_lsn,
&iip->ili_item.li_lsn);
Converted: direct spinlock substitution:
.. code-block:: c
spin_lock(&iip->ili_item.li_ref.lock);
iip->ili_last_fields = iip->ili_fields;
iip->ili_fields = 0;
set_bit(XFS_LI_FLUSHING, &iip->ili_item.li_flags);
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_trans_ail_copy_lsn(mp->m_ail, &iip->ili_flush_lsn,
&iip->ili_item.li_lsn);
No refcount changes here — the ILI is alive because the
transaction that dirtied it took a reference, and the CIL pin
also holds one. The flush path runs between CIL pin and IO
completion.
``xfs_ifree_mark_inode_stale()`` also modifies fields under
``ili_lock`` (moving ``ili_fields → ili_last_fields`` for dirty
non-flushing inodes). Same direct substitution with the lockref
spinlock.
Pattern 7: Abort/Shutdown State Modification
--------------------------------------------
Abort paths clear all ILI state and remove from AIL. They occur
during shutdown, stale inode handling, or IO error processing.
``xfs_iflush_abort()``:
Current:
1. ``xfs_trans_ail_delete()`` — remove from AIL
2. ``spin_lock(&iip->ili_lock)``
3. Read ``li_buf``, call ``xfs_iflush_abort_clean()``
4. ``spin_unlock(&iip->ili_lock)``
5. Clear IFLUSHING, release buffer
Converted:
.. code-block:: c
bool removed = xfs_trans_ail_delete(&iip->ili_item, 0);
spin_lock(&iip->ili_item.li_ref.lock);
bp = iip->ili_item.li_buf;
xfs_iflush_abort_clean(iip);
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_iflags_clear(ip, XFS_IFLUSHING);
/* drop ILI's buffer reference */
if (bp)
xfs_buf_rele(bp);
/* drop AIL ref — may be the last reference */
if (removed) {
if (xfs_inode_item_put(iip))
xfs_inode_item_teardown(iip);
}
The only ILI reference dropped here is the AIL reference (if the
item was in the AIL). There is no buffer attachment reference —
the ILI holds a reference to the buffer, not the reverse. The
state cleanup under the lockref spinlock (``xfs_iflush_abort_clean``)
must be done before the AIL ref drop, because after the AIL ref
drop the ILI may be freed.
``xfs_iflush_shutdown_abort()``:
This function has a complex lock dance: take ili_lock, read li_buf,
take buf reference, drop ili_lock, lock buffer, retake ili_lock.
With lockref:
.. code-block:: c
spin_lock(&iip->ili_item.li_ref.lock);
bp = iip->ili_item.li_buf;
if (!bp) {
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_iflush_abort(ip);
return;
}
xfs_buf_hold(bp);
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_buf_lock(bp);
spin_lock(&iip->ili_item.li_ref.lock);
if (!iip->ili_item.li_buf) {
/* raced with another removal */
xfs_iflush_abort_clean(iip);
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_iflags_clear(ip, XFS_IFLUSHING);
xfs_buf_relse(bp);
return;
}
spin_unlock(&iip->ili_item.li_ref.lock);
xfs_iflush_abort(ip);
xfs_buf_relse(bp);
The caller of ``xfs_iflush_shutdown_abort()`` holds a VFS
reference to the inode (it's accessed via the inode cache during
shutdown), which prevents the inode from being freed. The ILI
may be freed during the abort, but that's fine — we don't access
it after the abort returns.
Pattern 8: Structural Queries (AIL Ordering)
--------------------------------------------
``xfs_lock_inodes()`` / ``xfs_lock_two_inodes()``:
These read ``ip->i_itemp->ili_item.li_flags`` (XFS_LI_IN_AIL) for
lock ordering. Currently they dereference ``ip->i_itemp`` without
any NULL check, which is a latent bug — the ILI is only guaranteed
to exist if the inode has been dirtied in a transaction. In
practice, these are called during operations where all inodes have
been joined to a transaction, so ``i_itemp`` is non-NULL.
With dynamic lifecycle, these callers hold the ILOCK on the inode
they're checking. A dirty ILI with a transaction reference cannot
be torn down while the ILOCK is held (the transaction reference
is dropped by ``iop_committing`` which also unlocks the ILOCK).
So the dereference is safe and no get/put is needed.
However, ``xfs_lock_inodes()`` checks previously-locked inodes'
``i_itemp`` to decide whether to trylock subsequent inodes. The
previously-locked inodes may not have been joined to a transaction
yet (they're locked for a rename, for example), but they do hold
ILOCK. If the inode has never been dirtied, ``i_itemp`` is NULL,
and the ``lp && test_bit(...)`` check handles that correctly —
it is the same existence check optimisation as Pattern 1.
i_pincount Interaction
======================
The current ``i_pincount`` serves dual duty:
1. Prevents inode writeback while modifications are pinned in the
CIL (checked in ``xfs_inode_item_push`` and ``xfs_iflush_cluster``)
2. Implicitly prevents inode reclaim while the ILI is in the CIL
(reclaim asserts ``i_pincount == 0``)
With explicit ILI references, the CIL pin reference on the ILI
handles #2 directly. ``i_pincount`` remains for #1 — it is the
mechanism that prevents the AIL push from triggering writeback
while the inode's modifications are still in the CIL.
The ``atomic_dec_and_lock`` in ``xfs_inode_item_unpin()`` currently
atomically drops pincount and takes ``ili_lock`` to clear the
commit sequence numbers. With lockref:
.. code-block:: c
if (atomic_dec_and_lock(&ip->i_pincount,
&iip->ili_item.li_ref.lock)) {
iip->ili_commit_seq = 0;
iip->ili_datasync_seq = 0;
spin_unlock(&iip->ili_item.li_ref.lock);
wake_up_bit(&ip->i_flags, __XFS_IPINNED_BIT);
}
/* then drop CIL pin reference */
if (xfs_inode_item_put(iip))
xfs_inode_item_teardown(iip);
The ``atomic_dec_and_lock`` uses the lockref spinlock, so the
commit_seq clearing is still serialised. The CIL pin reference
drop is separate — it may or may not be the last reference.
Refcount Lifecycle Traces
=========================
Normal transaction → flush → IO completion (inode stays clean):
::
xfs_trans_ijoin: alloc ILI, refcount = 1 (trans ref)
iop_precommit: attach buffer (no ref change)
iop_pin: refcount = 2 (+ CIL pin ref)
iop_committing: refcount = 1 (- trans ref)
AIL insert: refcount = 2 (+ AIL ref)
iop_unpin: refcount = 1 (- CIL pin ref)
--- inode is in AIL with refcount 1 (AIL ref only) ---
xfs_iflush: no refcount change (just moves field state)
IO completion:
iflush_finish: detach from buffer (no ref change, just drops
ILI's buffer reference via xfs_buf_rele)
drop AIL ref: refcount = 0 → teardown → free
Note: the AIL ref drop is deferred until after ``xfs_iflush_finish()``
completes all ILI access, so the ILI is alive for the entire IO
completion sequence.
Re-dirtied during writeback:
::
--- ILI has refcount 1 (AIL ref), first flush in progress ---
xfs_trans_ijoin: get existing ILI, refcount = 2 (+ trans ref)
iop_precommit: buffer already attached (no ref change)
iop_pin: refcount = 3 (+ CIL pin ref)
iop_committing: refcount = 2 (- trans ref)
AIL insert (move): no refcount change (already in AIL)
iop_unpin: refcount = 1 (- CIL pin ref)
--- original IO completes ---
iflush_finish: ili_fields != 0 (re-dirtied), buffer NOT detached
ail_updates: flush_lsn != li_lsn (relogged), NOT removed from AIL
no AIL ref dropped, refcount stays 1
--- subsequent flush + IO for the new dirty state ---
iflush_finish: ili_fields == 0, detach from buffer
drop AIL ref: refcount = 0 → teardown → free
Multiple rapid re-dirties:
::
Initial: alloc, trans, pin, commit, AIL, unpin → refcount 1 (AIL ref)
Re-dirty #1: trans_ijoin +1 (2), pin +1 (3), committing -1 (2),
AIL move +0, unpin -1 (1)
IO complete: no AIL removal (relogged) → still 1
Re-dirty #2: trans_ijoin +1 (2), pin +1 (3), committing -1 (2),
AIL move +0, unpin -1 (1)
IO complete: AIL remove, deferred ref drop → 0 → teardown
Shutdown while in CIL:
::
After commit: refs = CIL pin (1). AIL ref not yet taken.
CIL abort: iop_unpin drops CIL pin ref → refcount 0 → teardown
(Item was never in AIL, no AIL ref to drop. Buffer attachment
is cleaned up as part of teardown — ILI drops its buffer ref.)
Shutdown after AIL insertion:
::
After CIL checkpoint: refs = AIL ref (1). CIL pin already dropped.
xfs_iflush_abort: removes from AIL, cleans state, drops ILI's
buffer reference.
Drop AIL ref: refcount = 0 → teardown → free
Stale inode (cluster freed):
::
iop_committed returns -1 → not inserted into AIL (no AIL ref)
iop_committed calls iop_unpin → drops CIL pin ref → refcount 0
→ teardown → free
(Buffer attachment is cleaned up as part of teardown.)
Note: the stale case is straightforward because the CIL pin ref is
the only reference at the point ``iop_committed`` runs (the
transaction ref was already dropped by ``iop_committing``, and no
AIL ref was taken because ``iop_committed`` returned -1 to prevent
AIL insertion).
Teardown Sequence
=================
When ``xfs_inode_item_put()`` returns true (last reference), the
caller holds the lockref spinlock. Teardown must:
1. ``lockref_mark_dead()`` — prevents any new ``get_not_dead()``
2. ``cmpxchg(&ip->i_itemp, iip, NULL)`` — detach from inode.
If cmpxchg fails, a concurrent ``xfs_trans_ijoin()`` already
allocated and attached a new ILI (this can happen if the dying
ILI's last ref was the CIL unpin, and a new transaction on the
same inode raced). The old ILI is effectively detached.
3. Drop the lockref spinlock
4. ``kvfree(iip->ili_item.li_lv_shadow)``
5. ``kmem_cache_free(xfs_ili_cache, iip)``
.. code-block:: c
static void
xfs_inode_item_teardown(
struct xfs_inode_log_item *iip)
{
struct xfs_inode *ip = iip->ili_inode;
/* lockref spinlock held on entry (from put_or_lock) */
ASSERT(!iip->ili_fields);
ASSERT(!iip->ili_last_fields);
lockref_mark_dead(&iip->ili_item.li_ref);
cmpxchg(&ip->i_itemp, iip, NULL);
spin_unlock(&iip->ili_item.li_ref.lock);
kvfree(iip->ili_item.li_lv_shadow);
kmem_cache_free(xfs_ili_cache, iip);
}
The asserts verify that the ILI is fully quiesced before teardown:
no dirty fields, no pending flush state. These are invariants — if
any context still has work to do with the ILI, it would hold a
reference and we would not reach teardown. The buffer detach
(``li_buf = NULL``, list removal) and the ILI's buffer reference
drop (``xfs_buf_rele``) must be done by the caller before reaching
teardown, since the teardown path does not handle buffer cleanup.
xfs_trans_ijoin() Allocation Race
=================================
With dynamic ILI lifecycle, two threads can race:
Thread A: IO completion drops last ref → teardown begins
Thread B: ``xfs_trans_ijoin()`` → ``xfs_inode_item_get()``
Scenario 1: Thread B calls ``lockref_get_not_dead()`` BEFORE
Thread A calls ``lockref_mark_dead()``:
- Thread B gets a reference (success). Thread A's
``lockref_put_or_lock()`` returned false (not last ref).
- No race. Thread B reuses the existing ILI.
Scenario 2: Thread B calls ``lockref_get_not_dead()`` AFTER
Thread A calls ``lockref_mark_dead()``:
- Thread B's ``lockref_get_not_dead()`` returns false.
- Thread B allocates a new ILI.
- Thread B attaches via ``cmpxchg(&ip->i_itemp, old, new)``.
If ``old == dying_iip``, cmpxchg succeeds (Thread A's cmpxchg
in teardown will see the new value and know it was superseded).
If ``old == NULL`` (Thread A already detached), cmpxchg
succeeds trivially.
- If ``old`` is something else (another thread already attached a
new ILI), Thread B's cmpxchg fails. Thread B frees its new ILI
and retries ``xfs_inode_item_get()``.
Scenario 3: Thread A and Thread B both in teardown (impossible):
- Only one thread can be "the last ref" via lockref_put_or_lock().
This ensures lock-free, race-free ILI lifecycle management.
Inode Reclaim Interaction
=========================
Current reclaim (``xfs_inode_free_callback()``) asserts the ILI
is not in the AIL, then calls ``xfs_inode_item_destroy()``. With
dynamic lifecycle:
- If the inode is clean at reclaim time, the ILI has already been
freed by the IO completion teardown. ``ip->i_itemp`` is NULL.
No action needed.
- If ``ip->i_itemp`` is non-NULL at reclaim, it means the ILI is
still alive. This should only happen if the inode was never
flushed (e.g. it was dirtied but then the filesystem shut down
before flush). The existing assertions (not in AIL, pincount 0)
still apply. The reclaim path drops the inode's implicit "owner"
reference — but with the new model, there IS no owner reference
per se. The inode pointer is just a lookup path, not a reference.
Resolution: at reclaim time, if ``ip->i_itemp`` is non-NULL, the
ILI must have refcount 0 or be dead (all references were dropped
during shutdown cleanup). We can assert this and then free
directly, or just check for NULL and skip if already freed:
.. code-block:: c
if (ip->i_itemp) {
ASSERT(!test_bit(XFS_LI_IN_AIL,
&ip->i_itemp->ili_item.li_flags));
/* ILI should have been freed by teardown, but shutdown may
* leave it stranded. Force-free it here. */
xfs_inode_item_destroy(ip);
}
In the shutdown case, the ILI may still be attached because the
shutdown abort path cleared its state but other references (from
the CIL abort) may have been the ones to actually drop the last
ref. Need to verify the shutdown teardown ordering ensures the
ILI is freed before reclaim runs. The RCU callback delay for
reclaim provides a grace period.
Actually, reclaim is ordered: IRECLAIM is set under i_flags_lock,
then the inode is removed from the radix tree, then
``call_rcu()`` defers ``xfs_inode_free_callback()``. All
active references (CIL, AIL, transactions) must have completed
before the inode reaches the reclaim path. So if the ILI is still
attached at ``xfs_inode_free_callback()`` time, it must be a
stranded item from shutdown. The existing ``xfs_inode_item_destroy()``
is the correct cleanup.
Summary of ili_lock → lockref Spinlock Mapping
===============================================
Every current use of ``spin_lock(&iip->ili_lock)`` maps directly
to ``spin_lock(&iip->ili_item.li_ref.lock)``:
================================== ======================================
Current ili_lock use Notes on conversion
================================== ======================================
precommit: merge dirty state Direct substitution.
committing: set commit_seq Direct substitution. Transaction ref
dropped after unlock.
unpin: clear commit_seq ``atomic_dec_and_lock`` with lockref
spinlock. CIL pin ref dropped after.
iflush: move fields Direct substitution.
iflush_finish: check/clear fields Direct substitution. ILI's buffer ref
(xfs_buf_rele) dropped after unlock.
iflush_abort: clean state Direct substitution. AIL ref dropped
after unlock and buffer cleanup.
iflush_shutdown_abort: lock dance Direct substitution (same dance with
lockref spinlock).
ifree_mark_inode_stale: move fields Direct substitution.
ifree: clear owner fields Direct substitution.
xfs_iunpin: read commit_seq Direct substitution.
xfs_fsync_flush_log: read seq Direct substitution.
xfs_bmbt_to_iomap: read seq Direct substitution.
xfs_log_force_inode: read seq Direct substitution.
================================== ======================================
Implementation Steps
====================
1. **Add li_ref lockref to xfs_log_item** (generic infrastructure,
shared with BLI/intent item conversions). Init in
``xfs_log_item_init()``. No behaviour change.
2. **Add xfs_inode_item_get/put wrappers.** Initially they just
wrap the raw pointer access with lockref_get_not_dead. No
callers changed yet.
3. **Replace ili_lock with lockref spinlock in all accessors.**
Mechanical substitution — change ``&iip->ili_lock`` to
``&iip->ili_item.li_ref.lock`` in all 14 call sites. Remove
``spin_lock_init(&iip->ili_lock)`` from ``xfs_inode_item_init``
(lockref init handles it). Remove ``ili_lock`` from the struct.
No behaviour change beyond which lock is acquired.
4. **Add CIL pin reference.** In ``iop_pin``, add ``lockref_get()``.
In ``iop_unpin``, add ``lockref_put_or_lock()`` after the
``i_pincount`` handling. If last ref, call teardown.
5. **Add AIL reference.** In the AIL insertion path, add
``lockref_get()`` when ``test_and_set_bit(IN_AIL)`` transitions
0→1. In ``xfs_iflush_ail_updates()`` and ``xfs_iflush_abort()``,
drop the AIL ref after AIL removal. If last ref, call teardown.
Note: in ``xfs_buf_inode_iodone()``, the AIL ref drop must be
deferred until after ``xfs_iflush_finish()`` completes.
6. **Add transaction reference management.** In ``xfs_trans_ijoin``,
use ``xfs_inode_item_get()`` instead of raw ``ip->i_itemp``
access. New ILI allocation starts with refcount 1. In
``iop_committing`` and ``iop_release``, add ref drop. If last
ref, call teardown.
7. **Add xfs_inode_item_teardown.** Implements mark_dead + cmpxchg
detachment + free. Called from any put that returns true (last
ref).
8. **Convert read-only accessors.** ``xfs_fsync_flush_log``,
``xfs_bmbt_to_iomap``, ``xfs_log_force_inode``: use get/put
wrapper around ILI access. ``xfs_inode_clean``: keep lockless
(safe per analysis above).
9. **Remove xfs_inode_item_destroy.** It is replaced by teardown.
Keep a stripped version for the reclaim-time stranded ILI
cleanup (shutdown case only).
10. **Remove ili_lock field from struct.** Already removed in step 3.
11. **Update assertions.** Remove ``ASSERT(ip->i_itemp != NULL)``
in paths that now use the get wrapper. Keep assertions in paths
where liveness is structurally guaranteed (e.g. transaction
context that has joined the inode).
Open Questions
==============
1. **xfs_inode_item_precommit buffer read lock drop.** The current
code drops ``ili_lock``, reads the cluster buffer, reacquires
``ili_lock``. With lockref, dropping the spinlock is fine — the
transaction reference prevents teardown. But is the lockref
``lock`` field safe to drop and reacquire while the refcount is
> 0? Yes — the lockref spinlock is just a spinlock; the special
lockref semantics only apply to the ``lockref_*`` functions.
Holding the spinlock directly is the same as any spinlock.
2. **Performance of get/put on hot paths.** The lockref fast path
uses ``cmpxchg`` for get/put, avoiding the spinlock in the
common case. This should be comparable to the current
``atomic_inc``/``atomic_dec`` on ``i_pincount``. Measure on
real workloads.
3. **Inode reclaim ordering.** Need to verify that all ILI
references are guaranteed to be dropped before the RCU callback
runs ``xfs_inode_free_callback()``. The current model guarantees
this via IRECLAIM + pincount == 0 + not-in-AIL. With refcounting,
the guarantee is: all holders of ILI references observe the inode
state changes (IRECLAIM, etc.) and drop their references before
the RCU grace period expires.
4. **xfs_lock_inodes i_itemp dereference.** Should add a NULL check
for robustness, even though current callers guarantee non-NULL.
This is a pre-existing issue, not specific to this conversion.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert
2026-09-10 5:06 ` Dave Chinner
@ 2026-09-10 15:47 ` Jeffin Philip
2026-09-10 16:19 ` Carlos Maiolino
2026-09-11 0:26 ` Dave Chinner
0 siblings, 2 replies; 5+ messages in thread
From: Jeffin Philip @ 2026-09-10 15:47 UTC (permalink / raw)
To: dgc
Cc: cem, linux-kernel, linux-xfs, syzbot+a4fde844548510369112,
syzkaller-bugs, jeffinphilip14
On Wed, Sep 09, 2026, Dave Chinner wrote:
>The short term fix is to slap a bandaid on xfs_buf_submit() to wait
>for unpin before erroring out a pinned buffer on shutdown. That,
>however, doesn't fix the underlying lack of full lifecycle reference
>counts for log items.
>
>The long term fix is have the AIL require a reference count to be
>held on log items it tracks, and only have that refcount removed
>when the item is removed from the AIL. That's a much bigger rework
>of the code.
>
>FYI, I've attached two document below - the design for
>generic full lifecycle log item reference counting, and the design
>for converting the inode log item to be dynamic similar to the buf
>log item so we don't end up with millions of ILIs that are never
>used left sitting around in memory until it's owner inode is
>reclaimed.
I read the documents and have started writing the wrappers. Is anyone
else working on it?
Thanks,
Jeffin.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert
2026-09-10 15:47 ` Jeffin Philip
@ 2026-09-10 16:19 ` Carlos Maiolino
2026-09-11 0:26 ` Dave Chinner
1 sibling, 0 replies; 5+ messages in thread
From: Carlos Maiolino @ 2026-09-10 16:19 UTC (permalink / raw)
To: Jeffin Philip
Cc: dgc, linux-kernel, linux-xfs, syzbot+a4fde844548510369112,
syzkaller-bugs
On Thu, Sep 10, 2026 at 09:17:25PM +0530, Jeffin Philip wrote:
> On Wed, Sep 09, 2026, Dave Chinner wrote:
>
> >The short term fix is to slap a bandaid on xfs_buf_submit() to wait
> >for unpin before erroring out a pinned buffer on shutdown. That,
> >however, doesn't fix the underlying lack of full lifecycle reference
> >counts for log items.
> >
> >The long term fix is have the AIL require a reference count to be
> >held on log items it tracks, and only have that refcount removed
> >when the item is removed from the AIL. That's a much bigger rework
> >of the code.
> >
> >FYI, I've attached two document below - the design for
> >generic full lifecycle log item reference counting, and the design
> >for converting the inode log item to be dynamic similar to the buf
> >log item so we don't end up with millions of ILIs that are never
> >used left sitting around in memory until it's owner inode is
> >reclaimed.
>
> I read the documents and have started writing the wrappers. Is anyone
> else working on it?
>
I was going to as it clashes with some work I've been doing on this
area, but if you already started go for it.
> Thanks,
> Jeffin.
>
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert
2026-09-10 15:47 ` Jeffin Philip
2026-09-10 16:19 ` Carlos Maiolino
@ 2026-09-11 0:26 ` Dave Chinner
1 sibling, 0 replies; 5+ messages in thread
From: Dave Chinner @ 2026-09-11 0:26 UTC (permalink / raw)
To: Jeffin Philip
Cc: cem, linux-kernel, linux-xfs, syzbot+a4fde844548510369112,
syzkaller-bugs
On Thu, Sep 10, 2026 at 09:17:25PM +0530, Jeffin Philip wrote:
> On Wed, Sep 09, 2026, Dave Chinner wrote:
>
> >The short term fix is to slap a bandaid on xfs_buf_submit() to wait
> >for unpin before erroring out a pinned buffer on shutdown. That,
> >however, doesn't fix the underlying lack of full lifecycle reference
> >counts for log items.
> >
> >The long term fix is have the AIL require a reference count to be
> >held on log items it tracks, and only have that refcount removed
> >when the item is removed from the AIL. That's a much bigger rework
> >of the code.
> >
> >FYI, I've attached two document below - the design for
> >generic full lifecycle log item reference counting, and the design
> >for converting the inode log item to be dynamic similar to the buf
> >log item so we don't end up with millions of ILIs that are never
> >used left sitting around in memory until it's owner inode is
> >reclaimed.
>
> I read the documents and have started writing the wrappers. Is anyone
> else working on it?
Go ahead, but I wouldn't get too deep into it before any level of
design review has been performed.
FWIW, I've attached below the BLI lifecycle analysis I performed and
documented for the last one of these BLI UAF syzbot reports. If
should help give some background knowledge and insight into how
complex the log item interactions with the rest of the code are and
help people understand why I'm proposing we move to a generic log
item reference counting model.
It will also show that my previous analysis (which was trying to
work out how the READ IO completion path ran a write IO completion)
missed the xfs_buf_submit -> shutdown -> run completion on pinned
buffer code -> UAF in CIL shutdown processing path noted by this
syzbot report.
i.e. I analysed the AIL writeback path through
xfs_buf_delwri_submit(), but that doesn't allow pinned buffers into
the delwri buffer list. It didn't occur to me at the time there were
other paths to xfs_buf_delwri_submit() that could contain dirty
pinned buffers in the buffer list....
Cheers,
Dave.
--
Dave Chinner
dgc@kernel.org
============================================
XFS Buffer Log Item (BLI) Lifecycle Analysis
============================================
This documents the lifecycle of a buffer log item (xfs_buf_log_item)
from transaction acquisition through CIL commit, AIL management, and
writeback IO completion.
Reference Counting Model
========================
The BLI uses an atomic refcount (bli_refcount) with the following
unusual properties:
1. The BLI can exist with bli_refcount == 0 when it is resident in
the AIL. The AIL does not hold a BLI reference — it tracks the
item via the li_ail list linkage, and the buffer's b_log_item
pointer keeps the BLI accessible.
2. The pin operation (iop_pin) takes a BLI reference plus a buffer
hold and a buffer pin. The unpin operation (iop_unpin) drops all
three. This means the BLI is guaranteed to exist while pinned,
but once unpinned and inserted into the AIL, only the AIL linkage
and b_log_item pointer keep it alive.
3. The BLI is freed in xfs_buf_item_relse(), which asserts refcount
== 0 and the item is NOT in the AIL before detaching from the
buffer and freeing the structure.
4. A BLI can be simultaneously resident in the AIL and the CIL.
This happens when a buffer is dirtied by a new transaction while
a prior checkpoint's version of the BLI is still in the AIL
awaiting writeback. The new transaction re-pins the BLI into
the current CIL context while the AIL still tracks it from the
previous checkpoint.
5. Multiple active CIL references can exist at the same time. Each
CIL context that the BLI is first inserted into takes a pin
reference (bli_refcount + 1). If a buffer is dirtied across
multiple CIL checkpoints before any of them complete, the BLI
will have a pin reference from each outstanding checkpoint plus
the current transaction's reference. The refcount only reaches
0 when all checkpoints have completed and all transactions have
committed.
Phase 1: Transaction Acquisition
================================
Entry: xfs_trans_get_buf_map() or xfs_trans_read_buf_map():
1. Look up buffer in the buffer cache (xfs_buf_get_map / xfs_buf_read).
2. Check if the buffer is already joined to this transaction via
xfs_trans_buf_item_match(). If found, increment bli_recur and
return the buffer — the existing BLI and its transaction reference
are reused.
3. For a new buffer, call ``_xfs_trans_bjoin()``, which calls
``xfs_buf_item_init()`` followed by taking a transaction reference.
``xfs_buf_item_init()`` has two cases:
- No existing BLI (``bp->b_log_item == NULL``):
- Allocate a new BLI via kmem_cache_zalloc (bli_refcount initialised to 0),
- initialise the log item, attach to ``bp->b_log_item``
- take a buffer hold (``xfs_buf_hold``).
- Existing BLI (``bp->b_log_item != NULL``):
- The BLI is reused.
- This occurs when a prior transaction dirtied the buffer and the BLI is still
tracking it.
- The existing BLI's refcount depends on its current state:
- refcount == 0: the BLI is only resident in the AIL. All prior CIL
checkpoints that pinned it have completed and unpinned it. The BLI
is awaiting writeback.
- refcount > 0: the BLI is resident in the CIL (pinned by one or more
checkpoints that have not yet completed). It may also simultaneously
be in the AIL from an earlier checkpoint.
In both cases, ``xfs_buf_item_init()`` returns immediately without
modifying the BLI or its refcount.
After ``xfs_buf_item_init()`` returns, ``_xfs_trans_bjoin()`` then:
- ``atomic_inc(&bip->bli_refcount)`` — takes the transaction's reference.
For a newly allocated BLI this is 0 → 1. For a reused BLI this
increments whatever the current refcount is.
- Adds the BLI to the transaction's item list.
- Sets ``bp->b_transp = tp``.
State after acquisition:
- bli_refcount >= 1 (transaction reference)
- Buffer is locked and held
- BLI may or may not be in the AIL (from prior transaction)
- bli_recur tracks recursive gets within the same transaction
Phase 2: Transaction Modifications
==================================
``xfs_trans_log_buf(tp, bp, first, last)``:
- Calls ``xfs_trans_dirty_buf()`` which sets:
- XFS_BLI_DIRTY and XFS_BLI_LOGGED on bli_flags
- XFS_LI_DIRTY on the log item
- Marks the specific byte range dirty in the BLI's bitmap
``xfs_trans_dirty_buf(tp, bp)``:
- Sets XBF_DONE on the buffer
- Sets XFS_BLI_DIRTY | XFS_BLI_LOGGED
- If buffer was previously stale, clears stale state
``xfs_trans_binval(tp, bp)``:
- Sets XFS_BLI_STALE on the BLI
- Sets XBF_STALE on the buffer
- Sets XFS_BLF_CANCEL in the on-disk format
- Clears the dirty bitmap (the buffer content won't be logged)
- Marks as dirty/logged in the transaction (so it flows through CIL)
``xfs_trans_bhold(tp, bp)``:
- Sets XFS_BLI_HOLD — prevents buffer unlock at transaction commit
- Used when the caller needs the buffer to remain locked across
transaction boundaries (e.g. btree cursor holding a buffer while
rolling a transaction)
Phase 3: Transaction Commit — CIL Insertion
===========================================
``xfs_trans_commit()`` calls ``xlog_cil_commit()`` which:
#. For each dirty log item (including dirty BLIs):
- Calls ``iop_size()`` and ``iop_format()`` to prepare log vectors
- Inserts the formatted log vector into the CIL context
#. Pin (``xfs_buf_item_pin``) called the FIRST time an item enters a
CIL context (``li_lv == NULL``):
- ``xfs_buf_hold(bp)`` — take a buffer reference for the pin
- ``atomic_inc(&bli_refcount)`` — take the pin's BLI reference
- ``atomic_inc(&bp->b_pin_count)`` — pin the buffer against writeback
State: bli_refcount >= 2 (transaction + pin)
#. iop_committing (xfs_buf_item_committing) — called for every item
after CIL insertion, while still holding cil->xc_ctx_lock:
- Calls ``xfs_buf_item_release()`` which:
- Clears ``bp->b_transp = NULL``
- Clears ``XFS_BLI_LOGGED | XFS_BLI_HOLD | XFS_BLI_ORDERED``
- ``atomic_dec(&bli_refcount)`` — drops the transaction's reference
- If refcount reaches 0:
- Stale: calls ``xfs_buf_item_finish_stale()`` (see stale handling)
- Aborted/shutdown: calls ``xfs_buf_item_done()`` to remove from AIL
- Clean (not dirty): calls ``xfs_buf_item_relse()`` to free BLI
- Dirty: asserts BLI is in AIL (normal case after checkpoint
completion has already run — see race discussion)
- If not HOLD and not stale: ``xfs_buf_relse()`` (unlock + release)
Normal case after commit: ``bli_refcount == 1`` (pin reference only),
buffer is unlocked and released (but still held by pin reference).
CRITICAL: The iop_committing callback runs AFTER CIL insertion
and pin, but the CIL checkpoint can start (and complete!) before
iop_committing runs. This means:
- The item may have already been unpinned (refcount dropped by
unpin) before iop_committing drops the transaction reference
- The item may have already been inserted into the AIL and be
at refcount 0 (AIL-resident, no active references) before
iop_committing runs
- For stale buffers, this race is particularly important because
the last reference drop must perform completion processing
Phase 4: CIL Checkpoint — Journal Write
=======================================
When the CIL is pushed (xlog_cil_push_work):
#. The CIL context is switched and items are collected.
#. The checkpoint is written to the journal.
#. On journal IO completion (xlog_cil_committed):
- For each item in the checkpoint:
- Insert into the AIL via xfs_trans_ail_update_bulk()
- Call iop_unpin(lip, 0) — normal unpin
#. Unpin (xfs_buf_item_unpin, remove=0):
- freed = atomic_dec_and_test(&bli_refcount) — drops pin reference
- atomic_dec(&bp->b_pin_count) — unpin buffer, wake waiters
- If refcount > 0 (transaction reference still held — race case):
- Just drop the buffer reference (xfs_buf_rele) and return
- If refcount == 0 (normal case — transaction already committed):
- Non-stale: drop the buffer pin reference (xfs_buf_rele)
The BLI is now in the AIL with refcount == 0.
- Stale: drop extra buf ref, call xfs_buf_item_finish_stale(),
then xfs_buf_relse() the buffer
State after unpin (normal): bli_refcount == 0, item in AIL,
buffer unpinned and available for writeback.
CRITICAL: The BLI now exists with refcount == 0. The only things
keeping it alive are:
- bp->b_log_item pointer
- AIL list linkage (li_ail)
Any code that accesses the BLI must either hold the buffer lock
or the AIL lock to prevent the BLI from being freed underneath it.
Phase 5: AIL Writeback
======================
The AIL periodically pushes items toward the log tail. For BLIs, it calls
``xfs_buf_item_push()``:
- Called with AIL lock held
- Checks if buffer is pinned — returns XFS_ITEM_PINNED if so
- Tries to lock the buffer (trylock) — returns XFS_ITEM_LOCKED if it can't
- If buffer is locked:
- Adds buffer to the delwri queue (xfs_buf_delwri_queue)
- Unlocks the buffer
- Returns XFS_ITEM_FLUSHING
The buffer then sits on the delwri queue unlocked until the AIL
submits the queue. At submission time, the buffer is relocked and
rechecked — if it has been repinned (by a new transaction dirtying
it between push and submission) or otherwise cannot be written, the
IO is skipped. If the checks pass, the IO is submitted.
There is a significant window between iop_push() and actual IO
submission where the buffer is unlocked but the item is in the
ITEM_FLUSHING state. During this window:
- bli_refcount == 0 (assuming no active CIL pins)
- The buffer is unlocked and accessible to new transactions
- A new transaction can lock the buffer, find the existing BLI,
join it (incrementing refcount 0 → 1), dirty it, and commit —
which re-pins the BLI into the CIL
- The delwri submission will detect the re-pin and skip the IO
No BLI reference is taken for writeback. The BLI's existence is
guaranteed because:
- It's in the AIL (xfs_buf_item_relse asserts not-in-AIL)
- xfs_buf_item_done() is only called from IO completion, which
only occurs after IO has been successfully submitted
Phase 6: IO Completion
======================
xfs_buf_ioend() — called when buffer IO completes. The buffer is
locked for the entire duration of IO completion processing — it was
locked at IO submission time and remains locked until xfs_buf_ioend()
completes and the buffer is unlocked/released. This means
xfs_buf_item_done() and xfs_buf_item_relse() always run under the
buffer lock, which serialises BLI freeing against any other code that
holds the buffer lock and accesses the BLI.
#. For write completions:
- If error: calls error handler (xfs_buf_ioend_handle_error)
- If success:
- Clears retry state
- If bp->b_log_item exists: calls xfs_buf_item_done(bp)
#. xfs_buf_item_done():
- xfs_trans_ail_delete() — removes BLI from AIL
- xfs_buf_item_relse() — frees the BLI:
- Asserts bli_refcount == 0
- Asserts not in AIL (just removed)
- Clears bp->b_log_item = NULL
- xfs_buf_rele(bp) — drops the BLI's buffer hold (from init)
- Frees the BLI structure
State after IO completion: BLI is freed. Buffer's b_log_item is NULL. Buffer may
still exist in the cache for future use.
Stale Buffer Lifecycle
======================
Stale buffers (xfs_trans_binval) have a special lifecycle because the
buffer must remain locked from the time it's marked stale until all
completion processing is done. This prevents the stale buffer from
being found in the cache and used by another thread.
The stale path:
#. xfs_trans_binval() marks BLI stale, sets XFS_BLF_CANCEL
#. At CIL insertion (Phase 3b), the BLI is pinned normally.
#. At iop_committing (Phase 3c, xfs_buf_item_release):
- The transaction drops its reference
- If this was the last reference (refcount → 0):
- Calls xfs_buf_item_finish_stale() — removes from AIL, frees BLI
- Calls xfs_buf_relse(bp) — unlocks and releases the buffer
- Returns immediately — no further processing
- If pin reference still exists (normal case):
- Does NOT unlock the buffer (stale flag prevents unlock)
- Returns with buffer still locked — the pin/unpin path owns it
#. At checkpoint completion (Phase 4d, xfs_buf_item_unpin):
- Drops pin reference
- If this was the last reference (refcount → 0):
- Drops extra buffer ref
- Calls xfs_buf_item_finish_stale() — removes from AIL, frees BLI
- Calls xfs_buf_relse(bp) — unlocks and releases the buffer
#. Race between committing and completing:
Either iop_committing or iop_unpin can be the last reference
holder. Whichever drops refcount to 0 performs the stale
completion. The buffer remains locked until that point.
The race exists because the CIL checkpoint can complete (triggering
unpin) before the transaction commit finishes calling iop_committing.
The stale buffer protocol handles this by:
- Never unlocking the buffer at iop_committing if stale and
the pin reference still exists
- Having the last reference dropper always perform completion
- Completion always unlocks the buffer as its final step
Race Windows and Invariants
===========================
R1. CIL checkpoint completion vs. iop_committing race:
The CIL checkpoint can complete (calling iop_unpin) before the
committing transaction finishes calling iop_committing. When this
happens:
For normal (non-stale) buffers:
- iop_unpin drops pin reference: refcount → 1 (transaction ref)
- iop_unpin sees refcount > 0, just drops buffer ref and returns
- BLI is now in the AIL with refcount 1
- iop_committing later runs xfs_buf_item_release:
- Drops transaction ref: refcount → 0
- BLI is dirty and in AIL: leaves it alone (line 748-749)
- Unlocks buffer (normal release path)
For stale buffers:
- iop_unpin drops pin reference: refcount → 1
- iop_unpin sees refcount > 0, drops buffer ref and returns
- Buffer is still locked (stale protocol)
- iop_committing runs xfs_buf_item_release:
- Drops transaction ref: refcount → 0
- Stale: calls xfs_buf_item_finish_stale()
- xfs_buf_relse() unlocks and releases buffer
OR (opposite ordering):
- iop_committing drops transaction ref: refcount → 1 (pin ref)
- Buffer is NOT unlocked (stale flag prevents it at line 757)
- iop_unpin drops pin ref: refcount → 0
- Stale: calls xfs_buf_item_finish_stale()
- xfs_buf_relse() unlocks and releases buffer
R2. AIL push vs. IO completion race:
The AIL pushes a buffer for writeback via iop_push, which adds
it to the delwri queue and unlocks it. Later the delwri submission
relocks the buffer, rechecks state, and submits IO. The buffer
remains locked from IO submission through IO completion and
xfs_buf_item_done(). This means:
- A subsequent AIL push (iop_push) will trylock the buffer and
fail with XFS_ITEM_LOCKED while IO is in flight
- xfs_buf_item_done() runs under the buffer lock, so it cannot
race with any code that also holds the buffer lock
- Between iop_push unlock and delwri relock, the buffer is
accessible — but xfs_buf_item_done() cannot run during this
window because IO has not been submitted yet
R3. xfs_buf_item_put() with zero refcount and AIL residency:
When xfs_buf_item_put() drops refcount to 0, it checks whether
the BLI is in the AIL (line 622). If it is, the BLI remains alive
with refcount 0 — the AIL residency keeps it logically active even
though no code holds a counted reference.
This is the most unusual aspect of the model. The BLI at refcount
0 in the AIL is only protected by:
- The buffer lock (must be held for IO completion to free it)
- The AIL lock (protects the AIL list)
Any code accessing a BLI must ensure one of these locks is held.
R4. Transaction re-acquisition of an AIL-resident BLI:
When a new transaction gets a buffer that already has a BLI in the
AIL (refcount 0), xfs_buf_item_init() finds the existing BLI and
returns early. _xfs_trans_bjoin() then increments bli_refcount
from 0 → 1. This is safe because the buffer is locked at this
point, preventing concurrent IO completion from freeing the BLI.
BLI Reference Count Summary
===========================
Event bli_refcount change
----- ----------------------
_xfs_trans_bjoin() +1 (transaction reference)
xfs_buf_item_pin() +1 (CIL pin reference)
xfs_buf_item_release() -1 (transaction reference dropped)
xfs_buf_item_unpin() -1 (CIL pin reference dropped)
xfs_buf_item_done() asserts == 0, then frees
Normal lifecycle refcount trace:
0 → 1 (trans join) → 2 (CIL pin) → 1 (trans commit/release) →
0 (CIL unpin, AIL insert) → freed (IO completion)
The BLI exists at refcount 0 between CIL unpin and IO completion.
During this window, the BLI is in the AIL and the buffer is
available for writeback. Only the buffer lock protects the BLI
from being freed by IO completion while other code accesses it.
Buffer Reference Summary
========================
Event bp reference change
----- ----------------------
xfs_buf_item_init() +1 hold (BLI's reference to buffer)
xfs_buf_item_pin() +1 hold (pin's reference)
xfs_buf_item_unpin() -1 rele (pin's reference)
xfs_buf_item_relse() -1 rele (BLI's reference, at free)
xfs_buf_item_release() -1 relse (unlock+rele, if not hold/stale)
The buffer always has at least one reference from the BLI (taken
at init, released at relse) ensuring the buffer cannot be freed
while the BLI exists. Additional references from pins and
transaction holds ensure the buffer survives through the pipeline.
Inode Buffer Interactions
=========================
Inode buffers have several special properties that make their BLI
lifecycle more complex than regular metadata buffers.
Inode Buffer Types
------------------
There are three distinct buffer marking functions used during
different stages of inode buffer lifecycle:
xfs_trans_inode_buf(tp, bp):
Marks the buffer as an inode buffer (XFS_BLI_INODE_BUF). This
tells recovery to only replay the di_next_unlinked fields from
the buffer, not the full inode contents. This is set when logging
unlinked list modifications to an inode cluster buffer. Also sets
bp->b_iodone = xfs_buf_inode_iodone.
xfs_trans_inode_alloc_buf(tp, bp):
Marks the buffer as containing newly allocated inodes
(XFS_BLI_INODE_ALLOC_BUF). This flag has special AIL insertion
behaviour — see xfs_buf_item_committed() below. Also sets
bp->b_iodone = xfs_buf_inode_iodone.
xfs_trans_stale_inode_buf(tp, bp):
Marks the buffer as a stale inode buffer (XFS_BLI_STALE_INODE).
Used during xfs_ifree_cluster() when freeing an entire inode
cluster. Also sets bp->b_iodone = xfs_buf_inode_iodone.
Inode Allocation Buffer AIL Pinning
-----------------------------------
xfs_buf_item_committed() returns a special LSN for inode allocation
buffers. When a buffer with XFS_BLI_INODE_ALLOC_BUF is subsequently
relogged as an XFS_BLI_INODE_BUF (i.e. only the di_next_unlinked
fields are logged), the iop_committed callback returns the original
LSN rather than the new checkpoint's LSN. This keeps the buffer
pinned at its original position in the AIL.
This is necessary because:
- The original allocation log entry contains the full inode images
- The relog only contains di_next_unlinked updates
- If the buffer were moved forward in the AIL to the relog's LSN,
the original allocation record could be overwritten in the log
- Recovery needs the full inode images from the allocation to
initialise the inode cluster; without them, recovery fails
Inode Item Push Through Inode Cluster Buffer
--------------------------------------------
When the AIL pushes an inode item (xfs_inode_item_push), it does NOT
write the inode directly. Instead:
1. It locks the inode's cluster buffer (lip->li_buf, set during
xfs_inode_item_precommit)
2. Takes a buffer hold (xfs_buf_hold) for the flush operation
3. Calls xfs_iflush_cluster(bp) to flush all dirty inodes in the
cluster into the buffer
4. Queues the buffer for delwri writeback (xfs_buf_delwri_queue)
5. Releases the buffer (xfs_buf_relse)
xfs_iflush_cluster() iterates all inode items on bp->b_li_list,
flushing each dirty inode into the buffer via xfs_iflush(). The
b_li_list tracks inode log items attached to the buffer, not buffer
log items — this is separate from the BLI tracked via b_log_item.
The cluster buffer may also have a BLI (b_log_item) if the buffer
was logged for unlinked list updates. In this case, both the BLI and
multiple inode items are tracked on the same buffer, but through
different mechanisms:
- BLI: bp->b_log_item (single BLI for buffer-level changes)
- Inode items: bp->b_li_list (list of inode items for inode-level
changes flushed into the buffer)
IO Completion for Inode Buffers
-------------------------------
When IO completes on an inode buffer, xfs_buf_ioend() runs:
1. If bp->b_log_item exists: calls xfs_buf_item_done() to handle
the BLI (removes from AIL, frees BLI — standard Phase 6)
2. If bp->b_iodone is set (xfs_buf_inode_iodone): calls it to
handle all inode items on bp->b_li_list
xfs_buf_inode_iodone() processes each inode item:
- Stale inodes (XFS_ISTALE): calls xfs_iflush_abort() to clean up
- Flushed inodes: updates AIL position, clears flush state
The BLI completion (step 1) and inode item completion (step 2) both
run under the buffer lock during IO completion. They do not race with
each other.
xfs_iflush_cluster() Failure and xfs_buf_fail() Race
----------------------------------------------------
When xfs_iflush_cluster() encounters an error (e.g. corruption
detected during inode flush), it:
1. Calls xfs_force_shutdown() to shut down the filesystem
2. Calls xfs_buf_fail(bp) to simulate IO completion with -EIO
xfs_buf_fail() is called with the buffer locked. It marks the buffer
stale, sets -EIO, and calls xfs_buf_ioend(). This runs the full IO
completion path including xfs_buf_item_done() (for the BLI) and
xfs_buf_inode_iodone() (for attached inode items).
Race with BLI push: xfs_iflush_cluster() is called from
xfs_inode_item_push(), which is the inode item's AIL push callback.
This runs with the buffer locked and the AIL lock dropped. The
buffer's BLI may have already been pushed for writeback by a prior
AIL push (xfs_buf_item_push). Consider:
1. AIL pushes the BLI via xfs_buf_item_push():
- Locks buffer, queues to delwri list, unlocks buffer
- Returns XFS_ITEM_FLUSHING
2. AIL pushes an inode item on the same buffer via
xfs_inode_item_push():
- Locks buffer (succeeds because BLI push unlocked it)
- Calls xfs_iflush_cluster() which fails
- Calls xfs_buf_fail(bp)
- xfs_buf_ioend() runs xfs_buf_item_done() — frees the BLI
3. Meanwhile the delwri queue still references this buffer. When
delwri submission relocks the buffer, the BLI has already been
freed by the xfs_buf_fail() path. The buffer is now stale so
delwri submission should detect this and skip it.
The reverse is also possible:
1. AIL pushes an inode item, xfs_iflush_cluster() succeeds
2. Buffer queued to delwri, unlocked
3. AIL pushes the BLI on the same buffer
- BLI's iop_push locks the buffer, queues to delwri (or finds
it already queued), unlocks
4. Both pushes want the same buffer written back — the delwri queue
handles this because xfs_buf_delwri_queue() is idempotent for
buffers already on the queue.
Inode Cluster Freeing (xfs_ifree_cluster)
-----------------------------------------
When an entire inode cluster is freed, xfs_ifree_cluster():
1. Gets the cluster buffer into the transaction (xfs_trans_get_buf)
2. Marks all cached inodes as XFS_ISTALE
3. Calls xfs_trans_stale_inode_buf() — sets XFS_BLI_STALE_INODE
4. Calls xfs_trans_binval() — marks buffer and BLI stale
The XFS_BLI_STALE_INODE flag causes special handling in
xfs_buf_item_finish_stale(). Instead of the normal stale BLI
cleanup, it:
- Calls xfs_buf_item_done() — removes BLI from AIL, frees it
- Calls xfs_buf_inode_iodone() — processes stale inode items,
calling xfs_iflush_abort() for each to clean up their AIL state
- Asserts bp->b_li_list is empty after processing
This is necessary because stale inode buffers have inode items
attached that must also be cleaned up when the buffer is invalidated.
Normal stale buffer processing (xfs_buf_item_finish_stale without
STALE_INODE) only handles the BLI itself.
Shutdown Impact on Inode Buffer Processing
------------------------------------------
During shutdown, xfs_iflush_cluster() detects the shutdown state
(xlog_is_shutdown) for each inode it processes:
- Waits for the inode to be unpinned (xfs_iunpin_wait)
- Calls xfs_iflush_abort() to clean up the inode's AIL state
- Sets error = -EIO but continues processing remaining inodes
- After the loop, calls xfs_force_shutdown() + xfs_buf_fail()
The shutdown sequence in xfs_iflush_cluster is ordered deliberately:
1. xfs_force_shutdown() first — kills the log
2. xfs_buf_fail() second — fails the buffer
This ordering matters for INODE_ALLOC buffers because if the buffer
is failed before the log is shut down, the buffer unpin could allow
the ICREATE intent to be removed from the log. If the system crashes
after that but before the inode cluster is initialised on disk,
recovery would fail because the ICREATE intent is gone but the inode
cluster buffer was never written.
Shutdown Impact on BLI Lifecycle
================================
A filesystem shutdown (triggered by xfs_force_shutdown()) can occur at
any point during the BLI lifecycle. Shutdown is detected by testing
xlog_is_shutdown(log). Once set, shutdown state is permanent — it
never clears. The shutdown changes how BLIs are processed at every
stage of the lifecycle, introducing additional race windows between
normal completion paths and the shutdown-triggered abort paths.
Shutdown and Transaction Commit (Phase 3)
-----------------------------------------
xfs_buf_item_release() (called via iop_committing) checks for both
XFS_LI_ABORTED and xlog_is_shutdown(). If either is true and the
refcount drops to zero, it calls xfs_buf_item_done() to remove the
BLI from the AIL and free it, rather than leaving it in the AIL for
writeback.
This handles the case where a shutdown occurs after the BLI has been
inserted into the AIL by a prior checkpoint but before the current
transaction's commit completes. The BLI may be dirty and in the AIL,
but because we're shut down there's no point leaving it for writeback.
For stale buffers during shutdown, xfs_buf_item_release() can be
the last reference holder if the checkpoint aborted before unpinning.
In this case it calls xfs_buf_item_finish_stale() which removes from
the AIL and frees the BLI, then unlocks the buffer. This is the
same completion path as the normal stale case but triggered from
transaction commit rather than checkpoint completion.
Shutdown and CIL Checkpoint Completion (Phase 4)
------------------------------------------------
When a CIL checkpoint fails (journal IO error) or the log is already
shut down when xlog_cil_committed() runs, it sets abort = true.
xlog_cil_ail_insert() with abort:
- Sets XFS_LI_ABORTED on every log item in the checkpoint
- Does NOT insert items into the AIL
- Calls iop_unpin(lip, 1) with remove=true for each item
xfs_buf_item_unpin() with remove=true:
- Drops the BLI refcount and buffer pin count as normal
- If refcount > 0 (transaction ref still held): drops buffer ref
and returns. The abort flag is set on the item for
xfs_buf_item_release() to handle later.
- If refcount == 0 (transaction already committed):
- For stale buffers: drops extra ref, calls finish_stale, unlocks
- For non-stale: calls xfs_buf_fail(bp) which:
- Locks the buffer
- Marks it stale, sets -EIO
- Calls xfs_buf_ioend() which runs xfs_buf_item_done()
- This removes from AIL (or handles not-in-AIL) and frees BLI
- The buffer is then released (async flag set by xfs_buf_fail)
Shutdown and IO Completion (Phase 6)
------------------------------------
xfs_buf_ioend_handle_error() checks xlog_is_shutdown() first. If the
log is shut down:
- Skips all retry logic
- Marks the buffer stale
- Returns false, allowing xfs_buf_ioend() to continue
- xfs_buf_ioend() then calls xfs_buf_item_done() which removes the
BLI from the AIL and frees it
If a permanent IO error triggers a shutdown (xfs_force_shutdown in
xfs_buf_ioend_handle_error), subsequent error handling takes the
shutdown path.
For transient errors (not yet permanent, not yet shutdown):
- Sets XFS_LI_FAILED on the BLI's log item
- Clears XFS_LI_FLUSHING
- Releases the buffer (unlock + rele)
- The buffer is NOT freed — the BLI remains in the AIL
- The AIL will re-push the item later for retry
Shutdown and xfs_buf_item_put()
-------------------------------
xfs_buf_item_put() handles the case where a dirty BLI has refcount
dropping to 0 but is NOT in the AIL. This can happen when:
- A checkpoint aborted, setting XFS_LI_ABORTED on the BLI
- The BLI was never inserted into the AIL (abort skips AIL insert)
- A subsequent xfs_trans_brelse() releases a clean reference to the
same buffer, calling xfs_buf_item_put()
- The refcount reaches 0 with a dirty, aborted BLI not in the AIL
In this case, xfs_buf_item_put() frees the BLI via
xfs_buf_item_relse(). It asserts that a dirty BLI not in the AIL
must have XFS_LI_ABORTED set.
Three-Way Shutdown Race: Stale Buffers
--------------------------------------
The most complex shutdown race involves stale buffers where three
events can race:
1. Transaction commit (iop_committing → xfs_buf_item_release)
2. Checkpoint completion (xlog_cil_ail_insert → iop_unpin)
3. Shutdown occurring concurrently
Scenario A — Shutdown during normal stale processing:
- Transaction commits, iop_committing drops refcount → 1 (pin ref)
- Buffer stays locked (stale protocol)
- Shutdown occurs
- Checkpoint completion runs with abort=true
- iop_unpin(remove=true) drops refcount → 0
- Stale path: drops extra ref, calls finish_stale, unlocks buffer
Result: Handled correctly — unpin is the last ref, does completion.
Scenario B — Shutdown before checkpoint completion:
- Checkpoint is written, but journal IO hasn't completed
- Shutdown occurs
- Transaction commit runs iop_committing:
- Sees aborted or xlog_is_shutdown()
- Drops refcount → 1 (pin ref still held)
- Stale: does NOT unlock buffer (line 757)
- Returns without doing completion
- Later, checkpoint abort runs iop_unpin(remove=true):
- Drops refcount → 0
- Stale: finish_stale, unlock buffer
Result: Handled correctly — unpin does completion.
Scenario C — Shutdown causes iop_committing to be last ref:
- Checkpoint completes BEFORE transaction finishes commit
- iop_unpin drops refcount → 1 (transaction ref)
- Buffer ref dropped, returns
- Shutdown occurs
- Transaction commit runs iop_committing:
- Drops refcount → 0 (last ref)
- Stale: calls xfs_buf_item_finish_stale()
- xfs_buf_relse() unlocks and releases buffer
Result: Handled correctly — committing does completion.
Scenario D — Shutdown with already-aborted checkpoint:
- CIL push fails before journal IO starts
- Checkpoint runs xlog_cil_committed with abort=true
- XFS_LI_ABORTED set on BLI
- iop_unpin(remove=true) called:
- If refcount → 0: stale finish or xfs_buf_fail()
- If refcount > 0: drops ref, returns. Transaction commit
will find LI_ABORTED and handle via xfs_buf_item_done()
Result: Handled correctly — either unpin or committing cleans up.
Key invariant: For stale buffers, exactly one of iop_committing or
iop_unpin will be the last reference holder. Whichever it is performs
completion processing. The buffer remains locked until that point,
preventing any other access. The shutdown state does not change this
fundamental invariant — it only determines which path runs last and
whether the XFS_LI_ABORTED flag is set.
Shutdown and AIL Writeback (Phase 5)
------------------------------------
When the filesystem is shut down, the AIL may still contain dirty
BLIs. The AIL push will still attempt to process them:
- xfs_buf_item_push() checks if the buffer is pinned. During
shutdown, buffers may be unpinned by abort processing, making
them eligible for push.
- The buffer is added to the delwri queue and unlocked.
- At delwri submission, xfs_buf_delwri_submit() relocks the buffer.
If the filesystem is shut down, the IO submission will fail and
xfs_buf_ioend_handle_error() takes the shutdown path, which
marks the buffer stale and allows xfs_buf_item_done() to clean up.
Alternatively, the AIL can be emptied directly during shutdown via
the abort path in xlog_cil_ail_insert() which calls
iop_unpin(remove=true) → xfs_buf_fail() → xfs_buf_ioend() →
xfs_buf_item_done(). This removes BLIs from the AIL without
requiring actual writeback.
UAF Scenario Analysis: Syzbot BLI free during read IO
=====================================================
Observed scenario (syzbot, v7.1-rc3, 5 hits in 12 hours):
- Single-threaded application reads a buffer, BLI is created
- Later the same buffer is "read again", IO submitted, and
xfs_buf_ioend for the read IO frees the BLI
- CIL push trips over the freed BLI → use-after-free
- Corruption detected at xfs_btree_lookup_get_block (xfs_bnobt
block 0x8) shortly before
- KASAN "freed by" stack:
kmem_cache_free ← __xfs_buf_ioend ← xfs_buf_iowait ←
xfs_buf_read_map ← xfs_trans_read_buf_map ←
xfs_btree_read_buf_block ← xfs_btree_lookup_get_block ←
xfs_btree_lookup ← xfs_alloc_fixup_trees
Note: __xfs_buf_ioend is an older name for xfs_buf_ioend; there is
no difference in logic or lifecycle handling between versions.
Analysis of code paths:
1. Read IO completion cannot free a BLI.
xfs_buf_ioend() branches on XBF_READ (line 1114). The read path
(lines 1114-1123) runs the verifier and sets XBF_DONE on success.
It does NOT call xfs_buf_item_done() or interact with the BLI in
any way. Only the write/else path (lines 1124-1149) calls
xfs_buf_item_done() at line 1145-1146.
Therefore, for a read IO to free the BLI, xfs_buf_ioend() must
take the write/else branch. This requires XBF_READ to NOT be set
in b_flags when the check at line 1114 runs.
2. _xfs_buf_read() correctly sets XBF_READ.
_xfs_buf_read() (line 618) does:
.. code-block:: c
bp->b_flags &= ~(XBF_WRITE | XBF_ASYNC | XBF_READ_AHEAD | XBF_DONE);
bp->b_flags |= XBF_READ;
This clears any write flags and sets XBF_READ before submitting
the IO. For synchronous reads, XBF_ASYNC is cleared, so
xfs_buf_iowait() runs xfs_buf_ioend() in the caller's context.
3. xfs_buf_submit() shutdown path preserves XBF_READ.
If the log is shut down when xfs_buf_submit() runs (line 1379),
it takes the ioerror path which clears XBF_DONE and stales the
buffer (lines 1407-1408), but does NOT clear XBF_READ. For sync
IO, it signals b_iowait completion (line 1413). xfs_buf_iowait
then calls xfs_buf_ioend which still sees XBF_READ and takes the
read path. No BLI freeing.
4. xfs_buf_fail() takes the write path.
xfs_buf_fail() (line 1194) does NOT set XBF_READ. When it calls
xfs_buf_ioend(), the write/else branch is taken, which calls
xfs_buf_item_done() and frees the BLI. But xfs_buf_fail() is
only called from shutdown/abort paths, not from the synchronous
read chain shown in the KASAN stack.
5. xfs_buf_find_lock() resets stale buffers but preserves b_log_item.
When a stale buffer is found in the cache (line 424), all flags
except _XBF_KMEM are cleared and b_ops is set to NULL (lines
430-431). But b_log_item is NOT cleared. This is correct — the
BLI intentionally survives across stale events because:
- The BLI tracks the buffer through its entire log lifecycle
- The CIL always relogs the full dirty state on relog
- The BLI pins the buffer in memory preventing reclaim
- A stale+reinit+relog sequence produces the correct journal
contents because the relog captures the new buffer contents
After xfs_buf_find_lock clears XBF_STALE and XBF_DONE, a
subsequent xfs_buf_read_map() will re-read the buffer from disk.
This is expected — the buffer is being reinitialised with fresh
on-disk contents. The BLI remains attached and will track any
new modifications made by the transaction that reads it.
6. xfs_buf_reverify() cannot clear XBF_DONE on buffers with b_ops.
xfs_buf_reverify() (line 649) returns immediately without doing
anything if bp->b_ops is already set:
.. code-block:: c
if (!ops || bp->b_ops)
return 0;
A buffer that was previously read and dirtied through a
transaction always has b_ops set from the original read. So
reverify cannot be the mechanism that clears XBF_DONE on a
dirty cached buffer. This eliminates reverify failure as a
possible cause.
7. Readahead cannot clear XBF_DONE on a DONE buffer.
xfs_buf_readahead_map() (line 757) checks XBF_DONE first. If
set, it calls xfs_buf_reverify (which returns immediately if
b_ops is set), then releases the buffer. The flag-clearing code
at line 764 only runs for !DONE buffers. So readahead cannot
modify a DONE buffer's state.
Exhaustive search for XBF_DONE clearing paths:
XBF_DONE is cleared by:
a. _xfs_buf_read() — line 618. This is the re-read itself.
b. xfs_buf_submit() ioerror path — line 1407.
c. xfs_buf_read_map() error path — line 718.
d. xfs_buf_readahead_map() — line 764 (only for !DONE buffers).
e. xfs_buf_reverify() — line 655 (only when b_ops was NULL and
verifier fails; cannot occur on normally-used buffers).
f. xfs_buf_find_lock() — line 430 (clears all flags on stale
buffers; this is the normal reinitialisation path).
g. xfs_buf_fail() — line 1200 (shutdown/abort path only).
Of these, only (f) can clear XBF_DONE on a buffer with an active
BLI during normal operation. This is expected behaviour — the stale
buffer is being reinitialised, and the BLI correctly survives.
Conclusion:
No code path exists in the current kernel where read IO completion
in xfs_buf_ioend() can free a BLI. The read branch (XBF_READ set)
has no BLI interaction. The write/else branch (XBF_READ not set)
calls xfs_buf_item_done() which frees the BLI, but this branch
cannot be reached during a properly-flagged read IO.
The buffer is locked during the entire synchronous read path (from
_xfs_buf_read through xfs_buf_iowait and xfs_buf_ioend), preventing
concurrent modification of b_flags. No code within the locked read
path clears XBF_READ between _xfs_buf_read setting it (line 619)
and xfs_buf_ioend checking it (line 1114).
Given:
- Exhaustive code analysis shows no legitimate path for read IO
to free a BLI
- The buffer lock prevents concurrent b_flags modification during
synchronous read IO
- The bug was seen only 5 times in 12 hours on v7.1-rc3, never
on any other kernel version
- The system was running syzbot with many concurrent tests
The most likely explanation is external memory corruption from an
unrelated kernel bug that stomped on the buffer's b_flags field,
clearing XBF_READ between _xfs_buf_read setting it and
xfs_buf_ioend checking it. With XBF_READ cleared, xfs_buf_ioend
takes the write/else branch, calls xfs_buf_item_done(), frees the
BLI, and the subsequent CIL access to the freed BLI triggers the
KASAN use-after-free report.
The corruption detection at xfs_btree_lookup_get_block ("xfs_bnobt
block 0x8") is likely a real error from the underlying filesystem
image having been intentionally corrupted as part of the syzbot
test, not related to the memory corruption that caused the UAF.
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-11 0:26 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-10 0:16 [syzbot] [xfs?] KASAN: slab-use-after-free Write in xlog_cil_ail_insert syzbot
2026-09-10 5:06 ` Dave Chinner
2026-09-10 15:47 ` Jeffin Philip
2026-09-10 16:19 ` Carlos Maiolino
2026-09-11 0:26 ` Dave Chinner
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.