[PATCH v2 8/8] selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips
From: Jeff Layton
Date: Tue Aug 11 2026 - 08:08:25 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
- connections accepted and calls answered are counted in a page shared with
the test
- 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.
Several of these exist to catch a revert rather than to describe the
interface, since none of them 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.
- val_bad_transport, extended to require that the stub saw no traffic.
Reaching svc_xprt_create_from_sa() means nfsd_create_serv() ran, and
svc_bind() pings rpcbind at client creation and then sweeps stale entries
with svc_unregister(), so silence is what shows the request was refused
up front.
- val_second_entry_bad, extended with a LISTENER_GET. -EAFNOSUPPORT is
also what svc_xprt_create_from_sa() returns for the bad entry, and the
doit keeps the listeners it did create, so the well-formed tcp entry
ahead of it would otherwise still be up.
- 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.
- func_empty_destroys uses the connection count. LISTENER_GET replies empty
for a destroyed serv and for a live one with no permsocks alike, but only
nfsd_destroy_serv() reaches svc_xprt_destroy_all(..., unregister=true) ->
svc_rpcb_cleanup() -> rpcb_put_local(), which drops the last user and
shuts the local client down. The next serv has to reconnect.
- sem_busy_on_change and sem_busy_on_remove read the listeners back, since
-EBUSY says nothing about what the doit did before it returned.
find_listener() matches the address as well as the transport, family and
port; every listener here is created on loopback, so a reply naming
0.0.0.0 has to fail.
Assisted-by: LLM
Signed-off-by: Jeff Layton <jlayton@xxxxxxxxxx>
---
.../testing/selftests/nfsd/nfsd_netlink_listener.c | 554 ++++++++++++++++++++-
1 file changed, 548 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..9972b0db45ff 100644
--- a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -3,30 +3,41 @@
* 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/mman.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 +47,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 +340,293 @@ static int listener_get(struct listener_ent *out, int max)
return parse_listener_get(rbuf, n, out, max);
}
+/*
+ * Every listener these tests create comes from put_listener_af(), so the
+ * address is always loopback. Match on it too: without that, a reply that
+ * gave the right transport and port on the wrong address (0.0.0.0, say)
+ * would pass.
+ */
+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))
+ continue;
+ if (family == AF_INET6) {
+ if (memcmp(&e[i].a6, &in6addr_loopback, sizeof(e[i].a6)))
+ continue;
+ } else if (e[i].a4.s_addr != htonl(INADDR_LOOPBACK)) {
+ continue;
+ }
+ 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.
+ *
+ * The stub also keeps counters in a page shared with the test, so a test can
+ * assert that the kernel never talked to rpcbind at all, or that it dropped
+ * the local rpcbind client and had to reconnect.
+ */
+#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 };
+
+struct rpcb_stub_stats {
+ unsigned int conns; /* connections accepted */
+ unsigned int calls; /* calls answered */
+};
+
+static volatile struct rpcb_stub_stats *rpcb_stats; /* MAP_SHARED */
+
+static int rpcb_stats_alloc(void)
+{
+ void *p = mmap(NULL, sizeof(*rpcb_stats), PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+ if (p == MAP_FAILED)
+ return -1;
+ rpcb_stats = p;
+ return 0;
+}
+
+/*
+ * The stub bumps these before it replies and the kernel waits for that reply,
+ * so whatever a netlink request provoked is visible once it returns.
+ */
+static int rpcb_calls(void)
+{
+ return rpcb_stats ? (int)rpcb_stats->calls : 0;
+}
+
+static int rpcb_conns(void)
+{
+ return rpcb_stats ? (int)rpcb_stats->conns : 0;
+}
+
+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;
+ }
+
+ if (rpcb_stats)
+ rpcb_stats->calls++;
+
+ 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;
+ /*
+ * poll() ran with the old n, so it did not
+ * write this revents. The loop below reads it.
+ */
+ pfd[n].revents = 0;
+ n++;
+ if (rpcb_stats)
+ rpcb_stats->conns++;
+ }
+ }
+
+ 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 +665,35 @@ FIXTURE_SETUP(nfsd_listener)
nfsd_family = genl_resolve_nfsd();
if (nfsd_family < 0)
SKIP(return, "nfsd genl family not found (modprobe nfsd?)");
+
+ if (rpcb_stats_alloc() < 0)
+ SKIP(return, "mmap(rpcbind stub counters): %s", strerror(errno));
+
+ 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);
+ }
+ if (rpcb_stats) {
+ munmap((void *)rpcb_stats, sizeof(*rpcb_stats));
+ rpcb_stats = NULL;
+ }
}
/* ===================== 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];
@@ -411,13 +728,21 @@ TEST_F(nfsd_listener, val_missing_transport)
* A name matching no transport class must be refused before nfsd_mutex is
* taken, so it never reaches svc_xprt_create_from_sa() and its
* request_module("svc%s", name) upcall.
+ *
+ * The errno cannot show that -- svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. The rpcbind traffic can:
+ * getting that far means nfsd_create_serv() ran, and svc_bind() pings
+ * rpcbind at client creation and then sweeps stale entries with
+ * svc_unregister(). A silent stub is the proof nothing was created.
*/
TEST_F(nfsd_listener, val_bad_transport)
{
char attrs[64];
int off = put_listener(attrs, 0, "bogus_xprt", TEST_PORT);
+ ASSERT_EQ(0, rpcb_calls());
EXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));
+ EXPECT_EQ(0, rpcb_calls());
}
TEST_F(nfsd_listener, val_addr_too_short)
@@ -469,14 +794,49 @@ TEST_F(nfsd_listener, val_second_entry_bad)
struct sockaddr_storage ss = { .ss_family = AF_UNIX };
struct raw_listener bad = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
.addr_len = sizeof(struct sockaddr_in) };
+ struct listener_ent got[MAX_LISTENERS];
char attrs[128];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
off = put_raw_listener(attrs, off, &bad);
/* The whole request is rejected during validation; nothing applied. */
EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+ /*
+ * Again the errno alone does not say so: svc_xprt_create_from_sa()
+ * also returns -EAFNOSUPPORT, and the doit keeps the listeners it did
+ * manage to create, so the well-formed tcp entry ahead of the bad one
+ * would still be up.
+ */
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
}
+/*
+ * 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 +845,186 @@ 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));
+}
+
+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));
+}
+
+/*
+ * LISTENER_GET cannot tell a destroyed serv from a live one with no
+ * permsocks: nfsd_nl_listener_get_doit() replies empty either way. The
+ * rpcbind client can. nfsd_destroy_serv() is the only path that reaches
+ * svc_xprt_destroy_all(..., unregister=true) -> svc_rpcb_cleanup() ->
+ * rpcb_put_local(), which drops the last user and shuts the local client
+ * down; the next serv then has to connect again. Leaving the serv in place
+ * would keep the first connection and the stub would see just the one.
+ */
+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);
+ int conns;
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ conns = rpcb_conns();
+ ASSERT_GT(conns, 0);
+
+ EXPECT_EQ(0, listener_set(NULL, 0)); /* empty -> destroy serv */
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_GT(rpcb_conns(), conns);
+}
+
+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));
+}
+
+/* ===================== 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)
+{
+ 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, threads_set(1)); /* threads now running */
+ EXPECT_EQ(-EBUSY, listener_set(two, o2)); /* add refused */
+
+ /* refused means refused: the udp listener must not have been added */
+ EXPECT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+
+ threads_set(0); /* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ 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 */
+
+ /* the doit moves the permsocks to a temp list before it can fail */
+ EXPECT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+
+ threads_set(0);
+}
+
TEST_HARNESS_MAIN
--
2.55.0