WARNING in filename_mkdirat: end_dirop() unlocks the wrong inode and leaks the parent i_rwsem (v6.19+ regression, still in 7.2-rc7)
杨贵琨 <[email protected]>
| Newsgroups | dev.linux.lists.ntfs3,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <ABkAlQBXK0gwKdPbcemgOqpX.1.1787549121738.Hmail.2025244165@tju.edu.cn> |
Hello,
While fuzzing mainline with syzkaller I hit "WARNING in filename_mkdirat"
(syzbot previously reported the same title as [1]; that report was
auto-obsoleted on 2026/07/18 and the bug is still present). I have a
reliable C reproducer, and the WARN is only the first symptom: the same
execution permanently leaks a directory's write-locked i_rwsem, so every
later operation in that directory hangs (hung-task reports and, with
hung_task_panic=1, a panic).
The warning
-----------
DEBUG_RWSEMS_WARN_ON((rwsem_owner(sem) != current) &&
!rwsem_test_oflags(sem, RWSEM_NONSPINNABLE)):
count = 0x0, magic = 0xffff88804367ec18, owner = 0x0,
curr 0xffff88802163cc00, list not empty
WARNING: kernel/locking/rwsem.c:1412 at __up_write
CPU: 0 PID: 13812 Comm: syz.3.169
RIP: 0010:up_write+0x229/0x5c0
Call Trace:
inode_unlock include/linux/fs.h:1034
end_dirop fs/namei.c:2955
end_creating include/linux/namei.h:117
end_creating_path fs/namei.c:4995
filename_mkdirat+0x26f/0x5d0 fs/namei.c:5314
__x64_sys_mkdirat+0x95/0x100 fs/namei.c:5327
i.e. up_write() on an i_rwsem this task does not hold (in other
occurrences count was 0x1/0x3 — read-held by unrelated tasks).
Seconds later, other threads of the same reproducer hang forever waiting
for a write lock that nobody will ever release:
INFO: task racer:9471 blocked for more than 143 seconds.
task:racer state:D stack:27384
Call Trace:
__schedule+0x12e4/0x64e0
rwsem_down_write_slowpath+0x3a4/0x1280
down_write_nested+0x1d9/0x210
filename_create+0x1b1/0x400
filename_mkdirat+0xd8/0x5d0
__x64_sys_mkdirat+0x95/0x100
Reproducer
----------
An NTFS image crafted by syzkaller (2 MB; the MFT makes one directory
reachable through several directory entries) is mounted once, and N
threads then race mkdirat()/renameat2()/newfstatat() on the names
"file0", "file0/file0", "file1", "file7" inside the mount. On a cold
mount the WARNING triggers within seconds (I saw 7 s and 38 s in two
fresh boots, out of three attempts); afterwards the leaked lock hangs
further mkdirat() calls forever.
The image and a ready-to-run QEMU setup are available on request; the
racer itself is appended below. The original syzkaller program is a
single syz_mount_image$ntfs3 call followed by renameat2 + two mkdirat
calls, run with collide (two threads).
Analysis (hypothesis, not fully proven)
---------------------------------------
The dirop rework of v6.19 (4037d966f034 "VFS: introduce start_dirop()
and end_dirop()", fe497f0759e0 "VFS: change vfs_mkdir() to unlock on
failure", plus 88d5baf69082 "Change inode_operations.mkdir to return
struct dentry *") established the invariant that filename_mkdirat()
unlocks the parent through the *final* dentry:
end_dirop(de) { inode_unlock(de->d_parent->d_inode); dput(de); }
The locked inode, however, is the parent resolved at start_dirop()
time. I could not construct an imbalance from the generic code alone;
every path I traced (vfs_mkdir() error path end_creating(),
filename_create() fail label, the retry_estale/delegation retries) is
balanced for a filesystem like ntfs3 whose ->mkdir only returns NULL or
ERR_PTR.
What breaks the invariant is a dentry whose ->d_parent changes while
its creator is between start_dirop() and end_dirop(). d_splice_alias()
on a directory with a *connected* alias (possible on ntfs3 with a
crafted image where the same directory inode is reachable under two
paths; ntfs_lookup() calls d_splice_alias() directly) invokes
__d_unalias(), which locks only the alias's *old* parent (shared,
trylock) and s_vfs_rename_mutex — cf. lock_rename(), which locks *both*
parents — and then __d_move()s the alias under the new parent:
fs/dcache.c __d_unalias():
if (!mutex_trylock(&dentry->d_sb->s_vfs_rename_mutex))
goto out_err;
if (!inode_trylock_shared(alias->d_parent->d_inode))
goto out_err;
...
__d_move(alias, dentry, false);
A task that resolved a path through the old parent and holds that
parent's i_rwsem exclusive (start_dirop) while holding the moved
dentry will, at end_dirop(), unlock the *new* parent's inode (free or
read-held by others -> the WARN above) and leak the old parent's write
lock (-> the permanent hang).
Fix directions might include making __d_unalias() refuse to move an
alias whose d_parent is write-locked (or taking the new parent into
account the way lock_rename() does), or making the dirop helpers
unlock the inode they actually locked rather than the one the dentry
points at at unlock time.
Kernel: stock mainline 7.2.0-rc7-00232-gdcb68831eac7, syzkaller's
upstream KASAN config (KASAN inline, lockdep enabled).
[1] https://syzkaller.appspot.com/bug?extid=8e725133f14f47d38632
("WARNING in filename_mkdirat", occurrences 2026/02/10 - 2026/05/11,
auto-obsoleted 2026/07/18; first occurrence postdates the v6.19 dirop
rework)
---- racer.c ----
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <linux/loop.h>
static volatile int stop = 0;
static void *racer(void *arg)
{
long id = (long)arg;
while (!stop) {
mkdirat(AT_FDCWD, "./file0", 0700);
mkdirat(AT_FDCWD, "./file0/file0", 0700);
mkdirat(AT_FDCWD, "./file1", 0700);
mkdirat(AT_FDCWD, "./file0/file1", 0700);
switch (id % 4) {
case 0:
syscall(SYS_renameat2, AT_FDCWD, "./file0",
AT_FDCWD, "./file7", 0);
break;
case 1:
syscall(SYS_renameat2, AT_FDCWD, "./file7",
AT_FDCWD, "./file0", 0);
break;
case 2:
syscall(SYS_newfstatat, AT_FDCWD, "./file0/file0",
NULL, 0);
syscall(SYS_newfstatat, AT_FDCWD, "./file1/file1",
NULL, 0);
break;
case 3:
rmdir("./file0/file0");
rmdir("./file0/file1");
break;
}
}
return NULL;
}
int main(int argc, char **argv)
{
const char *img = argc > 1 ? argv[1] : "./ntfs3.img";
const char *mnt = argc > 2 ? argv[2] : "/tmp/m";
int n = argc > 3 ? atoi(argv[3]) : 16;
int secs = argc > 4 ? atoi(argv[4]) : 60;
char loopdev[64];
pthread_t t[128];
int cfd, idx, ld, fd;
mkdir(mnt, 0777);
cfd = open("/dev/loop-control", O_RDWR);
idx = cfd >= 0 ? ioctl(cfd, LOOP_CTL_GET_FREE) : 0;
if (cfd >= 0)
close(cfd);
snprintf(loopdev, sizeof(loopdev), "/dev/loop%d", idx < 0 ? 0 : idx);
ld = open(loopdev, O_RDWR);
fd = open(img, O_RDWR);
if (ld < 0 || fd < 0 || ioctl(ld, LOOP_SET_FD, fd) < 0) {
fprintf(stderr, "loop setup failed: %s\n", strerror(errno));
return 1;
}
close(fd);
/* Must close the loop fd before mount(2), else EBUSY. */
close(ld);
if (mount(loopdev, mnt, "ntfs3", 0, "") < 0) {
fprintf(stderr, "mount failed: %s\n", strerror(errno));
return 1;
}
if (chdir(mnt) < 0) {
perror("chdir");
return 1;
}
fprintf(stderr, "racing %d threads for %ds\n", n, secs);
for (long i = 0; i < n; i++)
pthread_create(&t[i], NULL, racer, (void *)i);
sleep(secs);
stop = 1;
for (int i = 0; i < n; i++)
pthread_join(t[i], NULL);
fprintf(stderr, "done\n");
return 0;
}
Run: ./racer ntfs3.img /tmp/m 16 60 (watch dmesg for
DEBUG_RWSEMS_WARN_ON, then for tasks blocked in down_write_nested)
Reported-by: syzbot
Found-by: syzkaller on linux-7.2.0-rc7-00232-gdcb68831eac7
1_mkdirat.zip
(application/x-zip-compressed, 368.4 KB) - not displayed