public inbox for linux-nfs@vger.kernel.org
 help / color / mirror / Atom feed
From: Jeff Layton <jlayton@kernel.org>
To: Chuck Lever <cel@kernel.org>, NeilBrown <neilb@ownmail.net>,
	Olga Kornievskaia <okorniev@redhat.com>,
	Dai Ngo <dai.ngo@oracle.com>, Tom Talpey <tom@talpey.com>
Cc: linux-nfs@vger.kernel.org, Chuck Lever <chuck.lever@oracle.com>
Subject: Re: [PATCH v2] exportfs: release NFSv4 state when last client is unexported
Date: Tue, 07 Apr 2026 15:51:20 -0400	[thread overview]
Message-ID: <1949725ad3cebae2f38aa7cceed218f974b9f8a4.camel@kernel.org> (raw)
In-Reply-To: <20260406172653.1962-1-cel@kernel.org>

On Mon, 2026-04-06 at 13:26 -0400, Chuck Lever wrote:
> From: Chuck Lever <chuck.lever@oracle.com>
> 
> When exportfs -u removes the last client for a given export path,
> NFSv4 state (opens, locks, delegations) acquired through that
> export may still hold the underlying filesystem busy, preventing
> unmount.
> 
> After removing an export entry, check whether any active exports
> remain for the same path across all client types.  If none remain,
> send NFSD_CMD_UNLOCK_EXPORT to the kernel via generic netlink,
> which revokes the associated NFSv4 state and closes cached file
> handles.
> 
> This uses a new shared helper, nfsd_nl_cmd_str(), in
> support/nfs/nfsdnl.c that handles the netlink socket setup, message
> construction, and ACK handling for simple nfsd commands carrying
> a single string attribute.  The helper is conditionally compiled
> when libnl3 is available (gated by the existing nfsdctl configure
> option).
> 
> Also update the local nfsd_netlink.h copy with the
> NFSD_CMD_UNLOCK_IP, NFSD_CMD_UNLOCK_FILESYSTEM, and
> NFSD_CMD_UNLOCK_EXPORT command and attribute definitions.
> 
> Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
> ---
>  configure.ac                   |   2 +
>  support/include/nfsd_netlink.h |  24 +++++++
>  support/include/nfsdnl.h       |  34 +++++++++
>  support/nfs/Makefile.am        |   7 ++
>  support/nfs/nfsdnl.c           | 124 +++++++++++++++++++++++++++++++++
>  utils/exportfs/exportfs.c      |  65 ++++++++++++++++-
>  utils/exportfs/exportfs.man    |  17 +++++
>  7 files changed, 271 insertions(+), 2 deletions(-)
>  create mode 100644 support/include/nfsdnl.h
>  create mode 100644 support/nfs/nfsdnl.c
> 
> diff --git a/configure.ac b/configure.ac
> index 115c611fa5e3..8ca06fd62b47 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -256,6 +256,8 @@ PKG_CHECK_MODULES(LIBNL3, libnl-3.0 >= 3.1)
>  PKG_CHECK_MODULES(LIBNLGENL3, libnl-genl-3.0 >= 3.1)
>  
>  AC_CHECK_HEADERS(linux/nfsd_netlink.h)
> +AC_DEFINE([HAVE_NFSD_NETLINK], 1,
> +	  [Define to 1 if nfsd generic netlink support is available])
>  
>  # ensure we have the expkey attributes
>  AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <linux/nfsd_netlink.h>]],
> diff --git a/support/include/nfsd_netlink.h b/support/include/nfsd_netlink.h
> index 2d708d24cbd2..a6a831866be8 100644
> --- a/support/include/nfsd_netlink.h
> +++ b/support/include/nfsd_netlink.h
> @@ -128,6 +128,27 @@ enum {
>  	NFSD_A_POOL_MODE_MAX = (__NFSD_A_POOL_MODE_MAX - 1)
>  };
>  
> +enum {
> +	NFSD_A_UNLOCK_IP_ADDRESS = 1,
> +
> +	__NFSD_A_UNLOCK_IP_MAX,
> +	NFSD_A_UNLOCK_IP_MAX = (__NFSD_A_UNLOCK_IP_MAX - 1)
> +};
> +
> +enum {
> +	NFSD_A_UNLOCK_FILESYSTEM_PATH = 1,
> +
> +	__NFSD_A_UNLOCK_FILESYSTEM_MAX,
> +	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_A_FSLOCATION_HOST = 1,
>  	NFSD_A_FSLOCATION_PATH,
> @@ -229,6 +250,9 @@ enum {
>  	NFSD_CMD_EXPKEY_GET_REQS,
>  	NFSD_CMD_EXPKEY_SET_REQS,
>  	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)
> diff --git a/support/include/nfsdnl.h b/support/include/nfsdnl.h
> new file mode 100644
> index 000000000000..352801e5cc43
> --- /dev/null
> +++ b/support/include/nfsdnl.h
> @@ -0,0 +1,34 @@
> +/*
> + * Helper for sending nfsd generic netlink commands.
> + *
> + * Used by both nfsdctl and exportfs.
> + */
> +
> +#ifndef NFS_UTILS_NFSDNL_H
> +#define NFS_UTILS_NFSDNL_H
> +
> +#ifdef HAVE_NFSD_NETLINK
> +
> +/**
> + * nfsd_nl_cmd_str - send an nfsd netlink command carrying a string attribute
> + * @cmd:   NFSD_CMD_* command number
> + * @attr:  NFSD_A_* attribute number
> + * @value: NUL-terminated string value for the attribute
> + *
> + * Opens a genetlink connection, resolves the "nfsd" family, sends a
> + * single "do" command with one string attribute, waits for the ACK,
> + * and cleans up.
> + *
> + * Returns 0 on success or a negative errno on failure.
> + */
> +int nfsd_nl_cmd_str(int cmd, int attr, const char *value);
> +
> +#else
> +
> +static inline int nfsd_nl_cmd_str(int cmd, int attr, const char *value)
> +{
> +	return -ENOSYS;
> +}
> +
> +#endif /* HAVE_NFSD_NETLINK */
> +#endif /* NFS_UTILS_NFSDNL_H */
> diff --git a/support/nfs/Makefile.am b/support/nfs/Makefile.am
> index 5bfd71a9c8da..64ad5d075b9e 100644
> --- a/support/nfs/Makefile.am
> +++ b/support/nfs/Makefile.am
> @@ -11,6 +11,13 @@ libnfs_la_SOURCES = exports.c rmtab.c xio.c rpcmisc.c rpcdispatch.c \
>  libnfs_la_LIBADD = libnfsconf.la -luuid
>  libnfs_la_CPPFLAGS = $(AM_CPPFLAGS) $(CPPFLAGS) -I$(top_srcdir)/support/reexport
>  
> +if CONFIG_NFSDCTL
> +libnfs_la_SOURCES += nfsdnl.c
> +libnfs_la_CPPFLAGS += $(LIBNL3_CFLAGS) $(LIBNLGENL3_CFLAGS) \
> +		      -I$(top_srcdir)/utils/nfsdctl
> +libnfs_la_LIBADD += $(LIBNL3_LIBS) $(LIBNLGENL3_LIBS)
> +endif
> +
>  libnfsconf_la_SOURCES = conffile.c xlog.c
>  
>  MAINTAINERCLEANFILES = Makefile.in
> diff --git a/support/nfs/nfsdnl.c b/support/nfs/nfsdnl.c
> new file mode 100644
> index 000000000000..ece0b57afd4b
> --- /dev/null
> +++ b/support/nfs/nfsdnl.c
> @@ -0,0 +1,124 @@
> +/*
> + * nfsdnl.c -- send nfsd generic netlink commands
> + *
> + * Helper shared by nfsdctl and exportfs.
> + */
> +
> +#ifdef HAVE_CONFIG_H
> +#include <config.h>
> +#endif
> +
> +#include <errno.h>
> +#include <string.h>
> +
> +#include <netlink/genl/genl.h>
> +#include <netlink/genl/ctrl.h>
> +#include <netlink/msg.h>
> +#include <netlink/attr.h>
> +
> +#include "xlog.h"
> +#include "nfsdnl.h"
> +
> +#ifdef USE_SYSTEM_NFSD_NETLINK_H
> +#include <linux/nfsd_netlink.h>
> +#else
> +#include "nfsd_netlink.h"
> +#endif
> +
> +#define NFSDNL_BUFSIZE	(4096)
> +
> +static int error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err,
> +			 void *arg)
> +{
> +	int *ret = arg;
> +	*ret = err->error;
> +	return NL_STOP;
> +}
> +
> +static int finish_handler(struct nl_msg *msg, void *arg)
> +{
> +	int *ret = arg;
> +	*ret = 0;
> +	return NL_SKIP;
> +}
> +
> +static int ack_handler(struct nl_msg *msg __attribute__((unused)),
> +		       void *arg)
> +{
> +	int *ret = arg;
> +	*ret = 0;
> +	return NL_STOP;
> +}
> +
> +/**
> + * nfsd_nl_cmd_str - send an nfsd netlink command carrying a string attribute
> + * @cmd:   NFSD_CMD_* command number
> + * @attr:  NFSD_A_* attribute number
> + * @value: NUL-terminated string value for the attribute
> + *
> + * Returns 0 on success or a negative errno on failure.
> + */
> +int nfsd_nl_cmd_str(int cmd, int attr, const char *value)
> +{
> +	struct genlmsghdr *ghdr;
> +	struct nl_sock *sock;
> +	struct nl_msg *msg;
> +	struct nl_cb *cb;
> +	int family;
> +	int ret;
> +
> +	sock = nl_socket_alloc();
> +	if (!sock)
> +		return -ENOMEM;
> +	if (genl_connect(sock)) {
> +		ret = -ECONNREFUSED;
> +		goto out_sock;
> +	}
> +	nl_socket_set_buffer_size(sock, NFSDNL_BUFSIZE, NFSDNL_BUFSIZE);
> +
> +	family = genl_ctrl_resolve(sock, NFSD_FAMILY_NAME);
> +	if (family < 0) {
> +		ret = family;
> +		goto out_sock;
> +	}
> +
> +	msg = nlmsg_alloc();
> +	if (!msg) {
> +		ret = -ENOMEM;
> +		goto out_sock;
> +	}
> +	if (!genlmsg_put(msg, 0, 0, family, 0, 0, 0, 0)) {
> +		ret = -ENOMEM;
> +		goto out_msg;
> +	}
> +
> +	ghdr = nlmsg_data(nlmsg_hdr(msg));
> +	ghdr->cmd = (__u8)cmd;
> +	nla_put_string(msg, attr, value);
> +
> +	cb = nl_cb_alloc(NL_CB_CUSTOM);
> +	if (!cb) {
> +		ret = -ENOMEM;
> +		goto out_msg;
> +	}
> +
> +	ret = nl_send_auto(sock, msg);
> +	if (ret < 0)
> +		goto out_cb;
> +
> +	ret = 1;
> +	nl_cb_err(cb, NL_CB_CUSTOM, error_handler, &ret);
> +	nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, finish_handler, &ret);
> +	nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, ack_handler, &ret);
> +
> +	while (ret > 0)
> +		nl_recvmsgs(sock, cb);
> +
> +out_cb:
> +	nl_cb_put(cb);
> +out_msg:
> +	nlmsg_free(msg);
> +out_sock:
> +	nl_socket_free(sock);
> +	return ret;
> +}
> diff --git a/utils/exportfs/exportfs.c b/utils/exportfs/exportfs.c
> index 93f0bcd7ad56..768d2db7ea8f 100644
> --- a/utils/exportfs/exportfs.c
> +++ b/utils/exportfs/exportfs.c
> @@ -39,6 +39,15 @@
>  #include "xlog.h"
>  #include "conffile.h"
>  #include "reexport.h"
> +#include "nfsdnl.h"
> +
> +#ifdef HAVE_NFSD_NETLINK
> +#ifdef USE_SYSTEM_NFSD_NETLINK_H
> +#include <linux/nfsd_netlink.h>
> +#else
> +#include "nfsd_netlink.h"
> +#endif
> +#endif
>  
>  #include <netlink/genl/genl.h>
>  #include <netlink/genl/ctrl.h>
> @@ -63,6 +72,7 @@ static void release_lockfile(void);
>  
>  static const char *lockfile = EXP_LOCKFILE;
>  static int _lockfd = -1;
> +static int f_unexport_all;
>  
>  /*
>   * If we aren't careful, changes made by exportfs can be lost
> @@ -246,7 +256,8 @@ main(int argc, char **argv)
>  	 * don't care about what should be exported, as that
>  	 * may require DNS lookups..
>  	 */
> -	if (! ( !f_export && f_all)) {
> +	f_unexport_all = !f_export && f_all;
> +	if (!f_unexport_all) {
>  		/* note: xtab_*_read does not update entries if they already exist,
>  		 * so this will not lose new options
>  		 */
> @@ -380,6 +391,26 @@ exportfs(char *arg, char *options, int verbose)
>  		xlog(L_ERROR, "Invalid export syntax: %s", arg);
>  }
>  
> +/*
> + * Check whether any active export remains for the given path across
> + * all client types.  Returns true if at least one export still has
> + * m_xtabent set.
> + */
> +static int
> +path_still_exported(const char *path, size_t nlen)
> +{
> +	nfs_export *exp;
> +	int i;
> +
> +	for (i = 0; i < MCL_MAXTYPES; i++)
> +		for (exp = exportlist[i].p_head; exp; exp = exp->m_next)
> +			if (exp->m_xtabent &&
> +			    strlen(exp->m_export.e_path) == nlen &&
> +			    strncmp(path, exp->m_export.e_path, nlen) == 0)
> +				return 1;
> +	return 0;
> +}
> +
>  static void
>  unexportfs_parsed(char *hname, char *path, int verbose)
>  {
> @@ -434,9 +465,39 @@ unexportfs_parsed(char *hname, char *path, int verbose)
>  		exp->m_mayexport = 0;
>  		success = 1;
>  	}
> -	if (!success)
> +	if (!success) {
>  		xlog(L_ERROR, "Could not find '%s:%s' to unexport.", hname, path);
> +		goto out;
> +	}
>  
> +	/*
> +	 * If no exports remain for this path, ask the kernel to
> +	 * revoke any NFSv4 state and close cached file handles
> +	 * associated with exports of this path.  This enables the
> +	 * underlying filesystem to be unmounted.
> +	 *
> +	 * Skip this during "exportfs -ua" -- that is a shutdown
> +	 * operation.  Clients should wait for nfsd to restart and
> +	 * reclaim state through the grace period rather than
> +	 * receiving NFS4ERR_ADMIN_REVOKED.
> +	 */
> +#ifdef HAVE_NFSD_NETLINK

I don't think you need this #ifdef. With the latest exportfs changes,
netlink support is pretty much required to build.

> +	if (!f_unexport_all && !path_still_exported(path, nlen)) {
> +		char pathbuf[NFS_MAXPATHLEN + 1];
> +		int ret;
> +
> +		memcpy(pathbuf, path, nlen);
> +		pathbuf[nlen] = '\0';
> +		ret = nfsd_nl_cmd_str(NFSD_CMD_UNLOCK_EXPORT,
> +				      NFSD_A_UNLOCK_EXPORT_PATH,
> +				      pathbuf);
> +		if (ret && ret != -ENOSYS)
> +			xlog(L_WARNING,
> +			     "Failed to release state for %s: %s",
> +			     pathbuf, strerror(-ret));
> +	}
> +#endif
> +out:
>  	nfs_freeaddrinfo(ai);
>  }
>  
> diff --git a/utils/exportfs/exportfs.man b/utils/exportfs/exportfs.man
> index 3737ee81ab27..b5e0c63a63f2 100644
> --- a/utils/exportfs/exportfs.man
> +++ b/utils/exportfs/exportfs.man
> @@ -256,6 +256,23 @@ pair. This deletes the specified entry from
>  .I /var/lib/nfs/etab
>  and removes the corresponding kernel entry (if any).
>  .PP
> +When the last client for a given export path is unexported,
> +.B exportfs
> +signals the kernel to revoke NFSv4 state (opens, locks, and
> +delegations) and release cached state for that path.
> +Without this revocation, retained state would prevent the
> +underlying filesystem from being unmounted.
> +Affected clients receive
> +.B NFS4ERR_ADMIN_REVOKED
> +errors for operations that use revoked state.
> +.PP
> +.B "exportfs \-ua"
> +does not revoke NFSv4 state, however.
> +If
> +.B nfsd
> +is then restarted, clients may reclaim state during the
> +grace period.
> +.PP
>  .SS Dumping the Export Table
>  Invoking
>  .B exportfs

The rest looks good.

Reviewed-by: Jeff Layton <jlayton@kernel.org>

      reply	other threads:[~2026-04-07 19:51 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-06 17:26 [PATCH v2] exportfs: release NFSv4 state when last client is unexported Chuck Lever
2026-04-07 19:51 ` Jeff Layton [this message]

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=1949725ad3cebae2f38aa7cceed218f974b9f8a4.camel@kernel.org \
    --to=jlayton@kernel.org \
    --cc=cel@kernel.org \
    --cc=chuck.lever@oracle.com \
    --cc=dai.ngo@oracle.com \
    --cc=linux-nfs@vger.kernel.org \
    --cc=neilb@ownmail.net \
    --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