Linux Btrfs filesystem development
 help / color / mirror / Atom feed
* [PATCH 0/4] btrfs-progs: check: fix and refactor load_quota_info()
@ 2026-10-05  9:01 Qu Wenruo
  2026-10-05  9:01 ` [PATCH 1/4] btrfs-progs: check: fix the inverted condition to load qgroups Qu Wenruo
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Qu Wenruo @ 2026-10-05  9:01 UTC (permalink / raw)
  To: linux-btrfs

The btrfs-check code of qgroup verification is pretty old, and there is
a long existing bug from day 1 that causes incorrect initial search
condition.

The first patch is to fix that bug.

Then the second one to reduce the memory usage of qgroup_count
structure.

The third one is to refactor load_quota_info() to follow the modern
standard and practice.

Finally add a test case as a regression test.

Qu Wenruo (4):
  btrfs-progs: check: fix the inverted condition to load qgroups
  btrfs-progs: check: remove unused qgroup_count::key
  btrfs-progs: check: refactor load_quota_info()
  btrfs-progs: fsck-tests: add a new test case for qgroup tree check

 check/qgroup-verify.c                         | 155 +++++++++---------
 tests/fsck-tests/076-large-quota-tree/test.sh |  28 ++++
 2 files changed, 103 insertions(+), 80 deletions(-)
 create mode 100644 tests/fsck-tests/076-large-quota-tree/test.sh

--
2.55.0


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

* [PATCH 1/4] btrfs-progs: check: fix the inverted condition to load qgroups
  2026-10-05  9:01 [PATCH 0/4] btrfs-progs: check: fix and refactor load_quota_info() Qu Wenruo
@ 2026-10-05  9:01 ` Qu Wenruo
  2026-10-05  9:01 ` [PATCH 2/4] btrfs-progs: check: remove unused qgroup_count::key Qu Wenruo
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Qu Wenruo @ 2026-10-05  9:01 UTC (permalink / raw)
  To: linux-btrfs

[BUG]
When a qgroup tree is higher than level 0 and has a qgroup relation
between the first subvolume and a higher level qgroup, the btrfs check
can failed to load qgroup tree.

A minimal reproducer looks like the following:

  mkfs.btrfs -f -K -n 4k $dev > /dev/null
  mount $dev $mnt
  btrfs quota enable "$mnt"
  btrfs quota rescan -w $mnt
  for i in $(seq 1 32); do
          btrfs subvolume create "$mnt/s$i" >/dev/null
  done
  id=$(btrfs subvolume list "$mnt" | awk 'NR==1 {print $2}')
  btrfs qgroup create 1/1 "$mnt"
  btrfs qgroup assign "0/$id" 1/1 "$mnt"
  umount $mnt
  btrfs check -Q $dev

The failure looks like:

 Opening filesystem to check...
 Checking filesystem on /dev/test/scratch1
 UUID: fbe3bc62-168a-4fec-8ef5-ba5f92d8c22c
 Print quota groups report for /dev/test/scratch1
 UUID: fbe3bc62-168a-4fec-8ef5-ba5f92d8c22c
 ERROR: failed to add qgroup relation, member=256 parent=281474976710657: No such file or directory
 ERROR: loading qgroups from disk: -2
 extent buffer leak: start 30789632 len 4096
 extent buffer leak: start 30793728 len 4096

[CAUSE]
With extra debug output, the following one line shows the problem pretty
well:

 load_quota_info: search_relations=0 search_key=(246 0 0)

That's for the first loop of load_quota_info(), which should start from
the first leaf and load all qgroups into memory.

However the search key is not (0 0 0), it's the inverted condition
initializing the search key cause btrfs-check to grab the leaf with
qgroup relation items, not the first leaf.

Thus btrfs-check failed to load the qgroups in earlier leaves, causing
the above failure.

[FIX]
The code is not that well written, but as a hot fix just revert the
condition to fix the bug.

Issues: #1180
Signed-off-by: Qu Wenruo <wqu@suse.com>
---
 check/qgroup-verify.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/check/qgroup-verify.c b/check/qgroup-verify.c
index 68002ec2e2ae..7993cd22a213 100644
--- a/check/qgroup-verify.c
+++ b/check/qgroup-verify.c
@@ -953,7 +953,7 @@ loop:
 	 * items. The 2nd pass picks up relation items and glues them to their
 	 * respective count structures.
 	 */
-	key.objectid = search_relations ? 0 : BTRFS_QGROUP_RELATION_KEY;
+	key.objectid = search_relations ? BTRFS_QGROUP_RELATION_KEY : 0;
 	key.type = 0;
 	key.offset = 0;
 
-- 
2.55.0


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

* [PATCH 2/4] btrfs-progs: check: remove unused qgroup_count::key
  2026-10-05  9:01 [PATCH 0/4] btrfs-progs: check: fix and refactor load_quota_info() Qu Wenruo
  2026-10-05  9:01 ` [PATCH 1/4] btrfs-progs: check: fix the inverted condition to load qgroups Qu Wenruo
