AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/11 12:17 flow patch-triage
1m Args:
null

Results:
{
  "EnableConfigs": [
    "NFSD",
    "NFSD_V4"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "rpcb_create_local"
  ],
  "KMSANReasoning": "The patch series primarily adds validation for netlink transport names, sets an extended ACK message on error, configures a timeout for local rpcbind clients, and adds error unwinding for RPC service registration. It does not introduce any new structures sent to user space, nor does it modify bounds checking or data length calculations in a way that could expose uninitialized memory. The netlink attribute payload accessed via `nla_data` is guaranteed to be null-terminated by the `NLA_NUL_STRING` policy, so `strcmp` will not read past the initialized buffer. Any potential out-of-bounds accesses or memory corruption bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch modifies the NFSD netlink listener configuration logic to validate transport names and limit the number of listeners. It also updates the local rpcbind client to use a specific timeout and modifies the sunrpc service registration logic to properly unwind registrations on failure. These changes affect core NFSD and sunrpc logic reachable from userspace via netlink and RPC operations.",
  "WorthFuzzing": true
}

1/1 2026/08/11 12:17 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 95e42039d503f9b0731fb044e87feb6e1a839415\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Tue Aug 11 12:17:16 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c\nindex 7e1d5a5d6511e..f73a2ea425571 100644\n--- a/fs/nfsd/nfsctl.c\n+++ b/fs/nfsd/nfsctl.c\n@@ -1973,21 +1973,39 @@ int nfsd_nl_version_get_doit(struct sk_buff *skb, struct genl_info *info)\n \treturn err;\n }\n \n+/*\n+ * Transport classes NFSD knows how to instantiate. Vetting the name here\n+ * keeps a bogus string from reaching svc_xprt_create_from_sa(), where an\n+ * unknown name triggers a request_module(\"svc%s\", name) upcall under\n+ * nfsd_mutex.\n+ */\n+static bool nfsd_nl_transport_supported(const char *name)\n+{\n+\tstatic const char * const supported[] = { \"tcp\", \"udp\", \"rdma\" };\n+\tint i;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(supported); i++)\n+\t\tif (!strcmp(name, supported[i]))\n+\t\t\treturn true;\n+\treturn false;\n+}\n+\n+/* Upper bound on the number of listeners a single request may carry. */\n+#define NFSD_NL_LISTENER_MAX\t1024\n+\n /**\n  * nfsd_nl_validate_listeners - sanity-check the listener list from userland\n  * @info: netlink metadata and command arguments\n  *\n- * Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that each entry\n- * is well-formed: it parses against the policy, carries both an address and\n- * a transport name, and the address is long enough for its family. Doing\n- * this up front lets the callers below assume every entry is valid and\n- * guarantees we make no changes when the request is malformed.\n+ * Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that the list is\n+ * not oversized and that each entry is well-formed.\n  *\n  * Return: 0 if every entry is valid, or a negative errno otherwise.\n  */\n static int nfsd_nl_validate_listeners(struct genl_info *info)\n {\n \tconst struct nlattr *attr;\n+\tunsigned int count = 0;\n \tint rem;\n \n \tnlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info-\u003enlhdr,\n@@ -1996,6 +2014,11 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)\n \t\tstruct sockaddr *sa;\n \t\tint err;\n \n+\t\tif (++count \u003e NFSD_NL_LISTENER_MAX) {\n+\t\t\tNL_SET_ERR_MSG(info-\u003eextack, \"too many listeners\");\n+\t\t\treturn -E2BIG;\n+\t\t}\n+\n \t\terr = nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\n \t\t\t\t       nfsd_sock_nl_policy, info-\u003eextack);\n \t\tif (err \u003c 0)\n@@ -2004,6 +2027,9 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)\n \t\tif (!tb[NFSD_A_SOCK_ADDR] || !tb[NFSD_A_SOCK_TRANSPORT_NAME])\n \t\t\treturn -EINVAL;\n \n+\t\tif (!nfsd_nl_transport_supported(nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])))\n+\t\t\treturn -EPROTONOSUPPORT;\n+\n \t\tsa = nla_data(tb[NFSD_A_SOCK_ADDR]);\n \t\tif (nla_len(tb[NFSD_A_SOCK_ADDR]) \u003c sizeof(sa-\u003esa_family))\n \t\t\treturn -EINVAL;\n@@ -2156,8 +2182,12 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n \t\tret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,\n \t\t\t\t\t      current_cred());\n \t\t/* always save the latest error */\n-\t\tif (ret \u003c 0)\n+\t\tif (ret \u003c 0) {\n+\t\t\tNL_SET_ERR_MSG_FMT(info-\u003eextack,\n+\t\t\t\t\t   \"cannot create %s listener: %d\",\n+\t\t\t\t\t   xcl_name, ret);\n \t\t\terr = ret;\n+\t\t}\n \t}\n \n \tif (!serv-\u003esv_nrthreads \u0026\u0026 list_empty(\u0026nn-\u003enfsd_serv-\u003esv_permsocks))\ndiff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c\nindex 6aa372188c86a..0aa376b82a526 100644\n--- a/net/sunrpc/rpcb_clnt.c\n+++ b/net/sunrpc/rpcb_clnt.c\n@@ -221,6 +221,16 @@ static void rpcb_set_local(struct net *net, struct rpc_clnt *clnt,\n # define SUN_LEN(ptr) (offsetof(struct sockaddr_un, sun_path)\t\t\\\n \t\t      + 1 + strlen((ptr)-\u003esun_path + 1))\n \n+/*\n+ * The kernel's rpcbind client talks only to the local rpcbind, over loopback\n+ * or a local AF_LOCAL socket, where a healthy rpcbind answers in microseconds.\n+ */\n+static const struct rpc_timeout rpcb_local_timeout = {\n+\t.to_initval\t= 1 * HZ,\n+\t.to_maxval\t= 1 * HZ,\n+\t.to_retries\t= 0,\n+};\n+\n /*\n  * Returns zero on success, otherwise a negative errno value\n  * is returned.\n@@ -238,6 +248,7 @@ static int rpcb_create_af_local(struct net *net,\n \t\t.version\t= RPCBVERS_2,\n \t\t.authflavor\t= RPC_AUTH_NULL,\n \t\t.cred\t\t= current_cred(),\n+\t\t.timeout\t= \u0026rpcb_local_timeout,\n \t\t/*\n \t\t * We turn off the idle timeout to prevent the kernel\n \t\t * from automatically disconnecting the socket.\n@@ -312,6 +323,7 @@ static int rpcb_create_local_net(struct net *net)\n \t\t.version\t= RPCBVERS_2,\n \t\t.authflavor\t= RPC_AUTH_UNIX,\n \t\t.cred\t\t= current_cred(),\n+\t\t.timeout\t= \u0026rpcb_local_timeout,\n \t\t.flags\t\t= RPC_CLNT_CREATE_NOPING,\n \t};\n \tstruct rpc_clnt *clnt, *clnt4;\ndiff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c\nindex 05c3e6e2f6594..7c414634c753b 100644\n--- a/net/sunrpc/svc.c\n+++ b/net/sunrpc/svc.c\n@@ -1183,6 +1183,29 @@ int svc_generic_rpcbind_set(struct net *net,\n }\n EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\n \n+/*\n+ * Undo the [program, version] registrations that this svc_register() call\n+ * already made, stopping at version @nvers of program @nprog.\n+ *\n+ * Note that rpcbind matches RPCBPROC_UNSET on [program, version, netid] and\n+ * ignores the address, so this clears the netid rather than the one port.\n+ * That is the granularity svc_delete_xprt() unregisters at as well.\n+ */\n+static void svc_unwind_register(const struct svc_serv *serv, struct net *net,\n+\t\t\t\tconst int family, const unsigned short proto,\n+\t\t\t\tunsigned int nprog, unsigned int nvers)\n+{\n+\tunsigned int p, i;\n+\n+\tfor (p = 0; p \u003c= nprog; p++) {\n+\t\tstruct svc_program *progp = \u0026serv-\u003esv_programs[p];\n+\t\tunsigned int last = p \u003c nprog ? progp-\u003epg_nvers : nvers;\n+\n+\t\tfor (i = 0; i \u003c last; i++)\n+\t\t\tprogp-\u003epg_rpcbind_set(net, progp, i, family, proto, 0);\n+\t}\n+}\n+\n /**\n  * svc_register - register an RPC service with the local portmapper\n  * @serv: svc_serv struct for the service to register\n@@ -1191,7 +1214,8 @@ EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\n  * @proto: transport protocol number to advertise\n  * @port: port to advertise\n  *\n- * Service is registered for any address in the passed-in protocol family\n+ * Service is registered for any address in the passed-in protocol family.\n+ * A @port of 0 unregisters instead, and then every program is attempted.\n  */\n int svc_register(const struct svc_serv *serv, struct net *net,\n \t\t const int family, const unsigned short proto,\n@@ -1208,13 +1232,21 @@ int svc_register(const struct svc_serv *serv, struct net *net,\n \t\tstruct svc_program *progp = \u0026serv-\u003esv_programs[p];\n \n \t\tfor (i = 0; i \u003c progp-\u003epg_nvers; i++) {\n+\t\t\tint ret;\n \n-\t\t\terror = progp-\u003epg_rpcbind_set(net, progp, i,\n+\t\t\tret = progp-\u003epg_rpcbind_set(net, progp, i,\n \t\t\t\t\tfamily, proto, port);\n-\t\t\tif (error \u003c 0) {\n+\t\t\tif (ret \u003c 0) {\n \t\t\t\tprintk(KERN_WARNING \"svc: failed to register \"\n \t\t\t\t\t\"%sv%u RPC service (errno %d).\\n\",\n-\t\t\t\t\tprogp-\u003epg_name, i, -error);\n+\t\t\t\t\tprogp-\u003epg_name, i, -ret);\n+\t\t\t\tif (port) {\n+\t\t\t\t\tsvc_unwind_register(serv, net, family,\n+\t\t\t\t\t\t\t    proto, p, i);\n+\t\t\t\t\treturn ret;\n+\t\t\t\t}\n+\t\t\t\tif (!error)\n+\t\t\t\t\terror = ret;\n \t\t\t\tbreak;\n \t\t\t}\n \t\t}\ndiff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile\nindex 84343fd1e354b..460508de9374f 100644\n--- a/tools/testing/selftests/Makefile\n+++ b/tools/testing/selftests/Makefile\n@@ -89,6 +89,7 @@ TARGETS += net/packetdrill\n TARGETS += net/ppp\n TARGETS += net/rds\n TARGETS += net/tcp_ao\n+TARGETS += nfsd\n TARGETS += nolibc\n TARGETS += pci_endpoint\n TARGETS += pcie_bwctrl\ndiff --git a/tools/testing/selftests/nfsd/.gitignore b/tools/testing/selftests/nfsd/.gitignore\nnew file mode 100644\nindex 0000000000000..19e6dec04d8e9\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/.gitignore\n@@ -0,0 +1 @@\n+nfsd_netlink_listener\ndiff --git a/tools/testing/selftests/nfsd/Makefile b/tools/testing/selftests/nfsd/Makefile\nnew file mode 100644\nindex 0000000000000..15ac65549d259\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/Makefile\n@@ -0,0 +1,6 @@\n+# SPDX-License-Identifier: GPL-2.0\n+CFLAGS += $(KHDR_INCLUDES) -Wall\n+\n+TEST_GEN_PROGS := nfsd_netlink_listener\n+\n+include ../lib.mk\ndiff --git a/tools/testing/selftests/nfsd/config b/tools/testing/selftests/nfsd/config\nnew file mode 100644\nindex 0000000000000..ab84523fbedf3\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/config\n@@ -0,0 +1,8 @@\n+CONFIG_NAMESPACES=y\n+CONFIG_NET_NS=y\n+CONFIG_SHMEM=y\n+CONFIG_TMPFS=y\n+CONFIG_UNIX=y\n+CONFIG_IPV6=y\n+CONFIG_NFSD=y\n+CONFIG_NFSD_V4=y\ndiff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c\nnew file mode 100644\nindex 0000000000000..9972b0db45ff9\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c\n@@ -0,0 +1,1030 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Regression tests for the NFSD generic-netlink listener interface\n+ * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).\n+ *\n+ * Three groups:\n+ *   validation  - malformed/abusive LISTENER_SET requests are rejected by\n+ *                 nfsd_nl_validate_listeners(), before nfsd_mutex is taken.\n+ *   functional  - create/add/remove listeners and verify LISTENER_GET\n+ *                 reflects the set (round-trip of transport + addr:port).\n+ *   semantics   - once threads are running (THREADS_SET) a listener change\n+ *                 is refused with -EBUSY.\n+ *\n+ * Each test runs in its own private net + mount namespace (unshare in\n+ * FIXTURE_SETUP). /run is masked there: a pathname AF_LOCAL connect is not\n+ * scoped by the network namespace, since unix_find_bsd() resolves by inode\n+ * and takes no struct net, so the kernel's rpcbind client would otherwise be\n+ * able to reach the rpcbind running on the host. Anything that creates a\n+ * serv is served by the per-netns rpcbind stub below instead.\n+ */\n+#define _GNU_SOURCE\n+#include \u003cerrno.h\u003e\n+#include \u003cpoll.h\u003e\n+#include \u003csched.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003cstddef.h\u003e\n+#include \u003cstdint.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003csys/mount.h\u003e\n+#include \u003csys/prctl.h\u003e\n+#include \u003csys/socket.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003csys/time.h\u003e\n+#include \u003csys/un.h\u003e\n+#include \u003csys/wait.h\u003e\n+#include \u003cnet/if.h\u003e\n+#include \u003cnetinet/in.h\u003e\n+#include \u003clinux/netlink.h\u003e\n+#include \u003clinux/genetlink.h\u003e\n+\n+#include \"../kselftest_harness.h\"\n+\n+/* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */\n+#define NFSD_FAMILY_NAME\t\t\"nfsd\"\n+#define NFSD_CMD_THREADS_SET\t\t2\n+#define NFSD_CMD_LISTENER_SET\t\t6\n+#define NFSD_CMD_LISTENER_GET\t\t7\n+#define NFSD_A_SERVER_THREADS\t\t1\n+#define NFSD_A_SERVER_SOCK_ADDR\t\t1\t/* per-listener nest */\n+#define NFSD_A_SOCK_ADDR\t\t1\t/* inside the nest */\n+#define NFSD_A_SOCK_TRANSPORT_NAME\t2\t/* inside the nest */\n+\n+#define NLA_ALIGN4(len)\t\t\t(((len) + 3) \u0026 ~3)\n+#define TEST_PORT\t\t\t20049\n+#define MAX_LISTENERS\t\t\t8\n+#define RECV_TIMEO_SEC\t\t\t30\n+\n+static int nfsd_family;\t\t\t/* set per-test in FIXTURE_SETUP */\n+\n+static void die(const char *msg)\n+{\n+\tperror(msg);\n+\texit(1);\n+}\n+\n+/* ------------------- minimal generic-netlink plumbing ------------------- */\n+\n+static int genl_open(void)\n+{\n+\tstruct sockaddr_nl sa = { .nl_family = AF_NETLINK };\n+\tstruct timeval tv = { .tv_sec = RECV_TIMEO_SEC };\n+\tint fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);\n+\n+\tif (fd \u003c 0)\n+\t\tdie(\"socket(NETLINK_GENERIC)\");\n+\tif (bind(fd, (void *)\u0026sa, sizeof(sa)) \u003c 0)\n+\t\tdie(\"bind(netlink)\");\n+\tsetsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, \u0026tv, sizeof(tv));\n+\treturn fd;\n+}\n+\n+/* Append an attribute at @off; return the new (aligned) offset. */\n+static int put_attr(char *buf, int off, uint16_t type,\n+\t\t    const void *data, int len)\n+{\n+\tstruct nlattr *na = (void *)(buf + off);\n+\n+\tna-\u003enla_type = type;\n+\tna-\u003enla_len = NLA_HDRLEN + len;\n+\tif (len)\n+\t\tmemcpy(buf + off + NLA_HDRLEN, data, len);\n+\treturn off + NLA_ALIGN4(NLA_HDRLEN + len);\n+}\n+\n+/* Build a genl message header into @buf; return the offset past it. */\n+static int genl_hdr(char *buf, uint16_t type, uint16_t flags, uint8_t cmd)\n+{\n+\tstruct nlmsghdr *nlh = (void *)buf;\n+\tstruct genlmsghdr *gnl = (void *)(buf + NLMSG_HDRLEN);\n+\n+\tmemset(buf, 0, NLMSG_HDRLEN + GENL_HDRLEN);\n+\tnlh-\u003enlmsg_type = type;\n+\tnlh-\u003enlmsg_flags = flags;\n+\tnlh-\u003enlmsg_seq = 1;\n+\tgnl-\u003ecmd = cmd;\n+\tgnl-\u003eversion = 1;\n+\treturn NLMSG_HDRLEN + GENL_HDRLEN;\n+}\n+\n+/* Send an nfsd command with an ACK; return the ACK errno (\u003c= 0). */\n+static int genl_request(uint8_t cmd, const char *attrs, int attrs_len)\n+{\n+\tchar buf[1 \u003c\u003c 20], rbuf[4096];\n+\tstruct nlmsghdr *nlh = (void *)buf;\n+\tint fd = genl_open();\n+\tint off, n, ret;\n+\n+\toff = genl_hdr(buf, nfsd_family, NLM_F_REQUEST | NLM_F_ACK, cmd);\n+\tif (attrs_len) {\n+\t\tmemcpy(buf + off, attrs, attrs_len);\n+\t\toff += attrs_len;\n+\t}\n+\tnlh-\u003enlmsg_len = off;\n+\n+\tif (send(fd, buf, off, 0) \u003c 0)\n+\t\tdie(\"send(genl)\");\n+\n+\tn = recv(fd, rbuf, sizeof(rbuf), 0);\n+\tif (n \u003c 0)\n+\t\tret = (errno == EAGAIN || errno == EWOULDBLOCK) ? -ETIMEDOUT : -errno;\n+\telse if (((struct nlmsghdr *)rbuf)-\u003enlmsg_type == NLMSG_ERROR)\n+\t\tret = ((struct nlmsgerr *)NLMSG_DATA(rbuf))-\u003eerror;\n+\telse\n+\t\tret = 0;\n+\tclose(fd);\n+\treturn ret;\n+}\n+\n+/* Send a command and return the full reply message; -errno on failure. */\n+static int genl_request_reply(uint8_t cmd, char *rbuf, size_t rlen)\n+{\n+\tchar buf[256];\n+\tstruct nlmsghdr *nlh = (void *)buf;\n+\tint fd = genl_open();\n+\tint off, n, ret;\n+\n+\toff = genl_hdr(buf, nfsd_family, NLM_F_REQUEST, cmd);\n+\tnlh-\u003enlmsg_len = off;\n+\n+\tif (send(fd, buf, off, 0) \u003c 0)\n+\t\tdie(\"send(genl reply)\");\n+\n+\tn = recv(fd, rbuf, rlen, 0);\n+\tif (n \u003c 0)\n+\t\tret = (errno == EAGAIN || errno == EWOULDBLOCK) ? -ETIMEDOUT : -errno;\n+\telse if (((struct nlmsghdr *)rbuf)-\u003enlmsg_type == NLMSG_ERROR)\n+\t\tret = ((struct nlmsgerr *)NLMSG_DATA(rbuf))-\u003eerror;\n+\telse\n+\t\tret = n;\n+\tclose(fd);\n+\treturn ret;\n+}\n+\n+/* Resolve the \"nfsd\" genl family id; -1 if not registered. */\n+static int genl_resolve_nfsd(void)\n+{\n+\tchar buf[1024], rbuf[4096];\n+\tstruct nlmsghdr *nlh = (void *)buf;\n+\tstruct nlmsghdr *rh = (void *)rbuf;\n+\tstruct nlattr *na;\n+\tint fd, off, left, id = -1;\n+\n+\tfd = genl_open();\n+\toff = genl_hdr(buf, GENL_ID_CTRL, NLM_F_REQUEST, CTRL_CMD_GETFAMILY);\n+\toff = put_attr(buf, off, CTRL_ATTR_FAMILY_NAME,\n+\t\t       NFSD_FAMILY_NAME, sizeof(NFSD_FAMILY_NAME));\n+\tnlh-\u003enlmsg_len = off;\n+\n+\tif (send(fd, buf, off, 0) \u003c 0)\n+\t\tdie(\"send(GETFAMILY)\");\n+\tif (recv(fd, rbuf, sizeof(rbuf), 0) \u003c 0)\n+\t\tdie(\"recv(GETFAMILY)\");\n+\tclose(fd);\n+\n+\tif (rh-\u003enlmsg_type == NLMSG_ERROR)\n+\t\treturn -1;\n+\n+\tna = (void *)((char *)NLMSG_DATA(rh) + GENL_HDRLEN);\n+\tleft = rh-\u003enlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;\n+\twhile (left \u003e= (int)NLA_HDRLEN) {\n+\t\tif (na-\u003enla_type == CTRL_ATTR_FAMILY_ID) {\n+\t\t\tid = *(uint16_t *)((char *)na + NLA_HDRLEN);\n+\t\t\tbreak;\n+\t\t}\n+\t\tleft -= NLA_ALIGN4(na-\u003enla_len);\n+\t\tna = (void *)((char *)na + NLA_ALIGN4(na-\u003enla_len));\n+\t}\n+\treturn id;\n+}\n+\n+/* ------------------- listener request builders ------------------- */\n+\n+/* Fine-grained control for negative tests: any field can be omitted/malformed. */\n+struct raw_listener {\n+\tconst char *xprt;\t/* NULL -\u003e omit NFSD_A_SOCK_TRANSPORT_NAME */\n+\tint emit_addr;\t\t/* 0 -\u003e omit NFSD_A_SOCK_ADDR */\n+\tconst void *addr;\n+\tint addr_len;\t\t/* bytes to emit for NFSD_A_SOCK_ADDR */\n+};\n+\n+static int put_raw_listener(char *buf, int off, const struct raw_listener *r)\n+{\n+\tstruct nlattr *nest = (void *)(buf + off);\n+\tint inner = off + NLA_HDRLEN;\n+\n+\tif (r-\u003eemit_addr)\n+\t\tinner = put_attr(buf, inner, NFSD_A_SOCK_ADDR, r-\u003eaddr, r-\u003eaddr_len);\n+\tif (r-\u003exprt)\n+\t\tinner = put_attr(buf, inner, NFSD_A_SOCK_TRANSPORT_NAME,\n+\t\t\t\t r-\u003exprt, strlen(r-\u003exprt) + 1);\n+\tnest-\u003enla_type = NFSD_A_SERVER_SOCK_ADDR | NLA_F_NESTED;\n+\tnest-\u003enla_len = inner - off;\n+\treturn off + NLA_ALIGN4(nest-\u003enla_len);\n+}\n+\n+/* Well-formed loopback listener for @family (AF_INET or AF_INET6). */\n+static int put_listener_af(char *buf, int off, const char *xprt, int family,\n+\t\t\t   uint16_t port)\n+{\n+\tstruct sockaddr_storage ss = {0};\n+\tstruct raw_listener r = { .xprt = xprt, .emit_addr = 1, .addr = \u0026ss };\n+\n+\tif (family == AF_INET6) {\n+\t\tstruct sockaddr_in6 *s6 = (void *)\u0026ss;\n+\n+\t\ts6-\u003esin6_family = AF_INET6;\n+\t\ts6-\u003esin6_port = htons(port);\n+\t\ts6-\u003esin6_addr = in6addr_loopback;\n+\t\tr.addr_len = sizeof(*s6);\n+\t} else {\n+\t\tstruct sockaddr_in *s4 = (void *)\u0026ss;\n+\n+\t\ts4-\u003esin_family = AF_INET;\n+\t\ts4-\u003esin_port = htons(port);\n+\t\ts4-\u003esin_addr.s_addr = htonl(INADDR_LOOPBACK);\n+\t\tr.addr_len = sizeof(*s4);\n+\t}\n+\treturn put_raw_listener(buf, off, \u0026r);\n+}\n+\n+static int put_listener(char *buf, int off, const char *xprt, uint16_t port)\n+{\n+\treturn put_listener_af(buf, off, xprt, AF_INET, port);\n+}\n+\n+/* ------------------- LISTENER_GET parsing ------------------- */\n+\n+struct listener_ent {\n+\tchar xprt[16];\n+\tint family;\n+\tuint16_t port;\n+\tstruct in_addr a4;\n+\tstruct in6_addr a6;\n+};\n+\n+static int parse_listener_get(const char *rbuf, int len,\n+\t\t\t      struct listener_ent *out, int max)\n+{\n+\tconst struct nlmsghdr *nlh = (const void *)rbuf;\n+\tconst struct nlattr *na;\n+\tint left, count = 0;\n+\n+\t(void)len;\n+\tna = (const void *)(rbuf + NLMSG_HDRLEN + GENL_HDRLEN);\n+\tleft = nlh-\u003enlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;\n+\n+\twhile (left \u003e= (int)NLA_HDRLEN) {\n+\t\tint alen = na-\u003enla_len;\n+\n+\t\tif ((na-\u003enla_type \u0026 NLA_TYPE_MASK) == NFSD_A_SERVER_SOCK_ADDR \u0026\u0026\n+\t\t    count \u003c max) {\n+\t\t\tconst struct nlattr *in = (const void *)((char *)na + NLA_HDRLEN);\n+\t\t\tint ileft = alen - NLA_HDRLEN;\n+\t\t\tstruct listener_ent *e = \u0026out[count];\n+\n+\t\t\tmemset(e, 0, sizeof(*e));\n+\t\t\twhile (ileft \u003e= (int)NLA_HDRLEN) {\n+\t\t\t\tconst void *d = (const char *)in + NLA_HDRLEN;\n+\t\t\t\tint t = in-\u003enla_type \u0026 NLA_TYPE_MASK;\n+\n+\t\t\t\tif (t == NFSD_A_SOCK_TRANSPORT_NAME) {\n+\t\t\t\t\tstrncpy(e-\u003exprt, d, sizeof(e-\u003exprt) - 1);\n+\t\t\t\t} else if (t == NFSD_A_SOCK_ADDR) {\n+\t\t\t\t\tconst struct sockaddr_storage *ss = d;\n+\n+\t\t\t\t\te-\u003efamily = ss-\u003ess_family;\n+\t\t\t\t\tif (ss-\u003ess_family == AF_INET) {\n+\t\t\t\t\t\tconst struct sockaddr_in *s = d;\n+\n+\t\t\t\t\t\te-\u003ea4 = s-\u003esin_addr;\n+\t\t\t\t\t\te-\u003eport = ntohs(s-\u003esin_port);\n+\t\t\t\t\t} else if (ss-\u003ess_family == AF_INET6) {\n+\t\t\t\t\t\tconst struct sockaddr_in6 *s = d;\n+\n+\t\t\t\t\t\te-\u003ea6 = s-\u003esin6_addr;\n+\t\t\t\t\t\te-\u003eport = ntohs(s-\u003esin6_port);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\tileft -= NLA_ALIGN4(in-\u003enla_len);\n+\t\t\t\tin = (const void *)((char *)in + NLA_ALIGN4(in-\u003enla_len));\n+\t\t\t}\n+\t\t\tcount++;\n+\t\t}\n+\t\tleft -= NLA_ALIGN4(alen);\n+\t\tna = (const void *)((char *)na + NLA_ALIGN4(alen));\n+\t}\n+\treturn count;\n+}\n+\n+/* ------------------- convenience wrappers ------------------- */\n+\n+static int listener_set(const char *attrs, int len)\n+{\n+\treturn genl_request(NFSD_CMD_LISTENER_SET, attrs, len);\n+}\n+\n+/* Fetch the current listeners; returns count (\u003e=0) or -errno. */\n+static int listener_get(struct listener_ent *out, int max)\n+{\n+\tchar rbuf[8192];\n+\tint n = genl_request_reply(NFSD_CMD_LISTENER_GET, rbuf, sizeof(rbuf));\n+\n+\tif (n \u003c 0)\n+\t\treturn n;\n+\treturn parse_listener_get(rbuf, n, out, max);\n+}\n+\n+/*\n+ * Every listener these tests create comes from put_listener_af(), so the\n+ * address is always loopback. Match on it too: without that, a reply that\n+ * gave the right transport and port on the wrong address (0.0.0.0, say)\n+ * would pass.\n+ */\n+static struct listener_ent *find_listener(struct listener_ent *e, int n,\n+\t\t\t\t\t  const char *xprt, int family,\n+\t\t\t\t\t  uint16_t port)\n+{\n+\tint i;\n+\n+\tfor (i = 0; i \u003c n; i++) {\n+\t\tif (e[i].family != family || e[i].port != port ||\n+\t\t    strcmp(e[i].xprt, xprt))\n+\t\t\tcontinue;\n+\t\tif (family == AF_INET6) {\n+\t\t\tif (memcmp(\u0026e[i].a6, \u0026in6addr_loopback, sizeof(e[i].a6)))\n+\t\t\t\tcontinue;\n+\t\t} else if (e[i].a4.s_addr != htonl(INADDR_LOOPBACK)) {\n+\t\t\tcontinue;\n+\t\t}\n+\t\treturn \u0026e[i];\n+\t}\n+\treturn NULL;\n+}\n+\n+/* Start (@n \u003e 0) or stop (@n == 0) nfsd threads in this netns. */\n+static int threads_set(int n)\n+{\n+\tchar attrs[64];\n+\tuint32_t v = n;\n+\tint off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, \u0026v, sizeof(v));\n+\n+\treturn genl_request(NFSD_CMD_THREADS_SET, attrs, off);\n+}\n+\n+/* ------------------- per-netns local rpcbind stub ------------------- */\n+\n+/*\n+ * Creating a listener registers with rpcbind: svc_xprt_create_from_sa()\n+ * passes flags of 0, so pmap_register is true in svc_setup_socket(), and\n+ * nfsd_version3 is registerable by default and does not set vs_rpcb_optnl,\n+ * so a registration failure aborts listener creation. The abstract AF_LOCAL\n+ * name the kernel tries first is per-netns (unix_find_abstract() takes a\n+ * struct net), so answer it here and stay out of the host's rpcbind.\n+ *\n+ * Arguments are never decoded. The NULL procedure gets an empty success and\n+ * SET/UNSET get TRUE, for both RPCBVERS_2 and RPCBVERS_4. v4 has to be\n+ * answered because __svc_rpcb_register6() turns a v4 refusal into\n+ * -EAFNOSUPPORT, which would fail every IPv6 listener.\n+ *\n+ * In RPCB_STUB_REFUSE mode SET is answered FALSE instead, which\n+ * rpcb_register_call() reports as -EACCES. UNSET is left alone: only\n+ * svc_unregister() issues it, and it discards the result.\n+ *\n+ * The stub also keeps counters in a page shared with the test, so a test can\n+ * assert that the kernel never talked to rpcbind at all, or that it dropped\n+ * the local rpcbind client and had to reconnect.\n+ */\n+#define RPCB_PROGRAM\t\t100000\n+#define RPCB_PROC_NULL\t\t0\n+#define RPCB_PROC_SET\t\t1\n+#define RPCB_PROC_UNSET\t\t2\n+#define RPCB_ABSTRACT_NAME\t\"/run/rpcbind.sock\"\n+#define RPCB_STUB_MAXCONN\t4\n+\n+enum { RPCB_STUB_ACCEPT, RPCB_STUB_REFUSE };\n+\n+struct rpcb_stub_stats {\n+\tunsigned int conns;\t\t/* connections accepted */\n+\tunsigned int calls;\t\t/* calls answered */\n+};\n+\n+static volatile struct rpcb_stub_stats *rpcb_stats;\t/* MAP_SHARED */\n+\n+static int rpcb_stats_alloc(void)\n+{\n+\tvoid *p = mmap(NULL, sizeof(*rpcb_stats), PROT_READ | PROT_WRITE,\n+\t\t       MAP_SHARED | MAP_ANONYMOUS, -1, 0);\n+\n+\tif (p == MAP_FAILED)\n+\t\treturn -1;\n+\trpcb_stats = p;\n+\treturn 0;\n+}\n+\n+/*\n+ * The stub bumps these before it replies and the kernel waits for that reply,\n+ * so whatever a netlink request provoked is visible once it returns.\n+ */\n+static int rpcb_calls(void)\n+{\n+\treturn rpcb_stats ? (int)rpcb_stats-\u003ecalls : 0;\n+}\n+\n+static int rpcb_conns(void)\n+{\n+\treturn rpcb_stats ? (int)rpcb_stats-\u003econns : 0;\n+}\n+\n+static int rpcb_stub_listen(void)\n+{\n+\tstruct sockaddr_un sun = { .sun_family = AF_UNIX };\n+\tsize_t nlen = strlen(RPCB_ABSTRACT_NAME);\n+\tsocklen_t alen;\n+\tint fd;\n+\n+\t/* Abstract names are length-delimited, so the length must match. */\n+\tmemcpy(sun.sun_path + 1, RPCB_ABSTRACT_NAME, nlen);\n+\talen = offsetof(struct sockaddr_un, sun_path) + 1 + nlen;\n+\n+\tfd = socket(AF_UNIX, SOCK_STREAM, 0);\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tif (bind(fd, (struct sockaddr *)\u0026sun, alen) \u003c 0 ||\n+\t    listen(fd, RPCB_STUB_MAXCONN) \u003c 0) {\n+\t\tclose(fd);\n+\t\treturn -1;\n+\t}\n+\treturn fd;\n+}\n+\n+static int rpcb_stub_read(int fd, void *buf, size_t len)\n+{\n+\tsize_t done = 0;\n+\n+\twhile (done \u003c len) {\n+\t\tssize_t n = read(fd, (char *)buf + done, len - done);\n+\n+\t\tif (n \u003c= 0)\n+\t\t\treturn -1;\n+\t\tdone += n;\n+\t}\n+\treturn 0;\n+}\n+\n+/* Handle one record-marked RPC call. Returns -1 when the peer is done. */\n+static int rpcb_stub_call(int fd, int mode)\n+{\n+\tuint32_t mark, call[6], rep[7];\n+\tunsigned int len, nrep = 6;\n+\tsize_t replen;\n+\n+\tif (rpcb_stub_read(fd, \u0026mark, sizeof(mark)))\n+\t\treturn -1;\n+\tlen = ntohl(mark) \u0026 0x7fffffff;\n+\tif (len \u003c sizeof(call) || len \u003e 4096)\n+\t\treturn -1;\n+\tif (rpcb_stub_read(fd, call, sizeof(call)))\n+\t\treturn -1;\n+\n+\t/* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */\n+\tfor (len -= sizeof(call); len; ) {\n+\t\tchar sink[256];\n+\t\tunsigned int n = len \u003e sizeof(sink) ? sizeof(sink) : len;\n+\n+\t\tif (rpcb_stub_read(fd, sink, n))\n+\t\t\treturn -1;\n+\t\tlen -= n;\n+\t}\n+\n+\tif (rpcb_stats)\n+\t\trpcb_stats-\u003ecalls++;\n+\n+\trep[0] = call[0];\t\t/* xid */\n+\trep[1] = htonl(1);\t\t/* REPLY */\n+\trep[2] = htonl(0);\t\t/* MSG_ACCEPTED */\n+\trep[3] = htonl(0);\t\t/* verifier flavor AUTH_NULL */\n+\trep[4] = htonl(0);\t\t/* verifier length */\n+\trep[5] = htonl(0);\t\t/* SUCCESS */\n+\n+\tif (ntohl(call[3]) != RPCB_PROGRAM) {\n+\t\trep[5] = htonl(1);\t/* PROG_UNAVAIL */\n+\t} else {\n+\t\tswitch (ntohl(call[5])) {\n+\t\tcase RPCB_PROC_NULL:\n+\t\t\tbreak;\n+\t\tcase RPCB_PROC_SET:\n+\t\t\trep[6] = htonl(mode == RPCB_STUB_REFUSE ? 0 : 1);\n+\t\t\tnrep = 7;\n+\t\t\tbreak;\n+\t\tcase RPCB_PROC_UNSET:\n+\t\t\trep[6] = htonl(1);\t/* TRUE */\n+\t\t\tnrep = 7;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\trep[5] = htonl(3);\t/* PROC_UNAVAIL */\n+\t\t}\n+\t}\n+\n+\treplen = nrep * sizeof(rep[0]);\n+\tmark = htonl(0x80000000 | replen);\n+\tif (write(fd, \u0026mark, sizeof(mark)) != (ssize_t)sizeof(mark) ||\n+\t    write(fd, rep, replen) != (ssize_t)replen)\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n+static void rpcb_stub_serve(int lfd, int mode)\n+{\n+\tstruct pollfd pfd[1 + RPCB_STUB_MAXCONN];\n+\tnfds_t n = 1, i;\n+\n+\tpfd[0].fd = lfd;\n+\n+\tfor (;;) {\n+\t\t/* stop polling the listener when full, or poll() spins */\n+\t\tpfd[0].events = n \u003c 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;\n+\n+\t\tif (poll(pfd, n, -1) \u003c 0)\n+\t\t\treturn;\n+\n+\t\tif (pfd[0].revents \u0026 POLLIN) {\n+\t\t\tint c = accept(lfd, NULL, NULL);\n+\n+\t\t\tif (c \u003e= 0) {\n+\t\t\t\tpfd[n].fd = c;\n+\t\t\t\tpfd[n].events = POLLIN;\n+\t\t\t\t/*\n+\t\t\t\t * poll() ran with the old n, so it did not\n+\t\t\t\t * write this revents. The loop below reads it.\n+\t\t\t\t */\n+\t\t\t\tpfd[n].revents = 0;\n+\t\t\t\tn++;\n+\t\t\t\tif (rpcb_stats)\n+\t\t\t\t\trpcb_stats-\u003econns++;\n+\t\t\t}\n+\t\t}\n+\n+\t\tfor (i = 1; i \u003c n; i++) {\n+\t\t\tif (!(pfd[i].revents \u0026 (POLLIN | POLLHUP | POLLERR)))\n+\t\t\t\tcontinue;\n+\t\t\tif (rpcb_stub_call(pfd[i].fd, mode)) {\n+\t\t\t\tclose(pfd[i].fd);\n+\t\t\t\tpfd[i] = pfd[--n];\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n+/* Returns the stub's pid, or -1. The socket is listening before we fork. */\n+static pid_t rpcb_stub_start(int mode)\n+{\n+\tint lfd = rpcb_stub_listen();\n+\tpid_t pid;\n+\n+\tif (lfd \u003c 0)\n+\t\treturn -1;\n+\n+\tpid = fork();\n+\tif (pid \u003c 0) {\n+\t\tclose(lfd);\n+\t\treturn -1;\n+\t}\n+\tif (pid == 0) {\n+\t\tsignal(SIGPIPE, SIG_IGN);\n+\t\tprctl(PR_SET_PDEATHSIG, SIGKILL);\n+\t\tif (getppid() == 1)\t\t/* raced with parent exit */\n+\t\t\t_exit(0);\n+\t\trpcb_stub_serve(lfd, mode);\n+\t\t_exit(0);\n+\t}\n+\n+\tclose(lfd);\n+\treturn pid;\n+}\n+\n+/*\n+ * Swap the stub for one in @mode. Safe before the first request: no serv\n+ * exists yet, so the kernel has not connected and the abstract name is free\n+ * again once the old stub has been reaped.\n+ */\n+static int rpcb_stub_restart(pid_t *pid, int mode)\n+{\n+\tif (*pid \u003e 0) {\n+\t\tkill(*pid, SIGKILL);\n+\t\twaitpid(*pid, NULL, 0);\n+\t}\n+\t*pid = rpcb_stub_start(mode);\n+\treturn *pid \u003e 0 ? 0 : -1;\n+}\n+\n+/* --------------------------- fixture --------------------------- */\n+\n+FIXTURE(nfsd_listener) {\n+\tpid_t rpcbd;\n+};\n+\n+FIXTURE_SETUP(nfsd_listener)\n+{\n+\tstruct ifreq ifr = {0};\n+\tstruct stat st;\n+\tint s;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"must be run as root\");\n+\tif (unshare(CLONE_NEWNET | CLONE_NEWNS) \u003c 0)\n+\t\tSKIP(return, \"unshare(NEWNET|NEWNS): %s\", strerror(errno));\n+\tif (mount(\"\", \"/\", NULL, MS_REC | MS_PRIVATE, NULL) \u003c 0)\n+\t\tSKIP(return, \"mount(/ private): %s\", strerror(errno));\n+\n+\t/*\n+\t * Keep the kernel's rpcbind client inside this namespace. The\n+\t * abstract socket it tries first is per-netns, but the\n+\t * \"/var/run/rpcbind.sock\" fallback is not, so hide the path.\n+\t */\n+\tif (mount(\"tmpfs\", \"/run\", \"tmpfs\", 0, NULL) \u003c 0)\n+\t\tSKIP(return, \"mount(tmpfs on /run): %s\", strerror(errno));\n+\tif (lstat(\"/var/run\", \u0026st) == 0 \u0026\u0026 S_ISDIR(st.st_mode) \u0026\u0026\n+\t    mount(\"tmpfs\", \"/var/run\", \"tmpfs\", 0, NULL) \u003c 0)\n+\t\tSKIP(return, \"mount(tmpfs on /var/run): %s\", strerror(errno));\n+\n+\t/* Bring loopback up so listener binds (127.0.0.1 / ::1) work. */\n+\ts = socket(AF_INET, SOCK_DGRAM, 0);\n+\tASSERT_GE(s, 0);\n+\tstrcpy(ifr.ifr_name, \"lo\");\n+\tASSERT_EQ(0, ioctl(s, SIOCGIFFLAGS, \u0026ifr));\n+\tifr.ifr_flags |= IFF_UP | IFF_RUNNING;\n+\tASSERT_EQ(0, ioctl(s, SIOCSIFFLAGS, \u0026ifr));\n+\tclose(s);\n+\n+\tnfsd_family = genl_resolve_nfsd();\n+\tif (nfsd_family \u003c 0)\n+\t\tSKIP(return, \"nfsd genl family not found (modprobe nfsd?)\");\n+\n+\tif (rpcb_stats_alloc() \u003c 0)\n+\t\tSKIP(return, \"mmap(rpcbind stub counters): %s\", strerror(errno));\n+\n+\tself-\u003erpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);\n+\tif (self-\u003erpcbd \u003c 0)\n+\t\tSKIP(return, \"cannot start the rpcbind stub: %s\",\n+\t\t     strerror(errno));\n+}\n+\n+FIXTURE_TEARDOWN(nfsd_listener)\n+{\n+\tif (self-\u003erpcbd \u003e 0) {\n+\t\tkill(self-\u003erpcbd, SIGKILL);\n+\t\twaitpid(self-\u003erpcbd, NULL, 0);\n+\t}\n+\tif (rpcb_stats) {\n+\t\tmunmap((void *)rpcb_stats, sizeof(*rpcb_stats));\n+\t\trpcb_stats = NULL;\n+\t}\n+}\n+\n+/* ===================== validation / negative ===================== */\n+\n+TEST_F(nfsd_listener, val_empty_list_ok)\n+{\n+\tEXPECT_EQ(0, listener_set(NULL, 0));\n+}\n+\n+TEST_F(nfsd_listener, val_too_many)\n+{\n+\tstatic char attrs[1 \u003c\u003c 20];\n+\tint i, off = 0;\n+\n+\tfor (i = 0; i \u003c 1025; i++)\t\t/* \u003e NFSD_NL_LISTENER_MAX (1024) */\n+\t\toff = put_listener(attrs, off, \"udp\", TEST_PORT);\n+\tEXPECT_EQ(-E2BIG, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_missing_addr)\n+{\n+\tchar attrs[64];\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 0 };\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_missing_transport)\n+{\n+\tstruct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };\n+\tstruct raw_listener r = { .xprt = NULL, .emit_addr = 1,\n+\t\t\t\t  .addr = \u0026s4, .addr_len = sizeof(s4) };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+/*\n+ * A name matching no transport class must be refused before nfsd_mutex is\n+ * taken, so it never reaches svc_xprt_create_from_sa() and its\n+ * request_module(\"svc%s\", name) upcall.\n+ *\n+ * The errno cannot show that -- svc_xprt_create_from_sa() returns\n+ * -EPROTONOSUPPORT for an unknown name too. The rpcbind traffic can:\n+ * getting that far means nfsd_create_serv() ran, and svc_bind() pings\n+ * rpcbind at client creation and then sweeps stale entries with\n+ * svc_unregister(). A silent stub is the proof nothing was created.\n+ */\n+TEST_F(nfsd_listener, val_bad_transport)\n+{\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"bogus_xprt\", TEST_PORT);\n+\n+\tASSERT_EQ(0, rpcb_calls());\n+\tEXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));\n+\tEXPECT_EQ(0, rpcb_calls());\n+}\n+\n+TEST_F(nfsd_listener, val_addr_too_short)\n+{\n+\tunsigned char tiny = 0;\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1,\n+\t\t\t\t  .addr = \u0026tiny, .addr_len = 1 };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_inet_short)\n+{\n+\tstruct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026s4,\n+\t\t\t\t  .addr_len = sizeof(sa_family_t) + 2 };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_inet6_short)\n+{\n+\tstruct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) };\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026s6,\n+\t\t\t\t  .addr_len = sizeof(struct sockaddr_in) };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_bad_family)\n+{\n+\tstruct sockaddr_storage ss = { .ss_family = AF_UNIX };\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026ss,\n+\t\t\t\t  .addr_len = sizeof(struct sockaddr_in) };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_second_entry_bad)\n+{\n+\tstruct sockaddr_storage ss = { .ss_family = AF_UNIX };\n+\tstruct raw_listener bad = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026ss,\n+\t\t\t\t    .addr_len = sizeof(struct sockaddr_in) };\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[128];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\toff = put_raw_listener(attrs, off, \u0026bad);\n+\t/* The whole request is rejected during validation; nothing applied. */\n+\tEXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));\n+\t/*\n+\t * Again the errno alone does not say so: svc_xprt_create_from_sa()\n+\t * also returns -EAFNOSUPPORT, and the doit keeps the listeners it did\n+\t * manage to create, so the well-formed tcp entry ahead of the bad one\n+\t * would still be up.\n+\t */\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+}\n+\n+/*\n+ * A rejected request must leave the listeners that are already up alone.\n+ * The errno alone does not show that: svc_xprt_create_from_sa() returns\n+ * -EPROTONOSUPPORT for an unknown name too. What differs is how far the\n+ * request gets -- without the check in nfsd_nl_validate_listeners(),\n+ * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener\n+ * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the\n+ * name fails.\n+ */\n+TEST_F(nfsd_listener, val_reject_keeps_listeners)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar good[64], bad[64];\n+\tint og = put_listener(good, 0, \"tcp\", TEST_PORT);\n+\tint ob = put_listener(bad, 0, \"bogus_xprt\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(good, og));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\n+\tEXPECT_EQ(-EPROTONOSUPPORT, listener_set(bad, ob));\n+\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+/* ===================== functional / round-trip ===================== */\n+\n+/* LISTENER_GET with no serv in this netns returns an empty list. */\n+TEST_F(nfsd_listener, func_get_empty)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+}\n+\n+TEST_F(nfsd_listener, func_create_tcp)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_create_udp)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"udp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"udp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_create_multi)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[128];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\toff = put_listener(attrs, off, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(2, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"tcp\", AF_INET, TEST_PORT));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"udp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_idempotent)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tEXPECT_EQ(0, listener_set(attrs, off));\t\t/* re-set same list */\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_add)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar one[64], two[128];\n+\tint o1 = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\tint o2 = put_listener(two, 0, \"tcp\", TEST_PORT);\n+\n+\to2 = put_listener(two, o2, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(one, o1));\n+\tASSERT_EQ(0, listener_set(two, o2));\t\t/* add udp, keep tcp */\n+\tASSERT_EQ(2, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"tcp\", AF_INET, TEST_PORT));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"udp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_remove_subset)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar both[128], one[64];\n+\tint ob = put_listener(both, 0, \"tcp\", TEST_PORT);\n+\tint oo = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\n+\tob = put_listener(both, ob, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(both, ob));\n+\tASSERT_EQ(0, listener_set(one, oo));\t\t/* drop udp */\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+/*\n+ * LISTENER_GET cannot tell a destroyed serv from a live one with no\n+ * permsocks: nfsd_nl_listener_get_doit() replies empty either way. The\n+ * rpcbind client can. nfsd_destroy_serv() is the only path that reaches\n+ * svc_xprt_destroy_all(..., unregister=true) -\u003e svc_rpcb_cleanup() -\u003e\n+ * rpcb_put_local(), which drops the last user and shuts the local client\n+ * down; the next serv then has to connect again. Leaving the serv in place\n+ * would keep the first connection and the stub would see just the one.\n+ */\n+TEST_F(nfsd_listener, func_empty_destroys)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tint conns;\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tconns = rpcb_conns();\n+\tASSERT_GT(conns, 0);\n+\n+\tEXPECT_EQ(0, listener_set(NULL, 0));\t\t/* empty -\u003e destroy serv */\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tEXPECT_GT(rpcb_conns(), conns);\n+}\n+\n+TEST_F(nfsd_listener, func_ipv6)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off, s;\n+\n+\ts = socket(AF_INET6, SOCK_STREAM, 0);\n+\tif (s \u003c 0)\n+\t\tSKIP(return, \"IPv6 unavailable: %s\", strerror(errno));\n+\tclose(s);\n+\n+\toff = put_listener_af(attrs, 0, \"tcp\", AF_INET6, TEST_PORT);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET6, TEST_PORT));\n+}\n+\n+/* ===================== rpcbind registration ===================== */\n+\n+/*\n+ * A rpcbind that refuses the registration must fail listener creation,\n+ * whatever CONFIG_NFS_LOCALIO is set to.\n+ *\n+ * The error has to survive svc_register()'s walk over sv_programs to get\n+ * here. With CONFIG_NFS_LOCALIO=y the trailing nfslocalio program has only\n+ * a NULL and a vs_hidden version, and svc_generic_rpcbind_set() reports 0\n+ * for both, so an svc_register() that keeps the last result rather than the\n+ * first hands back success and the listener comes up regardless.\n+ */\n+TEST_F(nfsd_listener, sem_register_refused)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, rpcb_stub_restart(\u0026self-\u003erpcbd, RPCB_STUB_REFUSE));\n+\n+\tEXPECT_EQ(-EACCES, listener_set(attrs, off));\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+}\n+\n+/* ===================== threads / -EBUSY semantics ===================== */\n+\n+TEST_F(nfsd_listener, sem_busy_on_change)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar one[64], two[128];\n+\tint o1 = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\tint o2 = put_listener(two, 0, \"tcp\", TEST_PORT);\n+\n+\to2 = put_listener(two, o2, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(one, o1));\n+\tASSERT_EQ(0, threads_set(1));\t\t\t/* threads now running */\n+\tEXPECT_EQ(-EBUSY, listener_set(two, o2));\t/* add refused */\n+\n+\t/* refused means refused: the udp listener must not have been added */\n+\tEXPECT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+\n+\tthreads_set(0);\t\t\t\t\t/* stop before netns exit */\n+}\n+\n+TEST_F(nfsd_listener, sem_busy_on_remove)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar one[64];\n+\tint o1 = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(one, o1));\n+\tASSERT_EQ(0, threads_set(1));\n+\tEXPECT_EQ(-EBUSY, listener_set(NULL, 0));\t/* remove refused */\n+\n+\t/* the doit moves the permsocks to a temp list before it can fail */\n+\tEXPECT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+\n+\tthreads_set(0);\n+}\n+\n+TEST_HARNESS_MAIN\ndiff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings\nnew file mode 100644\nindex 0000000000000..6091b45d226ba\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/settings\n@@ -0,0 +1 @@\n+timeout=120\n"
}

