Re: end_dirop() can unlock the i_rwsem of a re-parented dentry on PREEMPT_RT (DEBUG_LOCKS_WARN_ON in filename_symlinkat)

From: NeilBrown

Date: Mon Nov 09 2026 - 04:06:50 EST


On Fri, 06 Nov 2026, Shuai Liu wrote:
> Hi Al, Neil,
>
> I've been digging into this syzbot report and would like to share an
> analysis and ask for your insight on where the invariant breaks.
>
> syzbot dashboard: https://syzkaller.appspot.com/bug?extid=2ed46b6b748df855347f
> Title: WARNING in filename_symlinkat (PREEMPT_RT, still open)
> First reported: February 2026; syzbot's own retests are flaky
> (10 of the last 15 retests report "no crash"), and earlier fix
> attempts posted to the discussion thread did not resolve it.

Interesting - thanks for looking into this.

>
> 1. What the warning means
> -------------------------
>
> The crash is a DEBUG_LOCKS_WARN_ON(rt_mutex_owner(lock) != current)
> fired from inode_unlock() called by end_dirop(), via:
>
> filename_symlinkat
> -> end_creating_path() (fs/namei.c)
> -> end_creating() (include/linux/namei.h)
> -> end_dirop() (fs/namei.c)
> -> inode_unlock(de->d_parent->d_inode)
>
> end_dirop() re-derives the directory to unlock from the *current*
> value of de->d_parent:
>
> struct inode *dir = de->d_parent->d_inode;
> ...
> inode_unlock(dir);
>
> while start_dirop() locked the parent's i_rwsem *before* performing
> lookup_one_qstr_excl(). The warning therefore means that between
> start_dirop() and end_dirop(), the returned dentry was re-parented:
> we took the lock on inode A but are now releasing inode B. On
> !PREEMPT_RT the same mismatch presumably happens silently.

That is certainly one thing that the warning could mean.
It could also mean that the ->d_parent point was corrupted.
Or that the ->d_inode pointer in the parent was corrupted.
Or that the i_rwsem in the parent inode was corrupted.
Or that the i_rwsem was unlocked or re-initialised since it was locked.

None of these seem likely, but we shouldn't exclude the possibility.

The original version of this syzbot report (e.g. 2nd Feb) was caused
by i_rwsem being re-initialised (aka corrupted).
It was fixed by
Commit: 2ff7cf7e0640 ("gfs2: Call unlock_new_inode before d_instantiate")

The corruption happened because unlock_inode_new() reinitialised the
i_rwsem *after* it has been exposed in the dcache via a d_instantiate()
call.

That bug is fixed in the more recent instances. But something is
causing a similar symptom.

It appears that syzkaller is exercising lots of different filesystems
and we don't know which one triggered the problem. It would be useful
if we could get a crashdump - is that possible?


>
> 2. What I checked and could not break
> -------------------------------------
>
> For de->d_parent to change, something must move `de` itself (d_move()
> family) while we hold it. I audited the plausible candidates:
>
> a) lookup_one_qstr_excl()/d_lookup(): lookups hash under the parent
> and never re-parent an existing dentry. No window.
>
> b) d_splice_alias_ops()/d_splice_alias() during a lookup of a
> directory, which is the classic way a dentry gets moved to
> another parent:
> - IS_ROOT alias branch: __d_move(new, dentry) under
> write_seqlock(&rename_lock) moves the *existing* alias into
> the freshly allocated lookup dentry's slot; the destination
> is a new dentry, not the dirop's held one.
> - !IS_ROOT branch: __d_unalias(dentry, new) takes
> s_vfs_rename_mutex and then inode_trylock_shared() on the
> alias's old parent, and bails out with -ESTALE if the
> trylock fails; the moving victim is the alias, not `de`.
>
> By static reading I could not construct an interleaving in which
> the dentry returned by start_dirop() gets its d_parent changed
> while the dirop's exclusive parent lock is held. Either I am
> missing a caller, or the mechanism is subtler than the alias
> paths -- hence this email.
>
> 3. Instrumentation result: ~48k iterations, zero hits
> ------------------------------------------------------
>
> To get runtime evidence I added two DEBUG probes to the tip of the
> paths in question (both compiled only under CONFIG_PREEMPT_RT):
>
> - end_dirop(): read rt_mutex_owner() of de->d_parent->d_inode and
> warn with full context (dentry/parent pointers, %pd names, inode
> numbers, d_flags, filesystem/superblock, comm, pid, owner comm)
> whenever owner != current -- i.e. log *before* the kernel's own
> debug check fires;
>
> - end_creating_path(): compare path->dentry (the parent whose lock
> start_creating_path() took) against dentry->d_parent at the end,
> to catch divergence even if the ownership warning never fires.
>
> I then looped syzbot's C reproducer (gfs2 with lock_nolock, mount /
> chdir / creat / open(".") / symlinkat+renameat2 racing threads) under
> QEMU/KVM with the syzbot-provided PREEMPT_RT config, across ~48,000
> reproducer iterations. Result: zero probe hits, zero false
> positives -- the probes print nothing in a completely healthy run,
> and the kernel-side check never fired either.
>
> So the window is real (syzbot hit it) but extremely narrow, and my
> repro cannot reach it locally.
>
> 4. Questions
> ------------
>
> Q1: Is end_dirop()'s contract deliberately "unlock whatever
> de->d_parent points to now", relying on an assumption that no
> path can re-parent the dentry while the dirop parent lock is
> held? Or would it be more robust for start_dirop() to
> remember the exact directory it locked (e.g. return the locked
> inode alongside the dentry) and have end_dirop() unlock that
> captured inode?

Yes, that contract is deliberate. start_dirop() finds a dentry and
locks it so that the parent *cannot* change.
So assuming that it hasn't changed is the same as assuming the it is
behaving correctly.

There is an exception in that an alternate dentry may be substituted
(returned from d_splice_alias()) but it must have the same parent.

>
> Q2: Which callers of d_move()/__d_move() do you see as plausible
> candidates for re-parenting a held create/remove dentry?
> Candidates I could not fully rule out by reading are callers
> outside the lookup path itself (rename on another mount of the
> same file, network/cluster filesystems revalidating and
> re-parenting dentries, overlayfs redirect/merge behavior, ...).

I cannot see any of them as plausible.


>
> Q3: If no re-parenting path exists, could the divergence instead
> originate at start time -- i.e. start_creating_path() locking
> one parent while the dentry handed to end_creating() was
> created under a different one? My end_creating_path() probe
> compares exactly that and never fired, which argues against
> it, but a second pair of eyes on that layer would help.

I strongly suspect a corruption of some sort.

NeilBrown


>
> Happy to share the instrumented kernel, probe diffs, and full boot
> logs. The reproducer is the one attached to the syzbot report, so
> anyone with a PREEMPT_RT build can try it directly.
>
> Thanks,
> Shuai Liu (刘帅)
>
>
>