@ 2026-10-05  9:01 ` Qu Wenruo
  2026-10-05  9:01 ` [PATCH 3/4] btrfs-progs: check: refactor load_quota_info() Qu Wenruo
  2026-10-05  9:01 ` [PATCH 4/4] btrfs-progs: fsck-tests: add a new test case for qgroup tree check Qu Wenruo
  3 siblings, 0 replies; 5+ messages in thread
From: Qu Wenruo @ 2026-10-05  9:01 UTC (permalink / raw)
  To: linux-btrfs

That member is only set during insert, but never read again.
And it's against the common practice to use btrfs_key structure for
in-memory structures.

Just remove the member to save some space for qgroup_count::key.

Signed-off-by: Qu Wenruo <wqu@suse.com>
---
 check/qgroup-verify.c | 2 --
 1 file changed, 2 deletions(-)

diff --git a/check/qgroup-verify.c b/check/qgroup-verify.c
index 7993cd22a213..433d89b47d3a 100644
--- a/check/qgroup-verify.c
+++ b/check/qgroup-verify.c
@@ -60,7 +60,6 @@ struct qgroup_count {
 	u64 qgroupid;
 	int subvol_exists;
 
-	struct btrfs_disk_key key;
 	struct qgroup_info diskinfo;
 
 	struct qgroup_info info;
@@ -863,7 +862,6 @@ static struct qgroup_count *alloc_count(struct btrfs_disk_key *key,
 
 	if (c) {
 		c->qgroupid = btrfs_disk_key_offset(key);
-		c->key = *key;
 
 		item = &c->diskinfo;
 		item->referenced = btrfs_qgroup_info_rfer(leaf, disk);
-- 
2.55.0


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

* [PATCH 3/4] btrfs-progs: check: refactor load_quota_info()
  2026-10-05  9:01 [PATCH 0/4] btrfs-progs: check: fix and refactor load_quota_info() Qu Wenruo
  2026-10-05  9:01 ` [PATCH 1/4] btrfs-progs: check: fix the inverted condition to load qgroups Qu Wenruo
  2026-10-05  9:01 ` [PATCH 2/4] btrfs-progs: check: remove unused qgroup_count::key Qu Wenruo
@ 2026-10-05  9:01 ` Qu Wenruo
  2026-10-05  9:01 ` [PATCH 4/4] btrfs-progs: fsck-tests: add a new test case for qgroup tree check Qu Wenruo
  3 siblings, 0 replies; 5+ messages in thread
From: Qu Wenruo @ 2026-10-05  9:01 UTC (permalink / raw)
  To: linux-btrfs

Modernize that function by:

- Use btrfs_key when possible
  The structure btrfs_disk_key is only for on-disk usage, every
  in-memory operation is based on btrfs_key.

  This includes the function alloc_count(), which now also accepts
  btrfs_key as a parameter.

- Enhance the error handling
  The path is not released when we hit an error.

- Use an dedicated helper to scan one leaf
  Instead of nested loops, which is bug-prone for continue/break calls.

- Remove the unnecessary double loop
  There is a local variable @search_relations, which has caused a long
  existing bug before.

  The idea is to skip all the QGROUP_LIMIT items, but we can skip them
  very quickly, and it's way better for one's mental health just to do a
  full tree iteration.

Signed-off-by: Qu Wenruo <wqu@suse.com>
---
 check/qgroup-verify.c | 153 +++++++++++++++++++++---------------------
 1 file changed, 75 insertions(+), 78 deletions(-)

diff --git a/check/qgroup-verify.c b/check/qgroup-verify.c
index 433d89b47d3a..63902ffa6f5b 100644
--- a/check/qgroup-verify.c
+++ b/check/qgroup-verify.c
@@ -853,7 +853,7 @@ static struct qgroup_count *find_count(u64 qgroupid)
 	return NULL;
 }
 
-static struct qgroup_count *alloc_count(struct btrfs_disk_key *key,
+static struct qgroup_count *alloc_count(struct btrfs_key *key,
 					struct extent_buffer *leaf,
 					struct btrfs_qgroup_info_item *disk)
 {
@@ -861,7 +861,7 @@ static struct qgroup_count *alloc_count(struct btrfs_disk_key *key,
 	struct qgroup_info *item;
 
 	if (c) {
-		c->qgroupid = btrfs_disk_key_offset(key);
+		c->qgroupid = key->offset;
 
 		item = &c->diskinfo;
 		item->referenced = btrfs_qgroup_info_rfer(leaf, disk);
@@ -928,30 +928,80 @@ static void read_qgroup_status(struct btrfs_fs_info *info, struct extent_buffer
 	ct->scan_progress = btrfs_qgroup_status_rescan(eb, status_item);
 }
 
+static int load_one_quota_leaf(struct extent_buffer *leaf)
+{
+	struct btrfs_fs_info *fs_info = leaf->fs_info;
+	const u32 nr_items = btrfs_header_nritems(leaf);
+
+	for (int i = 0; i < nr_items; i++) {
+		struct qgroup_count *qc;
+		struct btrfs_qgroup_info_item *qii;
+		struct btrfs_key key;
+		struct btrfs_key root_key;
+		struct btrfs_root *subv_root;
+		int ret;
+
+		btrfs_item_key_to_cpu(leaf, &key, i);
+
+		if (key.type == BTRFS_QGROUP_RELATION_KEY) {
+			ret = add_qgroup_relation(key.objectid,
+						  key.offset);
+			if (ret) {
+				errno = -ret;
+				error(
+	"failed to add qgroup relation, member=%llu parent=%llu: %m",
+				      key.objectid, key.offset);
+				return ret;
+			}
+			continue;
+		}
+
+		if (key.type == BTRFS_QGROUP_STATUS_KEY) {
+			read_qgroup_status(fs_info, leaf, i, &counts);
+			continue;
+		}
+
+		/*
+		 * At this point, we can ignore anything that
+		 * isn't a qgroup info.
+		 */
+		if (key.type != BTRFS_QGROUP_INFO_KEY)
+			continue;
+
+		qii = btrfs_item_ptr(leaf, i, struct btrfs_qgroup_info_item);
+
+		qc = alloc_count(&key, leaf, qii);
+		if (!qc) {
+			ret = ENOMEM;
+			error_mem(NULL);
+			return ret;
+		}
+
+		root_key.objectid = key.offset;
+		root_key.type = BTRFS_ROOT_ITEM_KEY;
+		root_key.offset = (u64)-1;
+		subv_root = btrfs_read_fs_root_no_cache(fs_info, &root_key);
+		if (!IS_ERR(subv_root)) {
+			qc->subvol_exists = 1;
+			btrfs_free_fs_root(subv_root);
+		}
+	}
+	return 0;
+}
+
 static int load_quota_info(struct btrfs_fs_info *info)
 {
 	int ret;
 	struct btrfs_root *root = info->quota_root;
-	struct btrfs_root *tmproot;
 	struct btrfs_path path = { 0 };
 	struct btrfs_key key;
-	struct btrfs_key root_key;
-	struct btrfs_disk_key disk_key;
 	struct extent_buffer *leaf;
-	struct btrfs_qgroup_info_item *item;
-	struct qgroup_count *count;
-	int i, nr;
-	int search_relations = 0;
 
 	if (btrfs_fs_incompat(info, SIMPLE_QUOTA))
 		counts.simple = 1;
-loop:
-	/*
-	 * Do 2 passes, the first allocates group counts and reads status
-	 * items. The 2nd pass picks up relation items and glues them to their
-	 * respective count structures.
-	 */
-	key.objectid = search_relations ? BTRFS_QGROUP_RELATION_KEY : 0;
+
+	/* Go through all qgroup leaves, and load qgroups and relationship items. */
+	key.objectid = 0;
 	key.type = 0;
 	key.offset = 0;
 
@@ -964,72 +1014,19 @@ loop:
 	while (1) {
 		leaf = path.nodes[0];
 
-		nr = btrfs_header_nritems(leaf);
-		for(i = 0; i < nr; i++) {
-			btrfs_item_key(leaf, &disk_key, i);
-			btrfs_disk_key_to_cpu(&key, &disk_key);
-
-			if (search_relations) {
-				if (key.type == BTRFS_QGROUP_RELATION_KEY) {
-					ret = add_qgroup_relation(key.objectid,
-								  key.offset);
-					if (ret) {
-						errno = -ret;
-						error(
-		"failed to add qgroup relation, member=%llu parent=%llu: %m",
-						      key.objectid, key.offset);
-						goto out;
-					}
-				}
-				continue;
-			}
-
-			if (key.type == BTRFS_QGROUP_STATUS_KEY) {
-				read_qgroup_status(info, leaf, i, &counts);
-				continue;
-			}
-
-			/*
-			 * At this point, we can ignore anything that
-			 * isn't a qgroup info.
-			 */
-			if (key.type != BTRFS_QGROUP_INFO_KEY)
-				continue;
-
-			item = btrfs_item_ptr(leaf, i,
-					      struct btrfs_qgroup_info_item);
-
-			count = alloc_count(&disk_key, leaf, item);
-			if (!count) {
-				ret = ENOMEM;
-				error_mem(NULL);
-				goto out;
-			}
-
-			root_key.objectid = key.offset;
-			root_key.type = BTRFS_ROOT_ITEM_KEY;
-			root_key.offset = (u64)-1;
-			tmproot = btrfs_read_fs_root_no_cache(info, &root_key);
-			if (tmproot && !IS_ERR(tmproot)) {
-				count->subvol_exists = 1;
-				btrfs_free_fs_root(tmproot);
-			}
-		}
-
+		ret = load_one_quota_leaf(leaf);
+		if (ret < 0)
+			goto out;
 		ret = btrfs_next_leaf(root, &path);
-		if (ret != 0)
+		if (ret < 0)
+			goto out;
+		if (ret > 0) {
+			ret = 0;
 			break;
+		}
 	}
-
-	ret = 0;
-	btrfs_release_path(&path);
-
-	if (!search_relations) {
-		search_relations = 1;
-		goto loop;
-	}
-
 out:
+	btrfs_release_path(&path);
 	return ret;
 }
 
-- 
2.55.0


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

* [PATCH 4/4] btrfs-progs: fsck-tests: add a new test case for qgroup tree check
  2026-10-05  9:01 [PATCH 0/4] btrfs-progs: check: fix and refactor load_quota_info() Qu Wenruo
                   ` (2 preceding siblings ...)
  2026-10-05  9:01 ` [PATCH 3/4] btrfs-progs: check: refactor load_quota_info() Qu Wenruo
@ 2026-10-05  9:01 ` Qu Wenruo
  3 siblings, 0 replies; 5+ messages in thread