2/1 2026/08/11 12:17 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "NFSD",
    "NFSD_V4"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "rpcb_create_local"
  ],
  "Reasoning": "The patch modifies the NFSD netlink listener configuration logic to validate transport names and limit the number of listeners. It also updates the local rpcbind client to use a specific timeout and modifies the sunrpc service registration logic to properly unwind registrations on failure. These changes affect core NFSD and sunrpc logic reachable from userspace via netlink and RPC operations.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 95e42039d503f9b0731fb044e87feb6e1a839415
Author: syz-cluster <triage@syzkaller.com>
Date:   Tue Aug 11 12:17:16 2026 +0000

    syz-cluster: applied patch under review

diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index 7e1d5a5d6511e..f73a2ea425571 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -1973,21 +1973,39 @@ int nfsd_nl_version_get_doit(struct sk_buff *skb, struct genl_info *info)
 	return err;
 }
 
+/*
+ * Transport classes NFSD knows how to instantiate. Vetting the name here
+ * keeps a bogus string from reaching svc_xprt_create_from_sa(), where an
+ * unknown name triggers a request_module("svc%s", name) upcall under
+ * nfsd_mutex.
+ */
+static bool nfsd_nl_transport_supported(const char *name)
+{
+	static const char * const supported[] = { "tcp", "udp", "rdma" };
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(supported); i++)
+		if (!strcmp(name, supported[i]))
+			return true;
+	return false;
+}
+
+/* Upper bound on the number of listeners a single request may carry. */
+#define NFSD_NL_LISTENER_MAX	1024
+
 /**
  * nfsd_nl_validate_listeners - sanity-check the listener list from userland
  * @info: netlink metadata and command arguments
  *
- * Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that each entry
- * is well-formed: it parses against the policy, carries both an address and
- * a transport name, and the address is long enough for its family. Doing
- * this up front lets the callers below assume every entry is valid and
- * guarantees we make no changes when the request is malformed.
+ * Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that the list is
+ * not oversized and that each entry is well-formed.
  *
  * Return: 0 if every entry is valid, or a negative errno otherwise.
  */
 static int nfsd_nl_validate_listeners(struct genl_info *info)
 {
 	const struct nlattr *attr;
+	unsigned int count = 0;
 	int rem;
 
 	nlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info->nlhdr,
@@ -1996,6 +2014,11 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 		struct sockaddr *sa;
 		int err;
 
+		if (++count > NFSD_NL_LISTENER_MAX) {
+			NL_SET_ERR_MSG(info->extack, "too many listeners");
+			return -E2BIG;
+		}
+
 		err = nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,
 				       nfsd_sock_nl_policy, info->extack);
 		if (err < 0)
