From 25dde935988bc0cc2991cedbea90a2ce65066d54 Mon Sep 17 00:00:00 2001 From: Trond Myklebust Date: Fri, 11 Sep 2026 11:00:02 -0400 Subject: [PATCH 01/68] NFSv4: Fix a regression in the handling of CB_RECALL_ANY When the flag NFS_DELEGATION_RETURN was replaced by the server->delegations_return list, the test in nfs_mark_return_if_closed_delegation() was replaced by one that stops all further marking of delegations for return when closed as soon as the server->delegations_return list is non-empty, breaking CB_RECALL_ANY behaviour. While the intention at the time was probably to check if delegation->entry was non-empty in order to emulate the effect of the NFS_DELEGATION_RETURN test, that is no longer an option, since delegation->entry is also used for the LRU list. By just removing the offending check altogether, the patch can end up moving delegations from the server->delegations_delayed list prematurely, triggering an early retry. However that should be mostly harmless behaviour. Fixes: 0ebe655bd033 ("NFS: add a separate delegation return list") Cc: stable@vger.kernel.org Reviewed-by: Christoph Hellwig Signed-off-by: Trond Myklebust --- fs/nfs/delegation.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/nfs/delegation.c b/fs/nfs/delegation.c index ab3e441464a6..ad774316f716 100644 --- a/fs/nfs/delegation.c +++ b/fs/nfs/delegation.c @@ -934,8 +934,7 @@ nfs_mark_return_if_closed_delegation(struct nfs_server *server, { struct inode *inode; - if (!list_empty_careful(&server->delegations_return) || - test_bit(NFS_DELEGATION_RETURN_IF_CLOSED, &delegation->flags)) + if (test_bit(NFS_DELEGATION_RETURN_IF_CLOSED, &delegation->flags)) return; spin_lock(&delegation->lock); inode = delegation->inode; From a0db57fee35ece5603f852df1b665e01e0556f65 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Wed, 26 Aug 2026 10:34:15 -0400 Subject: [PATCH 02/68] xprtrdma: Fix Write chunk payload corruption on a short reply header An NFSv3 server may return READ3resok with attributes_follow set to FALSE. The client has already reserved enough head iovec space for the largest possible reply header, so such a reply falls 84 bytes short of that estimate. rpcrdma_inline_fixup() points the head iovec at the received message but leaves its length at the reserved size, so it runs past the header into stale receive buffer content. xdr_realign_pages() then shifts those bytes into the page list, and the READ returns transport data where READ payload belongs. nfs3_read_done() records the shorter header size, so on an ordinary NFSv3 mount, only the first READ is corrupted. However, the FlexFiles layout driver installs its own completion callback, so every READ from a data server is corrupt. To address this, reduce the head iovec to the header bytes actually received when a Write chunk carries the READ payload. An auth flavor that transforms the payload cannot use a Write chunk, so scoping it to Write chunks leaves krb5[ip] alone. Fixes: cb0ae1fbb2f5 ("xprtrdma: Do not update {head, tail}.iov_len in rpcrdma_inline_fixup()") Reported-by: Rakesh Bhargava Closes: https://lore.kernel.org/linux-nfs/SA6PR06MB104695205126871E29444567A8BA02@SA6PR06MB10469.namprd06.prod.outlook.com/ Signed-off-by: Chuck Lever Signed-off-by: Trond Myklebust --- net/sunrpc/xprtrdma/rpc_rdma.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/net/sunrpc/xprtrdma/rpc_rdma.c b/net/sunrpc/xprtrdma/rpc_rdma.c index 1285f04cdac1..ba8bdca8a49f 100644 --- a/net/sunrpc/xprtrdma/rpc_rdma.c +++ b/net/sunrpc/xprtrdma/rpc_rdma.c @@ -1243,9 +1243,19 @@ rpcrdma_decode_msg(struct rpcrdma_xprt *r_xprt, struct rpcrdma_rep *rep, /* Build the RPC reply's Payload stream in rqst->rq_rcv_buf */ base = (char *)xdr_inline_decode(xdr, 0); - rpclen = xdr_stream_remaining(xdr); + rpclen = rep->rr_hdrbuf.len - xdr_stream_pos(xdr); + if (writelist) + rpclen &= ~3; + if (unlikely(!rpclen)) + return -EIO; + if (writelist && rpclen < rqst->rq_rcv_buf.head[0].iov_len) { + rqst->rq_rcv_buf.head[0].iov_len = rpclen; + rqst->rq_private_buf.head[0].iov_len = rpclen; + } + r_xprt->rx_stats.fixup_copy_count += - rpcrdma_inline_fixup(rqst, base, rpclen, writelist & 3); + rpcrdma_inline_fixup(rqst, base, rpclen, + xdr_pad_size(writelist)); r_xprt->rx_stats.total_rdma_reply += writelist; return rpclen + xdr_align_size(writelist); From 72233aabc78cd0415c850641bc2a6e4fea23173f Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Wed, 16 Sep 2026 16:43:36 -0400 Subject: [PATCH 03/68] NFS: use TCP for MOUNT when xprtsec= selects TLS An NFSv2 or NFSv3 text mount that specifies xprtsec= but no mountproto= crashes in nfs_init_timeout_values(): RIP: 0010:nfs_init_timeout_values+0x10a/0x110 [nfs] Call Trace: nfs_mount+0x1c3/0x330 [nfs] nfs_try_get_tree+0x1e5/0x460 [nfs] nfs_get_tree+0x64d/0x6d0 [nfs] vfs_get_tree+0x2b/0xe0 vfs_cmd_create+0x5f/0xd0 vfs_fsconfig_locked+0x50/0x130 __se_sys_fsconfig+0x2f9/0x3a0 nfs_validate_transport_protocol() sets nfs_server.protocol to XPRT_TRANSPORT_TCP_TLS when xprtsec= is present, but the switch in nfs_set_mount_transport_protocol() has no case for that value. mount_server.protocol stays zero, and nfs_init_timeout_values() calls BUG() on a transport it does not recognize. mount.nfs always appends mountproto= after probing the server's rpcbind, so only a caller that bypasses it, such as "mount -i" or a program that drives fsconfig() directly, reaches the crash. Select TCP for MOUNT when the NFS transport is XPRT_TRANSPORT_TCP_TLS. nfs_mount() requests no transport security, so MOUNT already runs over plain TCP whenever mount.nfs supplies mountproto=tcp. Fixes: c8407f2e560c ("NFS: Add an "xprtsec=" NFS mount option") Signed-off-by: Chuck Lever Reviewed-by: Prabhakar Pujeri Signed-off-by: Trond Myklebust --- fs/nfs/fs_context.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/nfs/fs_context.c b/fs/nfs/fs_context.c index 1967de7d1dff..a7393ff05494 100644 --- a/fs/nfs/fs_context.c +++ b/fs/nfs/fs_context.c @@ -402,6 +402,7 @@ static void nfs_set_mount_transport_protocol(struct nfs_fs_context *ctx) ctx->mount_server.protocol = XPRT_TRANSPORT_UDP; break; case XPRT_TRANSPORT_TCP: + case XPRT_TRANSPORT_TCP_TLS: case XPRT_TRANSPORT_RDMA: ctx->mount_server.protocol = XPRT_TRANSPORT_TCP; } From 211cda4fdf33011cee39f8970c4625b207142d6b Mon Sep 17 00:00:00 2001 From: Jimmy Zuber Date: Tue, 22 Sep 2026 17:56:56 +0000 Subject: [PATCH 04/68] NFS: Fix stale negative cache on failed nfs_mkdir() Since commit 8376583b84a1 ("nfs: change mkdir inode_operation to return alternate dentry if needed"), nfs_mkdir() no longer drops the dentry on RPC failure. A negative cache value can then result in spurious ENOENT results for further operations within that directory, such as open or stat. We observed this breaking some applications where an EEXIST on mkdir caused the application to assume the directory to exist, only to fail operations within that directory due to the retained negative cache entry. The other create paths, nfs_create() and nfs_mknod(), still drop the dentry on failure. This patch restores the previous behavior. Fixes: 8376583b84a1 ("nfs: change mkdir inode_operation to return alternate dentry if needed") Cc: stable@vger.kernel.org # v6.15+ Signed-off-by: Jimmy Zuber Reviewed-by: NeilBrown Signed-off-by: Trond Myklebust --- fs/nfs/dir.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c index 49394123bd09..a26c459ffb04 100644 --- a/fs/nfs/dir.c +++ b/fs/nfs/dir.c @@ -2490,6 +2490,8 @@ struct dentry *nfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, trace_nfs_mkdir_enter(dir, dentry); ret = NFS_PROTO(dir)->mkdir(dir, dentry, &attr); trace_nfs_mkdir_exit(dir, dentry, PTR_ERR_OR_ZERO(ret)); + if (IS_ERR(ret)) + d_drop(dentry); return ret; } EXPORT_SYMBOL_GPL(nfs_mkdir); From d91ae903f4764da43f570b8228de7ab2f79591e1 Mon Sep 17 00:00:00 2001 From: Pranjal Shrivastava Date: Fri, 14 Aug 2026 14:32:51 +0000 Subject: [PATCH 05/68] nfs: make nfs_page pin-aware Modernizing the NFS Direct I/O path to use iov_iter_extract_pages() introduces page pinning (GUP) instead of standard page referencing. To handle this correctly, nfs_page must track whether it holds a pin or a standard reference. Introduce a new flag, PG_PINNED, to struct nfs_page. Update the creation path (nfs_page_create_from_page and nfs_page_create_from_folio) to accept a pinned bool and set the flag accordingly. If the page is pinned, we skip the existing reference increment (get_page/folio_get) as the pin itself acts as a reference. Update nfs_clear_request() & nfs_direct_release_pages() to use unpin_user_page() or unpin_user_folio() instead of only refcount decrement (put_page) when PG_PINNED flag is set. Finally, ensure subrequests inherit the pinning status from their parent request. Signed-off-by: Pranjal Shrivastava Reviewed-by: Christoph Hellwig Signed-off-by: Anna Schumaker --- fs/nfs/direct.c | 22 +++++++++++++------- fs/nfs/pagelist.c | 44 +++++++++++++++++++++++++++++++--------- fs/nfs/read.c | 2 +- fs/nfs/write.c | 2 +- include/linux/nfs_page.h | 3 +++ 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/fs/nfs/direct.c b/fs/nfs/direct.c index ccafdc1ce64d..6a7f487fd43e 100644 --- a/fs/nfs/direct.c +++ b/fs/nfs/direct.c @@ -145,11 +145,17 @@ static void nfs_direct_file_adjust_size_locked(struct inode *inode, } } -static void nfs_direct_release_pages(struct page **pages, unsigned int npages) +static void nfs_direct_release_pages(struct page **pages, unsigned int npages, + bool pinned) { unsigned int i; - for (i = 0; i < npages; i++) - put_page(pages[i]); + + if (pinned) { + unpin_user_pages(pages, npages); + } else { + for (i = 0; i < npages; i++) + put_page(pages[i]); + } } void nfs_init_cinfo_from_dreq(struct nfs_commit_info *cinfo, @@ -351,7 +357,8 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, unsigned int req_len = min_t(size_t, bytes, PAGE_SIZE - pgbase); /* XXX do we need to do the eof zeroing found in async_filler? */ req = nfs_page_create_from_page(dreq->ctx, pagevec[i], - pgbase, pos, req_len); + false, pgbase, pos, + req_len); if (IS_ERR(req)) { result = PTR_ERR(req); break; @@ -366,7 +373,7 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, requested_bytes += req_len; pos += req_len; } - nfs_direct_release_pages(pagevec, npages); + nfs_direct_release_pages(pagevec, npages, false); kvfree(pagevec); if (result < 0) break; @@ -887,7 +894,8 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, unsigned int req_len = min_t(size_t, bytes, PAGE_SIZE - pgbase); req = nfs_page_create_from_page(dreq->ctx, pagevec[i], - pgbase, pos, req_len); + false, pgbase, pos, + req_len); if (IS_ERR(req)) { result = PTR_ERR(req); break; @@ -930,7 +938,7 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, desc.pg_error = 0; defer = true; } - nfs_direct_release_pages(pagevec, npages); + nfs_direct_release_pages(pagevec, npages, false); kvfree(pagevec); if (result < 0) break; diff --git a/fs/nfs/pagelist.c b/fs/nfs/pagelist.c index 7dd478ffc2fa..a562cfe2a126 100644 --- a/fs/nfs/pagelist.c +++ b/fs/nfs/pagelist.c @@ -404,20 +404,28 @@ static struct nfs_page *nfs_page_create(struct nfs_lock_context *l_ctx, return req; } -static void nfs_page_assign_folio(struct nfs_page *req, struct folio *folio) +static void nfs_page_assign_folio(struct nfs_page *req, struct folio *folio, + bool pinned) { if (folio != NULL) { req->wb_folio = folio; - folio_get(folio); + if (pinned) + set_bit(PG_PINNED, &req->wb_flags); + else + folio_get(folio); set_bit(PG_FOLIO, &req->wb_flags); } } -static void nfs_page_assign_page(struct nfs_page *req, struct page *page) +static void nfs_page_assign_page(struct nfs_page *req, struct page *page, + bool pinned) { if (page != NULL) { req->wb_page = page; - get_page(page); + if (pinned) + set_bit(PG_PINNED, &req->wb_flags); + else + get_page(page); } } @@ -425,6 +433,7 @@ static void nfs_page_assign_page(struct nfs_page *req, struct page *page) * nfs_page_create_from_page - Create an NFS read/write request. * @ctx: open context to use * @page: page to write + * @pinned: true if page is pinned * @pgbase: starting offset within the page for the write * @offset: file offset for the write * @count: number of bytes to read/write @@ -435,6 +444,7 @@ static void nfs_page_assign_page(struct nfs_page *req, struct page *page) */ struct nfs_page *nfs_page_create_from_page(struct nfs_open_context *ctx, struct page *page, + bool pinned, unsigned int pgbase, loff_t offset, unsigned int count) { @@ -446,7 +456,7 @@ struct nfs_page *nfs_page_create_from_page(struct nfs_open_context *ctx, ret = nfs_page_create(l_ctx, pgbase, offset >> PAGE_SHIFT, offset_in_page(offset), count); if (!IS_ERR(ret)) { - nfs_page_assign_page(ret, page); + nfs_page_assign_page(ret, page, pinned); nfs_page_group_init(ret, NULL); } nfs_put_lock_context(l_ctx); @@ -457,6 +467,7 @@ struct nfs_page *nfs_page_create_from_page(struct nfs_open_context *ctx, * nfs_page_create_from_folio - Create an NFS read/write request. * @ctx: open context to use * @folio: folio to write + * @pinned: true if folio is pinned * @offset: starting offset within the folio for the write * @count: number of bytes to read/write * @@ -466,6 +477,7 @@ struct nfs_page *nfs_page_create_from_page(struct nfs_open_context *ctx, */ struct nfs_page *nfs_page_create_from_folio(struct nfs_open_context *ctx, struct folio *folio, + bool pinned, unsigned int offset, unsigned int count) { @@ -476,7 +488,7 @@ struct nfs_page *nfs_page_create_from_folio(struct nfs_open_context *ctx, return ERR_CAST(l_ctx); ret = nfs_page_create(l_ctx, offset, folio->index, offset, count); if (!IS_ERR(ret)) { - nfs_page_assign_folio(ret, folio); + nfs_page_assign_folio(ret, folio, pinned); nfs_page_group_init(ret, NULL); } nfs_put_lock_context(l_ctx); @@ -498,9 +510,11 @@ nfs_create_subreq(struct nfs_page *req, offset, count); if (!IS_ERR(ret)) { if (folio) - nfs_page_assign_folio(ret, folio); + nfs_page_assign_folio(ret, folio, + test_bit(PG_PINNED, &req->wb_flags)); else - nfs_page_assign_page(ret, page); + nfs_page_assign_page(ret, page, + test_bit(PG_PINNED, &req->wb_flags)); /* find the last request */ for (last = req->wb_head; last->wb_this_page != req->wb_head; @@ -552,11 +566,21 @@ static void nfs_clear_request(struct nfs_page *req) struct nfs_open_context *ctx; if (folio != NULL) { - folio_put(folio); + if (test_and_clear_bit(PG_PINNED, &req->wb_flags)) { + if (req == req->wb_head) + unpin_user_folio(folio, 1); + } else { + folio_put(folio); + } req->wb_folio = NULL; clear_bit(PG_FOLIO, &req->wb_flags); } else if (page != NULL) { - put_page(page); + if (test_and_clear_bit(PG_PINNED, &req->wb_flags)) { + if (req == req->wb_head) + unpin_user_page(page); + } else { + put_page(page); + } req->wb_page = NULL; } if (l_ctx != NULL) { diff --git a/fs/nfs/read.c b/fs/nfs/read.c index 2b70bd2b934b..e7497b029d6c 100644 --- a/fs/nfs/read.c +++ b/fs/nfs/read.c @@ -324,7 +324,7 @@ int nfs_read_add_folio(struct nfs_pageio_descriptor *pgio, aligned_len = min_t(unsigned int, ALIGN(len, rsize), fsize); - new = nfs_page_create_from_folio(ctx, folio, 0, aligned_len); + new = nfs_page_create_from_folio(ctx, folio, false, 0, aligned_len); if (IS_ERR(new)) { error = PTR_ERR(new); if (nfs_netfs_folio_unlock(folio)) diff --git a/fs/nfs/write.c b/fs/nfs/write.c index 623e7ef1f73d..7f173dbbeb9d 100644 --- a/fs/nfs/write.c +++ b/fs/nfs/write.c @@ -1089,7 +1089,7 @@ static struct nfs_page *nfs_setup_write_request(struct nfs_open_context *ctx, req = nfs_try_to_update_request(folio, offset, bytes); if (req != NULL) goto out; - req = nfs_page_create_from_folio(ctx, folio, offset, bytes); + req = nfs_page_create_from_folio(ctx, folio, false, offset, bytes); if (IS_ERR(req)) goto out; nfs_inode_add_request(req); diff --git a/include/linux/nfs_page.h b/include/linux/nfs_page.h index 4b9a35dbc062..fd7aafe7cb54 100644 --- a/include/linux/nfs_page.h +++ b/include/linux/nfs_page.h @@ -38,6 +38,7 @@ enum { PG_REMOVE, /* page group sync bit in write path */ PG_CONTENDED1, /* Is someone waiting for a lock? */ PG_CONTENDED2, /* Is someone waiting for a lock? */ + PG_PINNED, /* page is pinned by GUP */ }; struct nfs_inode; @@ -125,11 +126,13 @@ struct nfs_pageio_descriptor { extern struct nfs_page *nfs_page_create_from_page(struct nfs_open_context *ctx, struct page *page, + bool pinned, unsigned int pgbase, loff_t offset, unsigned int count); extern struct nfs_page *nfs_page_create_from_folio(struct nfs_open_context *ctx, struct folio *folio, + bool pinned, unsigned int offset, unsigned int count); extern void nfs_release_request(struct nfs_page *); From 00ba5f7320d0416a38fda4c7ab3382a2eaed996a Mon Sep 17 00:00:00 2001 From: Pranjal Shrivastava Date: Fri, 14 Aug 2026 14:32:52 +0000 Subject: [PATCH 06/68] nfs: track number of pinned pages in nfs_page Track the number of pinned pages in nfs_page to handle unpinning correctly, ensuring that only primary requests perform the final unpinning operation, preventing subrequests from incorrectly performing unpinning on behalf of their parent requests. Add wb_nr_pinned to struct nfs_page to store the count of pinned pages owned by the request. Update request creation and cleanup helpers to initialize and use wb_nr_pinned for primary requests. Use the nfs_page_array_len() helper to calculate the number of pages spanned by a request's offset and length. Signed-off-by: Pranjal Shrivastava Reviewed-by: Christoph Hellwig Signed-off-by: Anna Schumaker --- fs/nfs/pagelist.c | 13 +++++++++---- include/linux/nfs_page.h | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/fs/nfs/pagelist.c b/fs/nfs/pagelist.c index a562cfe2a126..b9ccf2a87e3c 100644 --- a/fs/nfs/pagelist.c +++ b/fs/nfs/pagelist.c @@ -457,6 +457,8 @@ struct nfs_page *nfs_page_create_from_page(struct nfs_open_context *ctx, offset_in_page(offset), count); if (!IS_ERR(ret)) { nfs_page_assign_page(ret, page, pinned); + if (pinned) + ret->wb_nr_pinned = 1; nfs_page_group_init(ret, NULL); } nfs_put_lock_context(l_ctx); @@ -489,6 +491,9 @@ struct nfs_page *nfs_page_create_from_folio(struct nfs_open_context *ctx, ret = nfs_page_create(l_ctx, offset, folio->index, offset, count); if (!IS_ERR(ret)) { nfs_page_assign_folio(ret, folio, pinned); + if (pinned) + ret->wb_nr_pinned = nfs_page_array_len(offset_in_page(offset), + count); nfs_page_group_init(ret, NULL); } nfs_put_lock_context(l_ctx); @@ -567,8 +572,8 @@ static void nfs_clear_request(struct nfs_page *req) if (folio != NULL) { if (test_and_clear_bit(PG_PINNED, &req->wb_flags)) { - if (req == req->wb_head) - unpin_user_folio(folio, 1); + if (req->wb_nr_pinned > 0) + unpin_user_folio(folio, req->wb_nr_pinned); } else { folio_put(folio); } @@ -576,8 +581,8 @@ static void nfs_clear_request(struct nfs_page *req) clear_bit(PG_FOLIO, &req->wb_flags); } else if (page != NULL) { if (test_and_clear_bit(PG_PINNED, &req->wb_flags)) { - if (req == req->wb_head) - unpin_user_page(page); + if (req->wb_nr_pinned > 0) + unpin_user_pages(&page, req->wb_nr_pinned); } else { put_page(page); } diff --git a/include/linux/nfs_page.h b/include/linux/nfs_page.h index fd7aafe7cb54..080fa3e23580 100644 --- a/include/linux/nfs_page.h +++ b/include/linux/nfs_page.h @@ -59,6 +59,7 @@ struct nfs_page { struct nfs_page *wb_this_page; /* list of reqs for this page */ struct nfs_page *wb_head; /* head pointer for req list */ unsigned short wb_nio; /* Number of I/O attempts */ + unsigned int wb_nr_pinned; /* Number of pinned pages */ }; struct nfs_pgio_mirror; From 43280750bc5aa691d0afe4d68bdae87b75d3ac58 Mon Sep 17 00:00:00 2001 From: Pranjal Shrivastava Date: Fri, 14 Aug 2026 14:32:53 +0000 Subject: [PATCH 07/68] nfs: introduce nfs_release_request_list helper Introduce a centralized helper, nfs_release_request_list, to handle the bulk release of nfs_page requests from a list. This serves as a preparatory step for two upcoming improvements: 1. Pin-Aware Cleanup: As we migrate to iov_iter_extract_* API, requests will hold pins (GUP) instead of standard references. The helper ensures that the correct unpinning logic gets applied consistently across all requests in a list. 2. Folio Support: In subsequent patches where nfs_page structures will cover multi-page folios, this helper provides a clean infrastructure to unlock these larger units of I/O in bulk during completion, similar to the pattern in bio_release_pages. Additionally, refactor nfs_read_sync_pgio_error() to utilize this new helper. Signed-off-by: Pranjal Shrivastava Reviewed-by: Shivaji Kant Reviewed-by: Christoph Hellwig Signed-off-by: Anna Schumaker --- fs/nfs/direct.c | 8 +------- fs/nfs/pagelist.c | 17 +++++++++++++++++ include/linux/nfs_page.h | 4 ++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/fs/nfs/direct.c b/fs/nfs/direct.c index 6a7f487fd43e..8b094f8ef087 100644 --- a/fs/nfs/direct.c +++ b/fs/nfs/direct.c @@ -294,13 +294,7 @@ static void nfs_direct_read_completion(struct nfs_pgio_header *hdr) static void nfs_read_sync_pgio_error(struct list_head *head, int error) { - struct nfs_page *req; - - while (!list_empty(head)) { - req = nfs_list_entry(head->next); - nfs_list_remove_request(req); - nfs_release_request(req); - } + nfs_release_request_list(head); } static void nfs_direct_pgio_init(struct nfs_pgio_header *hdr) diff --git a/fs/nfs/pagelist.c b/fs/nfs/pagelist.c index b9ccf2a87e3c..71f0ce2bc4ea 100644 --- a/fs/nfs/pagelist.c +++ b/fs/nfs/pagelist.c @@ -628,6 +628,23 @@ void nfs_release_request(struct nfs_page *req) } EXPORT_SYMBOL_GPL(nfs_release_request); +/* + * nfs_release_request_list - Release a list of NFS read/write requests + * @head: list of requests to release + * + * Removes each request from the list and drops it's refcount. + */ +void nfs_release_request_list(struct list_head *head) +{ + while (!list_empty(head)) { + struct nfs_page *req = nfs_list_entry(head->next); + + nfs_list_remove_request(req); + nfs_release_request(req); + } +} +EXPORT_SYMBOL_GPL(nfs_release_request_list); + /* * nfs_generic_pg_test - determine if requests can be coalesced * @desc: pointer to descriptor diff --git a/include/linux/nfs_page.h b/include/linux/nfs_page.h index 080fa3e23580..c38e4b380be5 100644 --- a/include/linux/nfs_page.h +++ b/include/linux/nfs_page.h @@ -136,8 +136,8 @@ extern struct nfs_page *nfs_page_create_from_folio(struct nfs_open_context *ctx, bool pinned, unsigned int offset, unsigned int count); -extern void nfs_release_request(struct nfs_page *); - +void nfs_release_request(struct nfs_page *req); +void nfs_release_request_list(struct list_head *head); extern void nfs_pageio_init(struct nfs_pageio_descriptor *desc, struct inode *inode, From a2bb86ade0bc14446b3b5872c1513fdd35a15dbf Mon Sep 17 00:00:00 2001 From: Pranjal Shrivastava Date: Fri, 14 Aug 2026 14:32:54 +0000 Subject: [PATCH 08/68] nfs: migrate direct I/O to iov_iter_extract_pages Migrate the NFS Direct I/O path away from the legacy iov_iter_get_pages_alloc2() API to the modern iov_iter_extract_pages API. The transition aligns NFS with the modern VFS extraction model and serves as a preparatory step for supporting requirements such as page pinning via GUP for DMA. The migration fixes a bug in the Direct I/O loop where pages were being unpinned immediately after request creation. With the new extraction model, pins are held until the I/O is complete. Manual release in the loop is correspondingly updated to only clean up failed pages. Signed-off-by: Pranjal Shrivastava Reviewed-by: Shivaji Kant Reviewed-by: Christoph Hellwig Signed-off-by: Anna Schumaker --- fs/nfs/direct.c | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/fs/nfs/direct.c b/fs/nfs/direct.c index 8b094f8ef087..59642e886a36 100644 --- a/fs/nfs/direct.c +++ b/fs/nfs/direct.c @@ -148,14 +148,8 @@ static void nfs_direct_file_adjust_size_locked(struct inode *inode, static void nfs_direct_release_pages(struct page **pages, unsigned int npages, bool pinned) { - unsigned int i; - - if (pinned) { + if (pinned) unpin_user_pages(pages, npages); - } else { - for (i = 0; i < npages; i++) - put_page(pages[i]); - } } void nfs_init_cinfo_from_dreq(struct nfs_commit_info *cinfo, @@ -334,16 +328,17 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, inode_dio_begin(inode); while (iov_iter_count(iter)) { - struct page **pagevec; + struct page **pagevec = NULL; size_t bytes; size_t pgbase; unsigned npages, i; + bool pinned = iov_iter_extract_will_pin(iter); - result = iov_iter_get_pages_alloc2(iter, &pagevec, - rsize, &pgbase); + result = iov_iter_extract_pages(iter, &pagevec, + rsize, ~0U, 0, &pgbase); if (result < 0) break; - + bytes = result; npages = (result + pgbase + PAGE_SIZE - 1) / PAGE_SIZE; for (i = 0; i < npages; i++) { @@ -351,7 +346,7 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, unsigned int req_len = min_t(size_t, bytes, PAGE_SIZE - pgbase); /* XXX do we need to do the eof zeroing found in async_filler? */ req = nfs_page_create_from_page(dreq->ctx, pagevec[i], - false, pgbase, pos, + pinned, pgbase, pos, req_len); if (IS_ERR(req)) { result = PTR_ERR(req); @@ -360,6 +355,7 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, if (!nfs_pageio_add_request(&desc, req)) { result = desc.pg_error; nfs_release_request(req); + i++; break; } pgbase = 0; @@ -367,7 +363,8 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, requested_bytes += req_len; pos += req_len; } - nfs_direct_release_pages(pagevec, npages, false); + if (i < npages) + nfs_direct_release_pages(pagevec + i, npages - i, pinned); kvfree(pagevec); if (result < 0) break; @@ -871,13 +868,14 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, NFS_I(inode)->write_io += iov_iter_count(iter); while (iov_iter_count(iter)) { - struct page **pagevec; + struct page **pagevec = NULL; size_t bytes; size_t pgbase; unsigned npages, i; + bool pinned = iov_iter_extract_will_pin(iter); - result = iov_iter_get_pages_alloc2(iter, &pagevec, - wsize, &pgbase); + result = iov_iter_extract_pages(iter, &pagevec, + wsize, ~0U, 0, &pgbase); if (result < 0) break; @@ -888,7 +886,7 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, unsigned int req_len = min_t(size_t, bytes, PAGE_SIZE - pgbase); req = nfs_page_create_from_page(dreq->ctx, pagevec[i], - false, pgbase, pos, + pinned, pgbase, pos, req_len); if (IS_ERR(req)) { result = PTR_ERR(req); @@ -898,6 +896,7 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, if (desc.pg_error < 0) { nfs_free_request(req); result = desc.pg_error; + i++; break; } @@ -919,6 +918,7 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, if (desc.pg_error < 0 && desc.pg_error != -EAGAIN) { result = desc.pg_error; nfs_unlock_and_release_request(req); + i++; break; } @@ -932,7 +932,8 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, desc.pg_error = 0; defer = true; } - nfs_direct_release_pages(pagevec, npages, false); + if (i < npages) + nfs_direct_release_pages(pagevec + i, npages - i, pinned); kvfree(pagevec); if (result < 0) break; From 7851fd653cb98f556a5d2ca368484e369874c2e5 Mon Sep 17 00:00:00 2001 From: Pranjal Shrivastava Date: Fri, 14 Aug 2026 14:32:55 +0000 Subject: [PATCH 09/68] nfs: introduce nfs_direct_extract_pages helper Introduce nfs_direct_extract_pages() in direct.c to centralize page extraction and request creation for the Direct I/O path. The helper manages extraction from the iters and builds a list of nfs_page requests Refactor nfs_direct_read_schedule_iovec() and nfs_direct_write_schedule_iovec() to utilize the new helper, unifying the extraction logic on both paths. Signed-off-by: Pranjal Shrivastava Reviewed-by: Shivaji Kant Reviewed-by: Christoph Hellwig Signed-off-by: Anna Schumaker --- fs/nfs/direct.c | 130 ++++++++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/fs/nfs/direct.c b/fs/nfs/direct.c index 59642e886a36..a3c6e8f4ea06 100644 --- a/fs/nfs/direct.c +++ b/fs/nfs/direct.c @@ -152,6 +152,50 @@ static void nfs_direct_release_pages(struct page **pages, unsigned int npages, unpin_user_pages(pages, npages); } +static ssize_t nfs_direct_extract_pages(struct nfs_direct_req *dreq, + struct iov_iter *iter, + size_t size, loff_t *pos, + struct list_head *list) +{ + bool pinned = iov_iter_extract_will_pin(iter); + struct page **pagevec = NULL; + ssize_t result, bytes = 0; + int err = 0; + unsigned int npages, i; + size_t pgbase; + + result = iov_iter_extract_pages(iter, &pagevec, size, ~0U, 0, &pgbase); + if (result <= 0) + return result; + + npages = (result + pgbase + PAGE_SIZE - 1) >> PAGE_SHIFT; + for (i = 0; i < npages; i++) { + struct nfs_page *req; + unsigned int req_len = min_t(size_t, result - bytes, PAGE_SIZE - pgbase); + + req = nfs_page_create_from_page(dreq->ctx, pagevec[i], + pinned, pgbase, *pos, + req_len); + if (IS_ERR(req)) { + err = PTR_ERR(req); + break; + } + + list_add_tail(&req->wb_list, list); + pgbase = 0; + bytes += req_len; + *pos += req_len; + } + + if (i < npages) { + iov_iter_revert(iter, result - bytes); + nfs_direct_release_pages(pagevec + i, npages - i, pinned); + } + + kvfree(pagevec); + return bytes ? bytes : err; +} + void nfs_init_cinfo_from_dreq(struct nfs_commit_info *cinfo, struct nfs_direct_req *dreq) { @@ -320,6 +364,7 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, ssize_t result = -EINVAL; size_t requested_bytes = 0; size_t rsize = max_t(size_t, NFS_SERVER(inode)->rsize, PAGE_SIZE); + LIST_HEAD(nfs_page_list); nfs_pageio_init_read(&desc, dreq->inode, false, &nfs_direct_read_completion_ops); @@ -328,44 +373,23 @@ static ssize_t nfs_direct_read_schedule_iovec(struct nfs_direct_req *dreq, inode_dio_begin(inode); while (iov_iter_count(iter)) { - struct page **pagevec = NULL; - size_t bytes; - size_t pgbase; - unsigned npages, i; - bool pinned = iov_iter_extract_will_pin(iter); - - result = iov_iter_extract_pages(iter, &pagevec, - rsize, ~0U, 0, &pgbase); + result = nfs_direct_extract_pages(dreq, iter, rsize, &pos, &nfs_page_list); if (result < 0) break; - bytes = result; - npages = (result + pgbase + PAGE_SIZE - 1) / PAGE_SIZE; - for (i = 0; i < npages; i++) { - struct nfs_page *req; - unsigned int req_len = min_t(size_t, bytes, PAGE_SIZE - pgbase); - /* XXX do we need to do the eof zeroing found in async_filler? */ - req = nfs_page_create_from_page(dreq->ctx, pagevec[i], - pinned, pgbase, pos, - req_len); - if (IS_ERR(req)) { - result = PTR_ERR(req); - break; - } + while (!list_empty(&nfs_page_list)) { + struct nfs_page *req = nfs_list_entry(nfs_page_list.next); + size_t req_len = req->wb_bytes; + + nfs_list_remove_request(req); if (!nfs_pageio_add_request(&desc, req)) { result = desc.pg_error; nfs_release_request(req); - i++; + nfs_release_request_list(&nfs_page_list); break; } - pgbase = 0; - bytes -= req_len; requested_bytes += req_len; - pos += req_len; } - if (i < npages) - nfs_direct_release_pages(pagevec + i, npages - i, pinned); - kvfree(pagevec); if (result < 0) break; } @@ -856,6 +880,7 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, ssize_t result = 0; size_t requested_bytes = 0; size_t wsize = max_t(size_t, NFS_SERVER(inode)->wsize, PAGE_SIZE); + LIST_HEAD(nfs_page_list); bool defer = false; trace_nfs_direct_write_schedule_iovec(dreq); @@ -868,57 +893,32 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, NFS_I(inode)->write_io += iov_iter_count(iter); while (iov_iter_count(iter)) { - struct page **pagevec = NULL; - size_t bytes; - size_t pgbase; - unsigned npages, i; - bool pinned = iov_iter_extract_will_pin(iter); - - result = iov_iter_extract_pages(iter, &pagevec, - wsize, ~0U, 0, &pgbase); + result = nfs_direct_extract_pages(dreq, iter, wsize, &pos, &nfs_page_list); if (result < 0) break; - bytes = result; - npages = (result + pgbase + PAGE_SIZE - 1) / PAGE_SIZE; - for (i = 0; i < npages; i++) { - struct nfs_page *req; - unsigned int req_len = min_t(size_t, bytes, PAGE_SIZE - pgbase); - - req = nfs_page_create_from_page(dreq->ctx, pagevec[i], - pinned, pgbase, pos, - req_len); - if (IS_ERR(req)) { - result = PTR_ERR(req); - break; - } - - if (desc.pg_error < 0) { - nfs_free_request(req); - result = desc.pg_error; - i++; - break; - } - - pgbase = 0; - bytes -= req_len; - requested_bytes += req_len; - pos += req_len; + while (!list_empty(&nfs_page_list)) { + struct nfs_page *req = nfs_list_entry(nfs_page_list.next); + size_t req_len = req->wb_bytes; + nfs_list_remove_request(req); if (defer) { nfs_mark_request_commit(req, NULL, &cinfo, 0); + requested_bytes += req_len; continue; } nfs_lock_request(req); - if (nfs_pageio_add_request(&desc, req)) + if (nfs_pageio_add_request(&desc, req)) { + requested_bytes += req_len; continue; + } /* Exit on hard errors */ if (desc.pg_error < 0 && desc.pg_error != -EAGAIN) { result = desc.pg_error; nfs_unlock_and_release_request(req); - i++; + nfs_release_request_list(&nfs_page_list); break; } @@ -929,12 +929,10 @@ static ssize_t nfs_direct_write_schedule_iovec(struct nfs_direct_req *dreq, spin_unlock(&dreq->lock); nfs_unlock_request(req); nfs_mark_request_commit(req, NULL, &cinfo, 0); + requested_bytes += req_len; desc.pg_error = 0; defer = true; } - if (i < npages) - nfs_direct_release_pages(pagevec + i, npages - i, pinned); - kvfree(pagevec); if (result < 0) break; } From 1e87497aac8d2d4aee9b288ba43f58d1690b9a89 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Sat, 8 Aug 2026 11:40:13 -0400 Subject: [PATCH 10/68] SUNRPC: reject a client-side TLS alert record that is not two octets tls_alert_recv() reads two octets from the kvec it is handed and does not check the length (net/handshake/alert.c). xs_sock_process_cmsg() calls it for any alert record, and the alert[] buffer that xs_sock_recv_cmsg() supplies carries no initializer. A one-octet alert body leaves the description read from uninitialized stack and reported through trace_tls_alert_recv(). The peer controls that length. Neither tls_rx_msg_size() nor tls_rx_one_record() enforces the two-octet Alert payload. A TLS 1.3 record carrying only the inner content-type octet decrypts to a zero-length payload. RFC 8446 Section 5.1 requires a record with an Alert type to carry exactly one message, so any other length is malformed. RFC 9289 Section 5 bars RPC-with-TLS from negotiating a version below TLS 1.3, so no other alert framing applies. Require exactly two octets before parsing and return -EACCES otherwise. xs_stream_data_receive() already treats -EACCES as a fatal alert and reports it to the pending tasks. Gate the path on a control message rather than a positive count so that a zero-length record reaches the check. Fixes: cc5d59081fa2 ("sunrpc: fix client side handling of tls alerts") Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- net/sunrpc/xprtsock.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/net/sunrpc/xprtsock.c b/net/sunrpc/xprtsock.c index 7f60723fa64d..827475296921 100644 --- a/net/sunrpc/xprtsock.c +++ b/net/sunrpc/xprtsock.c @@ -407,9 +407,25 @@ xs_sock_recv_cmsg(struct socket *sock, unsigned int *msg_flags, int flags) iov_iter_kvec(&msg.msg_iter, ITER_DEST, &alert_kvec, 1, alert_kvec.iov_len); ret = sock_recvmsg(sock, &msg, flags); - if (ret > 0) { - if (tls_get_record_type(sock->sk, &u.cmsg) == TLS_RECORD_TYPE_ALERT) + /* put_cmsg() shrinks msg_controllen, so a short one means + * kTLS filled in u.cmsg. + */ + if (ret >= 0 && msg.msg_controllen < sizeof(u)) { + if (tls_get_record_type(sock->sk, &u.cmsg) == + TLS_RECORD_TYPE_ALERT) { + /* RFC 8446 Section 5.1 requires a record with an + * Alert type to carry exactly one message. An alert + * is two octets. tls_alert_recv() reads both without + * checking the length. alert_kvec caps the count at + * two, so a longer record fills it as well. kTLS + * sets MSG_EOR only once the record has been + * drained. + */ + if (ret != sizeof(alert) || + !(msg.msg_flags & MSG_EOR)) + return -EACCES; iov_iter_revert(&msg.msg_iter, ret); + } ret = xs_sock_process_cmsg(sock, &msg, msg_flags, &u.cmsg, -EAGAIN); } From 9b7b1f89b93a3ec1ed121e1ab28450acde815bff Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Sat, 8 Aug 2026 11:40:14 -0400 Subject: [PATCH 11/68] SUNRPC: treat every client-side TLS error alert as fatal xs_sock_process_cmsg() decides whether an alert ends the session by reading the alert's level octet. RFC 8446 Section 6 retired that field. The severity is implicit in the description, and a receiver treats every alert listed in Section 6.2 as an error alert "regardless of the AlertLevel in the message". A peer that aborts with unexpected_message but leaves the legacy octet set to warning makes the client return -EAGAIN. xs_stream_data_receive() wakes no pending task for that error, so RPC Calls queued on a dead TLS session wait for their timeouts to expire. Decide from the alert description instead. close_notify and user_canceled are the closure alerts (RFC 8446 Section 6.1). Every other description ends the session, including one this kernel does not recognize. Fixes: 39067dda1d86 ("SUNRPC: Use new helpers to handle TLS Alerts") Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- net/sunrpc/xprtsock.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/net/sunrpc/xprtsock.c b/net/sunrpc/xprtsock.c index 827475296921..963e46a51421 100644 --- a/net/sunrpc/xprtsock.c +++ b/net/sunrpc/xprtsock.c @@ -375,8 +375,18 @@ xs_sock_process_cmsg(struct socket *sock, struct msghdr *msg, break; case TLS_RECORD_TYPE_ALERT: tls_alert_recv(sock->sk, msg, &level, &description); - ret = (level == TLS_ALERT_LEVEL_FATAL) ? - -EACCES : -EAGAIN; + /* RFC 8446 Section 6: every alert but a closure alert is + * an error alert, whatever the legacy AlertLevel octet + * says. + */ + switch (description) { + case TLS_ALERT_DESC_CLOSE_NOTIFY: + case TLS_ALERT_DESC_USER_CANCELED: + ret = -EAGAIN; + break; + default: + ret = -EACCES; + } break; default: /* discard this record type */ From 3f8ffd29579dc832bc194b7a21efc68608ce79fc Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Sat, 8 Aug 2026 11:40:15 -0400 Subject: [PATCH 12/68] SUNRPC: fold xs_sock_process_cmsg() into its only caller xs_sock_process_cmsg() switches on the TLS record type, and every arm but TLS_RECORD_TYPE_ALERT returns the -EAGAIN its caller passed in. The DATA arm clears MSG_EOR in the caller's msghdr, but xs_sock_recvmsg() has already cleared that flag before the call. Deriving the record type a second time inside the helper also fires trace_tls_contenttype() twice for every alert. Move the alert handling into xs_sock_recv_cmsg() and delete the helper. Every other record type still returns -EAGAIN. The DATA arm's account of MSG_EOR moves to xs_sock_recvmsg(), where the flag is cleared. Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- net/sunrpc/xprtsock.c | 85 +++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/net/sunrpc/xprtsock.c b/net/sunrpc/xprtsock.c index 963e46a51421..f5a5136327ba 100644 --- a/net/sunrpc/xprtsock.c +++ b/net/sunrpc/xprtsock.c @@ -356,45 +356,6 @@ xs_alloc_sparse_pages(struct xdr_buf *buf, size_t want, gfp_t gfp) return want; } -static int -xs_sock_process_cmsg(struct socket *sock, struct msghdr *msg, - unsigned int *msg_flags, struct cmsghdr *cmsg, int ret) -{ - u8 content_type = tls_get_record_type(sock->sk, cmsg); - u8 level, description; - - switch (content_type) { - case 0: - break; - case TLS_RECORD_TYPE_DATA: - /* TLS sets EOR at the end of each application data - * record, even though there might be more frames - * waiting to be decrypted. - */ - *msg_flags &= ~MSG_EOR; - break; - case TLS_RECORD_TYPE_ALERT: - tls_alert_recv(sock->sk, msg, &level, &description); - /* RFC 8446 Section 6: every alert but a closure alert is - * an error alert, whatever the legacy AlertLevel octet - * says. - */ - switch (description) { - case TLS_ALERT_DESC_CLOSE_NOTIFY: - case TLS_ALERT_DESC_USER_CANCELED: - ret = -EAGAIN; - break; - default: - ret = -EACCES; - } - break; - default: - /* discard this record type */ - ret = -EAGAIN; - } - return ret; -} - static int xs_sock_recv_cmsg(struct socket *sock, unsigned int *msg_flags, int flags) { @@ -412,6 +373,7 @@ xs_sock_recv_cmsg(struct socket *sock, unsigned int *msg_flags, int flags) .msg_control = &u, .msg_controllen = sizeof(u), }; + u8 level, description; int ret; iov_iter_kvec(&msg.msg_iter, ITER_DEST, &alert_kvec, 1, @@ -421,23 +383,32 @@ xs_sock_recv_cmsg(struct socket *sock, unsigned int *msg_flags, int flags) * kTLS filled in u.cmsg. */ if (ret >= 0 && msg.msg_controllen < sizeof(u)) { - if (tls_get_record_type(sock->sk, &u.cmsg) == - TLS_RECORD_TYPE_ALERT) { - /* RFC 8446 Section 5.1 requires a record with an - * Alert type to carry exactly one message. An alert - * is two octets. tls_alert_recv() reads both without - * checking the length. alert_kvec caps the count at - * two, so a longer record fills it as well. kTLS - * sets MSG_EOR only once the record has been - * drained. - */ - if (ret != sizeof(alert) || - !(msg.msg_flags & MSG_EOR)) - return -EACCES; - iov_iter_revert(&msg.msg_iter, ret); + if (tls_get_record_type(sock->sk, &u.cmsg) != + TLS_RECORD_TYPE_ALERT) + return -EAGAIN; + /* RFC 8446 Section 5.1: a record with an Alert type carries + * exactly one message, and an alert is two octets. + * tls_alert_recv() reads both without checking the length. + * alert_kvec caps the count at two, so a longer record + * fills it as well. kTLS sets MSG_EOR only once the + * record has been drained. + */ + if (ret != sizeof(alert) || !(msg.msg_flags & MSG_EOR)) + return -EACCES; + iov_iter_revert(&msg.msg_iter, ret); + tls_alert_recv(sock->sk, &msg, &level, &description); + /* RFC 8446 Section 6: every alert but a closure alert is + * an error alert, whatever the legacy AlertLevel octet + * says. + */ + switch (description) { + case TLS_ALERT_DESC_CLOSE_NOTIFY: + case TLS_ALERT_DESC_USER_CANCELED: + ret = -EAGAIN; + break; + default: + ret = -EACCES; } - ret = xs_sock_process_cmsg(sock, &msg, msg_flags, &u.cmsg, - -EAGAIN); } return ret; } @@ -451,6 +422,10 @@ xs_sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags, size_t seek) ret = sock_recvmsg(sock, msg, flags); /* Handle TLS inband control message lazily */ if (msg->msg_flags & MSG_CTRUNC) { + /* TLS sets EOR at the end of each application data + * record, even though there might be more frames + * waiting to be decrypted. + */ msg->msg_flags &= ~(MSG_CTRUNC | MSG_EOR); if (ret == 0 || ret == -EIO) ret = xs_sock_recv_cmsg(sock, &msg->msg_flags, flags); From 28c950413605fb5b6cb3863c5e3274ea69701097 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Fri, 4 Sep 2026 08:56:02 -0400 Subject: [PATCH 13/68] NFSv4.1/pnfs: suspend pNFS on NFS4ERR_TOOSMALL from LAYOUTGET If the layout for the requested range is larger than the size the client advertised in loga_maxcount, RFC 8881 Section 18.43.3 has the metadata server return NFS4ERR_TOOSMALL. The client caps loga_maxcount at a single page, so a flexfiles server that stripes a layout segment across several dozen data servers produces this error today. The client has no handling for it: nfs4_stat_to_errno() maps the error to -ETOOSMALL during decode, which nothing in the layoutget path recognizes and nfs_error_is_fatal() does not consider fatal, so pnfs_update_layout() clears the layout fail bit and returns no segment. The I/O falls back to the MDS, but because no fail bit was set, every subsequent pageio attempt sends another LAYOUTGET that is doomed to the same NFS4ERR_TOOSMALL. Files whose layouts do not fit the reply buffer never use pNFS and pay an extra round trip on every pageio. Map -ETOOSMALL to -EMSGSIZE in the layoutget exception handler and have pnfs_update_layout() treat it like NFS4ERR_LAYOUTUNAVAILABLE: mark the layout mode as failed and fall back to I/O through the MDS. Fixes: d600ad1f2bdb ("NFS41: pop some layoutget errors to application") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/nfs4proc.c | 8 ++++++++ fs/nfs/pnfs.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/fs/nfs/nfs4proc.c b/fs/nfs/nfs4proc.c index 04b1987115d5..e41c792a2725 100644 --- a/fs/nfs/nfs4proc.c +++ b/fs/nfs/nfs4proc.c @@ -9680,6 +9680,14 @@ nfs4_layoutget_handle_exception(struct rpc_task *task, case -NFS4ERR_BADLAYOUT: status = -EOVERFLOW; goto out; + /* + * NFS4ERR_TOOSMALL means the layout for the requested range + * exceeds what the client advertised in loga_maxcount (see + * RFC8881 section 18.43.3). + */ + case -ETOOSMALL: + status = -EMSGSIZE; + goto out; /* * NFS4ERR_LAYOUTTRYLATER is a conflict with another client * (or clients) writing to the same RAID stripe except when diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 4f9c0f639014..c5d1951d5400 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -2331,6 +2331,8 @@ pnfs_update_layout(struct inode *ino, break; case -ENODATA: /* The server returned NFS4ERR_LAYOUTUNAVAILABLE */ + case -EMSGSIZE: + /* The layout exceeded loga_maxcount (NFS4ERR_TOOSMALL) */ pnfs_layout_set_fail_bit( lo, pnfs_iomode_to_fail_bit(iomode)); lseg = NULL; From fcfcf04e9fb9c93b13dc51932ea995ff279c705f Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Fri, 4 Sep 2026 08:56:03 -0400 Subject: [PATCH 14/68] NFSv4.1/pnfs: derive loga_maxcount from the LAYOUTGET reply buffer The client advertises a fixed PNFS_LAYOUT_MAXSIZE (4096) in loga_maxcount no matter what reply buffer it actually allocated. The two only happen to agree for layout drivers that cap max_layoutget_response at a single page. Block and SCSI layouts allocate a session-sized reply buffer, yet still tell the server it must not send more than 4KB of layout: a server whose extent list encodes larger than a page returns NFS4ERR_TOOSMALL even though the client has ample room to receive it. Set loga_maxcount from the size of the page array allocated for the reply. The client never advertises more than it can receive, and any future change to the reply buffer sizing automatically carries its wire advertisement with it. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/pnfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index c5d1951d5400..9bef2997f512 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -1210,7 +1210,7 @@ pnfs_alloc_init_layoutget_args(struct inode *ino, lgp->args.minlength = i_size - range->offset; } } - lgp->args.maxcount = PNFS_LAYOUT_MAXSIZE; + lgp->args.maxcount = lgp->args.layout.pglen; pnfs_copy_range(&lgp->args.range, range); lgp->args.type = server->pnfs_curr_ld->id; lgp->args.inode = ino; From 99ce1c14294236f0902df9870850ff1c3ed24381 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Fri, 4 Sep 2026 08:56:04 -0400 Subject: [PATCH 15/68] NFSv4.1/pnfs: retry LAYOUTGET with a larger reply buffer on NFS4ERR_TOOSMALL Layout drivers cap the LAYOUTGET reply buffer with max_layoutget_response; the flexfiles driver caps it at a single page, which limits a striped layout segment to roughly 28 stripes. A server striping wider than that returns NFS4ERR_TOOSMALL, and the client falls back to I/O through the MDS -- pNFS never engages for those files. Instead of failing over to the MDS on the first NFS4ERR_TOOSMALL, retry the LAYOUTGET once with the reply buffer raised to the session's maximum response size, the same bound GETDEVICEINFO already uses. The server offers no size hint in the TOOSMALL error, and the page array is transient (freed when the RPC completes), so a single jump to the ceiling is preferred over incremental growth. If the layout does not fit even the session-sized buffer, fall back to the MDS as before. The common path is unchanged: the first LAYOUTGET for a layout is still sent with the driver's default reply buffer, and larger buffers are only ever allocated against servers that actually hand out wide layouts. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/pnfs.c | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 9bef2997f512..0c938d17b6c6 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -1167,11 +1167,12 @@ pnfs_alloc_init_layoutget_args(struct inode *ino, struct nfs_open_context *ctx, const nfs4_stateid *stateid, const struct pnfs_layout_range *range, - gfp_t gfp_flags) + size_t min_reply_sz, gfp_t gfp_flags) { struct nfs_server *server = pnfs_find_server(ino, ctx); size_t max_reply_sz = server->pnfs_curr_ld->max_layoutget_response; - size_t max_pages = max_response_pages(server); + size_t session_pages = max_response_pages(server); + size_t max_pages = session_pages; struct nfs4_layoutget *lgp; dprintk("--> %s\n", __func__); @@ -1186,6 +1187,17 @@ pnfs_alloc_init_layoutget_args(struct inode *ino, max_pages = npages; } + /* + * A previous LAYOUTGET for this layout did not fit the reply + * buffer: raise the layout driver's default up to the session's + * maximum response size. + */ + if (min_reply_sz) { + size_t npages = (min_reply_sz + PAGE_SIZE - 1) >> PAGE_SHIFT; + if (npages > max_pages) + max_pages = min(npages, session_pages); + } + lgp->args.layout.pages = nfs4_alloc_pages(max_pages, gfp_flags); if (!lgp->args.layout.pages) { kfree(lgp); @@ -2145,6 +2157,7 @@ pnfs_update_layout(struct inode *ino, .inode = ino, }; unsigned long giveup = jiffies + (clp->cl_lease_time << 1); + size_t reply_sz = 0; bool first; if (!pnfs_enabled_sb(NFS_SERVER(ino))) { @@ -2304,7 +2317,8 @@ pnfs_update_layout(struct inode *ino, if (arg.length != NFS4_MAX_UINT64) arg.length = PAGE_ALIGN(arg.length); - lgp = pnfs_alloc_init_layoutget_args(ino, ctx, &stateid, &arg, gfp_flags); + lgp = pnfs_alloc_init_layoutget_args(ino, ctx, &stateid, &arg, reply_sz, + gfp_flags); if (!lgp) { lseg = ERR_PTR(-ENOMEM); trace_pnfs_update_layout(ino, pos, count, iomode, lo, NULL, @@ -2331,12 +2345,29 @@ pnfs_update_layout(struct inode *ino, break; case -ENODATA: /* The server returned NFS4ERR_LAYOUTUNAVAILABLE */ - case -EMSGSIZE: - /* The layout exceeded loga_maxcount (NFS4ERR_TOOSMALL) */ pnfs_layout_set_fail_bit( lo, pnfs_iomode_to_fail_bit(iomode)); lseg = NULL; goto out_put_layout_hdr; + case -EMSGSIZE: { + /* + * The layout exceeded loga_maxcount (NFS4ERR_TOOSMALL): + * retry once with the reply buffer raised to the + * session's maximum response size before falling back + * to I/O through the MDS. + */ + size_t max = max_response_pages(server) << PAGE_SHIFT; + + if (reply_sz < max) { + reply_sz = max; + exception.retry = 1; + break; + } + pnfs_layout_set_fail_bit( + lo, pnfs_iomode_to_fail_bit(iomode)); + lseg = NULL; + goto out_put_layout_hdr; + } default: if (!nfs_error_is_fatal(PTR_ERR(lseg))) { pnfs_layout_clear_fail_bit(lo, pnfs_iomode_to_fail_bit(iomode)); @@ -2450,7 +2481,7 @@ static void _lgopen_prepare_attached(struct nfs4_opendata *data, lo = _pnfs_grab_empty_layout(ino, ctx); if (!lo) return; - lgp = pnfs_alloc_init_layoutget_args(ino, ctx, ¤t_stateid, &rng, + lgp = pnfs_alloc_init_layoutget_args(ino, ctx, ¤t_stateid, &rng, 0, nfs_io_gfp_mask()); if (!lgp) { clear_and_wake_up_bit(NFS_LAYOUT_FIRST_LAYOUTGET, &lo->plh_flags); @@ -2476,7 +2507,7 @@ static void _lgopen_prepare_floating(struct nfs4_opendata *data, }; struct nfs4_layoutget *lgp; - lgp = pnfs_alloc_init_layoutget_args(ino, ctx, ¤t_stateid, &rng, + lgp = pnfs_alloc_init_layoutget_args(ino, ctx, ¤t_stateid, &rng, 0, nfs_io_gfp_mask()); if (!lgp) return; From 60bb8276fb3bab693bde411d1aa1af2a10a15db5 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Fri, 4 Sep 2026 08:56:05 -0400 Subject: [PATCH 16/68] NFSv4.1/pnfs: treat an oversized LAYOUTGET reply as -EMSGSIZE decode_layoutget() already detects a server that sends a layout body larger than the reply buffer we provided ("server cheating in layoutget reply") but maps it to -EINVAL, which pnfs_update_layout() treats as a transient error: the client falls back to the MDS for this I/O only and re-sends a doomed LAYOUTGET on every subsequent pageio attempt. A server that overruns the reply buffer has ignored loga_maxcount, but the client can recover the same way it recovers from a conformant server's NFS4ERR_TOOSMALL: return -EMSGSIZE from exactly this check so that the layoutget path retries once with a session-sized reply buffer and otherwise marks the layout mode failed and falls back to the MDS. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/nfs4xdr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/nfs/nfs4xdr.c b/fs/nfs/nfs4xdr.c index fc049ce4ba8a..8b3d96b4a0f0 100644 --- a/fs/nfs/nfs4xdr.c +++ b/fs/nfs/nfs4xdr.c @@ -6187,7 +6187,7 @@ static int decode_layoutget(struct xdr_stream *xdr, struct rpc_rqst *req, dprintk("NFS: server cheating in layoutget reply: " "layout len %u > recvd %u\n", res->layoutp->len, recvd); - status = -EINVAL; + status = -EMSGSIZE; goto out; } From 0e180168d597942ea4acb47599d8682830346a67 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Fri, 4 Sep 2026 08:56:06 -0400 Subject: [PATCH 17/68] NFSv4.1/pnfs: remember when a server needs a larger LAYOUTGET reply buffer When a LAYOUTGET only succeeds after escalating the reply buffer, every layout fetched from that server is likely to need the larger buffer: remember the escalated size on the nfs_server and use it as the floor for subsequent LAYOUTGET reply buffers, skipping the doomed attempt at the layout driver's default size. This also lets the LAYOUTGET attached to OPEN benefit: the lgopen path is best-effort with no retry of its own, so without the learned size it would fail with NFS4ERR_TOOSMALL at every open against a server handing out wide layouts, and layouts would only ever be acquired by the I/O path's retry. The field is a hint: reads and writes are racy by design, the value only ever grows toward the session's maximum response size, and a stale-low read merely costs one escalation round trip. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/pnfs.c | 15 ++++++++++++--- include/linux/nfs_fs_sb.h | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 0c938d17b6c6..3e8d1a1fd827 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -1188,10 +1188,12 @@ pnfs_alloc_init_layoutget_args(struct inode *ino, } /* - * A previous LAYOUTGET for this layout did not fit the reply - * buffer: raise the layout driver's default up to the session's - * maximum response size. + * A previous LAYOUTGET on this layout or on this server did not + * fit the reply buffer: raise the layout driver's default up to + * the session's maximum response size. */ + if (!min_reply_sz) + min_reply_sz = READ_ONCE(server->lg_reply_sz); if (min_reply_sz) { size_t npages = (min_reply_sz + PAGE_SIZE - 1) >> PAGE_SHIFT; if (npages > max_pages) @@ -2387,6 +2389,13 @@ pnfs_update_layout(struct inode *ino, goto lookup_again; } } else { + /* + * A LAYOUTGET that only succeeded with an escalated reply + * buffer: remember the size so that future LAYOUTGETs to + * this server skip the attempt at the driver's default. + */ + if (reply_sz) + WRITE_ONCE(server->lg_reply_sz, reply_sz); pnfs_layout_clear_fail_bit(lo, pnfs_iomode_to_fail_bit(iomode)); } diff --git a/include/linux/nfs_fs_sb.h b/include/linux/nfs_fs_sb.h index 34d294774f8c..3e6bae7e5221 100644 --- a/include/linux/nfs_fs_sb.h +++ b/include/linux/nfs_fs_sb.h @@ -248,6 +248,10 @@ struct nfs_server { that are supported on this filesystem */ struct pnfs_layoutdriver_type *pnfs_curr_ld; /* Active layout driver */ + unsigned int lg_reply_sz; /* Learned LAYOUTGET reply + buffer size, when the layout + driver's default has proved + too small */ struct rpc_wait_queue roc_rpcwaitq; /* the following fields are protected by nfs_client->cl_lock */ From b5d62810ca21e8e587f2ebc9b78dcc1609f42e0c Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Fri, 4 Sep 2026 08:56:07 -0400 Subject: [PATCH 18/68] NFSv4/flexfiles: allocate the per-mirror stripe array with kvzalloc_objs Each mirror's stripe array is a single contiguous allocation of dss_count * sizeof(struct nfs4_ff_layout_ds_stripe) -- roughly 300 bytes per stripe. With the LAYOUTGET reply buffer no longer capped at a single page, a wide striped layout can push this well past the high-order allocation comfort zone (a 2048-stripe mirror is a ~600KB contiguous allocation) where it can fail under memory fragmentation. Use kvzalloc_objs() so wide stripe arrays fall back to vmalloc. Note the vmalloc fallback is unavailable when the pageio path allocates under memalloc_noio (swap over pNFS); that case simply behaves as before. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 7fe8b91fa47c..1267eab830fd 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -285,8 +285,8 @@ static struct nfs4_ff_layout_mirror *ff_layout_alloc_mirror(u32 dss_count, mirror->dss_count = dss_count; mirror->dss = - kzalloc_objs(struct nfs4_ff_layout_ds_stripe, dss_count, - gfp_flags); + kvzalloc_objs(struct nfs4_ff_layout_ds_stripe, dss_count, + gfp_flags); if (mirror->dss == NULL) { kfree(mirror); return NULL; @@ -315,7 +315,7 @@ static void ff_layout_free_mirror(struct nfs4_ff_layout_mirror *mirror) nfs4_ff_layout_put_deviceid(mirror->dss[dss_id].mirror_ds); } - kfree(mirror->dss); + kvfree(mirror->dss); kfree(mirror); } From a7c8e96d1b9fa327cbdea514d047c8295b28d49a Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:03 -0400 Subject: [PATCH 19/68] NFSv4/pnfs: Free the netid when draining a data-server address list nfs4_pnfs_ds_addr_free() releases all three of an address entry's allocations, but the four sites that drain a decoded list before it reaches a data server open-code the loop and free only da_remotestr and the entry, leaking da_netid. Both layout drivers drain that list when nfs4_pnfs_ds_add() returned a data server already in the cache, and again on their error exits. Lift the drain into nfs4_pnfs_ds_addr_list_free() so an address list has one owner. A striping mount reaches the cached case on nearly every GETDEVICEINFO, since many deviceIDs name the same data servers. Fixes: 4be78d26810b ("NFSv4/pNFS: Store the transport type in struct nfs4_pnfs_ds_addr") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/filelayout/filelayoutdev.c | 17 ++--------------- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 17 ++--------------- fs/nfs/pnfs.h | 1 + fs/nfs/pnfs_nfs.c | 21 +++++++++++++-------- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/fs/nfs/filelayout/filelayoutdev.c b/fs/nfs/filelayout/filelayoutdev.c index 88bc79ec3459..57e0654cd98c 100644 --- a/fs/nfs/filelayout/filelayoutdev.c +++ b/fs/nfs/filelayout/filelayoutdev.c @@ -177,27 +177,14 @@ nfs4_fl_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev, trace_fl_getdevinfo(server, &pdev->dev_id, dsaddr->ds_list[i]->ds_remotestr); /* If DS was already in cache, free ds addrs */ - while (!list_empty(&dsaddrs)) { - da = list_first_entry(&dsaddrs, - struct nfs4_pnfs_ds_addr, - da_node); - list_del_init(&da->da_node); - kfree(da->da_remotestr); - kfree(da); - } + nfs4_pnfs_ds_addr_list_free(&dsaddrs); } folio_put(scratch); return dsaddr; out_err_drain_dsaddrs: - while (!list_empty(&dsaddrs)) { - da = list_first_entry(&dsaddrs, struct nfs4_pnfs_ds_addr, - da_node); - list_del_init(&da->da_node); - kfree(da->da_remotestr); - kfree(da); - } + nfs4_pnfs_ds_addr_list_free(&dsaddrs); out_err_free_deviceid: nfs4_fl_free_deviceid(dsaddr); /* stripe_indicies was part of dsaddr */ diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index 5c0216bd5fce..6cd3859d4a24 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -159,26 +159,13 @@ nfs4_ff_alloc_deviceid_node(struct nfs_server *server, struct pnfs_device *pdev, goto out_err_drain_dsaddrs; /* If DS was already in cache, free ds addrs */ - while (!list_empty(&dsaddrs)) { - da = list_first_entry(&dsaddrs, - struct nfs4_pnfs_ds_addr, - da_node); - list_del_init(&da->da_node); - kfree(da->da_remotestr); - kfree(da); - } + nfs4_pnfs_ds_addr_list_free(&dsaddrs); folio_put(scratch); return new_ds; out_err_drain_dsaddrs: - while (!list_empty(&dsaddrs)) { - da = list_first_entry(&dsaddrs, struct nfs4_pnfs_ds_addr, - da_node); - list_del_init(&da->da_node); - kfree(da->da_remotestr); - kfree(da); - } + nfs4_pnfs_ds_addr_list_free(&dsaddrs); kfree(ds_versions); out_scratch: diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 70d20f779678..5725ea06efc2 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -427,6 +427,7 @@ int nfs4_pnfs_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds, struct nfs4_pnfs_ds_addr *nfs4_decode_mp_ds_addr(struct net *net, struct xdr_stream *xdr, gfp_t gfp_flags); +void nfs4_pnfs_ds_addr_list_free(struct list_head *dsaddrs); void pnfs_layout_mark_request_commit(struct nfs_page *req, struct pnfs_layout_segment *lseg, struct nfs_commit_info *cinfo, diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c index 93d63f75a355..5bd014b981bf 100644 --- a/fs/nfs/pnfs_nfs.c +++ b/fs/nfs/pnfs_nfs.c @@ -633,23 +633,28 @@ static void nfs4_pnfs_ds_addr_free(struct nfs4_pnfs_ds_addr *da) kfree(da); } -static void destroy_ds(struct nfs4_pnfs_ds *ds) +void nfs4_pnfs_ds_addr_list_free(struct list_head *dsaddrs) { struct nfs4_pnfs_ds_addr *da; + while (!list_empty(dsaddrs)) { + da = list_first_entry(dsaddrs, struct nfs4_pnfs_ds_addr, + da_node); + list_del_init(&da->da_node); + nfs4_pnfs_ds_addr_free(da); + } +} +EXPORT_SYMBOL_GPL(nfs4_pnfs_ds_addr_list_free); + +static void destroy_ds(struct nfs4_pnfs_ds *ds) +{ dprintk("--> %s\n", __func__); ifdebug(FACILITY) print_ds(ds); nfs_put_client(ds->ds_clp); - while (!list_empty(&ds->ds_addrs)) { - da = list_first_entry(&ds->ds_addrs, - struct nfs4_pnfs_ds_addr, - da_node); - list_del_init(&da->da_node); - nfs4_pnfs_ds_addr_free(da); - } + nfs4_pnfs_ds_addr_list_free(&ds->ds_addrs); kfree(ds->ds_remotestr); kfree(ds); From 0cc1191538b1523dfe944eedc799fdd146c05fbf Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:04 -0400 Subject: [PATCH 20/68] NFSv4/flexfiles: Use the full 64-bit stripe_unit ff_layout_alloc_lseg() decodes stripe_unit as the 64-bit value the protocol defines and struct nfs4_ff_layout_segment stores it as one, but both consumers narrow it back to 32 bits: nfs4_ff_layout_calc_dss_id() divides with do_div(), which casts the divisor, and ff_layout_pg_test() copies it into a u32 first. A stripe_unit that does not fit is silently truncated, so the client stripes on a unit the server did not specify -- or divides by zero, if the low 32 bits happen to be clear. Divide by the full value, with div64_u64() and div64_u64_rem(). Fixes: 20b1d75fb840 ("NFSv4/flexfiles: Add support for striped layouts") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 12 ++++++------ fs/nfs/flexfilelayout/flexfilelayout.h | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 1267eab830fd..5895824974b2 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -995,9 +995,9 @@ ff_layout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, { unsigned int size; u64 p_stripe, r_stripe; - u32 stripe_offset; + u64 stripe_offset; u64 segment_offset = pgio->pg_lseg->pls_range.offset; - u32 stripe_unit = FF_LAYOUT_LSEG(pgio->pg_lseg)->stripe_unit; + u64 stripe_unit = FF_LAYOUT_LSEG(pgio->pg_lseg)->stripe_unit; /* calls nfs_generic_pg_test */ size = pnfs_generic_pg_test(pgio, prev, req); @@ -1010,21 +1010,21 @@ ff_layout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, if (prev) { p_stripe = (u64)req_offset(prev) - segment_offset; r_stripe = (u64)req_offset(req) - segment_offset; - do_div(p_stripe, stripe_unit); - do_div(r_stripe, stripe_unit); + p_stripe = div64_u64(p_stripe, stripe_unit); + r_stripe = div64_u64(r_stripe, stripe_unit); if (p_stripe != r_stripe) return 0; } /* calculate remaining bytes in the current stripe */ - div_u64_rem((u64)req_offset(req) - segment_offset, + div64_u64_rem((u64)req_offset(req) - segment_offset, stripe_unit, &stripe_offset); WARN_ON_ONCE(stripe_offset > stripe_unit); if (stripe_offset >= stripe_unit) return 0; - return min(stripe_unit - (unsigned int)stripe_offset, size); + return min_t(u64, stripe_unit - stripe_offset, size); } static void diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h index a5bd00f69e82..ec69cd1c3ae9 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.h +++ b/fs/nfs/flexfilelayout/flexfilelayout.h @@ -220,7 +220,7 @@ nfs4_ff_layout_calc_dss_id(const u64 stripe_unit, const u32 dss_count, const lof if (dss_count == 1 || stripe_unit == 0) return 0; - do_div(tmp, stripe_unit); + tmp = div64_u64(tmp, stripe_unit); return do_div(tmp, dss_count); } From 32f0967ea296d447925465c98e7eec71d4e2f755 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:05 -0400 Subject: [PATCH 21/68] NFSv4/pnfs: bound the CB_NOTIFY_DEVICEID array count before allocating decode_devicenotify_args() hands the server's notification count straight to kmalloc_objs() as the array length, without checking it against the message that carried it. Bound it the way nfs4xdr.c bounds an attribute length, against xdr_stream_remaining(). Fixes: 1be5683b03a7 ("pnfs: CB_NOTIFY_DEVICEID") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/callback_xdr.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/nfs/callback_xdr.c b/fs/nfs/callback_xdr.c index 88e1af0fda01..b1bc2c02b5a6 100644 --- a/fs/nfs/callback_xdr.c +++ b/fs/nfs/callback_xdr.c @@ -271,6 +271,13 @@ __be32 decode_devicenotify_args(struct svc_rqst *rqstp, if (n == 0) goto out; + /* sanity check the count against the remaining stream */ + if (n > xdr_stream_remaining(xdr) / + ((4 * sizeof(uint32_t)) + NFS4_DEVICEID4_SIZE)) { + status = htonl(NFS4ERR_BADXDR); + goto out; + } + args->devs = kmalloc_objs(*args->devs, n); if (!args->devs) { status = htonl(NFS4ERR_DELAY); From a0cb76dc3b2919af8257fc08b4f9a79a35442483 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:06 -0400 Subject: [PATCH 22/68] pNFS: Fix CB_NOTIFY_DEVICEID CHANGE to consume ndc_immediate decode_devicenotify_args() gated the trailing ndc_immediate on cbd_layout_type rather than cbd_notify_type, so the flag was never consumed and a multi-item cnda_changes<> misaligned after the first entry. Fixes: 1be5683b03a7 ("pnfs: CB_NOTIFY_DEVICEID") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/callback_xdr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/nfs/callback_xdr.c b/fs/nfs/callback_xdr.c index b1bc2c02b5a6..5a1f05469268 100644 --- a/fs/nfs/callback_xdr.c +++ b/fs/nfs/callback_xdr.c @@ -319,7 +319,7 @@ __be32 decode_devicenotify_args(struct svc_rqst *rqstp, memcpy(dev->cbd_dev_id.data, p, NFS4_DEVICEID4_SIZE); p += XDR_QUADLEN(NFS4_DEVICEID4_SIZE); - if (dev->cbd_layout_type == NOTIFY_DEVICEID4_CHANGE) { + if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) { p = xdr_inline_decode(xdr, sizeof(uint32_t)); if (unlikely(p == NULL)) { status = htonl(NFS4ERR_BADXDR); From 448591e1f82902e5714bd436d6367adc7fbaea90 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:07 -0400 Subject: [PATCH 23/68] NFSv4/flexfiles: Use the full 64-bit offset for read DS selection The read data-server selection helpers declare their offset parameter as u32, truncating the file offset before nfs4_ff_layout_calc_dss_id() computes the stripe index. The write path already passes the full offset; make the read path match. Fixes: 4934ccbeaed3 ("NFSv4/flexfiles: Read path updates for striped layouts") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 5895824974b2..a36f29880e94 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -878,7 +878,7 @@ ff_layout_mark_ds_reachable(struct pnfs_layout_segment *lseg, u32 idx, u32 dss_i static struct nfs4_pnfs_ds * ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, - u32 offset, u32 *dss_id, + u64 offset, u32 *dss_id, bool check_device) { struct nfs4_ff_layout_segment *fls = FF_LAYOUT_LSEG(lseg); @@ -914,7 +914,7 @@ ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, static struct nfs4_pnfs_ds * ff_layout_choose_any_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, - u32 offset, u32 *dss_id) + u64 offset, u32 *dss_id) { return ff_layout_choose_ds_for_read(lseg, start_idx, best_idx, offset, dss_id, false); @@ -923,7 +923,7 @@ ff_layout_choose_any_ds_for_read(struct pnfs_layout_segment *lseg, static struct nfs4_pnfs_ds * ff_layout_choose_valid_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, - u32 offset, u32 *dss_id) + u64 offset, u32 *dss_id) { return ff_layout_choose_ds_for_read(lseg, start_idx, best_idx, offset, dss_id, true); @@ -932,7 +932,7 @@ ff_layout_choose_valid_ds_for_read(struct pnfs_layout_segment *lseg, static struct nfs4_pnfs_ds * ff_layout_choose_best_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, - u32 offset, u32 *dss_id) + u64 offset, u32 *dss_id) { struct nfs4_pnfs_ds *ds; @@ -947,7 +947,7 @@ ff_layout_choose_best_ds_for_read(struct pnfs_layout_segment *lseg, static struct nfs4_pnfs_ds * ff_layout_get_ds_for_read(struct nfs_pageio_descriptor *pgio, u32 *best_idx, - u32 offset, + u64 offset, u32 *dss_id) { struct pnfs_layout_segment *lseg = pgio->pg_lseg; From a84c62c4f03706b2c2f01dcc5fe614a4233ee75d Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:08 -0400 Subject: [PATCH 24/68] NFSv4/flexfiles: Bound page coalescing on the absolute stripe offset ff_layout_pg_test() was copied from the files layout, which partitions data servers relative to pattern_offset and bounds coalescing with a segment-relative offset. Flexfiles has no pattern_offset: nfs4_ff_layout_calc_dss_id() partitions on the absolute file offset (RFC 8435 Section 6). So when a layout segment's offset is not stripe-unit-aligned, the segment-relative coalescing window is shifted off the absolute stripe grid and a coalesced I/O can straddle a stripe boundary, sending the bytes past it to the wrong data server. Bound coalescing on the absolute offset to match calc_dss_id(). Aligned segments, including all whole-file layouts, are unchanged. Fixes: 4934ccbeaed3 ("NFSv4/flexfiles: Read path updates for striped layouts") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index a36f29880e94..d2ea67dfdb9b 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -996,7 +996,6 @@ ff_layout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, unsigned int size; u64 p_stripe, r_stripe; u64 stripe_offset; - u64 segment_offset = pgio->pg_lseg->pls_range.offset; u64 stripe_unit = FF_LAYOUT_LSEG(pgio->pg_lseg)->stripe_unit; /* calls nfs_generic_pg_test */ @@ -1008,8 +1007,8 @@ ff_layout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, /* see if req and prev are in the same stripe */ if (prev) { - p_stripe = (u64)req_offset(prev) - segment_offset; - r_stripe = (u64)req_offset(req) - segment_offset; + p_stripe = (u64)req_offset(prev); + r_stripe = (u64)req_offset(req); p_stripe = div64_u64(p_stripe, stripe_unit); r_stripe = div64_u64(r_stripe, stripe_unit); @@ -1018,7 +1017,7 @@ ff_layout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, } /* calculate remaining bytes in the current stripe */ - div64_u64_rem((u64)req_offset(req) - segment_offset, + div64_u64_rem((u64)req_offset(req), stripe_unit, &stripe_offset); WARN_ON_ONCE(stripe_offset > stripe_unit); From 7f2d221b01f356b6bb3bb6efef998f8b28339824 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:09 -0400 Subject: [PATCH 25/68] NFSv4/filelayout: Anchor page coalescing on pattern_offset filelayout_pg_test() bounds page coalescing to a single stripe unit using an offset relative to pls_range.offset, but the data server is selected by nfs4_fl_calc_j_index() using an offset relative to pattern_offset. When a segment's pattern_offset and range offset are not congruent modulo the stripe unit, the coalescing window is shifted off the DS-selection grid, so a coalesced I/O can straddle a stripe boundary and send the bytes past it to the wrong data server. Anchor coalescing on pattern_offset to match nfs4_fl_calc_j_index(). Stripe-congruent segments, including the common whole-file pattern_offset 0 case, are unchanged. Fixes: c6194271f94b ("pnfs: filelayout: support non page aligned layouts") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/filelayout/filelayout.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/nfs/filelayout/filelayout.c b/fs/nfs/filelayout/filelayout.c index 72e20b56fbc7..0d53277f972e 100644 --- a/fs/nfs/filelayout/filelayout.c +++ b/fs/nfs/filelayout/filelayout.c @@ -796,7 +796,7 @@ filelayout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, unsigned int size; u64 p_stripe, r_stripe; u32 stripe_offset; - u64 segment_offset = pgio->pg_lseg->pls_range.offset; + u64 pattern_offset = FILELAYOUT_LSEG(pgio->pg_lseg)->pattern_offset; u32 stripe_unit = FILELAYOUT_LSEG(pgio->pg_lseg)->stripe_unit; /* calls nfs_generic_pg_test */ @@ -808,8 +808,8 @@ filelayout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, /* see if req and prev are in the same stripe */ if (prev) { - p_stripe = (u64)req_offset(prev) - segment_offset; - r_stripe = (u64)req_offset(req) - segment_offset; + p_stripe = (u64)req_offset(prev) - pattern_offset; + r_stripe = (u64)req_offset(req) - pattern_offset; do_div(p_stripe, stripe_unit); do_div(r_stripe, stripe_unit); @@ -818,7 +818,7 @@ filelayout_pg_test(struct nfs_pageio_descriptor *pgio, struct nfs_page *prev, } /* calculate remaining bytes in the current stripe */ - div_u64_rem((u64)req_offset(req) - segment_offset, + div_u64_rem((u64)req_offset(req) - pattern_offset, stripe_unit, &stripe_offset); WARN_ON_ONCE(stripe_offset > stripe_unit); From e85acae4cda89c075bc94520fdcf5901595fffae Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:10 -0400 Subject: [PATCH 26/68] NFSv4/flexfiles: Reference the device node across DS setup The flexfiles I/O setup paths read the mirror's pinned per-stripe device node (mirror->dss[dss_id].mirror_ds) repeatedly and locklessly, relying on the mirror's lifetime pin. To prepare for re-resolving that pointer in place on CB_NOTIFY_DEVICEID CHANGE, readers must hold their own reference on the node they are using rather than trusting the pin. Replace ff_layout_init_mirror_ds() with ff_layout_get_mirror_ds(), which resolves on first use as before but returns the node with its own reference held. Thread the referenced node through nfs4_ff_layout_prepare_ds() and the DS selection helpers so each setup path snapshots the node once, and drop the reference when setup is done. No functional change: the pointer is still resolved once and pinned for the life of the mirror. Each reference this adds is released on every exit from the path that took it, error paths included. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 140 ++++++++++++++-------- fs/nfs/flexfilelayout/flexfilelayout.h | 16 ++- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 78 +++++++----- 3 files changed, 143 insertions(+), 91 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index d2ea67dfdb9b..7a908faa4131 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -875,7 +875,7 @@ ff_layout_mark_ds_reachable(struct pnfs_layout_segment *lseg, u32 idx, u32 dss_i nfs4_mark_deviceid_available(devid); } -static struct nfs4_pnfs_ds * +static struct nfs4_ff_layout_ds * ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, u64 offset, u32 *dss_id, @@ -883,7 +883,9 @@ ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, { struct nfs4_ff_layout_segment *fls = FF_LAYOUT_LSEG(lseg); struct nfs4_ff_layout_mirror *mirror; - struct nfs4_pnfs_ds *ds = ERR_PTR(-EAGAIN); + struct nfs4_ff_layout_ds *mirror_ds; + struct nfs4_ff_layout_ds *ret = ERR_PTR(-EAGAIN); + struct nfs4_pnfs_ds *ds; u32 idx; /* mirrors are initially sorted by efficiency */ @@ -893,25 +895,32 @@ ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, fls->stripe_unit, fls->mirror_array[idx]->dss_count, offset); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, *dss_id, false); - if (IS_ERR(ds)) + mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, + *dss_id); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, + *dss_id, false); + if (IS_ERR(ds)) { + nfs4_ff_layout_put_deviceid(mirror_ds); + ret = ERR_CAST(ds); continue; + } if (check_device && - nfs4_test_deviceid_unavailable(&mirror->dss[*dss_id].mirror_ds->id_node)) { + nfs4_test_deviceid_unavailable(&mirror_ds->id_node)) { + nfs4_ff_layout_put_deviceid(mirror_ds); // reinitialize the error state in case if this is the last iteration - ds = ERR_PTR(-EINVAL); + ret = ERR_PTR(-EINVAL); continue; } *best_idx = idx; - break; + return mirror_ds; } - return ds; + return ret; } -static struct nfs4_pnfs_ds * +static struct nfs4_ff_layout_ds * ff_layout_choose_any_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, u64 offset, u32 *dss_id) @@ -920,7 +929,7 @@ ff_layout_choose_any_ds_for_read(struct pnfs_layout_segment *lseg, offset, dss_id, false); } -static struct nfs4_pnfs_ds * +static struct nfs4_ff_layout_ds * ff_layout_choose_valid_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, u64 offset, u32 *dss_id) @@ -929,34 +938,36 @@ ff_layout_choose_valid_ds_for_read(struct pnfs_layout_segment *lseg, offset, dss_id, true); } -static struct nfs4_pnfs_ds * +static struct nfs4_ff_layout_ds * ff_layout_choose_best_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, u64 offset, u32 *dss_id) { - struct nfs4_pnfs_ds *ds; + struct nfs4_ff_layout_ds *mirror_ds; - ds = ff_layout_choose_valid_ds_for_read(lseg, start_idx, best_idx, - offset, dss_id); - if (!IS_ERR(ds)) - return ds; + mirror_ds = ff_layout_choose_valid_ds_for_read(lseg, start_idx, + best_idx, offset, + dss_id); + if (!IS_ERR(mirror_ds)) + return mirror_ds; return ff_layout_choose_any_ds_for_read(lseg, start_idx, best_idx, offset, dss_id); } -static struct nfs4_pnfs_ds * +static struct nfs4_ff_layout_ds * ff_layout_get_ds_for_read(struct nfs_pageio_descriptor *pgio, u32 *best_idx, u64 offset, u32 *dss_id) { struct pnfs_layout_segment *lseg = pgio->pg_lseg; - struct nfs4_pnfs_ds *ds; + struct nfs4_ff_layout_ds *mirror_ds; - ds = ff_layout_choose_best_ds_for_read(lseg, pgio->pg_mirror_idx, - best_idx, offset, dss_id); - if (!IS_ERR(ds) || !pgio->pg_mirror_idx) - return ds; + mirror_ds = ff_layout_choose_best_ds_for_read(lseg, + pgio->pg_mirror_idx, + best_idx, offset, dss_id); + if (!IS_ERR(mirror_ds) || !pgio->pg_mirror_idx) + return mirror_ds; return ff_layout_choose_best_ds_for_read(lseg, 0, best_idx, offset, dss_id); } @@ -1031,8 +1042,7 @@ ff_layout_pg_init_read(struct nfs_pageio_descriptor *pgio, struct nfs_page *req) { struct nfs_pgio_mirror *pgm; - struct nfs4_ff_layout_mirror *mirror; - struct nfs4_pnfs_ds *ds; + struct nfs4_ff_layout_ds *mirror_ds; u32 ds_idx, dss_id; if (NFS_SERVER(pgio->pg_inode)->flags & @@ -1054,9 +1064,9 @@ ff_layout_pg_init_read(struct nfs_pageio_descriptor *pgio, /* Reset wb_nio, since getting layout segment was successful */ req->wb_nio = 0; - ds = ff_layout_get_ds_for_read(pgio, &ds_idx, - req_offset(req), &dss_id); - if (IS_ERR(ds)) { + mirror_ds = ff_layout_get_ds_for_read(pgio, &ds_idx, + req_offset(req), &dss_id); + if (IS_ERR(mirror_ds)) { if (!ff_layout_no_fallback_to_mds(pgio->pg_lseg)) goto out_mds; pnfs_generic_pg_cleanup(pgio); @@ -1065,9 +1075,9 @@ ff_layout_pg_init_read(struct nfs_pageio_descriptor *pgio, goto retry; } - mirror = FF_LAYOUT_COMP(pgio->pg_lseg, ds_idx); pgm = &pgio->pg_mirrors[0]; - pgm->pg_bsize = mirror->dss[dss_id].mirror_ds->ds_versions[0].rsize; + pgm->pg_bsize = mirror_ds->ds_versions[0].rsize; + nfs4_ff_layout_put_deviceid(mirror_ds); pgio->pg_mirror_idx = ds_idx; return; @@ -1102,6 +1112,7 @@ ff_layout_pg_init_write(struct nfs_pageio_descriptor *pgio, struct nfs_page *req) { struct nfs4_ff_layout_mirror *mirror; + struct nfs4_ff_layout_ds *mirror_ds; struct nfs_pgio_mirror *pgm; struct nfs4_pnfs_ds *ds; u32 i, dss_id; @@ -1133,9 +1144,12 @@ ff_layout_pg_init_write(struct nfs_pageio_descriptor *pgio, FF_LAYOUT_LSEG(pgio->pg_lseg)->stripe_unit, mirror->dss_count, req_offset(req)); + mirror_ds = ff_layout_get_mirror_ds(pgio->pg_lseg->pls_layout, + mirror, dss_id); ds = nfs4_ff_layout_prepare_ds(pgio->pg_lseg, mirror, - dss_id, true); + mirror_ds, dss_id, true); if (IS_ERR(ds)) { + nfs4_ff_layout_put_deviceid(mirror_ds); if (!ff_layout_no_fallback_to_mds(pgio->pg_lseg)) goto out_mds; pnfs_generic_pg_cleanup(pgio); @@ -1144,7 +1158,8 @@ ff_layout_pg_init_write(struct nfs_pageio_descriptor *pgio, goto retry; } pgm = &pgio->pg_mirrors[i]; - pgm->pg_bsize = mirror->dss[dss_id].mirror_ds->ds_versions[0].wsize; + pgm->pg_bsize = mirror_ds->ds_versions[0].wsize; + nfs4_ff_layout_put_deviceid(mirror_ds); } if (NFS_SERVER(pgio->pg_inode)->flags & @@ -1276,14 +1291,16 @@ static void ff_layout_resend_pnfs_read(struct nfs_pgio_header *hdr) u32 idx = hdr->pgio_mirror_idx + 1; u32 new_idx = 0; u32 dss_id = 0; - struct nfs4_pnfs_ds *ds; + struct nfs4_ff_layout_ds *mirror_ds; - ds = ff_layout_choose_any_ds_for_read(hdr->lseg, idx, &new_idx, - hdr->args.offset, &dss_id); - if (IS_ERR(ds)) + mirror_ds = ff_layout_choose_any_ds_for_read(hdr->lseg, idx, &new_idx, + hdr->args.offset, &dss_id); + if (IS_ERR(mirror_ds)) { pnfs_error_mark_layout_for_return(hdr->inode, hdr->lseg); - else + } else { + nfs4_ff_layout_put_deviceid(mirror_ds); ff_layout_send_layouterror(hdr->lseg); + } pnfs_read_resend_pnfs(hdr, new_idx); } @@ -2166,6 +2183,7 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) struct rpc_clnt *ds_clnt; struct nfsd_file *localio; struct nfs4_ff_layout_mirror *mirror; + struct nfs4_ff_layout_ds *mirror_ds; const struct cred *ds_cred; loff_t offset = hdr->args.offset; u32 idx = hdr->pgio_mirror_idx; @@ -2183,22 +2201,24 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) FF_LAYOUT_LSEG(lseg)->stripe_unit, mirror->dss_count, offset); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, dss_id, false); + mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, dss_id); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, false); if (IS_ERR(ds)) { ds_fatal_error = nfs_error_is_fatal(PTR_ERR(ds)); goto out_failed; } - ds_clnt = nfs4_ff_find_or_create_ds_client(mirror, ds->ds_clp, - hdr->inode, dss_id); + ds_clnt = nfs4_ff_find_or_create_ds_client(mirror_ds, ds->ds_clp, + hdr->inode); if (IS_ERR(ds_clnt)) goto out_failed; - ds_cred = ff_layout_get_ds_cred(mirror, &lseg->pls_range, hdr->cred, dss_id); + ds_cred = ff_layout_get_ds_cred(mirror, &lseg->pls_range, hdr->cred, + mirror_ds, dss_id); if (!ds_cred) goto out_failed; - vers = nfs4_ff_layout_ds_version(mirror, dss_id); + vers = nfs4_ff_layout_ds_version(mirror_ds); dprintk("%s USE DS: %s cl_count %d vers %d\n", __func__, ds->ds_remotestr, refcount_read(&ds->ds_clp->cl_count), vers); @@ -2210,7 +2230,8 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) if (fh) hdr->args.fh = fh; - nfs4_ff_layout_select_ds_stateid(mirror, dss_id, &hdr->args.stateid); + nfs4_ff_layout_select_ds_stateid(mirror, mirror_ds, dss_id, + &hdr->args.stateid); /* * Note that if we ever decide to split across DSes, @@ -2233,9 +2254,11 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) &ff_layout_read_call_ops_v4, 0, RPC_TASK_SOFTCONN, localio); put_cred(ds_cred); + nfs4_ff_layout_put_deviceid(mirror_ds); return PNFS_ATTEMPTED; out_failed: + nfs4_ff_layout_put_deviceid(mirror_ds); if (ff_layout_avoid_mds_available_ds(lseg) && !ds_fatal_error) return PNFS_TRY_AGAIN; if (ff_layout_no_fallback_to_mds(lseg)) { @@ -2261,6 +2284,7 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) struct rpc_clnt *ds_clnt; struct nfsd_file *localio; struct nfs4_ff_layout_mirror *mirror; + struct nfs4_ff_layout_ds *mirror_ds; const struct cred *ds_cred; loff_t offset = hdr->args.offset; int vers; @@ -2274,22 +2298,24 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) FF_LAYOUT_LSEG(lseg)->stripe_unit, mirror->dss_count, offset); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, dss_id, true); + mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, dss_id); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, true); if (IS_ERR(ds)) { ds_fatal_error = nfs_error_is_fatal(PTR_ERR(ds)); goto out_failed; } - ds_clnt = nfs4_ff_find_or_create_ds_client(mirror, ds->ds_clp, - hdr->inode, dss_id); + ds_clnt = nfs4_ff_find_or_create_ds_client(mirror_ds, ds->ds_clp, + hdr->inode); if (IS_ERR(ds_clnt)) goto out_failed; - ds_cred = ff_layout_get_ds_cred(mirror, &lseg->pls_range, hdr->cred, dss_id); + ds_cred = ff_layout_get_ds_cred(mirror, &lseg->pls_range, hdr->cred, + mirror_ds, dss_id); if (!ds_cred) goto out_failed; - vers = nfs4_ff_layout_ds_version(mirror, dss_id); + vers = nfs4_ff_layout_ds_version(mirror_ds); dprintk("%s ino %llu sync %d req %zu@%llu DS: %s cl_count %d vers %d\n", __func__, hdr->inode->i_ino, sync, (size_t) hdr->args.count, @@ -2304,7 +2330,8 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) if (fh) hdr->args.fh = fh; - nfs4_ff_layout_select_ds_stateid(mirror, dss_id, &hdr->args.stateid); + nfs4_ff_layout_select_ds_stateid(mirror, mirror_ds, dss_id, + &hdr->args.stateid); /* * Note that if we ever decide to split across DSes, @@ -2326,9 +2353,11 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) &ff_layout_write_call_ops_v4, sync, RPC_TASK_SOFTCONN, localio); put_cred(ds_cred); + nfs4_ff_layout_put_deviceid(mirror_ds); return PNFS_ATTEMPTED; out_failed: + nfs4_ff_layout_put_deviceid(mirror_ds); if (ff_layout_avoid_mds_available_ds(lseg) && !ds_fatal_error) return PNFS_TRY_AGAIN; if (ff_layout_no_fallback_to_mds(lseg)) { @@ -2363,6 +2392,7 @@ static int ff_layout_initiate_commit(struct nfs_commit_data *data, int how) struct rpc_clnt *ds_clnt; struct nfsd_file *localio; struct nfs4_ff_layout_mirror *mirror; + struct nfs4_ff_layout_ds *mirror_ds = NULL; const struct cred *ds_cred; u32 idx, dss_id; int vers, ret; @@ -2375,20 +2405,22 @@ static int ff_layout_initiate_commit(struct nfs_commit_data *data, int how) idx = calc_mirror_idx_from_commit(lseg, data->ds_commit_index); mirror = FF_LAYOUT_COMP(lseg, idx); dss_id = calc_dss_id_from_commit(lseg, data->ds_commit_index); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, dss_id, true); + mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, dss_id); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, true); if (IS_ERR(ds)) goto out_err; - ds_clnt = nfs4_ff_find_or_create_ds_client(mirror, ds->ds_clp, - data->inode, dss_id); + ds_clnt = nfs4_ff_find_or_create_ds_client(mirror_ds, ds->ds_clp, + data->inode); if (IS_ERR(ds_clnt)) goto out_err; - ds_cred = ff_layout_get_ds_cred(mirror, &lseg->pls_range, data->cred, dss_id); + ds_cred = ff_layout_get_ds_cred(mirror, &lseg->pls_range, data->cred, + mirror_ds, dss_id); if (!ds_cred) goto out_err; - vers = nfs4_ff_layout_ds_version(mirror, dss_id); + vers = nfs4_ff_layout_ds_version(mirror_ds); dprintk("%s ino %llu, how %d cl_count %d vers %d\n", __func__, data->inode->i_ino, how, refcount_read(&ds->ds_clp->cl_count), @@ -2414,8 +2446,10 @@ static int ff_layout_initiate_commit(struct nfs_commit_data *data, int how) &ff_layout_commit_call_ops_v4, how, RPC_TASK_SOFTCONN, localio); put_cred(ds_cred); + nfs4_ff_layout_put_deviceid(mirror_ds); return ret; out_err: + nfs4_ff_layout_put_deviceid(mirror_ds); pnfs_generic_prepare_to_resend_writes(data); pnfs_generic_commit_release(data); return -EAGAIN; diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h index ec69cd1c3ae9..f9e491a0347e 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.h +++ b/fs/nfs/flexfilelayout/flexfilelayout.h @@ -207,9 +207,9 @@ ff_layout_no_read_on_rw(struct pnfs_layout_segment *lseg) } static inline int -nfs4_ff_layout_ds_version(const struct nfs4_ff_layout_mirror *mirror, u32 dss_id) +nfs4_ff_layout_ds_version(const struct nfs4_ff_layout_ds *mirror_ds) { - return mirror->dss[dss_id].mirror_ds->ds_versions[0].version; + return mirror_ds->ds_versions[0].version; } static inline u32 @@ -245,23 +245,29 @@ struct nfs_fh * nfs4_ff_layout_select_ds_fh(struct nfs4_ff_layout_mirror *mirror, u32 dss_id); void nfs4_ff_layout_select_ds_stateid(const struct nfs4_ff_layout_mirror *mirror, + const struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id, nfs4_stateid *stateid); +struct nfs4_ff_layout_ds * +ff_layout_get_mirror_ds(struct pnfs_layout_hdr *lo, + struct nfs4_ff_layout_mirror *mirror, + u32 dss_id); struct nfs4_pnfs_ds * nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, struct nfs4_ff_layout_mirror *mirror, + struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id, bool fail_return); struct rpc_clnt * -nfs4_ff_find_or_create_ds_client(struct nfs4_ff_layout_mirror *mirror, +nfs4_ff_find_or_create_ds_client(const struct nfs4_ff_layout_ds *mirror_ds, struct nfs_client *ds_clp, - struct inode *inode, - u32 dss_id); + struct inode *inode); const struct cred *ff_layout_get_ds_cred(struct nfs4_ff_layout_mirror *mirror, const struct pnfs_layout_range *range, const struct cred *mdscred, + const struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id); bool ff_layout_avoid_mds_available_ds(struct pnfs_layout_segment *lseg); bool ff_layout_avoid_read_on_rw(struct pnfs_layout_segment *lseg); diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index 6cd3859d4a24..3712a47b20d0 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -304,24 +304,33 @@ nfs4_ff_layout_select_ds_fh(struct nfs4_ff_layout_mirror *mirror, u32 dss_id) void nfs4_ff_layout_select_ds_stateid(const struct nfs4_ff_layout_mirror *mirror, + const struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id, nfs4_stateid *stateid) { - if (nfs4_ff_layout_ds_version(mirror, dss_id) == 4) + if (nfs4_ff_layout_ds_version(mirror_ds) == 4) nfs4_stateid_copy(stateid, &mirror->dss[dss_id].stateid); } -static bool -ff_layout_init_mirror_ds(struct pnfs_layout_hdr *lo, - struct nfs4_ff_layout_mirror *mirror, - u32 dss_id) +/* + * Resolve the stripe's deviceid on first use and pin the node on the + * mirror. Returns a node the caller must put, or an ERR_PTR. + */ +struct nfs4_ff_layout_ds * +ff_layout_get_mirror_ds(struct pnfs_layout_hdr *lo, + struct nfs4_ff_layout_mirror *mirror, + u32 dss_id) { - if (mirror == NULL) - goto outerr; - if (mirror->dss[dss_id].mirror_ds == NULL) { - struct nfs4_deviceid_node *node; - struct nfs4_ff_layout_ds *mirror_ds = ERR_PTR(-ENODEV); + struct nfs4_ff_layout_ds *mirror_ds; + if (mirror == NULL) + return ERR_PTR(-ENODEV); + + mirror_ds = mirror->dss[dss_id].mirror_ds; + if (mirror_ds == NULL) { + struct nfs4_deviceid_node *node; + + mirror_ds = ERR_PTR(-ENODEV); node = nfs4_find_get_deviceid(NFS_SERVER(lo->plh_inode), &mirror->dss[dss_id].devid, lo->plh_lc_cred, GFP_KERNEL); @@ -332,20 +341,23 @@ ff_layout_init_mirror_ds(struct pnfs_layout_hdr *lo, if (cmpxchg(&mirror->dss[dss_id].mirror_ds, NULL, mirror_ds) && mirror_ds != ERR_PTR(-ENODEV)) nfs4_put_deviceid_node(node); + + mirror_ds = mirror->dss[dss_id].mirror_ds; } - if (IS_ERR(mirror->dss[dss_id].mirror_ds)) - goto outerr; - - return true; -outerr: - return false; + if (IS_ERR(mirror_ds)) + return mirror_ds; + if (!atomic_inc_not_zero(&mirror_ds->id_node.ref)) + return ERR_PTR(-ENODEV); + return mirror_ds; } /** * nfs4_ff_layout_prepare_ds - prepare a DS connection for an RPC call * @lseg: the layout segment we're operating on * @mirror: layout mirror describing the DS to use + * @mirror_ds: referenced device node for the stripe, from + * ff_layout_get_mirror_ds() (may be an ERR_PTR) * @dss_id: DS stripe id to select stripe to use * @fail_return: return layout on connect failure? * @@ -363,6 +375,7 @@ ff_layout_init_mirror_ds(struct pnfs_layout_hdr *lo, struct nfs4_pnfs_ds * nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, struct nfs4_ff_layout_mirror *mirror, + struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id, bool fail_return) { @@ -372,10 +385,10 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, unsigned int max_payload; int status = -EAGAIN; - if (!ff_layout_init_mirror_ds(lseg->pls_layout, mirror, dss_id)) + if (IS_ERR_OR_NULL(mirror_ds)) goto noconnect; - ds = mirror->dss[dss_id].mirror_ds->ds; + ds = mirror_ds->ds; if (READ_ONCE(ds->ds_clp)) goto out; /* matching smp_wmb() in _nfs4_pnfs_v3/4_ds_connect */ @@ -384,11 +397,11 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, /* FIXME: For now we assume the server sent only one version of NFS * to use for the DS. */ - status = nfs4_pnfs_ds_connect(s, ds, &mirror->dss[dss_id].mirror_ds->id_node, + status = nfs4_pnfs_ds_connect(s, ds, &mirror_ds->id_node, dataserver_timeo, dataserver_retrans, - mirror->dss[dss_id].mirror_ds->ds_versions[0].version, - mirror->dss[dss_id].mirror_ds->ds_versions[0].minor_version, - mirror->dss[dss_id].mirror_ds->ds_versions[0].tightly_coupled); + mirror_ds->ds_versions[0].version, + mirror_ds->ds_versions[0].minor_version, + mirror_ds->ds_versions[0].tightly_coupled); /* connect success, check rsize/wsize limit */ if (!status) { @@ -401,10 +414,10 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, max_payload = nfs_block_size(rpc_max_payload(ds->ds_clp->cl_rpcclient), NULL); - if (mirror->dss[dss_id].mirror_ds->ds_versions[0].rsize > max_payload) - mirror->dss[dss_id].mirror_ds->ds_versions[0].rsize = max_payload; - if (mirror->dss[dss_id].mirror_ds->ds_versions[0].wsize > max_payload) - mirror->dss[dss_id].mirror_ds->ds_versions[0].wsize = max_payload; + if (mirror_ds->ds_versions[0].rsize > max_payload) + mirror_ds->ds_versions[0].rsize = max_payload; + if (mirror_ds->ds_versions[0].wsize > max_payload) + mirror_ds->ds_versions[0].wsize = max_payload; goto out; } noconnect: @@ -424,11 +437,12 @@ const struct cred * ff_layout_get_ds_cred(struct nfs4_ff_layout_mirror *mirror, const struct pnfs_layout_range *range, const struct cred *mdscred, + const struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id) { const struct cred *cred; - if (mirror && !mirror->dss[dss_id].mirror_ds->ds_versions[0].tightly_coupled) { + if (mirror && !mirror_ds->ds_versions[0].tightly_coupled) { cred = ff_layout_get_mirror_cred(mirror, range->iomode, dss_id); if (!cred) cred = get_cred(mdscred); @@ -440,20 +454,18 @@ ff_layout_get_ds_cred(struct nfs4_ff_layout_mirror *mirror, /** * nfs4_ff_find_or_create_ds_client - Find or create a DS rpc client - * @mirror: pointer to the mirror + * @mirror_ds: device node for the stripe * @ds_clp: nfs_client for the DS * @inode: pointer to inode - * @dss_id: DS stripe id * * Find or create a DS rpc client with th MDS server rpc client auth flavor * in the nfs_client cl_ds_clients list. */ struct rpc_clnt * -nfs4_ff_find_or_create_ds_client(struct nfs4_ff_layout_mirror *mirror, - struct nfs_client *ds_clp, struct inode *inode, - u32 dss_id) +nfs4_ff_find_or_create_ds_client(const struct nfs4_ff_layout_ds *mirror_ds, + struct nfs_client *ds_clp, struct inode *inode) { - switch (mirror->dss[dss_id].mirror_ds->ds_versions[0].version) { + switch (mirror_ds->ds_versions[0].version) { case 3: /* For NFSv3 DS, flavor is set when creating DS connections */ return ds_clp->cl_rpcclient; From 9be31883a2e59a68478e656433445a3a82ad48fb Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:11 -0400 Subject: [PATCH 27/68] NFSv4/flexfiles: Carry the device node reference across each I/O Rather than dropping the device node reference when DS setup completes, transfer it to the in-flight I/O: carry it on nfs_pgio_header and nfs_commit_data as ds_dev, and release it when the header or commit data is released, alongside the lseg reference. Convert the completion paths to use the carried node instead of re-reading the mirror's pinned pointer: DS error tracking, marking the deviceid available/unavailable, and deleting the deviceid on connection errors now act on the node the I/O was actually sent to. Once a CHANGE notification can re-point the pinned pointer mid-flight, this keeps error attribution on the old device rather than its replacement (and removes a NULL dereference had the pointer been reset to NULL). FF_LAYOUT_DEVID_NODE() is now unused; remove it. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 82 ++++++++++------------- fs/nfs/flexfilelayout/flexfilelayout.h | 15 +---- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 11 +-- fs/nfs/pnfs.c | 2 + fs/nfs/pnfs.h | 8 +++ fs/nfs/pnfs_nfs.c | 1 + include/linux/nfs_xdr.h | 2 + 7 files changed, 58 insertions(+), 63 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 7a908faa4131..928e691af9e7 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -857,24 +857,6 @@ nfs4_ff_layout_stat_io_end_write(struct rpc_task *task, spin_unlock(&mirror->lock); } -static void -ff_layout_mark_ds_unreachable(struct pnfs_layout_segment *lseg, u32 idx, u32 dss_id) -{ - struct nfs4_deviceid_node *devid = FF_LAYOUT_DEVID_NODE(lseg, idx, dss_id); - - if (devid) - nfs4_mark_deviceid_unavailable(devid); -} - -static void -ff_layout_mark_ds_reachable(struct pnfs_layout_segment *lseg, u32 idx, u32 dss_id) -{ - struct nfs4_deviceid_node *devid = FF_LAYOUT_DEVID_NODE(lseg, idx, dss_id); - - if (devid) - nfs4_mark_deviceid_available(devid); -} - static struct nfs4_ff_layout_ds * ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, u32 start_idx, u32 *best_idx, @@ -1333,11 +1315,10 @@ static int ff_layout_async_handle_error_v4(struct rpc_task *task, struct nfs4_state *state, struct nfs_client *clp, struct pnfs_layout_segment *lseg, - u32 idx, u32 dss_id) + struct nfs4_deviceid_node *devid) { struct pnfs_layout_hdr *lo = lseg->pls_layout; struct inode *inode = lo->plh_inode; - struct nfs4_deviceid_node *devid = FF_LAYOUT_DEVID_NODE(lseg, idx, dss_id); struct nfs4_slot_table *tbl = nfs4_has_session(clp) ? &clp->cl_session->fc_slot_table : clp->cl_slot_tbl; @@ -1410,8 +1391,9 @@ static int ff_layout_async_handle_error_v4(struct rpc_task *task, case -ENODEV: dprintk("%s DS connection error %d\n", __func__, task->tk_status); - nfs4_delete_deviceid(devid->ld, devid->nfs_client, - &devid->deviceid); + if (devid) + nfs4_delete_deviceid(devid->ld, devid->nfs_client, + &devid->deviceid); rpc_wake_up(&tbl->slot_tbl_waitq); break; default: @@ -1435,9 +1417,8 @@ static int ff_layout_async_handle_error_v3(struct rpc_task *task, u32 op_status, struct nfs_client *clp, struct pnfs_layout_segment *lseg, - u32 idx, u32 dss_id) + struct nfs4_deviceid_node *devid) { - struct nfs4_deviceid_node *devid = FF_LAYOUT_DEVID_NODE(lseg, idx, dss_id); switch (op_status) { case NFS_OK: @@ -1483,8 +1464,9 @@ static int ff_layout_async_handle_error_v3(struct rpc_task *task, default: dprintk("%s DS connection error %d\n", __func__, task->tk_status); - nfs4_delete_deviceid(devid->ld, devid->nfs_client, - &devid->deviceid); + if (devid) + nfs4_delete_deviceid(devid->ld, devid->nfs_client, + &devid->deviceid); } out_reset_to_pnfs: /* FIXME: Need to prevent infinite looping here. */ @@ -1501,12 +1483,13 @@ static int ff_layout_async_handle_error(struct rpc_task *task, struct nfs4_state *state, struct nfs_client *clp, struct pnfs_layout_segment *lseg, - u32 idx, u32 dss_id) + struct nfs4_deviceid_node *devid) { int vers = clp->cl_nfs_mod->rpc_vers->number; if (task->tk_status >= 0) { - ff_layout_mark_ds_reachable(lseg, idx, dss_id); + if (devid) + nfs4_mark_deviceid_available(devid); return 0; } @@ -1517,10 +1500,10 @@ static int ff_layout_async_handle_error(struct rpc_task *task, switch (vers) { case 3: return ff_layout_async_handle_error_v3(task, op_status, clp, - lseg, idx, dss_id); + lseg, devid); case 4: return ff_layout_async_handle_error_v4(task, op_status, state, - clp, lseg, idx, dss_id); + clp, lseg, devid); default: /* should never happen */ WARN_ON_ONCE(1); @@ -1529,6 +1512,7 @@ static int ff_layout_async_handle_error(struct rpc_task *task, } static void ff_layout_io_track_ds_error(struct pnfs_layout_segment *lseg, + struct nfs4_deviceid_node *devid, u32 idx, u32 dss_id, u64 offset, u64 length, u32 *op_status, int opnum, int error) { @@ -1578,8 +1562,8 @@ static void ff_layout_io_track_ds_error(struct pnfs_layout_segment *lseg, mirror = FF_LAYOUT_COMP(lseg, idx); err = ff_layout_track_ds_error(FF_LAYOUT_FROM_HDR(lseg->pls_layout), - mirror, dss_id, offset, length, status, opnum, - nfs_io_gfp_mask()); + mirror, devid, dss_id, offset, length, + status, opnum, nfs_io_gfp_mask()); /* * I/O we cancelled ourselves to return a recalled or revoked layout @@ -1596,7 +1580,8 @@ static void ff_layout_io_track_ds_error(struct pnfs_layout_segment *lseg, case NFS4ERR_PERM: break; case NFS4ERR_NXIO: - ff_layout_mark_ds_unreachable(lseg, idx, dss_id); + if (devid) + nfs4_mark_deviceid_unavailable(devid); /* * Don't return the layout if this is a read and we still * have layouts to try @@ -1625,7 +1610,7 @@ static int ff_layout_read_done_cb(struct rpc_task *task, int err; if (task->tk_status < 0) { - ff_layout_io_track_ds_error(hdr->lseg, + ff_layout_io_track_ds_error(hdr->lseg, hdr->ds_dev, hdr->pgio_mirror_idx, dss_id, hdr->args.offset, hdr->args.count, &hdr->res.op_status, OP_READ, @@ -1636,8 +1621,7 @@ static int ff_layout_read_done_cb(struct rpc_task *task, err = ff_layout_async_handle_error(task, hdr->res.op_status, hdr->args.context->state, hdr->ds_clp, hdr->lseg, - hdr->pgio_mirror_idx, - dss_id); + hdr->ds_dev); trace_nfs4_pnfs_read(hdr, err); clear_bit(NFS_IOHDR_RESEND_PNFS, &hdr->flags); @@ -1830,7 +1814,7 @@ static int ff_layout_write_done_cb(struct rpc_task *task, int err; if (task->tk_status < 0) { - ff_layout_io_track_ds_error(hdr->lseg, + ff_layout_io_track_ds_error(hdr->lseg, hdr->ds_dev, hdr->pgio_mirror_idx, dss_id, hdr->args.offset, hdr->args.count, &hdr->res.op_status, OP_WRITE, @@ -1841,8 +1825,7 @@ static int ff_layout_write_done_cb(struct rpc_task *task, err = ff_layout_async_handle_error(task, hdr->res.op_status, hdr->args.context->state, hdr->ds_clp, hdr->lseg, - hdr->pgio_mirror_idx, - dss_id); + hdr->ds_dev); trace_nfs4_pnfs_write(hdr, err); clear_bit(NFS_IOHDR_RESEND_PNFS, &hdr->flags); @@ -1884,7 +1867,7 @@ static int ff_layout_commit_done_cb(struct rpc_task *task, u32 dss_id = calc_dss_id_from_commit(data->lseg, data->ds_commit_index); if (task->tk_status < 0) { - ff_layout_io_track_ds_error(data->lseg, idx, dss_id, + ff_layout_io_track_ds_error(data->lseg, data->ds_dev, idx, dss_id, data->args.offset, data->args.count, &data->res.op_status, OP_COMMIT, task->tk_status); @@ -1892,8 +1875,8 @@ static int ff_layout_commit_done_cb(struct rpc_task *task, } err = ff_layout_async_handle_error(task, data->res.op_status, - NULL, data->ds_clp, data->lseg, idx, - dss_id); + NULL, data->ds_clp, data->lseg, + data->ds_dev); trace_nfs4_pnfs_commit_ds(data, err); switch (err) { @@ -2248,13 +2231,16 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) ff_layout_read_record_layoutstats_start(&hdr->task, hdr); } + /* Transfer the device node reference to the I/O; put on release */ + pnfs_put_ds_dev(hdr->ds_dev); + hdr->ds_dev = &mirror_ds->id_node; + /* Perform an asynchronous read to ds */ nfs_initiate_pgio(ds_clnt, hdr, ds_cred, ds->ds_clp->rpc_ops, vers == 3 ? &ff_layout_read_call_ops_v3 : &ff_layout_read_call_ops_v4, 0, RPC_TASK_SOFTCONN, localio); put_cred(ds_cred); - nfs4_ff_layout_put_deviceid(mirror_ds); return PNFS_ATTEMPTED; out_failed: @@ -2347,13 +2333,16 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) ff_layout_write_record_layoutstats_start(&hdr->task, hdr); } + /* Transfer the device node reference to the I/O; put on release */ + pnfs_put_ds_dev(hdr->ds_dev); + hdr->ds_dev = &mirror_ds->id_node; + /* Perform an asynchronous write */ nfs_initiate_pgio(ds_clnt, hdr, ds_cred, ds->ds_clp->rpc_ops, vers == 3 ? &ff_layout_write_call_ops_v3 : &ff_layout_write_call_ops_v4, sync, RPC_TASK_SOFTCONN, localio); put_cred(ds_cred); - nfs4_ff_layout_put_deviceid(mirror_ds); return PNFS_ATTEMPTED; out_failed: @@ -2441,12 +2430,15 @@ static int ff_layout_initiate_commit(struct nfs_commit_data *data, int how) ff_layout_commit_record_layoutstats_start(&data->task, data); } + /* Transfer the device node reference to the commit; put on release */ + pnfs_put_ds_dev(data->ds_dev); + data->ds_dev = &mirror_ds->id_node; + ret = nfs_initiate_commit(ds_clnt, data, ds->ds_clp->rpc_ops, vers == 3 ? &ff_layout_commit_call_ops_v3 : &ff_layout_commit_call_ops_v4, how, RPC_TASK_SOFTCONN, localio); put_cred(ds_cred); - nfs4_ff_layout_put_deviceid(mirror_ds); return ret; out_err: nfs4_ff_layout_put_deviceid(mirror_ds); diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h index f9e491a0347e..7eb47ce09442 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.h +++ b/fs/nfs/flexfilelayout/flexfilelayout.h @@ -162,20 +162,6 @@ FF_LAYOUT_COMP(struct pnfs_layout_segment *lseg, u32 idx) return NULL; } -static inline struct nfs4_deviceid_node * -FF_LAYOUT_DEVID_NODE(struct pnfs_layout_segment *lseg, u32 idx, u32 dss_id) -{ - struct nfs4_ff_layout_mirror *mirror = FF_LAYOUT_COMP(lseg, idx); - - if (mirror != NULL) { - struct nfs4_ff_layout_ds *mirror_ds = mirror->dss[dss_id].mirror_ds; - - if (!IS_ERR_OR_NULL(mirror_ds)) - return &mirror_ds->id_node; - } - return NULL; -} - static inline u32 FF_LAYOUT_MIRROR_COUNT(struct pnfs_layout_segment *lseg) { @@ -232,6 +218,7 @@ void nfs4_ff_layout_put_deviceid(struct nfs4_ff_layout_ds *mirror_ds); void nfs4_ff_layout_free_deviceid(struct nfs4_ff_layout_ds *mirror_ds); int ff_layout_track_ds_error(struct nfs4_flexfile_layout *flo, struct nfs4_ff_layout_mirror *mirror, + const struct nfs4_deviceid_node *devid, u32 dss_id, u64 offset, u64 length, int status, enum nfs_opnum4 opnum, gfp_t gfp_flags); void ff_layout_send_layouterror(struct pnfs_layout_segment *lseg); diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index 3712a47b20d0..ebcfba447879 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -243,6 +243,7 @@ ff_layout_add_ds_error_locked(struct nfs4_flexfile_layout *flo, int ff_layout_track_ds_error(struct nfs4_flexfile_layout *flo, struct nfs4_ff_layout_mirror *mirror, + const struct nfs4_deviceid_node *devid, u32 dss_id, u64 offset, u64 length, int status, enum nfs_opnum4 opnum, gfp_t gfp_flags) { @@ -251,7 +252,7 @@ int ff_layout_track_ds_error(struct nfs4_flexfile_layout *flo, if (status == 0) return 0; - if (IS_ERR_OR_NULL(mirror->dss[dss_id].mirror_ds)) + if (devid == NULL) return -EINVAL; dserr = kmalloc_obj(*dserr, gfp_flags); @@ -264,8 +265,7 @@ int ff_layout_track_ds_error(struct nfs4_flexfile_layout *flo, dserr->status = status; dserr->opnum = opnum; nfs4_stateid_copy(&dserr->stateid, &mirror->dss[dss_id].stateid); - memcpy(&dserr->deviceid, &mirror->dss[dss_id].mirror_ds->id_node.deviceid, - NFS4_DEVICEID4_SIZE); + memcpy(&dserr->deviceid, &devid->deviceid, NFS4_DEVICEID4_SIZE); spin_lock(&flo->generic_hdr.plh_inode->i_lock); ff_layout_add_ds_error_locked(flo, dserr); @@ -422,7 +422,10 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, } noconnect: ff_layout_track_ds_error(FF_LAYOUT_FROM_HDR(lseg->pls_layout), - mirror, dss_id, lseg->pls_range.offset, + mirror, + IS_ERR_OR_NULL(mirror_ds) ? + NULL : &mirror_ds->id_node, + dss_id, lseg->pls_range.offset, lseg->pls_range.length, NFS4ERR_NXIO, OP_ILLEGAL, GFP_NOIO); ff_layout_send_layouterror(lseg); diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 3e8d1a1fd827..a74ffdda5bcd 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -3145,6 +3145,7 @@ pnfs_do_write(struct nfs_pageio_descriptor *desc, static void pnfs_writehdr_free(struct nfs_pgio_header *hdr) { + pnfs_put_ds_dev(hdr->ds_dev); pnfs_put_lseg(hdr->lseg); nfs_pgio_header_free(hdr); } @@ -3290,6 +3291,7 @@ pnfs_do_read(struct nfs_pageio_descriptor *desc, struct nfs_pgio_header *hdr) static void pnfs_readhdr_free(struct nfs_pgio_header *hdr) { + pnfs_put_ds_dev(hdr->ds_dev); pnfs_put_lseg(hdr->lseg); nfs_pgio_header_free(hdr); } diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 5725ea06efc2..a2c70573dda5 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -385,6 +385,14 @@ void nfs4_init_deviceid_node(struct nfs4_deviceid_node *, struct nfs_server *, const struct nfs4_deviceid *); bool nfs4_put_deviceid_node(struct nfs4_deviceid_node *); void nfs4_mark_deviceid_available(struct nfs4_deviceid_node *node); + +/* Put the device node reference carried by an in-flight I/O, if any */ +static inline void pnfs_put_ds_dev(struct nfs4_deviceid_node *dev) +{ + if (dev) + nfs4_put_deviceid_node(dev); +} + void nfs4_mark_deviceid_unavailable(struct nfs4_deviceid_node *node); bool nfs4_test_deviceid_unavailable(struct nfs4_deviceid_node *node); void nfs4_deviceid_purge_client(const struct nfs_client *); diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c index 5bd014b981bf..e0e3fc7414e6 100644 --- a/fs/nfs/pnfs_nfs.c +++ b/fs/nfs/pnfs_nfs.c @@ -55,6 +55,7 @@ void pnfs_generic_commit_release(void *calldata) struct nfs_commit_data *data = calldata; data->completion_ops->completion(data); + pnfs_put_ds_dev(data->ds_dev); pnfs_put_lseg(data->lseg); nfs_put_client(data->ds_clp); nfs_commitdata_release(data); diff --git a/include/linux/nfs_xdr.h b/include/linux/nfs_xdr.h index 7ed8fdb930d6..c0e29b4dfa62 100644 --- a/include/linux/nfs_xdr.h +++ b/include/linux/nfs_xdr.h @@ -1693,6 +1693,7 @@ struct nfs_pgio_header { struct nfs_client *ds_clp; /* pNFS data server */ u32 ds_commit_idx; /* ds index if ds_clp is set */ u32 pgio_mirror_idx;/* mirror index in pgio layer */ + struct nfs4_deviceid_node *ds_dev; /* device node ref held across the I/O */ }; struct nfs_mds_commit_info { @@ -1731,6 +1732,7 @@ struct nfs_commit_data { struct nfs_open_context *context; struct pnfs_layout_segment *lseg; struct nfs_client *ds_clp; /* pNFS data server */ + struct nfs4_deviceid_node *ds_dev; /* device node ref held across the commit */ int ds_commit_index; loff_t lwb; const struct rpc_call_ops *mds_ops; From dbde656f2f7a8dced33413c83a0cb6b634bfb26e Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:12 -0400 Subject: [PATCH 28/68] NFSv4/flexfiles: Hold a device node reference for layoutstats encoding ff_layout_mirror_prepare_stats() records a stripe's device under i_lock, but the layoutstats/layoutreturn XDR encode runs later and dereferences the mirror's pinned device node (for the DS netaddr) with no reference of its own. Once a CHANGE notification can re-point the pinned node, that read becomes use-after-free. Take a node reference at prepare time and carry it with the stripe in a per-devinfo nfs4_ff_layoutstat_priv, released in ff_layout_free_layoutstats(). The priv entries live alongside the devinfo array: co-allocated for LAYOUTSTATS, an additional member of nfs4_flexfile_layoutreturn_args for LAYOUTRETURN. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 53 +++++++++++++++++--------- fs/nfs/flexfilelayout/flexfilelayout.h | 11 ++++++ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 928e691af9e7..ecdacaae80c6 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -44,10 +44,11 @@ static void ff_layout_read_record_layoutstats_done(struct rpc_task *task, static int ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, struct nfs42_layoutstat_devinfo *devinfo, + struct nfs4_ff_layoutstat_priv *priv, int dev_limit, enum nfs4_ff_op_type type); static void ff_layout_encode_ff_layoutupdate(struct xdr_stream *xdr, const struct nfs42_layoutstat_devinfo *devinfo, - struct nfs4_ff_layout_ds_stripe *dss_info); + struct nfs4_ff_layoutstat_priv *priv); static struct pnfs_layout_hdr * ff_layout_alloc_layout_hdr(struct inode *inode, gfp_t gfp_flags) @@ -2725,7 +2726,7 @@ ff_layout_prepare_layoutreturn(struct nfs4_layoutreturn_args *args) spin_lock(&args->inode->i_lock); ff_args->num_dev = ff_layout_mirror_prepare_stats( - &ff_layout->generic_hdr, &ff_args->devinfo[0], + &ff_layout->generic_hdr, &ff_args->devinfo[0], &ff_args->priv[0], ARRAY_SIZE(ff_args->devinfo), NFS4_FF_OP_LAYOUTRETURN); spin_unlock(&args->inode->i_lock); @@ -2900,10 +2901,11 @@ ff_layout_encode_io_latency(struct xdr_stream *xdr, static void ff_layout_encode_ff_layoutupdate(struct xdr_stream *xdr, const struct nfs42_layoutstat_devinfo *devinfo, - struct nfs4_ff_layout_ds_stripe *dss_info) + struct nfs4_ff_layoutstat_priv *priv) { + struct nfs4_ff_layout_ds_stripe *dss_info = priv->dss_info; struct nfs4_pnfs_ds_addr *da; - struct nfs4_pnfs_ds *ds = dss_info->mirror_ds->ds; + struct nfs4_pnfs_ds *ds = priv->mirror_ds->ds; struct nfs_fh *fh = &dss_info->fh_versions[0]; __be32 *p; @@ -2950,10 +2952,10 @@ ff_layout_encode_layoutstats(struct xdr_stream *xdr, const void *args, static void ff_layout_free_layoutstats(struct nfs4_xdr_opaque_data *opaque) { - struct nfs4_ff_layout_ds_stripe *dss_info = opaque->data; - struct nfs4_ff_layout_mirror *mirror = dss_info->mirror; + struct nfs4_ff_layoutstat_priv *priv = opaque->data; - ff_layout_put_mirror(mirror); + nfs4_ff_layout_put_deviceid(priv->mirror_ds); + ff_layout_put_mirror(priv->dss_info->mirror); } static const struct nfs4_xdr_opaque_ops layoutstat_ops = { @@ -2964,12 +2966,13 @@ static const struct nfs4_xdr_opaque_ops layoutstat_ops = { static int ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, struct nfs42_layoutstat_devinfo *devinfo, + struct nfs4_ff_layoutstat_priv *priv, int dev_limit, enum nfs4_ff_op_type type) { struct nfs4_flexfile_layout *ff_layout = FF_LAYOUT_FROM_HDR(lo); struct nfs4_ff_layout_mirror *mirror; struct nfs4_ff_layout_ds_stripe *dss_info; - struct nfs4_deviceid_node *dev; + struct nfs4_ff_layout_ds *mirror_ds; int i = 0, dss_id; list_for_each_entry(mirror, &ff_layout->mirrors, mirrors) { @@ -2977,7 +2980,8 @@ ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, dss_info = &mirror->dss[dss_id]; if (i >= dev_limit) break; - if (IS_ERR_OR_NULL(dss_info->mirror_ds)) + mirror_ds = dss_info->mirror_ds; + if (IS_ERR_OR_NULL(mirror_ds)) continue; if (!test_and_clear_bit(NFS4_FF_MIRROR_STAT_AVAIL, &mirror->flags) && @@ -2986,9 +2990,14 @@ ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, /* mirror refcount put in cleanup_layoutstats */ if (!refcount_inc_not_zero(&mirror->ref)) continue; - dev = &dss_info->mirror_ds->id_node; + /* + * The mirror's pin holds the node while we're under + * i_lock; take a reference for the encode, put in + * ff_layout_free_layoutstats(). + */ + atomic_inc(&mirror_ds->id_node.ref); memcpy(&devinfo->dev_id, - &dev->deviceid, + &mirror_ds->id_node.deviceid, NFS4_DEVICEID4_SIZE); devinfo->offset = 0; devinfo->length = NFS4_MAX_UINT64; @@ -3004,9 +3013,12 @@ ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, spin_unlock(&mirror->lock); devinfo->layout_type = LAYOUT_FLEX_FILES; devinfo->ld_private.ops = &layoutstat_ops; - devinfo->ld_private.data = &mirror->dss[dss_id]; + priv->dss_info = dss_info; + priv->mirror_ds = mirror_ds; + devinfo->ld_private.data = priv; devinfo++; + priv++; i++; } } @@ -3017,21 +3029,28 @@ static int ff_layout_prepare_layoutstats(struct nfs42_layoutstat_args *args) { struct pnfs_layout_hdr *lo; struct nfs4_flexfile_layout *ff_layout; + struct nfs4_ff_layoutstat_priv *priv; const int dev_count = PNFS_LAYOUTSTATS_MAXDEV; - /* For now, send at most PNFS_LAYOUTSTATS_MAXDEV statistics */ - args->devinfo = kmalloc_objs(*args->devinfo, dev_count, - nfs_io_gfp_mask()); + /* + * For now, send at most PNFS_LAYOUTSTATS_MAXDEV statistics. + * The per-devinfo private entries are co-allocated after the + * devinfo array and freed along with it. + */ + args->devinfo = kmalloc(dev_count * (sizeof(*args->devinfo) + + sizeof(*priv)), + nfs_io_gfp_mask()); if (!args->devinfo) return -ENOMEM; + priv = (struct nfs4_ff_layoutstat_priv *)&args->devinfo[dev_count]; spin_lock(&args->inode->i_lock); lo = NFS_I(args->inode)->layout; if (lo && pnfs_layout_is_valid(lo)) { ff_layout = FF_LAYOUT_FROM_HDR(lo); args->num_dev = ff_layout_mirror_prepare_stats( - &ff_layout->generic_hdr, &args->devinfo[0], dev_count, - NFS4_FF_OP_LAYOUTSTATS); + &ff_layout->generic_hdr, &args->devinfo[0], priv, + dev_count, NFS4_FF_OP_LAYOUTSTATS); } else args->num_dev = 0; spin_unlock(&args->inode->i_lock); diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h index 7eb47ce09442..8b42a98a2c4c 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.h +++ b/fs/nfs/flexfilelayout/flexfilelayout.h @@ -124,9 +124,20 @@ struct nfs4_flexfile_layout { unsigned long flags; }; +/* + * Per-devinfo private data for a layoutstats/layoutreturn encode: the + * stripe the stats describe plus a reference on its device node so the + * node (and its DS addresses) stay valid until the XDR encode runs. + */ +struct nfs4_ff_layoutstat_priv { + struct nfs4_ff_layout_ds_stripe *dss_info; + struct nfs4_ff_layout_ds *mirror_ds; +}; + struct nfs4_flexfile_layoutreturn_args { struct list_head errors; struct nfs42_layoutstat_devinfo devinfo[FF_LAYOUTSTATS_MAXDEV]; + struct nfs4_ff_layoutstat_priv priv[FF_LAYOUTSTATS_MAXDEV]; unsigned int num_errors; unsigned int num_dev; struct page *pages[1]; From b14c22e01c5c4f8dfe9c7165c801796e1ec6ebae Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:13 -0400 Subject: [PATCH 29/68] NFSv4/flexfiles: Make the pinned device node pointer RCU-managed Annotate mirror->dss[dss_id].mirror_ds as __rcu and convert the remaining readers, completing the preparation for re-pointing the pinned node while I/O is in flight: - ff_layout_get_mirror_ds() takes its reference under rcu_read_lock() with atomic_inc_not_zero(), retrying if it races a reset. The resolve path takes the caller's reference before publishing the node with cmpxchg(), so a concurrent reset cannot drop the last reference under the caller: one reference is held for the installed pointer and one for the caller, and the pointer's reference is released elsewhere by xchg + put. - The availability scans hold rcu_read_lock() across the walk; they only test flags on the RCU-freed node. - ff_layout_cancel_io() holds a reference across the cancel and disconnect calls. Both of its callers hold i_lock and nothing on that path sleeps, so the reference is not about blocking: it is what keeps the node alive between the RCU-protected read and the use of mirror_ds->ds. The final put is never reached here, because the mirror's own pin outlives the loop -- which matters, since that put ends in nfs_put_client() and cannot run under a spinlock. - ff_layout_mirror_prepare_stats() reads the pointer with rcu_dereference() under rcu_read_lock(). i_lock, which both callers hold, is what excludes the re-pointing walk added later in this series; it does not exclude the resolve path's cmpxchg(), which runs from I/O submission, so the read cannot claim i_lock as its update- side lock. A non-NULL pointer read there is stable regardless: the resolve path only ever installs over NULL. - ff_layout_free_mirror() tears down the last reference; no concurrency. The pointer is still only ever set once per mirror lifetime, so there is no behavior change; this commit makes the subsequent in-place re-resolve on CB_NOTIFY_DEVICEID CHANGE safe to introduce. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 35 ++++--- fs/nfs/flexfilelayout/flexfilelayout.h | 2 +- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 112 ++++++++++++++-------- 3 files changed, 94 insertions(+), 55 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index ecdacaae80c6..6edebc0e73a2 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -313,7 +313,9 @@ static void ff_layout_free_mirror(struct nfs4_ff_layout_mirror *mirror) cred = rcu_access_pointer(mirror->dss[dss_id].rw_cred); put_cred(cred); nfs_close_local_fh(&mirror->dss[dss_id].nfl); - nfs4_ff_layout_put_deviceid(mirror->dss[dss_id].mirror_ds); + /* the last reference to the mirror is gone; no concurrency */ + nfs4_ff_layout_put_deviceid(rcu_dereference_protected( + mirror->dss[dss_id].mirror_ds, 1)); } kvfree(mirror->dss); @@ -2498,22 +2500,29 @@ static void ff_layout_cancel_io(struct pnfs_layout_segment *lseg) for (idx = 0; idx < flseg->mirror_array_cnt; idx++) { mirror = flseg->mirror_array[idx]; for (dss_id = 0; dss_id < mirror->dss_count; dss_id++) { - mirror_ds = mirror->dss[dss_id].mirror_ds; - if (IS_ERR_OR_NULL(mirror_ds)) + rcu_read_lock(); + mirror_ds = rcu_dereference(mirror->dss[dss_id].mirror_ds); + if (IS_ERR_OR_NULL(mirror_ds) || + !atomic_inc_not_zero(&mirror_ds->id_node.ref)) { + rcu_read_unlock(); continue; - ds = mirror->dss[dss_id].mirror_ds->ds; + } + rcu_read_unlock(); + ds = mirror_ds->ds; if (!ds) - continue; + goto next; ds_clp = ds->ds_clp; if (!ds_clp) - continue; + goto next; clnt = ds_clp->cl_rpcclient; if (!clnt) - continue; + goto next; if (!rpc_cancel_tasks(clnt, -ECANCELED, ff_layout_match_io, lseg)) - continue; + goto next; rpc_clnt_disconnect(clnt); +next: + nfs4_ff_layout_put_deviceid(mirror_ds); } } } @@ -2975,12 +2984,13 @@ ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, struct nfs4_ff_layout_ds *mirror_ds; int i = 0, dss_id; + rcu_read_lock(); list_for_each_entry(mirror, &ff_layout->mirrors, mirrors) { for (dss_id = 0; dss_id < mirror->dss_count; ++dss_id) { dss_info = &mirror->dss[dss_id]; if (i >= dev_limit) break; - mirror_ds = dss_info->mirror_ds; + mirror_ds = rcu_dereference(dss_info->mirror_ds); if (IS_ERR_OR_NULL(mirror_ds)) continue; if (!test_and_clear_bit(NFS4_FF_MIRROR_STAT_AVAIL, @@ -2990,10 +3000,8 @@ ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, /* mirror refcount put in cleanup_layoutstats */ if (!refcount_inc_not_zero(&mirror->ref)) continue; - /* - * The mirror's pin holds the node while we're under - * i_lock; take a reference for the encode, put in - * ff_layout_free_layoutstats(). + /* The pin holds a reference; it is exchanged out only + * under i_lock. Put in ff_layout_free_layoutstats(). */ atomic_inc(&mirror_ds->id_node.ref); memcpy(&devinfo->dev_id, @@ -3022,6 +3030,7 @@ ff_layout_mirror_prepare_stats(struct pnfs_layout_hdr *lo, i++; } } + rcu_read_unlock(); return i; } diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h index 8b42a98a2c4c..72b11034851a 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.h +++ b/fs/nfs/flexfilelayout/flexfilelayout.h @@ -79,7 +79,7 @@ struct nfs4_ff_layout_ds_stripe { struct nfs4_ff_layout_mirror *mirror; struct nfs4_deviceid devid; u32 efficiency; - struct nfs4_ff_layout_ds *mirror_ds; + struct nfs4_ff_layout_ds __rcu *mirror_ds; u32 fh_versions_cnt; struct nfs_fh *fh_versions; nfs4_stateid stateid; diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index ebcfba447879..5cb09e5e2138 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -321,35 +321,54 @@ ff_layout_get_mirror_ds(struct pnfs_layout_hdr *lo, struct nfs4_ff_layout_mirror *mirror, u32 dss_id) { - struct nfs4_ff_layout_ds *mirror_ds; + struct nfs4_ff_layout_ds *mirror_ds, *old; + struct nfs4_deviceid_node *node; if (mirror == NULL) return ERR_PTR(-ENODEV); - mirror_ds = mirror->dss[dss_id].mirror_ds; - if (mirror_ds == NULL) { - struct nfs4_deviceid_node *node; - - mirror_ds = ERR_PTR(-ENODEV); - node = nfs4_find_get_deviceid(NFS_SERVER(lo->plh_inode), - &mirror->dss[dss_id].devid, lo->plh_lc_cred, - GFP_KERNEL); - if (node) - mirror_ds = FF_LAYOUT_MIRROR_DS(node); - - /* check for race with another call to this function */ - if (cmpxchg(&mirror->dss[dss_id].mirror_ds, NULL, mirror_ds) && - mirror_ds != ERR_PTR(-ENODEV)) - nfs4_put_deviceid_node(node); - - mirror_ds = mirror->dss[dss_id].mirror_ds; +retry: + rcu_read_lock(); + mirror_ds = rcu_dereference(mirror->dss[dss_id].mirror_ds); + if (mirror_ds && !IS_ERR(mirror_ds) && + atomic_inc_not_zero(&mirror_ds->id_node.ref)) { + rcu_read_unlock(); + return mirror_ds; } - + rcu_read_unlock(); if (IS_ERR(mirror_ds)) return mirror_ds; - if (!atomic_inc_not_zero(&mirror_ds->id_node.ref)) - return ERR_PTR(-ENODEV); - return mirror_ds; + if (mirror_ds != NULL) + /* raced with a reset; the field is being re-pointed */ + goto retry; + + node = nfs4_find_get_deviceid(NFS_SERVER(lo->plh_inode), + &mirror->dss[dss_id].devid, lo->plh_lc_cred, + GFP_KERNEL); + if (node) { + mirror_ds = FF_LAYOUT_MIRROR_DS(node); + /* + * Take the caller's reference before the pointer becomes + * visible below, so a concurrent reset of the installed + * pointer cannot drop the last reference under us. + */ + atomic_inc(&node->ref); + } else { + mirror_ds = ERR_PTR(-ENODEV); + } + + /* check for race with another call to this function */ + old = unrcu_pointer(cmpxchg(&mirror->dss[dss_id].mirror_ds, + NULL, RCU_INITIALIZER(mirror_ds))); + if (old == NULL) + return mirror_ds; + + /* lost the race; use the winner's node instead */ + if (node) { + nfs4_put_deviceid_node(node); + nfs4_put_deviceid_node(node); + } + goto retry; } /** @@ -573,49 +592,60 @@ unsigned int ff_layout_fetch_ds_ioerr(struct pnfs_layout_hdr *lo, static bool ff_read_layout_has_available_ds(struct pnfs_layout_segment *lseg) { struct nfs4_ff_layout_mirror *mirror; - struct nfs4_deviceid_node *devid; + struct nfs4_ff_layout_ds *mirror_ds; + bool ret = false; u32 idx, dss_id; + rcu_read_lock(); for (idx = 0; idx < FF_LAYOUT_MIRROR_COUNT(lseg); idx++) { mirror = FF_LAYOUT_COMP(lseg, idx); if (!mirror) continue; for (dss_id = 0; dss_id < mirror->dss_count; dss_id++) { - if (!mirror->dss[dss_id].mirror_ds) - return true; - if (IS_ERR(mirror->dss[dss_id].mirror_ds)) + mirror_ds = rcu_dereference(mirror->dss[dss_id].mirror_ds); + if (!mirror_ds) { + ret = true; + goto out; + } + if (IS_ERR(mirror_ds)) continue; - devid = &mirror->dss[dss_id].mirror_ds->id_node; - if (!nfs4_test_deviceid_unavailable(devid)) - return true; + if (!nfs4_test_deviceid_unavailable(&mirror_ds->id_node)) { + ret = true; + goto out; + } } } - - return false; +out: + rcu_read_unlock(); + return ret; } static bool ff_rw_layout_has_available_ds(struct pnfs_layout_segment *lseg) { struct nfs4_ff_layout_mirror *mirror; - struct nfs4_deviceid_node *devid; + struct nfs4_ff_layout_ds *mirror_ds; + bool ret = false; u32 idx, dss_id; + rcu_read_lock(); for (idx = 0; idx < FF_LAYOUT_MIRROR_COUNT(lseg); idx++) { mirror = FF_LAYOUT_COMP(lseg, idx); if (!mirror) - return false; + goto out; for (dss_id = 0; dss_id < mirror->dss_count; dss_id++) { - if (IS_ERR(mirror->dss[dss_id].mirror_ds)) - return false; - if (!mirror->dss[dss_id].mirror_ds) + mirror_ds = rcu_dereference(mirror->dss[dss_id].mirror_ds); + if (IS_ERR(mirror_ds)) + goto out; + if (!mirror_ds) continue; - devid = &mirror->dss[dss_id].mirror_ds->id_node; - if (nfs4_test_deviceid_unavailable(devid)) - return false; + if (nfs4_test_deviceid_unavailable(&mirror_ds->id_node)) + goto out; } } - - return FF_LAYOUT_MIRROR_COUNT(lseg) != 0; + ret = FF_LAYOUT_MIRROR_COUNT(lseg) != 0; +out: + rcu_read_unlock(); + return ret; } static bool ff_layout_has_available_ds(struct pnfs_layout_segment *lseg) From 6d050533f38783c8534151c75c0e06800dd558e7 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:14 -0400 Subject: [PATCH 30/68] pNFS: Add a reresolve_deviceid layout driver hook RFC 8881 Section 12.2.10 lets a server change a deviceid's mapping under live layouts by sending CB_NOTIFY_DEVICEID CHANGE instead of recalling the layouts, but the client's only response today is to unhash the cached device, which never reaches references pinned inside a layout driver's segments. Add a reresolve_deviceid hook to pnfs_layoutdriver_type and a generic driver, pnfs_layout_reresolve_deviceid_byclid(), that walks every layout on every server of the client using that driver and invokes the hook under the layout inode's i_lock. Because the final put of a device node can sleep (it may tear down the DS nfs_client), the hook must not drop references itself: for each node it un-pins it allocates an nfs4_deviceid_put entry and queues it on a list, and the generic driver puts the node and frees the entry once all locks are dropped. A deviceid node is a shared, refcounted object, so one re-resolve pass can unpin the same node more than once (multiple stripes, or multiple layouts over a common data server); a per-reference entry expresses that, where a single list_head embedded in the node could not. The walk is deliberately not gated on pnfs_layout_is_valid(). A header with NFS_LAYOUT_INVALID_STID set can still carry lsegs whose mirrors pin the stale node -- pnfs_mark_layout_stateid_invalid() sets the bit and reports whether segments were left behind -- and by the time the walk runs the cached device has already been unhashed, so nothing would re-resolve that pin later. Worse, a subsequent LAYOUTGET on the same header can pick the surviving mirror back up (the driver dedups mirrors by deviceid and filehandle) and carry the old mapping into a fresh layout. Un-pinning a device node does not touch the layout stateid, so the hook has no need of a valid one; it walks only the driver's mirror list, which i_lock protects, and the header cannot be freed under the walk because the driver frees it with kfree_rcu(). Note this differs from the reference-collection walker added later in the series, which does take a layout header reference and therefore does depend on the validity check for its refcount argument. No driver implements the hook yet, and nothing calls the walker: no behavior change. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/pnfs.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ fs/nfs/pnfs.h | 25 +++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index a74ffdda5bcd..69ea9abe28c9 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -2917,6 +2917,68 @@ pnfs_layout_return_unused_byclid(struct nfs_client *clp, &range); } +struct pnfs_reresolve_deviceid_args { + const struct pnfs_layoutdriver_type *ld; + const struct nfs4_deviceid *devid; + bool immediate; + struct list_head put_list; +}; + +static int pnfs_layout_reresolve_deviceid_byserver(struct nfs_server *server, + void *data) +{ + struct pnfs_reresolve_deviceid_args *args = data; + struct pnfs_layout_hdr *lo; + struct inode *inode; + + if (server->pnfs_curr_ld != args->ld) + return 0; + + rcu_read_lock(); + list_for_each_entry_rcu(lo, &server->layouts, plh_layouts) { + inode = lo->plh_inode; + if (!inode) + continue; + spin_lock(&inode->i_lock); + args->ld->reresolve_deviceid(lo, args->devid, args->immediate, + &args->put_list); + spin_unlock(&inode->i_lock); + } + rcu_read_unlock(); + return 0; +} + +/* + * Invoke @ld's reresolve_deviceid hook for @devid on every layout of @clp's + * servers, then drain the put_list once the locks are dropped. + */ +void +pnfs_layout_reresolve_deviceid_byclid(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *devid, + bool immediate) +{ + struct pnfs_reresolve_deviceid_args args = { + .ld = ld, + .devid = devid, + .immediate = immediate, + .put_list = LIST_HEAD_INIT(args.put_list), + }; + struct nfs4_deviceid_put *put, *tmp; + + if (!ld->reresolve_deviceid) + return; + + nfs_client_for_each_server(clp, + pnfs_layout_reresolve_deviceid_byserver, &args); + + list_for_each_entry_safe(put, tmp, &args.put_list, node) { + list_del(&put->node); + nfs4_put_deviceid_node(put->dev); + kfree(put); + } +} + /* Check if we have we have a valid layout but if there isn't an intersection * between the request and the pgio->pg_lseg, put this pgio->pg_lseg away. */ diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index a2c70573dda5..bb1ca7ba0221 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -171,6 +171,19 @@ struct pnfs_layoutdriver_type { struct nfs4_deviceid_node * (*alloc_deviceid_node) (struct nfs_server *server, struct pnfs_device *pdev, gfp_t gfp_flags); + /* + * Re-resolve @lo's references to the changed deviceid @id. Called + * under @lo's inode i_lock inside an RCU read-side critical section: + * must not sleep, allocations are GFP_ATOMIC. Rather than put the + * references it gives up (the final put can sleep), the hook + * allocates an nfs4_deviceid_put per reference and queues it on + * @put_list for the caller to put and free. On allocation failure + * it must leave the reference in place. + */ + void (*reresolve_deviceid)(struct pnfs_layout_hdr *lo, + const struct nfs4_deviceid *id, + bool immediate, + struct list_head *put_list); int (*prepare_layoutreturn) (struct nfs4_layoutreturn_args *); @@ -354,6 +367,10 @@ void pnfs_error_mark_layout_for_return(struct inode *inode, struct pnfs_layout_segment *lseg); void pnfs_layout_return_unused_byclid(struct nfs_client *clp, enum pnfs_iomode iomode); +void pnfs_layout_reresolve_deviceid_byclid(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *devid, + bool immediate); int pnfs_layout_handle_reboot(struct nfs_client *clp); /* nfs4_deviceid_flags */ @@ -376,6 +393,14 @@ struct nfs4_deviceid_node { atomic_t ref; }; +/* One reference given up by reresolve_deviceid; nodes are shared, so a + * single pass can unpin the same node more than once. + */ +struct nfs4_deviceid_put { + struct list_head node; + struct nfs4_deviceid_node *dev; +}; + struct nfs4_deviceid_node * nfs4_find_get_deviceid(struct nfs_server *server, const struct nfs4_deviceid *id, const struct cred *cred, From d599d14a2d738cda094f814ea485cc39949c7ea8 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:15 -0400 Subject: [PATCH 31/68] NFSv4/flexfiles: Implement in-place device re-resolve on CHANGE Implement the reresolve_deviceid hook: walk the layout's mirrors and, for every stripe whose raw deviceid matches the changed one, exchange the pinned device node out for NULL. In-flight I/O completes on the old node through its own reference; the next I/O to the stripe re-resolves via ff_layout_get_mirror_ds() and picks up the server's new mapping with a fresh GETDEVICEINFO. The un-pinned references are handed back on the walker's list to be put outside the locks. Matching uses the raw deviceid decoded from the layout (dss[].devid), so a stripe that was never resolved, or already exchanged, is left alone. A stripe whose resolution previously failed holds an ERR_PTR sentinel rather than a node; that is cleared too, so the stripe retries GETDEVICEINFO on its next I/O. Nothing dispatches CHANGE to the walker yet: no behavior change. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 6edebc0e73a2..55057f8f9a77 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -2527,6 +2527,45 @@ static void ff_layout_cancel_io(struct pnfs_layout_segment *lseg) } } +/* + * Un-pin every stripe node resolved from @id: in-flight I/O drains on the + * old node through its own reference, the next I/O re-resolves. + */ +static void ff_layout_reresolve_deviceid(struct pnfs_layout_hdr *lo, + const struct nfs4_deviceid *id, + bool immediate, + struct list_head *head) +{ + struct nfs4_flexfile_layout *flo = FF_LAYOUT_FROM_HDR(lo); + struct nfs4_ff_layout_mirror *mirror; + struct nfs4_ff_layout_ds *old; + struct nfs4_deviceid_put *put; + u32 dss_id; + + list_for_each_entry(mirror, &flo->mirrors, mirrors) { + for (dss_id = 0; dss_id < mirror->dss_count; dss_id++) { + if (memcmp(&mirror->dss[dss_id].devid, id, + sizeof(*id)) != 0) + continue; + /* Allocate before un-pinning: on failure the reference + * stays put rather than being dropped here, where the + * final put may not sleep. + */ + put = kzalloc_obj(*put, GFP_ATOMIC); + if (!put) + continue; + old = unrcu_pointer( + xchg(&mirror->dss[dss_id].mirror_ds, NULL)); + if (IS_ERR_OR_NULL(old)) { + kfree(put); + continue; + } + put->dev = &old->id_node; + list_add(&put->node, head); + } + } +} + static struct pnfs_ds_commit_info * ff_layout_get_ds_info(struct inode *inode) { @@ -3108,6 +3147,7 @@ static struct pnfs_layoutdriver_type flexfilelayout_type = { .pg_write_ops = &ff_layout_pg_write_ops, .get_ds_info = ff_layout_get_ds_info, .free_deviceid_node = ff_layout_free_deviceid_node, + .reresolve_deviceid = ff_layout_reresolve_deviceid, .read_pagelist = ff_layout_read_pagelist, .write_pagelist = ff_layout_write_pagelist, .alloc_deviceid_node = ff_layout_alloc_deviceid_node, From 39637450546c1692c4928a7eb46522c8e0dba3a0 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:16 -0400 Subject: [PATCH 32/68] NFSv4: Dispatch CB_NOTIFY_DEVICEID CHANGE to an in-place refresh nfs4_callback_devicenotify() treated CHANGE identically to DELETE: both only unhashed the cached device, so references pinned under live layouts kept sending I/O to the old mapping until the layouts were freed. Per RFC 8881 Section 12.2.10, CHANGE exists precisely so a server can change a mapping without recalling the layouts. For CHANGE, after unhashing the stale cache entry (so re-resolution cannot re-pin it), invoke the layout driver's re-resolve walker. The walker does not install the new mapping itself: it un-pins the stale node so the next I/O to that stripe re-resolves and fetches the new one, while I/O already in flight completes on the old node through its own reference. DELETE is unchanged, and drivers without a reresolve_deviceid hook see no change. Re-resolution is best effort. A lookup that hit the cache just before the unhash can still install that node after the walk has passed the stripe, and the walk skips nothing else; such a stripe keeps the old mapping until the next notification. Nothing is left inconsistent by that -- the old mapping is a valid address the server published -- so the race is tolerated rather than serialised against. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/callback_proc.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/nfs/callback_proc.c b/fs/nfs/callback_proc.c index 3fb10c8e4271..01372d8548e1 100644 --- a/fs/nfs/callback_proc.c +++ b/fs/nfs/callback_proc.c @@ -391,7 +391,16 @@ __be32 nfs4_callback_devicenotify(void *argp, void *resp, if (!ld) continue; } + /* + * Unhash the cached device first so re-resolution cannot + * re-pin the stale node, then re-point any references + * pinned under live layouts (RFC 8881 Section 12.2.10). + */ nfs4_delete_deviceid(ld, cps->clp, &dev->cbd_dev_id); + if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) + pnfs_layout_reresolve_deviceid_byclid(cps->clp, ld, + &dev->cbd_dev_id, + dev->cbd_immediate); } pnfs_put_layoutdriver(ld); out: From e1c7a4f94139cf2c49896653e3f1e6c4f25cff35 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:17 -0400 Subject: [PATCH 33/68] NFSv4/flexfiles: Honor ndc_immediate on CB_NOTIFY_DEVICEID CHANGE When a CHANGE notification carries ndc_immediate, RFC 8881 Section 20.12 says the change is enforced immediately and the client might not be able to complete pending I/O. In addition to un-pinning the stripe's device node, mark the old node unavailable. Marking does not recall the references already handed out. A write whose DS connection is already up keeps using the old node until that I/O errors: nfs4_ff_layout_prepare_ds() returns early on a live ds_clp, and the unavailable flag is only consulted when a connection is being established. What the mark does change is that a read skips the node while another mirror is usable, and that an IOMODE_RW segment still pinning it stops counting as fully available -- so I/O the server rejects falls back to the MDS rather than being retried against a mapping the server has already withdrawn. The walk can also exchange out a node that already carries the new mapping: a re-resolve that completed between the unhash and the walk reaching the stripe, raced by this walk or by the walk of a later CHANGE for the same deviceid. Ripping such a node out is harmless (it is still hashed, so the next I/O re-pins it from the cache), but it must not be marked unavailable. A superseded node is distinguishable by hashed-ness: it was unhashed before its walk began and is never re-inserted, while a fresh node is inserted before it is installed. Only mark nodes that are no longer hashed. That test is hlist_unhashed_lockless(): the hook runs under the layout inode's i_lock and rcu_read_lock(), but not under nfs4_deviceid_lock, which is what serializes the writers of node.pprev -- and __hlist_del() stores a neighbour's pprev with WRITE_ONCE(), so removing any other entry in the same bucket can write the field this test reads. Without ndc_immediate, pending I/O drains on the old mapping and only new I/O re-resolves, as before. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 55057f8f9a77..a5f4a1df298e 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -2560,6 +2560,13 @@ static void ff_layout_reresolve_deviceid(struct pnfs_layout_hdr *lo, kfree(put); continue; } + /* A node still hashed was fetched after the unhash + * and carries the new mapping; mark only the + * superseded ones. + */ + if (immediate && + hlist_unhashed_lockless(&old->id_node.node)) + nfs4_mark_deviceid_unavailable(&old->id_node); put->dev = &old->id_node; list_add(&put->node, head); } From 1507df1d0bfd723e93eddcc800629acac1ec401c Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:18 -0400 Subject: [PATCH 34/68] pNFS: Discard a GETDEVICEINFO reply that raced a CHANGE notification RFC 8881 Section 18.40.4: a GETDEVICEINFO reply in flight while the server changes the device mapping may carry the pre-change mapping; if it is inserted into the cache after the CHANGE notification unhashed the stale entry, the client re-caches stale data. Track a change epoch, bumped when a CHANGE notification is processed before the stale entry is unhashed. nfs4_find_get_deviceid() snapshots the epoch before issuing GETDEVICEINFO and, serialized against the unhash by nfs4_deviceid_lock at insert time, discards the reply and refetches if the epoch moved. A stale insert that instead precedes the unhash is removed by the unhash itself, so the cache does not retain the pre-change entry either way; a reference already handed to a caller in that ordering is dropped by the re-resolve walk instead. The refetch is bounded. The epoch is bumped once per CHANGE entry -- that is, at a rate the server chooses -- so an unbounded retry would let a server drive GETDEVICEINFO traffic without limit, and each discarded node can carry a DS client teardown and reconnect with it. After NFS4_DEVICEID_FETCH_RETRIES attempts the reply is accepted. That is safe because discarding is an optimisation rather than a correctness requirement: it avoids caching a mapping already known to be superseded, but before this patch the client cached whatever the reply carried, so the bounded case is no worse than the previous behaviour and a mapping that really is stale is corrected by the notification that follows. The epoch lives on the nfs_client, so a CHANGE delivered on one server's callback channel does not force an unrelated server's in-flight lookup to discard its reply and refetch. Mounts that share an nfs_client do share the counter; the deviceid cache is keyed per client ID, so that is the granularity the race is defined at. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/callback_proc.c | 4 ++++ fs/nfs/pnfs.h | 1 + fs/nfs/pnfs_dev.c | 25 +++++++++++++++++++++++++ include/linux/nfs_fs_sb.h | 2 ++ 4 files changed, 32 insertions(+) diff --git a/fs/nfs/callback_proc.c b/fs/nfs/callback_proc.c index 01372d8548e1..ea8c558b07b6 100644 --- a/fs/nfs/callback_proc.c +++ b/fs/nfs/callback_proc.c @@ -395,7 +395,11 @@ __be32 nfs4_callback_devicenotify(void *argp, void *resp, * Unhash the cached device first so re-resolution cannot * re-pin the stale node, then re-point any references * pinned under live layouts (RFC 8881 Section 12.2.10). + * The epoch bump lets an in-flight GETDEVICEINFO detect + * that its reply may predate the change. */ + if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) + nfs4_deviceid_bump_change_epoch(cps->clp); nfs4_delete_deviceid(ld, cps->clp, &dev->cbd_dev_id); if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) pnfs_layout_reresolve_deviceid_byclid(cps->clp, ld, diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index bb1ca7ba0221..9a5b8070f595 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -406,6 +406,7 @@ nfs4_find_get_deviceid(struct nfs_server *server, const struct nfs4_deviceid *id, const struct cred *cred, gfp_t gfp_mask); void nfs4_delete_deviceid(const struct pnfs_layoutdriver_type *, const struct nfs_client *, const struct nfs4_deviceid *); +void nfs4_deviceid_bump_change_epoch(struct nfs_client *clp); void nfs4_init_deviceid_node(struct nfs4_deviceid_node *, struct nfs_server *, const struct nfs4_deviceid *); bool nfs4_put_deviceid_node(struct nfs4_deviceid_node *); diff --git a/fs/nfs/pnfs_dev.c b/fs/nfs/pnfs_dev.c index 274abdd6d5f3..a3b28409539a 100644 --- a/fs/nfs/pnfs_dev.c +++ b/fs/nfs/pnfs_dev.c @@ -181,6 +181,21 @@ __nfs4_find_get_deviceid(struct nfs_server *server, return d; } +/* + * Bumped before the stale entry is unhashed, so an insert serialised + * after the unhash by nfs4_deviceid_lock observes the new epoch. + */ +void +nfs4_deviceid_bump_change_epoch(struct nfs_client *clp) +{ + atomic_inc(&clp->cl_deviceid_change_epoch); +} + +/* Discarding a raced reply is an optimisation, not a correctness + * requirement, and the epoch moves at the server's rate: bound it. + */ +#define NFS4_DEVICEID_FETCH_RETRIES 3 + struct nfs4_deviceid_node * nfs4_find_get_deviceid(struct nfs_server *server, const struct nfs4_deviceid *id, const struct cred *cred, @@ -188,11 +203,14 @@ nfs4_find_get_deviceid(struct nfs_server *server, { long hash = nfs4_deviceid_hash(id); struct nfs4_deviceid_node *d, *new; + int epoch, tries = 0; +retry: d = __nfs4_find_get_deviceid(server, id, hash); if (d) goto found; + epoch = atomic_read(&server->nfs_client->cl_deviceid_change_epoch); new = nfs4_get_device_info(server, id, cred, gfp_mask); if (!new) { trace_nfs4_find_deviceid(server, id, -ENOENT); @@ -200,6 +218,13 @@ nfs4_find_get_deviceid(struct nfs_server *server, } spin_lock(&nfs4_deviceid_lock); + if (atomic_read(&server->nfs_client->cl_deviceid_change_epoch) != epoch && + ++tries <= NFS4_DEVICEID_FETCH_RETRIES) { + /* a mapping changed while we fetched; ours may be stale */ + spin_unlock(&nfs4_deviceid_lock); + server->pnfs_curr_ld->free_deviceid_node(new); + goto retry; + } d = __nfs4_find_get_deviceid(server, id, hash); if (d) { spin_unlock(&nfs4_deviceid_lock); diff --git a/include/linux/nfs_fs_sb.h b/include/linux/nfs_fs_sb.h index 3e6bae7e5221..482daf1fa4b0 100644 --- a/include/linux/nfs_fs_sb.h +++ b/include/linux/nfs_fs_sb.h @@ -74,6 +74,8 @@ struct nfs_client { u64 cl_clientid; /* constant */ nfs4_verifier cl_confirm; /* Clientid verifier */ unsigned long cl_state; + /* bumped on each CB_NOTIFY_DEVICEID CHANGE for this client */ + atomic_t cl_deviceid_change_epoch; spinlock_t cl_lock; From 3c721c396f59039793d86e690520008c2ed86234 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:19 -0400 Subject: [PATCH 35/68] pNFS: Add deviceid reference query and collection walkers The CB_NOTIFY_DEVICEID DELETE race recovery (RFC 8881 Section 18.40.4) needs to ask whether any live layout still references a deviceID, and to enumerate those layouts for TEST_STATEID. Add a layout_references_deviceid hook (sibling of reresolve_deviceid; the flexfiles implementation memcmps each mirror stripe's decoded devid, valid independent of the pinned device node) and two walkers over the byserver pattern: - pnfs_layout_deviceid_referenced_byclid(): boolean existence query, early-stopping, entirely under i_lock. - pnfs_layout_collect_deviceid_refs(): collects each matching layout with the hdr pinned, the inode grabbed with its superblock active (a pinned hdr does not hold its inode -- same discipline as the bulk-destroy walker), and the layout stateid and cred snapshotted under i_lock, so the caller can issue sleeping RPCs against the collection. The collection walker pins each matching header with a plain pnfs_get_layout_hdr(), which cannot resurrect a dying header because the walker holds i_lock and has checked NFS_I()->layout == lo. pnfs_put_layout_hdr() drops the last reference under i_lock -- refcount_dec_and_lock() only decrements one to zero once it holds the lock -- and clears NFS_I()->layout in that same critical section, before it unlocks and frees. So a header still installed on its inode cannot have reached a zero refcount. That argument deliberately does not rest on NFS_LAYOUT_INVALID_STID. A header can reach its final put while still valid: a full LAYOUTRETURN ends in pnfs_layoutreturn_free_lsegs(), which resets the layout stateid rather than invalidating it, and that is the ordinary end of life for a return-on-close layout. The validity check the walkers do apply is a policy filter, not a lifetime guarantee. Because pnfs_put_layout_hdr() can send a layoutreturn and sleep, a header pinned for a layout whose inode can no longer be grabbed is put after the RCU read-side critical section, not within it. Both ways the walk can end early report it. A GFP_ATOMIC allocation failure aborts with -ENOMEM, and an inode that can no longer be grabbed -- igrab() fails from I_FREEING on, while the layout may still be valid and still name the deviceID -- aborts with -EAGAIN. Either way the caller is told the collection is partial instead of receiving a short list it would read as "no references". No callers yet; no behavior change. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 17 +++ fs/nfs/pnfs.c | 164 +++++++++++++++++++++++++ fs/nfs/pnfs.h | 29 +++++ 3 files changed, 210 insertions(+) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index a5f4a1df298e..2ea63fda0035 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -2527,6 +2527,22 @@ static void ff_layout_cancel_io(struct pnfs_layout_segment *lseg) } } +/* Called under @lo's inode i_lock. */ +static bool ff_layout_references_deviceid(struct pnfs_layout_hdr *lo, + const struct nfs4_deviceid *id) +{ + struct nfs4_flexfile_layout *flo = FF_LAYOUT_FROM_HDR(lo); + struct nfs4_ff_layout_mirror *mirror; + u32 dss_id; + + list_for_each_entry(mirror, &flo->mirrors, mirrors) + for (dss_id = 0; dss_id < mirror->dss_count; dss_id++) + if (memcmp(&mirror->dss[dss_id].devid, id, + sizeof(*id)) == 0) + return true; + return false; +} + /* * Un-pin every stripe node resolved from @id: in-flight I/O drains on the * old node through its own reference, the next I/O re-resolves. @@ -3155,6 +3171,7 @@ static struct pnfs_layoutdriver_type flexfilelayout_type = { .get_ds_info = ff_layout_get_ds_info, .free_deviceid_node = ff_layout_free_deviceid_node, .reresolve_deviceid = ff_layout_reresolve_deviceid, + .layout_references_deviceid = ff_layout_references_deviceid, .read_pagelist = ff_layout_read_pagelist, .write_pagelist = ff_layout_write_pagelist, .alloc_deviceid_node = ff_layout_alloc_deviceid_node, diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 69ea9abe28c9..fbe4b58b5b72 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -2979,6 +2979,170 @@ pnfs_layout_reresolve_deviceid_byclid(struct nfs_client *clp, } } +struct pnfs_deviceid_ref_args { + const struct pnfs_layoutdriver_type *ld; + const struct nfs4_deviceid *devid; + struct list_head *result; + bool found; +}; + +static int pnfs_layout_deviceid_referenced_byserver( + struct nfs_server *server, void *data) +{ + struct pnfs_deviceid_ref_args *args = data; + struct pnfs_layout_hdr *lo; + struct inode *inode; + + if (server->pnfs_curr_ld != args->ld) + return 0; + + rcu_read_lock(); + list_for_each_entry_rcu(lo, &server->layouts, plh_layouts) { + inode = lo->plh_inode; + if (!inode) + continue; + spin_lock(&inode->i_lock); + if (NFS_I(inode)->layout == lo && pnfs_layout_is_valid(lo) && + args->ld->layout_references_deviceid(lo, args->devid)) + args->found = true; + spin_unlock(&inode->i_lock); + if (args->found) + break; + } + rcu_read_unlock(); + return args->found; +} + +/* + * pnfs_layout_deviceid_referenced_byclid - does any live layout of + * @clp's servers using @ld still reference deviceid @devid? + */ +bool +pnfs_layout_deviceid_referenced_byclid(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *devid) +{ + struct pnfs_deviceid_ref_args args = { + .ld = ld, + .devid = devid, + }; + + if (!ld->layout_references_deviceid) + return false; + + nfs_client_for_each_server(clp, + pnfs_layout_deviceid_referenced_byserver, &args); + return args.found; +} + +static int pnfs_layout_collect_deviceid_refs_byserver( + struct nfs_server *server, void *data) +{ + struct pnfs_deviceid_ref_args *args = data; + struct nfs4_deviceid_ref *ref, *tmp; + struct pnfs_layout_hdr *lo; + struct inode *inode; + LIST_HEAD(putme); + bool matched; + int ret = 0; + + if (server->pnfs_curr_ld != args->ld) + return 0; + + rcu_read_lock(); + list_for_each_entry_rcu(lo, &server->layouts, plh_layouts) { + inode = lo->plh_inode; + if (!inode) + continue; + + spin_lock(&inode->i_lock); + matched = NFS_I(inode)->layout == lo && + pnfs_layout_is_valid(lo) && + args->ld->layout_references_deviceid(lo, args->devid); + if (!matched) { + spin_unlock(&inode->i_lock); + continue; + } + ref = kzalloc_obj(*ref, GFP_ATOMIC); + if (!ref) { + spin_unlock(&inode->i_lock); + ret = -ENOMEM; + break; + } + /* NFS_I()->layout == lo under i_lock means the refcount has + * not reached zero: pnfs_put_layout_hdr() decrements to zero + * and detaches in the same critical section. + */ + pnfs_get_layout_hdr(lo); + ref->lo = lo; + nfs4_stateid_copy(&ref->stateid, &lo->plh_stateid); + ref->cred = get_cred(lo->plh_lc_cred); + spin_unlock(&inode->i_lock); + + /* the pinned hdr does not hold the inode: grab it (and + * keep the superblock active) for use across RPCs + */ + ref->inode = nfs_igrab_and_active(inode); + if (!ref->inode) { + /* The layout may still name the deviceID, so report a + * partial list rather than silently shortening it. + * Defer the put: it can layoutreturn and sleep. + */ + list_add(&ref->node, &putme); + ret = -EAGAIN; + break; + } + list_add_tail(&ref->node, args->result); + } + rcu_read_unlock(); + + list_for_each_entry_safe(ref, tmp, &putme, node) { + list_del(&ref->node); + pnfs_put_layout_hdr(ref->lo); + put_cred(ref->cred); + kfree(ref); + } + return ret; +} + +/* + * Collect @clp's layouts referencing @devid onto @result as entries usable + * across sleeping RPCs; release with pnfs_layout_put_deviceid_refs(). + * A negative return means @result is only a partial set. + */ +int +pnfs_layout_collect_deviceid_refs(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *devid, + struct list_head *result) +{ + struct pnfs_deviceid_ref_args args = { + .ld = ld, + .devid = devid, + .result = result, + }; + + if (!ld->layout_references_deviceid) + return 0; + + return nfs_client_for_each_server(clp, + pnfs_layout_collect_deviceid_refs_byserver, &args); +} + +void +pnfs_layout_put_deviceid_refs(struct list_head *result) +{ + struct nfs4_deviceid_ref *ref, *tmp; + + list_for_each_entry_safe(ref, tmp, result, node) { + list_del(&ref->node); + put_cred(ref->cred); + pnfs_put_layout_hdr(ref->lo); + nfs_iput_and_deactive(ref->inode); + kfree(ref); + } +} + /* Check if we have we have a valid layout but if there isn't an intersection * between the request and the pgio->pg_lseg, put this pgio->pg_lseg away. */ diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 9a5b8070f595..8dd892d875e3 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -184,6 +184,12 @@ struct pnfs_layoutdriver_type { const struct nfs4_deviceid *id, bool immediate, struct list_head *put_list); + /* + * Does @lo hold any reference to deviceid @id? Called under + * @lo's inode i_lock; must not sleep. + */ + bool (*layout_references_deviceid)(struct pnfs_layout_hdr *lo, + const struct nfs4_deviceid *id); int (*prepare_layoutreturn) (struct nfs4_layoutreturn_args *); @@ -371,6 +377,29 @@ void pnfs_layout_reresolve_deviceid_byclid(struct nfs_client *clp, const struct pnfs_layoutdriver_type *ld, const struct nfs4_deviceid *devid, bool immediate); +bool pnfs_layout_deviceid_referenced_byclid(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *devid); + +/* + * One live layout referencing a deviceID, collected for the + * CB_NOTIFY_DEVICEID DELETE recovery: the hdr is pinned, the inode + * igrab'd with its superblock active, and the layout stateid and + * cred snapshotted for TEST_STATEID. + */ +struct nfs4_deviceid_ref { + struct list_head node; + struct pnfs_layout_hdr *lo; + struct inode *inode; + nfs4_stateid stateid; + const struct cred *cred; +}; + +int pnfs_layout_collect_deviceid_refs(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *devid, + struct list_head *result); +void pnfs_layout_put_deviceid_refs(struct list_head *result); int pnfs_layout_handle_reboot(struct nfs_client *clp); /* nfs4_deviceid_flags */ From 59b065e645f4d5014322935a1d74ad3200573e86 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:20 -0400 Subject: [PATCH 36/68] NFSv4/pnfs: Recover revoked layouts on a deleted deviceID RFC 8881 Section 20.12 lets a server send CB_NOTIFY_DEVICEID DELETE for a deviceID once it has revoked every layout referring to it. Revocation is not announced, so the client can still be holding what it believes are live layouts on that deviceID. Section 18.40.4 resolves that: TEST_STATEID each referring layout and recover the ones that come back revoked -- mark the layout stateid invalid, free the lsegs, FREE_STATEID to acknowledge. The callback thread cannot issue fore-channel RPCs, so suspects are queued on the nfs_client (dedup'd, holding a layoutdriver reference) and resolved by a new state-manager step keyed on NFS4CLNT_DEVICEID_DELETE. The worker re-collects the referring layouts, so layouts returned or recalled in the meantime are skipped. Drop the cached device once the collected layouts account for the delete: every one of them was revoked here. Section 18.48.3 defines TEST_STATEID's answers, and NFS4ERR_OLD_STATEID says the layout exists and was not revoked -- only that it moved on after this stateid was snapshotted -- so it counts against the delete as NFS4_OK does. Any other answer leaves the revocation unresolved and keeps the device cached, as does a layout the server still considers valid (verifying that one with GETDEVICEINFO comes next). A layout counts as revoked only if it was invalidated here; a stateid that no longer matches its layout is a stale snapshot. Invalidating one is paired with nfs_commit_inode(), since pnfs_clear_lseg_state() drops only the VALID and LAYOUTCOMMIT references, and an lseg still held by a commit bucket would keep the layout -- and the device nodes this recovery is trying to release -- alive. If the walk collects no referring layouts, the device is unreferenced and the delete is carried out directly. If the collection could not be completed, recovery leaves the device cached for the next notification. Nothing enqueues suspects yet, so no behavior change. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/nfs4_fs.h | 2 + fs/nfs/nfs4client.c | 2 + fs/nfs/nfs4proc.c | 83 +++++++++++++++++++++++++++++++++++++++ fs/nfs/nfs4state.c | 3 ++ fs/nfs/pnfs.c | 62 +++++++++++++++++++++++++++++ fs/nfs/pnfs.h | 19 +++++++++ include/linux/nfs_fs_sb.h | 2 + 7 files changed, 173 insertions(+) diff --git a/fs/nfs/nfs4_fs.h b/fs/nfs/nfs4_fs.h index b48e5b87cb2a..d642aca0adc3 100644 --- a/fs/nfs/nfs4_fs.h +++ b/fs/nfs/nfs4_fs.h @@ -52,6 +52,7 @@ enum nfs4_client_state { NFS4CLNT_RECALL_ANY_LAYOUT_READ, NFS4CLNT_RECALL_ANY_LAYOUT_RW, NFS4CLNT_DELEGRETURN_DELAYED, + NFS4CLNT_DEVICEID_DELETE, }; #define NFS4_RENEW_TIMEOUT 0x01 @@ -493,6 +494,7 @@ int nfs41_discover_server_trunking(struct nfs_client *clp, struct nfs_client **, const struct cred *); extern void nfs4_schedule_session_recovery(struct nfs4_session *, int); extern void nfs41_notify_server(struct nfs_client *); +extern void nfs4_deviceid_delete_recover_run(struct nfs_client *clp); bool nfs4_check_serverowner_major_id(struct nfs41_server_owner *o1, struct nfs41_server_owner *o2); diff --git a/fs/nfs/nfs4client.c b/fs/nfs/nfs4client.c index b661f446ea49..e6a589913666 100644 --- a/fs/nfs/nfs4client.c +++ b/fs/nfs/nfs4client.c @@ -217,6 +217,7 @@ struct nfs_client *nfs4_alloc_client(const struct nfs_client_initdata *cl_init) clp->cl_last_renewal = jiffies; init_waitqueue_head(&clp->cl_lock_waitq); INIT_LIST_HEAD(&clp->pending_cb_stateids); + INIT_LIST_HEAD(&clp->cl_deviceid_deletes); if (cl_init->minorversion != 0) __set_bit(NFS_CS_INFINITE_SLOTS, &clp->cl_flags); @@ -286,6 +287,7 @@ static void nfs4_shutdown_client(struct nfs_client *clp) nfs4_kill_renewd(clp); clp->cl_mvops->shutdown_client(clp); nfs4_destroy_callback(clp); + pnfs_deviceid_delete_queue_free(clp); if (__test_and_clear_bit(NFS_CS_IDMAP, &clp->cl_res_state)) nfs_idmap_delete(clp); diff --git a/fs/nfs/nfs4proc.c b/fs/nfs/nfs4proc.c index e41c792a2725..2192875c168e 100644 --- a/fs/nfs/nfs4proc.c +++ b/fs/nfs/nfs4proc.c @@ -10506,6 +10506,89 @@ static int nfs41_free_stateid(struct nfs_server *server, return ret; } +/* + * A DELETE for a deviceID we still hold layouts on implies the server + * revoked them: run the RFC 8881 Section 18.40.4 recovery. + */ +static void nfs4_deviceid_delete_recover(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *id) +{ + LIST_HEAD(layouts); + struct nfs4_deviceid_ref *ref; + bool revoked = false; + bool referenced = false; + bool inconclusive = false; + + if (pnfs_layout_collect_deviceid_refs(clp, ld, id, &layouts)) { + /* Only a partial list -- an allocation failed, or an inode is + * being evicted. Leave the device cached and recover on a + * later notification. + */ + pnfs_layout_put_deviceid_refs(&layouts); + return; + } + + if (list_empty(&layouts)) { + nfs4_delete_deviceid(ld, clp, id); + return; + } + + list_for_each_entry(ref, &layouts, node) { + struct pnfs_layout_hdr *lo = ref->lo; + struct inode *inode = ref->inode; + bool invalidated = false; + LIST_HEAD(head); + int status; + + status = nfs41_test_stateid(NFS_SERVER(inode), &ref->stateid, + ref->cred); + switch (status) { + case NFS_OK: + case -NFS4ERR_OLD_STATEID: + referenced = true; + break; + case -NFS4ERR_ADMIN_REVOKED: + case -NFS4ERR_DELEG_REVOKED: + case -NFS4ERR_EXPIRED: + case -NFS4ERR_BAD_STATEID: + spin_lock(&inode->i_lock); + if (pnfs_layout_is_valid(lo) && + nfs4_stateid_match_other(&ref->stateid, + &lo->plh_stateid)) { + pnfs_mark_layout_stateid_invalid(lo, &head); + revoked = true; + invalidated = true; + } + spin_unlock(&inode->i_lock); + pnfs_free_lseg_list(&head); + if (invalidated) + nfs_commit_inode(inode, 0); + nfs41_free_stateid(NFS_SERVER(inode), &ref->stateid, + ref->cred, true); + break; + default: + inconclusive = true; + break; + } + } + pnfs_layout_put_deviceid_refs(&layouts); + + if (revoked && !referenced && !inconclusive) + nfs4_delete_deviceid(ld, clp, id); +} + +void nfs4_deviceid_delete_recover_run(struct nfs_client *clp) +{ + struct nfs4_deviceid_delete *dd; + + while ((dd = pnfs_deviceid_delete_dequeue(clp)) != NULL) { + nfs4_deviceid_delete_recover(clp, dd->ld, &dd->id); + pnfs_put_layoutdriver(dd->ld); + kfree(dd); + } +} + static void nfs41_free_lock_state(struct nfs_server *server, struct nfs4_lock_state *lsp) { diff --git a/fs/nfs/nfs4state.c b/fs/nfs/nfs4state.c index a5dec0473e22..1faf9dafd331 100644 --- a/fs/nfs/nfs4state.c +++ b/fs/nfs/nfs4state.c @@ -2669,6 +2669,9 @@ static void nfs4_state_manager(struct nfs_client *clp) set_bit(NFS4CLNT_RUN_MANAGER, &clp->cl_state); } nfs4_layoutreturn_any_run(clp); + if (test_and_clear_bit(NFS4CLNT_DEVICEID_DELETE, + &clp->cl_state)) + nfs4_deviceid_delete_recover_run(clp); clear_bit(NFS4CLNT_RECALL_RUNNING, &clp->cl_state); } diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index fbe4b58b5b72..79f671355e5b 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -3143,6 +3143,68 @@ pnfs_layout_put_deviceid_refs(struct list_head *result) } } +/* + * Queue @id for the state manager's Section 18.40.4 recovery, + * dropping duplicates of an already-queued suspect. + */ +void pnfs_deviceid_delete_mark(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *id) +{ + struct nfs4_deviceid_delete *dd, *new; + + new = kzalloc_obj(*new, GFP_KERNEL); + if (!new) + return; /* lost notification; recovery waits for the next */ + new->ld = pnfs_find_layoutdriver(ld->id); + if (!new->ld) { + kfree(new); + return; + } + memcpy(&new->id, id, sizeof(new->id)); + + spin_lock(&clp->cl_lock); + list_for_each_entry(dd, &clp->cl_deviceid_deletes, list) { + if (dd->ld == new->ld && + !memcmp(&dd->id, &new->id, sizeof(dd->id))) { + spin_unlock(&clp->cl_lock); + pnfs_put_layoutdriver(new->ld); + kfree(new); + return; + } + } + list_add_tail(&new->list, &clp->cl_deviceid_deletes); + spin_unlock(&clp->cl_lock); + + set_bit(NFS4CLNT_DEVICEID_DELETE, &clp->cl_state); + nfs4_schedule_state_manager(clp); +} + +struct nfs4_deviceid_delete *pnfs_deviceid_delete_dequeue( + struct nfs_client *clp) +{ + struct nfs4_deviceid_delete *dd = NULL; + + spin_lock(&clp->cl_lock); + if (!list_empty(&clp->cl_deviceid_deletes)) { + dd = list_first_entry(&clp->cl_deviceid_deletes, + struct nfs4_deviceid_delete, list); + list_del(&dd->list); + } + spin_unlock(&clp->cl_lock); + return dd; +} + +void pnfs_deviceid_delete_queue_free(struct nfs_client *clp) +{ + struct nfs4_deviceid_delete *dd; + + while ((dd = pnfs_deviceid_delete_dequeue(clp)) != NULL) { + pnfs_put_layoutdriver(dd->ld); + kfree(dd); + } +} + /* Check if we have we have a valid layout but if there isn't an intersection * between the request and the pgio->pg_lseg, put this pgio->pg_lseg away. */ diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 8dd892d875e3..18ea4e8e0d85 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -400,6 +400,25 @@ int pnfs_layout_collect_deviceid_refs(struct nfs_client *clp, const struct nfs4_deviceid *devid, struct list_head *result); void pnfs_layout_put_deviceid_refs(struct list_head *result); + +/* + * A CB_NOTIFY_DEVICEID DELETE naming a deviceID that live layouts + * still reference (RFC 8881 Section 18.40.4). Queued on + * nfs_client.cl_deviceid_deletes under cl_lock for the state manager + * to resolve; holds a layoutdriver reference. + */ +struct nfs4_deviceid_delete { + struct list_head list; + const struct pnfs_layoutdriver_type *ld; + struct nfs4_deviceid id; +}; + +void pnfs_deviceid_delete_mark(struct nfs_client *clp, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *id); +struct nfs4_deviceid_delete *pnfs_deviceid_delete_dequeue( + struct nfs_client *clp); +void pnfs_deviceid_delete_queue_free(struct nfs_client *clp); int pnfs_layout_handle_reboot(struct nfs_client *clp); /* nfs4_deviceid_flags */ diff --git a/include/linux/nfs_fs_sb.h b/include/linux/nfs_fs_sb.h index 482daf1fa4b0..416c6f39f31d 100644 --- a/include/linux/nfs_fs_sb.h +++ b/include/linux/nfs_fs_sb.h @@ -103,6 +103,8 @@ struct nfs_client { /* The flags used for obtaining the clientid during EXCHANGE_ID */ u32 cl_exchange_flags; struct nfs4_session *cl_session; /* shared session */ + /* CB_NOTIFY_DEVICEID DELETE suspects, protected by cl_lock */ + struct list_head cl_deviceid_deletes; bool cl_preserve_clid; struct nfs41_server_owner *cl_serverowner; struct nfs41_server_scope *cl_serverscope; From 14286f49f0b87129656acc54a825a64b8c9744b3 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:21 -0400 Subject: [PATCH 37/68] NFSv4/pnfs: Confirm a deviceID delete via GETDEVICEINFO RFC 8881 Section 18.40.4: if TEST_STATEID says at least one layout referring to the deleted deviceID is still valid, the delete cannot be trusted -- verify it with GETDEVICEINFO. The device really being gone while the server also considers a referring layout valid means the server is faulty; recover by re-establishing the client ID and drop the cached device. Any other answer -- including the device existing, i.e. an erroneous DELETE -- keeps the cached device and the layout intact. Re-establishing the client ID is nfs4_reset_all_state(), which sets NFS4CLNT_PURGE_STATE so the state manager runs nfs4_purge_lease(): a fresh EXCHANGE_ID, then state reclaim with no grace period. The grace-less reclaim is the point -- the server has not rebooted, so there is nothing to reclaim under CLAIM_PREVIOUS, and the new client ID orphans the state held under the old one. The obvious-looking nfs4_schedule_lease_recovery() is not the right call here: it sets NFS4CLNT_CHECK_LEASE, which the state manager turns into a lease renewal, and on a healthy session -- which this one is, the server having just answered TEST_STATEID and GETDEVICEINFO on it -- that renewal succeeds and no EXCHANGE_ID is ever sent. This is the only path on which a device notification can escalate to a full client-ID reset, and every open, lock and delegation on the client is reclaimed as a result. From userspace that is indistinguishable from a spontaneous lease expiry, so the escalation is announced with a rate-limited warning naming the server. The raw-status probe calls nfs4_proc_getdeviceinfo() directly because nfs4_get_device_info() swallows the RPC status and cannot distinguish NFS4ERR_NOENT from a transient failure. A one-page reply buffer is enough: a device too large for it fails with something other than -ENOENT, which still proves existence. Still nothing enqueues suspects; no behavior change. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/nfs4_fs.h | 1 + fs/nfs/nfs4proc.c | 59 ++++++++++++++++++++++++++++++++++++++++------ fs/nfs/nfs4state.c | 2 +- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/fs/nfs/nfs4_fs.h b/fs/nfs/nfs4_fs.h index d642aca0adc3..76dae699d4d7 100644 --- a/fs/nfs/nfs4_fs.h +++ b/fs/nfs/nfs4_fs.h @@ -511,6 +511,7 @@ extern void nfs_inode_find_state_and_recover(struct inode *inode, const nfs4_stateid *stateid); extern int nfs4_state_mark_reclaim_nograce(struct nfs_client *, struct nfs4_state *); extern void nfs4_schedule_lease_recovery(struct nfs_client *); +extern void nfs4_reset_all_state(struct nfs_client *); extern int nfs4_wait_clnt_recover(struct nfs_client *clp); extern int nfs4_client_recover_expired_lease(struct nfs_client *clp); extern void nfs4_schedule_state_manager(struct nfs_client *); diff --git a/fs/nfs/nfs4proc.c b/fs/nfs/nfs4proc.c index 2192875c168e..518348e87dd8 100644 --- a/fs/nfs/nfs4proc.c +++ b/fs/nfs/nfs4proc.c @@ -10506,19 +10506,50 @@ static int nfs41_free_stateid(struct nfs_server *server, return ret; } +/* + * GETDEVICEINFO surfacing the raw status; nfs4_get_device_info() + * swallows it. A device too large for one page fails with something + * other than -ENOENT, which still proves existence. + */ +static int nfs4_deviceid_validate(struct nfs_server *server, + const struct pnfs_layoutdriver_type *ld, + const struct nfs4_deviceid *id, const struct cred *cred) +{ + struct pnfs_device pdev; + struct page *page; + int status; + + page = alloc_page(GFP_KERNEL); + if (!page) + return -ENOMEM; + + memset(&pdev, 0, sizeof(pdev)); + memcpy(&pdev.dev_id, id, sizeof(pdev.dev_id)); + pdev.layout_type = ld->id; + pdev.pages = &page; + pdev.pglen = PAGE_SIZE; + pdev.maxcount = PAGE_SIZE - nfs41_maxgetdevinfo_overhead; + + status = nfs4_proc_getdeviceinfo(server, &pdev, cred); + __free_page(page); + return status; +} + /* * A DELETE for a deviceID we still hold layouts on implies the server - * revoked them: run the RFC 8881 Section 18.40.4 recovery. + * revoked them: run the RFC 8881 Section 18.40.4 recovery. A layout the + * server still calls valid leaves the revocations unable to confirm the + * delete, so verify it with GETDEVICEINFO. */ static void nfs4_deviceid_delete_recover(struct nfs_client *clp, const struct pnfs_layoutdriver_type *ld, const struct nfs4_deviceid *id) { LIST_HEAD(layouts); - struct nfs4_deviceid_ref *ref; + struct nfs4_deviceid_ref *ref, *confirm = NULL; bool revoked = false; - bool referenced = false; bool inconclusive = false; + int status; if (pnfs_layout_collect_deviceid_refs(clp, ld, id, &layouts)) { /* Only a partial list -- an allocation failed, or an inode is @@ -10539,14 +10570,14 @@ static void nfs4_deviceid_delete_recover(struct nfs_client *clp, struct inode *inode = ref->inode; bool invalidated = false; LIST_HEAD(head); - int status; status = nfs41_test_stateid(NFS_SERVER(inode), &ref->stateid, ref->cred); switch (status) { case NFS_OK: case -NFS4ERR_OLD_STATEID: - referenced = true; + if (!confirm) + confirm = ref; break; case -NFS4ERR_ADMIN_REVOKED: case -NFS4ERR_DELEG_REVOKED: @@ -10572,10 +10603,24 @@ static void nfs4_deviceid_delete_recover(struct nfs_client *clp, break; } } - pnfs_layout_put_deviceid_refs(&layouts); - if (revoked && !referenced && !inconclusive) + if (confirm) { + status = nfs4_deviceid_validate(NFS_SERVER(confirm->inode), + ld, id, confirm->cred); + if (status == -ENOENT) { + /* Section 18.40.4 prescribes EXCHANGE_ID here; + * nfs4_schedule_lease_recovery() would only renew + * the existing lease. + */ + pr_warn_ratelimited("NFS: server %s deleted a deviceID referred to by a layout it still considers valid; re-establishing the client ID\n", + clp->cl_hostname); + nfs4_reset_all_state(clp); + nfs4_delete_deviceid(ld, clp, id); + } + } else if (revoked && !inconclusive) { nfs4_delete_deviceid(ld, clp, id); + } + pnfs_layout_put_deviceid_refs(&layouts); } void nfs4_deviceid_delete_recover_run(struct nfs_client *clp) diff --git a/fs/nfs/nfs4state.c b/fs/nfs/nfs4state.c index 1faf9dafd331..4c085c00abb1 100644 --- a/fs/nfs/nfs4state.c +++ b/fs/nfs/nfs4state.c @@ -2354,7 +2354,7 @@ void nfs41_notify_server(struct nfs_client *clp) nfs4_schedule_state_manager(clp); } -static void nfs4_reset_all_state(struct nfs_client *clp) +void nfs4_reset_all_state(struct nfs_client *clp) { if (test_and_set_bit(NFS4CLNT_LEASE_EXPIRED, &clp->cl_state) == 0) { set_bit(NFS4CLNT_PURGE_STATE, &clp->cl_state); From db485e06253e355f3a2fd1feb94f14bf7ec360ff Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:22 -0400 Subject: [PATCH 38/68] NFSv4/pnfs: Dispatch CB_NOTIFY_DEVICEID DELETE to race recovery Turn on the RFC 8881 Section 18.40.4 DELETE handling: a DELETE for a deviceID that no live layout references keeps today's cheap behavior (drop the cached device, no state-manager wake). A DELETE for a deviceID that live layouts still reference is deferred to the state manager, which TEST_STATEIDs the referring layouts, recovers revoked ones, and confirms or refutes the delete with GETDEVICEINFO. The deferred case no longer unhashes the device immediately: if the recovery concludes the DELETE was erroneous (the deviceID still exists and a referring layout is still valid), the client keeps using the cached device. Re-arm the state manager afterwards, as the delegation return above it does. The recovery issues synchronous RPCs, and a manager thread that starts and exits while it runs clears NFS4CLNT_RUN_MANAGER on its way out. Bump the deviceid change epoch for both notification types rather than only for CHANGE. A GETDEVICEINFO whose reply is already in flight can otherwise re-cache a device the notification has just invalidated; that is as true of a delete as of a change, and Section 18.40.4 opens by describing the race for the delete case. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/callback_proc.c | 24 +++++++++++++++--------- fs/nfs/nfs4state.c | 4 +++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/fs/nfs/callback_proc.c b/fs/nfs/callback_proc.c index ea8c558b07b6..c9b71dbae9ea 100644 --- a/fs/nfs/callback_proc.c +++ b/fs/nfs/callback_proc.c @@ -392,19 +392,25 @@ __be32 nfs4_callback_devicenotify(void *argp, void *resp, continue; } /* - * Unhash the cached device first so re-resolution cannot - * re-pin the stale node, then re-point any references - * pinned under live layouts (RFC 8881 Section 12.2.10). - * The epoch bump lets an in-flight GETDEVICEINFO detect - * that its reply may predate the change. + * Bump the epoch before touching the cache so a + * GETDEVICEINFO already in flight can detect that it + * predates the notification. A referenced DELETE may be + * racing revocation, so defer it to the state manager -- + * this thread cannot issue fore-channel RPCs. */ - if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) - nfs4_deviceid_bump_change_epoch(cps->clp); - nfs4_delete_deviceid(ld, cps->clp, &dev->cbd_dev_id); - if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) + nfs4_deviceid_bump_change_epoch(cps->clp); + if (dev->cbd_notify_type == NOTIFY_DEVICEID4_CHANGE) { + nfs4_delete_deviceid(ld, cps->clp, &dev->cbd_dev_id); pnfs_layout_reresolve_deviceid_byclid(cps->clp, ld, &dev->cbd_dev_id, dev->cbd_immediate); + } else if (pnfs_layout_deviceid_referenced_byclid(cps->clp, + ld, &dev->cbd_dev_id)) { + pnfs_deviceid_delete_mark(cps->clp, ld, + &dev->cbd_dev_id); + } else { + nfs4_delete_deviceid(ld, cps->clp, &dev->cbd_dev_id); + } } pnfs_put_layoutdriver(ld); out: diff --git a/fs/nfs/nfs4state.c b/fs/nfs/nfs4state.c index 4c085c00abb1..b5d6daa2c416 100644 --- a/fs/nfs/nfs4state.c +++ b/fs/nfs/nfs4state.c @@ -2670,8 +2670,10 @@ static void nfs4_state_manager(struct nfs_client *clp) } nfs4_layoutreturn_any_run(clp); if (test_and_clear_bit(NFS4CLNT_DEVICEID_DELETE, - &clp->cl_state)) + &clp->cl_state)) { nfs4_deviceid_delete_recover_run(clp); + set_bit(NFS4CLNT_RUN_MANAGER, &clp->cl_state); + } clear_bit(NFS4CLNT_RECALL_RUNNING, &clp->cl_state); } From 19932b33af6c1f9fb733915a29d8340fdca4c7c6 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:23 -0400 Subject: [PATCH 39/68] NFSv4/pnfs: Grow the deviceid cache hash table The global deviceid cache has 32 buckets shared by every server and layout type. Striping deployments initially anticipate device counts scaling to 1024 or more devices, which leaves those chains 32 entries deep for every resolution to walk. Grow to 256 buckets, four deep at that scale, for 2KB of BSS on 64-bit. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/pnfs_dev.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/nfs/pnfs_dev.c b/fs/nfs/pnfs_dev.c index a3b28409539a..e8143977965c 100644 --- a/fs/nfs/pnfs_dev.c +++ b/fs/nfs/pnfs_dev.c @@ -40,8 +40,11 @@ /* * Device ID RCU cache. A device ID is unique per server and layout type. + * + * 256 buckets keeps the chains short at the 1024-or-more devices a + * striping deployment expects, for 2KB of BSS on 64-bit. */ -#define NFS4_DEVICE_ID_HASH_BITS 5 +#define NFS4_DEVICE_ID_HASH_BITS 8 #define NFS4_DEVICE_ID_HASH_SIZE (1 << NFS4_DEVICE_ID_HASH_BITS) #define NFS4_DEVICE_ID_HASH_MASK (NFS4_DEVICE_ID_HASH_SIZE - 1) From 630295dfc6cc7209f30f882003a8365738a25881 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:24 -0400 Subject: [PATCH 40/68] NFSv4/pnfs: Re-home the data-server cache onto hash buckets The per-net data-server cache is a single list, and every GETDEVICEINFO decode walks all of it looking for a match, so filling the cache costs O(n^2) in the number of data servers -- which a striping mount does in one burst, at the same scale the deviceid cache was just sized for. Key it by the DS address set instead. This patch is the mechanical half: nfs4_pnfs_ds.ds_node becomes an hlist_node, netns init and teardown cover every bucket, and removal uses hlist_del_init (which needs no bucket reference). Insertion still targets bucket 0 and lookup still scans every entry, so behavior is unchanged; the key comes next. Splitting it this way keeps a bisect able to tell a list-conversion bug from a hash-key bug. The buckets live in struct nfs_net, so this costs 2KB per network namespace on 64-bit, paid once nfs.ko is loaded whether or not that namespace ever mounts NFS. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/client.c | 6 ++++-- fs/nfs/netns.h | 5 ++++- fs/nfs/pnfs.h | 2 +- fs/nfs/pnfs_nfs.c | 19 +++++++++++-------- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/fs/nfs/client.c b/fs/nfs/client.c index 60386330aeec..dbb5131375f8 100644 --- a/fs/nfs/client.c +++ b/fs/nfs/client.c @@ -1295,7 +1295,8 @@ void nfs_clients_init(struct net *net) INIT_LIST_HEAD(&nn->nfs_volume_list); #if IS_ENABLED(CONFIG_NFS_V4) idr_init(&nn->cb_ident_idr); - INIT_LIST_HEAD(&nn->nfs4_data_server_cache); + for (int i = 0; i < NFS4_DS_CACHE_HASH_SIZE; i++) + INIT_HLIST_HEAD(&nn->nfs4_data_server_cache[i]); spin_lock_init(&nn->nfs4_data_server_lock); #endif /* CONFIG_NFS_V4 */ spin_lock_init(&nn->nfs_client_lock); @@ -1315,7 +1316,8 @@ void nfs_clients_exit(struct net *net) WARN_ON_ONCE(!list_empty(&nn->nfs_client_list)); WARN_ON_ONCE(!list_empty(&nn->nfs_volume_list)); #if IS_ENABLED(CONFIG_NFS_V4) - WARN_ON_ONCE(!list_empty(&nn->nfs4_data_server_cache)); + for (int i = 0; i < NFS4_DS_CACHE_HASH_SIZE; i++) + WARN_ON_ONCE(!hlist_empty(&nn->nfs4_data_server_cache[i])); #endif /* CONFIG_NFS_V4 */ } diff --git a/fs/nfs/netns.h b/fs/nfs/netns.h index 36658579100d..560fa95726b0 100644 --- a/fs/nfs/netns.h +++ b/fs/nfs/netns.h @@ -31,7 +31,10 @@ struct nfs_net { unsigned short nfs_callback_tcpport; unsigned short nfs_callback_tcpport6; int cb_users[NFS4_MAX_MINOR_VERSION + 1]; - struct list_head nfs4_data_server_cache; +#define NFS4_DS_CACHE_HASH_BITS 8 +#define NFS4_DS_CACHE_HASH_SIZE (1 << NFS4_DS_CACHE_HASH_BITS) + /* every entry is still in bucket 0 until the key is added */ + struct hlist_head nfs4_data_server_cache[NFS4_DS_CACHE_HASH_SIZE]; spinlock_t nfs4_data_server_lock; #endif /* CONFIG_NFS_V4 */ struct nfs_netns_client *nfs_client; diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 18ea4e8e0d85..3a1c012ba18e 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -57,7 +57,7 @@ struct nfs4_pnfs_ds_addr { }; struct nfs4_pnfs_ds { - struct list_head ds_node; /* nfs4_pnfs_dev_hlist dev_dslist */ + struct hlist_node ds_node; /* nfs_net nfs4_data_server_cache */ char *ds_remotestr; /* comma sep list of addrs */ struct list_head ds_addrs; const struct net *ds_net; diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c index e0e3fc7414e6..f88a9784988a 100644 --- a/fs/nfs/pnfs_nfs.c +++ b/fs/nfs/pnfs_nfs.c @@ -604,7 +604,7 @@ _same_data_server_addrs_locked(const struct list_head *dsaddrs1, } /* - * Lookup DS by addresses and NFS version. nfs4_ds_cache_lock is held + * Lookup DS by addresses and NFS version. nfs4_data_server_lock is held */ static struct nfs4_pnfs_ds * _data_server_lookup_locked(const struct nfs_net *nn, @@ -612,10 +612,13 @@ _data_server_lookup_locked(const struct nfs_net *nn, { struct nfs4_pnfs_ds *ds; - list_for_each_entry(ds, &nn->nfs4_data_server_cache, ds_node) - if (ds->ds_version == version && - _same_data_server_addrs_locked(&ds->ds_addrs, dsaddrs)) - return ds; + for (int i = 0; i < NFS4_DS_CACHE_HASH_SIZE; i++) + hlist_for_each_entry(ds, &nn->nfs4_data_server_cache[i], + ds_node) + if (ds->ds_version == version && + _same_data_server_addrs_locked(&ds->ds_addrs, + dsaddrs)) + return ds; return NULL; } @@ -666,7 +669,7 @@ void nfs4_pnfs_ds_put(struct nfs4_pnfs_ds *ds) struct nfs_net *nn = net_generic(ds->ds_net, nfs_net_id); if (refcount_dec_and_lock(&ds->ds_count, &nn->nfs4_data_server_lock)) { - list_del_init(&ds->ds_node); + hlist_del_init(&ds->ds_node); spin_unlock(&nn->nfs4_data_server_lock); destroy_ds(ds); } @@ -753,11 +756,11 @@ nfs4_pnfs_ds_add(const struct net *net, struct list_head *dsaddrs, u32 version, list_splice_init(dsaddrs, &ds->ds_addrs); ds->ds_remotestr = remotestr; refcount_set(&ds->ds_count, 1); - INIT_LIST_HEAD(&ds->ds_node); + INIT_HLIST_NODE(&ds->ds_node); ds->ds_net = net; ds->ds_clp = NULL; ds->ds_version = version; - list_add(&ds->ds_node, &nn->nfs4_data_server_cache); + hlist_add_head(&ds->ds_node, &nn->nfs4_data_server_cache[0]); dprintk("%s add new data server %s\n", __func__, ds->ds_remotestr); } else { From bcb2579c9d9ecdfebcc295fef7e780bc0d72eaf1 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:25 -0400 Subject: [PATCH 41/68] NFSv4/pnfs: Key the data-server cache by its address set and version Populating the data-server cache was quadratic: every GETDEVICEINFO decode scanned the whole per-net cache under one spinlock. At the anticipated scale of 1024 data servers a striping mount pays that in a burst at first access, and again on notification-driven re-resolution. Hash each DS to a bucket keyed on the whole cache key -- per-address jhash over exactly the fields same_sockaddr() compares, combined by addition so multipath ordering cannot change the bucket, then the NFS version folded in -- with the existing comparator as the in-bucket tiebreaker. Lookup and insert now touch one bucket; teardown is unchanged (hlist_del_init needs no bucket). Keying on the whole set requires the comparator to test set equality, so it is tightened from the subset test it did before -- a subset match would hash to a different bucket and simply never be found. That test was also wrong in a way worth naming. It answered "is dsaddrs1 a subset of dsaddrs2", and the caller passes the cached list first, so a cached data server whose address set was contained in an incoming one was returned for that incoming set. The aliasing was therefore one-directional: cache {A,B} first and an incoming {A} did not match, but cache {A} first and an incoming {A,B} did. The consequence was mild, which is why it went unnoticed: every address on one device's multipath list names the same data server, so a merged entry's addresses are all paths that server also advertised. The effect is lost path diversity and a truncated ds_remotestr (and the netaddr flexfiles reports in layoutstats), not I/O sent to the wrong server. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/pnfs_nfs.c | 68 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c index f88a9784988a..c9bbcb765f4a 100644 --- a/fs/nfs/pnfs_nfs.c +++ b/fs/nfs/pnfs_nfs.c @@ -15,6 +15,8 @@ #include "nfs4session.h" #include "internal.h" +#include +#include #include "pnfs.h" #include "netns.h" #include "nfs4trace.h" @@ -577,8 +579,8 @@ same_sockaddr(struct sockaddr *addr1, struct sockaddr *addr2) } /* - * Checks if 'dsaddrs1' contains a subset of 'dsaddrs2'. If it does, - * declare a match. + * Checks if 'dsaddrs1' and 'dsaddrs2' hold the same set of addresses. + * If they do, declare a match. */ static bool _same_data_server_addrs_locked(const struct list_head *dsaddrs1, @@ -588,6 +590,10 @@ _same_data_server_addrs_locked(const struct list_head *dsaddrs1, struct sockaddr *sa1, *sa2; bool match = false; + if (list_count_nodes((struct list_head *)dsaddrs1) != + list_count_nodes((struct list_head *)dsaddrs2)) + return false; + list_for_each_entry(da1, dsaddrs1, da_node) { sa1 = (struct sockaddr *)&da1->da_addr; match = false; @@ -603,6 +609,47 @@ _same_data_server_addrs_locked(const struct list_head *dsaddrs1, return match; } +/* Hash family, address bytes, and port - as same_sockaddr() */ +static u32 +nfs4_ds_addr_hash(const struct sockaddr *sa) +{ + u32 h = sa->sa_family; + + switch (sa->sa_family) { + case AF_INET: { + const struct sockaddr_in *a = (const struct sockaddr_in *)sa; + + h = jhash(&a->sin_addr.s_addr, sizeof(a->sin_addr.s_addr), h); + h = jhash(&a->sin_port, sizeof(a->sin_port), h); + break; + } + case AF_INET6: { + const struct sockaddr_in6 *a = (const struct sockaddr_in6 *)sa; + + h = jhash(&a->sin6_addr, sizeof(a->sin6_addr), h); + h = jhash(&a->sin6_port, sizeof(a->sin6_port), h); + break; + } + } + return h; +} + +/* + * Bucket index for a DS cache key. Per-address hashes combine by + * addition so the multipath list order cannot change the bucket, + * matching the order-independent set comparison above. + */ +static u32 +nfs4_ds_cache_hash(const struct list_head *dsaddrs, u32 version) +{ + const struct nfs4_pnfs_ds_addr *da; + u32 h = 0; + + list_for_each_entry(da, dsaddrs, da_node) + h += nfs4_ds_addr_hash((const struct sockaddr *)&da->da_addr); + return hash_32(jhash_1word(version, h), NFS4_DS_CACHE_HASH_BITS); +} + /* * Lookup DS by addresses and NFS version. nfs4_data_server_lock is held */ @@ -611,14 +658,12 @@ _data_server_lookup_locked(const struct nfs_net *nn, const struct list_head *dsaddrs, u32 version) { struct nfs4_pnfs_ds *ds; + u32 bucket = nfs4_ds_cache_hash(dsaddrs, version); - for (int i = 0; i < NFS4_DS_CACHE_HASH_SIZE; i++) - hlist_for_each_entry(ds, &nn->nfs4_data_server_cache[i], - ds_node) - if (ds->ds_version == version && - _same_data_server_addrs_locked(&ds->ds_addrs, - dsaddrs)) - return ds; + hlist_for_each_entry(ds, &nn->nfs4_data_server_cache[bucket], ds_node) + if (ds->ds_version == version && + _same_data_server_addrs_locked(&ds->ds_addrs, dsaddrs)) + return ds; return NULL; } @@ -735,6 +780,7 @@ nfs4_pnfs_ds_add(const struct net *net, struct list_head *dsaddrs, u32 version, { struct nfs_net *nn = net_generic(net, nfs_net_id); struct nfs4_pnfs_ds *tmp_ds, *ds = NULL; + struct hlist_head *bucket; char *remotestr; if (list_empty(dsaddrs)) { @@ -748,6 +794,8 @@ nfs4_pnfs_ds_add(const struct net *net, struct list_head *dsaddrs, u32 version, /* this is only used for debugging, so it's ok if its NULL */ remotestr = nfs4_pnfs_remotestr(dsaddrs, gfp_flags); + /* @dsaddrs is empty after the splice below. */ + bucket = &nn->nfs4_data_server_cache[nfs4_ds_cache_hash(dsaddrs, version)]; spin_lock(&nn->nfs4_data_server_lock); tmp_ds = _data_server_lookup_locked(nn, dsaddrs, version); @@ -760,7 +808,7 @@ nfs4_pnfs_ds_add(const struct net *net, struct list_head *dsaddrs, u32 version, ds->ds_net = net; ds->ds_clp = NULL; ds->ds_version = version; - hlist_add_head(&ds->ds_node, &nn->nfs4_data_server_cache[0]); + hlist_add_head(&ds->ds_node, bucket); dprintk("%s add new data server %s\n", __func__, ds->ds_remotestr); } else { From e0697d5806ffa0206f8d63c558408559633fb2fe Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Tue, 15 Sep 2026 08:22:26 -0400 Subject: [PATCH 42/68] NFSv4/flexfiles: Add a dataserver_nconnect cap Data-server clients inherit the MDS nconnect setting. A striping mount multiplies that by every distinct data server: at the anticipated scale of 1024 and nconnect=16 the client opens 16k sockets plus their sunrpc slot tables, and striping already spreads I/O across the data servers, so a high per-DS transport count buys little for that workload. Add a dataserver_nconnect module parameter to the flexfiles layout driver alongside its existing dataserver_timeo and dataserver_retrans knobs, and thread the value through nfs4_pnfs_ds_connect() to both the v3 and v4 data-server client setup paths. The default of 0 preserves today's inherit-from-MDS behavior, so the cap is opt-in and non-regressing. The files layout passes 0, unchanged. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/filelayout/filelayoutdev.c | 2 +- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 7 +++++++ fs/nfs/internal.h | 3 ++- fs/nfs/nfs3client.c | 9 +++++++-- fs/nfs/nfs4client.c | 6 +++++- fs/nfs/pnfs.h | 3 ++- fs/nfs/pnfs_nfs.c | 21 ++++++++++++++------- 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/fs/nfs/filelayout/filelayoutdev.c b/fs/nfs/filelayout/filelayoutdev.c index 57e0654cd98c..5995f32f07ec 100644 --- a/fs/nfs/filelayout/filelayoutdev.c +++ b/fs/nfs/filelayout/filelayoutdev.c @@ -267,7 +267,7 @@ nfs4_fl_prepare_ds(struct pnfs_layout_segment *lseg, u32 ds_idx) goto out_test_devid; status = nfs4_pnfs_ds_connect(s, ds, devid, dataserver_timeo, - dataserver_retrans, 4, + dataserver_retrans, 0, 4, s->nfs_client->cl_minorversion, true); if (status) { nfs4_mark_deviceid_unavailable(devid); diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index 5cb09e5e2138..d52f485d0650 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -20,6 +20,7 @@ static unsigned int dataserver_timeo = NFS_DEF_TCP_TIMEO; static unsigned int dataserver_retrans; +static unsigned int dataserver_nconnect; static bool ff_layout_has_available_ds(struct pnfs_layout_segment *lseg); @@ -418,6 +419,7 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, */ status = nfs4_pnfs_ds_connect(s, ds, &mirror_ds->id_node, dataserver_timeo, dataserver_retrans, + dataserver_nconnect, mirror_ds->ds_versions[0].version, mirror_ds->ds_versions[0].minor_version, mirror_ds->ds_versions[0].tightly_coupled); @@ -676,3 +678,8 @@ module_param(dataserver_timeo, uint, 0644); MODULE_PARM_DESC(dataserver_timeo, "The time (in tenths of a second) the " "NFSv4.1 client waits for a response from a " " data server before it retries an NFS request."); +module_param(dataserver_nconnect, uint, 0644); +MODULE_PARM_DESC(dataserver_nconnect, "The maximum number of connections " + "the NFSv4.1 client opens to each data server, " + "capping the value inherited from the MDS nconnect " + "mount option. 0 (default) applies no cap."); diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h index abc81f5ae578..48f7c0e25da1 100644 --- a/fs/nfs/internal.h +++ b/fs/nfs/internal.h @@ -251,6 +251,7 @@ extern struct nfs_client *nfs4_set_ds_client(struct nfs_server *mds_srv, int ds_addrlen, int ds_proto, unsigned int ds_timeo, unsigned int ds_retrans, + unsigned int ds_nconnect, u32 minor_version, bool tightly_coupled); extern struct rpc_clnt *nfs4_find_or_create_ds_client(struct nfs_client *, @@ -260,7 +261,7 @@ extern void nfs4_session_limit_xasize(struct nfs_server *server); extern struct nfs_client *nfs3_set_ds_client(struct nfs_server *mds_srv, const struct sockaddr_storage *ds_addr, int ds_addrlen, int ds_proto, unsigned int ds_timeo, - unsigned int ds_retrans); + unsigned int ds_retrans, unsigned int ds_nconnect); #ifdef CONFIG_PROC_FS extern int __init nfs_fs_proc_init(void); extern void nfs_fs_proc_exit(void); diff --git a/fs/nfs/nfs3client.c b/fs/nfs/nfs3client.c index 5d97c1d38bb6..cf2f7be4b435 100644 --- a/fs/nfs/nfs3client.c +++ b/fs/nfs/nfs3client.c @@ -84,7 +84,8 @@ struct nfs_server *nfs3_clone_server(struct nfs_server *source, */ struct nfs_client *nfs3_set_ds_client(struct nfs_server *mds_srv, const struct sockaddr_storage *ds_addr, int ds_addrlen, - int ds_proto, unsigned int ds_timeo, unsigned int ds_retrans) + int ds_proto, unsigned int ds_timeo, unsigned int ds_retrans, + unsigned int ds_nconnect) { struct rpc_timeout ds_timeout; unsigned long connect_timeout = ds_timeo * (ds_retrans + 1) * HZ / 10; @@ -124,8 +125,12 @@ struct nfs_client *nfs3_set_ds_client(struct nfs_server *mds_srv, fallthrough; case XPRT_TRANSPORT_RDMA: case XPRT_TRANSPORT_TCP: - if (mds_clp->cl_nconnect > 1) + if (mds_clp->cl_nconnect > 1) { cl_init.nconnect = mds_clp->cl_nconnect; + if (ds_nconnect) + cl_init.nconnect = min(cl_init.nconnect, + ds_nconnect); + } } if (mds_srv->flags & NFS_MOUNT_NORESVPORT) diff --git a/fs/nfs/nfs4client.c b/fs/nfs/nfs4client.c index e6a589913666..fe779fb2ec72 100644 --- a/fs/nfs/nfs4client.c +++ b/fs/nfs/nfs4client.c @@ -794,7 +794,8 @@ static int nfs4_set_client(struct nfs_server *server, struct nfs_client *nfs4_set_ds_client(struct nfs_server *mds_srv, const struct sockaddr_storage *ds_addr, int ds_addrlen, int ds_proto, unsigned int ds_timeo, unsigned int ds_retrans, - u32 minor_version, bool tightly_coupled) + unsigned int ds_nconnect, u32 minor_version, + bool tightly_coupled) { struct rpc_timeout ds_timeout; struct nfs_client *mds_clp = mds_srv->nfs_client; @@ -832,6 +833,9 @@ struct nfs_client *nfs4_set_ds_client(struct nfs_server *mds_srv, case XPRT_TRANSPORT_TCP: if (mds_clp->cl_nconnect > 1) { cl_init.nconnect = mds_clp->cl_nconnect; + if (ds_nconnect) + cl_init.nconnect = min(cl_init.nconnect, + ds_nconnect); cl_init.max_connect = NFS_MAX_TRANSPORTS; } } diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 3a1c012ba18e..5cde5db63d29 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -504,7 +504,8 @@ struct nfs4_pnfs_ds *nfs4_pnfs_ds_add(const struct net *net, void nfs4_pnfs_v3_ds_connect_unload(void); int nfs4_pnfs_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds, struct nfs4_deviceid_node *devid, unsigned int timeo, - unsigned int retrans, u32 version, u32 minor_version, + unsigned int retrans, unsigned int nconnect, + u32 version, u32 minor_version, bool tightly_coupled); struct nfs4_pnfs_ds_addr *nfs4_decode_mp_ds_addr(struct net *net, struct xdr_stream *xdr, diff --git a/fs/nfs/pnfs_nfs.c b/fs/nfs/pnfs_nfs.c index c9bbcb765f4a..7adb6f941cf2 100644 --- a/fs/nfs/pnfs_nfs.c +++ b/fs/nfs/pnfs_nfs.c @@ -844,7 +844,8 @@ static struct nfs_client *(*get_v3_ds_connect)( int ds_addrlen, int ds_proto, unsigned int ds_timeo, - unsigned int ds_retrans); + unsigned int ds_retrans, + unsigned int ds_nconnect); static bool load_v3_ds_connect(void) { @@ -867,7 +868,8 @@ void nfs4_pnfs_v3_ds_connect_unload(void) static int _nfs4_pnfs_v3_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds, unsigned int timeo, - unsigned int retrans) + unsigned int retrans, + unsigned int nconnect) { struct nfs_client *clp = ERR_PTR(-EIO); struct nfs_client *mds_clp = mds_srv->nfs_client; @@ -919,7 +921,7 @@ static int _nfs4_pnfs_v3_ds_connect(struct nfs_server *mds_srv, ds_proto = XPRT_TRANSPORT_TCP_TLS; clp = get_v3_ds_connect(mds_srv, &da->da_addr, da->da_addrlen, - ds_proto, timeo, retrans); + ds_proto, timeo, retrans, nconnect); if (IS_ERR(clp)) continue; clp->cl_rpcclient->cl_softerr = 0; @@ -942,6 +944,7 @@ static int _nfs4_pnfs_v4_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds, unsigned int timeo, unsigned int retrans, + unsigned int nconnect, u32 minor_version, bool tightly_coupled) { @@ -1033,7 +1036,8 @@ static int _nfs4_pnfs_v4_ds_connect(struct nfs_server *mds_srv, clp = nfs4_set_ds_client(mds_srv, &da->da_addr, da->da_addrlen, ds_proto, - timeo, retrans, minor_version, + timeo, retrans, nconnect, + minor_version, tightly_coupled); if (IS_ERR(clp)) continue; @@ -1068,7 +1072,8 @@ static int _nfs4_pnfs_v4_ds_connect(struct nfs_server *mds_srv, */ int nfs4_pnfs_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds, struct nfs4_deviceid_node *devid, unsigned int timeo, - unsigned int retrans, u32 version, u32 minor_version, + unsigned int retrans, unsigned int nconnect, + u32 version, u32 minor_version, bool tightly_coupled) { int err; @@ -1088,11 +1093,13 @@ int nfs4_pnfs_ds_connect(struct nfs_server *mds_srv, struct nfs4_pnfs_ds *ds, switch (version) { case 3: - err = _nfs4_pnfs_v3_ds_connect(mds_srv, ds, timeo, retrans); + err = _nfs4_pnfs_v3_ds_connect(mds_srv, ds, timeo, retrans, + nconnect); break; case 4: err = _nfs4_pnfs_v4_ds_connect(mds_srv, ds, timeo, retrans, - minor_version, tightly_coupled); + nconnect, minor_version, + tightly_coupled); break; default: dprintk("%s: unsupported DS version %d\n", __func__, version); From 3a612faba5c40a59257db50e3a2da30c02a8c969 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Wed, 9 Sep 2026 13:11:47 -0400 Subject: [PATCH 43/68] NFSv4/flexfiles: report the intended opnum when DS connection setup fails When nfs4_ff_layout_prepare_ds() cannot establish a connection to a data server, it records a device error with opnum OP_ILLEGAL, since no operation ever made it to the wire. But servers can use the ff_ioerr4 opnum to distinguish failed read-class operations from write-class operations when deciding how to recover the affected mirror -- a mirror that may have missed writes needs to be brought back in sync, while one that merely failed to serve a read does not. OP_ILLEGAL gives the server nothing to act on. Every caller of nfs4_ff_layout_prepare_ds() knows which operation it was preparing to send, and the existing fail_return argument already divides the callers along the same boundary (false for READ, true for WRITE and COMMIT). Replace the boolean with the intended opnum, derive the layout return decision from it, and report it in the tracked device error instead of OP_ILLEGAL. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Reviewed-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 13 ++++++++----- fs/nfs/flexfilelayout/flexfilelayout.h | 2 +- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 14 ++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 2ea63fda0035..6fa6aa82d509 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -883,7 +883,7 @@ ff_layout_choose_ds_for_read(struct pnfs_layout_segment *lseg, mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, *dss_id); ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, - *dss_id, false); + *dss_id, OP_READ); if (IS_ERR(ds)) { nfs4_ff_layout_put_deviceid(mirror_ds); ret = ERR_CAST(ds); @@ -1132,7 +1132,7 @@ ff_layout_pg_init_write(struct nfs_pageio_descriptor *pgio, mirror_ds = ff_layout_get_mirror_ds(pgio->pg_lseg->pls_layout, mirror, dss_id); ds = nfs4_ff_layout_prepare_ds(pgio->pg_lseg, mirror, - mirror_ds, dss_id, true); + mirror_ds, dss_id, OP_WRITE); if (IS_ERR(ds)) { nfs4_ff_layout_put_deviceid(mirror_ds); if (!ff_layout_no_fallback_to_mds(pgio->pg_lseg)) @@ -2188,7 +2188,8 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) mirror->dss_count, offset); mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, dss_id); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, false); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, + OP_READ); if (IS_ERR(ds)) { ds_fatal_error = nfs_error_is_fatal(PTR_ERR(ds)); goto out_failed; @@ -2288,7 +2289,8 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) mirror->dss_count, offset); mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, dss_id); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, true); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, + OP_WRITE); if (IS_ERR(ds)) { ds_fatal_error = nfs_error_is_fatal(PTR_ERR(ds)); goto out_failed; @@ -2398,7 +2400,8 @@ static int ff_layout_initiate_commit(struct nfs_commit_data *data, int how) mirror = FF_LAYOUT_COMP(lseg, idx); dss_id = calc_dss_id_from_commit(lseg, data->ds_commit_index); mirror_ds = ff_layout_get_mirror_ds(lseg->pls_layout, mirror, dss_id); - ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, true); + ds = nfs4_ff_layout_prepare_ds(lseg, mirror, mirror_ds, dss_id, + OP_COMMIT); if (IS_ERR(ds)) goto out_err; diff --git a/fs/nfs/flexfilelayout/flexfilelayout.h b/fs/nfs/flexfilelayout/flexfilelayout.h index 72b11034851a..09c3cd6964fd 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.h +++ b/fs/nfs/flexfilelayout/flexfilelayout.h @@ -256,7 +256,7 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, struct nfs4_ff_layout_mirror *mirror, struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id, - bool fail_return); + enum nfs_opnum4 opnum); struct rpc_clnt * nfs4_ff_find_or_create_ds_client(const struct nfs4_ff_layout_ds *mirror_ds, diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index d52f485d0650..bc95628a811f 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -379,7 +379,7 @@ ff_layout_get_mirror_ds(struct pnfs_layout_hdr *lo, * @mirror_ds: referenced device node for the stripe, from * ff_layout_get_mirror_ds() (may be an ERR_PTR) * @dss_id: DS stripe id to select stripe to use - * @fail_return: return layout on connect failure? + * @opnum: operation this connection is being prepared for * * Try to prepare a DS connection to accept an RPC call. This involves * selecting a mirror to use and connecting the client to it if it's not @@ -387,8 +387,10 @@ ff_layout_get_mirror_ds(struct pnfs_layout_hdr *lo, * * Since we only need a single functioning mirror to satisfy a read, we don't * want to return the layout if there is one. For writes though, any down - * mirror should result in a LAYOUTRETURN. @fail_return is how we distinguish - * between the two cases. + * mirror should result in a LAYOUTRETURN. @opnum is how we distinguish + * between the two cases. On failure, @opnum is also reported in the tracked + * device error so that the server can tell which class of I/O the client + * was unable to send to the mirror. * * Returns a pointer to a connected DS object on success or NULL on failure. */ @@ -397,7 +399,7 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, struct nfs4_ff_layout_mirror *mirror, struct nfs4_ff_layout_ds *mirror_ds, u32 dss_id, - bool fail_return) + enum nfs_opnum4 opnum) { struct nfs4_pnfs_ds *ds; struct inode *ino = lseg->pls_layout->plh_inode; @@ -448,9 +450,9 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, NULL : &mirror_ds->id_node, dss_id, lseg->pls_range.offset, lseg->pls_range.length, NFS4ERR_NXIO, - OP_ILLEGAL, GFP_NOIO); + opnum, GFP_NOIO); ff_layout_send_layouterror(lseg); - if (fail_return || !ff_layout_has_available_ds(lseg)) + if (opnum != OP_READ || !ff_layout_has_available_ds(lseg)) pnfs_error_mark_layout_for_return(ino, lseg); ds = ERR_PTR(status); out: From 94dc092b208ad324981caa85fcf8416b905ca593 Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Wed, 9 Sep 2026 13:11:48 -0400 Subject: [PATCH 44/68] pNFS: allow layout drivers to cancel I/O to a single device The ->cancel_io() layout operation cancels all in-flight I/O for a layout segment that is being returned. When the return is triggered by the failure of a single mirror instance, cancelling I/O to the healthy mirrors is both unnecessary and harmful: requests that were already transmitted cannot be un-sent, and will complete on the data servers after the layout has been returned. Give ->cancel_io() a device ID argument identifying the failed device, and thread it through pnfs_error_mark_layout_for_return() and pnfs_mark_matching_lsegs_return(). The flexfiles implementation compares it against the raw device ID from the layout, so that it can identify the failed mirror instance even when its device ID node was never instantiated. A NULL device ID preserves the existing cancel-everything behavior, and all callers pass NULL for now, so this patch makes no change in behavior. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Reviewed-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/blocklayout/blocklayout.c | 6 +++-- fs/nfs/callback_proc.c | 2 +- fs/nfs/filelayout/filelayout.c | 4 +-- fs/nfs/flexfilelayout/flexfilelayout.c | 16 +++++++----- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 2 +- fs/nfs/pnfs.c | 32 ++++++++++++++--------- fs/nfs/pnfs.h | 14 ++++++---- 7 files changed, 46 insertions(+), 30 deletions(-) diff --git a/fs/nfs/blocklayout/blocklayout.c b/fs/nfs/blocklayout/blocklayout.c index d54a141a89b3..d86702e604f9 100644 --- a/fs/nfs/blocklayout/blocklayout.c +++ b/fs/nfs/blocklayout/blocklayout.c @@ -859,7 +859,8 @@ bl_pg_init_read(struct nfs_pageio_descriptor *pgio, struct nfs_page *req) if (pgio->pg_lseg && test_bit(NFS_LSEG_UNAVAILABLE, &pgio->pg_lseg->pls_flags)) { - pnfs_error_mark_layout_for_return(pgio->pg_inode, pgio->pg_lseg); + pnfs_error_mark_layout_for_return(pgio->pg_inode, pgio->pg_lseg, + NULL); pnfs_set_lo_fail(pgio->pg_lseg); nfs_pageio_reset_read_mds(pgio); } @@ -921,7 +922,8 @@ bl_pg_init_write(struct nfs_pageio_descriptor *pgio, struct nfs_page *req) if (pgio->pg_lseg && test_bit(NFS_LSEG_UNAVAILABLE, &pgio->pg_lseg->pls_flags)) { - pnfs_error_mark_layout_for_return(pgio->pg_inode, pgio->pg_lseg); + pnfs_error_mark_layout_for_return(pgio->pg_inode, pgio->pg_lseg, + NULL); pnfs_set_lo_fail(pgio->pg_lseg); nfs_pageio_reset_write_mds(pgio); } diff --git a/fs/nfs/callback_proc.c b/fs/nfs/callback_proc.c index c9b71dbae9ea..9d836cb72e78 100644 --- a/fs/nfs/callback_proc.c +++ b/fs/nfs/callback_proc.c @@ -292,7 +292,7 @@ static u32 initiate_file_draining(struct nfs_client *clp, switch (pnfs_mark_matching_lsegs_return(lo, &free_me_list, &args->cbl_range, be32_to_cpu(args->cbl_stateid.seqid), - args->cbl_layoutchanged)) { + args->cbl_layoutchanged, NULL)) { case 0: case -EBUSY: /* There are layout segments that need to be returned */ diff --git a/fs/nfs/filelayout/filelayout.c b/fs/nfs/filelayout/filelayout.c index 0d53277f972e..d1c08d529e02 100644 --- a/fs/nfs/filelayout/filelayout.c +++ b/fs/nfs/filelayout/filelayout.c @@ -186,7 +186,7 @@ static int filelayout_async_handle_error(struct rpc_task *task, dprintk("%s DS connection error %d\n", __func__, task->tk_status); nfs4_mark_deviceid_unavailable(devid); - pnfs_error_mark_layout_for_return(inode, lseg); + pnfs_error_mark_layout_for_return(inode, lseg, NULL); pnfs_set_lo_fail(lseg); rpc_wake_up(&tbl->slot_tbl_waitq); fallthrough; @@ -856,7 +856,7 @@ fl_pnfs_update_layout(struct inode *ino, status = filelayout_check_deviceid(lo, fl, gfp_flags); if (status) { - pnfs_error_mark_layout_for_return(ino, lseg); + pnfs_error_mark_layout_for_return(ino, lseg, NULL); pnfs_set_lo_fail(lseg); pnfs_put_lseg(lseg); lseg = NULL; diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 6fa6aa82d509..d27e0adb2709 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -1281,7 +1281,7 @@ static void ff_layout_resend_pnfs_read(struct nfs_pgio_header *hdr) mirror_ds = ff_layout_choose_any_ds_for_read(hdr->lseg, idx, &new_idx, hdr->args.offset, &dss_id); if (IS_ERR(mirror_ds)) { - pnfs_error_mark_layout_for_return(hdr->inode, hdr->lseg); + pnfs_error_mark_layout_for_return(hdr->inode, hdr->lseg, NULL); } else { nfs4_ff_layout_put_deviceid(mirror_ds); ff_layout_send_layouterror(hdr->lseg); @@ -1294,7 +1294,7 @@ static void ff_layout_reset_read(struct nfs_pgio_header *hdr) struct rpc_task *task = &hdr->task; pnfs_layoutcommit_inode(hdr->inode, false); - pnfs_error_mark_layout_for_return(hdr->inode, hdr->lseg); + pnfs_error_mark_layout_for_return(hdr->inode, hdr->lseg, NULL); if (!test_and_set_bit(NFS_IOHDR_REDO, &hdr->flags)) { dprintk("%s Reset task %5u for i/o through MDS " @@ -1594,7 +1594,7 @@ static void ff_layout_io_track_ds_error(struct pnfs_layout_segment *lseg, fallthrough; default: pnfs_error_mark_layout_for_return(lseg->pls_layout->plh_inode, - lseg); + lseg, NULL); } out: @@ -2256,7 +2256,7 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) * FF_FLAGS_NO_IO_THRU_MDS: force fresh LAYOUTGET, * never fall through to MDS I/O. */ - pnfs_error_mark_layout_for_return(hdr->inode, lseg); + pnfs_error_mark_layout_for_return(hdr->inode, lseg, NULL); return PNFS_TRY_AGAIN; } trace_pnfs_mds_fallback_read_pagelist(hdr->inode, @@ -2359,7 +2359,7 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) * FF_FLAGS_NO_IO_THRU_MDS: force fresh LAYOUTGET, * never fall through to MDS I/O. */ - pnfs_error_mark_layout_for_return(hdr->inode, lseg); + pnfs_error_mark_layout_for_return(hdr->inode, lseg, NULL); return PNFS_TRY_AGAIN; } trace_pnfs_mds_fallback_write_pagelist(hdr->inode, @@ -2490,7 +2490,8 @@ static bool ff_layout_match_io(const struct rpc_task *task, const void *data) return false; } -static void ff_layout_cancel_io(struct pnfs_layout_segment *lseg) +static void ff_layout_cancel_io(struct pnfs_layout_segment *lseg, + const struct nfs4_deviceid *devid) { struct nfs4_ff_layout_segment *flseg = FF_LAYOUT_LSEG(lseg); struct nfs4_ff_layout_mirror *mirror; @@ -2503,6 +2504,9 @@ static void ff_layout_cancel_io(struct pnfs_layout_segment *lseg) for (idx = 0; idx < flseg->mirror_array_cnt; idx++) { mirror = flseg->mirror_array[idx]; for (dss_id = 0; dss_id < mirror->dss_count; dss_id++) { + if (devid && memcmp(&mirror->dss[dss_id].devid, devid, + sizeof(*devid)) != 0) + continue; rcu_read_lock(); mirror_ds = rcu_dereference(mirror->dss[dss_id].mirror_ds); if (IS_ERR_OR_NULL(mirror_ds) || diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index bc95628a811f..7bb0f2094e9d 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -453,7 +453,7 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, opnum, GFP_NOIO); ff_layout_send_layouterror(lseg); if (opnum != OP_READ || !ff_layout_has_available_ds(lseg)) - pnfs_error_mark_layout_for_return(ino, lseg); + pnfs_error_mark_layout_for_return(ino, lseg, NULL); ds = ERR_PTR(status); out: return ds; diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 79f671355e5b..93a0852e1e8a 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -433,7 +433,7 @@ bool nfs4_layout_refresh_old_stateid(nfs4_stateid *dst, } /* Try to update the seqid to the most recent */ err = pnfs_mark_matching_lsegs_return(lo, &head, &range, 0, - true); + true, NULL); if (err != -EBUSY) { dst->seqid = lo->plh_stateid.seqid; *dst_range = range; @@ -487,7 +487,8 @@ static int pnfs_mark_layout_stateid_return(struct pnfs_layout_hdr *lo, .length = NFS4_MAX_UINT64, }; - return pnfs_mark_matching_lsegs_return(lo, lseg_list, &range, seq, true); + return pnfs_mark_matching_lsegs_return(lo, lseg_list, &range, seq, true, + NULL); } static int @@ -525,7 +526,7 @@ pnfs_layout_io_set_failed(struct pnfs_layout_hdr *lo, u32 iomode) spin_lock(&inode->i_lock); pnfs_layout_set_fail_bit(lo, pnfs_iomode_to_fail_bit(iomode)); - pnfs_mark_matching_lsegs_return(lo, &head, &range, 0, true); + pnfs_mark_matching_lsegs_return(lo, &head, &range, 0, true, NULL); spin_unlock(&inode->i_lock); pnfs_free_lseg_list(&head); dprintk("%s Setting layout IOMODE_%s fail bit\n", __func__, @@ -740,7 +741,7 @@ pnfs_mark_matching_lsegs_invalid(struct pnfs_layout_hdr *lo, if (mark_lseg_invalid(lseg, tmp_list)) continue; remaining++; - pnfs_lseg_cancel_io(server, lseg); + pnfs_lseg_cancel_io(server, lseg, NULL); } dprintk("%s:Return %i\n", __func__, remaining); return remaining; @@ -1476,7 +1477,7 @@ _pnfs_return_layout(struct inode *ino) } valid_layout = pnfs_layout_is_valid(lo); pnfs_clear_layoutcommit(ino, &tmp_list); - pnfs_mark_matching_lsegs_return(lo, &tmp_list, &range, 0, true); + pnfs_mark_matching_lsegs_return(lo, &tmp_list, &range, 0, true, NULL); /* Don't send a LAYOUTRETURN if list was initially empty */ @@ -2658,7 +2659,8 @@ pnfs_layout_process(struct nfs4_layoutget *lgp) .iomode = IOMODE_ANY, .length = NFS4_MAX_UINT64, }; - pnfs_mark_matching_lsegs_return(lo, &free_me, &range, 0, true); + pnfs_mark_matching_lsegs_return(lo, &free_me, &range, 0, true, + NULL); goto out_forget; } else { /* We have a completely new layout */ @@ -2691,6 +2693,7 @@ pnfs_layout_process(struct nfs4_layoutget *lgp) * @return_range: describe layout segment ranges to be returned * @seq: stateid seqid to match * @cancel_io: signal io be cancelled + * @devid: only cancel io directed at this device (all devices if NULL) * * This function is mainly intended for use by layoutrecall. It attempts * to free the layout segment immediately, or else to mark it for return @@ -2705,7 +2708,8 @@ int pnfs_mark_matching_lsegs_return(struct pnfs_layout_hdr *lo, struct list_head *tmp_list, const struct pnfs_layout_range *return_range, - u32 seq, bool cancel_io) + u32 seq, bool cancel_io, + const struct nfs4_deviceid *devid) { struct pnfs_layout_segment *lseg, *next; struct nfs_server *server = NFS_SERVER(lo->plh_inode); @@ -2732,7 +2736,7 @@ pnfs_mark_matching_lsegs_return(struct pnfs_layout_hdr *lo, remaining++; set_bit(NFS_LSEG_LAYOUTRETURN, &lseg->pls_flags); if (cancel_io) - pnfs_lseg_cancel_io(server, lseg); + pnfs_lseg_cancel_io(server, lseg, devid); } if (remaining) { @@ -2750,7 +2754,8 @@ pnfs_mark_matching_lsegs_return(struct pnfs_layout_hdr *lo, static void pnfs_mark_layout_for_return(struct inode *inode, - const struct pnfs_layout_range *range) + const struct pnfs_layout_range *range, + const struct nfs4_deviceid *devid) { struct pnfs_layout_hdr *lo; bool return_now = false; @@ -2768,7 +2773,7 @@ pnfs_mark_layout_for_return(struct inode *inode, * for how it works. */ if (pnfs_mark_matching_lsegs_return(lo, &lo->plh_return_segs, range, 0, - true) != -EBUSY) { + true, devid) != -EBUSY) { const struct cred *cred; nfs4_stateid stateid; enum pnfs_iomode iomode; @@ -2785,7 +2790,8 @@ pnfs_mark_layout_for_return(struct inode *inode, } void pnfs_error_mark_layout_for_return(struct inode *inode, - struct pnfs_layout_segment *lseg) + struct pnfs_layout_segment *lseg, + const struct nfs4_deviceid *devid) { struct pnfs_layout_range range = { .iomode = lseg->pls_range.iomode, @@ -2793,7 +2799,7 @@ void pnfs_error_mark_layout_for_return(struct inode *inode, .length = NFS4_MAX_UINT64, }; - pnfs_mark_layout_for_return(inode, &range); + pnfs_mark_layout_for_return(inode, &range, devid); } EXPORT_SYMBOL_GPL(pnfs_error_mark_layout_for_return); @@ -2883,7 +2889,7 @@ static int pnfs_layout_return_unused_byserver(struct nfs_server *server, pnfs_get_layout_hdr(lo); pnfs_set_plh_return_info(lo, range->iomode, 0); if (pnfs_mark_matching_lsegs_return(lo, &lo->plh_return_segs, - range, 0, true) != 0 || + range, 0, true, NULL) != 0 || !pnfs_prepare_layoutreturn(lo, &stateid, &cred, &iomode)) { spin_unlock(&inode->i_lock); rcu_read_unlock(); diff --git a/fs/nfs/pnfs.h b/fs/nfs/pnfs.h index 5cde5db63d29..2774d4adf4c3 100644 --- a/fs/nfs/pnfs.h +++ b/fs/nfs/pnfs.h @@ -197,7 +197,8 @@ struct pnfs_layoutdriver_type { int (*prepare_layoutcommit) (struct nfs4_layoutcommit_args *args); int (*prepare_layoutstats) (struct nfs42_layoutstat_args *args); - void (*cancel_io)(struct pnfs_layout_segment *lseg); + void (*cancel_io)(struct pnfs_layout_segment *lseg, + const struct nfs4_deviceid *devid); }; struct pnfs_commit_ops { @@ -320,7 +321,8 @@ int pnfs_mark_matching_lsegs_invalid(struct pnfs_layout_hdr *lo, int pnfs_mark_matching_lsegs_return(struct pnfs_layout_hdr *lo, struct list_head *tmp_list, const struct pnfs_layout_range *recall_range, - u32 seq, bool cancel_io); + u32 seq, bool cancel_io, + const struct nfs4_deviceid *devid); int pnfs_mark_layout_stateid_invalid(struct pnfs_layout_hdr *lo, struct list_head *lseg_list); bool pnfs_roc(struct inode *ino, struct nfs4_layoutreturn_args *args, @@ -370,7 +372,8 @@ int pnfs_read_done_resend_to_mds(struct nfs_pgio_header *); int pnfs_write_done_resend_to_mds(struct nfs_pgio_header *); struct nfs4_threshold *pnfs_mdsthreshold_alloc(void); void pnfs_error_mark_layout_for_return(struct inode *inode, - struct pnfs_layout_segment *lseg); + struct pnfs_layout_segment *lseg, + const struct nfs4_deviceid *devid); void pnfs_layout_return_unused_byclid(struct nfs_client *clp, enum pnfs_iomode iomode); void pnfs_layout_reresolve_deviceid_byclid(struct nfs_client *clp, @@ -774,10 +777,11 @@ pnfs_lseg_request_intersecting(struct pnfs_layout_segment *lseg, struct nfs_page } static inline void pnfs_lseg_cancel_io(struct nfs_server *server, - struct pnfs_layout_segment *lseg) + struct pnfs_layout_segment *lseg, + const struct nfs4_deviceid *devid) { if (server->pnfs_curr_ld->cancel_io) - server->pnfs_curr_ld->cancel_io(lseg); + server->pnfs_curr_ld->cancel_io(lseg, devid); } extern unsigned int layoutstats_timer; From 65b03734daaeae41b79d5b498f9e9ef90ecc2d5e Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Wed, 9 Sep 2026 13:11:49 -0400 Subject: [PATCH 45/68] NFSv4/flexfiles: only cancel I/O to a failed mirror instance When an error causes the flexfiles driver to return a layout, ff_layout_cancel_io() kills every in-flight RPC for the layout segment, across all mirror instances. Cancelled requests that had already been transmitted to a healthy data server cannot be un-sent: they complete on the data server after the client has sent its LAYOUTRETURN, and the metadata server then observes writes to a file for which no write layout is outstanding. RFC 8881 Section 20.3.4 recommends that the client wait for the response from in-process or in-flight READ, WRITE, or COMMIT operations before returning the layout, and the machinery for that wait already exists: the LAYOUTRETURN is deferred until every request drops its layout segment reference, and requests that have not yet been transmitted exit at RPC prepare time once the segment has been invalidated. Cancellation is only needed to avoid waiting forever on a device that will never answer. Pass the failed instance's device ID when marking the layout for return, so that ff_layout_cancel_io() cancels only I/O directed at the device we have given up on. In-flight I/O to the remaining healthy instances drains normally -- typically within a round trip -- before the LAYOUTRETURN is sent. If a spared instance turns out to be unresponsive, its requests fail with their own device error, and the resulting layout return cancels its I/O in turn. Layout recalls with clora_changed set, bulk returns, and layout revocations continue to cancel I/O to every device, as do error paths where no single failed device can be identified. Assisted-by: Claude:claude-fable-5 Signed-off-by: Benjamin Coddington Reviewed-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 9 ++++++--- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index d27e0adb2709..94cc324b591f 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -1594,7 +1594,8 @@ static void ff_layout_io_track_ds_error(struct pnfs_layout_segment *lseg, fallthrough; default: pnfs_error_mark_layout_for_return(lseg->pls_layout->plh_inode, - lseg, NULL); + lseg, + &mirror->dss[dss_id].devid); } out: @@ -2256,7 +2257,8 @@ ff_layout_read_pagelist(struct nfs_pgio_header *hdr) * FF_FLAGS_NO_IO_THRU_MDS: force fresh LAYOUTGET, * never fall through to MDS I/O. */ - pnfs_error_mark_layout_for_return(hdr->inode, lseg, NULL); + pnfs_error_mark_layout_for_return(hdr->inode, lseg, + &mirror->dss[dss_id].devid); return PNFS_TRY_AGAIN; } trace_pnfs_mds_fallback_read_pagelist(hdr->inode, @@ -2359,7 +2361,8 @@ ff_layout_write_pagelist(struct nfs_pgio_header *hdr, int sync) * FF_FLAGS_NO_IO_THRU_MDS: force fresh LAYOUTGET, * never fall through to MDS I/O. */ - pnfs_error_mark_layout_for_return(hdr->inode, lseg, NULL); + pnfs_error_mark_layout_for_return(hdr->inode, lseg, + &mirror->dss[dss_id].devid); return PNFS_TRY_AGAIN; } trace_pnfs_mds_fallback_write_pagelist(hdr->inode, diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index 7bb0f2094e9d..6165c41fcf62 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -453,7 +453,8 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, opnum, GFP_NOIO); ff_layout_send_layouterror(lseg); if (opnum != OP_READ || !ff_layout_has_available_ds(lseg)) - pnfs_error_mark_layout_for_return(ino, lseg, NULL); + pnfs_error_mark_layout_for_return(ino, lseg, + &mirror->dss[dss_id].devid); ds = ERR_PTR(status); out: return ds; From 9ba13ef0e3ad033d3ddbaeb740b12d135a88363d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Jean?= Date: Mon, 14 Sep 2026 20:42:38 +0000 Subject: [PATCH 46/68] NFS: filelayout: calculate dense stripe width in 64 bits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filelayout_get_dense_offset() calculates the stripe width from the stripe unit and count, where both values are provided by the server. Their product is stored on 32 bits, and can wrap to zero despite passing individual checks. For example, a 1 GiB stripe unit (0x40000000) and four stripes (4) cause the following div_u64() to raise a divide error during the first read through the dense layout. Update u32 to u64 and use div64_u64() instead of div_u64(). stripe_unit is a u32 and stripe_count is limited to 4096, so the product cannot overflow a u64. Fixes: cfe7f4120f8b ("NFSv4.1: filelayout i/o helpers") Assisted-by: Codex:gpt-5 Signed-off-by: Jérémy Jean Signed-off-by: Anna Schumaker --- fs/nfs/filelayout/filelayout.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/nfs/filelayout/filelayout.c b/fs/nfs/filelayout/filelayout.c index d1c08d529e02..53808e42546a 100644 --- a/fs/nfs/filelayout/filelayout.c +++ b/fs/nfs/filelayout/filelayout.c @@ -55,12 +55,13 @@ static loff_t filelayout_get_dense_offset(struct nfs4_filelayout_segment *flseg, loff_t offset) { - u32 stripe_width = flseg->stripe_unit * flseg->dsaddr->stripe_count; + u64 stripe_width = (u64)flseg->stripe_unit * + flseg->dsaddr->stripe_count; u64 stripe_no; u32 rem; offset -= flseg->pattern_offset; - stripe_no = div_u64(offset, stripe_width); + stripe_no = div64_u64(offset, stripe_width); div_u64_rem(offset, flseg->stripe_unit, &rem); return stripe_no * flseg->stripe_unit + rem; From 65a83a613c4630232fb3e382229719df8558f7ae Mon Sep 17 00:00:00 2001 From: Scott Mayhew Date: Tue, 22 Sep 2026 14:24:36 -0400 Subject: [PATCH 47/68] nfs: split up block layout and SCSI layout support Add two new config options PNFS_BLOCK_LAYOUT and PNFS_SCSI_LAYOUT so that SCSI layouts can be enabled without requiring block layouts. Since block layouts are considered deprecated, PNFS_BLOCK_LAYOUT is off by default. The original PNFS_BLOCK config is now invisible and gets selected when either of PNFS_BLOCK_LAYOUT or PNFS_SCSI_LAYOUT are enabled. Also added a dependency on BLOCK to the Kconfig to fix undefined symbol warnings from the kernel test robot. Signed-off-by: Scott Mayhew Reviewed-by: Christoph Hellwig Signed-off-by: Anna Schumaker --- fs/nfs/Kconfig | 17 ++++- fs/nfs/blocklayout/Makefile | 3 +- fs/nfs/blocklayout/blocklayout.c | 113 ++++++++++++++++++++++--------- fs/nfs/blocklayout/dev.c | 32 ++++++++- 4 files changed, 129 insertions(+), 36 deletions(-) diff --git a/fs/nfs/Kconfig b/fs/nfs/Kconfig index 6bb30543eff0..64c249f800a9 100644 --- a/fs/nfs/Kconfig +++ b/fs/nfs/Kconfig @@ -123,8 +123,23 @@ config PNFS_FILE_LAYOUT config PNFS_BLOCK tristate - depends on NFS_V4 && BLK_DEV_DM + +config PNFS_BLOCK_LAYOUT + bool "NFS client support for pNFS block layouts" + depends on NFS_V4 && BLOCK && BLK_DEV_DM + select PNFS_BLOCK + help + Enable support for the pNFS block-volume layout type (RFC 5663). + + If unsure, say N. + +config PNFS_SCSI_LAYOUT + bool "NFS client support for pNFS SCSI layouts" + depends on NFS_V4 && BLOCK default NFS_V4 + select PNFS_BLOCK + help + Enable suport for the pNFS SCSI layout type (RFC 8154). config PNFS_FLEXFILE_LAYOUT tristate diff --git a/fs/nfs/blocklayout/Makefile b/fs/nfs/blocklayout/Makefile index 7668a1bfb5fa..3403cb7fe201 100644 --- a/fs/nfs/blocklayout/Makefile +++ b/fs/nfs/blocklayout/Makefile @@ -4,4 +4,5 @@ # obj-$(CONFIG_PNFS_BLOCK) += blocklayoutdriver.o -blocklayoutdriver-y += blocklayout.o dev.o extent_tree.o rpc_pipefs.o +blocklayoutdriver-y += blocklayout.o dev.o extent_tree.o +blocklayoutdriver-$(CONFIG_PNFS_BLOCK_LAYOUT) += rpc_pipefs.o diff --git a/fs/nfs/blocklayout/blocklayout.c b/fs/nfs/blocklayout/blocklayout.c index d86702e604f9..82873ff370ef 100644 --- a/fs/nfs/blocklayout/blocklayout.c +++ b/fs/nfs/blocklayout/blocklayout.c @@ -470,18 +470,6 @@ static struct pnfs_layout_hdr *__bl_alloc_layout_hdr(struct inode *inode, return &bl->bl_layout; } -static struct pnfs_layout_hdr *bl_alloc_layout_hdr(struct inode *inode, - gfp_t gfp_flags) -{ - return __bl_alloc_layout_hdr(inode, gfp_flags, false); -} - -static struct pnfs_layout_hdr *sl_alloc_layout_hdr(struct inode *inode, - gfp_t gfp_flags) -{ - return __bl_alloc_layout_hdr(inode, gfp_flags, true); -} - static void bl_free_lseg(struct pnfs_layout_segment *lseg) { dprintk("%s enter\n", __func__); @@ -956,6 +944,13 @@ static const struct nfs_pageio_ops bl_pg_write_ops = { .pg_cleanup = pnfs_generic_pg_cleanup, }; +#ifdef CONFIG_PNFS_BLOCK_LAYOUT +static struct pnfs_layout_hdr *bl_alloc_layout_hdr(struct inode *inode, + gfp_t gfp_flags) +{ + return __bl_alloc_layout_hdr(inode, gfp_flags, false); +} + static struct pnfs_layoutdriver_type blocklayout_type = { .id = LAYOUT_BLOCK_VOLUME, .name = "LAYOUT_BLOCK_VOLUME", @@ -980,6 +975,48 @@ static struct pnfs_layoutdriver_type blocklayout_type = { .sync = pnfs_generic_sync, }; +static int __init pnfs_register_blocklayout(void) +{ + int ret; + + ret = bl_init_pipefs(); + if (ret) + return ret; + + ret = pnfs_register_layoutdriver(&blocklayout_type); + if (ret) { + bl_cleanup_pipefs(); + return ret; + } + + return 0; +} + +static void __exit pnfs_unregister_blocklayout(void) +{ + pnfs_unregister_layoutdriver(&blocklayout_type); + bl_cleanup_pipefs(); +} + +MODULE_ALIAS("nfs-layouttype4-3"); +#else +static int __init pnfs_register_blocklayout(void) +{ + return 0; +} + +static void __exit pnfs_unregister_blocklayout(void) +{ +} +#endif /* CONFIG_PNFS_BLOCK_LAYOUT */ + +#ifdef CONFIG_PNFS_SCSI_LAYOUT +static struct pnfs_layout_hdr *sl_alloc_layout_hdr(struct inode *inode, + gfp_t gfp_flags) +{ + return __bl_alloc_layout_hdr(inode, gfp_flags, true); +} + static struct pnfs_layoutdriver_type scsilayout_type = { .id = LAYOUT_SCSI, .name = "LAYOUT_SCSI", @@ -1004,6 +1041,28 @@ static struct pnfs_layoutdriver_type scsilayout_type = { .sync = pnfs_generic_sync, }; +static int __init pnfs_register_scsilayout(void) +{ + return pnfs_register_layoutdriver(&scsilayout_type); +} + +static void __exit pnfs_unregister_scsilayout(void) +{ + pnfs_unregister_layoutdriver(&scsilayout_type); +} + +MODULE_ALIAS("nfs-layouttype4-5"); +#else +static int __init pnfs_register_scsilayout(void) +{ + return 0; +} + +static void __exit pnfs_unregister_scsilayout(void) +{ +} +#endif /* CONFIG_PNFS_SCSI_LAYOUT */ + static int __init nfs4blocklayout_init(void) { @@ -1011,25 +1070,17 @@ static int __init nfs4blocklayout_init(void) dprintk("%s: NFSv4 Block Layout Driver Registering...\n", __func__); - ret = bl_init_pipefs(); + ret = pnfs_register_blocklayout(); if (ret) - goto out; + return ret; - ret = pnfs_register_layoutdriver(&blocklayout_type); - if (ret) - goto out_cleanup_pipe; + ret = pnfs_register_scsilayout(); + if (ret) { + pnfs_unregister_blocklayout(); + return ret; + } - ret = pnfs_register_layoutdriver(&scsilayout_type); - if (ret) - goto out_unregister_block; return 0; - -out_unregister_block: - pnfs_unregister_layoutdriver(&blocklayout_type); -out_cleanup_pipe: - bl_cleanup_pipefs(); -out: - return ret; } static void __exit nfs4blocklayout_exit(void) @@ -1037,13 +1088,9 @@ static void __exit nfs4blocklayout_exit(void) dprintk("%s: NFSv4 Block Layout Driver Unregistering...\n", __func__); - pnfs_unregister_layoutdriver(&scsilayout_type); - pnfs_unregister_layoutdriver(&blocklayout_type); - bl_cleanup_pipefs(); + pnfs_unregister_scsilayout(); + pnfs_unregister_blocklayout(); } -MODULE_ALIAS("nfs-layouttype4-3"); -MODULE_ALIAS("nfs-layouttype4-5"); - module_init(nfs4blocklayout_init); module_exit(nfs4blocklayout_exit); diff --git a/fs/nfs/blocklayout/dev.c b/fs/nfs/blocklayout/dev.c index c926b7e43827..00462e6affb1 100644 --- a/fs/nfs/blocklayout/dev.c +++ b/fs/nfs/blocklayout/dev.c @@ -15,6 +15,7 @@ #define NFSDBG_FACILITY NFSDBG_PNFS_LD +#ifdef CONFIG_PNFS_SCSI_LAYOUT static void bl_unregister_scsi(struct pnfs_block_dev *dev) { struct block_device *bdev = file_bdev(dev->bdev_file); @@ -45,6 +46,16 @@ static bool bl_register_scsi(struct pnfs_block_dev *dev) trace_bl_pr_key_reg(bdev, dev->pr_key); return true; } +#else +static void bl_unregister_scsi(struct pnfs_block_dev *dev) +{ +} + +static bool bl_register_scsi(struct pnfs_block_dev *dev) +{ + return false; +} +#endif /* CONFIG_PNFS_SCSI_LAYOUT */ static void bl_unregister_dev(struct pnfs_block_dev *dev) { @@ -292,7 +303,7 @@ static int bl_parse_deviceid(struct nfs_server *server, struct pnfs_block_dev *d, struct pnfs_block_volume *volumes, int idx, gfp_t gfp_mask); - +#ifdef CONFIG_PNFS_BLOCK_LAYOUT static int bl_parse_simple(struct nfs_server *server, struct pnfs_block_dev *d, struct pnfs_block_volume *volumes, int idx, gfp_t gfp_mask) @@ -320,7 +331,17 @@ bl_parse_simple(struct nfs_server *server, struct pnfs_block_dev *d, file_bdev(bdev_file)->bd_disk->disk_name); return 0; } +#else +static int +bl_parse_simple(struct nfs_server *server, struct pnfs_block_dev *d, + struct pnfs_block_volume *volumes, int idx, gfp_t gfp_mask) +{ + dprintk("unsupported volume type: %d\n", PNFS_BLOCK_VOLUME_SIMPLE); + return -EIO; +} +#endif /* CONFIG_PNFS_BLOCK_LAYOUT */ +#ifdef CONFIG_PNFS_SCSI_LAYOUT static bool bl_validate_designator(struct pnfs_block_volume *v) { @@ -449,6 +470,15 @@ bl_parse_scsi(struct nfs_server *server, struct pnfs_block_dev *d, d->bdev_file = NULL; return error; } +#else +static int +bl_parse_scsi(struct nfs_server *server, struct pnfs_block_dev *d, + struct pnfs_block_volume *volumes, int idx, gfp_t gfp_mask) +{ + dprintk("unsupported volume type: %d\n", PNFS_BLOCK_VOLUME_SCSI); + return -EIO; +} +#endif /* CONFIG_PNFS_SCSI_LAYOUT */ static int bl_parse_slice(struct nfs_server *server, struct pnfs_block_dev *d, From 4fa21ed1511a9ad3c348ac9ed32dc22a803ed1b7 Mon Sep 17 00:00:00 2001 From: Tigran Mkrtchyan Date: Fri, 2 Oct 2026 10:37:48 +0200 Subject: [PATCH 48/68] nfs/flexfile: fix doc of nfs4_ff_layout_prepare_ds function fix mismatch between function doc description and actual code introduced in commit f06bedfa62d5 ("pNFS/flexfiles: don't attempt pnfs on fatal DS errors") Signed-off-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index 6165c41fcf62..ff4d271baff5 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -392,7 +392,7 @@ ff_layout_get_mirror_ds(struct pnfs_layout_hdr *lo, * device error so that the server can tell which class of I/O the client * was unable to send to the mirror. * - * Returns a pointer to a connected DS object on success or NULL on failure. + * Returns a pointer to a connected DS object on success or an ERR_PTR value on failure. */ struct nfs4_pnfs_ds * nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, From 1ea352a0db8bcb6ff6820e9a8715aac0aa3c761f Mon Sep 17 00:00:00 2001 From: Mike Snitzer Date: Mon, 28 Sep 2026 11:54:25 -0400 Subject: [PATCH 49/68] NFS/localio: fix nfs_local_dio_misaligned tracepoint The intended focus of nfs_local_iters_setup_dio()'s call to trace_nfs_local_dio_misaligned() is on the middle segment being misaligned, yet the @offset passed in was local_dio->start_len. It would appear this was a cut-n-paste bug from the preceding nfs_local_iter_setup() call that passes local_dio->start_len. Fix this by passing the @offset as local_dio->middle_offset and calculate the start segment's offset rather than assume. Example traces, before this fix: python3-32744 [006] .l... 132946.352360: nfs_local_dio_write: fileid=00:33:1286 fhandle=0xf1f7c10b offset=1048759 count=1048576 mem_align=4 offset_align=512 start=1048759+329 middle=1049088+1048064 end=2097152+183 python3-32744 [006] .l... 132946.352360: nfs_local_dio_misaligned: fileid=00:33:1286 fhandle=0xf1f7c10b offset=329 count=1048064 mem_align=4 offset_align=512 start=329+329 middle=1049088+1048064 end=2097152+183 After this fix: python3-32744 [006] .l... 132946.352360: nfs_local_dio_write: fileid=00:33:1286 fhandle=0xf1f7c10b offset=1048759 count=1048576 mem_align=4 offset_align=512 start=1048759+329 middle=1049088+1048064 end=2097152+183 python3-32744 [006] .l... 132946.352360: nfs_local_dio_misaligned: fileid=00:33:1286 fhandle=0xf1f7c10b offset=1049088 count=1048064 mem_align=4 offset_align=512 start=1048759+329 middle=1049088+1048064 end=2097152+183 Fixes: 6a218b9c3183e ("nfs/localio: do not issue misaligned DIO out-of-order") Cc: stable@vger.kernel.org Signed-off-by: Mike Snitzer Signed-off-by: Anna Schumaker --- fs/nfs/localio.c | 2 +- fs/nfs/nfstrace.h | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fs/nfs/localio.c b/fs/nfs/localio.c index f42b6112a613..63c38dea50cc 100644 --- a/fs/nfs/localio.c +++ b/fs/nfs/localio.c @@ -446,7 +446,7 @@ nfs_local_iters_setup_dio(struct nfs_local_kiocb *iocb, int rw, if (unlikely(!iocb->iter_is_dio_aligned[n_iters])) { trace_nfs_local_dio_misaligned(iocb->hdr->inode, - local_dio->start_len, local_dio->middle_len, local_dio); + local_dio->middle_offset, local_dio->middle_len, local_dio); return 0; /* no DIO-aligned IO possible */ } iocb->end_iter_index = n_iters; diff --git a/fs/nfs/nfstrace.h b/fs/nfs/nfstrace.h index b15c1732c869..a32c76df72db 100644 --- a/fs/nfs/nfstrace.h +++ b/fs/nfs/nfstrace.h @@ -1772,7 +1772,10 @@ DECLARE_EVENT_CLASS(nfs_local_dio_class, __entry->count = count; __entry->mem_align = local_dio->mem_align; __entry->offset_align = local_dio->offset_align; - __entry->start = offset; + if (local_dio->start_len) + __entry->start = local_dio->middle_offset - local_dio->start_len; + else + __entry->start = 0; __entry->start_len = local_dio->start_len; __entry->middle = local_dio->middle_offset; __entry->middle_len = local_dio->middle_len; From db3ca539c1142d2c5380c5dbd3154ab7bb1c7d77 Mon Sep 17 00:00:00 2001 From: Mike Snitzer Date: Mon, 28 Sep 2026 11:54:26 -0400 Subject: [PATCH 50/68] NFS/localio: detect a short read or write before the iterator has moved nfs_local_call_read() and nfs_local_call_write() issue a misaligned DIO as up to three segments and must stop at the first short one, or the next segment lands at the wrong file offset. They test for that by comparing the bytes transferred against iov_iter_count() of the segment, read after read_iter() or write_iter() has advanced the iterator, when the count is the residual rather than the request: before the call: count == expected after the call: count == expected - status The test is therefore status < expected - status, and it fires only when less than half of the segment was transferred. A short I/O that covers between 50% and 99% of a segment slips through: the loop moves on with ki_pos advanced by the short amount, the next segment is issued at the wrong offset, and the header's byte count is over-reported to the caller. A write that slips through also never sets NFS_CONTEXT_WRITE_SYNC, which is what makes the writes that follow a short one synchronous. Take the segment's count before issuing it and compare against that. This is the same defect that commit 250ec14932d5 ("nfsd: fix partial-write detection in nfsd_direct_write") fixed in NFSD's version of this loop. Fixes: c817248fc831 ("nfs/localio: add proper O_DIRECT support for READ and WRITE") Fixes: d0497dd27452 ("nfs/localio: backfill missing partial read support for misaligned DIO") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-fable-5-1 Signed-off-by: Mike Snitzer Signed-off-by: Anna Schumaker --- fs/nfs/localio.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/fs/nfs/localio.c b/fs/nfs/localio.c index 63c38dea50cc..9fcae2391b72 100644 --- a/fs/nfs/localio.c +++ b/fs/nfs/localio.c @@ -674,6 +674,8 @@ static void nfs_local_call_read(struct work_struct *work) n_iters = atomic_read(&iocb->n_iters); for (int i = 0; i < n_iters ; i++) { + size_t expected; + if (iocb->iter_is_dio_aligned[i]) { iocb->kiocb.ki_flags |= IOCB_DIRECT; /* Only use AIO completion if DIO-aligned segment is last */ @@ -684,6 +686,8 @@ static void nfs_local_call_read(struct work_struct *work) } else iocb->kiocb.ki_flags &= ~IOCB_DIRECT; + /* read_iter() advances the iterator: measure it beforehand */ + expected = iov_iter_count(&iocb->iters[i]); scoped_with_creds(filp->f_cred) status = filp->f_op->read_iter(&iocb->kiocb, &iocb->iters[i]); @@ -691,7 +695,7 @@ static void nfs_local_call_read(struct work_struct *work) continue; /* Break on completion, errors, or short reads */ if (nfs_local_pgio_done(iocb, status) || status < 0 || - (size_t)status < iov_iter_count(&iocb->iters[i])) { + (size_t)status < expected) { nfs_local_read_iocb_done(iocb); break; } @@ -890,7 +894,7 @@ static void nfs_local_call_write(struct work_struct *work) file_start_write(filp); n_iters = atomic_read(&iocb->n_iters); for (int i = 0; i < n_iters ; i++) { - size_t icount; + size_t expected; if (iocb->iter_is_dio_aligned[i]) { iocb->kiocb.ki_flags |= IOCB_DIRECT; @@ -902,16 +906,17 @@ static void nfs_local_call_write(struct work_struct *work) } else iocb->kiocb.ki_flags &= ~IOCB_DIRECT; + /* write_iter() advances the iterator: measure it beforehand */ + expected = iov_iter_count(&iocb->iters[i]); scoped_with_creds(filp->f_cred) status = filp->f_op->write_iter(&iocb->kiocb, &iocb->iters[i]); if (status == -EIOCBQUEUED) continue; /* Break on completion, errors, or short writes */ - icount = iov_iter_count(&iocb->iters[i]); if (nfs_local_pgio_done(iocb, status) || status < 0 || - (size_t)status < icount) { - if ((size_t)status < icount) { + (size_t)status < expected) { + if ((size_t)status < expected) { struct nfs_lock_context *ctx = iocb->hdr->req->wb_lock_context; From 077d39c1ac8c9b2e2fad8e66a2ba6250ced1a860 Mon Sep 17 00:00:00 2001 From: Mike Snitzer Date: Mon, 28 Sep 2026 11:54:27 -0400 Subject: [PATCH 51/68] NFS/localio: report the stability a DIO WRITE actually has Since commit d32ddfeb5593 ("nfs/localio: Ensure DIO WRITE's IO on stable storage upon completion") a DIO WRITE issued through LOCALIO is persisted before it completes: nfs_local_iters_init() sets IOCB_DSYNC|IOCB_SYNC on it despite whatever the caller asked for, so that the buffered head and tail and the O_DIRECT middle of a misaligned write cannot complete out of order. The reply still reported the stability that was asked for, so an UNSTABLE write came back UNSTABLE, the client kept its pages on the commit list, and the COMMIT that followed ran an fsync for data that is already on stable storage. Report the stability the kiocb actually carries instead: FILE_SYNC when IOCB_SYNC is set, DATA_SYNC when only IOCB_DSYNC is. A write told FILE_SYNC has no reason to COMMIT and sends none. Buffered writes, and any write that asked for at least what it got, are unchanged. Fixes: d32ddfeb5593 ("nfs/localio: Ensure DIO WRITE's IO on stable storage upon completion") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-fable-5-1 Signed-off-by: Mike Snitzer Signed-off-by: Anna Schumaker --- fs/nfs/localio.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/fs/nfs/localio.c b/fs/nfs/localio.c index 9fcae2391b72..cdada6cbb13b 100644 --- a/fs/nfs/localio.c +++ b/fs/nfs/localio.c @@ -936,6 +936,7 @@ static void nfs_local_do_write(struct nfs_local_kiocb *iocb, const struct rpc_call_ops *call_ops) { struct nfs_pgio_header *hdr = iocb->hdr; + enum nfs3_stable_how committed = hdr->args.stable; dprintk("%s: vfs_write count=%u pos=%llu %s\n", __func__, hdr->args.count, hdr->args.offset, @@ -954,9 +955,20 @@ static void nfs_local_do_write(struct nfs_local_kiocb *iocb, iocb->kiocb.ki_flags |= IOCB_DSYNC|IOCB_SYNC; } + /* + * Report the stability the write will actually have. A DIO WRITE + * is persisted before it completes whatever was asked for, see + * nfs_local_iters_init(), and a caller told so has no reason to + * COMMIT data that is already on stable storage. + */ + if (iocb->kiocb.ki_flags & IOCB_SYNC) + committed = NFS_FILE_SYNC; + else if (iocb->kiocb.ki_flags & IOCB_DSYNC) + committed = NFS_DATA_SYNC; + nfs_local_pgio_init(hdr, call_ops); - nfs_set_local_verifier(hdr->inode, hdr->res.verf, hdr->args.stable); + nfs_set_local_verifier(hdr->inode, hdr->res.verf, committed); INIT_WORK(&iocb->work, nfs_local_call_write); if (nfs_local_defer_io()) From 78422f623c1e150e3cdfe62598c6e352a2dcfc5f Mon Sep 17 00:00:00 2001 From: Mike Snitzer Date: Mon, 28 Sep 2026 11:54:28 -0400 Subject: [PATCH 52/68] pNFS/flexfiles: honor FF_FLAGS_NO_IO_THRU_MDS over the mdsthreshold hint A flexfiles layout may carry FF_FLAGS_NO_IO_THRU_MDS, which tells the client that this file's data must not be read or written through the metadata server. The RFC 5661 mdsthreshold hint pulls the other way. When a server returns one, pnfs_within_mdsthreshold() answers "use MDS I/O" for every I/O below the threshold, pnfs_update_layout() then hands back no segment, and ff_layout_pg_init_read() reads through the MDS - the very thing the layout forbids. A server that sets both is asking for two incompatible things; the layout flag is the stronger statement, and the one whose violation the client cannot recover from. Record the policy on the inode, where it outlives the layout hdr that carried it, and consult it before the hint: - ff_layout_alloc_lseg() sets NFS_INO_NO_IO_THRU_MDS beside the existing sticky hdr-level NFS4_FF_HDR_NO_IO_THRU_MDS bit. - pnfs_within_mdsthreshold() returns false at once for such an inode, so no I/O is diverted to the MDS on the strength of the hint. The bit is never cleared for the life of the in-core inode. Servers are assumed to be consistent in their no-fallback policy per file, which is the assumption ff_layout_hdr_no_fallback_to_mds() already makes; a server that was not would lose the effect of its mdsthreshold hint - a SHOULD - on an inode that is already in core, and nothing else. Fixes: 260074cd8413 ("pNFS/flexfiles: Add support for FF_FLAGS_NO_IO_THRU_MDS") Assisted-by: Claude:claude-opus-5[1m] Signed-off-by: Mike Snitzer Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 5 ++++- fs/nfs/internal.h | 29 ++++++++++++++++++++++++++ fs/nfs/pnfs.c | 12 ++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 94cc324b591f..7d45401ee5b1 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -639,9 +639,12 @@ ff_layout_alloc_lseg(struct pnfs_layout_hdr *lh, if (!p) goto out_sort_mirrors; fls->flags = be32_to_cpup(p); - if (fls->flags & FF_FLAGS_NO_IO_THRU_MDS) + if (fls->flags & FF_FLAGS_NO_IO_THRU_MDS) { set_bit(NFS4_FF_HDR_NO_IO_THRU_MDS, &FF_LAYOUT_FROM_HDR(lh)->flags); + /* Outlives this layout hdr; see NFS_INO_NO_IO_THRU_MDS */ + nfs_set_no_io_thru_mds(lh->plh_inode); + } p = xdr_inline_decode(&stream, 4); if (!p) diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h index 48f7c0e25da1..959edbc9add2 100644 --- a/fs/nfs/internal.h +++ b/fs/nfs/internal.h @@ -546,6 +546,35 @@ static inline bool nfs_file_io_is_buffered(struct nfs_inode *nfsi) return test_bit(NFS_INO_ODIRECT, &nfsi->flags) == 0; } +/* + * Module-private nfs_inode->flags bit (not in , so no + * exported layout changes): set, and never cleared for the life of the + * in-core inode, once a layout driver has seen the server forbid I/O + * through the MDS for this file (flexfiles sets it from + * FF_FLAGS_NO_IO_THRU_MDS in ff_layout_alloc_lseg()). The RFC 5661 + * mdsthreshold hint must not be acted on for such a file, since the only + * thing pnfs_within_mdsthreshold() can ask for is the one thing the + * layout forbids. Servers are assumed to be consistent in their + * no-fallback policy per file, the same assumption + * ff_layout_hdr_no_fallback_to_mds() already makes; if one were not, the + * only effect is that its mdsthreshold hint - a SHOULD - stops being + * honored for an inode that is already in core. + */ +#define NFS_INO_NO_IO_THRU_MDS (30) + +static inline void nfs_set_no_io_thru_mds(struct inode *inode) +{ + struct nfs_inode *nfsi = NFS_I(inode); + + if (!test_bit(NFS_INO_NO_IO_THRU_MDS, &nfsi->flags)) + set_bit(NFS_INO_NO_IO_THRU_MDS, &nfsi->flags); +} + +static inline bool nfs_no_io_thru_mds(struct inode *inode) +{ + return test_bit(NFS_INO_NO_IO_THRU_MDS, &NFS_I(inode)->flags); +} + /* Must be called with exclusively locked inode->i_rwsem */ static inline void nfs_file_block_o_direct(struct nfs_inode *nfsi) { diff --git a/fs/nfs/pnfs.c b/fs/nfs/pnfs.c index 93a0852e1e8a..a057a06318c7 100644 --- a/fs/nfs/pnfs.c +++ b/fs/nfs/pnfs.c @@ -2019,7 +2019,9 @@ pnfs_find_lseg(struct pnfs_layout_hdr *lo, * is set to IOMODE_READ for a READ request, and set to IOMODE_RW for a * WRITE request. * - * A return of true means use MDS I/O. + * A return of true means use MDS I/O, so for a file whose layout forbids + * that (see NFS_INO_NO_IO_THRU_MDS) the hint is not evaluated at all: + * there is no answer it could give that this client may act on. * * From rfc 5661: * If a file's size is smaller than the file size threshold, data accesses @@ -2040,6 +2042,14 @@ static bool pnfs_within_mdsthreshold(struct nfs_open_context *ctx, if (t == NULL) return ret; + /* + * The server has told a layout driver that this file's I/O may not + * go through the MDS (flexfiles FF_FLAGS_NO_IO_THRU_MDS). Honor + * that over its own mdsthreshold hint. + */ + if (nfs_no_io_thru_mds(ino)) + return ret; + dprintk("%s bm=0x%x rd_sz=%llu wr_sz=%llu rd_io=%llu wr_io=%llu\n", __func__, t->bm, t->rd_sz, t->wr_sz, t->rd_io_sz, t->wr_io_sz); From 2f007ba074ad77e70d941e16956d1937dc3080c6 Mon Sep 17 00:00:00 2001 From: Mike Snitzer Date: Mon, 28 Sep 2026 11:54:29 -0400 Subject: [PATCH 53/68] pNFS/flexfiles: don't read through MDS when previous layout forbid it ff_layout_pg_init_read() falls back to the MDS whenever pnfs_update_layout() returns no segment and no error: a bulk CB_LAYOUTRECALL (FSID, DEVICEID or ALL), LAYOUTGETs blocked behind a layout being returned, or a LAYOUTGET refused with NFS4ERR_LAYOUTUNAVAILABLE or NFS4ERR_TOOSMALL, which set the per-iomode fail bit for PNFS_LAYOUTGET_RETRY_TIMEOUT. That is the read-side twin of the write path fixed by commit 1d62e659c0bf ("NFSv4/flexfiles: honor FF_FLAGS_NO_IO_THRU_MDS in pg_get_mirror_count_write"), and it is reached in exactly the situation that matters most: the file had a valid layout, the layout had to go back, and the next one has not arrived yet. The write side could answer -EAGAIN because writeback owns the retry and redirties the page. A read has no such owner, so -EAGAIN here would surface as an I/O error to the application. Wait for a layout instead, on the same terms the function already uses when the layout lookup itself returns -EAGAIN: sleep a second and retry, bounded by pg_maxretrans, which ff_layout_pg_init_read() sets only for soft and softerr mounts. A hard mount therefore waits, which is what a hard mount is for, and matches what the FF_FLAGS_NO_IO_THRU_MDS check on the data server selection a few lines above already does. That includes waiting out NFS4ERR_LAYOUTUNAVAILABLE: a file that was given a layout with this flag must not be read off the MDS, whatever the server says afterwards. The test asks the inode (NFS_INO_NO_IO_THRU_MDS) rather than the layout hdr, because in this path there is no segment, and the hdr that carried the flag may already have been destroyed. Where a segment is in hand - the data server selection above, ff_layout_read_pagelist() - the existing per-segment ff_layout_no_fallback_to_mds() test stays, since it is the more precise question. Fixes: 260074cd8413 ("pNFS/flexfiles: Add support for FF_FLAGS_NO_IO_THRU_MDS") Assisted-by: Claude:claude-opus-5[1m] Signed-off-by: Mike Snitzer Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 38 ++++++++++++++++++-------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 7d45401ee5b1..84d22f715653 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -1073,19 +1073,33 @@ ff_layout_pg_init_read(struct nfs_pageio_descriptor *pgio, if (pgio->pg_error < 0) { if (pgio->pg_error != -EAGAIN) return; - /* Retry getting layout segment if lower layer returned -EAGAIN */ - if (pgio->pg_maxretrans && req->wb_nio++ > pgio->pg_maxretrans) { - if (NFS_SERVER(pgio->pg_inode)->flags & NFS_MOUNT_SOFTERR) - pgio->pg_error = -ETIMEDOUT; - else - pgio->pg_error = -EIO; - return; - } - pgio->pg_error = 0; - /* Sleep for 1 second before retrying */ - ssleep(1); - goto retry; + goto retry_nolseg; } + /* + * No segment, and no error to report either: pnfs_update_layout() + * simply has nothing to give (NFS_LAYOUT_BULK_RECALL, a failed + * pnfs_layout_io_test, blocked LAYOUTGETs, a layout being + * returned). If the server forbids reading this file through the + * MDS there is no fallback to take, so wait for a layout on the + * same terms as the -EAGAIN above. The layout hdr that carried + * FF_FLAGS_NO_IO_THRU_MDS may itself be gone by now, which is why + * this asks the inode and not the hdr. + */ + if (!nfs_no_io_thru_mds(pgio->pg_inode)) + goto out_mds; +retry_nolseg: + /* Retry getting layout segment if lower layer returned -EAGAIN */ + if (pgio->pg_maxretrans && req->wb_nio++ > pgio->pg_maxretrans) { + if (NFS_SERVER(pgio->pg_inode)->flags & NFS_MOUNT_SOFTERR) + pgio->pg_error = -ETIMEDOUT; + else + pgio->pg_error = -EIO; + return; + } + pgio->pg_error = 0; + /* Sleep for 1 second before retrying */ + ssleep(1); + goto retry; out_mds: trace_pnfs_mds_fallback_pg_init_read(pgio->pg_inode, 0, NFS4_MAX_UINT64, IOMODE_READ, From ec906cf7bee60f3bbc8500bac14e65813e38ac93 Mon Sep 17 00:00:00 2001 From: Mike Snitzer Date: Mon, 28 Sep 2026 11:54:30 -0400 Subject: [PATCH 54/68] pNFS/flexfiles: don't reset to MDS for v4 error when previous layout forbid it ff_layout_async_handle_error_v4() ends in -NFS4ERR_RESET_TO_MDS, which makes ff_layout_read_release() / ff_layout_write_release() call ff_layout_reset_read() or ff_layout_reset_write(hdr, false), and those call pnfs_read_done_resend_to_mds() / pnfs_write_done_resend_to_mds(). Those build a page I/O descriptor with force_mds set, so the flexfiles ->pg_init() never runs and no layout is consulted anywhere on the way: the READ or WRITE goes to the metadata server unconditionally. For a layout carrying FF_FLAGS_NO_IO_THRU_MDS that is exactly what the server forbade. The fall-through into reset: is already safe, because ff_layout_avoid_mds_available_ds() returns true for a segment with the flag and so answers -NFS4ERR_RESET_TO_PNFS first. What is not safe is the goto: the invalid-layout errors (NFS4ERR_PNFS_NO_LAYOUT, STALE, BADHANDLE, ISDIR, FHEXPIRED, WRONG_TYPE) call pnfs_destroy_layout() and jump straight to reset:, over that check. That is the case where the file had a perfectly good layout, something forced it to be given back, and the client answers by writing to the MDS behind the server's back. Test at reset: itself, so it covers the goto and any future one, and ask the inode (NFS_INO_NO_IO_THRU_MDS): the callers that jump here have just destroyed the layout hdr, so the hdr-level flag is no longer reachable, while the inode-level one is set for the life of the in-core inode. -NFS4ERR_RESET_TO_PNFS is the right answer rather than an error. It is what the fall-through already returns for the same flag, and the resends it drives - ff_layout_resend_pnfs_read() and ff_layout_reset_write(hdr, true) - re-enter the pgio path with force_mds clear, so a fresh LAYOUTGET is taken, which is precisely what a destroyed layout needs. A LAYOUTGET that fails fatally makes pnfs_update_layout() return an error, which ->pg_init() reports; a non-fatal refusal (NFS4ERR_LAYOUTUNAVAILABLE, NFS4ERR_TOOSMALL) returns no segment instead, and the client keeps retrying through pNFS rather than falling back. ff_layout_async_handle_error_v3() needs no such change: it returns only -NFS4ERR_RESET_TO_PNFS or -EAGAIN. Fixes: 260074cd8413 ("pNFS/flexfiles: Add support for FF_FLAGS_NO_IO_THRU_MDS") Assisted-by: Claude:claude-opus-5[1m] Signed-off-by: Mike Snitzer Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index 84d22f715653..d4f2b57e9d5b 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -1423,6 +1423,23 @@ static int ff_layout_async_handle_error_v4(struct rpc_task *task, if (ff_layout_avoid_mds_available_ds(lseg)) return -NFS4ERR_RESET_TO_PNFS; reset: + /* + * FF_FLAGS_NO_IO_THRU_MDS: never resend through the MDS. The + * caller would do so with force_mds set (ff_layout_reset_read(), + * ff_layout_reset_write(hdr, false) -> pnfs_*_done_resend_to_mds()), + * which builds a descriptor out of the plain MDS page ops and so + * consults no layout at all -- this is the last point at which the + * policy can still be applied. Retry through pNFS instead, which + * takes a fresh LAYOUTGET; that is also the right answer for the + * invalid-layout cases that jump here, since they have just called + * pnfs_destroy_layout(). Having done so they can no longer ask the + * layout hdr about the flag, hence the inode. + */ + if (nfs_no_io_thru_mds(inode)) { + dprintk("%s Retry through pNFS, no MDS fallback. Error %d\n", + __func__, task->tk_status); + return -NFS4ERR_RESET_TO_PNFS; + } dprintk("%s Retry through MDS. Error %d\n", __func__, task->tk_status); return -NFS4ERR_RESET_TO_MDS; From 4fdfe2312cbed99b1438dcf20b348e72393f9633 Mon Sep 17 00:00:00 2001 From: Roberto Bergantinos Corpas Date: Thu, 17 Sep 2026 12:49:05 +0200 Subject: [PATCH 55/68] NFS: return DENIED in decode_lock_denied if we cannot decode owner After commit 43502f6e8d1e ("NFS: fix open_owner_id_maxsz and related fields.") we dramatically changed the size of the LOCK/LOCKT reply buffer from 164 to 52 bytes. This made visible a situation on a buggy NFS server that sent oversized denied responses which led to us returning EIO instead of DENIED. Now, apart from buggy server issue, this poses an interesting question: what if other implementations have legitimate but bigger than we expect lock owner response, especially now that we have reduced the size we allocate for it (20 bytes for the lock owner part). This change proposes to tackle that issue returning DENIED instead of EIO regardless of whether we manage to decode the owner or not: - At this point of decode_lock_denied we know there is an owner. - Server legitimately denied the lock request, with an owner that fits on NFS4_OPAQUE_LIMIT but we returned EIO instead to userspace. - The owner data is decoded but actually never used, decode_lock path simply retries, and decode_lockt turns it into 0 on file_lock and returns it to userspace. - LOCK/LOCKT are the last operations on the compound so it's not relevant if we didn't advance the pointer. - Also removes an inverted likely(!p) hint. Fixes: 43502f6e8d1e ("NFS: fix open_owner_id_maxsz and related fields.") Signed-off-by: Roberto Bergantinos Corpas Reviewed-by: Prabhakar Pujeri Reviewed-by: Jeff Layton Signed-off-by: Anna Schumaker --- fs/nfs/nfs4xdr.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/nfs/nfs4xdr.c b/fs/nfs/nfs4xdr.c index 8b3d96b4a0f0..740f3bc4a19d 100644 --- a/fs/nfs/nfs4xdr.c +++ b/fs/nfs/nfs4xdr.c @@ -5169,8 +5169,7 @@ static int decode_lock_denied(struct xdr_stream *xdr, struct file_lock *fl) p = xdr_decode_hyper(p, &clientid); /* read 8 bytes */ namelen = be32_to_cpup(p); /* read 4 bytes */ /* have read all 32 bytes now */ p = xdr_inline_decode(xdr, namelen); /* variable size field */ - if (likely(!p)) - return -EIO; + /* We have an owner here, return DENIED */ return -NFS4ERR_DENIED; } From 69fe71a4ddae3cda8ffcb858ec94a948fe109f69 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Tue, 6 Oct 2026 11:31:12 -0400 Subject: [PATCH 56/68] NFS: name the init_nfs_fs() error labels The unwind labels in init_nfs_fs() are numbered, and the numbering already skips out8, so a reader has to count the label block to learn what each one undoes. Inserting an init step means either renumbering every label below it or leaving the sequence out of order, and a goto that picks the wrong number unwinds the wrong step. Name each label for the step it undoes, as coding-style.rst asks. Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- fs/nfs/inode.c | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c index 3022454f7698..832923be43a9 100644 --- a/fs/nfs/inode.c +++ b/fs/nfs/inode.c @@ -2722,64 +2722,64 @@ static int __init init_nfs_fs(void) err = nfs_sysfs_init(); if (err < 0) - goto out10; + goto err_keyring; err = register_pernet_subsys(&nfs_net_ops); if (err < 0) - goto out9; + goto err_sysfs; err = nfsiod_start(); if (err) - goto out7; + goto err_pernet; err = nfs_fs_proc_init(); if (err) - goto out6; + goto err_nfsiod; err = nfs_init_nfspagecache(); if (err) - goto out5; + goto err_proc; err = nfs_init_inodecache(); if (err) - goto out4; + goto err_nfspagecache; err = nfs_init_readpagecache(); if (err) - goto out3; + goto err_inodecache; err = nfs_init_writepagecache(); if (err) - goto out2; + goto err_readpagecache; err = nfs_init_directcache(); if (err) - goto out1; + goto err_writepagecache; err = register_nfs_fs(); if (err) - goto out0; + goto err_directcache; return 0; -out0: +err_directcache: nfs_destroy_directcache(); -out1: +err_writepagecache: nfs_destroy_writepagecache(); -out2: +err_readpagecache: nfs_destroy_readpagecache(); -out3: +err_inodecache: nfs_destroy_inodecache(); -out4: +err_nfspagecache: nfs_destroy_nfspagecache(); -out5: +err_proc: nfs_fs_proc_exit(); -out6: +err_nfsiod: nfsiod_stop(); -out7: +err_pernet: unregister_pernet_subsys(&nfs_net_ops); -out9: +err_sysfs: nfs_sysfs_exit(); -out10: +err_keyring: nfs_exit_keyring(); return err; } From d8884f64c93d1b96b3c7f596c8c41c5e5e3e832a Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Tue, 6 Oct 2026 11:31:13 -0400 Subject: [PATCH 57/68] NFS: allocate the .nfs keyring per network namespace Commit 87268f7a4f1f ("nfs: create a kernel keyring") allocates one .nfs keyring at module load, and nothing in the NFS client reads it. One module-wide keyring also cannot isolate x.509 credentials between network namespaces. Each tlshd instance services the handshake socket of one network namespace, so a credential provisioned for that namespace's mounts has to be reachable by that tlshd but not by a tlshd in another user namespace. Allocate one .nfs keyring per network namespace in nfs_net_init() and release it in nfs_net_exit(). tlshd finds a keyring by name through /proc/keys, which is not namespace scoped, so each handshake request has to carry the keyring serial number instead. Allocate the keyring under a kernel credential rather than that of the task creating the namespace, so an LSM labels every namespace's keyring the same way. The keyring grants its owner every permission and everyone else none. Make the owner of the network namespace's user namespace the keyring's owner, so that in a container with its own user namespace the container's root can provision the keyring and its tlshd can link it. Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- fs/nfs/inode.c | 83 ++++++++++++++++++++++++++++---------------------- fs/nfs/netns.h | 2 ++ 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c index 832923be43a9..2b494fa5ecc0 100644 --- a/fs/nfs/inode.c +++ b/fs/nfs/inode.c @@ -2641,11 +2641,52 @@ static int nfsiod_start(void) unsigned int nfs_net_id; EXPORT_SYMBOL_GPL(nfs_net_id); +#ifdef CONFIG_KEYS +static int nfs_init_keyring(struct net *net) +{ + struct nfs_net *nn = net_generic(net, nfs_net_id); + struct cred *cred; + struct key *keyring; + + cred = prepare_kernel_cred(&init_task); + if (!cred) + return -ENOMEM; + keyring = keyring_alloc(".nfs", net->user_ns->owner, + net->user_ns->group, cred, + (KEY_POS_ALL & ~KEY_POS_SETATTR) | + (KEY_USR_ALL & ~KEY_USR_SETATTR), + KEY_ALLOC_NOT_IN_QUOTA, NULL, NULL); + put_cred(cred); + if (IS_ERR(keyring)) + return PTR_ERR(keyring); + nn->nfs_keyring = keyring; + return 0; +} + +static void nfs_exit_keyring(struct nfs_net *nn) +{ + key_put(nn->nfs_keyring); +} +#else +static inline int nfs_init_keyring(struct net *net) +{ + return 0; +} + +static inline void nfs_exit_keyring(struct nfs_net *nn) +{ +} +#endif /* CONFIG_KEYS */ + static int nfs_net_init(struct net *net) { struct nfs_net *nn = net_generic(net, nfs_net_id); int err; + err = nfs_init_keyring(net); + if (err) + return err; + nfs_clients_init(net); if (!rpc_proc_register(net, &nn->rpcstats)) { @@ -2663,14 +2704,18 @@ static int nfs_net_init(struct net *net) rpc_proc_unregister(net, "nfs"); err_proc_rpc: nfs_clients_exit(net); + nfs_exit_keyring(nn); return err; } static void nfs_net_exit(struct net *net) { + struct nfs_net *nn = net_generic(net, nfs_net_id); + rpc_proc_unregister(net, "nfs"); nfs_fs_proc_net_exit(net); nfs_clients_exit(net); + nfs_exit_keyring(nn); } static struct pernet_operations nfs_net_ops = { @@ -2680,35 +2725,6 @@ static struct pernet_operations nfs_net_ops = { .size = sizeof(struct nfs_net), }; -#ifdef CONFIG_KEYS -static struct key *nfs_keyring; - -static int __init nfs_init_keyring(void) -{ - nfs_keyring = keyring_alloc(".nfs", - GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, - current_cred(), - (KEY_POS_ALL & ~KEY_POS_SETATTR) | - (KEY_USR_ALL & ~KEY_USR_SETATTR), - KEY_ALLOC_NOT_IN_QUOTA, NULL, NULL); - return PTR_ERR_OR_ZERO(nfs_keyring); -} - -static void nfs_exit_keyring(void) -{ - key_put(nfs_keyring); -} -#else -static inline int nfs_init_keyring(void) -{ - return 0; -} - -static inline void nfs_exit_keyring(void) -{ -} -#endif /* CONFIG_KEYS */ - /* * Initialize NFS */ @@ -2716,13 +2732,9 @@ static int __init init_nfs_fs(void) { int err; - err = nfs_init_keyring(); - if (err) - return err; - err = nfs_sysfs_init(); if (err < 0) - goto err_keyring; + return err; err = register_pernet_subsys(&nfs_net_ops); if (err < 0) @@ -2779,8 +2791,6 @@ static int __init init_nfs_fs(void) unregister_pernet_subsys(&nfs_net_ops); err_sysfs: nfs_sysfs_exit(); -err_keyring: - nfs_exit_keyring(); return err; } @@ -2796,7 +2806,6 @@ static void __exit exit_nfs_fs(void) nfs_fs_proc_exit(); nfsiod_stop(); nfs_sysfs_exit(); - nfs_exit_keyring(); } /* Not quite true; I just maintain it */ diff --git a/fs/nfs/netns.h b/fs/nfs/netns.h index 560fa95726b0..ff023bf66189 100644 --- a/fs/nfs/netns.h +++ b/fs/nfs/netns.h @@ -16,6 +16,7 @@ struct bl_dev_msg { uint32_t major, minor; }; +struct key; struct nfs_netns_client; struct nfs_net { @@ -39,6 +40,7 @@ struct nfs_net { #endif /* CONFIG_NFS_V4 */ struct nfs_netns_client *nfs_client; spinlock_t nfs_client_lock; + struct key *nfs_keyring; ktime_t boot_time; struct rpc_stat rpcstats; #ifdef CONFIG_PROC_FS From 7856eb255ef9ea70a3a4f5764c8f1fcdb72b0896 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Tue, 6 Oct 2026 11:31:14 -0400 Subject: [PATCH 58/68] SUNRPC: pass a keyring serial number to the TLS handshake The handshake upcall links the keyring named in ta_keyring into tlshd's process keyring before tlshd reads the client certificate and private key. That link is how tlshd gains possession of keys that grant no user read permission. xprtsock never fills the field, so an x509 handshake can present only keys that tlshd's own credentials can read. Add a keyring serial number to struct xprtsec_parms and pass it to the x509 handshake, so an RPC client can name the keyring that holds its credentials. Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- fs/nfs/fs_context.c | 1 + fs/nfs/nfs3client.c | 1 + fs/nfs/nfs4client.c | 1 + include/linux/sunrpc/xprt.h | 1 + net/sunrpc/xprtsock.c | 1 + 5 files changed, 5 insertions(+) diff --git a/fs/nfs/fs_context.c b/fs/nfs/fs_context.c index 1967de7d1dff..f8f5f8b2954e 100644 --- a/fs/nfs/fs_context.c +++ b/fs/nfs/fs_context.c @@ -1750,6 +1750,7 @@ static int nfs_init_fs_context(struct fs_context *fc) ctx->minorversion = 0; ctx->need_mount = true; ctx->xprtsec.policy = RPC_XPRTSEC_NONE; + ctx->xprtsec.keyring_serial = TLS_NO_KEYRING; ctx->xprtsec.cert_serial = TLS_NO_CERT; ctx->xprtsec.privkey_serial = TLS_NO_PRIVKEY; diff --git a/fs/nfs/nfs3client.c b/fs/nfs/nfs3client.c index cf2f7be4b435..4d9d43f687e3 100644 --- a/fs/nfs/nfs3client.c +++ b/fs/nfs/nfs3client.c @@ -102,6 +102,7 @@ struct nfs_client *nfs3_set_ds_client(struct nfs_server *mds_srv, .cred = mds_srv->cred, .xprtsec = { .policy = RPC_XPRTSEC_NONE, + .keyring_serial = TLS_NO_KEYRING, .cert_serial = TLS_NO_CERT, .privkey_serial = TLS_NO_PRIVKEY, }, diff --git a/fs/nfs/nfs4client.c b/fs/nfs/nfs4client.c index fe779fb2ec72..5d054d3dced1 100644 --- a/fs/nfs/nfs4client.c +++ b/fs/nfs/nfs4client.c @@ -812,6 +812,7 @@ struct nfs_client *nfs4_set_ds_client(struct nfs_server *mds_srv, .cred = mds_srv->cred, .xprtsec = { .policy = RPC_XPRTSEC_NONE, + .keyring_serial = TLS_NO_KEYRING, .cert_serial = TLS_NO_CERT, .privkey_serial = TLS_NO_PRIVKEY, }, diff --git a/include/linux/sunrpc/xprt.h b/include/linux/sunrpc/xprt.h index a82045804d34..005730623f3b 100644 --- a/include/linux/sunrpc/xprt.h +++ b/include/linux/sunrpc/xprt.h @@ -145,6 +145,7 @@ struct xprtsec_parms { enum xprtsec_policies policy; /* authentication material */ + key_serial_t keyring_serial; key_serial_t cert_serial; key_serial_t privkey_serial; }; diff --git a/net/sunrpc/xprtsock.c b/net/sunrpc/xprtsock.c index f5a5136327ba..77e4c87bf2e1 100644 --- a/net/sunrpc/xprtsock.c +++ b/net/sunrpc/xprtsock.c @@ -2637,6 +2637,7 @@ static int xs_tls_handshake_sync(struct rpc_xprt *lower_xprt, struct xprtsec_par goto out_put_xprt; break; case RPC_XPRTSEC_TLS_X509: + args.ta_keyring = xprtsec->keyring_serial; args.ta_my_cert = xprtsec->cert_serial; args.ta_my_privkey = xprtsec->privkey_serial; rc = tls_client_hello_x509(&args, GFP_KERNEL); From e27b84e076c1b623218ebe2887ca21c2c3217f5e Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Tue, 6 Oct 2026 11:31:15 -0400 Subject: [PATCH 59/68] NFS: name the namespace .nfs keyring in the x509 handshake Keys on the per-namespace .nfs keyring need not grant user read permission once tlshd possesses the keyring, but nothing tells tlshd which keyring to possess. An xprtsec=mtls mount can present only keys that tlshd's own credentials can read, and one namespace's keys are not isolated from another's. Record the serial number of the namespace's .nfs keyring in the nfs_client's xprtsec parameters under the x509 policy, so tlshd possesses that keyring when it reads the certificate and private key. Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- fs/nfs/client.c | 9 ++++++++- fs/nfs/netns.h | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/fs/nfs/client.c b/fs/nfs/client.c index dbb5131375f8..5ebc8bf39dd7 100644 --- a/fs/nfs/client.c +++ b/fs/nfs/client.c @@ -191,6 +191,13 @@ struct nfs_client *nfs_alloc_client(const struct nfs_client_initdata *cl_init) clp->cl_principal = "*"; clp->cl_xprtsec = cl_init->xprtsec; + /* + * Every client in a namespace names the same keyring, so + * nfs_match_client() does not compare keyring_serial. + */ + if (clp->cl_xprtsec.policy == RPC_XPRTSEC_TLS_X509) + clp->cl_xprtsec.keyring_serial = + key_serial(nfs_net_keyring(clp->cl_net)); return clp; error_cleanup: @@ -549,7 +556,7 @@ int nfs_create_rpc_client(struct nfs_client *clp, .version = clp->rpc_ops->version, .authflavor = flavor, .cred = cl_init->cred, - .xprtsec = cl_init->xprtsec, + .xprtsec = clp->cl_xprtsec, .connect_timeout = cl_init->connect_timeout, .reconnect_timeout = cl_init->reconnect_timeout, }; diff --git a/fs/nfs/netns.h b/fs/nfs/netns.h index ff023bf66189..24f6697199e3 100644 --- a/fs/nfs/netns.h +++ b/fs/nfs/netns.h @@ -50,4 +50,11 @@ struct nfs_net { extern unsigned int nfs_net_id; +static inline struct key *nfs_net_keyring(struct net *net) +{ + struct nfs_net *nn = net_generic(net, nfs_net_id); + + return nn->nfs_keyring; +} + #endif From 4f6cd78de1a78d359d46148ee72b57467fa26f84 Mon Sep 17 00:00:00 2001 From: Chuck Lever Date: Tue, 6 Oct 2026 11:31:16 -0400 Subject: [PATCH 60/68] NFS: add a key type that reveals the namespace .nfs keyring serial number The per-namespace .nfs keyring is linked into no keyring that userspace possesses, so a tool that provisions x509 credentials, or a mount helper that searches for them, cannot reach it. Every keyring operation from userspace takes a serial number, and nothing hands this one out. Register an "nfs_keyring" key type whose request_key handler runs in the caller's context, without an upcall, and instantiates the key with the serial number of the caller's namespace .nfs keyring. Userspace reads the serial number with one request_key() and one keyctl_read(). A keyring search runs before the handler does, and a task keeps its session keyring across setns(). The type carries KEY_TYPE_NET_DOMAIN so a key instantiated in one namespace does not answer a request from another. Its preparse accepts no payload but the caller's own serial number, so a key planted by add_key() cannot misdirect a later request. Register the type after register_pernet_subsys() has installed nfs_net_id, and unregister it before the per-namespace state goes away. unregister_key_type() waits for in-flight request_key() calls to drain, so the handler never runs against a freed struct nfs_net. Signed-off-by: Chuck Lever Signed-off-by: Anna Schumaker --- Documentation/filesystems/nfs/index.rst | 1 + Documentation/filesystems/nfs/keyring.rst | 69 +++++++++++++++++ fs/nfs/inode.c | 94 ++++++++++++++++++++++- 3 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 Documentation/filesystems/nfs/keyring.rst diff --git a/Documentation/filesystems/nfs/index.rst b/Documentation/filesystems/nfs/index.rst index a29a212b5b4d..dab5f16aaad1 100644 --- a/Documentation/filesystems/nfs/index.rst +++ b/Documentation/filesystems/nfs/index.rst @@ -7,6 +7,7 @@ NFS :maxdepth: 1 client-identifier + keyring exporting localio pnfs diff --git a/Documentation/filesystems/nfs/keyring.rst b/Documentation/filesystems/nfs/keyring.rst new file mode 100644 index 000000000000..29367e71a801 --- /dev/null +++ b/Documentation/filesystems/nfs/keyring.rst @@ -0,0 +1,69 @@ +.. SPDX-License-Identifier: GPL-2.0 + +==================================== +The per-namespace NFS client keyring +==================================== + +The NFS client holds one keyring, named ".nfs", per network +namespace. An xprtsec=mtls mount presents the client certificate and +private key named by its cert_serial= and privkey_serial= mount +options, and the handshake links the mount's namespace .nfs keyring +into tlshd before tlshd reads those keys. Keys placed on the .nfs +keyring therefore need not grant user read permission: tlshd reaches +them as a possessor, and a tlshd in another network namespace never +possesses them. + +The keyring is owned by the owner of the network namespace's user +namespace, with KEY_POS_ALL and KEY_USR_ALL less SETATTR. It is not +charged to any quota. Keys added to it by userspace are charged to +the user that adds them. + +Finding the keyring serial number +================================= + +The .nfs keyring is not linked into any keyring that userspace +possesses, so a serial number is the only way to name it. The NFS +client registers a key type, "nfs_keyring", whose sole purpose is to +hand that serial number out. Its request_key handler runs in the +caller's context and does not upcall to /sbin/request-key. + +To obtain the serial number, request a key of type "nfs_keyring" with +the description ".nfs" and read its payload:: + + id=$(keyctl request2 nfs_keyring .nfs "" @s) + serial=$(keyctl print $id) + +The contract of the key type is: + + * The description is the string ".nfs". Any other description is + rejected with EINVAL before a key is allocated. + + * The callout info must be present. request_key() with a NULL + callout_info never invokes a handler and returns ENOKEY when no + matching key exists, so use request_key() with an empty string, or + ``keyctl request2`` rather than ``keyctl request``. The content of + the callout info is ignored. + + * The payload is the keyring serial number as decimal ASCII digits + with no terminating NUL. The return value of keyctl_read() gives + the length; a buffer of 12 bytes is sufficient. + + * add_key() with this type accepts only that payload. A key carrying + any other value is rejected with EINVAL, so a key found in the + caller's keyrings always holds the serial number of the caller's + namespace. + + * The key type carries KEY_TYPE_NET_DOMAIN. A key instantiated in one + network namespace does not answer a request made in another, so a + process that keeps its session keyring across setns() receives the + serial number of the namespace it is in at the time of the request. + +The serial number is not a secret. The keyring's own permissions +decide what a caller can do with it. + +Provisioning credentials +======================== + +Add the client certificate and private key to the .nfs keyring as +keys that grant no user read permission, then pass their serial +numbers as cert_serial= and privkey_serial= mount options. diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c index 2b494fa5ecc0..fe7a05dec02f 100644 --- a/fs/nfs/inode.c +++ b/fs/nfs/inode.c @@ -42,6 +42,12 @@ #include #include #include +#include +#include +#include +#ifdef CONFIG_KEYS +#include +#endif #include "nfs4_fs.h" #include "callback.h" @@ -2667,6 +2673,76 @@ static void nfs_exit_keyring(struct nfs_net *nn) { key_put(nn->nfs_keyring); } + +static int nfs_keyring_vet_description(const char *desc) +{ + return strcmp(desc, ".nfs") ? -EINVAL : 0; +} + +static int nfs_keyring_format_serial(char *buf, size_t len) +{ + struct key *keyring = nfs_net_keyring(current->nsproxy->net_ns); + + return snprintf(buf, len, "%d", key_serial(keyring)); +} + +/* + * A key planted by add_key() answers request_key() before the handler + * runs. Accept only the serial number the handler would produce. + */ +static int nfs_keyring_preparse(struct key_preparsed_payload *prep) +{ + char serial[12]; + int len; + + len = nfs_keyring_format_serial(serial, sizeof(serial)); + if (prep->datalen != len || !prep->data || + memcmp(prep->data, serial, len)) + return -EINVAL; + return user_preparse(prep); +} + +static int nfs_keyring_request_key(struct key *authkey, void *aux) +{ + struct request_key_auth *rka = get_request_key_auth(authkey); + char serial[12]; + int len, ret; + + len = nfs_keyring_format_serial(serial, sizeof(serial)); + ret = key_instantiate_and_link(rka->target_key, serial, len, + rka->dest_keyring, authkey); + if (ret < 0) + complete_request_key(authkey, ret); + return ret; +} + +/* + * A session keyring survives setns(). Without the net domain tag, a + * key instantiated in one namespace answers a request from another. + */ +static struct key_type key_type_nfs_keyring = { + .name = "nfs_keyring", + .flags = KEY_TYPE_NET_DOMAIN, + .vet_description = nfs_keyring_vet_description, + .preparse = nfs_keyring_preparse, + .free_preparse = user_free_preparse, + .instantiate = generic_key_instantiate, + .revoke = user_revoke, + .destroy = user_destroy, + .describe = user_describe, + .read = user_read, + .request_key = nfs_keyring_request_key, +}; + +static int __init nfs_register_key_type(void) +{ + return register_key_type(&key_type_nfs_keyring); +} + +static void nfs_unregister_key_type(void) +{ + unregister_key_type(&key_type_nfs_keyring); +} #else static inline int nfs_init_keyring(struct net *net) { @@ -2676,6 +2752,15 @@ static inline int nfs_init_keyring(struct net *net) static inline void nfs_exit_keyring(struct nfs_net *nn) { } + +static inline int nfs_register_key_type(void) +{ + return 0; +} + +static inline void nfs_unregister_key_type(void) +{ +} #endif /* CONFIG_KEYS */ static int nfs_net_init(struct net *net) @@ -2740,10 +2825,14 @@ static int __init init_nfs_fs(void) if (err < 0) goto err_sysfs; - err = nfsiod_start(); + err = nfs_register_key_type(); if (err) goto err_pernet; + err = nfsiod_start(); + if (err) + goto err_keytype; + err = nfs_fs_proc_init(); if (err) goto err_nfsiod; @@ -2787,6 +2876,8 @@ static int __init init_nfs_fs(void) nfs_fs_proc_exit(); err_nfsiod: nfsiod_stop(); +err_keytype: + nfs_unregister_key_type(); err_pernet: unregister_pernet_subsys(&nfs_net_ops); err_sysfs: @@ -2801,6 +2892,7 @@ static void __exit exit_nfs_fs(void) nfs_destroy_readpagecache(); nfs_destroy_inodecache(); nfs_destroy_nfspagecache(); + nfs_unregister_key_type(); unregister_pernet_subsys(&nfs_net_ops); unregister_nfs_fs(); nfs_fs_proc_exit(); From 0b5bb4e586f7b0a449f4d90a524f76a3be5db355 Mon Sep 17 00:00:00 2001 From: Tim Menninger Date: Thu, 24 Sep 2026 20:34:40 +0000 Subject: [PATCH 61/68] xprtrdma: prevent disconnect feedback during endpoint teardown xprt_autoclose() clears XPRT_CLOSE_WAIT before invoking the transport's ->close() operation. For RPC/RDMA, ->close() calls rpcrdma_xprt_disconnect(), which disconnects the RDMA CM endpoint and drains its QP. Since commit 31e62d25b5b8 ("xprtrdma: Simplify RPC wake-ups on connect"), RDMA_CM_EVENT_DISCONNECTED requests a SUNRPC transport disconnect. Today that path, along with failed completion handling, reaches xprt_force_disconnect() through rpcrdma_force_disconnect(). Commit c487eb7d8e41 ("xprtrdma: Clean up disconnect") later added re_force_disconnect to ensure that multiple failure indications request only one SUNRPC disconnect. However, the gate remains open when SUNRPC itself begins endpoint teardown. If re_force_disconnect is still clear, either a CM disconnect event or a completion error can set XPRT_CLOSE_WAIT again while the current close is in progress. When the transport send lock is subsequently released, xprt_clear_locked() observes XPRT_CLOSE_WAIT and queues another autoclose. Requesting a disconnect this way is necessary when a connection is lost unexpectedly, but not when the event was generated by rpcrdma_xprt_disconnect() itself. In that case SUNRPC is already committed to closing the transport. Mark re_force_disconnect before initiating local endpoint teardown so that CM events and completion errors caused by that teardown cannot request another close. An unexpected error that wins the race before local endpoint teardown begins can still request a disconnect normally. Use a fully ordered exchange so that the gate is globally closed before rdma_disconnect() can transition the QP to error and generate the callbacks that the gate suppresses. In 30-second traces of a sustained recovery loop, without this change there were 7,693 xprt_force_disconnect() calls, 6,267 SUNRPC autocloses, and 172,892 receive-side IB_WC_WR_FLUSH_ERR completions. With this change, the same tracepoints recorded 3,512 xprt_force_disconnect() calls, 3,212 autocloses, and 131,287 receive-side flush completions. Thus QP teardown continues to produce flushed completions, but closing the disconnect gate before teardown substantially reduces their feedback into the SUNRPC close path. Fixes: 31e62d25b5b8 ("xprtrdma: Simplify RPC wake-ups on connect") Cc: stable@vger.kernel.org Signed-off-by: Tim Menninger Reviewed-by: Chuck Lever Signed-off-by: Anna Schumaker --- net/sunrpc/xprtrdma/verbs.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/net/sunrpc/xprtrdma/verbs.c b/net/sunrpc/xprtrdma/verbs.c index be335eed329d..54957295a631 100644 --- a/net/sunrpc/xprtrdma/verbs.c +++ b/net/sunrpc/xprtrdma/verbs.c @@ -114,9 +114,9 @@ static void rpcrdma_xprt_drain(struct rpcrdma_xprt *r_xprt) rpcrdma_ep_put(ep); } -/* Ensure xprt_force_disconnect() is invoked exactly once when a - * connection is closed or lost. (The important thing is it needs - * to be invoked "at least" once). +/* Ensure xprt_force_disconnect() is invoked at most once for an + * unexpected endpoint failure. Local endpoint teardown closes this + * gate before it can generate CM events and flushed completions. */ void rpcrdma_force_disconnect(struct rpcrdma_ep *ep) { @@ -587,6 +587,9 @@ void rpcrdma_xprt_disconnect(struct rpcrdma_xprt *r_xprt) if (!ep) return; + /* Prevent teardown-generated events from requesting another close. */ + atomic_xchg(&ep->re_force_disconnect, 1); + id = ep->re_id; rc = rdma_disconnect(id); trace_xprtrdma_disconnect(r_xprt, rc); From 8c955dacf45f8964b98f26910ffecb5e26c02e31 Mon Sep 17 00:00:00 2001 From: Roberto Bergantinos Corpas Date: Tue, 21 Apr 2026 12:00:46 +0200 Subject: [PATCH 62/68] nfs: set gencount on protocol-specific remove commit bd4928ec799b ("NFS: Avoid changing nlink when file removes and attribute updates race") avoids races on attribute cache with any inflight RPC that may modify inode attributes (and gencount) during i.e. REMOVE. However it does not take into account that REMOVE may trigger a delegation return which also advances gencount (via the attribute refresh during delegation return), and in that case the gencount mismatch is not due to an inflight RPC that already reflected the removal. Push the setting of gencount under each version-specific layer, reordering correctly for nfsv4 case after the delegation returns. This fixes LTP/inotify04 failure after bd4928ec799b. Fixes: bd4928ec799b ("NFS: Avoid changing nlink when file removes and attribute updates race") Signed-off-by: Roberto Bergantinos Corpas Signed-off-by: Anna Schumaker --- fs/nfs/dir.c | 13 +++---------- fs/nfs/internal.h | 1 + fs/nfs/nfs3proc.c | 8 ++++++++ fs/nfs/nfs4proc.c | 5 +++++ fs/nfs/proc.c | 8 ++++++++ 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c index 49394123bd09..741144f04932 100644 --- a/fs/nfs/dir.c +++ b/fs/nfs/dir.c @@ -1935,7 +1935,7 @@ static int nfs_dentry_delete(const struct dentry *dentry) } /* Ensure that we revalidate inode->i_nlink */ -static void nfs_drop_nlink(struct inode *inode, unsigned long gencount) +void nfs_drop_nlink(struct inode *inode, unsigned long gencount) { struct nfs_inode *nfsi = NFS_I(inode); @@ -1949,6 +1949,7 @@ static void nfs_drop_nlink(struct inode *inode, unsigned long gencount) NFS_INO_INVALID_NLINK); spin_unlock(&inode->i_lock); } +EXPORT_SYMBOL_GPL(nfs_drop_nlink); /* * Called when the dentry loses inode. @@ -2554,7 +2555,6 @@ EXPORT_SYMBOL_GPL(nfs_rmdir); static int nfs_safe_remove(struct dentry *dentry) { struct inode *dir = d_inode(dentry->d_parent); - struct inode *inode = d_inode(dentry); int error = -EBUSY; dfprintk(VFS, "NFS: safe_remove(%pd2)\n", dentry); @@ -2566,14 +2566,7 @@ static int nfs_safe_remove(struct dentry *dentry) } trace_nfs_remove_enter(dir, dentry); - if (inode != NULL) { - unsigned long gencount = READ_ONCE(NFS_I(inode)->attr_gencount); - - error = NFS_PROTO(dir)->remove(dir, dentry); - if (error == 0) - nfs_drop_nlink(inode, gencount); - } else - error = NFS_PROTO(dir)->remove(dir, dentry); + error = NFS_PROTO(dir)->remove(dir, dentry); if (error == -ENOENT) nfs_dentry_handle_enoent(dentry); trace_nfs_remove_exit(dir, dentry, error); diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h index 959edbc9add2..42f170973000 100644 --- a/fs/nfs/internal.h +++ b/fs/nfs/internal.h @@ -395,6 +395,7 @@ extern unsigned long nfs_access_cache_count(struct shrinker *shrink, struct shrink_control *sc); extern unsigned long nfs_access_cache_scan(struct shrinker *shrink, struct shrink_control *sc); +extern void nfs_drop_nlink(struct inode *inode, unsigned long gencount); struct dentry *nfs_lookup(struct inode *, struct dentry *, unsigned int); void nfs_d_prune_case_insensitive_aliases(struct inode *inode); int nfs_create(struct mnt_idmap *, struct inode *, struct dentry *, diff --git a/fs/nfs/nfs3proc.c b/fs/nfs/nfs3proc.c index b80d0c5efc27..b5aa3100b627 100644 --- a/fs/nfs/nfs3proc.c +++ b/fs/nfs/nfs3proc.c @@ -448,13 +448,21 @@ nfs3_proc_remove(struct inode *dir, struct dentry *dentry) .rpc_resp = &res, }; int status = -ENOMEM; + struct inode *inode = d_inode(dentry); + unsigned long gencount; dprintk("NFS call remove %pd2\n", dentry); res.dir_attr = nfs_alloc_fattr(); if (res.dir_attr == NULL) goto out; + if (inode) + gencount = READ_ONCE(NFS_I(inode)->attr_gencount); + status = rpc_call_sync(NFS_CLIENT(dir), &msg, 0); + if (status == 0 && inode) + nfs_drop_nlink(inode, gencount); + nfs_post_op_update_inode(dir, res.dir_attr); nfs_free_fattr(res.dir_attr); out: diff --git a/fs/nfs/nfs4proc.c b/fs/nfs/nfs4proc.c index 518348e87dd8..0adc209dc5df 100644 --- a/fs/nfs/nfs4proc.c +++ b/fs/nfs/nfs4proc.c @@ -4969,12 +4969,14 @@ static int nfs4_proc_remove(struct inode *dir, struct dentry *dentry) }; struct inode *inode = d_inode(dentry); int err; + unsigned long gencount; if (inode) { if (inode->i_nlink == 1) nfs4_inode_return_delegation(inode); else nfs4_inode_make_writeable(inode); + gencount = READ_ONCE(NFS_I(inode)->attr_gencount); } do { err = _nfs4_proc_remove(dir, &dentry->d_name, NF4REG); @@ -4982,6 +4984,9 @@ static int nfs4_proc_remove(struct inode *dir, struct dentry *dentry) err = nfs4_handle_exception(NFS_SERVER(dir), err, &exception); } while (exception.retry); + + if (err == 0 && inode) + nfs_drop_nlink(inode, gencount); return err; } diff --git a/fs/nfs/proc.c b/fs/nfs/proc.c index 03c2c1f31be9..406d04b52a7d 100644 --- a/fs/nfs/proc.c +++ b/fs/nfs/proc.c @@ -323,9 +323,17 @@ nfs_proc_remove(struct inode *dir, struct dentry *dentry) .rpc_argp = &arg, }; int status; + struct inode *inode = d_inode(dentry); + unsigned long gencount; dprintk("NFS call remove %pd2\n",dentry); + if (inode) + gencount = READ_ONCE(NFS_I(inode)->attr_gencount); + status = rpc_call_sync(NFS_CLIENT(dir), &msg, 0); + if (status == 0 && inode) + nfs_drop_nlink(inode, gencount); + nfs_mark_for_revalidate(dir); dprintk("NFS reply remove: %d\n", status); From 9ee534a75912361308b1f956bc3e7895a082a422 Mon Sep 17 00:00:00 2001 From: Yongjian Mu Date: Mon, 12 Oct 2026 17:06:19 +0000 Subject: [PATCH 63/68] xprtrdma: trim head iovec when the payload arrives via a Write chunk The upper layer sets rq_rcv_buf.head[0].iov_len from its estimate of the reply header size. For RPCSEC_GSS that estimate includes auth->au_ralign, which gss_create_new() initializes to GSS_VERF_SLACK >> 2 (25 XDR words, or 100 bytes) and which is corrected to the real verifier size only after the first reply on that rpc_auth has been unwrapped. In this case, the krb5 MIC verifier is 36 bytes, so every request encoded before the first reply completes has a head that is 64 bytes too long. On TCP this is harmless: xdr_realign_pages() moves the excess head bytes, which are genuine reply data, into the page list. With RPC-over-RDMA and a Write chunk (sec=krb5, i.e. RPC_GSS_SVC_NONE, where DDP is allowed), the payload has already been placed directly into the page list, and the receive buffer holds only the inline part of the reply. rpcrdma_inline_fixup() clamps its local copy of the head length to the inline length but leaves head.iov_len untouched, so xdr_realign_pages() still sees iov_len > cur and shifts whatever lies past the inline reply in the receive buffer into the front of the directly-placed payload: rpc_xdr_recvfrom: head=[...,196] page=524288 tail=[...,64] rpc_xdr_alignment: nfsv4 READ offset=132 copied=64 The first 64 bytes returned by each such READ are incorrect (zeroed, or shifted right by 64 bytes depending on the kernel's xdr_shrink_bufhead() implementation). This is readily hit with pNFS flexfiles over RDMA and sec=krb5: the per-DS rpc_clnt gets a fresh rpc_auth whose first RPC is a large READ. Reads following the first completed reply return the expected data. Commit cb0ae1fbb2f5 ("xprtrdma: Do not update {head, tail}.iov_len in rpcrdma_inline_fixup()") removed the head-length correction while fixing the handling of pure-inline krb5p replies. When a Write chunk conveyed the payload, trim head.iov_len (in both rq_rcv_buf and rq_private_buf, which call_decode() expects to match) to the inline length actually received so that the XDR layer does not realign the page list. These READ replies have no trailing inline fields, and the payload is already in the page list, so the upper layer's head estimate can be safely reduced. Reduce buflen by the same amount to avoid treating unused head capacity as additional tail capacity. Pure inline replies and Reply chunk replies are unchanged. Tested on Linux 6.8.0 and 7.3-rc4 with NFSv4.1 pNFS flexfiles over RPC/RDMA and sec=krb5. Fixes: cb0ae1fbb2f5 ("xprtrdma: Do not update {head, tail}.iov_len in rpcrdma_inline_fixup()") Cc: stable@vger.kernel.org Signed-off-by: Yongjian Mu Reviewed-by: Chuck Lever Signed-off-by: Tim Menninger Signed-off-by: Anna Schumaker --- net/sunrpc/xprtrdma/rpc_rdma.c | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/net/sunrpc/xprtrdma/rpc_rdma.c b/net/sunrpc/xprtrdma/rpc_rdma.c index 1285f04cdac1..7e5713367291 100644 --- a/net/sunrpc/xprtrdma/rpc_rdma.c +++ b/net/sunrpc/xprtrdma/rpc_rdma.c @@ -984,12 +984,16 @@ void rpcrdma_reset_cwnd(struct rpcrdma_xprt *r_xprt) * @rqst: controlling RPC request * @srcp: points to RPC message payload in receive buffer * @copy_len: remaining length of receive buffer content - * @pad: Write chunk pad bytes needed (zero for pure inline) + * @writelist: bytes conveyed by the Write chunk (zero for pure inline) * * The upper layer has set the maximum number of bytes it can * receive in each component of rq_rcv_buf. These values are set in * the head.iov_len, page_len, tail.iov_len, and buflen fields. * + * For READ replies with a Write chunk, the payload is already in + * the page list. An overestimated head length and buflen can be + * reduced without moving reply data. + * * Unlike the TCP equivalent (xdr_partial_copy_from_skb), in * many cases this function simply updates iov_base pointers in * rq_rcv_buf to point directly to the received reply data, to @@ -998,13 +1002,16 @@ void rpcrdma_reset_cwnd(struct rpcrdma_xprt *r_xprt) * Returns the count of bytes which had to be memcopied. */ static unsigned long -rpcrdma_inline_fixup(struct rpc_rqst *rqst, char *srcp, int copy_len, int pad) +rpcrdma_inline_fixup(struct rpc_rqst *rqst, char *srcp, int copy_len, + u32 writelist) { unsigned long fixup_copy_count; int i, npages, curlen; char *destp; struct page **ppages; int page_base; + int pad = writelist & 3; + unsigned int delta; /* The head iovec is redirected to the RPC reply message * in the receive buffer, to avoid a memcopy. @@ -1016,8 +1023,20 @@ rpcrdma_inline_fixup(struct rpc_rqst *rqst, char *srcp, int copy_len, int pad) * head.iov_len bytes are copied into the page list. */ curlen = rqst->rq_rcv_buf.head[0].iov_len; - if (curlen > copy_len) + if (curlen > copy_len) { + /* These READ replies have no inline fields after the payload. + * Trim the head without moving the directly-placed data. + */ + if (writelist) { + delta = curlen - copy_len; + + rqst->rq_rcv_buf.head[0].iov_len = copy_len; + rqst->rq_private_buf.head[0].iov_len = copy_len; + rqst->rq_rcv_buf.buflen -= delta; + rqst->rq_private_buf.buflen -= delta; + } curlen = copy_len; + } srcp += curlen; copy_len -= curlen; @@ -1245,7 +1264,7 @@ rpcrdma_decode_msg(struct rpcrdma_xprt *r_xprt, struct rpcrdma_rep *rep, base = (char *)xdr_inline_decode(xdr, 0); rpclen = xdr_stream_remaining(xdr); r_xprt->rx_stats.fixup_copy_count += - rpcrdma_inline_fixup(rqst, base, rpclen, writelist & 3); + rpcrdma_inline_fixup(rqst, base, rpclen, writelist); r_xprt->rx_stats.total_rdma_reply += writelist; return rpclen + xdr_align_size(writelist); From 4b27670ea25b955b710021151471cb252240b1e0 Mon Sep 17 00:00:00 2001 From: Tigran Mkrtchyan Date: Fri, 9 Oct 2026 17:58:02 +0200 Subject: [PATCH 64/68] nfsv4/pnfs: use the MDS authflavor for data server connections When a pNFS client connects to an NFSv4.1 data server, the EXCHANGE_ID is always attempted with RPCSEC_GSS krb5i first, even if the auth flavor with the MDS is AUTH_SYS. On a host that runs rpc.gssd but has no usable machine credential, every first contact with a data server therefore upcalls to rpc.gssd before falling back to AUTH_SYS. As nfs4_discover_server_trunking is serialized by nfs_clid_init_mutex such an extra upcall creates a backlog of DS connections when many files accessed in parallel. Pass the authflavor of the MDS lease to nfs4_alloc_client() and skip the krb5i attempt if the MDS already ended up with AUTH_SYS. Nothing changes for the MDS itself, or for data servers of an MDS whose lease was established with krb5i. Fixes: 4edaa308888b ("NFS: Use "krb5i" to establish NFSv4 state whenever possible") Signed-off-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/internal.h | 1 + fs/nfs/nfs4client.c | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h index 42f170973000..f6f7f70b21e2 100644 --- a/fs/nfs/internal.h +++ b/fs/nfs/internal.h @@ -86,6 +86,7 @@ struct nfs_client_initdata { struct xprtsec_parms xprtsec; unsigned long connect_timeout; unsigned long reconnect_timeout; + rpc_authflavor_t authflavor; /* Auth flavor to use for DS connection */ }; /* diff --git a/fs/nfs/nfs4client.c b/fs/nfs/nfs4client.c index 5d054d3dced1..440f8496b717 100644 --- a/fs/nfs/nfs4client.c +++ b/fs/nfs/nfs4client.c @@ -230,8 +230,12 @@ struct nfs_client *nfs4_alloc_client(const struct nfs_client_initdata *cl_init) /* * Set up the connection to the server before we add add to the * global list. + * + * Prefer krb5i unless the caller already knows AUTH_UNIX is required. */ - err = nfs_create_rpc_client(clp, cl_init, RPC_AUTH_GSS_KRB5I); + err = -EINVAL; + if (cl_init->authflavor != RPC_AUTH_UNIX) + err = nfs_create_rpc_client(clp, cl_init, RPC_AUTH_GSS_KRB5I); if (err == -EINVAL) err = nfs_create_rpc_client(clp, cl_init, RPC_AUTH_UNIX); if (err < 0) @@ -816,6 +820,7 @@ struct nfs_client *nfs4_set_ds_client(struct nfs_server *mds_srv, .cert_serial = TLS_NO_CERT, .privkey_serial = TLS_NO_PRIVKEY, }, + .authflavor = mds_clp->cl_rpcclient->cl_auth->au_flavor, }; char buf[INET6_ADDRSTRLEN + 1]; From fde27289503beabe7e0856dac164a5ced4be0342 Mon Sep 17 00:00:00 2001 From: Tigran Mkrtchyan Date: Sun, 11 Oct 2026 00:18:38 +0200 Subject: [PATCH 65/68] pNFS/flexfiles: make pg_init retry loops killable When a task is killed while the pNFS client connects to a data server, nfs4_pnfs_ds_connect() fails with -ERESTARTSYS. If the layout has FF_FLAGS_NO_IO_THRU_MDS set, ff_layout_pg_init_read() cannot fall back to the MDS and retries after an uninterruptible ssleep. As the fatal signal stays pending, every new connect attempt is aborted the same way and the task loops forever without returning to user space: # grep -E 'SigPnd|ShdPnd' /proc/156021/status SigPnd: 0000000000000100 ShdPnd: 0000000000000100 # cat /proc/156021/stack msleep+0x2e/0x50 ff_layout_pg_init_read+0x267/0x310 [nfs_layout_flexfiles] __nfs_pageio_add_request+0x1c1/0x480 [nfs] nfs_pageio_add_request+0x21d/0x2c0 [nfs] nfs_read_add_folio+0x143/0x200 [nfs] nfs_readahead+0x204/0x290 [nfs] read_pages+0x62/0x230 page_cache_ra_unbounded+0x136/0x180 filemap_get_pages+0xf2/0x350 filemap_read+0xd5/0x320 nfs_file_read+0x6d/0xc0 [nfs] vfs_read+0x24f/0x3a0 __x64_sys_pread64+0x90/0xc0 do_syscall_64+0x5f/0xe0 Previous attempt propagates the error from nfs4_ff_layout_prepare_ds(), but only ff_layout_{read,write}_pagelist() look at it. The pg_init functions test IS_ERR() and retry on any error. Check for a fatal signal or an exiting task before sleeping in ff_layout_pg_init_read() and ff_layout_pg_init_write() and fail the I/O instead of retrying. Fixes: f06bedfa62d5 ("pNFS/flexfiles: don't attempt pnfs on fatal DS errors") Signed-off-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index d4f2b57e9d5b..ce3bb5f7a60a 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -960,6 +960,20 @@ ff_layout_get_ds_for_read(struct nfs_pageio_descriptor *pgio, offset, dss_id); } +/* + * Wait before retrying pg_init. Returns false, with pgio->pg_error set, + * if the task was killed or is exiting and must not retry. + */ +static bool ff_layout_pg_retry_wait(struct nfs_pageio_descriptor *pgio) +{ + if (fatal_signal_pending(current) || nfs_current_task_exiting()) { + pgio->pg_error = -ERESTARTSYS; + return false; + } + ssleep(1); + return true; +} + static void ff_layout_pg_get_read(struct nfs_pageio_descriptor *pgio, struct nfs_page *req, @@ -1059,7 +1073,8 @@ ff_layout_pg_init_read(struct nfs_pageio_descriptor *pgio, goto out_mds; pnfs_generic_pg_cleanup(pgio); /* Sleep for 1 second before retrying */ - ssleep(1); + if (!ff_layout_pg_retry_wait(pgio)) + return; goto retry; } @@ -1098,7 +1113,8 @@ ff_layout_pg_init_read(struct nfs_pageio_descriptor *pgio, } pgio->pg_error = 0; /* Sleep for 1 second before retrying */ - ssleep(1); + if (!ff_layout_pg_retry_wait(pgio)) + return; goto retry; out_mds: trace_pnfs_mds_fallback_pg_init_read(pgio->pg_inode, @@ -1156,7 +1172,8 @@ ff_layout_pg_init_write(struct nfs_pageio_descriptor *pgio, goto out_mds; pnfs_generic_pg_cleanup(pgio); /* Sleep for 1 second before retrying */ - ssleep(1); + if (!ff_layout_pg_retry_wait(pgio)) + return; goto retry; } pgm = &pgio->pg_mirrors[i]; From 8c13019a785d6adb1efd797ac03903758abfd7a8 Mon Sep 17 00:00:00 2001 From: Tigran Mkrtchyan Date: Sun, 11 Oct 2026 00:18:39 +0200 Subject: [PATCH 66/68] pNFS/flexfiles: don't report an interrupted DS connect as a DS error If nfs4_pnfs_ds_connect() is interrupted by a signal, nfs4_ff_layout_prepare_ds() records an NFS4ERR_NXIO error for the data server, sends a LAYOUTERROR to the MDS and may mark the layout for return. The data server is healthy in this case; only the calling task was killed. A bpftrace log of a killed task shows the connect failing with -ERESTARTSYS, followed by a LAYOUTERROR for a data server that completed the TCP handshake with the same client microseconds later (time in microseconds, abridged): python3 rpc_create (ret) 1xx.yy.50.179:24009 err=-512 kworker inet_sock_set_state :735 -> 1xx.yy.50.179:24009 CLOSE -> SYN_SENT swapper inet_sock_set_state :735 -> 1xx.yy.50.179:24009 SYN_SENT -> ESTABLISHED kworker inet_sock_set_state :735 -> 1xx.yy.50.179:24009 ESTABLISHED -> FIN_WAIT1 python3 nfs4_pnfs_ds_connect (ret) status=-512 python3 nfs4_ff_layout_prepare_ds (ret) err=-512 kworker rpc_request nfs v4 LAYOUTERROR async task=3/6752 kworker rpc_stats_latency LAYOUTERROR backlog=66us rtt=315us exec=421us Return the error to the caller without reporting a DS failure when the connect ended with -ERESTARTSYS or -EINTR. Signed-off-by: Tigran Mkrtchyan Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayoutdev.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/nfs/flexfilelayout/flexfilelayoutdev.c b/fs/nfs/flexfilelayout/flexfilelayoutdev.c index ff4d271baff5..3af0e6d361a5 100644 --- a/fs/nfs/flexfilelayout/flexfilelayoutdev.c +++ b/fs/nfs/flexfilelayout/flexfilelayoutdev.c @@ -443,6 +443,11 @@ nfs4_ff_layout_prepare_ds(struct pnfs_layout_segment *lseg, mirror_ds->ds_versions[0].wsize = max_payload; goto out; } + /* The connect was interrupted by a signal: this is not a DS error */ + if (status == -ERESTARTSYS || status == -EINTR) { + ds = ERR_PTR(status); + goto out; + } noconnect: ff_layout_track_ds_error(FF_LAYOUT_FROM_HDR(lseg->pls_layout), mirror, From 7fa2ad367abe6d3155daefb57331e7fce1a17afd Mon Sep 17 00:00:00 2001 From: Benjamin Coddington Date: Thu, 15 Oct 2026 07:33:07 -0400 Subject: [PATCH 67/68] NFSv4/flexfiles: fail I/O to the data server once a lock is lost When a client loses a byte-range lock, because its lease expired and it may not recover the lock (nfs.recover_lost_locks=0) or because reclaim failed, it marks the lock state NFS_LOCK_LOST and fails further I/O by that lock owner. Otherwise an application that believes it holds a lock over a range is never told that the lock is gone, and can overwrite data that another client has since written under its own lock on that range. The NFSv4 READ/WRITE and file layout I/O paths apply this in rpc_call_prepare, where nfs4_set_rw_stateid() returns -EIO for a lost lock. The flexfiles data server I/O path does not: the NFSv3 data server path never checked, and the check on the NFSv4 path was removed when the data server stateid began to be taken from the layout. Fail read and write RPCs to the data server in rpc_call_prepare when the lock is lost. The data server path has no use for the stateid that nfs4_set_rw_stateid() selects, and selecting it looks up a delegation or open stateid on every I/O. Instead add nfs4_lock_context_lost(), which only tests NFS_LOCK_LOST, and check it alongside the open state. Its lookup, __nfs4_lookup_lock_state(), is split out of __nfs4_find_lock_state() and takes no reference: a lock state is only unlinked from state->lock_states under state->state_lock, so one found while holding that lock cannot be freed before it is released. Use it in nfs4_copy_lock_stateid() as well, which only reads the lock state under that lock. That drops a reference get and put from stateid selection for all NFSv4 I/O without changing the stateid selected. Terminating the RPC in rpc_call_prepare is not enough on its own. The done callback reads the -EIO as a data server connection error, forgets the device ID, and resets the I/O to pNFS (or, for an NFSv4 data server, possibly to the MDS). After a reset to pNFS a new LAYOUTGET is granted and rpc_call_prepare fails the I/O again, without end. The existing NFS_CONTEXT_BAD check does not loop, since the open state is then invalid and pnfs_update_layout() fails the reset I/O, but it still forgets the device ID. So when an RPC that was never sent fails and the open context is bad, the open state is invalid, or the lock is lost, complete it with -EIO without error handling. An RPC that was sent and failed is handled as before, so a real data server error is still reported to the MDS. Fixes: d67ae825a59d ("pnfs/flexfiles: Add the FlexFile Layout Driver") Fixes: bb21ce0ad227 ("flexfiles: use per-mirror specified stateid for IO") Cc: stable@vger.kernel.org Tested-by: Arun Potharaju Assisted-by: LLM Signed-off-by: Benjamin Coddington Signed-off-by: Anna Schumaker --- fs/nfs/flexfilelayout/flexfilelayout.c | 25 +++++++++++++++-- fs/nfs/nfs4_fs.h | 2 ++ fs/nfs/nfs4state.c | 38 +++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/fs/nfs/flexfilelayout/flexfilelayout.c b/fs/nfs/flexfilelayout/flexfilelayout.c index ce3bb5f7a60a..9492d6bc1d1a 100644 --- a/fs/nfs/flexfilelayout/flexfilelayout.c +++ b/fs/nfs/flexfilelayout/flexfilelayout.c @@ -1653,6 +1653,15 @@ static void ff_layout_io_track_ds_error(struct pnfs_layout_segment *lseg, dprintk("%s: err %d op %d status %u\n", __func__, err, opnum, status); } +static bool ff_layout_io_state_lost(const struct nfs_pgio_header *hdr) +{ + struct nfs_open_context *ctx = hdr->args.context; + + return test_bit(NFS_CONTEXT_BAD, &ctx->flags) || + !nfs4_valid_open_stateid(ctx->state) || + nfs4_lock_context_lost(ctx->state, hdr->args.lock_context); +} + /* NFS_PROTO call done callback routines */ static int ff_layout_read_done_cb(struct rpc_task *task, struct nfs_pgio_header *hdr) @@ -1664,6 +1673,12 @@ static int ff_layout_read_done_cb(struct rpc_task *task, hdr->args.offset); int err; + if (task->tk_status < 0 && !RPC_WAS_SENT(task) && + ff_layout_io_state_lost(hdr)) { + task->tk_status = -EIO; + return 0; + } + if (task->tk_status < 0) { ff_layout_io_track_ds_error(hdr->lseg, hdr->ds_dev, hdr->pgio_mirror_idx, dss_id, @@ -1779,7 +1794,7 @@ static void ff_layout_read_record_layoutstats_done(struct rpc_task *task, static int ff_layout_read_prepare_common(struct rpc_task *task, struct nfs_pgio_header *hdr) { - if (unlikely(test_bit(NFS_CONTEXT_BAD, &hdr->args.context->flags))) { + if (unlikely(ff_layout_io_state_lost(hdr))) { rpc_exit(task, -EIO); return -EIO; } @@ -1868,6 +1883,12 @@ static int ff_layout_write_done_cb(struct rpc_task *task, loff_t end_offs = 0; int err; + if (task->tk_status < 0 && !RPC_WAS_SENT(task) && + ff_layout_io_state_lost(hdr)) { + task->tk_status = -EIO; + return 0; + } + if (task->tk_status < 0) { ff_layout_io_track_ds_error(hdr->lseg, hdr->ds_dev, hdr->pgio_mirror_idx, dss_id, @@ -2004,7 +2025,7 @@ static void ff_layout_write_record_layoutstats_done(struct rpc_task *task, static int ff_layout_write_prepare_common(struct rpc_task *task, struct nfs_pgio_header *hdr) { - if (unlikely(test_bit(NFS_CONTEXT_BAD, &hdr->args.context->flags))) { + if (unlikely(ff_layout_io_state_lost(hdr))) { rpc_exit(task, -EIO); return -EIO; } diff --git a/fs/nfs/nfs4_fs.h b/fs/nfs/nfs4_fs.h index 76dae699d4d7..2ad2efc43bb9 100644 --- a/fs/nfs/nfs4_fs.h +++ b/fs/nfs/nfs4_fs.h @@ -529,6 +529,8 @@ extern int nfs4_select_rw_stateid(struct nfs4_state *, fmode_t, const struct cred **); extern bool nfs4_copy_open_stateid(nfs4_stateid *dst, struct nfs4_state *state); +extern bool nfs4_lock_context_lost(struct nfs4_state *state, + const struct nfs_lock_context *l_ctx); extern struct nfs_seqid *nfs_alloc_seqid(struct nfs_seqid_counter *counter, gfp_t gfp_mask); extern int nfs_wait_on_sequence(struct nfs_seqid *seqid, struct rpc_task *task); diff --git a/fs/nfs/nfs4state.c b/fs/nfs/nfs4state.c index b5d6daa2c416..8cd178f1da4c 100644 --- a/fs/nfs/nfs4state.c +++ b/fs/nfs/nfs4state.c @@ -812,10 +812,13 @@ void nfs4_close_sync(struct nfs4_state *state, fmode_t fmode) * be made. Failing that, a "consistent" choice is preferable. The * consistent choice we make is to prefer the first owner, that of a * Posix lock. + * + * Called with state->state_lock held. No reference is taken: a lock + * state is freed only after it is unlinked under state->state_lock. */ static struct nfs4_lock_state * -__nfs4_find_lock_state(struct nfs4_state *state, - fl_owner_t owner, fl_owner_t owner2) +__nfs4_lookup_lock_state(struct nfs4_state *state, + fl_owner_t owner, fl_owner_t owner2) { struct nfs4_lock_state *pos, *ret = NULL; list_for_each_entry(pos, &state->lock_states, ls_locks) { @@ -826,6 +829,16 @@ __nfs4_find_lock_state(struct nfs4_state *state, if (pos->ls_owner == owner2) ret = pos; } + return ret; +} + +static struct nfs4_lock_state * +__nfs4_find_lock_state(struct nfs4_state *state, + fl_owner_t owner, fl_owner_t owner2) +{ + struct nfs4_lock_state *ret; + + ret = __nfs4_lookup_lock_state(state, owner, owner2); if (ret) refcount_inc(&ret->ls_count); return ret; @@ -968,7 +981,7 @@ static int nfs4_copy_lock_stateid(nfs4_stateid *dst, fl_flock_owner = l_ctx->open_context->flock_owner; spin_lock(&state->state_lock); - lsp = __nfs4_find_lock_state(state, owner, fl_flock_owner); + lsp = __nfs4_lookup_lock_state(state, owner, fl_flock_owner); if (lsp && test_bit(NFS_LOCK_LOST, &lsp->ls_flags)) ret = -EIO; else if (lsp != NULL && test_bit(NFS_LOCK_INITIALIZED, &lsp->ls_flags) != 0) { @@ -976,11 +989,28 @@ static int nfs4_copy_lock_stateid(nfs4_stateid *dst, ret = 0; } spin_unlock(&state->state_lock); - nfs4_put_lock_state(lsp); out: return ret; } +bool nfs4_lock_context_lost(struct nfs4_state *state, + const struct nfs_lock_context *l_ctx) +{ + struct nfs4_lock_state *lsp; + bool ret; + + if (!test_bit(LK_STATE_IN_USE, &state->flags)) + return false; + + spin_lock(&state->state_lock); + lsp = __nfs4_lookup_lock_state(state, l_ctx->lockowner, + l_ctx->open_context->flock_owner); + ret = lsp && test_bit(NFS_LOCK_LOST, &lsp->ls_flags); + spin_unlock(&state->state_lock); + return ret; +} +EXPORT_SYMBOL_GPL(nfs4_lock_context_lost); + bool nfs4_copy_open_stateid(nfs4_stateid *dst, struct nfs4_state *state) { bool ret; From 62e14699c4dfc98c514a44bf780678cf8622fc08 Mon Sep 17 00:00:00 2001 From: Jinpyo Lee Date: Mon, 12 Oct 2026 20:27:10 +0900 Subject: [PATCH 68/68] nfs/localio: pin UUIDs during global invalidation nfs_localio_invalidate_clients() moves UUID nodes embedded in nfs_client objects to a private list, then walks that list without retaining the containing clients. Concurrent client teardown can free an entry that the iterator has already selected, causing a use-after-free. Take a reference to each selected UUID under the namespace list lock. Keep this reference count in nfs_common, independently of cl_count, and have nfs_free_client() drain temporary UUID users before freeing the client. Releasing a UUID reference wakes waiting probes and signals completion when the last reference is dropped. It neither calls into nfs.ko nor performs final NFS client teardown in the NFSD invalidation context. Keep the UUID on the namespace list until its files have been closed. When another thread is already disabling it, wait for that thread to finish unlinking it instead of repeatedly selecting the same head entry. The wait releases the UUID spinlock so file cleanup can make progress. Append registrations in sequence order and capture the last sequence under the namespace list lock at the start of invalidation. Stop at that boundary so new registrations cannot indefinitely extend the sweep, even if its original last entry is removed concurrently. Publish the LOCALIO association under the UUID and namespace list locks before making the entry visible to invalidation. A temporary UUID reference allows the handshake to drop the global lookup lock before taking the UUID lock in the documented order; recheck the lookup in case the probe was withdrawn. A new probe waits for temporary UUID users to drain, then rechecks the association before reusing the UUID. This keeps a transient invalidation reference from discarding the client's LOCALIO revalidation attempt. Withdraw pending probes before dropping the owner's UUID reference. A deterministic same-base KASAN reproducer triggered the reported use-after-free on the unpatched tree. With this patch, the same controlled invalidation and teardown race completed without the target KASAN report or another selected kernel diagnostic. A separate all-live run recorded disable events for all 16 unique clients. Test-only synchronization hooks verified that a second disabler waits for unlink after ->net is cleared and that client teardown waits for a temporary UUID reference to drain. NFSv4.2 and NFSv3 read, write, and unmount smoke tests, export-flush and I/O stress, NFSD restarts, network namespace lifecycle tests, and module unloads passed with KASAN and lockdep enabled. A full x86_64 kernel and modules build with GCC 13.3, CONFIG_WERROR=y, NFS_FS=m, NFSD=m, and LOCALIO enabled completed without diagnostics. Module metadata showed that nfs_localio.ko depends only on sunrpc, while nfs.ko depends on lockd, nfs_localio, and sunrpc. A separate NFS_FS=m, NFSD=y build also linked vmlinux successfully. The vulnerability research and validation were conducted by members of the Tobabz team as part of the Best of the Best 15th program. Fixes: 085804110aa1 ("nfs_common: track all open nfsd_files per LOCALIO nfs_client") Assisted-by: LLM Signed-off-by: Jinpyo Lee Signed-off-by: Anna Schumaker --- fs/nfs/client.c | 2 +- fs/nfs_common/nfslocalio.c | 101 +++++++++++++++++++++++++++++++------ include/linux/nfslocalio.h | 12 +++++ 3 files changed, 98 insertions(+), 17 deletions(-) diff --git a/fs/nfs/client.c b/fs/nfs/client.c index 5ebc8bf39dd7..ea99b3c1057a 100644 --- a/fs/nfs/client.c +++ b/fs/nfs/client.c @@ -263,7 +263,7 @@ static void pnfs_init_server(struct nfs_server *server) */ void nfs_free_client(struct nfs_client *clp) { - nfs_localio_disable_client(clp); + nfs_localio_free_client(clp); /* -EIO all pending I/O */ if (!IS_ERR(clp->cl_rpcclient)) diff --git a/fs/nfs_common/nfslocalio.c b/fs/nfs_common/nfslocalio.c index 85aa03a7b020..1423a43dbd6e 100644 --- a/fs/nfs_common/nfslocalio.c +++ b/fs/nfs_common/nfslocalio.c @@ -24,6 +24,26 @@ static DEFINE_SPINLOCK(nfs_uuids_lock); * that is protected by nfs_uuids_lock. */ static LIST_HEAD(nfs_uuids); +/* Namespace registration order, protected by nfs_uuids_lock. */ +static u64 nfs_uuid_seq; + +/* The owner waits for temporary users before freeing its nfs_client. */ +static void nfs_uuid_get(nfs_uuid_t *nfs_uuid) +{ + refcount_inc(&nfs_uuid->refs); +} + +static void nfs_uuid_release(nfs_uuid_t *nfs_uuid) +{ + bool released; + + spin_lock(&nfs_uuid->lock); + released = refcount_dec_and_test(&nfs_uuid->refs); + wake_up_var_locked(&nfs_uuid->refs, &nfs_uuid->lock); + spin_unlock(&nfs_uuid->lock); + if (released) + complete(&nfs_uuid->released); +} /* * Lock ordering: @@ -43,6 +63,8 @@ void nfs_uuid_init(nfs_uuid_t *nfs_uuid) INIT_LIST_HEAD(&nfs_uuid->list); INIT_LIST_HEAD(&nfs_uuid->files); spin_lock_init(&nfs_uuid->lock); + refcount_set(&nfs_uuid->refs, 1); + init_completion(&nfs_uuid->released); nfs_uuid->nfs3_localio_probe_count = 0; } EXPORT_SYMBOL_GPL(nfs_uuid_init); @@ -50,7 +72,12 @@ EXPORT_SYMBOL_GPL(nfs_uuid_init); bool nfs_uuid_begin(nfs_uuid_t *nfs_uuid) { spin_lock(&nfs_uuid->lock); - if (rcu_access_pointer(nfs_uuid->net)) { + /* The caller retains the client while temporary UUID users drain. */ + wait_var_event_spinlock(&nfs_uuid->refs, + refcount_read(&nfs_uuid->refs) <= 1, + &nfs_uuid->lock); + if (rcu_access_pointer(nfs_uuid->net) || nfs_uuid->list_lock || + refcount_read(&nfs_uuid->refs) != 1) { /* This nfs_uuid is already in use */ spin_unlock(&nfs_uuid->lock); return false; @@ -77,7 +104,7 @@ void nfs_uuid_end(nfs_uuid_t *nfs_uuid) { if (!rcu_access_pointer(nfs_uuid->net)) { spin_lock(&nfs_uuid->lock); - if (!rcu_access_pointer(nfs_uuid->net)) { + if (!rcu_access_pointer(nfs_uuid->net) && !nfs_uuid->list_lock) { /* Not local, remove from nfs_uuids */ spin_lock(&nfs_uuids_lock); list_del_init(&nfs_uuid->list); @@ -113,28 +140,37 @@ void nfs_uuid_is_local(const uuid_t *uuid, struct list_head *list, spin_unlock(&nfs_uuids_lock); return; } + /* Drop the lookup lock before taking nfs_uuid->lock. */ + nfs_uuid_get(nfs_uuid); + spin_unlock(&nfs_uuids_lock); + + spin_lock(&nfs_uuid->lock); + spin_lock(&nfs_uuids_lock); + /* nfs_uuid_end() may have withdrawn the probe in the meantime. */ + if (nfs_uuid_lookup_locked(uuid) != nfs_uuid) + goto out_unlock; /* * We don't hold a ref on the net, but instead put * ourselves on @list (nn->local_clients) so the net * pointer can be invalidated. */ - spin_lock(list_lock); /* list_lock is nn->local_clients_lock */ - list_move(&nfs_uuid->list, list); - spin_unlock(list_lock); - - spin_unlock(&nfs_uuids_lock); - /* Once nfs_uuid is parented to @list, avoid global nfs_uuids_lock */ - spin_lock(&nfs_uuid->lock); - __module_get(mod); nfsd_mod = mod; + spin_lock(list_lock); /* list_lock is nn->local_clients_lock */ nfs_uuid->list_lock = list_lock; kref_get(&dom->ref); nfs_uuid->dom = dom; rcu_assign_pointer(nfs_uuid->net, net); + nfs_uuid->seq = ++nfs_uuid_seq; + list_move_tail(&nfs_uuid->list, list); + spin_unlock(list_lock); + +out_unlock: + spin_unlock(&nfs_uuids_lock); spin_unlock(&nfs_uuid->lock); + nfs_uuid_release(nfs_uuid); } EXPORT_SYMBOL_GPL(nfs_uuid_is_local); @@ -154,6 +190,10 @@ static bool nfs_uuid_put(nfs_uuid_t *nfs_uuid) struct nfs_file_localio *nfl; spin_lock(&nfs_uuid->lock); + /* A concurrent disabler can drop the lock while closing files. */ + wait_var_event_spinlock(&nfs_uuid->list_lock, + rcu_access_pointer(nfs_uuid->net) || + !nfs_uuid->list_lock, &nfs_uuid->lock); if (unlikely(!rcu_access_pointer(nfs_uuid->net))) { spin_unlock(&nfs_uuid->lock); return false; @@ -211,6 +251,7 @@ static bool nfs_uuid_put(nfs_uuid_t *nfs_uuid) } module_put(nfsd_mod); + wake_up_var_locked(&nfs_uuid->list_lock, &nfs_uuid->lock); spin_unlock(&nfs_uuid->lock); return true; @@ -223,22 +264,50 @@ void nfs_localio_disable_client(struct nfs_client *clp) } EXPORT_SYMBOL_GPL(nfs_localio_disable_client); +void nfs_localio_free_client(struct nfs_client *clp) +{ + nfs_uuid_t *nfs_uuid = &clp->cl_uuid; + + nfs_uuid_end(nfs_uuid); + nfs_localio_disable_client(clp); + /* Unlink before dropping the owner reference, then drain all users. */ + nfs_uuid_release(nfs_uuid); + wait_for_completion(&nfs_uuid->released); +} +EXPORT_SYMBOL_GPL(nfs_localio_free_client); + void nfs_localio_invalidate_clients(struct list_head *nn_local_clients, spinlock_t *nn_local_clients_lock) { - LIST_HEAD(local_clients); - nfs_uuid_t *nfs_uuid, *tmp; + nfs_uuid_t *nfs_uuid; struct nfs_client *clp; + u64 last_seq; spin_lock(nn_local_clients_lock); - list_splice_init(nn_local_clients, &local_clients); - spin_unlock(nn_local_clients_lock); - list_for_each_entry_safe(nfs_uuid, tmp, &local_clients, list) { - if (WARN_ON(nfs_uuid->list_lock != nn_local_clients_lock)) + if (list_empty(nn_local_clients)) + goto out_unlock; + /* New registrations go to the tail and belong to a later sweep. */ + last_seq = list_last_entry(nn_local_clients, nfs_uuid_t, list)->seq; + for (;;) { + nfs_uuid = list_first_entry_or_null(nn_local_clients, + nfs_uuid_t, list); + /* Use a signed difference to allow the sequence to wrap. */ + if (!nfs_uuid || (s64)(nfs_uuid->seq - last_seq) > 0) break; + if (WARN_ON_ONCE(nfs_uuid->list_lock != nn_local_clients_lock)) + break; + /* Teardown unlinks under this lock before dropping its reference. */ + nfs_uuid_get(nfs_uuid); + spin_unlock(nn_local_clients_lock); + clp = container_of(nfs_uuid, struct nfs_client, cl_uuid); nfs_localio_disable_client(clp); + nfs_uuid_release(nfs_uuid); + cond_resched(); + spin_lock(nn_local_clients_lock); } +out_unlock: + spin_unlock(nn_local_clients_lock); } EXPORT_SYMBOL_GPL(nfs_localio_invalidate_clients); diff --git a/include/linux/nfslocalio.h b/include/linux/nfslocalio.h index 3d91043254e6..1a8cfb045abc 100644 --- a/include/linux/nfslocalio.h +++ b/include/linux/nfslocalio.h @@ -8,8 +8,10 @@ #if IS_ENABLED(CONFIG_NFS_LOCALIO) +#include #include #include +#include #include #include #include @@ -28,6 +30,11 @@ struct nfs_file_localio; typedef struct { uuid_t uuid; unsigned nfs3_localio_probe_count; + /* Owner reference plus temporary references held by nfs_common */ + refcount_t refs; + struct completion released; + /* Namespace registration order, protected by list_lock */ + u64 seq; /* this struct is over a cacheline, avoid bouncing */ spinlock_t ____cacheline_aligned lock; struct list_head list; @@ -46,6 +53,7 @@ void nfs_uuid_is_local(const uuid_t *, struct list_head *, spinlock_t *, void nfs_localio_enable_client(struct nfs_client *clp); void nfs_localio_disable_client(struct nfs_client *clp); +void nfs_localio_free_client(struct nfs_client *clp); void nfs_localio_invalidate_clients(struct list_head *nn_local_clients, spinlock_t *nn_local_clients_lock); @@ -118,6 +126,10 @@ static inline void nfs_localio_disable_client(struct nfs_client *clp) { } +static inline void nfs_localio_free_client(struct nfs_client *clp) +{ +} + #endif /* CONFIG_NFS_LOCALIO */ #endif /* __LINUX_NFSLOCALIO_H */