public inbox for linux-btrfs@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] btrfs: qgroup race fix and cleanup
@ 2025-06-30 12:39 fdmanana
  2025-06-30 12:39 ` [PATCH 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
                   ` (3 more replies)
  0 siblings, 4 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 12:39 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

Fix a race that leads to a use-after-free plus cleanup no longer used
qgroup_ulist.

Filipe Manana (2):
  btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  btrfs: qgroup: remove no longer used fs_info->qgroup_ulist

 fs/btrfs/disk-io.c |  1 -
 fs/btrfs/fs.h      |  6 ------
 fs/btrfs/qgroup.c  | 53 +++++++++++++++-------------------------------
 3 files changed, 17 insertions(+), 43 deletions(-)

-- 
2.47.2


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

* [PATCH 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  2025-06-30 12:39 [PATCH 0/2] btrfs: qgroup race fix and cleanup fdmanana
@ 2025-06-30 12:39 ` fdmanana
  2025-06-30 12:39 ` [PATCH 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 12:39 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

There's a race between a task disabling quotas and another running the
rescan ioctl that can result in a use-after-free of qgroup records from
the fs_info->qgroup_tree rbtree.

This happens as follows:

1) Task A enters btrfs_ioctl_quota_rescan() -> btrfs_qgroup_rescan();

2) Task B enters btrfs_quota_disable() and calls
   btrfs_qgroup_wait_for_completion(), which does nothing because at that
   point fs_info->qgroup_rescan_running is false (it wasn't set yet by
   task A);

3) Task B calls btrfs_free_qgroup_config() which starts freeing qgroups
   from fs_info->qgroup_tree without taking the lock fs_info->qgroup_lock;

4) Task A enters qgroup_rescan_zero_tracking() which starts iterating
   the fs_info->qgroup_tree tree while holding fs_info->qgroup_lock,
   but task B is freeing qgroup records from that tree without holding
   the lock, resulting in a use-after-free.

Fix this by taking fs_info->qgroup_lock at btrfs_free_qgroup_config().
Also at btrfs_qgroup_rescan() don't start the rescan worker if quotas
were already disabled.

Reported-by: cen zhang <zzzccc427@gmail.com>
Link: https://lore.kernel.org/linux-btrfs/CAFRLqsV+cMDETFuzqdKSHk_FDm6tneea45krsHqPD6B3FetLpQ@mail.gmail.com/
Signed-off-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/qgroup.c | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index b83d9534adae..0b07431963e8 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -636,22 +636,30 @@ bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info)
 
 /*
  * This is called from close_ctree() or open_ctree() or btrfs_quota_disable(),
- * first two are in single-threaded paths.And for the third one, we have set
- * quota_root to be null with qgroup_lock held before, so it is safe to clean
- * up the in-memory structures without qgroup_lock held.
+ * first two are in single-threaded paths.
  */
 void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
 {
 	struct rb_node *n;
 	struct btrfs_qgroup *qgroup;
 
+	/*
+	 * btrfs_quota_disable() can be called concurrently with
+	 * btrfs_qgroup_rescan() -> qgroup_rescan_zero_tracking(), so take the
+	 * lock.
+	 */
+	spin_lock(&fs_info->qgroup_lock);
 	while ((n = rb_first(&fs_info->qgroup_tree))) {
 		qgroup = rb_entry(n, struct btrfs_qgroup, node);
 		rb_erase(n, &fs_info->qgroup_tree);
 		__del_qgroup_rb(qgroup);
+		spin_unlock(&fs_info->qgroup_lock);
 		btrfs_sysfs_del_one_qgroup(fs_info, qgroup);
 		kfree(qgroup);
+		spin_lock(&fs_info->qgroup_lock);
 	}
+	spin_unlock(&fs_info->qgroup_lock);
+
 	/*
 	 * We call btrfs_free_qgroup_config() when unmounting
 	 * filesystem and disabling quota, so we set qgroup_ulist
@@ -4036,9 +4044,11 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
 	qgroup_rescan_zero_tracking(fs_info);
 
 	mutex_lock(&fs_info->qgroup_rescan_lock);
-	fs_info->qgroup_rescan_running = true;
-	btrfs_queue_work(fs_info->qgroup_rescan_workers,
-			 &fs_info->qgroup_rescan_work);
+	if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {
+		fs_info->qgroup_rescan_running = true;
+		btrfs_queue_work(fs_info->qgroup_rescan_workers,
+				 &fs_info->qgroup_rescan_work);
+	}
 	mutex_unlock(&fs_info->qgroup_rescan_lock);
 
 	return 0;
-- 
2.47.2


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

* [PATCH 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist
  2025-06-30 12:39 [PATCH 0/2] btrfs: qgroup race fix and cleanup fdmanana
  2025-06-30 12:39 ` [PATCH 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
@ 2025-06-30 12:39 ` fdmanana
  2025-06-30 13:01 ` [PATCH v2 0/2] btrfs: qgroup race fix and cleanup fdmanana
  2025-06-30 13:07 ` [PATCH v3 0/2] btrfs: qgroup race fix and cleanup fdmanana
  3 siblings, 0 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 12:39 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

It's not used anymore after commit 091344508249 ("btrfs: qgroup: use
qgroup_iterator in qgroup_convert_meta()"), so remove it.

Signed-off-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/disk-io.c |  1 -
 fs/btrfs/fs.h      |  6 ------
 fs/btrfs/qgroup.c  | 31 +------------------------------
 3 files changed, 1 insertion(+), 37 deletions(-)

diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index f6fa951c6be9..ee4911452cfd 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -1947,7 +1947,6 @@ static void btrfs_init_qgroup(struct btrfs_fs_info *fs_info)
 	fs_info->qgroup_tree = RB_ROOT;
 	INIT_LIST_HEAD(&fs_info->dirty_qgroups);
 	fs_info->qgroup_seq = 1;
-	fs_info->qgroup_ulist = NULL;
 	fs_info->qgroup_rescan_running = false;
 	fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT;
 	mutex_init(&fs_info->qgroup_rescan_lock);
diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index b239e4b8421c..a731c883687d 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -740,12 +740,6 @@ struct btrfs_fs_info {
 	struct rb_root qgroup_tree;
 	spinlock_t qgroup_lock;
 
-	/*
-	 * Used to avoid frequently calling ulist_alloc()/ulist_free()
-	 * when doing qgroup accounting, it must be protected by qgroup_lock.
-	 */
-	struct ulist *qgroup_ulist;
-
 	/*
 	 * Protect user change for quota operations. If a transaction is needed,
 	 * it must be started before locking this lock.
diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index 0b07431963e8..aadd1dbf7fb0 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -397,12 +397,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
 	if (!fs_info->quota_root)
 		return 0;
 
-	fs_info->qgroup_ulist = ulist_alloc(GFP_KERNEL);
-	if (!fs_info->qgroup_ulist) {
-		ret = -ENOMEM;
-		goto out;
-	}
-
 	path = btrfs_alloc_path();
 	if (!path) {
 		ret = -ENOMEM;
@@ -587,8 +581,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
 		if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN)
 			ret = qgroup_rescan_init(fs_info, rescan_progress, 0);
 	} else {
-		ulist_free(fs_info->qgroup_ulist);
-		fs_info->qgroup_ulist = NULL;
 		fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN;
 		btrfs_sysfs_del_qgroups(fs_info);
 	}
@@ -660,13 +652,6 @@ void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
 	}
 	spin_unlock(&fs_info->qgroup_lock);
 
-	/*
-	 * We call btrfs_free_qgroup_config() when unmounting
-	 * filesystem and disabling quota, so we set qgroup_ulist
-	 * to be null here to avoid double free.
-	 */
-	ulist_free(fs_info->qgroup_ulist);
-	fs_info->qgroup_ulist = NULL;
 	btrfs_sysfs_del_qgroups(fs_info);
 }
 
@@ -1012,7 +997,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	struct btrfs_qgroup *qgroup = NULL;
 	struct btrfs_qgroup *prealloc = NULL;
 	struct btrfs_trans_handle *trans = NULL;
-	struct ulist *ulist = NULL;
 	const bool simple = (quota_ctl_args->cmd == BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA);
 	int ret = 0;
 	int slot;
@@ -1035,12 +1019,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (fs_info->quota_root)
 		goto out;
 
-	ulist = ulist_alloc(GFP_KERNEL);
-	if (!ulist) {
-		ret = -ENOMEM;
-		goto out;
-	}
-
 	ret = btrfs_sysfs_add_qgroups(fs_info);
 	if (ret < 0)
 		goto out;
@@ -1080,9 +1058,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (fs_info->quota_root)
 		goto out;
 
-	fs_info->qgroup_ulist = ulist;
-	ulist = NULL;
-
 	/*
 	 * initially create the quota tree
 	 */
@@ -1281,17 +1256,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (ret)
 		btrfs_put_root(quota_root);
 out:
-	if (ret) {
-		ulist_free(fs_info->qgroup_ulist);
-		fs_info->qgroup_ulist = NULL;
+	if (ret)
 		btrfs_sysfs_del_qgroups(fs_info);
-	}
 	mutex_unlock(&fs_info->qgroup_ioctl_lock);
 	if (ret && trans)
 		btrfs_end_transaction(trans);
 	else if (trans)
 		ret = btrfs_end_transaction(trans);
-	ulist_free(ulist);
 	kfree(prealloc);
 	return ret;
 }
-- 
2.47.2


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

* [PATCH v2 0/2] btrfs: qgroup race fix and cleanup
  2025-06-30 12:39 [PATCH 0/2] btrfs: qgroup race fix and cleanup fdmanana
  2025-06-30 12:39 ` [PATCH 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
  2025-06-30 12:39 ` [PATCH 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
@ 2025-06-30 13:01 ` fdmanana
  2025-06-30 13:01   ` [PATCH v2 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
  2025-06-30 13:01   ` [PATCH v2 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
  2025-06-30 13:07 ` [PATCH v3 0/2] btrfs: qgroup race fix and cleanup fdmanana
  3 siblings, 2 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 13:01 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

Fix a race that leads to a use-after-free plus cleanup no longer used
qgroup_ulist.

V2: Update patch 1/2 to return -ENOTCONN if quotas are disabled when
    we try to start the rescan worker.

Filipe Manana (2):
  btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  btrfs: qgroup: remove no longer used fs_info->qgroup_ulist

 fs/btrfs/disk-io.c |  1 -
 fs/btrfs/fs.h      |  6 -----
 fs/btrfs/qgroup.c  | 55 ++++++++++++++++------------------------------
 3 files changed, 19 insertions(+), 43 deletions(-)

-- 
2.47.2


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

* [PATCH v2 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  2025-06-30 13:01 ` [PATCH v2 0/2] btrfs: qgroup race fix and cleanup fdmanana
@ 2025-06-30 13:01   ` fdmanana
  2025-06-30 13:01   ` [PATCH v2 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
  1 sibling, 0 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 13:01 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

There's a race between a task disabling quotas and another running the
rescan ioctl that can result in a use-after-free of qgroup records from
the fs_info->qgroup_tree rbtree.

This happens as follows:

1) Task A enters btrfs_ioctl_quota_rescan() -> btrfs_qgroup_rescan();

2) Task B enters btrfs_quota_disable() and calls
   btrfs_qgroup_wait_for_completion(), which does nothing because at that
   point fs_info->qgroup_rescan_running is false (it wasn't set yet by
   task A);

3) Task B calls btrfs_free_qgroup_config() which starts freeing qgroups
   from fs_info->qgroup_tree without taking the lock fs_info->qgroup_lock;

4) Task A enters qgroup_rescan_zero_tracking() which starts iterating
   the fs_info->qgroup_tree tree while holding fs_info->qgroup_lock,
   but task B is freeing qgroup records from that tree without holding
   the lock, resulting in a use-after-free.

Fix this by taking fs_info->qgroup_lock at btrfs_free_qgroup_config().
Also at btrfs_qgroup_rescan() don't start the rescan worker if quotas
were already disabled.

Reported-by: cen zhang <zzzccc427@gmail.com>
Link: https://lore.kernel.org/linux-btrfs/CAFRLqsV+cMDETFuzqdKSHk_FDm6tneea45krsHqPD6B3FetLpQ@mail.gmail.com/
Signed-off-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/qgroup.c | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index b83d9534adae..b24cb54e4f42 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -636,22 +636,30 @@ bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info)
 
 /*
  * This is called from close_ctree() or open_ctree() or btrfs_quota_disable(),
- * first two are in single-threaded paths.And for the third one, we have set
- * quota_root to be null with qgroup_lock held before, so it is safe to clean
- * up the in-memory structures without qgroup_lock held.
+ * first two are in single-threaded paths.
  */
 void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
 {
 	struct rb_node *n;
 	struct btrfs_qgroup *qgroup;
 
+	/*
+	 * btrfs_quota_disable() can be called concurrently with
+	 * btrfs_qgroup_rescan() -> qgroup_rescan_zero_tracking(), so take the
+	 * lock.
+	 */
+	spin_lock(&fs_info->qgroup_lock);
 	while ((n = rb_first(&fs_info->qgroup_tree))) {
 		qgroup = rb_entry(n, struct btrfs_qgroup, node);
 		rb_erase(n, &fs_info->qgroup_tree);
 		__del_qgroup_rb(qgroup);
+		spin_unlock(&fs_info->qgroup_lock);
 		btrfs_sysfs_del_one_qgroup(fs_info, qgroup);
 		kfree(qgroup);
+		spin_lock(&fs_info->qgroup_lock);
 	}
+	spin_unlock(&fs_info->qgroup_lock);
+
 	/*
 	 * We call btrfs_free_qgroup_config() when unmounting
 	 * filesystem and disabling quota, so we set qgroup_ulist
@@ -4036,9 +4044,13 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
 	qgroup_rescan_zero_tracking(fs_info);
 
 	mutex_lock(&fs_info->qgroup_rescan_lock);
-	fs_info->qgroup_rescan_running = true;
-	btrfs_queue_work(fs_info->qgroup_rescan_workers,
-			 &fs_info->qgroup_rescan_work);
+	if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {
+		fs_info->qgroup_rescan_running = true;
+		btrfs_queue_work(fs_info->qgroup_rescan_workers,
+				 &fs_info->qgroup_rescan_work);
+	} else {
+		ret = -ENOTCONN;
+	}
 	mutex_unlock(&fs_info->qgroup_rescan_lock);
 
 	return 0;
-- 
2.47.2


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

* [PATCH v2 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist
  2025-06-30 13:01 ` [PATCH v2 0/2] btrfs: qgroup race fix and cleanup fdmanana
  2025-06-30 13:01   ` [PATCH v2 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
@ 2025-06-30 13:01   ` fdmanana
  1 sibling, 0 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 13:01 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

It's not used anymore after commit 091344508249 ("btrfs: qgroup: use
qgroup_iterator in qgroup_convert_meta()"), so remove it.

Signed-off-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/disk-io.c |  1 -
 fs/btrfs/fs.h      |  6 ------
 fs/btrfs/qgroup.c  | 31 +------------------------------
 3 files changed, 1 insertion(+), 37 deletions(-)

diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index f6fa951c6be9..ee4911452cfd 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -1947,7 +1947,6 @@ static void btrfs_init_qgroup(struct btrfs_fs_info *fs_info)
 	fs_info->qgroup_tree = RB_ROOT;
 	INIT_LIST_HEAD(&fs_info->dirty_qgroups);
 	fs_info->qgroup_seq = 1;
-	fs_info->qgroup_ulist = NULL;
 	fs_info->qgroup_rescan_running = false;
 	fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT;
 	mutex_init(&fs_info->qgroup_rescan_lock);
diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index b239e4b8421c..a731c883687d 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -740,12 +740,6 @@ struct btrfs_fs_info {
 	struct rb_root qgroup_tree;
 	spinlock_t qgroup_lock;
 
-	/*
-	 * Used to avoid frequently calling ulist_alloc()/ulist_free()
-	 * when doing qgroup accounting, it must be protected by qgroup_lock.
-	 */
-	struct ulist *qgroup_ulist;
-
 	/*
 	 * Protect user change for quota operations. If a transaction is needed,
 	 * it must be started before locking this lock.
diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index b24cb54e4f42..d689afa11327 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -397,12 +397,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
 	if (!fs_info->quota_root)
 		return 0;
 
-	fs_info->qgroup_ulist = ulist_alloc(GFP_KERNEL);
-	if (!fs_info->qgroup_ulist) {
-		ret = -ENOMEM;
-		goto out;
-	}
-
 	path = btrfs_alloc_path();
 	if (!path) {
 		ret = -ENOMEM;
@@ -587,8 +581,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
 		if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN)
 			ret = qgroup_rescan_init(fs_info, rescan_progress, 0);
 	} else {
-		ulist_free(fs_info->qgroup_ulist);
-		fs_info->qgroup_ulist = NULL;
 		fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN;
 		btrfs_sysfs_del_qgroups(fs_info);
 	}
@@ -660,13 +652,6 @@ void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
 	}
 	spin_unlock(&fs_info->qgroup_lock);
 
-	/*
-	 * We call btrfs_free_qgroup_config() when unmounting
-	 * filesystem and disabling quota, so we set qgroup_ulist
-	 * to be null here to avoid double free.
-	 */
-	ulist_free(fs_info->qgroup_ulist);
-	fs_info->qgroup_ulist = NULL;
 	btrfs_sysfs_del_qgroups(fs_info);
 }
 
@@ -1012,7 +997,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	struct btrfs_qgroup *qgroup = NULL;
 	struct btrfs_qgroup *prealloc = NULL;
 	struct btrfs_trans_handle *trans = NULL;
-	struct ulist *ulist = NULL;
 	const bool simple = (quota_ctl_args->cmd == BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA);
 	int ret = 0;
 	int slot;
@@ -1035,12 +1019,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (fs_info->quota_root)
 		goto out;
 
-	ulist = ulist_alloc(GFP_KERNEL);
-	if (!ulist) {
-		ret = -ENOMEM;
-		goto out;
-	}
-
 	ret = btrfs_sysfs_add_qgroups(fs_info);
 	if (ret < 0)
 		goto out;
@@ -1080,9 +1058,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (fs_info->quota_root)
 		goto out;
 
-	fs_info->qgroup_ulist = ulist;
-	ulist = NULL;
-
 	/*
 	 * initially create the quota tree
 	 */
@@ -1281,17 +1256,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (ret)
 		btrfs_put_root(quota_root);
 out:
-	if (ret) {
-		ulist_free(fs_info->qgroup_ulist);
-		fs_info->qgroup_ulist = NULL;
+	if (ret)
 		btrfs_sysfs_del_qgroups(fs_info);
-	}
 	mutex_unlock(&fs_info->qgroup_ioctl_lock);
 	if (ret && trans)
 		btrfs_end_transaction(trans);
 	else if (trans)
 		ret = btrfs_end_transaction(trans);
-	ulist_free(ulist);
 	kfree(prealloc);
 	return ret;
 }
-- 
2.47.2


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

* [PATCH v3 0/2] btrfs: qgroup race fix and cleanup
  2025-06-30 12:39 [PATCH 0/2] btrfs: qgroup race fix and cleanup fdmanana
                   ` (2 preceding siblings ...)
  2025-06-30 13:01 ` [PATCH v2 0/2] btrfs: qgroup race fix and cleanup fdmanana
@ 2025-06-30 13:07 ` fdmanana
  2025-06-30 13:07   ` [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
  2025-06-30 13:07   ` [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
  3 siblings, 2 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 13:07 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

Fix a race that leads to a use-after-free plus cleanup no longer used
qgroup_ulist.

V2: Update patch 1/2 to return -ENOTCONN if quotas are disabled when
    we try to start the rescan worker.
V3: Make patch 1/2 actually return -ENOTCONN.

Filipe Manana (2):
  btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  btrfs: qgroup: remove no longer used fs_info->qgroup_ulist

 fs/btrfs/disk-io.c |  1 -
 fs/btrfs/fs.h      |  6 -----
 fs/btrfs/qgroup.c  | 57 ++++++++++++++++------------------------------
 3 files changed, 20 insertions(+), 44 deletions(-)

-- 
2.47.2


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

* [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  2025-06-30 13:07 ` [PATCH v3 0/2] btrfs: qgroup race fix and cleanup fdmanana
@ 2025-06-30 13:07   ` fdmanana
  2025-06-30 16:32     ` Boris Burkov
  2025-06-30 21:22     ` Qu Wenruo
  2025-06-30 13:07   ` [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
  1 sibling, 2 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 13:07 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

There's a race between a task disabling quotas and another running the
rescan ioctl that can result in a use-after-free of qgroup records from
the fs_info->qgroup_tree rbtree.

This happens as follows:

1) Task A enters btrfs_ioctl_quota_rescan() -> btrfs_qgroup_rescan();

2) Task B enters btrfs_quota_disable() and calls
   btrfs_qgroup_wait_for_completion(), which does nothing because at that
   point fs_info->qgroup_rescan_running is false (it wasn't set yet by
   task A);

3) Task B calls btrfs_free_qgroup_config() which starts freeing qgroups
   from fs_info->qgroup_tree without taking the lock fs_info->qgroup_lock;

4) Task A enters qgroup_rescan_zero_tracking() which starts iterating
   the fs_info->qgroup_tree tree while holding fs_info->qgroup_lock,
   but task B is freeing qgroup records from that tree without holding
   the lock, resulting in a use-after-free.

