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 <[email protected]> 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