@@ -2004,6 +2027,9 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 		if (!tb[NFSD_A_SOCK_ADDR] || !tb[NFSD_A_SOCK_TRANSPORT_NAME])
 			return -EINVAL;
 
+		if (!nfsd_nl_transport_supported(nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])))
+			return -EPROTONOSUPPORT;
+
 		sa = nla_data(tb[NFSD_A_SOCK_ADDR]);
 		if (nla_len(tb[NFSD_A_SOCK_ADDR]) < sizeof(sa->sa_family))
 			return -EINVAL;
@@ -2156,8 +2182,12 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,
 					      current_cred());
 		/* always save the latest error */
-		if (ret < 0)
+		if (ret < 0) {
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener: %d",
+					   xcl_name, ret);
 			err = ret;
+		}
 	}
 
 	if (!serv->sv_nrthreads && list_empty(&nn->nfsd_serv->sv_permsocks))
diff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c
index 6aa372188c86a..0aa376b82a526 100644
--- a/net/sunrpc/rpcb_clnt.c
+++ b/net/sunrpc/rpcb_clnt.c
@@ -221,6 +221,16 @@ static void rpcb_set_local(struct net *net, struct rpc_clnt *clnt,
 # define SUN_LEN(ptr) (offsetof(struct sockaddr_un, sun_path)		\
 		      + 1 + strlen((ptr)->sun_path + 1))
 
