Re: [BUG] configfs: slab-use-after-free in configfs_drop_dentry() on rmdir

From: Breno Leitao

Date: Wed May 27 2026 - 06:32:56 EST


On Tue, May 26, 2026 at 08:42:00PM +0000, Farhad Alemi wrote:
> Hello Andreas and the configfs team,
>
> I am reporting a configfs use-after-free found by syzkaller.
>
> Summary:
> A configfs config-item rmdir(2) can land in configfs_drop_dentry() with
> sd->s_dentry pointing at a dentry whose RCU grace period has not yet
> elapsed. The spin_lock(&dentry->d_lock) at fs/configfs/inode.c:209 then
> reads freed slab memory and KASAN reports a slab-use-after-free.

Thanks for the report. I was able to reproduce it on linus' upstream
tree.

I've came up with the following fix, would you mind trying it, please?


commit d78c04736ef0ede76093c780a188560b6baed3c3
Author: Breno Leitao <leitao@xxxxxxxxxx>
Date: Wed May 27 06:25:49 2026 -0400

configfs: fix UAF in configfs_drop_dentry() after failed attribute lookup

When configfs_lookup() matches an attribute it publishes the new dentry
into sd->s_dentry (and dentry->d_fsdata) before calling
configfs_create() to allocate the inode:

dentry->d_fsdata = configfs_get(sd);
sd->s_dentry = dentry;
spin_unlock(&configfs_dirent_lock);

inode = configfs_create(dentry, mode);
if (IS_ERR(inode)) {
configfs_put(sd);
return ERR_CAST(inode);
}

If configfs_create() fails (e.g. new_inode() returns NULL under memory
pressure or fault injection), the lookup returns an error and the
caller dputs the now-negative dentry, which goes through
__dentry_kill() and is freed via call_rcu().

Because the dentry never gained an inode it does not go through the
.d_iput op (configfs_d_iput), which is the only place that clears
sd->s_dentry. sd therefore keeps a stale pointer to the freed dentry.
A subsequent rmdir of the parent item walks the parent's s_children
list in detach_attrs() and configfs_drop_dentry() does
spin_lock(&sd->s_dentry->d_lock) on freed memory:

BUG: KASAN: slab-use-after-free in _raw_spin_lock+0xac/0x110
Read of size 1 at addr ffff00012bacd028 by task repro/2440
_raw_spin_lock+0xac/0x110
configfs_drop_dentry+0x48/0x158 [configfs]
detach_attrs.isra.0+0x18c/0x494 [configfs]
configfs_rmdir+0x450/0x71c [configfs]
vfs_rmdir+0x170/0x620
...
Freed by task 0:
__d_free+0x28/0x34
rcu_do_batch+0x37c/0x1bd0
...
Last potentially related work creation:
call_rcu+0x34/0x68
dentry_free+0xe8/0x3e0
__dentry_kill+0x404/0x604
dput+0x14/0x30
lookup_open.isra.0+0x6ac/0xc00
path_openat+0xd18/0x2588

Fix this by tearing down the sd<->dentry linkage in the
configfs_create() error path, under configfs_dirent_lock, mirroring
what configfs_d_iput() would have done for a positive dentry.

Reported-by: Farhad Alemi <farhad.alemi@xxxxxxxxxxxx>
Signed-off-by: Breno Leitao <leitao@xxxxxxxxxx>

diff --git a/fs/configfs/dir.c b/fs/configfs/dir.c
index 362b6ff9b908..68d857dcb2d9 100644
--- a/fs/configfs/dir.c
+++ b/fs/configfs/dir.c
@@ -486,6 +486,22 @@ static struct dentry * configfs_lookup(struct inode *dir,

inode = configfs_create(dentry, mode);
if (IS_ERR(inode)) {
+ /*
+ * configfs_create() failed (e.g. -ENOMEM
+ * from new_inode()). The dentry will be
+ * dput()ed by the caller and freed via RCU;
+ * because it never gained an inode,
+ * configfs_d_iput() will not run to clear
+ * sd->s_dentry. Drop the linkage here so a
+ * later detach_attrs() walking the parent's
+ * s_children list does not dereference a
+ * freed dentry in configfs_drop_dentry().
+ */
+ spin_lock(&configfs_dirent_lock);
+ if (sd->s_dentry == dentry)
+ sd->s_dentry = NULL;
+ dentry->d_fsdata = NULL;
+ spin_unlock(&configfs_dirent_lock);
configfs_put(sd);
return ERR_CAST(inode);
}