Regression tests for the NFSD_CMD_LISTENER_SET checks that nfsd_nl_validate_listeners() runs before nfsd_mutex is taken: bad or absent transport name, missing address, truncated or unsupported sockaddr, bad address family, a malformed entry behind a well-formed one, and more than NFSD_NL_LISTENER_MAX entries. Plus a LISTENER_GET against an empty netns.
None of these reach nfsd_create_serv(), so nothing here creates a serv or registers with rpcbind. Tests that do need one come next, with a stub. Uses kselftest_harness.h so each test runs in its own net+mount namespace. /run is masked there: unix_find_bsd() resolves by inode and takes no struct net, so a "/var/run/rpcbind.sock" connect from this netns would otherwise reach the rpcbind on the host -- and svc_rpcb_setup() opens by calling svc_unregister(), which would clear the host's nfsd registrations. Signed-off-by: Jeff Layton <[email protected]> Assisted-by: LLM --- tools/testing/selftests/Makefile | 1 + tools/testing/selftests/nfsd/.gitignore | 1 + tools/testing/selftests/nfsd/Makefile | 6 + tools/testing/selftests/nfsd/config | 4 + .../testing/selftests/nfsd/nfsd_netlink_listener.c | 488 +++++++++++++++++++++ tools/testing/selftests/nfsd/settings | 1 + 6 files changed, 501 insertions(+) diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 8d4db2241cc2..5d615301d368 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -85,6 +85,7 @@ TARGETS += net/packetdrill TARGETS += net/ppp TARGETS += net/rds TARGETS += net/tcp_ao +TARGETS += nfsd TARGETS += nolibc TARGETS += pci_endpoint TARGETS += pcie_bwctrl diff --git a/tools/testing/selftests/nfsd/.gitignore b/tools/testing/selftests/nfsd/.gitignore new file mode 100644 index 000000000000..19e6dec04d8e --- /dev/null +++ b/tools/testing/selftests/nfsd/.gitignore @@ -0,0 +1 @@ +nfsd_netlink_listener diff --git a/tools/testing/selftests/nfsd/Makefile b/tools/testing/selftests/nfsd/Makefile new file mode 100644 index 000000000000..15ac65549d25 --- /dev/null +++ b/tools/testing/selftests/nfsd/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS += $(KHDR_INCLUDES) -Wall + +TEST_GEN_PROGS := nfsd_netlink_listener + +include ../lib.mk diff --git a/tools/testing/selftests/nfsd/config b/tools/testing/selftests/nfsd/config new file mode 100644 index 000000000000..e6945ff9551c --- /dev/null +++ b/tools/testing/selftests/nfsd/config @@ -0,0 +1,4 @@ +CONFIG_NET_NS=y +CONFIG_IPV6=y +CONFIG_NFSD=y +CONFIG_NFSD_V4=y diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c new file mode 100644 index 000000000000..ae28c224255f --- /dev/null +++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * 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. + * + * 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. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <sched.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mount.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <net/if.h> +#include <netinet/in.h> +#include <linux/netlink.h> +#include <linux/genetlink.h> + +#include "../kselftest_harness.h" + +/* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */ +#define NFSD_FAMILY_NAME "nfsd" +#define NFSD_CMD_LISTENER_SET 6 +#define NFSD_CMD_LISTENER_GET 7 +#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 */ + +#define NLA_ALIGN4(len) (((len) + 3) & ~3) +#define TEST_PORT 20049 +#define MAX_LISTENERS 8 +#define RECV_TIMEO_SEC 30 + +static int nfsd_family; /* set per-test in FIXTURE_SETUP */ + +static void die(const char *msg) +{ + perror(msg); + exit(1); +} + +/* ------------------- minimal generic-netlink plumbing ------------------- */ + +static int genl_open(void) +{ + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct timeval tv = { .tv_sec = RECV_TIMEO_SEC }; + int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); + + if (fd < 0) + die("socket(NETLINK_GENERIC)"); + if (bind(fd, (void *)&sa, sizeof(sa)) < 0) + die("bind(netlink)"); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + return fd; +} + +/* Append an attribute at @off; return the new (aligned) offset. */ +static int put_attr(char *buf, int off, uint16_t type, + const void *data, int len) +{ + struct nlattr *na = (void *)(buf + off); + + na->nla_type = type; + na->nla_len = NLA_HDRLEN + len; + if (len) + memcpy(buf + off + NLA_HDRLEN, data, len); + return off + NLA_ALIGN4(NLA_HDRLEN + len); +} + +/* Build a genl message header into @buf; return the offset past it. */ +static int genl_hdr(char *buf, uint16_t type, uint16_t flags, uint8_t cmd) +{ + struct nlmsghdr *nlh = (void *)buf; + struct genlmsghdr *gnl = (void *)(buf + NLMSG_HDRLEN); + + memset(buf, 0, NLMSG_HDRLEN + GENL_HDRLEN); + nlh->nlmsg_type = type; + nlh->nlmsg_flags = flags; + nlh->nlmsg_seq = 1; + gnl->cmd = cmd; + gnl->version = 1; + return NLMSG_HDRLEN + GENL_HDRLEN; +} + +/* Send an nfsd command with an ACK; return the ACK errno (<= 0). */ +static int genl_request(uint8_t cmd, const char *attrs, int attrs_len) +{ + char buf[1 << 20], rbuf[4096]; + struct nlmsghdr *nlh = (void *)buf; + int fd = genl_open(); + int off, n, ret; + + off = genl_hdr(buf, nfsd_family, NLM_F_REQUEST | NLM_F_ACK, cmd); + if (attrs_len) { + memcpy(buf + off, attrs, attrs_len); + off += attrs_len; + } + nlh->nlmsg_len = off; + + if (send(fd, buf, off, 0) < 0) + die("send(genl)"); + + n = recv(fd, rbuf, sizeof(rbuf), 0); + if (n < 0) + ret = (errno == EAGAIN || errno == EWOULDBLOCK) ? -ETIMEDOUT : -errno; + else if (((struct nlmsghdr *)rbuf)->nlmsg_type == NLMSG_ERROR) + ret = ((struct nlmsgerr *)NLMSG_DATA(rbuf))->error; + else + ret = 0; + close(fd); + return ret; +} + +/* Send a command and return the full reply message; -errno on failure. */ +static int genl_request_reply(uint8_t cmd, char *rbuf, size_t rlen) +{ + char buf[256]; + struct nlmsghdr *nlh = (void *)buf; + int fd = genl_open(); + int off, n, ret; + + off = genl_hdr(buf, nfsd_family, NLM_F_REQUEST, cmd); + nlh->nlmsg_len = off; + + if (send(fd, buf, off, 0) < 0) + die("send(genl reply)"); + + n = recv(fd, rbuf, rlen, 0); + if (n < 0) + ret = (errno == EAGAIN || errno == EWOULDBLOCK) ? -ETIMEDOUT : -errno; + else if (((struct nlmsghdr *)rbuf)->nlmsg_type == NLMSG_ERROR) + ret = ((struct nlmsgerr *)NLMSG_DATA(rbuf))->error; + else + ret = n; + close(fd); + return ret; +} + +/* Resolve the "nfsd" genl family id; -1 if not registered. */ +static int genl_resolve_nfsd(void) +{ + char buf[1024], rbuf[4096]; + struct nlmsghdr *nlh = (void *)buf; + struct nlmsghdr *rh = (void *)rbuf; + struct nlattr *na; + int fd, off, left, id = -1; + + fd = genl_open(); + off = genl_hdr(buf, GENL_ID_CTRL, NLM_F_REQUEST, CTRL_CMD_GETFAMILY); + off = put_attr(buf, off, CTRL_ATTR_FAMILY_NAME, + NFSD_FAMILY_NAME, sizeof(NFSD_FAMILY_NAME)); + nlh->nlmsg_len = off; + + if (send(fd, buf, off, 0) < 0) + die("send(GETFAMILY)"); + if (recv(fd, rbuf, sizeof(rbuf), 0) < 0) + die("recv(GETFAMILY)"); + close(fd); + + if (rh->nlmsg_type == NLMSG_ERROR) + return -1; + + na = (void *)((char *)NLMSG_DATA(rh) + GENL_HDRLEN); + left = rh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; + while (left >= (int)NLA_HDRLEN) { + if (na->nla_type == CTRL_ATTR_FAMILY_ID) { + id = *(uint16_t *)((char *)na + NLA_HDRLEN); + break; + } + left -= NLA_ALIGN4(na->nla_len); + na = (void *)((char *)na + NLA_ALIGN4(na->nla_len)); + } + return id; +} + +/* ------------------- listener request builders ------------------- */ + +/* Fine-grained control for negative tests: any field can be omitted/malformed. */ +struct raw_listener { + const char *xprt; /* NULL -> omit NFSD_A_SOCK_TRANSPORT_NAME */ + int emit_addr; /* 0 -> omit NFSD_A_SOCK_ADDR */ + const void *addr; + int addr_len; /* bytes to emit for NFSD_A_SOCK_ADDR */ +}; + +static int put_raw_listener(char *buf, int off, const struct raw_listener *r) +{ + struct nlattr *nest = (void *)(buf + off); + int inner = off + NLA_HDRLEN; + + if (r->emit_addr) + inner = put_attr(buf, inner, NFSD_A_SOCK_ADDR, r->addr, r->addr_len); + if (r->xprt) + inner = put_attr(buf, inner, NFSD_A_SOCK_TRANSPORT_NAME, + r->xprt, strlen(r->xprt) + 1); + nest->nla_type = NFSD_A_SERVER_SOCK_ADDR | NLA_F_NESTED; + nest->nla_len = inner - off; + return off + NLA_ALIGN4(nest->nla_len); +} + +/* Well-formed loopback listener for @family (AF_INET or AF_INET6). */ +static int put_listener_af(char *buf, int off, const char *xprt, int family, + uint16_t port) +{ + struct sockaddr_storage ss = {0}; + struct raw_listener r = { .xprt = xprt, .emit_addr = 1, .addr = &ss }; + + if (family == AF_INET6) { + struct sockaddr_in6 *s6 = (void *)&ss; + + s6->sin6_family = AF_INET6; + s6->sin6_port = htons(port); + s6->sin6_addr = in6addr_loopback; + r.addr_len = sizeof(*s6); + } else { + struct sockaddr_in *s4 = (void *)&ss; + + s4->sin_family = AF_INET; + s4->sin_port = htons(port); + s4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + r.addr_len = sizeof(*s4); + } + return put_raw_listener(buf, off, &r); +} + +static int put_listener(char *buf, int off, const char *xprt, uint16_t port) +{ + return put_listener_af(buf, off, xprt, AF_INET, port); +} + +/* ------------------- LISTENER_GET parsing ------------------- */ + +struct listener_ent { + char xprt[16]; + int family; + uint16_t port; + struct in_addr a4; + struct in6_addr a6; +}; + +static int parse_listener_get(const char *rbuf, int len, + struct listener_ent *out, int max) +{ + const struct nlmsghdr *nlh = (const void *)rbuf; + const struct nlattr *na; + int left, count = 0; + + (void)len; + na = (const void *)(rbuf + NLMSG_HDRLEN + GENL_HDRLEN); + left = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; + + while (left >= (int)NLA_HDRLEN) { + int alen = na->nla_len; + + if ((na->nla_type & NLA_TYPE_MASK) == NFSD_A_SERVER_SOCK_ADDR && + count < max) { + const struct nlattr *in = (const void *)((char *)na + NLA_HDRLEN); + int ileft = alen - NLA_HDRLEN; + struct listener_ent *e = &out[count]; + + memset(e, 0, sizeof(*e)); + while (ileft >= (int)NLA_HDRLEN) { + const void *d = (const char *)in + NLA_HDRLEN; + int t = in->nla_type & NLA_TYPE_MASK; + + if (t == NFSD_A_SOCK_TRANSPORT_NAME) { + strncpy(e->xprt, d, sizeof(e->xprt) - 1); + } else if (t == NFSD_A_SOCK_ADDR) { + const struct sockaddr_storage *ss = d; + + e->family = ss->ss_family; + if (ss->ss_family == AF_INET) { + const struct sockaddr_in *s = d; + + e->a4 = s->sin_addr; + e->port = ntohs(s->sin_port); + } else if (ss->ss_family == AF_INET6) { + const struct sockaddr_in6 *s = d; + + e->a6 = s->sin6_addr; + e->port = ntohs(s->sin6_port); + } + } + ileft -= NLA_ALIGN4(in->nla_len); + in = (const void *)((char *)in + NLA_ALIGN4(in->nla_len)); + } + count++; + } + left -= NLA_ALIGN4(alen); + na = (const void *)((char *)na + NLA_ALIGN4(alen)); + } + return count; +} + +/* ------------------- convenience wrappers ------------------- */ + +static int listener_set(const char *attrs, int len) +{ + return genl_request(NFSD_CMD_LISTENER_SET, attrs, len); +} + +/* Fetch the current listeners; returns count (>=0) or -errno. */ +static int listener_get(struct listener_ent *out, int max) +{ + char rbuf[8192]; + int n = genl_request_reply(NFSD_CMD_LISTENER_GET, rbuf, sizeof(rbuf)); + + if (n < 0) + return n; + return parse_listener_get(rbuf, n, out, max); +} + +/* --------------------------- fixture --------------------------- */ + +FIXTURE(nfsd_listener) { + int placeholder; +}; + +FIXTURE_SETUP(nfsd_listener) +{ + struct ifreq ifr = {0}; + struct stat st; + int s; + + if (geteuid() != 0) + SKIP(return, "must be run as root"); + if (unshare(CLONE_NEWNET | CLONE_NEWNS) < 0) + SKIP(return, "unshare(NEWNET|NEWNS): %s", strerror(errno)); + if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0) + SKIP(return, "mount(/ private): %s", strerror(errno)); + + /* + * Keep the kernel's rpcbind client inside this namespace. The + * abstract socket it tries first is per-netns, but the + * "/var/run/rpcbind.sock" fallback is not, so hide the path. + */ + if (mount("tmpfs", "/run", "tmpfs", 0, NULL) < 0) + SKIP(return, "mount(tmpfs on /run): %s", strerror(errno)); + if (lstat("/var/run", &st) == 0 && S_ISDIR(st.st_mode) && + mount("tmpfs", "/var/run", "tmpfs", 0, NULL) < 0) + SKIP(return, "mount(tmpfs on /var/run): %s", strerror(errno)); + + /* Bring loopback up so listener binds (127.0.0.1 / ::1) work. */ + s = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_GE(s, 0); + strcpy(ifr.ifr_name, "lo"); + ASSERT_EQ(0, ioctl(s, SIOCGIFFLAGS, &ifr)); + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + ASSERT_EQ(0, ioctl(s, SIOCSIFFLAGS, &ifr)); + close(s); + + nfsd_family = genl_resolve_nfsd(); + if (nfsd_family < 0) + SKIP(return, "nfsd genl family not found (modprobe nfsd?)"); +} + +FIXTURE_TEARDOWN(nfsd_listener) +{ +} + +/* ===================== validation / negative ===================== */ + +TEST_F(nfsd_listener, val_too_many) +{ + static char attrs[1 << 20]; + int i, off = 0; + + for (i = 0; i < 1025; i++) /* > NFSD_NL_LISTENER_MAX (1024) */ + off = put_listener(attrs, off, "udp", TEST_PORT); + EXPECT_EQ(-E2BIG, listener_set(attrs, off)); +} + +TEST_F(nfsd_listener, val_missing_addr) +{ + char attrs[64]; + struct raw_listener r = { .xprt = "tcp", .emit_addr = 0 }; + int off = put_raw_listener(attrs, 0, &r); + + EXPECT_EQ(-EINVAL, listener_set(attrs, off)); +} + +TEST_F(nfsd_listener, val_missing_transport) +{ + struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) }; + struct raw_listener r = { .xprt = NULL, .emit_addr = 1, + .addr = &s4, .addr_len = sizeof(s4) }; + char attrs[64]; + int off = put_raw_listener(attrs, 0, &r); + + EXPECT_EQ(-EINVAL, listener_set(attrs, off)); +} + +/* + * 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. + */ +TEST_F(nfsd_listener, val_bad_transport) +{ + char attrs[64]; + int off = put_listener(attrs, 0, "bogus_xprt", TEST_PORT); + + EXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off)); +} + +TEST_F(nfsd_listener, val_addr_too_short) +{ + unsigned char tiny = 0; + struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, + .addr = &tiny, .addr_len = 1 }; + char attrs[64]; + int off = put_raw_listener(attrs, 0, &r); + + EXPECT_EQ(-EINVAL, listener_set(attrs, off)); +} + +TEST_F(nfsd_listener, val_inet_short) +{ + struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) }; + struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s4, + .addr_len = sizeof(sa_family_t) + 2 }; + char attrs[64]; + int off = put_raw_listener(attrs, 0, &r); + + EXPECT_EQ(-EINVAL, listener_set(attrs, off)); +} + +TEST_F(nfsd_listener, val_inet6_short) +{ + struct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) }; + struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s6, + .addr_len = sizeof(struct sockaddr_in) }; + char attrs[64]; + int off = put_raw_listener(attrs, 0, &r); + + EXPECT_EQ(-EINVAL, listener_set(attrs, off)); +} + +TEST_F(nfsd_listener, val_bad_family) +{ + struct sockaddr_storage ss = { .ss_family = AF_UNIX }; + struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &ss, + .addr_len = sizeof(struct sockaddr_in) }; + char attrs[64]; + int off = put_raw_listener(attrs, 0, &r); + + EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off)); +} + +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) }; + 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)); +} + +/* LISTENER_GET with no serv in this netns returns an empty list. */ +TEST_F(nfsd_listener, func_get_empty) +{ + struct listener_ent got[MAX_LISTENERS]; + + EXPECT_EQ(0, listener_get(got, MAX_LISTENERS)); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings new file mode 100644 index 000000000000..6091b45d226b --- /dev/null +++ b/tools/testing/selftests/nfsd/settings @@ -0,0 +1 @@ +timeout=120 -- 2.55.0

