KCSAN: data-race in __d_drop / retain_dentry

From: Jianzhou Zhao

Date: Wed Mar 11 2026 - 04:04:05 EST




Subject: [BUG] fs: KCSAN: data-race in __d_drop / retain_dentry

Dear Maintainers,

We are writing to report a KCSAN-detected data race vulnerability within the VFS dcache subsystem (`fs/dcache.c` and `include/linux/list_bl.h`). This bug was found by our custom fuzzing tool, RacePilot. The race occurs when `__d_drop()` unhashes a dentry and maliciously clears `dentry->d_hash.pprev` concurrently against a lockless RCU reader executing `retain_dentry()` which calls `hlist_bl_unhashed()` to inspect `pprev`. We observed this bug on the Linux kernel version 6.18.0-08691-g2061f18ad76e-dirty.

Call Trace & Context
==================================================================
BUG: KCSAN: data-race in __d_drop / retain_dentry

write to 0xffff888013db3010 of 8 bytes by task 3021 on cpu 1:
__d_drop fs/dcache.c:607 [inline]
__d_drop+0x8c/0xd0 fs/dcache.c:601
__dentry_kill+0xbd/0x3e0 fs/dcache.c:715
dput fs/dcache.c:977 [inline]
dput+0x123/0x220 fs/dcache.c:964
handle_mounts fs/namei.c:1722 [inline]
step_into_slowpath+0x688/0x960 fs/namei.c:2081
...
__x64_sys_readlinkat+0x6f/0xa0 fs/stat.c:624

read to 0xffff888013db3010 of 8 bytes by task 4584 on cpu 0:
hlist_bl_unhashed include/linux/list_bl.h:57 [inline]
d_unhashed include/linux/dcache.h:374 [inline]
retain_dentry+0x79/0x320 fs/dcache.c:809
fast_dput fs/dcache.c:913 [inline]
dput+0x97/0x220 fs/dcache.c:971
end_dirop fs/namei.c:2939 [inline]
do_unlinkat+0x332/0x540 fs/namei.c:5483
...
__x64_sys_unlink+0x7d/0xa0 fs/namei.c:5513

value changed: 0xffff88807dbb55f8 -> 0x0000000000000000

Reported by Kernel Concurrency Sanitizer on:
CPU: 0 UID: 0 PID: 4584 Comm: systemd-udevd Not tainted 6.18.0-08691-g2061f18ad76e-dirty #50 PREEMPT(voluntary)
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
==================================================================

Execution Flow & Code Context
When a dentry receives its final decrement during a path operation (e.g., inside `dput`), its lifecycle might traverse `__dentry_kill()` leading to `__d_drop()`. Here, VFS manually eradicates the dentry from the hash list by assigning `NULL` to the internal double-linked list pointer tracker `pprev`:
```c
// fs/dcache.c
void __d_drop(struct dentry *dentry)
{
if (!d_unhashed(dentry)) {
___d_drop(dentry);
...
dentry->d_hash.pprev = NULL; // <-- Plain concurrent write
write_seqcount_invalidate(&dentry->d_seq);
}
}
```

Simultaneously, another thread undergoing an optimistic lockless `dput` (e.g., `fast_dput` resolving symbolic links or unlinking) probes whether the unreferenced dentry should be retained via `retain_dentry()`. `retain_dentry` verifies `d_unhashed()` relying on `hlist_bl_unhashed()`:
```c
// include/linux/list_bl.h
static inline bool hlist_bl_unhashed(const struct hlist_bl_node *h)
{
return !h->pprev; // <-- Plain concurrent read
}
```

Root Cause Analysis
A KCSAN data race materializes because `__d_drop` resets `dentry->d_hash.pprev` using standard assignments, while `retain_dentry` inspects `pprev` outside the dentry's lock protection. The Linux kernel explicitly permits optimistic RCU verification of `d_unhashed` within `retain_dentry` given that transient inaccuracies gracefully fall through to a strictly-locked verification slow-path (`locked: if (dentry->d_lockref.count || retain_dentry(dentry, true))`). This lockless access is an architectural optimization; however, reading and writing the 8-byte `pprev` pointer without safe `READ_ONCE()` and `WRITE_ONCE()` directives violates the Memory Model constraints under KCSAN and exposes potential tearing risks across compiler transformations.
Unfortunately, we were unable to generate a reproducer for this bug.

Potential Impact
This data race technically threatens architectures or compilers prone to load/store tearing resulting in a garbled non-NULL `pprev` pointer value snapshot. Nonetheless, functionally, the impact is minuscule: since `retain_dentry` simply falls back to the properly synchronized path if it wrongly presumes the dentry is not unhashed, there is no direct vulnerability. The data race nevertheless generates false-positive diagnostic spam obscuring more pertinent memory corruption flaws.

Proposed Fix
To codify the lockless RCU access paradigm for `hlist_bl_unhashed` reliably across VFS and properly placate KCSAN, we apply `WRITE_ONCE` to the hash decoupling and `READ_ONCE` for the state examination.

```diff
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -604,7 +604,7 @@ void __d_drop(struct dentry *dentry)
___d_drop(dentry);
__sanitizer_obj_cov_trace_pc(268);
__sanitizer_obj_cov_trace_pc(436);
- dentry->d_hash.pprev = NULL;
+ WRITE_ONCE(dentry->d_hash.pprev, NULL);
write_seqcount_invalidate(&dentry->d_seq);
}
}
--- a/include/linux/list_bl.h
+++ b/include/linux/list_bl.h
@@ -54,7 +54,7 @@ static inline bool hlist_bl_unhashed(const struct hlist_bl_node *h)
__sanitizer_obj_cov_trace_pc(386);
__sanitizer_obj_cov_trace_pc(1106);
__sanitizer_obj_cov_trace_pc(1107);
- return !h->pprev;
+ return !READ_ONCE(h->pprev);
}

static inline struct hlist_bl_node *hlist_bl_first(struct hlist_bl_head *h)
```

We would be highly honored if this could be of any help.

Best regards,
RacePilot Team