public inbox for linux-btrfs@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/3] btrfs: squota inheritance improvements
@ 2025-12-03 21:11 Boris Burkov
  2025-12-03 21:11 ` [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug Boris Burkov
                   ` (3 more replies)
  0 siblings, 4 replies; 10+ messages in thread
From: Boris Burkov @ 2025-12-03 21:11 UTC (permalink / raw)
  To: linux-btrfs, kernel-team

One bug fix for an off-by-one metadata accounting error and two generic
improvements. Details in the patches.

Boris Burkov (3):
  btrfs: fix qgroup_snapshot_quick_inherit() squota bug
  btrfs: check squota parent usage on membership change
  btrfs: relax squota parent qgroup deletion rule

 fs/btrfs/qgroup.c | 93 +++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 78 insertions(+), 15 deletions(-)

-- 
2.52.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug
  2025-12-03 21:11 [PATCH 0/3] btrfs: squota inheritance improvements Boris Burkov
@ 2025-12-03 21:11 ` Boris Burkov
  2025-12-04  3:31   ` Qu Wenruo
  2025-12-03 21:11 ` [PATCH 2/3] btrfs: check squota parent usage on membership change Boris Burkov
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 10+ messages in thread
From: Boris Burkov @ 2025-12-03 21:11 UTC (permalink / raw)
  To: linux-btrfs, kernel-team

qgroup_snapshot_quick_inherit() detects conditions where the snapshot
destination would land in the same parent qgroup as the snapshot source
subvolume. In this case we can avoid costly qgroup calculations and just
add the nodesize of the new snapshot to the parent.

However, in the case of squotas this is actually a double count, and
also an undercount for deeper qgroup nestings.

The following annotated script shows the issue:

  btrfs quota enable --simple "$mnt"

  # Create 2-level qgroup hierarchy
  btrfs qgroup create 2/100 "$mnt"  # Q2 (level 2)
  btrfs qgroup create 1/100 "$mnt"  # Q1 (level 1)
  btrfs qgroup assign 1/100 2/100 "$mnt"

  # Create base subvolume
  btrfs subvolume create "$mnt/base" >/dev/null
  base_id=$(btrfs subvolume show "$mnt/base" | grep 'Subvolume ID:' | awk '{print $3}')

  # Create intermediate snapshot and add to Q1
  btrfs subvolume snapshot "$mnt/base" "$mnt/intermediate" >/dev/null
  inter_id=$(btrfs subvolume show "$mnt/intermediate" | grep 'Subvolume ID:' | awk '{print $3}')
  btrfs qgroup assign "0/$inter_id" 1/100 "$mnt"

  # Create working snapshot with --inherit (auto-adds to Q1)
  # src=intermediate (in only Q1)
  # dst=snap (inheriting only into Q1)
  # This double counts the 16k nodesize of the snapshot in Q1, and
  # undercounts it in Q2.
  btrfs subvolume snapshot -i 1/100 "$mnt/intermediate" "$mnt/snap" >/dev/null
  snap_id=$(btrfs subvolume show "$mnt/snap" | grep 'Subvolume ID:' | awk '{print $3}')

  # Fully complete snapshot creation
  sync

  # Delete working snapshot
  # Q1 and Q2 will lose the full snap usage
  btrfs subvolume delete "$mnt/snap" >/dev/null

  # Delete intermediate and remove from Q1
  # Q1 and Q2 will lose the full intermediate usage
  btrfs qgroup remove "0/$inter_id" 1/100 "$mnt"
  btrfs subvolume delete "$mnt/intermediate" >/dev/null

  # Q1 should be at 0, but still has 16k. Q2 is "correct" at 0 (for now...)

  # Trigger cleaner, wait for deletions
  mount -o remount,sync=1 "$mnt"
  btrfs subvolume sync "$mnt" "$snap_id"
  btrfs subvolume sync "$mnt" "$inter_id"

  # Remove Q1 from Q2
  # Frees 16k more from Q2, underflowing it to 16EiB
  btrfs qgroup remove 1/100 2/100 "$mnt"

  # And show the bad state:
  btrfs qgroup show -pc "$mnt"

        Qgroupid    Referenced    Exclusive Parent   Child   Path
        --------    ----------    --------- ------   -----   ----
        0/5           16.00KiB     16.00KiB -        -       <toplevel>
        0/256         16.00KiB     16.00KiB -        -       base
        1/100         16.00KiB     16.00KiB -        -       <0 member qgroups>
        2/100         16.00EiB     16.00EiB -        -       <0 member qgroups>

Fix this by simply not doing this quick inheritance with squotas.

I suspect that it is also wrong in normal qgroups to not recurse up the
qgroup tree in the quick inherit case, though other consistency checks
will likely fix it anyway.

Fixes: b20fe56cd285 ("btrfs: qgroup: allow quick inherit if snapshot is created and added to the same parent")
Signed-off-by: Boris Burkov <boris@bur.io>
---
 fs/btrfs/qgroup.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index 9e2b53e90dcb..d46f2653510d 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -3223,6 +3223,9 @@ static int qgroup_snapshot_quick_inherit(struct btrfs_fs_info *fs_info,
 	struct btrfs_qgroup_list *list;
 	int nr_parents = 0;
 
+	if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_FULL)
+		return 0;
+
 	src = find_qgroup_rb(fs_info, srcid);
 	if (!src)
 		return -ENOENT;
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH 2/3] btrfs: check squota parent usage on membership change
  2025-12-03 21:11 [PATCH 0/3] btrfs: squota inheritance improvements Boris Burkov
  2025-12-03 21:11 ` [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug Boris Burkov
@ 2025-12-03 21:11 ` Boris Burkov
  2026-02-08 18:59   ` Chris Mason
  2025-12-03 21:11 ` [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule Boris Burkov
  2025-12-04  3:32 ` [PATCH 0/3] btrfs: squota inheritance improvements Qu Wenruo
  3 siblings, 1 reply; 10+ messages in thread
From: Boris Burkov @ 2025-12-03 21:11 UTC (permalink / raw)
  To: linux-btrfs, kernel-team

We could have detected the quick inherit bug more directly if we had
an extra warning about squota hierarchy consistency while modifying the
hierarchy. In squotas, the parent usage always simply adds up to the sum of
its children, so we can just check for that when changing membership and
detect more accounting bugs.

Signed-off-by: Boris Burkov <boris@bur.io>
---
 fs/btrfs/qgroup.c | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index d46f2653510d..9e7e0c2e98ac 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -346,6 +346,42 @@ int btrfs_verify_qgroup_counts(const struct btrfs_fs_info *fs_info, u64 qgroupid
 }
 #endif
 
+static bool squota_check_parent_usage(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *parent)
+{
+	u64 excl_sum = 0;
+	u64 rfer_sum = 0;
+	u64 excl_cmpr_sum = 0;
+	u64 rfer_cmpr_sum = 0;
+	struct btrfs_qgroup_list *glist;
+	int nr_members = 0;
+	bool mismatch;
+
+	if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_SIMPLE)
+		return false;
+	if (btrfs_qgroup_level(parent->qgroupid) == 0)
+		return false;
+
+	/* Eligible parent qgroup. Squota; level > 0; empty members list. */
+	list_for_each_entry(glist, &parent->members, next_member) {
+		excl_sum += glist->member->excl;
+		rfer_sum += glist->member->rfer;
+		excl_cmpr_sum += glist->member->excl_cmpr;
+		rfer_cmpr_sum += glist->member->rfer_cmpr;
+		nr_members++;
+	}
+	mismatch = (parent->excl != excl_sum || parent->rfer != rfer_sum ||
+		    parent->excl_cmpr != excl_cmpr_sum || parent->rfer_cmpr != excl_cmpr_sum);
+
+	WARN(mismatch,
+	     "parent squota qgroup %hu/%llu has mismatched usage from its %d members. "
+	     "%llu %llu %llu %llu vs %llu %llu %llu %llu\n",
+	     btrfs_qgroup_level(parent->qgroupid),
+	     btrfs_qgroup_subvolid(parent->qgroupid), nr_members, parent->excl,
+	     parent->rfer, parent->excl_cmpr, parent->rfer_cmpr, excl_sum,
+	     rfer_sum, excl_cmpr_sum, rfer_cmpr_sum);
+	return mismatch;
+}
+
 __printf(2, 3)
 static void qgroup_mark_inconsistent(struct btrfs_fs_info *fs_info, const char *fmt, ...)
 {
@@ -1569,6 +1605,7 @@ int btrfs_add_qgroup_relation(struct btrfs_trans_handle *trans, u64 src, u64 dst
 		goto out;
 	}
 	ret = quick_update_accounting(fs_info, src, dst, 1);
+	squota_check_parent_usage(fs_info, parent);
 	spin_unlock(&fs_info->qgroup_lock);
 out:
 	kfree(prealloc);
@@ -1625,6 +1662,8 @@ static int __del_qgroup_relation(struct btrfs_trans_handle *trans, u64 src,
 		spin_lock(&fs_info->qgroup_lock);
 		del_relation_rb(fs_info, src, dst);
 		ret = quick_update_accounting(fs_info, src, dst, -1);
+		ASSERT(parent);
+		squota_check_parent_usage(fs_info, parent);
 		spin_unlock(&fs_info->qgroup_lock);
 	}
 out:
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule
  2025-12-03 21:11 [PATCH 0/3] btrfs: squota inheritance improvements Boris Burkov
  2025-12-03 21:11 ` [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug Boris Burkov
  2025-12-03 21:11 ` [PATCH 2/3] btrfs: check squota parent usage on membership change Boris Burkov
@ 2025-12-03 21:11 ` Boris Burkov
  2025-12-04  3:19   ` Qu Wenruo
  2025-12-04  3:32 ` [PATCH 0/3] btrfs: squota inheritance improvements Qu Wenruo
  3 siblings, 1 reply; 10+ messages in thread
From: Boris Burkov @ 2025-12-03 21:11 UTC (permalink / raw)
  To: linux-btrfs, kernel-team

Currently, with squotas, we do not allow removing a parent qgroup with
no members if it still has usage accounted to it. This makes it really
difficult to recover from accounting bugs, as we have no good way of
getting back to 0 usage.

Instead, allow deletion (it's safe at 0 members..) while still warning
about the inconsistency by adding a squota parent check.

Signed-off-by: Boris Burkov <boris@bur.io>
---
 fs/btrfs/qgroup.c | 51 +++++++++++++++++++++++++++++++++--------------
 1 file changed, 36 insertions(+), 15 deletions(-)

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index 9e7e0c2e98ac..731ab71ff8ef 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -1458,6 +1458,7 @@ static void qgroup_iterator_clean(struct list_head *head)
 	}
 }
 
+
 /*
  * The easy accounting, we're updating qgroup relationship whose child qgroup
  * only has exclusive extents.
@@ -1730,6 +1731,36 @@ int btrfs_create_qgroup(struct btrfs_trans_handle *trans, u64 qgroupid)
 	return ret;
 }
 
+static bool can_delete_parent_qgroup(struct btrfs_qgroup *qgroup)
+
+{
+	ASSERT(btrfs_qgroup_level(qgroup->qgroupid));
+	return list_empty(&qgroup->members);
+}
+
+/*
+ * Return true if we can delete the squota qgroup and false otherwise.
+ *
+ * Rules for whether we can delete:
+ *
+ * A subvolume qgroup can be removed iff the subvolume is fully deleted, which
+ * is iff there is 0 usage in the qgroup.
+ *
+ * A higher level qgroup can be removed iff it has no members.
+ * Note: We audit its usage to warn on inconsitencies without blocking deletion.
+ */
+static bool can_delete_squota_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *qgroup)
+{
+	ASSERT(btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE);
+
+	if (btrfs_qgroup_level(qgroup->qgroupid) > 0) {
+		squota_check_parent_usage(fs_info, qgroup);
+		return can_delete_parent_qgroup(qgroup);
+	}
+
+	return !(qgroup->rfer || qgroup->excl || qgroup->rfer_cmpr || qgroup->excl_cmpr);
+}
+
 /*
  * Return 0 if we can not delete the qgroup (not empty or has children etc).
  * Return >0 if we can delete the qgroup.
@@ -1740,23 +1771,13 @@ static int can_delete_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup
 	struct btrfs_key key;
 	BTRFS_PATH_AUTO_FREE(path);
 
-	/*
-	 * Squota would never be inconsistent, but there can still be case
-	 * where a dropped subvolume still has qgroup numbers, and squota
-	 * relies on such qgroup for future accounting.
-	 *
-	 * So for squota, do not allow dropping any non-zero qgroup.
-	 */
-	if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE &&
-	    (qgroup->rfer || qgroup->excl || qgroup->excl_cmpr || qgroup->rfer_cmpr))
-		return 0;
+	/* Since squotas cannot be inconsistent, they have special rules for deletion. */
+	if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE)
+		return can_delete_squota_qgroup(fs_info, qgroup);
 
 	/* For higher level qgroup, we can only delete it if it has no child. */
-	if (btrfs_qgroup_level(qgroup->qgroupid)) {
-		if (!list_empty(&qgroup->members))
-			return 0;
-		return 1;
-	}
+	if (btrfs_qgroup_level(qgroup->qgroupid))
+		return can_delete_parent_qgroup(qgroup);
 
 	/*
 	 * For level-0 qgroups, we can only delete it if it has no subvolume
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule
  2025-12-03 21:11 ` [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule Boris Burkov
@ 2025-12-04  3:19   ` Qu Wenruo
  2025-12-04  4:05     ` Boris Burkov
  0 siblings, 1 reply; 10+ messages in thread
From: Qu Wenruo @ 2025-12-04  3:19 UTC (permalink / raw)
  To: Boris Burkov, linux-btrfs, kernel-team



在 2025/12/4 07:41, Boris Burkov 写道:
> Currently, with squotas, we do not allow removing a parent qgroup with
> no members if it still has usage accounted to it. This makes it really
> difficult to recover from accounting bugs, as we have no good way of
> getting back to 0 usage.
> 
> Instead, allow deletion (it's safe at 0 members..) while still warning
> about the inconsistency by adding a squota parent check.
> 
> Signed-off-by: Boris Burkov <boris@bur.io>
> ---
>   fs/btrfs/qgroup.c | 51 +++++++++++++++++++++++++++++++++--------------
>   1 file changed, 36 insertions(+), 15 deletions(-)
> 
> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index 9e7e0c2e98ac..731ab71ff8ef 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -1458,6 +1458,7 @@ static void qgroup_iterator_clean(struct list_head *head)
>   	}
>   }
>   
> +

A stray new line.

Otherwise looks good to me. I'll reply on the cover letter.

Thanks,
Qu

>   /*
>    * The easy accounting, we're updating qgroup relationship whose child qgroup
>    * only has exclusive extents.
> @@ -1730,6 +1731,36 @@ int btrfs_create_qgroup(struct btrfs_trans_handle *trans, u64 qgroupid)
>   	return ret;
>   }
>   
> +static bool can_delete_parent_qgroup(struct btrfs_qgroup *qgroup)
> +
> +{
> +	ASSERT(btrfs_qgroup_level(qgroup->qgroupid));
> +	return list_empty(&qgroup->members);
> +}
> +
> +/*
> + * Return true if we can delete the squota qgroup and false otherwise.
> + *
> + * Rules for whether we can delete:
> + *
> + * A subvolume qgroup can be removed iff the subvolume is fully deleted, which
> + * is iff there is 0 usage in the qgroup.
> + *
> + * A higher level qgroup can be removed iff it has no members.
> + * Note: We audit its usage to warn on inconsitencies without blocking deletion.
> + */
> +static bool can_delete_squota_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *qgroup)
> +{
> +	ASSERT(btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE);
> +
> +	if (btrfs_qgroup_level(qgroup->qgroupid) > 0) {
> +		squota_check_parent_usage(fs_info, qgroup);
> +		return can_delete_parent_qgroup(qgroup);
> +	}
> +
> +	return !(qgroup->rfer || qgroup->excl || qgroup->rfer_cmpr || qgroup->excl_cmpr);
> +}
> +
>   /*
>    * Return 0 if we can not delete the qgroup (not empty or has children etc).
>    * Return >0 if we can delete the qgroup.
> @@ -1740,23 +1771,13 @@ static int can_delete_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup
>   	struct btrfs_key key;
>   	BTRFS_PATH_AUTO_FREE(path);
>   
> -	/*
> -	 * Squota would never be inconsistent, but there can still be case
> -	 * where a dropped subvolume still has qgroup numbers, and squota
> -	 * relies on such qgroup for future accounting.
> -	 *
> -	 * So for squota, do not allow dropping any non-zero qgroup.
> -	 */
> -	if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE &&
> -	    (qgroup->rfer || qgroup->excl || qgroup->excl_cmpr || qgroup->rfer_cmpr))
> -		return 0;
> +	/* Since squotas cannot be inconsistent, they have special rules for deletion. */
> +	if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE)
> +		return can_delete_squota_qgroup(fs_info, qgroup);
>   
>   	/* For higher level qgroup, we can only delete it if it has no child. */
> -	if (btrfs_qgroup_level(qgroup->qgroupid)) {
> -		if (!list_empty(&qgroup->members))
> -			return 0;
> -		return 1;
> -	}
> +	if (btrfs_qgroup_level(qgroup->qgroupid))
> +		return can_delete_parent_qgroup(qgroup);
>   
>   	/*
>   	 * For level-0 qgroups, we can only delete it if it has no subvolume


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug
  2025-12-03 21:11 ` [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug Boris Burkov
@ 2025-12-04  3:31   ` Qu Wenruo
  0 siblings, 0 replies; 10+ messages in thread
From: Qu Wenruo @ 2025-12-04  3:31 UTC (permalink / raw)
  To: Boris Burkov, linux-btrfs, kernel-team



在 2025/12/4 07:41, Boris Burkov 写道:
> qgroup_snapshot_quick_inherit() detects conditions where the snapshot
> destination would land in the same parent qgroup as the snapshot source
> subvolume. In this case we can avoid costly qgroup calculations and just
> add the nodesize of the new snapshot to the parent.
> 
> However, in the case of squotas this is actually a double count, and
> also an undercount for deeper qgroup nestings.
> 
> The following annotated script shows the issue:
> 
>    btrfs quota enable --simple "$mnt"
> 
>    # Create 2-level qgroup hierarchy
>    btrfs qgroup create 2/100 "$mnt"  # Q2 (level 2)
>    btrfs qgroup create 1/100 "$mnt"  # Q1 (level 1)
>    btrfs qgroup assign 1/100 2/100 "$mnt"
> 
>    # Create base subvolume
>    btrfs subvolume create "$mnt/base" >/dev/null
>    base_id=$(btrfs subvolume show "$mnt/base" | grep 'Subvolume ID:' | awk '{print $3}')
> 
>    # Create intermediate snapshot and add to Q1
>    btrfs subvolume snapshot "$mnt/base" "$mnt/intermediate" >/dev/null
>    inter_id=$(btrfs subvolume show "$mnt/intermediate" | grep 'Subvolume ID:' | awk '{print $3}')
>    btrfs qgroup assign "0/$inter_id" 1/100 "$mnt"
> 
>    # Create working snapshot with --inherit (auto-adds to Q1)
>    # src=intermediate (in only Q1)
>    # dst=snap (inheriting only into Q1)
>    # This double counts the 16k nodesize of the snapshot in Q1, and
>    # undercounts it in Q2.
>    btrfs subvolume snapshot -i 1/100 "$mnt/intermediate" "$mnt/snap" >/dev/null
>    snap_id=$(btrfs subvolume show "$mnt/snap" | grep 'Subvolume ID:' | awk '{print $3}')
> 
>    # Fully complete snapshot creation
>    sync
> 
>    # Delete working snapshot
>    # Q1 and Q2 will lose the full snap usage
>    btrfs subvolume delete "$mnt/snap" >/dev/null
> 
>    # Delete intermediate and remove from Q1
>    # Q1 and Q2 will lose the full intermediate usage
>    btrfs qgroup remove "0/$inter_id" 1/100 "$mnt"
>    btrfs subvolume delete "$mnt/intermediate" >/dev/null
> 
>    # Q1 should be at 0, but still has 16k. Q2 is "correct" at 0 (for now...)
> 
>    # Trigger cleaner, wait for deletions
>    mount -o remount,sync=1 "$mnt"
>    btrfs subvolume sync "$mnt" "$snap_id"
>    btrfs subvolume sync "$mnt" "$inter_id"
> 
>    # Remove Q1 from Q2
>    # Frees 16k more from Q2, underflowing it to 16EiB
>    btrfs qgroup remove 1/100 2/100 "$mnt"
> 
>    # And show the bad state:
>    btrfs qgroup show -pc "$mnt"
> 
>          Qgroupid    Referenced    Exclusive Parent   Child   Path
>          --------    ----------    --------- ------   -----   ----
>          0/5           16.00KiB     16.00KiB -        -       <toplevel>
>          0/256         16.00KiB     16.00KiB -        -       base
>          1/100         16.00KiB     16.00KiB -        -       <0 member qgroups>
>          2/100         16.00EiB     16.00EiB -        -       <0 member qgroups>
> 
> Fix this by simply not doing this quick inheritance with squotas.
> 
> I suspect that it is also wrong in normal qgroups to not recurse up the
> qgroup tree in the quick inherit case, though other consistency checks
> will likely fix it anyway.

It's indeed wrong for the regular qgroup:

  mkfs.btrfs  -f -O quota $dev
  mount $dev $mnt
  btrfs subv create $mnt/subv1
  btrfs qgroup create 1/100 $mnt
  btrfs qgroup create 2/100 $mnt
  btrfs qgroup assign 1/100 2/100 $mnt
  btrfs qgroup assign 0/256 1/100 $mnt
  btrfs qgroup show -p --sync $mnt
  Qgroupid    Referenced    Exclusive Parent     Path
  --------    ----------    --------- ------     ----
  0/5           16.00KiB     16.00KiB -          <toplevel>
  0/256         16.00KiB     16.00KiB 1/100      subv1
  1/100         16.00KiB     16.00KiB 2/100      2/100<1 member qgroup>
  2/100         16.00KiB     16.00KiB -          <0 member qgroups>
  # So far so good

  btrfs subv snap -i 1/100 $mnt/subv1 $mnt/snap1
  btrfs qgroup show -p --sync $mnt
  Qgroupid    Referenced    Exclusive Parent     Path
  --------    ----------    --------- ------     ----
  0/5           16.00KiB     16.00KiB -          <toplevel>
  0/256         16.00KiB     16.00KiB 1/100      subv1
  0/257         16.00KiB     16.00KiB 1/100      snap1
  1/100         32.00KiB     32.00KiB 2/100      2/100<1 member qgroup>
  2/100         16.00KiB     16.00KiB -          <0 member qgroups>
  # Just as you suspected, higher qgroup didn't get updated.

  btrfs subv snap -i 1/100 /mnt/btrfs/subv1 /mnt/btrfs/subv2
  btrfs qgr show -prce /mnt/btrfs/

I'll fix the regular qgroup bug soon.

Thanks,
Qu

> 
> Fixes: b20fe56cd285 ("btrfs: qgroup: allow quick inherit if snapshot is created and added to the same parent")
> Signed-off-by: Boris Burkov <boris@bur.io>
> ---
>   fs/btrfs/qgroup.c | 3 +++
>   1 file changed, 3 insertions(+)
> 
> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index 9e2b53e90dcb..d46f2653510d 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -3223,6 +3223,9 @@ static int qgroup_snapshot_quick_inherit(struct btrfs_fs_info *fs_info,
>   	struct btrfs_qgroup_list *list;
>   	int nr_parents = 0;
>   
> +	if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_FULL)
> +		return 0;
> +
>   	src = find_qgroup_rb(fs_info, srcid);
>   	if (!src)
>   		return -ENOENT;


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 0/3] btrfs: squota inheritance improvements
  2025-12-03 21:11 [PATCH 0/3] btrfs: squota inheritance improvements Boris Burkov
                   ` (2 preceding siblings ...)
  2025-12-03 21:11 ` [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule Boris Burkov
@ 2025-12-04  3:32 ` Qu Wenruo
  3 siblings, 0 replies; 10+ messages in thread
From: Qu Wenruo @ 2025-12-04  3:32 UTC (permalink / raw)
  To: Boris Burkov, linux-btrfs, kernel-team



在 2025/12/4 07:41, Boris Burkov 写道:
> One bug fix for an off-by-one metadata accounting error and two generic
> improvements. Details in the patches.

Reviewed-by: Qu Wenruo <wqu@suse.com>

> 
> Boris Burkov (3):
>    btrfs: fix qgroup_snapshot_quick_inherit() squota bug

Thanks for the bug report of regular qgroup.

>    btrfs: check squota parent usage on membership change
>    btrfs: relax squota parent qgroup deletion rule

And don't forget to remove the stray new line at merge time.

Thanks,
Qu

> 
>   fs/btrfs/qgroup.c | 93 +++++++++++++++++++++++++++++++++++++++--------
>   1 file changed, 78 insertions(+), 15 deletions(-)
> 


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule
  2025-12-04  3:19   ` Qu Wenruo
@ 2025-12-04  4:05     ` Boris Burkov
  0 siblings, 0 replies; 10+ messages in thread
From: Boris Burkov @ 2025-12-04  4:05 UTC (permalink / raw)
  To: Qu Wenruo; +Cc: linux-btrfs, kernel-team

On Thu, Dec 04, 2025 at 01:49:53PM +1030, Qu Wenruo wrote:
> 
> 
> 在 2025/12/4 07:41, Boris Burkov 写道:
> > Currently, with squotas, we do not allow removing a parent qgroup with
> > no members if it still has usage accounted to it. This makes it really
> > difficult to recover from accounting bugs, as we have no good way of
> > getting back to 0 usage.
> > 
> > Instead, allow deletion (it's safe at 0 members..) while still warning
> > about the inconsistency by adding a squota parent check.
> > 
> > Signed-off-by: Boris Burkov <boris@bur.io>
> > ---
> >   fs/btrfs/qgroup.c | 51 +++++++++++++++++++++++++++++++++--------------
> >   1 file changed, 36 insertions(+), 15 deletions(-)
> > 
> > diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> > index 9e7e0c2e98ac..731ab71ff8ef 100644
> > --- a/fs/btrfs/qgroup.c
> > +++ b/fs/btrfs/qgroup.c
> > @@ -1458,6 +1458,7 @@ static void qgroup_iterator_clean(struct list_head *head)
> >   	}
> >   }
> > +
> 
> A stray new line.
> 
> Otherwise looks good to me. I'll reply on the cover letter.
> 
> Thanks,
> Qu
> 

I didn't quite get to sending the new fstest before leaving on a
vacation before LPC, but I intend to do it next week. So please don't
feel the burden to also prepare an fstest of the new reproducer.

Thanks for the review,
Boris

> >   /*
> >    * The easy accounting, we're updating qgroup relationship whose child qgroup
> >    * only has exclusive extents.
> > @@ -1730,6 +1731,36 @@ int btrfs_create_qgroup(struct btrfs_trans_handle *trans, u64 qgroupid)
> >   	return ret;
> >   }
> > +static bool can_delete_parent_qgroup(struct btrfs_qgroup *qgroup)
> > +
> > +{
> > +	ASSERT(btrfs_qgroup_level(qgroup->qgroupid));
> > +	return list_empty(&qgroup->members);
> > +}
> > +
> > +/*
> > + * Return true if we can delete the squota qgroup and false otherwise.
> > + *
> > + * Rules for whether we can delete:
> > + *
> > + * A subvolume qgroup can be removed iff the subvolume is fully deleted, which
> > + * is iff there is 0 usage in the qgroup.
> > + *
> > + * A higher level qgroup can be removed iff it has no members.
> > + * Note: We audit its usage to warn on inconsitencies without blocking deletion.
> > + */
> > +static bool can_delete_squota_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *qgroup)
> > +{
> > +	ASSERT(btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE);
> > +
> > +	if (btrfs_qgroup_level(qgroup->qgroupid) > 0) {
> > +		squota_check_parent_usage(fs_info, qgroup);
> > +		return can_delete_parent_qgroup(qgroup);
> > +	}
> > +
> > +	return !(qgroup->rfer || qgroup->excl || qgroup->rfer_cmpr || qgroup->excl_cmpr);
> > +}
> > +
> >   /*
> >    * Return 0 if we can not delete the qgroup (not empty or has children etc).
> >    * Return >0 if we can delete the qgroup.
> > @@ -1740,23 +1771,13 @@ static int can_delete_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup
> >   	struct btrfs_key key;
> >   	BTRFS_PATH_AUTO_FREE(path);
> > -	/*
> > -	 * Squota would never be inconsistent, but there can still be case
> > -	 * where a dropped subvolume still has qgroup numbers, and squota
> > -	 * relies on such qgroup for future accounting.
> > -	 *
> > -	 * So for squota, do not allow dropping any non-zero qgroup.
> > -	 */
> > -	if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE &&
> > -	    (qgroup->rfer || qgroup->excl || qgroup->excl_cmpr || qgroup->rfer_cmpr))
> > -		return 0;
> > +	/* Since squotas cannot be inconsistent, they have special rules for deletion. */
> > +	if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE)
> > +		return can_delete_squota_qgroup(fs_info, qgroup);
> >   	/* For higher level qgroup, we can only delete it if it has no child. */
> > -	if (btrfs_qgroup_level(qgroup->qgroupid)) {
> > -		if (!list_empty(&qgroup->members))
> > -			return 0;
> > -		return 1;
> > -	}
> > +	if (btrfs_qgroup_level(qgroup->qgroupid))
> > +		return can_delete_parent_qgroup(qgroup);
> >   	/*
> >   	 * For level-0 qgroups, we can only delete it if it has no subvolume
> 

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 2/3] btrfs: check squota parent usage on membership change
  2025-12-03 21:11 ` [PATCH 2/3] btrfs: check squota parent usage on membership change Boris Burkov
@ 2026-02-08 18:59   ` Chris Mason
  2026-02-11 18:55     ` Boris Burkov
  0 siblings, 1 reply; 10+ messages in thread
From: Chris Mason @ 2026-02-08 18:59 UTC (permalink / raw)
  To: Boris Burkov; +Cc: linux-btrfs, kernel-team

Boris Burkov <boris@bur.io> wrote:
> We could have detected the quick inherit bug more directly if we had
> an extra warning about squota hierarchy consistency while modifying the
> hierarchy. In squotas, the parent usage always simply adds up to the sum of
> its children, so we can just check for that when changing membership and
> detect more accounting bugs.

Hi everyone,

I'm running the btrfs for-next branch, along with the encryption patch set
through my AI review prompts:

https://github.com/masoncl/review-prompts

I'm working on some btrfs specific instructions, but hopefully these reviews
are already useful.  I've kept the headers to make it easier to double check
that I'm actually replying to the right email, but please let me know if any
parts of this are incorrect:

commit 9c46bcda5f347febdbb4d117fb21a37ffcec5fa4
Author: Boris Burkov <boris@bur.io>

btrfs: check squota parent usage on membership change

This commit adds a diagnostic check to detect squota hierarchy
consistency issues when modifying qgroup membership. The check verifies
that parent usage equals the sum of its children's usage.

> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index 206587820fec0..3a74759b59ee4 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -346,6 +346,42 @@ int btrfs_verify_qgroup_counts(const struct btrfs_fs_info *fs_info, u64 qgroupid
>  }
>  #endif
>
> +static bool squota_check_parent_usage(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *parent)
> +{
> +	u64 excl_sum = 0;
> +	u64 rfer_sum = 0;
> +	u64 excl_cmpr_sum = 0;
> +	u64 rfer_cmpr_sum = 0;
> +	struct btrfs_qgroup_list *glist;
> +	int nr_members = 0;
> +	bool mismatch;
> +
> +	if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_SIMPLE)
> +		return false;
> +	if (btrfs_qgroup_level(parent->qgroupid) == 0)
> +		return false;
> +
> +	/* Eligible parent qgroup. Squota; level > 0; empty members list. */
> +	list_for_each_entry(glist, &parent->members, next_member) {
> +		excl_sum += glist->member->excl;
> +		rfer_sum += glist->member->rfer;
> +		excl_cmpr_sum += glist->member->excl_cmpr;
> +		rfer_cmpr_sum += glist->member->rfer_cmpr;
> +		nr_members++;
> +	}
> +	mismatch = (parent->excl != excl_sum || parent->rfer != rfer_sum ||
> +		    parent->excl_cmpr != excl_cmpr_sum || parent->rfer_cmpr != excl_cmpr_sum);
                                                                            ^^^^^^^^^^^^^^^^