+/*
+ * The kernel's rpcbind client talks only to the local rpcbind, over loopback
+ * or a local AF_LOCAL socket, where a healthy rpcbind answers in microseconds.
+ */
+static const struct rpc_timeout rpcb_local_timeout = {
+	.to_initval	= 1 * HZ,
+	.to_maxval	= 1 * HZ,
+	.to_retries	= 0,
+};
+
 /*
  * Returns zero on success, otherwise a negative errno value
  * is returned.
@@ -238,6 +248,7 @@ static int rpcb_create_af_local(struct net *net,
 		.version	= RPCBVERS_2,
 		.authflavor	= RPC_AUTH_NULL,
 		.cred		= current_cred(),
+		.timeout	= &rpcb_local_timeout,
 		/*
 		 * We turn off the idle timeout to prevent the kernel
 		 * from automatically disconnecting the socket.
@@ -312,6 +323,7 @@ static int rpcb_create_local_net(struct net *net)
 		.version	= RPCBVERS_2,
 		.authflavor	= RPC_AUTH_UNIX,
 		.cred		= current_cred(),
+		.timeout	= &rpcb_local_timeout,
 		.flags		= RPC_CLNT_CREATE_NOPING,
 	};
 	struct rpc_clnt *clnt, *clnt4;
diff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c
index 05c3e6e2f6594..7c414634c753b 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1183,6 +1183,29 @@ int svc_generic_rpcbind_set(struct net *net,
 }
 EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
 
+/*
+ * Undo the [program, version] registrations that this svc_register() call
+ * already made, stopping at version @nvers of program @nprog.
+ *
+ * Note that rpcbind matches RPCBPROC_UNSET on [program, version, netid] and
+ * ignores the address, so this clears the netid rather than the one port.
+ * That is the granularity svc_delete_xprt() unregisters at as well.
+ */
+static void svc_unwind_register(const struct svc_serv *serv, struct net *net,
+				const int family, const unsigned short proto,
+				unsigned int nprog, unsigned int nvers)
+{
+	unsigned int p, i;
+
+	for (p = 0; p <= nprog; p++) {
+		struct svc_program *progp = &serv->sv_programs[p];
+		unsigned int last = p < nprog ? progp->pg_nvers : nvers;
+
+		for (i = 0; i < last; i++)
+			progp->pg_rpcbind_set(net, progp, i, family, proto, 0);
+	}
+}
+
 /**
  * svc_register - register an RPC service with the local portmapper
  * @serv: svc_serv struct for the service to register
@@ -1191,7 +1214,8 @@ EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
  * @proto: transport protocol number to advertise
  * @port: port to advertise
  *
- * Service is registered for any address in the passed-in protocol family
+ * Service is registered for any address in the passed-in protocol family.
+ * A @port of 0 unregisters instead, and then every program is attempted.
  */
 int svc_register(const struct svc_serv *serv, struct net *net,
 		 const int family, const unsigned short proto,
@@ -1208,13 +1232,21 @@ int svc_register(const struct svc_serv *serv, struct net *net,
 		struct svc_program *progp = &serv->sv_programs[p];
 
 		for (i = 0; i < progp->pg_nvers; i++) {
+			int ret;
 
-			error = progp->pg_rpcbind_set(net, progp, i,
+			ret = progp->pg_rpcbind_set(net, progp, i,
 					family, proto, port);
-			if (error < 0) {
+			if (ret < 0) {
 				printk(KERN_WARNING "svc: failed to register "
 					"%sv%u RPC service (errno %d).\n",
-					progp->pg_name, i, -error);
+					progp->pg_name, i, -ret);
+				if (port) {
+					svc_unwind_register(serv, net, family,
+							    proto, p, i);
+					return ret;
+				}
+				if (!error)
+					error = ret;
 				break;
 			}
 		}
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 84343fd1e354b..460508de9374f 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -89,6 +89,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 0000000000000..19e6dec04d8e9
--- /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 0000000000000..15ac65549d259
--- /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 0000000000000..ab84523fbedf3
--- /dev/null
+++ b/tools/testing/selftests/nfsd/config
@@ -0,0 +1,8 @@
+CONFIG_NAMESPACES=y
+CONFIG_NET_NS=y
+CONFIG_SHMEM=y
+CONFIG_TMPFS=y
+CONFIG_UNIX=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 0000000000000..9972b0db45ff9
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -0,0 +1,1030 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression tests for the NFSD generic-netlink listener interface
+ * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).
+ *
+ * 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. 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>
+#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_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 */
+
+#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);
+}
+
+/*
+ * 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) {
+	pid_t rpcbd;
+};
+
+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?)");
+
+	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];
+	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.
+ *
+ * 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)
+{
+	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) };
+	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)
+{
+	struct listener_ent got[MAX_LISTENERS];
+
+	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
diff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings
new file mode 100644
index 0000000000000..6091b45d226ba
--- /dev/null
+++ b/tools/testing/selftests/nfsd/settings
@@ -0,0 +1 @@
+timeout=120

3/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 17930
							output: 40
							thoughts: 720
						

Thoughts:
**Analyzing a Patch Series for NFSD and SunRPC**

Okay, let's break down this patch series. My initial impression is that it touches several critical areas: the NFSD netlink interface, RPC bind client/server interactions, and includes a new selftest, always a good sign.

First, the `fs/nfsd/nfsctl.c` changes are particularly interesting. We're introducing `nfsd_nl_transport_supported`, which is a gatekeeper for transport types in the netlink interface, specifically limiting it to TCP, UDP, and RDMA, which is sensible. The `nfsd_nl_validate_listeners` function imposes a hard limit of `NFSD_NL_LISTENER_MAX` on the number of listeners, preventing excessive resource consumption. Furthermore, we've got some error reporting improvements in `nfsd_nl_listener_set_doit`, using `NL_SET_ERR_MSG_FMT`, which is a good practice for debugging.

The `net/sunrpc/rpcb_clnt.c` patch adds `rpcb_local_timeout`, a crucial addition for local rpcbind client operations. It looks like we're setting a small initial timeout and no retries. This hints at some careful tuning to avoid unnecessary delays in local RPC communication. This timeout variable is utilized in the functions `rpcb_create_af_local` and `rpcb_create_local_net`.

The `net/sunrpc/svc.c` changes address RPC service registration. The addition of `svc_unwind_register` is good, indicating efforts to make sure registration is always handled correctly. We now see an unwinding of registration if `progp->pg_rpcbind_set` fails in `svc_register`, which is a definite improvement. It looks like it also fixes an issue in the error return path of that same function.

Finally, the `tools/testing/selftests/nfsd/` addition of `nfsd_netlink_listener.c` is fantastic. This means we've got a new selftest dedicated to the NFSD generic-netlink listener interface.

Now, the crucial question: are these changes accessible from userspace and are they worth fuzzing? Definitely. The netlink interface for NFSD, and RPC/NFS operations are all inherently userspace-accessible. Given the nature of these changes – core NFSD netlink logic, RPC service registration – they are absolutely worth fuzzing. We are talking about critical parts of network file system interactions, so a good fuzzing campaign is essential.

So, where do we focus? My attention is drawn to:
*   `nfsd_nl_listener_set_doit`: This is the netlink entry point for setting up listeners.
*   `nfsd_nl_validate_listeners`: This is a potentially vulnerable code block.
*   `svc_register`: Any issues in service registration can lead to serious instability.
*   `rpcb_create_af_local`
*   `rpcb_create_local_net`

Time to dig into `fs/nfsd/nfsctl.c` and see how `nfsd_nl_validate_listeners` is called to understand the context and potential attack surface.



4/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "nfsd_nl_validate_listeners",
  "PathPrefix": "fs/nfsd/nfsctl.c"
}

Results:
{
  "Output": "fs/nfsd/nfsctl.c=1982=static bool nfsd_nl_transport_supported(const char *name)\n--\nfs/nfsd/nfsctl.c-1996-/**\nfs/nfsd/nfsctl.c:1997: * nfsd_nl_validate_listeners - sanity-check the listener list from userland\nfs/nfsd/nfsctl.c-1998- * @info: netlink metadata and command arguments\n--\nfs/nfsd/nfsctl.c-2004- */\nfs/nfsd/nfsctl.c:2005:static int nfsd_nl_validate_listeners(struct genl_info *info)\nfs/nfsd/nfsctl.c-2006-{\n--\nfs/nfsd/nfsctl.c=2063=int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2078-\t */\nfs/nfsd/nfsctl.c:2079:\terr = nfsd_nl_validate_listeners(info);\nfs/nfsd/nfsctl.c-2080-\tif (err)\n--\nfs/nfsd/nfsctl.c-2108-\nfs/nfsd/nfsctl.c:2109:\t\t/* validated up front in nfsd_nl_validate_listeners() */\nfs/nfsd/nfsctl.c-2110-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\n--\nfs/nfsd/nfsctl.c-2164-\nfs/nfsd/nfsctl.c:2165:\t\t/* validated up front in nfsd_nl_validate_listeners() */\nfs/nfsd/nfsctl.c-2166-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\n"
}

5/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 19928
							output: 31
							thoughts: 41
						

Thoughts:
Here's my analysis of the provided information, as if these were my internal thoughts:

**Dissecting the Netlink Handler**

Alright, so `nfsd_nl_listener_set_doit` is the key function handling netlink interactions for NFS. That's good to know – it gives me a starting point. Now, to understand how this all fits together, I need to trace its connections. My gut feeling tells me that the registration process is crucial for this handler to function correctly. I'll head over to `net/sunrpc/svc.c` and zero in on `svc_register`. That's where I would expect to find the mechanism by which this netlink handler is integrated with the RPC service infrastructure. This should tell me how it's initialized and brought into the system.



6/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "svc_register",
  "PathPrefix": "net/sunrpc/svc.c"
}

