diff options
| author | Chuck Lever <chuck.lever@oracle.com> | 2026-04-19 14:53:06 -0400 |
|---|---|---|
| committer | Chuck Lever <chuck.lever@oracle.com> | 2026-05-28 11:31:26 -0400 |
| commit | 083eabeaf4c707909cd07187f4bf49ccd0d3c4f1 (patch) | |
| tree | 18b46dae96361941f95d6ff8f70ab4ada7556f08 | |
| parent | 6433e35f3632fe6dbf18ad62c9a704f8996471a1 (diff) | |
| download | linux-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.yaml | 27 | ||||
| -rw-r--r-- | fs/nfsd/netlink.c | 12 | ||||
| -rw-r--r-- | fs/nfsd/netlink.h | 1 | ||||
| -rw-r--r-- | fs/nfsd/nfs4state.c | 67 | ||||
| -rw-r--r-- | fs/nfsd/nfsctl.c | 45 | ||||
| -rw-r--r-- | fs/nfsd/state.h | 5 | ||||
| -rw-r--r-- | fs/nfsd/trace.h | 19 | ||||
| -rw-r--r-- | include/uapi/linux/nfsd_netlink.h | 8 |
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) |