From: Qu Wenruo @ 2026-10-05  9:01 UTC (permalink / raw)
  To: linux-btrfs

The workload is straight forward:

- Create a btrfs with qgroup full mode

- Create 512 subvolumes
  This should cause at least 2 leaves, even using 64K nodesize.

- Create qgroup 1/1 and assign 0/5 to it

- Run btrfs-check on the fs
  Unpatched progs will load the last leaf first, add the qgroup relation
  item without loading the qgroups in the previous leaf.

Signed-off-by: Qu Wenruo <wqu@suse.com>
---
 tests/fsck-tests/076-large-quota-tree/test.sh | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 tests/fsck-tests/076-large-quota-tree/test.sh

diff --git a/tests/fsck-tests/076-large-quota-tree/test.sh b/tests/fsck-tests/076-large-quota-tree/test.sh
new file mode 100644
index 000000000000..0697fa06795b
--- /dev/null
+++ b/tests/fsck-tests/076-large-quota-tree/test.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# Make sure "btrfs check" can properly handle a quota tree whose level
+# is larger than 0.
+
+source "$TEST_TOP/common" || exit
+
+check_prereq btrfs
+check_prereq mkfs.btrfs
+
+setup_root_helper
+prepare_test_dev
+
+run_check_mkfs_test_dev -O quota
+run_check_mount_test_dev
+# Create 512 subvolumes, which should cause at least 2 leaves even for 64K nodesize.
+for ((i = 0; i < 512; i++)); do
+	run_check $SUDO_HELPER "$TOP/btrfs" subvolume create "$TEST_MNT/subv_$i"
+done
+
+# Create qgroup 1/1 and add 0/5 to it, which should cause unpatched
+# btrfs-check to only load the last leaf for relation items, without
+# loading the involved qgroups.
+run_check $SUDO_HELPER "$TOP/btrfs" qgroup create 1/1 "$TEST_MNT"
+run_check $SUDO_HELPER "$TOP/btrfs" qgroup assign 0/5 1/1 "$TEST_MNT"
+run_check_umount_test_dev
+
+run_check $SUDO_HELPER "$TOP/btrfs" check "$TEST_DEV"
-- 
2.55.0


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

end of thread, other threads:[~2026-10-05  9:01 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-05  9:01 [PATCH 0/4] btrfs-progs: check: fix and refactor load_quota_info() Qu Wenruo
2026-10-05  9:01 ` [PATCH 1/4] btrfs-progs: check: fix the inverted condition to load qgroups Qu Wenruo
2026-10-05  9:01 ` [PATCH 2/4] btrfs-progs: check: remove unused qgroup_count::key Qu Wenruo
2026-10-05  9:01 ` [PATCH 3/4] btrfs-progs: check: refactor load_quota_info() Qu Wenruo
2026-10-05  9:01 ` [PATCH 4/4] btrfs-progs: fsck-tests: add a new test case for qgroup tree check Qu Wenruo

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