Results:
{
  "Output": "net/sunrpc/svc.c=1074=static int __svc_rpcb_register6(struct net *net, const u32 program,\n--\nnet/sunrpc/svc.c-1117- */\nnet/sunrpc/svc.c:1118:static int __svc_register(struct net *net, const char *progname,\nnet/sunrpc/svc.c-1119-\t\t\t  const u32 program, const u32 version,\n--\nnet/sunrpc/svc.c-1137-\nnet/sunrpc/svc.c:1138:\ttrace_svc_register(progname, version, family, protocol, port, error);\nnet/sunrpc/svc.c-1139-\treturn error;\n--\nnet/sunrpc/svc.c=1143=int svc_rpcbind_set_version(struct net *net,\n--\nnet/sunrpc/svc.c-1148-{\nnet/sunrpc/svc.c:1149:\treturn __svc_register(net, progp-\u003epg_name, progp-\u003epg_prog,\nnet/sunrpc/svc.c-1150-\t\t\t\tversion, family, proto, port);\n--\nnet/sunrpc/svc.c=1184=EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\n--\nnet/sunrpc/svc.c-1186-/*\nnet/sunrpc/svc.c:1187: * Undo the [program, version] registrations that this svc_register() call\nnet/sunrpc/svc.c-1188- * already made, stopping at version @nvers of program @nprog.\n--\nnet/sunrpc/svc.c=1194=static void svc_unwind_register(const struct svc_serv *serv, struct net *net,\n--\nnet/sunrpc/svc.c-1209-/**\nnet/sunrpc/svc.c:1210: * svc_register - register an RPC service with the local portmapper\nnet/sunrpc/svc.c-1211- * @serv: svc_serv struct for the service to register\n--\nnet/sunrpc/svc.c-1219- */\nnet/sunrpc/svc.c:1220:int svc_register(const struct svc_serv *serv, struct net *net,\nnet/sunrpc/svc.c-1221-\t\t const int family, const unsigned short proto,\n"
}

