From ce4e92cb9eb218eb7053e8d90d3adcbb998faf32 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Mon, 19 Oct 2026 13:32:51 +0200 Subject: [PATCH 1/2] fuse: add FUSE_STATX_LIGHT for stat without recalling DLM grants Under DLM the server owns the size, blocks, mtime and ctime of a file another node writes through its writeback cache. To answer a statx exactly, it has to revoke or downgrade that node's write grant, forcing a flush and a later re-acquire. A plain ls -l, find or monitoring agent on one node can so keep breaking the grant of a writer on another. Add FUSE_STATX_LIGHT to fuse_statx_in.sx_flags, telling the server that the caller does not need cluster-coherent size, blocks and timestamps and that it may answer from its current values without recalling locks. It is only sent for a user stat() of a regular file without AT_STATX_FORCE_SYNC. Internal attribute updates (SEEK_END, O_APPEND, EOF checks, permission checks, setattr) keep sending full requests. A light reply may lag behind writes, so it does not update the inode: a stale size must not shrink i_size or the page cache, and i_time, inval_mask and FUSE_I_DLM_LOCKED stay as they were. It only fills the kstat, taking the larger size and the later mtime/ctime of the reply and the inode, so this node still sees its own unflushed writes. Signed-off-by: Horst Birthelmer --- fs/fuse/dir.c | 48 +++++++++++++++++++++++++++++++++++---- include/uapi/linux/fuse.h | 9 ++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 39d358a67d66a9..8cf2a4ab99fe52 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -1361,15 +1361,51 @@ static void fuse_attr_to_statx(struct fuse_attr *attr, struct fuse_statx *sx, ui sx->blksize = attr->blksize; } +/* + * A FUSE_STATX_LIGHT reply may lag behind writes the server has not seen, + * so it does not update the inode: a stale size would shrink i_size and + * the page cache. This node's own unflushed writes are still reported, + * by taking the larger size and the later stamps of the two. + */ +static void fuse_statx_light_merge(struct inode *inode, struct fuse_attr *attr, + struct fuse_statx *sx) +{ + loff_t size = i_size_read(inode); + struct timespec64 cached, reported; + + if ((sx->mask & STATX_SIZE) && size > (loff_t) attr->size) + attr->size = size; + + if (sx->mask & STATX_MTIME) { + cached = inode_get_mtime(inode); + reported.tv_sec = attr->mtime; + reported.tv_nsec = min_t(u32, attr->mtimensec, NSEC_PER_SEC - 1); + if (timespec64_compare(&cached, &reported) > 0) { + attr->mtime = cached.tv_sec; + attr->mtimensec = cached.tv_nsec; + } + } + if (sx->mask & STATX_CTIME) { + cached = inode_get_ctime(inode); + reported.tv_sec = attr->ctime; + reported.tv_nsec = min_t(u32, attr->ctimensec, NSEC_PER_SEC - 1); + if (timespec64_compare(&cached, &reported) > 0) { + attr->ctime = cached.tv_sec; + attr->ctimensec = cached.tv_nsec; + } + } +} + /* * @param sx_mask request mask send to to fuse-server * @param mandatory_sx_mask subset of (or complete) sx_mask that the server * has to fulfill * @param srv_size where to report the size the server sent, or NULL + * @param light send FUSE_STATX_LIGHT; the reply then only fills @stat */ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode, struct file *file, struct kstat *stat, u32 sx_mask, - u32 mandatory_sx_mask, loff_t *srv_size) + u32 mandatory_sx_mask, loff_t *srv_size, bool light) { int err; struct fuse_attr attr; @@ -1399,7 +1435,7 @@ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode, * For permission checks, we only need mode, uid, gid. * This is an optimization to avoid fetching all stats when not needed. */ - inarg.sx_flags = 0; + inarg.sx_flags = light ? FUSE_STATX_LIGHT : 0; inarg.sx_mask = sx_mask; args.opcode = FUSE_STATX; args.nodeid = get_node_id(inode); @@ -1441,7 +1477,9 @@ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode, *srv_size = sx->size; fuse_statx_to_attr(&outarg.stat, &attr); - if (sx->mask & STATX_BASIC_STATS) { + if (light) { + fuse_statx_light_merge(inode, &attr, sx); + } else if (sx->mask & STATX_BASIC_STATS) { fuse_change_attributes(inode, &attr, &outarg.stat, ATTR_TIMEOUT(&outarg), attr_version); } @@ -1650,6 +1688,8 @@ static int fuse_update_get_attr(struct mnt_idmap *idmap, struct inode *inode, u32 cache_mask = fuse_get_cache_mask(inode); u32 mandatory_sx_mask = request_mask & STATX_BASIC_STATS; u32 sx_mask = request_mask; + bool light = stat && S_ISREG(inode->i_mode) && + !(flags & AT_STATX_FORCE_SYNC); /* FUSE only supports basic stats and possibly btime */ sx_mask &= STATX_BASIC_STATS | STATX_BTIME; @@ -1672,7 +1712,7 @@ static int fuse_update_get_attr(struct mnt_idmap *idmap, struct inode *inode, forget_all_cached_acls(inode); if (!fc->no_statx) { err = fuse_do_statx(idmap, inode, file, stat, sx_mask, - mandatory_sx_mask, srv_size); + mandatory_sx_mask, srv_size, light); if (err == -ENOSYS) { fc->no_statx = 1; err = 0; diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index eb31ec25dad1f7..257e380e18a1e2 100644 --- a/include/uapi/linux/fuse.h +++ b/include/uapi/linux/fuse.h @@ -534,6 +534,15 @@ struct fuse_file_lock { */ #define FUSE_GETATTR_FH (1 << 0) +/** + * Statx flags (fuse_statx_in.sx_flags), beside the AT_STATX_* values + * + * FUSE_STATX_LIGHT: the caller does not need cluster-coherent size, blocks, + * mtime and ctime. The server may return its current + * values without recalling locks other clients hold. + */ +#define FUSE_STATX_LIGHT (1U << 31) + /** * Lock flags */ From a617517d6e7e576cd875e9712bef5c047c6d7fe2 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 27 Oct 2026 18:53:24 +0100 Subject: [PATCH 2/2] fuse: add FUSE_RELEASE_CACHED_READ to report reads the server never saw A read served from the page cache never reaches the server, and a FUSE_READ may be a page fill for a write or a mapping. So the server cannot tell whether the user read a file, which it needs to update atime. Record a cached read(2), splice or sendfile on the file handle and drop the record again when a DLM lock request or a FUSE_READ is built on the handle or the handle writes: the bit then reports a read after the last request the server saw on the handle, so the server stamps atime on its own requests and once more at release when the bit is set. Report the record with FUSE_RELEASE_CACHED_READ in the release. Direct IO, passthrough, copy_file_range and mappings are not reported. The kernel applies no policy, O_NOATIME stays in the release's open flags. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 24 ++++++++++++++++++++++-- fs/fuse/fuse_dlm_cache.c | 3 +++ fs/fuse/fuse_i.h | 3 +++ include/uapi/linux/fuse.h | 6 ++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index a0b6ff41968eea..99b5f9ca331437 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -492,6 +492,8 @@ static void fuse_prepare_release(struct fuse_inode *fi, struct fuse_file *ff, memset(ff->args, 0, sizeof(*ff->args)); ra->inarg.fh = ff->fh; ra->inarg.flags = flags; + if (READ_ONCE(ff->cached_read)) + ra->inarg.release_flags |= FUSE_RELEASE_CACHED_READ; ra->args.in_numargs = 1; ra->args.in_args[0].size = sizeof(struct fuse_release_in); ra->args.in_args[0].value = &ra->inarg; @@ -760,6 +762,8 @@ void fuse_read_args_fill(struct fuse_io_args *ia, struct file *file, loff_t pos, struct fuse_file *ff = file->private_data; struct fuse_args *args = &ia->ap.args; + /* A request the server sees ends the page cache read record */ + WRITE_ONCE(ff->cached_read, false); ia->read.in.fh = ff->fh; ia->read.in.offset = pos; ia->read.in.size = count; @@ -1419,6 +1423,7 @@ static size_t fuse_read_fault_window(struct kiocb *iocb, size_t count, static ssize_t fuse_cache_read_iter(struct kiocb *iocb, struct iov_iter *to) { struct file *file = iocb->ki_filp; + struct fuse_file *ff = file->private_data; struct inode *inode = file->f_mapping->host; struct fuse_conn *fc = get_fuse_conn(inode); struct fuse_inode *fi = get_fuse_inode(inode); @@ -1427,6 +1432,12 @@ static ssize_t fuse_cache_read_iter(struct kiocb *iocb, struct iov_iter *to) bool range_locked = false; ssize_t res; + /* + * Record a page cache read for the release. A FUSE_READ built on the + * handle, which the server sees, or a write clears it again. + */ + WRITE_ONCE(ff->cached_read, true); + /* * In auto invalidate mode, always update attributes on read. * Otherwise, only update if we attempt to read past EOF (to ensure @@ -2928,6 +2939,13 @@ static ssize_t fuse_file_write_iter(struct kiocb *iocb, struct iov_iter *from) if (fuse_is_bad(inode)) return -EIO; + /* + * A write drops the record: the release reports a cached read after + * the handle's last write, so its order against mtime is kept. A + * read before the write the server saw start as a READ grant request. + */ + WRITE_ONCE(ff->cached_read, false); + if (FUSE_IS_DAX(inode)) return fuse_dax_write_iter(iocb, from); @@ -2949,8 +2967,9 @@ static ssize_t fuse_splice_read(struct file *in, loff_t *ppos, /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */ if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO)) return fuse_passthrough_splice_read(in, ppos, pipe, len, flags); - else - return filemap_splice_read(in, ppos, pipe, len, flags); + + WRITE_ONCE(ff->cached_read, true); + return filemap_splice_read(in, ppos, pipe, len, flags); } static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out, @@ -4950,6 +4969,7 @@ static ssize_t __fuse_copy_file_range(struct file *file_in, loff_t pos_in, file_update_time(file_out); fuse_write_update_attr(inode_out, pos_out + outarg.size, outarg.size); + WRITE_ONCE(ff_out->cached_read, false); err = outarg.size; out: diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index c641f1cb8ddbcf..48fe971fcc420f 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -795,6 +795,9 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, if (rlock) fuse_range_lock_mark_init(fi, rlock); + /* A request the server sees ends the page cache read record */ + WRITE_ONCE(ff->cached_read, false); + memset(&inarg, 0, sizeof(inarg)); inarg.fh = ff->fh; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 5fa86547ed89cd..19fa148b834048 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -406,6 +406,9 @@ struct fuse_file { /** Has flock been performed on this file? */ bool flock:1; + + /** Page cache read, nothing the server saw since. Sent at release */ + bool cached_read; }; /** One input argument of a request */ diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index 257e380e18a1e2..60608518420676 100644 --- a/include/uapi/linux/fuse.h +++ b/include/uapi/linux/fuse.h @@ -525,9 +525,15 @@ struct fuse_file_lock { /** * Release flags + * + * FUSE_RELEASE_CACHED_READ: the user read from the page cache: a read(2), + * splice or sendfile ran on this handle and no DLM + * lock request, FUSE_READ or write followed on it, so + * the server never saw that read, e.g. for atime. */ #define FUSE_RELEASE_FLUSH (1 << 0) #define FUSE_RELEASE_FLOCK_UNLOCK (1 << 1) +#define FUSE_RELEASE_CACHED_READ (1U << 31) /** * Getattr flags