KCSAN: data-race in step_into_slowpath / vfs_unlink
From: Jianzhou Zhao
Date: Wed Mar 11 2026 - 04:03:58 EST
Subject: [BUG] fs: KCSAN: data-race in step_into_slowpath / vfs_unlink
Dear Maintainers,
We are writing to report a KCSAN-detected data race vulnerability within the VFS subsystem (`fs/namei.c` and `include/linux/dcache.h`). This bug was found by our custom fuzzing tool, RacePilot. The race concerns the `d_flags` field of a `dentry` structure: `dont_mount()` modifies the flags concurrently with lockless RCU path walks checking `d_is_symlink()` inside `step_into_slowpath()`. We observed this bug on the Linux kernel version 6.18.0-08691-g2061f18ad76e-dirty.
Call Trace & Context
==================================================================
BUG: KCSAN: data-race in step_into_slowpath / vfs_unlink
write to 0xffff8880134699c0 of 4 bytes by task 3611 on cpu 1:
dont_mount home/kfuzz/linux/include/linux/dcache.h:391 [inline]
vfs_unlink+0x37d/0x6e0 home/kfuzz/linux/fs/namei.c:5392
do_unlinkat+0x301/0x4e0 home/kfuzz/linux/fs/namei.c:5460
__do_sys_unlink home/kfuzz/linux/fs/namei.c:5495 [inline]
...
read to 0xffff8880134699c0 of 4 bytes by task 3015 on cpu 0:
__d_entry_type home/kfuzz/linux/include/linux/dcache.h:425 [inline]
d_is_symlink home/kfuzz/linux/include/linux/dcache.h:455 [inline]
step_into_slowpath+0x11d/0x910 home/kfuzz/linux/fs/namei.c:2068
step_into home/kfuzz/linux/fs/namei.c:2119 [inline]
walk_component home/kfuzz/linux/fs/namei.c:2258 [inline]
lookup_last home/kfuzz/linux/fs/namei.c:2753 [inline]
path_lookupat+0x422/0x740 home/kfuzz/linux/fs/namei.c:2777
filename_lookup+0x1d3/0x3f0 home/kfuzz/linux/fs/namei.c:2806
do_readlinkat.part.0+0x5f/0x250 home/kfuzz/linux/fs/stat.c:594
...
value changed: 0x00300080 -> 0x00004080
Reported by Kernel Concurrency Sanitizer on:
CPU: 0 UID: 0 PID: 3015 Comm: systemd-udevd Not tainted 6.18.0-08691-g2061f18ad76e-dirty #42 PREEMPT(voluntary)
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
==================================================================
Execution Flow & Code Context
The race occurs when a process is unlinking a file (`vfs_unlink`). As part of removing the dentry, VFS utilizes `dont_mount` to guarantee the dentry is flagged as `DCACHE_CANT_MOUNT`, preventing further interactions spanning mount layers. This operation applies a spinlock around the mutation:
```c
// include/linux/dcache.h
static inline void dont_mount(struct dentry *dentry)
{
spin_lock(&dentry->d_lock);
dentry->d_flags |= DCACHE_CANT_MOUNT; // <-- Modifies d_flags
spin_unlock(&dentry->d_lock);
}
```
However, another concurrent process resolving paths (e.g., executing `readlinkat`) may traverse through `step_into()` into `step_into_slowpath()`. The slowpath evaluates whether to follow symbolic links using `d_is_symlink()`, which executes a lockless read on `dentry->d_flags`:
```c
// include/linux/dcache.h
static inline unsigned __d_entry_type(const struct dentry *dentry)
{
return dentry->d_flags & DCACHE_ENTRY_TYPE; // <-- Plain concurrent read
}
static inline bool d_is_symlink(const struct dentry *dentry)
{
return __d_entry_type(dentry) == DCACHE_SYMLINK_TYPE;
}
```
Root Cause Analysis
A KCSAN data race arises because one thread configures the internal `d_flags` synchronously within a spinlock but without explicit compiler bounds (`WRITE_ONCE`), while a second thread concurrently navigates the paths. `__d_entry_type` evaluates `dentry->d_flags` via a plain read (`& DCACHE_ENTRY_TYPE`). Because path lookups are largely optimized locklessly (RCU walks), `d_flags` bits can fluctuate concurrently. The writer changes `DCACHE_CANT_MOUNT` bits which inadvertently triggers standard diagnostics against the masking read of the entry type.
Unfortunately, we were unable to generate a reproducer for this bug.
Potential Impact
This data race is functionally benign. The type encoding bits `DCACHE_ENTRY_TYPE` are initialized when the dentry is instantiated and they do not shift dynamically for a specific live dentry pointer instance. The concurrent mutation only manipulates disjoint bit ranges (like `DCACHE_CANT_MOUNT`). However, the lack of annotations can trigger compiler tearing optimisations on architectures with distinct load/store granularities or simply pollute logs with false positive alarms, obscuring valid concurrency issues.
Proposed Fix
To accurately reflect the RCU memory model design that allows lockless reads of `d_flags` bits against asynchronous modifiers acting on orthogonal masks, we should decorate the read access in `__d_entry_type` with the `data_race()` marker.
```diff
--- a/include/linux/dcache.h
+++ b/include/linux/dcache.h
@@ -426,7 +426,7 @@ static inline bool d_mountpoint(const struct dentry *dentry)
*/
static inline unsigned __d_entry_type(const struct dentry *dentry)
{
- return dentry->d_flags & DCACHE_ENTRY_TYPE;
+ return data_race(dentry->d_flags) & DCACHE_ENTRY_TYPE;
}
static inline bool d_is_miss(const struct dentry *dentry)
```
We would be highly honored if this could be of any help.
Best regards,
RacePilot Team