PROBLEM: FUSE_NOTIFY_INVAL_ENTRY leaves stale negative dentry after c9ba789dad15
Артем Лабазов <[email protected]>
| Newsgroups | dev.linux.lists.regressions,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <CAFbF8N7++zopZuEcsKRxBV_sgOGCbzCY0hOyMw1SiGAtuzGhyQ@mail.gmail.com> |
Hi,
I am reporting a FUSE/VFS regression introduced by:
c9ba789dad15ba65662bba17595c0aeaa0cfcf1c
VFS: introduce start_creating_noperm() and start_removing_noperm()
After a FUSE filesystem returns a cached negative lookup for a name, then
later makes that name visible and sends FUSE_NOTIFY_INVAL_ENTRY for it,
readdir() can list the entry while stat()/open() on the same path still
returns ENOENT.
Bisect result:
good: v6.17
bad: v6.19
first bad commit:
c9ba789dad15ba65662bba17595c0aeaa0cfcf1c
VFS: introduce start_creating_noperm() and start_removing_noperm()
Adjacent commits:
bd6ede8a06e89ca5a94a8b51cea792705d1b8ca2 GOOD
c9ba789dad15ba65662bba17595c0aeaa0cfcf1c BAD
The bad commit changes fs/fuse/dir.c:fuse_reverse_inval_entry() to use
start_removing_noperm().
I attached a minimal libfuse3 reproducer. It does:
1. lookup("config") returns ino=0 with non-zero entry_timeout, causing
the kernel to cache a negative dentry.
2. The filesystem makes "config" visible.
3. It calls fuse_lowlevel_notify_inval_entry(root, "config").
4. The test checks whether readdir lists "config" and stat("config")
succeeds.
Build:
cc -O2 -Wall -Wextra repro.c -o repro $(pkg-config --cflags --libs
fuse3) -lpthread
Run:
mkdir -p mnt
./repro ./mnt
Observed results:
Linux 6.17.0:
notify_inval_entry rc=0
listed=1 stat=OK
GOOD
exit code 0
Linux 6.19.0:
notify_inval_entry rc=-2
listed=1 stat=No such file or directory
BAD
exit code 1
Linux 7.1.0-rc5-00426-g670b77dfebe7:
notify_inval_entry rc=-2
listed=1 stat=No such file or directory
BAD
exit code 1
#regzbot introduced: c9ba789dad15ba65662bba17595c0aeaa0cfcf1c
Assisted-by: OpenCode:openai/gpt-5.5
Thanks,
Artem
repro.c
(application/octet-stream, 4.3 KB)
// SPDX-License-Identifier: GPL-2.0
// Reproducer for c9ba789dad15: cached negative FUSE dentry survives
// FUSE_NOTIFY_INVAL_ENTRY. Exit: 0=good, 1=bad, 2=error.
// Build: cc -O2 -Wall -Wextra repro.c -o repro $(pkg-config --cflags --libs fuse3) -lpthread
#define FUSE_USE_VERSION 34
#include <dirent.h>
#include <errno.h>
#include <fuse_lowlevel.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
enum { ROOT = 1, CONFIG = 2 };
static struct fuse_session *se;
static volatile int visible;
static void attr(struct stat *st, fuse_ino_t ino)
{
memset(st, 0, sizeof(*st));
st->st_ino = ino;
st->st_mode = S_IFDIR | 0755;
st->st_nlink = 2;
st->st_uid = getuid();
st->st_gid = getgid();
}
static void entry(fuse_req_t req, fuse_ino_t ino, double t)
{
struct fuse_entry_param e = {.ino = ino, .generation = 1, .entry_timeout = t,
.attr_timeout = t};
if (ino)
attr(&e.attr, ino);
fuse_reply_entry(req, &e);
}
static void lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
{
if (parent != ROOT) {
fuse_reply_err(req, ENOENT);
return;
}
if (!strcmp(name, "config"))
return entry(req, visible ? CONFIG : 0, 600.0);
fuse_reply_err(req, ENOENT);
}
static void getattr(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info *fi)
{
(void)fi;
if (ino == ROOT || ino == CONFIG) {
struct stat st;
attr(&st, ino);
fuse_reply_attr(req, &st, 600.0);
return;
}
fuse_reply_err(req, ENOENT);
}
static void add(fuse_req_t req, char **p, size_t *sz, const char *name,
fuse_ino_t ino)
{
struct stat st;
attr(&st, ino);
size_t old = *sz;
*sz += fuse_add_direntry(req, NULL, 0, name, NULL, 0);
*p = realloc(*p, *sz);
fuse_add_direntry(req, *p + old, *sz - old, name, &st, *sz);
}
static void readdir_cb(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
struct fuse_file_info *fi)
{
(void)fi;
if (ino != ROOT) {
fuse_reply_err(req, ENOTDIR);
return;
}
char *buf = NULL;
size_t sz = 0;
add(req, &buf, &sz, ".", ROOT);
add(req, &buf, &sz, "..", ROOT);
if (visible)
add(req, &buf, &sz, "config", CONFIG);
if (off < (off_t)sz)
fuse_reply_buf(req, buf + off, sz - off < size ? sz - off : size);
else
fuse_reply_buf(req, NULL, 0);
free(buf);
}
static void opendir_cb(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi)
{
ino == ROOT ? fuse_reply_open(req, fi) : fuse_reply_err(req, ENOTDIR);
}
static const struct fuse_lowlevel_ops ops = {
.lookup = lookup,
.getattr = getattr,
.opendir = opendir_cb,
.readdir = readdir_cb,
};
static void *loop(void *arg) {
fuse_session_loop(arg);
return NULL;
}
static int listed(const char *mnt)
{
DIR *d = opendir(mnt);
if (!d)
return 0;
int seen = 0;
struct dirent *de;
while ((de = readdir(d)))
seen |= !strcmp(de->d_name, "config");
closedir(d);
return seen;
}
static int user(const char *mnt)
{
char c[4096];
struct stat st;
snprintf(c, sizeof(c), "%s/config", mnt);
if (stat(c, &st) == 0 || errno != ENOENT)
return fprintf(stderr, "expected initial ENOENT\n"), 2;
if (listed(mnt))
return fprintf(stderr, "config listed too early\n"), 2;
visible = 1;
int rc =
fuse_lowlevel_notify_inval_entry(se, ROOT, "config", strlen("config"));
fprintf(stderr, "notify_inval_entry rc=%d\n", rc);
usleep(100000); // let notify reach the kernel
errno = 0;
int seen = listed(mnt), ok = !stat(c, &st), err = errno;
fprintf(stderr, "listed=%d stat=%s\n", seen, ok ? "OK" : strerror(err));
if (seen && !ok && err == ENOENT)
return puts("BAD"), 1;
return puts("GOOD"), 0;
}
int main(int argc, char **argv)
{
if (argc != 2)
return fprintf(stderr, "usage: %s <mountpoint>\n", argv[0]), 2;
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
fuse_opt_add_arg(&args, argv[0]);
fuse_opt_add_arg(&args, "-o");
fuse_opt_add_arg(&args, "auto_unmount,fsname=invalrepro");
se = fuse_session_new(&args, &ops, sizeof(ops), NULL);
if (!se || fuse_session_mount(se, argv[1]))
return perror("fuse mount"), 2;
pthread_t lt;
pthread_create(<, NULL, loop, se);
int ret = user(argv[1]);
fuse_session_exit(se);
fuse_session_unmount(se);
pthread_cancel(lt);
pthread_join(lt, NULL);
fuse_session_destroy(se);
return ret;
}