From 8953df6fc7a2ec1e74077d244a63f91e390e61c9 Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Thu, 15 Oct 2026 21:04:31 -0700 Subject: [PATCH] fuse: raise i_size from a read reply under DLM with the writeback cache Under DLM with the writeback cache, i_size is this node's view of the file: each cached write claims its own extension under fi->lock, and the server's size is not consulted while the node holds a grant. Bytes another node wrote past this node's i_size are therefore "past EOF" to every piece of generic code that looks at i_size, even once a read has brought them into the page cache. The read-modify-write read of a cached write does exactly that. In RED-51747 three nodes write interleaved 47008-byte blocks of one file. The lower writer's write ends inside a page whose tail already holds the upper writer's bytes on the server. The lower writer claims i_size up to the end of its own block, reads the page under its write grant, copies its bytes in, and the page is correct. When the page is written back, iomap_writeback_handle_eof() zeroes it from the local i_size to the page end. The previous commit keeps those zeros off the wire, but they stay in the folio, which is clean and uptodate: once a later local write raises i_size past the page, a buffered read on this node returns the zeros from the cache, and if this node dirties the page again the whole block is written and the zeros reach the server after all. A read reply is evidence of the file's size. The server answers short past its EOF, so num_read bytes at pos mean the file reaches at least pos + num_read. The grant the read was made under keeps it there: a remote truncate revokes every grant above its new size before it proceeds, which fuse_attr_cache_mask() already relies on, and a local truncate holds i_rwsem exclusive and sets FUSE_I_SIZE_UNSTABLE until the page cache has been cut. Raise i_size to pos + num_read when a read reply arrives, in fuse_do_readfolio() and fuse_readpages_end(), under fi->lock. The folio that holds another node's bytes is then inside i_size, and neither iomap_writeback_handle_eof() nor iomap_block_needs_zeroing() treats its contents as post-EOF. The clamp in fuse_iomap_writeback_range() remains as the second guard. The update follows fuse_read_update_size(): it bumps attr_version, so an attribute reply older than this evidence does not shrink the size back, and it is skipped when the attributes have moved on since the read was sent or while FUSE_I_SIZE_UNSTABLE is set, so a reply issued before a truncate cannot undo it. The O_TRUNC open path bumps attr_version before it empties the cache, so that check covers it as well. The cost is that an unrelated attribute reply landing between a read's request and its reply skips the raise; in that case the clamp still keeps the zeros off the wire, and the next read of the folio raises the size. The retract of a speculative extension after a short cached write only fires when i_size still equals the claimed end, so a size raised past it by a read is left alone. Only the DLM + writeback-cache path is changed. Without DLM the writeback cache is single-client and i_size is authoritative; without the writeback cache the server's size is applied on every attribute reply. Tested on a 6.17.13 lockdep build with two passthrough_hp mounts, --dlm --nopassthrough, over one backing directory, replaying the sequence above: mount A writes [0, 4196) and syncs, drops its cache in place of the revoke, mount B writes [4196, 12288) and syncs, A rewrites [4096, 4196) and syncs, extends the file by one byte, reads [4096, 8192) buffered, and rewrites [4096, 4196) once more. With the previous commit alone the buffered read returns 3996 zero bytes from the cache and the final writeback puts the same zeros on the server; with this commit both return B's bytes. Lockdep is silent. Signed-off-by: Allison Henderson Co-Authored-By: Claude Fable 5.1 --- fs/fuse/file.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 6113bf69491caa..85341698d6e543 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1008,6 +1008,44 @@ static void fuse_short_read(struct inode *inode, u64 attr_ver, size_t num_read, } } +/* + * Under DLM with the writeback cache, i_size is this node's view: each cached + * write claims its own extension, and the server's size is not consulted. A + * read reply is evidence of more. The server answers short past its EOF, so + * num_read bytes at pos mean the file reaches pos + num_read, and the grant + * the read was made under keeps it there: a remote truncate revokes every + * grant above its new size before it proceeds (see fuse_attr_cache_mask()). + * + * Without this the bytes another node wrote past the local i_size, read into + * a folio by the read-modify-write read of a cached write, count as post-EOF: + * iomap_writeback_handle_eof() zeroes them in the folio when the folio is + * written back, and they stay zero in the cache until the next revoke. Raise + * i_size to what the reply proves, so nothing in the folio is past EOF. + * + * Only a reply newer than the attributes may raise the size, as in + * fuse_read_update_size(): a reply issued before a truncate must not undo it. + */ +static void fuse_read_extend_size(struct inode *inode, u64 attr_ver, + size_t num_read, struct fuse_args_pages *ap) +{ + struct fuse_conn *fc = get_fuse_conn(inode); + struct fuse_inode *fi = get_fuse_inode(inode); + loff_t end; + + if (!fc->dlm || !fc->writeback_cache || !num_read) + return; + + end = folio_pos(ap->folios[0]) + ap->descs[0].offset + num_read; + + spin_lock(&fi->lock); + if (attr_ver >= fi->attr_version && 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) { @@ -1057,6 +1095,8 @@ static int fuse_do_readfolio(struct file *file, struct folio *folio, if (res < desc.length) fuse_short_read(inode, attr_ver, res, &ia.ap); + fuse_read_extend_size(inode, attr_ver, res, &ia.ap); + return 0; } @@ -1148,6 +1188,10 @@ 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) + fuse_read_extend_size(inode, ia->read.attr_ver, num_read, + ap); + fuse_invalidate_atime(inode); }