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: 2+ 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
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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox