From 558aa8d5a682f8c2d23bc954e526745429ae89d4 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 19 Aug 2026 11:31:06 +0200 Subject: [PATCH 1/5] fuse: return the error when the in-gate DLM re-lock fails The re-lock after waiting on the coherency gate funnels its failure through the common exit, but written still holds the positive count generic_write_checks() returned, so the failed write claims full success without having dirtied a single page, and generic_write_sync() runs over a range that was never written. Put the error into written, the way the killpriv failure path above does. The ubuntu-hwe variant of this function never had the problem: it carries the count in err and leaves written zero until the data is actually written. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index a9198a7eb283e2..f954d84037c89d 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1830,6 +1830,7 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) /* The gate is already dropped; funnel * the failure through the one audited * exit. */ + written = err; wb_guard = false; goto wb_out; } From ea2621e8792fc188afe3689cf56fa737e145ca87 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 19 Aug 2026 11:31:33 +0200 Subject: [PATCH 2/5] fuse: re-lock the append range when generic_write_checks() moves it The append grant is taken at a sampled i_size, but the exclusive inode lock does not pin it: attribute replies move i_size under fi->lock alone. generic_write_checks() then rewrites ki_pos to the fresher i_size and the write lands past the granted range, dirtying the cache without DLM coverage. The in-gate re-validation checks the stale range and cannot catch it. Request the lock again for the final ki_pos when it moved, and let dlm_pos track it so the in-gate re-validation guards the range the write really lands in. [hbi: rhel10 port of ubuntu-hwe c4a1a4241930; the failure is funnelled through written, matching the error flow of this variant] Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index f954d84037c89d..0803a5a96a5c83 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1769,9 +1769,8 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) /* * An append write lands at the current EOF no matter * what ki_pos holds: generic_write_checks() rewrites - * ki_pos to i_size for IOCB_APPEND, and i_size is - * stable here because append writes hold the inode lock - * exclusive. Lock where the data will land. + * ki_pos to i_size for IOCB_APPEND. Lock where the + * data will land. */ dlm_pos = i_size_read(inode); dlm_len = iov_iter_count(from); @@ -1786,6 +1785,26 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) if (written <= 0) goto wb_out; + /* + * The exclusive inode lock does not pin i_size for the append: + * attribute replies move it under fi->lock alone, so + * generic_write_checks() may have put ki_pos past the granted + * range. Re-lock where the write really lands; dlm_pos tracks + * it so the in-gate re-validation below guards the same range. + */ + if (fc->dlm && (iocb->ki_flags & IOCB_APPEND) && + iocb->ki_pos != dlm_pos) { + dlm_pos = iocb->ki_pos; + dlm_len = iov_iter_count(from); + + err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len, + &dlm_unrecorded); + if (err) { + written = err; + goto wb_out; + } + } + /* * Kill suid/sgid and stamp the timestamps here, before the * gate, instead of leaving them to From 92bdfab4f29ab4597d4cb72be8069b3454707c1e Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 19 Aug 2026 11:31:57 +0200 Subject: [PATCH 3/5] fuse: drop the expansion zero-fill Read-modify-writes of folios past the server EOF go back to reading: the zero-fill trusts fi->server_size under a recorded write grant, but every source of the bound can predate the grant, and the grant request is what makes a competing node flush. Data flushed in that window sits above a stale bound and gets zeroed over. Keeping the bound honest would take a server EOF observation after every recorded write grant, and those GETATTRs are server traffic and complexity the saved READs are not worth. Partial folios of expanding writes will be sent as separate FUSE_WRITE requests instead, which needs no read-modify-write at all. Remove the fi->server_size bound with its grow and shrink sites and the zero-fill in fuse_write_begin(). [hbi: rhel10 counterpart of ubuntu-hwe 98690625686d; the grant verification machinery was never ported here, so only the zero-fill in fuse_write_begin() and the fi->server_size bound are removed] Signed-off-by: Horst Birthelmer --- fs/fuse/dir.c | 8 ------- fs/fuse/file.c | 61 ------------------------------------------------ fs/fuse/fuse_i.h | 13 ----------- fs/fuse/inode.c | 14 ----------- 4 files changed, 96 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 99af2777432201..ce398f117ef593 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2105,7 +2105,6 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (fc->dlm && fc->writeback_cache) fuse_dlm_cache_release_locks(fi); spin_lock(&fi->lock); - fi->server_size = 0; i_size_write(inode, 0); spin_unlock(&fi->lock); truncate_pagecache(inode, 0); @@ -2201,13 +2200,6 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, /* see the comment in fuse_change_attributes() */ if (!is_wb || is_truncate) i_size_write(inode, outarg.attr.size); - /* - * A truncate settles the size on the server; only shrink the - * server-materialized bound: growing just exposes zeros, which the - * bound need not cover (see fuse_write_begin()). - */ - if (is_truncate && (loff_t) outarg.attr.size < fi->server_size) - fi->server_size = outarg.attr.size; if (is_truncate) { /* NOTE: this may release/reacquire fi->lock */ diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 0803a5a96a5c83..e2ed4ec9904613 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -333,7 +333,6 @@ static void fuse_truncate_update_attr(struct inode *inode, struct file *file) spin_lock(&fi->lock); fi->attr_version = atomic64_inc_return(&fc->attr_version); - fi->server_size = 0; i_size_write(inode, 0); spin_unlock(&fi->lock); file_update_time(file); @@ -1301,15 +1300,6 @@ bool fuse_write_update_attr(struct inode *inode, loff_t pos, ssize_t written) spin_lock(&fi->lock); fi->attr_version = atomic64_inc_return(&fc->attr_version); - if (written > 0 && S_ISREG(inode->i_mode)) { - /* - * The server acknowledged data up to @pos, keep the - * server-materialized bound in sync for the expansion - * zero-fill in fuse_write_begin(). - */ - if (pos > fi->server_size) - fi->server_size = pos; - } if (written > 0 && pos > inode->i_size) { i_size_write(inode, pos); ret = true; @@ -2384,20 +2374,6 @@ static void fuse_writepage_end(struct fuse_mount *fm, struct fuse_args *args, if (!fc->writeback_cache) fuse_invalidate_attr_mask(inode, FUSE_STATX_MODIFY); spin_lock(&fi->lock); - if (!error) { - struct fuse_write_in *inarg = &wpa->ia.write.in; - - /* - * The server acknowledged this writeback, so data up to the - * end of the request is materialized on the server. Advance - * the bound before the pages end writeback below, i.e. before - * they can go clean and be reclaimed, so that - * fuse_write_begin() can never zero-fill a reclaimed range - * the server holds data in. - */ - if ((loff_t) (inarg->offset + inarg->size) > fi->server_size) - fi->server_size = inarg->offset + inarg->size; - } fi->writectr--; fuse_writepage_finish(wpa); spin_unlock(&fi->lock); @@ -2760,42 +2736,6 @@ static int fuse_write_begin(struct file *file, struct address_space *mapping, goto success; } - /* - * The folio is inside i_size but may still sit in a range the server - * holds no data for: a shared-lock writer extends i_size past regions - * it has not written yet (see fuse_write_end()), and every such folio - * would otherwise be read back from the server although it cannot - * contain data. Zero-fill locally instead when the server is known to - * hold nothing in the range and we hold the DLM write lock covering - * it: - * - * - fi->server_size bounds the data materialized on the server - * (writeback and direct write acknowledgements, server - * attributes), - * - local data not yet acknowledged sits in uptodate folios, which - * are already handled above, - * - the page-granular DLM write lock excludes data written by other - * nodes, re-checked against the live lock tree so a revoked lock - * falls back to reading. - */ - if (fc->dlm) { - struct fuse_inode *fi = get_fuse_inode(mapping->host); - loff_t fpos = folio_pos(folio); - size_t fsz = folio_size(folio); - bool hole; - - spin_lock(&fi->lock); - hole = fpos >= fi->server_size; - spin_unlock(&fi->lock); - - if (hole && fuse_dlm_range_is_locked(fi, fpos, fpos + fsz - 1, - FUSE_PAGE_LOCK_WRITE)) { - folio_zero_range(folio, 0, fsz); - folio_mark_uptodate(folio); - goto success; - } - } - err = fuse_do_readpage(file, &folio->page); if (err) goto cleanup; @@ -3874,7 +3814,6 @@ void fuse_init_file_inode(struct inode *inode, unsigned int flags) fuse_dlm_cache_init(fi); fi->writectr = 0; fi->iocachectr = 0; - fi->server_size = 0; init_waitqueue_head(&fi->page_waitq); init_waitqueue_head(&fi->direct_io_waitq); /* diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index c6e9791105671d..8dc81ff1f27452 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -189,19 +189,6 @@ struct fuse_inode { /* dlm locked areas we have sent lock requests for */ struct fuse_dlm_cache dlm_locked_areas; - /* - * Server-materialized size: an upper bound for how far - * the server holds file data. Seeded from - * server-reported attributes, advanced when the server - * acknowledges data (writeback completion, - * fuse_write_update_attr()), lowered again on - * truncate. A read-modify-write of a folio starting - * at or past this bound needs no READ request under a - * held DLM write lock: the server has no data there - * (see fuse_write_begin()). Protected by fi->lock. - */ - loff_t server_size; - /* * Per-inode read/write coherency gate for the * forced-direct-IO feature. Cache-serving buffered reads diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index b4b4cec2d65671..69694ed4ee9105 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -571,12 +571,10 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr loff_t oldsize; struct timespec64 old_mtime; bool have_size = !sx || (sx->mask & STATX_SIZE); - u64 srv_size; cache_mask = fuse_attr_cache_mask(inode, attr, have_size); spin_lock(&fi->lock); - srv_size = attr->size; if (cache_mask & STATX_SIZE) attr->size = i_size_read(inode); @@ -596,18 +594,6 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr return; } - /* - * srv_size is the size the server reported before the writeback - * cache_mask above replaced attr->size with the local value. It - * bounds how far the server can hold data, letting the buffered write - * path zero-fill expansion read-modify-writes instead of sending READ - * requests, see fuse_write_begin(). Only ever grow it here: stale - * attributes were rejected above and truncation lowers it directly. - */ - if (have_size && S_ISREG(inode->i_mode) && - (loff_t) srv_size > fi->server_size) - fi->server_size = srv_size; - old_mtime = inode_get_mtime(inode); fuse_change_attributes_common(inode, attr, sx, attr_valid, cache_mask, evict_ctr); From c0454b92fbe6cc6a4a5151784afa328bfe279d2d Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Mon, 3 Aug 2026 11:57:59 +0200 Subject: [PATCH 4/5] fuse: write unaligned edges through to the server under DLM With the expansion zero-fill gone, every unaligned buffered write under DLM pays a read-modify-write READ per partial folio, and past the server EOF that READ returns zero bytes: a wasted round trip for data that cannot exist. The write path cannot tell the wasted READ from a needed one without trusting a bound the grant may predate. Do not complete partial folios at all. Split the buffered write (fuse_dlm_buffered_write()): cache only the page-aligned interior, whole folios need no read-modify-write, and send the unaligned head and tail through the existing writethrough path (fuse_perform_write()). A partial folio is written byte-exact and left non-uptodate, so nothing is read and neighbouring writers sharing a boundary folio accumulate their bytes on the server. A sub-page write with no aligned interior goes fully through. The DLM write lock is taken over the whole range before i_rwsem, so all chunks are covered. The read-modify-write in fuse_write_begin() stays functional for the partial folios that still reach it, like the buffered fallback of a direct write. [hbi: rewritten 2026-08-19 to match ubuntu-hwe 4b782b76eddd: the WARN_ON_ONCE guard in fuse_write_begin() is gone (the RMW path stays functional, e.g. for the buffered fallback of a direct write) and the interior goes through generic_perform_write on this base] Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index e2ed4ec9904613..96d58ab68b0ca2 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1636,6 +1636,87 @@ static int fuse_cache_wr_dlm_lock(struct file *file, loff_t pos, size_t len, return 0; } +/* + * Write @len bytes of @from at the current iocb position, either straight + * through to the server (@through, for an unaligned edge) or into the page + * cache (@through == false, for the aligned interior). Both primitives + * consume @len bytes from @from and advance iocb->ki_pos; the iterator is + * temporarily capped to @len so the unconsumed tail stays available for the + * next chunk. Returns bytes written (< @len means a short write, the caller + * stops) or a negative error. + */ +static ssize_t fuse_dlm_write_chunk(struct kiocb *iocb, struct iov_iter *from, + size_t len, bool through) +{ + size_t hidden; + ssize_t res; + + if (!len) + return 0; + + /* Cap the iterator to this chunk, keeping the tail for later chunks. */ + hidden = iov_iter_count(from) - len; + iov_iter_truncate(from, len); + res = through ? fuse_perform_write(iocb, from) + : generic_perform_write(iocb, from); + /* Restore from the iterator's own residue, so short writes/errors + * (which leave it partly advanced) reexpand to the exact remainder. */ + iov_iter_reexpand(from, iov_iter_count(from) + hidden); + + return res; +} + +/* + * Buffered write under DLM. A partial folio dirtied for writeback would + * have to be completed by reading the untouched remainder back from the + * server, and for a write past the server EOF that READ can only return + * zero bytes: a wasted round trip per unaligned edge. So cache only the + * page-aligned interior, whole folios need no read-modify-write, and + * route the unaligned head and tail straight through to the server. The + * writethrough path writes just those bytes and leaves the folio + * non-uptodate, doing no read, and each edge lands as an independent + * FUSE_WRITE, so writers sharing a boundary folio accumulate their bytes + * on the server. Aligned writes take the interior path whole; a + * sub-page write with no aligned interior goes fully through. + */ +static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, + struct iov_iter *from) +{ + loff_t pos = iocb->ki_pos; + loff_t end = pos + iov_iter_count(from); + loff_t mid_start = round_up(pos, PAGE_SIZE); + loff_t mid_end = round_down(end, PAGE_SIZE); + ssize_t res, total = 0; + + /* No whole folio inside the write: nothing cacheable, all through. */ + if (mid_end <= mid_start) + return fuse_perform_write(iocb, from); + + /* Unaligned head [pos, mid_start): through. */ + res = fuse_dlm_write_chunk(iocb, from, mid_start - pos, true); + if (res < 0) + return total ? total : res; + total += res; + if (res < mid_start - pos) + return total; + + /* Aligned interior [mid_start, mid_end): cached whole folios. */ + res = fuse_dlm_write_chunk(iocb, from, mid_end - mid_start, false); + if (res < 0) + return total ? total : res; + total += res; + if (res < mid_end - mid_start) + return total; + + /* Unaligned tail [mid_end, end): through. */ + res = fuse_dlm_write_chunk(iocb, from, end - mid_end, true); + if (res < 0) + return total ? total : res; + total += res; + + return total; +} + static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) { struct file *file = iocb->ki_filp; @@ -1853,7 +1934,17 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) written = direct_write_fallback(iocb, from, written, generic_perform_write(iocb, from)); } else { - written = generic_perform_write(iocb, from); + /* + * Under DLM the unaligned edges go through to the + * server instead of being completed by a + * read-modify-write READ (see + * fuse_dlm_buffered_write()); only whole folios are + * cached for writeback. + */ + if (fc->dlm) + written = fuse_dlm_buffered_write(iocb, from); + else + written = generic_perform_write(iocb, from); } wb_out: if (wb_guard) From 4f352ab4bddf519e2f973718dbf052a5de67eaa8 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Thu, 20 Aug 2026 13:14:00 +0200 Subject: [PATCH 5/5] fuse: mark DLM edge writes with FUSE_WRITE_CACHE Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 96d58ab68b0ca2..48247876177b5d 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1444,7 +1444,8 @@ static inline unsigned int fuse_wr_pages(loff_t pos, size_t len, max_pages); } -static ssize_t fuse_perform_write(struct kiocb *iocb, struct iov_iter *ii) +static ssize_t fuse_perform_write(struct kiocb *iocb, struct iov_iter *ii, + bool cache) { struct address_space *mapping = iocb->ki_filp->f_mapping; struct inode *inode = mapping->host; @@ -1474,6 +1475,14 @@ static ssize_t fuse_perform_write(struct kiocb *iocb, struct iov_iter *ii) if (count <= 0) { err = count; } else { + /* + * On behalf of a buffered write whose bytes bypass + * the page cache (DLM unaligned edges): the server + * must classify them like the writeback they + * replace. + */ + if (cache) + ia.write.in.write_flags |= FUSE_WRITE_CACHE; err = fuse_send_write_pages(&ia, iocb, inode, pos, count); if (!err) { @@ -1657,7 +1666,7 @@ static ssize_t fuse_dlm_write_chunk(struct kiocb *iocb, struct iov_iter *from, /* Cap the iterator to this chunk, keeping the tail for later chunks. */ hidden = iov_iter_count(from) - len; iov_iter_truncate(from, len); - res = through ? fuse_perform_write(iocb, from) + res = through ? fuse_perform_write(iocb, from, true) : generic_perform_write(iocb, from); /* Restore from the iterator's own residue, so short writes/errors * (which leave it partly advanced) reexpand to the exact remainder. */ @@ -1675,8 +1684,9 @@ static ssize_t fuse_dlm_write_chunk(struct kiocb *iocb, struct iov_iter *from, * route the unaligned head and tail straight through to the server. The * writethrough path writes just those bytes and leaves the folio * non-uptodate, doing no read, and each edge lands as an independent - * FUSE_WRITE, so writers sharing a boundary folio accumulate their bytes - * on the server. Aligned writes take the interior path whole; a + * FUSE_WRITE carrying FUSE_WRITE_CACHE like the writeback it replaces, + * so writers sharing a boundary folio accumulate their bytes on the + * server. Aligned writes take the interior path whole; a * sub-page write with no aligned interior goes fully through. */ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, @@ -1690,7 +1700,7 @@ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, /* No whole folio inside the write: nothing cacheable, all through. */ if (mid_end <= mid_start) - return fuse_perform_write(iocb, from); + return fuse_perform_write(iocb, from, true); /* Unaligned head [pos, mid_start): through. */ res = fuse_dlm_write_chunk(iocb, from, mid_start - pos, true); @@ -2006,9 +2016,9 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) if (written < 0 || !iov_iter_count(from)) goto out; written = direct_write_fallback(iocb, from, written, - fuse_perform_write(iocb, from)); + fuse_perform_write(iocb, from, false)); } else { - written = fuse_perform_write(iocb, from); + written = fuse_perform_write(iocb, from, false); } out: if (wb_guard)