Is parent->rfer_cmpr being compared to the wrong variable? The code
compares parent->rfer_cmpr to excl_cmpr_sum, but rfer_cmpr_sum was
calculated above and never used. Should this be comparing
parent->rfer_cmpr != rfer_cmpr_sum instead?

> +
> +	WARN(mismatch,
> +	     "parent squota qgroup %hu/%llu has mismatched usage from its %d members. "
> +	     "%llu %llu %llu %llu vs %llu %llu %llu %llu\n",
> +	     btrfs_qgroup_level(parent->qgroupid),
> +	     btrfs_qgroup_subvolid(parent->qgroupid), nr_members, parent->excl,
> +	     parent->rfer, parent->excl_cmpr, parent->rfer_cmpr, excl_sum,
> +	     rfer_sum, excl_cmpr_sum, rfer_cmpr_sum);
> +	return mismatch;
> +}

[ ... ]



^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 2/3] btrfs: check squota parent usage on membership change
  2026-02-08 18:59   ` Chris Mason
@ 2026-02-11 18:55     ` Boris Burkov
  0 siblings, 0 replies; 10+ messages in thread
From: Boris Burkov @ 2026-02-11 18:55 UTC (permalink / raw)
  To: Chris Mason; +Cc: linux-btrfs, kernel-team

