[PATCH] NFS: defer access-cache entry and group_info free past RCU

From: Cheng Yongkang

Date: Sun Oct 25 2026 - 14:05:06 EST


nfs_access_free_entry() drops the group_info reference synchronously
while the nfs_access_entry itself is only freed after an RCU grace
period. The lockless lookup path (nfs_access_get_cached_rcu() ->
access_cmp()) can still be walking group_info->ngroups/gid[] after the
entry was replaced and unlinked, so put_group_info() can reach
groups_free() -> kvfree() under a reader and free the array it is
reading: a slab-use-after-free read.

put_group_info() may end in kvfree(), so it cannot run from a plain RCU
callback. Defer both the group_info reference drop and the entry free
to process context after a grace period with queue_rcu_work() on
nfsiod_workqueue. The entry accounting (nfs_access_nr_entries) and all
call-site locking are unchanged; the work only drops the reference and
frees the entry.

On a 7.3 KASAN kernel, a userspace NFSv3 server plus eight readers
doing faccessat() with 2048 supplementary gids, and serialized
credential replacements that detach the previous killer's group_info
reference, reproduced:

BUG: KASAN: use-after-free in nfs_access_get_cached+0xb8f/0xf80
Read of size 4 at addr ... by task poc/9552
Call Trace:
nfs_access_get_cached
nfs_do_access
nfs_permission
inode_permission
do_faccessat
Page last allocated by task ...:
__kvmalloc_node_noprof
__se_sys_setgroups
Page last freed by task ...:
nfs_access_free_entry
nfs_access_add_cache
nfs_do_access

The trigger needs CAP_SETGID in the guest for the setgroups-based
credential churn; the faulty read path is the ordinary access(2) /
faccessat(2) path used by any NFS user. A negative control without the
credential detach ran 873 replacement rounds and 2.89M reader
iterations without a report.

With the fix, the same PoC ran 3/3 full 60-second runs (~2,863
replacement rounds and ~16.1M reader iterations total) with no KASAN,
BUG, WARN or panic, and POC_RC=0/umount 0 each time. A regression
covering mount/stat/access(R/W), cached and uncached credentials,
supplementary gids, 20 distinct creds, the cache-size shrinker,
drop_caches and post-eviction accesses passed 19/19 with no kernel
report. checkpatch --strict is clean.

Fixes: 6238aec83f3f ("NFS: don't store 'struct cred *' in struct nfs_access_entry")
Assisted-by: LLM
Signed-off-by: Cheng Yongkang <teel4res@xxxxxxxxx>
---
fs/nfs/dir.c | 21 +++++++++++++++++----
include/linux/nfs_fs.h | 3 ++-
2 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c
--- a/fs/nfs/dir.c
+++ b/fs/nfs/dir.c
@@ -2898,10 +2898,25 @@
module_param(nfs_access_max_cachesize, ulong, 0644);
MODULE_PARM_DESC(nfs_access_max_cachesize, "NFS access maximum total cache length");

-static void nfs_access_free_entry(struct nfs_access_entry *entry)
+static void nfs_access_free_entry_work(struct work_struct *work)
{
+ struct nfs_access_entry *entry =
+ container_of(to_rcu_work(work), struct nfs_access_entry, rcu_work);
+
put_group_info(entry->group_info);
- kfree_rcu(entry, rcu_head);
+ kfree(entry);
+}
+
+static void nfs_access_free_entry(struct nfs_access_entry *entry)
+{
+ /*
+ * The entry can still be reached by RCU readers that dereference
+ * its group_info in access_cmp(). Drop the group_info reference
+ * only after a grace period, in process context because
+ * put_group_info() may end in kvfree().
+ */
+ INIT_RCU_WORK(&entry->rcu_work, nfs_access_free_entry_work);
+ queue_rcu_work(nfsiod_workqueue, &entry->rcu_work);
smp_mb__before_atomic();
atomic_long_dec(&nfs_access_nr_entries);
smp_mb__after_atomic();
diff --git a/include/linux/nfs_fs.h b/include/linux/nfs_fs.h
--- a/include/linux/nfs_fs.h
+++ b/include/linux/nfs_fs.h
@@ -26,6 +26,7 @@
#include <linux/refcount.h>
#include <linux/rwsem.h>
#include <linux/wait.h>
+#include <linux/workqueue.h>

#include <linux/sunrpc/debug.h>
#include <linux/sunrpc/auth.h>
@@ -65,7 +66,7 @@
struct group_info *group_info;
u64 timestamp;
__u32 mask;
- struct rcu_head rcu_head;
+ struct rcu_work rcu_work;
};

struct nfs_lock_context {