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>
This commit is contained in:
Chuck Lever 2026-04-19 14:53:06 -04:00 committed by Chuck Lever
parent ba0cde5dc8
commit 2eac189bb0
8 changed files with 184 additions and 0 deletions

View 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:

View 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[] = {

View 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,

View 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)
{

View 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

View 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)
{
}

View 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,

View 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)