]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
btrfs: clamp to avoid squota underflow
authorBoris Burkov <boris@bur.io>
Mon, 11 May 2026 20:06:24 +0000 (13:06 -0700)
committerDavid Sterba <dsterba@suse.com>
Sat, 16 May 2026 01:07:20 +0000 (03:07 +0200)
Simple quota accounting can undercount metadata tree block allocations
in certain scenarios. When an undercounted subvolume is deleted and its
tree blocks freed, the free deltas decrement rfer/excl past zero,
wrapping the u64 to a value near U64_MAX.

Once wrapped, can_delete_squota_qgroup() sees non-zero rfer and refuses
to delete the qgroup. The qgroup becomes permanently orphaned in the
quota tree, since there is no subvolume left to generate frees that
would bring the counter back to zero.

While we ultimately want to fix any mis-accounting at the source, it is
also helpful and worthwhile to mitigate the damage by clamping rfer and
excl to zero on underflow rather than allowing the u64 to wrap. This at
least allows us to clean up the messed up qgroups on subvol deletion.

Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Boris Burkov <boris@bur.io>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
fs/btrfs/qgroup.c

index 5f33727a79722123dad7e2136ffc0d25734b9abb..e9e7091e1452f57baa206bf76f0280659e05c9c3 100644 (file)
@@ -4967,8 +4967,19 @@ int btrfs_record_squota_delta(struct btrfs_fs_info *fs_info,
        list_for_each_entry(qg, &qgroup_list, iterator) {
                struct btrfs_qgroup_list *glist;
 
-               qg->excl += num_bytes * sign;
-               qg->rfer += num_bytes * sign;
+               ASSERT(qg->excl == qg->rfer);
+               if (WARN_ON_ONCE(sign < 0 && qg->excl < num_bytes)) {
+                       btrfs_warn(fs_info,
+                                  "squota underflow qg %hu/%llu excl %llu num_bytes %llu",
+                                  btrfs_qgroup_level(qg->qgroupid),
+                                  btrfs_qgroup_subvolid(qg->qgroupid),
+                                  qg->excl, num_bytes);
+                       qg->excl = 0;
+                       qg->rfer = 0;
+               } else {
+                       qg->excl += num_bytes * sign;
+                       qg->rfer += num_bytes * sign;
+               }
                qgroup_dirty(fs_info, qg);
 
                list_for_each_entry(glist, &qg->groups, next_group)