From 83201804efa4a5168be754e1dfc9b2faee760cac Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Tue, 9 Jun 2026 16:45:39 +0100 Subject: [PATCH 01/12] btrfs: fix use-after-free on reloc root after error in insert_dirty_subvol() If during relocation we fail in insert_dirty_subvol() because btrfs_update_reloc_root() returned an error, we will leave a root's reloc_root field pointing to a reloc root that was freed instead of NULL, resulting later in a use-after-free, or double free attempt during unmount. The sequence of steps is this: 1) During relocation the call to btrfs_update_reloc_root() in insert_dirty_subvol() fails, so insert_dirty_subvol() returns the error to merge_reloc_root() without adding the root to the list rc->dirty_subvol_roots; 2) Then merge_reloc_root() aborts the current transaction because insert_dirty_subvol() returned an error; 3) Up the call chain, merge_reloc_roots() gets the error, adds the reloc root for root X to the local reloc_roots list and jumps to the 'out' label, where it calls free_reloc_roots() to free all the reloc roots in the local reloc_roots list. This frees the reloc root for root X; 4) We go up the call chain to relocate_block_group() which calls clean_dirty_subvols() to go over dirty roots and set their ->reloc_root field to NULL, but root X is not in the dirty_subvol_roots list, so its ->reloc_root still points to a reloc root; 5) Relocation finishes, with an error and a transaction abort, but the ->reloc_root field for root X still points to the reloc root that was freed in step 3; 6) When unmounting the fs we end up calling: btrfs_free_fs_roots() btrfs_drop_and_free_fs_root() --> calls btrfs_put_root() against root X's ->reloc_root which is not NULL and points to the already freed reloc root in step 4 above Resulting in a use-after-free to a double free attempt. Syzbot reported this with the following dmesg/syslog: [ 106.004389][ T5339] BTRFS error (device loop0 state A): Transaction aborted (error -5) [ 106.014266][ T5339] BTRFS: error (device loop0 state A) in merge_reloc_root:1655: errno=-5 IO failure [ 106.021891][ T1061] BTRFS error (device loop0 state A): error while writing out transaction: -5 [ 106.026964][ T1061] BTRFS warning (device loop0 state A): Skipping commit of aborted transaction. [ 106.033807][ T5340] BTRFS error (device loop0 state A): bdev /dev/loop0 errs: wr 3, rd 0, flush 0, corrupt 0, gen 0 [ 106.039265][ T1061] BTRFS: error (device loop0 state A) in cleanup_transaction:2067: errno=-5 IO failure [ 106.044382][ T5339] BTRFS info (device loop0 state EA): forced readonly [ 106.074329][ T5339] BTRFS: error (device loop0 state EA) in merge_reloc_roots:1887: errno=-5 IO failure [ 106.081004][ T5356] BTRFS info (device loop0 state EA): scrub: started on devid 1 [ 106.085611][ T5339] BTRFS info (device loop0 state EA): balance: ended with status: -30 [ 106.089517][ T5356] BTRFS info (device loop0 state EA): scrub: not finished on devid 1 with status: -30 [ 106.662365][ T5338] BTRFS info (device loop0 state EA): last unmount of filesystem 3a375e4e-b156-4d76-a2ad-16e198ce1409 [ 106.682946][ T5338] ================================================================== [ 106.686574][ T5338] BUG: KASAN: slab-use-after-free in btrfs_put_root+0x2f/0x250 [ 106.690090][ T5338] Write of size 4 at addr ffff88803f978630 by task syz.0.0/5338 [ 106.693173][ T5338] [ 106.694279][ T5338] CPU: 0 UID: 0 PID: 5338 Comm: syz.0.0 Not tainted syzkaller #0 PREEMPT(full) [ 106.694293][ T5338] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 [ 106.694300][ T5338] Call Trace: [ 106.694308][ T5338] [ 106.694314][ T5338] dump_stack_lvl+0xe8/0x150 [ 106.694331][ T5338] print_address_description+0x55/0x1e0 [ 106.694343][ T5338] ? btrfs_put_root+0x2f/0x250 [ 106.694358][ T5338] print_report+0x58/0x70 [ 106.694368][ T5338] kasan_report+0x117/0x150 [ 106.694384][ T5338] ? btrfs_put_root+0x2f/0x250 [ 106.694399][ T5338] kasan_check_range+0x264/0x2c0 [ 106.694416][ T5338] btrfs_put_root+0x2f/0x250 [ 106.694430][ T5338] btrfs_drop_and_free_fs_root+0x160/0x210 [ 106.694447][ T5338] btrfs_free_fs_roots+0x2f9/0x3c0 [ 106.694464][ T5338] ? __pfx_btrfs_free_fs_roots+0x10/0x10 [ 106.694479][ T5338] ? free_root_pointers+0x5bf/0x5f0 [ 106.694494][ T5338] close_ctree+0x798/0x12d0 [ 106.694511][ T5338] ? __pfx_close_ctree+0x10/0x10 [ 106.694526][ T5338] ? _raw_spin_unlock_irqrestore+0x74/0x80 [ 106.694599][ T5338] ? rcu_preempt_deferred_qs_irqrestore+0x906/0xbc0 [ 106.694620][ T5338] ? __rcu_read_unlock+0x83/0xe0 [ 106.694636][ T5338] ? btrfs_put_super+0x48/0x1c0 [ 106.694652][ T5338] ? __pfx_btrfs_put_super+0x10/0x10 [ 106.694667][ T5338] generic_shutdown_super+0x13d/0x2d0 [ 106.694682][ T5338] kill_anon_super+0x3b/0x70 [ 106.694695][ T5338] btrfs_kill_super+0x41/0x50 [ 106.694710][ T5338] deactivate_locked_super+0xbc/0x130 [ 106.694722][ T5338] cleanup_mnt+0x437/0x4d0 [ 106.694736][ T5338] ? _raw_spin_unlock_irq+0x23/0x50 [ 106.694752][ T5338] task_work_run+0x1d9/0x270 [ 106.694769][ T5338] ? __pfx_task_work_run+0x10/0x10 [ 106.694784][ T5338] ? do_raw_spin_unlock+0x4d/0x210 [ 106.694802][ T5338] do_exit+0x70f/0x22c0 [ 106.694817][ T5338] ? trace_irq_disable+0x3b/0x140 [ 106.694835][ T5338] ? __pfx_do_exit+0x10/0x10 [ 106.694848][ T5338] ? preempt_schedule_thunk+0x16/0x30 [ 106.694863][ T5338] ? preempt_schedule_common+0x82/0xd0 [ 106.694878][ T5338] ? preempt_schedule_thunk+0x16/0x30 [ 106.694892][ T5338] do_group_exit+0x21b/0x2d0 [ 106.694906][ T5338] ? entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 106.694918][ T5338] __x64_sys_exit_group+0x3f/0x40 [ 106.694932][ T5338] x64_sys_call+0x221a/0x2240 [ 106.694944][ T5338] do_syscall_64+0x174/0x580 [ 106.694954][ T5338] ? clear_bhb_loop+0x40/0x90 [ 106.694967][ T5338] entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 106.694978][ T5338] RIP: 0033:0x7f958ef9ce59 [ 106.694988][ T5338] Code: Unable to access opcode bytes at 0x7f958ef9ce2f. [ 106.694994][ T5338] RSP: 002b:00007fffd4058318 EFLAGS: 00000246 ORIG_RAX: 00000000000000e7 [ 106.695008][ T5338] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00007f958ef9ce59 [ 106.695015][ T5338] RDX: 00007f958c3f8000 RSI: 0000000000000000 RDI: 0000000000000000 [ 106.695022][ T5338] RBP: 0000000000000003 R08: 0000000000000000 R09: 00007f958f1e73e0 [ 106.695028][ T5338] R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000 [ 106.695034][ T5338] R13: 00007f958f1e73e0 R14: 0000000000000003 R15: 00007fffd40583d0 [ 106.695046][ T5338] [ 106.695050][ T5338] [ 106.821635][ T5338] Allocated by task 1061: [ 106.823446][ T5338] kasan_save_track+0x3e/0x80 [ 106.825498][ T5338] __kasan_kmalloc+0x93/0xb0 [ 106.827381][ T5338] __kmalloc_cache_noprof+0x31c/0x660 [ 106.829525][ T5338] btrfs_alloc_root+0x75/0x930 [ 106.831458][ T5338] read_tree_root_path+0x127/0xb00 [ 106.833556][ T5338] btrfs_read_tree_root+0x34/0x60 [ 106.835553][ T5338] create_reloc_root+0x6b3/0xcb0 [ 106.837556][ T5338] btrfs_init_reloc_root+0x2ec/0x4b0 [ 106.839557][ T5338] record_root_in_trans+0x2ab/0x350 [ 106.841685][ T5338] btrfs_record_root_in_trans+0x15c/0x180 [ 106.844237][ T5338] start_transaction+0x39c/0x1820 [ 106.846638][ T5338] btrfs_finish_one_ordered+0x88e/0x2680 [ 106.849436][ T5338] btrfs_work_helper+0x37b/0xc20 [ 106.851549][ T5338] process_scheduled_works+0xb5d/0x1860 [ 106.853807][ T5338] worker_thread+0xa53/0xfc0 [ 106.855773][ T5338] kthread+0x389/0x470 [ 106.857548][ T5338] ret_from_fork+0x514/0xb70 [ 106.859493][ T5338] ret_from_fork_asm+0x1a/0x30 [ 106.861504][ T5338] [ 106.862527][ T5338] Freed by task 5339: [ 106.864224][ T5338] kasan_save_track+0x3e/0x80 [ 106.866180][ T5338] kasan_save_free_info+0x46/0x50 [ 106.868371][ T5338] __kasan_slab_free+0x5c/0x80 [ 106.870462][ T5338] kfree+0x1c5/0x640 [ 106.872180][ T5338] __del_reloc_root+0x341/0x3b0 [ 106.874290][ T5338] free_reloc_roots+0x5f/0x90 [ 106.876282][ T5338] merge_reloc_roots+0x73f/0x8a0 [ 106.878489][ T5338] relocate_block_group+0xbcc/0xe70 [ 106.880742][ T5338] do_nonremap_reloc+0xa8/0x5b0 [ 106.882885][ T5338] btrfs_relocate_block_group+0x7e6/0xc40 [ 106.885336][ T5338] btrfs_relocate_chunk+0x115/0x820 [ 106.887502][ T5338] __btrfs_balance+0x1db0/0x2ae0 [ 106.889543][ T5338] btrfs_balance+0xaf3/0x11b0 [ 106.891456][ T5338] btrfs_ioctl_balance+0x3d3/0x610 [ 106.893672][ T5338] __se_sys_ioctl+0xfc/0x170 [ 106.895530][ T5338] do_syscall_64+0x174/0x580 [ 106.897518][ T5338] entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 106.900101][ T5338] [ 106.901123][ T5338] The buggy address belongs to the object at ffff88803f978000 [ 106.901123][ T5338] which belongs to the cache kmalloc-4k of size 4096 [ 106.906907][ T5338] The buggy address is located 1584 bytes inside of [ 106.906907][ T5338] freed 4096-byte region [ffff88803f978000, ffff88803f979000) [ 106.912980][ T5338] [ 106.914022][ T5338] The buggy address belongs to the physical page: [ 106.916716][ T5338] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x3f978 [ 106.920390][ T5338] head: order:3 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0 [ 106.923834][ T5338] flags: 0x4fff00000000040(head|node=1|zone=1|lastcpupid=0x7ff) [ 106.927104][ T5338] page_type: f5(slab) [ 106.928898][ T5338] raw: 04fff00000000040 ffff88801ac42140 dead000000000122 0000000000000000 [ 106.932507][ T5338] raw: 0000000000000000 0000000800040004 00000000f5000000 0000000000000000 [ 106.936193][ T5338] head: 04fff00000000040 ffff88801ac42140 dead000000000122 0000000000000000 [ 106.939856][ T5338] head: 0000000000000000 0000000800040004 00000000f5000000 0000000000000000 [ 106.943601][ T5338] head: 04fff00000000003 fffffffffffffe01 00000000ffffffff 00000000ffffffff [ 106.947268][ T5338] head: ffffffffffffffff 0000000000000000 00000000ffffffff 0000000000000008 [ 106.950988][ T5338] page dumped because: kasan: bad access detected [ 106.953710][ T5338] page_owner tracks the page as allocated [ 106.956198][ T5338] page last allocated via order 3, migratetype Unmovable, gfp_mask 0xd2820(GFP_ATOMIC|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 24, tgid 24 (kworker/u4:2), ts 105728970387, free_ts 29540875453 [ 106.964984][ T5338] post_alloc_hook+0x22d/0x280 [ 106.966956][ T5338] get_page_from_freelist+0x2593/0x2610 [ 106.969307][ T5338] __alloc_frozen_pages_noprof+0x18d/0x380 [ 106.971839][ T5338] allocate_slab+0x77/0x660 [ 106.973709][ T5338] refill_objects+0x339/0x3d0 [ 106.975696][ T5338] __pcs_replace_empty_main+0x321/0x720 [ 106.978136][ T5338] __kmalloc_node_track_caller_noprof+0x572/0x7b0 [ 106.981009][ T5338] __alloc_skb+0x2c1/0x7d0 [ 106.982983][ T5338] nsim_dev_trap_report_work+0x29a/0xb90 [ 106.985356][ T5338] process_scheduled_works+0xb5d/0x1860 [ 106.987710][ T5338] worker_thread+0xa53/0xfc0 [ 106.989847][ T5338] kthread+0x389/0x470 [ 106.991727][ T5338] ret_from_fork+0x514/0xb70 [ 106.993722][ T5338] ret_from_fork_asm+0x1a/0x30 [ 106.995900][ T5338] page last free pid 77 tgid 77 stack trace: [ 106.998479][ T5338] __free_frozen_pages+0xc1c/0xd30 [ 107.000819][ T5338] vfree+0x1d1/0x2f0 [ 107.002631][ T5338] delayed_vfree_work+0x55/0x80 [ 107.004848][ T5338] process_scheduled_works+0xb5d/0x1860 [ 107.007366][ T5338] worker_thread+0xa53/0xfc0 [ 107.009388][ T5338] kthread+0x389/0x470 [ 107.011177][ T5338] ret_from_fork+0x514/0xb70 [ 107.013313][ T5338] ret_from_fork_asm+0x1a/0x30 [ 107.015454][ T5338] [ 107.016460][ T5338] Memory state around the buggy address: [ 107.019052][ T5338] ffff88803f978500: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 107.022691][ T5338] ffff88803f978580: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 107.026264][ T5338] >ffff88803f978600: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 107.029721][ T5338] ^ [ 107.032062][ T5338] ffff88803f978680: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 107.035547][ T5338] ffff88803f978700: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb [ 107.038865][ T5338] ================================================================== Fix this by resetting a root's ->reloc_root if we get an error while trying to merge a reloc root. Reported-by: syzbot+b3d472d13f9d7bf20669@syzkaller.appspotmail.com Link: https://lore.kernel.org/linux-btrfs/6a1ebde9.c1435f33.112120.0176.GAE@google.com/ Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index 0d63d117db59..a8d0acb0ad35 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -1518,6 +1518,17 @@ static int insert_dirty_subvol(struct btrfs_trans_handle *trans, return 0; } +static void clear_reloc_root(struct btrfs_root *root) +{ + root->reloc_root = NULL; + /* + * Need barrier to ensure clear_bit() only happens after + * root->reloc_root = NULL. Pairs with have_reloc_root(). + */ + smp_wmb(); + clear_bit(BTRFS_ROOT_DEAD_RELOC_TREE, &root->state); +} + static int clean_dirty_subvols(struct reloc_control *rc) { struct btrfs_root *root; @@ -1532,13 +1543,7 @@ static int clean_dirty_subvols(struct reloc_control *rc) struct btrfs_root *reloc_root = root->reloc_root; list_del_init(&root->reloc_dirty_list); - root->reloc_root = NULL; - /* - * Need barrier to ensure clear_bit() only happens after - * root->reloc_root = NULL. Pairs with have_reloc_root. - */ - smp_wmb(); - clear_bit(BTRFS_ROOT_DEAD_RELOC_TREE, &root->state); + clear_reloc_root(root); if (reloc_root) { /* * btrfs_drop_snapshot drops our ref we hold for @@ -1914,13 +1919,32 @@ void merge_reloc_roots(struct reloc_control *rc) goto out; } ret = merge_reloc_root(rc, root); - btrfs_put_root(root); if (ret) { - if (list_empty(&reloc_root->root_list)) + /* + * Clear the reloc root since below we will call + * free_reloc_roots(), otherwise we leave + * root->reloc_root pointing to a freed reloc + * root and trigger a use-after-free during + * unmount or elsewhere. + */ + clear_reloc_root(root); + btrfs_put_root(root); + /* + * We are adding the reloc_root to the local + * reloc_roots list, so we add a ref for this + * list which will be dropped below by the call + * to free_reloc_roots(). + */ + if (list_empty(&reloc_root->root_list)) { list_add_tail(&reloc_root->root_list, &reloc_roots); + btrfs_grab_root(reloc_root); + } + /* Now drop the ref for root->reloc_root. */ + btrfs_put_root(reloc_root); goto out; } + btrfs_put_root(root); } else { if (!IS_ERR(root)) { if (root->reloc_root == reloc_root) { From b78fe9563e2d5ae47805f1e5dc722c91fd30e1f8 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 12 Jun 2026 11:52:55 +0100 Subject: [PATCH 02/12] btrfs: fix reloc root cleanup in merge_reloc_roots() If the root we got has zero root refs in its root item, we are resetting the root's ->reloc_root without using barriers like we do everywhere else. Sashiko complained about this while reviewing another patch, and it's correct (see the Link tag below). Also, we should not clear BTRFS_ROOT_DEAD_RELOC_TREE from the root unless the root points to the reloc root we have. Fix this by using clear_reloc_root(), which issues the memory barrier after setting the root's ->reloc_root to NULL and before clearing the bit BTRFS_ROOT_DEAD_RELOC_TREE from the root. Link: https://sashiko.dev/#/patchset/cf84f1a217c719e25b6b69e4298dd7afd36c9427.1781194426.git.fdmanana%40suse.com Reviewed-by: Boris Burkov Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index a8d0acb0ad35..75bf2e5fcb6e 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -1948,11 +1948,10 @@ void merge_reloc_roots(struct reloc_control *rc) } else { if (!IS_ERR(root)) { if (root->reloc_root == reloc_root) { - root->reloc_root = NULL; + clear_reloc_root(root); + /* Drop the ref for root->reloc_root. */ btrfs_put_root(reloc_root); } - clear_bit(BTRFS_ROOT_DEAD_RELOC_TREE, - &root->state); btrfs_put_root(root); } From b95181f3929ff98949fa9460ca93eccebbf2d7fc Mon Sep 17 00:00:00 2001 From: You-Kai Zheng Date: Tue, 16 Jun 2026 18:39:07 +0800 Subject: [PATCH 03/12] btrfs: declare btrfs_ioctl_search_args_v2::buf as __u8 The variable-sized buffer buf in struct btrfs_ioctl_search_args_v2 is declared as __u64[], but it holds a packed byte stream of search results, where all offsets into the buffer are in bytes. Declaring buf as __u64[] makes it easy for user space to write incorrect pointer arithmetic: adding a byte offset directly to a __u64 pointer scales the offset by 8, landing at byte position offset*8 instead of offset. This recently caused an infinite loop in btrfs-progs: the accessor read all-zero data from misaddressed items, which fed zeroed search keys back into the ioctl loop and spun forever. The issue was worked around at the time by disabling TREE_SEARCH_V2 entirely in btrfs-progs (d73e69824854: "btrfs-progs: temporarily disable usage of v2 of search tree ioctl"). The kernel side already treats buf as a byte buffer, so change the declaration to __u8[] to match the actual semantics and prevent similar misuse in user space. The change is ABI compatible: both the structure size and alignment are unchanged. Fixes: cc68a8a5a433 ("btrfs: new ioctl TREE_SEARCH_V2") Reviewed-by: Qu Wenruo Signed-off-by: You-Kai Zheng Reviewed-by: David Sterba Signed-off-by: David Sterba --- include/uapi/linux/btrfs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/uapi/linux/btrfs.h b/include/uapi/linux/btrfs.h index 9b576603b3f1..0a13baf3d8d1 100644 --- a/include/uapi/linux/btrfs.h +++ b/include/uapi/linux/btrfs.h @@ -598,7 +598,7 @@ struct btrfs_ioctl_search_args_v2 { __u64 buf_size; /* in - size of buffer * out - on EOVERFLOW: needed size * to store item */ - __u64 buf[]; /* out - found items */ + __u8 buf[]; /* out - found items */ }; /* With a @src_length of zero, the range from @src_offset->EOF is cloned! */ From ae4316f332e03e628712e9dfb89f2b7d3c70c21a Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 23 Jun 2026 21:07:14 +0930 Subject: [PATCH 04/12] btrfs: do not try compression for data reloc inodes [BUG] There is a syzbot report that the check inside get_new_location() triggered: BTRFS info (device loop0): found 31 extents, stage: move data extents BTRFS info (device loop0): leaf 8908800 gen 16 total ptrs 28 free space 1676 owner 18446744073709551607 item 0 key (256 INODE_ITEM 0) itemoff 3835 itemsize 160 inode generation 5 transid 0 size 0 nbytes 0 block group 0 mode 40755 links 1 uid 0 gid 0 rdev 0 sequence 0 flags 0x0 atime 1669132761.0 ctime 1669132761.0 mtime 1669132761.0 otime 0.0 item 1 key (256 INODE_REF 256) itemoff 3823 itemsize 12 index 0 name_len 2 item 2 key (258 INODE_ITEM 0) itemoff 3663 itemsize 160 inode generation 1 transid 16 size 733184 nbytes 106496 block group 0 mode 100600 links 0 uid 0 gid 0 rdev 0 sequence 24 flags 0x18 item 3 key (258 EXTENT_DATA 0) itemoff 3595 itemsize 68 generation 16 type 0 inline extent data size 47 ram_bytes 4096 compression 1 [...] item 27 key (18446744073709551611 ORPHAN_ITEM 258) itemoff 2376 itemsize 0 BTRFS error (device loop0): unexpected non-zero offset in file extent item for data reloc inode 258 key offset 0 offset 9277520992061368337 ------------[ cut here ]------------ btrfs_abort_should_print_stack(__error) [CAUSE] The above dump tree shows the first file extent item is inlined, which should make no sense for data reloc inodes, as such inodes just represent where the data extents are in the relocation destination chunk. However the relocation path preallocates space for each block, then dirties them, cluster by cluster. It's possible to have a single block at the beginning of the block group, and no other block in the same cluster. So relocation will preallocate a file extent for that block and dirty the first block. Then memory pressure forces the data reloc inode to be written back, before any other blocks are dirtied/allocated. Finally commit 3eaf5f082c4c ("btrfs: extract inlined creation into a dedicated delalloc helper") changed the sequence of delalloc. Before that commit we always tried NOCOW first, so that dirtied block would be written back into the preallocated space, and appear as a regular extent. But with that commit, we always try inline first, and since compression is forced, we try compressing the first block, and then inline the compressed data, resulting in the above inlined file extent in the data reloc tree. Then the check in get_new_location() will check the file offset, without checking if the file extent is inlined or not, resulting in the above failure. [FIX] Do not allow compression for data reloc inodes. Since data reloc inode sizes are always block aligned, as long as we do not compress, @data_len will always be at least one block, and that will cause can_cow_file_range_inline() to return false, thus no inlined extent will be created. Reported-by: syzbot+d950c6ba09b79f6e1864@syzkaller.appspotmail.com Link: https://lore.kernel.org/linux-btrfs/6a373dc5.764cf64f.168fbe.0001.GAE@google.com/ Fixes: 3eaf5f082c4c ("btrfs: extract inlined creation into a dedicated delalloc helper") CC: stable@vger.kernel.org Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/btrfs_inode.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index d5d81f9546c3..7fdc6c3fd066 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -476,6 +476,8 @@ static inline bool btrfs_inode_can_compress(const struct btrfs_inode *inode) if (inode->flags & BTRFS_INODE_NODATACOW || inode->flags & BTRFS_INODE_NODATASUM) return false; + if (btrfs_is_data_reloc_root(inode->root)) + return false; return true; } From 800b51960215417752f6712ce7e384ca49ecff39 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 23 Jun 2026 21:07:15 +0930 Subject: [PATCH 05/12] btrfs: reject inline file extents item in get_new_location() Commit a6908f88c9da ("btrfs: validate data reloc tree file extent item members") introduced extra checks on file extent items for data reloc inodes, but it checked the file extent offset without checking if the file extent is inlined. This can lead to either false alerts (as the offset member is inside the inlined data) or even reading beyond the item range. This has already triggered a warning in a syzbot report. Although the root fix is to avoid compression for data reloc inodes, for the sake of consistency, reject inlined file extents first. Fixes: a6908f88c9da ("btrfs: validate data reloc tree file extent item members") CC: stable@vger.kernel.org Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index 75bf2e5fcb6e..6409c2cc1926 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -890,6 +890,13 @@ static int get_new_location(struct inode *reloc_inode, u64 *new_bytenr, leaf = path->nodes[0]; fi = btrfs_item_ptr(leaf, path->slots[0], struct btrfs_file_extent_item); + if (unlikely(btrfs_file_extent_type(leaf, fi) == BTRFS_FILE_EXTENT_INLINE)) { + btrfs_print_leaf(leaf); + btrfs_err(fs_info, + "unexpected inline file extent item for data reloc inode %llu key offset %llu", + btrfs_ino(BTRFS_I(reloc_inode)), bytenr); + return -EUCLEAN; + } /* * The cluster-boundary key searched above is always written by From 9411aafdf352b8d72668732af5a37dcb27383e25 Mon Sep 17 00:00:00 2001 From: Dave Chen Date: Fri, 26 Jun 2026 14:45:24 +0800 Subject: [PATCH 06/12] btrfs: only account delalloc bytes for regular file inodes in btrfs_getattr() btrfs_getattr() unconditionally reads BTRFS_I(inode)->new_delalloc_bytes and adds it (sector-aligned) to stat->blocks for every inode type. However, new_delalloc_bytes lives in a union with last_dir_index_offset: union { u64 new_delalloc_bytes; /* files only */ u64 last_dir_index_offset; /* directories only */ }; For a directory inode this memory holds last_dir_index_offset, which is set during directory logging (e.g. flush_dir_items_batch()) to the offset of the last logged BTRFS_DIR_INDEX_KEY. That offset grows with the number of entries ever created in the directory (dir indexes are monotonic and never reused), so it can be arbitrarily large. As a result, after a directory has been logged (e.g. via an fsync that triggers directory logging), btrfs_getattr() reports inflated st_blocks for that directory. The inflation is purely in-core and disappears after the inode is evicted and reloaded (btrfs_alloc_inode() zeroes the union), e.g. after a remount. Reproducer (on a btrfs filesystem): D=/mnt/btrfs/d mkdir -p $D for i in $(seq 1 20000); do touch $D/f$i; done sync # commit, push dir index high touch $D/trigger # dirty the dir in a new transaction xfs_io -c fsync $D # log the directory -> sets last_dir_index_offset stat -c '%b' $D # st_blocks is now inflated (e.g. 40) # umount + mount -> st_blocks drops back to the correct value The evict path already knows this union is type-dependent and guards the corresponding WARN_ON with !S_ISDIR() in btrfs_destroy_inode(); only btrfs_getattr() was missing the equivalent check. Only read new_delalloc_bytes for regular files, which are the only inodes that ever set it. Reviewed-by: Filipe Manana Signed-off-by: Dave Chen Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/inode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 16f097f58acc..04ea10b61bbb 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -8068,7 +8068,8 @@ static int btrfs_getattr(struct mnt_idmap *idmap, stat->result_mask |= STATX_SUBVOL; spin_lock(&BTRFS_I(inode)->lock); - delalloc_bytes = BTRFS_I(inode)->new_delalloc_bytes; + delalloc_bytes = S_ISREG(inode->i_mode) ? + BTRFS_I(inode)->new_delalloc_bytes : 0; inode_bytes = inode_get_bytes(inode); spin_unlock(&BTRFS_I(inode)->lock); stat->blocks = (ALIGN(inode_bytes, blocksize) + From f0c1f14cc103a28ba80b3e48b88f23026709db3a Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 29 Jun 2026 15:47:34 +0100 Subject: [PATCH 07/12] btrfs: fix NULL pointer deref during assertion in btrfs_backref_free_node() In btrfs_backref_free_node() we have the following assertion: ASSERT(node->eb == NULL, "node->eb->start=%llu", node->eb->start); and a user reported the following crash: Oops: general protection fault, probably for non-canonical address 0xdffffc0000000000: 0000 [#1] SMP KASAN NOPTI KASAN: null-ptr-deref in range [0x0000000000000000-0x0000000000000007] CPU: 0 UID: 0 PID: 10422 Comm: syz.0.17 Not tainted 7.1.0-02765-g6b5a2b7d9bc1-dirty #44 PREEMPT(full) Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014 RIP: 0010:btrfs_backref_free_node fs/btrfs/backref.c:3057 [inline] RIP: 0010:btrfs_backref_free_node+0xb9/0x200 fs/btrfs/backref.c:3051 Code: 00 fc ff (...) RSP: 0018:ffa0000006b0f3c0 EFLAGS: 00010246 RAX: dffffc0000000000 RBX: 0000000000000000 RCX: ffffffff840eb78b RDX: 0000000000000000 RSI: ffffffff840eafa5 RDI: ff110000742ab768 RBP: ff110000742ab700 R08: 0000000000000000 R09: 0000000000000000 R10: ff110000742ab700 R11: 00000000000a81f9 R12: ff11000107a92020 R13: ff1100005c182ea8 R14: 0000000000000000 R15: dffffc0000000000 FS: 0000555575536500(0000) GS:ff11000183985000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 00007fa3d0e9d580 CR3: 000000002232a000 CR4: 0000000000753ef0 PKRU: 00000000 Call Trace: btrfs_backref_cleanup_node+0x27/0x30 fs/btrfs/backref.c:3133 relocate_tree_block fs/btrfs/relocation.c:2604 [inline] relocate_tree_blocks+0x11b0/0x1a20 fs/btrfs/relocation.c:2707 relocate_block_group+0x499/0xf30 fs/btrfs/relocation.c:3635 do_nonremap_reloc fs/btrfs/relocation.c:5323 [inline] btrfs_relocate_block_group+0x1749/0x5fb0 fs/btrfs/relocation.c:5490 btrfs_relocate_chunk+0x12b/0x950 fs/btrfs/volumes.c:3647 __btrfs_balance fs/btrfs/volumes.c:4586 [inline] btrfs_balance+0x1c7f/0x55c0 fs/btrfs/volumes.c:4973 btrfs_ioctl_balance fs/btrfs/ioctl.c:3474 [inline] btrfs_ioctl+0x38a4/0x5d20 fs/btrfs/ioctl.c:5570 vfs_ioctl fs/ioctl.c:51 [inline] __do_sys_ioctl fs/ioctl.c:597 [inline] __se_sys_ioctl fs/ioctl.c:583 [inline] __x64_sys_ioctl+0x18f/0x210 fs/ioctl.c:583 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline] do_syscall_64+0x11f/0x860 arch/x86/entry/syscall_64.c:94 entry_SYSCALL_64_after_hwframe+0x77/0x7f RIP: 0033:0x7fb38e3b56dd Code: 02 b8 ff (...) RSP: 002b:00007fff04115788 EFLAGS: 00000246 ORIG_RAX: 0000000000000010 RAX: ffffffffffffffda RBX: 00007fb38f6b0020 RCX: 00007fb38e3b56dd RDX: 00002000000003c0 RSI: 00000000c4009420 RDI: 0000000000000004 RBP: 00007fb38e451b48 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000 R13: 0000000000000000 R14: 00007fb38f6b0020 R15: 00007fb38f6b002c It seems that this happens on some systems for some reason, when the ASSERT() macro calls the inline function verify_assert_printk_format() to evaluate the format string and arguments, causing the NULL pointer dereference on node->eb. So change the assertion to check for a NULL node->eb before dereferencing it. Also, while at it, make the assertion more useful by printing the owner of the extent buffer as well as its level. Reported-by: Yue Sun Link: https://lore.kernel.org/linux-btrfs/20260626065542.38413-1-samsun1006219@gmail.com/ Fixes: c4e7778580d6 ("btrfs: use verbose assertions in backref.c") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/backref.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/backref.c b/fs/btrfs/backref.c index 23c3eeb58dc1..1be632c742bd 100644 --- a/fs/btrfs/backref.c +++ b/fs/btrfs/backref.c @@ -3054,7 +3054,10 @@ void btrfs_backref_free_node(struct btrfs_backref_cache *cache, if (node) { ASSERT(list_empty(&node->list)); ASSERT(list_empty(&node->lower)); - ASSERT(node->eb == NULL, "node->eb->start=%llu", node->eb->start); + ASSERT(node->eb == NULL, "node->eb->start=%llu level=%d owner=%llu", + node->eb ? node->eb->start : 0, + node->eb ? btrfs_header_level(node->eb) : 0, + node->eb ? btrfs_header_owner(node->eb) : 0); cache->nr_nodes--; btrfs_put_root(node->root); kfree(node); From 8b5a09ceb61b18b1f0797cd30a549d7dc85d8d50 Mon Sep 17 00:00:00 2001 From: Dave Chen Date: Mon, 29 Jun 2026 15:08:43 +0800 Subject: [PATCH 08/12] btrfs: fix u32 to s64 type conversion in dirty_metadata_bytes accounting The percpu_counter dirty_metadata_bytes is updated by negating eb->len and passing it to percpu_counter_add_batch(), whose amount parameter is s64. Since commit 84cda1a6087d ("btrfs: cache folio size and shift in extent_buffer"), eb->len is u32. The u32 result of -eb->len, when widened to the s64 parameter, becomes a large positive value instead of the intended negative value. For eb->len == 16384 the counter adds +4294950912 instead of subtracting 16384. The counter therefore grows on every metadata writeback instead of shrinking by the extent buffer size, permanently exceeding BTRFS_DIRTY_METADATA_THRESH and causing __btrfs_btree_balance_dirty() to trigger balance_dirty_pages_ratelimited() unconditionally, adding unnecessary writeback pressure. Cast eb->len to s64 before negation at both call sites so the subtraction is performed in signed 64-bit arithmetic. Reviewed-by: Filipe Manana Fixes: 84cda1a6087d ("btrfs: cache folio size and shift in extent_buffer") Signed-off-by: Dave Chen Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 7d604524e83c..de5785117a47 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -2004,7 +2004,7 @@ static noinline_for_stack bool lock_extent_buffer_for_io(struct extent_buffer *e btrfs_set_header_flag(eb, BTRFS_HEADER_FLAG_WRITTEN); percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, - -eb->len, + -(s64)eb->len, fs_info->dirty_metadata_batch); ret = true; } else { @@ -3774,7 +3774,7 @@ void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans, return; buffer_tree_clear_mark(eb, PAGECACHE_TAG_DIRTY); - percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, -eb->len, + percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, -(s64)eb->len, fs_info->dirty_metadata_batch); for (int i = 0; i < num_extent_folios(eb); i++) { From 5eff4d5b17fa1950e80bfd1ba43dc0699e61a644 Mon Sep 17 00:00:00 2001 From: Leo Martins Date: Tue, 30 Jun 2026 12:58:05 -0700 Subject: [PATCH 09/12] btrfs: don't propagate EXTENT_FLAG_LOGGING to split extent maps When btrfs_drop_extent_map_range() splits an extent map, the new split maps inherit the original map's flags through a local 'flags' variable. Commit f86f7a75e2fb ("btrfs: use the flags of an extent map to identify the compression type") changed the EXTENT_FLAG_LOGGING clearing to operate on em->flags instead of that local 'flags' copy, so a split of an extent map that is currently being logged wrongly inherits EXTENT_FLAG_LOGGING. The flag is then never cleared on the split, and when it is freed while still on the inode's modified_extents list (for example by the extent map shrinker) it trips the WARN_ON(!list_empty(&em->list)) in btrfs_free_extent_map() and leads to a use-after-free. Clear EXTENT_FLAG_LOGGING from the local 'flags' copy used for the splits and only clear EXTENT_FLAG_PINNED from em->flags, restoring the behaviour prior to f86f7a75e2fb. CC: Jeff Layton Link: https://lore.kernel.org/all/20260629-btrfs-skip-logging-v1-1-4e3a28c1acaf@kernel.org/ Fixes: f86f7a75e2fb ("btrfs: use the flags of an extent map to identify the compression type") Reviewed-by: Jeff Layton Reviewed-by: Filipe Manana Signed-off-by: Leo Martins Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/extent_map.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/extent_map.c b/fs/btrfs/extent_map.c index fce9c5cc0122..6ad7b39ae358 100644 --- a/fs/btrfs/extent_map.c +++ b/fs/btrfs/extent_map.c @@ -866,13 +866,13 @@ void btrfs_drop_extent_map_range(struct btrfs_inode *inode, u64 start, u64 end, goto next; } - flags = em->flags; /* * In case we split the extent map, we want to preserve the * EXTENT_FLAG_LOGGING flag on our extent map, but we don't want * it on the new extent maps. */ - em->flags &= ~(EXTENT_FLAG_PINNED | EXTENT_FLAG_LOGGING); + flags = em->flags & ~EXTENT_FLAG_LOGGING; + em->flags &= ~EXTENT_FLAG_PINNED; modified = !list_empty(&em->list); /* From 9b73625a4f24971d7a1a07df5d7fd58c07bf3f9f Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 7 Jul 2026 16:24:30 +0930 Subject: [PATCH 10/12] btrfs: fix a regression where PAGECACHE_TAG_DIRTY is never cleared [BUG] The following script (already submitted as generic/798) will report incorrect dirty page numbers, with 64K page size systems and 4K fs block size: # mkfs.btrfs -s 4k -f $dev # mount $dev $mnt # xfs_io -f -c "pwrite 0 64K" -c fsync -c "cachestat 0 64K" $mnt/foobar Cached: 1, Dirty: 1, Writeback: 0, Evicted: 0, Recently Evicted: 0 Note that the dirtied page number is still 1. [CAUSE] The cachestat() goes through the XArray of the page cache, but instead of checking each folio's flag, it uses the PAGECACHE_TAG_DIRTY tag to report dirty pages. Since commit 095be159f3eb ("btrfs: unify folio dirty flag clearing"), btrfs replaced a folio_clear_dirty_for_io() call inside extent_write_cache_pages() with folio_test_dirty(). This will cause the following call sequence for the folio at file offset 0: extent_write_cache_pages() |- folio_test_dirty() | The folio is still dirty, continue to writeback. | |- extent_writepage() |- extent_writepage_io() |- submit_one_sector() for range [0, 4K) | |- btrfs_folio_clear_dirty() | |- btrfs_folio_set_writeback() | |- folio_start_writeback() | It's the first writeback block, we set the writeback | flag for the folio. | But the folio is still dirty, PAGECACHE_TAG_DIRTY is | kept | |- submit_one_sector() for range [4K, 8K) | |- btrfs_folio_clear_dirty() | |- btrfs_folio_set_writeback() | The folio already has writeback flag, no need to call | folio_start_writeback() | | ... |- submit_one_sector() for range [60K, 64K) |- btrfs_folio_clear_dirty() |- btrfs_folio_set_writeback() The folio already has writeback flag, no need to call folio_start_writeback() So the PAGECACHE_TAG_DIRTY is never cleared. Meanwhile for the old code, before that commit, the sequence looks like: extent_write_cache_pages() |- folio_clear_dirty_for_io() | The folio is still dirty, so continue to writeback. | But the folio dirty flag is cleared now. | |- extent_writepage() |- extent_writepage_io() |- submit_one_sector() for range [0, 4K) | |- btrfs_folio_clear_dirty() | |- btrfs_folio_set_writeback() | |- folio_start_writeback() | |- xas_clear(PAGECACHE_TAG) | | It's the first writeback block, we set the writeback | flag for the folio. | And the folio is not dirty, PAGECACHE_TAG_DIRTY is | cleared | |- submit_one_sector() for range [4K, 8K) | |- btrfs_folio_clear_dirty() | |- btrfs_folio_set_writeback() | The folio already has writeback flag, no need to call | folio_start_writeback() | | ... |- submit_one_sector() for range [60K, 64K) |- btrfs_folio_clear_dirty() |- btrfs_folio_set_writeback() The folio already has writeback flag, no need to call folio_start_writeback() Unlike the new code, old code will clear PAGECACHE_TAG_DIRTY for the first writeback block. There is a deeper problem, dirty and writeback folio flags are updated at very different timing. The dirty flag is only cleared when the last sub-folio block has dirty flag cleared. But the writeback flag is set when the first block starts writeback, and later blocks that go through writeback will not call folio_start_writeback() again. If we rely on folio_start_writeback() to update the PAGECACHE_TAG_DIRTY and PAGECACHE_TAG_TOWRITE, it will always be incorrect in one way or another. [FIX] Do not let folio_start_writeback() do any PAGECACHE_TAG_TOWRITE handling. Instead, manually clear both PAGECACHE_TAG_TOWRITE and PAGECACHE_TAG_DIRTY flags when the folio is no longer dirty during btrfs_subpage_set_writeback(). However this is only a hot-fix, for the long term solution we will follow iomap, by calling folio_start_writeback() immediately for the whole folio, and folio_end_writeback() after all writeback finished for the folio. Fixes: 095be159f3eb ("btrfs: unify folio dirty flag clearing") Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/subpage.c | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/subpage.c b/fs/btrfs/subpage.c index 56060acac2e9..2a9397be8116 100644 --- a/fs/btrfs/subpage.c +++ b/fs/btrfs/subpage.c @@ -359,6 +359,23 @@ void btrfs_subpage_set_dirty(const struct btrfs_fs_info *fs_info, folio_mark_dirty(folio); } +static void folio_clear_tags(struct folio *folio) +{ + struct address_space *mapping = folio_mapping(folio); + XA_STATE(xas, &mapping->i_pages, folio->index); + unsigned long flags; + + ASSERT(folio_test_locked(folio)); + ASSERT(mapping); + ASSERT(mapping_use_writeback_tags(mapping)); + + xas_lock_irqsave(&xas, flags); + xas_load(&xas); + xas_clear_mark(&xas, PAGECACHE_TAG_DIRTY); + xas_clear_mark(&xas, PAGECACHE_TAG_TOWRITE); + xas_unlock_irqrestore(&xas, flags); +} + /* * Extra clear_and_test function for subpage dirty bitmap. * @@ -403,7 +420,6 @@ void btrfs_subpage_set_writeback(const struct btrfs_fs_info *fs_info, unsigned int start_bit = subpage_calc_start_bit(fs_info, folio, writeback, start, len); unsigned long flags; - bool keep_write; spin_lock_irqsave(&bfs->lock, flags); bitmap_set(bfs->bitmaps, start_bit, len >> fs_info->sectorsize_bits); @@ -413,10 +429,14 @@ void btrfs_subpage_set_writeback(const struct btrfs_fs_info *fs_info, * folio. Doing so can cause WB_SYNC_ALL writepages() to overlook it, * assume writeback is complete, and exit too early — violating sync * ordering guarantees. + * + * Instead we manually clear the DIRTY and TOWRITE tags after the folio + * is no longer dirty. */ - keep_write = folio_test_dirty(folio); if (!folio_test_writeback(folio)) - __folio_start_writeback(folio, keep_write); + __folio_start_writeback(folio, true); + if (!folio_test_dirty(folio)) + folio_clear_tags(folio); spin_unlock_irqrestore(&bfs->lock, flags); } From 6a8269b6459ed870a8156c106a0f597383907872 Mon Sep 17 00:00:00 2001 From: Guanghui Yang <3497809730@qq.com> Date: Sun, 12 Jul 2026 03:17:28 +0000 Subject: [PATCH 11/12] btrfs: free mapping node on duplicate reloc root insert __add_reloc_root() allocates a mapping_node before inserting it into rc->reloc_root_tree. If rb_simple_insert() finds an existing entry, it returns the existing rb_node and leaves the newly allocated node unlinked. The error path then returns -EEXIST without freeing the new node. Since the node was never inserted into reloc_root_tree, the later cleanup in put_reloc_control() cannot find it either. Free the newly allocated node before returning -EEXIST. The callers currently assert that -EEXIST should not happen, so this is a defensive cleanup for an unexpected duplicate insert path. If the path is ever reached, the local allocation should still be released. Fixes: 57a304cfd43b ("btrfs: do not panic in __add_reloc_root") Reviewed-by: Qu Wenruo Signed-off-by: Guanghui Yang <3497809730@qq.com> Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index 6409c2cc1926..fc5c14b5adad 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -588,6 +588,7 @@ static int __add_reloc_root(struct btrfs_root *root, struct reloc_control *rc) btrfs_err(fs_info, "Duplicate root found for start=%llu while inserting into relocation tree", node->bytenr); + kfree(node); return -EEXIST; } From 75a41e3e51eb0d6f7a4969c5056298c2aa3e85f2 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Mon, 13 Jul 2026 09:50:42 +0200 Subject: [PATCH 12/12] btrfs: fix GET_SUBVOL_INFO after compat refactor btrfs_search_slot() returns a positive value when the search key does not exactly match an item. This is expected here, since offset 0 is used to find the first ROOT_BACKREF for the subvolume and the actual key has the parent root ID as its offset. Before the compat ioctl refactoring, the native handler still copied the filled structure to userspace when the search returned 1. After the lookup was moved to a shared helper, both native and compat callers treat the positive return value as a failure and skip copy_to_user(), leaving BTRFS_IOC_GET_SUBVOL_INFO unusable for non-top-level subvolumes. Reset ret after successfully validating and reading the ROOT_BACKREF so the helper reports success and both callers copy the result to userspace. Fixes: 538e5bdbc899 ("btrfs: add 32-bit compat ioctl for BTRFS_IOC_GET_SUBVOL_INFO") Reviewed-by: Qu Wenruo Signed-off-by: Daan De Meyer Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 81e87bc39828..68b33f365fda 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -2048,6 +2048,7 @@ static int _btrfs_ioctl_get_subvol_info(struct inode *inode, ret = -ENOENT; goto out; } + ret = 0; } out: