* [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount
@ 2026-04-08 12:29 Chuck Lever
2026-04-08 12:29 ` [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation Chuck Lever
` (9 more replies)
0 siblings, 10 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever, Dai Ngo
When an NFS server exports a filesystem and clients hold NFSv4
state (opens, locks, delegations), unmounting the underlying
filesystem fails with EBUSY. The /proc/fs/nfsd/unlock_ip and
/proc/fs/nfsd/unlock_fs procfs interfaces handle this, but have
no netlink equivalents, and unlock_fs operates at whole-superblock
granularity.
This series adds three new NFSD netlink commands, each with its own
attribute set:
- NFSD_CMD_UNLOCK_IP releases NLM locks held by a client IP
address. Netlink equivalent of write_unlock_ip.
- NFSD_CMD_UNLOCK_FILESYSTEM revokes all NFS state on a
superblock. Netlink equivalent of write_unlock_fs.
- NFSD_CMD_UNLOCK_EXPORT revokes NFSv4 state acquired through
exports of a specific path, regardless of client.
UNLOCK_FILESYSTEM and UNLOCK_EXPORT serve different intents.
UNLOCK_FILESYSTEM means "unmounting /data, release everything
on this superblock." UNLOCK_EXPORT means "no clients remain for
/data/projectA, release only the state acquired through exports
of that path." Userspace (exportfs -u) sends UNLOCK_EXPORT after
removing the last client for a given path, enabling the underlying
filesystem to be unmounted.
The path-only design for UNLOCK_EXPORT avoids the auth_domain
naming complexity (use_ipaddr vs hostname-based domains) by not
requiring the caller to identify a specific client. Since this
mechanism is to be used to enable umount, this seemed like a
reasonable compromise.
---
Changes since v7:
- Rebase on Jeff's mountd netlink patches
- Fix pre-existing state revocation bugs
Changes since v6:
- Send the complete series (v5 was missing patches 6 and 7)
Changes since v5:
- Rename state_lock => nn->deleg_lock
Changes since v4:
- 1/9 has been queued in nfsd-testing
- Split single NFSD_CMD_UNLOCK into three separate commands
- UNLOCK_EXPORT takes path only, no client attribute to avoid
auth_domain naming complexity with use_ipaddr
Changes since v3:
- All VFS changes replaced with new netlink "unlock" operation
Changes since v2:
- Replace fs_pin with an SRCU umount notifier chain in VFS
- Merge the pending COPY cancellation patch
- Replace xa_cmpxchg() with xa_insert()
- Use cancel_work_sync() instead of flush_workqueue()
- Remove rcu_barrier()
- Correct misleading claims in kdoc comments and commit messages
Changes since v1:
- Explain why drop_client() is being renamed
- Finish implementing revocation on umount
- Rename pin_insert_group
- Clarified log output and code comments
- Hold nfsd_mutex while closing nfsd_files
---
Chuck Lever (9):
NFSD: Fix infinite loop in layout state revocation
NFSD: Handle layout stid in nfsd4_drop_revoked_stid()
NFSD: Extract revoke_one_stid() utility function
NFSD: Add NFSD_CMD_UNLOCK_IP netlink command
NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM netlink command
NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid()
NFSD: Track svc_export in nfs4_stid
NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
NFSD: Close cached file handles when revoking export state
Documentation/netlink/specs/nfsd.yaml | 61 +++++++++
fs/nfsd/filecache.c | 46 +++++++
fs/nfsd/filecache.h | 1 +
fs/nfsd/netlink.c | 36 +++++
fs/nfsd/netlink.h | 3 +
fs/nfsd/nfs4layouts.c | 2 +
fs/nfsd/nfs4state.c | 240 ++++++++++++++++++++++++----------
fs/nfsd/nfsctl.c | 126 +++++++++++++++++-
fs/nfsd/state.h | 6 +
fs/nfsd/trace.h | 32 ++++-
include/uapi/linux/nfsd_netlink.h | 24 ++++
11 files changed, 498 insertions(+), 79 deletions(-)
---
base-commit: b495a392b2748dca31d2a4b404632c6f907aa136
change-id: 20260318-umount-kills-nfsv4-state-138218f2f4e0
Best regards,
--
Chuck Lever
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 13:58 ` Jeff Layton
2026-04-08 12:29 ` [PATCH v8 2/9] NFSD: Handle layout stid in nfsd4_drop_revoked_stid() Chuck Lever
` (8 subsequent siblings)
9 siblings, 1 reply; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever, Dai Ngo
From: Chuck Lever <chuck.lever@oracle.com>
find_one_sb_stid() skips stids whose sc_status is non-zero, but the
SC_TYPE_LAYOUT case in nfsd4_revoke_states() never sets sc_status
before calling nfsd4_close_layout(). The retry loop therefore finds
the same layout stid on every iteration, hanging the revoker
indefinitely.
Fixes: 1e33e1414bec ("nfsd: allow layout state to be admin-revoked.")
Reported-by: Dai Ngo <dai.ngo@oracle.com>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
fs/nfsd/nfs4state.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 07df4511ba23..c6cb67cf02ad 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1872,6 +1872,13 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
break;
case SC_TYPE_LAYOUT:
ls = layoutstateid(stid);
+ spin_lock(&clp->cl_lock);
+ if (stid->sc_status == 0) {
+ stid->sc_status |=
+ SC_STATUS_ADMIN_REVOKED;
+ atomic_inc(&clp->cl_admin_revoked);
+ }
+ spin_unlock(&clp->cl_lock);
nfsd4_close_layout(ls);
break;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 2/9] NFSD: Handle layout stid in nfsd4_drop_revoked_stid()
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
2026-04-08 12:29 ` [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 13:59 ` Jeff Layton
2026-04-08 12:29 ` [PATCH v8 3/9] NFSD: Extract revoke_one_stid() utility function Chuck Lever
` (7 subsequent siblings)
9 siblings, 1 reply; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
nfsd4_drop_revoked_stid() has no SC_TYPE_LAYOUT case, so when a
client sends FREE_STATEID for an admin-revoked layout stid, the
default branch releases cl_lock and returns without unhashing or
releasing the stid. The stid remains in the IDR and on the
per-client list until the client is destroyed.
Remove the layout stid from the per-client list and call
nfs4_put_stid() to drop the creation reference. When the
refcount reaches zero, nfsd4_free_layout_stateid() handles the
remaining cleanup: cancelling the fence worker, removing from
the per-file list, and freeing the slab object.
Fixes: 1e33e1414bec ("nfsd: allow layout state to be admin-revoked.")
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
fs/nfsd/nfs4state.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index c6cb67cf02ad..ae5e1a20197c 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -5070,6 +5070,7 @@ static void nfsd4_drop_revoked_stid(struct nfs4_stid *s)
{
struct nfs4_client *cl = s->sc_client;
LIST_HEAD(reaplist);
+ struct nfs4_layout_stateid *ls;
struct nfs4_ol_stateid *stp;
struct nfs4_delegation *dp;
bool unhashed;
@@ -5095,6 +5096,12 @@ static void nfsd4_drop_revoked_stid(struct nfs4_stid *s)
spin_unlock(&cl->cl_lock);
nfs4_put_stid(s);
break;
+ case SC_TYPE_LAYOUT:
+ ls = layoutstateid(s);
+ list_del_init(&ls->ls_perclnt);
+ spin_unlock(&cl->cl_lock);
+ nfs4_put_stid(s);
+ break;
default:
spin_unlock(&cl->cl_lock);
}
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 3/9] NFSD: Extract revoke_one_stid() utility function
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
2026-04-08 12:29 ` [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation Chuck Lever
2026-04-08 12:29 ` [PATCH v8 2/9] NFSD: Handle layout stid in nfsd4_drop_revoked_stid() Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 12:29 ` [PATCH v8 4/9] NFSD: Add NFSD_CMD_UNLOCK_IP netlink command Chuck Lever
` (6 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
The per-stateid revocation logic in nfsd4_revoke_states() handles
four stateid types in a deeply nested switch. Extract two helpers:
revoke_ol_stid() performs admin-revocation of an open or lock
stateid with st_mutex already held: marks the stateid as
SC_STATUS_ADMIN_REVOKED, closes POSIX locks for lock stateids,
and releases file access.
revoke_one_stid() dispatches by sc_type, acquires st_mutex with
the appropriate lockdep class for open and lock stateids, and
handles delegation unhash and layout close inline.
No functional change. Preparation for adding export-scoped state
revocation which reuses revoke_one_stid().
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
fs/nfsd/nfs4state.c | 151 ++++++++++++++++++++++++++--------------------------
1 file changed, 75 insertions(+), 76 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index ae5e1a20197c..b095b1beaac0 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1775,6 +1775,80 @@ static struct nfs4_stid *find_one_sb_stid(struct nfs4_client *clp,
return stid;
}
+static void revoke_ol_stid(struct nfs4_client *clp,
+ struct nfs4_ol_stateid *stp)
+{
+ struct nfs4_stid *stid = &stp->st_stid;
+
+ lockdep_assert_held(&stp->st_mutex);
+ spin_lock(&clp->cl_lock);
+ if (stid->sc_status == 0) {
+ stid->sc_status |= SC_STATUS_ADMIN_REVOKED;
+ atomic_inc(&clp->cl_admin_revoked);
+ spin_unlock(&clp->cl_lock);
+ if (stid->sc_type == SC_TYPE_LOCK) {
+ struct nfs4_lockowner *lo =
+ lockowner(stp->st_stateowner);
+ struct nfsd_file *nf;
+
+ nf = find_any_file(stp->st_stid.sc_file);
+ if (nf) {
+ get_file(nf->nf_file);
+ filp_close(nf->nf_file, (fl_owner_t)lo);
+ nfsd_file_put(nf);
+ }
+ }
+ release_all_access(stp);
+ } else
+ spin_unlock(&clp->cl_lock);
+}
+
+static void revoke_one_stid(struct nfsd_net *nn, struct nfs4_client *clp,
+ struct nfs4_stid *stid)
+{
+ struct nfs4_ol_stateid *stp;
+ struct nfs4_delegation *dp;
+
+ switch (stid->sc_type) {
+ case SC_TYPE_OPEN:
+ stp = openlockstateid(stid);
+ mutex_lock_nested(&stp->st_mutex, OPEN_STATEID_MUTEX);
+ revoke_ol_stid(clp, stp);
+ mutex_unlock(&stp->st_mutex);
+ break;
+ case SC_TYPE_LOCK:
+ stp = openlockstateid(stid);
+ mutex_lock_nested(&stp->st_mutex, LOCK_STATEID_MUTEX);
+ revoke_ol_stid(clp, stp);
+ mutex_unlock(&stp->st_mutex);
+ break;
+ case SC_TYPE_DELEG:
+ /*
+ * Extra reference guards against concurrent FREE_STATEID.
+ */
+ refcount_inc(&stid->sc_count);
+ dp = delegstateid(stid);
+ spin_lock(&nn->deleg_lock);
+ if (!unhash_delegation_locked(dp, SC_STATUS_ADMIN_REVOKED))
+ dp = NULL;
+ spin_unlock(&nn->deleg_lock);
+ if (dp)
+ revoke_delegation(dp);
+ else
+ nfs4_put_stid(stid);
+ break;
+ case SC_TYPE_LAYOUT:
+ spin_lock(&clp->cl_lock);
+ if (stid->sc_status == 0) {
+ stid->sc_status |= SC_STATUS_ADMIN_REVOKED;
+ atomic_inc(&clp->cl_admin_revoked);
+ }
+ spin_unlock(&clp->cl_lock);
+ nfsd4_close_layout(layoutstateid(stid));
+ break;
+ }
+}
+
/**
* nfsd4_revoke_states - revoke all nfsv4 states associated with given filesystem
* @nn: used to identify instance of nfsd (there is one per net namespace)
@@ -1805,83 +1879,8 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
struct nfs4_stid *stid = find_one_sb_stid(clp, sb,
sc_types);
if (stid) {
- struct nfs4_ol_stateid *stp;
- struct nfs4_delegation *dp;
- struct nfs4_layout_stateid *ls;
-
spin_unlock(&nn->client_lock);
- switch (stid->sc_type) {
- case SC_TYPE_OPEN:
- stp = openlockstateid(stid);
- mutex_lock_nested(&stp->st_mutex,
- OPEN_STATEID_MUTEX);
-
- spin_lock(&clp->cl_lock);
- if (stid->sc_status == 0) {
- stid->sc_status |=
- SC_STATUS_ADMIN_REVOKED;
- atomic_inc(&clp->cl_admin_revoked);
- spin_unlock(&clp->cl_lock);
- release_all_access(stp);
- } else
- spin_unlock(&clp->cl_lock);
- mutex_unlock(&stp->st_mutex);
- break;
- case SC_TYPE_LOCK:
- stp = openlockstateid(stid);
- mutex_lock_nested(&stp->st_mutex,
- LOCK_STATEID_MUTEX);
- spin_lock(&clp->cl_lock);
- if (stid->sc_status == 0) {
- struct nfs4_lockowner *lo =
- lockowner(stp->st_stateowner);
- struct nfsd_file *nf;
-
- stid->sc_status |=
- SC_STATUS_ADMIN_REVOKED;
- atomic_inc(&clp->cl_admin_revoked);
- spin_unlock(&clp->cl_lock);
- nf = find_any_file(stp->st_stid.sc_file);
- if (nf) {
- get_file(nf->nf_file);
- filp_close(nf->nf_file,
- (fl_owner_t)lo);
- nfsd_file_put(nf);
- }
- release_all_access(stp);
- } else
- spin_unlock(&clp->cl_lock);
- mutex_unlock(&stp->st_mutex);
- break;
- case SC_TYPE_DELEG:
- /* Extra reference guards against concurrent
- * FREE_STATEID; revoke_delegation() consumes
- * it, otherwise release it directly.
- */
- refcount_inc(&stid->sc_count);
- dp = delegstateid(stid);
- spin_lock(&nn->deleg_lock);
- if (!unhash_delegation_locked(
- dp, SC_STATUS_ADMIN_REVOKED))
- dp = NULL;
- spin_unlock(&nn->deleg_lock);
- if (dp)
- revoke_delegation(dp);
- else
- nfs4_put_stid(stid);
- break;
- case SC_TYPE_LAYOUT:
- ls = layoutstateid(stid);
- spin_lock(&clp->cl_lock);
- if (stid->sc_status == 0) {
- stid->sc_status |=
- SC_STATUS_ADMIN_REVOKED;
- atomic_inc(&clp->cl_admin_revoked);
- }
- spin_unlock(&clp->cl_lock);
- nfsd4_close_layout(ls);
- break;
- }
+ revoke_one_stid(nn, clp, stid);
nfs4_put_stid(stid);
spin_lock(&nn->client_lock);
if (clp->cl_minorversion == 0)
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 4/9] NFSD: Add NFSD_CMD_UNLOCK_IP netlink command
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (2 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 3/9] NFSD: Extract revoke_one_stid() utility function Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 12:29 ` [PATCH v8 5/9] NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM " Chuck Lever
` (5 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
The existing write_unlock_ip procfs interface releases NLM file
locks held by a specific client IP address, but procfs provides
no structured way to extend that operation to other scopes such as
revoking NFSv4 state.
Add NFSD_CMD_UNLOCK_IP as a dedicated netlink command for
releasing NLM locks by client address. The command accepts a
binary sockaddr_in or sockaddr_in6 in its address attribute.
The handler validates the address family and length, then calls
nlmsvc_unlock_all_by_ip() to release matching NLM locks. Because
lockd is a single global instance, that call operates across
all network namespaces regardless of which namespace the caller
inhabits.
A separate netlink command for filesystem-scoped unlock is added in
a subsequent commit.
The nfsd_ctl_unlock_ip tracepoint is updated from string-based
address logging to __sockaddr, which stores the binary sockaddr
and formats it with %pISpc. This affects both the new netlink path
and the existing procfs write_unlock_ip path, giving consistent
structured output in both cases.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
Documentation/netlink/specs/nfsd.yaml | 18 ++++++++++++++++
fs/nfsd/netlink.c | 12 +++++++++++
fs/nfsd/netlink.h | 1 +
fs/nfsd/nfsctl.c | 40 ++++++++++++++++++++++++++++++++++-
fs/nfsd/trace.h | 13 ++++++------
include/uapi/linux/nfsd_netlink.h | 8 +++++++
6 files changed, 85 insertions(+), 7 deletions(-)
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index 40eca7c15680..2c4883cfd50a 100644
--- a/Documentation/netlink/specs/nfsd.yaml
+++ b/Documentation/netlink/specs/nfsd.yaml
@@ -301,6 +301,15 @@ attribute-sets:
type: u32
enum: cache-type
enum-as-flags: true
+ -
+ name: unlock-ip
+ attributes:
+ -
+ name: address
+ type: binary
+ doc: struct sockaddr_in or struct sockaddr_in6.
+ checks:
+ min-len: 16
operations:
list:
@@ -455,6 +464,15 @@ operations:
request:
attributes:
- mask
+ -
+ name: unlock-ip
+ doc: release NLM locks held by an IP address
+ attribute-set: unlock-ip
+ flags: [admin-perm]
+ do:
+ request:
+ attributes:
+ - address
mcast-groups:
list:
diff --git a/fs/nfsd/netlink.c b/fs/nfsd/netlink.c
index 30c4f8be3df9..96d326095768 100644
--- a/fs/nfsd/netlink.c
+++ b/fs/nfsd/netlink.c
@@ -113,6 +113,11 @@ static const struct nla_policy nfsd_cache_flush_nl_policy[NFSD_A_CACHE_FLUSH_MAS
[NFSD_A_CACHE_FLUSH_MASK] = NLA_POLICY_MASK(NLA_U32, 0x3),
};
+/* NFSD_CMD_UNLOCK_IP - do */
+static const struct nla_policy nfsd_unlock_ip_nl_policy[NFSD_A_UNLOCK_IP_ADDRESS + 1] = {
+ [NFSD_A_UNLOCK_IP_ADDRESS] = NLA_POLICY_MIN_LEN(16),
+};
+
/* Ops table for nfsd */
static const struct genl_split_ops nfsd_nl_ops[] = {
{
@@ -203,6 +208,13 @@ static const struct genl_split_ops nfsd_nl_ops[] = {
.maxattr = NFSD_A_CACHE_FLUSH_MASK,
.flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
},
+ {
+ .cmd = NFSD_CMD_UNLOCK_IP,
+ .doit = nfsd_nl_unlock_ip_doit,
+ .policy = nfsd_unlock_ip_nl_policy,
+ .maxattr = NFSD_A_UNLOCK_IP_ADDRESS,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
};
static const struct genl_multicast_group nfsd_nl_mcgrps[] = {
diff --git a/fs/nfsd/netlink.h b/fs/nfsd/netlink.h
index cc89732ed71b..88edbbc68453 100644
--- a/fs/nfsd/netlink.h
+++ b/fs/nfsd/netlink.h
@@ -39,6 +39,7 @@ int nfsd_nl_expkey_get_reqs_dumpit(struct sk_buff *skb,
struct netlink_callback *cb);
int nfsd_nl_expkey_set_reqs_doit(struct sk_buff *skb, struct genl_info *info);
int nfsd_nl_cache_flush_doit(struct sk_buff *skb, struct genl_info *info);
+int nfsd_nl_unlock_ip_doit(struct sk_buff *skb, struct genl_info *info);
enum {
NFSD_NLGRP_NONE,
diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index 3241bcfc2c6f..c5ce4af28287 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -247,7 +247,7 @@ static ssize_t write_unlock_ip(struct file *file, char *buf, size_t size)
if (rpc_pton(net, fo_path, size, sap, salen) == 0)
return -EINVAL;
- trace_nfsd_ctl_unlock_ip(net, buf);
+ trace_nfsd_ctl_unlock_ip(net, sap, svc_addr_len(sap));
return nlmsvc_unlock_all_by_ip(sap);
}
@@ -2279,6 +2279,44 @@ int nfsd_cache_notify(struct cache_detail *cd, struct cache_head *h, u32 cache_t
NFSD_NLGRP_EXPORTD, GFP_KERNEL);
}
+/**
+ * nfsd_nl_unlock_ip_doit - release NLM locks held by an IP address
+ * @skb: reply buffer
+ * @info: netlink metadata and command arguments
+ *
+ * Return: 0 on success or a negative errno.
+ */
+int nfsd_nl_unlock_ip_doit(struct sk_buff *skb, struct genl_info *info)
+{
+ struct sockaddr *sap;
+
+ if (GENL_REQ_ATTR_CHECK(info, NFSD_A_UNLOCK_IP_ADDRESS))
+ return -EINVAL;
+ sap = nla_data(info->attrs[NFSD_A_UNLOCK_IP_ADDRESS]);
+ switch (sap->sa_family) {
+ case AF_INET:
+ if (nla_len(info->attrs[NFSD_A_UNLOCK_IP_ADDRESS]) <
+ sizeof(struct sockaddr_in))
+ return -EINVAL;
+ break;
+ case AF_INET6:
+ if (nla_len(info->attrs[NFSD_A_UNLOCK_IP_ADDRESS]) <
+ sizeof(struct sockaddr_in6))
+ return -EINVAL;
+ break;
+ default:
+ return -EAFNOSUPPORT;
+ }
+ /*
+ * nlmsvc_unlock_all_by_ip() releases matching locks
+ * across all network namespaces because lockd operates
+ * a single global instance.
+ */
+ trace_nfsd_ctl_unlock_ip(genl_info_net(info), sap,
+ svc_addr_len(sap));
+ return nlmsvc_unlock_all_by_ip(sap);
+}
+
/**
* nfsd_net_init - Prepare the nfsd_net portion of a new net namespace
* @net: a freshly-created network namespace
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index 5ad38f50836d..976815f6f30f 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -1985,19 +1985,20 @@ TRACE_EVENT(nfsd_cb_recall_any_done,
TRACE_EVENT(nfsd_ctl_unlock_ip,
TP_PROTO(
const struct net *net,
- const char *address
+ const struct sockaddr *addr,
+ const unsigned int addrlen
),
- TP_ARGS(net, address),
+ TP_ARGS(net, addr, addrlen),
TP_STRUCT__entry(
__field(unsigned int, netns_ino)
- __string(address, address)
+ __sockaddr(addr, addrlen)
),
TP_fast_assign(
__entry->netns_ino = net->ns.inum;
- __assign_str(address);
+ __assign_sockaddr(addr, addr, addrlen);
),
- TP_printk("address=%s",
- __get_str(address)
+ TP_printk("addr=%pISpc",
+ __get_sockaddr(addr)
)
);
diff --git a/include/uapi/linux/nfsd_netlink.h b/include/uapi/linux/nfsd_netlink.h
index 060c43675599..90ef1e686769 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -204,6 +204,13 @@ enum {
NFSD_A_CACHE_FLUSH_MAX = (__NFSD_A_CACHE_FLUSH_MAX - 1)
};
+enum {
+ NFSD_A_UNLOCK_IP_ADDRESS = 1,
+
+ __NFSD_A_UNLOCK_IP_MAX,
+ NFSD_A_UNLOCK_IP_MAX = (__NFSD_A_UNLOCK_IP_MAX - 1)
+};
+
enum {
NFSD_CMD_RPC_STATUS_GET = 1,
NFSD_CMD_THREADS_SET,
@@ -220,6 +227,7 @@ enum {
NFSD_CMD_EXPKEY_GET_REQS,
NFSD_CMD_EXPKEY_SET_REQS,
NFSD_CMD_CACHE_FLUSH,
+ NFSD_CMD_UNLOCK_IP,
__NFSD_CMD_MAX,
NFSD_CMD_MAX = (__NFSD_CMD_MAX - 1)
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 5/9] NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM netlink command
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (3 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 4/9] NFSD: Add NFSD_CMD_UNLOCK_IP netlink command Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 12:29 ` [PATCH v8 6/9] NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid() Chuck Lever
` (4 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
Add NFSD_CMD_UNLOCK_FILESYSTEM as a dedicated netlink command for
revoking NFS state under a filesystem path, providing a netlink
equivalent of /proc/fs/nfsd/unlock_fs.
The command requires a "path" string attribute containing the
filesystem path whose state should be released. The handler
resolves the path to its superblock, then cancels async copies,
releases NLM locks, and revokes NFSv4 state on that superblock.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
Documentation/netlink/specs/nfsd.yaml | 16 ++++++++++++++
fs/nfsd/netlink.c | 12 +++++++++++
fs/nfsd/netlink.h | 1 +
fs/nfsd/nfsctl.c | 40 +++++++++++++++++++++++++++++++++++
include/uapi/linux/nfsd_netlink.h | 8 +++++++
5 files changed, 77 insertions(+)
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index 2c4883cfd50a..f3b21d4ba660 100644
--- a/Documentation/netlink/specs/nfsd.yaml
+++ b/Documentation/netlink/specs/nfsd.yaml
@@ -310,6 +310,13 @@ attribute-sets:
doc: struct sockaddr_in or struct sockaddr_in6.
checks:
min-len: 16
+ -
+ name: unlock-filesystem
+ attributes:
+ -
+ name: path
+ type: string
+ doc: Filesystem path whose state should be released.
operations:
list:
@@ -473,6 +480,15 @@ operations:
request:
attributes:
- address
+ -
+ name: unlock-filesystem
+ doc: revoke NFS state under a filesystem path
+ attribute-set: unlock-filesystem
+ flags: [admin-perm]
+ do:
+ request:
+ attributes:
+ - path
mcast-groups:
list:
diff --git a/fs/nfsd/netlink.c b/fs/nfsd/netlink.c
index 96d326095768..dae7ad560bd8 100644
--- a/fs/nfsd/netlink.c
+++ b/fs/nfsd/netlink.c
@@ -118,6 +118,11 @@ static const struct nla_policy nfsd_unlock_ip_nl_policy[NFSD_A_UNLOCK_IP_ADDRESS
[NFSD_A_UNLOCK_IP_ADDRESS] = NLA_POLICY_MIN_LEN(16),
};
+/* NFSD_CMD_UNLOCK_FILESYSTEM - do */
+static const struct nla_policy nfsd_unlock_filesystem_nl_policy[NFSD_A_UNLOCK_FILESYSTEM_PATH + 1] = {
+ [NFSD_A_UNLOCK_FILESYSTEM_PATH] = { .type = NLA_NUL_STRING, },
+};
+
/* Ops table for nfsd */
static const struct genl_split_ops nfsd_nl_ops[] = {
{
@@ -215,6 +220,13 @@ static const struct genl_split_ops nfsd_nl_ops[] = {
.maxattr = NFSD_A_UNLOCK_IP_ADDRESS,
.flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
},
+ {
+ .cmd = NFSD_CMD_UNLOCK_FILESYSTEM,
+ .doit = nfsd_nl_unlock_filesystem_doit,
+ .policy = nfsd_unlock_filesystem_nl_policy,
+ .maxattr = NFSD_A_UNLOCK_FILESYSTEM_PATH,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
};
static const struct genl_multicast_group nfsd_nl_mcgrps[] = {
diff --git a/fs/nfsd/netlink.h b/fs/nfsd/netlink.h
index 88edbbc68453..29bd5468d401 100644
--- a/fs/nfsd/netlink.h
+++ b/fs/nfsd/netlink.h
@@ -40,6 +40,7 @@ int nfsd_nl_expkey_get_reqs_dumpit(struct sk_buff *skb,
int nfsd_nl_expkey_set_reqs_doit(struct sk_buff *skb, struct genl_info *info);
int nfsd_nl_cache_flush_doit(struct sk_buff *skb, struct genl_info *info);
int nfsd_nl_unlock_ip_doit(struct sk_buff *skb, struct genl_info *info);
+int nfsd_nl_unlock_filesystem_doit(struct sk_buff *skb, struct genl_info *info);
enum {
NFSD_NLGRP_NONE,
diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index c5ce4af28287..c35774786374 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -2317,6 +2317,46 @@ int nfsd_nl_unlock_ip_doit(struct sk_buff *skb, struct genl_info *info)
return nlmsvc_unlock_all_by_ip(sap);
}
+/**
+ * nfsd_nl_unlock_filesystem_doit - revoke NFS state under a filesystem path
+ * @skb: reply buffer
+ * @info: netlink metadata and command arguments
+ *
+ * Return: 0 on success or a negative errno.
+ */
+int nfsd_nl_unlock_filesystem_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct net *net = genl_info_net(info);
+ struct nfsd_net *nn = net_generic(net, nfsd_net_id);
+ struct path path;
+ int error;
+
+ if (GENL_REQ_ATTR_CHECK(info, NFSD_A_UNLOCK_FILESYSTEM_PATH))
+ return -EINVAL;
+
+ trace_nfsd_ctl_unlock_fs(net,
+ nla_data(info->attrs[NFSD_A_UNLOCK_FILESYSTEM_PATH]));
+ error = kern_path(
+ nla_data(info->attrs[NFSD_A_UNLOCK_FILESYSTEM_PATH]),
+ 0, &path);
+ if (error)
+ return error;
+
+ nfsd4_cancel_copy_by_sb(net, path.dentry->d_sb);
+ error = nlmsvc_unlock_all_by_sb(path.dentry->d_sb);
+
+ mutex_lock(&nfsd_mutex);
+ if (nn->nfsd_serv)
+ nfsd4_revoke_states(nn, path.dentry->d_sb);
+ else
+ error = -EINVAL;
+ mutex_unlock(&nfsd_mutex);
+
+ path_put(&path);
+ return error;
+}
+
/**
* nfsd_net_init - Prepare the nfsd_net portion of a new net namespace
* @net: a freshly-created network namespace
diff --git a/include/uapi/linux/nfsd_netlink.h b/include/uapi/linux/nfsd_netlink.h
index 90ef1e686769..d01096c06d72 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -211,6 +211,13 @@ enum {
NFSD_A_UNLOCK_IP_MAX = (__NFSD_A_UNLOCK_IP_MAX - 1)
};
+enum {
+ NFSD_A_UNLOCK_FILESYSTEM_PATH = 1,
+
+ __NFSD_A_UNLOCK_FILESYSTEM_MAX,
+ NFSD_A_UNLOCK_FILESYSTEM_MAX = (__NFSD_A_UNLOCK_FILESYSTEM_MAX - 1)
+};
+
enum {
NFSD_CMD_RPC_STATUS_GET = 1,
NFSD_CMD_THREADS_SET,
@@ -228,6 +235,7 @@ enum {
NFSD_CMD_EXPKEY_SET_REQS,
NFSD_CMD_CACHE_FLUSH,
NFSD_CMD_UNLOCK_IP,
+ NFSD_CMD_UNLOCK_FILESYSTEM,
__NFSD_CMD_MAX,
NFSD_CMD_MAX = (__NFSD_CMD_MAX - 1)
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 6/9] NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid()
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (4 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 5/9] NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM " Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 12:29 ` [PATCH v8 7/9] NFSD: Track svc_export in nfs4_stid Chuck Lever
` (3 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
Replace idr_for_each_entry_ul() with a while loop over
idr_get_next_ul() for consistency with find_one_export_stid(),
added in a subsequent commit.
No change in behavior.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
fs/nfsd/nfs4state.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index b095b1beaac0..1478ff741b79 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1760,17 +1760,19 @@ static struct nfs4_stid *find_one_sb_stid(struct nfs4_client *clp,
struct super_block *sb,
unsigned int sc_types)
{
- unsigned long id, tmp;
+ unsigned long id = 0;
struct nfs4_stid *stid;
spin_lock(&clp->cl_lock);
- idr_for_each_entry_ul(&clp->cl_stateids, stid, tmp, id)
+ while ((stid = idr_get_next_ul(&clp->cl_stateids, &id)) != NULL) {
if ((stid->sc_type & sc_types) &&
stid->sc_status == 0 &&
stid->sc_file->fi_inode->i_sb == sb) {
refcount_inc(&stid->sc_count);
break;
}
+ id++;
+ }
spin_unlock(&clp->cl_lock);
return stid;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 7/9] NFSD: Track svc_export in nfs4_stid
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (5 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 6/9] NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid() Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 12:29 ` [PATCH v8 8/9] NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command Chuck Lever
` (2 subsequent siblings)
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
Add an sc_export field to struct nfs4_stid so that each stateid
records the export under which it was acquired. The export
reference is taken via exp_get() at stateid creation and released
via exp_put() in nfs4_put_stid().
Open stateids record the export from current_fh->fh_export.
Lock stateids and delegations inherit the export from their
parent open stateid. Layout stateids inherit from their
parent stateid. Directory delegations record the export from
cstate->current_fh.
A subsequent commit uses sc_export to scope state revocation to a
specific export, avoiding the need to walk inode dentry aliases at
revocation time.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
fs/nfsd/nfs4layouts.c | 2 ++
fs/nfsd/nfs4state.c | 16 +++++++++++++++-
fs/nfsd/state.h | 1 +
3 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/fs/nfsd/nfs4layouts.c b/fs/nfsd/nfs4layouts.c
index c3543d456702..c550b83f4432 100644
--- a/fs/nfsd/nfs4layouts.c
+++ b/fs/nfsd/nfs4layouts.c
@@ -234,6 +234,8 @@ nfsd4_alloc_layout_stateid(struct nfsd4_compound_state *cstate,
get_nfs4_file(fp);
stp->sc_file = fp;
+ if (parent->sc_export)
+ stp->sc_export = exp_get(parent->sc_export);
ls = layoutstateid(stp);
INIT_LIST_HEAD(&ls->ls_perclnt);
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 1478ff741b79..7b010fa21188 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1168,6 +1168,7 @@ alloc_init_deleg(struct nfs4_client *clp, struct nfs4_file *fp,
void
nfs4_put_stid(struct nfs4_stid *s)
{
+ struct svc_export *exp = s->sc_export;
struct nfs4_file *fp = s->sc_file;
struct nfs4_client *clp = s->sc_client;
@@ -1183,6 +1184,8 @@ nfs4_put_stid(struct nfs4_stid *s)
nfs4_free_cpntf_statelist(clp->net, s);
spin_unlock(&clp->cl_lock);
s->sc_free(s);
+ if (exp)
+ exp_put(exp);
if (fp)
put_nfs4_file(fp);
}
@@ -6186,6 +6189,8 @@ nfs4_set_delegation(struct nfsd4_open *open, struct nfs4_ol_stateid *stp,
dp = alloc_init_deleg(clp, fp, odstate, dl_type);
if (!dp)
goto out_delegees;
+ if (stp->st_stid.sc_export)
+ dp->dl_stid.sc_export = exp_get(stp->st_stid.sc_export);
fl = nfs4_alloc_init_lease(dp);
if (!fl)
@@ -6520,8 +6525,11 @@ nfsd4_process_open2(struct svc_rqst *rqstp, struct svc_fh *current_fh, struct nf
goto out;
}
- if (!open->op_stp)
+ if (!open->op_stp) {
new_stp = true;
+ stp->st_stid.sc_export =
+ exp_get(current_fh->fh_export);
+ }
}
/*
@@ -8217,6 +8225,9 @@ init_lock_stateid(struct nfs4_ol_stateid *stp, struct nfs4_lockowner *lo,
stp->st_stateowner = nfs4_get_stateowner(&lo->lo_owner);
get_nfs4_file(fp);
stp->st_stid.sc_file = fp;
+ if (open_stp->st_stid.sc_export)
+ stp->st_stid.sc_export =
+ exp_get(open_stp->st_stid.sc_export);
stp->st_access_bmap = 0;
stp->st_deny_bmap = open_stp->st_deny_bmap;
stp->st_openstp = open_stp;
@@ -9543,6 +9554,9 @@ nfsd_get_dir_deleg(struct nfsd4_compound_state *cstate,
dp = alloc_init_deleg(clp, fp, NULL, NFS4_OPEN_DELEGATE_READ);
if (!dp)
goto out_delegees;
+ if (cstate->current_fh.fh_export)
+ dp->dl_stid.sc_export =
+ exp_get(cstate->current_fh.fh_export);
fl = nfs4_alloc_init_lease(dp);
if (!fl)
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index 953675eba5c3..7d7e99eeffa5 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -145,6 +145,7 @@ struct nfs4_stid {
spinlock_t sc_lock;
struct nfs4_client *sc_client;
struct nfs4_file *sc_file;
+ struct svc_export *sc_export;
void (*sc_free)(struct nfs4_stid *);
};
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 8/9] NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (6 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 7/9] NFSD: Track svc_export in nfs4_stid Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 12:29 ` [PATCH v8 9/9] NFSD: Close cached file handles when revoking export state Chuck Lever
2026-04-08 13:56 ` [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Jeff Layton
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
When a filesystem is exported to NFS clients, NFSv4 state
(opens, locks, delegations, layouts) holds references that
prevent the underlying filesystem from being unmounted.
NFSD_CMD_UNLOCK_FILESYSTEM addresses this at superblock
granularity, but administrators unexporting a single path on a
shared filesystem (e.g., one of several exports on the same device)
need finer control.
Add NFSD_CMD_UNLOCK_EXPORT, which revokes NFSv4 state acquired
through exports of a specific path. Matching is by path identity
(dentry + vfsmount) via the sc_export field on each nfs4_stid,
so multiple svc_export objects for the same path -- one per
auth_domain -- are handled correctly without requiring the caller
to name a specific client.
The command takes a single "path" attribute. Userspace (exportfs
-u) sends this after removing the last client for a given path,
enabling the underlying filesystem to be unmounted. When multiple
clients share an export path, individual unexports do not trigger
state revocation; only the final one does.
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
Documentation/netlink/specs/nfsd.yaml | 27 ++++++++++++++
fs/nfsd/netlink.c | 12 +++++++
fs/nfsd/netlink.h | 1 +
fs/nfsd/nfs4state.c | 67 +++++++++++++++++++++++++++++++++++
fs/nfsd/nfsctl.c | 45 +++++++++++++++++++++++
fs/nfsd/state.h | 5 +++
fs/nfsd/trace.h | 19 ++++++++++
include/uapi/linux/nfsd_netlink.h | 8 +++++
8 files changed, 184 insertions(+)
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index f3b21d4ba660..17e714ef683d 100644
--- a/Documentation/netlink/specs/nfsd.yaml
+++ b/Documentation/netlink/specs/nfsd.yaml
@@ -317,6 +317,19 @@ attribute-sets:
name: path
type: string
doc: Filesystem path whose state should be released.
+ -
+ name: unlock-export
+ attributes:
+ -
+ name: path
+ type: string
+ doc: >-
+ Export path whose NFSv4 state should be revoked.
+ All state (opens, locks, delegations, layouts) acquired
+ through any export of this path is revoked, regardless
+ of which client holds the state. Intended for use after
+ all clients have been unexported from a given path,
+ enabling the underlying filesystem to be unmounted.
operations:
list:
@@ -489,6 +502,20 @@ operations:
request:
attributes:
- path
+ -
+ name: unlock-export
+ doc: >-
+ Revoke NFSv4 state acquired through exports of a given path.
+ Unlike unlock-filesystem, which operates at superblock granularity,
+ this command targets only state associated with a specific export
+ path. Userspace (exportfs -u) sends this after removing the last
+ client for a path so the underlying filesystem can be unmounted.
+ attribute-set: unlock-export
+ flags: [admin-perm]
+ do:
+ request:
+ attributes:
+ - path
mcast-groups:
list:
diff --git a/fs/nfsd/netlink.c b/fs/nfsd/netlink.c
index dae7ad560bd8..df5b0e2fb286 100644
--- a/fs/nfsd/netlink.c
+++ b/fs/nfsd/netlink.c
@@ -123,6 +123,11 @@ static const struct nla_policy nfsd_unlock_filesystem_nl_policy[NFSD_A_UNLOCK_FI
[NFSD_A_UNLOCK_FILESYSTEM_PATH] = { .type = NLA_NUL_STRING, },
};
+/* NFSD_CMD_UNLOCK_EXPORT - do */
+static const struct nla_policy nfsd_unlock_export_nl_policy[NFSD_A_UNLOCK_EXPORT_PATH + 1] = {
+ [NFSD_A_UNLOCK_EXPORT_PATH] = { .type = NLA_NUL_STRING, },
+};
+
/* Ops table for nfsd */
static const struct genl_split_ops nfsd_nl_ops[] = {
{
@@ -227,6 +232,13 @@ static const struct genl_split_ops nfsd_nl_ops[] = {
.maxattr = NFSD_A_UNLOCK_FILESYSTEM_PATH,
.flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
},
+ {
+ .cmd = NFSD_CMD_UNLOCK_EXPORT,
+ .doit = nfsd_nl_unlock_export_doit,
+ .policy = nfsd_unlock_export_nl_policy,
+ .maxattr = NFSD_A_UNLOCK_EXPORT_PATH,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
};
static const struct genl_multicast_group nfsd_nl_mcgrps[] = {
diff --git a/fs/nfsd/netlink.h b/fs/nfsd/netlink.h
index 29bd5468d401..af41aa0d4a65 100644
--- a/fs/nfsd/netlink.h
+++ b/fs/nfsd/netlink.h
@@ -41,6 +41,7 @@ int nfsd_nl_expkey_set_reqs_doit(struct sk_buff *skb, struct genl_info *info);
int nfsd_nl_cache_flush_doit(struct sk_buff *skb, struct genl_info *info);
int nfsd_nl_unlock_ip_doit(struct sk_buff *skb, struct genl_info *info);
int nfsd_nl_unlock_filesystem_doit(struct sk_buff *skb, struct genl_info *info);
+int nfsd_nl_unlock_export_doit(struct sk_buff *skb, struct genl_info *info);
enum {
NFSD_NLGRP_NONE,
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 7b010fa21188..35de5364e587 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1903,6 +1903,73 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
spin_unlock(&nn->client_lock);
}
+static struct nfs4_stid *find_one_export_stid(struct nfs4_client *clp,
+ const struct path *path,
+ unsigned int sc_types)
+{
+ unsigned long id = 0;
+ struct nfs4_stid *stid;
+
+ spin_lock(&clp->cl_lock);
+ while ((stid = idr_get_next_ul(&clp->cl_stateids, &id)) != NULL) {
+ if ((stid->sc_type & sc_types) &&
+ stid->sc_status == 0 &&
+ stid->sc_export &&
+ path_equal(&stid->sc_export->ex_path, path)) {
+ refcount_inc(&stid->sc_count);
+ break;
+ }
+ id++;
+ }
+ spin_unlock(&clp->cl_lock);
+ return stid;
+}
+
+/**
+ * nfsd4_revoke_export_states - revoke nfsv4 states acquired through an export
+ * @nn: used to identify instance of nfsd (there is one per net namespace)
+ * @path: export path whose states should be revoked
+ *
+ * All nfs4 states (open, lock, delegation, layout) acquired through any
+ * export matching @path are revoked, regardless of which client holds
+ * them. Matching is by path identity (dentry + vfsmount), so multiple
+ * svc_export objects for the same path -- one per auth_domain -- are
+ * handled correctly.
+ *
+ * Userspace (exportfs -u) sends this after removing the last client
+ * for a path, enabling the underlying filesystem to be unmounted.
+ */
+void nfsd4_revoke_export_states(struct nfsd_net *nn, const struct path *path)
+{
+ unsigned int idhashval;
+ unsigned int sc_types;
+
+ sc_types = SC_TYPE_OPEN | SC_TYPE_LOCK | SC_TYPE_DELEG | SC_TYPE_LAYOUT;
+
+ spin_lock(&nn->client_lock);
+ for (idhashval = 0; idhashval < CLIENT_HASH_SIZE; idhashval++) {
+ struct list_head *head = &nn->conf_id_hashtbl[idhashval];
+ struct nfs4_client *clp;
+ retry:
+ list_for_each_entry(clp, head, cl_idhash) {
+ struct nfs4_stid *stid = find_one_export_stid(
+ clp, path,
+ sc_types);
+ if (stid) {
+ spin_unlock(&nn->client_lock);
+ revoke_one_stid(nn, clp, stid);
+ nfs4_put_stid(stid);
+ spin_lock(&nn->client_lock);
+ if (clp->cl_minorversion == 0)
+ nn->nfs40_last_revoke =
+ ktime_get_boottime_seconds();
+ goto retry;
+ }
+ }
+ }
+ spin_unlock(&nn->client_lock);
+}
+
static inline int
hash_sessionid(struct nfs4_sessionid *sessionid)
{
diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index c35774786374..65f4c757a0f4 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -2357,6 +2357,51 @@ int nfsd_nl_unlock_filesystem_doit(struct sk_buff *skb,
return error;
}
+/**
+ * nfsd_nl_unlock_export_doit - revoke NFSv4 state for an export path
+ * @skb: reply buffer
+ * @info: netlink metadata and command arguments
+ *
+ * Revokes all NFSv4 state (opens, locks, delegations, layouts) acquired
+ * through any export of the given path, regardless of which client holds
+ * the state. Userspace (exportfs -u) sends this after removing the last
+ * client for a path so the underlying filesystem can be unmounted.
+ *
+ * Unlike NFSD_CMD_UNLOCK_FILESYSTEM, which operates at superblock
+ * granularity, this command revokes only the state associated with
+ * exports of a specific path.
+ *
+ * Return: 0 on success or a negative errno.
+ */
+int nfsd_nl_unlock_export_doit(struct sk_buff *skb, struct genl_info *info)
+{
+ struct net *net = genl_info_net(info);
+ struct nfsd_net *nn = net_generic(net, nfsd_net_id);
+ struct path path;
+ int error;
+
+ if (GENL_REQ_ATTR_CHECK(info, NFSD_A_UNLOCK_EXPORT_PATH))
+ return -EINVAL;
+
+ trace_nfsd_ctl_unlock_export(net,
+ nla_data(info->attrs[NFSD_A_UNLOCK_EXPORT_PATH]));
+ error = kern_path(
+ nla_data(info->attrs[NFSD_A_UNLOCK_EXPORT_PATH]),
+ 0, &path);
+ if (error)
+ return error;
+
+ mutex_lock(&nfsd_mutex);
+ if (nn->nfsd_serv)
+ nfsd4_revoke_export_states(nn, &path);
+ else
+ error = -EINVAL;
+ mutex_unlock(&nfsd_mutex);
+
+ path_put(&path);
+ return error;
+}
+
/**
* nfsd_net_init - Prepare the nfsd_net portion of a new net namespace
* @net: a freshly-created network namespace
diff --git a/fs/nfsd/state.h b/fs/nfsd/state.h
index 7d7e99eeffa5..811c148f36fc 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -862,6 +862,7 @@ struct nfsd_file *find_any_file(struct nfs4_file *f);
#ifdef CONFIG_NFSD_V4
void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb);
+void nfsd4_revoke_export_states(struct nfsd_net *nn, const struct path *path);
void nfsd4_cancel_copy_by_sb(struct net *net, struct super_block *sb);
int nfsd_net_cb_init(struct nfsd_net *nn);
void nfsd_net_cb_shutdown(struct nfsd_net *nn);
@@ -869,6 +870,10 @@ void nfsd_net_cb_shutdown(struct nfsd_net *nn);
static inline void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
{
}
+static inline void nfsd4_revoke_export_states(struct nfsd_net *nn,
+ const struct path *path)
+{
+}
static inline void nfsd4_cancel_copy_by_sb(struct net *net, struct super_block *sb)
{
}
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index 976815f6f30f..a13d18447324 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -2021,6 +2021,25 @@ TRACE_EVENT(nfsd_ctl_unlock_fs,
)
);
+TRACE_EVENT(nfsd_ctl_unlock_export,
+ TP_PROTO(
+ const struct net *net,
+ const char *path
+ ),
+ TP_ARGS(net, path),
+ TP_STRUCT__entry(
+ __field(unsigned int, netns_ino)
+ __string(path, path)
+ ),
+ TP_fast_assign(
+ __entry->netns_ino = net->ns.inum;
+ __assign_str(path);
+ ),
+ TP_printk("path=%s",
+ __get_str(path)
+ )
+);
+
TRACE_EVENT(nfsd_ctl_filehandle,
TP_PROTO(
const struct net *net,
diff --git a/include/uapi/linux/nfsd_netlink.h b/include/uapi/linux/nfsd_netlink.h
index d01096c06d72..f5b75d5caba9 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -218,6 +218,13 @@ enum {
NFSD_A_UNLOCK_FILESYSTEM_MAX = (__NFSD_A_UNLOCK_FILESYSTEM_MAX - 1)
};
+enum {
+ NFSD_A_UNLOCK_EXPORT_PATH = 1,
+
+ __NFSD_A_UNLOCK_EXPORT_MAX,
+ NFSD_A_UNLOCK_EXPORT_MAX = (__NFSD_A_UNLOCK_EXPORT_MAX - 1)
+};
+
enum {
NFSD_CMD_RPC_STATUS_GET = 1,
NFSD_CMD_THREADS_SET,
@@ -236,6 +243,7 @@ enum {
NFSD_CMD_CACHE_FLUSH,
NFSD_CMD_UNLOCK_IP,
NFSD_CMD_UNLOCK_FILESYSTEM,
+ NFSD_CMD_UNLOCK_EXPORT,
__NFSD_CMD_MAX,
NFSD_CMD_MAX = (__NFSD_CMD_MAX - 1)
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* [PATCH v8 9/9] NFSD: Close cached file handles when revoking export state
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (7 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 8/9] NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command Chuck Lever
@ 2026-04-08 12:29 ` Chuck Lever
2026-04-08 13:56 ` [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Jeff Layton
9 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 12:29 UTC (permalink / raw)
To: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
From: Chuck Lever <chuck.lever@oracle.com>
When NFSD_CMD_UNLOCK_EXPORT revokes NFSv4 state for an export path,
GC-managed nfsd_file entries for files under that path may remain
in the file cache. These cached handles hold the underlying
filesystem busy, preventing a subsequent unmount.
Add nfsd_file_close_export(), which walks the nfsd_file hash table
and closes GC-eligible entries whose underlying file resides on the
same filesystem and is a descendant of the export path. Because
nfsd_file entries do not carry an export reference, the ancestry
check uses is_subdir() on the file's dentry. False positives --
closing a cached handle that did not originate from the target
export -- are harmless; the handle is simply reopened on the next
access.
The handler calls nfsd_file_close_export() before revoking NFSv4
state, mirroring the order used by NFSD_CMD_UNLOCK_FILESYSTEM
(which cancels copies and releases NLM locks before revoking
state). Both calls run under nfsd_mutex.
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
---
fs/nfsd/filecache.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
fs/nfsd/filecache.h | 1 +
fs/nfsd/nfsctl.c | 5 +++--
3 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/fs/nfsd/filecache.c b/fs/nfsd/filecache.c
index 1e2b38ed1d35..24511c3208db 100644
--- a/fs/nfsd/filecache.c
+++ b/fs/nfsd/filecache.c
@@ -724,6 +724,52 @@ nfsd_file_close_inode_sync(struct inode *inode)
nfsd_file_dispose_list(&dispose);
}
+/**
+ * nfsd_file_close_export - close cached file handles for an export
+ * @net: net namespace in which to operate
+ * @path: export path whose cached files should be closed
+ *
+ * Close out GC-managed nfsd_file entries whose underlying file is on
+ * the same filesystem as, and a descendant of, @path. nfsd_file
+ * entries do not carry an export reference, so the check uses the
+ * file's dentry ancestry. False positives (closing a cached handle
+ * that did not originate from the target export) are harmless -- the
+ * handle is simply reopened on the next access.
+ *
+ * Called from the NFSD_CMD_UNLOCK_EXPORT handler before revoking
+ * NFSv4 state, to ensure that cached file handles do not hold the
+ * filesystem busy.
+ */
+void nfsd_file_close_export(struct net *net, const struct path *path)
+{
+ struct rhashtable_iter iter;
+ struct nfsd_file *nf;
+ LIST_HEAD(dispose);
+
+ rhltable_walk_enter(&nfsd_file_rhltable, &iter);
+ do {
+ rhashtable_walk_start(&iter);
+
+ nf = rhashtable_walk_next(&iter);
+ while (!IS_ERR_OR_NULL(nf)) {
+ if (nf->nf_net == net &&
+ test_bit(NFSD_FILE_GC, &nf->nf_flags) &&
+ nf->nf_file &&
+ file_inode(nf->nf_file)->i_sb ==
+ path->dentry->d_sb &&
+ is_subdir(nf->nf_file->f_path.dentry,
+ path->dentry))
+ nfsd_file_cond_queue(nf, &dispose);
+ nf = rhashtable_walk_next(&iter);
+ }
+
+ rhashtable_walk_stop(&iter);
+ } while (nf == ERR_PTR(-EAGAIN));
+ rhashtable_walk_exit(&iter);
+
+ nfsd_file_dispose_list(&dispose);
+}
+
static int
nfsd_file_lease_notifier_call(struct notifier_block *nb, unsigned long arg,
void *data)
diff --git a/fs/nfsd/filecache.h b/fs/nfsd/filecache.h
index b383dbc5b921..683b6437cacc 100644
--- a/fs/nfsd/filecache.h
+++ b/fs/nfsd/filecache.h
@@ -70,6 +70,7 @@ struct net *nfsd_file_put_local(struct nfsd_file __rcu **nf);
struct nfsd_file *nfsd_file_get(struct nfsd_file *nf);
struct file *nfsd_file_file(struct nfsd_file *nf);
void nfsd_file_close_inode_sync(struct inode *inode);
+void nfsd_file_close_export(struct net *net, const struct path *path);
void nfsd_file_net_dispose(struct nfsd_net *nn);
bool nfsd_file_is_cached(struct inode *inode);
__be32 nfsd_file_acquire_gc(struct svc_rqst *rqstp, struct svc_fh *fhp,
diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index 65f4c757a0f4..c76849f316f7 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -2392,9 +2392,10 @@ int nfsd_nl_unlock_export_doit(struct sk_buff *skb, struct genl_info *info)
return error;
mutex_lock(&nfsd_mutex);
- if (nn->nfsd_serv)
+ if (nn->nfsd_serv) {
+ nfsd_file_close_export(net, &path);
nfsd4_revoke_export_states(nn, &path);
- else
+ } else
error = -EINVAL;
mutex_unlock(&nfsd_mutex);
--
2.53.0
^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
` (8 preceding siblings ...)
2026-04-08 12:29 ` [PATCH v8 9/9] NFSD: Close cached file handles when revoking export state Chuck Lever
@ 2026-04-08 13:56 ` Jeff Layton
2026-04-08 13:57 ` Chuck Lever
9 siblings, 1 reply; 14+ messages in thread
From: Jeff Layton @ 2026-04-08 13:56 UTC (permalink / raw)
To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
nit: subject is a little misleading. You're not doing this on unmount
anymore, but rather when unexporting.
On Wed, 2026-04-08 at 08:29 -0400, Chuck Lever wrote:
> When an NFS server exports a filesystem and clients hold NFSv4
> state (opens, locks, delegations), unmounting the underlying
> filesystem fails with EBUSY. The /proc/fs/nfsd/unlock_ip and
> /proc/fs/nfsd/unlock_fs procfs interfaces handle this, but have
> no netlink equivalents, and unlock_fs operates at whole-superblock
> granularity.
>
> This series adds three new NFSD netlink commands, each with its own
> attribute set:
>
> - NFSD_CMD_UNLOCK_IP releases NLM locks held by a client IP
> address. Netlink equivalent of write_unlock_ip.
>
> - NFSD_CMD_UNLOCK_FILESYSTEM revokes all NFS state on a
> superblock. Netlink equivalent of write_unlock_fs.
>
> - NFSD_CMD_UNLOCK_EXPORT revokes NFSv4 state acquired through
> exports of a specific path, regardless of client.
>
> UNLOCK_FILESYSTEM and UNLOCK_EXPORT serve different intents.
> UNLOCK_FILESYSTEM means "unmounting /data, release everything
> on this superblock." UNLOCK_EXPORT means "no clients remain for
> /data/projectA, release only the state acquired through exports
> of that path." Userspace (exportfs -u) sends UNLOCK_EXPORT after
> removing the last client for a given path, enabling the underlying
> filesystem to be unmounted.
>
> The path-only design for UNLOCK_EXPORT avoids the auth_domain
> naming complexity (use_ipaddr vs hostname-based domains) by not
> requiring the caller to identify a specific client. Since this
> mechanism is to be used to enable umount, this seemed like a
> reasonable compromise.
>
> ---
> Changes since v7:
> - Rebase on Jeff's mountd netlink patches
> - Fix pre-existing state revocation bugs
>
> Changes since v6:
> - Send the complete series (v5 was missing patches 6 and 7)
>
> Changes since v5:
> - Rename state_lock => nn->deleg_lock
>
> Changes since v4:
> - 1/9 has been queued in nfsd-testing
> - Split single NFSD_CMD_UNLOCK into three separate commands
> - UNLOCK_EXPORT takes path only, no client attribute to avoid
> auth_domain naming complexity with use_ipaddr
>
> Changes since v3:
> - All VFS changes replaced with new netlink "unlock" operation
>
> Changes since v2:
> - Replace fs_pin with an SRCU umount notifier chain in VFS
> - Merge the pending COPY cancellation patch
> - Replace xa_cmpxchg() with xa_insert()
> - Use cancel_work_sync() instead of flush_workqueue()
> - Remove rcu_barrier()
> - Correct misleading claims in kdoc comments and commit messages
>
> Changes since v1:
> - Explain why drop_client() is being renamed
> - Finish implementing revocation on umount
> - Rename pin_insert_group
> - Clarified log output and code comments
> - Hold nfsd_mutex while closing nfsd_files
>
> ---
> Chuck Lever (9):
> NFSD: Fix infinite loop in layout state revocation
> NFSD: Handle layout stid in nfsd4_drop_revoked_stid()
> NFSD: Extract revoke_one_stid() utility function
> NFSD: Add NFSD_CMD_UNLOCK_IP netlink command
> NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM netlink command
> NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid()
> NFSD: Track svc_export in nfs4_stid
> NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
> NFSD: Close cached file handles when revoking export state
>
> Documentation/netlink/specs/nfsd.yaml | 61 +++++++++
> fs/nfsd/filecache.c | 46 +++++++
> fs/nfsd/filecache.h | 1 +
> fs/nfsd/netlink.c | 36 +++++
> fs/nfsd/netlink.h | 3 +
> fs/nfsd/nfs4layouts.c | 2 +
> fs/nfsd/nfs4state.c | 240 ++++++++++++++++++++++++----------
> fs/nfsd/nfsctl.c | 126 +++++++++++++++++-
> fs/nfsd/state.h | 6 +
> fs/nfsd/trace.h | 32 ++++-
> include/uapi/linux/nfsd_netlink.h | 24 ++++
> 11 files changed, 498 insertions(+), 79 deletions(-)
> ---
> base-commit: b495a392b2748dca31d2a4b404632c6f907aa136
> change-id: 20260318-umount-kills-nfsv4-state-138218f2f4e0
>
> Best regards,
> --
> Chuck Lever
--
Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount
2026-04-08 13:56 ` [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Jeff Layton
@ 2026-04-08 13:57 ` Chuck Lever
0 siblings, 0 replies; 14+ messages in thread
From: Chuck Lever @ 2026-04-08 13:57 UTC (permalink / raw)
To: Jeff Layton, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
On 4/8/26 9:56 AM, Jeff Layton wrote:
> nit: subject is a little misleading. You're not doing this on unmount
> anymore, but rather when unexporting.
The Subject: matches what I used on the v1 posting. But you're correct,
the mission of this patch series has changed.
> On Wed, 2026-04-08 at 08:29 -0400, Chuck Lever wrote:
>> When an NFS server exports a filesystem and clients hold NFSv4
>> state (opens, locks, delegations), unmounting the underlying
>> filesystem fails with EBUSY. The /proc/fs/nfsd/unlock_ip and
>> /proc/fs/nfsd/unlock_fs procfs interfaces handle this, but have
>> no netlink equivalents, and unlock_fs operates at whole-superblock
>> granularity.
>>
>> This series adds three new NFSD netlink commands, each with its own
>> attribute set:
>>
>> - NFSD_CMD_UNLOCK_IP releases NLM locks held by a client IP
>> address. Netlink equivalent of write_unlock_ip.
>>
>> - NFSD_CMD_UNLOCK_FILESYSTEM revokes all NFS state on a
>> superblock. Netlink equivalent of write_unlock_fs.
>>
>> - NFSD_CMD_UNLOCK_EXPORT revokes NFSv4 state acquired through
>> exports of a specific path, regardless of client.
>>
>> UNLOCK_FILESYSTEM and UNLOCK_EXPORT serve different intents.
>> UNLOCK_FILESYSTEM means "unmounting /data, release everything
>> on this superblock." UNLOCK_EXPORT means "no clients remain for
>> /data/projectA, release only the state acquired through exports
>> of that path." Userspace (exportfs -u) sends UNLOCK_EXPORT after
>> removing the last client for a given path, enabling the underlying
>> filesystem to be unmounted.
>>
>> The path-only design for UNLOCK_EXPORT avoids the auth_domain
>> naming complexity (use_ipaddr vs hostname-based domains) by not
>> requiring the caller to identify a specific client. Since this
>> mechanism is to be used to enable umount, this seemed like a
>> reasonable compromise.
>>
>> ---
>> Changes since v7:
>> - Rebase on Jeff's mountd netlink patches
>> - Fix pre-existing state revocation bugs
>>
>> Changes since v6:
>> - Send the complete series (v5 was missing patches 6 and 7)
>>
>> Changes since v5:
>> - Rename state_lock => nn->deleg_lock
>>
>> Changes since v4:
>> - 1/9 has been queued in nfsd-testing
>> - Split single NFSD_CMD_UNLOCK into three separate commands
>> - UNLOCK_EXPORT takes path only, no client attribute to avoid
>> auth_domain naming complexity with use_ipaddr
>>
>> Changes since v3:
>> - All VFS changes replaced with new netlink "unlock" operation
>>
>> Changes since v2:
>> - Replace fs_pin with an SRCU umount notifier chain in VFS
>> - Merge the pending COPY cancellation patch
>> - Replace xa_cmpxchg() with xa_insert()
>> - Use cancel_work_sync() instead of flush_workqueue()
>> - Remove rcu_barrier()
>> - Correct misleading claims in kdoc comments and commit messages
>>
>> Changes since v1:
>> - Explain why drop_client() is being renamed
>> - Finish implementing revocation on umount
>> - Rename pin_insert_group
>> - Clarified log output and code comments
>> - Hold nfsd_mutex while closing nfsd_files
>>
>> ---
>> Chuck Lever (9):
>> NFSD: Fix infinite loop in layout state revocation
>> NFSD: Handle layout stid in nfsd4_drop_revoked_stid()
>> NFSD: Extract revoke_one_stid() utility function
>> NFSD: Add NFSD_CMD_UNLOCK_IP netlink command
>> NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM netlink command
>> NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid()
>> NFSD: Track svc_export in nfs4_stid
>> NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
>> NFSD: Close cached file handles when revoking export state
>>
>> Documentation/netlink/specs/nfsd.yaml | 61 +++++++++
>> fs/nfsd/filecache.c | 46 +++++++
>> fs/nfsd/filecache.h | 1 +
>> fs/nfsd/netlink.c | 36 +++++
>> fs/nfsd/netlink.h | 3 +
>> fs/nfsd/nfs4layouts.c | 2 +
>> fs/nfsd/nfs4state.c | 240 ++++++++++++++++++++++++----------
>> fs/nfsd/nfsctl.c | 126 +++++++++++++++++-
>> fs/nfsd/state.h | 6 +
>> fs/nfsd/trace.h | 32 ++++-
>> include/uapi/linux/nfsd_netlink.h | 24 ++++
>> 11 files changed, 498 insertions(+), 79 deletions(-)
>> ---
>> base-commit: b495a392b2748dca31d2a4b404632c6f907aa136
>> change-id: 20260318-umount-kills-nfsv4-state-138218f2f4e0
>>
>> Best regards,
>> --
>> Chuck Lever
>
--
Chuck Lever
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation
2026-04-08 12:29 ` [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation Chuck Lever
@ 2026-04-08 13:58 ` Jeff Layton
0 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-04-08 13:58 UTC (permalink / raw)
To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
On Wed, 2026-04-08 at 08:29 -0400, Chuck Lever wrote:
> From: Chuck Lever <chuck.lever@oracle.com>
>
> find_one_sb_stid() skips stids whose sc_status is non-zero, but the
> SC_TYPE_LAYOUT case in nfsd4_revoke_states() never sets sc_status
> before calling nfsd4_close_layout(). The retry loop therefore finds
> the same layout stid on every iteration, hanging the revoker
> indefinitely.
>
> Fixes: 1e33e1414bec ("nfsd: allow layout state to be admin-revoked.")
> Reported-by: Dai Ngo <dai.ngo@oracle.com>
> Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
> ---
> fs/nfsd/nfs4state.c | 7 +++++++
> 1 file changed, 7 insertions(+)
>
> diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
> index 07df4511ba23..c6cb67cf02ad 100644
> --- a/fs/nfsd/nfs4state.c
> +++ b/fs/nfsd/nfs4state.c
> @@ -1872,6 +1872,13 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
> break;
> case SC_TYPE_LAYOUT:
> ls = layoutstateid(stid);
> + spin_lock(&clp->cl_lock);
> + if (stid->sc_status == 0) {
> + stid->sc_status |=
> + SC_STATUS_ADMIN_REVOKED;
> + atomic_inc(&clp->cl_admin_revoked);
> + }
> + spin_unlock(&clp->cl_lock);
> nfsd4_close_layout(ls);
> break;
> }
Nice catch.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v8 2/9] NFSD: Handle layout stid in nfsd4_drop_revoked_stid()
2026-04-08 12:29 ` [PATCH v8 2/9] NFSD: Handle layout stid in nfsd4_drop_revoked_stid() Chuck Lever
@ 2026-04-08 13:59 ` Jeff Layton
0 siblings, 0 replies; 14+ messages in thread
From: Jeff Layton @ 2026-04-08 13:59 UTC (permalink / raw)
To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey
Cc: linux-nfs, linux-fsdevel, Chuck Lever
On Wed, 2026-04-08 at 08:29 -0400, Chuck Lever wrote:
> From: Chuck Lever <chuck.lever@oracle.com>
>
> nfsd4_drop_revoked_stid() has no SC_TYPE_LAYOUT case, so when a
> client sends FREE_STATEID for an admin-revoked layout stid, the
> default branch releases cl_lock and returns without unhashing or
> releasing the stid. The stid remains in the IDR and on the
> per-client list until the client is destroyed.
>
> Remove the layout stid from the per-client list and call
> nfs4_put_stid() to drop the creation reference. When the
> refcount reaches zero, nfsd4_free_layout_stateid() handles the
> remaining cleanup: cancelling the fence worker, removing from
> the per-file list, and freeing the slab object.
>
> Fixes: 1e33e1414bec ("nfsd: allow layout state to be admin-revoked.")
> Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
> ---
> fs/nfsd/nfs4state.c | 7 +++++++
> 1 file changed, 7 insertions(+)
>
> diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
> index c6cb67cf02ad..ae5e1a20197c 100644
> --- a/fs/nfsd/nfs4state.c
> +++ b/fs/nfsd/nfs4state.c
> @@ -5070,6 +5070,7 @@ static void nfsd4_drop_revoked_stid(struct nfs4_stid *s)
> {
> struct nfs4_client *cl = s->sc_client;
> LIST_HEAD(reaplist);
> + struct nfs4_layout_stateid *ls;
> struct nfs4_ol_stateid *stp;
> struct nfs4_delegation *dp;
> bool unhashed;
> @@ -5095,6 +5096,12 @@ static void nfsd4_drop_revoked_stid(struct nfs4_stid *s)
> spin_unlock(&cl->cl_lock);
> nfs4_put_stid(s);
> break;
> + case SC_TYPE_LAYOUT:
> + ls = layoutstateid(s);
> + list_del_init(&ls->ls_perclnt);
> + spin_unlock(&cl->cl_lock);
> + nfs4_put_stid(s);
> + break;
> default:
> spin_unlock(&cl->cl_lock);
> }
Reviewed-by: Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-04-08 13:59 UTC | newest]
Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-08 12:29 [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Chuck Lever
2026-04-08 12:29 ` [PATCH v8 1/9] NFSD: Fix infinite loop in layout state revocation Chuck Lever
2026-04-08 13:58 ` Jeff Layton
2026-04-08 12:29 ` [PATCH v8 2/9] NFSD: Handle layout stid in nfsd4_drop_revoked_stid() Chuck Lever
2026-04-08 13:59 ` Jeff Layton
2026-04-08 12:29 ` [PATCH v8 3/9] NFSD: Extract revoke_one_stid() utility function Chuck Lever
2026-04-08 12:29 ` [PATCH v8 4/9] NFSD: Add NFSD_CMD_UNLOCK_IP netlink command Chuck Lever
2026-04-08 12:29 ` [PATCH v8 5/9] NFSD: Add NFSD_CMD_UNLOCK_FILESYSTEM " Chuck Lever
2026-04-08 12:29 ` [PATCH v8 6/9] NFSD: Replace idr_for_each_entry_ul in find_one_sb_stid() Chuck Lever
2026-04-08 12:29 ` [PATCH v8 7/9] NFSD: Track svc_export in nfs4_stid Chuck Lever
2026-04-08 12:29 ` [PATCH v8 8/9] NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command Chuck Lever
2026-04-08 12:29 ` [PATCH v8 9/9] NFSD: Close cached file handles when revoking export state Chuck Lever
2026-04-08 13:56 ` [PATCH v8 0/9] Automatic NFSv4 state revocation on filesystem unmount Jeff Layton
2026-04-08 13:57 ` Chuck Lever
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox