[PATCH 7/7] selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips

From: Jeff Layton

Date: Mon Aug 10 2026 - 13:45:24 EST


Creating a listener registers with rpcbind: svc_xprt_create_from_sa() passes
flags of 0, so pmap_register is true in svc_setup_socket(), and
nfsd_version3 is enabled by default and does not set vs_rpcb_optnl, so a
registration failure aborts listener creation. A fresh netns has no rpcbind,
and reaching the host's is not an option -- svc_rpcb_setup() opens by
calling svc_unregister(), which would clear the host's nfsd entries.

Serve it from within the namespace instead. The abstract AF_LOCAL name the
kernel tries first is per-netns (unix_find_abstract() takes a struct net),
so bind "\0/run/rpcbind.sock" and fork a minimal responder:

- arguments are never decoded; the NULL procedure gets an empty success and
SET/UNSET get TRUE
- RPCBVERS_4 is answered as well as RPCBVERS_2, because
__svc_rpcb_register6() turns a v4 refusal into -EAFNOSUPPORT and that
would fail every IPv6 listener
- PR_SET_PDEATHSIG plus an explicit kill in FIXTURE_TEARDOWN, so no stub
outlives its test

With that in place, add the tests that need a serv: create/add/remove and
LISTENER_GET round-trips (tcp, udp, multi, idempotent re-set, subset
removal, empty-list serv destroy, IPv6), the empty-list request, and the
-EBUSY refusal once THREADS_SET has started threads.

Two of the new tests exist to catch a revert rather than to describe the
interface, since neither is visible in the errno alone:

- val_reject_keeps_listeners. An unknown transport name ends in
-EPROTONOSUPPORT either way, because svc_xprt_create_from_sa() returns
that too. What differs is that without the up-front check
nfsd_nl_listener_set_doit() has already destroyed the listeners that did
not match by the time the name fails.
- sem_register_refused, which restarts the stub in a mode that answers
RPCBPROC_SET with FALSE. rpcb_register_call() turns that into -EACCES,
which must reach userland and leave no listener behind. On
CONFIG_NFS_LOCALIO=y it does not, unless svc_register() keeps the first
error: nfslocalio is last in nfsd_programs and its NULL and vs_hidden
versions both report success, overwriting the failure.

Signed-off-by: Jeff Layton <jlayton@xxxxxxxxxx>
Assisted-by: LLM
---
.../testing/selftests/nfsd/nfsd_netlink_listener.c | 444 ++++++++++++++++++++-
1 file changed, 438 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
index ae28c224255f..3e3307680d7d 100644
--- a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -3,30 +3,40 @@
* Regression tests for the NFSD generic-netlink listener interface
* (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).
*
- * These cover the request validation that nfsd_nl_validate_listeners() does
- * before nfsd_mutex is taken: bad or absent transport name, missing address,
- * truncated or unsupported sockaddr, oversized list. None of them reach
- * nfsd_create_serv(), so nothing here creates a serv or talks to rpcbind.
+ * Three groups:
+ * validation - malformed/abusive LISTENER_SET requests are rejected by
+ * nfsd_nl_validate_listeners(), before nfsd_mutex is taken.
+ * functional - create/add/remove listeners and verify LISTENER_GET
+ * reflects the set (round-trip of transport + addr:port).
+ * semantics - once threads are running (THREADS_SET) a listener change
+ * is refused with -EBUSY.
*
* Each test runs in its own private net + mount namespace (unshare in
* FIXTURE_SETUP). /run is masked there: a pathname AF_LOCAL connect is not
* scoped by the network namespace, since unix_find_bsd() resolves by inode
* and takes no struct net, so the kernel's rpcbind client would otherwise be
- * able to reach the rpcbind running on the host.
+ * able to reach the rpcbind running on the host. Anything that creates a
+ * serv is served by the per-netns rpcbind stub below instead.
*/
#define _GNU_SOURCE
#include <errno.h>
+#include <poll.h>
#include <sched.h>
+#include <signal.h>
+#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
+#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
+#include <sys/un.h>
+#include <sys/wait.h>
#include <net/if.h>
#include <netinet/in.h>
#include <linux/netlink.h>
@@ -36,8 +46,10 @@

/* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */
#define NFSD_FAMILY_NAME "nfsd"
+#define NFSD_CMD_THREADS_SET 2
#define NFSD_CMD_LISTENER_SET 6
#define NFSD_CMD_LISTENER_GET 7
+#define NFSD_A_SERVER_THREADS 1
#define NFSD_A_SERVER_SOCK_ADDR 1 /* per-listener nest */
#define NFSD_A_SOCK_ADDR 1 /* inside the nest */
#define NFSD_A_SOCK_TRANSPORT_NAME 2 /* inside the nest */
@@ -327,10 +339,233 @@ static int listener_get(struct listener_ent *out, int max)
return parse_listener_get(rbuf, n, out, max);
}

+static struct listener_ent *find_listener(struct listener_ent *e, int n,
+ const char *xprt, int family,
+ uint16_t port)
+{
+ int i;
+
+ for (i = 0; i < n; i++)
+ if (e[i].family == family && e[i].port == port &&
+ !strcmp(e[i].xprt, xprt))
+ return &e[i];
+ return NULL;
+}
+
+/* Start (@n > 0) or stop (@n == 0) nfsd threads in this netns. */
+static int threads_set(int n)
+{
+ char attrs[64];
+ uint32_t v = n;
+ int off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, &v, sizeof(v));
+
+ return genl_request(NFSD_CMD_THREADS_SET, attrs, off);
+}
+
+/* ------------------- per-netns local rpcbind stub ------------------- */
+
+/*
+ * Creating a listener registers with rpcbind: svc_xprt_create_from_sa()
+ * passes flags of 0, so pmap_register is true in svc_setup_socket(), and
+ * nfsd_version3 is registerable by default and does not set vs_rpcb_optnl,
+ * so a registration failure aborts listener creation. The abstract AF_LOCAL
+ * name the kernel tries first is per-netns (unix_find_abstract() takes a
+ * struct net), so answer it here and stay out of the host's rpcbind.
+ *
+ * Arguments are never decoded. The NULL procedure gets an empty success and
+ * SET/UNSET get TRUE, for both RPCBVERS_2 and RPCBVERS_4. v4 has to be
+ * answered because __svc_rpcb_register6() turns a v4 refusal into
+ * -EAFNOSUPPORT, which would fail every IPv6 listener.
+ *
+ * In RPCB_STUB_REFUSE mode SET is answered FALSE instead, which
+ * rpcb_register_call() reports as -EACCES. UNSET is left alone: only
+ * svc_unregister() issues it, and it discards the result.
+ */
+#define RPCB_PROGRAM 100000
+#define RPCB_PROC_NULL 0
+#define RPCB_PROC_SET 1
+#define RPCB_PROC_UNSET 2
+#define RPCB_ABSTRACT_NAME "/run/rpcbind.sock"
+#define RPCB_STUB_MAXCONN 4
+
+enum { RPCB_STUB_ACCEPT, RPCB_STUB_REFUSE };
+
+static int rpcb_stub_listen(void)
+{
+ struct sockaddr_un sun = { .sun_family = AF_UNIX };
+ size_t nlen = strlen(RPCB_ABSTRACT_NAME);
+ socklen_t alen;
+ int fd;
+
+ /* Abstract names are length-delimited, so the length must match. */
+ memcpy(sun.sun_path + 1, RPCB_ABSTRACT_NAME, nlen);
+ alen = offsetof(struct sockaddr_un, sun_path) + 1 + nlen;
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0)
+ return -1;
+ if (bind(fd, (struct sockaddr *)&sun, alen) < 0 ||
+ listen(fd, RPCB_STUB_MAXCONN) < 0) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int rpcb_stub_read(int fd, void *buf, size_t len)
+{
+ size_t done = 0;
+
+ while (done < len) {
+ ssize_t n = read(fd, (char *)buf + done, len - done);
+
+ if (n <= 0)
+ return -1;
+ done += n;
+ }
+ return 0;
+}
+
+/* Handle one record-marked RPC call. Returns -1 when the peer is done. */
+static int rpcb_stub_call(int fd, int mode)
+{
+ uint32_t mark, call[6], rep[7];
+ unsigned int len, nrep = 6;
+ size_t replen;
+
+ if (rpcb_stub_read(fd, &mark, sizeof(mark)))
+ return -1;
+ len = ntohl(mark) & 0x7fffffff;
+ if (len < sizeof(call) || len > 4096)
+ return -1;
+ if (rpcb_stub_read(fd, call, sizeof(call)))
+ return -1;
+
+ /* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */
+ for (len -= sizeof(call); len; ) {
+ char sink[256];
+ unsigned int n = len > sizeof(sink) ? sizeof(sink) : len;
+
+ if (rpcb_stub_read(fd, sink, n))
+ return -1;
+ len -= n;
+ }
+
+ rep[0] = call[0]; /* xid */
+ rep[1] = htonl(1); /* REPLY */
+ rep[2] = htonl(0); /* MSG_ACCEPTED */
+ rep[3] = htonl(0); /* verifier flavor AUTH_NULL */
+ rep[4] = htonl(0); /* verifier length */
+ rep[5] = htonl(0); /* SUCCESS */
+
+ if (ntohl(call[3]) != RPCB_PROGRAM) {
+ rep[5] = htonl(1); /* PROG_UNAVAIL */
+ } else {
+ switch (ntohl(call[5])) {
+ case RPCB_PROC_NULL:
+ break;
+ case RPCB_PROC_SET:
+ rep[6] = htonl(mode == RPCB_STUB_REFUSE ? 0 : 1);
+ nrep = 7;
+ break;
+ case RPCB_PROC_UNSET:
+ rep[6] = htonl(1); /* TRUE */
+ nrep = 7;
+ break;
+ default:
+ rep[5] = htonl(3); /* PROC_UNAVAIL */
+ }
+ }
+
+ replen = nrep * sizeof(rep[0]);
+ mark = htonl(0x80000000 | replen);
+ if (write(fd, &mark, sizeof(mark)) != (ssize_t)sizeof(mark) ||
+ write(fd, rep, replen) != (ssize_t)replen)
+ return -1;
+ return 0;
+}
+
+static void rpcb_stub_serve(int lfd, int mode)
+{
+ struct pollfd pfd[1 + RPCB_STUB_MAXCONN];
+ nfds_t n = 1, i;
+
+ pfd[0].fd = lfd;
+
+ for (;;) {
+ /* stop polling the listener when full, or poll() spins */
+ pfd[0].events = n < 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;
+
+ if (poll(pfd, n, -1) < 0)
+ return;
+
+ if (pfd[0].revents & POLLIN) {
+ int c = accept(lfd, NULL, NULL);
+
+ if (c >= 0) {
+ pfd[n].fd = c;
+ pfd[n].events = POLLIN;
+ n++;
+ }
+ }
+
+ for (i = 1; i < n; i++) {
+ if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR)))
+ continue;
+ if (rpcb_stub_call(pfd[i].fd, mode)) {
+ close(pfd[i].fd);
+ pfd[i] = pfd[--n];
+ }
+ }
+ }
+}
+
+/* Returns the stub's pid, or -1. The socket is listening before we fork. */
+static pid_t rpcb_stub_start(int mode)
+{
+ int lfd = rpcb_stub_listen();
+ pid_t pid;
+
+ if (lfd < 0)
+ return -1;
+
+ pid = fork();
+ if (pid < 0) {
+ close(lfd);
+ return -1;
+ }
+ if (pid == 0) {
+ signal(SIGPIPE, SIG_IGN);
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ if (getppid() == 1) /* raced with parent exit */
+ _exit(0);
+ rpcb_stub_serve(lfd, mode);
+ _exit(0);
+ }
+
+ close(lfd);
+ return pid;
+}
+
+/*
+ * Swap the stub for one in @mode. Safe before the first request: no serv
+ * exists yet, so the kernel has not connected and the abstract name is free
+ * again once the old stub has been reaped.
+ */
+static int rpcb_stub_restart(pid_t *pid, int mode)
+{
+ if (*pid > 0) {
+ kill(*pid, SIGKILL);
+ waitpid(*pid, NULL, 0);
+ }
+ *pid = rpcb_stub_start(mode);
+ return *pid > 0 ? 0 : -1;
+}
+
/* --------------------------- fixture --------------------------- */

FIXTURE(nfsd_listener) {
- int placeholder;
+ pid_t rpcbd;
};

FIXTURE_SETUP(nfsd_listener)
@@ -369,14 +604,28 @@ FIXTURE_SETUP(nfsd_listener)
nfsd_family = genl_resolve_nfsd();
if (nfsd_family < 0)
SKIP(return, "nfsd genl family not found (modprobe nfsd?)");
+
+ self->rpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);
+ if (self->rpcbd < 0)
+ SKIP(return, "cannot start the rpcbind stub: %s",
+ strerror(errno));
}

FIXTURE_TEARDOWN(nfsd_listener)
{
+ if (self->rpcbd > 0) {
+ kill(self->rpcbd, SIGKILL);
+ waitpid(self->rpcbd, NULL, 0);
+ }
}

/* ===================== validation / negative ===================== */

+TEST_F(nfsd_listener, val_empty_list_ok)
+{
+ EXPECT_EQ(0, listener_set(NULL, 0));
+}
+
TEST_F(nfsd_listener, val_too_many)
{
static char attrs[1 << 20];
@@ -477,6 +726,33 @@ TEST_F(nfsd_listener, val_second_entry_bad)
EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
}

+/*
+ * A rejected request must leave the listeners that are already up alone.
+ * The errno alone does not show that: svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. What differs is how far the
+ * request gets -- without the check in nfsd_nl_validate_listeners(),
+ * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener
+ * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the
+ * name fails.
+ */
+TEST_F(nfsd_listener, val_reject_keeps_listeners)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char good[64], bad[64];
+ int og = put_listener(good, 0, "tcp", TEST_PORT);
+ int ob = put_listener(bad, 0, "bogus_xprt", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(good, og));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+
+ EXPECT_EQ(-EPROTONOSUPPORT, listener_set(bad, ob));
+
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+/* ===================== functional / round-trip ===================== */
+
/* LISTENER_GET with no serv in this netns returns an empty list. */
TEST_F(nfsd_listener, func_get_empty)
{
@@ -485,4 +761,160 @@ TEST_F(nfsd_listener, func_get_empty)
EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
}

