]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command
authorChuck Lever <chuck.lever@oracle.com>
Sun, 19 Apr 2026 18:53:06 +0000 (14:53 -0400)
committerChuck Lever <cel@kernel.org>
Tue, 9 Jun 2026 20:32:59 +0000 (16:32 -0400)
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>
Documentation/netlink/specs/nfsd.yaml
fs/nfsd/netlink.c
fs/nfsd/netlink.h
fs/nfsd/nfs4state.c
fs/nfsd/nfsctl.c
fs/nfsd/state.h
fs/nfsd/trace.h
include/uapi/linux/nfsd_netlink.h

index e121c54033a06cfcf6da246830448048ae9a034b..8f36fadd68f75b3b041722de2d7abbfdc37d4c8b 100644 (file)
@@ -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:
index 63df3c4cf63a0e437d42efdd611e582ff3d23e82..fbee3676d2539039f0aff85964dcefa10d36a66a 100644 (file)
@@ -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[] = {
index 29bd5468d40197b9b9579d3416d761dda74b094f..af41aa0d4a65dc520093393a3245f79a949293f7 100644 (file)
@@ -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,
index 475d2b36ecd1700c811b7056e85fdbb3781ae140..2cf021b202a64d6c69ca995f75b026a85ede23d9 100644 (file)
@@ -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)
 {
index f7f104cc457ddb679f0f7541f1dc6924765808c6..dd4fb9249213c52ec5c6abb868dc2c9ebaca994c 100644 (file)
@@ -2354,6 +2354,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
index 43bd965077e1d079e3ee26c8127cadef9db20d3e..dec83e92650d11fb2822bb161b74ffbfd82da577 100644 (file)
@@ -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)
 {
 }
index 9a73e7a1acf2a7256fe92fa91163aadd5d3177cb..1c5a1e50f9465149f06ff978b86a3fb657137e9e 100644 (file)
@@ -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,
index d01096c06d724871b3f26f69922cdd926d8ef25b..f5b75d5caba9f4eb7a5282871c210108050f41eb 100644 (file)
@@ -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)