From 7499e44c802a045737de4b48c4baaa0dc26fd4ea Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 26 Aug 2026 14:59:50 +0200 Subject: [PATCH 01/41] fuse: refuse a writeback connection whose block is not a page fuse_dlm_buffered_write() sends the unaligned edges of a write to the server itself so no partly written block is dirtied, and cuts at PAGE_SIZE. Nothing checked that a block is a page: fc->blkbits keeps whatever blksize= asked for, and attr->blksize sets inode->i_blkbits per inode. iomap then goes back for the remainder of an edge block and dirties what the write already sent. Refuse the connection at FUSE_INIT when a writeback cache is negotiated on a block that is not a page, and pin inode->i_blkbits afterwards. st_blksize still reports what the server named. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 31 +++++++++++++++++++------------ fs/fuse/inode.c | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 7c5b5e8b4ba268..f28a48ac59dcfb 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1805,18 +1805,25 @@ static ssize_t fuse_dlm_write_chunk(struct kiocb *iocb, struct iov_iter *from, } /* - * Buffered write under DLM. A partial page 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 pages 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 page - * non-uptodate, doing no read, and each edge lands as an independent - * FUSE_WRITE carrying FUSE_WRITE_CACHE like the writeback it replaces, - * so writers sharing a boundary page accumulate their bytes on the - * server. Aligned writes take the interior path whole; a - * sub-page write with no aligned interior goes fully through. + * Buffered write under DLM. A partly written page 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 pages + * 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 page non-uptodate, doing no read, and each + * edge lands as an independent FUSE_WRITE carrying FUSE_WRITE_CACHE + * like the writeback it replaces, so writers sharing a boundary page + * accumulate their bytes on the server. Aligned writes take the + * interior path whole; a sub-page write with no aligned interior goes + * fully through. + * + * The page is the block here: iomap tracks a folio a block at a time + * and __iomap_write_begin() skips the fill only for a block the write + * covers whole, so the cut has to land on block bounds. A writeback + * connection is refused unless the two are the same size; see + * process_init_reply(). */ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, struct iov_iter *from, diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index d54676b73abf9e..bb52782c064d3b 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -371,8 +371,17 @@ static void fuse_change_attributes_common_sx(struct inode *inode, } } - /* Common fields for both statx and getattr */ - if (attr->blksize != 0) + /* + * Common fields for both statx and getattr. + * + * A writeback connection was refused at FUSE_INIT unless its block + * is a page, for the reasons given there, and a server naming a + * different one per inode does not get to take that back. What it + * named is still reported as st_blksize out of + * fi->cached_i_blkbits; this is only what the page cache is + * tracked in. + */ + if (attr->blksize != 0 && !fc->writeback_cache) inode->i_blkbits = ilog2(attr->blksize); else inode->i_blkbits = inode->i_sb->s_blocksize_bits; @@ -1869,8 +1878,31 @@ static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args, } if (flags & FUSE_ASYNC_DIO) fc->async_dio = 1; - if (flags & FUSE_WRITEBACK_CACHE) + if (flags & FUSE_WRITEBACK_CACHE) { + /* + * A buffered write goes through iomap, which + * tracks a folio a block at a time and fills + * any block the write covers only part of. + * Writeback then sends whole dirty blocks. + * Both of those are cut at the page in this + * filesystem: fuse_dlm_buffered_write() sends + * the unaligned edges of a write to the server + * itself so that no partly written block is + * ever dirtied, and it cuts at PAGE_SIZE. + * + * A block that is not a page breaks that, and + * quietly: the fill would come back for the + * remainder of an edge block, from a server + * that need not hold anything there. Refuse + * the connection instead. + */ + if (fc->blkbits != PAGE_SHIFT) { + pr_err("fuse: writeback cache needs a page sized block, got %u\n", + 1U << fc->blkbits); + ok = false; + } fc->writeback_cache = 1; + } if (flags & FUSE_PARALLEL_DIROPS) fc->parallel_dirops = 1; if (flags & FUSE_HANDLE_KILLPRIV) From 8c2ff0e081ec2970dfc6a0bcdb15f66f56d83eab Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 21 Aug 2026 14:07:44 +0200 Subject: [PATCH 02/41] fuse: take a DLM read lock for the readahead window Readahead fills the page cache past the range fuse_cache_read_iter() locked, so those folios get no revoke when a remote node writes them. Request a read grant over the whole window in fuse_readahead() before any folio is consumed, and skip the window when the request fails. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index f28a48ac59dcfb..e1ce3c0487da33 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1154,6 +1154,34 @@ static void fuse_readahead(struct readahead_control *rac) if (fuse_is_bad(inode)) return; + /* + * Readahead fills the page cache past the range the reader locked, + * so take a DLM read grant over the whole window here too. Folios + * the server handed out no lock for are folios it will not revoke + * when a remote node writes them, and a later read would be served + * from stale cache. Take the grant before any folio is pulled off + * @rac, so the window is either fully covered or not populated. + * + * Speculative pages are not worth serving uncovered: on a failed + * request drop the window and let read_pages() clean up the folios + * left in @rac. A server without DLM support answers -ENOSYS and + * clears fc->dlm, which is not a failure. + * + * This can run inside the coherency gate, which + * fuse_cache_read_iter() holds across generic_file_read_iter(), so + * the round trip leans on the same server contract that lets a + * cache-miss FUSE_READ block there: replies are serviced on threads + * other than the one delivering a NOTIFY invalidate. + */ + if (fc->writeback_cache && fc->dlm) { + int err = fuse_get_dlm_lock(rac->file, readahead_pos(rac), + readahead_length(rac), + FUSE_PAGE_LOCK_READ); + + if (err < 0 && err != -ENOSYS) + return; + } + max_pages = min_t(unsigned int, fc->max_pages, fc->max_read / PAGE_SIZE); From b774e9f31ad6d0fafaf61058aee690f58fd68370 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 11:03:23 +0200 Subject: [PATCH 03/41] fuse: give DLM ranges a lifecycle state A FUSE_DLM_WB_LOCK reply and a NOTIFY revoke run on different threads, so a revoke can arrive before the grant is recorded, find nothing, and leave a grant that is never taken back. revoke_gen caught this with a per-inode counter that cannot say which range was hit, so any revoke re-requested every grant in flight. Add enum fuse_dlm_range_state: REQUESTED or REVOKED on the new cache->pending list while in flight, GRANTED in cache->ranges. A revoke marks the pending requests it overlaps, fuse_dlm_request_commit() drops a marked grant, and unlinking the request and recording its grant is one step under the cache lock. Pending requests stay off the interval tree, so no tree walker needs a state filter. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 267 ++++++++++++++++++++++++++++----------- fs/fuse/fuse_dlm_cache.h | 50 ++++++-- 2 files changed, 227 insertions(+), 90 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index bc6dbae2d5aeb0..1795321c1f84f0 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -1,6 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-only /* * FUSE page lock cache implementation + * + * cache->ranges records the grants the server has given this client. A + * grant still on the wire covers nothing and must not appear there, but + * a revoke has to be able to find it: otherwise a revoke processed + * before the grant is recorded removes nothing, and the grant recorded + * afterwards is never taken back. + * + * A range therefore carries enum fuse_dlm_range_state: + * + * - REQUESTED, on cache->pending, while its FUSE_DLM_WB_LOCK is in + * flight. + * + * - REVOKED, still on cache->pending, once a revoke has overlapped it. + * fuse_dlm_request_commit() drops such a grant instead of recording + * it. + * + * - GRANTED, in cache->ranges. The only state + * fuse_dlm_range_is_locked() reports as covered. + * + * In-flight requests are kept off the tree so the state is consulted + * only where a request is retired, not by every tree walker. */ #include "fuse_i.h" #include "fuse_dlm_cache.h" @@ -11,9 +32,19 @@ #include +/* Lifecycle of a range; see the file comment above */ +enum fuse_dlm_range_state { + /* FUSE_DLM_WB_LOCK in flight, on cache->pending */ + FUSE_DLM_RANGE_REQUESTED, + /* Revoked while in flight; the grant must not be recorded */ + FUSE_DLM_RANGE_REVOKED, + /* Recorded grant, in cache->ranges */ + FUSE_DLM_RANGE_GRANTED, +}; + /* A range of pages with a lock */ struct fuse_dlm_range { - /* Interval tree node */ + /* Interval tree node; only linked while GRANTED */ struct rb_node rb; /* Start page offset (inclusive) */ uint64_t start; @@ -21,9 +52,11 @@ struct fuse_dlm_range { uint64_t end; /* Subtree end value for interval tree */ uint64_t __subtree_end; - /* Lock mode */ + /* Lock mode, as FUSE_PCACHE_LK_READ / FUSE_PCACHE_LK_WRITE */ enum fuse_page_lock_mode mode; - /* Temporary list entry for operations */ + /* Lifecycle state; see enum fuse_dlm_range_state */ + enum fuse_dlm_range_state state; + /* Temporary list entry for operations, and the cache->pending link */ struct list_head list; }; @@ -46,6 +79,31 @@ INTERVAL_TREE_DEFINE(struct fuse_dlm_range, rb, uint64_t, __subtree_end, fuse_dlm_range_start, fuse_dlm_range_last, static, fuse_page_it); +/** + * fuse_dlm_kill_pending - mark in-flight requests overlapping [start, end] + * @cache: The page cache + * @start: Start page offset of the revoked region + * @end: End page offset of the revoked region + * + * A revoke overlapping a request still on the wire has nothing to remove + * from the tree, since that grant is not recorded yet. Marking it makes + * fuse_dlm_request_commit() drop the grant instead of recording it. + * + * The nodes are owned by the threads waiting on their replies: mark + * only, never remove or free. + * + * Caller holds @cache->lock for write. + */ +static void fuse_dlm_kill_pending(struct fuse_dlm_cache *cache, + uint64_t start, uint64_t end) +{ + struct fuse_dlm_range *req; + + list_for_each_entry(req, &cache->pending, list) + if (req->start <= end && start <= req->end) + req->state = FUSE_DLM_RANGE_REVOKED; +} + /** * fuse_page_cache_init - Initialize a page cache lock manager * @cache: The cache to initialize @@ -63,7 +121,7 @@ int fuse_dlm_cache_init(struct fuse_inode *inode) init_rwsem(&cache->lock); cache->ranges = RB_ROOT_CACHED; - cache->revoke_gen = 0; + INIT_LIST_HEAD(&cache->pending); return 0; } @@ -85,7 +143,11 @@ void fuse_dlm_cache_release_locks(struct fuse_inode *inode) /* Release all locks */ down_write(&cache->lock); - WRITE_ONCE(cache->revoke_gen, cache->revoke_gen + 1); + /* + * Every grant goes, so every request in flight is revoked. Mark + * only; each node is owned by the thread waiting on its reply. + */ + fuse_dlm_kill_pending(cache, 0, U64_MAX); while ((node = rb_first_cached(&cache->ranges)) != NULL) { range = rb_entry(node, struct fuse_dlm_range, rb); fuse_page_it_remove(range, &cache->ranges); @@ -168,13 +230,11 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, } /** - * __fuse_dlm_lock_range - Lock a range of pages - * @cache: The page cache + * fuse_dlm_lock_range_locked - Record a granted range of pages + * @inode: The fuse inode * @start: Start page offset * @end: End page offset * @mode: Lock mode (read or write) - * @genp: If non-NULL, the revocation generation sampled before the grant - * was requested; recording fails with -EAGAIN if it has moved * * Add a locked range on the specified range of pages. * If parts of the range are already locked, only add the remaining parts. @@ -183,11 +243,16 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, * - READ locks are compatible with existing WRITE locks (downgrade not needed) * - WRITE locks need to upgrade existing READ locks * + * Everything inserted here is FUSE_DLM_RANGE_GRANTED: this runs only + * after the server has answered. + * + * Caller holds the cache lock for write. + * * Return: 0 on success, negative error code on failure */ -static int __fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode, - const uint64_t *genp) +static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, + uint64_t end, + enum fuse_page_lock_mode mode) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range, *new_range, *next; @@ -205,19 +270,6 @@ static int __fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, lock_mode = (mode == FUSE_PAGE_LOCK_READ) ? FUSE_PCACHE_LK_READ : FUSE_PCACHE_LK_WRITE; - down_write(&cache->lock); - - /* - * A revoke was processed after @genp was sampled; the grant this - * record carries may be the very one it targeted (a revoke of a - * not-yet-recorded grant removes nothing and would never be - * retried). Refuse, the caller re-requests. - */ - if (genp && cache->revoke_gen != *genp) { - up_write(&cache->lock); - return -EAGAIN; - } - /* Find all ranges that overlap with [start, end] */ range = fuse_page_it_iter_first(&cache->ranges, start, end); while (range) { @@ -243,6 +295,7 @@ static int __fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = range->start - 1; new_range->mode = lock_mode; + new_range->state = FUSE_DLM_RANGE_GRANTED; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -269,6 +322,7 @@ static int __fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = end; new_range->mode = lock_mode; + new_range->state = FUSE_DLM_RANGE_GRANTED; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -289,7 +343,6 @@ static int __fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, /* Try to merge adjacent ranges with the same mode */ fuse_dlm_try_merge(cache, start, end); - up_write(&cache->lock); return 0; out_free: @@ -309,37 +362,110 @@ static int __fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, } } - up_write(&cache->lock); return ret; } int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, uint64_t end, enum fuse_page_lock_mode mode) { - return __fuse_dlm_lock_range(inode, start, end, mode, NULL); + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + int ret; + + down_write(&cache->lock); + ret = fuse_dlm_lock_range_locked(inode, start, end, mode); + up_write(&cache->lock); + + return ret; +} + +/** + * fuse_dlm_request_begin - publish a lock request before it is sent + * @inode: the fuse inode + * @req: caller-owned storage for the request, live until commit or abort + * @start: start page offset being requested (inclusive) + * @end: end page offset being requested (inclusive) + * @mode: FUSE_PAGE_LOCK_READ or FUSE_PAGE_LOCK_WRITE + * + * A FUSE_DLM_WB_LOCK reply and a NOTIFY revoke are serviced on different + * threads, so a revoke can be processed before the grant the reply + * carries is recorded. Publishing the request before it leaves gives + * that revoke a node to mark; without one it removes nothing, and the + * grant recorded afterwards is never taken back by any later NOTIFY. + * + * The request covers nothing while in flight, so it is kept off the + * tree. @req is reachable only through cache->pending, which both + * fuse_dlm_request_commit() and fuse_dlm_request_abort() unlink under + * the cache lock before the caller returns; stack storage is therefore + * fine and nothing is allocated here. + */ +void fuse_dlm_request_begin(struct fuse_inode *inode, + struct fuse_dlm_range *req, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + + RB_CLEAR_NODE(&req->rb); + req->start = start; + req->end = end; + req->mode = (mode == FUSE_PAGE_LOCK_READ) ? FUSE_PCACHE_LK_READ : + FUSE_PCACHE_LK_WRITE; + req->state = FUSE_DLM_RANGE_REQUESTED; + + down_write(&cache->lock); + list_add_tail(&req->list, &cache->pending); + up_write(&cache->lock); } -int fuse_dlm_lock_range_gen(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode, - uint64_t gen) +/** + * fuse_dlm_request_commit - retire a request and record its grant + * @inode: the fuse inode + * @req: the request published by fuse_dlm_request_begin() + * @start: start page offset the server granted (inclusive) + * @end: end page offset the server granted (inclusive) + * @mode: the mode that was requested + * + * Unlinking @req and recording the grant are one step under the cache + * lock, so a revoke lands either before it and is seen on @req, or after + * it and finds the grant in the tree. + * + * @req is retired in every case and may be reused. + * + * Return: -EAGAIN if a revoke overlapped @req while it was in flight, + * nothing recorded; otherwise the result of recording the grant. + */ +int fuse_dlm_request_commit(struct fuse_inode *inode, + struct fuse_dlm_range *req, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode) { - return __fuse_dlm_lock_range(inode, start, end, mode, &gen); + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + bool revoked; + int ret = 0; + + down_write(&cache->lock); + list_del(&req->list); + revoked = req->state == FUSE_DLM_RANGE_REVOKED; + if (!revoked) + ret = fuse_dlm_lock_range_locked(inode, start, end, mode); + up_write(&cache->lock); + + return revoked ? -EAGAIN : ret; } /** - * fuse_dlm_revoke_gen - sample the revocation generation + * fuse_dlm_request_abort - retire a request that got no usable reply * @inode: the fuse inode + * @req: the request published by fuse_dlm_request_begin() * - * Sampled before a FUSE_DLM_WB_LOCK request leaves the client. The - * reply and a NOTIFY revoke can be serviced on different threads, so a - * revoke may be processed between the reply arriving and its grant - * being recorded. fuse_dlm_lock_range_gen() re-checks the generation - * under the cache lock and refuses to record a grant such a revoke may - * have already killed. + * Nothing is recorded, so a mark left by a revoke does not matter. */ -uint64_t fuse_dlm_revoke_gen(struct fuse_inode *inode) +void fuse_dlm_request_abort(struct fuse_inode *inode, + struct fuse_dlm_range *req) { - return READ_ONCE(inode->dlm_locked_areas.revoke_gen); + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + + down_write(&cache->lock); + list_del(&req->list); + up_write(&cache->lock); } /** @@ -436,12 +562,11 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, down_write(&cache->lock); /* - * Unconditional, even when nothing overlaps: the revoke racing - * with an in-flight grant finds an empty tree precisely because - * the grant is not recorded yet, and the bump is what makes the - * recording side notice (see fuse_dlm_lock_range_gen()). + * Before touching the tree, and even when nothing in the tree + * overlaps: a revoke racing an in-flight grant finds no overlap + * because that grant is not recorded yet. */ - WRITE_ONCE(cache->revoke_gen, cache->revoke_gen + 1); + fuse_dlm_kill_pending(cache, start, end); /* Find all ranges that overlap with [start, end] */ range = fuse_page_it_iter_first(&cache->ranges, start, end); @@ -646,7 +771,7 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, FUSE_ARGS(args); struct fuse_dlm_lock_in inarg; struct fuse_dlm_lock_out outarg; - uint64_t gen; + struct fuse_dlm_range req; int err; /* An empty range needs no lock. */ @@ -664,16 +789,6 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, if (fuse_dlm_lock_is_held(fi, offset, length, mode)) return 0; /* we already have this area locked */ - /* - * Sample the revocation generation before the request leaves. - * The reply and a NOTIFY revoke are serviced on different - * threads, so a revoke aimed at the grant this request returns - * can be processed before the grant is recorded below -- - * recording it anyway would resurrect a dead grant that no later - * NOTIFY will ever remove. - */ - gen = fuse_dlm_revoke_gen(fi); - memset(&inarg, 0, sizeof(inarg)); inarg.fh = ff->fh; @@ -693,39 +808,39 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, args.out_numargs = 1; args.out_args[0].size = sizeof(outarg); args.out_args[0].value = &outarg; + + /* Publish before sending; see fuse_dlm_request_begin() */ + fuse_dlm_request_begin(fi, &req, inarg.start, inarg.end, mode); + err = fuse_simple_request(fm, &args); - if (err == -ENOSYS) { - /* fuse server does not support dlm, save the info */ - fc->dlm = 0; + if (err) { + fuse_dlm_request_abort(fi, &req); + if (err == -ENOSYS) { + /* fuse server does not support dlm, save the info */ + fc->dlm = 0; + } return err; } - if (err) - return err; - if (inarg.start < outarg.start || inarg.end > outarg.end) { /* fuse server is seriously broken */ + fuse_dlm_request_abort(fi, &req); pr_warn("fuse: dlm lock request for %llu:%llu returned %llu:%llu bytes\n", inarg.start, inarg.end, outarg.start, outarg.end); fuse_abort_conn(fc); return -EIO; } - /* - * The server granted the lock; record it so - * fuse_dlm_lock_is_held() sees it. - */ - err = fuse_dlm_lock_range_gen(fi, outarg.start, outarg.end, mode, gen); + /* Retire the request and record the grant */ + err = fuse_dlm_request_commit(fi, &req, outarg.start, outarg.end, mode); if (err == -EAGAIN) { /* - * A revoke was processed while the request was in flight; - * the grant may already be dead, so re-request instead of - * recording it. Retry until a grant survives long enough to - * be recorded: giving up here would hand the caller an error - * for a range no one else holds, and the write path turns - * that into a failed write. Each pass makes a fresh server - * round trip, so a revoke storm throttles this loop rather - * than spinning it. + * A revoke overlapping this range was processed while the + * request was in flight, so the grant is dead. Retry + * rather than fail: no one else holds the range, and the + * write path turns an error into a failed write. Each + * pass is a fresh round trip, so a revoke storm throttles + * the loop. */ goto restart; } diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 30fdbb26bd3daf..6f7c2c2fde0fd1 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -13,6 +13,7 @@ struct fuse_inode; +struct fuse_dlm_range; /* Lock modes for page ranges */ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; @@ -26,19 +27,25 @@ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; */ #define FUSE_DLM_GRANT_UNRECORDED 1 -/* Page cache lock manager */ +/* + * Page cache lock manager. + * + * @ranges holds the grants the client has been given and not had taken + * back. A request still on the wire covers nothing and lives on + * @pending instead, so tree walkers never filter on state. See enum + * fuse_dlm_range_state in fuse_dlm_cache.c. + */ struct fuse_dlm_cache { - /* Lock protecting the tree */ + /* Lock protecting the tree and the pending list */ struct rw_semaphore lock; - /* Interval tree of locked ranges */ + /* Interval tree of granted ranges (FUSE_DLM_RANGE_GRANTED) */ struct rb_root_cached ranges; /* - * Bumped under @lock by every revocation - * (fuse_dlm_unlock_range(), fuse_dlm_cache_release_locks()); - * lets fuse_get_dlm_lock() order recording a reply's grant - * against revokes processed while the reply was in flight. + * FUSE_DLM_WB_LOCK requests in flight (REQUESTED, or REVOKED once + * a revoke has overlapped one). Owned by the queueing thread; the + * revoke paths mark them only. */ - uint64_t revoke_gen; + struct list_head pending; }; /* Initialize a page cache lock manager */ @@ -51,13 +58,28 @@ void fuse_dlm_cache_release_locks(struct fuse_inode *inode); int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, uint64_t end, enum fuse_page_lock_mode mode); -/* As above, but refuse (-EAGAIN) if a revoke ran since @gen was sampled */ -int fuse_dlm_lock_range_gen(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode, - uint64_t gen); +/* + * Publish a FUSE_DLM_WB_LOCK for [start, end] before it is sent, so a + * revoke processed while the reply is on the wire can mark it. @req is + * caller-owned storage, live until the matching commit or abort. + */ +void fuse_dlm_request_begin(struct fuse_inode *inode, + struct fuse_dlm_range *req, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode); + +/* + * Retire @req and record the grant [start, end] as one step under the + * cache lock. -EAGAIN means a revoke overlapped @req in flight and + * nothing was recorded; the caller must request again. @req is retired + * either way. + */ +int fuse_dlm_request_commit(struct fuse_inode *inode, + struct fuse_dlm_range *req, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode); -/* Sample the revocation generation (see fuse_dlm_lock_range_gen()) */ -uint64_t fuse_dlm_revoke_gen(struct fuse_inode *inode); +/* Retire @req without recording anything */ +void fuse_dlm_request_abort(struct fuse_inode *inode, + struct fuse_dlm_range *req); /* Unlock a range of pages */ int fuse_dlm_unlock_range(struct fuse_inode *inode, uint64_t start, From 744438377947dcd72f41e18b982c66e61bb71ea8 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 11:10:09 +0200 Subject: [PATCH 04/41] fuse: make the DLM lock mode part of the range state struct fuse_dlm_range carried a lifecycle state and a mode, the mode stored as FUSE_PCACHE_LK_READ/_WRITE (1 and 2) in a field typed enum fuse_page_lock_mode, whose enumerators are 0 and 1. The two are never independent, and fuse_dlm_range_is_locked() relied on the value ordering to let a write grant cover a read. Replace GRANTED with READ and WRITE and drop the mode field and the FUSE_PCACHE_LK_* values. fuse_dlm_state_satisfies() replaces the ordinal comparison. fuse_dlm_request_begin() no longer takes a mode: the one that reaches the tree is the one passed to the commit. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 99 +++++++++++++++++++++++----------------- fs/fuse/fuse_dlm_cache.h | 7 +-- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 1795321c1f84f0..e9a59c1431d6df 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -17,8 +17,10 @@ * fuse_dlm_request_commit() drops such a grant instead of recording * it. * - * - GRANTED, in cache->ranges. The only state - * fuse_dlm_range_is_locked() reports as covered. + * - READ or WRITE, in cache->ranges. The only states + * fuse_dlm_range_is_locked() reports as covered; the mode is not a + * separate field, since a range is either not held or held in one + * definite mode. * * In-flight requests are kept off the tree so the state is consulted * only where a request is retired, not by every tree walker. @@ -38,13 +40,15 @@ enum fuse_dlm_range_state { FUSE_DLM_RANGE_REQUESTED, /* Revoked while in flight; the grant must not be recorded */ FUSE_DLM_RANGE_REVOKED, - /* Recorded grant, in cache->ranges */ - FUSE_DLM_RANGE_GRANTED, + /* Granted shared, in cache->ranges */ + FUSE_DLM_RANGE_READ, + /* Granted exclusive, in cache->ranges */ + FUSE_DLM_RANGE_WRITE, }; /* A range of pages with a lock */ struct fuse_dlm_range { - /* Interval tree node; only linked while GRANTED */ + /* Interval tree node; only linked once granted */ struct rb_node rb; /* Start page offset (inclusive) */ uint64_t start; @@ -52,17 +56,35 @@ struct fuse_dlm_range { uint64_t end; /* Subtree end value for interval tree */ uint64_t __subtree_end; - /* Lock mode, as FUSE_PCACHE_LK_READ / FUSE_PCACHE_LK_WRITE */ - enum fuse_page_lock_mode mode; - /* Lifecycle state; see enum fuse_dlm_range_state */ + /* Lifecycle and, once granted, the mode; see the enum above */ enum fuse_dlm_range_state state; /* Temporary list entry for operations, and the cache->pending link */ struct list_head list; }; -/* Lock modes for FUSE page cache */ -#define FUSE_PCACHE_LK_READ 1 /* Shared read lock */ -#define FUSE_PCACHE_LK_WRITE 2 /* Exclusive write lock */ +/* The state a grant in @mode is recorded under */ +static inline enum fuse_dlm_range_state +fuse_dlm_granted_state(enum fuse_page_lock_mode mode) +{ + return mode == FUSE_PAGE_LOCK_READ ? FUSE_DLM_RANGE_READ : + FUSE_DLM_RANGE_WRITE; +} + +/** + * fuse_dlm_state_satisfies - is a range in @held usable for @want + * @held: the state of a range recorded in the tree + * @want: FUSE_DLM_RANGE_READ or FUSE_DLM_RANGE_WRITE + * + * A WRITE grant is exclusive and so covers a READ request; nothing else + * substitutes for anything. The two pending states never appear in the + * tree and cover nothing. + */ +static inline bool fuse_dlm_state_satisfies(enum fuse_dlm_range_state held, + enum fuse_dlm_range_state want) +{ + return held == want || + (held == FUSE_DLM_RANGE_WRITE && want == FUSE_DLM_RANGE_READ); +} /* Interval tree definitions for page ranges */ static inline uint64_t fuse_dlm_range_start(struct fuse_dlm_range *range) @@ -210,8 +232,8 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, struct fuse_dlm_range, rb); } - /* Try to merge with next range if adjacent and same mode */ - if (next && range->mode == next->mode && + /* Try to merge with next range if adjacent and same state */ + if (next && range->state == next->state && range->end + 1 == next->start) { /* Merge ranges: re-insert so __subtree_end is updated */ fuse_page_it_remove(next, &cache->ranges); @@ -243,8 +265,8 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, * - READ locks are compatible with existing WRITE locks (downgrade not needed) * - WRITE locks need to upgrade existing READ locks * - * Everything inserted here is FUSE_DLM_RANGE_GRANTED: this runs only - * after the server has answered. + * Everything inserted here is READ or WRITE: this runs only after the + * server has answered. * * Caller holds the cache lock for write. * @@ -256,7 +278,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range, *new_range, *next; - int lock_mode; + enum fuse_dlm_range_state want; bool covered_to_end = false; int ret = 0; LIST_HEAD(to_lock); @@ -266,9 +288,8 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, if (!cache || start > end) return -EINVAL; - /* Convert to lock mode */ - lock_mode = (mode == FUSE_PAGE_LOCK_READ) ? FUSE_PCACHE_LK_READ : - FUSE_PCACHE_LK_WRITE; + /* The state this grant records */ + want = fuse_dlm_granted_state(mode); /* Find all ranges that overlap with [start, end] */ range = fuse_page_it_iter_first(&cache->ranges, start, end); @@ -277,8 +298,8 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, next = fuse_page_it_iter_next(range, start, end); /* Check lock compatibility */ - if (lock_mode == FUSE_PCACHE_LK_WRITE && - lock_mode != range->mode) { + if (want == FUSE_DLM_RANGE_WRITE && + range->state != FUSE_DLM_RANGE_WRITE) { /* we own the lock but have to update it. */ list_add_tail(&range->list, &to_upgrade); } @@ -294,8 +315,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = range->start - 1; - new_range->mode = lock_mode; - new_range->state = FUSE_DLM_RANGE_GRANTED; + new_range->state = want; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -321,8 +341,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = end; - new_range->mode = lock_mode; - new_range->state = FUSE_DLM_RANGE_GRANTED; + new_range->state = want; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -331,7 +350,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* update locks, if any lock is in this list it has the wrong mode */ list_for_each_entry(range, &to_upgrade, list) { /* Update the lock mode */ - range->mode = lock_mode; + range->state = want; } /* Add all new ranges to the tree */ @@ -356,9 +375,9 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* Restore original lock modes for any partially upgraded locks */ list_for_each_entry(range, &to_upgrade, list) { - if (lock_mode == FUSE_PCACHE_LK_WRITE) { + if (want == FUSE_DLM_RANGE_WRITE) { /* We upgraded this lock but failed later, downgrade it back */ - range->mode = FUSE_PCACHE_LK_READ; + range->state = FUSE_DLM_RANGE_READ; } } @@ -384,7 +403,10 @@ int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, * @req: caller-owned storage for the request, live until commit or abort * @start: start page offset being requested (inclusive) * @end: end page offset being requested (inclusive) - * @mode: FUSE_PAGE_LOCK_READ or FUSE_PAGE_LOCK_WRITE + * + * The mode is not recorded here: until the server answers the range is + * held in neither, and the mode that reaches the tree is the one passed + * to fuse_dlm_request_commit(). * * A FUSE_DLM_WB_LOCK reply and a NOTIFY revoke are serviced on different * threads, so a revoke can be processed before the grant the reply @@ -400,15 +422,13 @@ int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, */ void fuse_dlm_request_begin(struct fuse_inode *inode, struct fuse_dlm_range *req, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode) + uint64_t end) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; RB_CLEAR_NODE(&req->rb); req->start = start; req->end = end; - req->mode = (mode == FUSE_PAGE_LOCK_READ) ? FUSE_PCACHE_LK_READ : - FUSE_PCACHE_LK_WRITE; req->state = FUSE_DLM_RANGE_REQUESTED; down_write(&cache->lock); @@ -623,17 +643,14 @@ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range; - int lock_mode = 0; + enum fuse_dlm_range_state want; uint64_t current_start = start; if (!cache || start > end) return false; - /* Convert to lock mode if specified */ - if (mode == FUSE_PAGE_LOCK_READ) - lock_mode = FUSE_PCACHE_LK_READ; - else if (mode == FUSE_PAGE_LOCK_WRITE) - lock_mode = FUSE_PCACHE_LK_WRITE; + /* The state a range has to be in to cover this request */ + want = fuse_dlm_granted_state(mode); down_read(&cache->lock); @@ -650,7 +667,7 @@ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, * re-requesting a READ lock for a range we already hold * a WRITE lock on (e.g. read-after-write). */ - if (lock_mode && range->mode < lock_mode) { + if (!fuse_dlm_state_satisfies(range->state, want)) { /* Held lock is weaker than requested */ up_read(&cache->lock); return false; @@ -708,7 +725,7 @@ bool fuse_dlm_write_grant_exists(struct fuse_inode *fi) down_read(&cache->lock); for (range = fuse_dlm_find_overlapping(cache, 0, U64_MAX); range; range = fuse_page_it_iter_next(range, 0, U64_MAX)) { - if (range->mode == FUSE_PCACHE_LK_WRITE) { + if (range->state == FUSE_DLM_RANGE_WRITE) { held = true; break; } @@ -810,7 +827,7 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, args.out_args[0].value = &outarg; /* Publish before sending; see fuse_dlm_request_begin() */ - fuse_dlm_request_begin(fi, &req, inarg.start, inarg.end, mode); + fuse_dlm_request_begin(fi, &req, inarg.start, inarg.end); err = fuse_simple_request(fm, &args); if (err) { diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 6f7c2c2fde0fd1..383a9a2174950f 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -38,7 +38,7 @@ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; struct fuse_dlm_cache { /* Lock protecting the tree and the pending list */ struct rw_semaphore lock; - /* Interval tree of granted ranges (FUSE_DLM_RANGE_GRANTED) */ + /* Interval tree of granted ranges (FUSE_DLM_RANGE_READ/_WRITE) */ struct rb_root_cached ranges; /* * FUSE_DLM_WB_LOCK requests in flight (REQUESTED, or REVOKED once @@ -61,11 +61,12 @@ int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, /* * Publish a FUSE_DLM_WB_LOCK for [start, end] before it is sent, so a * revoke processed while the reply is on the wire can mark it. @req is - * caller-owned storage, live until the matching commit or abort. + * caller-owned storage, live until the matching commit or abort. The + * mode is not recorded until the grant is, so only the commit takes it. */ void fuse_dlm_request_begin(struct fuse_inode *inode, struct fuse_dlm_range *req, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode); + uint64_t end); /* * Retire @req and record the grant [start, end] as one step under the From 83e334c152cab61ff94811eabb17eb539159b827 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 11:17:08 +0200 Subject: [PATCH 05/41] fuse: record what the page cache under a DLM range holds A granted range does not say whether anything is cached under it, so a revoke assumes the worst and drops with invalidate_inode_pages2_range(), which launders dirty folios and waits for a FUSE_WRITE reply. Add enum fuse_dlm_range_content: nothing cached, data the server has seen, or data it has not. It is an upper bound, raised in fuse_dlm_range_touched() from the two points that authorise cached IO, so it moves before the data lands. Only fuse_dlm_ranges_flushed() lowers it, under i_rwsem held exclusive; a mapped inode is skipped. No caller acts on this yet. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 18 ++++ fs/fuse/fuse_dlm_cache.c | 198 +++++++++++++++++++++++++++++++++++++-- fs/fuse/fuse_dlm_cache.h | 21 +++++ 3 files changed, 229 insertions(+), 8 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index e1ce3c0487da33..ea8471cb673e80 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -683,6 +683,14 @@ static int fuse_fsync(struct file *file, loff_t start, loff_t end, if (err) goto out; + /* + * [start, end] is on the server now, so the DLM ranges inside it + * go back to clean. i_rwsem is held exclusive here, which keeps a + * cached write from dirtying them again first. + */ + if (fc->dlm && fc->writeback_cache) + fuse_dlm_ranges_flushed(get_fuse_inode(inode), start, end); + err = sync_inode_metadata(inode, 1); if (err) goto out; @@ -3100,6 +3108,16 @@ static int fuse_get_page_mkwrite_lock(struct file *file, loff_t offset, size_t l fuse_abort_conn(fc); err = -EINVAL; } + + /* + * The fault is about to dirty this page. This grant is not + * recorded, so raise the bound on whatever range covers the page; + * a page outside every range already reports dirty. + */ + if (!err && fc->dlm) + fuse_dlm_range_touched(get_fuse_inode(inode), inarg.start, + inarg.end, FUSE_PAGE_LOCK_WRITE); + return err; } /* diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index e9a59c1431d6df..e86d127f987a0b 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -24,6 +24,11 @@ * * In-flight requests are kept off the tree so the state is consulted * only where a request is retired, not by every tree walker. + * + * A granted range also records what the page cache under it may hold + * (enum fuse_dlm_range_content). That is an upper bound: lowering it + * while data is still dirty would let a revoke drop unwritten data, so + * only fuse_dlm_ranges_flushed() lowers it. */ #include "fuse_i.h" #include "fuse_dlm_cache.h" @@ -46,6 +51,19 @@ enum fuse_dlm_range_state { FUSE_DLM_RANGE_WRITE, }; +/* + * What the page cache under a granted range may hold. Ordered so that + * raising the bound is a max(); only fuse_dlm_ranges_flushed() lowers it. + */ +enum fuse_dlm_range_content { + /* Nothing cached under this grant */ + FUSE_DLM_CONTENT_EMPTY, + /* May hold data the server has already seen */ + FUSE_DLM_CONTENT_CLEAN, + /* May hold data the server has not seen */ + FUSE_DLM_CONTENT_DIRTY, +}; + /* A range of pages with a lock */ struct fuse_dlm_range { /* Interval tree node; only linked once granted */ @@ -58,6 +76,8 @@ struct fuse_dlm_range { uint64_t __subtree_end; /* Lifecycle and, once granted, the mode; see the enum above */ enum fuse_dlm_range_state state; + /* Upper bound on the page cache under this range; see the enum */ + enum fuse_dlm_range_content content; /* Temporary list entry for operations, and the cache->pending link */ struct list_head list; }; @@ -232,8 +252,14 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, struct fuse_dlm_range, rb); } - /* Try to merge with next range if adjacent and same state */ + /* + * Merge only neighbours agreeing on both state and content: + * a coalesced range carries one content bound for all of + * itself, so merging across a content boundary would lose + * where that bound actually applies. + */ if (next && range->state == next->state && + range->content == next->content && range->end + 1 == next->start) { /* Merge ranges: re-insert so __subtree_end is updated */ fuse_page_it_remove(next, &cache->ranges); @@ -316,6 +342,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = range->start - 1; new_range->state = want; + new_range->content = FUSE_DLM_CONTENT_EMPTY; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -342,6 +369,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = end; new_range->state = want; + new_range->content = FUSE_DLM_CONTENT_EMPTY; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -430,6 +458,8 @@ void fuse_dlm_request_begin(struct fuse_inode *inode, req->start = start; req->end = end; req->state = FUSE_DLM_RANGE_REQUESTED; + /* Nothing reads this while the request is pending; publish it set */ + req->content = FUSE_DLM_CONTENT_EMPTY; down_write(&cache->lock); list_add_tail(&req->list, &cache->pending); @@ -488,6 +518,142 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, up_write(&cache->lock); } +/** + * fuse_dlm_range_touched - record that IO is about to reach the page cache + * @inode: the fuse inode + * @start: start page offset the IO covers (inclusive) + * @end: end page offset the IO covers (inclusive) + * @mode: FUSE_PAGE_LOCK_READ if the range is only being populated, + * FUSE_PAGE_LOCK_WRITE if it is being dirtied + * + * Raises the content bound of every granted range overlapping + * [start, end], never lowers one. + * + * Called from fuse_get_dlm_lock() and fuse_get_page_mkwrite_lock(), the + * two points that authorise cached IO under DLM, rather than from the + * page cache: no cached IO reaches a folio without passing one of them, + * so the bound is raised before the data lands. + * + * The bound is per tree node, and a node can be wider than the IO (the + * server may grant more than was asked for, and grants merge), so a 4K + * write marks whatever node covers it. + */ +void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + enum fuse_dlm_range_content level; + struct fuse_dlm_range *range; + + if (start > end) + return; + + level = mode == FUSE_PAGE_LOCK_WRITE ? FUSE_DLM_CONTENT_DIRTY : + FUSE_DLM_CONTENT_CLEAN; + + down_write(&cache->lock); + for (range = fuse_dlm_find_overlapping(cache, start, end); range; + range = fuse_page_it_iter_next(range, start, end)) + if (range->content < level) + range->content = level; + up_write(&cache->lock); +} + +/** + * fuse_dlm_ranges_flushed - [start, end] of the page cache is on the server + * @inode: the fuse inode + * @start: start byte offset written back and waited out (inclusive) + * @end: end byte offset written back and waited out (inclusive) + * + * Moves ranges lying wholly inside [start, end] back to clean. This is + * the one transition that lowers the bound, so nothing may be dirtying + * the mapping while it runs: + * + * - A cached write holds i_rwsem, which the caller holds exclusive. + * + * - A fault does not, so bail out if the inode is mapped. The test is + * made under the cache lock, and a fault can only dirty a folio after + * fuse_get_page_mkwrite_lock() has taken that same lock, so a mapping + * created after the test cannot get past this. + * + * A range only partly inside [start, end] keeps its bound: the rest of + * it was not written back. + */ +void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, + uint64_t end) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + struct fuse_dlm_range *range; + + if (start > end) + return; + + down_write(&cache->lock); + + if (mapping_mapped(inode->inode.i_mapping)) + goto out; + + for (range = fuse_dlm_find_overlapping(cache, start, end); range; + range = fuse_page_it_iter_next(range, start, end)) + if (range->start >= start && range->end <= end && + range->content == FUSE_DLM_CONTENT_DIRTY) + range->content = FUSE_DLM_CONTENT_CLEAN; + +out: + up_write(&cache->lock); +} + +/** + * fuse_dlm_range_may_be_dirty - can [start, end] hold unwritten data + * @inode: the fuse inode + * @start: start page offset (inclusive) + * @end: end page offset (inclusive) + * + * A part of the range with no recorded grant counts as dirty, which + * covers pages dirtied through fuse_get_page_mkwrite_lock() (no grant is + * recorded there) and grants that failed to record. + * + * Return: false only when every page of [start, end] is covered by a + * grant not written under since it was last flushed. + */ +bool fuse_dlm_range_may_be_dirty(struct fuse_inode *inode, uint64_t start, + uint64_t end) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + struct fuse_dlm_range *range; + uint64_t current_start = start; + bool covered_to_end = false; + bool dirty = false; + + if (!cache || start > end) + return true; + + down_read(&cache->lock); + + for (range = fuse_dlm_find_overlapping(cache, start, end); range; + range = fuse_page_it_iter_next(range, start, end)) { + /* A gap before this range: nothing is recorded for it */ + if (current_start < range->start || + range->content == FUSE_DLM_CONTENT_DIRTY) { + dirty = true; + break; + } + + if (range->end >= end) { + covered_to_end = true; + break; + } + + /* Safe: range->end < end, so this cannot wrap */ + current_start = range->end + 1; + } + + up_read(&cache->lock); + + /* A gap at the tail counts the same as one in the middle */ + return dirty || !covered_to_end; +} + /** * fuse_dlm_punch_hole - Punch a hole in a locked range * @cache: The page cache @@ -789,12 +955,21 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, struct fuse_dlm_lock_in inarg; struct fuse_dlm_lock_out outarg; struct fuse_dlm_range req; + uint64_t pg_start, pg_end; int err; /* An empty range needs no lock. */ if (!length) return 0; + /* + * note that the offset and length don't have to be page aligned + * here but since we only get here on writeback caching we will + * send out page aligned requests + */ + pg_start = (uint64_t)offset & PAGE_MASK; + pg_end = ((uint64_t)offset + length - 1) | (PAGE_SIZE - 1); + restart: /* note that this can be run from different processes * at the same time. It is intentionally not protected @@ -803,17 +978,17 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, * The early exit uses the same helper the callers re-validate * with, so this check and a later fuse_dlm_lock_is_held() can * never disagree about what counts as covered. */ - if (fuse_dlm_lock_is_held(fi, offset, length, mode)) - return 0; /* we already have this area locked */ + if (fuse_dlm_lock_is_held(fi, offset, length, mode)) { + /* we already have this area locked */ + fuse_dlm_range_touched(fi, pg_start, pg_end, mode); + return 0; + } memset(&inarg, 0, sizeof(inarg)); inarg.fh = ff->fh; - /* note that the offset and length don't have to be page aligned - * here but since we only get here on writeback caching we will - * send out page aligned requests */ - inarg.start = offset & PAGE_MASK; - inarg.end = (offset + length - 1) | (PAGE_SIZE - 1); + inarg.start = pg_start; + inarg.end = pg_end; inarg.type = (mode == FUSE_PAGE_LOCK_WRITE) ? FUSE_DLM_LOCK_WRITE : FUSE_DLM_LOCK_READ; @@ -862,6 +1037,13 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, goto restart; } + /* + * Raise the content bound before the caller touches the page + * cache. A grant that failed to record has no range to raise; + * fuse_dlm_range_may_be_dirty() reports it dirty anyway. + */ + fuse_dlm_range_touched(fi, pg_start, pg_end, mode); + /* * A failure to record (small-allocation -ENOMEM) does not undo * the grant: coverage exists cluster-wide, only the local diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 383a9a2174950f..4294705b766e0b 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -97,6 +97,27 @@ bool fuse_dlm_lock_is_held(struct fuse_inode *inode, loff_t offset, /* Is any part of the file held for write? */ bool fuse_dlm_write_grant_exists(struct fuse_inode *inode); +/* + * Record that cached IO in @mode is about to reach the page cache over + * [start, end]. Only raises the recorded content, never lowers it. + */ +void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode); + +/* + * [start, end] has been written back and waited out. Caller holds + * i_rwsem exclusive; a mapped inode is left alone. + */ +void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, + uint64_t end); + +/* + * Can [start, end] hold data the server has not seen? A part of it with + * no recorded grant counts as dirty. + */ +bool fuse_dlm_range_may_be_dirty(struct fuse_inode *inode, uint64_t start, + uint64_t end); + /* This is the interface to the filesystem */ int fuse_get_dlm_lock(struct file *file, loff_t offset, size_t length, enum fuse_page_lock_mode mode); From a4ab6d83b23276d2cbc83a3469339810696c1677 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 11:48:29 +0200 Subject: [PATCH 06/41] fuse: record the exact byte range a cached write dirties Grants are page aligned, buffered writes need not be, and fuse_dlm_range_touched() marked whole tree nodes: a 4K write into a 1M grant claimed the whole grant, so the record could not say which part of a boundary page this client wrote. Split at both ends of the marked region and mark only what it covers. Both halves keep state and content, so grant coverage is unchanged and fuse_dlm_try_merge() recoalesces. fuse_get_dlm_lock() stops marking on the write path, where its range is the page aligned request; fuse_cache_write_iter() marks the exact range instead. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 11 +++++ fs/fuse/fuse_dlm_cache.c | 97 +++++++++++++++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index ea8471cb673e80..259d09f0b74ad2 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -2194,6 +2194,17 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) pagecache_isize_extended(inode, orig_size, pos); } + /* + * Mark the exact bytes about to be dirtied, before they + * are. The DLM grant covering them is page aligned and + * this is not; that difference is the record of which part + * of a boundary page this client actually wrote. A short + * write leaves the unreached tail marked, which overstates. + */ + if (fc->dlm) + fuse_dlm_range_touched(fi, pos, end - 1, + FUSE_PAGE_LOCK_WRITE); + /* * Under DLM the unaligned edges go through to the server * instead of being completed by a read-modify-write READ diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index e86d127f987a0b..1dbfcb6969e233 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -518,6 +518,51 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, up_write(&cache->lock); } +/** + * fuse_dlm_split_at - make @off start a range + * @cache: The page cache + * @off: byte offset to split at + * + * Splits the range containing @off in two, both halves keeping the state + * and content of the original, so a later marking can apply to one side + * only. A no-op when @off already starts a range or falls in a gap. + * + * Caller holds @cache->lock for write. + * + * Return: 0, or -ENOMEM. A caller that cannot split must mark more than + * it meant to, never less. + */ +static int fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) +{ + struct fuse_dlm_range *range, *tail; + + if (!off) + return 0; + + range = fuse_page_it_iter_first(&cache->ranges, off, off); + if (!range || range->start == off) + return 0; + + tail = kmalloc(sizeof(*tail), GFP_KERNEL); + if (!tail) + return -ENOMEM; + + *tail = *range; + INIT_LIST_HEAD(&tail->list); + tail->start = off; + + /* + * Bounds are never edited in place: the interval tree caches a + * subtree end that only insertion recomputes. + */ + fuse_page_it_remove(range, &cache->ranges); + range->end = off - 1; + fuse_page_it_insert(range, &cache->ranges); + fuse_page_it_insert(tail, &cache->ranges); + + return 0; +} + /** * fuse_dlm_range_touched - record that IO is about to reach the page cache * @inode: the fuse inode @@ -526,17 +571,17 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, * @mode: FUSE_PAGE_LOCK_READ if the range is only being populated, * FUSE_PAGE_LOCK_WRITE if it is being dirtied * - * Raises the content bound of every granted range overlapping - * [start, end], never lowers one. + * Raises the content bound over exactly [start, end], never lowers it. + * Grants are page aligned but a write need not be, so ranges are split + * at both ends first and only the covered part is marked; the untouched + * remainder of a boundary page keeps its own bound and stays out of + * writeback. If a split cannot be allocated the whole overlapping range + * is marked, which overstates rather than understates. * - * Called from fuse_get_dlm_lock() and fuse_get_page_mkwrite_lock(), the - * two points that authorise cached IO under DLM, rather than from the - * page cache: no cached IO reaches a folio without passing one of them, - * so the bound is raised before the data lands. - * - * The bound is per tree node, and a node can be wider than the IO (the - * server may grant more than was asked for, and grants merge), so a 4K - * write marks whatever node covers it. + * Called from fuse_get_dlm_lock(), fuse_cache_write_iter() and + * fuse_get_page_mkwrite_lock(), which between them cover every way + * cached IO reaches a folio under DLM, so the bound is raised before the + * data lands. */ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, uint64_t end, enum fuse_page_lock_mode mode) @@ -552,13 +597,41 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, FUSE_DLM_CONTENT_CLEAN; down_write(&cache->lock); + + /* + * A split that cannot allocate leaves the range whole, and the + * loop below then marks more than was written. + */ + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); + for (range = fuse_dlm_find_overlapping(cache, start, end); range; range = fuse_page_it_iter_next(range, start, end)) if (range->content < level) range->content = level; + + /* Recoalesce whatever the split left equal on both sides */ + fuse_dlm_try_merge(cache, start, end); + up_write(&cache->lock); } +/* + * Marking done by fuse_get_dlm_lock() itself. A read populates whole + * pages, so the page aligned request range is the right thing to mark. + * A write grant is not marked here: the request range is page aligned + * and the write inside it need not be, and marking the alignment would + * claim bytes the writer never touched. fuse_cache_write_iter() marks + * the exact range instead, before it writes. + */ +static void fuse_dlm_mark_populated(struct fuse_inode *inode, uint64_t start, + uint64_t end, enum fuse_page_lock_mode mode) +{ + if (mode == FUSE_PAGE_LOCK_READ) + fuse_dlm_range_touched(inode, start, end, mode); +} + /** * fuse_dlm_ranges_flushed - [start, end] of the page cache is on the server * @inode: the fuse inode @@ -980,7 +1053,7 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, * never disagree about what counts as covered. */ if (fuse_dlm_lock_is_held(fi, offset, length, mode)) { /* we already have this area locked */ - fuse_dlm_range_touched(fi, pg_start, pg_end, mode); + fuse_dlm_mark_populated(fi, pg_start, pg_end, mode); return 0; } @@ -1042,7 +1115,7 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, * cache. A grant that failed to record has no range to raise; * fuse_dlm_range_may_be_dirty() reports it dirty anyway. */ - fuse_dlm_range_touched(fi, pg_start, pg_end, mode); + fuse_dlm_mark_populated(fi, pg_start, pg_end, mode); /* * A failure to record (small-allocation -ENOMEM) does not undo From a259b4545b9a90346705ae7e5a1b66bd76d3bbf7 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 11:52:15 +0200 Subject: [PATCH 07/41] fuse: write back only the bytes a DLM range records as written Writeback sent every dirty folio whole, which is why an unaligned edge cannot be cached: the untouched remainder of a boundary page would go to the server with it. fuse_dlm_dirty_run() walks forward while the recorded content stays the same, so fuse_iomap_writeback_range() sends one written run at a time. An unwritten run is reported as IOMAP_HOLE, which iomap skips, and a folio with no written run has its writeback ended by iomap itself. A range not covered by write grants throughout still goes whole: only a write grant makes this client the one that dirties it. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 164 +++++++++++++++++++++++++++++++++++++-- fs/fuse/fuse_dlm_cache.c | 74 ++++++++++++++++++ fs/fuse/fuse_dlm_cache.h | 8 ++ 3 files changed, 239 insertions(+), 7 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 259d09f0b74ad2..01566cbcffc204 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -556,11 +556,15 @@ u64 fuse_lock_owner_id(struct fuse_conn *fc, fl_owner_t id) return (u64) v0 + ((u64) v1 << 32); } +struct fuse_wb_token; + struct fuse_writepage_args { struct fuse_io_args ia; struct list_head queue_entry; struct inode *inode; struct fuse_sync_bucket *bucket; + /* One per entry of ia.ap.folios, see struct fuse_wb_token */ + struct fuse_wb_token **tokens; }; /* @@ -2603,6 +2607,77 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out, return iter_file_splice_write(pipe, out, ppos, len, flags); } +/* + * A folio is written back one recorded run at a time, and a run that does + * not reach both folio edges forces a new request, so the runs of one folio + * end up in requests that complete independently. iomap counts a folio's + * outstanding writes in ifs->write_bytes_pending, but a folio of a single + * block carries no iomap_folio_state, and there iomap_finish_folio_write() + * ends the writeback on every call. Count the runs here instead and end + * the folio writeback once, on the last one. + */ +struct fuse_wb_token { + refcount_t refs; + struct inode *inode; + struct folio *folio; +}; + +/* + * Take @folio into writeback and open the count. The caller keeps the + * returned reference as a bias, so the count cannot reach zero while + * further runs of the same folio are still being queued. + */ +static struct fuse_wb_token *fuse_wb_token_alloc(struct inode *inode, + struct folio *folio) +{ + struct fuse_wb_token *token; + + /* As iomap allocates the state this stands in for */ + token = kmalloc(sizeof(*token), GFP_NOFS | __GFP_NOFAIL); + refcount_set(&token->refs, 1); + token->inode = inode; + token->folio = folio; + iomap_start_folio_write(inode, folio, 1); + + return token; +} + +static struct fuse_wb_token *fuse_wb_token_get(struct fuse_wb_token *token) +{ + refcount_inc(&token->refs); + return token; +} + +static void fuse_wb_token_put(struct fuse_wb_token *token) +{ + if (token && refcount_dec_and_test(&token->refs)) { + iomap_finish_folio_write(token->inode, token->folio, 1); + kfree(token); + } +} + +/* + * The folios, descs and tokens of a writeback request come from one + * allocation, which kfree(ap->folios) releases. + */ +static struct folio **fuse_wb_folios_alloc(unsigned int nfolios, gfp_t flags, + struct fuse_folio_desc **descs, + struct fuse_wb_token ***tokens) +{ + struct folio **folios; + + folios = kzalloc(nfolios * (sizeof(struct folio *) + + sizeof(struct fuse_folio_desc) + + sizeof(struct fuse_wb_token *)), flags); + if (!folios) + return NULL; + + *descs = (void *) (folios + nfolios); + *tokens = (void *) (*descs + nfolios); + + return folios; +} + static void fuse_writepage_free(struct fuse_writepage_args *wpa) { struct fuse_args_pages *ap = &wpa->ia.ap; @@ -2629,7 +2704,7 @@ static void fuse_writepage_finish(struct fuse_writepage_args *wpa) * scope of the fi->lock alleviates xarray lock * contention and noticeably improves performance. */ - iomap_finish_folio_write(inode, ap->folios[i], 1); + fuse_wb_token_put(wpa->tokens[i]); wake_up(&fi->page_waitq); } @@ -2777,7 +2852,8 @@ static struct fuse_writepage_args *fuse_writepage_args_alloc(void) if (wpa) { ap = &wpa->ia.ap; ap->num_folios = 0; - ap->folios = fuse_folios_alloc(1, GFP_NOFS, &ap->descs); + ap->folios = fuse_wb_folios_alloc(1, GFP_NOFS, &ap->descs, + &wpa->tokens); if (!ap->folios) { kfree(wpa); wpa = NULL; @@ -2802,13 +2878,15 @@ static void fuse_writepage_add_to_bucket(struct fuse_conn *fc, } static void fuse_writepage_args_page_fill(struct fuse_writepage_args *wpa, struct folio *folio, - uint32_t folio_index, loff_t offset, unsigned len) + uint32_t folio_index, loff_t offset, unsigned int len, + struct fuse_wb_token *token) { struct fuse_args_pages *ap = &wpa->ia.ap; ap->folios[folio_index] = folio; ap->descs[folio_index].offset = offset; ap->descs[folio_index].length = len; + wpa->tokens[folio_index] = fuse_wb_token_get(token); } static struct fuse_writepage_args *fuse_writepage_args_setup(struct folio *folio, @@ -2841,6 +2919,12 @@ struct fuse_fill_wb_data { struct fuse_writepage_args *wpa; struct fuse_file *ff; unsigned int max_folios; + /* + * The folio currently being split into runs, and the count that + * holds its writeback open until the last run has been queued. + */ + struct folio *wb_folio; + struct fuse_wb_token *wb_token; /* * nr_bytes won't overflow since fuse_writepage_need_send() caps * wb requests to never exceed fc->max_pages (which has an upper bound @@ -2855,21 +2939,25 @@ static bool fuse_pages_realloc(struct fuse_fill_wb_data *data, struct fuse_args_pages *ap = &data->wpa->ia.ap; struct folio **folios; struct fuse_folio_desc *descs; + struct fuse_wb_token **tokens; unsigned int nfolios = min_t(unsigned int, max_t(unsigned int, data->max_folios * 2, FUSE_DEFAULT_MAX_PAGES_PER_REQ), max_pages); WARN_ON(nfolios <= data->max_folios); - folios = fuse_folios_alloc(nfolios, GFP_NOFS, &descs); + folios = fuse_wb_folios_alloc(nfolios, GFP_NOFS, &descs, &tokens); if (!folios) return false; memcpy(folios, ap->folios, sizeof(struct folio *) * ap->num_folios); memcpy(descs, ap->descs, sizeof(struct fuse_folio_desc) * ap->num_folios); + memcpy(tokens, data->wpa->tokens, + sizeof(struct fuse_wb_token *) * ap->num_folios); kfree(ap->folios); ap->folios = folios; ap->descs = descs; + data->wpa->tokens = tokens; data->max_folios = nfolios; return true; @@ -2951,10 +3039,60 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, struct inode *inode = wpc->inode; struct fuse_inode *fi = get_fuse_inode(inode); struct fuse_conn *fc = get_fuse_conn(inode); - loff_t offset = offset_in_folio(folio, pos); + loff_t offset; WARN_ON_ONCE(!data); + /* + * A folio iomap has not asked about before: the one before it has all + * of its runs queued, so let go of the bias holding its count open. + */ + if (data->wb_folio != folio) { + fuse_wb_token_put(data->wb_token); + data->wb_token = NULL; + data->wb_folio = folio; + } + + /* + * Under DLM the page cache can hold a partly written folio: a write + * that is not page aligned dirties only its own bytes, and the rest + * of the boundary page was never read in. Send only what was + * written, so the untouched remainder is not handed to the server. + * + * fuse_dlm_dirty_run() reports the run from @pos that is uniformly + * written or uniformly not, and refuses to answer for a range not + * covered by write grants throughout, where something outside its + * record could have dirtied the folio. In that case the whole + * range goes, as it did before. + * + * A run that was not written is reported to iomap as a hole, which + * skips it and, for a folio with no written run at all, ends the + * folio writeback itself. iomap calls back for the next run. + */ + if (fc->dlm && fc->writeback_cache) { + bool dirty; + size_t run = fuse_dlm_dirty_run(fi, pos, len, &dirty); + + /* + * wpc->iomap.type carries over from the previous run and from + * the previous folio, so anything that is written has to say + * so. Left at a stale IOMAP_HOLE, iomap takes the folio for + * one it never queued and ends its writeback while the write + * is still in flight. + */ + wpc->iomap.type = IOMAP_MAPPED; + + if (run) { + if (!dirty) { + wpc->iomap.type = IOMAP_HOLE; + return run; + } + len = run; + } + } + + offset = offset_in_folio(folio, pos); + if (!data->ff) { data->ff = fuse_write_file_get(fi); if (!data->ff) @@ -2976,9 +3114,16 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, ap = &wpa->ia.ap; } - iomap_start_folio_write(inode, folio, 1); + /* + * The first run of this folio that is actually sent takes it into + * writeback. A folio with no run at all never gets here, and iomap + * ends its writeback itself. + */ + if (!data->wb_token) + data->wb_token = fuse_wb_token_alloc(inode, folio); + fuse_writepage_args_page_fill(wpa, folio, ap->num_folios, - offset, len); + offset, len, data->wb_token); data->nr_bytes += len; ap->num_folios++; @@ -2995,6 +3140,11 @@ static int fuse_iomap_writeback_submit(struct iomap_writepage_ctx *wpc, WARN_ON_ONCE(!data); + /* No more runs are coming for the folio last seen */ + fuse_wb_token_put(data->wb_token); + data->wb_token = NULL; + data->wb_folio = NULL; + if (data->wpa) { WARN_ON(!data->wpa->ia.ap.num_folios); fuse_writepages_send(wpc->inode, data); diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 1dbfcb6969e233..cfd383188fa4ca 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -676,6 +676,80 @@ void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } +/** + * fuse_dlm_dirty_run - length of the uniform run starting at @pos + * @inode: the fuse inode + * @pos: byte offset to start at + * @len: bytes of interest from @pos + * @dirty: set to whether the returned run was written under a grant + * + * Walks forward from @pos while the recorded content stays the same and + * returns how far that run reaches, capped at @len. Lets writeback ask + * which part of a folio this client actually wrote, so the untouched + * remainder of a boundary page is not sent to the server. + * + * Return: the run length, or 0 when the record cannot answer for + * [pos, pos + len). That happens when the range is not covered by write + * grants throughout, in which case something outside this record may + * have dirtied it and the caller must write the whole range. + */ +size_t fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, size_t len, + bool *dirty) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + struct fuse_dlm_range *range; + uint64_t end, cur = pos; + bool run_dirty = false; + bool first = true; + size_t run = 0; + + if (!len) + return 0; + end = pos + len - 1; + + down_read(&cache->lock); + + for (range = fuse_dlm_find_overlapping(cache, pos, end); range; + range = fuse_page_it_iter_next(range, pos, end)) { + bool range_dirty; + + /* + * A gap, or a range held only for read: this client cannot + * have written it, but nothing here proves nobody else + * dirtied the folio, so refuse to answer. + */ + if (range->start > cur || range->state != FUSE_DLM_RANGE_WRITE) + goto unknown; + + range_dirty = range->content == FUSE_DLM_CONTENT_DIRTY; + if (first) { + run_dirty = range_dirty; + first = false; + } else if (range_dirty != run_dirty) { + goto out; /* the run ends where content changes */ + } + + if (range->end >= end) { + run = len; + goto out; + } + + /* Safe: range->end < end, so this cannot wrap */ + cur = range->end + 1; + run = cur - pos; + } + + /* Ran out of ranges before reaching @end */ +unknown: + up_read(&cache->lock); + return 0; + +out: + up_read(&cache->lock); + *dirty = run_dirty; + return run; +} + /** * fuse_dlm_range_may_be_dirty - can [start, end] hold unwritten data * @inode: the fuse inode diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 4294705b766e0b..acd0e6584a1e98 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -111,6 +111,14 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, uint64_t end); +/* + * Length of the run from @pos, capped at @len, over which the recorded + * content does not change; @dirty says which it is. 0 means the record + * cannot answer and the caller must assume the whole range is dirty. + */ +size_t fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, size_t len, + bool *dirty); + /* * Can [start, end] hold data the server has not seen? A part of it with * no recorded grant counts as dirty. From ef3f058a44246142f9af7eb473d20a5044512a47 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 12:09:33 +0200 Subject: [PATCH 08/41] fuse: let a NOTIFY invalidate ask what it has to do A data invalidation always dropped the range with invalidate_inode_pages2_range(), which launders dirty folios and waits for a FUSE_WRITE reply, even for a range holding nothing. Ask instead, under the gate so nothing populates or dirties in between. filemap_range_has_page() says whether anything is cached, and fuse_dlm_range_may_be_dirty() whether laundering is needed; when it is not, invalidate_mapping_pages() drops the same folios without waiting for the server. fuse_cache_write_iter() also marks the folio pagecache_isize_extended() dirties, or the record would report it clean. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 14 ++++++- fs/fuse/inode.c | 97 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 01566cbcffc204..d80bf9299cba08 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -2193,9 +2193,19 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) } spin_unlock(&fi->lock); - /* Zero the tail of the folio straddling the old EOF. */ - if (extended && orig_size < pos) + /* + * Zero the tail of the folio straddling the old EOF. + * That dirties it without going through the write + * below, so raise the record first or it would keep + * calling that page clean. + */ + if (extended && orig_size < pos) { + if (fc->dlm) + fuse_dlm_range_touched(fi, orig_size, + pos - 1, + FUSE_PAGE_LOCK_WRITE); pagecache_isize_extended(inode, orig_size, pos); + } } /* diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index bb52782c064d3b..4b93b121ad721a 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -910,22 +910,28 @@ static void fuse_dlm_revoke_inval_range(struct fuse_inode *fi, loff_t offset, * Drop a page-cache range on behalf of a NOTIFY invalidate. * * invalidate_inode_pages2_range() waits out folios under writeback and - * launders dirty ones, both of which need a FUSE_WRITE reply. While - * writepages are frozen (fuse_set_nowrite(): truncate, O_TRUNC open, fsync, - * pre-SETATTR flush) no reply can arrive, because fuse_flush_writepages() - * parks the request on fi->queued_writes until fuse_release_nowrite(). A - * server that revokes from inside the handler it is revoking for then - * deadlocks against its own reply. fuse_do_setattr() states the same rule - * for its own invalidate. + * launders dirty ones, both of which need a FUSE_WRITE reply. It is only + * needed when the range can hold data the server has not seen. * - * So while frozen use invalidate_mapping_pages(), which skips dirty and - * under-writeback folios and never blocks. The stale clean folios still - * go, and the freezes that span a request drop the cache themselves once - * they complete: fuse_do_setattr() invalidates the mapping after releasing - * the freeze, the O_TRUNC open path calls truncate_pagecache(). + * @may_be_dirty false says it cannot, on the strength of the DLM range + * record: every way a folio gets dirtied under a grant raises that record + * before the data lands, so a range it reports clean has no dirty folio to + * launder. invalidate_mapping_pages() then drops the same folios without + * ever waiting for the server. + * + * The same substitution is forced while writepages are frozen + * (fuse_set_nowrite(): truncate, O_TRUNC open, fsync, pre-SETATTR flush), + * where no reply can arrive because fuse_flush_writepages() parks the + * request on fi->queued_writes until fuse_release_nowrite(). A server that + * revokes from inside the handler it is revoking for would otherwise + * deadlock against its own reply. fuse_do_setattr() states the same rule + * for its own invalidate. There the dirty folios are left behind, and the + * freezes that span a request drop the cache themselves once they complete: + * fuse_do_setattr() invalidates the mapping after releasing the freeze, the + * O_TRUNC open path calls truncate_pagecache(). */ static void fuse_notify_invalidate_range(struct inode *inode, pgoff_t start, - pgoff_t end) + pgoff_t end, bool may_be_dirty) { struct fuse_inode *fi = get_fuse_inode(inode); bool frozen; @@ -934,7 +940,7 @@ static void fuse_notify_invalidate_range(struct inode *inode, pgoff_t start, frozen = fi->writectr < 0; spin_unlock(&fi->lock); - if (frozen) + if (frozen || !may_be_dirty) invalidate_mapping_pages(inode->i_mapping, start, end); else invalidate_inode_pages2_range(inode->i_mapping, start, end); @@ -946,8 +952,12 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, struct percpu_rw_semaphore *wb_sem = NULL; struct fuse_inode *fi; struct inode *inode; + uint64_t pg_first; + uint64_t pg_last; + loff_t end_byte; pgoff_t pg_start; pgoff_t pg_end; + bool tracked; inode = fuse_ilookup(fc, nodeid, NULL); if (!inode) @@ -980,6 +990,17 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, else pg_end = (offset + len - 1) >> PAGE_SHIFT; + /* + * Byte bounds of the same region, and the page aligned form + * the DLM record is asked about. A write smaller than a page + * marks only its own bytes, so the query has to cover whole + * pages or a dirty edge inside a page would not be seen. + */ + end_byte = len <= 0 ? LLONG_MAX : offset + len - 1; + pg_first = (uint64_t)offset & PAGE_MASK; + pg_last = len <= 0 ? U64_MAX : + (((uint64_t)offset + len - 1) | (PAGE_SIZE - 1)); + /* * A data invalidation means another (remote) entity is modifying * the file. Two things happen here: @@ -1024,13 +1045,15 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, * a mapping needs the page cache, and fuse_file_mmap() * reverts any latch it races with. */ - if (S_ISREG(inode->i_mode) && fc->writeback_cache && - fc->dlm && !FUSE_IS_DAX(inode) && - !fuse_inode_backing(fi)) + tracked = S_ISREG(inode->i_mode) && fc->writeback_cache && + fc->dlm && !FUSE_IS_DAX(inode) && + !fuse_inode_backing(fi); + if (tracked) wb_sem = fi->wb_inval_rwsem; if (wb_sem) { bool hot, has_writer, latched = false; + bool may_be_dirty, has_pages; spin_lock(&fi->lock); hot = fuse_notify_inval_hot(fi); @@ -1055,6 +1078,22 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, if (fc->dlm && fc->writeback_cache) fuse_dlm_revoke_inval_range(fi, offset, len); + /* + * Ask what is left to do, under the gate so no + * reader can populate and no writer can dirty + * between the answer and the drop below. + * + * Nothing cached in the range means the drop is a + * no-op; the revoke above was the whole job. + * Otherwise the record decides whether the drop has + * to launder, which is what makes it wait for a + * FUSE_WRITE reply. + */ + has_pages = filemap_range_has_page(inode->i_mapping, + offset, end_byte); + may_be_dirty = fuse_dlm_range_may_be_dirty(fi, pg_first, + pg_last); + if (enable_notify_dio && hot && has_writer && !mapping_mapped(inode->i_mapping) && !fuse_inode_force_dio(inode)) { @@ -1069,14 +1108,17 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, /* * Latched: drop the whole mapping (dirty folios * outside the notified range would be invisible to - * the forced direct reads). Otherwise just the - * notified range. + * the forced direct reads), and the record says + * nothing about the rest of the file, so launder. + * Otherwise just the notified range, and only if + * anything is cached there. */ if (fuse_inode_force_dio(inode)) - fuse_notify_invalidate_range(inode, 0, -1); - else + fuse_notify_invalidate_range(inode, 0, -1, true); + else if (has_pages) fuse_notify_invalidate_range(inode, pg_start, - pg_end); + pg_end, + may_be_dirty); percpu_up_write(wb_sem); @@ -1084,12 +1126,17 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, pr_info_ratelimited("FUSE: inode %llu latched to direct IO on invalidation notify storm\n", nodeid); } else { - /* No gate on this inode (DAX, backing, non-regular, + /* + * No gate on this inode (DAX, backing, non-regular, * or the gate allocation failed): drop the lock - * range unserialized (best-effort), as before. */ + * range unserialized (best-effort), as before. No + * record to consult either, so assume the range can + * hold unwritten data. + */ if (fc->dlm && fc->writeback_cache) fuse_dlm_revoke_inval_range(fi, offset, len); - fuse_notify_invalidate_range(inode, pg_start, pg_end); + fuse_notify_invalidate_range(inode, pg_start, pg_end, + true); } } iput(inode); From 1ef5278e36048c22704d60c132228238a31df4e3 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 12:22:07 +0200 Subject: [PATCH 09/41] fuse: keep a revoked DLM range instead of deleting it fuse_dlm_unlock_range() removed the ranges it revoked, so a grant taken away looked like a gap. fuse_dlm_dirty_run() calls a gap UNKNOWN and writeback sends the whole range, so a writer that passed fuse_dlm_lock_is_held() before a revoke and dirtied after it had its bytes sent for a range another node now holds. Mark the range FUSE_DLM_RANGE_REVOKED instead; one with nothing cached under it is still removed. It covers nothing, so the IO paths request again. fuse_dlm_dirty_run() returns enum fuse_dlm_run so writeback can tell the two apart and call fuse_dlm_regrant_range() before sending a revoked run. Splitting at the revoke bounds replaces the trim and punch-hole arithmetic, so fuse_dlm_punch_hole() goes. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 62 ++++--- fs/fuse/fuse_dlm_cache.c | 346 ++++++++++++++++++++++----------------- fs/fuse/fuse_dlm_cache.h | 45 +++-- fs/fuse/inode.c | 52 ++++-- 4 files changed, 307 insertions(+), 198 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index d80bf9299cba08..7858b72b3b891c 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -3053,6 +3053,12 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, WARN_ON_ONCE(!data); + if (!data->ff) { + data->ff = fuse_write_file_get(fi); + if (!data->ff) + return -EIO; + } + /* * A folio iomap has not asked about before: the one before it has all * of its runs queued, so let go of the bias holding its count open. @@ -3069,19 +3075,12 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, * of the boundary page was never read in. Send only what was * written, so the untouched remainder is not handed to the server. * - * fuse_dlm_dirty_run() reports the run from @pos that is uniformly - * written or uniformly not, and refuses to answer for a range not - * covered by write grants throughout, where something outside its - * record could have dirtied the folio. In that case the whole - * range goes, as it did before. - * - * A run that was not written is reported to iomap as a hole, which - * skips it and, for a folio with no written run at all, ends the - * folio writeback itself. iomap calls back for the next run. + * fuse_dlm_dirty_run() classifies the run from @pos and says how far + * the answer holds. iomap calls back for the rest. */ if (fc->dlm && fc->writeback_cache) { - bool dirty; - size_t run = fuse_dlm_dirty_run(fi, pos, len, &dirty); + size_t run = len; + int err; /* * wpc->iomap.type carries over from the previous run and from @@ -3092,23 +3091,42 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, */ wpc->iomap.type = IOMAP_MAPPED; - if (run) { - if (!dirty) { - wpc->iomap.type = IOMAP_HOLE; - return run; - } + switch (fuse_dlm_dirty_run(fi, pos, len, &run)) { + case FUSE_DLM_RUN_UNKNOWN: + /* + * No record for this range: send it whole, as + * without DLM. + */ + break; + case FUSE_DLM_RUN_CLEAN: + /* + * Nothing was written here. Reported as a hole so + * iomap skips it and, for a folio with no written + * run at all, ends the folio writeback itself. + */ + wpc->iomap.type = IOMAP_HOLE; + return run; + case FUSE_DLM_RUN_REVOKED: + /* + * Written under a grant the server has since taken + * away. The bytes are real, so hold the range again + * rather than lose them. A failure leaves the run + * classified revoked and the folio dirty, so the next + * writeback tries again; only a hard error stops it. + */ + err = fuse_dlm_regrant_range(data->ff, inode, pos, + pos + run - 1); + if (err < 0 && err != -ENOSYS) + return err; + fallthrough; + case FUSE_DLM_RUN_DIRTY: len = run; + break; } } offset = offset_in_folio(folio, pos); - if (!data->ff) { - data->ff = fuse_write_file_get(fi); - if (!data->ff) - return -EIO; - } - if (wpa && fuse_writepage_need_send(fc, pos, len, ap, data, wpc->wbc)) { fuse_writepages_send(inode, data); data->wpa = NULL; diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index cfd383188fa4ca..711355dd4384a0 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -13,9 +13,13 @@ * - REQUESTED, on cache->pending, while its FUSE_DLM_WB_LOCK is in * flight. * - * - REVOKED, still on cache->pending, once a revoke has overlapped it. - * fuse_dlm_request_commit() drops such a grant instead of recording - * it. + * - REVOKED, in either place. On cache->pending it is a request a + * revoke overlapped while it was in flight, and + * fuse_dlm_request_commit() drops that grant instead of recording it. + * In cache->ranges it is a grant that was recorded and has since been + * taken away, kept because the page cache under it is still + * described. It covers nothing either way, so the IO paths ask + * again. * * - READ or WRITE, in cache->ranges. The only states * fuse_dlm_range_is_locked() reports as covered; the mode is not a @@ -43,7 +47,11 @@ enum fuse_dlm_range_state { /* FUSE_DLM_WB_LOCK in flight, on cache->pending */ FUSE_DLM_RANGE_REQUESTED, - /* Revoked while in flight; the grant must not be recorded */ + /* + * On cache->pending, revoked in flight and the grant must not be + * recorded. In cache->ranges, granted once and taken away, kept to + * describe the page cache under it. Covers nothing either way. + */ FUSE_DLM_RANGE_REVOKED, /* Granted shared, in cache->ranges */ FUSE_DLM_RANGE_READ, @@ -323,12 +331,16 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* Get next overlapping range before we potentially modify the tree */ next = fuse_page_it_iter_next(range, start, end); - /* Check lock compatibility */ - if (want == FUSE_DLM_RANGE_WRITE && - range->state != FUSE_DLM_RANGE_WRITE) { - /* we own the lock but have to update it. */ + /* + * A revoked range is covered again by this grant, and a read + * range needs upgrading when a write is granted. Either way + * the recorded content carries over: the page cache under it + * did not change because the grant did. + */ + if (range->state == FUSE_DLM_RANGE_REVOKED || + (want == FUSE_DLM_RANGE_WRITE && + range->state != FUSE_DLM_RANGE_WRITE)) list_add_tail(&range->list, &to_upgrade); - } /* If WRITE lock already exists - nothing to do */ /* If there's a gap before this range, we need to add the missing range */ @@ -375,11 +387,9 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, list_add_tail(&new_range->list, &to_lock); } - /* update locks, if any lock is in this list it has the wrong mode */ - list_for_each_entry(range, &to_upgrade, list) { - /* Update the lock mode */ + /* Everything on this list is now covered in @want */ + list_for_each_entry(range, &to_upgrade, list) range->state = want; - } /* Add all new ranges to the tree */ list_for_each_entry(new_range, &to_lock, list) { @@ -401,14 +411,10 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, kfree(new_range); } - /* Restore original lock modes for any partially upgraded locks */ - list_for_each_entry(range, &to_upgrade, list) { - if (want == FUSE_DLM_RANGE_WRITE) { - /* We upgraded this lock but failed later, downgrade it back */ - range->state = FUSE_DLM_RANGE_READ; - } - } - + /* + * Nothing to undo on @to_upgrade: every goto here is taken before + * the loop above runs, so no state has been changed yet. + */ return ret; } @@ -676,78 +682,142 @@ void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } +/* How a range in @state / @content classifies for writeback */ +static enum fuse_dlm_run fuse_dlm_classify(struct fuse_dlm_range *range) +{ + if (range->content != FUSE_DLM_CONTENT_DIRTY) + return FUSE_DLM_RUN_CLEAN; + + return range->state == FUSE_DLM_RANGE_REVOKED ? FUSE_DLM_RUN_REVOKED : + FUSE_DLM_RUN_DIRTY; +} + /** - * fuse_dlm_dirty_run - length of the uniform run starting at @pos + * fuse_dlm_dirty_run - classify the run starting at @pos * @inode: the fuse inode * @pos: byte offset to start at * @len: bytes of interest from @pos - * @dirty: set to whether the returned run was written under a grant + * @run: set to how far the classification holds, capped at @len * - * Walks forward from @pos while the recorded content stays the same and - * returns how far that run reaches, capped at @len. Lets writeback ask - * which part of a folio this client actually wrote, so the untouched - * remainder of a boundary page is not sent to the server. + * Walks forward from @pos while the classification stays the same, so + * writeback can ask what to do with one run of a folio at a time. * - * Return: the run length, or 0 when the record cannot answer for - * [pos, pos + len). That happens when the range is not covered by write - * grants throughout, in which case something outside this record may - * have dirtied it and the caller must write the whole range. + * Return: + * %FUSE_DLM_RUN_UNKNOWN - no record for [pos, pos + len): either a gap, + * or a range held only for read, which this client cannot have + * written but which does not rule out someone else having dirtied + * the folio. @run is not set; the caller writes the whole range. + * %FUSE_DLM_RUN_CLEAN - nothing was written here. + * %FUSE_DLM_RUN_DIRTY - written under a grant this client still holds. + * %FUSE_DLM_RUN_REVOKED - written, but the grant has since been taken + * away. The bytes are real and must not be lost, so the caller has + * to hold the range again before sending them. */ -size_t fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, size_t len, - bool *dirty) +enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, + size_t len, size_t *run) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + enum fuse_dlm_run kind = FUSE_DLM_RUN_UNKNOWN; struct fuse_dlm_range *range; uint64_t end, cur = pos; - bool run_dirty = false; bool first = true; - size_t run = 0; + size_t reach = 0; if (!len) - return 0; + return FUSE_DLM_RUN_UNKNOWN; end = pos + len - 1; down_read(&cache->lock); for (range = fuse_dlm_find_overlapping(cache, pos, end); range; range = fuse_page_it_iter_next(range, pos, end)) { - bool range_dirty; + enum fuse_dlm_run range_kind; /* * A gap, or a range held only for read: this client cannot - * have written it, but nothing here proves nobody else + * have written it, and nothing here proves nobody else * dirtied the folio, so refuse to answer. */ - if (range->start > cur || range->state != FUSE_DLM_RANGE_WRITE) + if (range->start > cur || range->state == FUSE_DLM_RANGE_READ) goto unknown; - range_dirty = range->content == FUSE_DLM_CONTENT_DIRTY; + range_kind = fuse_dlm_classify(range); if (first) { - run_dirty = range_dirty; + kind = range_kind; first = false; - } else if (range_dirty != run_dirty) { - goto out; /* the run ends where content changes */ + } else if (range_kind != kind) { + goto out; /* the run ends where the answer does */ } if (range->end >= end) { - run = len; + reach = len; goto out; } /* Safe: range->end < end, so this cannot wrap */ cur = range->end + 1; - run = cur - pos; + reach = cur - pos; } /* Ran out of ranges before reaching @end */ unknown: up_read(&cache->lock); - return 0; + return FUSE_DLM_RUN_UNKNOWN; out: up_read(&cache->lock); - *dirty = run_dirty; - return run; + *run = reach; + return kind; +} + +/** + * fuse_dlm_ranges_dropped - the page cache under [start, end] is gone + * @inode: the fuse inode + * @start: start page offset (inclusive) + * @end: end page offset (inclusive) + * + * A revoked range exists only to describe page cache dirtied before the + * grant was taken away. Once that cache is gone the range has nothing + * left to say and is freed; a range still held goes back to describing + * nothing. + * + * The caller must have established that the range really is empty, not + * merely asked for it to be dropped: a folio that survived an + * invalidate is still there, and claiming otherwise would let writeback + * send it with no record of where it came from. + */ +void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, + uint64_t end) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + struct fuse_dlm_range *range, *next; + + if (start > end) + return; + + down_write(&cache->lock); + + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); + + range = fuse_page_it_iter_first(&cache->ranges, start, end); + while (range) { + next = fuse_page_it_iter_next(range, start, end); + + if (range->state == FUSE_DLM_RANGE_REVOKED) { + fuse_page_it_remove(range, &cache->ranges); + kfree(range); + } else { + range->content = FUSE_DLM_CONTENT_EMPTY; + } + + range = next; + } + + fuse_dlm_try_merge(cache, start, end); + + up_write(&cache->lock); } /** @@ -802,83 +872,26 @@ bool fuse_dlm_range_may_be_dirty(struct fuse_inode *inode, uint64_t start, } /** - * fuse_dlm_punch_hole - Punch a hole in a locked range - * @cache: The page cache - * @start: Start page offset of the hole - * @end: End page offset of the hole - * - * Create a hole in a locked range by splitting it into two ranges. - * - * Return: 0 on success, negative error code on failure - */ -static int fuse_dlm_punch_hole(struct fuse_dlm_cache *cache, uint64_t start, - uint64_t end) -{ - struct fuse_dlm_range *range, *new_range; - int ret = 0; - - if (!cache || start > end) - return -EINVAL; - - /* Find a range that contains [start, end] */ - range = fuse_dlm_find_overlapping(cache, start, end); - if (!range) { - ret = -EINVAL; - goto out; - } - - /* If the hole is at the beginning of the range */ - if (start == range->start) { - fuse_page_it_remove(range, &cache->ranges); - range->start = end + 1; - fuse_page_it_insert(range, &cache->ranges); - goto out; - } - - /* If the hole is at the end of the range */ - if (end == range->end) { - fuse_page_it_remove(range, &cache->ranges); - range->end = start - 1; - fuse_page_it_insert(range, &cache->ranges); - goto out; - } - - /* The hole is in the middle, need to split */ - new_range = kmalloc(sizeof(*new_range), GFP_KERNEL); - if (!new_range) { - ret = -ENOMEM; - goto out; - } - - /* Copy properties from original range */ - *new_range = *range; - INIT_LIST_HEAD(&new_range->list); - - /* Adjust ranges */ - new_range->start = end + 1; - range->end = start - 1; - - /* Update interval tree */ - fuse_page_it_remove(range, &cache->ranges); - fuse_page_it_insert(range, &cache->ranges); - fuse_page_it_insert(new_range, &cache->ranges); - -out: - return ret; -} - -/** - * fuse_dlm_unlock_range - Unlock a range of pages - * @cache: The page cache + * fuse_dlm_unlock_range - Revoke the grants over a range of pages + * @inode: The fuse inode * @start: Start page offset * @end: End page offset * - * Release locks on the specified range of pages. An inverted range is - * rejected rather than silently removing nothing: the callers revoke - * coverage, and a revoke that quietly keeps the grant alive would let - * the re-validating IO paths trust a lock the server has taken away. - * To drop every grant use fuse_dlm_cache_release_locks() (there is no - * in-band sentinel range for it). + * The server has taken [start, end] back. A range that has nothing + * cached under it is removed; one that has is kept and marked + * FUSE_DLM_RANGE_REVOKED, so the page cache it covers stays described. + * Removing it instead would leave a gap, and a gap reads as "no record", + * which is what an untracked range looks like: writeback would then send + * folios dirtied under the grant that was just taken away. + * + * A revoked range covers nothing, so fuse_dlm_range_is_locked() reports + * it uncovered and the IO paths request again. + * + * An inverted range is rejected rather than silently revoking nothing: + * the callers revoke coverage, and a revoke that quietly keeps the grant + * alive would let the re-validating IO paths trust a lock the server has + * taken away. To drop every grant use fuse_dlm_cache_release_locks() + * (there is no in-band sentinel range for it). * * Return: 0 on success, negative error code on failure */ @@ -887,7 +900,6 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range, *next; - int ret = 0; if (!cache || start > end) return -EINVAL; @@ -901,42 +913,35 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, */ fuse_dlm_kill_pending(cache, start, end); - /* Find all ranges that overlap with [start, end] */ + /* + * Split so the revoked region has its own ranges. A split that + * cannot allocate leaves the range whole and revokes more than the + * server asked for, which costs a re-request and nothing else. + */ + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); + range = fuse_page_it_iter_first(&cache->ranges, start, end); while (range) { - /* Get next overlapping range before we potentially modify the tree */ + /* Get next overlapping range before we modify the tree */ next = fuse_page_it_iter_next(range, start, end); - /* Check if we need to punch a hole */ - if (start > range->start && end < range->end) { - /* Punch a hole in the middle */ - ret = fuse_dlm_punch_hole(cache, start, end); - if (ret) - goto out; - /* After punching a hole, we're done */ - break; - } else if (start > range->start) { - /* Adjust the end of the range */ - fuse_page_it_remove(range, &cache->ranges); - range->end = start - 1; - fuse_page_it_insert(range, &cache->ranges); - } else if (end < range->end) { - /* Adjust the start of the range */ - fuse_page_it_remove(range, &cache->ranges); - range->start = end + 1; - fuse_page_it_insert(range, &cache->ranges); - } else { - /* Complete overlap, remove the range */ + if (range->content == FUSE_DLM_CONTENT_EMPTY) { + /* Nothing cached under it, so nothing to describe */ fuse_page_it_remove(range, &cache->ranges); kfree(range); + } else { + range->state = FUSE_DLM_RANGE_REVOKED; } range = next; } -out: + fuse_dlm_try_merge(cache, start, end); + up_write(&cache->lock); - return ret; + return 0; } /** @@ -1027,7 +1032,11 @@ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, * server has not seen, so its mtime and ctime run ahead of anything the * server can report. * - * Return: true if at least one recorded range is held for write + * A revoked range still recorded as dirty counts too: the grant is gone + * but the unwritten data is not, so the local times are still ahead. + * + * Return: true if at least one recorded range is held for write, or was + * and still has unwritten data under it */ bool fuse_dlm_write_grant_exists(struct fuse_inode *fi) { @@ -1038,7 +1047,9 @@ bool fuse_dlm_write_grant_exists(struct fuse_inode *fi) down_read(&cache->lock); for (range = fuse_dlm_find_overlapping(cache, 0, U64_MAX); range; range = fuse_page_it_iter_next(range, 0, U64_MAX)) { - if (range->state == FUSE_DLM_RANGE_WRITE) { + if (range->state == FUSE_DLM_RANGE_WRITE || + (range->state == FUSE_DLM_RANGE_REVOKED && + range->content == FUSE_DLM_CONTENT_DIRTY)) { held = true; break; } @@ -1089,11 +1100,10 @@ bool fuse_dlm_lock_is_held(struct fuse_inode *fi, loff_t offset, * re-validating the grant must not re-request on a nonzero return or * they would spin. */ -int fuse_get_dlm_lock(struct file *file, loff_t offset, - size_t length, enum fuse_page_lock_mode mode) +static int __fuse_get_dlm_lock(struct fuse_file *ff, struct inode *inode, + loff_t offset, size_t length, + enum fuse_page_lock_mode mode) { - struct fuse_file *ff = file->private_data; - struct inode *inode = file_inode(file); struct fuse_conn *fc = get_fuse_conn(inode); struct fuse_inode *fi = get_fuse_inode(inode); struct fuse_mount *fm = ff->fm; @@ -1204,3 +1214,33 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, return 0; } + +int fuse_get_dlm_lock(struct file *file, loff_t offset, + size_t length, enum fuse_page_lock_mode mode) +{ + return __fuse_get_dlm_lock(file->private_data, file_inode(file), + offset, length, mode); +} + +/** + * fuse_dlm_regrant_range - hold [start, end] again for writeback + * @ff: a fuse file open for writing on @inode + * @inode: the inode + * @start: start page offset (inclusive) + * @end: end page offset (inclusive) + * + * Writeback found bytes dirtied under a grant the server has since taken + * away (FUSE_DLM_RUN_REVOKED). They cannot be dropped, so take the range + * again before sending them. Whatever the other holder wrote in between + * is overwritten, which for two writers that never synchronised is a + * legitimate order. + * + * Recording the grant flips the range back to held, so the run classifies + * as FUSE_DLM_RUN_DIRTY from here on. + */ +int fuse_dlm_regrant_range(struct fuse_file *ff, struct inode *inode, + uint64_t start, uint64_t end) +{ + return __fuse_get_dlm_lock(ff, inode, start, end - start + 1, + FUSE_PAGE_LOCK_WRITE); +} diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index acd0e6584a1e98..28b8cbae0c1f98 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -14,6 +14,7 @@ struct fuse_inode; struct fuse_dlm_range; +struct fuse_file; /* Lock modes for page ranges */ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; @@ -30,15 +31,16 @@ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; /* * Page cache lock manager. * - * @ranges holds the grants the client has been given and not had taken - * back. A request still on the wire covers nothing and lives on - * @pending instead, so tree walkers never filter on state. See enum - * fuse_dlm_range_state in fuse_dlm_cache.c. + * @ranges holds the grants the client has been given, and the ones it + * has had taken back that still describe page cache + * (FUSE_DLM_RANGE_REVOKED). A request still on the wire covers nothing + * and lives on @pending instead, so tree walkers never filter on state. + * See enum fuse_dlm_range_state in fuse_dlm_cache.c. */ struct fuse_dlm_cache { /* Lock protecting the tree and the pending list */ struct rw_semaphore lock; - /* Interval tree of granted ranges (FUSE_DLM_RANGE_READ/_WRITE) */ + /* Interval tree of recorded ranges, granted or revoked */ struct rb_root_cached ranges; /* * FUSE_DLM_WB_LOCK requests in flight (REQUESTED, or REVOKED once @@ -111,13 +113,36 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, uint64_t end); +/* What writeback should do with a run; see fuse_dlm_dirty_run() */ +enum fuse_dlm_run { + /* No record: write the whole range, as without DLM */ + FUSE_DLM_RUN_UNKNOWN, + /* Nothing was written here */ + FUSE_DLM_RUN_CLEAN, + /* Written under a grant still held */ + FUSE_DLM_RUN_DIRTY, + /* Written, but the grant has since been taken away */ + FUSE_DLM_RUN_REVOKED, +}; + /* - * Length of the run from @pos, capped at @len, over which the recorded - * content does not change; @dirty says which it is. 0 means the record - * cannot answer and the caller must assume the whole range is dirty. + * Classify the run from @pos, setting @run to how far the answer holds, + * capped at @len. @run is left alone for FUSE_DLM_RUN_UNKNOWN. */ -size_t fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, size_t len, - bool *dirty); +enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, + size_t len, size_t *run); + +/* + * The page cache under [start, end] is gone: free the revoked ranges over + * it and reset the content of the ones still held. The caller must have + * established the range really is empty. + */ +void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, + uint64_t end); + +/* Hold [start, end] again so writeback can send what it found revoked */ +int fuse_dlm_regrant_range(struct fuse_file *ff, struct inode *inode, + uint64_t start, uint64_t end); /* * Can [start, end] hold data the server has not seen? A part of it with diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 4b93b121ad721a..3d8932491b638b 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1068,25 +1068,15 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, */ percpu_down_write(wb_sem); - /* - * Revoke the DLM lock range under the gate write - * side, atomically with the page drop: gate readers - * re-validate their grant right after entering, and - * a grant that passed that check must stay visible - * for their whole gate hold. - */ - if (fc->dlm && fc->writeback_cache) - fuse_dlm_revoke_inval_range(fi, offset, len); - /* * Ask what is left to do, under the gate so no * reader can populate and no writer can dirty * between the answer and the drop below. * * Nothing cached in the range means the drop is a - * no-op; the revoke above was the whole job. - * Otherwise the record decides whether the drop has - * to launder, which is what makes it wait for a + * no-op and the revoke is the whole job. Otherwise + * the record decides whether the drop has to + * launder, which is what makes it wait for a * FUSE_WRITE reply. */ has_pages = filemap_range_has_page(inode->i_mapping, @@ -1094,6 +1084,30 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, may_be_dirty = fuse_dlm_range_may_be_dirty(fi, pg_first, pg_last); + /* + * Start unwritten data on its way while the grant + * still covers it, rather than leaving it to the + * drop below. After the revoke those bytes + * classify as FUSE_DLM_RUN_REVOKED, and writeback + * would take the range again to send them: a DLM + * round trip from inside the handler the server is + * waiting on. do_writepages() runs in this context, + * so the classification is made before the revoke. + */ + if (has_pages && may_be_dirty) + filemap_fdatawrite_range(inode->i_mapping, + offset, end_byte); + + /* + * Revoke the DLM lock range under the gate write + * side, atomically with the page drop: gate readers + * re-validate their grant right after entering, and + * a grant that passed that check must stay visible + * for their whole gate hold. + */ + if (fc->dlm && fc->writeback_cache) + fuse_dlm_revoke_inval_range(fi, offset, len); + if (enable_notify_dio && hot && has_writer && !mapping_mapped(inode->i_mapping) && !fuse_inode_force_dio(inode)) { @@ -1120,6 +1134,18 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, pg_end, may_be_dirty); + /* + * A revoked range exists to describe page cache + * dirtied before the grant went; with that cache + * gone it has nothing left to say. Only when it + * really went: an invalidate can leave a busy folio + * behind, and that folio still needs its record. + */ + if (has_pages && + !filemap_range_has_page(inode->i_mapping, offset, + end_byte)) + fuse_dlm_ranges_dropped(fi, pg_first, pg_last); + percpu_up_write(wb_sem); if (latched) From 02a9e2ace5de36d5ccd540c8f926008fdf03df10 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 12:25:13 +0200 Subject: [PATCH 10/41] fuse: drop the per-inode coherency gate wb_inval_rwsem fenced cached IO out for the whole of a NOTIFY invalidate, so a writer could not dirty the page cache under a grant being revoked. The record does that now: a write records its bytes before dirtying them, a revoke marks the range instead of forgetting it, and writeback takes the range again before sending anything marked that way. A read never needed the fence. Gone with it: the re-validation and retry loops that existed only because the gate had to be dropped around a FUSE_DLM_WB_LOCK round trip, fuse_cache_wr_dlm_lock()'s unrecorded flag, and the percpu_rw_semaphore. setattr and O_TRUNC lose it too, holding i_rwsem exclusive. Signed-off-by: Horst Birthelmer --- fs/fuse/dir.c | 35 +++------- fs/fuse/file.c | 169 ++++++++++------------------------------------- fs/fuse/fuse_i.h | 15 ----- fs/fuse/inode.c | 154 +++++++++++++++--------------------------- 4 files changed, 97 insertions(+), 276 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 5f426228f4c76a..62641f724b6746 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2106,34 +2106,24 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, WARN_ON(!(attr->ia_valid & ATTR_SIZE)); WARN_ON(attr->ia_size != 0); if (fc->atomic_o_trunc) { - struct percpu_rw_semaphore *wb_sem = fi->wb_inval_rwsem; - /* * No need to send request to userspace, since actual * truncation has already been done by OPEN. But still * need to truncate page cache. * - * Revoke and drop under the coherency gate write side, - * like the NOTIFY invalidate path: a gate reader that - * already re-validated its grant must not have the - * lock tree and the cache yanked mid-hold, or it - * would repopulate the truncated range trusting a - * grant that no longer exists. Waiting for gate - * readers here is safe: we hold i_rwsem exclusive, so - * no gate holder can be waiting on it (the write path - * takes i_rwsem before the gate, the read path never - * takes it). + * Dropping every grant here does not need a reader or + * writer fenced out: truncate_pagecache() discards the + * folios rather than writing them, and a write racing + * this is a write racing an O_TRUNC open, which has no + * order to preserve. i_rwsem is held exclusive + * anyway, so no cached write is in progress. */ - if (wb_sem) - percpu_down_write(wb_sem); if (fc->dlm && fc->writeback_cache) fuse_dlm_cache_release_locks(fi); spin_lock(&fi->lock); i_size_write(inode, 0); spin_unlock(&fi->lock); truncate_pagecache(inode, 0); - if (wb_sem) - percpu_up_write(wb_sem); goto out; } file = NULL; @@ -2237,23 +2227,16 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, */ if ((is_truncate || !is_wb) && S_ISREG(inode->i_mode) && oldsize != outarg.attr.size) { - struct percpu_rw_semaphore *wb_sem = fi->wb_inval_rwsem; - /* - * Revoke and drop under the coherency gate write side; see - * the atomic-O_TRUNC branch above. i_rwsem is held - * exclusive here as well (setattr), so waiting out gate - * readers cannot deadlock. + * Revoke past the new size and drop what is beyond it; see + * the atomic-O_TRUNC branch above for why this needs nothing + * fenced out. i_rwsem is held exclusive here as well. */ - if (wb_sem) - percpu_down_write(wb_sem); if (fc->dlm && fc->writeback_cache) fuse_dlm_unlock_range(fi, outarg.attr.size & PAGE_MASK, -1); truncate_pagecache(inode, outarg.attr.size); invalidate_inode_pages2(mapping); - if (wb_sem) - percpu_up_write(wb_sem); } clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 7858b72b3b891c..5afd85b8dab4d8 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -469,8 +469,8 @@ void fuse_file_release(struct inode *inode, struct fuse_file *ff, * If this release dropped the last writer, fuse_prepare_release() * cleared the forced-direct-IO latch (under fi->lock). Drop any clean * folios a read racing the latch may have repopulated so they cannot be - * served stale once caching mode resumes. No inode lock or - * wb_inval_rwsem: release may run on the fuse server thread (async fput + * served stale once caching mode resumes. No inode lock: release may + * run on the fuse server thread (async fput * from aio completion), where blocking on a contended inode lock could * stall the connection. Writes were routed direct while latched, so * only clean folios exist and this invalidate is server-free; the last @@ -1179,11 +1179,9 @@ static void fuse_readahead(struct readahead_control *rac) * left in @rac. A server without DLM support answers -ENOSYS and * clears fc->dlm, which is not a failure. * - * This can run inside the coherency gate, which - * fuse_cache_read_iter() holds across generic_file_read_iter(), so - * the round trip leans on the same server contract that lets a - * cache-miss FUSE_READ block there: replies are serviced on threads - * other than the one delivering a NOTIFY invalidate. + * The round trip is taken before any folio of the window is locked + * and with nothing fenced out, so it holds up this reader and + * nothing else. */ if (fc->writeback_cache && fc->dlm) { int err = fuse_get_dlm_lock(rac->file, readahead_pos(rac), @@ -1267,21 +1265,12 @@ static void fuse_readahead(struct readahead_control *rac) static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to); -/* - * Bound on re-requesting a revoked DLM grant before a cached read is - * served unlocked; see fuse_cache_read_iter(). - */ -#define FUSE_DLM_READ_RETRIES 3 - static ssize_t fuse_cache_read_iter(struct kiocb *iocb, struct iov_iter *to) { struct file *file = iocb->ki_filp; struct inode *inode = file->f_mapping->host; struct fuse_conn *fc = get_fuse_conn(inode); - struct fuse_inode *fi = get_fuse_inode(inode); - struct percpu_rw_semaphore *wb_sem = fi->wb_inval_rwsem; ssize_t res; - int lock_err = 0; /* * In auto invalidate mode, always update attributes on read. @@ -1299,65 +1288,21 @@ static ssize_t fuse_cache_read_iter(struct kiocb *iocb, struct iov_iter *to) /* if we have dlm support acquire a read lock for the area * we are reading from. */ if (fc->writeback_cache && fc->dlm) - lock_err = fuse_get_dlm_lock(file, iocb->ki_pos, - iov_iter_count(to), - FUSE_PAGE_LOCK_READ); + fuse_get_dlm_lock(file, iocb->ki_pos, iov_iter_count(to), + FUSE_PAGE_LOCK_READ); /* - * Fence the cache-serving read against a NOTIFY invalidate so we never - * hand back a folio the server has just superseded. The gate read side - * is per-CPU cheap; the NOTIFY holds the write side with priority. - * Re-check the forced-DIO latch under it: if a storm latched us while we - * waited on a pending writer, reroute to direct like the buffered write - * path, so we do not repopulate the cache the latch just dropped. - * wb_sem is NULL on non-writeback+dlm mounts (gate inactive). + * A NOTIFY invalidate racing this read drops the folios it + * supersedes, so the read either misses and refetches or returns + * data that was current when it was copied. There is nothing to + * fence: unlike a write, a read leaves nothing behind that could + * reach the server under a grant it no longer holds. */ - if (wb_sem) { - int tries = FUSE_DLM_READ_RETRIES; - -retry: - percpu_down_read(wb_sem); - if (fuse_inode_force_dio(inode)) { - percpu_up_read(wb_sem); - return fuse_direct_read_iter(iocb, to); - } - /* - * The DLM lock was requested before entering the gate, and - * the NOTIFY invalidate we may just have waited on revokes - * locks under the gate write side. Re-check the grant here - * and re-request with the gate dropped, so a - * FUSE_DLM_WB_LOCK round trip never parks a pending - * invalidate behind our own gate hold. Once the check - * passes the lock cannot go away for the rest of the gate - * hold. A failed or unrecorded request falls through - * unlocked, as before: the retry is taken even then (the - * latch must be re-checked under the re-entered gate), so - * lock_err has to stay sticky across it -- seeded by the - * pre-gate request above -- or a grant that failed would - * be re-requested forever. The retry is also bounded: a - * remote writer can revoke each successful grant before - * the gate is re-entered, and a reader-only inode has no - * force-DIO latch to end such a storm, so after - * FUSE_DLM_READ_RETRIES re-requests the read is served - * unlocked rather than looping without bound. - */ - if (!lock_err && fc->dlm && tries-- > 0 && - !fuse_dlm_lock_is_held(fi, iocb->ki_pos, - iov_iter_count(to), - FUSE_PAGE_LOCK_READ)) { - percpu_up_read(wb_sem); - lock_err = fuse_get_dlm_lock(file, iocb->ki_pos, - iov_iter_count(to), - FUSE_PAGE_LOCK_READ); - goto retry; - } - } + if (fuse_inode_force_dio(inode)) + return fuse_direct_read_iter(iocb, to); res = generic_file_read_iter(iocb, to); - if (wb_sem) - percpu_up_read(wb_sem); - return res; } @@ -1949,19 +1894,13 @@ static void fuse_cache_wr_unlock(struct inode *inode, bool exclusive) * fc->dlm: the server has no DLM, proceed as a plain cached write. Any * other failure means the cache would be dirtied without DLM coverage - * the caller must fail the write instead. A granted-but-unrecorded - * lock (positive return) is covered cluster-wide; proceed, but flag it - * so the in-gate re-validation skips a check an invisible grant could - * never pass. + * lock (positive return) is covered cluster-wide; proceed. */ -static int fuse_cache_wr_dlm_lock(struct file *file, loff_t pos, size_t len, - bool *unrecorded) +static int fuse_cache_wr_dlm_lock(struct file *file, loff_t pos, size_t len) { int err = fuse_get_dlm_lock(file, pos, len, FUSE_PAGE_LOCK_WRITE); - if (err < 0 && err != -ENOSYS) - return err; - *unrecorded = err > 0; - return 0; + return (err < 0 && err != -ENOSYS) ? err : 0; } static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) @@ -1974,11 +1913,8 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) ssize_t err, count; struct fuse_conn *fc = get_fuse_conn(inode); struct fuse_inode *fi = get_fuse_inode(inode); - struct percpu_rw_semaphore *wb_sem = fi->wb_inval_rwsem; bool writeback = false; - bool wb_guard = false; bool exclusive = true; - bool dlm_unrecorded = false; loff_t dlm_pos = 0; size_t dlm_len = 0; @@ -2039,8 +1975,7 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) dlm_pos = iocb->ki_pos; dlm_len = iov_iter_count(from); - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len, - &dlm_unrecorded); + err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len); if (err) return err; @@ -2076,8 +2011,7 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) dlm_pos = i_size_read(inode); dlm_len = iov_iter_count(from); - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len, - &dlm_unrecorded); + err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len); if (err) goto out; } @@ -2098,8 +2032,7 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) dlm_pos = iocb->ki_pos; dlm_len = count; - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len, - &dlm_unrecorded); + err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len); if (err) goto out; } @@ -2113,9 +2046,8 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) * writeback inode first flushes and freezes writepages), and * security_inode_killpriv() can drop the capability xattr with another * round trip. A server may have to invalidate this inode from inside - * such a handler; its NOTIFY_INVAL_INODE then blocks in - * percpu_down_write() draining a gate reader that is itself waiting for - * the reply. Nothing held under the gate may wait for the server. + * such a handler, and it must not find this write holding anything + * it needs. * * This also runs before the forced-DIO re-route below, so a re-routed * write repeats it; there is nothing left to do the second time. @@ -2124,31 +2056,21 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) if (err) goto out; - wb_guard = !!wb_sem; - if (wb_guard) { -retry: - percpu_down_read(wb_sem); - if (fuse_inode_force_dio(inode)) { - percpu_up_read(wb_sem); - fuse_cache_wr_unlock(inode, exclusive); - return fuse_direct_write_iter(iocb, from); - } - if (writeback && fc->dlm && !dlm_unrecorded && - !fuse_dlm_lock_is_held(fi, dlm_pos, dlm_len, - FUSE_PAGE_LOCK_WRITE)) { - percpu_up_read(wb_sem); - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len, - &dlm_unrecorded); - if (err) { - /* The gate is already dropped; funnel the - * failure through the one audited exit. */ - wb_guard = false; - goto out; - } - goto retry; - } + if (fuse_inode_force_dio(inode)) { + fuse_cache_wr_unlock(inode, exclusive); + return fuse_direct_write_iter(iocb, from); } + /* + * A NOTIFY invalidate can revoke the grant requested above between + * here and the dirtying below, and nothing stops it: the bytes are + * caught on the way out instead. fuse_dlm_range_touched() records + * them before they are dirtied, fuse_dlm_unlock_range() marks the + * range revoked rather than forgetting it, and writeback holds the + * range again before sending anything it finds marked that way. So + * a write racing a revoke costs a round trip, not coverage. + */ + task_io_account_write(count); if (iocb->ki_flags & IOCB_DIRECT) { @@ -2257,8 +2179,6 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) written = fuse_perform_write(iocb, from, false); } out: - if (wb_guard) - percpu_up_read(wb_sem); fuse_cache_wr_unlock(inode, exclusive); if (written > 0) written = generic_write_sync(iocb, written); @@ -3382,7 +3302,7 @@ static int fuse_file_mmap(struct file *file, struct vm_area_struct *vma) /* * If the inode was latched into forced direct IO after a remote-modify * notification, a mapping needs the page cache, so revert to caching - * mode. Revert without the inode lock or wb_inval_rwsem: ->mmap runs + * mode. Revert without the inode lock: ->mmap runs * under mmap_lock and the buffered write path holds both across a fault * on the user buffer (which takes mmap_lock), so taking either here * would invert lock order (ABBA). Clearing the latch and dropping the @@ -4226,23 +4146,6 @@ void fuse_init_file_inode(struct inode *inode, unsigned int flags) fi->iocachectr = 0; init_waitqueue_head(&fi->page_waitq); init_waitqueue_head(&fi->direct_io_waitq); - /* - * Coherency gate for the forced-direct-IO feature; only writeback+dlm - * regular files need it. A percpu_rw_semaphore embeds per-CPU state, - * so allocate it out of line and only when the mount can use it rather - * than paying it on every inode. On failure leave it NULL: the gate - * stays inactive (best-effort invalidate) and the inode is still usable. - */ - fi->wb_inval_rwsem = NULL; - if (fc->writeback_cache && fc->dlm) { - struct percpu_rw_semaphore *sem = kmalloc(sizeof(*sem), GFP_KERNEL); - - if (sem && percpu_init_rwsem(sem)) { - kfree(sem); - sem = NULL; - } - fi->wb_inval_rwsem = sem; - } fi->notify_stamp = jiffies; fi->notify_interval_ewma = FUSE_NOTIFY_EWMA_SEED << FUSE_NOTIFY_EWMA_SHIFT; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 31580b7834e296..e3de135c291220 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -202,21 +202,6 @@ struct fuse_inode { /* dlm locked areas we have sent lock requests for */ struct fuse_dlm_cache dlm_locked_areas; - /* - * Serializes buffered-write page-cache dirtying against - * the forced-direct-IO latch transition driven by - * NOTIFY_INVAL_INODE (fuse_reverse_inval_inode()), which - * may be delivered by the same server thread that still - * owes a reply to an in-flight write holding the inode - * lock. The buffered writer holds this for read around - * the dirtying and re-checks the latch under it; the - * NOTIFY latch site takes it for write (trylock, never - * blocking) around its page-cache invalidate + latch set. - * Only regular files initialise it -- it shares storage - * with the readdir-cache union arm. - */ - struct percpu_rw_semaphore *wb_inval_rwsem; - /* * Rate of FUSE_NOTIFY_INVAL_INODE data invalidations * for this whole file: notify_stamp is the jiffies of diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 3d8932491b638b..d7e4fbf1993208 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -220,23 +220,6 @@ static void fuse_evict_inode(struct inode *inode) WARN_ON(!list_empty(&fi->queued_writes)); fuse_dlm_cache_release_locks(fi); } - - /* - * Free the coherency gate here rather than in ->free_inode: that runs - * from an RCU callback, where percpu_free_rwsem() may sleep in - * rcu_sync_dtor() if the write side has not fully quiesced. No user - * can remain by eviction time: gate readers hold a file reference and - * a concurrent notify holds an inode reference. wb_inval_rwsem lives - * in the regular-file union arm and is only ever allocated for regular - * files, so gate on S_ISREG (but not fuse_is_bad() -- bad-marked - * regular files still own a gate); a directory's overlapping - * readdir-cache fields must not be misread. - */ - if (S_ISREG(inode->i_mode) && fi->wb_inval_rwsem) { - percpu_free_rwsem(fi->wb_inval_rwsem); - kfree(fi->wb_inval_rwsem); - fi->wb_inval_rwsem = NULL; - } } static int fuse_reconfigure(struct fs_context *fsc) @@ -949,7 +932,6 @@ static void fuse_notify_invalidate_range(struct inode *inode, pgoff_t start, int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, loff_t offset, loff_t len) { - struct percpu_rw_semaphore *wb_sem = NULL; struct fuse_inode *fi; struct inode *inode; uint64_t pg_first; @@ -1002,56 +984,48 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, (((uint64_t)offset + len - 1) | (PAGE_SIZE - 1)); /* - * A data invalidation means another (remote) entity is modifying - * the file. Two things happen here: + * A data invalidation means another (remote) entity is + * modifying the file. Two things happen here: * - * 1. Coherency. Drop the affected page-cache range so no local - * read returns a folio the remote modify has superseded. This - * runs under the write side of the per-inode coherency gate - * (wb_inval_rwsem), which fences cache-serving buffered reads - * and buffered writes out for the whole invalidate. Unlike the - * old best-effort trylock this BLOCKS -- the notify has - * priority: percpu_down_write() parks new gate readers, drains - * in-flight ones, then invalidates. A blocking writer here is - * safe only under a server that services request replies on - * threads other than the one delivering this notify: the write - * side waits for gate readers to drain, and a cache-miss read - * holds the read side across its FUSE_READ round-trip. redfs' - * dlm server provides that contract; a server that cannot must - * not enable writeback+dlm. + * 1. Coherency. Drop the affected page-cache range so no + * local read returns a folio the remote modify has + * superseded. Nothing is fenced out for it. A read + * racing the drop either misses and refetches or returns + * data that was current when it was copied. A write + * racing it is caught on the way out instead: its bytes + * were recorded before they were dirtied, this revoke + * marks the range rather than forgetting it, and + * writeback holds the range again before sending + * anything it finds marked that way. * - * 2. Latch. Keep a moving average (fuse_notify_inval_hot(), under - * fi->lock, updated for every data invalidation) of how fast - * these arrive; when they come in a rapid stream -- a remote - * writer repeatedly invalidating -- and the inode is also open - * for writing here, latch it into direct IO until the last - * writer closes or it is mmapped. When latched, drop the whole - * mapping rather than just the notified range, or dirty folios - * outside it would be invisible to the forced direct reads - * (stale read / lost write). Latching is opt-in via the - * enable_notify_dio module parameter and off by default; the - * average is kept up to date either way, so enabling it at - * runtime takes effect on the next storm rather than after a - * warm-up. Clearing it at runtime stops new latches but lets + * 2. Latch. Keep a moving average (fuse_notify_inval_hot(), + * under fi->lock, updated for every data invalidation) of + * how fast these arrive; when they come in a rapid stream + * -- a remote writer repeatedly invalidating -- and the + * inode is also open for writing here, latch it into + * direct IO until the last writer closes or it is mmapped. + * When latched, drop the whole mapping rather than just + * the notified range, or dirty folios outside it would be + * invisible to the forced direct reads (stale read / lost + * write). Latching is opt-in via the enable_notify_dio + * module parameter and off by default; the average is kept + * up to date either way, so enabling it at runtime takes + * effect on the next storm rather than after a warm-up. + * Clearing it at runtime stops new latches but lets * already-latched inodes run out on the usual exits (last * writer closes, or mmap). * - * The gate (and the average) exist only for writeback+dlm regular - * files; elsewhere wb_sem is NULL and the invalidate runs - * unserialized (best-effort), as before. An mmapped inode - * keeps the gate -- fuse_cache_read_iter() and - * fuse_cache_write_iter() enter it unconditionally and rely - * on the revoke staying fenced -- but is never latched: - * a mapping needs the page cache, and fuse_file_mmap() - * reverts any latch it races with. + * The average and the latch exist only for writeback+dlm + * regular files; elsewhere there is no record to consult and + * the range is dropped as it always was. An mmapped inode is + * never latched: a mapping needs the page cache, and + * fuse_file_mmap() reverts any latch it races with. */ tracked = S_ISREG(inode->i_mode) && fc->writeback_cache && fc->dlm && !FUSE_IS_DAX(inode) && !fuse_inode_backing(fi); - if (tracked) - wb_sem = fi->wb_inval_rwsem; - if (wb_sem) { + if (tracked) { bool hot, has_writer, latched = false; bool may_be_dirty, has_pages; @@ -1061,23 +1035,11 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, spin_unlock(&fi->lock); /* - * Priority write side: park new gate readers, - * drain in-flight ones, then invalidate. Blocks - * (unlike the old trylock) -- see the contract in - * the comment above. - */ - percpu_down_write(wb_sem); - - /* - * Ask what is left to do, under the gate so no - * reader can populate and no writer can dirty - * between the answer and the drop below. - * - * Nothing cached in the range means the drop is a - * no-op and the revoke is the whole job. Otherwise - * the record decides whether the drop has to - * launder, which is what makes it wait for a - * FUSE_WRITE reply. + * What this notify has to do. Nothing cached in the + * range means the drop is a no-op and the revoke is + * the whole job. Otherwise the record says whether + * the drop has to launder, which is what makes it + * wait for a FUSE_WRITE reply. */ has_pages = filemap_range_has_page(inode->i_mapping, offset, end_byte); @@ -1086,27 +1048,19 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, /* * Start unwritten data on its way while the grant - * still covers it, rather than leaving it to the - * drop below. After the revoke those bytes - * classify as FUSE_DLM_RUN_REVOKED, and writeback - * would take the range again to send them: a DLM - * round trip from inside the handler the server is - * waiting on. do_writepages() runs in this context, - * so the classification is made before the revoke. + * still covers it, rather than leaving it to the drop + * below. After the revoke those bytes classify as + * FUSE_DLM_RUN_REVOKED, and writeback would take the + * range again to send them: a DLM round trip from + * inside the handler the server is waiting on. + * do_writepages() runs in this context, so the + * classification is made before the revoke. */ if (has_pages && may_be_dirty) filemap_fdatawrite_range(inode->i_mapping, offset, end_byte); - /* - * Revoke the DLM lock range under the gate write - * side, atomically with the page drop: gate readers - * re-validate their grant right after entering, and - * a grant that passed that check must stay visible - * for their whole gate hold. - */ - if (fc->dlm && fc->writeback_cache) - fuse_dlm_revoke_inval_range(fi, offset, len); + fuse_dlm_revoke_inval_range(fi, offset, len); if (enable_notify_dio && hot && has_writer && !mapping_mapped(inode->i_mapping) && @@ -1136,28 +1090,24 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, /* * A revoked range exists to describe page cache - * dirtied before the grant went; with that cache - * gone it has nothing left to say. Only when it - * really went: an invalidate can leave a busy folio - * behind, and that folio still needs its record. + * dirtied before the grant went; with that cache gone + * it has nothing left to say. Only when it really + * went: an invalidate can leave a busy folio behind, + * and that folio still needs its record. */ if (has_pages && !filemap_range_has_page(inode->i_mapping, offset, end_byte)) fuse_dlm_ranges_dropped(fi, pg_first, pg_last); - percpu_up_write(wb_sem); - if (latched) pr_info_ratelimited("FUSE: inode %llu latched to direct IO on invalidation notify storm\n", nodeid); } else { /* - * No gate on this inode (DAX, backing, non-regular, - * or the gate allocation failed): drop the lock - * range unserialized (best-effort), as before. No - * record to consult either, so assume the range can - * hold unwritten data. + * No record on this inode (DAX, backing, non-regular, + * or no DLM), so assume the range can hold unwritten + * data and drop it as before. */ if (fc->dlm && fc->writeback_cache) fuse_dlm_revoke_inval_range(fi, offset, len); From 544bbad2ad001924144b8f28d3bcbc6a2e549fcc Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:00:57 +0200 Subject: [PATCH 11/41] fuse: read part of a folio without zeroing the rest fuse_do_readfolio() asks for page_zeroing, and fuse_copy_folio() answers that by zeroing the whole folio whenever the request covers less than all of it. That is right for a whole folio read, where the zeroing fills a short reply, and wrong for anything smaller: fuse_iomap_read_folio_range() already asks for one block of a large folio at a time, so a read-modify-write of one block wipes every other block of the folio while their iomap uptodate bits still call them valid. Add fuse_read_folio_range(), which asks without page_zeroing and zeroes exactly what the reply left short, and use it for the partial reads. fuse_do_readfolio() keeps the whole folio callers. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 5afd85b8dab4d8..c7a464d3306ad5 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1019,6 +1019,58 @@ static int fuse_do_readfolio(struct file *file, struct folio *folio, return 0; } +/** + * fuse_read_folio_range - read part of a folio from the server + * @file: file to read through + * @folio: the folio to fill + * @off: offset within @folio to start at + * @len: bytes to read + * + * fuse_do_readfolio() cannot serve a partial folio: it asks for + * page_zeroing, and fuse_copy_folio() answers that by zeroing the whole + * folio whenever the request covers less than all of it. Ask without it + * and zero exactly what the reply left short, which is the server saying + * the file ends there. + * + * Return: 0, AOP_TRUNCATED_PAGE, or a negative error. + */ +static int fuse_read_folio_range(struct file *file, struct folio *folio, + size_t off, size_t len) +{ + struct inode *inode = folio->mapping->host; + struct fuse_mount *fm = get_fuse_mount(inode); + loff_t pos = folio_pos(folio) + off; + struct fuse_folio_desc desc = { + .offset = off, + .length = len, + }; + struct fuse_io_args ia = { + .ap.args.out_pages = true, + .ap.num_folios = 1, + .ap.folios = &folio, + .ap.descs = &desc, + }; + ssize_t res; + + /* Don't overflow end offset */ + if (pos + (desc.length - 1) == LLONG_MAX) + desc.length--; + + fuse_read_args_fill(&ia, file, pos, desc.length, FUSE_READ); + res = fuse_simple_request(fm, &ia.ap.args); + if (res < 0) { + /* See fuse_do_readfolio() for why READ can return -EDEADLK */ + if ((res == -EDEADLK || res == -EAGAIN) && fm->fc->dlm) + res = AOP_TRUNCATED_PAGE; + return res; + } + + if (res < desc.length) + folio_zero_range(folio, off + res, desc.length - res); + + return 0; +} + static int fuse_read_folio(struct file *file, struct folio *folio) { struct inode *inode = folio->mapping->host; @@ -1048,7 +1100,7 @@ static int fuse_iomap_read_folio_range(const struct iomap_iter *iter, size_t off = offset_in_folio(folio, pos); int ret; - ret = fuse_do_readfolio(file, folio, off, len); + ret = fuse_read_folio_range(file, folio, off, len); /* * TEMPORARY WORKAROUND for iomap write deadlock: From 3fca830126a32dd145f3c8f07408b2ba4339eade Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:01:48 +0200 Subject: [PATCH 12/41] fuse: bound the run a DLM classification covers, including no record fuse_dlm_dirty_run() walked the ranges overlapping the query and gave up on the first gap, returning FUSE_DLM_RUN_UNKNOWN without saying how far that answer held, so a run classified up to the gap was thrown away and the caller sent the whole query range whole. Treat a gap as a classification of its own and report how far it reaches, the same as any other run. A caller now always learns the extent of the answer it got, and a record that covers part of the query is used for that part instead of being discarded. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 6 ++- fs/fuse/fuse_dlm_cache.c | 99 +++++++++++++++++++++------------------- fs/fuse/fuse_dlm_cache.h | 3 +- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index c7a464d3306ad5..f93ddb2aa90a1b 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -3066,9 +3066,11 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, switch (fuse_dlm_dirty_run(fi, pos, len, &run)) { case FUSE_DLM_RUN_UNKNOWN: /* - * No record for this range: send it whole, as - * without DLM. + * No record for this run: send it whole, as without + * DLM. Only this run, since the record may well + * describe what follows it. */ + len = run; break; case FUSE_DLM_RUN_CLEAN: /* diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 711355dd4384a0..724826bb10b4f7 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -692,6 +692,39 @@ static enum fuse_dlm_run fuse_dlm_classify(struct fuse_dlm_range *range) FUSE_DLM_RUN_DIRTY; } +/** + * fuse_dlm_kind_at - how the byte at @cur classifies, and how far that holds + * @cache: the page cache + * @cur: byte offset to classify + * @end: last byte of interest + * @last: set to the last byte the answer covers + * + * A gap in the record, or a range held only for read, is + * %FUSE_DLM_RUN_UNKNOWN: this client cannot have written it. Either way + * @last says how far to look next, so no caller has to rediscover it. + * + * Caller holds @cache->lock. + */ +static enum fuse_dlm_run fuse_dlm_kind_at(struct fuse_dlm_cache *cache, + uint64_t cur, uint64_t end, + uint64_t *last) +{ + struct fuse_dlm_range *range; + + range = fuse_page_it_iter_first(&cache->ranges, cur, end); + if (!range || range->start > cur) { + /* Nothing recorded up to the next range, or to @end */ + *last = range ? range->start - 1 : end; + return FUSE_DLM_RUN_UNKNOWN; + } + + *last = min(range->end, end); + if (range->state == FUSE_DLM_RANGE_READ) + return FUSE_DLM_RUN_UNKNOWN; + + return fuse_dlm_classify(range); +} + /** * fuse_dlm_dirty_run - classify the run starting at @pos * @inode: the fuse inode @@ -699,29 +732,26 @@ static enum fuse_dlm_run fuse_dlm_classify(struct fuse_dlm_range *range) * @len: bytes of interest from @pos * @run: set to how far the classification holds, capped at @len * - * Walks forward from @pos while the classification stays the same, so - * writeback can ask what to do with one run of a folio at a time. + * Walks forward from @pos while the classification stays the same, so a + * caller can ask what to do with one run of a folio at a time. * * Return: - * %FUSE_DLM_RUN_UNKNOWN - no record for [pos, pos + len): either a gap, - * or a range held only for read, which this client cannot have - * written but which does not rule out someone else having dirtied - * the folio. @run is not set; the caller writes the whole range. + * %FUSE_DLM_RUN_UNKNOWN - no record for the run: either a gap, or a range + *\theld only for read, which this client cannot have written. * %FUSE_DLM_RUN_CLEAN - nothing was written here. * %FUSE_DLM_RUN_DIRTY - written under a grant this client still holds. * %FUSE_DLM_RUN_REVOKED - written, but the grant has since been taken - * away. The bytes are real and must not be lost, so the caller has - * to hold the range again before sending them. + *\taway. The bytes are real and must not be lost, so a caller sending + *\tthem has to hold the range again first. + * + * @run is set for every return except a zero @len, and is never 0. */ enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, size_t len, size_t *run) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - enum fuse_dlm_run kind = FUSE_DLM_RUN_UNKNOWN; - struct fuse_dlm_range *range; - uint64_t end, cur = pos; - bool first = true; - size_t reach = 0; + enum fuse_dlm_run kind; + uint64_t end, reach; if (!len) return FUSE_DLM_RUN_UNKNOWN; @@ -729,44 +759,21 @@ enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, down_read(&cache->lock); - for (range = fuse_dlm_find_overlapping(cache, pos, end); range; - range = fuse_page_it_iter_next(range, pos, end)) { - enum fuse_dlm_run range_kind; + kind = fuse_dlm_kind_at(cache, pos, end, &reach); + while (reach < end) { + enum fuse_dlm_run next; + uint64_t last; - /* - * A gap, or a range held only for read: this client cannot - * have written it, and nothing here proves nobody else - * dirtied the folio, so refuse to answer. - */ - if (range->start > cur || range->state == FUSE_DLM_RANGE_READ) - goto unknown; - - range_kind = fuse_dlm_classify(range); - if (first) { - kind = range_kind; - first = false; - } else if (range_kind != kind) { - goto out; /* the run ends where the answer does */ - } - - if (range->end >= end) { - reach = len; - goto out; - } - - /* Safe: range->end < end, so this cannot wrap */ - cur = range->end + 1; - reach = cur - pos; + /* Safe: reach < end, so this cannot wrap */ + next = fuse_dlm_kind_at(cache, reach + 1, end, &last); + if (next != kind) + break; + reach = last; } - /* Ran out of ranges before reaching @end */ -unknown: up_read(&cache->lock); - return FUSE_DLM_RUN_UNKNOWN; -out: - up_read(&cache->lock); - *run = reach; + *run = reach - pos + 1; return kind; } diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 28b8cbae0c1f98..b9c2d857df32d3 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -127,7 +127,8 @@ enum fuse_dlm_run { /* * Classify the run from @pos, setting @run to how far the answer holds, - * capped at @len. @run is left alone for FUSE_DLM_RUN_UNKNOWN. + * capped at @len. @run is set for every query of a non-zero @len, no + * record included, and is never 0. */ enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, size_t len, size_t *run); From f88ac89695dc27fcdf7d71f437fafafb99e9b42d Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:03:13 +0200 Subject: [PATCH 13/41] fuse: leave a partly written folio unfilled instead of reading it A cached write covering part of a folio makes iomap read the rest in so the folio can be called valid. Under DLM that read buys nothing: writeback already declines to send the server's own bytes back, an expanding write pays a round trip for a range that holds nothing, and a write-only handle has the READ refused. Skip it. fuse_iomap_read_folio_range() leaves the range alone, fuse_iomap_put_folio() clears the uptodate flag under the folio lock and records what the copy actually reached, and fuse_read_folio() fills the record's gaps on the next read. Only for folios of a single block: clearing the flag on a larger one leaves iomap's per block bits set, which a read consults through ->is_partially_uptodate. Such a folio can be dirty and invalid at once, so fuse_read_folio() waits out writeback first. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 191 +++++++++++++++++++++++++++++++++------ fs/fuse/fuse_dlm_cache.c | 73 ++++++++++++++- fs/fuse/fuse_dlm_cache.h | 7 ++ fs/fuse/fuse_i.h | 6 ++ 4 files changed, 244 insertions(+), 33 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index f93ddb2aa90a1b..714af8cb31e238 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1071,16 +1071,84 @@ static int fuse_read_folio_range(struct file *file, struct folio *folio, return 0; } +/** + * fuse_read_folio_merge - fill @folio without disturbing what is written + * @file: file to read through + * @folio: the folio to fill + * + * A folio a partial write left behind holds the bytes that write copied + * and nothing else. The record says where they are; only the gaps + * between them are the server's to fill, and reading over them would + * lose data the server has not seen yet. + * + * Return: 0, AOP_TRUNCATED_PAGE, or a negative error. + */ +static int fuse_read_folio_merge(struct file *file, struct folio *folio) +{ + struct inode *inode = folio->mapping->host; + struct fuse_inode *fi = get_fuse_inode(inode); + uint64_t pos = folio_pos(folio); + size_t size = folio_size(folio); + size_t done = 0; + + while (done < size) { + size_t run = size - done; + int err; + + switch (fuse_dlm_dirty_run(fi, pos + done, size - done, &run)) { + case FUSE_DLM_RUN_DIRTY: + case FUSE_DLM_RUN_REVOKED: + /* Written here and not on the server yet: keep it */ + break; + default: + err = fuse_read_folio_range(file, folio, done, run); + if (err) + return err; + break; + } + + if (WARN_ON_ONCE(!run)) + return -EIO; + done += run; + } + + return 0; +} + static int fuse_read_folio(struct file *file, struct folio *folio) { struct inode *inode = folio->mapping->host; + struct fuse_conn *fc = get_fuse_conn(inode); int err; err = -EIO; if (fuse_is_bad(inode)) goto out; - err = fuse_do_readfolio(file, folio, 0, folio_size(folio)); + /* + * Writeback unlocks a folio as soon as it has handed it over, with + * the writeback flag still on it, so this can be reached while a + * FUSE_WRITE is still reading out of it. Filling it now would + * rewrite what is being sent, and past the end of the file the reply + * comes back short and zeroes it. Nothing reached here before a + * partial write started leaving folios invalid, because a dirty + * folio was always valid and never came this way. + */ + folio_wait_writeback(folio); + + /* + * Only a folio still holding what a write put in it has anything to + * keep. The record describes the inode, not one incarnation of a + * folio: a clean folio was either never written through here or has + * been reclaimed and allocated again since, and either way it holds + * none of what the record names. One that was merely being written + * back is clean by now, and the record stopped naming those bytes + * when they went, so it reads whole. + */ + if (fc->dlm && fc->writeback_cache && folio_test_dirty(folio)) + err = fuse_read_folio_merge(file, folio); + else + err = fuse_do_readfolio(file, folio, 0, folio_size(folio)); if (!err) folio_mark_uptodate(folio); @@ -1100,6 +1168,33 @@ static int fuse_iomap_read_folio_range(const struct iomap_iter *iter, size_t off = offset_in_folio(folio, pos); int ret; + /* + * Under DLM this fills nothing. These are the bytes the write does + * not cover, so they belong to the server, and writeback already + * declines to send them back; fetching them buys only the right to + * call the folio valid, at a round trip an expanding write spends on + * a range that holds nothing, over a handle that may not even be + * able to read. Leave them alone. fuse_iomap_put_folio() takes the + * uptodate flag back off the folio, so the first reader fetches them + * and nothing invents a value in the meantime. + * + * Only for a folio the page cache tracks in one piece. Clearing the + * uptodate flag of a folio of several blocks leaves iomap's per block + * bits set behind it, and a read asks those through + * ->is_partially_uptodate rather than calling ->read_folio, so it + * would be served exactly the bytes this skipped. + */ + if (fc->dlm && fc->writeback_cache && + i_blocksize(inode) == folio_size(folio)) { + struct fuse_dlm_retry *wr; + + wr = xa_load(&fc->dlm_retry_tasks, (unsigned long) current); + if (wr) { + wr->deferred = folio; + return 0; + } + } + ret = fuse_read_folio_range(file, folio, off, len); /* @@ -1722,8 +1817,48 @@ static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive, bool uncached) } } +/* + * Called for every folio the write touched, with the folio still locked + * and @copied set to what actually landed in it. Owns the unlock. + */ +static void fuse_iomap_put_folio(struct inode *inode, loff_t pos, + unsigned int copied, struct folio *folio) +{ + struct fuse_conn *fc = get_fuse_conn(inode); + struct fuse_inode *fi = get_fuse_inode(inode); + + if (fc->dlm && fc->writeback_cache) { + struct fuse_dlm_retry *wr; + + /* + * Record the bytes now that they are in the folio. Marking + * the intended range before the copy claimed what a short or + * failed one never reached, and on a folio left invalid + * below writeback would send exactly those bytes. + */ + if (copied) + fuse_dlm_range_written(fi, pos, pos + copied - 1); + + /* + * The fill was skipped, so outside what was just recorded + * this folio holds bytes nobody wrote. iomap called it + * valid on the way in; take that back while it is still + * locked, so no reader ever sees them. + */ + wr = xa_load(&fc->dlm_retry_tasks, (unsigned long) current); + if (wr && wr->deferred == folio) { + wr->deferred = NULL; + folio_clear_uptodate(folio); + } + } + + folio_unlock(folio); + folio_put(folio); +} + static const struct iomap_write_ops fuse_iomap_write_ops = { .read_folio_range = fuse_iomap_read_folio_range, + .put_folio = fuse_iomap_put_folio, }; static int fuse_iomap_begin(struct inode *inode, loff_t offset, loff_t length, @@ -1779,6 +1914,7 @@ static ssize_t fuse_writeback_write_iter(struct kiocb *iocb, * would re-enter with len==0 and livelock on a 0-length mapping. */ retry_state.retry_needed = false; + retry_state.deferred = NULL; /* * Use iomap so that we can do granular uptodate reads @@ -2116,11 +2252,12 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) /* * A NOTIFY invalidate can revoke the grant requested above between * here and the dirtying below, and nothing stops it: the bytes are - * caught on the way out instead. fuse_dlm_range_touched() records - * them before they are dirtied, fuse_dlm_unlock_range() marks the - * range revoked rather than forgetting it, and writeback holds the - * range again before sending anything it finds marked that way. So - * a write racing a revoke costs a round trip, not coverage. + * caught on the way out instead. fuse_dlm_range_written() records + * them as they land, covering the range itself if the revoke got + * there first, fuse_dlm_unlock_range() marks a range revoked rather + * than forgetting it, and writeback holds the range again before + * sending anything it finds marked that way. So a write racing a + * revoke costs a round trip, not coverage. */ task_io_account_write(count); @@ -2169,35 +2306,22 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) /* * Zero the tail of the folio straddling the old EOF. - * That dirties it without going through the write - * below, so raise the record first or it would keep - * calling that page clean. + * Inert while the fuse block size is PAGE_SIZE, which + * it always is, and nothing is recorded for it either + * way: claiming the whole gap as written would hand + * writeback bytes no one wrote. */ - if (extended && orig_size < pos) { - if (fc->dlm) - fuse_dlm_range_touched(fi, orig_size, - pos - 1, - FUSE_PAGE_LOCK_WRITE); + if (extended && orig_size < pos) pagecache_isize_extended(inode, orig_size, pos); - } } - /* - * Mark the exact bytes about to be dirtied, before they - * are. The DLM grant covering them is page aligned and - * this is not; that difference is the record of which part - * of a boundary page this client actually wrote. A short - * write leaves the unreached tail marked, which overstates. - */ - if (fc->dlm) - fuse_dlm_range_touched(fi, pos, end - 1, - FUSE_PAGE_LOCK_WRITE); - /* * 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 pages are - * cached for writeback. + * cached for writeback, and their bytes are recorded in + * fuse_iomap_put_folio(), which is told what each folio + * actually took. */ if (fc->dlm) written = fuse_dlm_buffered_write(iocb, from, file); @@ -3066,10 +3190,17 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, switch (fuse_dlm_dirty_run(fi, pos, len, &run)) { case FUSE_DLM_RUN_UNKNOWN: /* - * No record for this run: send it whole, as without - * DLM. Only this run, since the record may well - * describe what follows it. + * No record for this run. Over a folio the server + * filled, the bytes are its own and go back whole, as + * without DLM; only this run, since the record may + * well describe what follows it. Over one a partial + * write left waiting for its fill, nobody ever wrote + * them, and sending them would invent content. */ + if (!folio_test_uptodate(folio)) { + wpc->iomap.type = IOMAP_HOLE; + return run; + } len = run; break; case FUSE_DLM_RUN_CLEAN: diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 724826bb10b4f7..7652a37fcd9bf9 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -345,7 +345,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* If there's a gap before this range, we need to add the missing range */ if (current_start < range->start) { - new_range = kmalloc(sizeof(*new_range), GFP_KERNEL); + new_range = kmalloc(sizeof(*new_range), GFP_NOFS); if (!new_range) { ret = -ENOMEM; goto out_free; @@ -372,7 +372,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* If there's a gap after the last range to the end, extend the range */ if (!covered_to_end && current_start <= end) { - new_range = kmalloc(sizeof(*new_range), GFP_KERNEL); + new_range = kmalloc(sizeof(*new_range), GFP_NOFS); if (!new_range) { ret = -ENOMEM; goto out_free; @@ -549,7 +549,7 @@ static int fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) if (!range || range->start == off) return 0; - tail = kmalloc(sizeof(*tail), GFP_KERNEL); + tail = kmalloc(sizeof(*tail), GFP_NOFS); if (!tail) return -ENOMEM; @@ -623,6 +623,73 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } +/** + * fuse_dlm_range_written - record that [start, end] holds written bytes + * @inode: the fuse inode + * @start: first byte written (inclusive) + * @end: last byte written (inclusive) + * + * fuse_dlm_range_touched() in write mode, except that a part of + * [start, end] no range covers is given one instead of going + * unrecorded. The write happened under a grant; if nothing covers those + * bytes by the time they are recorded, the grant was taken away in + * between, which is what FUSE_DLM_RANGE_REVOKED says: the bytes are + * real, the grant is gone, and writeback has to hold the range again + * before sending them. Left unrecorded they would read as never + * written, and writeback would drop them. + */ +void fuse_dlm_range_written(struct fuse_inode *inode, uint64_t start, + uint64_t end) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + struct fuse_dlm_range *range, *fill; + uint64_t cur = start; + + if (start > end) + return; + + down_write(&cache->lock); + + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); + + while (cur <= end) { + uint64_t reach; + + range = fuse_page_it_iter_first(&cache->ranges, cur, end); + if (!range || range->start > cur) { + reach = range ? range->start - 1 : end; + + /* + * Losing this would lose the bytes, so it cannot be + * allowed to fail; iomap allocates the state it keeps + * per folio the same way. + */ + fill = kmalloc(sizeof(*fill), + GFP_NOFS | __GFP_NOFAIL); + fill->start = cur; + fill->end = reach; + fill->state = FUSE_DLM_RANGE_REVOKED; + fill->content = FUSE_DLM_CONTENT_DIRTY; + INIT_LIST_HEAD(&fill->list); + fuse_page_it_insert(fill, &cache->ranges); + } else { + reach = min(range->end, end); + if (range->content < FUSE_DLM_CONTENT_DIRTY) + range->content = FUSE_DLM_CONTENT_DIRTY; + } + + if (reach == end) + break; + cur = reach + 1; + } + + fuse_dlm_try_merge(cache, start, end); + + up_write(&cache->lock); +} + /* * Marking done by fuse_get_dlm_lock() itself. A read populates whole * pages, so the page aligned request range is the right thing to mark. diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index b9c2d857df32d3..cffe90c39f86a4 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -106,6 +106,13 @@ bool fuse_dlm_write_grant_exists(struct fuse_inode *inode); void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, uint64_t end, enum fuse_page_lock_mode mode); +/* + * Record that [start, end] holds bytes this client wrote. Unlike + * fuse_dlm_range_touched(), a part of it no range covers is given one. + */ +void fuse_dlm_range_written(struct fuse_inode *inode, uint64_t start, + uint64_t end); + /* * [start, end] has been written back and waited out. Caller holds * i_rwsem exclusive; a mapped inode is left alone. diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index e3de135c291220..97d042ea155ba5 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -695,6 +695,12 @@ struct fuse_sync_bucket { */ struct fuse_dlm_retry { bool retry_needed; + /* + * Folio whose fill fuse_iomap_read_folio_range() left undone, so + * fuse_iomap_put_folio() knows to take the uptodate flag back off + * before anyone else can see it. + */ + struct folio *deferred; }; /** From bdc02ee98b3306c18cdcfa778dce3797849e45e5 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:08:49 +0200 Subject: [PATCH 14/41] fuse: stop naming bytes the record has already handed over The record describes the inode, not one incarnation of a folio. A folio left invalid by a partial write is clean once writeback has sent what it held, and reclaim may take it; the record still names those bytes, so fuse_read_folio_merge() would decline to fetch them for the next folio at that index. Lower the record as writeback hands a run over, for an invalid folio only. The folio lock orders it against a rewrite. fuse_dlm_split_at() can no longer fail: it now decides which bytes exist. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 18 +++++++++++ fs/fuse/fuse_dlm_cache.c | 69 +++++++++++++++++++++++++++++----------- fs/fuse/fuse_dlm_cache.h | 6 ++++ 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 714af8cb31e238..5a20bc5d88e639 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -3226,8 +3226,26 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, fallthrough; case FUSE_DLM_RUN_DIRTY: len = run; + /* + * On a folio the page cache does not consider valid, + * the record is the only thing saying which of its + * bytes are real. Once they are on their way the + * folio need not survive, and a later one at the same + * index must not be told it holds them. + */ + if (!folio_test_uptodate(folio)) + fuse_dlm_range_sent(fi, pos, pos + len - 1); break; } + } else if (!folio_test_uptodate(folio)) { + /* + * A folio a deferred fill left behind, reached with the + * record no longer consulted because the server turned out + * to have no DLM after all. Nothing here is known to be + * real, so send none of it. + */ + wpc->iomap.type = IOMAP_HOLE; + return len; } offset = offset_in_folio(folio, pos); diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 7652a37fcd9bf9..2dd1c58c86e948 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -535,23 +535,22 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, * * Caller holds @cache->lock for write. * - * Return: 0, or -ENOMEM. A caller that cannot split must mark more than - * it meant to, never less. + * Cannot fail: the split decides which bytes a caller goes on to name, + * and both naming more than was written and lowering more than was sent + * lose data. iomap allocates the state it keeps per folio the same way. */ -static int fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) +static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) { struct fuse_dlm_range *range, *tail; if (!off) - return 0; + return; range = fuse_page_it_iter_first(&cache->ranges, off, off); if (!range || range->start == off) - return 0; + return; - tail = kmalloc(sizeof(*tail), GFP_NOFS); - if (!tail) - return -ENOMEM; + tail = kmalloc(sizeof(*tail), GFP_NOFS | __GFP_NOFAIL); *tail = *range; INIT_LIST_HEAD(&tail->list); @@ -565,8 +564,6 @@ static int fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) range->end = off - 1; fuse_page_it_insert(range, &cache->ranges); fuse_page_it_insert(tail, &cache->ranges); - - return 0; } /** @@ -604,10 +601,6 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, down_write(&cache->lock); - /* - * A split that cannot allocate leaves the range whole, and the - * loop below then marks more than was written. - */ fuse_dlm_split_at(cache, start); if (end < U64_MAX) fuse_dlm_split_at(cache, end + 1); @@ -623,6 +616,48 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } +/** + * fuse_dlm_range_sent - [start, end] is on its way to the server + * @inode: the fuse inode + * @start: first byte handed over (inclusive) + * @end: last byte handed over (inclusive) + * + * Lowers the recorded content of [start, end] back to clean. Writeback + * calls this for a folio the page cache does not consider valid, where + * the record is the only thing saying which of its bytes are real: once + * they have been handed over the folio need not survive, and a later one + * at the same index must not be told that bytes it does not hold were + * written. + * + * The caller holds the folio lock, so a write dirtying the range again + * either came before this and is in what is being sent, or comes after + * and records itself. + */ +void fuse_dlm_range_sent(struct fuse_inode *inode, uint64_t start, + uint64_t end) +{ + struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; + struct fuse_dlm_range *range; + + if (start > end) + return; + + down_write(&cache->lock); + + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); + + for (range = fuse_dlm_find_overlapping(cache, start, end); range; + range = fuse_page_it_iter_next(range, start, end)) + if (range->content == FUSE_DLM_CONTENT_DIRTY) + range->content = FUSE_DLM_CONTENT_CLEAN; + + fuse_dlm_try_merge(cache, start, end); + + up_write(&cache->lock); +} + /** * fuse_dlm_range_written - record that [start, end] holds written bytes * @inode: the fuse inode @@ -987,11 +1022,7 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, */ fuse_dlm_kill_pending(cache, start, end); - /* - * Split so the revoked region has its own ranges. A split that - * cannot allocate leaves the range whole and revokes more than the - * server asked for, which costs a re-request and nothing else. - */ + /* Split so the revoked region has its own ranges */ fuse_dlm_split_at(cache, start); if (end < U64_MAX) fuse_dlm_split_at(cache, end + 1); diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index cffe90c39f86a4..e601dd9e68c6b0 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -106,6 +106,12 @@ bool fuse_dlm_write_grant_exists(struct fuse_inode *inode); void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, uint64_t end, enum fuse_page_lock_mode mode); +/* + * [start, end] is on its way to the server: the record stops naming it. + */ +void fuse_dlm_range_sent(struct fuse_inode *inode, uint64_t start, + uint64_t end); + /* * Record that [start, end] holds bytes this client wrote. Unlike * fuse_dlm_range_touched(), a part of it no range covers is given one. From 1243863e58a89745557cb6e4f6dcf510bda70eff Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:20:58 +0200 Subject: [PATCH 15/41] fuse: bound the re-request of a grant a revoke killed in flight fuse_dlm_request_commit() reports -EAGAIN when a revoke overlapped a request while it was on the wire, and __fuse_get_dlm_lock() went round again with nothing stopping it. A remote node revoking as fast as the grants are handed out keeps that going for as long as it likes, and writeback asks for a grant with a folio locked, so the loop is not merely slow, it holds a folio hostage and the task is unkillable while it does. Give it a count and a signal check. Both are generous: every pass is a whole round trip, so reaching either means the range is genuinely being fought over and the caller is better told than left spinning. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 2dd1c58c86e948..a24d01d9745af0 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -38,11 +38,18 @@ #include "fuse_dlm_cache.h" #include +#include #include #include #include +/* + * How often to ask again for a grant a revoke killed while it was in + * flight, before giving up on the range. Each pass is a round trip. + */ +#define FUSE_DLM_GRANT_RETRIES 16 + /* Lifecycle of a range; see the file comment above */ enum fuse_dlm_range_state { /* FUSE_DLM_WB_LOCK in flight, on cache->pending */ @@ -1218,6 +1225,7 @@ static int __fuse_get_dlm_lock(struct fuse_file *ff, struct inode *inode, struct fuse_dlm_lock_out outarg; struct fuse_dlm_range req; uint64_t pg_start, pg_end; + int tries = FUSE_DLM_GRANT_RETRIES; int err; /* An empty range needs no lock. */ @@ -1292,10 +1300,18 @@ static int __fuse_get_dlm_lock(struct fuse_file *ff, struct inode *inode, * A revoke overlapping this range was processed while the * request was in flight, so the grant is dead. Retry * rather than fail: no one else holds the range, and the - * write path turns an error into a failed write. Each - * pass is a fresh round trip, so a revoke storm throttles - * the loop. + * write path turns an error into a failed write. + * + * Not forever, though. Every pass is a whole round trip, + * which throttles the loop but does not end it, and + * writeback asks for a grant with a folio locked, so a node + * revoking as fast as the grants arrive would hold that + * folio and this task for as long as it kept going. */ + if (fatal_signal_pending(current)) + return -EINTR; + if (!tries--) + return -EIO; goto restart; } From c7d382e9d344acae602546cae70773221f6c9b31 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:21:15 +0200 Subject: [PATCH 16/41] fuse: free the DLM ranges a truncate emptied fuse_do_setattr() revokes the grants past the new size and discards the page cache there, but never tells the record. A revoked range is kept because it describes page cache dirtied before the grant went, so every truncate leaves ranges describing folios that no longer exist. Nothing frees them before the inode, and until then the inode reports unwritten data and every later invalidate over that region launders. Call fuse_dlm_ranges_dropped() from the first whole page above the new size; the page holding the new end of the file survives, so its record has to. Signed-off-by: Horst Birthelmer --- fs/fuse/dir.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 62641f724b6746..df226667ca674c 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2233,10 +2233,21 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, * fenced out. i_rwsem is held exclusive here as well. */ if (fc->dlm && fc->writeback_cache) - fuse_dlm_unlock_range(fi, outarg.attr.size & PAGE_MASK, -1); + fuse_dlm_unlock_range(fi, outarg.attr.size & PAGE_MASK, + U64_MAX); truncate_pagecache(inode, outarg.attr.size); invalidate_inode_pages2(mapping); + + /* + * The cache above the new size is gone, so the ranges + * describing it have nothing left to say. From the first + * whole page above it: the page holding the new end of the + * file survives the truncate, and so does its record. + */ + if (fc->dlm && fc->writeback_cache) + fuse_dlm_ranges_dropped(fi, PAGE_ALIGN(outarg.attr.size), + U64_MAX); } clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); From c9fab6c5b4c69c80445f33d2f9f8f4edd71d0224 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:21:57 +0200 Subject: [PATCH 17/41] fuse: wait out the writeback a NOTIFY invalidate starts The handler starts writeback so the bytes go while the grant still covers them, then drops the range. When the record says the range may be dirty the drop launders, and laundering waits out the folios just put under writeback: the handler waits for FUSE_WRITE replies it queued itself. Wait here instead, before the revoke, so nothing classifies as revoked and re-requests the range from inside the handler. A server revoking from a thread it also answers FUSE_WRITE on still deadlocks; the comment now says so. Signed-off-by: Horst Birthelmer --- fs/fuse/inode.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index d7e4fbf1993208..fc70c1b53cd290 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1047,7 +1047,7 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, pg_last); /* - * Start unwritten data on its way while the grant + * Put unwritten data on the server while the grant * still covers it, rather than leaving it to the drop * below. After the revoke those bytes classify as * FUSE_DLM_RUN_REVOKED, and writeback would take the @@ -1055,10 +1055,21 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, * inside the handler the server is waiting on. * do_writepages() runs in this context, so the * classification is made before the revoke. + * + * Waited out here rather than left to the drop, which + * launders when the record says the range may be dirty + * and so waits for these same replies. One explicit + * wait, before the revoke, and the drop then finds + * nothing under writeback to block on. Either way a + * server that revokes from a thread it also needs to + * answer FUSE_WRITE on deadlocks here, the same + * contract fuse_notify_invalidate_range() states for a + * frozen inode. The error is left to the mapping, + * where fsync collects it. */ if (has_pages && may_be_dirty) - filemap_fdatawrite_range(inode->i_mapping, - offset, end_byte); + filemap_write_and_wait_range(inode->i_mapping, + offset, end_byte); fuse_dlm_revoke_inval_range(fi, offset, len); From 53140d0b12c0f56503cfd1c8bfe2bde38f78181b Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:23:05 +0200 Subject: [PATCH 18/41] fuse: flush the page cache before re-routing to direct IO The forced-direct-IO latch is checked once, so a buffered write that passed the check just before a notify set the latch dirties the page cache after the notify dropped the mapping. The coherency gate used to fence that. What is left behind is invisible to the direct path: a direct read misses the dirty folio, and a direct write lands underneath it, after which the invalidate launders rather than drops and puts the stale folio on the server on top. Write the range back before re-routing. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 5a20bc5d88e639..bbd374078b1c9d 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1445,8 +1445,22 @@ static ssize_t fuse_cache_read_iter(struct kiocb *iocb, struct iov_iter *to) * fence: unlike a write, a read leaves nothing behind that could * reach the server under a grant it no longer holds. */ - if (fuse_inode_force_dio(inode)) + if (fuse_inode_force_dio(inode)) { + size_t count = iov_iter_count(to); + + /* + * A write that passed this same check just before the latch + * took hold dirtied the page cache after the notify dropped + * it, and a direct read does not look there. Send it first. + */ + if (count) { + res = filemap_write_and_wait_range(inode->i_mapping, + iocb->ki_pos, iocb->ki_pos + count - 1); + if (res) + return res; + } return fuse_direct_read_iter(iocb, to); + } res = generic_file_read_iter(iocb, to); @@ -2245,7 +2259,19 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) goto out; if (fuse_inode_force_dio(inode)) { + /* + * As on the read side, only worse: the direct write would + * land under whatever a write racing the latch left dirty, + * and the invalidate fuse_direct_write_iter() does after it + * launders rather than drops, putting that folio on the + * server on top. Send it first and the order is ordinary. + */ + if (count) + err = filemap_write_and_wait_range(inode->i_mapping, + iocb->ki_pos, iocb->ki_pos + count - 1); fuse_cache_wr_unlock(inode, exclusive); + if (err) + return err; return fuse_direct_write_iter(iocb, from); } From 248056598d62d7966ba7d77a67d7b4fab4758e87 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 25 Aug 2026 20:24:05 +0200 Subject: [PATCH 19/41] fuse: say byte offset where the record means byte offset The kernel-doc of the DLM record says page offset throughout, but every one of those arguments is a byte offset that happens to be page aligned, and the same file computes real page indices a few lines from some of them. The code is right; the wording invites passing one for the other. Say byte offset, and note on struct fuse_dlm_range that grants arrive page aligned while the content bound is split to the exact bytes an unaligned write covers, so the two are not the same thing. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 61 +++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index a24d01d9745af0..4a4cefa866c175 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -83,9 +83,12 @@ enum fuse_dlm_range_content { struct fuse_dlm_range { /* Interval tree node; only linked once granted */ struct rb_node rb; - /* Start page offset (inclusive) */ + /* + * The range, as byte offsets, both inclusive. Grants arrive page + * aligned; the content bound below is split to the exact bytes an + * unaligned write covers, so these need not be. + */ uint64_t start; - /* End page offset (inclusive) */ uint64_t end; /* Subtree end value for interval tree */ uint64_t __subtree_end; @@ -139,8 +142,8 @@ INTERVAL_TREE_DEFINE(struct fuse_dlm_range, rb, uint64_t, __subtree_end, /** * fuse_dlm_kill_pending - mark in-flight requests overlapping [start, end] * @cache: The page cache - * @start: Start page offset of the revoked region - * @end: End page offset of the revoked region + * @start: Start byte offset of the revoked region + * @end: End byte offset of the revoked region * * A revoke overlapping a request still on the wire has nothing to remove * from the tree, since that grant is not recorded yet. Marking it makes @@ -216,8 +219,8 @@ void fuse_dlm_cache_release_locks(struct fuse_inode *inode) /** * fuse_dlm_find_overlapping - Find a range that overlaps with [start, end] * @cache: The page cache - * @start: Start page offset - * @end: End page offset + * @start: Start byte offset + * @end: End byte offset * * Return: Pointer to the first overlapping range, or NULL if none found */ @@ -231,8 +234,8 @@ fuse_dlm_find_overlapping(struct fuse_dlm_cache *cache, uint64_t start, /** * fuse_page_try_merge - Try to merge ranges within a specific region * @cache: The page cache - * @start: Start page offset - * @end: End page offset + * @start: Start byte offset + * @end: End byte offset * * Attempt to merge ranges within and adjacent to the specified region * that have the same lock mode. @@ -295,8 +298,8 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, /** * fuse_dlm_lock_range_locked - Record a granted range of pages * @inode: The fuse inode - * @start: Start page offset - * @end: End page offset + * @start: Start byte offset + * @end: End byte offset * @mode: Lock mode (read or write) * * Add a locked range on the specified range of pages. @@ -442,8 +445,8 @@ int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, * fuse_dlm_request_begin - publish a lock request before it is sent * @inode: the fuse inode * @req: caller-owned storage for the request, live until commit or abort - * @start: start page offset being requested (inclusive) - * @end: end page offset being requested (inclusive) + * @start: start byte offset being requested (inclusive) + * @end: end byte offset being requested (inclusive) * * The mode is not recorded here: until the server answers the range is * held in neither, and the mode that reaches the tree is the one passed @@ -483,8 +486,8 @@ void fuse_dlm_request_begin(struct fuse_inode *inode, * fuse_dlm_request_commit - retire a request and record its grant * @inode: the fuse inode * @req: the request published by fuse_dlm_request_begin() - * @start: start page offset the server granted (inclusive) - * @end: end page offset the server granted (inclusive) + * @start: start byte offset the server granted (inclusive) + * @end: end byte offset the server granted (inclusive) * @mode: the mode that was requested * * Unlinking @req and recording the grant are one step under the cache @@ -576,8 +579,8 @@ static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) /** * fuse_dlm_range_touched - record that IO is about to reach the page cache * @inode: the fuse inode - * @start: start page offset the IO covers (inclusive) - * @end: end page offset the IO covers (inclusive) + * @start: start byte offset the IO covers (inclusive) + * @end: end byte offset the IO covers (inclusive) * @mode: FUSE_PAGE_LOCK_READ if the range is only being populated, * FUSE_PAGE_LOCK_WRITE if it is being dirtied * @@ -889,8 +892,8 @@ enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, /** * fuse_dlm_ranges_dropped - the page cache under [start, end] is gone * @inode: the fuse inode - * @start: start page offset (inclusive) - * @end: end page offset (inclusive) + * @start: start byte offset (inclusive) + * @end: end byte offset (inclusive) * * A revoked range exists only to describe page cache dirtied before the * grant was taken away. Once that cache is gone the range has nothing @@ -939,8 +942,8 @@ void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, /** * fuse_dlm_range_may_be_dirty - can [start, end] hold unwritten data * @inode: the fuse inode - * @start: start page offset (inclusive) - * @end: end page offset (inclusive) + * @start: start byte offset (inclusive) + * @end: end byte offset (inclusive) * * A part of the range with no recorded grant counts as dirty, which * covers pages dirtied through fuse_get_page_mkwrite_lock() (no grant is @@ -990,8 +993,8 @@ bool fuse_dlm_range_may_be_dirty(struct fuse_inode *inode, uint64_t start, /** * fuse_dlm_unlock_range - Revoke the grants over a range of pages * @inode: The fuse inode - * @start: Start page offset - * @end: End page offset + * @start: Start byte offset + * @end: End byte offset * * The server has taken [start, end] back. A range that has nothing * cached under it is removed; one that has is kept and marked @@ -1057,11 +1060,11 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, } /** - * fuse_dlm_range_is_locked - Check if a page range is already locked - * @cache: The page cache - * @start: Start page offset - * @end: End page offset - * @mode: Lock mode to check for (or NULL to check for any lock) + * fuse_dlm_range_is_locked - Check if a byte range is already locked + * @inode: The fuse inode + * @start: Start byte offset + * @end: End byte offset + * @mode: Lock mode to check for * * Check if the specified range of pages is already locked. * The entire range must be locked for this to return true. @@ -1347,8 +1350,8 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, * fuse_dlm_regrant_range - hold [start, end] again for writeback * @ff: a fuse file open for writing on @inode * @inode: the inode - * @start: start page offset (inclusive) - * @end: end page offset (inclusive) + * @start: start byte offset (inclusive) + * @end: end byte offset (inclusive) * * Writeback found bytes dirtied under a grant the server has since taken * away (FUSE_DLM_RUN_REVOKED). They cannot be dropped, so take the range From d474c45a69b3f855e267c1bafdecf06b1e757b80 Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 19:43:09 -0700 Subject: [PATCH 20/41] fuse: split DLM ranges at the bounds of a recorded grant fuse_dlm_lock_range_locked() put every range overlapping the grant on the upgrade list whole, without splitting at the grant bounds the way fuse_dlm_unlock_range() does. A range extending past the grant then had its uncovered part upgraded with it: holding a read grant on [0, 8191] and being granted write on [4096, 8191] recorded the whole node as held for write, so fuse_dlm_lock_is_held() reported [0, 4095] covered, a cached write there never sent FUSE_DLM_WB_LOCK, and the server went on thinking this client held only a read there -- cluster exclusion broken without any error to see. The revoked case is worse. Writeback re-grants only the run it is about to send, but the commit flipped the whole revoked range back to held, so revoked-dirty bytes outside the re-granted run reclassified as FUSE_DLM_RUN_DIRTY and were sent without taking the range again -- exactly the lost update FUSE_DLM_RANGE_REVOKED exists to prevent. Split at both grant bounds before walking, so upgrades apply only inside the grant. Both halves of a split keep state and content, and fuse_dlm_try_merge() at the end recoalesces whatever stayed equal. Signed-off-by: Allison Henderson --- fs/fuse/fuse_dlm_cache.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 4a4cefa866c175..0601fa25517565 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -139,6 +139,8 @@ INTERVAL_TREE_DEFINE(struct fuse_dlm_range, rb, uint64_t, __subtree_end, fuse_dlm_range_start, fuse_dlm_range_last, static, fuse_page_it); +static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off); + /** * fuse_dlm_kill_pending - mark in-flight requests overlapping [start, end] * @cache: The page cache @@ -335,6 +337,19 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* The state this grant records */ want = fuse_dlm_granted_state(mode); + /* + * Ranges are upgraded whole below, so split at the grant bounds + * first: a range extending past the grant would otherwise have its + * uncovered part upgraded with it, recording coverage the server + * never gave. A read range half-covered by a write grant would + * report the other half held for write, and a revoked range + * half-regranted would report its still-revoked bytes as held, so + * writeback would send them without taking the range again. + */ + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); + /* Find all ranges that overlap with [start, end] */ range = fuse_page_it_iter_first(&cache->ranges, start, end); while (range) { From 1bf503a690331dc12f37825cd1e8af9cbb1aedbd Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 19:43:34 -0700 Subject: [PATCH 21/41] fuse: drop the DLM ranges a truncate's invalidate empties fuse_do_setattr() frees the record above the new size, but the invalidate_inode_pages2() it runs empties the mapping below it too, and the record there was left alone. A dirty folio is laundered by that invalidate, which puts its bytes on the server, but laundering lowers the record only for a folio the page cache does not consider valid (fuse_dlm_range_sent()), so an ordinary uptodate dirty folio leaves a DIRTY range describing page cache that no longer exists. The next unaligned write to such a page allocates a fresh folio, leaves it unfilled, and records only its own bytes. The stale DIRTY run over the rest of the page then makes fuse_read_folio_merge() keep folio bytes nobody wrote, and writeback classifies and sends them -- bad data both to a local reader and to the server. fsx reaches this with any truncate-then-partial-write sequence. Drop the record below the new size as well, and only when the drop really emptied it: a busy folio that survives the invalidate still needs its record, so ask filemap_range_has_page() first, the same rule the NOTIFY invalidate path applies before its drop. Signed-off-by: Allison Henderson --- fs/fuse/dir.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index df226667ca674c..acb39c6a23656b 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2248,6 +2248,20 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (fc->dlm && fc->writeback_cache) fuse_dlm_ranges_dropped(fi, PAGE_ALIGN(outarg.attr.size), U64_MAX); + + /* + * invalidate_inode_pages2() emptied the mapping below the new + * size too (laundering anything dirty first, so those bytes + * are on the server). Ranges left DIRTY there would make the + * next partial write to the page keep and flush folio bytes + * nobody wrote. Only when the drop really emptied it: a busy + * folio that survived still needs its record, and a fault + * populating after the check keeps its page visible to it. + */ + if (fc->dlm && fc->writeback_cache && outarg.attr.size && + !filemap_range_has_page(mapping, 0, outarg.attr.size - 1)) + fuse_dlm_ranges_dropped(fi, 0, + PAGE_ALIGN(outarg.attr.size) - 1); } clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); From 01722bf39638dca4fc12ae8c0f103835597d3fb6 Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 19:43:58 -0700 Subject: [PATCH 22/41] fuse: forget the DLM record an O_TRUNC open discards The atomic O_TRUNC branch of fuse_do_setattr() releases every DLM range before dropping the cache, but the far more common path -- fuse_open() with fc->atomic_o_trunc -- called truncate_pagecache() and left the whole record standing. Ranges recorded DIRTY then described folios that no longer existed, and the next unaligned write to such a page left a fresh folio unfilled while the stale run made fuse_read_folio_merge() keep its unwritten bytes and writeback send them: bad data from something as plain as 'echo x > file' followed by a partial write. Release the record before the truncate, exactly as fuse_do_setattr() does; i_rwsem is held exclusive here (is_wb_truncate), so no cached write is between recording and dirtying. The FOPEN_KEEP_CACHE-less open one line below drops the mapping with invalidate_inode_pages2() and has the same problem. Drop the record there too, but only when the invalidate really emptied the mapping, under the same filemap_range_has_page() rule the NOTIFY path applies: a busy folio that survived keeps its record. Signed-off-by: Allison Henderson --- fs/fuse/file.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index bbd374078b1c9d..ccaca8740eb72f 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -384,10 +384,32 @@ static int fuse_open(struct inode *inode, struct file *file) if (is_wb_truncate || dax_truncate) fuse_release_nowrite(inode); if (!err) { - if (is_truncate) + if (is_truncate) { + /* + * Every grant goes with the cache, as on the + * fuse_do_setattr() O_TRUNC path: a record left + * behind would keep naming bytes the folios no + * longer hold. i_rwsem is held exclusive + * (is_wb_truncate), so no cached write is mid-record. + */ + if (fc->dlm && fc->writeback_cache) + fuse_dlm_cache_release_locks(fi); truncate_pagecache(inode, 0); - else if (!(ff->open_flags & FOPEN_KEEP_CACHE)) + } else if (!(ff->open_flags & FOPEN_KEEP_CACHE)) { invalidate_inode_pages2(inode->i_mapping); + /* + * Only when the drop really emptied the mapping; a + * folio that survived still needs its record. This + * open holds no lock against concurrent IO, but + * neither does the invalidate above -- anything + * populated or dirtied after it keeps its page, and + * the check sees that page. + */ + if (fc->dlm && fc->writeback_cache && + !filemap_range_has_page(inode->i_mapping, 0, + LLONG_MAX)) + fuse_dlm_ranges_dropped(fi, 0, U64_MAX); + } } if (dax_truncate) filemap_invalidate_unlock(inode->i_mapping); From 94d198f3b14d71066c3bbf985acbe00882785ac3 Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 19:44:25 -0700 Subject: [PATCH 23/41] fuse: drop the DLM ranges a punch hole empties FALLOC_FL_PUNCH_HOLE and FALLOC_FL_ZERO_RANGE flush the span, punch it on the server, and drop it from the page cache with truncate_pagecache_range() -- and told the record nothing. The flush lowers the record only for folios the page cache does not consider valid, so an uptodate dirty folio inside the hole left a DIRTY range describing page cache that no longer exists. The next unaligned write into the hole allocates a fresh folio, leaves it unfilled, and records its own bytes; the stale DIRTY run over the rest of the page makes fuse_read_folio_merge() keep folio bytes nobody wrote and writeback send them. fsx exercises punch hole against partial writes constantly, which is where the bad-data failures on this branch come from. Free the record over the whole pages inside the hole, under the same filemap_range_has_page() rule the NOTIFY path applies before its drop. The partial pages at the edges survive the truncate with their punched part zeroed -- those zeroes are real bytes the folio really holds -- so their record has to survive with them. Signed-off-by: Allison Henderson --- fs/fuse/file.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index ccaca8740eb72f..aac87c27be50f2 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -4201,9 +4201,29 @@ static long fuse_file_fallocate(struct file *file, int mode, loff_t offset, file_update_time(file); } - if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_ZERO_RANGE)) + if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_ZERO_RANGE)) { truncate_pagecache_range(inode, offset, offset + length - 1); + /* + * The whole pages inside the hole are gone; a record left + * over them would make the next partial write to one keep + * and flush folio bytes nobody wrote. The pages straddling + * the ends survive with their punched part zeroed, so their + * record still names real (now zero) bytes and stays. Only + * when the drop really emptied the span: a busy folio that + * survived keeps its record. + */ + if (fm->fc->dlm && fm->fc->writeback_cache) { + uint64_t first = PAGE_ALIGN(offset); + uint64_t last = (uint64_t)(offset + length) & PAGE_MASK; + + if (first < last && + !filemap_range_has_page(inode->i_mapping, first, + last - 1)) + fuse_dlm_ranges_dropped(fi, first, last - 1); + } + } + fuse_invalidate_attr_mask(inode, FUSE_STATX_MODSIZE); out: From 66894f6248cd6aff09a981e3397b4acd2548597f Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 19:44:49 -0700 Subject: [PATCH 24/41] fuse: drop the DLM ranges copy_file_range invalidates __fuse_copy_file_range() flushes the destination span, lets the server copy, and then drops the copied pages with truncate_inode_pages_range() as stale -- without telling the record. As on the truncate and punch hole paths, an uptodate dirty folio flushed and then dropped leaves a DIRTY range describing page cache that no longer exists, and the next partial write there keeps and flushes folio bytes nobody wrote. Free the record over the dropped span, gated on filemap_range_has_page() like the other drops: the copy runs under i_rwsem but faults do not, and a folio a fault put back after the truncate keeps its record. Signed-off-by: Allison Henderson --- fs/fuse/file.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index aac87c27be50f2..2dbf7dcfad82c6 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -4330,9 +4330,24 @@ static ssize_t __fuse_copy_file_range(struct file *file_in, loff_t pos_in, if (err) goto out; - truncate_inode_pages_range(inode_out->i_mapping, - ALIGN_DOWN(pos_out, PAGE_SIZE), - ALIGN(pos_out + outarg.size, PAGE_SIZE) - 1); + { + loff_t lstart = ALIGN_DOWN(pos_out, PAGE_SIZE); + loff_t lend = ALIGN(pos_out + outarg.size, PAGE_SIZE) - 1; + + truncate_inode_pages_range(inode_out->i_mapping, lstart, lend); + + /* + * The record over the dropped span has nothing left to + * describe, and left DIRTY it would make the next partial + * write there keep and flush folio bytes nobody wrote. + * Only when the drop really emptied it: a folio a + * concurrent fault put back keeps its record. + */ + if (fc->dlm && fc->writeback_cache && + !filemap_range_has_page(inode_out->i_mapping, lstart, + lend)) + fuse_dlm_ranges_dropped(fi_out, lstart, lend); + } file_update_time(file_out); fuse_write_update_attr(inode_out, pos_out + outarg.size, outarg.size); From acec51758f05ae4339c7bc9201832fe43c2a575e Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 20:25:38 -0700 Subject: [PATCH 25/41] fuse: keep the local size while the tail is dirty fuse_attr_cache_mask() trusted the local i_size over the server's only while a recorded write grant covered [attr->size, i_size). Two paths leave locally-extended data with no such record: a fault dirties pages under a page-mkwrite lock that is never recorded, and a local truncate revokes its own tail grants, after which cached writes extend the file again. On the next attribute refresh the check failed, the server's smaller size was applied, and truncate_pagecache() discarded dirty pages past it -- cached data destroyed by a GETATTR. The invalidate that follows then dropped the rest of the mapping with the record left standing, arming the stale-DIRTY bad-data path on every page of the file. generic/075 hits this within a few hundred fsx operations; the failure point moves with the attribute timeout, which is what made the runs look nondeterministic. Keep STATX_SIZE cached also while anything in [attr->size, i_size) is dirty or under writeback: those bytes exist only here, and the server cannot have a newer opinion about a size it has never seen. A remote truncate still lands, exactly as the design intends: its revoke launders and drops the tail first, so nothing is dirty there by the time the smaller size arrives. The no-grant early return learns the same rule, since a mapping dirtied only through page-mkwrite has no recorded grant at all. Signed-off-by: Allison Henderson --- fs/fuse/inode.c | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index fc70c1b53cd290..8a7b6e9a48eb3c 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -550,16 +550,36 @@ static u32 fuse_attr_cache_mask(struct inode *inode, struct fuse_attr *attr, !S_ISREG(inode->i_mode)) return cache_mask; - if (!fuse_dlm_write_grant_exists(fi)) + /* + * A dirty mapping keeps the local attributes authoritative even + * when no grant is recorded: a fault dirties pages under a + * page-mkwrite lock that is never recorded, and a truncate revokes + * the tail grants itself while cached writes above the new size + * are still waiting for writeback. + */ + if (!fuse_dlm_write_grant_exists(fi) && + !mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY) && + !mapping_tagged(inode->i_mapping, PAGECACHE_TAG_WRITEBACK)) return cache_mask; if (mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY) || mapping_tagged(inode->i_mapping, PAGECACHE_TAG_WRITEBACK)) cache_mask |= STATX_MTIME | STATX_CTIME; + /* + * The local size stays authoritative while the extension is + * covered by a write grant, and also while anything in + * [attr->size, size) is dirty or under writeback: those bytes + * exist only here, and taking the server's smaller size would + * truncate them away before they are ever sent. The grant check + * alone misses them, because a page-mkwrite grant is never + * recorded and a local truncate revokes its own tail grants. + */ if (have_size && size > (loff_t) attr->size && - fuse_dlm_lock_is_held(fi, attr->size, size - attr->size, - FUSE_PAGE_LOCK_WRITE)) + (fuse_dlm_lock_is_held(fi, attr->size, size - attr->size, + FUSE_PAGE_LOCK_WRITE) || + filemap_range_needs_writeback(inode->i_mapping, attr->size, + size - 1))) cache_mask |= STATX_SIZE; return cache_mask; From bb6f5f9d88dcfb7bc2f689c6da46631828946a09 Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Tue, 25 Aug 2026 20:25:57 -0700 Subject: [PATCH 26/41] fuse: drop the DLM ranges an attribute change invalidates When an attribute reply is applied with the size not served from the cache, fuse_change_attributes_i() truncates the page cache to the server's size and, when the data may be stale, drops the whole mapping with invalidate_inode_pages2() -- and told the record nothing. This is the same hole just closed on the truncate, O_TRUNC open, punch hole and copy_file_range paths, reached from every GETATTR, and it is the drop behind the generic/075 corruption: the server-side trace shows a folio whose bytes were flushed correctly once, dropped here with its record left DIRTY, repopulated by a later partial write that leaves the folio unfilled, and then written back with the stale record naming the unfilled head -- zeroes sent over data the server already had. Free the record over what really went: unconditionally above the new size, where truncate_pagecache() leaves nothing behind, and over the rest only when the invalidate emptied the mapping, under the same filemap_range_has_page() rule as the other drops, so a folio that survived or was faulted back keeps its record. Signed-off-by: Allison Henderson --- fs/fuse/inode.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 8a7b6e9a48eb3c..6fd717d6ba54b5 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -666,6 +666,26 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr if (inval) invalidate_inode_pages2(inode->i_mapping); + + /* + * The DLM record has to follow the cache out, as on every + * other path that drops it. A range left DIRTY over a + * dropped folio makes the next partial write to that page + * keep and flush folio bytes nobody wrote. The pages above + * the new size are gone unconditionally; the rest only when + * the invalidate really emptied the mapping, so a folio that + * survived (or was faulted back) keeps its record. + */ + if (fc->dlm && fc->writeback_cache) { + if (have_size && oldsize != attr->size) + fuse_dlm_ranges_dropped(fi, + PAGE_ALIGN(attr->size), + U64_MAX); + if (inval && + !filemap_range_has_page(inode->i_mapping, 0, + LLONG_MAX)) + fuse_dlm_ranges_dropped(fi, 0, U64_MAX); + } } if (IS_ENABLED(CONFIG_FUSE_DAX)) From 6752d39362ef316c8e16f5c1c944e6cc1828f858 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 26 Aug 2026 14:42:20 +0200 Subject: [PATCH 27/41] fuse: report an error from a DLM buffered write that wrote nothing The interior stage of fuse_dlm_buffered_write() returns @total on error. For a page aligned start the head chunk is empty, so @total is zero and an interior failure returns zero: no error and no bytes. fuse_cache_write_iter() then falls back to @err, which still holds the positive count from generic_write_checks(), and a write that failed outright reports every byte as written. Return the error while nothing has landed, a short write once something has, in every chunk. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 2dbf7dcfad82c6..c405e1cf75857f 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -2048,10 +2048,17 @@ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, if (mid_end <= mid_start) return fuse_perform_write(iocb, from, true); + /* + * Every chunk reports a failure the same way: the error while + * nothing has landed, a short write once something has. Returning + * what has landed when that is nothing reports no error and no + * bytes, which fuse_cache_write_iter() turns into the full count. + */ + /* Unaligned head [pos, mid_start): through. */ res = fuse_dlm_write_chunk(iocb, from, file, mid_start - pos, true); if (res < 0) - return res; + return total ? total : res; total += res; if (res < mid_start - pos) return total; @@ -2059,15 +2066,16 @@ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, /* Aligned interior [mid_start, mid_end): cached whole pages. */ res = fuse_dlm_write_chunk(iocb, from, file, mid_end - mid_start, false); if (res < 0) - return total; + 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, file, end - mid_end, true); - if (res > 0) - total += res; + if (res < 0) + return total ? total : res; + total += res; return total; } From 48908a66a768e76e2efab902fb0b4754490b1861 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 26 Aug 2026 15:03:24 +0200 Subject: [PATCH 28/41] fuse: drop the deferred fill of a partly written folio fuse_iomap_read_folio_range() skipped the fill under DLM and left the folio invalid, so a write covering part of a block need not read the remainder back from a server that may hold nothing there. A DLM buffered write is cut at the block size now, so its interior covers whole blocks and the unaligned edges never reach iomap. Nothing reaches the skip. It goes, with the invalid folio, its zero fill, fuse_iomap_put_folio() and the cursor between fuse_iomap_begin() and fuse_iomap_end(). Writeback's copy of the assumption goes too, and that one is a bug on its own: iomap leaves a large folio partly valid whenever a write covers some of its blocks and not others, so with no DLM such a folio has its dirty blocks reported as a hole and dropped. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 83 ++---------------------------------------------- fs/fuse/fuse_i.h | 6 ---- 2 files changed, 2 insertions(+), 87 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index c405e1cf75857f..f4a8a846c729d9 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1190,33 +1190,6 @@ static int fuse_iomap_read_folio_range(const struct iomap_iter *iter, size_t off = offset_in_folio(folio, pos); int ret; - /* - * Under DLM this fills nothing. These are the bytes the write does - * not cover, so they belong to the server, and writeback already - * declines to send them back; fetching them buys only the right to - * call the folio valid, at a round trip an expanding write spends on - * a range that holds nothing, over a handle that may not even be - * able to read. Leave them alone. fuse_iomap_put_folio() takes the - * uptodate flag back off the folio, so the first reader fetches them - * and nothing invents a value in the meantime. - * - * Only for a folio the page cache tracks in one piece. Clearing the - * uptodate flag of a folio of several blocks leaves iomap's per block - * bits set behind it, and a read asks those through - * ->is_partially_uptodate rather than calling ->read_folio, so it - * would be served exactly the bytes this skipped. - */ - if (fc->dlm && fc->writeback_cache && - i_blocksize(inode) == folio_size(folio)) { - struct fuse_dlm_retry *wr; - - wr = xa_load(&fc->dlm_retry_tasks, (unsigned long) current); - if (wr) { - wr->deferred = folio; - return 0; - } - } - ret = fuse_read_folio_range(file, folio, off, len); /* @@ -1853,48 +1826,8 @@ static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive, bool uncached) } } -/* - * Called for every folio the write touched, with the folio still locked - * and @copied set to what actually landed in it. Owns the unlock. - */ -static void fuse_iomap_put_folio(struct inode *inode, loff_t pos, - unsigned int copied, struct folio *folio) -{ - struct fuse_conn *fc = get_fuse_conn(inode); - struct fuse_inode *fi = get_fuse_inode(inode); - - if (fc->dlm && fc->writeback_cache) { - struct fuse_dlm_retry *wr; - - /* - * Record the bytes now that they are in the folio. Marking - * the intended range before the copy claimed what a short or - * failed one never reached, and on a folio left invalid - * below writeback would send exactly those bytes. - */ - if (copied) - fuse_dlm_range_written(fi, pos, pos + copied - 1); - - /* - * The fill was skipped, so outside what was just recorded - * this folio holds bytes nobody wrote. iomap called it - * valid on the way in; take that back while it is still - * locked, so no reader ever sees them. - */ - wr = xa_load(&fc->dlm_retry_tasks, (unsigned long) current); - if (wr && wr->deferred == folio) { - wr->deferred = NULL; - folio_clear_uptodate(folio); - } - } - - folio_unlock(folio); - folio_put(folio); -} - static const struct iomap_write_ops fuse_iomap_write_ops = { .read_folio_range = fuse_iomap_read_folio_range, - .put_folio = fuse_iomap_put_folio, }; static int fuse_iomap_begin(struct inode *inode, loff_t offset, loff_t length, @@ -1950,7 +1883,6 @@ static ssize_t fuse_writeback_write_iter(struct kiocb *iocb, * would re-enter with len==0 and livelock on a 0-length mapping. */ retry_state.retry_needed = false; - retry_state.deferred = NULL; /* * Use iomap so that we can do granular uptodate reads @@ -2374,10 +2306,8 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *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 pages are - * cached for writeback, and their bytes are recorded in - * fuse_iomap_put_folio(), which is told what each folio - * actually took. + * (see fuse_dlm_buffered_write()); only whole blocks are + * cached for writeback. */ if (fc->dlm) written = fuse_dlm_buffered_write(iocb, from, file); @@ -3293,15 +3223,6 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, fuse_dlm_range_sent(fi, pos, pos + len - 1); break; } - } else if (!folio_test_uptodate(folio)) { - /* - * A folio a deferred fill left behind, reached with the - * record no longer consulted because the server turned out - * to have no DLM after all. Nothing here is known to be - * real, so send none of it. - */ - wpc->iomap.type = IOMAP_HOLE; - return len; } offset = offset_in_folio(folio, pos); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 97d042ea155ba5..e3de135c291220 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -695,12 +695,6 @@ struct fuse_sync_bucket { */ struct fuse_dlm_retry { bool retry_needed; - /* - * Folio whose fill fuse_iomap_read_folio_range() left undone, so - * fuse_iomap_put_folio() knows to take the uptodate flag back off - * before anyone else can see it. - */ - struct folio *deferred; }; /** From d75a174d339b8eabb3560d11739922cd63e86a29 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 26 Aug 2026 15:05:24 +0200 Subject: [PATCH 29/41] fuse: hold the grant for writeback instead of classifying the run Writeback asked the record what each dirty run was, because a boundary folio could hold this client's bytes next to the server's. Nothing partly written is dirtied any more, so every run iomap reports dirty was written whole by this client. Call fuse_dlm_regrant_range() for the whole run instead and let it early-exit on a grant still held; fuse_dlm_dirty_run() and fuse_dlm_range_sent() go with the classification. fuse_read_folio_merge() asked the same question because reading a dirty folio whole would lose what a write left in it. iomap answers it through ->is_partially_uptodate, which fuse already publishes. That also fixes a mount with no DLM, where the merge was skipped for want of a record and the folio read whole. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 134 ++++++++++++---------------------- fs/fuse/fuse_dlm_cache.c | 150 ++------------------------------------- fs/fuse/fuse_dlm_cache.h | 26 ------- fs/fuse/inode.c | 11 ++- 4 files changed, 57 insertions(+), 264 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index f4a8a846c729d9..093a41f36dc1a0 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1094,44 +1094,49 @@ static int fuse_read_folio_range(struct file *file, struct folio *folio, } /** - * fuse_read_folio_merge - fill @folio without disturbing what is written + * fuse_read_folio_merge - fill @folio without disturbing what it holds * @file: file to read through * @folio: the folio to fill * - * A folio a partial write left behind holds the bytes that write copied - * and nothing else. The record says where they are; only the gaps - * between them are the server's to fill, and reading over them would - * lose data the server has not seen yet. + * iomap tracks a folio a block at a time, and a write that covered some + * of its blocks and not others leaves it valid in the ones it covered + * and not in the rest. The valid ones hold what that write put there, + * which writeback may not have sent yet; reading over them would lose + * it. Fetch the rest, in as few requests as the gaps allow. + * + * A folio the page cache tracks in one piece has no per block state to + * ask, and none to have: it is dirty only if a write covered it whole, + * and then it is valid and never reaches here. * * Return: 0, AOP_TRUNCATED_PAGE, or a negative error. */ static int fuse_read_folio_merge(struct file *file, struct folio *folio) { - struct inode *inode = folio->mapping->host; - struct fuse_inode *fi = get_fuse_inode(inode); - uint64_t pos = folio_pos(folio); + size_t bsize = i_blocksize(folio->mapping->host); size_t size = folio_size(folio); - size_t done = 0; + size_t off = 0; - while (done < size) { - size_t run = size - done; + while (off < size) { + size_t run = 0; int err; - switch (fuse_dlm_dirty_run(fi, pos + done, size - done, &run)) { - case FUSE_DLM_RUN_DIRTY: - case FUSE_DLM_RUN_REVOKED: - /* Written here and not on the server yet: keep it */ - break; - default: - err = fuse_read_folio_range(file, folio, done, run); - if (err) - return err; + /* Skip what the folio already holds */ + while (off < size && + iomap_is_partially_uptodate(folio, off, bsize)) + off += bsize; + + /* Take the gap behind it in one request */ + while (off + run < size && + !iomap_is_partially_uptodate(folio, off + run, bsize)) + run += bsize; + + if (!run) break; - } - if (WARN_ON_ONCE(!run)) - return -EIO; - done += run; + err = fuse_read_folio_range(file, folio, off, run); + if (err) + return err; + off += run; } return 0; @@ -1160,14 +1165,10 @@ static int fuse_read_folio(struct file *file, struct folio *folio) /* * Only a folio still holding what a write put in it has anything to - * keep. The record describes the inode, not one incarnation of a - * folio: a clean folio was either never written through here or has - * been reclaimed and allocated again since, and either way it holds - * none of what the record names. One that was merely being written - * back is clean by now, and the record stopped naming those bytes - * when they went, so it reads whole. + * keep. One that was merely being written back is clean by now, + * waited out just above, and reads whole. */ - if (fc->dlm && fc->writeback_cache && folio_test_dirty(folio)) + if (fc->writeback_cache && folio_test_dirty(folio)) err = fuse_read_folio_merge(file, folio); else err = fuse_do_readfolio(file, folio, 0, folio_size(folio)); @@ -3152,16 +3153,19 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, } /* - * Under DLM the page cache can hold a partly written folio: a write - * that is not page aligned dirties only its own bytes, and the rest - * of the boundary page was never read in. Send only what was - * written, so the untouched remainder is not handed to the server. + * Every run iomap reports dirty was written whole by this client: + * the unaligned edges of a cached write go to the server directly + * and the interior covers whole blocks, so nothing partly written + * is ever dirtied. There is nothing to classify, only the grant to + * make sure of: a revoke may have arrived since the write, and + * these bytes must not go out from under one. * - * fuse_dlm_dirty_run() classifies the run from @pos and says how far - * the answer holds. iomap calls back for the rest. + * fuse_dlm_regrant_range() takes the range back when it has gone, + * and walks the record once under the lock held for read when it + * has not. A failure leaves the folio dirty, so the next writeback + * tries again; only a hard error stops it. */ if (fc->dlm && fc->writeback_cache) { - size_t run = len; int err; /* @@ -3173,56 +3177,10 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, */ wpc->iomap.type = IOMAP_MAPPED; - switch (fuse_dlm_dirty_run(fi, pos, len, &run)) { - case FUSE_DLM_RUN_UNKNOWN: - /* - * No record for this run. Over a folio the server - * filled, the bytes are its own and go back whole, as - * without DLM; only this run, since the record may - * well describe what follows it. Over one a partial - * write left waiting for its fill, nobody ever wrote - * them, and sending them would invent content. - */ - if (!folio_test_uptodate(folio)) { - wpc->iomap.type = IOMAP_HOLE; - return run; - } - len = run; - break; - case FUSE_DLM_RUN_CLEAN: - /* - * Nothing was written here. Reported as a hole so - * iomap skips it and, for a folio with no written - * run at all, ends the folio writeback itself. - */ - wpc->iomap.type = IOMAP_HOLE; - return run; - case FUSE_DLM_RUN_REVOKED: - /* - * Written under a grant the server has since taken - * away. The bytes are real, so hold the range again - * rather than lose them. A failure leaves the run - * classified revoked and the folio dirty, so the next - * writeback tries again; only a hard error stops it. - */ - err = fuse_dlm_regrant_range(data->ff, inode, pos, - pos + run - 1); - if (err < 0 && err != -ENOSYS) - return err; - fallthrough; - case FUSE_DLM_RUN_DIRTY: - len = run; - /* - * On a folio the page cache does not consider valid, - * the record is the only thing saying which of its - * bytes are real. Once they are on their way the - * folio need not survive, and a later one at the same - * index must not be told it holds them. - */ - if (!folio_test_uptodate(folio)) - fuse_dlm_range_sent(fi, pos, pos + len - 1); - break; - } + err = fuse_dlm_regrant_range(data->ff, inode, pos, + pos + len - 1); + if (err < 0 && err != -ENOSYS) + return err; } offset = offset_in_folio(folio, pos); diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 0601fa25517565..fa2156997cef3a 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -641,48 +641,6 @@ void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } -/** - * fuse_dlm_range_sent - [start, end] is on its way to the server - * @inode: the fuse inode - * @start: first byte handed over (inclusive) - * @end: last byte handed over (inclusive) - * - * Lowers the recorded content of [start, end] back to clean. Writeback - * calls this for a folio the page cache does not consider valid, where - * the record is the only thing saying which of its bytes are real: once - * they have been handed over the folio need not survive, and a later one - * at the same index must not be told that bytes it does not hold were - * written. - * - * The caller holds the folio lock, so a write dirtying the range again - * either came before this and is in what is being sent, or comes after - * and records itself. - */ -void fuse_dlm_range_sent(struct fuse_inode *inode, uint64_t start, - uint64_t end) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - struct fuse_dlm_range *range; - - if (start > end) - return; - - down_write(&cache->lock); - - fuse_dlm_split_at(cache, start); - if (end < U64_MAX) - fuse_dlm_split_at(cache, end + 1); - - for (range = fuse_dlm_find_overlapping(cache, start, end); range; - range = fuse_page_it_iter_next(range, start, end)) - if (range->content == FUSE_DLM_CONTENT_DIRTY) - range->content = FUSE_DLM_CONTENT_CLEAN; - - fuse_dlm_try_merge(cache, start, end); - - up_write(&cache->lock); -} - /** * fuse_dlm_range_written - record that [start, end] holds written bytes * @inode: the fuse inode @@ -809,101 +767,6 @@ void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } -/* How a range in @state / @content classifies for writeback */ -static enum fuse_dlm_run fuse_dlm_classify(struct fuse_dlm_range *range) -{ - if (range->content != FUSE_DLM_CONTENT_DIRTY) - return FUSE_DLM_RUN_CLEAN; - - return range->state == FUSE_DLM_RANGE_REVOKED ? FUSE_DLM_RUN_REVOKED : - FUSE_DLM_RUN_DIRTY; -} - -/** - * fuse_dlm_kind_at - how the byte at @cur classifies, and how far that holds - * @cache: the page cache - * @cur: byte offset to classify - * @end: last byte of interest - * @last: set to the last byte the answer covers - * - * A gap in the record, or a range held only for read, is - * %FUSE_DLM_RUN_UNKNOWN: this client cannot have written it. Either way - * @last says how far to look next, so no caller has to rediscover it. - * - * Caller holds @cache->lock. - */ -static enum fuse_dlm_run fuse_dlm_kind_at(struct fuse_dlm_cache *cache, - uint64_t cur, uint64_t end, - uint64_t *last) -{ - struct fuse_dlm_range *range; - - range = fuse_page_it_iter_first(&cache->ranges, cur, end); - if (!range || range->start > cur) { - /* Nothing recorded up to the next range, or to @end */ - *last = range ? range->start - 1 : end; - return FUSE_DLM_RUN_UNKNOWN; - } - - *last = min(range->end, end); - if (range->state == FUSE_DLM_RANGE_READ) - return FUSE_DLM_RUN_UNKNOWN; - - return fuse_dlm_classify(range); -} - -/** - * fuse_dlm_dirty_run - classify the run starting at @pos - * @inode: the fuse inode - * @pos: byte offset to start at - * @len: bytes of interest from @pos - * @run: set to how far the classification holds, capped at @len - * - * Walks forward from @pos while the classification stays the same, so a - * caller can ask what to do with one run of a folio at a time. - * - * Return: - * %FUSE_DLM_RUN_UNKNOWN - no record for the run: either a gap, or a range - *\theld only for read, which this client cannot have written. - * %FUSE_DLM_RUN_CLEAN - nothing was written here. - * %FUSE_DLM_RUN_DIRTY - written under a grant this client still holds. - * %FUSE_DLM_RUN_REVOKED - written, but the grant has since been taken - *\taway. The bytes are real and must not be lost, so a caller sending - *\tthem has to hold the range again first. - * - * @run is set for every return except a zero @len, and is never 0. - */ -enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, - size_t len, size_t *run) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - enum fuse_dlm_run kind; - uint64_t end, reach; - - if (!len) - return FUSE_DLM_RUN_UNKNOWN; - end = pos + len - 1; - - down_read(&cache->lock); - - kind = fuse_dlm_kind_at(cache, pos, end, &reach); - while (reach < end) { - enum fuse_dlm_run next; - uint64_t last; - - /* Safe: reach < end, so this cannot wrap */ - next = fuse_dlm_kind_at(cache, reach + 1, end, &last); - if (next != kind) - break; - reach = last; - } - - up_read(&cache->lock); - - *run = reach - pos + 1; - return kind; -} - /** * fuse_dlm_ranges_dropped - the page cache under [start, end] is gone * @inode: the fuse inode @@ -1368,14 +1231,13 @@ int fuse_get_dlm_lock(struct file *file, loff_t offset, * @start: start byte offset (inclusive) * @end: end byte offset (inclusive) * - * Writeback found bytes dirtied under a grant the server has since taken - * away (FUSE_DLM_RUN_REVOKED). They cannot be dropped, so take the range - * again before sending them. Whatever the other holder wrote in between - * is overwritten, which for two writers that never synchronised is a - * legitimate order. + * Writeback holds the range again before sending a folio, since a revoke + * may have arrived between the write and the send. Whatever the other + * holder wrote in between is overwritten, which for two writers that + * never synchronised is a legitimate order. * - * Recording the grant flips the range back to held, so the run classifies - * as FUSE_DLM_RUN_DIRTY from here on. + * A range still held is the ordinary case: the grant is found recorded + * and nothing is sent to the server. */ int fuse_dlm_regrant_range(struct fuse_file *ff, struct inode *inode, uint64_t start, uint64_t end) diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index e601dd9e68c6b0..03ce8114f6ea7a 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -106,12 +106,6 @@ bool fuse_dlm_write_grant_exists(struct fuse_inode *inode); void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, uint64_t end, enum fuse_page_lock_mode mode); -/* - * [start, end] is on its way to the server: the record stops naming it. - */ -void fuse_dlm_range_sent(struct fuse_inode *inode, uint64_t start, - uint64_t end); - /* * Record that [start, end] holds bytes this client wrote. Unlike * fuse_dlm_range_touched(), a part of it no range covers is given one. @@ -126,26 +120,6 @@ void fuse_dlm_range_written(struct fuse_inode *inode, uint64_t start, void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, uint64_t end); -/* What writeback should do with a run; see fuse_dlm_dirty_run() */ -enum fuse_dlm_run { - /* No record: write the whole range, as without DLM */ - FUSE_DLM_RUN_UNKNOWN, - /* Nothing was written here */ - FUSE_DLM_RUN_CLEAN, - /* Written under a grant still held */ - FUSE_DLM_RUN_DIRTY, - /* Written, but the grant has since been taken away */ - FUSE_DLM_RUN_REVOKED, -}; - -/* - * Classify the run from @pos, setting @run to how far the answer holds, - * capped at @len. @run is set for every query of a non-zero @len, no - * record included, and is never 0. - */ -enum fuse_dlm_run fuse_dlm_dirty_run(struct fuse_inode *inode, uint64_t pos, - size_t len, size_t *run); - /* * The page cache under [start, end] is gone: free the revoked ranges over * it and reset the content of the ones still held. The caller must have diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 6fd717d6ba54b5..55a025f5d705f8 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1089,12 +1089,11 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, /* * Put unwritten data on the server while the grant * still covers it, rather than leaving it to the drop - * below. After the revoke those bytes classify as - * FUSE_DLM_RUN_REVOKED, and writeback would take the - * range again to send them: a DLM round trip from - * inside the handler the server is waiting on. - * do_writepages() runs in this context, so the - * classification is made before the revoke. + * below. After the revoke writeback would have to + * take the range again to send those bytes: a DLM + * round trip from inside the handler the server is + * waiting on. do_writepages() runs in this context, + * so the grant is asked for before the revoke. * * Waited out here rather than left to the drop, which * launders when the record says the range may be dirty From 9bb6233178171b339d0d28f89efd514606c76692 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 26 Aug 2026 15:07:37 +0200 Subject: [PATCH 30/41] fuse: drop the content bound from the DLM record struct fuse_dlm_range carried an upper bound on what the page cache under it held. Writeback no longer consults it, and the three remaining users have exact page cache equivalents: fuse_dlm_range_may_be_dirty() is filemap_range_needs_writeback(), the keep-or-free decision for a revoked range is filemap_range_has_page(), and the revoked-and-dirty arm of fuse_dlm_write_grant_exists() is covered by its caller's PAGECACHE_TAG tests. Remove enum fuse_dlm_range_content and fuse_dlm_range::content, and with them fuse_dlm_range_touched(), fuse_dlm_range_written(), fuse_dlm_ranges_flushed(), fuse_dlm_raise_dirty_shared(), fuse_dlm_mark_populated(), fuse_dlm_touched_level() and fuse_dlm_range_may_be_dirty(). fuse_iomap_end() has nothing left to do. The record is written only on a grant and on a revoke now, so a cached write does not take the per inode cache lock at all. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 27 +--- fs/fuse/fuse_dlm_cache.c | 314 ++++----------------------------------- fs/fuse/fuse_dlm_cache.h | 30 +--- fs/fuse/inode.c | 10 +- 4 files changed, 40 insertions(+), 341 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 093a41f36dc1a0..c53f749d695867 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -709,14 +709,6 @@ static int fuse_fsync(struct file *file, loff_t start, loff_t end, if (err) goto out; - /* - * [start, end] is on the server now, so the DLM ranges inside it - * go back to clean. i_rwsem is held exclusive here, which keeps a - * cached write from dirtying them again first. - */ - if (fc->dlm && fc->writeback_cache) - fuse_dlm_ranges_flushed(get_fuse_inode(inode), start, end); - err = sync_inode_metadata(inode, 1); if (err) goto out; @@ -2241,12 +2233,10 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) /* * A NOTIFY invalidate can revoke the grant requested above between * here and the dirtying below, and nothing stops it: the bytes are - * caught on the way out instead. fuse_dlm_range_written() records - * them as they land, covering the range itself if the revoke got - * there first, fuse_dlm_unlock_range() marks a range revoked rather - * than forgetting it, and writeback holds the range again before - * sending anything it finds marked that way. So a write racing a - * revoke costs a round trip, not coverage. + * caught on the way out instead. fuse_dlm_unlock_range() keeps a + * revoked range for as long as there is page cache under it, and + * writeback holds the range again before sending anything. So a + * write racing a revoke costs a round trip, not coverage. */ task_io_account_write(count); @@ -3356,15 +3346,6 @@ static int fuse_get_page_mkwrite_lock(struct file *file, loff_t offset, size_t l err = -EINVAL; } - /* - * The fault is about to dirty this page. This grant is not - * recorded, so raise the bound on whatever range covers the page; - * a page outside every range already reports dirty. - */ - if (!err && fc->dlm) - fuse_dlm_range_touched(get_fuse_inode(inode), inarg.start, - inarg.end, FUSE_PAGE_LOCK_WRITE); - return err; } /* diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index fa2156997cef3a..1a7851054090fd 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -29,15 +29,15 @@ * In-flight requests are kept off the tree so the state is consulted * only where a request is retired, not by every tree walker. * - * A granted range also records what the page cache under it may hold - * (enum fuse_dlm_range_content). That is an upper bound: lowering it - * while data is still dirty would let a revoke drop unwritten data, so - * only fuse_dlm_ranges_flushed() lowers it. + * The record says nothing about the page cache under a range. What is + * cached there, and whether the server has seen it, is what the page + * cache itself answers. */ #include "fuse_i.h" #include "fuse_dlm_cache.h" #include +#include #include #include #include @@ -66,27 +66,14 @@ enum fuse_dlm_range_state { FUSE_DLM_RANGE_WRITE, }; -/* - * What the page cache under a granted range may hold. Ordered so that - * raising the bound is a max(); only fuse_dlm_ranges_flushed() lowers it. - */ -enum fuse_dlm_range_content { - /* Nothing cached under this grant */ - FUSE_DLM_CONTENT_EMPTY, - /* May hold data the server has already seen */ - FUSE_DLM_CONTENT_CLEAN, - /* May hold data the server has not seen */ - FUSE_DLM_CONTENT_DIRTY, -}; - /* A range of pages with a lock */ struct fuse_dlm_range { /* Interval tree node; only linked once granted */ struct rb_node rb; /* * The range, as byte offsets, both inclusive. Grants arrive page - * aligned; the content bound below is split to the exact bytes an - * unaligned write covers, so these need not be. + * aligned, and a range is split only at the bounds of another, so + * these are page aligned too. */ uint64_t start; uint64_t end; @@ -94,8 +81,6 @@ struct fuse_dlm_range { uint64_t __subtree_end; /* Lifecycle and, once granted, the mode; see the enum above */ enum fuse_dlm_range_state state; - /* Upper bound on the page cache under this range; see the enum */ - enum fuse_dlm_range_content content; /* Temporary list entry for operations, and the cache->pending link */ struct list_head list; }; @@ -272,14 +257,8 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, struct fuse_dlm_range, rb); } - /* - * Merge only neighbours agreeing on both state and content: - * a coalesced range carries one content bound for all of - * itself, so merging across a content boundary would lose - * where that bound actually applies. - */ + /* Merge neighbours the server has given us on the same terms */ if (next && range->state == next->state && - range->content == next->content && range->end + 1 == next->start) { /* Merge ranges: re-insert so __subtree_end is updated */ fuse_page_it_remove(next, &cache->ranges); @@ -358,9 +337,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* * A revoked range is covered again by this grant, and a read - * range needs upgrading when a write is granted. Either way - * the recorded content carries over: the page cache under it - * did not change because the grant did. + * range needs upgrading when a write is granted. */ if (range->state == FUSE_DLM_RANGE_REVOKED || (want == FUSE_DLM_RANGE_WRITE && @@ -379,7 +356,6 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = range->start - 1; new_range->state = want; - new_range->content = FUSE_DLM_CONTENT_EMPTY; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -406,7 +382,6 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = end; new_range->state = want; - new_range->content = FUSE_DLM_CONTENT_EMPTY; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -490,7 +465,6 @@ void fuse_dlm_request_begin(struct fuse_inode *inode, req->end = end; req->state = FUSE_DLM_RANGE_REQUESTED; /* Nothing reads this while the request is pending; publish it set */ - req->content = FUSE_DLM_CONTENT_EMPTY; down_write(&cache->lock); list_add_tail(&req->list, &cache->pending); @@ -555,8 +529,8 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, * @off: byte offset to split at * * Splits the range containing @off in two, both halves keeping the state - * and content of the original, so a later marking can apply to one side - * only. A no-op when @off already starts a range or falls in a gap. + * of the original, so a revoke can apply to one side only. A no-op when + * @off already starts a range or falls in a gap. * * Caller holds @cache->lock for write. * @@ -591,182 +565,6 @@ static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) fuse_page_it_insert(tail, &cache->ranges); } -/** - * fuse_dlm_range_touched - record that IO is about to reach the page cache - * @inode: the fuse inode - * @start: start byte offset the IO covers (inclusive) - * @end: end byte offset the IO covers (inclusive) - * @mode: FUSE_PAGE_LOCK_READ if the range is only being populated, - * FUSE_PAGE_LOCK_WRITE if it is being dirtied - * - * Raises the content bound over exactly [start, end], never lowers it. - * Grants are page aligned but a write need not be, so ranges are split - * at both ends first and only the covered part is marked; the untouched - * remainder of a boundary page keeps its own bound and stays out of - * writeback. If a split cannot be allocated the whole overlapping range - * is marked, which overstates rather than understates. - * - * Called from fuse_get_dlm_lock(), fuse_cache_write_iter() and - * fuse_get_page_mkwrite_lock(), which between them cover every way - * cached IO reaches a folio under DLM, so the bound is raised before the - * data lands. - */ -void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - enum fuse_dlm_range_content level; - struct fuse_dlm_range *range; - - if (start > end) - return; - - level = mode == FUSE_PAGE_LOCK_WRITE ? FUSE_DLM_CONTENT_DIRTY : - FUSE_DLM_CONTENT_CLEAN; - - down_write(&cache->lock); - - fuse_dlm_split_at(cache, start); - if (end < U64_MAX) - fuse_dlm_split_at(cache, end + 1); - - for (range = fuse_dlm_find_overlapping(cache, start, end); range; - range = fuse_page_it_iter_next(range, start, end)) - if (range->content < level) - range->content = level; - - /* Recoalesce whatever the split left equal on both sides */ - fuse_dlm_try_merge(cache, start, end); - - up_write(&cache->lock); -} - -/** - * fuse_dlm_range_written - record that [start, end] holds written bytes - * @inode: the fuse inode - * @start: first byte written (inclusive) - * @end: last byte written (inclusive) - * - * fuse_dlm_range_touched() in write mode, except that a part of - * [start, end] no range covers is given one instead of going - * unrecorded. The write happened under a grant; if nothing covers those - * bytes by the time they are recorded, the grant was taken away in - * between, which is what FUSE_DLM_RANGE_REVOKED says: the bytes are - * real, the grant is gone, and writeback has to hold the range again - * before sending them. Left unrecorded they would read as never - * written, and writeback would drop them. - */ -void fuse_dlm_range_written(struct fuse_inode *inode, uint64_t start, - uint64_t end) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - struct fuse_dlm_range *range, *fill; - uint64_t cur = start; - - if (start > end) - return; - - down_write(&cache->lock); - - fuse_dlm_split_at(cache, start); - if (end < U64_MAX) - fuse_dlm_split_at(cache, end + 1); - - while (cur <= end) { - uint64_t reach; - - range = fuse_page_it_iter_first(&cache->ranges, cur, end); - if (!range || range->start > cur) { - reach = range ? range->start - 1 : end; - - /* - * Losing this would lose the bytes, so it cannot be - * allowed to fail; iomap allocates the state it keeps - * per folio the same way. - */ - fill = kmalloc(sizeof(*fill), - GFP_NOFS | __GFP_NOFAIL); - fill->start = cur; - fill->end = reach; - fill->state = FUSE_DLM_RANGE_REVOKED; - fill->content = FUSE_DLM_CONTENT_DIRTY; - INIT_LIST_HEAD(&fill->list); - fuse_page_it_insert(fill, &cache->ranges); - } else { - reach = min(range->end, end); - if (range->content < FUSE_DLM_CONTENT_DIRTY) - range->content = FUSE_DLM_CONTENT_DIRTY; - } - - if (reach == end) - break; - cur = reach + 1; - } - - fuse_dlm_try_merge(cache, start, end); - - up_write(&cache->lock); -} - -/* - * Marking done by fuse_get_dlm_lock() itself. A read populates whole - * pages, so the page aligned request range is the right thing to mark. - * A write grant is not marked here: the request range is page aligned - * and the write inside it need not be, and marking the alignment would - * claim bytes the writer never touched. fuse_cache_write_iter() marks - * the exact range instead, before it writes. - */ -static void fuse_dlm_mark_populated(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode) -{ - if (mode == FUSE_PAGE_LOCK_READ) - fuse_dlm_range_touched(inode, start, end, mode); -} - -/** - * fuse_dlm_ranges_flushed - [start, end] of the page cache is on the server - * @inode: the fuse inode - * @start: start byte offset written back and waited out (inclusive) - * @end: end byte offset written back and waited out (inclusive) - * - * Moves ranges lying wholly inside [start, end] back to clean. This is - * the one transition that lowers the bound, so nothing may be dirtying - * the mapping while it runs: - * - * - A cached write holds i_rwsem, which the caller holds exclusive. - * - * - A fault does not, so bail out if the inode is mapped. The test is - * made under the cache lock, and a fault can only dirty a folio after - * fuse_get_page_mkwrite_lock() has taken that same lock, so a mapping - * created after the test cannot get past this. - * - * A range only partly inside [start, end] keeps its bound: the rest of - * it was not written back. - */ -void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, - uint64_t end) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - struct fuse_dlm_range *range; - - if (start > end) - return; - - down_write(&cache->lock); - - if (mapping_mapped(inode->inode.i_mapping)) - goto out; - - for (range = fuse_dlm_find_overlapping(cache, start, end); range; - range = fuse_page_it_iter_next(range, start, end)) - if (range->start >= start && range->end <= end && - range->content == FUSE_DLM_CONTENT_DIRTY) - range->content = FUSE_DLM_CONTENT_CLEAN; - -out: - up_write(&cache->lock); -} - /** * fuse_dlm_ranges_dropped - the page cache under [start, end] is gone * @inode: the fuse inode @@ -805,8 +603,6 @@ void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, if (range->state == FUSE_DLM_RANGE_REVOKED) { fuse_page_it_remove(range, &cache->ranges); kfree(range); - } else { - range->content = FUSE_DLM_CONTENT_EMPTY; } range = next; @@ -817,57 +613,6 @@ void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, up_write(&cache->lock); } -/** - * fuse_dlm_range_may_be_dirty - can [start, end] hold unwritten data - * @inode: the fuse inode - * @start: start byte offset (inclusive) - * @end: end byte offset (inclusive) - * - * A part of the range with no recorded grant counts as dirty, which - * covers pages dirtied through fuse_get_page_mkwrite_lock() (no grant is - * recorded there) and grants that failed to record. - * - * Return: false only when every page of [start, end] is covered by a - * grant not written under since it was last flushed. - */ -bool fuse_dlm_range_may_be_dirty(struct fuse_inode *inode, uint64_t start, - uint64_t end) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - struct fuse_dlm_range *range; - uint64_t current_start = start; - bool covered_to_end = false; - bool dirty = false; - - if (!cache || start > end) - return true; - - down_read(&cache->lock); - - for (range = fuse_dlm_find_overlapping(cache, start, end); range; - range = fuse_page_it_iter_next(range, start, end)) { - /* A gap before this range: nothing is recorded for it */ - if (current_start < range->start || - range->content == FUSE_DLM_CONTENT_DIRTY) { - dirty = true; - break; - } - - if (range->end >= end) { - covered_to_end = true; - break; - } - - /* Safe: range->end < end, so this cannot wrap */ - current_start = range->end + 1; - } - - up_read(&cache->lock); - - /* A gap at the tail counts the same as one in the middle */ - return dirty || !covered_to_end; -} - /** * fuse_dlm_unlock_range - Revoke the grants over a range of pages * @inode: The fuse inode @@ -920,12 +665,23 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, /* Get next overlapping range before we modify the tree */ next = fuse_page_it_iter_next(range, start, end); - if (range->content == FUSE_DLM_CONTENT_EMPTY) { - /* Nothing cached under it, so nothing to describe */ + /* + * A revoked range is kept only to say that the page cache + * under it was dirtied under a grant that has gone, so that + * writeback takes the range again before sending it. With + * nothing cached there it has nothing to say. + * + * A grant with no end is recorded to U64_MAX; the page cache + * is indexed by a signed offset, so ask it about as much of + * that as it can name. + */ + if (filemap_range_has_page(inode->inode.i_mapping, range->start, + min_t(uint64_t, range->end, + LLONG_MAX))) { + range->state = FUSE_DLM_RANGE_REVOKED; + } else { fuse_page_it_remove(range, &cache->ranges); kfree(range); - } else { - range->state = FUSE_DLM_RANGE_REVOKED; } range = next; @@ -944,9 +700,6 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, * @end: End byte offset * @mode: Lock mode to check for * - * Check if the specified range of pages is already locked. - * The entire range must be locked for this to return true. - * * Return: true if the entire range is locked, false otherwise */ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, @@ -1040,9 +793,7 @@ bool fuse_dlm_write_grant_exists(struct fuse_inode *fi) down_read(&cache->lock); for (range = fuse_dlm_find_overlapping(cache, 0, U64_MAX); range; range = fuse_page_it_iter_next(range, 0, U64_MAX)) { - if (range->state == FUSE_DLM_RANGE_WRITE || - (range->state == FUSE_DLM_RANGE_REVOKED && - range->content == FUSE_DLM_CONTENT_DIRTY)) { + if (range->state == FUSE_DLM_RANGE_WRITE) { held = true; break; } @@ -1130,8 +881,10 @@ static int __fuse_get_dlm_lock(struct fuse_file *ff, struct inode *inode, * with, so this check and a later fuse_dlm_lock_is_held() can * never disagree about what counts as covered. */ if (fuse_dlm_lock_is_held(fi, offset, length, mode)) { - /* we already have this area locked */ - fuse_dlm_mark_populated(fi, pg_start, pg_end, mode); + /* + * Already covered, and the record says nothing beyond that, + * so this is one shared acquisition end to end. + */ return 0; } @@ -1196,13 +949,6 @@ static int __fuse_get_dlm_lock(struct fuse_file *ff, struct inode *inode, goto restart; } - /* - * Raise the content bound before the caller touches the page - * cache. A grant that failed to record has no range to raise; - * fuse_dlm_range_may_be_dirty() reports it dirty anyway. - */ - fuse_dlm_mark_populated(fi, pg_start, pg_end, mode); - /* * A failure to record (small-allocation -ENOMEM) does not undo * the grant: coverage exists cluster-wide, only the local diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 03ce8114f6ea7a..f12ad0e27777e6 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -99,31 +99,9 @@ bool fuse_dlm_lock_is_held(struct fuse_inode *inode, loff_t offset, /* Is any part of the file held for write? */ bool fuse_dlm_write_grant_exists(struct fuse_inode *inode); -/* - * Record that cached IO in @mode is about to reach the page cache over - * [start, end]. Only raises the recorded content, never lowers it. - */ -void fuse_dlm_range_touched(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode); - -/* - * Record that [start, end] holds bytes this client wrote. Unlike - * fuse_dlm_range_touched(), a part of it no range covers is given one. - */ -void fuse_dlm_range_written(struct fuse_inode *inode, uint64_t start, - uint64_t end); - -/* - * [start, end] has been written back and waited out. Caller holds - * i_rwsem exclusive; a mapped inode is left alone. - */ -void fuse_dlm_ranges_flushed(struct fuse_inode *inode, uint64_t start, - uint64_t end); - /* * The page cache under [start, end] is gone: free the revoked ranges over - * it and reset the content of the ones still held. The caller must have - * established the range really is empty. + * it. The caller must have established the range really is empty. */ void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, uint64_t end); @@ -132,12 +110,6 @@ void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, int fuse_dlm_regrant_range(struct fuse_file *ff, struct inode *inode, uint64_t start, uint64_t end); -/* - * Can [start, end] hold data the server has not seen? A part of it with - * no recorded grant counts as dirty. - */ -bool fuse_dlm_range_may_be_dirty(struct fuse_inode *inode, uint64_t start, - uint64_t end); /* This is the interface to the filesystem */ int fuse_get_dlm_lock(struct file *file, loff_t offset, diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 55a025f5d705f8..56ff100d98f41d 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1077,14 +1077,14 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, /* * What this notify has to do. Nothing cached in the * range means the drop is a no-op and the revoke is - * the whole job. Otherwise the record says whether - * the drop has to launder, which is what makes it - * wait for a FUSE_WRITE reply. + * the whole job. Otherwise the page cache says + * whether the drop has to launder, which is what + * makes it wait for a FUSE_WRITE reply. */ has_pages = filemap_range_has_page(inode->i_mapping, offset, end_byte); - may_be_dirty = fuse_dlm_range_may_be_dirty(fi, pg_first, - pg_last); + may_be_dirty = filemap_range_needs_writeback( + inode->i_mapping, offset, end_byte); /* * Put unwritten data on the server while the grant From b387e081947172cdfef31335dec23e1b903e22b3 Mon Sep 17 00:00:00 2001 From: Allison Henderson Date: Wed, 26 Aug 2026 10:08:46 -0700 Subject: [PATCH 31/41] fuse: do not hand O_APPEND to the server under the writeback cache With the writeback cache the kernel owns append positioning: a cached O_APPEND write is placed at the local i_size, and writeback later sends FUSE_WRITE requests with explicit offsets. Those offsets are only honoured if the server does not re-apply O_APPEND itself -- on Linux, pwrite(2) on a descriptor opened O_APPEND appends regardless of the offset argument. libfuse's passthrough examples know this and strip the flag, but only on FUSE_OPEN; a file created with FUSE_CREATE kept an O_APPEND backing descriptor, and every writeback run landed at the server's EOF instead of its offset. That is not just a theoretical hazard. Writeback legitimately covers the same bytes twice -- a folio whose record still names sent bytes is re-sent from its page start -- and with offsets honoured that re-send is idempotent. Appended instead, each flush's overlap with the previous one is duplicated at EOF and the file grows: generic/069 fails with exactly this shape (an appended file 230356 bytes too long, every byte of the excess a repeated flush overlap; server-side IO traces show each pwrite landing at EOF, sum of pwrites equal to the final file size). Strip O_APPEND from the flags sent in FUSE_OPEN, FUSE_CREATE and the compound open, the same way O_TRUNC is suppressed without atomic_o_trunc. The server cannot use the flag correctly under writeback caching anyway: appending server-side would order writeback runs by arrival, not by offset. Non-writeback mounts are unchanged, since there the server really does own append positioning. Signed-off-by: Allison Henderson --- fs/fuse/dir.c | 4 ++++ fs/fuse/file.c | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index acb39c6a23656b..4a79304c07347f 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -726,6 +726,10 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, memset(&inarg, 0, sizeof(inarg)); memset(&outentry, 0, sizeof(outentry)); inarg.flags = flags; + + /* The kernel owns append positioning; see fuse_send_open() */ + if (fm->fc->writeback_cache) + inarg.flags &= ~O_APPEND; inarg.mode = mode; inarg.umask = current_umask(); diff --git a/fs/fuse/file.c b/fs/fuse/file.c index c53f749d695867..6f27eee73aa93b 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -71,6 +71,17 @@ static int fuse_send_open(struct fuse_mount *fm, u64 nodeid, if (!fm->fc->atomic_o_trunc) inarg.flags &= ~O_TRUNC; + /* + * With the writeback cache the kernel owns append positioning: + * writeback sends FUSE_WRITE with explicit offsets, and a server + * that opens its backing file O_APPEND has pwrite(2) ignore them + * (Linux appends regardless of offset). Any re-sent or reordered + * run is then placed at EOF: duplicated data and a growing file. + * Do not hand the flag to the server at all. + */ + if (fm->fc->writeback_cache) + inarg.flags &= ~O_APPEND; + if (fm->fc->handle_killpriv_v2 && (inarg.flags & O_TRUNC) && !capable(CAP_FSETID)) { inarg.open_flags |= FUSE_OPEN_KILL_SUIDGID; @@ -174,6 +185,10 @@ static int fuse_compound_open_getattr(struct fuse_mount *fm, u64 nodeid, if (!fm->fc->atomic_o_trunc) open_in.flags &= ~O_TRUNC; + /* The kernel owns append positioning; see fuse_send_open() */ + if (fm->fc->writeback_cache) + open_in.flags &= ~O_APPEND; + if (fm->fc->handle_killpriv_v2 && (open_in.flags & O_TRUNC) && !capable(CAP_FSETID)) open_in.open_flags |= FUSE_OPEN_KILL_SUIDGID; From bab69caf9aeb4de64a94d197d2a1b0f4316e1828 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:40:21 +0200 Subject: [PATCH 32/41] fuse: ask the page cache alone whether local attributes win fuse_attr_cache_mask() consulted fuse_dlm_write_grant_exists() only to return early, and every test that follows already answers no when no grant is held: fuse_dlm_lock_is_held() cannot report a range covered for write with no write range recorded, and filemap_range_needs_writeback() cannot report bytes that no PAGECACHE_TAG marks. Drop the early return and its duplicated mapping_tagged() pair, and with its last caller fuse_dlm_write_grant_exists(). Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 35 ----------------------------------- fs/fuse/fuse_dlm_cache.h | 3 --- fs/fuse/inode.c | 21 ++++++++------------- 3 files changed, 8 insertions(+), 51 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 1a7851054090fd..cfeb7e0998ba62 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -768,41 +768,6 @@ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, return true; } -/** - * fuse_dlm_write_grant_exists - does the inode hold an exclusive grant anywhere - * @fi: the fuse inode - * - * Unlike fuse_dlm_range_is_locked(), which asks whether one range is fully - * covered, this asks whether any part of the file is held exclusively. A - * client that holds a write grant may be sitting on dirty page cache the - * server has not seen, so its mtime and ctime run ahead of anything the - * server can report. - * - * A revoked range still recorded as dirty counts too: the grant is gone - * but the unwritten data is not, so the local times are still ahead. - * - * Return: true if at least one recorded range is held for write, or was - * and still has unwritten data under it - */ -bool fuse_dlm_write_grant_exists(struct fuse_inode *fi) -{ - struct fuse_dlm_cache *cache = &fi->dlm_locked_areas; - struct fuse_dlm_range *range; - bool held = false; - - down_read(&cache->lock); - for (range = fuse_dlm_find_overlapping(cache, 0, U64_MAX); range; - range = fuse_page_it_iter_next(range, 0, U64_MAX)) { - if (range->state == FUSE_DLM_RANGE_WRITE) { - held = true; - break; - } - } - up_read(&cache->lock); - - return held; -} - /** * fuse_dlm_lock_is_held - check that a byte range is covered by a granted lock * @fi: the fuse inode diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index f12ad0e27777e6..d94f2faa4742c9 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -96,9 +96,6 @@ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, bool fuse_dlm_lock_is_held(struct fuse_inode *inode, loff_t offset, size_t length, enum fuse_page_lock_mode mode); -/* Is any part of the file held for write? */ -bool fuse_dlm_write_grant_exists(struct fuse_inode *inode); - /* * The page cache under [start, end] is gone: free the revoked ranges over * it. The caller must have established the range really is empty. diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 56ff100d98f41d..0a9c76a728394e 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -519,14 +519,14 @@ u32 fuse_get_cache_mask(struct inode *inode) * for exactly what the grant covers: * * - size, when the server reports less than i_size and the tail it does not - * know about, [srv_size, i_size), is entirely under a write grant. Taking - * the server's answer would shrink i_size and have truncate_pagecache() - * throw the unwritten tail away. - * - mtime and ctime, while a write grant covers unwritten data: our writes - * have stamped them locally and the server's stamps predate them. Only - * while the cache is actually dirty, not for as long as the grant lives: - * a grant is held until it is revoked or the inode is evicted, and past - * the writeback the server's stamps are the newer ones. Keeping ours + * know about, [attr->size, i_size), is entirely under a write grant. + * Taking the server's answer would shrink i_size and have + * truncate_pagecache() throw the unwritten tail away. + * - mtime and ctime, while the page cache is dirty or under writeback: our + * writes have stamped them locally and the server's stamps predate them. + * Only while the cache is actually dirty, not for as long as a grant + * lives: a grant is held until it is revoked or the inode is evicted, and + * past the writeback the server's stamps are the newer ones. Keeping ours * beyond that would hide a remote chown or chmod indefinitely. * * A remote truncate cannot slip through. It has to revoke the grant first, @@ -557,11 +557,6 @@ static u32 fuse_attr_cache_mask(struct inode *inode, struct fuse_attr *attr, * the tail grants itself while cached writes above the new size * are still waiting for writeback. */ - if (!fuse_dlm_write_grant_exists(fi) && - !mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY) && - !mapping_tagged(inode->i_mapping, PAGECACHE_TAG_WRITEBACK)) - return cache_mask; - if (mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY) || mapping_tagged(inode->i_mapping, PAGECACHE_TAG_WRITEBACK)) cache_mask |= STATX_MTIME | STATX_CTIME; From 43948b7bb9b9713af9d3ac3810b3a8c2befb4d0a Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:41:45 +0200 Subject: [PATCH 33/41] fuse: drop the DLM cache entry points nothing calls fuse_dlm_lock_range() has had no caller since grants are recorded through fuse_dlm_request_commit(), and fuse_dlm_range_is_locked() is reached only through fuse_dlm_lock_is_held(). fuse_dlm_find_overlapping() only named fuse_page_it_iter_first(). The cache is embedded in struct fuse_inode, so the !cache tests can never fire and fuse_dlm_cache_init() cannot fail. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 68 ++++++++-------------------------------- fs/fuse/fuse_dlm_cache.h | 10 +----- 2 files changed, 14 insertions(+), 64 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index cfeb7e0998ba62..5ce4264c110832 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -152,30 +152,21 @@ static void fuse_dlm_kill_pending(struct fuse_dlm_cache *cache, } /** - * fuse_page_cache_init - Initialize a page cache lock manager - * @cache: The cache to initialize - * - * Initialize a page cache lock manager for a FUSE inode. - * - * Return: 0 on success, negative error code on failure + * fuse_dlm_cache_init - Initialize a page cache lock manager + * @inode: The fuse inode to initialize the cache of */ -int fuse_dlm_cache_init(struct fuse_inode *inode) +void fuse_dlm_cache_init(struct fuse_inode *inode) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - if (!cache) - return -EINVAL; - init_rwsem(&cache->lock); cache->ranges = RB_ROOT_CACHED; INIT_LIST_HEAD(&cache->pending); - - return 0; } /** - * fuse_page_cache_destroy - Clean up a page cache lock manager - * @cache: The cache to clean up + * fuse_dlm_cache_release_locks - Clean up a page cache lock manager + * @inode: The fuse inode to clean up the cache of * * Release all locks and free all resources associated with the cache. */ @@ -185,9 +176,6 @@ void fuse_dlm_cache_release_locks(struct fuse_inode *inode) struct fuse_dlm_range *range; struct rb_node *node; - if (!cache) - return; - /* Release all locks */ down_write(&cache->lock); /* @@ -204,22 +192,7 @@ void fuse_dlm_cache_release_locks(struct fuse_inode *inode) } /** - * fuse_dlm_find_overlapping - Find a range that overlaps with [start, end] - * @cache: The page cache - * @start: Start byte offset - * @end: End byte offset - * - * Return: Pointer to the first overlapping range, or NULL if none found - */ -static struct fuse_dlm_range * -fuse_dlm_find_overlapping(struct fuse_dlm_cache *cache, uint64_t start, - uint64_t end) -{ - return fuse_page_it_iter_first(&cache->ranges, start, end); -} - -/** - * fuse_page_try_merge - Try to merge ranges within a specific region + * fuse_dlm_try_merge - Try to merge ranges within a specific region * @cache: The page cache * @start: Start byte offset * @end: End byte offset @@ -234,9 +207,6 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, uint64_t first = start ? start - 1 : start; uint64_t last = end < U64_MAX ? end + 1 : end; - if (!cache) - return; - /* * Find the first range that might need merging. Directly adjacent * ranges can merge, hence the region is widened by one unit to each @@ -310,7 +280,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, LIST_HEAD(to_upgrade); uint64_t current_start = start; - if (!cache || start > end) + if (start > end) return -EINVAL; /* The state this grant records */ @@ -418,19 +388,6 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, return ret; } -int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - int ret; - - down_write(&cache->lock); - ret = fuse_dlm_lock_range_locked(inode, start, end, mode); - up_write(&cache->lock); - - return ret; -} - /** * fuse_dlm_request_begin - publish a lock request before it is sent * @inode: the fuse inode @@ -643,7 +600,7 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range, *next; - if (!cache || start > end) + if (start > end) return -EINVAL; down_write(&cache->lock); @@ -702,15 +659,16 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, * * Return: true if the entire range is locked, false otherwise */ -bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode) +static bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, + uint64_t end, + enum fuse_page_lock_mode mode) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range; enum fuse_dlm_range_state want; uint64_t current_start = start; - if (!cache || start > end) + if (start > end) return false; /* The state a range has to be in to cover this request */ @@ -719,7 +677,7 @@ bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, down_read(&cache->lock); /* Find the first range that overlaps with [start, end] */ - range = fuse_dlm_find_overlapping(cache, start, end); + range = fuse_page_it_iter_first(&cache->ranges, start, end); /* Check if the entire range is covered */ while (range && current_start <= end) { diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index d94f2faa4742c9..fc345a06654182 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -51,15 +51,11 @@ struct fuse_dlm_cache { }; /* Initialize a page cache lock manager */ -int fuse_dlm_cache_init(struct fuse_inode *inode); +void fuse_dlm_cache_init(struct fuse_inode *inode); /* Clean up a page cache lock manager */ void fuse_dlm_cache_release_locks(struct fuse_inode *inode); -/* Lock a range of pages */ -int fuse_dlm_lock_range(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode); - /* * Publish a FUSE_DLM_WB_LOCK for [start, end] before it is sent, so a * revoke processed while the reply is on the wire can mark it. @req is @@ -88,10 +84,6 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, int fuse_dlm_unlock_range(struct fuse_inode *inode, uint64_t start, uint64_t end); -/* Check if a page range is already locked */ -bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, - uint64_t end, enum fuse_page_lock_mode mode); - /* Re-validate a fuse_get_dlm_lock() grant against the live lock tree */ bool fuse_dlm_lock_is_held(struct fuse_inode *inode, loff_t offset, size_t length, enum fuse_page_lock_mode mode); From e9b8e739dfafc60a6ba3fe1f280197e5f249078a Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:42:21 +0200 Subject: [PATCH 34/41] fuse: walk the DLM tree once to answer coverage fuse_dlm_range_is_locked() had four exits, each unlocking for itself, and a tail that could not be reached: the loop leaves current_start at most end, so the trailing test always said uncovered. Fold the walk into one loop with a single unlock. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 52 +++++++++++----------------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 5ce4264c110832..ac32b28715a1ad 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -667,6 +667,7 @@ static bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, struct fuse_dlm_range *range; enum fuse_dlm_range_state want; uint64_t current_start = start; + bool covered = false; if (start > end) return false; @@ -676,54 +677,29 @@ static bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, down_read(&cache->lock); - /* Find the first range that overlaps with [start, end] */ - range = fuse_page_it_iter_first(&cache->ranges, start, end); - - /* Check if the entire range is covered */ - while (range && current_start <= end) { + for (range = fuse_page_it_iter_first(&cache->ranges, start, end); range; + range = fuse_page_it_iter_next(range, start, end)) { /* - * The held lock must be at least as strong as the one - * requested. A WRITE lock (exclusive) satisfies a READ - * request, so only treat the range as uncovered when the - * held mode is weaker than what we ask for. This avoids - * re-requesting a READ lock for a range we already hold - * a WRITE lock on (e.g. read-after-write). + * A gap before this range leaves the request uncovered, and + * so does a range held less strongly than it asks for. A + * WRITE grant is exclusive and does cover a READ request, + * which keeps a read-after-write from asking again. */ - if (!fuse_dlm_state_satisfies(range->state, want)) { - /* Held lock is weaker than requested */ - up_read(&cache->lock); - return false; - } - - /* Check if there's a gap before this range */ - if (current_start < range->start) { - /* Found a gap */ - up_read(&cache->lock); - return false; - } + if (current_start < range->start || + !fuse_dlm_state_satisfies(range->state, want)) + break; - /* Covered through the end of the requested range? */ if (range->end >= end) { - up_read(&cache->lock); - return true; + covered = true; + break; } - /* Move current_start past this range */ current_start = range->end + 1; - - /* Get next overlapping range */ - range = fuse_page_it_iter_next(range, start, end); - } - - /* Check if we covered the entire range */ - if (current_start <= end) { - /* There's a gap at the end */ - up_read(&cache->lock); - return false; } up_read(&cache->lock); - return true; + + return covered; } /** From f611a1a57d871e1ddb0f685a2151a5dfef6a0c22 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:42:45 +0200 Subject: [PATCH 35/41] fuse: drop the writeback iomap type reset Nothing reports a hole any more: both writepage contexts start at IOMAP_MAPPED and fuse_iomap_writeback_range() no longer lowers the type, so setting it per run says nothing. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 6f27eee73aa93b..4ee45106d0825b 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -3171,19 +3171,9 @@ static ssize_t fuse_iomap_writeback_range(struct iomap_writepage_ctx *wpc, * tries again; only a hard error stops it. */ if (fc->dlm && fc->writeback_cache) { - int err; - - /* - * wpc->iomap.type carries over from the previous run and from - * the previous folio, so anything that is written has to say - * so. Left at a stale IOMAP_HOLE, iomap takes the folio for - * one it never queued and ends its writeback while the write - * is still in flight. - */ - wpc->iomap.type = IOMAP_MAPPED; + int err = fuse_dlm_regrant_range(data->ff, inode, pos, + pos + len - 1); - err = fuse_dlm_regrant_range(data->ff, inode, pos, - pos + len - 1); if (err < 0 && err != -ENOSYS) return err; } From 2535e345008fd9e52c737a9683c93c628d0add12 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:43:58 +0200 Subject: [PATCH 36/41] fuse: take the append DLM grant once, after the write checks An append was locked twice: at i_size before generic_write_checks(), then again at ki_pos when the checks moved it. The first grant covers a range the write need not use and the second is what the write runs under, so take it only where the range is settled, which also bounds it by the count the checks allow. fuse_cache_wr_exclusive_lock() was likewise called twice, the first result live only while the DLM probe cannot run. Call it once, after the probe. dlm_pos and dlm_len have no reader left. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 98 +++++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 65 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 4ee45106d0825b..0945ea0d8d203e 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -2086,9 +2086,7 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) struct fuse_conn *fc = get_fuse_conn(inode); struct fuse_inode *fi = get_fuse_inode(inode); bool writeback = false; - bool exclusive = true; - loff_t dlm_pos = 0; - size_t dlm_len = 0; + bool exclusive; if (fuse_inode_force_dio(inode)) return fuse_direct_write_iter(iocb, from); @@ -2132,94 +2130,64 @@ static ssize_t fuse_cache_write_iter(struct kiocb *iocb, struct iov_iter *from) writeback = true; } - exclusive = fuse_cache_wr_exclusive_lock(iocb, writeback); - /* * Request the DLM write lock before taking i_rwsem: the request is * an unbounded cluster round trip, and holding the writer-priority * rwsem across it would park a truncate -- and behind it every - * later writer -- for the duration. The grant-to-use window this - * leaves open is closed by the in-gate re-validation below. Only - * the append case must wait for the lock: its range depends on - * i_size, which is stable only under the exclusive inode lock. + * later writer -- for the duration. Only the append case must wait + * for the lock: its range depends on i_size, which is settled by + * generic_write_checks() under the exclusive inode lock. + * + * The request may find that the server has no DLM at all and clear + * fc->dlm, so pick the lock mode after it rather than before. The + * relaxed shared lock is only sound under DLM: the shared path + * claims the i_size extension up front, which stops iomap from + * zeroing beyond EOF, and the zero-fill that replaces it in + * fuse_iomap_read_folio_range() is itself gated on fc->dlm. Chosen + * too early, an expanding write would fall through to a READ of a + * range that cannot hold data -- which fails outright on a handle + * the client opened write-only. */ if (writeback && fc->dlm && !(iocb->ki_flags & IOCB_APPEND)) { - dlm_pos = iocb->ki_pos; - dlm_len = iov_iter_count(from); - - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len); + err = fuse_cache_wr_dlm_lock(file, iocb->ki_pos, + iov_iter_count(from)); if (err) return err; - - /* - * The request above may have found that the server has no DLM - * at all, in which case it cleared fc->dlm. The relaxed shared - * lock was chosen just before, while fc->dlm still read 1, and - * it is only sound under DLM: the shared path claims the i_size - * extension up front, which stops iomap from zeroing beyond - * EOF, and the zero-fill that replaces it in - * fuse_iomap_read_folio_range() is itself gated on fc->dlm. - * Left as chosen, an expanding write would fall through to a - * READ of a range that cannot hold data -- which fails outright - * on a handle the client opened write-only. Re-decide now, - * while no lock is held yet. - */ - exclusive = fuse_cache_wr_exclusive_lock(iocb, writeback); } + exclusive = fuse_cache_wr_exclusive_lock(iocb, writeback); + if (exclusive) inode_lock(inode); else inode_lock_shared(inode); - /* note that this small code dup will save us a lot of headache later - * when appends are done concurrently without using parallel direct writes */ - if (writeback && fc->dlm && (iocb->ki_flags & IOCB_APPEND)) { - /* - * 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. Lock where the data will land. - */ - dlm_pos = i_size_read(inode); - dlm_len = iov_iter_count(from); - - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len); - if (err) - goto out; - } - err = count = generic_write_checks(iocb, from); if (err <= 0) goto 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. + * An append lands at the EOF generic_write_checks() has just written + * into ki_pos, not where the caller pointed, and the exclusive inode + * lock does not pin i_size either: attribute replies move it under + * fi->lock alone. Take the grant here, where the range is settled. */ - if (writeback && fc->dlm && (iocb->ki_flags & IOCB_APPEND) && - iocb->ki_pos != dlm_pos) { - dlm_pos = iocb->ki_pos; - dlm_len = count; - - err = fuse_cache_wr_dlm_lock(file, dlm_pos, dlm_len); + if (writeback && fc->dlm && (iocb->ki_flags & IOCB_APPEND)) { + err = fuse_cache_wr_dlm_lock(file, iocb->ki_pos, count); if (err) goto out; } /* - * Kill suid/sgid and stamp the timestamps here, before the gate, - * instead of leaving them next to the write itself. kiocb_modified() - * -> file_remove_privs() is the one that reaches the server: without - * handle_killpriv[_v2] fuse_setattr() kills the bits by asking it (a - * FUSE_GETATTR to refresh the mode, then a FUSE_SETATTR, which for a - * writeback inode first flushes and freezes writepages), and - * security_inode_killpriv() can drop the capability xattr with another - * round trip. A server may have to invalidate this inode from inside - * such a handler, and it must not find this write holding anything - * it needs. + * Kill suid/sgid and stamp the timestamps here, ahead of the write + * itself. kiocb_modified() -> file_remove_privs() is the one that + * reaches the server: without handle_killpriv[_v2] fuse_setattr() + * kills the bits by asking it (a FUSE_GETATTR to refresh the mode, + * then a FUSE_SETATTR, which for a writeback inode first flushes and + * freezes writepages), and security_inode_killpriv() can drop the + * capability xattr with another round trip. A server may have to + * invalidate this inode from inside such a handler, and it must not + * find this write holding anything it needs. * * This also runs before the forced-DIO re-route below, so a re-routed * write repeats it; there is nothing left to do the second time. From 3a422109d5c1b9e9a82316478f36bf7943d5fa32 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:44:36 +0200 Subject: [PATCH 37/41] fuse: loop over the chunks of a DLM buffered write The head, interior and tail differed only in their bounds and in whether they bypass the page cache, and each repeated the same short-write and error handling. Describe them as an array and run one loop. Signed-off-by: Horst Birthelmer --- fs/fuse/file.c | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 0945ea0d8d203e..5b690cf71e5c3d 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1982,7 +1982,17 @@ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, 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); + /* Unaligned head, cached interior of whole pages, unaligned tail */ + const struct { + loff_t len; + bool through; + } chunk[] = { + { mid_start - pos, true }, + { mid_end - mid_start, false }, + { end - mid_end, true }, + }; ssize_t res, total = 0; + unsigned int i; /* No whole page inside the write: nothing cacheable, all through. */ if (mid_end <= mid_start) @@ -1994,28 +2004,15 @@ static ssize_t fuse_dlm_buffered_write(struct kiocb *iocb, * what has landed when that is nothing reports no error and no * bytes, which fuse_cache_write_iter() turns into the full count. */ - - /* Unaligned head [pos, mid_start): through. */ - res = fuse_dlm_write_chunk(iocb, from, file, 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 pages. */ - res = fuse_dlm_write_chunk(iocb, from, file, 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, file, end - mid_end, true); - if (res < 0) - return total ? total : res; - total += res; + for (i = 0; i < ARRAY_SIZE(chunk); i++) { + res = fuse_dlm_write_chunk(iocb, from, file, chunk[i].len, + chunk[i].through); + if (res < 0) + return total ? total : res; + total += res; + if (res < chunk[i].len) + break; + } return total; } From 5e56bb122807610712e47986cc995b6d5c845860 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 21:17:41 +0200 Subject: [PATCH 38/41] fuse: forget a revoked DLM range instead of keeping it A revoked range was kept in the tree so writeback could tell it from a gap and take the grant back before sending. Writeback now holds the range again for every run it sends, so the two are the same answer: ask the server. Remove the range on revoke, and with the state gone drop fuse_dlm_ranges_dropped() and the six callers that scanned the page cache to decide whether to free it. Signed-off-by: Horst Birthelmer --- fs/fuse/dir.c | 24 -------- fs/fuse/file.c | 55 ++--------------- fs/fuse/fuse_dlm_cache.c | 124 +++++++-------------------------------- fs/fuse/fuse_dlm_cache.h | 18 ++---- fs/fuse/inode.c | 44 +------------- 5 files changed, 32 insertions(+), 233 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 4a79304c07347f..97f7cdcab43289 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2242,30 +2242,6 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, truncate_pagecache(inode, outarg.attr.size); invalidate_inode_pages2(mapping); - - /* - * The cache above the new size is gone, so the ranges - * describing it have nothing left to say. From the first - * whole page above it: the page holding the new end of the - * file survives the truncate, and so does its record. - */ - if (fc->dlm && fc->writeback_cache) - fuse_dlm_ranges_dropped(fi, PAGE_ALIGN(outarg.attr.size), - U64_MAX); - - /* - * invalidate_inode_pages2() emptied the mapping below the new - * size too (laundering anything dirty first, so those bytes - * are on the server). Ranges left DIRTY there would make the - * next partial write to the page keep and flush folio bytes - * nobody wrote. Only when the drop really emptied it: a busy - * folio that survived still needs its record, and a fault - * populating after the check keeps its page visible to it. - */ - if (fc->dlm && fc->writeback_cache && outarg.attr.size && - !filemap_range_has_page(mapping, 0, outarg.attr.size - 1)) - fuse_dlm_ranges_dropped(fi, 0, - PAGE_ALIGN(outarg.attr.size) - 1); } clear_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 5b690cf71e5c3d..e21a08dd5704df 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -412,18 +412,6 @@ static int fuse_open(struct inode *inode, struct file *file) truncate_pagecache(inode, 0); } else if (!(ff->open_flags & FOPEN_KEEP_CACHE)) { invalidate_inode_pages2(inode->i_mapping); - /* - * Only when the drop really emptied the mapping; a - * folio that survived still needs its record. This - * open holds no lock against concurrent IO, but - * neither does the invalidate above -- anything - * populated or dirtied after it keeps its page, and - * the check sees that page. - */ - if (fc->dlm && fc->writeback_cache && - !filemap_range_has_page(inode->i_mapping, 0, - LLONG_MAX)) - fuse_dlm_ranges_dropped(fi, 0, U64_MAX); } } if (dax_truncate) @@ -4039,29 +4027,9 @@ static long fuse_file_fallocate(struct file *file, int mode, loff_t offset, file_update_time(file); } - if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_ZERO_RANGE)) { + if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_ZERO_RANGE)) truncate_pagecache_range(inode, offset, offset + length - 1); - /* - * The whole pages inside the hole are gone; a record left - * over them would make the next partial write to one keep - * and flush folio bytes nobody wrote. The pages straddling - * the ends survive with their punched part zeroed, so their - * record still names real (now zero) bytes and stays. Only - * when the drop really emptied the span: a busy folio that - * survived keeps its record. - */ - if (fm->fc->dlm && fm->fc->writeback_cache) { - uint64_t first = PAGE_ALIGN(offset); - uint64_t last = (uint64_t)(offset + length) & PAGE_MASK; - - if (first < last && - !filemap_range_has_page(inode->i_mapping, first, - last - 1)) - fuse_dlm_ranges_dropped(fi, first, last - 1); - } - } - fuse_invalidate_attr_mask(inode, FUSE_STATX_MODSIZE); out: @@ -4168,24 +4136,9 @@ static ssize_t __fuse_copy_file_range(struct file *file_in, loff_t pos_in, if (err) goto out; - { - loff_t lstart = ALIGN_DOWN(pos_out, PAGE_SIZE); - loff_t lend = ALIGN(pos_out + outarg.size, PAGE_SIZE) - 1; - - truncate_inode_pages_range(inode_out->i_mapping, lstart, lend); - - /* - * The record over the dropped span has nothing left to - * describe, and left DIRTY it would make the next partial - * write there keep and flush folio bytes nobody wrote. - * Only when the drop really emptied it: a folio a - * concurrent fault put back keeps its record. - */ - if (fc->dlm && fc->writeback_cache && - !filemap_range_has_page(inode_out->i_mapping, lstart, - lend)) - fuse_dlm_ranges_dropped(fi_out, lstart, lend); - } + truncate_inode_pages_range(inode_out->i_mapping, + ALIGN_DOWN(pos_out, PAGE_SIZE), + ALIGN(pos_out + outarg.size, PAGE_SIZE) - 1); file_update_time(file_out); fuse_write_update_attr(inode_out, pos_out + outarg.size, outarg.size); diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index ac32b28715a1ad..8842b73181ac4e 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -13,13 +13,9 @@ * - REQUESTED, on cache->pending, while its FUSE_DLM_WB_LOCK is in * flight. * - * - REVOKED, in either place. On cache->pending it is a request a - * revoke overlapped while it was in flight, and - * fuse_dlm_request_commit() drops that grant instead of recording it. - * In cache->ranges it is a grant that was recorded and has since been - * taken away, kept because the page cache under it is still - * described. It covers nothing either way, so the IO paths ask - * again. + * - REVOKED, on cache->pending: a request a revoke overlapped while it + * was in flight, so fuse_dlm_request_commit() drops that grant + * instead of recording it. * * - READ or WRITE, in cache->ranges. The only states * fuse_dlm_range_is_locked() reports as covered; the mode is not a @@ -31,13 +27,14 @@ * * The record says nothing about the page cache under a range. What is * cached there, and whether the server has seen it, is what the page - * cache itself answers. + * cache itself answers. A revoked grant is therefore forgotten, not + * kept: writeback holds the range again for every run it sends, and an + * absent record and a revoked one both make it ask. */ #include "fuse_i.h" #include "fuse_dlm_cache.h" #include -#include #include #include #include @@ -55,9 +52,8 @@ enum fuse_dlm_range_state { /* FUSE_DLM_WB_LOCK in flight, on cache->pending */ FUSE_DLM_RANGE_REQUESTED, /* - * On cache->pending, revoked in flight and the grant must not be - * recorded. In cache->ranges, granted once and taken away, kept to - * describe the page cache under it. Covers nothing either way. + * On cache->pending, revoked in flight: the grant must not be + * recorded. Never in cache->ranges, which holds grants only. */ FUSE_DLM_RANGE_REVOKED, /* Granted shared, in cache->ranges */ @@ -290,10 +286,8 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, * Ranges are upgraded whole below, so split at the grant bounds * first: a range extending past the grant would otherwise have its * uncovered part upgraded with it, recording coverage the server - * never gave. A read range half-covered by a write grant would - * report the other half held for write, and a revoked range - * half-regranted would report its still-revoked bytes as held, so - * writeback would send them without taking the range again. + * never gave, and a read range half-covered by a write grant would + * report the other half held for write. */ fuse_dlm_split_at(cache, start); if (end < U64_MAX) @@ -305,13 +299,9 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* Get next overlapping range before we potentially modify the tree */ next = fuse_page_it_iter_next(range, start, end); - /* - * A revoked range is covered again by this grant, and a read - * range needs upgrading when a write is granted. - */ - if (range->state == FUSE_DLM_RANGE_REVOKED || - (want == FUSE_DLM_RANGE_WRITE && - range->state != FUSE_DLM_RANGE_WRITE)) + /* A read range needs upgrading when a write is granted */ + if (want == FUSE_DLM_RANGE_WRITE && + range->state != FUSE_DLM_RANGE_WRITE) list_add_tail(&range->list, &to_upgrade); /* If WRITE lock already exists - nothing to do */ @@ -522,69 +512,17 @@ static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) fuse_page_it_insert(tail, &cache->ranges); } -/** - * fuse_dlm_ranges_dropped - the page cache under [start, end] is gone - * @inode: the fuse inode - * @start: start byte offset (inclusive) - * @end: end byte offset (inclusive) - * - * A revoked range exists only to describe page cache dirtied before the - * grant was taken away. Once that cache is gone the range has nothing - * left to say and is freed; a range still held goes back to describing - * nothing. - * - * The caller must have established that the range really is empty, not - * merely asked for it to be dropped: a folio that survived an - * invalidate is still there, and claiming otherwise would let writeback - * send it with no record of where it came from. - */ -void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, - uint64_t end) -{ - struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; - struct fuse_dlm_range *range, *next; - - if (start > end) - return; - - down_write(&cache->lock); - - fuse_dlm_split_at(cache, start); - if (end < U64_MAX) - fuse_dlm_split_at(cache, end + 1); - - range = fuse_page_it_iter_first(&cache->ranges, start, end); - while (range) { - next = fuse_page_it_iter_next(range, start, end); - - if (range->state == FUSE_DLM_RANGE_REVOKED) { - fuse_page_it_remove(range, &cache->ranges); - kfree(range); - } - - range = next; - } - - fuse_dlm_try_merge(cache, start, end); - - up_write(&cache->lock); -} - /** * fuse_dlm_unlock_range - Revoke the grants over a range of pages * @inode: The fuse inode * @start: Start byte offset * @end: End byte offset * - * The server has taken [start, end] back. A range that has nothing - * cached under it is removed; one that has is kept and marked - * FUSE_DLM_RANGE_REVOKED, so the page cache it covers stays described. - * Removing it instead would leave a gap, and a gap reads as "no record", - * which is what an untracked range looks like: writeback would then send - * folios dirtied under the grant that was just taken away. - * - * A revoked range covers nothing, so fuse_dlm_range_is_locked() reports - * it uncovered and the IO paths request again. + * The server has taken [start, end] back, so the grants over it are + * removed and the IO paths ask again. Page cache dirtied under a grant + * that has gone is not lost by this: writeback takes the range again for + * every run it sends, and a range it finds unrecorded is a range it asks + * for. * * An inverted range is rejected rather than silently revoking nothing: * the callers revoke coverage, and a revoke that quietly keeps the grant @@ -594,8 +532,8 @@ void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, * * Return: 0 on success, negative error code on failure */ -int fuse_dlm_unlock_range(struct fuse_inode *inode, - uint64_t start, uint64_t end) +int fuse_dlm_unlock_range(struct fuse_inode *inode, uint64_t start, + uint64_t end) { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range, *next; @@ -622,30 +560,12 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, /* Get next overlapping range before we modify the tree */ next = fuse_page_it_iter_next(range, start, end); - /* - * A revoked range is kept only to say that the page cache - * under it was dirtied under a grant that has gone, so that - * writeback takes the range again before sending it. With - * nothing cached there it has nothing to say. - * - * A grant with no end is recorded to U64_MAX; the page cache - * is indexed by a signed offset, so ask it about as much of - * that as it can name. - */ - if (filemap_range_has_page(inode->inode.i_mapping, range->start, - min_t(uint64_t, range->end, - LLONG_MAX))) { - range->state = FUSE_DLM_RANGE_REVOKED; - } else { - fuse_page_it_remove(range, &cache->ranges); - kfree(range); - } + fuse_page_it_remove(range, &cache->ranges); + kfree(range); range = next; } - fuse_dlm_try_merge(cache, start, end); - up_write(&cache->lock); return 0; } diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index fc345a06654182..88627af36eb5de 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -31,16 +31,15 @@ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; /* * Page cache lock manager. * - * @ranges holds the grants the client has been given, and the ones it - * has had taken back that still describe page cache - * (FUSE_DLM_RANGE_REVOKED). A request still on the wire covers nothing - * and lives on @pending instead, so tree walkers never filter on state. - * See enum fuse_dlm_range_state in fuse_dlm_cache.c. + * @ranges holds the grants the client has been given. A request still on + * the wire covers nothing and lives on @pending instead, so tree walkers + * never filter on state. See enum fuse_dlm_range_state in + * fuse_dlm_cache.c. */ struct fuse_dlm_cache { /* Lock protecting the tree and the pending list */ struct rw_semaphore lock; - /* Interval tree of recorded ranges, granted or revoked */ + /* Interval tree of the grants held */ struct rb_root_cached ranges; /* * FUSE_DLM_WB_LOCK requests in flight (REQUESTED, or REVOKED once @@ -88,13 +87,6 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, uint64_t start, bool fuse_dlm_lock_is_held(struct fuse_inode *inode, loff_t offset, size_t length, enum fuse_page_lock_mode mode); -/* - * The page cache under [start, end] is gone: free the revoked ranges over - * it. The caller must have established the range really is empty. - */ -void fuse_dlm_ranges_dropped(struct fuse_inode *inode, uint64_t start, - uint64_t end); - /* Hold [start, end] again so writeback can send what it found revoked */ int fuse_dlm_regrant_range(struct fuse_file *ff, struct inode *inode, uint64_t start, uint64_t end); diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 0a9c76a728394e..7e573b4e7e4004 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -661,26 +661,6 @@ static void fuse_change_attributes_i(struct inode *inode, struct fuse_attr *attr if (inval) invalidate_inode_pages2(inode->i_mapping); - - /* - * The DLM record has to follow the cache out, as on every - * other path that drops it. A range left DIRTY over a - * dropped folio makes the next partial write to that page - * keep and flush folio bytes nobody wrote. The pages above - * the new size are gone unconditionally; the rest only when - * the invalidate really emptied the mapping, so a folio that - * survived (or was faulted back) keeps its record. - */ - if (fc->dlm && fc->writeback_cache) { - if (have_size && oldsize != attr->size) - fuse_dlm_ranges_dropped(fi, - PAGE_ALIGN(attr->size), - U64_MAX); - if (inval && - !filemap_range_has_page(inode->i_mapping, 0, - LLONG_MAX)) - fuse_dlm_ranges_dropped(fi, 0, U64_MAX); - } } if (IS_ENABLED(CONFIG_FUSE_DAX)) @@ -969,8 +949,6 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, { struct fuse_inode *fi; struct inode *inode; - uint64_t pg_first; - uint64_t pg_last; loff_t end_byte; pgoff_t pg_start; pgoff_t pg_end; @@ -1007,16 +985,8 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, else pg_end = (offset + len - 1) >> PAGE_SHIFT; - /* - * Byte bounds of the same region, and the page aligned form - * the DLM record is asked about. A write smaller than a page - * marks only its own bytes, so the query has to cover whole - * pages or a dirty edge inside a page would not be seen. - */ + /* Byte bounds of the same region */ end_byte = len <= 0 ? LLONG_MAX : offset + len - 1; - pg_first = (uint64_t)offset & PAGE_MASK; - pg_last = len <= 0 ? U64_MAX : - (((uint64_t)offset + len - 1) | (PAGE_SIZE - 1)); /* * A data invalidation means another (remote) entity is @@ -1133,18 +1103,6 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, pg_end, may_be_dirty); - /* - * A revoked range exists to describe page cache - * dirtied before the grant went; with that cache gone - * it has nothing left to say. Only when it really - * went: an invalidate can leave a busy folio behind, - * and that folio still needs its record. - */ - if (has_pages && - !filemap_range_has_page(inode->i_mapping, offset, - end_byte)) - fuse_dlm_ranges_dropped(fi, pg_first, pg_last); - if (latched) pr_info_ratelimited("FUSE: inode %llu latched to direct IO on invalidation notify storm\n", nodeid); From d6231335cf3e602916f289fe6ec3f9f0b2772d1b Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:49:45 +0200 Subject: [PATCH 39/41] fuse: record the DLM lock mode instead of a range state With revoked ranges gone from the tree, enum fuse_dlm_range_state said two unrelated things: which mode a grant is held in, and whether a request in flight has been killed. Split them into the mode itself and a bool, so the tree carries the mode the callers already speak in and fuse_dlm_granted_state() goes. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 117 ++++++++++++--------------------------- fs/fuse/fuse_dlm_cache.h | 8 +-- 2 files changed, 38 insertions(+), 87 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index 8842b73181ac4e..cae9b77499b717 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -2,28 +2,17 @@ /* * FUSE page lock cache implementation * - * cache->ranges records the grants the server has given this client. A - * grant still on the wire covers nothing and must not appear there, but - * a revoke has to be able to find it: otherwise a revoke processed - * before the grant is recorded removes nothing, and the grant recorded - * afterwards is never taken back. - * - * A range therefore carries enum fuse_dlm_range_state: - * - * - REQUESTED, on cache->pending, while its FUSE_DLM_WB_LOCK is in - * flight. - * - * - REVOKED, on cache->pending: a request a revoke overlapped while it - * was in flight, so fuse_dlm_request_commit() drops that grant - * instead of recording it. - * - * - READ or WRITE, in cache->ranges. The only states - * fuse_dlm_range_is_locked() reports as covered; the mode is not a - * separate field, since a range is either not held or held in one - * definite mode. - * - * In-flight requests are kept off the tree so the state is consulted - * only where a request is retired, not by every tree walker. + * cache->ranges records the grants the server has given this client, + * each with the mode it is held in. A grant still on the wire covers + * nothing and must not appear there, but a revoke has to be able to find + * it: otherwise a revoke processed before the grant is recorded removes + * nothing, and the grant recorded afterwards is never taken back. A + * request in flight therefore waits on cache->pending, where a revoke + * marks it killed and fuse_dlm_request_commit() drops the grant instead + * of recording it. + * + * Keeping requests off the tree leaves every tree walker looking at + * grants alone. * * The record says nothing about the page cache under a range. What is * cached there, and whether the server has seen it, is what the page @@ -47,21 +36,6 @@ */ #define FUSE_DLM_GRANT_RETRIES 16 -/* Lifecycle of a range; see the file comment above */ -enum fuse_dlm_range_state { - /* FUSE_DLM_WB_LOCK in flight, on cache->pending */ - FUSE_DLM_RANGE_REQUESTED, - /* - * On cache->pending, revoked in flight: the grant must not be - * recorded. Never in cache->ranges, which holds grants only. - */ - FUSE_DLM_RANGE_REVOKED, - /* Granted shared, in cache->ranges */ - FUSE_DLM_RANGE_READ, - /* Granted exclusive, in cache->ranges */ - FUSE_DLM_RANGE_WRITE, -}; - /* A range of pages with a lock */ struct fuse_dlm_range { /* Interval tree node; only linked once granted */ @@ -75,34 +49,26 @@ struct fuse_dlm_range { uint64_t end; /* Subtree end value for interval tree */ uint64_t __subtree_end; - /* Lifecycle and, once granted, the mode; see the enum above */ - enum fuse_dlm_range_state state; + /* The mode a grant in cache->ranges is held in */ + enum fuse_page_lock_mode mode; + /* A revoke overlapped this request in flight; cache->pending only */ + bool killed; /* Temporary list entry for operations, and the cache->pending link */ struct list_head list; }; -/* The state a grant in @mode is recorded under */ -static inline enum fuse_dlm_range_state -fuse_dlm_granted_state(enum fuse_page_lock_mode mode) -{ - return mode == FUSE_PAGE_LOCK_READ ? FUSE_DLM_RANGE_READ : - FUSE_DLM_RANGE_WRITE; -} - /** - * fuse_dlm_state_satisfies - is a range in @held usable for @want - * @held: the state of a range recorded in the tree - * @want: FUSE_DLM_RANGE_READ or FUSE_DLM_RANGE_WRITE + * fuse_dlm_mode_satisfies - is a grant in @held usable for @want + * @held: the mode a range in the tree is held in + * @want: the mode being asked for * - * A WRITE grant is exclusive and so covers a READ request; nothing else - * substitutes for anything. The two pending states never appear in the - * tree and cover nothing. + * A WRITE grant is exclusive and so covers a READ request; a READ grant + * covers only READ. */ -static inline bool fuse_dlm_state_satisfies(enum fuse_dlm_range_state held, - enum fuse_dlm_range_state want) +static inline bool fuse_dlm_mode_satisfies(enum fuse_page_lock_mode held, + enum fuse_page_lock_mode want) { - return held == want || - (held == FUSE_DLM_RANGE_WRITE && want == FUSE_DLM_RANGE_READ); + return held == want || held == FUSE_PAGE_LOCK_WRITE; } /* Interval tree definitions for page ranges */ @@ -144,7 +110,7 @@ static void fuse_dlm_kill_pending(struct fuse_dlm_cache *cache, list_for_each_entry(req, &cache->pending, list) if (req->start <= end && start <= req->end) - req->state = FUSE_DLM_RANGE_REVOKED; + req->killed = true; } /** @@ -224,7 +190,7 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, } /* Merge neighbours the server has given us on the same terms */ - if (next && range->state == next->state && + if (next && range->mode == next->mode && range->end + 1 == next->start) { /* Merge ranges: re-insert so __subtree_end is updated */ fuse_page_it_remove(next, &cache->ranges); @@ -256,9 +222,6 @@ static void fuse_dlm_try_merge(struct fuse_dlm_cache *cache, uint64_t start, * - READ locks are compatible with existing WRITE locks (downgrade not needed) * - WRITE locks need to upgrade existing READ locks * - * Everything inserted here is READ or WRITE: this runs only after the - * server has answered. - * * Caller holds the cache lock for write. * * Return: 0 on success, negative error code on failure @@ -269,7 +232,6 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range, *new_range, *next; - enum fuse_dlm_range_state want; bool covered_to_end = false; int ret = 0; LIST_HEAD(to_lock); @@ -279,9 +241,6 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, if (start > end) return -EINVAL; - /* The state this grant records */ - want = fuse_dlm_granted_state(mode); - /* * Ranges are upgraded whole below, so split at the grant bounds * first: a range extending past the grant would otherwise have its @@ -300,10 +259,8 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, next = fuse_page_it_iter_next(range, start, end); /* A read range needs upgrading when a write is granted */ - if (want == FUSE_DLM_RANGE_WRITE && - range->state != FUSE_DLM_RANGE_WRITE) + if (!fuse_dlm_mode_satisfies(range->mode, mode)) list_add_tail(&range->list, &to_upgrade); - /* If WRITE lock already exists - nothing to do */ /* If there's a gap before this range, we need to add the missing range */ if (current_start < range->start) { @@ -315,7 +272,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = range->start - 1; - new_range->state = want; + new_range->mode = mode; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); @@ -341,15 +298,15 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, new_range->start = current_start; new_range->end = end; - new_range->state = want; + new_range->mode = mode; INIT_LIST_HEAD(&new_range->list); list_add_tail(&new_range->list, &to_lock); } - /* Everything on this list is now covered in @want */ + /* Everything on this list is now covered in @mode */ list_for_each_entry(range, &to_upgrade, list) - range->state = want; + range->mode = mode; /* Add all new ranges to the tree */ list_for_each_entry(new_range, &to_lock, list) { @@ -410,8 +367,8 @@ void fuse_dlm_request_begin(struct fuse_inode *inode, RB_CLEAR_NODE(&req->rb); req->start = start; req->end = end; - req->state = FUSE_DLM_RANGE_REQUESTED; - /* Nothing reads this while the request is pending; publish it set */ + req->killed = false; + /* Nothing reads the mode while the request is pending */ down_write(&cache->lock); list_add_tail(&req->list, &cache->pending); @@ -445,7 +402,7 @@ int fuse_dlm_request_commit(struct fuse_inode *inode, down_write(&cache->lock); list_del(&req->list); - revoked = req->state == FUSE_DLM_RANGE_REVOKED; + revoked = req->killed; if (!revoked) ret = fuse_dlm_lock_range_locked(inode, start, end, mode); up_write(&cache->lock); @@ -475,7 +432,7 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, * @cache: The page cache * @off: byte offset to split at * - * Splits the range containing @off in two, both halves keeping the state + * Splits the range containing @off in two, both halves keeping the mode * of the original, so a revoke can apply to one side only. A no-op when * @off already starts a range or falls in a gap. * @@ -585,16 +542,12 @@ static bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, { struct fuse_dlm_cache *cache = &inode->dlm_locked_areas; struct fuse_dlm_range *range; - enum fuse_dlm_range_state want; uint64_t current_start = start; bool covered = false; if (start > end) return false; - /* The state a range has to be in to cover this request */ - want = fuse_dlm_granted_state(mode); - down_read(&cache->lock); for (range = fuse_page_it_iter_first(&cache->ranges, start, end); range; @@ -606,7 +559,7 @@ static bool fuse_dlm_range_is_locked(struct fuse_inode *inode, uint64_t start, * which keeps a read-after-write from asking again. */ if (current_start < range->start || - !fuse_dlm_state_satisfies(range->state, want)) + !fuse_dlm_mode_satisfies(range->mode, mode)) break; if (range->end >= end) { diff --git a/fs/fuse/fuse_dlm_cache.h b/fs/fuse/fuse_dlm_cache.h index 88627af36eb5de..98cc834d76521e 100644 --- a/fs/fuse/fuse_dlm_cache.h +++ b/fs/fuse/fuse_dlm_cache.h @@ -33,8 +33,7 @@ enum fuse_page_lock_mode { FUSE_PAGE_LOCK_READ, FUSE_PAGE_LOCK_WRITE }; * * @ranges holds the grants the client has been given. A request still on * the wire covers nothing and lives on @pending instead, so tree walkers - * never filter on state. See enum fuse_dlm_range_state in - * fuse_dlm_cache.c. + * see grants only. See struct fuse_dlm_range in fuse_dlm_cache.c. */ struct fuse_dlm_cache { /* Lock protecting the tree and the pending list */ @@ -42,9 +41,8 @@ struct fuse_dlm_cache { /* Interval tree of the grants held */ struct rb_root_cached ranges; /* - * FUSE_DLM_WB_LOCK requests in flight (REQUESTED, or REVOKED once - * a revoke has overlapped one). Owned by the queueing thread; the - * revoke paths mark them only. + * FUSE_DLM_WB_LOCK requests in flight. Owned by the queueing + * thread; the revoke paths only mark them killed. */ struct list_head pending; }; From 77be6bef0b765387e9b668d3e006cef41f613554 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:50:08 +0200 Subject: [PATCH 40/41] fuse: allocate a DLM range in one place The gap before an overlapping range and the gap after the last one built the same record twice. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index cae9b77499b717..efbe952b273d9c 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -88,6 +88,23 @@ INTERVAL_TREE_DEFINE(struct fuse_dlm_range, rb, uint64_t, __subtree_end, static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off); +/* A grant record for [start, end] held in @mode, not yet in the tree */ +static struct fuse_dlm_range *fuse_dlm_range_new(uint64_t start, uint64_t end, + enum fuse_page_lock_mode mode) +{ + struct fuse_dlm_range *range = kmalloc(sizeof(*range), GFP_NOFS); + + if (!range) + return NULL; + + range->start = start; + range->end = end; + range->mode = mode; + INIT_LIST_HEAD(&range->list); + + return range; +} + /** * fuse_dlm_kill_pending - mark in-flight requests overlapping [start, end] * @cache: The page cache @@ -264,17 +281,13 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* If there's a gap before this range, we need to add the missing range */ if (current_start < range->start) { - new_range = kmalloc(sizeof(*new_range), GFP_NOFS); + new_range = fuse_dlm_range_new(current_start, + range->start - 1, mode); if (!new_range) { ret = -ENOMEM; goto out_free; } - new_range->start = current_start; - new_range->end = range->start - 1; - new_range->mode = mode; - INIT_LIST_HEAD(&new_range->list); - list_add_tail(&new_range->list, &to_lock); } @@ -290,17 +303,12 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, /* If there's a gap after the last range to the end, extend the range */ if (!covered_to_end && current_start <= end) { - new_range = kmalloc(sizeof(*new_range), GFP_NOFS); + new_range = fuse_dlm_range_new(current_start, end, mode); if (!new_range) { ret = -ENOMEM; goto out_free; } - new_range->start = current_start; - new_range->end = end; - new_range->mode = mode; - INIT_LIST_HEAD(&new_range->list); - list_add_tail(&new_range->list, &to_lock); } From fa698ae78e14c4404367b6d9d9b14f38cdf3d50a Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Fri, 28 Aug 2026 20:51:26 +0200 Subject: [PATCH 41/41] fuse: split the DLM tree at both bounds in one call Recording a grant and revoking one both need every overlapping range to lie inside the region, and both spelled out the same pair of splits with the same U64_MAX guard. Name it, and with fuse_dlm_split_at() moved above its callers the forward declaration goes. Signed-off-by: Horst Birthelmer --- fs/fuse/fuse_dlm_cache.c | 109 +++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/fs/fuse/fuse_dlm_cache.c b/fs/fuse/fuse_dlm_cache.c index efbe952b273d9c..a953ae8f1b16ef 100644 --- a/fs/fuse/fuse_dlm_cache.c +++ b/fs/fuse/fuse_dlm_cache.c @@ -83,10 +83,63 @@ static inline uint64_t fuse_dlm_range_last(struct fuse_dlm_range *range) } INTERVAL_TREE_DEFINE(struct fuse_dlm_range, rb, uint64_t, __subtree_end, - fuse_dlm_range_start, fuse_dlm_range_last, static, - fuse_page_it); + fuse_dlm_range_start, fuse_dlm_range_last, static, + fuse_page_it); -static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off); +/** + * fuse_dlm_split_at - make @off start a range + * @cache: The page cache + * @off: byte offset to split at + * + * Splits the range containing @off in two, both halves keeping the mode + * of the original, so a revoke can apply to one side only. A no-op when + * @off already starts a range or falls in a gap. + * + * Caller holds @cache->lock for write. + * + * Cannot fail: the split decides which bytes a caller goes on to name, + * and both naming more than was written and lowering more than was sent + * lose data. iomap allocates the state it keeps per folio the same way. + */ +static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) +{ + struct fuse_dlm_range *range, *tail; + + if (!off) + return; + + range = fuse_page_it_iter_first(&cache->ranges, off, off); + if (!range || range->start == off) + return; + + tail = kmalloc(sizeof(*tail), GFP_NOFS | __GFP_NOFAIL); + + *tail = *range; + INIT_LIST_HEAD(&tail->list); + tail->start = off; + + /* + * Bounds are never edited in place: the interval tree caches a + * subtree end that only insertion recomputes. + */ + fuse_page_it_remove(range, &cache->ranges); + range->end = off - 1; + fuse_page_it_insert(range, &cache->ranges); + fuse_page_it_insert(tail, &cache->ranges); +} + +/* + * Make @start and @end + 1 range bounds, so every range overlapping + * [start, end] lies wholly inside it and can be revoked or upgraded + * whole. Caller holds @cache->lock for write. + */ +static void fuse_dlm_split_bounds(struct fuse_dlm_cache *cache, uint64_t start, + uint64_t end) +{ + fuse_dlm_split_at(cache, start); + if (end < U64_MAX) + fuse_dlm_split_at(cache, end + 1); +} /* A grant record for [start, end] held in @mode, not yet in the tree */ static struct fuse_dlm_range *fuse_dlm_range_new(uint64_t start, uint64_t end, @@ -265,9 +318,7 @@ static int fuse_dlm_lock_range_locked(struct fuse_inode *inode, uint64_t start, * never gave, and a read range half-covered by a write grant would * report the other half held for write. */ - fuse_dlm_split_at(cache, start); - if (end < U64_MAX) - fuse_dlm_split_at(cache, end + 1); + fuse_dlm_split_bounds(cache, start, end); /* Find all ranges that overlap with [start, end] */ range = fuse_page_it_iter_first(&cache->ranges, start, end); @@ -435,48 +486,6 @@ void fuse_dlm_request_abort(struct fuse_inode *inode, up_write(&cache->lock); } -/** - * fuse_dlm_split_at - make @off start a range - * @cache: The page cache - * @off: byte offset to split at - * - * Splits the range containing @off in two, both halves keeping the mode - * of the original, so a revoke can apply to one side only. A no-op when - * @off already starts a range or falls in a gap. - * - * Caller holds @cache->lock for write. - * - * Cannot fail: the split decides which bytes a caller goes on to name, - * and both naming more than was written and lowering more than was sent - * lose data. iomap allocates the state it keeps per folio the same way. - */ -static void fuse_dlm_split_at(struct fuse_dlm_cache *cache, uint64_t off) -{ - struct fuse_dlm_range *range, *tail; - - if (!off) - return; - - range = fuse_page_it_iter_first(&cache->ranges, off, off); - if (!range || range->start == off) - return; - - tail = kmalloc(sizeof(*tail), GFP_NOFS | __GFP_NOFAIL); - - *tail = *range; - INIT_LIST_HEAD(&tail->list); - tail->start = off; - - /* - * Bounds are never edited in place: the interval tree caches a - * subtree end that only insertion recomputes. - */ - fuse_page_it_remove(range, &cache->ranges); - range->end = off - 1; - fuse_page_it_insert(range, &cache->ranges); - fuse_page_it_insert(tail, &cache->ranges); -} - /** * fuse_dlm_unlock_range - Revoke the grants over a range of pages * @inode: The fuse inode @@ -516,9 +525,7 @@ int fuse_dlm_unlock_range(struct fuse_inode *inode, uint64_t start, fuse_dlm_kill_pending(cache, start, end); /* Split so the revoked region has its own ranges */ - fuse_dlm_split_at(cache, start); - if (end < U64_MAX) - fuse_dlm_split_at(cache, end + 1); + fuse_dlm_split_bounds(cache, start, end); range = fuse_page_it_iter_first(&cache->ranges, start, end); while (range) {