On Sun, Feb 08, 2026 at 10:59:01AM -0800, Chris Mason wrote:
> Boris Burkov <boris@bur.io> wrote:
> > We could have detected the quick inherit bug more directly if we had
> > an extra warning about squota hierarchy consistency while modifying the
> > hierarchy. In squotas, the parent usage always simply adds up to the sum of
> > its children, so we can just check for that when changing membership and
> > detect more accounting bugs.
> 
> Hi everyone,
> 
> I'm running the btrfs for-next branch, along with the encryption patch set
> through my AI review prompts:
> 
> https://github.com/masoncl/review-prompts
> 
> I'm working on some btrfs specific instructions, but hopefully these reviews
> are already useful.  I've kept the headers to make it easier to double check
> that I'm actually replying to the right email, but please let me know if any
> parts of this are incorrect:
> 
> commit 9c46bcda5f347febdbb4d117fb21a37ffcec5fa4
> Author: Boris Burkov <boris@bur.io>
> 
> btrfs: check squota parent usage on membership change
> 
> This commit adds a diagnostic check to detect squota hierarchy
> consistency issues when modifying qgroup membership. The check verifies
> that parent usage equals the sum of its children's usage.
> 
> > diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> > index 206587820fec0..3a74759b59ee4 100644
> > --- a/fs/btrfs/qgroup.c
> > +++ b/fs/btrfs/qgroup.c
> > @@ -346,6 +346,42 @@ int btrfs_verify_qgroup_counts(const struct btrfs_fs_info *fs_info, u64 qgroupid
> >  }
> >  #endif
> >
> > +static bool squota_check_parent_usage(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *parent)
> > +{
> > +	u64 excl_sum = 0;
> > +	u64 rfer_sum = 0;
> > +	u64 excl_cmpr_sum = 0;
> > +	u64 rfer_cmpr_sum = 0;
> > +	struct btrfs_qgroup_list *glist;
> > +	int nr_members = 0;
> > +	bool mismatch;
> > +
> > +	if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_SIMPLE)
> > +		return false;
> > +	if (btrfs_qgroup_level(parent->qgroupid) == 0)
> > +		return false;
> > +
> > +	/* Eligible parent qgroup. Squota; level > 0; empty members list. */
> > +	list_for_each_entry(glist, &parent->members, next_member) {
> > +		excl_sum += glist->member->excl;
> > +		rfer_sum += glist->member->rfer;
> > +		excl_cmpr_sum += glist->member->excl_cmpr;
> > +		rfer_cmpr_sum += glist->member->rfer_cmpr;
> > +		nr_members++;
> > +	}
> > +	mismatch = (parent->excl != excl_sum || parent->rfer != rfer_sum ||
> > +		    parent->excl_cmpr != excl_cmpr_sum || parent->rfer_cmpr != excl_cmpr_sum);
>                                                                             ^^^^^^^^^^^^^^^^
> 
> Is parent->rfer_cmpr being compared to the wrong variable? The code
> compares parent->rfer_cmpr to excl_cmpr_sum, but rfer_cmpr_sum was
> calculated above and never used. Should this be comparing
> parent->rfer_cmpr != rfer_cmpr_sum instead?
> 

