aboutsummaryrefslogtreecommitdiffstats
diff options
authorChuck Lever <chuck.lever@oracle.com>2026-04-19 14:53:06 -0400
committerChuck Lever <chuck.lever@oracle.com>2026-05-28 11:31:26 -0400
commit083eabeaf4c707909cd07187f4bf49ccd0d3c4f1 (patch)
tree18b46dae96361941f95d6ff8f70ab4ada7556f08
parent6433e35f3632fe6dbf18ad62c9a704f8996471a1 (diff)
downloadlinux-next-history-083eabeaf4c707909cd07187f4bf49ccd0d3c4f1.tar.gz
NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
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. Reviewed-by: Jeff Layton <jlayton@kernel.org> Tested-by: Dai Ngo <dai.ngo@oracle.com> Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
-rw-r--r--Documentation/netlink/specs/nfsd.yaml27
-rw-r--r--fs/nfsd/netlink.c12
-rw-r--r--fs/nfsd/netlink.h1
-rw-r--r--fs/nfsd/nfs4state.c67
-rw-r--r--fs/nfsd/nfsctl.c45
-rw-r--r--fs/nfsd/state.h5
-rw-r--r--fs/nfsd/trace.h19
-rw-r--r--include/uapi/linux/nfsd_netlink.h8
8 files changed, 184 insertions, 0 deletions
diff --git a/Documentation/netlink/specs/nfsd.yaml b/Documentation/netlink/specs/nfsd.yaml
index e121c54033a06..8f36fadd68f75 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 63df3c4cf63a0..fbee3676d2539 100644
--- a/fs/nfsd/netlink.c
+++ b/fs/nfsd/netlink.c
@@ -113,6 +113,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[] = {
{
@@ -213,6 +218,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 29bd5468d4019..af41aa0d4a65d 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 475d2b36ecd17..2cf021b202a64 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1911,6 +1911,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 6e7244a7e3c1b..3b16a1d8ef2ed 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -2345,6 +2345,51 @@ int nfsd_nl_unlock_filesystem_doit(struct sk_buff *skb,
}
/**
+ * 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 43bd965077e1d..dec83e92650d1 100644
--- a/fs/nfsd/state.h
+++ b/fs/nfsd/state.h
@@ -863,6 +863,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);
@@ -870,6 +871,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 976815f6f30fa..a13d18447324b 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 d01096c06d724..f5b75d5caba9f 100644
--- a/include/uapi/linux/nfsd_netlink.h
+++ b/include/uapi/linux/nfsd_netlink.h
@@ -219,6 +219,13 @@ enum {
};
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,
NFSD_CMD_THREADS_GET,
@@ -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)