Fix this by taking fs_info->qgroup_lock at btrfs_free_qgroup_config().
Also at btrfs_qgroup_rescan() don't start the rescan worker if quotas
were already disabled.

Reported-by: cen zhang <zzzccc427@gmail.com>
Link: https://lore.kernel.org/linux-btrfs/CAFRLqsV+cMDETFuzqdKSHk_FDm6tneea45krsHqPD6B3FetLpQ@mail.gmail.com/
Signed-off-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/qgroup.c | 26 +++++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index b83d9534adae..8fa874ef80b3 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -636,22 +636,30 @@ bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info)
 
 /*
  * This is called from close_ctree() or open_ctree() or btrfs_quota_disable(),
- * first two are in single-threaded paths.And for the third one, we have set
- * quota_root to be null with qgroup_lock held before, so it is safe to clean
- * up the in-memory structures without qgroup_lock held.
+ * first two are in single-threaded paths.
  */
 void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
 {
 	struct rb_node *n;
 	struct btrfs_qgroup *qgroup;
 
+	/*
+	 * btrfs_quota_disable() can be called concurrently with
+	 * btrfs_qgroup_rescan() -> qgroup_rescan_zero_tracking(), so take the
+	 * lock.
+	 */
+	spin_lock(&fs_info->qgroup_lock);
 	while ((n = rb_first(&fs_info->qgroup_tree))) {
 		qgroup = rb_entry(n, struct btrfs_qgroup, node);
 		rb_erase(n, &fs_info->qgroup_tree);
 		__del_qgroup_rb(qgroup);
+		spin_unlock(&fs_info->qgroup_lock);
 		btrfs_sysfs_del_one_qgroup(fs_info, qgroup);
 		kfree(qgroup);
+		spin_lock(&fs_info->qgroup_lock);
 	}
+	spin_unlock(&fs_info->qgroup_lock);
+
 	/*
 	 * We call btrfs_free_qgroup_config() when unmounting
 	 * filesystem and disabling quota, so we set qgroup_ulist
@@ -4036,12 +4044,16 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
 	qgroup_rescan_zero_tracking(fs_info);
 
 	mutex_lock(&fs_info->qgroup_rescan_lock);
-	fs_info->qgroup_rescan_running = true;
-	btrfs_queue_work(fs_info->qgroup_rescan_workers,
-			 &fs_info->qgroup_rescan_work);
+	if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {
+		fs_info->qgroup_rescan_running = true;
+		btrfs_queue_work(fs_info->qgroup_rescan_workers,
+				 &fs_info->qgroup_rescan_work);
+	} else {
+		ret = -ENOTCONN;
+	}
 	mutex_unlock(&fs_info->qgroup_rescan_lock);
 
-	return 0;
+	return ret;
 }
 
 int btrfs_qgroup_wait_for_completion(struct btrfs_fs_info *fs_info,
-- 
2.47.2


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

* [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist
  2025-06-30 13:07 ` [PATCH v3 0/2] btrfs: qgroup race fix and cleanup fdmanana
  2025-06-30 13:07   ` [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
@ 2025-06-30 13:07   ` fdmanana
  2025-06-30 16:34     ` Boris Burkov
  2025-06-30 21:24     ` Qu Wenruo
  1 sibling, 2 replies; 14+ messages in thread
From: fdmanana @ 2025-06-30 13:07 UTC (permalink / raw)
  To: linux-btrfs

From: Filipe Manana <fdmanana@suse.com>

It's not used anymore after commit 091344508249 ("btrfs: qgroup: use
qgroup_iterator in qgroup_convert_meta()"), so remove it.

Signed-off-by: Filipe Manana <fdmanana@suse.com>
---
 fs/btrfs/disk-io.c |  1 -
 fs/btrfs/fs.h      |  6 ------
 fs/btrfs/qgroup.c  | 31 +------------------------------
 3 files changed, 1 insertion(+), 37 deletions(-)

diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index f6fa951c6be9..ee4911452cfd 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -1947,7 +1947,6 @@ static void btrfs_init_qgroup(struct btrfs_fs_info *fs_info)
 	fs_info->qgroup_tree = RB_ROOT;
 	INIT_LIST_HEAD(&fs_info->dirty_qgroups);
 	fs_info->qgroup_seq = 1;
-	fs_info->qgroup_ulist = NULL;
 	fs_info->qgroup_rescan_running = false;
 	fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT;
 	mutex_init(&fs_info->qgroup_rescan_lock);
diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index b239e4b8421c..a731c883687d 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -740,12 +740,6 @@ struct btrfs_fs_info {
 	struct rb_root qgroup_tree;
 	spinlock_t qgroup_lock;
 
-	/*
-	 * Used to avoid frequently calling ulist_alloc()/ulist_free()
-	 * when doing qgroup accounting, it must be protected by qgroup_lock.
-	 */
-	struct ulist *qgroup_ulist;
-
 	/*
 	 * Protect user change for quota operations. If a transaction is needed,
 	 * it must be started before locking this lock.
diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index 8fa874ef80b3..98a53e6edb2c 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -397,12 +397,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
 	if (!fs_info->quota_root)
 		return 0;
 
-	fs_info->qgroup_ulist = ulist_alloc(GFP_KERNEL);
-	if (!fs_info->qgroup_ulist) {
-		ret = -ENOMEM;
-		goto out;
-	}
-
 	path = btrfs_alloc_path();
 	if (!path) {
 		ret = -ENOMEM;
@@ -587,8 +581,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
 		if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN)
 			ret = qgroup_rescan_init(fs_info, rescan_progress, 0);
 	} else {
-		ulist_free(fs_info->qgroup_ulist);
-		fs_info->qgroup_ulist = NULL;
 		fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN;
 		btrfs_sysfs_del_qgroups(fs_info);
 	}
@@ -660,13 +652,6 @@ void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
 	}
 	spin_unlock(&fs_info->qgroup_lock);
 
-	/*
-	 * We call btrfs_free_qgroup_config() when unmounting
-	 * filesystem and disabling quota, so we set qgroup_ulist
-	 * to be null here to avoid double free.
-	 */
-	ulist_free(fs_info->qgroup_ulist);
-	fs_info->qgroup_ulist = NULL;
 	btrfs_sysfs_del_qgroups(fs_info);
 }
 
@@ -1012,7 +997,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	struct btrfs_qgroup *qgroup = NULL;
 	struct btrfs_qgroup *prealloc = NULL;
 	struct btrfs_trans_handle *trans = NULL;
-	struct ulist *ulist = NULL;
 	const bool simple = (quota_ctl_args->cmd == BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA);
 	int ret = 0;
 	int slot;
@@ -1035,12 +1019,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (fs_info->quota_root)
 		goto out;
 
-	ulist = ulist_alloc(GFP_KERNEL);
-	if (!ulist) {
-		ret = -ENOMEM;
-		goto out;
-	}
-
 	ret = btrfs_sysfs_add_qgroups(fs_info);
 	if (ret < 0)
 		goto out;
@@ -1080,9 +1058,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (fs_info->quota_root)
 		goto out;
 
-	fs_info->qgroup_ulist = ulist;
-	ulist = NULL;
-
 	/*
 	 * initially create the quota tree
 	 */
@@ -1281,17 +1256,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
 	if (ret)
 		btrfs_put_root(quota_root);
 out:
