syzbot
WARNING in btrfs_put_root
Status:
moderation: reported on 2026/11/08 03:40
Subsystems:
btrfs
Labels:
prio:normal
[Documentation on labels]
Reported-by: syzbot+819d6f2a57af1519c023@syzkaller.appspotmail.com
First crash: 1h41m, last: 1h41m
✨ AI-generated root cause summary:
When a Btrfs filesystem encounters a transaction abort (reported in state EA), inodes being evicted during unmount (particularly via delayed iputs in close_ctree(), which runs after btrfs_error_commit_super()) fail to persist their delayed inode updates in btrfs_commit_inode_delayed_inode() because transaction joining fails. In btrfs_evict_inode(), this error causes execution to jump directly to the out label, bypassing btrfs_kill_delayed_inode_items(). Because the delayed items remain uncleaned, __btrfs_release_delayed_node() re-queues the delayed node into delayed_root->node_list and retains a reference, preventing refcount_dec_and_test() from hitting zero and skipping xa_erase(&root->delayed_nodes, ...). When the root is finally freed in btrfs_put_root(), root->delayed_nodes is still non-empty, triggering WARN_ON(!xa_empty(&root->delayed_nodes)).
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
1. Context and Involved Entities
- Source Files:
fs/btrfs/inode.c: Inode eviction handling (btrfs_evict_inode()).
fs/btrfs/disk-io.c: Filesystem teardown and root destruction (close_ctree(), btrfs_put_root(), btrfs_free_fs_info()).
fs/btrfs/delayed-inode.c: Delayed node tracking, commit, and release (btrfs_commit_inode_delayed_inode(), btrfs_remove_delayed_node(), __btrfs_release_delayed_node()).
- Key Structs and Fields:
struct btrfs_root::delayed_nodes: XArray tracking delayed nodes associated with inodes in that root.
struct btrfs_inode::delayed_node: Pointer caching the associated struct btrfs_delayed_node.
struct btrfs_delayed_node::refs: Reference count of the delayed node (one held by btrfs_inode::delayed_node, one held by btrfs_delayed_root::node_list when count > 0, plus any transient caller references).
struct btrfs_delayed_node::count: Count of pending delayed items/updates.
struct btrfs_fs_info::delayed_root: Global delayed root tracking lists (node_list, prepare_list).
2. Triggering Sequence and Lifecycle Invariant Violation
-
Transaction Abort State (state EA):
During filesystem operation or unmount, an error occurred and the current transaction was aborted (BTRFS_FS_STATE_TRANS_ABORTED set, reflected in the state EA log message before unmount).
-
Unmount Sequence in close_ctree():
During unmount (generic_shutdown_super() $\to$ btrfs_put_super() $\to$ close_ctree()):
- At line 4394 of
fs/btrfs/disk-io.c, close_ctree() checks BTRFS_FS_ERROR(fs_info) and invokes btrfs_error_commit_super(fs_info). This calls btrfs_cleanup_transaction(fs_info), which invokes btrfs_destroy_delayed_inodes(fs_info) to destroy any delayed items currently in fs_info->delayed_root.node_list and verifies the list with btrfs_assert_delayed_root_empty(fs_info).
- Subsequently, at lines 4455 and 4478 of
fs/btrfs/disk-io.c, close_ctree() calls btrfs_run_delayed_iputs(fs_info). This processes pending delayed iputs generated by preceding operations and ordered extents, calling iput(&inode->vfs_inode).
-
Error Handling Defect in btrfs_evict_inode():
When iput() drops the last reference to an inode, VFS calls btrfs_evict_inode():
ret = btrfs_commit_inode_delayed_inode(BTRFS_I(inode));
if (ret)
goto out;
btrfs_kill_delayed_inode_items(BTRFS_I(inode));
...
out:
btrfs_remove_delayed_node(BTRFS_I(inode));
clear_inode:
clear_inode(inode);
- In
btrfs_commit_inode_delayed_inode(), btrfs_join_transaction() fails with -EROFS because the transaction has been aborted.
- Because
ret < 0, btrfs_evict_inode() immediately executes goto out;, bypassing btrfs_kill_delayed_inode_items(). The delayed node retains BTRFS_DELAYED_NODE_INODE_DIRTY and its item count (delayed_node->count > 0).
-
Reference Imbalance and Omission from XArray Erase:
At label out:, btrfs_evict_inode() invokes btrfs_remove_delayed_node():
void btrfs_remove_delayed_node(struct btrfs_inode *inode)
{
struct btrfs_delayed_node *delayed_node;
delayed_node = READ_ONCE(inode->delayed_node);
if (!delayed_node)
return;
inode->delayed_node = NULL;
btrfs_release_delayed_node(delayed_node, &delayed_node->inode_cache_tracker);
}
In __btrfs_release_delayed_node():
mutex_lock(&delayed_node->mutex);
if (delayed_node->count)
btrfs_queue_delayed_node(delayed_root, delayed_node, mod);
else
btrfs_dequeue_delayed_node(delayed_root, delayed_node);
mutex_unlock(&delayed_node->mutex);
btrfs_delayed_node_ref_tracker_free(delayed_node, tracker);
if (refcount_dec_and_test(&delayed_node->refs)) {
struct btrfs_root *root = delayed_node->root;
xa_erase(&root->delayed_nodes, delayed_node->inode_id);
...
kmem_cache_free(delayed_node_cache, delayed_node);
}
- Because
delayed_node->count remains $> 0$, btrfs_queue_delayed_node() queues the node into delayed_root->node_list (and increments node->refs if not already queued).
- When
refcount_dec_and_test(&delayed_node->refs) decrements the reference held by inode->delayed_node, delayed_node->refs does not reach zero because delayed_root->node_list still holds a reference.
- As a result,
xa_erase(&root->delayed_nodes, delayed_node->inode_id) is skipped, leaving the delayed node stored in root->delayed_nodes.
- The inode itself is cleared and deallocated, removing it from
root->inodes.
- Because
btrfs_destroy_delayed_inodes() in close_ctree() ran prior to btrfs_run_delayed_iputs(), no further cleanup pass iterates or kills delayed nodes.
-
Failure Manifestation:
After close_ctree() finishes, btrfs_kill_super() calls btrfs_free_fs_info(), which calls btrfs_put_root(fs_info->fs_root). When dropping the last reference to the root:
if (WARN_ON(!xa_empty(&root->inodes)))
xa_destroy(&root->inodes);
if (WARN_ON(!xa_empty(&root->delayed_nodes)))
xa_destroy(&root->delayed_nodes);
xa_empty(&root->inodes) is true (all inodes were destroyed), but xa_empty(&root->delayed_nodes) is false, triggering the reported warning at fs/btrfs/disk-io.c:1844.
Sample crash report:
BTRFS info (device loop0 state EA): last unmount of filesystem c5fa74be-7b06-468c-a5da-b62cb7064054
------------[ cut here ]------------
!xa_empty(&root->delayed_nodes)
WARNING: fs/btrfs/disk-io.c:1844 at btrfs_put_root+0x1f1/0x250 fs/btrfs/disk-io.c:1844, CPU#1: syz-executor/5628
Modules linked in:
CPU: 1 UID: 0 PID: 5628 Comm: syz-executor Not tainted syzkaller #0 PREEMPT(full)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 11/06/2026
RIP: 0010:btrfs_put_root+0x1f1/0x250 fs/btrfs/disk-io.c:1844
Code: cb 0e fe e8 91 88 a3 fd 48 8d bb 40 05 00 00 90 0f 0b 90 e8 41 8f de 07 e9 a3 fe ff ff e8 77 88 a3 fd 48 8d bb 90 05 00 00 90 <0f> 0b 90 e8 27 8f de 07 e9 b5 fe ff ff e8 5d 88 a3 fd 90 0f 0b 90
RSP: 0018:ffffc9000301fd18 EFLAGS: 00010293
RAX: ffffffff84258649 RBX: ffff88805339e000 RCX: ffff88807876be80
RDX: 0000000000000000 RSI: 0000000000000001 RDI: ffff88805339e590
RBP: 0000000000000001 R08: ffff88805339e62b R09: 1ffff1100a673cc5
R10: dffffc0000000000 R11: ffffed100a673cc6 R12: dffffc0000000000
R13: ffff888057bd3558 R14: ffff88805339e5d8 R15: dffffc0000000000
FS: 000055556ef27500(0000) GS:ffff88812350a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000001b34916ff8 CR3: 0000000057876000 CR4: 0000000000350ef0
Call Trace:
<TASK>
btrfs_free_fs_info+0x252/0x360 fs/btrfs/disk-io.c:1257
deactivate_locked_super+0xbe/0x110 fs/super.c:558
cleanup_mnt+0x204/0x2a0 fs/namespace.c:1396
task_work_run+0x1d9/0x260 kernel/task_work.c:233
resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
__exit_to_user_mode_loop kernel/entry/common.c:70 [inline]
exit_to_user_mode_loop+0x204/0x770 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:343 [inline]
do_syscall_64+0x328/0x520 arch/x86/entry/syscall_64.c:89
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f22bd3a0d57
Code: a2 c7 05 9c a9 25 00 00 00 00 00 eb 96 e8 e1 12 00 00 90 31 f6 e9 09 00 00 00 66 0f 1f 84 00 00 00 00 00 b8 a6 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 01 c3 48 c7 c2 e8 ff ff ff f7 d8 64 89 02 b8
RSP: 002b:00007fff8aacb128 EFLAGS: 00000246 ORIG_RAX: 00000000000000a6
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 00007f22bd3a0d57
RDX: 0000000000000000 RSI: 0000000000000009 RDI: 00007fff8aacb1e0
RBP: 00007fff8aacb1e0 R08: 00007fff8aacc1e0 R09: 00000000ffffffff
R10: 0000000000000000 R11: 0000000000000246 R12: 00007fff8aacc270
R13: 00007f22bd436fb3 R14: 0000000000020668 R15: 00007fff8aacc2b0
</TASK>
* Struck through repros no longer work on HEAD.