fuse: invalidate readdir cache on epoch bump

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.

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

Fixes: 2396356a94 ("fuse: add more control over cache invalidation behaviour")
Signed-off-by: Jun Wu <quark@meta.com>
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Reviewed-by: Luis Henriques <luis@igalia.com>
Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>
This commit is contained in:
Jun Wu 2026-05-14 17:14:12 -07:00 committed by Miklos Szeredi
parent 6af3330ec5
commit 0fa8346099
3 changed files with 12 additions and 4 deletions

View File

@ -190,6 +190,11 @@ struct fuse_inode {
*/
struct timespec64 mtime;
/**
* @epoch: epoch of fc when cache was started
*/
int epoch;
/**
* @iversion: iversion of directory when cache was
* started

View File

@ -348,9 +348,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)
{

View File

@ -489,6 +489,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
@ -530,6 +531,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;
@ -543,7 +545,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;
}