| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/08/10 21:39 | flow | patch-triage |
1mArgs:null Results: {
"EnableConfigs": [
"NFSD",
"NFSD_V4",
"SUNRPC",
"NET_NS",
"IPV6"
],
"FocusSymbols": [
"nfsd_nl_listener_set_doit",
"svc_register",
"rpcb_create_local"
],
"KMSANReasoning": "The patch series introduces validation checks for NFSD netlink listener attributes (e.g., limiting the number of listeners to `NFSD_NL_LISTENER_MAX` and validating the transport name via `nfsd_nl_transport_supported`). It also fixes error code propagation in `svc_register` and sets a local timeout for rpcbind clients. \n\nThe netlink attribute `NFSD_A_SOCK_TRANSPORT_NAME` is defined as `NLA_NUL_STRING` in `nfsd_sock_nl_policy`, ensuring that `nla_data()` returns a properly null-terminated string before it is passed to `strcmp()` in `nfsd_nl_transport_supported`. \n\nNo new complex data structures are allocated or sent to user space, and there are no changes that could expose uninitialized stack or heap memory. Any potential issues introduced by these changes (such as out-of-bounds accesses or logic bugs) would be effectively caught by standard KASAN and other default sanitizers. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false,
"Reasoning": "The patch modifies the NFSD netlink listener configuration logic, adds validation for transport classes, limits the number of listeners, introduces a timeout for local rpcbind calls, and fixes error handling in svc_register. These are reachable core kernel changes in the NFS server and RPC subsystems that have functional impact and should be fuzzed.",
"WorthFuzzing": true
} |
| 1/1 | 2026/08/10 21:39 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 601eb7048fd1cc371951f58af00cc250f9e2bac3\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Mon Aug 10 21:39:37 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..e9cfceb1a3342 100644\n--- a/net/sunrpc/svc.c\n+++ b/net/sunrpc/svc.c\n@@ -1208,13 +1208,16 @@ 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 (!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..e6945ff9551c4\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/config\n@@ -0,0 +1,4 @@\n+CONFIG_NET_NS=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..3e3307680d7d4\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c\n@@ -0,0 +1,920 @@\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/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+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 \u0026\u0026 e[i].port == port \u0026\u0026\n+\t\t !strcmp(e[i].xprt, xprt))\n+\t\t\treturn \u0026e[i];\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+#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+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+\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\tn++;\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+\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+}\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+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+\tEXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));\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+\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+}\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+\tEXPECT_EQ(htonl(INADDR_LOOPBACK), got[0].a4.s_addr);\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+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+\n+\tASSERT_EQ(0, listener_set(attrs, off));\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+\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+\tEXPECT_EQ(0, memcmp(\u0026got[0].a6, \u0026in6addr_loopback, sizeof(in6addr_loopback)));\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+\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+\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+\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+\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/10 21:39 | agent | patch-evaluator |
0mModel:best-expensive Args: null Results: {
"EnableConfigs": [
"NFSD",
"NFSD_V4",
"SUNRPC",
"NET_NS",
"IPV6"
],
"FocusSymbols": [
"nfsd_nl_listener_set_doit",
"svc_register",
"rpcb_create_local"
],
"Reasoning": "The patch modifies the NFSD netlink listener configuration logic, adds validation for transport classes, limits the number of listeners, introduces a timeout for local rpcbind calls, and fixes error handling in svc_register. These are reachable core kernel changes in the NFS server and RPC subsystems that have functional impact and should be fuzzed.",
"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 601eb7048fd1cc371951f58af00cc250f9e2bac3
Author: syz-cluster <triage@syzkaller.com>
Date: Mon Aug 10 21:39:37 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..e9cfceb1a3342 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1208,13 +1208,16 @@ 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 (!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..e6945ff9551c4
--- /dev/null
+++ b/tools/testing/selftests/nfsd/config
@@ -0,0 +1,4 @@
+CONFIG_NET_NS=y
+CONFIG_IPV6=y
+CONFIG_NFSD=y
+CONFIG_NFSD_V4=y
diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
new file mode 100644
index 0000000000000..3e3307680d7d4
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -0,0 +1,920 @@
+// 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/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);
+}
+
+static struct listener_ent *find_listener(struct listener_ent *e, int n,
+ const char *xprt, int family,
+ uint16_t port)
+{
+ int i;
+
+ for (i = 0; i < n; i++)
+ if (e[i].family == family && e[i].port == port &&
+ !strcmp(e[i].xprt, xprt))
+ return &e[i];
+ return NULL;
+}
+
+/* Start (@n > 0) or stop (@n == 0) nfsd threads in this netns. */
+static int threads_set(int n)
+{
+ char attrs[64];
+ uint32_t v = n;
+ int off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, &v, sizeof(v));
+
+ return genl_request(NFSD_CMD_THREADS_SET, attrs, off);
+}
+
+/* ------------------- per-netns local rpcbind stub ------------------- */
+
+/*
+ * Creating a listener registers with rpcbind: svc_xprt_create_from_sa()
+ * passes flags of 0, so pmap_register is true in svc_setup_socket(), and
+ * nfsd_version3 is registerable by default and does not set vs_rpcb_optnl,
+ * so a registration failure aborts listener creation. The abstract AF_LOCAL
+ * name the kernel tries first is per-netns (unix_find_abstract() takes a
+ * struct net), so answer it here and stay out of the host's rpcbind.
+ *
+ * Arguments are never decoded. The NULL procedure gets an empty success and
+ * SET/UNSET get TRUE, for both RPCBVERS_2 and RPCBVERS_4. v4 has to be
+ * answered because __svc_rpcb_register6() turns a v4 refusal into
+ * -EAFNOSUPPORT, which would fail every IPv6 listener.
+ *
+ * In RPCB_STUB_REFUSE mode SET is answered FALSE instead, which
+ * rpcb_register_call() reports as -EACCES. UNSET is left alone: only
+ * svc_unregister() issues it, and it discards the result.
+ */
+#define RPCB_PROGRAM 100000
+#define RPCB_PROC_NULL 0
+#define RPCB_PROC_SET 1
+#define RPCB_PROC_UNSET 2
+#define RPCB_ABSTRACT_NAME "/run/rpcbind.sock"
+#define RPCB_STUB_MAXCONN 4
+
+enum { RPCB_STUB_ACCEPT, RPCB_STUB_REFUSE };
+
+static int rpcb_stub_listen(void)
+{
+ struct sockaddr_un sun = { .sun_family = AF_UNIX };
+ size_t nlen = strlen(RPCB_ABSTRACT_NAME);
+ socklen_t alen;
+ int fd;
+
+ /* Abstract names are length-delimited, so the length must match. */
+ memcpy(sun.sun_path + 1, RPCB_ABSTRACT_NAME, nlen);
+ alen = offsetof(struct sockaddr_un, sun_path) + 1 + nlen;
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0)
+ return -1;
+ if (bind(fd, (struct sockaddr *)&sun, alen) < 0 ||
+ listen(fd, RPCB_STUB_MAXCONN) < 0) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int rpcb_stub_read(int fd, void *buf, size_t len)
+{
+ size_t done = 0;
+
+ while (done < len) {
+ ssize_t n = read(fd, (char *)buf + done, len - done);
+
+ if (n <= 0)
+ return -1;
+ done += n;
+ }
+ return 0;
+}
+
+/* Handle one record-marked RPC call. Returns -1 when the peer is done. */
+static int rpcb_stub_call(int fd, int mode)
+{
+ uint32_t mark, call[6], rep[7];
+ unsigned int len, nrep = 6;
+ size_t replen;
+
+ if (rpcb_stub_read(fd, &mark, sizeof(mark)))
+ return -1;
+ len = ntohl(mark) & 0x7fffffff;
+ if (len < sizeof(call) || len > 4096)
+ return -1;
+ if (rpcb_stub_read(fd, call, sizeof(call)))
+ return -1;
+
+ /* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */
+ for (len -= sizeof(call); len; ) {
+ char sink[256];
+ unsigned int n = len > sizeof(sink) ? sizeof(sink) : len;
+
+ if (rpcb_stub_read(fd, sink, n))
+ return -1;
+ len -= n;
+ }
+
+ rep[0] = call[0]; /* xid */
+ rep[1] = htonl(1); /* REPLY */
+ rep[2] = htonl(0); /* MSG_ACCEPTED */
+ rep[3] = htonl(0); /* verifier flavor AUTH_NULL */
+ rep[4] = htonl(0); /* verifier length */
+ rep[5] = htonl(0); /* SUCCESS */
+
+ if (ntohl(call[3]) != RPCB_PROGRAM) {
+ rep[5] = htonl(1); /* PROG_UNAVAIL */
+ } else {
+ switch (ntohl(call[5])) {
+ case RPCB_PROC_NULL:
+ break;
+ case RPCB_PROC_SET:
+ rep[6] = htonl(mode == RPCB_STUB_REFUSE ? 0 : 1);
+ nrep = 7;
+ break;
+ case RPCB_PROC_UNSET:
+ rep[6] = htonl(1); /* TRUE */
+ nrep = 7;
+ break;
+ default:
+ rep[5] = htonl(3); /* PROC_UNAVAIL */
+ }
+ }
+
+ replen = nrep * sizeof(rep[0]);
+ mark = htonl(0x80000000 | replen);
+ if (write(fd, &mark, sizeof(mark)) != (ssize_t)sizeof(mark) ||
+ write(fd, rep, replen) != (ssize_t)replen)
+ return -1;
+ return 0;
+}
+
+static void rpcb_stub_serve(int lfd, int mode)
+{
+ struct pollfd pfd[1 + RPCB_STUB_MAXCONN];
+ nfds_t n = 1, i;
+
+ pfd[0].fd = lfd;
+
+ for (;;) {
+ /* stop polling the listener when full, or poll() spins */
+ pfd[0].events = n < 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;
+
+ if (poll(pfd, n, -1) < 0)
+ return;
+
+ if (pfd[0].revents & POLLIN) {
+ int c = accept(lfd, NULL, NULL);
+
+ if (c >= 0) {
+ pfd[n].fd = c;
+ pfd[n].events = POLLIN;
+ n++;
+ }
+ }
+
+ for (i = 1; i < n; i++) {
+ if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR)))
+ continue;
+ if (rpcb_stub_call(pfd[i].fd, mode)) {
+ close(pfd[i].fd);
+ pfd[i] = pfd[--n];
+ }
+ }
+ }
+}
+
+/* Returns the stub's pid, or -1. The socket is listening before we fork. */
+static pid_t rpcb_stub_start(int mode)
+{
+ int lfd = rpcb_stub_listen();
+ pid_t pid;
+
+ if (lfd < 0)
+ return -1;
+
+ pid = fork();
+ if (pid < 0) {
+ close(lfd);
+ return -1;
+ }
+ if (pid == 0) {
+ signal(SIGPIPE, SIG_IGN);
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ if (getppid() == 1) /* raced with parent exit */
+ _exit(0);
+ rpcb_stub_serve(lfd, mode);
+ _exit(0);
+ }
+
+ close(lfd);
+ return pid;
+}
+
+/*
+ * Swap the stub for one in @mode. Safe before the first request: no serv
+ * exists yet, so the kernel has not connected and the abstract name is free
+ * again once the old stub has been reaped.
+ */
+static int rpcb_stub_restart(pid_t *pid, int mode)
+{
+ if (*pid > 0) {
+ kill(*pid, SIGKILL);
+ waitpid(*pid, NULL, 0);
+ }
+ *pid = rpcb_stub_start(mode);
+ return *pid > 0 ? 0 : -1;
+}
+
+/* --------------------------- fixture --------------------------- */
+
+FIXTURE(nfsd_listener) {
+ 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?)");
+
+ self->rpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);
+ if (self->rpcbd < 0)
+ SKIP(return, "cannot start the rpcbind stub: %s",
+ strerror(errno));
+}
+
+FIXTURE_TEARDOWN(nfsd_listener)
+{
+ if (self->rpcbd > 0) {
+ kill(self->rpcbd, SIGKILL);
+ waitpid(self->rpcbd, NULL, 0);
+ }
+}
+
+/* ===================== validation / negative ===================== */
+
+TEST_F(nfsd_listener, val_empty_list_ok)
+{
+ EXPECT_EQ(0, listener_set(NULL, 0));
+}
+
+TEST_F(nfsd_listener, val_too_many)
+{
+ static char attrs[1 << 20];
+ int i, off = 0;
+
+ for (i = 0; i < 1025; i++) /* > NFSD_NL_LISTENER_MAX (1024) */
+ off = put_listener(attrs, off, "udp", TEST_PORT);
+ EXPECT_EQ(-E2BIG, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_addr)
+{
+ char attrs[64];
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 0 };
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_transport)
+{
+ struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+ struct raw_listener r = { .xprt = NULL, .emit_addr = 1,
+ .addr = &s4, .addr_len = sizeof(s4) };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+/*
+ * A name matching no transport class must be refused before nfsd_mutex is
+ * taken, so it never reaches svc_xprt_create_from_sa() and its
+ * request_module("svc%s", name) upcall.
+ */
+TEST_F(nfsd_listener, val_bad_transport)
+{
+ char attrs[64];
+ int off = put_listener(attrs, 0, "bogus_xprt", TEST_PORT);
+
+ EXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_addr_too_short)
+{
+ unsigned char tiny = 0;
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1,
+ .addr = &tiny, .addr_len = 1 };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet_short)
+{
+ struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s4,
+ .addr_len = sizeof(sa_family_t) + 2 };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet6_short)
+{
+ struct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) };
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s6,
+ .addr_len = sizeof(struct sockaddr_in) };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_bad_family)
+{
+ struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+ .addr_len = sizeof(struct sockaddr_in) };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_second_entry_bad)
+{
+ struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+ struct raw_listener bad = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+ .addr_len = sizeof(struct sockaddr_in) };
+ char attrs[128];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ off = put_raw_listener(attrs, off, &bad);
+ /* The whole request is rejected during validation; nothing applied. */
+ EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+}
+
+/*
+ * 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));
+ EXPECT_EQ(htonl(INADDR_LOOPBACK), got[0].a4.s_addr);
+}
+
+TEST_F(nfsd_listener, func_create_udp)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "udp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_multi)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[128];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ off = put_listener(attrs, off, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+ EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_idempotent)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_set(attrs, off)); /* re-set same list */
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_add)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char one[64], two[128];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+ int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+ o2 = put_listener(two, o2, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, listener_set(two, o2)); /* add udp, keep tcp */
+ ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+ EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_remove_subset)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char both[128], one[64];
+ int ob = put_listener(both, 0, "tcp", TEST_PORT);
+ int oo = put_listener(one, 0, "tcp", TEST_PORT);
+
+ ob = put_listener(both, ob, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(both, ob));
+ ASSERT_EQ(0, listener_set(one, oo)); /* drop udp */
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_empty_destroys)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_set(NULL, 0)); /* empty -> destroy serv */
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+TEST_F(nfsd_listener, func_ipv6)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off, s;
+
+ s = socket(AF_INET6, SOCK_STREAM, 0);
+ if (s < 0)
+ SKIP(return, "IPv6 unavailable: %s", strerror(errno));
+ close(s);
+
+ off = put_listener_af(attrs, 0, "tcp", AF_INET6, TEST_PORT);
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET6, TEST_PORT));
+ EXPECT_EQ(0, memcmp(&got[0].a6, &in6addr_loopback, sizeof(in6addr_loopback)));
+}
+
+/* ===================== rpcbind registration ===================== */
+
+/*
+ * A rpcbind that refuses the registration must fail listener creation,
+ * whatever CONFIG_NFS_LOCALIO is set to.
+ *
+ * The error has to survive svc_register()'s walk over sv_programs to get
+ * here. With CONFIG_NFS_LOCALIO=y the trailing nfslocalio program has only
+ * a NULL and a vs_hidden version, and svc_generic_rpcbind_set() reports 0
+ * for both, so an svc_register() that keeps the last result rather than the
+ * first hands back success and the listener comes up regardless.
+ */
+TEST_F(nfsd_listener, sem_register_refused)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, rpcb_stub_restart(&self->rpcbd, RPCB_STUB_REFUSE));
+
+ EXPECT_EQ(-EACCES, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/* ===================== threads / -EBUSY semantics ===================== */
+
+TEST_F(nfsd_listener, sem_busy_on_change)
+{
+ char one[64], two[128];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+ int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+ o2 = put_listener(two, o2, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, threads_set(1)); /* threads now running */
+ EXPECT_EQ(-EBUSY, listener_set(two, o2)); /* add refused */
+ threads_set(0); /* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+ char one[64];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, threads_set(1));
+ EXPECT_EQ(-EBUSY, listener_set(NULL, 0)); /* remove refused */
+ threads_set(0);
+}
+
+TEST_HARNESS_MAIN
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/10 21:39 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 16028 output: 26 thoughts: 529 Thoughts: **Analyzing the NFS and RPC Patch Series** Okay, so I'm looking at this patch series, and it's touching some pretty critical areas: `fs/nfsd/nfsctl.c`, `net/sunrpc/rpcb_clnt.c`, and `net/sunrpc/svc.c`. Plus, there's a new selftest, which is always good. Let's break this down. First, `fs/nfsd/nfsctl.c` has changes that are adding validation for transport classes using `nfsd_nl_transport_supported` and limiting the number of listeners with `NFSD_NL_LISTENER_MAX`. This is important for robustness and preventing resource exhaustion. Then, over in `net/sunrpc/rpcb_clnt.c`, there's a timeout being added for local rpcbind calls (`rpcb_local_timeout`). That's likely aimed at handling potential blocking scenarios more gracefully. Finally, `net/sunrpc/svc.c` seems to be fixing an error handling issue in `svc_register`, making it return the first error encountered, instead of overwriting that. This is a subtle but important change that improves the error reporting. I need to confirm these functions' reachability and functional impact, so I am going to check those files specifically. I've noted that `fs/nfsd/nfsctl.c` is touching `nfsd_nl_validate_listeners` and `nfsd_nl_listener_set_doit`, `net/sunrpc/rpcb_clnt.c` is going to deal with `rpcb_create_af_local` and `rpcb_create_local_net`, and `net/sunrpc/svc.c` changes are going to impact `svc_register`. These functions are definitely core kernel logic, and the changes are definitely reachable and will have functional impact. Now, I want to confirm whether these functions are static or not. This affects their scope and how they're used. I found that `nfsd_nl_validate_listeners` is static, while `nfsd_nl_listener_set_doit` is not. In `net/sunrpc/rpcb_clnt.c`, I confirmed that both `rpcb_create_af_local` and `rpcb_create_local_net` are static. Finally, `svc_register` in `net/sunrpc/svc.c` is not static. I'm going to leverage `grepper` now to track down all the precise definitions and usages of each function to fully understand the impact. |
| 4/2 | 2026/08/10 21:39 | tool | grepper |
0mArgs:{
"Expression": "nfsd_nl_listener_set_doit"
}Results: {
"Output": "fs/nfsd/netlink.c=122=static const struct genl_split_ops nfsd_nl_ops[] = {\n--\nfs/nfsd/netlink.c-153-\t\t.cmd\t\t= NFSD_CMD_LISTENER_SET,\nfs/nfsd/netlink.c:154:\t\t.doit\t\t= nfsd_nl_listener_set_doit,\nfs/nfsd/netlink.c-155-\t\t.policy\t\t= nfsd_listener_set_nl_policy,\n--\nfs/nfsd/netlink.h=29=int nfsd_nl_version_get_doit(struct sk_buff *skb, struct genl_info *info);\nfs/nfsd/netlink.h:30:int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info);\nfs/nfsd/netlink.h-31-int nfsd_nl_listener_get_doit(struct sk_buff *skb, struct genl_info *info);\n--\nfs/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--\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c=716=TEST_F(nfsd_listener, val_second_entry_bad)\n--\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c-733- * request gets -- without the check in nfsd_nl_validate_listeners(),\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c:734: * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c-735- * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the\n"
} |
| 5/2 | 2026/08/10 21:39 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 17849 output: 17 thoughts: 33 Thoughts: Okay, here's my take on those initial thoughts, framed as a concise summary for an expert: **Prioritizing Fuzzing Targets** Alright, looking at this code, my mind immediately jumps to `nfsd_nl_listener_set_doit`. Seems like a prime target for fuzzing. It's got that network interaction element, which often means complex input handling and potential vulnerabilities. Now, I'm also considering `svc_register`. It's a key function for service registration, always a sensitive area from a security perspective. I need to take a closer look at the data flows and potential interaction points in this area. Both seem like good starting points, but it's important to assess which one presents the most immediate risk or highest potential for reward in terms of uncovering vulnerabilities. |
| 6/2 | 2026/08/10 21:39 | tool | grepper |
0mArgs:{
"Expression": "svc_register"
}Results: {
"Output": "include/linux/sunrpc/svc.h=474=void\t\t svc_process_bc(struct rpc_rqst *req, struct svc_rqst *rqstp);\ninclude/linux/sunrpc/svc.h:475:int\t\t svc_register(const struct svc_serv *, struct net *, const int,\ninclude/linux/sunrpc/svc.h-476-\t\t\t\tconst unsigned short, const unsigned short);\n--\nnet/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: * svc_register - register an RPC service with the local portmapper\nnet/sunrpc/svc.c-1188- * @serv: svc_serv struct for the service to register\n--\nnet/sunrpc/svc.c-1195- */\nnet/sunrpc/svc.c:1196:int svc_register(const struct svc_serv *serv, struct net *net,\nnet/sunrpc/svc.c-1197-\t\t const int family, const unsigned short proto,\n--\nnet/sunrpc/svc_xprt.c=1107=static void svc_delete_xprt(struct svc_xprt *xprt)\n--\nnet/sunrpc/svc_xprt.c-1118-\nnet/sunrpc/svc_xprt.c:1119:\t\tif (svc_register(serv, xprt-\u003expt_net, sock-\u003esk-\u003esk_family,\nnet/sunrpc/svc_xprt.c-1120-\t\t\t\t sock-\u003esk-\u003esk_protocol, 0) \u003c 0)\n--\nnet/sunrpc/svcsock.c=1439=static struct svc_sock *svc_setup_socket(struct svc_serv *serv,\n--\nnet/sunrpc/svcsock.c-1472-\nnet/sunrpc/svcsock.c:1473:\t\terr = svc_register(serv, sock_net(sock-\u003esk), inet-\u003esk_family,\nnet/sunrpc/svcsock.c-1474-\t\t\t\t inet-\u003esk_protocol,\n--\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c=852=TEST_F(nfsd_listener, func_ipv6)\n--\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c-875- *\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c:876: * The error has to survive svc_register()'s walk over sv_programs to get\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c-877- * here. With CONFIG_NFS_LOCALIO=y the trailing nfslocalio program has only\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c-878- * a NULL and a vs_hidden version, and svc_generic_rpcbind_set() reports 0\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c:879: * for both, so an svc_register() that keeps the last result rather than the\ntools/testing/selftests/nfsd/nfsd_netlink_listener.c-880- * first hands back success and the listener comes up regardless.\n"
} |
| 7/2 | 2026/08/10 21:39 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 19243 output: 22 thoughts: 48 Thoughts: Okay, here's my understanding, considering I'm trying to select the best function for something: **Function Candidate Evaluation** Alright, let's see. My initial thought is that `svc_register` looks like a strong contender. However, I need to be thorough. The question is whether it's the best option, or if there might be something even more perfectly suited for my needs. I'm now thinking about `rpcb_create_af_local` and `rpcb_create_local_net`. The immediate concern is that they are static functions. Static functions have their scope limited. The main issue here is if the call-sites are also within that scope. Are they only called from within other static functions, or are they called from a non-static context? This is the crucial point for determining if they will work as desired, especially considering my understanding of the larger design. If they're called outside their static scope, I have a problem. I need to investigate the call graph and their respective contexts. |
| 8/2 | 2026/08/10 21:40 | tool | grepper |
0mArgs:{
"Expression": "rpcb_create_af_local"
}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/10 21:40 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 19891 output: 22 thoughts: 29 Thoughts: Okay, here's my summary, taking the perspective of someone deeply familiar with system internals: **Tracing the Origins of Abstract and Unix Domain Socket Creation in rpcbind** Alright, time to get my hands dirty. I need to understand the lifecycle of abstract and Unix domain socket creation within `rpcbind`. My focus is on tracing how these mechanisms are *instantiated* – specifically, the calls to `rpcb_create_local_abstract` and `rpcb_create_local_unix`. I want to identify the initiating points, the context in which these functions are called, and any configuration parameters that influence their invocation. This level of detail is crucial for really understanding the behavior of `rpcbind` when dealing with various socket types. Knowing *where* and *why* these functions are called will reveal a lot about the application architecture, its error handling, and, possibly, any vulnerabilities or optimizations that might be present. Let's dig in and see what the code reveals. |
| 10/2 | 2026/08/10 21:40 | tool | grepper |
0mArgs:{
"Expression": "rpcb_create_local_abstract"
}Results: {
"Output": "net/sunrpc/rpcb_clnt.c=238=static int rpcb_create_af_local(struct net *net,\n--\nnet/sunrpc/rpcb_clnt.c-284-\nnet/sunrpc/rpcb_clnt.c:285:static int rpcb_create_local_abstract(struct net *net)\nnet/sunrpc/rpcb_clnt.c-286-{\n--\nnet/sunrpc/rpcb_clnt.c=357=int rpcb_create_local(struct net *net)\n--\nnet/sunrpc/rpcb_clnt.c-368-\nnet/sunrpc/rpcb_clnt.c:369:\tif (rpcb_create_local_abstract(net) != 0 \u0026\u0026\nnet/sunrpc/rpcb_clnt.c-370-\t rpcb_create_local_unix(net) != 0)\n"
} |
| 11/2 | 2026/08/10 21:40 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 20391 output: 20 thoughts: 20 Thoughts: Here's my summary, as though I'm thinking through this process: **Diving into `rpcb_create_local`** Okay, so `rpcb_create_local` isn't declared as `static`. That immediately piques my interest. Non-static functions usually imply a broader scope of usage, potentially indicating it's part of a public interface, or at least called from various places within the codebase. Given that, let's get down to brass tacks and thoroughly analyze where this function is actually being *used*. Time to track down all its call sites and understand its role in this system. This will give me a better understanding of its impact and importance. I need to figure out the context. |
| 12/2 | 2026/08/10 21:40 | tool | grepper |
0mArgs:{
"Expression": "rpcb_create_local"
}Results: {
"Output": "include/linux/sunrpc/clnt.h=197=struct rpc_xprt\t*rpc_task_get_xprt(struct rpc_clnt *clnt,\n--\ninclude/linux/sunrpc/clnt.h-199-\ninclude/linux/sunrpc/clnt.h:200:int\t\trpcb_create_local(struct net *);\ninclude/linux/sunrpc/clnt.h-201-void\t\trpcb_put_local(struct net *);\n--\nnet/sunrpc/rpcb_clnt.c=206=static void rpcb_set_local(struct net *net, struct rpc_clnt *clnt,\n--\nnet/sunrpc/rpcb_clnt.c-211-\nnet/sunrpc/rpcb_clnt.c:212:\t/* Protected by rpcb_create_local_mutex */\nnet/sunrpc/rpcb_clnt.c-213-\tsn-\u003erpcb_local_clnt = clnt;\n--\nnet/sunrpc/rpcb_clnt.c=238=static int rpcb_create_af_local(struct net *net,\n--\nnet/sunrpc/rpcb_clnt.c-284-\nnet/sunrpc/rpcb_clnt.c:285:static int rpcb_create_local_abstract(struct net *net)\nnet/sunrpc/rpcb_clnt.c-286-{\n--\nnet/sunrpc/rpcb_clnt.c-294-\nnet/sunrpc/rpcb_clnt.c:295:static int rpcb_create_local_unix(struct net *net)\nnet/sunrpc/rpcb_clnt.c-296-{\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-356- */\nnet/sunrpc/rpcb_clnt.c:357:int rpcb_create_local(struct net *net)\nnet/sunrpc/rpcb_clnt.c-358-{\nnet/sunrpc/rpcb_clnt.c:359:\tstatic DEFINE_MUTEX(rpcb_create_local_mutex);\nnet/sunrpc/rpcb_clnt.c-360-\tint result = 0;\n--\nnet/sunrpc/rpcb_clnt.c-364-\nnet/sunrpc/rpcb_clnt.c:365:\tmutex_lock(\u0026rpcb_create_local_mutex);\nnet/sunrpc/rpcb_clnt.c-366-\tif (rpcb_get_local(net))\n--\nnet/sunrpc/rpcb_clnt.c-368-\nnet/sunrpc/rpcb_clnt.c:369:\tif (rpcb_create_local_abstract(net) != 0 \u0026\u0026\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-\nnet/sunrpc/rpcb_clnt.c-373-out:\nnet/sunrpc/rpcb_clnt.c:374:\tmutex_unlock(\u0026rpcb_create_local_mutex);\nnet/sunrpc/rpcb_clnt.c-375-\treturn result;\n--\nnet/sunrpc/svc.c=301=static int svc_rpcb_setup(struct svc_serv *serv, struct net *net)\n--\nnet/sunrpc/svc.c-304-\nnet/sunrpc/svc.c:305:\terr = rpcb_create_local(net);\nnet/sunrpc/svc.c-306-\tif (err)\n"
} |
| 13/2 | 2026/08/10 21:40 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 21633 output: 35 thoughts: 138 Thoughts: **Analyzing the RPC Server Setup and Related Configurations** Okay, so I see that `rpcb_create_local` is being called from within `svc_rpcb_setup`, which is part of the RPC server initialization process. That makes sense; it's a critical part of the setup. I've also noted that the code changes are primarily in `nfsd_nl_listener_set_doit`, `rpcb_create_local`, and `svc_register`. These functions must be key to the new behavior. Now, I need to understand what configuration options are enabling this new functionality. Let's see... the selftest config mentions `CONFIG_NET_NS=y`, `CONFIG_IPV6=y`, `CONFIG_NFSD=y`, and `CONFIG_NFSD_V4=y`. That gives me a starting point. `NET_NS` is clearly important for network namespaces, `IPV6` for obvious reasons, and `NFSD`/`NFSD_V4` relate to the NFS server itself. I'll need to dig deeper, though. I should examine `fs/nfsd/Kconfig` to look for any additional dependencies or relationships that might be less obvious. There could be other related kernel features or configuration options that are also involved. It's important to have a complete picture. |
| 14/2 | 2026/08/10 21:40 | tool | read-file |
0mArgs:{
"File": "fs/nfsd/Kconfig",
"FirstLine": 1,
"LineCount": 100
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\tconfig NFSD\n 3:\t\ttristate \"NFS server support\"\n 4:\t\tdepends on INET\n 5:\t\tdepends on FILE_LOCKING\n 6:\t\tdepends on FSNOTIFY\n 7:\t\tselect CRC32\n 8:\t\tselect CRYPTO_LIB_MD5 if NFSD_LEGACY_CLIENT_TRACKING\n 9:\t\tselect CRYPTO_LIB_SHA256 if NFSD_V4\n 10:\t\tselect CRYPTO # required by RPCSEC_GSS_KRB5 and signed filehandles\n 11:\t\tselect LOCKD\n 12:\t\tselect SUNRPC\n 13:\t\tselect EXPORTFS\n 14:\t\tselect NFS_COMMON\n 15:\t\tselect NFS_ACL_SUPPORT if NFSD_V2_ACL\n 16:\t\tselect NFS_ACL_SUPPORT if NFSD_V3_ACL\n 17:\t\tdepends on MULTIUSER\n 18:\t\thelp\n 19:\t\t Choose Y here if you want to allow other computers to access\n 20:\t\t files residing on this system using Sun's Network File System\n 21:\t\t protocol. To compile the NFS server support as a module,\n 22:\t\t choose M here: the module will be called nfsd.\n 23:\t\n 24:\t\t You may choose to use a user-space NFS server instead, in which\n 25:\t\t case you can choose N here.\n 26:\t\n 27:\t\t To export local file systems using NFS, you also need to install\n 28:\t\t user space programs which can be found in the Linux nfs-utils\n 29:\t\t package, available from http://linux-nfs.org/. More detail about\n 30:\t\t the Linux NFS server implementation is available via the\n 31:\t\t exports(5) man page.\n 32:\t\n 33:\t\t Below you can choose which versions of the NFS protocol are\n 34:\t\t available to clients mounting the NFS server on this system.\n 35:\t\t Support for NFS version 3 (RFC 1813) is always available when\n 36:\t\t CONFIG_NFSD is selected.\n 37:\t\n 38:\t\t If unsure, say N.\n 39:\t\n 40:\tconfig NFSD_V2\n 41:\t\tbool \"NFS server support for NFS version 2 (DEPRECATED)\"\n 42:\t\tdepends on NFSD\n 43:\t\tdefault n\n 44:\t\thelp\n 45:\t\t NFSv2 (RFC 1094) was the first publicly-released version of NFS.\n 46:\t\t Unless you are hosting ancient (1990's era) NFS clients, you don't\n 47:\t\t need this.\n 48:\t\n 49:\t\t If unsure, say N.\n 50:\t\n 51:\tconfig NFSD_V2_ACL\n 52:\t\tbool \"NFS server support for the NFSv2 ACL protocol extension\"\n 53:\t\tdepends on NFSD_V2\n 54:\t\n 55:\tconfig NFSD_V3_ACL\n 56:\t\tbool \"NFS server support for the NFSv3 ACL protocol extension\"\n 57:\t\tdepends on NFSD\n 58:\t\thelp\n 59:\t\t Solaris NFS servers support an auxiliary NFSv3 ACL protocol that\n 60:\t\t never became an official part of the NFS version 3 protocol.\n 61:\t\t This protocol extension allows applications on NFS clients to\n 62:\t\t manipulate POSIX Access Control Lists on files residing on NFS\n 63:\t\t servers. NFS servers enforce POSIX ACLs on local files whether\n 64:\t\t this protocol is available or not.\n 65:\t\n 66:\t\t This option enables support in your system's NFS server for the\n 67:\t\t NFSv3 ACL protocol extension allowing NFS clients to manipulate\n 68:\t\t POSIX ACLs on files exported by your system's NFS server. NFS\n 69:\t\t clients which support the Solaris NFSv3 ACL protocol can then\n 70:\t\t access and modify ACLs on your NFS server.\n 71:\t\n 72:\t\t To store ACLs on your NFS server, you also need to enable ACL-\n 73:\t\t related CONFIG options for your local file systems of choice.\n 74:\t\n 75:\t\t If unsure, say N.\n 76:\t\n 77:\tconfig NFSD_V4\n 78:\t\tbool \"NFS server support for NFS version 4\"\n 79:\t\tdepends on NFSD \u0026\u0026 PROC_FS\n 80:\t\tselect FS_POSIX_ACL\n 81:\t\tselect RPCSEC_GSS_KRB5\n 82:\t\tselect GRACE_PERIOD\n 83:\t\tselect NFS_V4_2_SSC_HELPER if NFS_V4_2\n 84:\t\thelp\n 85:\t\t This option enables support in your system's NFS server for\n 86:\t\t version 4 of the NFS protocol (RFC 3530).\n 87:\t\n 88:\t\t To export files using NFSv4, you need to install additional user\n 89:\t\t space programs which can be found in the Linux nfs-utils package,\n 90:\t\t available from http://linux-nfs.org/.\n 91:\t\n 92:\t\t If unsure, say N.\n 93:\t\n 94:\tconfig NFSD_PNFS\n 95:\t\tbool\n 96:\t\n 97:\tconfig NFSD_BLOCKLAYOUT\n 98:\t\tbool \"NFSv4.1 server support for pNFS block layouts\"\n 99:\t\tdepends on NFSD_V4 \u0026\u0026 BLOCK\n 100:\t\tselect NFSD_PNFS\n"
} |
| 15/2 | 2026/08/10 21:40 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 23558 output: 155 thoughts: 483 Thoughts: **Analyzing NFS Netlink Listener Configuration** Okay, let's break this down. My initial assessment of the kernel configuration revealed a few key options that seem relevant to this area: `NFSD`, `NFSD_V4`, `SUNRPC`, `INET`, `FILE_LOCKING`, `FSNOTIFY`, `MULTIUSER`, and `PROC_FS`. The fact that `SUNRPC` is selected by `NFSD` is something I'll keep in mind. Also, the inclusion of `NET_NS` and `IPV6` from the selftest config is interesting, let's keep them in mind, even if they aren't explicit configuration options. I initially flagged a few function symbols as potential areas of focus: `nfsd_nl_listener_set_doit`, `svc_register`, and `rpcb_create_local`. However, after a closer look, it seems none of these are likely to be on a performance-critical path. `nfsd_nl_listener_set_doit` is a netlink command handler, which isn't typically hot. Similarly, both `svc_register` and `rpcb_create_local` are called during the setup of RPC services, not during actual data transfer. I then did a quick sanity check to see if there were any other functions that may be relevant, I checked the other functions that were nearby. The functions `nfsd_nl_validate_listeners`, `nfsd_nl_transport_supported`, `rpcb_create_af_local`, and `rpcb_create_local_net` appear to be static, which likely means they are either inlined or not externally visible. So, circling back to the original focus symbols: * `nfsd_nl_listener_set_doit` * `svc_register` * `rpcb_create_local` Now, let's confirm that `nfsd_nl_listener_set_doit` is the *only* netlink handler being modified. Yes, it looks like it is the main entry point for the listener set command. Next, I need to make a note of the relevant `EnableConfigs`: * `NFSD` * `NFSD_V4` * `SUNRPC` (Again, noting its dependency) * `NET_NS` * `IPV6` I'll verify that `SUNRPC` is indeed a config option, which it is. Finally, I'll confirm that `nfsd_nl_listener_set_doit` is actually compiled in. Yes, it's located within `fs/nfsd/nfsctl.c`, which is part of the `nfsd` module. Now I will compile the results. |
| 16/2 | 2026/08/10 21:40 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"NFSD",
"NFSD_V4",
"SUNRPC",
"NET_NS",
"IPV6"
],
"FocusSymbols": [
"nfsd_nl_listener_set_doit",
"svc_register",
"rpcb_create_local"
],
"Reasoning": "The patch modifies the NFSD netlink listener configuration logic, adds validation for transport classes, limits the number of listeners, introduces a timeout for local rpcbind calls, and fixes error handling in svc_register. These are reachable core kernel changes in the NFS server and RPC subsystems that have functional impact and should be fuzzed.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"NFSD",
"NFSD_V4",
"SUNRPC",
"NET_NS",
"IPV6"
],
"FocusSymbols": [
"nfsd_nl_listener_set_doit",
"svc_register",
"rpcb_create_local"
],
"Reasoning": "The patch modifies the NFSD netlink listener configuration logic, adds validation for transport classes, limits the number of listeners, introduces a timeout for local rpcbind calls, and fixes error handling in svc_register. These are reachable core kernel changes in the NFS server and RPC subsystems that have functional impact and should be fuzzed.",
"WorthFuzzing": true
} |
| 17/1 | 2026/08/10 21:40 | action | If |
0mArgs:{
"WorthFuzzing": true
}Results: null |
| 18/2 | 2026/08/10 21:40 | agent | kmsan-evaluator |
0mModel:best-expensive Args: null Results: {
"KMSANReasoning": "The patch series introduces validation checks for NFSD netlink listener attributes (e.g., limiting the number of listeners to `NFSD_NL_LISTENER_MAX` and validating the transport name via `nfsd_nl_transport_supported`). It also fixes error code propagation in `svc_register` and sets a local timeout for rpcbind clients. \n\nThe netlink attribute `NFSD_A_SOCK_TRANSPORT_NAME` is defined as `NLA_NUL_STRING` in `nfsd_sock_nl_policy`, ensuring that `nla_data()` returns a properly null-terminated string before it is passed to `strcmp()` in `nfsd_nl_transport_supported`. \n\nNo new complex data structures are allocated or sent to user space, and there are no changes that could expose uninitialized stack or heap memory. Any potential issues introduced by these changes (such as out-of-bounds accesses or logic bugs) would be effectively caught by standard KASAN and other default sanitizers. 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 601eb7048fd1cc371951f58af00cc250f9e2bac3
Author: syz-cluster <triage@syzkaller.com>
Date: Mon Aug 10 21:39:37 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..e9cfceb1a3342 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1208,13 +1208,16 @@ 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 (!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..e6945ff9551c4
--- /dev/null
+++ b/tools/testing/selftests/nfsd/config
@@ -0,0 +1,4 @@
+CONFIG_NET_NS=y
+CONFIG_IPV6=y
+CONFIG_NFSD=y
+CONFIG_NFSD_V4=y
diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
new file mode 100644
index 0000000000000..3e3307680d7d4
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -0,0 +1,920 @@
+// 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/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);
+}
+
+static struct listener_ent *find_listener(struct listener_ent *e, int n,
+ const char *xprt, int family,
+ uint16_t port)
+{
+ int i;
+
+ for (i = 0; i < n; i++)
+ if (e[i].family == family && e[i].port == port &&
+ !strcmp(e[i].xprt, xprt))
+ return &e[i];
+ return NULL;
+}
+
+/* Start (@n > 0) or stop (@n == 0) nfsd threads in this netns. */
+static int threads_set(int n)
+{
+ char attrs[64];
+ uint32_t v = n;
+ int off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, &v, sizeof(v));
+
+ return genl_request(NFSD_CMD_THREADS_SET, attrs, off);
+}
+
+/* ------------------- per-netns local rpcbind stub ------------------- */
+
+/*
+ * Creating a listener registers with rpcbind: svc_xprt_create_from_sa()
+ * passes flags of 0, so pmap_register is true in svc_setup_socket(), and
+ * nfsd_version3 is registerable by default and does not set vs_rpcb_optnl,
+ * so a registration failure aborts listener creation. The abstract AF_LOCAL
+ * name the kernel tries first is per-netns (unix_find_abstract() takes a
+ * struct net), so answer it here and stay out of the host's rpcbind.
+ *
+ * Arguments are never decoded. The NULL procedure gets an empty success and
+ * SET/UNSET get TRUE, for both RPCBVERS_2 and RPCBVERS_4. v4 has to be
+ * answered because __svc_rpcb_register6() turns a v4 refusal into
+ * -EAFNOSUPPORT, which would fail every IPv6 listener.
+ *
+ * In RPCB_STUB_REFUSE mode SET is answered FALSE instead, which
+ * rpcb_register_call() reports as -EACCES. UNSET is left alone: only
+ * svc_unregister() issues it, and it discards the result.
+ */
+#define RPCB_PROGRAM 100000
+#define RPCB_PROC_NULL 0
+#define RPCB_PROC_SET 1
+#define RPCB_PROC_UNSET 2
+#define RPCB_ABSTRACT_NAME "/run/rpcbind.sock"
+#define RPCB_STUB_MAXCONN 4
+
+enum { RPCB_STUB_ACCEPT, RPCB_STUB_REFUSE };
+
+static int rpcb_stub_listen(void)
+{
+ struct sockaddr_un sun = { .sun_family = AF_UNIX };
+ size_t nlen = strlen(RPCB_ABSTRACT_NAME);
+ socklen_t alen;
+ int fd;
+
+ /* Abstract names are length-delimited, so the length must match. */
+ memcpy(sun.sun_path + 1, RPCB_ABSTRACT_NAME, nlen);
+ alen = offsetof(struct sockaddr_un, sun_path) + 1 + nlen;
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0)
+ return -1;
+ if (bind(fd, (struct sockaddr *)&sun, alen) < 0 ||
+ listen(fd, RPCB_STUB_MAXCONN) < 0) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int rpcb_stub_read(int fd, void *buf, size_t len)
+{
+ size_t done = 0;
+
+ while (done < len) {
+ ssize_t n = read(fd, (char *)buf + done, len - done);
+
+ if (n <= 0)
+ return -1;
+ done += n;
+ }
+ return 0;
+}
+
+/* Handle one record-marked RPC call. Returns -1 when the peer is done. */
+static int rpcb_stub_call(int fd, int mode)
+{
+ uint32_t mark, call[6], rep[7];
+ unsigned int len, nrep = 6;
+ size_t replen;
+
+ if (rpcb_stub_read(fd, &mark, sizeof(mark)))
+ return -1;
+ len = ntohl(mark) & 0x7fffffff;
+ if (len < sizeof(call) || len > 4096)
+ return -1;
+ if (rpcb_stub_read(fd, call, sizeof(call)))
+ return -1;
+
+ /* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */
+ for (len -= sizeof(call); len; ) {
+ char sink[256];
+ unsigned int n = len > sizeof(sink) ? sizeof(sink) : len;
+
+ if (rpcb_stub_read(fd, sink, n))
+ return -1;
+ len -= n;
+ }
+
+ rep[0] = call[0]; /* xid */
+ rep[1] = htonl(1); /* REPLY */
+ rep[2] = htonl(0); /* MSG_ACCEPTED */
+ rep[3] = htonl(0); /* verifier flavor AUTH_NULL */
+ rep[4] = htonl(0); /* verifier length */
+ rep[5] = htonl(0); /* SUCCESS */
+
+ if (ntohl(call[3]) != RPCB_PROGRAM) {
+ rep[5] = htonl(1); /* PROG_UNAVAIL */
+ } else {
+ switch (ntohl(call[5])) {
+ case RPCB_PROC_NULL:
+ break;
+ case RPCB_PROC_SET:
+ rep[6] = htonl(mode == RPCB_STUB_REFUSE ? 0 : 1);
+ nrep = 7;
+ break;
+ case RPCB_PROC_UNSET:
+ rep[6] = htonl(1); /* TRUE */
+ nrep = 7;
+ break;
+ default:
+ rep[5] = htonl(3); /* PROC_UNAVAIL */
+ }
+ }
+
+ replen = nrep * sizeof(rep[0]);
+ mark = htonl(0x80000000 | replen);
+ if (write(fd, &mark, sizeof(mark)) != (ssize_t)sizeof(mark) ||
+ write(fd, rep, replen) != (ssize_t)replen)
+ return -1;
+ return 0;
+}
+
+static void rpcb_stub_serve(int lfd, int mode)
+{
+ struct pollfd pfd[1 + RPCB_STUB_MAXCONN];
+ nfds_t n = 1, i;
+
+ pfd[0].fd = lfd;
+
+ for (;;) {
+ /* stop polling the listener when full, or poll() spins */
+ pfd[0].events = n < 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;
+
+ if (poll(pfd, n, -1) < 0)
+ return;
+
+ if (pfd[0].revents & POLLIN) {
+ int c = accept(lfd, NULL, NULL);
+
+ if (c >= 0) {
+ pfd[n].fd = c;
+ pfd[n].events = POLLIN;
+ n++;
+ }
+ }
+
+ for (i = 1; i < n; i++) {
+ if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR)))
+ continue;
+ if (rpcb_stub_call(pfd[i].fd, mode)) {
+ close(pfd[i].fd);
+ pfd[i] = pfd[--n];
+ }
+ }
+ }
+}
+
+/* Returns the stub's pid, or -1. The socket is listening before we fork. */
+static pid_t rpcb_stub_start(int mode)
+{
+ int lfd = rpcb_stub_listen();
+ pid_t pid;
+
+ if (lfd < 0)
+ return -1;
+
+ pid = fork();
+ if (pid < 0) {
+ close(lfd);
+ return -1;
+ }
+ if (pid == 0) {
+ signal(SIGPIPE, SIG_IGN);
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ if (getppid() == 1) /* raced with parent exit */
+ _exit(0);
+ rpcb_stub_serve(lfd, mode);
+ _exit(0);
+ }
+
+ close(lfd);
+ return pid;
+}
+
+/*
+ * Swap the stub for one in @mode. Safe before the first request: no serv
+ * exists yet, so the kernel has not connected and the abstract name is free
+ * again once the old stub has been reaped.
+ */
+static int rpcb_stub_restart(pid_t *pid, int mode)
+{
+ if (*pid > 0) {
+ kill(*pid, SIGKILL);
+ waitpid(*pid, NULL, 0);
+ }
+ *pid = rpcb_stub_start(mode);
+ return *pid > 0 ? 0 : -1;
+}
+
+/* --------------------------- fixture --------------------------- */
+
+FIXTURE(nfsd_listener) {
+ 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?)");
+
+ self->rpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);
+ if (self->rpcbd < 0)
+ SKIP(return, "cannot start the rpcbind stub: %s",
+ strerror(errno));
+}
+
+FIXTURE_TEARDOWN(nfsd_listener)
+{
+ if (self->rpcbd > 0) {
+ kill(self->rpcbd, SIGKILL);
+ waitpid(self->rpcbd, NULL, 0);
+ }
+}
+
+/* ===================== validation / negative ===================== */
+
+TEST_F(nfsd_listener, val_empty_list_ok)
+{
+ EXPECT_EQ(0, listener_set(NULL, 0));
+}
+
+TEST_F(nfsd_listener, val_too_many)
+{
+ static char attrs[1 << 20];
+ int i, off = 0;
+
+ for (i = 0; i < 1025; i++) /* > NFSD_NL_LISTENER_MAX (1024) */
+ off = put_listener(attrs, off, "udp", TEST_PORT);
+ EXPECT_EQ(-E2BIG, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_addr)
+{
+ char attrs[64];
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 0 };
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_transport)
+{
+ struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+ struct raw_listener r = { .xprt = NULL, .emit_addr = 1,
+ .addr = &s4, .addr_len = sizeof(s4) };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+/*
+ * A name matching no transport class must be refused before nfsd_mutex is
+ * taken, so it never reaches svc_xprt_create_from_sa() and its
+ * request_module("svc%s", name) upcall.
+ */
+TEST_F(nfsd_listener, val_bad_transport)
+{
+ char attrs[64];
+ int off = put_listener(attrs, 0, "bogus_xprt", TEST_PORT);
+
+ EXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_addr_too_short)
+{
+ unsigned char tiny = 0;
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1,
+ .addr = &tiny, .addr_len = 1 };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet_short)
+{
+ struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s4,
+ .addr_len = sizeof(sa_family_t) + 2 };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet6_short)
+{
+ struct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) };
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s6,
+ .addr_len = sizeof(struct sockaddr_in) };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_bad_family)
+{
+ struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+ struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+ .addr_len = sizeof(struct sockaddr_in) };
+ char attrs[64];
+ int off = put_raw_listener(attrs, 0, &r);
+
+ EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_second_entry_bad)
+{
+ struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+ struct raw_listener bad = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+ .addr_len = sizeof(struct sockaddr_in) };
+ char attrs[128];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ off = put_raw_listener(attrs, off, &bad);
+ /* The whole request is rejected during validation; nothing applied. */
+ EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+}
+
+/*
+ * 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));
+ EXPECT_EQ(htonl(INADDR_LOOPBACK), got[0].a4.s_addr);
+}
+
+TEST_F(nfsd_listener, func_create_udp)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "udp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_multi)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[128];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ off = put_listener(attrs, off, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+ EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_idempotent)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_set(attrs, off)); /* re-set same list */
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_add)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char one[64], two[128];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+ int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+ o2 = put_listener(two, o2, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, listener_set(two, o2)); /* add udp, keep tcp */
+ ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+ EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_remove_subset)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char both[128], one[64];
+ int ob = put_listener(both, 0, "tcp", TEST_PORT);
+ int oo = put_listener(one, 0, "tcp", TEST_PORT);
+
+ ob = put_listener(both, ob, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(both, ob));
+ ASSERT_EQ(0, listener_set(one, oo)); /* drop udp */
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_empty_destroys)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_set(NULL, 0)); /* empty -> destroy serv */
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+TEST_F(nfsd_listener, func_ipv6)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off, s;
+
+ s = socket(AF_INET6, SOCK_STREAM, 0);
+ if (s < 0)
+ SKIP(return, "IPv6 unavailable: %s", strerror(errno));
+ close(s);
+
+ off = put_listener_af(attrs, 0, "tcp", AF_INET6, TEST_PORT);
+ ASSERT_EQ(0, listener_set(attrs, off));
+ ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+ EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET6, TEST_PORT));
+ EXPECT_EQ(0, memcmp(&got[0].a6, &in6addr_loopback, sizeof(in6addr_loopback)));
+}
+
+/* ===================== rpcbind registration ===================== */
+
+/*
+ * A rpcbind that refuses the registration must fail listener creation,
+ * whatever CONFIG_NFS_LOCALIO is set to.
+ *
+ * The error has to survive svc_register()'s walk over sv_programs to get
+ * here. With CONFIG_NFS_LOCALIO=y the trailing nfslocalio program has only
+ * a NULL and a vs_hidden version, and svc_generic_rpcbind_set() reports 0
+ * for both, so an svc_register() that keeps the last result rather than the
+ * first hands back success and the listener comes up regardless.
+ */
+TEST_F(nfsd_listener, sem_register_refused)
+{
+ struct listener_ent got[MAX_LISTENERS];
+ char attrs[64];
+ int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, rpcb_stub_restart(&self->rpcbd, RPCB_STUB_REFUSE));
+
+ EXPECT_EQ(-EACCES, listener_set(attrs, off));
+ EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/* ===================== threads / -EBUSY semantics ===================== */
+
+TEST_F(nfsd_listener, sem_busy_on_change)
+{
+ char one[64], two[128];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+ int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+ o2 = put_listener(two, o2, "udp", TEST_PORT);
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, threads_set(1)); /* threads now running */
+ EXPECT_EQ(-EBUSY, listener_set(two, o2)); /* add refused */
+ threads_set(0); /* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+ char one[64];
+ int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+
+ ASSERT_EQ(0, listener_set(one, o1));
+ ASSERT_EQ(0, threads_set(1));
+ EXPECT_EQ(-EBUSY, listener_set(NULL, 0)); /* remove refused */
+ threads_set(0);
+}
+
+TEST_HARNESS_MAIN
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
|
| 19/3 | 2026/08/10 21:40 | llm | kmsan-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 16025 output: 23 thoughts: 579 Thoughts: **Analyzing This NFSd Netlink Listener Patch** Alright, let's break down this patch. My immediate focus is on the core changes and potential implications, especially regarding security and stability, given my expertise in this area. First, I see modifications to `fs/nfsd/nfsctl.c`, `net/sunrpc/rpcb_clnt.c`, and `net/sunrpc/svc.c`, alongside a new self-test for the NFSd netlink listener. Good to see the addition of a selftest; that's critical for regression prevention. In `fs/nfsd/nfsctl.c`, the introduction of `nfsd_nl_transport_supported` and the modification of `nfsd_nl_validate_listeners` immediately catch my eye. Specifically, I note the limiting of listeners to `NFSD_NL_LISTENER_MAX` (1024), a sensible precaution against DoS. The inclusion of the check using `nfsd_nl_transport_supported` within the validation process is also welcome. The change to set the error message via `NL_SET_ERR_MSG_FMT` in `nfsd_nl_listener_set_doit` on `svc_xprt_create_from_sa` failure is a good debugging improvement. Moving to `net/sunrpc/rpcb_clnt.c`, the addition and use of `rpcb_local_timeout` in `rpcb_create_af_local` and `rpcb_create_local_net` suggests a potential performance or robustness improvement related to local RPC connections. I'll need to examine the timeout values and their impact on overall RPC latency. In `net/sunrpc/svc.c`, the change to `svc_register` to retain the first error from `progp->pg_rpcbind_set` is a significant one. The previous behavior of overwriting an initial error with zero is, frankly, incorrect and could lead to silent failures. This is a clear improvement in error reporting, and I'm glad to see this correction. Now, my more immediate concern is with the potential for vulnerabilities. I see a potential area to assess in `nfsd_nl_validate_listeners`. Specifically the following lines: ```c 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; ``` Here, `nla_data` retrieves the payload of a netlink attribute. This is being passed to `nfsd_nl_transport_supported`, which, from a brief glance, seems to use `strcmp`. My question is, is the payload of this netlink attribute guaranteed to be null-terminated? If not, we have a clear potential for a buffer overflow with `strcmp`. I *must* verify the definition of `nfsd_sock_nl_policy` in `fs/nfsd/nfsctl.c` to see if the netlink attribute definition addresses null termination. This is a critical point that needs a thorough investigation to ensure this patch doesn't introduce a security hole. This is the first thing I'll check. |
| 20/3 | 2026/08/10 21:40 | tool | grepper |
0mArgs:{
"Expression": "nfsd_sock_nl_policy"
}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"
} |
| 21/3 | 2026/08/10 21:40 | llm | kmsan-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 18187 output: 37 thoughts: 29 Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience: **Diving into the NFS Netlink Policy** Okay, time to get to the heart of this. My initial thought is to zero in on the `nfsd_sock_nl_policy` structure. I immediately know this is where the kernel defines the Netlink message policy for the NFS server's socket interaction. I'll need to go directly to `fs/nfsd/netlink.c` – that's the obvious place. Let's see what that policy definition reveals about the expected message format, data types, and any security restrictions in place. Understanding this policy is crucial for diagnosing or developing any Netlink-based tools that interact with the NFS server. I'm expecting this policy to outline the attributes accepted by `nfsd` and their associated validations. The details of the policy are key to understanding the expected content of the incoming and outgoing Netlink messages. |
| 22/3 | 2026/08/10 21:40 | tool | read-file |
0mArgs:{
"File": "fs/nfsd/netlink.c",
"FirstLine": 35,
"LineCount": 15
}Results: {
"Contents": " 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"
} |
| 23/3 | 2026/08/10 21:40 | llm | kmsan-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 18873 output: 256 thoughts: 512 Thoughts: **Reasoning Through the Patch** Okay, let's break this down. First, regarding the `[NFSD_A_SOCK_TRANSPORT_NAME]` definition, knowing that `.type = NLA_NUL_STRING` guarantees that `nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])` will be null-terminated. This is crucial for avoiding any potential buffer overruns or uninitialized memory reads. The patch itself doesn't introduce any new allocations, so we're not dealing with uninitialized memory issues there. The changes are all centered around validation and error handling, which is precisely the kind of thing KASAN and other standard bug detectors are designed to catch if something slips through. So, in terms of uninitialized memory risks, this section is a non-issue. Next, I need to confirm the `svc_register` changes. It looks like it's a simple, and rather straightforward, fix for an error code propagation bug. The patch is simply ensuring the correct error code is returned. No new memory is being introduced and no memory that existed before is being changed to be uninitialized. Finally, I need to check the changes in `rpcb_create_af_local` and `rpcb_create_local_net`. It seems the update adds a `rpcb_local_timeout`. It is defining a local timeout with an initival of `1 * HZ`, a maxval of `1 * HZ`, and `0` retries. This appears to be setting up a timeout structure, but again, there are no uninitialized memory issues that I can see here. Based on this review, I'm confident that there are no uninitialized memory issues. We can safely mark NeedsKMSAN as false. I'll summarize this reasoning for my notes and the reviewers. |
| 24/3 | 2026/08/10 21:40 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch series introduces validation checks for NFSD netlink listener attributes (e.g., limiting the number of listeners to `NFSD_NL_LISTENER_MAX` and validating the transport name via `nfsd_nl_transport_supported`). It also fixes error code propagation in `svc_register` and sets a local timeout for rpcbind clients. \n\nThe netlink attribute `NFSD_A_SOCK_TRANSPORT_NAME` is defined as `NLA_NUL_STRING` in `nfsd_sock_nl_policy`, ensuring that `nla_data()` returns a properly null-terminated string before it is passed to `strcmp()` in `nfsd_nl_transport_supported`. \n\nNo new complex data structures are allocated or sent to user space, and there are no changes that could expose uninitialized stack or heap memory. Any potential issues introduced by these changes (such as out-of-bounds accesses or logic bugs) would be effectively caught by standard KASAN and other default sanitizers. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch series introduces validation checks for NFSD netlink listener attributes (e.g., limiting the number of listeners to `NFSD_NL_LISTENER_MAX` and validating the transport name via `nfsd_nl_transport_supported`). It also fixes error code propagation in `svc_register` and sets a local timeout for rpcbind clients. \n\nThe netlink attribute `NFSD_A_SOCK_TRANSPORT_NAME` is defined as `NLA_NUL_STRING` in `nfsd_sock_nl_policy`, ensuring that `nla_data()` returns a properly null-terminated string before it is passed to `strcmp()` in `nfsd_nl_transport_supported`. \n\nNo new complex data structures are allocated or sent to user space, and there are no changes that could expose uninitialized stack or heap memory. Any potential issues introduced by these changes (such as out-of-bounds accesses or logic bugs) would be effectively caught by standard KASAN and other default sanitizers. Therefore, a dedicated KMSAN fuzzing session is not required.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|