I believe this is correct, but "irrelevant" because rfer == excl in
squota. (and rfer_cmpr == excl_cmpr == 0)

But might as well clean it up, as it is needlessly messy. I'll send the patch.

Thanks,
Boris

> > +
> > +	WARN(mismatch,
> > +	     "parent squota qgroup %hu/%llu has mismatched usage from its %d members. "
> > +	     "%llu %llu %llu %llu vs %llu %llu %llu %llu\n",
> > +	     btrfs_qgroup_level(parent->qgroupid),
> > +	     btrfs_qgroup_subvolid(parent->qgroupid), nr_members, parent->excl,
> > +	     parent->rfer, parent->excl_cmpr, parent->rfer_cmpr, excl_sum,
> > +	     rfer_sum, excl_cmpr_sum, rfer_cmpr_sum);
> > +	return mismatch;
> > +}
> 
> [ ... ]
> 
> 

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-02-11 18:56 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-03 21:11 [PATCH 0/3] btrfs: squota inheritance improvements Boris Burkov
2025-12-03 21:11 ` [PATCH 1/3] btrfs: fix qgroup_snapshot_quick_inherit() squota bug Boris Burkov
2025-12-04  3:31   ` Qu Wenruo
2025-12-03 21:11 ` [PATCH 2/3] btrfs: check squota parent usage on membership change Boris Burkov
2026-02-08 18:59   ` Chris Mason
2026-02-11 18:55     ` Boris Burkov
2025-12-03 21:11 ` [PATCH 3/3] btrfs: relax squota parent qgroup deletion rule Boris Burkov
2025-12-04  3:19   ` Qu Wenruo
2025-12-04  4:05     ` Boris Burkov
2025-12-04  3:32 ` [PATCH 0/3] btrfs: squota inheritance improvements Qu Wenruo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox