* [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