From cb8d2bdcb8241b66ca4ac4868f20e12cd6881ebc Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Wed, 14 Jan 2026 15:53:38 +0100 Subject: [PATCH 1/6] fuse: fix race when disposing stale dentries In fuse_dentry_tree_work() just before d_dispose_if_unused() the dentry could get evicted, resulting in UAF. Move unlocking dentry_hash[i].lock to after the dispose. To do this, fuse_dentry_tree_del_node() needs to be moved from fuse_dentry_prune() to fuse_dentry_release() to prevent an ABBA deadlock. The lock ordering becomes: -> dentry_bucket.lock -> dentry.d_lock Reported-by: Al Viro Closes: https://lore.kernel.org/all/20251206014242.GO1712166@ZenIV/ Fixes: ab84ad597386 ("fuse: new work queue to periodically invalidate expired dentries") Signed-off-by: Miklos Szeredi Link: https://patch.msgid.link/20260114145344.468856-2-mszeredi@redhat.com Signed-off-by: Christian Brauner --- fs/fuse/dir.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index dbb55bad5476..ea90dd682bc3 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -172,8 +172,8 @@ static void fuse_dentry_tree_work(struct work_struct *work) if (time_after64(get_jiffies_64(), fd->time)) { rb_erase(&fd->node, &dentry_hash[i].tree); RB_CLEAR_NODE(&fd->node); - spin_unlock(&dentry_hash[i].lock); d_dispose_if_unused(fd->dentry, &dispose); + spin_unlock(&dentry_hash[i].lock); cond_resched(); spin_lock(&dentry_hash[i].lock); } else @@ -479,18 +479,12 @@ static int fuse_dentry_init(struct dentry *dentry) return 0; } -static void fuse_dentry_prune(struct dentry *dentry) +static void fuse_dentry_release(struct dentry *dentry) { struct fuse_dentry *fd = dentry->d_fsdata; if (!RB_EMPTY_NODE(&fd->node)) fuse_dentry_tree_del_node(dentry); -} - -static void fuse_dentry_release(struct dentry *dentry) -{ - struct fuse_dentry *fd = dentry->d_fsdata; - kfree_rcu(fd, rcu); } @@ -527,7 +521,6 @@ const struct dentry_operations fuse_dentry_operations = { .d_revalidate = fuse_dentry_revalidate, .d_delete = fuse_dentry_delete, .d_init = fuse_dentry_init, - .d_prune = fuse_dentry_prune, .d_release = fuse_dentry_release, .d_automount = fuse_dentry_automount, }; From 1e2c1af1beb395841743e240a59ab37edc9a7d33 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Wed, 14 Jan 2026 15:53:39 +0100 Subject: [PATCH 2/6] fuse: make sure dentry is evicted if stale d_dispose_if_unused() may find the dentry with a positive refcount, in which case it won't be put on the dispose list even though it has already timed out. "Reinstall" the d_delete() callback, which was optimized out in fuse_dentry_settime(). This will result in the dentry being evicted as soon as the refcount hits zero. Fixes: ab84ad597386 ("fuse: new work queue to periodically invalidate expired dentries") Signed-off-by: Miklos Szeredi Link: https://patch.msgid.link/20260114145344.468856-3-mszeredi@redhat.com Signed-off-by: Christian Brauner --- fs/fuse/dir.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index ea90dd682bc3..c9922af79dfa 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -172,6 +172,10 @@ static void fuse_dentry_tree_work(struct work_struct *work) if (time_after64(get_jiffies_64(), fd->time)) { rb_erase(&fd->node, &dentry_hash[i].tree); RB_CLEAR_NODE(&fd->node); + spin_lock(&fd->dentry->d_lock); + /* If dentry is still referenced, let next dput release it */ + fd->dentry->d_flags |= DCACHE_OP_DELETE; + spin_unlock(&fd->dentry->d_lock); d_dispose_if_unused(fd->dentry, &dispose); spin_unlock(&dentry_hash[i].lock); cond_resched(); From 09f7a43ae501541030f42670351032f3c8bfa06e Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Wed, 14 Jan 2026 15:53:40 +0100 Subject: [PATCH 3/6] fuse: add need_resched() before unlocking bucket In fuse_dentry_tree_work() no need to unlock/lock dentry_hash[i].lock on each iteration. Suggested-by: Al Viro Signed-off-by: Miklos Szeredi Link: https://patch.msgid.link/20260114145344.468856-4-mszeredi@redhat.com Signed-off-by: Christian Brauner --- fs/fuse/dir.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index c9922af79dfa..93bde5b8c181 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -177,9 +177,11 @@ static void fuse_dentry_tree_work(struct work_struct *work) fd->dentry->d_flags |= DCACHE_OP_DELETE; spin_unlock(&fd->dentry->d_lock); d_dispose_if_unused(fd->dentry, &dispose); - spin_unlock(&dentry_hash[i].lock); - cond_resched(); - spin_lock(&dentry_hash[i].lock); + if (need_resched()) { + spin_unlock(&dentry_hash[i].lock); + cond_resched(); + spin_lock(&dentry_hash[i].lock); + } } else break; node = rb_first(&dentry_hash[i].tree); From 3926746b553455faaff2387b9a617c98d936980d Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Wed, 14 Jan 2026 15:53:41 +0100 Subject: [PATCH 4/6] fuse: clean up fuse_dentry_tree_work() - Change time_after64() time_before64(), since the latter is exclusively used in this file to compare dentry/inode timeout with current time. - Move the break statement from the else branch to the if branch, reducing indentation. Signed-off-by: Miklos Szeredi Link: https://patch.msgid.link/20260114145344.468856-5-mszeredi@redhat.com Signed-off-by: Christian Brauner --- fs/fuse/dir.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 93bde5b8c181..def2b2e1f96b 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -169,21 +169,21 @@ static void fuse_dentry_tree_work(struct work_struct *work) node = rb_first(&dentry_hash[i].tree); while (node) { fd = rb_entry(node, struct fuse_dentry, node); - if (time_after64(get_jiffies_64(), fd->time)) { - rb_erase(&fd->node, &dentry_hash[i].tree); - RB_CLEAR_NODE(&fd->node); - spin_lock(&fd->dentry->d_lock); - /* If dentry is still referenced, let next dput release it */ - fd->dentry->d_flags |= DCACHE_OP_DELETE; - spin_unlock(&fd->dentry->d_lock); - d_dispose_if_unused(fd->dentry, &dispose); - if (need_resched()) { - spin_unlock(&dentry_hash[i].lock); - cond_resched(); - spin_lock(&dentry_hash[i].lock); - } - } else + if (!time_before64(fd->time, get_jiffies_64())) break; + + rb_erase(&fd->node, &dentry_hash[i].tree); + RB_CLEAR_NODE(&fd->node); + spin_lock(&fd->dentry->d_lock); + /* If dentry is still referenced, let next dput release it */ + fd->dentry->d_flags |= DCACHE_OP_DELETE; + spin_unlock(&fd->dentry->d_lock); + d_dispose_if_unused(fd->dentry, &dispose); + if (need_resched()) { + spin_unlock(&dentry_hash[i].lock); + cond_resched(); + spin_lock(&dentry_hash[i].lock); + } node = rb_first(&dentry_hash[i].tree); } spin_unlock(&dentry_hash[i].lock); From fa79401a9c35fe2ba590599d7617789761f574a9 Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Wed, 14 Jan 2026 15:53:42 +0100 Subject: [PATCH 5/6] fuse: shrink once after all buckets have been scanned In fuse_dentry_tree_work() move the shrink_dentry_list() out from the loop. Suggested-by: Al Viro Signed-off-by: Miklos Szeredi Link: https://patch.msgid.link/20260114145344.468856-6-mszeredi@redhat.com Signed-off-by: Christian Brauner --- fs/fuse/dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index def2b2e1f96b..3927cb069236 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -187,8 +187,8 @@ static void fuse_dentry_tree_work(struct work_struct *work) node = rb_first(&dentry_hash[i].tree); } spin_unlock(&dentry_hash[i].lock); - shrink_dentry_list(&dispose); } + shrink_dentry_list(&dispose); if (inval_wq) schedule_delayed_work(&dentry_tree_work, From 79d11311f64d3e9fbc20ac95b7df6f917221329f Mon Sep 17 00:00:00 2001 From: Miklos Szeredi Date: Wed, 14 Jan 2026 15:53:43 +0100 Subject: [PATCH 6/6] vfs: document d_dispose_if_unused() Add a warning about the danger of using this function without proper locking preventing eviction. Signed-off-by: Miklos Szeredi Link: https://patch.msgid.link/20260114145344.468856-7-mszeredi@redhat.com Signed-off-by: Christian Brauner --- fs/dcache.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/dcache.c b/fs/dcache.c index dc2fff4811d1..66dd1bb830d1 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -1104,6 +1104,16 @@ struct dentry *d_find_alias_rcu(struct inode *inode) return de; } +/** + * d_dispose_if_unused - move unreferenced dentries to shrink list + * @dentry: dentry in question + * @dispose: head of shrink list + * + * If dentry has no external references, move it to shrink list. + * + * NOTE!!! The caller is responsible for preventing eviction of the dentry by + * holding dentry->d_inode->i_lock or equivalent. + */ void d_dispose_if_unused(struct dentry *dentry, struct list_head *dispose) { spin_lock(&dentry->d_lock);