7/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 20786
							output: 40
							thoughts: 51
						

Thoughts:
Okay, here's my take on those thoughts, framed as my own:

**Investigating the `svc_register` Function**

Right, `svc_register` is an *exported* function, which immediately makes it important to understand its purpose and behavior. My initial thought is to dive into the core RPC client registration mechanisms. Given its nature, I'd expect it to tie into how services are announced and discovered. Let's see...I'm going to check `net/sunrpc/rpcb_clnt.c`. I'm specifically looking for `rpcb_create_af_local` and `rpcb_create_local_net`. Those names suggest they're related to creating RPC broadcast sockets for local and network registration. Understanding how those functions are called in relation to `svc_register` will be crucial to getting a handle on how this function works in the bigger picture. This should reveal how the registration process works in detail.



8/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "rpcb_create_af_local",
  "PathPrefix": "net/sunrpc/rpcb_clnt.c"
}

Results:
{
  "Output": "net/sunrpc/rpcb_clnt.c=228=static const struct rpc_timeout rpcb_local_timeout = {\n--\nnet/sunrpc/rpcb_clnt.c-237- */\nnet/sunrpc/rpcb_clnt.c:238:static int rpcb_create_af_local(struct net *net,\nnet/sunrpc/rpcb_clnt.c-239-\t\t\t\tconst struct sockaddr_un *addr)\n--\nnet/sunrpc/rpcb_clnt.c=285=static int rpcb_create_local_abstract(struct net *net)\n--\nnet/sunrpc/rpcb_clnt.c-291-\nnet/sunrpc/rpcb_clnt.c:292:\treturn rpcb_create_af_local(net, \u0026rpcb_localaddr_abstract);\nnet/sunrpc/rpcb_clnt.c-293-}\n--\nnet/sunrpc/rpcb_clnt.c=295=static int rpcb_create_local_unix(struct net *net)\n--\nnet/sunrpc/rpcb_clnt.c-301-\nnet/sunrpc/rpcb_clnt.c:302:\treturn rpcb_create_af_local(net, \u0026rpcb_localaddr_unix);\nnet/sunrpc/rpcb_clnt.c-303-}\n"
}

9/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 21439
							output: 40
							thoughts: 27
						

Thoughts:
Here's a summary of my immediate thoughts, focusing on the static nature of these functions:

**Static Function Observations**

Okay, so I'm looking at `rpcb_create_af_local` and `rpcb_create_local_net`. The most immediately striking thing is that these functions are declared as static. This has specific implications, of course, depending on the context. I'm noting that because it fundamentally changes how they can be used. Their scope is limited to the current compilation unit, which means these are internal helper functions, not intended for external calls. That suggests they're likely involved in some lower-level internal setup or specific aspect of local networking configuration within the larger RPC system. Thinking further, their static nature almost certainly means they are not exported, which is standard practice for internal libraries. This is an immediate and useful piece of information when dealing with APIs.



10/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "rpcb_create_local_net",
  "PathPrefix": "net/sunrpc/rpcb_clnt.c"
}

Results:
{
  "Output": "net/sunrpc/rpcb_clnt.c=295=static int rpcb_create_local_unix(struct net *net)\n--\nnet/sunrpc/rpcb_clnt.c-308- */\nnet/sunrpc/rpcb_clnt.c:309:static int rpcb_create_local_net(struct net *net)\nnet/sunrpc/rpcb_clnt.c-310-{\n--\nnet/sunrpc/rpcb_clnt.c=357=int rpcb_create_local(struct net *net)\n--\nnet/sunrpc/rpcb_clnt.c-370-\t    rpcb_create_local_unix(net) != 0)\nnet/sunrpc/rpcb_clnt.c:371:\t\tresult = rpcb_create_local_net(net);\nnet/sunrpc/rpcb_clnt.c-372-\n"
}

11/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 21934
							output: 43
							thoughts: 46
						

Thoughts:
Here's my analysis of the provided information, as if these were my internal thoughts:

**Dissecting the Kernel Entry Point**

Okay, so `rpcb_create_local` is the designated entry point, the primary function initiating this process. My first instinct, given the context of NFS, is to immediately dive into the network file system aspects. Let's see how this ties into the network namespace configuration and control. I know `nfsd` is crucial.

Given the potential network aspects here, I'll now look within the `fs/nfsd/` directory and specifically the `nfsctl.c` file. I'm hoping to understand how this relates to network namespace listeners. I'm going to look for the function `nfsd_nl_listener_set_doit`. That should provide the key information on the interaction with network namespaces, and network configuration. That is probably the configuration function I'm looking for.



12/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "nfsd_nl_listener_set_doit",
  "PathPrefix": "fs/nfsd/nfsctl.c"
}

Results:
{
  "Output": "fs/nfsd/nfsctl.c=2005=static int nfsd_nl_validate_listeners(struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2056-/**\nfs/nfsd/nfsctl.c:2057: * nfsd_nl_listener_set_doit - set the nfs running sockets\nfs/nfsd/nfsctl.c-2058- * @skb: reply buffer\n--\nfs/nfsd/nfsctl.c-2062- */\nfs/nfsd/nfsctl.c:2063:int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\nfs/nfsd/nfsctl.c-2064-{\n"
}

13/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 22446
							output: 43
							thoughts: 68
						

Thoughts:
Here's my take on the thought process:

**Diving into the Netlink NFS Daemon**

Okay, so `nfsd_nl_listener_set_doit` is the core function for handling netlink commands within the NFS daemon. My initial thought is to figure out if there are more netlink functions within this subsystem. I'll need to locate them, so it's time to check the source code, specifically `fs/nfsd/nfsctl.c`. My hunch is that `nfsd_nl_version_get_doit` will contain some clues and perhaps indicate other similar netlink functions. Let's see what else is in that file.



14/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "nfsd_nl_version_get_doit",
  "PathPrefix": "fs/nfsd/nfsctl.c"
}

Results:
{
  "Output": "fs/nfsd/nfsctl.c=1835=int nfsd_nl_version_set_doit(struct sk_buff *skb, struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-1896-/**\nfs/nfsd/nfsctl.c:1897: * nfsd_nl_version_get_doit - get the enabled status for all supported nfs versions\nfs/nfsd/nfsctl.c-1898- * @skb: reply buffer\n--\nfs/nfsd/nfsctl.c-1902- */\nfs/nfsd/nfsctl.c:1903:int nfsd_nl_version_get_doit(struct sk_buff *skb, struct genl_info *info)\nfs/nfsd/nfsctl.c-1904-{\n"
}

15/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 22939
							output: 31
							thoughts: 86
						

Thoughts:
Here's my thought process, distilled:

**Focusing on NFS Server Registration**

Alright, let's zero in on the core registration mechanisms for this NFS server scenario. The key functions to investigate appear to be `nfsd_nl_listener_set_doit`, `svc_register`, and `rpcb_create_local`. These seem like the critical pieces where the NFS server announces itself and integrates with the RPC framework.

Now, let's quickly confirm the relevant configuration options are enabled. I'll double-check that `CONFIG_NFSD` and `CONFIG_SUNRPC` are set appropriately, as those are the obvious prerequisites for an NFS server to be built.

Time to delve into the kernel's configuration files. I'll need to examine `fs/nfsd/Kconfig` and `net/sunrpc/Kconfig` to understand how these configuration options influence the build process and possibly to identify any other related configurations that could be influencing this behaviour. This will give me a broader picture of the setup and potential dependencies.



16/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "config NFSD",
  "PathPrefix": "fs/nfsd/Kconfig"
}

Results:
{
  "Output": "fs/nfsd/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\nfs/nfsd/Kconfig:2:config NFSD\nfs/nfsd/Kconfig-3-\ttristate \"NFS server support\"\n--\nfs/nfsd/Kconfig-39-\nfs/nfsd/Kconfig:40:config NFSD_V2\nfs/nfsd/Kconfig-41-\tbool \"NFS server support for NFS version 2 (DEPRECATED)\"\n--\nfs/nfsd/Kconfig-50-\nfs/nfsd/Kconfig:51:config NFSD_V2_ACL\nfs/nfsd/Kconfig-52-\tbool \"NFS server support for the NFSv2 ACL protocol extension\"\n--\nfs/nfsd/Kconfig-54-\nfs/nfsd/Kconfig:55:config NFSD_V3_ACL\nfs/nfsd/Kconfig-56-\tbool \"NFS server support for the NFSv3 ACL protocol extension\"\n--\nfs/nfsd/Kconfig-76-\nfs/nfsd/Kconfig:77:config NFSD_V4\nfs/nfsd/Kconfig-78-\tbool \"NFS server support for NFS version 4\"\n--\nfs/nfsd/Kconfig-93-\nfs/nfsd/Kconfig:94:config NFSD_PNFS\nfs/nfsd/Kconfig-95-\tbool\nfs/nfsd/Kconfig-96-\nfs/nfsd/Kconfig:97:config NFSD_BLOCKLAYOUT\nfs/nfsd/Kconfig-98-\tbool \"NFSv4.1 server support for pNFS block layouts\"\n--\nfs/nfsd/Kconfig-109-\nfs/nfsd/Kconfig:110:config NFSD_SCSILAYOUT\nfs/nfsd/Kconfig-111-\tbool \"NFSv4.1 server support for pNFS SCSI layouts\"\n--\nfs/nfsd/Kconfig-123-\nfs/nfsd/Kconfig:124:config NFSD_FLEXFILELAYOUT\nfs/nfsd/Kconfig-125-\tbool \"NFSv4.1 server support for pNFS Flex File layouts\"\n--\nfs/nfsd/Kconfig-140-\nfs/nfsd/Kconfig:141:config NFSD_V4_2_INTER_SSC\nfs/nfsd/Kconfig-142-\tbool \"NFSv4.2 inter server to server COPY\"\n--\nfs/nfsd/Kconfig-150-\nfs/nfsd/Kconfig:151:config NFSD_V4_SECURITY_LABEL\nfs/nfsd/Kconfig-152-\tbool \"Provide Security Label support for NFSv4 server\"\n--\nfs/nfsd/Kconfig-163-\nfs/nfsd/Kconfig:164:config NFSD_LEGACY_CLIENT_TRACKING\nfs/nfsd/Kconfig-165-\tbool \"Support legacy NFSv4 client tracking methods (DEPRECATED)\"\n--\nfs/nfsd/Kconfig-179-\nfs/nfsd/Kconfig:180:config NFSD_V4_POSIX_ACLS\nfs/nfsd/Kconfig-181-\tbool \"Support NFSv4 POSIX draft ACLs\"\n"
}

17/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24138
							output: 30
							thoughts: 23
						

Thoughts:
Here's my thought process, as requested:

**Navigating the NFS Configuration Labyrinth**

Okay, so I'm dealing with `NFSD` – that's the core of the NFS server configuration, no doubt. But I know that the kernel config can be a tangled web. Where's the best place to start? My gut tells me that `net/sunrpc/Kconfig` is likely to hold some crucial information about the RPC layer, which, of course, underpins NFS. Let's dive in there. I'm betting that's where I'll find the dependencies and related configurations that are relevant to getting `NFSD` behaving as expected. Time to see what treasures this Kconfig file holds!



18/2 2026/08/11 12:17 tool grepper
0m Args:
{
  "Expression": "config SUNRPC",
  "PathPrefix": "net/sunrpc/Kconfig"
}

