[PATCH v1 3/3] nfsd: report per-client NFSv4 state usage through Netlink

From: Prabhakar Pujeri

Date: Tue Aug 25 2026 - 02:55:05 EST


A client can retain substantial server-side state even when its lease
and callback channel look healthy. Monitoring tools need compact
per-client totals to identify which client owns that state before doing
a detailed inspection through the nfsd filesystem.

Extend client-get replies with separate counts for sessions and open,
lock, delegation, and layout stateid records. A lock stateid represents
a lock-owner/file pair and can cover multiple byte-range locks, so
document that these are record counts rather than protocol operation or
byte-range counts.

Walk both the session list and stateid IDR under cl_lock. free_client()
already detaches its session list under that lock, so a client pinned by
the dump cannot race this traversal with session teardown.

Signed-off-by: Prabhakar Pujeri <prabhakar.pujeri@xxxxxxxx>
---
.../admin-guide/nfs/nfsd-admin-interfaces.rst | 4 ++
Documentation/netlink/specs/nfsd.yaml | 27 ++++++++++++-
fs/nfsd/nfs4state.c | 40 ++++++++++++++++++-
include/uapi/linux/nfsd_netlink.h | 5 +++
4 files changed, 73 insertions(+), 3 deletions(-)

diff --git a/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst b/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst
index bd7a8f71592c..076b20f4bf6a 100644
--- a/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst
+++ b/Documentation/admin-guide/nfs/nfsd-admin-interfaces.rst
@@ -41,6 +41,10 @@ by its server-generated client ID and transport address, then reports its
minor version, client and callback states, signed lease time remaining, and
whether an NFSv4.1 or later client sent RECLAIM_COMPLETE.

+The dump also reports separate counts for sessions and open, lock, delegation,
+and layout stateids. A lock stateid represents state for one lock owner and
+file, not necessarily one byte-range lock.
+
The dump is a best-effort snapshot; clients can change between messages.

The existing ``/proc/fs/nfsd/clients/`` files remain available for inspection.
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index 5df329ebe0b1..1707de076d43 100644
--- a/Documentation/netlink/specs/nfsd.yaml
+++ b/Documentation/netlink/specs/nfsd.yaml
@@ -480,6 +480,26 @@ attribute-sets:
type: u32
enum: callback-state
doc: Health of the client's callback channel.
+ -
+ name: sessions
+ type: u32
+ doc: Number of NFSv4.1 or later sessions owned by the client.
+ -
+ name: open-stateids
+ type: u32
+ doc: Number of open stateid records owned by the client.
+ -
+ name: lock-stateids
+ type: u32
+ doc: Number of lock-owner/file stateid records, not byte-range locks.
+ -
+ name: delegation-stateids
+ type: u32
+ doc: Number of delegation stateid records owned by the client.
+ -
+ name: layout-stateids
+ type: u32
+ doc: Number of pNFS layout stateid records owned by the client.

operations:
list:
@@ -695,7 +715,7 @@ operations:
- proc4cb-ops
-
name: client-get
- doc: dump NFSv4 clients
+ doc: dump NFSv4 clients and their current state usage
attribute-set: client
flags: [admin-perm]
dump:
@@ -708,6 +728,11 @@ operations:
- lease-remaining
- reclaim-complete
- callback-state
+ - sessions
+ - open-stateids
+ - lock-stateids
+ - delegation-stateids
+ - layout-stateids

mcast-groups:
list:
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 9add35dfb58b..f023cfff818d 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -2822,7 +2822,7 @@ free_client(struct nfs4_client *clp)
{
LIST_HEAD(reaplist);

- /* client_info_show() walks cl_sessions under cl_lock */
+ /* Client reporting walks cl_sessions under cl_lock. */
spin_lock(&clp->cl_lock);
list_splice_init(&clp->cl_sessions, &reaplist);
spin_unlock(&clp->cl_lock);
@@ -3246,6 +3246,11 @@ struct nfsd4_nl_client {
u32 minor_version;
u32 state;
u32 callback_state;
+ u32 sessions;
+ u32 open_stateids;
+ u32 lock_stateids;
+ u32 delegation_stateids;
+ u32 layout_stateids;
bool reclaim_complete;
};

@@ -3310,6 +3315,8 @@ static void nfsd4_nl_client_snapshot(struct nfsd_net *nn,
struct nfs4_client *clp,
struct nfsd4_nl_client *client)
{
+ struct nfs4_stid *stid;
+ unsigned long id, tmp;
unsigned int state;
time64_t last_renew;
bool confirmed;
@@ -3332,6 +3339,26 @@ static void nfsd4_nl_client_snapshot(struct nfsd_net *nn,
nfsd4_nl_callback_state(READ_ONCE(clp->cl_cb_state));
client->reclaim_complete =
test_bit(NFSD4_CLIENT_RECLAIM_COMPLETE, &clp->cl_flags);
+
+ spin_lock(&clp->cl_lock);
+ client->sessions = list_count_nodes(&clp->cl_sessions);
+ idr_for_each_entry_ul(&clp->cl_stateids, stid, tmp, id) {
+ switch (stid->sc_type) {
+ case SC_TYPE_OPEN:
+ client->open_stateids++;
+ break;
+ case SC_TYPE_LOCK:
+ client->lock_stateids++;
+ break;
+ case SC_TYPE_DELEG:
+ client->delegation_stateids++;
+ break;
+ case SC_TYPE_LAYOUT:
+ client->layout_stateids++;
+ break;
+ }
+ }
+ spin_unlock(&clp->cl_lock);
}

static int nfsd4_nl_client_compose_msg(struct sk_buff *skb,
@@ -3359,7 +3386,16 @@ static int nfsd4_nl_client_compose_msg(struct sk_buff *skb,
(client->reclaim_complete &&
nla_put_flag(skb, NFSD_A_CLIENT_RECLAIM_COMPLETE)) ||
nla_put_u32(skb, NFSD_A_CLIENT_CALLBACK_STATE,
- client->callback_state))
+ client->callback_state) ||
+ nla_put_u32(skb, NFSD_A_CLIENT_SESSIONS, client->sessions) ||
+ nla_put_u32(skb, NFSD_A_CLIENT_OPEN_STATEIDS,
+ client->open_stateids) ||
+ nla_put_u32(skb, NFSD_A_CLIENT_LOCK_STATEIDS,
+ client->lock_stateids) ||
+ nla_put_u32(skb, NFSD_A_CLIENT_DELEGATION_STATEIDS,
+ client->delegation_stateids) ||
+ nla_put_u32(skb, NFSD_A_CLIENT_LAYOUT_STATEIDS,
+ client->layout_stateids))
goto err_cancel;

genlmsg_end(skb, hdr);
diff --git a/include/uapi/linux/nfsd_netlink.h b/include/uapi/linux/nfsd_netlink.h
index ae495382092d..49c9531a98b7 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -290,6 +290,11 @@ enum {
NFSD_A_CLIENT_LEASE_REMAINING,
NFSD_A_CLIENT_RECLAIM_COMPLETE,
NFSD_A_CLIENT_CALLBACK_STATE,
+ NFSD_A_CLIENT_SESSIONS,
+ NFSD_A_CLIENT_OPEN_STATEIDS,
+ NFSD_A_CLIENT_LOCK_STATEIDS,
+ NFSD_A_CLIENT_DELEGATION_STATEIDS,
+ NFSD_A_CLIENT_LAYOUT_STATEIDS,

__NFSD_A_CLIENT_MAX,
NFSD_A_CLIENT_MAX = (__NFSD_A_CLIENT_MAX - 1)
--
2.54.0