Received: from smtp.kernel.org (aws-us-west-2-korg-mail-alma10-1.taild15c8.ts.net [100.103.45.18]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C7EAE3CCFD4; Thu, 17 Sep 2026 18:59:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=100.103.45.18 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789671588; cv=none; b=pvFaWazCJ/ivQLIVlg/lXXP0nOgO5A5WYzVlHGTmBY+dw0ztFgIMS2++rf3k6WklRfkFdsCZuMSgPv2TZEdj8WXCXpZZo9hpGzqwDwhpB+Q7AY9A/2o1tacsFhWeKbgfuQzA9VVXTbWAdc5VB/7IomwjMzDL1jD2rdlQVjTMi6Q= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789671588; c=relaxed/simple; bh=2v4z7QtT395d82Klf0vkuDB0Ey6SiOLGbYhkQColFvc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=YoQFVghr12K8vyxtZSR/vKHtWjtBrYc7zaXFvuMM858CQ6r2h1WCXfw4HqGq5VwiNBm8GIywyPNypvDU5I+v1+A9h4gPRkdBfe6CnIZEXWVjceE90cGyK2aTaPPsqnJgxQxNNhD/AAsPhqQkRaF5bslaSeWh8QmkU/D7eyyNs6Q= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linuxfoundation.org header.i=@linuxfoundation.org header.b=Nf5JEeOt; arc=none smtp.client-ip=100.103.45.18 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linuxfoundation.org header.i=@linuxfoundation.org header.b="Nf5JEeOt" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 2C20E1F000FF; Thu, 17 Sep 2026 18:59:45 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linuxfoundation.org; s=korg; t=1789671586; bh=PPgrFLdD5bh0G7Z2RoVcm2bLijYFbJPQXV1n3Sljk4k=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=Nf5JEeOteS1mzHKJiTZa22XVsVgY5YLk8I5MyoNLJ3Txn5NvvbMuChFY3vJyMsAiB b7oVQkwZULP+CRhX6KiPyDjY8lPvAHNO1Zwy08EipRTGNJ71HOpVFMkolYChca3pHK cFaGInw+3SF7ubeEzr3wy8Mcc8UEccQ3KAHrglDE= From: Greg Kroah-Hartman To: stable@vger.kernel.org Cc: Greg Kroah-Hartman , patches@lists.linux.dev, Chris Mason , Jeff Layton , Chuck Lever , Sasha Levin Subject: [PATCH 6.12 1025/1102] nfsd: dedup nfs4_client_to_reclaim inserts Date: Thu, 17 Sep 2026 16:16:08 +0100 Message-ID: <20260917151606.497344785@linuxfoundation.org> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260917151539.408551884@linuxfoundation.org> References: <20260917151539.408551884@linuxfoundation.org> User-Agent: quilt/0.69 X-stable: review X-Patchwork-Hint: ignore Precedence: bulk X-Mailing-List: patches@lists.linux.dev List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 6.12-stable review patch. If anyone has any objections, please let me know. ------------------ From: Jeff Layton [ Upstream commit 9b57793863f69c608691c98e57c2f9be2db9dfed ] nfs4_client_to_reclaim() unconditionally allocates a new nfs4_client_reclaim, prepends it to reclaim_str_hashtbl[], and bumps reclaim_str_hashtbl_size with no check for an existing entry for the same client name. After a reboot with a populated recovery directory that inflates the counter by one for every client that reclaims: boot: load_recdir() nfs4_client_to_reclaim(name) /* entry #1, size++ */ grace: RECLAIM_COMPLETE __nfsd4_create_reclaim_record_grace() nfs4_client_to_reclaim(name) /* entry #2, size++ */ inc_reclaim_complete() ends the grace period early only when atomic_inc_return(&nn->nr_reclaim_complete) == nn->reclaim_str_hashtbl_size With reclaim_str_hashtbl_size at 2N and nr_reclaim_complete capped at N, the equality never holds and the fast end-of-grace path is dead. The grace period always runs out the full 90-second laundromat timer, and the shadow entry left in the hash table carries a dangling cr_clp for any reader that walks it. Fix nfs4_client_to_reclaim() to look the name up with nfsd4_find_reclaim_client() first and, on a hit, fold the new princhash into the existing record (if it lacks one) and return that record without allocating or touching reclaim_str_hashtbl_size. On kmemdup() failure during the fold-in, return NULL so __cld_pipe_inprogress_downcall() surfaces -EFAULT to nfsdcld, matching the miss-path contract. Add an rw_semaphore (reclaim_str_hashtbl_lock) to struct nfsd_net that serialises all access to reclaim_str_hashtbl[] and reclaim_str_hashtbl_size. Writers (nfs4_client_to_reclaim, nfs4_remove_reclaim_record callers) hold the write side; readers (nfsd4_cld_check*, inc_reclaim_complete, clients_still_reclaiming, nfs4_has_reclaimed_state, nfsd4_check_legacy_client) hold the read side. All call sites are in sleepable context, and none is a hot path, so the rwsem cost is negligible. Reported-by: Chris Mason Fixes: 362063a595be ("nfsd: keep a tally of RECLAIM_COMPLETE operations when using nfsdcld") Cc: stable@vger.kernel.org Assisted-by: kres:claude-opus-4-7 Signed-off-by: Jeff Layton Link: https://patch.msgid.link/20260530-nfsd-fixes-v2-4-f27e8eb4d974@kernel.org Signed-off-by: Chuck Lever [ retained 6.12 boolean fields and crypto-shash API while adding balanced unlocks to older error paths. ] Signed-off-by: Sasha Levin Signed-off-by: Greg Kroah-Hartman --- fs/nfsd/netns.h | 6 ++++ fs/nfsd/nfs4recover.c | 39 ++++++++++++++++++++++++++----- fs/nfsd/nfs4state.c | 61 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 14 deletions(-) --- a/fs/nfsd/netns.h +++ b/fs/nfsd/netns.h @@ -84,6 +84,7 @@ struct nfsd_net { */ struct list_head *reclaim_str_hashtbl; int reclaim_str_hashtbl_size; + struct rw_semaphore reclaim_str_hashtbl_lock; struct list_head *conf_id_hashtbl; struct rb_root conf_name_tree; struct list_head *unconf_id_hashtbl; @@ -96,7 +97,10 @@ struct nfsd_net { * close_lru holds (open) stateowner queue ordered by nfs4_stateowner.so_time * for last close replay. * - * All of the above fields are protected by the client_mutex. + * reclaim_str_hashtbl[], reclaim_str_hashtbl_size are protected by + * reclaim_str_hashtbl_lock. + * + * All of the remaining fields are protected by the client_lock. */ struct list_head client_lru; struct list_head close_lru; --- a/fs/nfsd/nfs4recover.c +++ b/fs/nfsd/nfs4recover.c @@ -355,10 +355,12 @@ __nfsd4_remove_reclaim_record_grace(cons return; } name.len = len; + down_write(&nn->reclaim_str_hashtbl_lock); crp = nfsd4_find_reclaim_client(name, nn); - kfree(name.data); if (crp) nfs4_remove_reclaim_record(crp, nn); + up_write(&nn->reclaim_str_hashtbl_lock); + kfree(name.data); } static void @@ -559,6 +561,7 @@ nfs4_legacy_state_init(struct net *net) for (i = 0; i < CLIENT_HASH_SIZE; i++) INIT_LIST_HEAD(&nn->reclaim_str_hashtbl[i]); nn->reclaim_str_hashtbl_size = 0; + init_rwsem(&nn->reclaim_str_hashtbl_lock); return 0; } @@ -678,13 +681,16 @@ nfsd4_check_legacy_client(struct nfs4_cl goto out_enoent; } name.len = HEXDIR_LEN; + down_read(&nn->reclaim_str_hashtbl_lock); crp = nfsd4_find_reclaim_client(name, nn); - kfree(name.data); if (crp) { set_bit(NFSD4_CLIENT_STABLE, &clp->cl_flags); crp->cr_clp = clp; - return 0; } + up_read(&nn->reclaim_str_hashtbl_lock); + kfree(name.data); + if (crp) + return 0; out_enoent: return -ENOENT; @@ -1285,6 +1291,7 @@ nfsd4_cld_check(struct nfs4_client *clp) return 0; /* look for it in the reclaim hashtable otherwise */ + down_read(&nn->reclaim_str_hashtbl_lock); crp = nfsd4_find_reclaim_client(clp->cl_name, nn); if (crp) goto found; @@ -1296,13 +1303,16 @@ nfsd4_cld_check(struct nfs4_client *clp) struct xdr_netobj name; status = nfs4_make_rec_clidname(dname, &clp->cl_name); - if (status) + if (status) { + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; + } name.data = kmemdup(dname, HEXDIR_LEN, GFP_KERNEL); if (!name.data) { dprintk("%s: failed to allocate memory for name.data!\n", __func__); + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; } name.len = HEXDIR_LEN; @@ -1313,9 +1323,11 @@ nfsd4_cld_check(struct nfs4_client *clp) } #endif + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; found: crp->cr_clp = clp; + up_read(&nn->reclaim_str_hashtbl_lock); return 0; } @@ -1335,6 +1347,7 @@ nfsd4_cld_check_v2(struct nfs4_client *c return 0; /* look for it in the reclaim hashtable otherwise */ + down_read(&nn->reclaim_str_hashtbl_lock); crp = nfsd4_find_reclaim_client(clp->cl_name, nn); if (crp) goto found; @@ -1345,13 +1358,16 @@ nfsd4_cld_check_v2(struct nfs4_client *c char dname[HEXDIR_LEN]; status = nfs4_make_rec_clidname(dname, &clp->cl_name); - if (status) + if (status) { + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; + } name.data = kmemdup(dname, HEXDIR_LEN, GFP_KERNEL); if (!name.data) { dprintk("%s: failed to allocate memory for name.data\n", __func__); + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; } name.len = HEXDIR_LEN; @@ -1362,6 +1378,7 @@ nfsd4_cld_check_v2(struct nfs4_client *c } #endif + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; found: if (crp->cr_princhash.len) { @@ -1369,26 +1386,33 @@ found: principal = clp->cl_cred.cr_raw_principal; else if (clp->cl_cred.cr_principal) principal = clp->cl_cred.cr_principal; - if (principal == NULL) + if (principal == NULL) { + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; + } cksum.len = crypto_shash_digestsize(tfm); cksum.data = kmalloc(cksum.len, GFP_KERNEL); - if (cksum.data == NULL) + if (cksum.data == NULL) { + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; + } status = crypto_shash_tfm_digest(tfm, principal, strlen(principal), cksum.data); if (status) { kfree(cksum.data); + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; } if (memcmp(crp->cr_princhash.data, cksum.data, crp->cr_princhash.len)) { kfree(cksum.data); + up_read(&nn->reclaim_str_hashtbl_lock); return -ENOENT; } kfree(cksum.data); } crp->cr_clp = clp; + up_read(&nn->reclaim_str_hashtbl_lock); return 0; } @@ -1488,6 +1512,7 @@ nfs4_cld_state_init(struct net *net) for (i = 0; i < CLIENT_HASH_SIZE; i++) INIT_LIST_HEAD(&nn->reclaim_str_hashtbl[i]); nn->reclaim_str_hashtbl_size = 0; + init_rwsem(&nn->reclaim_str_hashtbl_lock); nn->track_reclaim_completes = true; atomic_set(&nn->nr_reclaim_complete, 0); --- a/fs/nfsd/nfs4state.c +++ b/fs/nfsd/nfs4state.c @@ -2565,14 +2565,21 @@ static void inc_reclaim_complete(struct if (!nn->track_reclaim_completes) return; - if (!nfsd4_find_reclaim_client(clp->cl_name, nn)) + + down_read(&nn->reclaim_str_hashtbl_lock); + if (!nfsd4_find_reclaim_client(clp->cl_name, nn)) { + up_read(&nn->reclaim_str_hashtbl_lock); return; + } if (atomic_inc_return(&nn->nr_reclaim_complete) == nn->reclaim_str_hashtbl_size) { + up_read(&nn->reclaim_str_hashtbl_lock); printk(KERN_INFO "NFSD: all clients done reclaiming, ending NFSv4 grace period (net %x)\n", clp->net->ns.inum); nfsd4_end_grace(nn); + return; } + up_read(&nn->reclaim_str_hashtbl_lock); } static void expire_client(struct nfs4_client *clp) @@ -6467,10 +6474,15 @@ static bool clients_still_reclaiming(str if (READ_ONCE(nn->grace_end_forced)) return false; - if (nn->track_reclaim_completes && - atomic_read(&nn->nr_reclaim_complete) == - nn->reclaim_str_hashtbl_size) - return false; + if (nn->track_reclaim_completes) { + int size; + + down_read(&nn->reclaim_str_hashtbl_lock); + size = nn->reclaim_str_hashtbl_size; + up_read(&nn->reclaim_str_hashtbl_lock); + if (atomic_read(&nn->nr_reclaim_complete) == size) + return false; + } if (!nn->somebody_reclaimed) return false; nn->somebody_reclaimed = false; @@ -8673,9 +8685,13 @@ bool nfs4_has_reclaimed_state(struct xdr_netobj name, struct nfsd_net *nn) { struct nfs4_client_reclaim *crp; + bool found; + down_read(&nn->reclaim_str_hashtbl_lock); crp = nfsd4_find_reclaim_client(name, nn); - return (crp && crp->cr_clp); + found = (crp && crp->cr_clp); + up_read(&nn->reclaim_str_hashtbl_lock); + return found; } /* @@ -8688,10 +8704,39 @@ nfs4_client_to_reclaim(struct xdr_netobj unsigned int strhashval; struct nfs4_client_reclaim *crp; + down_write(&nn->reclaim_str_hashtbl_lock); + + /* + * A reclaim record for this client name may already exist (for + * example, populated at boot from the recovery directory before + * an in-grace RECLAIM_COMPLETE or an nfsdcld downcall delivers + * the same name). Dedup here so reclaim_str_hashtbl_size stays + * equal to the number of distinct client names; inc_reclaim_complete + * relies on that equality to end the grace period via the fast path. + */ + crp = nfsd4_find_reclaim_client(name, nn); + if (crp) { + if (princhash.len && crp->cr_princhash.len == 0) { + void *pdata = kmemdup(princhash.data, princhash.len, + GFP_KERNEL); + if (pdata) { + crp->cr_princhash.data = pdata; + crp->cr_princhash.len = princhash.len; + } else { + dprintk("%s: failed to allocate memory for princhash.data!\n", + __func__); + crp = NULL; + } + } + up_write(&nn->reclaim_str_hashtbl_lock); + return crp; + } + name.data = kmemdup(name.data, name.len, GFP_KERNEL); if (!name.data) { dprintk("%s: failed to allocate memory for name.data!\n", __func__); + up_write(&nn->reclaim_str_hashtbl_lock); return NULL; } if (princhash.len) { @@ -8700,6 +8745,7 @@ nfs4_client_to_reclaim(struct xdr_netobj dprintk("%s: failed to allocate memory for princhash.data!\n", __func__); kfree(name.data); + up_write(&nn->reclaim_str_hashtbl_lock); return NULL; } } else @@ -8719,6 +8765,7 @@ nfs4_client_to_reclaim(struct xdr_netobj kfree(name.data); kfree(princhash.data); } + up_write(&nn->reclaim_str_hashtbl_lock); return crp; } @@ -8738,6 +8785,7 @@ nfs4_release_reclaim(struct nfsd_net *nn struct nfs4_client_reclaim *crp = NULL; int i; + down_write(&nn->reclaim_str_hashtbl_lock); for (i = 0; i < CLIENT_HASH_SIZE; i++) { while (!list_empty(&nn->reclaim_str_hashtbl[i])) { crp = list_entry(nn->reclaim_str_hashtbl[i].next, @@ -8746,6 +8794,7 @@ nfs4_release_reclaim(struct nfsd_net *nn } } WARN_ON_ONCE(nn->reclaim_str_hashtbl_size); + up_write(&nn->reclaim_str_hashtbl_lock); } /*