From: Jun Wu <quark@lihdd.net>
To: miklos@szeredi.hu
Cc: linux-fsdevel@vger.kernel.org, Jun Wu <quark@meta.com>
Subject: [PATCH] fuse: invalidate readdir cache on epoch bump
Date: Tue, 31 Mar 2026 15:39:27 -0700 [thread overview]
Message-ID: <20260331223927.763741-1-quark@lihdd.net> (raw)
From: Jun Wu <quark@meta.com>
FUSE_NOTIFY_INC_EPOCH invalidates dentries, but does not invalidate cached
readdir results. A process with cwd inside a FUSE mount can therefore
observe stale readdir(".") output after an epoch bump.
Fix this by recording epoch in the readdir cache and checking it on reuse.
Add FUSE_EPOCH_READDIR init flag so userspace can detect this change.
Minimal reproducer:
- mount a tiny FUSE fs with an empty root directory
- on opendir, enable fi->cache_readdir and fi->keep_cache
- chdir into the mount and call readdir(".") to populate readdir cache
- make the FUSE server report one file in the root directory
- send only FUSE_NOTIFY_INC_EPOCH
- call readdir(".") again; before this change it stays stale, after this
change it sees the new file
Signed-off-by: Jun Wu <quark@meta.com>
---
fs/fuse/dev.c | 6 +++---
fs/fuse/fuse_i.h | 3 +++
fs/fuse/inode.c | 2 +-
fs/fuse/readdir.c | 5 ++++-
include/uapi/linux/fuse.h | 3 +++
5 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 0b0241f47170..595f90f44772 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -2039,9 +2039,9 @@ static int fuse_notify_resend(struct fuse_conn *fc)
}
/*
- * Increments the fuse connection epoch. This will result of dentries from
- * previous epochs to be invalidated. Additionally, if inval_wq is set, a work
- * queue is scheduled to trigger the invalidation.
+ * Increments the fuse connection epoch. This will cause dentries and
+ * readdir caches from previous epochs to be invalidated. Additionally,
+ * if inval_wq is set, a work queue is scheduled to trigger the invalidation.
*/
static int fuse_notify_inc_epoch(struct fuse_conn *fc)
{
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 7f16049387d1..9b479f597938 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -191,6 +191,9 @@ struct fuse_inode {
/* iversion of directory when cache was started */
u64 iversion;
+ /* epoch of fc when cache was started */
+ int epoch;
+
/* protects above fields */
spinlock_t lock;
} rdc;
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index c795abe47a4f..5685e696ee2f 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1506,7 +1506,7 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
FUSE_SECURITY_CTX | FUSE_CREATE_SUPP_GROUP |
FUSE_HAS_EXPIRE_ONLY | FUSE_DIRECT_IO_ALLOW_MMAP |
FUSE_NO_EXPORT_SUPPORT | FUSE_HAS_RESEND | FUSE_ALLOW_IDMAP |
- FUSE_REQUEST_TIMEOUT;
+ FUSE_REQUEST_TIMEOUT | FUSE_EPOCH_READDIR;
#ifdef CONFIG_FUSE_DAX
if (fm->fc->dax)
flags |= FUSE_MAP_ALIGNMENT;
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index c2aae2eef086..240b0e993780 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -438,6 +438,7 @@ static void fuse_rdc_reset(struct inode *inode)
fi->rdc.version++;
fi->rdc.size = 0;
fi->rdc.pos = 0;
+ fi->rdc.epoch = 0;
}
#define UNCACHED 1
@@ -479,6 +480,7 @@ static int fuse_readdir_cached(struct file *file, struct dir_context *ctx)
if (!ctx->pos && !fi->rdc.size) {
fi->rdc.mtime = inode_get_mtime(inode);
fi->rdc.iversion = inode_query_iversion(inode);
+ fi->rdc.epoch = atomic_read(&fc->epoch);
}
spin_unlock(&fi->rdc.lock);
return UNCACHED;
@@ -492,7 +494,8 @@ static int fuse_readdir_cached(struct file *file, struct dir_context *ctx)
struct timespec64 mtime = inode_get_mtime(inode);
if (inode_peek_iversion(inode) != fi->rdc.iversion ||
- !timespec64_equal(&fi->rdc.mtime, &mtime)) {
+ !timespec64_equal(&fi->rdc.mtime, &mtime) ||
+ fi->rdc.epoch != atomic_read(&fc->epoch)) {
fuse_rdc_reset(inode);
goto retry_locked;
}
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index c13e1f9a2f12..b0bd96ac1e84 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -240,6 +240,7 @@
* - add FUSE_COPY_FILE_RANGE_64
* - add struct fuse_copy_file_range_out
* - add FUSE_NOTIFY_PRUNE
+ * - add FUSE_EPOCH_READDIR
*/
#ifndef _LINUX_FUSE_H
@@ -448,6 +449,7 @@ struct fuse_file_lock {
* FUSE_OVER_IO_URING: Indicate that client supports io-uring
* FUSE_REQUEST_TIMEOUT: kernel supports timing out requests.
* init_out.request_timeout contains the timeout (in secs)
+ * FUSE_EPOCH_READDIR: epoch bump also invalidates readdir caches
*/
#define FUSE_ASYNC_READ (1 << 0)
#define FUSE_POSIX_LOCKS (1 << 1)
@@ -495,6 +497,7 @@ struct fuse_file_lock {
#define FUSE_ALLOW_IDMAP (1ULL << 40)
#define FUSE_OVER_IO_URING (1ULL << 41)
#define FUSE_REQUEST_TIMEOUT (1ULL << 42)
+#define FUSE_EPOCH_READDIR (1ULL << 43)
/**
* CUSE INIT request/reply flags
--
2.53.0
next reply other threads:[~2026-03-31 22:39 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-31 22:39 Jun Wu [this message]
2026-05-12 19:58 ` [PATCH] fuse: invalidate readdir cache on epoch bump Joanne Koong
2026-05-14 23:09 ` [PATCH v2] " Jun Wu
2026-05-15 0:14 ` Jun Wu
2026-05-15 8:53 ` Luis Henriques
2026-05-15 15:36 ` Joanne Koong
2026-05-15 16:26 ` Luis Henriques
[not found] ` <CAFfKmRW_Dqtb_DwAgK5Oq5kTAnGY6bmRpYP-Gw2+PVBtU4YVtQ@mail.gmail.com>
2026-05-15 20:25 ` Luis Henriques
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260331223927.763741-1-quark@lihdd.net \
--to=quark@lihdd.net \
--cc=linux-fsdevel@vger.kernel.org \
--cc=miklos@szeredi.hu \
--cc=quark@meta.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.