Results:
{
  "Output": "net/sunrpc/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\nnet/sunrpc/Kconfig:2:config SUNRPC\nnet/sunrpc/Kconfig-3-\ttristate\n--\nnet/sunrpc/Kconfig-5-\nnet/sunrpc/Kconfig:6:config SUNRPC_GSS\nnet/sunrpc/Kconfig-7-\ttristate\n--\nnet/sunrpc/Kconfig-10-\nnet/sunrpc/Kconfig:11:config SUNRPC_BACKCHANNEL\nnet/sunrpc/Kconfig-12-\tbool\n--\nnet/sunrpc/Kconfig-14-\nnet/sunrpc/Kconfig:15:config SUNRPC_SWAP\nnet/sunrpc/Kconfig-16-\tbool\n--\nnet/sunrpc/Kconfig=19=config RPCSEC_GSS_KRB5\n--\nnet/sunrpc/Kconfig-35-\nnet/sunrpc/Kconfig:36:config SUNRPC_DEBUG\nnet/sunrpc/Kconfig-37-\tbool \"RPC: Enable dprintk debugging\"\n--\nnet/sunrpc/Kconfig-49-\nnet/sunrpc/Kconfig:50:config SUNRPC_DEBUG_TRACE\nnet/sunrpc/Kconfig-51-\tbool \"RPC: Send dfprintk() output to the trace buffer\"\n--\nnet/sunrpc/Kconfig-63-\nnet/sunrpc/Kconfig:64:config SUNRPC_XPRT_RDMA\nnet/sunrpc/Kconfig-65-\ttristate \"RPC-over-RDMA transport\"\n"
}

19/2 2026/08/11 12:17 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24745
							output: 36
							thoughts: 263
						

Thoughts:
**My Analysis of NFSD's RPC Registration**

Okay, so `NFSD` is using `SUNRPC`. Seems logical, and that's the starting point. Since `NFSD` handles this dependency, I think we're good there.

Now, I need to make absolutely sure I have the right function names. First up, I want to confirm that `nfsd_nl_listener_set_doit` is indeed the correct function. *Yep*, that's the one. Okay, next, let's just make sure `svc_register` is what I think it is. *Confirmed*. And how about `rpcb_create_local`? *Yep, that is the correct one*.

Alright, let's see if there are any other important pieces to consider. Ah, `nfsd_nl_validate_listeners` is static, but it's called within `nfsd_nl_listener_set_doit`. I could include it explicitly, or just trust that `nfsd_nl_listener_set_doit` does the work. Since it's static, it might be inlined. But it's also a reasonably large function, so it might not be inlined. I think to be safe, I'll include both `nfsd_nl_listener_set_doit` and `svc_register`. Better to err on the side of completeness.

Finally, I should probably take a look at `svc_generic_rpcbind_set` in `net/sunrpc/svc.c` just to be sure. That seems to be a key function in this area too.



20/2 2026/08/11 12:18 tool grepper
0m Args:
{
  "Expression": "svc_generic_rpcbind_set",
  "PathPrefix": "net/sunrpc/svc.c"
}

Results:
{
  "Output": "net/sunrpc/svc.c=1143=int svc_rpcbind_set_version(struct net *net,\n--\nnet/sunrpc/svc.c-1153-\nnet/sunrpc/svc.c:1154:int svc_generic_rpcbind_set(struct net *net,\nnet/sunrpc/svc.c-1155-\t\t\t    const struct svc_program *progp,\n--\nnet/sunrpc/svc.c-1183-}\nnet/sunrpc/svc.c:1184:EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\nnet/sunrpc/svc.c-1185-\n"
}

21/2 2026/08/11 12:18 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25574
							output: 144
							thoughts: 428
						

Thoughts:
**Analyzing the NFSd RPC Bind Patch**

Okay, let's break down this patch. My immediate thought is that `svc_generic_rpcbind_set` is being exported, so it's a key piece in this puzzle. It looks like the core change involves enhancing `svc_register` to gracefully manage errors from `progp->pg_rpcbind_set` and unwind registrations accordingly. Smart move, that's crucial for robustness.

The modifications to `rpcb_create_af_local` and `rpcb_create_local_net` to utilize `rpcb_local_timeout` are also important. We're clearly dealing with refinements in local RPC binding behavior. And limiting the number of listeners in `nfsd_nl_validate_listeners` is definitely worth checking. The transport names check is a good security measure.

So, the key symbols this patch is touching are: `nfsd_nl_listener_set_doit`, `svc_register`, and `rpcb_create_local`.

The dependency on `NFSD` is obvious, but let's just confirm. Checking `tools/testing/selftests/nfsd/config` is a good practice. Aha, there it is: `NFSD` and `NFSD_V4` are present, so we're good on that front.

Now, let's verify reachability. The call chain for `rpcb_create_local` looks like: `rpcb_create_local` -> `svc_rpcb_setup` -> `svc_bind` -> `svc_xprt_create_from_sa` -> `nfsd_nl_listener_set_doit`. Yep, reachable. And for `svc_register`: `svc_register` -> `svc_bind` -> `svc_xprt_create_from_sa` -> `nfsd_nl_listener_set_doit`. Again, reachable.

Alright, that covers the core logic and dependencies. I'm satisfied. Time to set the results.



22/2 2026/08/11 12:18 tool set-results
0m Args:
{
  "EnableConfigs": [
    "NFSD",
    "NFSD_V4"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "rpcb_create_local"
  ],
  "Reasoning": "The patch modifies the NFSD netlink listener configuration logic to validate transport names and limit the number of listeners. It also updates the local rpcbind client to use a specific timeout and modifies the sunrpc service registration logic to properly unwind registrations on failure. These changes affect core NFSD and sunrpc logic reachable from userspace via netlink and RPC operations.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "NFSD",
    "NFSD_V4"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "rpcb_create_local"
  ],
  "Reasoning": "The patch modifies the NFSD netlink listener configuration logic to validate transport names and limit the number of listeners. It also updates the local rpcbind client to use a specific timeout and modifies the sunrpc service registration logic to properly unwind registrations on failure. These changes affect core NFSD and sunrpc logic reachable from userspace via netlink and RPC operations.",
  "WorthFuzzing": true
}

23/1 2026/08/11 12:18 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

24/2 2026/08/11 12:18 agent kmsan-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "KMSANReasoning": "The patch series primarily adds validation for netlink transport names, sets an extended ACK message on error, configures a timeout for local rpcbind clients, and adds error unwinding for RPC service registration. It does not introduce any new structures sent to user space, nor does it modify bounds checking or data length calculations in a way that could expose uninitialized memory. The netlink attribute payload accessed via `nla_data` is guaranteed to be null-terminated by the `NLA_NUL_STRING` policy, so `strcmp` will not read past the initialized buffer. Any potential out-of-bounds accesses or memory corruption bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 95e42039d503f9b0731fb044e87feb6e1a839415
Author: syz-cluster <triage@syzkaller.com>
Date:   Tue Aug 11 12:17:16 2026 +0000

    syz-cluster: applied patch under review

diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index 7e1d5a5d6511e..f73a2ea425571 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -1973,21 +1973,39 @@ int nfsd_nl_version_get_doit(struct sk_buff *skb, struct genl_info *info)
 	return err;
 }
 
+/*
+ * Transport classes NFSD knows how to instantiate. Vetting the name here
+ * keeps a bogus string from reaching svc_xprt_create_from_sa(), where an
+ * unknown name triggers a request_module("svc%s", name) upcall under
+ * nfsd_mutex.
+ */
+static bool nfsd_nl_transport_supported(const char *name)
+{
+	static const char * const supported[] = { "tcp", "udp", "rdma" };
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(supported); i++)
+		if (!strcmp(name, supported[i]))
+			return true;
+	return false;
+}
+
+/* Upper bound on the number of listeners a single request may carry. */
+#define NFSD_NL_LISTENER_MAX	1024
+
 /**
  * nfsd_nl_validate_listeners - sanity-check the listener list from userland
  * @info: netlink metadata and command arguments
  *
- * Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that each entry
- * is well-formed: it parses against the policy, carries both an address and
- * a transport name, and the address is long enough for its family. Doing
- * this up front lets the callers below assume every entry is valid and
- * guarantees we make no changes when the request is malformed.
+ * Walk every NFSD_A_SERVER_SOCK_ADDR attribute and confirm that the list is
+ * not oversized and that each entry is well-formed.
  *
  * Return: 0 if every entry is valid, or a negative errno otherwise.
  */
 static int nfsd_nl_validate_listeners(struct genl_info *info)
 {
 	const struct nlattr *attr;
+	unsigned int count = 0;
 	int rem;
 
 	nlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info->nlhdr,
@@ -1996,6 +2014,11 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 		struct sockaddr *sa;
 		int err;
 
+		if (++count > NFSD_NL_LISTENER_MAX) {
+			NL_SET_ERR_MSG(info->extack, "too many listeners");
+			return -E2BIG;
+		}
+
 		err = nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,
 				       nfsd_sock_nl_policy, info->extack);
 		if (err < 0)
@@ -2004,6 +2027,9 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 		if (!tb[NFSD_A_SOCK_ADDR] || !tb[NFSD_A_SOCK_TRANSPORT_NAME])
 			return -EINVAL;
 
+		if (!nfsd_nl_transport_supported(nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])))
+			return -EPROTONOSUPPORT;
+
 		sa = nla_data(tb[NFSD_A_SOCK_ADDR]);
 		if (nla_len(tb[NFSD_A_SOCK_ADDR]) < sizeof(sa->sa_family))
 			return -EINVAL;
@@ -2156,8 +2182,12 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,
 					      current_cred());
 		/* always save the latest error */
-		if (ret < 0)
+		if (ret < 0) {
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener: %d",
+					   xcl_name, ret);
 			err = ret;
+		}
 	}
 
 	if (!serv->sv_nrthreads && list_empty(&nn->nfsd_serv->sv_permsocks))
diff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c
index 6aa372188c86a..0aa376b82a526 100644
--- a/net/sunrpc/rpcb_clnt.c
+++ b/net/sunrpc/rpcb_clnt.c
@@ -221,6 +221,16 @@ static void rpcb_set_local(struct net *net, struct rpc_clnt *clnt,
 # define SUN_LEN(ptr) (offsetof(struct sockaddr_un, sun_path)		\
 		      + 1 + strlen((ptr)->sun_path + 1))
 