-	if (ret) {
-		ulist_free(fs_info->qgroup_ulist);
-		fs_info->qgroup_ulist = NULL;
+	if (ret)
 		btrfs_sysfs_del_qgroups(fs_info);
-	}
 	mutex_unlock(&fs_info->qgroup_ioctl_lock);
 	if (ret && trans)
 		btrfs_end_transaction(trans);
 	else if (trans)
 		ret = btrfs_end_transaction(trans);
-	ulist_free(ulist);
 	kfree(prealloc);
 	return ret;
 }
-- 
2.47.2


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

* Re: [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  2025-06-30 13:07   ` [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
@ 2025-06-30 16:32     ` Boris Burkov
  2025-06-30 16:53       ` Filipe Manana
  2025-06-30 21:22     ` Qu Wenruo
  1 sibling, 1 reply; 14+ messages in thread
From: Boris Burkov @ 2025-06-30 16:32 UTC (permalink / raw)
  To: fdmanana; +Cc: linux-btrfs

On Mon, Jun 30, 2025 at 02:07:47PM +0100, fdmanana@kernel.org wrote:
> From: Filipe Manana <fdmanana@suse.com>
> 
> There's a race between a task disabling quotas and another running the
> rescan ioctl that can result in a use-after-free of qgroup records from
> the fs_info->qgroup_tree rbtree.
> 
> This happens as follows:
> 
> 1) Task A enters btrfs_ioctl_quota_rescan() -> btrfs_qgroup_rescan();
> 
> 2) Task B enters btrfs_quota_disable() and calls
>    btrfs_qgroup_wait_for_completion(), which does nothing because at that
>    point fs_info->qgroup_rescan_running is false (it wasn't set yet by
>    task A);
> 
> 3) Task B calls btrfs_free_qgroup_config() which starts freeing qgroups
>    from fs_info->qgroup_tree without taking the lock fs_info->qgroup_lock;
> 
> 4) Task A enters qgroup_rescan_zero_tracking() which starts iterating
>    the fs_info->qgroup_tree tree while holding fs_info->qgroup_lock,
>    but task B is freeing qgroup records from that tree without holding
>    the lock, resulting in a use-after-free.
> 
> Fix this by taking fs_info->qgroup_lock at btrfs_free_qgroup_config().
> Also at btrfs_qgroup_rescan() don't start the rescan worker if quotas
> were already disabled.
> 
> Reported-by: cen zhang <zzzccc427@gmail.com>
> Link: https://lore.kernel.org/linux-btrfs/CAFRLqsV+cMDETFuzqdKSHk_FDm6tneea45krsHqPD6B3FetLpQ@mail.gmail.com/
> Signed-off-by: Filipe Manana <fdmanana@suse.com>

Reviewed-by: Boris Burkov <boris@bur.io>

> ---
>  fs/btrfs/qgroup.c | 26 +++++++++++++++++++-------
>  1 file changed, 19 insertions(+), 7 deletions(-)
> 
> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index b83d9534adae..8fa874ef80b3 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -636,22 +636,30 @@ bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info)
>  
>  /*
>   * This is called from close_ctree() or open_ctree() or btrfs_quota_disable(),
> - * first two are in single-threaded paths.And for the third one, we have set
> - * quota_root to be null with qgroup_lock held before, so it is safe to clean
> - * up the in-memory structures without qgroup_lock held.
> + * first two are in single-threaded paths.
>   */
>  void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
>  {
>  	struct rb_node *n;
>  	struct btrfs_qgroup *qgroup;
>  
> +	/*
> +	 * btrfs_quota_disable() can be called concurrently with
> +	 * btrfs_qgroup_rescan() -> qgroup_rescan_zero_tracking(), so take the
> +	 * lock.
> +	 */
> +	spin_lock(&fs_info->qgroup_lock);
>  	while ((n = rb_first(&fs_info->qgroup_tree))) {
>  		qgroup = rb_entry(n, struct btrfs_qgroup, node);
>  		rb_erase(n, &fs_info->qgroup_tree);
>  		__del_qgroup_rb(qgroup);
> +		spin_unlock(&fs_info->qgroup_lock);
>  		btrfs_sysfs_del_one_qgroup(fs_info, qgroup);
>  		kfree(qgroup);
> +		spin_lock(&fs_info->qgroup_lock);
>  	}
> +	spin_unlock(&fs_info->qgroup_lock);
> +
>  	/*
>  	 * We call btrfs_free_qgroup_config() when unmounting
>  	 * filesystem and disabling quota, so we set qgroup_ulist
> @@ -4036,12 +4044,16 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
>  	qgroup_rescan_zero_tracking(fs_info);
>  
>  	mutex_lock(&fs_info->qgroup_rescan_lock);
> -	fs_info->qgroup_rescan_running = true;
> -	btrfs_queue_work(fs_info->qgroup_rescan_workers,
> -			 &fs_info->qgroup_rescan_work);
> +	if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {

could this be one of the helpers like !btrfs_qgroup_enabled() or maybe
even better !btrfs_qgroup_full_accounting()?

> +		fs_info->qgroup_rescan_running = true;
> +		btrfs_queue_work(fs_info->qgroup_rescan_workers,
> +				 &fs_info->qgroup_rescan_work);
> +	} else {
> +		ret = -ENOTCONN;
> +	}
>  	mutex_unlock(&fs_info->qgroup_rescan_lock);
>  
> -	return 0;
> +	return ret;
>  }
>  
>  int btrfs_qgroup_wait_for_completion(struct btrfs_fs_info *fs_info,
> -- 
> 2.47.2
> 

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

* Re: [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist
  2025-06-30 13:07   ` [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
@ 2025-06-30 16:34     ` Boris Burkov
  2025-06-30 21:24     ` Qu Wenruo
  1 sibling, 0 replies; 14+ messages in thread
From: Boris Burkov @ 2025-06-30 16:34 UTC (permalink / raw)
  To: fdmanana; +Cc: linux-btrfs

On Mon, Jun 30, 2025 at 02:07:48PM +0100, fdmanana@kernel.org wrote:
> From: Filipe Manana <fdmanana@suse.com>
> 
> It's not used anymore after commit 091344508249 ("btrfs: qgroup: use
> qgroup_iterator in qgroup_convert_meta()"), so remove it.
> 
> Signed-off-by: Filipe Manana <fdmanana@suse.com>

Reviewed-by: Boris Burkov <boris@bur.io>

> ---
>  fs/btrfs/disk-io.c |  1 -
>  fs/btrfs/fs.h      |  6 ------
>  fs/btrfs/qgroup.c  | 31 +------------------------------
>  3 files changed, 1 insertion(+), 37 deletions(-)
> 
> diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
> index f6fa951c6be9..ee4911452cfd 100644
> --- a/fs/btrfs/disk-io.c
> +++ b/fs/btrfs/disk-io.c
> @@ -1947,7 +1947,6 @@ static void btrfs_init_qgroup(struct btrfs_fs_info *fs_info)
>  	fs_info->qgroup_tree = RB_ROOT;
>  	INIT_LIST_HEAD(&fs_info->dirty_qgroups);
>  	fs_info->qgroup_seq = 1;
> -	fs_info->qgroup_ulist = NULL;
>  	fs_info->qgroup_rescan_running = false;
>  	fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT;
>  	mutex_init(&fs_info->qgroup_rescan_lock);
> diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
> index b239e4b8421c..a731c883687d 100644
> --- a/fs/btrfs/fs.h
> +++ b/fs/btrfs/fs.h
> @@ -740,12 +740,6 @@ struct btrfs_fs_info {
>  	struct rb_root qgroup_tree;
>  	spinlock_t qgroup_lock;
>  
> -	/*
> -	 * Used to avoid frequently calling ulist_alloc()/ulist_free()
> -	 * when doing qgroup accounting, it must be protected by qgroup_lock.
> -	 */
> -	struct ulist *qgroup_ulist;
> -
>  	/*
>  	 * Protect user change for quota operations. If a transaction is needed,
>  	 * it must be started before locking this lock.
> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index 8fa874ef80b3..98a53e6edb2c 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -397,12 +397,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
>  	if (!fs_info->quota_root)
>  		return 0;
>  
> -	fs_info->qgroup_ulist = ulist_alloc(GFP_KERNEL);
> -	if (!fs_info->qgroup_ulist) {
> -		ret = -ENOMEM;
> -		goto out;
> -	}
> -
>  	path = btrfs_alloc_path();
>  	if (!path) {
>  		ret = -ENOMEM;
> @@ -587,8 +581,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
>  		if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN)
>  			ret = qgroup_rescan_init(fs_info, rescan_progress, 0);
>  	} else {
> -		ulist_free(fs_info->qgroup_ulist);
> -		fs_info->qgroup_ulist = NULL;
>  		fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN;
>  		btrfs_sysfs_del_qgroups(fs_info);
>  	}
> @@ -660,13 +652,6 @@ void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
>  	}
>  	spin_unlock(&fs_info->qgroup_lock);
>  
> -	/*
> -	 * We call btrfs_free_qgroup_config() when unmounting
> -	 * filesystem and disabling quota, so we set qgroup_ulist
> -	 * to be null here to avoid double free.
> -	 */
> -	ulist_free(fs_info->qgroup_ulist);
> -	fs_info->qgroup_ulist = NULL;
>  	btrfs_sysfs_del_qgroups(fs_info);
>  }
>  
> @@ -1012,7 +997,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>  	struct btrfs_qgroup *qgroup = NULL;
>  	struct btrfs_qgroup *prealloc = NULL;
>  	struct btrfs_trans_handle *trans = NULL;
> -	struct ulist *ulist = NULL;
>  	const bool simple = (quota_ctl_args->cmd == BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA);
>  	int ret = 0;
>  	int slot;
> @@ -1035,12 +1019,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>  	if (fs_info->quota_root)
>  		goto out;
>  
> -	ulist = ulist_alloc(GFP_KERNEL);
> -	if (!ulist) {
> -		ret = -ENOMEM;
> -		goto out;
> -	}
> -
>  	ret = btrfs_sysfs_add_qgroups(fs_info);
>  	if (ret < 0)
>  		goto out;
> @@ -1080,9 +1058,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>  	if (fs_info->quota_root)
>  		goto out;
>  
> -	fs_info->qgroup_ulist = ulist;
> -	ulist = NULL;
> -
>  	/*
>  	 * initially create the quota tree
>  	 */
> @@ -1281,17 +1256,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>  	if (ret)
>  		btrfs_put_root(quota_root);
>  out:
> -	if (ret) {
> -		ulist_free(fs_info->qgroup_ulist);
> -		fs_info->qgroup_ulist = NULL;
> +	if (ret)
>  		btrfs_sysfs_del_qgroups(fs_info);
> -	}
>  	mutex_unlock(&fs_info->qgroup_ioctl_lock);
>  	if (ret && trans)
>  		btrfs_end_transaction(trans);
>  	else if (trans)
>  		ret = btrfs_end_transaction(trans);
> -	ulist_free(ulist);
>  	kfree(prealloc);
>  	return ret;
>  }
> -- 
> 2.47.2
> 

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

