From fb0ac9ec36768bd493cdc521cff26309d2c629b7 Mon Sep 17 00:00:00 2001 From: Yong Ze Chen Date: Fri, 16 Oct 2026 06:19:42 +0000 Subject: [PATCH] fuse: raise i_size over bytes a DLM read reply returned Under DLM with the writeback cache, a node's i_size only covers its own writes. When two nodes share a page at a block boundary, the lower writer's read before its partial write returns the upper writer's bytes from the server, but they lie past the local i_size. iomap_writeback_handle_eof() then zeroes them in the cached folio as if they were past EOF, the folio stays uptodate, and if i_size grows before the send is cropped the zeroes overwrite the other node's data on the server (RED-51747). A read reply proves the file extends at least to pos + res, since the server clips reads at its size. Raise i_size to cover those bytes in fuse_do_readfolio() and fuse_readpages_end(), so the folio tail is never treated as past EOF. The raise must not resurrect bytes a truncate removed while the read was in flight. attr_version cannot guard this, because every revoke bumps it, including the revoke of the range being read. Add fi->size_gen instead, bumped only by a local truncate and by a whole-file invalidate, which is how a remote truncate reaches the client, and drop the raise when it changed. Co-Authored-By: Claude Opus 5.5 --- fs/fuse/dir.c | 14 ++++++++++++++ fs/fuse/file.c | 41 +++++++++++++++++++++++++++++++++++++++++ fs/fuse/fuse_i.h | 14 ++++++++++++++ fs/fuse/inode.c | 6 ++++++ 4 files changed, 75 insertions(+) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 0c6d54b98a0f31..24f5d67f6b2287 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2403,7 +2403,11 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, } if (is_truncate) { + /* Drop read replies that may predate the truncate. */ + spin_lock(&fi->lock); + fuse_bump_size_gen(fi); set_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); + spin_unlock(&fi->lock); if (trust_local_cmtime && attr->ia_size != inode->i_size) attr->ia_valid |= ATTR_MTIME | ATTR_CTIME; } @@ -2500,6 +2504,11 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, fuse_range_lock_release(fi, &rlock); } + if (is_truncate) { + spin_lock(&fi->lock); + fuse_bump_size_gen(fi); + spin_unlock(&fi->lock); + } clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); out: if (fault_blocked) @@ -2508,6 +2517,11 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, return 0; error: + if (is_truncate) { + spin_lock(&fi->lock); + fuse_bump_size_gen(fi); + spin_unlock(&fi->lock); + } clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); if (fault_blocked) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index bdc1cf0dab75df..7776db4b51e498 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1008,6 +1008,36 @@ static void fuse_short_read(struct inode *inode, u64 attr_ver, size_t num_read, } } +/* + * Under DLM with the writeback cache, i_size covers only this node's own + * writes. A read reply proves the file extends at least to the bytes it + * returned (the server clips reads at its size), and they may be another + * node's data in a page this node shares. Raise i_size over them, so that + * iomap_writeback_handle_eof() never zeroes them as if they were past EOF. + * + * Every revoke bumps attr_version, including the revoke of the very range + * being read, so attr_version cannot gate this. size_gen moves only for a + * local truncate or a whole-file invalidate, which is how a remote truncate + * reaches us; a reply that may predate either is dropped. + */ +static void fuse_dlm_read_extend_size(struct inode *inode, loff_t end, + u64 size_gen) +{ + struct fuse_conn *fc = get_fuse_conn(inode); + struct fuse_inode *fi = get_fuse_inode(inode); + + if (!fc->dlm || !fc->writeback_cache) + return; + + spin_lock(&fi->lock); + if (fi->size_gen == size_gen && end > inode->i_size && + !test_bit(FUSE_I_SIZE_UNSTABLE, &fi->state)) { + fi->attr_version = atomic64_inc_return(&fc->attr_version); + i_size_write(inode, end); + } + spin_unlock(&fi->lock); +} + static int fuse_do_readfolio(struct file *file, struct folio *folio, size_t off, size_t len) { @@ -1028,8 +1058,10 @@ static int fuse_do_readfolio(struct file *file, struct folio *folio, }; ssize_t res; u64 attr_ver; + u64 size_gen; attr_ver = fuse_get_attr_version(fm->fc); + size_gen = READ_ONCE(get_fuse_inode(inode)->size_gen); /* Don't overflow end offset */ if (pos + (desc.length - 1) == LLONG_MAX) @@ -1057,6 +1089,9 @@ static int fuse_do_readfolio(struct file *file, struct folio *folio, if (res < desc.length) fuse_short_read(inode, attr_ver, res, &ia.ap); + if (res > 0) + fuse_dlm_read_extend_size(inode, pos + res, size_gen); + return 0; } @@ -1148,6 +1183,11 @@ static void fuse_readpages_end(struct fuse_mount *fm, struct fuse_args *args, if (!err && num_read < count) fuse_short_read(inode, ia->read.attr_ver, num_read, ap); + if (!err && num_read) + fuse_dlm_read_extend_size(inode, + ia->read.in.offset + num_read, + ia->read.size_gen); + fuse_invalidate_atime(inode); } @@ -1185,6 +1225,7 @@ static void fuse_send_readpages(struct fuse_io_args *ia, struct file *file, fuse_read_args_fill(ia, file, pos, count, FUSE_READ); ia->read.attr_ver = fuse_get_attr_version(fm->fc); + ia->read.size_gen = READ_ONCE(get_fuse_inode(file_inode(file))->size_gen); if (fm->fc->async_read) { ia->ff = fuse_file_get(ff); ap->args.end = fuse_readpages_end; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 96a4a119e1b574..89b75b3c2eb2ba 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -193,6 +193,13 @@ struct fuse_inode { /** Version of last attribute change */ u64 attr_version; + /* + * Bumped under fi->lock whenever the file may have shrunk behind a + * read in flight: a local truncate, or a whole-file invalidate (the + * shape a remote truncate takes). See fuse_dlm_read_extend_size(). + */ + u64 size_gen; + union { /* read/write io cache (regular file only) */ struct { @@ -1248,6 +1255,12 @@ static inline u64 fuse_get_attr_version(struct fuse_conn *fc) return atomic64_read(&fc->attr_version); } +static inline void fuse_bump_size_gen(struct fuse_inode *fi) +{ + lockdep_assert_held(&fi->lock); + WRITE_ONCE(fi->size_gen, fi->size_gen + 1); +} + static inline u64 fuse_get_evict_ctr(struct fuse_conn *fc) { return atomic64_read(&fc->evict_ctr); @@ -1333,6 +1346,7 @@ struct fuse_io_args { struct { struct fuse_read_in in; u64 attr_ver; + u64 size_gen; } read; struct { struct fuse_write_in in; diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 9eb794a59e6371..164a2b106d5677 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -979,6 +979,12 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, fi = get_fuse_inode(inode); spin_lock(&fi->lock); fi->attr_version = atomic64_inc_return(&fc->attr_version); + /* + * A remote truncate reaches us as a whole-file invalidate; read + * replies in flight may predate it (see fuse_dlm_read_extend_size()). + */ + if (offset <= 0 && len <= 0) + fuse_bump_size_gen(fi); spin_unlock(&fi->lock); if (fc->inval_inode_entries)