+/*
+ * The kernel's rpcbind client talks only to the local rpcbind, over loopback
+ * or a local AF_LOCAL socket, where a healthy rpcbind answers in microseconds.
+ */
+static const struct rpc_timeout rpcb_local_timeout = {
+	.to_initval	= 1 * HZ,
+	.to_maxval	= 1 * HZ,
+	.to_retries	= 0,
+};
+
 /*
  * Returns zero on success, otherwise a negative errno value
  * is returned.
@@ -238,6 +248,7 @@ static int rpcb_create_af_local(struct net *net,
 		.version	= RPCBVERS_2,
 		.authflavor	= RPC_AUTH_NULL,
 		.cred		= current_cred(),
+		.timeout	= &rpcb_local_timeout,
 		/*
 		 * We turn off the idle timeout to prevent the kernel
 		 * from automatically disconnecting the socket.
@@ -312,6 +323,7 @@ static int rpcb_create_local_net(struct net *net)
 		.version	= RPCBVERS_2,
 		.authflavor	= RPC_AUTH_UNIX,
 		.cred		= current_cred(),
+		.timeout	= &rpcb_local_timeout,
 		.flags		= RPC_CLNT_CREATE_NOPING,
 	};
 	struct rpc_clnt *clnt, *clnt4;
diff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c
index 05c3e6e2f6594..7c414634c753b 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1183,6 +1183,29 @@ int svc_generic_rpcbind_set(struct net *net,
 }
 EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
 
+/*
+ * Undo the [program, version] registrations that this svc_register() call
+ * already made, stopping at version @nvers of program @nprog.
+ *
+ * Note that rpcbind matches RPCBPROC_UNSET on [program, version, netid] and
+ * ignores the address, so this clears the netid rather than the one port.
+ * That is the granularity svc_delete_xprt() unregisters at as well.
+ */
+static void svc_unwind_register(const struct svc_serv *serv, struct net *net,
+				const int family, const unsigned short proto,
+				unsigned int nprog, unsigned int nvers)
+{
+	unsigned int p, i;
+
+	for (p = 0; p <= nprog; p++) {
+		struct svc_program *progp = &serv->sv_programs[p];
+		unsigned int last = p < nprog ? progp->pg_nvers : nvers;
+
+		for (i = 0; i < last; i++)
+			progp->pg_rpcbind_set(net, progp, i, family, proto, 0);
+	}
+}
+
 /**
  * svc_register - register an RPC service with the local portmapper
  * @serv: svc_serv struct for the service to register
@@ -1191,7 +1214,8 @@ EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
  * @proto: transport protocol number to advertise
  * @port: port to advertise
  *
- * Service is registered for any address in the passed-in protocol family
+ * Service is registered for any address in the passed-in protocol family.
+ * A @port of 0 unregisters instead, and then every program is attempted.
  */
 int svc_register(const struct svc_serv *serv, struct net *net,
 		 const int family, const unsigned short proto,
@@ -1208,13 +1232,21 @@ int svc_register(const struct svc_serv *serv, struct net *net,
 		struct svc_program *progp = &serv->sv_programs[p];
 
 		for (i = 0; i < progp->pg_nvers; i++) {
+			int ret;
 
-			error = progp->pg_rpcbind_set(net, progp, i,
+			ret = progp->pg_rpcbind_set(net, progp, i,
 					family, proto, port);
-			if (error < 0) {
+			if (ret < 0) {
 				printk(KERN_WARNING "svc: failed to register "
 					"%sv%u RPC service (errno %d).\n",
-					progp->pg_name, i, -error);
+					progp->pg_name, i, -ret);
+				if (port) {
+					svc_unwind_register(serv, net, family,
+							    proto, p, i);
+					return ret;
+				}
+				if (!error)
+					error = ret;
 				break;
 			}
 		}
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 84343fd1e354b..460508de9374f 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -89,6 +89,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 0000000000000..19e6dec04d8e9
--- /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 0000000000000..15ac65549d259
--- /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 0000000000000..ab84523fbedf3
--- /dev/null
+++ b/tools/testing/selftests/nfsd/config
@@ -0,0 +1,8 @@
+CONFIG_NAMESPACES=y
+CONFIG_NET_NS=y
+CONFIG_SHMEM=y
+CONFIG_TMPFS=y
+CONFIG_UNIX=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 0000000000000..9972b0db45ff9
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -0,0 +1,1030 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression tests for the NFSD generic-netlink listener interface
+ * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).
+ *
+ * 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. 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>
+#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_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 */
+
+#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);
+}
+
+/*
+ * 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) {
+	pid_t rpcbd;
+};
+
+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?)");
+
+	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];
+	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.
+ *
+ * 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)
+{
+	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) };
+	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)
+{
+	struct listener_ent got[MAX_LISTENERS];
+
+	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
diff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings
new file mode 100644
index 0000000000000..6091b45d226ba
--- /dev/null
+++ b/tools/testing/selftests/nfsd/settings
@@ -0,0 +1 @@
+timeout=120

25/3 2026/08/11 12:18 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 17927
							output: 40
							thoughts: 524
						

Thoughts:
**KMSAN Fuzzing Session Justification**

Okay, let's break this down. We have a patch series here, and the key question is whether it warrants a dedicated KMSAN fuzzing session. My initial impression is that the changes are diverse enough that it's worth considering, but let's dive into the specifics.

First, the modifications are spread across several files, including `fs/nfsd/nfsctl.c`, `net/sunrpc/rpcb_clnt.c`, `net/sunrpc/svc.c`, and a new selftest in `tools/testing/selftests/nfsd/*`. That's a reasonable distribution, indicating potential impact across the NFS daemon and its underlying RPC mechanisms.

Now, the part that's screaming "uninitialized memory" at me is the `fs/nfsd/nfsctl.c` changes, particularly around netlink transport validation. Specifically, `nfsd_nl_transport_supported` looks like the prime suspect. It takes a `const char *name` from the netlink attribute data (`nla_data`), and compares it with a set of supported transport names like "tcp", "udp", and "rdma".

The critical question here is, is the `name` coming from `nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])` guaranteed to be null-terminated? `nla_data` simply gives us a pointer to the attribute's payload. `strcmp` is then used, which is where the risk lies. If the netlink attribute payload isn't null-terminated, `strcmp` could read past the end of the allocated memory, a classic KMSAN issue.

I need to confirm whether `nla_data` is null-terminated or not. I know it *typically* is if the policy specified `NLA_NUL_STRING` or `NLA_STRING`. So, I'm checking the `nfsd_sock_nl_policy` now in `fs/nfsd/nfsctl.c` to see how that attribute is defined. A quick check of that policy will tell me if the null-termination is ensured.

If it's not null-terminated, this is definitely a KMSAN candidate. If it *is* guaranteed to be null-terminated by the netlink attribute's policy, then the risk drops significantly for that particular change. However, I still need to quickly analyze the other changes to ensure nothing else stands out. The new self tests are also valuable, and I'll review them briefly as well. But right now, this potential string-related vulnerability is my focus.



26/3 2026/08/11 12:18 tool grepper
0m Args:
{
  "Expression": "nfsd_sock_nl_policy",
  "PathPrefix": "fs/nfsd/nfsctl.c"
}

Results:
{
  "Output": "fs/nfsd/nfsctl.c=2005=static int nfsd_nl_validate_listeners(struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2022-\t\terr = nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2023:\t\t\t\t       nfsd_sock_nl_policy, info-\u003eextack);\nfs/nfsd/nfsctl.c-2024-\t\tif (err \u003c 0)\n--\nfs/nfsd/nfsctl.c=2063=int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2110-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2111:\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\nfs/nfsd/nfsctl.c-2112-\t\t\tcontinue;\n--\nfs/nfsd/nfsctl.c-2166-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2167:\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\nfs/nfsd/nfsctl.c-2168-\t\t\tcontinue;\n"
}

27/3 2026/08/11 12:18 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 19470
							output: 35
							thoughts: 19
						

Thoughts:
Here's my thought process, summarized as you requested:

**Navigating the Codebase: Tracing `nfsd_sock_nl_policy`**

Okay, so I need to understand this `nfsd_sock_nl_policy`. My immediate instinct is to figure out where it's *defined*. Where is this thing coming from? Let's trace this function or variable definition and see how it's used. This kind of investigation is fundamental; I need to know the origin and scope to grasp its purpose and behavior within the NFS daemon's network interface handling. Let's dig in and see what we can find.



28/3 2026/08/11 12:18 tool grepper
0m Args:
{
  "Expression": "nfsd_sock_nl_policy",
  "PathPrefix": "fs/nfsd/"
}

Results:
{
  "Output": "fs/nfsd/netlink.c=35=const struct nla_policy nfsd_fslocations_nl_policy[NFSD_A_FSLOCATIONS_LOCATION + 1] = {\n--\nfs/nfsd/netlink.c-38-\nfs/nfsd/netlink.c:39:const struct nla_policy nfsd_sock_nl_policy[NFSD_A_SOCK_TRANSPORT_NAME + 1] = {\nfs/nfsd/netlink.c-40-\t[NFSD_A_SOCK_ADDR] = { .type = NLA_BINARY, },\n--\nfs/nfsd/netlink.c=82=static const struct nla_policy nfsd_listener_set_nl_policy[NFSD_A_SERVER_SOCK_ADDR + 1] = {\nfs/nfsd/netlink.c:83:\t[NFSD_A_SERVER_SOCK_ADDR] = NLA_POLICY_NESTED(nfsd_sock_nl_policy),\nfs/nfsd/netlink.c-84-};\n--\nfs/nfsd/netlink.h=19=extern const struct nla_policy nfsd_fslocations_nl_policy[NFSD_A_FSLOCATIONS_LOCATION + 1];\nfs/nfsd/netlink.h:20:extern const struct nla_policy nfsd_sock_nl_policy[NFSD_A_SOCK_TRANSPORT_NAME + 1];\nfs/nfsd/netlink.h-21-extern const struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_FSID + 1];\n--\nfs/nfsd/nfsctl.c=2005=static int nfsd_nl_validate_listeners(struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2022-\t\terr = nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2023:\t\t\t\t       nfsd_sock_nl_policy, info-\u003eextack);\nfs/nfsd/nfsctl.c-2024-\t\tif (err \u003c 0)\n--\nfs/nfsd/nfsctl.c=2063=int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2110-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2111:\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\nfs/nfsd/nfsctl.c-2112-\t\t\tcontinue;\n--\nfs/nfsd/nfsctl.c-2166-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2167:\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\nfs/nfsd/nfsctl.c-2168-\t\t\tcontinue;\n"
}

29/3 2026/08/11 12:18 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 20486
							output: 37
							thoughts: 29
						

Thoughts:
Here's my analysis of the provided text, rendered in the first person, assuming I'm an expert in the field:

**Diving into `nfsd_sock_nl_policy`**

Alright, let's get down to brass tacks. I need to take a look at the `nfsd_sock_nl_policy` structure, specifically in the `fs/nfsd/netlink.c` file. This immediately tells me we're dealing with the Netlink interface within the NFS server daemon (nfsd). My assumption is that this policy likely defines the structure and validation rules for messages exchanged over this Netlink socket related to NFS operations. I'll be looking to understand the attributes and constraints imposed, as this will inform how the NFS server interacts with userspace applications and other kernel modules. This is a crucial element for security and efficient operation, and potentially the entry point for various NFS administration and control tasks. Time to dig in and see what's what.



30/3 2026/08/11 12:18 tool read-file
0m Args:
{
  "File": "fs/nfsd/netlink.c",
  "FirstLine": 30,
  "LineCount": 20
}

Results:
{
  "Contents": "  30:\tconst struct nla_policy nfsd_fslocation_nl_policy[NFSD_A_FSLOCATION_PATH + 1] = {\n  31:\t\t[NFSD_A_FSLOCATION_HOST] = { .type = NLA_NUL_STRING, },\n  32:\t\t[NFSD_A_FSLOCATION_PATH] = { .type = NLA_NUL_STRING, },\n  33:\t};\n  34:\t\n  35:\tconst struct nla_policy nfsd_fslocations_nl_policy[NFSD_A_FSLOCATIONS_LOCATION + 1] = {\n  36:\t\t[NFSD_A_FSLOCATIONS_LOCATION] = NLA_POLICY_NESTED(nfsd_fslocation_nl_policy),\n  37:\t};\n  38:\t\n  39:\tconst struct nla_policy nfsd_sock_nl_policy[NFSD_A_SOCK_TRANSPORT_NAME + 1] = {\n  40:\t\t[NFSD_A_SOCK_ADDR] = { .type = NLA_BINARY, },\n  41:\t\t[NFSD_A_SOCK_TRANSPORT_NAME] = { .type = NLA_NUL_STRING, },\n  42:\t};\n  43:\t\n  44:\tconst struct nla_policy nfsd_svc_export_nl_policy[NFSD_A_SVC_EXPORT_FSID + 1] = {\n  45:\t\t[NFSD_A_SVC_EXPORT_SEQNO] = { .type = NLA_U64, },\n  46:\t\t[NFSD_A_SVC_EXPORT_CLIENT] = { .type = NLA_NUL_STRING, },\n  47:\t\t[NFSD_A_SVC_EXPORT_PATH] = { .type = NLA_NUL_STRING, },\n  48:\t\t[NFSD_A_SVC_EXPORT_NEGATIVE] = { .type = NLA_FLAG, },\n  49:\t\t[NFSD_A_SVC_EXPORT_EXPIRY] = { .type = NLA_U64, },\n"
}

31/3 2026/08/11 12:18 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 21295
							output: 178
							thoughts: 1230
						

Thoughts:
**Patch Analysis: KMSAN Concerns**

Okay, let's break this down. First, the crucial bit: `[NFSD_A_SOCK_TRANSPORT_NAME] = { .type = NLA_NUL_STRING, }`. This means that `nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])` *is* guaranteed to be null-terminated. Good, `strcmp` is safe, no uninitialized memory reads there. We're clear on that front.

Now, let's scan the rest.

*   In `fs/nfsd/nfsctl.c`, the patch introduces `nfsd_nl_transport_supported`. It's just a simple string comparison against a hardcoded list ("tcp", "udp", "rdma"). No problems there; string comparison is straightforward. Also, it adds a check, verifying the transport name against this function. No issues. It updates an error message if `svc_xprt_create_from_sa` fails. Again, just setting an error message; straightforward.

*   `net/sunrpc/rpcb_clnt.c` defines a `rpcb_local_timeout` and assigns a pointer to it. Nothing complex here.

*   `net/sunrpc/svc.c` adds a function `svc_unwind_register` and modifies a loop in the `svc_program` iteration. This looks like error handling and registration unwinding. If a registration fails, it attempts to "unwind" the registration process.

Alright, so does this whole thing expose uninitialized memory risks? No.

1.  No new structures are allocated and sent to userspace.
2.  No uninitialized variables are used in any conditional logic.
3.  No bounds checks are modified in a way that introduces the possibility of reading uninitialized memory.
4.  The `nla_data` guarantee with the null termination is a key factor here.

Therefore, no KMSAN is needed. KASAN and other standard debugging tools should be sufficient to catch any out-of-bounds or use-after-free bugs if they existed. I'm going to set `NeedsKMSAN=false` and explain this reasoning in the commit message.



32/3 2026/08/11 12:18 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch series primarily adds validation for netlink transport names, sets an extended ACK message on error, configures a timeout for local rpcbind clients, and adds error unwinding for RPC service registration. It does not introduce any new structures sent to user space, nor does it modify bounds checking or data length calculations in a way that could expose uninitialized memory. The netlink attribute payload accessed via `nla_data` is guaranteed to be null-terminated by the `NLA_NUL_STRING` policy, so `strcmp` will not read past the initialized buffer. Any potential out-of-bounds accesses or memory corruption bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch series primarily adds validation for netlink transport names, sets an extended ACK message on error, configures a timeout for local rpcbind clients, and adds error unwinding for RPC service registration. It does not introduce any new structures sent to user space, nor does it modify bounds checking or data length calculations in a way that could expose uninitialized memory. The netlink attribute payload accessed via `nla_data` is guaranteed to be null-terminated by the `NLA_NUL_STRING` policy, so `strcmp` will not read past the initialized buffer. Any potential out-of-bounds accesses or memory corruption bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)