* Re: [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  2025-06-30 16:32     ` Boris Burkov
@ 2025-06-30 16:53       ` Filipe Manana
  0 siblings, 0 replies; 14+ messages in thread
From: Filipe Manana @ 2025-06-30 16:53 UTC (permalink / raw)
  To: Boris Burkov; +Cc: linux-btrfs

On Mon, Jun 30, 2025 at 5:31 PM Boris Burkov <boris@bur.io> wrote:
>
> On Mon, Jun 30, 2025 at 02:07:47PM +0100, fdmanana@kernel.org wrote:
> > From: Filipe Manana <fdmanana@suse.com>
> >
> > There's a race between a task disabling quotas and another running the
> > rescan ioctl that can result in a use-after-free of qgroup records from
> > the fs_info->qgroup_tree rbtree.
> >
> > This happens as follows:
> >
> > 1) Task A enters btrfs_ioctl_quota_rescan() -> btrfs_qgroup_rescan();
> >
> > 2) Task B enters btrfs_quota_disable() and calls
> >    btrfs_qgroup_wait_for_completion(), which does nothing because at that
> >    point fs_info->qgroup_rescan_running is false (it wasn't set yet by
> >    task A);
> >
> > 3) Task B calls btrfs_free_qgroup_config() which starts freeing qgroups
> >    from fs_info->qgroup_tree without taking the lock fs_info->qgroup_lock;
> >
> > 4) Task A enters qgroup_rescan_zero_tracking() which starts iterating
> >    the fs_info->qgroup_tree tree while holding fs_info->qgroup_lock,
> >    but task B is freeing qgroup records from that tree without holding
> >    the lock, resulting in a use-after-free.
> >
> > Fix this by taking fs_info->qgroup_lock at btrfs_free_qgroup_config().
> > Also at btrfs_qgroup_rescan() don't start the rescan worker if quotas
> > were already disabled.
> >
> > Reported-by: cen zhang <zzzccc427@gmail.com>
> > Link: https://lore.kernel.org/linux-btrfs/CAFRLqsV+cMDETFuzqdKSHk_FDm6tneea45krsHqPD6B3FetLpQ@mail.gmail.com/
> > Signed-off-by: Filipe Manana <fdmanana@suse.com>
>
> Reviewed-by: Boris Burkov <boris@bur.io>
>
> > ---
> >  fs/btrfs/qgroup.c | 26 +++++++++++++++++++-------
> >  1 file changed, 19 insertions(+), 7 deletions(-)
> >
> > diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> > index b83d9534adae..8fa874ef80b3 100644
> > --- a/fs/btrfs/qgroup.c
> > +++ b/fs/btrfs/qgroup.c
> > @@ -636,22 +636,30 @@ bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info)
> >
> >  /*
> >   * This is called from close_ctree() or open_ctree() or btrfs_quota_disable(),
> > - * first two are in single-threaded paths.And for the third one, we have set
> > - * quota_root to be null with qgroup_lock held before, so it is safe to clean
> > - * up the in-memory structures without qgroup_lock held.
> > + * first two are in single-threaded paths.
> >   */
> >  void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
> >  {
> >       struct rb_node *n;
> >       struct btrfs_qgroup *qgroup;
> >
> > +     /*
> > +      * btrfs_quota_disable() can be called concurrently with
> > +      * btrfs_qgroup_rescan() -> qgroup_rescan_zero_tracking(), so take the
> > +      * lock.
> > +      */
> > +     spin_lock(&fs_info->qgroup_lock);
> >       while ((n = rb_first(&fs_info->qgroup_tree))) {
> >               qgroup = rb_entry(n, struct btrfs_qgroup, node);
> >               rb_erase(n, &fs_info->qgroup_tree);
> >               __del_qgroup_rb(qgroup);
> > +             spin_unlock(&fs_info->qgroup_lock);
> >               btrfs_sysfs_del_one_qgroup(fs_info, qgroup);
> >               kfree(qgroup);
> > +             spin_lock(&fs_info->qgroup_lock);
> >       }
> > +     spin_unlock(&fs_info->qgroup_lock);
> > +
> >       /*
> >        * We call btrfs_free_qgroup_config() when unmounting
> >        * filesystem and disabling quota, so we set qgroup_ulist
> > @@ -4036,12 +4044,16 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
> >       qgroup_rescan_zero_tracking(fs_info);
> >
> >       mutex_lock(&fs_info->qgroup_rescan_lock);
> > -     fs_info->qgroup_rescan_running = true;
> > -     btrfs_queue_work(fs_info->qgroup_rescan_workers,
> > -                      &fs_info->qgroup_rescan_work);
> > +     if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {
>
> could this be one of the helpers like !btrfs_qgroup_enabled() or maybe
> even better !btrfs_qgroup_full_accounting()?

Yes, that's fine. I'll update to that when pushing to for-next:

diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
index 98a53e6edb2c..5f1b4990f56f 100644
--- a/fs/btrfs/qgroup.c
+++ b/fs/btrfs/qgroup.c
@@ -4015,7 +4015,12 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
        qgroup_rescan_zero_tracking(fs_info);

        mutex_lock(&fs_info->qgroup_rescan_lock);
-       if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {
+       /*
+        * The rescan worker is only for full accounting qgroups, check if it's
+        * enabled as it is pointless to queue it otherwise. A concurrent quota
+        * disable may also have just cleared BTRFS_FS_QUOTA_ENABLED.
+        */
+       if (btrfs_qgroup_full_accounting(fs_info)) {
                fs_info->qgroup_rescan_running = true;
                btrfs_queue_work(fs_info->qgroup_rescan_workers,
                                 &fs_info->qgroup_rescan_work);


Thanks.

>
> > +             fs_info->qgroup_rescan_running = true;
> > +             btrfs_queue_work(fs_info->qgroup_rescan_workers,
> > +                              &fs_info->qgroup_rescan_work);
> > +     } else {
> > +             ret = -ENOTCONN;
> > +     }
> >       mutex_unlock(&fs_info->qgroup_rescan_lock);
> >
> > -     return 0;
> > +     return ret;
> >  }
> >
> >  int btrfs_qgroup_wait_for_completion(struct btrfs_fs_info *fs_info,
> > --
> > 2.47.2
> >

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

* Re: [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl
  2025-06-30 13:07   ` [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
  2025-06-30 16:32     ` Boris Burkov
@ 2025-06-30 21:22     ` Qu Wenruo
  1 sibling, 0 replies; 14+ messages in thread
From: Qu Wenruo @ 2025-06-30 21:22 UTC (permalink / raw)
  To: fdmanana, linux-btrfs



在 2025/6/30 22:37, fdmanana@kernel.org 写道:
> From: Filipe Manana <fdmanana@suse.com>
> 
> There's a race between a task disabling quotas and another running the
> rescan ioctl that can result in a use-after-free of qgroup records from
> the fs_info->qgroup_tree rbtree.
> 
> This happens as follows:
> 
> 1) Task A enters btrfs_ioctl_quota_rescan() -> btrfs_qgroup_rescan();
> 
> 2) Task B enters btrfs_quota_disable() and calls
>     btrfs_qgroup_wait_for_completion(), which does nothing because at that
>     point fs_info->qgroup_rescan_running is false (it wasn't set yet by
>     task A);
> 
> 3) Task B calls btrfs_free_qgroup_config() which starts freeing qgroups
>     from fs_info->qgroup_tree without taking the lock fs_info->qgroup_lock;
> 
> 4) Task A enters qgroup_rescan_zero_tracking() which starts iterating
>     the fs_info->qgroup_tree tree while holding fs_info->qgroup_lock,
>     but task B is freeing qgroup records from that tree without holding
>     the lock, resulting in a use-after-free.
> 
> Fix this by taking fs_info->qgroup_lock at btrfs_free_qgroup_config().
> Also at btrfs_qgroup_rescan() don't start the rescan worker if quotas
> were already disabled.
> 
> Reported-by: cen zhang <zzzccc427@gmail.com>
> Link: https://lore.kernel.org/linux-btrfs/CAFRLqsV+cMDETFuzqdKSHk_FDm6tneea45krsHqPD6B3FetLpQ@mail.gmail.com/
> Signed-off-by: Filipe Manana <fdmanana@suse.com>

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

Thanks,
Qu


> ---
>   fs/btrfs/qgroup.c | 26 +++++++++++++++++++-------
>   1 file changed, 19 insertions(+), 7 deletions(-)
> 
> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index b83d9534adae..8fa874ef80b3 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -636,22 +636,30 @@ bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info)
>   
>   /*
>    * This is called from close_ctree() or open_ctree() or btrfs_quota_disable(),
> - * first two are in single-threaded paths.And for the third one, we have set
> - * quota_root to be null with qgroup_lock held before, so it is safe to clean
> - * up the in-memory structures without qgroup_lock held.
> + * first two are in single-threaded paths.
>    */
>   void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
>   {
>   	struct rb_node *n;
>   	struct btrfs_qgroup *qgroup;
>   
> +	/*
> +	 * btrfs_quota_disable() can be called concurrently with
> +	 * btrfs_qgroup_rescan() -> qgroup_rescan_zero_tracking(), so take the
> +	 * lock.
> +	 */
> +	spin_lock(&fs_info->qgroup_lock);
>   	while ((n = rb_first(&fs_info->qgroup_tree))) {
>   		qgroup = rb_entry(n, struct btrfs_qgroup, node);
>   		rb_erase(n, &fs_info->qgroup_tree);
>   		__del_qgroup_rb(qgroup);
> +		spin_unlock(&fs_info->qgroup_lock);
>   		btrfs_sysfs_del_one_qgroup(fs_info, qgroup);
>   		kfree(qgroup);
> +		spin_lock(&fs_info->qgroup_lock);
>   	}
> +	spin_unlock(&fs_info->qgroup_lock);
> +
>   	/*
>   	 * We call btrfs_free_qgroup_config() when unmounting
>   	 * filesystem and disabling quota, so we set qgroup_ulist
> @@ -4036,12 +4044,16 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info)
>   	qgroup_rescan_zero_tracking(fs_info);
>   
>   	mutex_lock(&fs_info->qgroup_rescan_lock);
> -	fs_info->qgroup_rescan_running = true;
> -	btrfs_queue_work(fs_info->qgroup_rescan_workers,
> -			 &fs_info->qgroup_rescan_work);
> +	if (test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) {
> +		fs_info->qgroup_rescan_running = true;
> +		btrfs_queue_work(fs_info->qgroup_rescan_workers,
> +				 &fs_info->qgroup_rescan_work);
> +	} else {
> +		ret = -ENOTCONN;
> +	}
>   	mutex_unlock(&fs_info->qgroup_rescan_lock);
>   
> -	return 0;
> +	return ret;
>   }
>   
>   int btrfs_qgroup_wait_for_completion(struct btrfs_fs_info *fs_info,


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

* Re: [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist
  2025-06-30 13:07   ` [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
  2025-06-30 16:34     ` Boris Burkov
@ 2025-06-30 21:24     ` Qu Wenruo
  1 sibling, 0 replies; 14+ messages in thread
From: Qu Wenruo @ 2025-06-30 21:24 UTC (permalink / raw)
  To: fdmanana, linux-btrfs



在 2025/6/30 22:37, fdmanana@kernel.org 写道:
> From: Filipe Manana <fdmanana@suse.com>
> 
> It's not used anymore after commit 091344508249 ("btrfs: qgroup: use
> qgroup_iterator in qgroup_convert_meta()"), so remove it.
> 
> Signed-off-by: Filipe Manana <fdmanana@suse.com>

Thanks a lot for catching this.

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

Thanks,
Qu

> ---
>   fs/btrfs/disk-io.c |  1 -
>   fs/btrfs/fs.h      |  6 ------
>   fs/btrfs/qgroup.c  | 31 +------------------------------
>   3 files changed, 1 insertion(+), 37 deletions(-)
> 
> diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
> index f6fa951c6be9..ee4911452cfd 100644
> --- a/fs/btrfs/disk-io.c
> +++ b/fs/btrfs/disk-io.c
> @@ -1947,7 +1947,6 @@ static void btrfs_init_qgroup(struct btrfs_fs_info *fs_info)
>   	fs_info->qgroup_tree = RB_ROOT;
>   	INIT_LIST_HEAD(&fs_info->dirty_qgroups);
>   	fs_info->qgroup_seq = 1;
> -	fs_info->qgroup_ulist = NULL;
>   	fs_info->qgroup_rescan_running = false;
>   	fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT;
>   	mutex_init(&fs_info->qgroup_rescan_lock);
> diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
> index b239e4b8421c..a731c883687d 100644
> --- a/fs/btrfs/fs.h
> +++ b/fs/btrfs/fs.h
> @@ -740,12 +740,6 @@ struct btrfs_fs_info {
>   	struct rb_root qgroup_tree;
>   	spinlock_t qgroup_lock;
>   
> -	/*
> -	 * Used to avoid frequently calling ulist_alloc()/ulist_free()
> -	 * when doing qgroup accounting, it must be protected by qgroup_lock.
> -	 */
> -	struct ulist *qgroup_ulist;
> -
>   	/*
>   	 * Protect user change for quota operations. If a transaction is needed,
>   	 * it must be started before locking this lock.
> diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c
> index 8fa874ef80b3..98a53e6edb2c 100644
> --- a/fs/btrfs/qgroup.c
> +++ b/fs/btrfs/qgroup.c
> @@ -397,12 +397,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
>   	if (!fs_info->quota_root)
>   		return 0;
>   
> -	fs_info->qgroup_ulist = ulist_alloc(GFP_KERNEL);
> -	if (!fs_info->qgroup_ulist) {
> -		ret = -ENOMEM;
> -		goto out;
> -	}
> -
>   	path = btrfs_alloc_path();
>   	if (!path) {
>   		ret = -ENOMEM;
> @@ -587,8 +581,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info)
>   		if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN)
>   			ret = qgroup_rescan_init(fs_info, rescan_progress, 0);
>   	} else {
> -		ulist_free(fs_info->qgroup_ulist);
> -		fs_info->qgroup_ulist = NULL;
>   		fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN;
>   		btrfs_sysfs_del_qgroups(fs_info);
>   	}
> @@ -660,13 +652,6 @@ void btrfs_free_qgroup_config(struct btrfs_fs_info *fs_info)
>   	}
>   	spin_unlock(&fs_info->qgroup_lock);
>   
> -	/*
> -	 * We call btrfs_free_qgroup_config() when unmounting
> -	 * filesystem and disabling quota, so we set qgroup_ulist
> -	 * to be null here to avoid double free.
> -	 */
> -	ulist_free(fs_info->qgroup_ulist);
> -	fs_info->qgroup_ulist = NULL;
>   	btrfs_sysfs_del_qgroups(fs_info);
>   }
>   
> @@ -1012,7 +997,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>   	struct btrfs_qgroup *qgroup = NULL;
>   	struct btrfs_qgroup *prealloc = NULL;
>   	struct btrfs_trans_handle *trans = NULL;
> -	struct ulist *ulist = NULL;
>   	const bool simple = (quota_ctl_args->cmd == BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA);
>   	int ret = 0;
>   	int slot;
> @@ -1035,12 +1019,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>   	if (fs_info->quota_root)
>   		goto out;
>   
> -	ulist = ulist_alloc(GFP_KERNEL);
> -	if (!ulist) {
> -		ret = -ENOMEM;
> -		goto out;
> -	}
> -
>   	ret = btrfs_sysfs_add_qgroups(fs_info);
>   	if (ret < 0)
>   		goto out;
> @@ -1080,9 +1058,6 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>   	if (fs_info->quota_root)
>   		goto out;
>   
> -	fs_info->qgroup_ulist = ulist;
> -	ulist = NULL;
> -
>   	/*
>   	 * initially create the quota tree
>   	 */
> @@ -1281,17 +1256,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info,
>   	if (ret)
>   		btrfs_put_root(quota_root);
>   out:
> -	if (ret) {
> -		ulist_free(fs_info->qgroup_ulist);
> -		fs_info->qgroup_ulist = NULL;
> +	if (ret)
>   		btrfs_sysfs_del_qgroups(fs_info);
> -	}
>   	mutex_unlock(&fs_info->qgroup_ioctl_lock);
>   	if (ret && trans)
>   		btrfs_end_transaction(trans);
>   	else if (trans)
>   		ret = btrfs_end_transaction(trans);
> -	ulist_free(ulist);
>   	kfree(prealloc);
>   	return ret;
>   }


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

end of thread, other threads:[~2025-06-30 21:24 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-06-30 12:39 [PATCH 0/2] btrfs: qgroup race fix and cleanup fdmanana
2025-06-30 12:39 ` [PATCH 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
2025-06-30 12:39 ` [PATCH 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
2025-06-30 13:01 ` [PATCH v2 0/2] btrfs: qgroup race fix and cleanup fdmanana
2025-06-30 13:01   ` [PATCH v2 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
2025-06-30 13:01   ` [PATCH v2 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
2025-06-30 13:07 ` [PATCH v3 0/2] btrfs: qgroup race fix and cleanup fdmanana
2025-06-30 13:07   ` [PATCH v3 1/2] btrfs: qgroup: fix race between quota disable and quota rescan ioctl fdmanana
2025-06-30 16:32     ` Boris Burkov
2025-06-30 16:53       ` Filipe Manana
2025-06-30 21:22     ` Qu Wenruo
2025-06-30 13:07   ` [PATCH v3 2/2] btrfs: qgroup: remove no longer used fs_info->qgroup_ulist fdmanana
2025-06-30 16:34     ` Boris Burkov
2025-06-30 21:24     ` Qu Wenruo

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