+TEST_F(nfsd_listener, func_create_tcp)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+ EXPECT_EQ(htonl(INADDR_LOOPBACK), got[0].a4.s_addr);
+}
+
+TEST_F(nfsd_listener, func_create_udp)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "udp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_multi)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[128];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ off = put_listener(attrs, off, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+ EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_idempotent)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_set(attrs, off)); /* re-set same list */
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_add)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char one[64], two[128];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+ int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+ o2 = put_listener(two, o2, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, listener_set(two, o2)); /* add udp, keep tcp */
+ ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+ EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_remove_subset)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char both[128], one[64];
+ int ob = put_listener(both, 0, "tcp", TEST_PORT);
+ int oo = put_listener(one, 0, "tcp", TEST_PORT);
+
+ ob = put_listener(both, ob, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(both, ob));
+ ASSERT_EQ(0, listener_set(one, oo)); /* drop udp */
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_empty_destroys)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_set(NULL, 0)); /* empty -> destroy serv */
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+TEST_F(nfsd_listener, func_ipv6)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off, s;
+
+ s = socket(AF_INET6, SOCK_STREAM, 0);
+ if (s < 0)
+ SKIP(return, "IPv6 unavailable: %s", strerror(errno));
+ close(s);
+
+ off = put_listener_af(attrs, 0, "tcp", AF_INET6, TEST_PORT);
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET6, TEST_PORT));
+ EXPECT_EQ(0, memcmp(&got[0].a6, &in6addr_loopback, sizeof(in6addr_loopback)));
+}
+
+/* ===================== rpcbind registration ===================== */
+
+/*
+ * A rpcbind that refuses the registration must fail listener creation,
+ * whatever CONFIG_NFS_LOCALIO is set to.
+ *
+ * The error has to survive svc_register()'s walk over sv_programs to get
+ * here. With CONFIG_NFS_LOCALIO=y the trailing nfslocalio program has only
+ * a NULL and a vs_hidden version, and svc_generic_rpcbind_set() reports 0
+ * for both, so an svc_register() that keeps the last result rather than the
+ * first hands back success and the listener comes up regardless.
+ */
+TEST_F(nfsd_listener, sem_register_refused)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, rpcb_stub_restart(&self->rpcbd, RPCB_STUB_REFUSE));
+
+ EXPECT_EQ(-EACCES, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/* ===================== threads / -EBUSY semantics ===================== */
+
+TEST_F(nfsd_listener, sem_busy_on_change)
+{
+ char one[64], two[128];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+ int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+ o2 = put_listener(two, o2, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, threads_set(1)); /* threads now running */
+ EXPECT_EQ(-EBUSY, listener_set(two, o2)); /* add refused */
+ threads_set(0); /* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+ char one[64];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, threads_set(1));
+ EXPECT_EQ(-EBUSY, listener_set(NULL, 0)); /* remove refused */
+ threads_set(0);
+}
+
TEST_HARNESS_MAIN

--
2.55.0