From: Chuck Lever <cel@kernel.org>
To: NeilBrown <neil@brown.name>, Jeff Layton <jlayton@kernel.org>,
Olga Kornievskaia <okorniev@redhat.com>,
Dai Ngo <Dai.Ngo@oracle.com>, Tom Talpey <tom@talpey.com>
Cc: linux-nfs@vger.kernel.org, linux-fsdevel@vger.kernel.org,
Chuck Lever <chuck.lever@oracle.com>
Subject: [PATCH v8 8/9] NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
Date: Wed, 08 Apr 2026 08:29:58 -0400 [thread overview]
Message-ID: <20260408-umount-kills-nfsv4-state-v8-8-6e02a1d03d60@oracle.com> (raw)
In-Reply-To: <20260408-umount-kills-nfsv4-state-v8-0-6e02a1d03d60@oracle.com>
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
next prev parent reply other threads:[~2026-04-08 12:30 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Chuck Lever [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260408-umount-kills-nfsv4-state-v8-8-6e02a1d03d60@oracle.com \
--to=cel@kernel.org \
--cc=Dai.Ngo@oracle.com \
--cc=chuck.lever@oracle.com \
--cc=jlayton@kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-nfs@vger.kernel.org \
--cc=neil@brown.name \
--cc=okorniev@redhat.com \
--cc=tom@talpey.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).