From: Jeff Layton <jlayton@kernel.org>
To: "Steve Dickson" <steved@redhat.com>,
"Mantas Mikulėnas" <grawity@gmail.com>
Cc: Chuck Lever <cel@kernel.org>,
linux-nfs@vger.kernel.org, Jeff Layton <jlayton@kernel.org>
Subject: [PATCH nfs-utils v3 02/11] mountd: handle unmountable paths and junctions in the netlink downcall
Date: Mon, 14 Sep 2026 09:14:11 -0400 [thread overview]
Message-ID: <20260914-nl-crossmnt-v3-2-a984a6c94829@kernel.org> (raw)
In-Reply-To: <20260914-nl-crossmnt-v3-0-a984a6c94829@kernel.org>
Two things nfsd_export() does that cache_nl_process_export() did not:
- If is_mountpoint() fails with an error that isn't a plain lookup
failure, we can't tell whether the export is available. Defer the
request instead of answering "not exported".
- When no export matches, check for a junction before denying, so
referrals still work. Only when the client resolved, as nfsd_export()
does: client_check() dereferences the addrinfo for wildcard and
netgroup clients.
Split lookup_nonexport() so both downcalls share the junction lookup, and
factor the per-request work into nl_add_export_req() so the deferred path
can reuse it. Deferred requests go on their own list, deduped on
(client, path) and retried from cache_process() every RETRY_SEC. The
kernel keeps the upcall pending in the meantime.
Assisted-by: LLM
Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
support/export/cache.c | 324 +++++++++++++++++++++++++++++++++++++++----------
1 file changed, 258 insertions(+), 66 deletions(-)
diff --git a/support/export/cache.c b/support/export/cache.c
index e8b13409ad7a..0ad98c539407 100644
--- a/support/export/cache.c
+++ b/support/export/cache.c
@@ -784,6 +784,18 @@ struct delayed {
struct delayed *next;
} *delayed;
+/* Fold one retry queue head's deadline into the running minimum */
+static time_t retry_delay(time_t *last_attempt, time_t now, time_t delay)
+{
+ time_t d;
+
+ if (*last_attempt > now)
+ /* Clock updated - retry immediately */
+ *last_attempt = now - RETRY_SEC;
+ d = *last_attempt + RETRY_SEC - now;
+ return d < delay ? d : delay;
+}
+
static int nfsd_handle_fh(int f, char *bp, int blen)
{
/* request are:
@@ -1162,8 +1174,18 @@ static int export_attrs_build(struct export_attrs *ea, char *path,
* NFSD_CMD_SVC_EXPORT_SET_REQS.
*/
static nfs_export *lookup_export(char *dom, char *path, struct addrinfo *ai);
+static struct exportent *lookup_nonexport_ent(char *dom, char *path,
+ struct addrinfo *ai);
static struct nl_msg *cache_nl_new_msg(int family, int cmd, int flags);
+static void free_junction(struct exportent *eep)
+{
+ if (!eep)
+ return;
+ exportent_release(eep);
+ free(eep);
+}
+
static struct nl_sock *nfsd_nl_notify_sock; /* multicast notifications */
static struct nl_sock *nfsd_nl_cmd_sock; /* GET_REQS / SET_REQS commands */
static int nfsd_nl_family;
@@ -1632,6 +1654,197 @@ static int cache_nl_set_reqs(struct nl_sock *sock, struct nl_msg *msg)
return ret;
}
+static bool nl_msg_has_reqs(struct nl_msg *msg)
+{
+ return genlmsg_attrlen(nlmsg_data(nlmsg_hdr(msg)), 0) > 0;
+}
+
+enum export_result {
+ EXPORT_ANSWERED,
+ EXPORT_RETRY, /* not resolvable yet, ask again later */
+ EXPORT_NOMEM, /* *msgp is gone, caller must give up */
+};
+
+/*
+ * Resolve one svc_export request and append the answer to *msgp, sending
+ * and replacing the message if it fills up.
+ */
+static enum export_result nl_add_export_req(struct nl_msg **msgp, char *dom,
+ char *path)
+{
+ struct addrinfo *ai = NULL;
+ nfs_export *found = NULL;
+ struct exportent *epp = NULL;
+ struct exportent *junction = NULL;
+ enum export_result res = EXPORT_ANSWERED;
+ int ttl = 0;
+
+ if (is_ipaddr_client(dom)) {
+ ai = lookup_client_addr(dom);
+ if (!ai)
+ xlog(D_AUTH, "%s: failed to resolve client %s",
+ __func__, dom);
+ }
+
+ if (ai || !is_ipaddr_client(dom)) {
+ found = lookup_export(dom, path, ai);
+ if (!found) {
+ junction = lookup_nonexport_ent(dom, path, ai);
+ epp = junction;
+ }
+ }
+
+ if (found) {
+ char *mp = found->m_export.e_mountpoint;
+
+ if (mp && !*mp)
+ mp = found->m_export.e_path;
+ errno = 0;
+ if (mp && !is_mountpoint(mp)) {
+ /*
+ * A strange error means we can't tell whether it is a
+ * mountpoint. Retry later rather than answer wrongly.
+ */
+ if (errno != 0 && !path_lookup_error(errno)) {
+ res = EXPORT_RETRY;
+ goto out;
+ }
+ /* Exportpoint is not mounted, so tell kernel it
+ * is not available. This will cause it not to
+ * appear in the V4 Pseudo-root, so a "mount" of
+ * this path will fail, just like with V3.
+ */
+ xlog(L_WARNING,
+ "Cannot export path '%s': not a mountpoint", path);
+ ttl = 60;
+ } else {
+ epp = &found->m_export;
+ }
+ }
+
+ if (nfsd_nl_add_export(*msgp, dom, path, epp, ttl) < 0) {
+ cache_nl_set_reqs(nfsd_nl_cmd_sock, *msgp);
+ nlmsg_free(*msgp);
+ *msgp = cache_nl_new_msg(nfsd_nl_family,
+ NFSD_CMD_SVC_EXPORT_SET_REQS, 0);
+ if (!*msgp) {
+ res = EXPORT_NOMEM;
+ goto out;
+ }
+ if (nfsd_nl_add_export(*msgp, dom, path, epp, ttl) < 0)
+ xlog(L_WARNING, "%s: skipping oversized entry for %s",
+ __func__, path);
+ }
+out:
+ free_junction(junction);
+ nfs_freeaddrinfo(ai);
+ return res;
+}
+
+/*
+ * is_mountpoint() can fail with a strange error - the ETIMEDOUT a re-exported
+ * "softerr" NFS mount can give, say - leaving us unable to say whether the
+ * path is exportable. Set the request aside and try again later.
+ */
+struct delayed_export {
+ char *client;
+ char *path;
+ time_t last_attempt;
+ struct delayed_export *next;
+};
+
+static struct delayed_export *delayed_export;
+
+static void delayed_export_enqueue(struct delayed_export *d)
+{
+ struct delayed_export **dp = &delayed_export;
+
+ d->last_attempt = time(NULL);
+ d->next = NULL;
+ while (*dp)
+ dp = &(*dp)->next;
+ *dp = d;
+}
+
+static void delayed_export_free(struct delayed_export *d)
+{
+ free(d->client);
+ free(d->path);
+ free(d);
+}
+
+static void delayed_export_flush(void)
+{
+ while (delayed_export) {
+ struct delayed_export *d = delayed_export;
+
+ delayed_export = d->next;
+ delayed_export_free(d);
+ }
+}
+
+static void nl_delay_export(char *dom, char *path)
+{
+ struct delayed_export *d;
+
+ for (d = delayed_export; d; d = d->next)
+ if (!strcmp(d->client, dom) && !strcmp(d->path, path))
+ return;
+
+ d = calloc(1, sizeof(*d));
+ if (!d)
+ return;
+
+ d->client = strdup(dom);
+ d->path = strdup(path);
+ if (!d->client || !d->path) {
+ delayed_export_free(d);
+ return;
+ }
+
+ delayed_export_enqueue(d);
+}
+
+/*
+ * Retry the oldest deferred request if it is due. Entries are queued in
+ * time order, so only the head can be ready.
+ */
+static void nl_retry_export(void)
+{
+ struct delayed_export *d = delayed_export;
+ struct nl_msg *msg;
+
+ if (!d || d->last_attempt + RETRY_SEC > time(NULL))
+ return;
+
+ delayed_export = d->next;
+ d->next = NULL;
+
+ auth_reload();
+
+ msg = cache_nl_new_msg(nfsd_nl_family,
+ NFSD_CMD_SVC_EXPORT_SET_REQS, 0);
+ if (!msg) {
+ delayed_export_enqueue(d);
+ return;
+ }
+
+ switch (nl_add_export_req(&msg, d->client, d->path)) {
+ case EXPORT_ANSWERED:
+ if (nl_msg_has_reqs(msg))
+ cache_nl_set_reqs(nfsd_nl_cmd_sock, msg);
+ delayed_export_free(d);
+ break;
+ case EXPORT_RETRY:
+ delayed_export_enqueue(d);
+ break;
+ case EXPORT_NOMEM:
+ delayed_export_enqueue(d);
+ return; /* msg is already gone */
+ }
+ nlmsg_free(msg);
+}
+
static void cache_nl_process_export(void)
{
struct export_req *reqs = NULL;
@@ -1657,57 +1870,19 @@ static void cache_nl_process_export(void)
goto out_free;
for (i = 0; i < nreqs; i++) {
- char *dom = reqs[i].client;
- char *path = reqs[i].path;
- struct addrinfo *ai = NULL;
- nfs_export *found = NULL;
- struct exportent *epp = NULL;
- int ttl = 0;
-
- if (is_ipaddr_client(dom)) {
- ai = lookup_client_addr(dom);
- if (!ai)
- xlog(D_AUTH, "cache_nl_process_export: "
- "failed to resolve client %s", dom);
- }
-
- if (ai || !is_ipaddr_client(dom))
- found = lookup_export(dom, path, ai);
-
- if (found) {
- char *mp = found->m_export.e_mountpoint;
-
- if (mp && !*mp)
- mp = found->m_export.e_path;
- if (mp && !is_mountpoint(mp)) {
- xlog(L_WARNING,
- "Cannot export path '%s': not a mountpoint",
- path);
- ttl = 60;
- } else {
- epp = &found->m_export;
- }
- }
-
- if (nfsd_nl_add_export(msg, dom, path, epp, ttl) < 0) {
- cache_nl_set_reqs(nfsd_nl_cmd_sock, msg);
- nlmsg_free(msg);
- msg = cache_nl_new_msg(nfsd_nl_family,
- NFSD_CMD_SVC_EXPORT_SET_REQS, 0);
- if (!msg) {
- nfs_freeaddrinfo(ai);
- goto out_free;
- }
- if (nfsd_nl_add_export(msg, dom, path,
- epp, ttl) < 0)
- xlog(L_WARNING, "%s: skipping oversized "
- "entry for %s", __func__, path);
+ switch (nl_add_export_req(&msg, reqs[i].client, reqs[i].path)) {
+ case EXPORT_ANSWERED:
+ break;
+ case EXPORT_RETRY:
+ nl_delay_export(reqs[i].client, reqs[i].path);
+ break;
+ case EXPORT_NOMEM:
+ goto out_free;
}
-
- nfs_freeaddrinfo(ai);
}
- cache_nl_set_reqs(nfsd_nl_cmd_sock, msg);
+ if (nl_msg_has_reqs(msg))
+ cache_nl_set_reqs(nfsd_nl_cmd_sock, msg);
nlmsg_free(msg);
out_free:
@@ -2971,29 +3146,32 @@ out:
return exp;
}
-static void lookup_nonexport(int f, char *buf, int buflen, char *dom, char *path,
+static struct exportent *lookup_nonexport_ent(char *dom, char *path,
struct addrinfo *ai)
{
- struct exportent *eep;
-
- eep = lookup_junction(dom, path, ai);
- dump_to_cache(f, buf, buflen, dom, path, eep, 0);
- if (eep == NULL)
- return;
- exportent_release(eep);
- free(eep);
+ return lookup_junction(dom, path, ai);
}
#else /* !HAVE_JUNCTION_SUPPORT */
-static void lookup_nonexport(int f, char *buf, int buflen, char *dom, char *path,
- struct addrinfo *UNUSED(ai))
+static struct exportent *lookup_nonexport_ent(char *UNUSED(dom),
+ char *UNUSED(path), struct addrinfo *UNUSED(ai))
{
- dump_to_cache(f, buf, buflen, dom, path, NULL, 0);
+ return NULL;
}
#endif /* !HAVE_JUNCTION_SUPPORT */
+static void lookup_nonexport(int f, char *buf, int buflen, char *dom, char *path,
+ struct addrinfo *ai)
+{
+ struct exportent *eep;
+
+ eep = lookup_nonexport_ent(dom, path, ai);
+ dump_to_cache(f, buf, buflen, dom, path, eep, 0);
+ free_junction(eep);
+}
+
static void nfsd_export(int f)
{
/* requests are:
@@ -3201,13 +3379,15 @@ int cache_process(fd_set *readfds)
cache_set_fds(readfds);
v4clients_set_fds(readfds);
- if (delayed) {
+ if (delayed || delayed_export) {
time_t now = time(NULL);
- time_t delay;
- if (delayed->last_attempt > now)
- /* Clock updated - retry immediately */
- delayed->last_attempt = now - RETRY_SEC;
- delay = delayed->last_attempt + RETRY_SEC - now;
+ time_t delay = RETRY_SEC;
+
+ if (delayed)
+ delay = retry_delay(&delayed->last_attempt, now, delay);
+ if (delayed_export)
+ delay = retry_delay(&delayed_export->last_attempt, now,
+ delay);
if (delay < 0)
delay = 0;
tv.tv_sec = delay;
@@ -3225,6 +3405,8 @@ int cache_process(fd_set *readfds)
}
}
+ nl_retry_export();
+
switch (selret) {
case -1:
if (errno == EINTR || errno == ECONNREFUSED
@@ -3430,6 +3612,16 @@ cache_fork_workers(char *prog, int num_threads)
if (pid == 0) {
/* worker child */
+ /*
+ * cache_open() drains the netlink downcalls before we
+ * get here, so anything it deferred is now on the retry
+ * queue of every worker. Let the first worker own
+ * those, or they get answered once per worker over the
+ * shared command socket.
+ */
+ if (i > 0)
+ delayed_export_flush();
+
/* Re-enable the default action on SIGTERM et al
* so that workers die naturally when sent them.
* Only the parent unregisters with pmap and
--
2.55.0
next prev parent reply other threads:[~2026-09-14 13:14 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-14 13:14 [PATCH nfs-utils v3 00/11] mountd: bugfixes for up/downcall interfaces Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 01/11] mountd: factor out the per-path export attribute computation Jeff Layton
2026-09-14 13:14 ` Jeff Layton [this message]
2026-09-14 13:14 ` [PATCH nfs-utils v3 03/11] mountd: answer requests the kernel rejects on the netlink downcall Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 04/11] mountd: don't leak the parent export's fsid onto crossmnt submounts Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 05/11] mountd: retry unresolvable fsid lookups on the netlink downcall Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 06/11] mountd: bound the junction path before copying it into e_path Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 07/11] mountd: give each worker its own netlink command socket Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 08/11] mountd: retry export attributes that fail to resolve for a passing reason Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 09/11] mountd: drop a deferred fsid lookup once it has been answered Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 10/11] mountd: bound the retry queues Jeff Layton
2026-09-14 13:14 ` [PATCH nfs-utils v3 11/11] mountd: don't warn about pipefs submounts nobody asked to export Jeff Layton
2026-09-15 9:24 ` [PATCH nfs-utils v3 00/11] mountd: bugfixes for up/downcall interfaces Mantas Mikulėnas
2026-09-17 7:21 ` Steve Dickson
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=20260914-nl-crossmnt-v3-2-a984a6c94829@kernel.org \
--to=jlayton@kernel.org \
--cc=cel@kernel.org \
--cc=grawity@gmail.com \
--cc=linux-nfs@vger.kernel.org \
--cc=steved@redhat.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