AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/09/01 00:20 flow patch-triage
1m Args:
null

Results:
{
  "EnableConfigs": [
    "NFSD",
    "SUNRPC"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "svc_rpcb_failure_count",
    "svc_generic_rpcbind_set"
  ],
  "KMSANReasoning": "The patch set enhances NFSD generic netlink listener management by validating transport names early, limiting the maximum number of listeners per request, tracking rpcbind failures via an atomic counter in struct svc_serv, and adding comprehensive kselftests.\n\nKMSAN vs KASAN applicability analysis:\n1. No uninitialized memory or structure padding is copied to user space: netlink extack strings and return codes use constant strings or formatted messages with valid attributes.\n2. The newly added struct member (sv_rpcb_failures in struct svc_serv) is allocated using kzalloc_obj (*serv) in __svc_create(), ensuring it is always zero-initialized.\n3. All local variables and branching conditions (e.g. skipped_rpcb, bad_attr, bad_xprt, rpcb_failures, noanswer) are initialized before use.\n4. Any potential issues with listener lists, locking, or socket teardown are standard lifecycle and synchronization concerns covered by KASAN and LOCKDEP.\n\nTherefore, this patch series does not introduce or expose any uninitialized memory risks and does not warrant a dedicated KMSAN session.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch updates the generic netlink interface for NFSD listeners (nfsd_nl_listener_set_doit) by adding bounds checking, unsupported transport validation, and improving error handling when communicating with local rpcbind during listener creation/removal (svc_register, svc_rpcb_failure_count, svc_generic_rpcbind_set). These changes introduce new logic and error handling paths in reachable core kernel subsystems (NFSD and SunRPC) that should be fuzzed.",
  "WorthFuzzing": true
}

1/1 2026/09/01 00:20 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit dcc400252da4944425eeebdaaf6ff74f523cc034\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Tue Sep 1 00:20:23 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/MAINTAINERS b/MAINTAINERS\nindex 0b42e898f4d8e..30f403f996c2a 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -14208,6 +14208,7 @@ F:\tinclude/uapi/linux/nfsd/\n F:\tinclude/uapi/linux/sunrpc/\n F:\tnet/sunrpc/\n F:\ttools/net/sunrpc/\n+F:\ttools/testing/selftests/nfsd/\n \n KERNEL NFSD BLOCK and SCSI LAYOUT DRIVER\n R:\tChristoph Hellwig \u003chch@lst.de\u003e\ndiff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c\nindex adb032b7311a3..2d26da12051c5 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,13 @@ 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\tNL_SET_ERR_MSG_ATTR(info-\u003eextack,\n+\t\t\t\t\t    tb[NFSD_A_SOCK_TRANSPORT_NAME],\n+\t\t\t\t\t    \"unsupported transport name\");\n+\t\t\treturn -EPROTONOSUPPORT;\n+\t\t}\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@@ -2037,8 +2067,12 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)\n int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n {\n \tstruct net *net = genl_info_net(info);\n+\tconst struct nlattr *bad_attr = NULL;\n \tstruct svc_xprt *xprt, *tmp;\n+\tconst char *bad_xprt = NULL;\n+\tunsigned int rpcb_failures;\n \tconst struct nlattr *attr;\n+\tbool skipped_rpcb = false;\n \tstruct svc_serv *serv;\n \tLIST_HEAD(permsocks);\n \tstruct nfsd_net *nn;\n@@ -2128,13 +2162,15 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n \tif (delete)\n \t\tsvc_xprt_destroy_all(serv, net, false);\n \n+\trpcb_failures = svc_rpcb_failure_count(serv);\n+\n \t/* walk list of addrs again, open any that still don't exist */\n \tnlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info-\u003enlhdr,\n \t\t\t\t GENL_HDRLEN, rem) {\n \t\tstruct nlattr *tb[NFSD_A_SOCK_MAX + 1];\n \t\tconst char *xcl_name;\n \t\tstruct sockaddr *sa;\n-\t\tint ret;\n+\t\tint flags, ret;\n \n \t\t/* validated up front in nfsd_nl_validate_listeners() */\n \t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\n@@ -2153,11 +2189,46 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n \t\t\tcontinue;\n \t\t}\n \n-\t\tret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,\n+\t\tflags = skipped_rpcb ? SVC_SOCK_ANONYMOUS : 0;\n+\t\tret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, flags,\n \t\t\t\t\t      current_cred());\n+\n+\t\tif (!skipped_rpcb \u0026\u0026\n+\t\t    svc_rpcb_failure_count(serv) != rpcb_failures) {\n+\t\t\tskipped_rpcb = true;\n+\t\t\tif (ret \u003c 0)\n+\t\t\t\tret = svc_xprt_create_from_sa(serv, xcl_name,\n+\t\t\t\t\t\t\t      net, sa,\n+\t\t\t\t\t\t\t      SVC_SOCK_ANONYMOUS,\n+\t\t\t\t\t\t\t      current_cred());\n+\t\t}\n+\n \t\t/* always save the latest error */\n-\t\tif (ret \u003c 0)\n+\t\tif (ret \u003c 0) {\n+\t\t\tbad_attr = attr;\n+\t\t\tbad_xprt = xcl_name;\n \t\t\terr = ret;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * The ack carries the errno of the last entry that failed. Point at\n+\t * that entry as well, since several entries can share a transport\n+\t * name and the errno alone cannot tell them apart.\n+\t */\n+\tif (err) {\n+\t\tNL_SET_BAD_ATTR(info-\u003eextack, bad_attr);\n+\t\tif (skipped_rpcb)\n+\t\t\tNL_SET_ERR_MSG_FMT(info-\u003eextack,\n+\t\t\t\t\t   \"cannot create %s listener; rpcbind did not answer\",\n+\t\t\t\t\t   bad_xprt);\n+\t\telse\n+\t\t\tNL_SET_ERR_MSG_FMT(info-\u003eextack,\n+\t\t\t\t\t   \"cannot create %s listener\",\n+\t\t\t\t\t   bad_xprt);\n+\t} else if (skipped_rpcb) {\n+\t\tNL_SET_ERR_MSG(info-\u003eextack,\n+\t\t\t       \"rpcbind did not answer, some listeners are not registered\");\n \t}\n \n \tif (!serv-\u003esv_nrthreads \u0026\u0026 list_empty(\u0026nn-\u003enfsd_serv-\u003esv_permsocks))\ndiff --git a/include/linux/sunrpc/clnt.h b/include/linux/sunrpc/clnt.h\nindex 3c2b8c355ab3a..30344c0d6a9d7 100644\n--- a/include/linux/sunrpc/clnt.h\n+++ b/include/linux/sunrpc/clnt.h\n@@ -199,7 +199,8 @@ struct rpc_xprt\t*rpc_task_get_xprt(struct rpc_clnt *clnt,\n \n int\t\trpcb_create_local(struct net *);\n void\t\trpcb_put_local(struct net *);\n-int\t\trpcb_register(struct net *, u32, u32, int, unsigned short);\n+int\t\trpcb_register(struct net *net, u32 prog, u32 vers, int prot,\n+\t\t\t      unsigned short port);\n int\t\trpcb_v4_register(struct net *net, const u32 program,\n \t\t\t\t const u32 version,\n \t\t\t\t const struct sockaddr *address,\ndiff --git a/include/linux/sunrpc/svc.h b/include/linux/sunrpc/svc.h\nindex 2db1b9ec5658d..5fa9417e034d7 100644\n--- a/include/linux/sunrpc/svc.h\n+++ b/include/linux/sunrpc/svc.h\n@@ -78,6 +78,7 @@ struct svc_serv {\n \tunsigned int\t\tsv_max_payload;\t/* datagram payload size */\n \tunsigned int\t\tsv_max_mesg;\t/* max_payload + 1 page for overheads */\n \tunsigned int\t\tsv_xdrsize;\t/* XDR buffer size */\n+\tatomic_t\t\tsv_rpcb_failures; /* unanswered rpcbind calls */\n \tstruct list_head\tsv_permsocks;\t/* all permanent sockets */\n \tstruct list_head\tsv_tempsocks;\t/* all temporary sockets */\n \tint\t\t\tsv_tmpcnt;\t/* count of temporary \"valid\" sockets */\n@@ -451,6 +452,7 @@ int sunrpc_set_pool_mode(const char *val);\n int sunrpc_get_pool_mode(char *val, size_t size);\n void svc_rpcb_cleanup(struct svc_serv *serv, struct net *net);\n int svc_bind(struct svc_serv *serv, struct net *net);\n+unsigned int svc_rpcb_failure_count(struct svc_serv *serv);\n struct svc_serv *svc_create(struct svc_program *, unsigned int,\n \t\t\t    int (*threadfn)(void *data));\n bool\t\t   svc_rqst_replace_page(struct svc_rqst *rqstp,\n@@ -471,8 +473,9 @@ unsigned int\t   svc_serv_maxthreads(const struct svc_serv *serv);\n int\t\t   svc_pool_stats_open(struct svc_info *si, struct file *file);\n void\t\t   svc_process(struct svc_rqst *rqstp);\n void\t\t   svc_process_bc(struct rpc_rqst *req, struct svc_rqst *rqstp);\n-int\t\t   svc_register(const struct svc_serv *, struct net *, const int,\n-\t\t\t\tconst unsigned short, const unsigned short);\n+int\t\t   svc_register(struct svc_serv *serv, struct net *net,\n+\t\t\t\tconst int family, const unsigned short proto,\n+\t\t\t\tconst unsigned short port);\n \n void\t\t   svc_wake_up(struct svc_serv *);\n void\t\t   svc_reserve(struct svc_rqst *rqstp, int space);\ndiff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c\nindex 4c0b7fefee4e2..8b9621129115f 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;\n@@ -400,7 +412,8 @@ static struct rpc_clnt *rpcb_create(struct net *net, const char *nodename,\n \treturn rpc_create(\u0026args);\n }\n \n-static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt, struct rpc_message *msg, bool is_set)\n+static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt,\n+\t\t\t      struct rpc_message *msg, bool is_set)\n {\n \tint flags = RPC_TASK_NOCONNECT;\n \tint error, result = 0;\n@@ -410,8 +423,22 @@ static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt, stru\n \tmsg-\u003erpc_resp = \u0026result;\n \n \terror = rpc_call_sync(clnt, msg, flags);\n-\tif (error \u003c 0)\n-\t\treturn error;\n+\tif (error \u003c 0) {\n+\t\tswitch (error) {\n+\t\t/* rpcbind answered; the reply itself carries the error */\n+\t\tcase -EPROTONOSUPPORT:\n+\t\tcase -EPFNOSUPPORT:\n+\t\tcase -EOPNOTSUPP:\n+\t\tcase -EACCES:\n+\t\t/* the call never made it onto the wire */\n+\t\tcase -ENOMEM:\n+\t\tcase -EMSGSIZE:\n+\t\tcase -ERESTARTSYS:\n+\t\t\treturn error;\n+\t\t}\n+\t\t/* anything else, we assume that rpcbind isn't functional */\n+\t\treturn -EIO;\n+\t}\n \n \tif (!result)\n \t\treturn -EACCES;\ndiff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c\nindex 8297bad2b1777..c00ae00b6a121 100644\n--- a/net/sunrpc/svc.c\n+++ b/net/sunrpc/svc.c\n@@ -1179,10 +1179,40 @@ int svc_generic_rpcbind_set(struct net *net,\n \terror = svc_rpcbind_set_version(net, progp, version,\n \t\t\t\t\tfamily, proto, port);\n \n+\t/* -EIO means no answer, not a refusal, so vs_rpcb_optnl must keep it. */\n+\tif (error == -EIO)\n+\t\treturn error;\n+\n \treturn (vers-\u003evs_rpcb_optnl) ? 0 : error;\n }\n EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\n \n+/**\n+ * svc_rpcb_failure_count - local rpcbind calls for @serv that got no answer\n+ * @serv: RPC service to query\n+ *\n+ * svc_register() adds one for each of its calls that got no answer. A reply\n+ * that refuses one entry does not count, because rpcbind answered and the\n+ * next entry may still succeed.\n+ *\n+ * The count is kept per serv rather than per net. The local rpcbind client\n+ * is per-net and lockd shares it, but a count that another service can move\n+ * says nothing about this serv's own calls.\n+ *\n+ * This is for callers that cannot see the svc_register() return, because a\n+ * transport class sits in between. Such a caller reads the count before it\n+ * starts and compares as it goes, so there is no state to reset between\n+ * operations. The count never resets, and callers must not attach meaning\n+ * to the value itself.\n+ *\n+ * Return: the number of unanswered calls since this serv was created.\n+ */\n+unsigned int svc_rpcb_failure_count(struct svc_serv *serv)\n+{\n+\treturn atomic_read(\u0026serv-\u003esv_rpcb_failures);\n+}\n+EXPORT_SYMBOL_GPL(svc_rpcb_failure_count);\n+\n /**\n  * svc_register - register an RPC service with the local portmapper\n  * @serv: svc_serv struct for the service to register\n@@ -1193,10 +1223,11 @@ EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\n  *\n  * Service is registered for any address in the passed-in protocol family\n  */\n-int svc_register(const struct svc_serv *serv, struct net *net,\n+int svc_register(struct svc_serv *serv, struct net *net,\n \t\t const int family, const unsigned short proto,\n \t\t const unsigned short port)\n {\n+\tbool\t\t\tnoanswer = false;\n \tunsigned int\t\tp, i;\n \tint\t\t\terror = 0;\n \n@@ -1208,18 +1239,34 @@ 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\tconst struct svc_version *vers = progp-\u003epg_vers[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 == -EIO) {\n+\t\t\t\tnoanswer = true;\n+\t\t\t\tif (vers \u0026\u0026 vers-\u003evs_rpcb_optnl)\n+\t\t\t\t\tret = 0;\n+\t\t\t}\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}\n+\n+\t\t/* Give up on trying to register anything if it didn't respond */\n+\t\tif (noanswer)\n+\t\t\tbreak;\n \t}\n \n+\tif (noanswer)\n+\t\tatomic_inc(\u0026serv-\u003esv_rpcb_failures);\n+\n \treturn error;\n }\n \n@@ -1230,8 +1277,8 @@ int svc_register(const struct svc_serv *serv, struct net *net,\n  * any \"inet6\" entries anyway.  So a PMAP_UNSET should be sufficient\n  * in this case to clear all existing entries for [program, version].\n  */\n-static void __svc_unregister(struct net *net, const u32 program, const u32 version,\n-\t\t\t     const char *progname)\n+static int __svc_unregister(struct net *net, const u32 program, const u32 version,\n+\t\t\t    const char *progname)\n {\n \tint error;\n \n@@ -1245,6 +1292,7 @@ static void __svc_unregister(struct net *net, const u32 program, const u32 versi\n \t\terror = rpcb_register(net, program, version, 0, 0);\n \n \ttrace_svc_unregister(progname, version, error);\n+\treturn error;\n }\n \n /*\n@@ -1271,10 +1319,13 @@ static void svc_unregister(const struct svc_serv *serv, struct net *net)\n \t\t\t\tcontinue;\n \t\t\tif (progp-\u003epg_vers[i]-\u003evs_hidden)\n \t\t\t\tcontinue;\n-\t\t\t__svc_unregister(net, progp-\u003epg_prog, i, progp-\u003epg_name);\n+\t\t\tif (__svc_unregister(net, progp-\u003epg_prog, i,\n+\t\t\t\t\t     progp-\u003epg_name) == -EIO)\n+\t\t\t\tgoto out;\n \t\t}\n \t}\n \n+out:\n \trcu_read_lock();\n \tsighand = rcu_dereference(current-\u003esighand);\n \tspin_lock_irqsave(\u0026sighand-\u003esiglock, flags);\ndiff --git a/net/sunrpc/svc_xprt.c b/net/sunrpc/svc_xprt.c\nindex 40040af588fb2..7e471c92f23ab 100644\n--- a/net/sunrpc/svc_xprt.c\n+++ b/net/sunrpc/svc_xprt.c\n@@ -1101,6 +1101,22 @@ static void call_xpt_users(struct svc_xprt *xprt)\n \tspin_unlock(\u0026xprt-\u003expt_lock);\n }\n \n+/*\n+ * If rpcbind stops answering, every listener still to be destroyed would\n+ * only wait out the same timeout again. Drop the flag on all of the\n+ * remaining listeners.\n+ */\n+static void svc_xprt_clear_rpcb_unreg(struct svc_serv *serv, struct net *net)\n+{\n+\tstruct svc_xprt *xprt;\n+\n+\tspin_lock_bh(\u0026serv-\u003esv_lock);\n+\tlist_for_each_entry(xprt, \u0026serv-\u003esv_permsocks, xpt_list)\n+\t\tif (xprt-\u003expt_net == net)\n+\t\t\tclear_bit(XPT_RPCB_UNREG, \u0026xprt-\u003expt_flags);\n+\tspin_unlock_bh(\u0026serv-\u003esv_lock);\n+}\n+\n /*\n  * Remove a dead transport\n  */\n@@ -1115,11 +1131,15 @@ static void svc_delete_xprt(struct svc_xprt *xprt)\n \t\tstruct svc_sock *svsk = container_of(xprt, struct svc_sock,\n \t\t\t\t\t\t     sk_xprt);\n \t\tstruct socket *sock = svsk-\u003esk_sock;\n+\t\tunsigned int failures = svc_rpcb_failure_count(serv);\n \n \t\tif (svc_register(serv, xprt-\u003expt_net, sock-\u003esk-\u003esk_family,\n \t\t\t\t sock-\u003esk-\u003esk_protocol, 0) \u003c 0)\n \t\t\tpr_warn(\"failed to unregister %s with rpcbind\\n\",\n \t\t\t\txprt-\u003expt_class-\u003excl_name);\n+\n+\t\tif (svc_rpcb_failure_count(serv) != failures)\n+\t\t\tsvc_xprt_clear_rpcb_unreg(serv, xprt-\u003expt_net);\n \t}\n \n \tif (test_and_set_bit(XPT_DEAD, \u0026xprt-\u003expt_flags))\ndiff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile\nindex 2d960626750e3..d881ba39cad40 100644\n--- a/tools/testing/selftests/Makefile\n+++ b/tools/testing/selftests/Makefile\n@@ -90,6 +90,7 @@ TARGETS += net/packetdrill\n TARGETS += net/ppp\n TARGETS += net/rds\n TARGETS += net/tcp_ao\n+TARGETS += nfsd\n TARGETS += nolibc\n TARGETS += pci_endpoint\n TARGETS += pcie_bwctrl\ndiff --git a/tools/testing/selftests/nfsd/.gitignore b/tools/testing/selftests/nfsd/.gitignore\nnew file mode 100644\nindex 0000000000000..19e6dec04d8e9\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/.gitignore\n@@ -0,0 +1 @@\n+nfsd_netlink_listener\ndiff --git a/tools/testing/selftests/nfsd/Makefile b/tools/testing/selftests/nfsd/Makefile\nnew file mode 100644\nindex 0000000000000..15ac65549d259\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/Makefile\n@@ -0,0 +1,6 @@\n+# SPDX-License-Identifier: GPL-2.0\n+CFLAGS += $(KHDR_INCLUDES) -Wall\n+\n+TEST_GEN_PROGS := nfsd_netlink_listener\n+\n+include ../lib.mk\ndiff --git a/tools/testing/selftests/nfsd/config b/tools/testing/selftests/nfsd/config\nnew file mode 100644\nindex 0000000000000..ab84523fbedf3\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/config\n@@ -0,0 +1,8 @@\n+CONFIG_NAMESPACES=y\n+CONFIG_NET_NS=y\n+CONFIG_SHMEM=y\n+CONFIG_TMPFS=y\n+CONFIG_UNIX=y\n+CONFIG_IPV6=y\n+CONFIG_NFSD=y\n+CONFIG_NFSD_V4=y\ndiff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c\nnew file mode 100644\nindex 0000000000000..89d1da825b954\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c\n@@ -0,0 +1,1328 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * Regression tests for the NFSD generic-netlink listener interface\n+ * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).\n+ *\n+ * Three groups:\n+ *   validation  - malformed/abusive LISTENER_SET requests are rejected by\n+ *                 nfsd_nl_validate_listeners(), before nfsd_mutex is taken.\n+ *   functional  - create/add/remove listeners and verify LISTENER_GET\n+ *                 reflects the set (round-trip of transport + addr:port).\n+ *   semantics   - once threads are running (THREADS_SET) a listener change\n+ *                 is refused with -EBUSY.\n+ *\n+ * Each test runs in its own private net + mount namespace (unshare in\n+ * FIXTURE_SETUP). /run is masked there: a pathname AF_LOCAL connect is not\n+ * scoped by the network namespace, since unix_find_bsd() resolves by inode\n+ * and takes no struct net, so the kernel's rpcbind client would otherwise be\n+ * able to reach the rpcbind running on the host. Anything that creates a\n+ * serv is served by the per-netns rpcbind stub below instead.\n+ */\n+#define _GNU_SOURCE\n+#include \u003cerrno.h\u003e\n+#include \u003cpoll.h\u003e\n+#include \u003csched.h\u003e\n+#include \u003csignal.h\u003e\n+#include \u003cstddef.h\u003e\n+#include \u003cstdint.h\u003e\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003csys/mount.h\u003e\n+#include \u003csys/prctl.h\u003e\n+#include \u003csys/socket.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003csys/stat.h\u003e\n+#include \u003csys/time.h\u003e\n+#include \u003csys/un.h\u003e\n+#include \u003csys/wait.h\u003e\n+#include \u003cnet/if.h\u003e\n+#include \u003cnetinet/in.h\u003e\n+#include \u003clinux/netlink.h\u003e\n+#include \u003clinux/genetlink.h\u003e\n+\n+#include \"../kselftest_harness.h\"\n+\n+/* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */\n+#define NFSD_FAMILY_NAME\t\t\"nfsd\"\n+#define NFSD_CMD_THREADS_SET\t\t2\n+#define NFSD_CMD_VERSION_SET\t\t4\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+#define NFSD_A_SERVER_PROTO_VERSION\t1\t/* per-version nest */\n+#define NFSD_A_VERSION_MAJOR\t\t1\t/* inside the version nest */\n+#define NFSD_A_VERSION_MINOR\t\t2\t/* inside the version nest */\n+#define NFSD_A_VERSION_ENABLED\t\t3\t/* inside the version 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 = -1;\t\t/* set per-test in FIXTURE_SETUP */\n+\n+/* Extack message from the last genl_request(); empty if there was none. */\n+static char last_extack[128];\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+\tint on = 1;\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+\t/*\n+\t * Ask for extack, and cap the ack so the request is not echoed back:\n+\t * the TLVs then always follow the fixed part of the error message.\n+\t */\n+\tsetsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, \u0026on, sizeof(on));\n+\tsetsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, \u0026on, sizeof(on));\n+\treturn fd;\n+}\n+\n+/* Stash the extack message of an ack, if it carries one. */\n+static void parse_extack(const char *rbuf)\n+{\n+\tconst struct nlmsghdr *nlh = (const void *)rbuf;\n+\tconst struct nlattr *na;\n+\tint off, left;\n+\n+\tlast_extack[0] = '\\0';\n+\tif (nlh-\u003enlmsg_type != NLMSG_ERROR ||\n+\t    !(nlh-\u003enlmsg_flags \u0026 NLM_F_ACK_TLVS))\n+\t\treturn;\n+\n+\toff = NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(struct nlmsgerr));\n+\tleft = nlh-\u003enlmsg_len - off;\n+\tna = (const void *)(rbuf + off);\n+\n+\twhile (left \u003e= (int)NLA_HDRLEN) {\n+\t\tif ((na-\u003enla_type \u0026 NLA_TYPE_MASK) == NLMSGERR_ATTR_MSG) {\n+\t\t\tstrncpy(last_extack, (const char *)na + NLA_HDRLEN,\n+\t\t\t\tsizeof(last_extack) - 1);\n+\t\t\tlast_extack[sizeof(last_extack) - 1] = '\\0';\n+\t\t\treturn;\n+\t\t}\n+\t\tleft -= NLA_ALIGN4(na-\u003enla_len);\n+\t\tna = (const void *)((const char *)na + NLA_ALIGN4(na-\u003enla_len));\n+\t}\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+\tlast_extack[0] = '\\0';\n+\tn = recv(fd, rbuf, sizeof(rbuf), 0);\n+\tif (n \u003c 0) {\n+\t\tret = (errno == EAGAIN || errno == EWOULDBLOCK) ? -ETIMEDOUT : -errno;\n+\t} else if (((struct nlmsghdr *)rbuf)-\u003enlmsg_type == NLMSG_ERROR) {\n+\t\tret = ((struct nlmsgerr *)NLMSG_DATA(rbuf))-\u003eerror;\n+\t\tparse_extack(rbuf);\n+\t} else {\n+\t\tret = 0;\n+\t}\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+/*\n+ * Enable exactly one NFS version in this netns. NFSD_CMD_VERSION_SET clears\n+ * every version first, so one nest is enough to leave the server v4-only.\n+ * It refuses once a serv exists, so call it before any listener.\n+ */\n+static int version_set_only(uint32_t major, uint32_t minor)\n+{\n+\tchar attrs[64];\n+\tstruct nlattr *nest = (void *)attrs;\n+\tint inner = NLA_HDRLEN;\n+\n+\tinner = put_attr(attrs, inner, NFSD_A_VERSION_MAJOR,\n+\t\t\t \u0026major, sizeof(major));\n+\tinner = put_attr(attrs, inner, NFSD_A_VERSION_MINOR,\n+\t\t\t \u0026minor, sizeof(minor));\n+\tinner = put_attr(attrs, inner, NFSD_A_VERSION_ENABLED, NULL, 0);\n+\tnest-\u003enla_type = NFSD_A_SERVER_PROTO_VERSION | NLA_F_NESTED;\n+\tnest-\u003enla_len = inner;\n+\n+\treturn genl_request(NFSD_CMD_VERSION_SET, attrs, NLA_ALIGN4(inner));\n+}\n+\n+/* Fetch the current listeners; returns count (\u003e=0) or -errno. */\n+static int listener_get(struct listener_ent *out, int max)\n+{\n+\tchar rbuf[8192];\n+\tint n = genl_request_reply(NFSD_CMD_LISTENER_GET, rbuf, sizeof(rbuf));\n+\n+\tif (n \u003c 0)\n+\t\treturn n;\n+\treturn parse_listener_get(rbuf, n, out, max);\n+}\n+\n+/*\n+ * Every listener these tests create comes from put_listener_af(), so the\n+ * address is always loopback. Match on it too: without that, a reply that\n+ * gave the right transport and port on the wrong address (0.0.0.0, say)\n+ * would pass.\n+ */\n+static struct listener_ent *find_listener(struct listener_ent *e, int n,\n+\t\t\t\t\t  const char *xprt, int family,\n+\t\t\t\t\t  uint16_t port)\n+{\n+\tint i;\n+\n+\tfor (i = 0; i \u003c n; i++) {\n+\t\tif (e[i].family != family || e[i].port != port ||\n+\t\t    strcmp(e[i].xprt, xprt))\n+\t\t\tcontinue;\n+\t\tif (family == AF_INET6) {\n+\t\t\tif (memcmp(\u0026e[i].a6, \u0026in6addr_loopback, sizeof(e[i].a6)))\n+\t\t\t\tcontinue;\n+\t\t} else if (e[i].a4.s_addr != htonl(INADDR_LOOPBACK)) {\n+\t\t\tcontinue;\n+\t\t}\n+\t\treturn \u0026e[i];\n+\t}\n+\treturn NULL;\n+}\n+\n+/* Start (@n \u003e 0) or stop (@n == 0) nfsd threads in this netns. */\n+static int threads_set(int n)\n+{\n+\tchar attrs[64];\n+\tuint32_t v = n;\n+\tint off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, \u0026v, sizeof(v));\n+\n+\treturn genl_request(NFSD_CMD_THREADS_SET, attrs, off);\n+}\n+\n+/* ------------------- per-netns local rpcbind stub ------------------- */\n+\n+/*\n+ * Creating a listener registers with rpcbind: nfsd_nl_listener_set_doit()\n+ * passes no SVC_SOCK_ANONYMOUS for the first entry of a request, so\n+ * pmap_register is true in svc_setup_socket(). The fixture's server has v3\n+ * enabled, and nfsd_version3 does not set vs_rpcb_optnl, so a failure there\n+ * comes back out of svc_register() and takes the listener down with it.\n+ * With nothing listening, every attempt first waits out the local rpcbind\n+ * timeout. The abstract AF_LOCAL name the kernel tries first is per-netns\n+ * (unix_find_abstract() takes a struct net), so answer it here and stay out\n+ * 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 leave every IPv6 listener unregistered.\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+ * In RPCB_STUB_SILENT mode a SET or an UNSET is read and nothing is written\n+ * back, so the kernel waits out its own timeout. That is the only mode that\n+ * makes rpcb_register_call() report a call that got no answer, which is what\n+ * the per-net failure count records. The NULL procedure is still answered:\n+ * rpcb_create_af_local() builds its client without RPC_CLNT_CREATE_NOPING, so\n+ * rpc_create() pings, and a ping that goes unanswered drops the kernel onto\n+ * the loopback rpcb_create_local_net() client, which never reaches this stub.\n+ *\n+ * The stub also keeps counters and the mode in a page shared with the test, so\n+ * a test can assert that the kernel never talked to rpcbind at all, or that it\n+ * dropped the local rpcbind client and had to reconnect.\n+ *\n+ * The mode lives there rather than in the child so that a test can change it\n+ * with a serv already up. Killing and restarting the stub would close the\n+ * connection the kernel holds, and rpcb_register_call() issues UNSET over\n+ * AF_LOCAL with RPC_TASK_NOCONNECT, so the next call would fail at once with\n+ * -ENOTCONN instead of waiting out a timeout.\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, RPCB_STUB_SILENT };\n+\n+struct rpcb_stub_stats {\n+\tunsigned int conns;\t\t/* connections accepted */\n+\tunsigned int calls;\t\t/* calls received */\n+\tunsigned int mode;\t\t/* RPCB_STUB_*, read on every call */\n+};\n+\n+static volatile struct rpcb_stub_stats *rpcb_stats;\t/* MAP_SHARED */\n+\n+static int rpcb_stats_alloc(void)\n+{\n+\tvoid *p = mmap(NULL, sizeof(*rpcb_stats), PROT_READ | PROT_WRITE,\n+\t\t       MAP_SHARED | MAP_ANONYMOUS, -1, 0);\n+\n+\tif (p == MAP_FAILED)\n+\t\treturn -1;\n+\trpcb_stats = p;\n+\treturn 0;\n+}\n+\n+/*\n+ * The stub bumps these before it replies and the kernel waits for that reply,\n+ * so whatever a netlink request provoked is visible once it returns.\n+ */\n+static int rpcb_calls(void)\n+{\n+\treturn rpcb_stats ? (int)rpcb_stats-\u003ecalls : 0;\n+}\n+\n+static int rpcb_conns(void)\n+{\n+\treturn rpcb_stats ? (int)rpcb_stats-\u003econns : 0;\n+}\n+\n+/* Takes effect on the stub's next call; the caller has not sent one yet. */\n+static void rpcb_stub_set_mode(int mode)\n+{\n+\trpcb_stats-\u003emode = mode;\n+}\n+\n+static int rpcb_stub_listen(void)\n+{\n+\tstruct sockaddr_un sun = { .sun_family = AF_UNIX };\n+\tsize_t nlen = strlen(RPCB_ABSTRACT_NAME);\n+\tsocklen_t alen;\n+\tint fd;\n+\n+\t/* Abstract names are length-delimited, so the length must match. */\n+\tmemcpy(sun.sun_path + 1, RPCB_ABSTRACT_NAME, nlen);\n+\talen = offsetof(struct sockaddr_un, sun_path) + 1 + nlen;\n+\n+\tfd = socket(AF_UNIX, SOCK_STREAM, 0);\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tif (bind(fd, (struct sockaddr *)\u0026sun, alen) \u003c 0 ||\n+\t    listen(fd, RPCB_STUB_MAXCONN) \u003c 0) {\n+\t\tclose(fd);\n+\t\treturn -1;\n+\t}\n+\treturn fd;\n+}\n+\n+static int rpcb_stub_read(int fd, void *buf, size_t len)\n+{\n+\tsize_t done = 0;\n+\n+\twhile (done \u003c len) {\n+\t\tssize_t n = read(fd, (char *)buf + done, len - done);\n+\n+\t\tif (n \u003c= 0)\n+\t\t\treturn -1;\n+\t\tdone += n;\n+\t}\n+\treturn 0;\n+}\n+\n+/* Handle one record-marked RPC call. Returns -1 when the peer is done. */\n+static int rpcb_stub_call(int fd)\n+{\n+\tunsigned int len, nrep = 6, mode = rpcb_stats-\u003emode;\n+\tuint32_t mark, call[6], rep[7];\n+\tsize_t replen;\n+\n+\tif (rpcb_stub_read(fd, \u0026mark, sizeof(mark)))\n+\t\treturn -1;\n+\tlen = ntohl(mark) \u0026 0x7fffffff;\n+\tif (len \u003c sizeof(call) || len \u003e 4096)\n+\t\treturn -1;\n+\tif (rpcb_stub_read(fd, call, sizeof(call)))\n+\t\treturn -1;\n+\n+\t/* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */\n+\tfor (len -= sizeof(call); len; ) {\n+\t\tchar sink[256];\n+\t\tunsigned int n = len \u003e sizeof(sink) ? sizeof(sink) : len;\n+\n+\t\tif (rpcb_stub_read(fd, sink, n))\n+\t\t\treturn -1;\n+\t\tlen -= n;\n+\t}\n+\n+\tif (rpcb_stats)\n+\t\trpcb_stats-\u003ecalls++;\n+\n+\trep[0] = call[0];\t\t/* xid */\n+\trep[1] = htonl(1);\t\t/* REPLY */\n+\trep[2] = htonl(0);\t\t/* MSG_ACCEPTED */\n+\trep[3] = htonl(0);\t\t/* verifier flavor AUTH_NULL */\n+\trep[4] = htonl(0);\t\t/* verifier length */\n+\trep[5] = htonl(0);\t\t/* SUCCESS */\n+\n+\tif (ntohl(call[3]) != RPCB_PROGRAM) {\n+\t\trep[5] = htonl(1);\t/* PROG_UNAVAIL */\n+\t} else {\n+\t\tunsigned int proc = ntohl(call[5]);\n+\n+\t\tswitch (proc) {\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+\n+\t\t/*\n+\t\t * Answer nothing, so the caller waits out its timeout. The\n+\t\t * NULL procedure is answered even here: the kernel pings at\n+\t\t * client creation, and a ping with no answer takes it off\n+\t\t * this socket entirely.\n+\t\t */\n+\t\tif (mode == RPCB_STUB_SILENT \u0026\u0026 proc != RPCB_PROC_NULL)\n+\t\t\treturn 0;\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)\n+{\n+\tstruct pollfd pfd[1 + RPCB_STUB_MAXCONN];\n+\tnfds_t n = 1, i;\n+\n+\tpfd[0].fd = lfd;\n+\n+\tfor (;;) {\n+\t\t/* stop polling the listener when full, or poll() spins */\n+\t\tpfd[0].events = n \u003c 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;\n+\n+\t\tif (poll(pfd, n, -1) \u003c 0)\n+\t\t\treturn;\n+\n+\t\tif (pfd[0].revents \u0026 POLLIN) {\n+\t\t\tint c = accept(lfd, NULL, NULL);\n+\n+\t\t\tif (c \u003e= 0) {\n+\t\t\t\tpfd[n].fd = c;\n+\t\t\t\tpfd[n].events = POLLIN;\n+\t\t\t\t/*\n+\t\t\t\t * poll() ran with the old n, so it did not\n+\t\t\t\t * write this revents. The loop below reads it.\n+\t\t\t\t */\n+\t\t\t\tpfd[n].revents = 0;\n+\t\t\t\tn++;\n+\t\t\t\tif (rpcb_stats)\n+\t\t\t\t\trpcb_stats-\u003econns++;\n+\t\t\t}\n+\t\t}\n+\n+\t\tfor (i = 1; i \u003c n; i++) {\n+\t\t\tif (!(pfd[i].revents \u0026 (POLLIN | POLLHUP | POLLERR)))\n+\t\t\t\tcontinue;\n+\t\t\tif (rpcb_stub_call(pfd[i].fd)) {\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+\trpcb_stats-\u003emode = mode;\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);\n+\t\t_exit(0);\n+\t}\n+\n+\tclose(lfd);\n+\treturn pid;\n+}\n+\n+/* --------------------------- fixture --------------------------- */\n+\n+FIXTURE(nfsd_listener) {\n+\tpid_t rpcbd;\n+};\n+\n+FIXTURE_SETUP(nfsd_listener)\n+{\n+\tstruct ifreq ifr = {0};\n+\tstruct stat st;\n+\tint s;\n+\n+\tif (geteuid() != 0)\n+\t\tSKIP(return, \"must be run as root\");\n+\tif (unshare(CLONE_NEWNET | CLONE_NEWNS) \u003c 0)\n+\t\tSKIP(return, \"unshare(NEWNET|NEWNS): %s\", strerror(errno));\n+\tif (mount(\"\", \"/\", NULL, MS_REC | MS_PRIVATE, NULL) \u003c 0)\n+\t\tSKIP(return, \"mount(/ private): %s\", strerror(errno));\n+\n+\t/*\n+\t * Keep the kernel's rpcbind client inside this namespace. The\n+\t * abstract socket it tries first is per-netns, but the\n+\t * \"/var/run/rpcbind.sock\" fallback is not, so hide the path.\n+\t */\n+\tif (mount(\"tmpfs\", \"/run\", \"tmpfs\", 0, NULL) \u003c 0)\n+\t\tSKIP(return, \"mount(tmpfs on /run): %s\", strerror(errno));\n+\tif (lstat(\"/var/run\", \u0026st) == 0 \u0026\u0026 S_ISDIR(st.st_mode) \u0026\u0026\n+\t    mount(\"tmpfs\", \"/var/run\", \"tmpfs\", 0, NULL) \u003c 0)\n+\t\tSKIP(return, \"mount(tmpfs on /var/run): %s\", strerror(errno));\n+\n+\t/* Bring loopback up so listener binds (127.0.0.1 / ::1) work. */\n+\ts = socket(AF_INET, SOCK_DGRAM, 0);\n+\tASSERT_GE(s, 0);\n+\tstrcpy(ifr.ifr_name, \"lo\");\n+\tASSERT_EQ(0, ioctl(s, SIOCGIFFLAGS, \u0026ifr));\n+\tifr.ifr_flags |= IFF_UP | IFF_RUNNING;\n+\tASSERT_EQ(0, ioctl(s, SIOCSIFFLAGS, \u0026ifr));\n+\tclose(s);\n+\n+\tnfsd_family = genl_resolve_nfsd();\n+\tif (nfsd_family \u003c 0)\n+\t\tSKIP(return, \"nfsd genl family not found (modprobe nfsd?)\");\n+\n+\tif (rpcb_stats_alloc() \u003c 0)\n+\t\tSKIP(return, \"mmap(rpcbind stub counters): %s\", strerror(errno));\n+\n+\tself-\u003erpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);\n+\tif (self-\u003erpcbd \u003c 0)\n+\t\tSKIP(return, \"cannot start the rpcbind stub: %s\",\n+\t\t     strerror(errno));\n+}\n+\n+FIXTURE_TEARDOWN(nfsd_listener)\n+{\n+\t/*\n+\t * A listener holds a reference to this netns, which outlives the test\n+\t * process, so anything still up leaks it. Threads pin the listeners in\n+\t * turn; dropping them destroys the serv and everything under it.\n+\t */\n+\tif (nfsd_family \u003e= 0 \u0026\u0026 listener_set(NULL, 0) == -EBUSY)\n+\t\tthreads_set(0);\n+\n+\tif (self-\u003erpcbd \u003e 0) {\n+\t\tkill(self-\u003erpcbd, SIGKILL);\n+\t\twaitpid(self-\u003erpcbd, NULL, 0);\n+\t}\n+\tif (rpcb_stats) {\n+\t\tmunmap((void *)rpcb_stats, sizeof(*rpcb_stats));\n+\t\trpcb_stats = NULL;\n+\t}\n+}\n+\n+/* ===================== validation / negative ===================== */\n+\n+TEST_F(nfsd_listener, val_empty_list_ok)\n+{\n+\tEXPECT_EQ(0, listener_set(NULL, 0));\n+}\n+\n+TEST_F(nfsd_listener, val_too_many)\n+{\n+\tstatic char attrs[1 \u003c\u003c 20];\n+\tint i, off = 0;\n+\n+\tfor (i = 0; i \u003c 1025; i++)\t\t/* \u003e NFSD_NL_LISTENER_MAX (1024) */\n+\t\toff = put_listener(attrs, off, \"udp\", TEST_PORT);\n+\tEXPECT_EQ(-E2BIG, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_missing_addr)\n+{\n+\tchar attrs[64];\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 0 };\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_missing_transport)\n+{\n+\tstruct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };\n+\tstruct raw_listener r = { .xprt = NULL, .emit_addr = 1,\n+\t\t\t\t  .addr = \u0026s4, .addr_len = sizeof(s4) };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+/*\n+ * A name matching no transport class must be refused before nfsd_mutex is\n+ * taken, so it never reaches svc_xprt_create_from_sa() and its\n+ * request_module(\"svc%s\", name) upcall.\n+ *\n+ * The errno cannot show that -- svc_xprt_create_from_sa() returns\n+ * -EPROTONOSUPPORT for an unknown name too. The rpcbind traffic can:\n+ * getting that far means nfsd_create_serv() ran, and svc_bind() pings\n+ * rpcbind at client creation and then sweeps stale entries with\n+ * svc_unregister(). A silent stub is the proof nothing was created.\n+ */\n+TEST_F(nfsd_listener, val_bad_transport)\n+{\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"bogus_xprt\", TEST_PORT);\n+\n+\tASSERT_EQ(0, rpcb_calls());\n+\tEXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));\n+\tEXPECT_EQ(0, rpcb_calls());\n+}\n+\n+TEST_F(nfsd_listener, val_addr_too_short)\n+{\n+\tunsigned char tiny = 0;\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1,\n+\t\t\t\t  .addr = \u0026tiny, .addr_len = 1 };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_inet_short)\n+{\n+\tstruct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026s4,\n+\t\t\t\t  .addr_len = sizeof(sa_family_t) + 2 };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_inet6_short)\n+{\n+\tstruct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) };\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026s6,\n+\t\t\t\t  .addr_len = sizeof(struct sockaddr_in) };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EINVAL, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_bad_family)\n+{\n+\tstruct sockaddr_storage ss = { .ss_family = AF_UNIX };\n+\tstruct raw_listener r = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026ss,\n+\t\t\t\t  .addr_len = sizeof(struct sockaddr_in) };\n+\tchar attrs[64];\n+\tint off = put_raw_listener(attrs, 0, \u0026r);\n+\n+\tEXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));\n+}\n+\n+TEST_F(nfsd_listener, val_second_entry_bad)\n+{\n+\tstruct sockaddr_storage ss = { .ss_family = AF_UNIX };\n+\tstruct raw_listener bad = { .xprt = \"tcp\", .emit_addr = 1, .addr = \u0026ss,\n+\t\t\t\t    .addr_len = sizeof(struct sockaddr_in) };\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[128];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\toff = put_raw_listener(attrs, off, \u0026bad);\n+\t/* The whole request is rejected during validation; nothing applied. */\n+\tEXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));\n+\t/*\n+\t * Again the errno alone does not say so: svc_xprt_create_from_sa()\n+\t * also returns -EAFNOSUPPORT, and the doit keeps the listeners it did\n+\t * manage to create, so the well-formed tcp entry ahead of the bad one\n+\t * would still be up.\n+\t */\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+}\n+\n+/*\n+ * A rejected request must leave the listeners that are already up alone.\n+ * The errno alone does not show that: svc_xprt_create_from_sa() returns\n+ * -EPROTONOSUPPORT for an unknown name too. What differs is how far the\n+ * request gets -- without the check in nfsd_nl_validate_listeners(),\n+ * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener\n+ * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the\n+ * name fails.\n+ */\n+TEST_F(nfsd_listener, val_reject_keeps_listeners)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar good[64], bad[64];\n+\tint og = put_listener(good, 0, \"tcp\", TEST_PORT);\n+\tint ob = put_listener(bad, 0, \"bogus_xprt\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(good, og));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\n+\tEXPECT_EQ(-EPROTONOSUPPORT, listener_set(bad, ob));\n+\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+/* ===================== functional / round-trip ===================== */\n+\n+/* LISTENER_GET with no serv in this netns returns an empty list. */\n+TEST_F(nfsd_listener, func_get_empty)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+}\n+\n+TEST_F(nfsd_listener, func_create_tcp)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tEXPECT_STREQ(\"\", last_extack);\t\t/* nothing to warn about */\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_create_udp)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"udp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"udp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_create_multi)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[128];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\toff = put_listener(attrs, off, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(2, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"tcp\", AF_INET, TEST_PORT));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"udp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_idempotent)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tEXPECT_EQ(0, listener_set(attrs, off));\t\t/* re-set same list */\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_add)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar one[64], two[128];\n+\tint o1 = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\tint o2 = put_listener(two, 0, \"tcp\", TEST_PORT);\n+\n+\to2 = put_listener(two, o2, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(one, o1));\n+\tASSERT_EQ(0, listener_set(two, o2));\t\t/* add udp, keep tcp */\n+\tASSERT_EQ(2, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"tcp\", AF_INET, TEST_PORT));\n+\tEXPECT_NE(NULL, find_listener(got, 2, \"udp\", AF_INET, TEST_PORT));\n+}\n+\n+TEST_F(nfsd_listener, func_remove_subset)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar both[128], one[64];\n+\tint ob = put_listener(both, 0, \"tcp\", TEST_PORT);\n+\tint oo = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\n+\tob = put_listener(both, ob, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(both, ob));\n+\tASSERT_EQ(0, listener_set(one, oo));\t\t/* drop udp */\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+}\n+\n+/*\n+ * LISTENER_GET cannot tell a destroyed serv from a live one with no\n+ * permsocks: nfsd_nl_listener_get_doit() replies empty either way. The\n+ * rpcbind client can. nfsd_destroy_serv() is the only path that reaches\n+ * svc_xprt_destroy_all(..., unregister=true) -\u003e svc_rpcb_cleanup() -\u003e\n+ * rpcb_put_local(), which drops the last user and shuts the local client\n+ * down; the next serv then has to connect again. Leaving the serv in place\n+ * would keep the first connection and the stub would see just the one.\n+ */\n+TEST_F(nfsd_listener, func_empty_destroys)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tint conns;\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tconns = rpcb_conns();\n+\tASSERT_GT(conns, 0);\n+\n+\tEXPECT_EQ(0, listener_set(NULL, 0));\t\t/* empty -\u003e destroy serv */\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tEXPECT_GT(rpcb_conns(), conns);\n+}\n+\n+TEST_F(nfsd_listener, func_ipv6)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off, s;\n+\n+\ts = socket(AF_INET6, SOCK_STREAM, 0);\n+\tif (s \u003c 0)\n+\t\tSKIP(return, \"IPv6 unavailable: %s\", strerror(errno));\n+\tclose(s);\n+\n+\toff = put_listener_af(attrs, 0, \"tcp\", AF_INET6, TEST_PORT);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tASSERT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET6, TEST_PORT));\n+}\n+\n+/* ===================== rpcbind registration ===================== */\n+\n+/*\n+ * A rpcbind that refuses the registration takes the listener down with it.\n+ * svc_register() fails, so svc_setup_socket() fails, so no listener is\n+ * created. -EACCES alone does not show that, since a bind can return it\n+ * too, so read the listener set back as well.\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+\trpcb_stub_set_mode(RPCB_STUB_REFUSE);\n+\n+\tEXPECT_EQ(-EACCES, listener_set(attrs, off));\n+\tEXPECT_STRNE(\"\", last_extack);\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+}\n+\n+/*\n+ * A listener that cannot be created reports which one it was: the errno\n+ * alone does not name the entry in a multi-listener request.\n+ */\n+TEST_F(nfsd_listener, sem_create_failure_extack)\n+{\n+\tstruct sockaddr_in s4 = { .sin_family = AF_INET,\n+\t\t\t\t  .sin_port = htons(TEST_PORT),\n+\t\t\t\t  .sin_addr.s_addr = htonl(INADDR_LOOPBACK) };\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[64];\n+\tint off = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tint s;\n+\n+\t/* squat on the port so the listener cannot bind */\n+\ts = socket(AF_INET, SOCK_STREAM, 0);\n+\tASSERT_GE(s, 0);\n+\tASSERT_EQ(0, bind(s, (struct sockaddr *)\u0026s4, sizeof(s4)));\n+\n+\tEXPECT_EQ(-EADDRINUSE, listener_set(attrs, off));\n+\tEXPECT_STRNE(\"\", last_extack);\n+\tEXPECT_EQ(0, listener_get(got, MAX_LISTENERS));\n+\tclose(s);\n+}\n+\n+/* ============ one rpcbind attempt for each request ============ */\n+\n+/*\n+ * Every listener used to register on its own, so a rpcbind that never\n+ * answers cost one timeout for each entry. Ask for one listener, then for\n+ * three, and compare what the stub saw. Three entries must not cost three\n+ * times as much.\n+ *\n+ * The stub has to stay silent rather than refuse. A refusal is an answer,\n+ * and rpcbind refuses one entry at a time, so the count ignores it.\n+ */\n+TEST_F(nfsd_listener, rpcb_stop_after_failure)\n+{\n+\tint before, one, three, off;\n+\tchar attrs[192];\n+\n+\trpcb_stub_set_mode(RPCB_STUB_SILENT);\n+\n+\tbefore = rpcb_calls();\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tlistener_set(attrs, off);\n+\tone = rpcb_calls() - before;\n+\tASSERT_GT(one, 0);\n+\n+\tASSERT_EQ(0, listener_set(attrs, 0));\n+\n+\tbefore = rpcb_calls();\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 1);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 2);\n+\tlistener_set(attrs, off);\n+\tthree = rpcb_calls() - before;\n+\n+\t/* the second and third entries must not reach rpcbind at all */\n+\tEXPECT_LE(three, one);\n+}\n+\n+/*\n+ * The entry that finds rpcbind silent is the one that pays for the\n+ * discovery, and v3 has no vs_rpcb_optnl to discard the error, so it is the\n+ * only entry whose listener would be lost. Nothing distinguishes it from the\n+ * rest of the request, and a retry of the same request would fail the same\n+ * entry again, so the set would stay short for as long as rpcbind was quiet.\n+ *\n+ * Ask for three listeners against a silent stub and require the whole set,\n+ * a success, and a warning that says why.\n+ */\n+TEST_F(nfsd_listener, rpcb_silent_set_complete)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar attrs[192];\n+\tint off;\n+\n+\trpcb_stub_set_mode(RPCB_STUB_SILENT);\n+\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 1);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 2);\n+\tEXPECT_EQ(0, listener_set(attrs, off));\n+\n+\t/* the first entry is not the odd one out */\n+\tEXPECT_EQ(3, listener_get(got, MAX_LISTENERS));\n+\t/* no errno reports this, so the ack has to */\n+\tEXPECT_STRNE(\"\", last_extack);\n+}\n+\n+/*\n+ * The case that needs the count rather than a failed listener. NFSv4 sets\n+ * vs_rpcb_optnl, so svc_generic_rpcbind_set() discards the error, every\n+ * listener comes up, and nothing reports a failure. Without the fix each\n+ * entry still waits for rpcbind on its own.\n+ *\n+ * Make the server v4-only, answer no SET, and require three things: the\n+ * listeners come up, the ack warns that they are not registered, and the\n+ * stub does not see one round trip for each entry.\n+ */\n+TEST_F(nfsd_listener, rpcb_v4_only_bounded)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tint before, one, three, off;\n+\tchar attrs[192];\n+\n+\t/* refuses once a serv exists, so this has to come first */\n+\tASSERT_EQ(0, version_set_only(4, 1));\n+\trpcb_stub_set_mode(RPCB_STUB_SILENT);\n+\n+\tbefore = rpcb_calls();\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tone = rpcb_calls() - before;\n+\tASSERT_GT(one, 0);\n+\n+\t/* start over, so the second measurement also builds a serv */\n+\tASSERT_EQ(0, listener_set(attrs, 0));\n+\n+\tbefore = rpcb_calls();\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 1);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 2);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\tthree = rpcb_calls() - before;\n+\n+\t/* the listeners are up even though rpcbind never answered */\n+\tEXPECT_EQ(3, listener_get(got, MAX_LISTENERS));\n+\t/* and the ack says they are unregistered, since no errno can */\n+\tEXPECT_STRNE(\"\", last_extack);\n+\tEXPECT_LE(three, one);\n+}\n+\n+/*\n+ * The stop applies to one request only. After rpcbind starts answering,\n+ * the next request must register without any other step.\n+ */\n+TEST_F(nfsd_listener, rpcb_retry_next_request)\n+{\n+\tint before, after, off;\n+\tchar attrs[192];\n+\n+\trpcb_stub_set_mode(RPCB_STUB_SILENT);\n+\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 1);\n+\tlistener_set(attrs, off);\n+\tASSERT_EQ(0, listener_set(attrs, 0));\n+\n+\t/* rpcbind recovers */\n+\trpcb_stub_set_mode(RPCB_STUB_ACCEPT);\n+\n+\tbefore = rpcb_calls();\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tEXPECT_EQ(0, listener_set(attrs, off));\n+\tafter = rpcb_calls();\n+\n+\t/* a fresh request starts from a fresh reading and tries again */\n+\tEXPECT_GT(after, before);\n+\tEXPECT_STREQ(\"\", last_extack);\n+}\n+\n+/*\n+ * The same rule on the way out. Removing a listener unregisters it, so a\n+ * rpcbind that stops answering used to cost one timeout for each listener\n+ * removed. Register one listener while the stub answers, silence the stub,\n+ * remove it and count; then do the same with three.\n+ *\n+ * Both measurements also pay the svc_unregister() sweep that\n+ * nfsd_destroy_serv() runs once the last listener is gone, so that cancels\n+ * out of the comparison.\n+ */\n+TEST_F(nfsd_listener, rpcb_unreg_stop_after_failure)\n+{\n+\tint before, one, three, off;\n+\tchar attrs[192];\n+\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\n+\trpcb_stub_set_mode(RPCB_STUB_SILENT);\n+\tbefore = rpcb_calls();\n+\tASSERT_EQ(0, listener_set(NULL, 0));\n+\tone = rpcb_calls() - before;\n+\tASSERT_GT(one, 0);\n+\n+\trpcb_stub_set_mode(RPCB_STUB_ACCEPT);\n+\toff = put_listener(attrs, 0, \"tcp\", TEST_PORT);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 1);\n+\toff = put_listener(attrs, off, \"tcp\", TEST_PORT + 2);\n+\tASSERT_EQ(0, listener_set(attrs, off));\n+\n+\trpcb_stub_set_mode(RPCB_STUB_SILENT);\n+\tbefore = rpcb_calls();\n+\tASSERT_EQ(0, listener_set(NULL, 0));\n+\tthree = rpcb_calls() - before;\n+\n+\t/* the second and third removals must not reach rpcbind at all */\n+\tEXPECT_LE(three, one);\n+}\n+\n+/* ===================== threads / -EBUSY semantics ===================== */\n+\n+TEST_F(nfsd_listener, sem_busy_on_change)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar one[64], two[128];\n+\tint o1 = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\tint o2 = put_listener(two, 0, \"tcp\", TEST_PORT);\n+\n+\to2 = put_listener(two, o2, \"udp\", TEST_PORT);\n+\tASSERT_EQ(0, listener_set(one, o1));\n+\tASSERT_EQ(0, threads_set(1));\t\t\t/* threads now running */\n+\tEXPECT_EQ(-EBUSY, listener_set(two, o2));\t/* add refused */\n+\n+\t/* refused means refused: the udp listener must not have been added */\n+\tEXPECT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+\n+\tthreads_set(0);\t\t\t\t\t/* stop before netns exit */\n+}\n+\n+TEST_F(nfsd_listener, sem_busy_on_remove)\n+{\n+\tstruct listener_ent got[MAX_LISTENERS];\n+\tchar one[64];\n+\tint o1 = put_listener(one, 0, \"tcp\", TEST_PORT);\n+\n+\tASSERT_EQ(0, listener_set(one, o1));\n+\tASSERT_EQ(0, threads_set(1));\n+\tEXPECT_EQ(-EBUSY, listener_set(NULL, 0));\t/* remove refused */\n+\n+\t/* the doit moves the permsocks to a temp list before it can fail */\n+\tEXPECT_EQ(1, listener_get(got, MAX_LISTENERS));\n+\tEXPECT_NE(NULL, find_listener(got, 1, \"tcp\", AF_INET, TEST_PORT));\n+\n+\tthreads_set(0);\n+}\n+\n+TEST_HARNESS_MAIN\ndiff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings\nnew file mode 100644\nindex 0000000000000..6091b45d226ba\n--- /dev/null\n+++ b/tools/testing/selftests/nfsd/settings\n@@ -0,0 +1 @@\n+timeout=120\n"
}

2/1 2026/09/01 00:20 agent patch-evaluator
0m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "NFSD",
    "SUNRPC"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "svc_rpcb_failure_count",
    "svc_generic_rpcbind_set"
  ],
  "Reasoning": "The patch updates the generic netlink interface for NFSD listeners (nfsd_nl_listener_set_doit) by adding bounds checking, unsupported transport validation, and improving error handling when communicating with local rpcbind during listener creation/removal (svc_register, svc_rpcb_failure_count, svc_generic_rpcbind_set). These changes introduce new logic and error handling paths in reachable core kernel subsystems (NFSD and SunRPC) that 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 dcc400252da4944425eeebdaaf6ff74f523cc034
Author: syz-cluster <triage@syzkaller.com>
Date:   Tue Sep 1 00:20:23 2026 +0000

    syz-cluster: applied patch under review

diff --git a/MAINTAINERS b/MAINTAINERS
index 0b42e898f4d8e..30f403f996c2a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14208,6 +14208,7 @@ F:	include/uapi/linux/nfsd/
 F:	include/uapi/linux/sunrpc/
 F:	net/sunrpc/
 F:	tools/net/sunrpc/
+F:	tools/testing/selftests/nfsd/
 
 KERNEL NFSD BLOCK and SCSI LAYOUT DRIVER
 R:	Christoph Hellwig <hch@lst.de>
diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index adb032b7311a3..2d26da12051c5 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,13 @@ 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]))) {
+			NL_SET_ERR_MSG_ATTR(info->extack,
+					    tb[NFSD_A_SOCK_TRANSPORT_NAME],
+					    "unsupported 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;
@@ -2037,8 +2067,12 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 {
 	struct net *net = genl_info_net(info);
+	const struct nlattr *bad_attr = NULL;
 	struct svc_xprt *xprt, *tmp;
+	const char *bad_xprt = NULL;
+	unsigned int rpcb_failures;
 	const struct nlattr *attr;
+	bool skipped_rpcb = false;
 	struct svc_serv *serv;
 	LIST_HEAD(permsocks);
 	struct nfsd_net *nn;
@@ -2128,13 +2162,15 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 	if (delete)
 		svc_xprt_destroy_all(serv, net, false);
 
+	rpcb_failures = svc_rpcb_failure_count(serv);
+
 	/* walk list of addrs again, open any that still don't exist */
 	nlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info->nlhdr,
 				 GENL_HDRLEN, rem) {
 		struct nlattr *tb[NFSD_A_SOCK_MAX + 1];
 		const char *xcl_name;
 		struct sockaddr *sa;
-		int ret;
+		int flags, ret;
 
 		/* validated up front in nfsd_nl_validate_listeners() */
 		if (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,
@@ -2153,11 +2189,46 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 			continue;
 		}
 
-		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,
+		flags = skipped_rpcb ? SVC_SOCK_ANONYMOUS : 0;
+		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, flags,
 					      current_cred());
+
+		if (!skipped_rpcb &&
+		    svc_rpcb_failure_count(serv) != rpcb_failures) {
+			skipped_rpcb = true;
+			if (ret < 0)
+				ret = svc_xprt_create_from_sa(serv, xcl_name,
+							      net, sa,
+							      SVC_SOCK_ANONYMOUS,
+							      current_cred());
+		}
+
 		/* always save the latest error */
-		if (ret < 0)
+		if (ret < 0) {
+			bad_attr = attr;
+			bad_xprt = xcl_name;
 			err = ret;
+		}
+	}
+
+	/*
+	 * The ack carries the errno of the last entry that failed. Point at
+	 * that entry as well, since several entries can share a transport
+	 * name and the errno alone cannot tell them apart.
+	 */
+	if (err) {
+		NL_SET_BAD_ATTR(info->extack, bad_attr);
+		if (skipped_rpcb)
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener; rpcbind did not answer",
+					   bad_xprt);
+		else
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener",
+					   bad_xprt);
+	} else if (skipped_rpcb) {
+		NL_SET_ERR_MSG(info->extack,
+			       "rpcbind did not answer, some listeners are not registered");
 	}
 
 	if (!serv->sv_nrthreads && list_empty(&nn->nfsd_serv->sv_permsocks))
diff --git a/include/linux/sunrpc/clnt.h b/include/linux/sunrpc/clnt.h
index 3c2b8c355ab3a..30344c0d6a9d7 100644
--- a/include/linux/sunrpc/clnt.h
+++ b/include/linux/sunrpc/clnt.h
@@ -199,7 +199,8 @@ struct rpc_xprt	*rpc_task_get_xprt(struct rpc_clnt *clnt,
 
 int		rpcb_create_local(struct net *);
 void		rpcb_put_local(struct net *);
-int		rpcb_register(struct net *, u32, u32, int, unsigned short);
+int		rpcb_register(struct net *net, u32 prog, u32 vers, int prot,
+			      unsigned short port);
 int		rpcb_v4_register(struct net *net, const u32 program,
 				 const u32 version,
 				 const struct sockaddr *address,
diff --git a/include/linux/sunrpc/svc.h b/include/linux/sunrpc/svc.h
index 2db1b9ec5658d..5fa9417e034d7 100644
--- a/include/linux/sunrpc/svc.h
+++ b/include/linux/sunrpc/svc.h
@@ -78,6 +78,7 @@ struct svc_serv {
 	unsigned int		sv_max_payload;	/* datagram payload size */
 	unsigned int		sv_max_mesg;	/* max_payload + 1 page for overheads */
 	unsigned int		sv_xdrsize;	/* XDR buffer size */
+	atomic_t		sv_rpcb_failures; /* unanswered rpcbind calls */
 	struct list_head	sv_permsocks;	/* all permanent sockets */
 	struct list_head	sv_tempsocks;	/* all temporary sockets */
 	int			sv_tmpcnt;	/* count of temporary "valid" sockets */
@@ -451,6 +452,7 @@ int sunrpc_set_pool_mode(const char *val);
 int sunrpc_get_pool_mode(char *val, size_t size);
 void svc_rpcb_cleanup(struct svc_serv *serv, struct net *net);
 int svc_bind(struct svc_serv *serv, struct net *net);
+unsigned int svc_rpcb_failure_count(struct svc_serv *serv);
 struct svc_serv *svc_create(struct svc_program *, unsigned int,
 			    int (*threadfn)(void *data));
 bool		   svc_rqst_replace_page(struct svc_rqst *rqstp,
@@ -471,8 +473,9 @@ unsigned int	   svc_serv_maxthreads(const struct svc_serv *serv);
 int		   svc_pool_stats_open(struct svc_info *si, struct file *file);
 void		   svc_process(struct svc_rqst *rqstp);
 void		   svc_process_bc(struct rpc_rqst *req, struct svc_rqst *rqstp);
-int		   svc_register(const struct svc_serv *, struct net *, const int,
-				const unsigned short, const unsigned short);
+int		   svc_register(struct svc_serv *serv, struct net *net,
+				const int family, const unsigned short proto,
+				const unsigned short port);
 
 void		   svc_wake_up(struct svc_serv *);
 void		   svc_reserve(struct svc_rqst *rqstp, int space);
diff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c
index 4c0b7fefee4e2..8b9621129115f 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;
@@ -400,7 +412,8 @@ static struct rpc_clnt *rpcb_create(struct net *net, const char *nodename,
 	return rpc_create(&args);
 }
 
-static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt, struct rpc_message *msg, bool is_set)
+static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt,
+			      struct rpc_message *msg, bool is_set)
 {
 	int flags = RPC_TASK_NOCONNECT;
 	int error, result = 0;
@@ -410,8 +423,22 @@ static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt, stru
 	msg->rpc_resp = &result;
 
 	error = rpc_call_sync(clnt, msg, flags);
-	if (error < 0)
-		return error;
+	if (error < 0) {
+		switch (error) {
+		/* rpcbind answered; the reply itself carries the error */
+		case -EPROTONOSUPPORT:
+		case -EPFNOSUPPORT:
+		case -EOPNOTSUPP:
+		case -EACCES:
+		/* the call never made it onto the wire */
+		case -ENOMEM:
+		case -EMSGSIZE:
+		case -ERESTARTSYS:
+			return error;
+		}
+		/* anything else, we assume that rpcbind isn't functional */
+		return -EIO;
+	}
 
 	if (!result)
 		return -EACCES;
diff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c
index 8297bad2b1777..c00ae00b6a121 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1179,10 +1179,40 @@ int svc_generic_rpcbind_set(struct net *net,
 	error = svc_rpcbind_set_version(net, progp, version,
 					family, proto, port);
 
+	/* -EIO means no answer, not a refusal, so vs_rpcb_optnl must keep it. */
+	if (error == -EIO)
+		return error;
+
 	return (vers->vs_rpcb_optnl) ? 0 : error;
 }
 EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
 
+/**
+ * svc_rpcb_failure_count - local rpcbind calls for @serv that got no answer
+ * @serv: RPC service to query
+ *
+ * svc_register() adds one for each of its calls that got no answer. A reply
+ * that refuses one entry does not count, because rpcbind answered and the
+ * next entry may still succeed.
+ *
+ * The count is kept per serv rather than per net. The local rpcbind client
+ * is per-net and lockd shares it, but a count that another service can move
+ * says nothing about this serv's own calls.
+ *
+ * This is for callers that cannot see the svc_register() return, because a
+ * transport class sits in between. Such a caller reads the count before it
+ * starts and compares as it goes, so there is no state to reset between
+ * operations. The count never resets, and callers must not attach meaning
+ * to the value itself.
+ *
+ * Return: the number of unanswered calls since this serv was created.
+ */
+unsigned int svc_rpcb_failure_count(struct svc_serv *serv)
+{
+	return atomic_read(&serv->sv_rpcb_failures);
+}
+EXPORT_SYMBOL_GPL(svc_rpcb_failure_count);
+
 /**
  * svc_register - register an RPC service with the local portmapper
  * @serv: svc_serv struct for the service to register
@@ -1193,10 +1223,11 @@ EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
  *
  * Service is registered for any address in the passed-in protocol family
  */
-int svc_register(const struct svc_serv *serv, struct net *net,
+int svc_register(struct svc_serv *serv, struct net *net,
 		 const int family, const unsigned short proto,
 		 const unsigned short port)
 {
+	bool			noanswer = false;
 	unsigned int		p, i;
 	int			error = 0;
 
@@ -1208,18 +1239,34 @@ 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++) {
+			const struct svc_version *vers = progp->pg_vers[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 == -EIO) {
+				noanswer = true;
+				if (vers && vers->vs_rpcb_optnl)
+					ret = 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;
 			}
 		}
+
+		/* Give up on trying to register anything if it didn't respond */
+		if (noanswer)
+			break;
 	}
 
+	if (noanswer)
+		atomic_inc(&serv->sv_rpcb_failures);
+
 	return error;
 }
 
@@ -1230,8 +1277,8 @@ int svc_register(const struct svc_serv *serv, struct net *net,
  * any "inet6" entries anyway.  So a PMAP_UNSET should be sufficient
  * in this case to clear all existing entries for [program, version].
  */
-static void __svc_unregister(struct net *net, const u32 program, const u32 version,
-			     const char *progname)
+static int __svc_unregister(struct net *net, const u32 program, const u32 version,
+			    const char *progname)
 {
 	int error;
 
@@ -1245,6 +1292,7 @@ static void __svc_unregister(struct net *net, const u32 program, const u32 versi
 		error = rpcb_register(net, program, version, 0, 0);
 
 	trace_svc_unregister(progname, version, error);
+	return error;
 }
 
 /*
@@ -1271,10 +1319,13 @@ static void svc_unregister(const struct svc_serv *serv, struct net *net)
 				continue;
 			if (progp->pg_vers[i]->vs_hidden)
 				continue;
-			__svc_unregister(net, progp->pg_prog, i, progp->pg_name);
+			if (__svc_unregister(net, progp->pg_prog, i,
+					     progp->pg_name) == -EIO)
+				goto out;
 		}
 	}
 
+out:
 	rcu_read_lock();
 	sighand = rcu_dereference(current->sighand);
 	spin_lock_irqsave(&sighand->siglock, flags);
diff --git a/net/sunrpc/svc_xprt.c b/net/sunrpc/svc_xprt.c
index 40040af588fb2..7e471c92f23ab 100644
--- a/net/sunrpc/svc_xprt.c
+++ b/net/sunrpc/svc_xprt.c
@@ -1101,6 +1101,22 @@ static void call_xpt_users(struct svc_xprt *xprt)
 	spin_unlock(&xprt->xpt_lock);
 }
 
+/*
+ * If rpcbind stops answering, every listener still to be destroyed would
+ * only wait out the same timeout again. Drop the flag on all of the
+ * remaining listeners.
+ */
+static void svc_xprt_clear_rpcb_unreg(struct svc_serv *serv, struct net *net)
+{
+	struct svc_xprt *xprt;
+
+	spin_lock_bh(&serv->sv_lock);
+	list_for_each_entry(xprt, &serv->sv_permsocks, xpt_list)
+		if (xprt->xpt_net == net)
+			clear_bit(XPT_RPCB_UNREG, &xprt->xpt_flags);
+	spin_unlock_bh(&serv->sv_lock);
+}
+
 /*
  * Remove a dead transport
  */
@@ -1115,11 +1131,15 @@ static void svc_delete_xprt(struct svc_xprt *xprt)
 		struct svc_sock *svsk = container_of(xprt, struct svc_sock,
 						     sk_xprt);
 		struct socket *sock = svsk->sk_sock;
+		unsigned int failures = svc_rpcb_failure_count(serv);
 
 		if (svc_register(serv, xprt->xpt_net, sock->sk->sk_family,
 				 sock->sk->sk_protocol, 0) < 0)
 			pr_warn("failed to unregister %s with rpcbind\n",
 				xprt->xpt_class->xcl_name);
+
+		if (svc_rpcb_failure_count(serv) != failures)
+			svc_xprt_clear_rpcb_unreg(serv, xprt->xpt_net);
 	}
 
 	if (test_and_set_bit(XPT_DEAD, &xprt->xpt_flags))
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 2d960626750e3..d881ba39cad40 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -90,6 +90,7 @@ TARGETS += net/packetdrill
 TARGETS += net/ppp
 TARGETS += net/rds
 TARGETS += net/tcp_ao
+TARGETS += nfsd
 TARGETS += nolibc
 TARGETS += pci_endpoint
 TARGETS += pcie_bwctrl
diff --git a/tools/testing/selftests/nfsd/.gitignore b/tools/testing/selftests/nfsd/.gitignore
new file mode 100644
index 0000000000000..19e6dec04d8e9
--- /dev/null
+++ b/tools/testing/selftests/nfsd/.gitignore
@@ -0,0 +1 @@
+nfsd_netlink_listener
diff --git a/tools/testing/selftests/nfsd/Makefile b/tools/testing/selftests/nfsd/Makefile
new file mode 100644
index 0000000000000..15ac65549d259
--- /dev/null
+++ b/tools/testing/selftests/nfsd/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += $(KHDR_INCLUDES) -Wall
+
+TEST_GEN_PROGS := nfsd_netlink_listener
+
+include ../lib.mk
diff --git a/tools/testing/selftests/nfsd/config b/tools/testing/selftests/nfsd/config
new file mode 100644
index 0000000000000..ab84523fbedf3
--- /dev/null
+++ b/tools/testing/selftests/nfsd/config
@@ -0,0 +1,8 @@
+CONFIG_NAMESPACES=y
+CONFIG_NET_NS=y
+CONFIG_SHMEM=y
+CONFIG_TMPFS=y
+CONFIG_UNIX=y
+CONFIG_IPV6=y
+CONFIG_NFSD=y
+CONFIG_NFSD_V4=y
diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
new file mode 100644
index 0000000000000..89d1da825b954
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -0,0 +1,1328 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression tests for the NFSD generic-netlink listener interface
+ * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).
+ *
+ * Three groups:
+ *   validation  - malformed/abusive LISTENER_SET requests are rejected by
+ *                 nfsd_nl_validate_listeners(), before nfsd_mutex is taken.
+ *   functional  - create/add/remove listeners and verify LISTENER_GET
+ *                 reflects the set (round-trip of transport + addr:port).
+ *   semantics   - once threads are running (THREADS_SET) a listener change
+ *                 is refused with -EBUSY.
+ *
+ * Each test runs in its own private net + mount namespace (unshare in
+ * FIXTURE_SETUP). /run is masked there: a pathname AF_LOCAL connect is not
+ * scoped by the network namespace, since unix_find_bsd() resolves by inode
+ * and takes no struct net, so the kernel's rpcbind client would otherwise be
+ * able to reach the rpcbind running on the host. Anything that creates a
+ * serv is served by the per-netns rpcbind stub below instead.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <poll.h>
+#include <sched.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/netlink.h>
+#include <linux/genetlink.h>
+
+#include "../kselftest_harness.h"
+
+/* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */
+#define NFSD_FAMILY_NAME		"nfsd"
+#define NFSD_CMD_THREADS_SET		2
+#define NFSD_CMD_VERSION_SET		4
+#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 NFSD_A_SERVER_PROTO_VERSION	1	/* per-version nest */
+#define NFSD_A_VERSION_MAJOR		1	/* inside the version nest */
+#define NFSD_A_VERSION_MINOR		2	/* inside the version nest */
+#define NFSD_A_VERSION_ENABLED		3	/* inside the version 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 = -1;		/* set per-test in FIXTURE_SETUP */
+
+/* Extack message from the last genl_request(); empty if there was none. */
+static char last_extack[128];
+
+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);
+	int on = 1;
+
+	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));
+	/*
+	 * Ask for extack, and cap the ack so the request is not echoed back:
+	 * the TLVs then always follow the fixed part of the error message.
+	 */
+	setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &on, sizeof(on));
+	setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
+	return fd;
+}
+
+/* Stash the extack message of an ack, if it carries one. */
+static void parse_extack(const char *rbuf)
+{
+	const struct nlmsghdr *nlh = (const void *)rbuf;
+	const struct nlattr *na;
+	int off, left;
+
+	last_extack[0] = '\0';
+	if (nlh->nlmsg_type != NLMSG_ERROR ||
+	    !(nlh->nlmsg_flags & NLM_F_ACK_TLVS))
+		return;
+
+	off = NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(struct nlmsgerr));
+	left = nlh->nlmsg_len - off;
+	na = (const void *)(rbuf + off);
+
+	while (left >= (int)NLA_HDRLEN) {
+		if ((na->nla_type & NLA_TYPE_MASK) == NLMSGERR_ATTR_MSG) {
+			strncpy(last_extack, (const char *)na + NLA_HDRLEN,
+				sizeof(last_extack) - 1);
+			last_extack[sizeof(last_extack) - 1] = '\0';
+			return;
+		}
+		left -= NLA_ALIGN4(na->nla_len);
+		na = (const void *)((const char *)na + NLA_ALIGN4(na->nla_len));
+	}
+}
+
+/* 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)");
+
+	last_extack[0] = '\0';
+	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;
+		parse_extack(rbuf);
+	} 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);
+}
+
+/*
+ * Enable exactly one NFS version in this netns. NFSD_CMD_VERSION_SET clears
+ * every version first, so one nest is enough to leave the server v4-only.
+ * It refuses once a serv exists, so call it before any listener.
+ */
+static int version_set_only(uint32_t major, uint32_t minor)
+{
+	char attrs[64];
+	struct nlattr *nest = (void *)attrs;
+	int inner = NLA_HDRLEN;
+
+	inner = put_attr(attrs, inner, NFSD_A_VERSION_MAJOR,
+			 &major, sizeof(major));
+	inner = put_attr(attrs, inner, NFSD_A_VERSION_MINOR,
+			 &minor, sizeof(minor));
+	inner = put_attr(attrs, inner, NFSD_A_VERSION_ENABLED, NULL, 0);
+	nest->nla_type = NFSD_A_SERVER_PROTO_VERSION | NLA_F_NESTED;
+	nest->nla_len = inner;
+
+	return genl_request(NFSD_CMD_VERSION_SET, attrs, NLA_ALIGN4(inner));
+}
+
+/* Fetch the current listeners; returns count (>=0) or -errno. */
+static int listener_get(struct listener_ent *out, int max)
+{
+	char rbuf[8192];
+	int n = genl_request_reply(NFSD_CMD_LISTENER_GET, rbuf, sizeof(rbuf));
+
+	if (n < 0)
+		return n;
+	return parse_listener_get(rbuf, n, out, max);
+}
+
+/*
+ * Every listener these tests create comes from put_listener_af(), so the
+ * address is always loopback. Match on it too: without that, a reply that
+ * gave the right transport and port on the wrong address (0.0.0.0, say)
+ * would pass.
+ */
+static struct listener_ent *find_listener(struct listener_ent *e, int n,
+					  const char *xprt, int family,
+					  uint16_t port)
+{
+	int i;
+
+	for (i = 0; i < n; i++) {
+		if (e[i].family != family || e[i].port != port ||
+		    strcmp(e[i].xprt, xprt))
+			continue;
+		if (family == AF_INET6) {
+			if (memcmp(&e[i].a6, &in6addr_loopback, sizeof(e[i].a6)))
+				continue;
+		} else if (e[i].a4.s_addr != htonl(INADDR_LOOPBACK)) {
+			continue;
+		}
+		return &e[i];
+	}
+	return NULL;
+}
+
+/* Start (@n > 0) or stop (@n == 0) nfsd threads in this netns. */
+static int threads_set(int n)
+{
+	char attrs[64];
+	uint32_t v = n;
+	int off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, &v, sizeof(v));
+
+	return genl_request(NFSD_CMD_THREADS_SET, attrs, off);
+}
+
+/* ------------------- per-netns local rpcbind stub ------------------- */
+
+/*
+ * Creating a listener registers with rpcbind: nfsd_nl_listener_set_doit()
+ * passes no SVC_SOCK_ANONYMOUS for the first entry of a request, so
+ * pmap_register is true in svc_setup_socket(). The fixture's server has v3
+ * enabled, and nfsd_version3 does not set vs_rpcb_optnl, so a failure there
+ * comes back out of svc_register() and takes the listener down with it.
+ * With nothing listening, every attempt first waits out the local rpcbind
+ * timeout. 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 leave every IPv6 listener unregistered.
+ *
+ * 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.
+ *
+ * In RPCB_STUB_SILENT mode a SET or an UNSET is read and nothing is written
+ * back, so the kernel waits out its own timeout. That is the only mode that
+ * makes rpcb_register_call() report a call that got no answer, which is what
+ * the per-net failure count records. The NULL procedure is still answered:
+ * rpcb_create_af_local() builds its client without RPC_CLNT_CREATE_NOPING, so
+ * rpc_create() pings, and a ping that goes unanswered drops the kernel onto
+ * the loopback rpcb_create_local_net() client, which never reaches this stub.
+ *
+ * The stub also keeps counters and the mode in a page shared with the test, so
+ * a test can assert that the kernel never talked to rpcbind at all, or that it
+ * dropped the local rpcbind client and had to reconnect.
+ *
+ * The mode lives there rather than in the child so that a test can change it
+ * with a serv already up. Killing and restarting the stub would close the
+ * connection the kernel holds, and rpcb_register_call() issues UNSET over
+ * AF_LOCAL with RPC_TASK_NOCONNECT, so the next call would fail at once with
+ * -ENOTCONN instead of waiting out a timeout.
+ */
+#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, RPCB_STUB_SILENT };
+
+struct rpcb_stub_stats {
+	unsigned int conns;		/* connections accepted */
+	unsigned int calls;		/* calls received */
+	unsigned int mode;		/* RPCB_STUB_*, read on every call */
+};
+
+static volatile struct rpcb_stub_stats *rpcb_stats;	/* MAP_SHARED */
+
+static int rpcb_stats_alloc(void)
+{
+	void *p = mmap(NULL, sizeof(*rpcb_stats), PROT_READ | PROT_WRITE,
+		       MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+	if (p == MAP_FAILED)
+		return -1;
+	rpcb_stats = p;
+	return 0;
+}
+
+/*
+ * The stub bumps these before it replies and the kernel waits for that reply,
+ * so whatever a netlink request provoked is visible once it returns.
+ */
+static int rpcb_calls(void)
+{
+	return rpcb_stats ? (int)rpcb_stats->calls : 0;
+}
+
+static int rpcb_conns(void)
+{
+	return rpcb_stats ? (int)rpcb_stats->conns : 0;
+}
+
+/* Takes effect on the stub's next call; the caller has not sent one yet. */
+static void rpcb_stub_set_mode(int mode)
+{
+	rpcb_stats->mode = mode;
+}
+
+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)
+{
+	unsigned int len, nrep = 6, mode = rpcb_stats->mode;
+	uint32_t mark, call[6], rep[7];
+	size_t replen;
+
+	if (rpcb_stub_read(fd, &mark, sizeof(mark)))
+		return -1;
+	len = ntohl(mark) & 0x7fffffff;
+	if (len < sizeof(call) || len > 4096)
+		return -1;
+	if (rpcb_stub_read(fd, call, sizeof(call)))
+		return -1;
+
+	/* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */
+	for (len -= sizeof(call); len; ) {
+		char sink[256];
+		unsigned int n = len > sizeof(sink) ? sizeof(sink) : len;
+
+		if (rpcb_stub_read(fd, sink, n))
+			return -1;
+		len -= n;
+	}
+
+	if (rpcb_stats)
+		rpcb_stats->calls++;
+
+	rep[0] = call[0];		/* xid */
+	rep[1] = htonl(1);		/* REPLY */
+	rep[2] = htonl(0);		/* MSG_ACCEPTED */
+	rep[3] = htonl(0);		/* verifier flavor AUTH_NULL */
+	rep[4] = htonl(0);		/* verifier length */
+	rep[5] = htonl(0);		/* SUCCESS */
+
+	if (ntohl(call[3]) != RPCB_PROGRAM) {
+		rep[5] = htonl(1);	/* PROG_UNAVAIL */
+	} else {
+		unsigned int proc = ntohl(call[5]);
+
+		switch (proc) {
+		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 */
+		}
+
+		/*
+		 * Answer nothing, so the caller waits out its timeout. The
+		 * NULL procedure is answered even here: the kernel pings at
+		 * client creation, and a ping with no answer takes it off
+		 * this socket entirely.
+		 */
+		if (mode == RPCB_STUB_SILENT && proc != RPCB_PROC_NULL)
+			return 0;
+	}
+
+	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)
+{
+	struct pollfd pfd[1 + RPCB_STUB_MAXCONN];
+	nfds_t n = 1, i;
+
+	pfd[0].fd = lfd;
+
+	for (;;) {
+		/* stop polling the listener when full, or poll() spins */
+		pfd[0].events = n < 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;
+
+		if (poll(pfd, n, -1) < 0)
+			return;
+
+		if (pfd[0].revents & POLLIN) {
+			int c = accept(lfd, NULL, NULL);
+
+			if (c >= 0) {
+				pfd[n].fd = c;
+				pfd[n].events = POLLIN;
+				/*
+				 * poll() ran with the old n, so it did not
+				 * write this revents. The loop below reads it.
+				 */
+				pfd[n].revents = 0;
+				n++;
+				if (rpcb_stats)
+					rpcb_stats->conns++;
+			}
+		}
+
+		for (i = 1; i < n; i++) {
+			if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR)))
+				continue;
+			if (rpcb_stub_call(pfd[i].fd)) {
+				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;
+
+	rpcb_stats->mode = mode;
+
+	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);
+		_exit(0);
+	}
+
+	close(lfd);
+	return pid;
+}
+
+/* --------------------------- fixture --------------------------- */
+
+FIXTURE(nfsd_listener) {
+	pid_t rpcbd;
+};
+
+FIXTURE_SETUP(nfsd_listener)
+{
+	struct ifreq ifr = {0};
+	struct stat st;
+	int s;
+
+	if (geteuid() != 0)
+		SKIP(return, "must be run as root");
+	if (unshare(CLONE_NEWNET | CLONE_NEWNS) < 0)
+		SKIP(return, "unshare(NEWNET|NEWNS): %s", strerror(errno));
+	if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
+		SKIP(return, "mount(/ private): %s", strerror(errno));
+
+	/*
+	 * Keep the kernel's rpcbind client inside this namespace. The
+	 * abstract socket it tries first is per-netns, but the
+	 * "/var/run/rpcbind.sock" fallback is not, so hide the path.
+	 */
+	if (mount("tmpfs", "/run", "tmpfs", 0, NULL) < 0)
+		SKIP(return, "mount(tmpfs on /run): %s", strerror(errno));
+	if (lstat("/var/run", &st) == 0 && S_ISDIR(st.st_mode) &&
+	    mount("tmpfs", "/var/run", "tmpfs", 0, NULL) < 0)
+		SKIP(return, "mount(tmpfs on /var/run): %s", strerror(errno));
+
+	/* Bring loopback up so listener binds (127.0.0.1 / ::1) work. */
+	s = socket(AF_INET, SOCK_DGRAM, 0);
+	ASSERT_GE(s, 0);
+	strcpy(ifr.ifr_name, "lo");
+	ASSERT_EQ(0, ioctl(s, SIOCGIFFLAGS, &ifr));
+	ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
+	ASSERT_EQ(0, ioctl(s, SIOCSIFFLAGS, &ifr));
+	close(s);
+
+	nfsd_family = genl_resolve_nfsd();
+	if (nfsd_family < 0)
+		SKIP(return, "nfsd genl family not found (modprobe nfsd?)");
+
+	if (rpcb_stats_alloc() < 0)
+		SKIP(return, "mmap(rpcbind stub counters): %s", strerror(errno));
+
+	self->rpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);
+	if (self->rpcbd < 0)
+		SKIP(return, "cannot start the rpcbind stub: %s",
+		     strerror(errno));
+}
+
+FIXTURE_TEARDOWN(nfsd_listener)
+{
+	/*
+	 * A listener holds a reference to this netns, which outlives the test
+	 * process, so anything still up leaks it. Threads pin the listeners in
+	 * turn; dropping them destroys the serv and everything under it.
+	 */
+	if (nfsd_family >= 0 && listener_set(NULL, 0) == -EBUSY)
+		threads_set(0);
+
+	if (self->rpcbd > 0) {
+		kill(self->rpcbd, SIGKILL);
+		waitpid(self->rpcbd, NULL, 0);
+	}
+	if (rpcb_stats) {
+		munmap((void *)rpcb_stats, sizeof(*rpcb_stats));
+		rpcb_stats = NULL;
+	}
+}
+
+/* ===================== validation / negative ===================== */
+
+TEST_F(nfsd_listener, val_empty_list_ok)
+{
+	EXPECT_EQ(0, listener_set(NULL, 0));
+}
+
+TEST_F(nfsd_listener, val_too_many)
+{
+	static char attrs[1 << 20];
+	int i, off = 0;
+
+	for (i = 0; i < 1025; i++)		/* > NFSD_NL_LISTENER_MAX (1024) */
+		off = put_listener(attrs, off, "udp", TEST_PORT);
+	EXPECT_EQ(-E2BIG, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_addr)
+{
+	char attrs[64];
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 0 };
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_transport)
+{
+	struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+	struct raw_listener r = { .xprt = NULL, .emit_addr = 1,
+				  .addr = &s4, .addr_len = sizeof(s4) };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+/*
+ * A name matching no transport class must be refused before nfsd_mutex is
+ * taken, so it never reaches svc_xprt_create_from_sa() and its
+ * request_module("svc%s", name) upcall.
+ *
+ * The errno cannot show that -- svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. The rpcbind traffic can:
+ * getting that far means nfsd_create_serv() ran, and svc_bind() pings
+ * rpcbind at client creation and then sweeps stale entries with
+ * svc_unregister(). A silent stub is the proof nothing was created.
+ */
+TEST_F(nfsd_listener, val_bad_transport)
+{
+	char attrs[64];
+	int off = put_listener(attrs, 0, "bogus_xprt", TEST_PORT);
+
+	ASSERT_EQ(0, rpcb_calls());
+	EXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));
+	EXPECT_EQ(0, rpcb_calls());
+}
+
+TEST_F(nfsd_listener, val_addr_too_short)
+{
+	unsigned char tiny = 0;
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1,
+				  .addr = &tiny, .addr_len = 1 };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet_short)
+{
+	struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s4,
+				  .addr_len = sizeof(sa_family_t) + 2 };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet6_short)
+{
+	struct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) };
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s6,
+				  .addr_len = sizeof(struct sockaddr_in) };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_bad_family)
+{
+	struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+				  .addr_len = sizeof(struct sockaddr_in) };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_second_entry_bad)
+{
+	struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+	struct raw_listener bad = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+				    .addr_len = sizeof(struct sockaddr_in) };
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[128];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	off = put_raw_listener(attrs, off, &bad);
+	/* The whole request is rejected during validation; nothing applied. */
+	EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+	/*
+	 * Again the errno alone does not say so: svc_xprt_create_from_sa()
+	 * also returns -EAFNOSUPPORT, and the doit keeps the listeners it did
+	 * manage to create, so the well-formed tcp entry ahead of the bad one
+	 * would still be up.
+	 */
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/*
+ * A rejected request must leave the listeners that are already up alone.
+ * The errno alone does not show that: svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. What differs is how far the
+ * request gets -- without the check in nfsd_nl_validate_listeners(),
+ * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener
+ * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the
+ * name fails.
+ */
+TEST_F(nfsd_listener, val_reject_keeps_listeners)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char good[64], bad[64];
+	int og = put_listener(good, 0, "tcp", TEST_PORT);
+	int ob = put_listener(bad, 0, "bogus_xprt", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(good, og));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+
+	EXPECT_EQ(-EPROTONOSUPPORT, listener_set(bad, ob));
+
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+/* ===================== functional / round-trip ===================== */
+
+/* LISTENER_GET with no serv in this netns returns an empty list. */
+TEST_F(nfsd_listener, func_get_empty)
+{
+	struct listener_ent got[MAX_LISTENERS];
+
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+TEST_F(nfsd_listener, func_create_tcp)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_STREQ("", last_extack);		/* nothing to warn about */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_udp)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "udp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_multi)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[128];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	off = put_listener(attrs, off, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+	EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_idempotent)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_EQ(0, listener_set(attrs, off));		/* re-set same list */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_add)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64], two[128];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+	int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+	o2 = put_listener(two, o2, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, listener_set(two, o2));		/* add udp, keep tcp */
+	ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+	EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_remove_subset)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char both[128], one[64];
+	int ob = put_listener(both, 0, "tcp", TEST_PORT);
+	int oo = put_listener(one, 0, "tcp", TEST_PORT);
+
+	ob = put_listener(both, ob, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(both, ob));
+	ASSERT_EQ(0, listener_set(one, oo));		/* drop udp */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+/*
+ * LISTENER_GET cannot tell a destroyed serv from a live one with no
+ * permsocks: nfsd_nl_listener_get_doit() replies empty either way. The
+ * rpcbind client can. nfsd_destroy_serv() is the only path that reaches
+ * svc_xprt_destroy_all(..., unregister=true) -> svc_rpcb_cleanup() ->
+ * rpcb_put_local(), which drops the last user and shuts the local client
+ * down; the next serv then has to connect again. Leaving the serv in place
+ * would keep the first connection and the stub would see just the one.
+ */
+TEST_F(nfsd_listener, func_empty_destroys)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	int conns;
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	conns = rpcb_conns();
+	ASSERT_GT(conns, 0);
+
+	EXPECT_EQ(0, listener_set(NULL, 0));		/* empty -> destroy serv */
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_GT(rpcb_conns(), conns);
+}
+
+TEST_F(nfsd_listener, func_ipv6)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off, s;
+
+	s = socket(AF_INET6, SOCK_STREAM, 0);
+	if (s < 0)
+		SKIP(return, "IPv6 unavailable: %s", strerror(errno));
+	close(s);
+
+	off = put_listener_af(attrs, 0, "tcp", AF_INET6, TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET6, TEST_PORT));
+}
+
+/* ===================== rpcbind registration ===================== */
+
+/*
+ * A rpcbind that refuses the registration takes the listener down with it.
+ * svc_register() fails, so svc_setup_socket() fails, so no listener is
+ * created. -EACCES alone does not show that, since a bind can return it
+ * too, so read the listener set back as well.
+ */
+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);
+
+	rpcb_stub_set_mode(RPCB_STUB_REFUSE);
+
+	EXPECT_EQ(-EACCES, listener_set(attrs, off));
+	EXPECT_STRNE("", last_extack);
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/*
+ * A listener that cannot be created reports which one it was: the errno
+ * alone does not name the entry in a multi-listener request.
+ */
+TEST_F(nfsd_listener, sem_create_failure_extack)
+{
+	struct sockaddr_in s4 = { .sin_family = AF_INET,
+				  .sin_port = htons(TEST_PORT),
+				  .sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	int s;
+
+	/* squat on the port so the listener cannot bind */
+	s = socket(AF_INET, SOCK_STREAM, 0);
+	ASSERT_GE(s, 0);
+	ASSERT_EQ(0, bind(s, (struct sockaddr *)&s4, sizeof(s4)));
+
+	EXPECT_EQ(-EADDRINUSE, listener_set(attrs, off));
+	EXPECT_STRNE("", last_extack);
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+	close(s);
+}
+
+/* ============ one rpcbind attempt for each request ============ */
+
+/*
+ * Every listener used to register on its own, so a rpcbind that never
+ * answers cost one timeout for each entry. Ask for one listener, then for
+ * three, and compare what the stub saw. Three entries must not cost three
+ * times as much.
+ *
+ * The stub has to stay silent rather than refuse. A refusal is an answer,
+ * and rpcbind refuses one entry at a time, so the count ignores it.
+ */
+TEST_F(nfsd_listener, rpcb_stop_after_failure)
+{
+	int before, one, three, off;
+	char attrs[192];
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	listener_set(attrs, off);
+	one = rpcb_calls() - before;
+	ASSERT_GT(one, 0);
+
+	ASSERT_EQ(0, listener_set(attrs, 0));
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	listener_set(attrs, off);
+	three = rpcb_calls() - before;
+
+	/* the second and third entries must not reach rpcbind at all */
+	EXPECT_LE(three, one);
+}
+
+/*
+ * The entry that finds rpcbind silent is the one that pays for the
+ * discovery, and v3 has no vs_rpcb_optnl to discard the error, so it is the
+ * only entry whose listener would be lost. Nothing distinguishes it from the
+ * rest of the request, and a retry of the same request would fail the same
+ * entry again, so the set would stay short for as long as rpcbind was quiet.
+ *
+ * Ask for three listeners against a silent stub and require the whole set,
+ * a success, and a warning that says why.
+ */
+TEST_F(nfsd_listener, rpcb_silent_set_complete)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[192];
+	int off;
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	EXPECT_EQ(0, listener_set(attrs, off));
+
+	/* the first entry is not the odd one out */
+	EXPECT_EQ(3, listener_get(got, MAX_LISTENERS));
+	/* no errno reports this, so the ack has to */
+	EXPECT_STRNE("", last_extack);
+}
+
+/*
+ * The case that needs the count rather than a failed listener. NFSv4 sets
+ * vs_rpcb_optnl, so svc_generic_rpcbind_set() discards the error, every
+ * listener comes up, and nothing reports a failure. Without the fix each
+ * entry still waits for rpcbind on its own.
+ *
+ * Make the server v4-only, answer no SET, and require three things: the
+ * listeners come up, the ack warns that they are not registered, and the
+ * stub does not see one round trip for each entry.
+ */
+TEST_F(nfsd_listener, rpcb_v4_only_bounded)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	int before, one, three, off;
+	char attrs[192];
+
+	/* refuses once a serv exists, so this has to come first */
+	ASSERT_EQ(0, version_set_only(4, 1));
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	one = rpcb_calls() - before;
+	ASSERT_GT(one, 0);
+
+	/* start over, so the second measurement also builds a serv */
+	ASSERT_EQ(0, listener_set(attrs, 0));
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	three = rpcb_calls() - before;
+
+	/* the listeners are up even though rpcbind never answered */
+	EXPECT_EQ(3, listener_get(got, MAX_LISTENERS));
+	/* and the ack says they are unregistered, since no errno can */
+	EXPECT_STRNE("", last_extack);
+	EXPECT_LE(three, one);
+}
+
+/*
+ * The stop applies to one request only. After rpcbind starts answering,
+ * the next request must register without any other step.
+ */
+TEST_F(nfsd_listener, rpcb_retry_next_request)
+{
+	int before, after, off;
+	char attrs[192];
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	listener_set(attrs, off);
+	ASSERT_EQ(0, listener_set(attrs, 0));
+
+	/* rpcbind recovers */
+	rpcb_stub_set_mode(RPCB_STUB_ACCEPT);
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	EXPECT_EQ(0, listener_set(attrs, off));
+	after = rpcb_calls();
+
+	/* a fresh request starts from a fresh reading and tries again */
+	EXPECT_GT(after, before);
+	EXPECT_STREQ("", last_extack);
+}
+
+/*
+ * The same rule on the way out. Removing a listener unregisters it, so a
+ * rpcbind that stops answering used to cost one timeout for each listener
+ * removed. Register one listener while the stub answers, silence the stub,
+ * remove it and count; then do the same with three.
+ *
+ * Both measurements also pay the svc_unregister() sweep that
+ * nfsd_destroy_serv() runs once the last listener is gone, so that cancels
+ * out of the comparison.
+ */
+TEST_F(nfsd_listener, rpcb_unreg_stop_after_failure)
+{
+	int before, one, three, off;
+	char attrs[192];
+
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+	before = rpcb_calls();
+	ASSERT_EQ(0, listener_set(NULL, 0));
+	one = rpcb_calls() - before;
+	ASSERT_GT(one, 0);
+
+	rpcb_stub_set_mode(RPCB_STUB_ACCEPT);
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	ASSERT_EQ(0, listener_set(attrs, off));
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+	before = rpcb_calls();
+	ASSERT_EQ(0, listener_set(NULL, 0));
+	three = rpcb_calls() - before;
+
+	/* the second and third removals must not reach rpcbind at all */
+	EXPECT_LE(three, one);
+}
+
+/* ===================== threads / -EBUSY semantics ===================== */
+
+TEST_F(nfsd_listener, sem_busy_on_change)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64], two[128];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+	int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+	o2 = put_listener(two, o2, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, threads_set(1));			/* threads now running */
+	EXPECT_EQ(-EBUSY, listener_set(two, o2));	/* add refused */
+
+	/* refused means refused: the udp listener must not have been added */
+	EXPECT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+
+	threads_set(0);					/* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, threads_set(1));
+	EXPECT_EQ(-EBUSY, listener_set(NULL, 0));	/* remove refused */
+
+	/* the doit moves the permsocks to a temp list before it can fail */
+	EXPECT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+
+	threads_set(0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings
new file mode 100644
index 0000000000000..6091b45d226ba
--- /dev/null
+++ b/tools/testing/selftests/nfsd/settings
@@ -0,0 +1 @@
+timeout=120

3/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 25607
							output: 28
							thoughts: 456
						

Thoughts:
**Fuzzing Feasibility Analysis: NFSD Netlink and RPC Registration Patch**

Okay, let's break this down. First, I need to figure out if this patch introduces functional changes that warrant fuzzing. A quick scan of the modified files reveals a few key areas of interest. The `MAINTAINERS` file update is likely just documentation, so not super important for fuzzing. However, the changes to `fs/nfsd/nfsctl.c`, specifically around the netlink handler (`nfsd_nl_listener_set_doit`, etc.), immediately catches my eye. This is a potential attack surface.

Then I see updates to sunrpc headers and core RPC registration/deregistration logic in files like `net/sunrpc/rpcb_clnt.c`, `net/sunrpc/svc.c`, and `net/sunrpc/svc_xprt.c`. There's new timeout logic (`rpcb_local_timeout`), updated error handling, and alterations to registration functions (`svc_register`, `rpcb_register_call`). This is good!

The added selftests in the `tools/testing/selftests/...` directory also confirm that the maintainers have provided a way to test their functionality; although, these won't be enough for a good fuzzing campaign.

Essentially, this patch touches core NFSD netlink listener management, sunrpc registration with rpcbind, including error handling and timeouts, and state management during RPC service registration and deregistration. This is all *clearly* reachable kernel logic, including the generic netlink interface, which means it's accessible from user space without special hardware, making it a prime candidate for fuzzing. I can reach these changes without special device access.

Now, let's run the Kconfig checks to make sure everything's enabled and figure out what the symbols look like using the tools. We can start crafting our fuzzer based on those findings.



4/2 2026/09/01 00:20 tool grepper
0m Args:
{
  "Expression": "config NFSD",
  "PathPrefix": "fs/nfsd"
}

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

5/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 27365
							output: 34
							thoughts: 47
						

Thoughts:
Okay, here's my take on those initial thoughts, framed as a summary of my thought process:

**Diving into the NFS Server Configuration**

Alright, before I go any further, I need to get a clear picture of what's *required* for the `fs/nfsd/nfsctl.c` module and its interaction with `net/sunrpc`. That means I should start by looking at the configuration files. My first instinct is to check `fs/nfsd/Kconfig` to identify the relevant kernel configuration options. This will tell me what flags, if enabled, actually build the code I'm interested in and their dependencies. Following that, I'll need to look at `fs/nfsd/Makefile` to see how the build process handles the dependencies, including the relationship with the `net/sunrpc` directory. This should clarify how the components fit together and ensure I understand the build structure and dependencies before proceeding. Understanding the dependencies is going to be the key to understanding the full picture of the system.



6/2 2026/09/01 00:20 tool read-file
0m Args:
{
  "File": "fs/nfsd/Kconfig",
  "FirstLine": 1,
  "LineCount": 40
}

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"
}

7/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 28264
							output: 33
							thoughts: 12
						

Thoughts:
Here's my take on those thoughts:

**Analyzing the NFS Daemon Makefile**

Okay, time to dive into `fs/nfsd/Makefile`. I need to understand the build process and dependencies for the NFS server components. This Makefile is the key, and where everything related to the kernel-side NFS daemon is orchestrated. I’ll be looking for things like:

*   What source files are included?
*   What object files are generated?
*   How are the kernel modules compiled and linked?
*   Are there any specific compiler flags or optimization settings?
*   What dependencies are defined, both within the NFS subsystem and on other kernel components or libraries?
*   Are there any custom build rules or scripts at play?

Knowing this will allow me to track down the source of any issues, contribute to development, or optimize NFSd performance. I know I have a good understanding of what to look for, so let's get started.



8/2 2026/09/01 00:20 tool read-file
0m Args:
{
  "File": "fs/nfsd/Makefile",
  "FirstLine": 1,
  "LineCount": 30
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0\n   2:\t#\n   3:\t# Makefile for the Linux nfs server\n   4:\t#\n   5:\t\n   6:\tccflags-y += -I$(src)\t\t\t# needed for trace events\n   7:\t\n   8:\tobj-$(CONFIG_NFSD)\t+= nfsd.o\n   9:\t\n  10:\t# this one should be compiled first, as the tracing macros can easily blow up\n  11:\tnfsd-y\t\t\t+= trace.o\n  12:\t\n  13:\tnfsd-y \t\t\t+= nfssvc.o nfsctl.o nfsfh.o vfs.o \\\n  14:\t\t\t\t   export.o auth.o lockd.o nfscache.o \\\n  15:\t\t\t\t   stats.o filecache.o nfs3proc.o nfs3xdr.o \\\n  16:\t\t\t\t   netlink.o\n  17:\tnfsd-$(CONFIG_NFSD_V2) += nfsproc.o nfsxdr.o\n  18:\tnfsd-$(CONFIG_NFSD_V2_ACL) += nfs2acl.o\n  19:\tnfsd-$(CONFIG_NFSD_V3_ACL) += nfs3acl.o\n  20:\tnfsd-$(CONFIG_NFSD_V4)\t+= nfs4proc.o nfs4xdr.o nfs4state.o nfs4idmap.o \\\n  21:\t\t\t\t   nfs4acl.o nfs4callback.o nfs4recover.o nfs4xdr_gen.o\n  22:\tnfsd-$(CONFIG_NFSD_PNFS) += nfs4layouts.o\n  23:\tnfsd-$(CONFIG_NFSD_BLOCKLAYOUT) += blocklayout.o blocklayoutxdr.o\n  24:\tnfsd-$(CONFIG_NFSD_SCSILAYOUT) += blocklayout.o blocklayoutxdr.o\n  25:\tnfsd-$(CONFIG_NFSD_FLEXFILELAYOUT) += flexfilelayout.o flexfilelayoutxdr.o\n  26:\tnfsd-$(CONFIG_NFS_LOCALIO) += localio.o\n  27:\tnfsd-$(CONFIG_DEBUG_FS) += debugfs.o\n  28:\t\n  29:\t#\n  30:\t# XDR code generation (requires Python and additional packages)\n"
}

9/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 29130
							output: 32
							thoughts: 80
						

Thoughts:
Here's my summary, as I see it:

**Verifying NFS Control Compilation**

Okay, so `nfsctl.c` is definitely being brought into the build process when the NFS daemon is enabled, thanks to `CONFIG_NFSD` and the linking in the `nfsd-y` variable. Specifically, `nfsd-y += nfssvc.o nfsctl.o ...` confirms its inclusion. Now, I know that the `net/sunrpc/` directory is also supposed to be compiled when the NFS daemon is enabled, and is selected via `CONFIG_NFSD`. Given the dependency, I'll quickly check the `net/sunrpc/Makefile` to make absolutely certain that's the case. It's always a good idea to confirm these dependencies in the Makefiles themselves.



10/2 2026/09/01 00:20 tool read-file
0m Args:
{
  "File": "net/sunrpc/Makefile",
  "FirstLine": 1,
  "LineCount": 40
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0\n   2:\t#\n   3:\t# Makefile for Linux kernel SUN RPC\n   4:\t#\n   5:\t\n   6:\t\n   7:\tobj-$(CONFIG_SUNRPC) += sunrpc.o\n   8:\tobj-$(CONFIG_SUNRPC_GSS) += auth_gss/\n   9:\tobj-$(CONFIG_SUNRPC_XPRT_RDMA) += xprtrdma/\n  10:\t\n  11:\tsunrpc-y := clnt.o xprt.o socklib.o xprtsock.o sched.o \\\n  12:\t\t    auth.o auth_null.o auth_tls.o auth_unix.o \\\n  13:\t\t    svc.o svcsock.o svcauth.o svcauth_unix.o \\\n  14:\t\t    addr.o rpcb_clnt.o timer.o xdr.o \\\n  15:\t\t    sunrpc_syms.o cache.o rpc_pipe.o sysfs.o \\\n  16:\t\t    svc_xprt.o \\\n  17:\t\t    xprtmultipath.o netlink.o\n  18:\tsunrpc-$(CONFIG_SUNRPC_DEBUG) += debugfs.o\n  19:\tsunrpc-$(CONFIG_SUNRPC_BACKCHANNEL) += backchannel_rqst.o\n  20:\tsunrpc-$(CONFIG_PROC_FS) += stats.o\n  21:\tsunrpc-$(CONFIG_SYSCTL) += sysctl.o\n"
}

11/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 29815
							output: 27
							thoughts: 379
						

Thoughts:
**Analyzing the Impact of This Patch**

Okay, let's dive into this patch and figure out its implications. My immediate thought is to trace the impact. I need to identify the key functions this patch modifies and pinpoint the symbols that demand my immediate attention.

Alright, here's what the patch seems to touch:

1.  `nfsd_nl_listener_set_doit` in `fs/nfsd/nfsctl.c`: This is a non-static Netlink command handler, so it's probably pretty important in the nfsd context.
2.  `nfsd_nl_validate_listeners` in `fs/nfsd/nfsctl.c`: This one is static, but it's called by the previous one. Definitely relevant.
3.  `nfsd_nl_transport_supported` in `fs/nfsd/nfsctl.c`: Another static function within `nfsd`, worth a second look, though potentially less critical given its static nature.
4.  `svc_rpcb_failure_count` in `net/sunrpc/svc.c`: This one's *exported* using `EXPORT_SYMBOL_GPL`, which means it's accessible from other modules, so it needs closer scrutiny.
5.  `svc_register` in `net/sunrpc/svc.c`: It's exported and non-static, so, just like `svc_rpcb_failure_count`, I'll need to check how the patch modifies it, and its declared in `include/linux/sunrpc/svc.h`.
6.  `svc_generic_rpcbind_set` in `net/sunrpc/svc.c`: Another exported symbol, with the `EXPORT_SYMBOL_GPL` marker. This one is also in my high-priority list.
7.  `svc_delete_xprt` in `net/sunrpc/svc_xprt.c` and `svc_xprt_clear_rpcb_unreg`: These are both static, so their impact might be more localized, but still worth a look within the `svc_xprt` context.
8. `rpcb_register_call` in `net/sunrpc/rpcb_clnt.c`: This one is static, I'll need to investigate what uses it.

Since `svc_register` is critical to our architecture, let's verify if it's exported.



12/2 2026/09/01 00:20 tool grepper
0m Args:
{
  "Expression": "svc_register",
  "PathPrefix": "net/sunrpc"
}

Results:
{
  "Output": "net/sunrpc/svc.c=1074=static int __svc_rpcb_register6(struct net *net, const u32 program,\n--\nnet/sunrpc/svc.c-1117- */\nnet/sunrpc/svc.c:1118:static int __svc_register(struct net *net, const char *progname,\nnet/sunrpc/svc.c-1119-\t\t\t  const u32 program, const u32 version,\n--\nnet/sunrpc/svc.c-1137-\nnet/sunrpc/svc.c:1138:\ttrace_svc_register(progname, version, family, protocol, port, error);\nnet/sunrpc/svc.c-1139-\treturn error;\n--\nnet/sunrpc/svc.c=1143=int svc_rpcbind_set_version(struct net *net,\n--\nnet/sunrpc/svc.c-1148-{\nnet/sunrpc/svc.c:1149:\treturn __svc_register(net, progp-\u003epg_name, progp-\u003epg_prog,\nnet/sunrpc/svc.c-1150-\t\t\t\tversion, family, proto, port);\n--\nnet/sunrpc/svc.c=1188=EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);\n--\nnet/sunrpc/svc.c-1193- *\nnet/sunrpc/svc.c:1194: * svc_register() adds one for each of its calls that got no answer. A reply\nnet/sunrpc/svc.c-1195- * that refuses one entry does not count, because rpcbind answered and the\n--\nnet/sunrpc/svc.c-1201- *\nnet/sunrpc/svc.c:1202: * This is for callers that cannot see the svc_register() return, because a\nnet/sunrpc/svc.c-1203- * transport class sits in between. Such a caller reads the count before it\n--\nnet/sunrpc/svc.c=1214=EXPORT_SYMBOL_GPL(svc_rpcb_failure_count);\n--\nnet/sunrpc/svc.c-1216-/**\nnet/sunrpc/svc.c:1217: * svc_register - register an RPC service with the local portmapper\nnet/sunrpc/svc.c-1218- * @serv: svc_serv struct for the service to register\n--\nnet/sunrpc/svc.c-1225- */\nnet/sunrpc/svc.c:1226:int svc_register(struct svc_serv *serv, struct net *net,\nnet/sunrpc/svc.c-1227-\t\t const int family, const unsigned short proto,\n--\nnet/sunrpc/svc_xprt.c=1123=static void svc_delete_xprt(struct svc_xprt *xprt)\n--\nnet/sunrpc/svc_xprt.c-1135-\nnet/sunrpc/svc_xprt.c:1136:\t\tif (svc_register(serv, xprt-\u003expt_net, sock-\u003esk-\u003esk_family,\nnet/sunrpc/svc_xprt.c-1137-\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"
}

13/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 31716
							output: 30
							thoughts: 13
						

Thoughts:
**Checking for Exported Functions**

Okay, time to get to the bottom of this. First things first, I need to verify something fundamental – let's see if the function `svc_register` is actually exported. This is a crucial first step; if it's not exported, then any further investigation is a dead end. Gotta make sure it's accessible before I waste any more time digging into the deeper workings. Right, let's confirm the export status of `svc_register`.



14/2 2026/09/01 00:20 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "EXPORT_.*svc_register",
  "PathPrefix": "net/sunrpc"
}

Results:
{
  "error": "no matches"
}

15/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 31877
							output: 37
							thoughts: 204
						

Thoughts:
**Analyzing Kernel Symbol Declarations**

Okay, let's see, I'm looking at some kernel symbols here. My initial thought is to categorize and understand their scope. I see `svc_register` is a regular function, not static, declared in `svc.c` but it is declared in `include/linux/sunrpc/svc.h`. That makes sense; it's likely a core function that needs to be accessible outside of just that particular file. Then I note `svc_rpcb_failure_count` and `svc_generic_rpcbind_set`, both of which are also non-static, but crucially, they are `EXPORT_SYMBOL_GPL`. That export is vital; it means these symbols are intentionally made available for use by GPL-licensed kernel modules. Good. Understanding the intent of symbol export is half the battle.

Now, I'm seeing `nfsd_nl_listener_set_doit`. It's non-static as well, defined as a function `int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)`, and it's residing in `fs/nfsd/nfsctl.c`. The fact that it's in the `nfsd` directory immediately flags it as something related to the NFS daemon and potentially a control function. The naming suggests it's likely involved in setting up or configuring a Netlink listener related to NFS. Okay, let's take a closer look at that function next, based on the file path.



16/2 2026/09/01 00:20 tool grepper
0m Args:
{
  "Expression": "nfsd_nl_listener_set_doit",
  "PathPrefix": "fs/nfsd"
}

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-2060-/**\nfs/nfsd/nfsctl.c:2061: * nfsd_nl_listener_set_doit - set the nfs running sockets\nfs/nfsd/nfsctl.c-2062- * @skb: reply buffer\n--\nfs/nfsd/nfsctl.c-2066- */\nfs/nfsd/nfsctl.c:2067:int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\nfs/nfsd/nfsctl.c-2068-{\n"
}

17/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 32949
							output: 27
							thoughts: 183
						

Thoughts:
**NFSD and SUNRPC Configuration Review**

Okay, time to make sure these NFS server configurations are dialed in correctly. First, I need to verify the dependencies and selections for `NFSD` and `SUNRPC`. Let's start with `fs/nfsd/Kconfig`. Right, it looks like `NFSD` depends on `INET`, `FILE_LOCKING`, `FSNOTIFY`, and `MULTIUSER`. That makes sense; networking, file locking, notifications, and multiuser support are all fundamental for a network file server. It also *selects* `SUNRPC`, `LOCKD`, `EXPORTFS`, `NFS_COMMON`, and `CRC32`. Good, `SUNRPC` is indeed being selected. The presence of `LOCKD` implies we're dealing with mandatory locking, `EXPORTFS` is for exporting file systems. `NFS_COMMON` sounds like a useful default. And CRC32 is a standard checksum. The key requirement is `CONFIG_NFSD`, which is crucial.

Now, let's see if we need to consider any `NFSD` variants like `NFSD_V4`, and if anything else is relevant. Back in `fs/nfsd/Kconfig`, it's confirmed that `NFSD` can be compiled as either built-in (`y`) or module (`m`). That's good, providing flexibility. I'll re-check the main options to be sure. Specifically, I'll confirm `CONFIG_NFSD`, `CONFIG_SUNRPC`, and `CONFIG_NET` are properly set.

Finally, just a quick sanity check to be exhaustive... let's sweep through `net/sunrpc/Kconfig` to make sure there aren't any other related configurations I should be considering.



18/2 2026/09/01 00:20 tool grepper
0m Args:
{
  "Expression": "config SUNRPC",
  "PathPrefix": "net/sunrpc"
}

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

19/2 2026/09/01 00:20 llm patch-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 33936
							output: 184
							thoughts: 269
						

Thoughts:
**My Assessment of the NFSD Changes**

Okay, let's break this down. First, I need to confirm the foundational configuration. I see that `tools/testing/selftests/nfsd/config` has the necessary flags set: `CONFIG_NAMESPACES`, `CONFIG_NET_NS`, `CONFIG_SHMEM`, `CONFIG_TMPFS`, `CONFIG_UNIX`, `CONFIG_IPV6`, `CONFIG_NFSD`, and `CONFIG_NFSD_V4`. That confirms `NFSD` (and by extension, `SUNRPC`) as the critical element here.

Now, let's dig into the WorthFuzzing analysis. This is important to ensure these are actually *functional* changes worth investigating. Yes, it's adding some validation – crucial for security – and setting limits on listeners, which can prevent exhaustion. Plus, it's handling `rpcbind` failures better, along with timeout configuration, and adjusting error codes. These are all positive signs.

Next, the reachability in QEMU/VM is confirmed. Great. Generic netlink `nfsd` family commands (`NFSD_CMD_LISTENER_SET`) will be our entry point.

Finally, the focus symbols are listed: `nfsd_nl_listener_set_doit`, `svc_register`, `svc_rpcb_failure_count`, and `svc_generic_rpcbind_set`. These will be key areas to examine more closely.

Alright, I have enough data to move forward. Let's call `set-results`.



20/2 2026/09/01 00:21 tool set-results
0m Args:
{
  "EnableConfigs": [
    "NFSD",
    "SUNRPC"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "svc_rpcb_failure_count",
    "svc_generic_rpcbind_set"
  ],
  "Reasoning": "The patch updates the generic netlink interface for NFSD listeners (nfsd_nl_listener_set_doit) by adding bounds checking, unsupported transport validation, and improving error handling when communicating with local rpcbind during listener creation/removal (svc_register, svc_rpcb_failure_count, svc_generic_rpcbind_set). These changes introduce new logic and error handling paths in reachable core kernel subsystems (NFSD and SunRPC) that should be fuzzed.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "NFSD",
    "SUNRPC"
  ],
  "FocusSymbols": [
    "nfsd_nl_listener_set_doit",
    "svc_register",
    "svc_rpcb_failure_count",
    "svc_generic_rpcbind_set"
  ],
  "Reasoning": "The patch updates the generic netlink interface for NFSD listeners (nfsd_nl_listener_set_doit) by adding bounds checking, unsupported transport validation, and improving error handling when communicating with local rpcbind during listener creation/removal (svc_register, svc_rpcb_failure_count, svc_generic_rpcbind_set). These changes introduce new logic and error handling paths in reachable core kernel subsystems (NFSD and SunRPC) that should be fuzzed.",
  "WorthFuzzing": true
}

21/1 2026/09/01 00:21 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

22/2 2026/09/01 00:21 agent kmsan-evaluator
0m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch set enhances NFSD generic netlink listener management by validating transport names early, limiting the maximum number of listeners per request, tracking rpcbind failures via an atomic counter in struct svc_serv, and adding comprehensive kselftests.\n\nKMSAN vs KASAN applicability analysis:\n1. No uninitialized memory or structure padding is copied to user space: netlink extack strings and return codes use constant strings or formatted messages with valid attributes.\n2. The newly added struct member (sv_rpcb_failures in struct svc_serv) is allocated using kzalloc_obj (*serv) in __svc_create(), ensuring it is always zero-initialized.\n3. All local variables and branching conditions (e.g. skipped_rpcb, bad_attr, bad_xprt, rpcb_failures, noanswer) are initialized before use.\n4. Any potential issues with listener lists, locking, or socket teardown are standard lifecycle and synchronization concerns covered by KASAN and LOCKDEP.\n\nTherefore, this patch series does not introduce or expose any uninitialized memory risks and does not warrant a dedicated KMSAN session.",
  "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 dcc400252da4944425eeebdaaf6ff74f523cc034
Author: syz-cluster <triage@syzkaller.com>
Date:   Tue Sep 1 00:20:23 2026 +0000

    syz-cluster: applied patch under review

diff --git a/MAINTAINERS b/MAINTAINERS
index 0b42e898f4d8e..30f403f996c2a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14208,6 +14208,7 @@ F:	include/uapi/linux/nfsd/
 F:	include/uapi/linux/sunrpc/
 F:	net/sunrpc/
 F:	tools/net/sunrpc/
+F:	tools/testing/selftests/nfsd/
 
 KERNEL NFSD BLOCK and SCSI LAYOUT DRIVER
 R:	Christoph Hellwig <hch@lst.de>
diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index adb032b7311a3..2d26da12051c5 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,13 @@ 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]))) {
+			NL_SET_ERR_MSG_ATTR(info->extack,
+					    tb[NFSD_A_SOCK_TRANSPORT_NAME],
+					    "unsupported 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;
@@ -2037,8 +2067,12 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 {
 	struct net *net = genl_info_net(info);
+	const struct nlattr *bad_attr = NULL;
 	struct svc_xprt *xprt, *tmp;
+	const char *bad_xprt = NULL;
+	unsigned int rpcb_failures;
 	const struct nlattr *attr;
+	bool skipped_rpcb = false;
 	struct svc_serv *serv;
 	LIST_HEAD(permsocks);
 	struct nfsd_net *nn;
@@ -2128,13 +2162,15 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 	if (delete)
 		svc_xprt_destroy_all(serv, net, false);
 
+	rpcb_failures = svc_rpcb_failure_count(serv);
+
 	/* walk list of addrs again, open any that still don't exist */
 	nlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info->nlhdr,
 				 GENL_HDRLEN, rem) {
 		struct nlattr *tb[NFSD_A_SOCK_MAX + 1];
 		const char *xcl_name;
 		struct sockaddr *sa;
-		int ret;
+		int flags, ret;
 
 		/* validated up front in nfsd_nl_validate_listeners() */
 		if (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,
@@ -2153,11 +2189,46 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 			continue;
 		}
 
-		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,
+		flags = skipped_rpcb ? SVC_SOCK_ANONYMOUS : 0;
+		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, flags,
 					      current_cred());
+
+		if (!skipped_rpcb &&
+		    svc_rpcb_failure_count(serv) != rpcb_failures) {
+			skipped_rpcb = true;
+			if (ret < 0)
+				ret = svc_xprt_create_from_sa(serv, xcl_name,
+							      net, sa,
+							      SVC_SOCK_ANONYMOUS,
+							      current_cred());
+		}
+
 		/* always save the latest error */
-		if (ret < 0)
+		if (ret < 0) {
+			bad_attr = attr;
+			bad_xprt = xcl_name;
 			err = ret;
+		}
+	}
+
+	/*
+	 * The ack carries the errno of the last entry that failed. Point at
+	 * that entry as well, since several entries can share a transport
+	 * name and the errno alone cannot tell them apart.
+	 */
+	if (err) {
+		NL_SET_BAD_ATTR(info->extack, bad_attr);
+		if (skipped_rpcb)
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener; rpcbind did not answer",
+					   bad_xprt);
+		else
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener",
+					   bad_xprt);
+	} else if (skipped_rpcb) {
+		NL_SET_ERR_MSG(info->extack,
+			       "rpcbind did not answer, some listeners are not registered");
 	}
 
 	if (!serv->sv_nrthreads && list_empty(&nn->nfsd_serv->sv_permsocks))
diff --git a/include/linux/sunrpc/clnt.h b/include/linux/sunrpc/clnt.h
index 3c2b8c355ab3a..30344c0d6a9d7 100644
--- a/include/linux/sunrpc/clnt.h
+++ b/include/linux/sunrpc/clnt.h
@@ -199,7 +199,8 @@ struct rpc_xprt	*rpc_task_get_xprt(struct rpc_clnt *clnt,
 
 int		rpcb_create_local(struct net *);
 void		rpcb_put_local(struct net *);
-int		rpcb_register(struct net *, u32, u32, int, unsigned short);
+int		rpcb_register(struct net *net, u32 prog, u32 vers, int prot,
+			      unsigned short port);
 int		rpcb_v4_register(struct net *net, const u32 program,
 				 const u32 version,
 				 const struct sockaddr *address,
diff --git a/include/linux/sunrpc/svc.h b/include/linux/sunrpc/svc.h
index 2db1b9ec5658d..5fa9417e034d7 100644
--- a/include/linux/sunrpc/svc.h
+++ b/include/linux/sunrpc/svc.h
@@ -78,6 +78,7 @@ struct svc_serv {
 	unsigned int		sv_max_payload;	/* datagram payload size */
 	unsigned int		sv_max_mesg;	/* max_payload + 1 page for overheads */
 	unsigned int		sv_xdrsize;	/* XDR buffer size */
+	atomic_t		sv_rpcb_failures; /* unanswered rpcbind calls */
 	struct list_head	sv_permsocks;	/* all permanent sockets */
 	struct list_head	sv_tempsocks;	/* all temporary sockets */
 	int			sv_tmpcnt;	/* count of temporary "valid" sockets */
@@ -451,6 +452,7 @@ int sunrpc_set_pool_mode(const char *val);
 int sunrpc_get_pool_mode(char *val, size_t size);
 void svc_rpcb_cleanup(struct svc_serv *serv, struct net *net);
 int svc_bind(struct svc_serv *serv, struct net *net);
+unsigned int svc_rpcb_failure_count(struct svc_serv *serv);
 struct svc_serv *svc_create(struct svc_program *, unsigned int,
 			    int (*threadfn)(void *data));
 bool		   svc_rqst_replace_page(struct svc_rqst *rqstp,
@@ -471,8 +473,9 @@ unsigned int	   svc_serv_maxthreads(const struct svc_serv *serv);
 int		   svc_pool_stats_open(struct svc_info *si, struct file *file);
 void		   svc_process(struct svc_rqst *rqstp);
 void		   svc_process_bc(struct rpc_rqst *req, struct svc_rqst *rqstp);
-int		   svc_register(const struct svc_serv *, struct net *, const int,
-				const unsigned short, const unsigned short);
+int		   svc_register(struct svc_serv *serv, struct net *net,
+				const int family, const unsigned short proto,
+				const unsigned short port);
 
 void		   svc_wake_up(struct svc_serv *);
 void		   svc_reserve(struct svc_rqst *rqstp, int space);
diff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c
index 4c0b7fefee4e2..8b9621129115f 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;
@@ -400,7 +412,8 @@ static struct rpc_clnt *rpcb_create(struct net *net, const char *nodename,
 	return rpc_create(&args);
 }
 
-static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt, struct rpc_message *msg, bool is_set)
+static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt,
+			      struct rpc_message *msg, bool is_set)
 {
 	int flags = RPC_TASK_NOCONNECT;
 	int error, result = 0;
@@ -410,8 +423,22 @@ static int rpcb_register_call(struct sunrpc_net *sn, struct rpc_clnt *clnt, stru
 	msg->rpc_resp = &result;
 
 	error = rpc_call_sync(clnt, msg, flags);
-	if (error < 0)
-		return error;
+	if (error < 0) {
+		switch (error) {
+		/* rpcbind answered; the reply itself carries the error */
+		case -EPROTONOSUPPORT:
+		case -EPFNOSUPPORT:
+		case -EOPNOTSUPP:
+		case -EACCES:
+		/* the call never made it onto the wire */
+		case -ENOMEM:
+		case -EMSGSIZE:
+		case -ERESTARTSYS:
+			return error;
+		}
+		/* anything else, we assume that rpcbind isn't functional */
+		return -EIO;
+	}
 
 	if (!result)
 		return -EACCES;
diff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c
index 8297bad2b1777..c00ae00b6a121 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1179,10 +1179,40 @@ int svc_generic_rpcbind_set(struct net *net,
 	error = svc_rpcbind_set_version(net, progp, version,
 					family, proto, port);
 
+	/* -EIO means no answer, not a refusal, so vs_rpcb_optnl must keep it. */
+	if (error == -EIO)
+		return error;
+
 	return (vers->vs_rpcb_optnl) ? 0 : error;
 }
 EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
 
+/**
+ * svc_rpcb_failure_count - local rpcbind calls for @serv that got no answer
+ * @serv: RPC service to query
+ *
+ * svc_register() adds one for each of its calls that got no answer. A reply
+ * that refuses one entry does not count, because rpcbind answered and the
+ * next entry may still succeed.
+ *
+ * The count is kept per serv rather than per net. The local rpcbind client
+ * is per-net and lockd shares it, but a count that another service can move
+ * says nothing about this serv's own calls.
+ *
+ * This is for callers that cannot see the svc_register() return, because a
+ * transport class sits in between. Such a caller reads the count before it
+ * starts and compares as it goes, so there is no state to reset between
+ * operations. The count never resets, and callers must not attach meaning
+ * to the value itself.
+ *
+ * Return: the number of unanswered calls since this serv was created.
+ */
+unsigned int svc_rpcb_failure_count(struct svc_serv *serv)
+{
+	return atomic_read(&serv->sv_rpcb_failures);
+}
+EXPORT_SYMBOL_GPL(svc_rpcb_failure_count);
+
 /**
  * svc_register - register an RPC service with the local portmapper
  * @serv: svc_serv struct for the service to register
@@ -1193,10 +1223,11 @@ EXPORT_SYMBOL_GPL(svc_generic_rpcbind_set);
  *
  * Service is registered for any address in the passed-in protocol family
  */
-int svc_register(const struct svc_serv *serv, struct net *net,
+int svc_register(struct svc_serv *serv, struct net *net,
 		 const int family, const unsigned short proto,
 		 const unsigned short port)
 {
+	bool			noanswer = false;
 	unsigned int		p, i;
 	int			error = 0;
 
@@ -1208,18 +1239,34 @@ 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++) {
+			const struct svc_version *vers = progp->pg_vers[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 == -EIO) {
+				noanswer = true;
+				if (vers && vers->vs_rpcb_optnl)
+					ret = 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;
 			}
 		}
+
+		/* Give up on trying to register anything if it didn't respond */
+		if (noanswer)
+			break;
 	}
 
+	if (noanswer)
+		atomic_inc(&serv->sv_rpcb_failures);
+
 	return error;
 }
 
@@ -1230,8 +1277,8 @@ int svc_register(const struct svc_serv *serv, struct net *net,
  * any "inet6" entries anyway.  So a PMAP_UNSET should be sufficient
  * in this case to clear all existing entries for [program, version].
  */
-static void __svc_unregister(struct net *net, const u32 program, const u32 version,
-			     const char *progname)
+static int __svc_unregister(struct net *net, const u32 program, const u32 version,
+			    const char *progname)
 {
 	int error;
 
@@ -1245,6 +1292,7 @@ static void __svc_unregister(struct net *net, const u32 program, const u32 versi
 		error = rpcb_register(net, program, version, 0, 0);
 
 	trace_svc_unregister(progname, version, error);
+	return error;
 }
 
 /*
@@ -1271,10 +1319,13 @@ static void svc_unregister(const struct svc_serv *serv, struct net *net)
 				continue;
 			if (progp->pg_vers[i]->vs_hidden)
 				continue;
-			__svc_unregister(net, progp->pg_prog, i, progp->pg_name);
+			if (__svc_unregister(net, progp->pg_prog, i,
+					     progp->pg_name) == -EIO)
+				goto out;
 		}
 	}
 
+out:
 	rcu_read_lock();
 	sighand = rcu_dereference(current->sighand);
 	spin_lock_irqsave(&sighand->siglock, flags);
diff --git a/net/sunrpc/svc_xprt.c b/net/sunrpc/svc_xprt.c
index 40040af588fb2..7e471c92f23ab 100644
--- a/net/sunrpc/svc_xprt.c
+++ b/net/sunrpc/svc_xprt.c
@@ -1101,6 +1101,22 @@ static void call_xpt_users(struct svc_xprt *xprt)
 	spin_unlock(&xprt->xpt_lock);
 }
 
+/*
+ * If rpcbind stops answering, every listener still to be destroyed would
+ * only wait out the same timeout again. Drop the flag on all of the
+ * remaining listeners.
+ */
+static void svc_xprt_clear_rpcb_unreg(struct svc_serv *serv, struct net *net)
+{
+	struct svc_xprt *xprt;
+
+	spin_lock_bh(&serv->sv_lock);
+	list_for_each_entry(xprt, &serv->sv_permsocks, xpt_list)
+		if (xprt->xpt_net == net)
+			clear_bit(XPT_RPCB_UNREG, &xprt->xpt_flags);
+	spin_unlock_bh(&serv->sv_lock);
+}
+
 /*
  * Remove a dead transport
  */
@@ -1115,11 +1131,15 @@ static void svc_delete_xprt(struct svc_xprt *xprt)
 		struct svc_sock *svsk = container_of(xprt, struct svc_sock,
 						     sk_xprt);
 		struct socket *sock = svsk->sk_sock;
+		unsigned int failures = svc_rpcb_failure_count(serv);
 
 		if (svc_register(serv, xprt->xpt_net, sock->sk->sk_family,
 				 sock->sk->sk_protocol, 0) < 0)
 			pr_warn("failed to unregister %s with rpcbind\n",
 				xprt->xpt_class->xcl_name);
+
+		if (svc_rpcb_failure_count(serv) != failures)
+			svc_xprt_clear_rpcb_unreg(serv, xprt->xpt_net);
 	}
 
 	if (test_and_set_bit(XPT_DEAD, &xprt->xpt_flags))
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 2d960626750e3..d881ba39cad40 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -90,6 +90,7 @@ TARGETS += net/packetdrill
 TARGETS += net/ppp
 TARGETS += net/rds
 TARGETS += net/tcp_ao
+TARGETS += nfsd
 TARGETS += nolibc
 TARGETS += pci_endpoint
 TARGETS += pcie_bwctrl
diff --git a/tools/testing/selftests/nfsd/.gitignore b/tools/testing/selftests/nfsd/.gitignore
new file mode 100644
index 0000000000000..19e6dec04d8e9
--- /dev/null
+++ b/tools/testing/selftests/nfsd/.gitignore
@@ -0,0 +1 @@
+nfsd_netlink_listener
diff --git a/tools/testing/selftests/nfsd/Makefile b/tools/testing/selftests/nfsd/Makefile
new file mode 100644
index 0000000000000..15ac65549d259
--- /dev/null
+++ b/tools/testing/selftests/nfsd/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += $(KHDR_INCLUDES) -Wall
+
+TEST_GEN_PROGS := nfsd_netlink_listener
+
+include ../lib.mk
diff --git a/tools/testing/selftests/nfsd/config b/tools/testing/selftests/nfsd/config
new file mode 100644
index 0000000000000..ab84523fbedf3
--- /dev/null
+++ b/tools/testing/selftests/nfsd/config
@@ -0,0 +1,8 @@
+CONFIG_NAMESPACES=y
+CONFIG_NET_NS=y
+CONFIG_SHMEM=y
+CONFIG_TMPFS=y
+CONFIG_UNIX=y
+CONFIG_IPV6=y
+CONFIG_NFSD=y
+CONFIG_NFSD_V4=y
diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
new file mode 100644
index 0000000000000..89d1da825b954
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -0,0 +1,1328 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression tests for the NFSD generic-netlink listener interface
+ * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).
+ *
+ * Three groups:
+ *   validation  - malformed/abusive LISTENER_SET requests are rejected by
+ *                 nfsd_nl_validate_listeners(), before nfsd_mutex is taken.
+ *   functional  - create/add/remove listeners and verify LISTENER_GET
+ *                 reflects the set (round-trip of transport + addr:port).
+ *   semantics   - once threads are running (THREADS_SET) a listener change
+ *                 is refused with -EBUSY.
+ *
+ * Each test runs in its own private net + mount namespace (unshare in
+ * FIXTURE_SETUP). /run is masked there: a pathname AF_LOCAL connect is not
+ * scoped by the network namespace, since unix_find_bsd() resolves by inode
+ * and takes no struct net, so the kernel's rpcbind client would otherwise be
+ * able to reach the rpcbind running on the host. Anything that creates a
+ * serv is served by the per-netns rpcbind stub below instead.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <poll.h>
+#include <sched.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <linux/netlink.h>
+#include <linux/genetlink.h>
+
+#include "../kselftest_harness.h"
+
+/* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */
+#define NFSD_FAMILY_NAME		"nfsd"
+#define NFSD_CMD_THREADS_SET		2
+#define NFSD_CMD_VERSION_SET		4
+#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 NFSD_A_SERVER_PROTO_VERSION	1	/* per-version nest */
+#define NFSD_A_VERSION_MAJOR		1	/* inside the version nest */
+#define NFSD_A_VERSION_MINOR		2	/* inside the version nest */
+#define NFSD_A_VERSION_ENABLED		3	/* inside the version 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 = -1;		/* set per-test in FIXTURE_SETUP */
+
+/* Extack message from the last genl_request(); empty if there was none. */
+static char last_extack[128];
+
+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);
+	int on = 1;
+
+	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));
+	/*
+	 * Ask for extack, and cap the ack so the request is not echoed back:
+	 * the TLVs then always follow the fixed part of the error message.
+	 */
+	setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &on, sizeof(on));
+	setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
+	return fd;
+}
+
+/* Stash the extack message of an ack, if it carries one. */
+static void parse_extack(const char *rbuf)
+{
+	const struct nlmsghdr *nlh = (const void *)rbuf;
+	const struct nlattr *na;
+	int off, left;
+
+	last_extack[0] = '\0';
+	if (nlh->nlmsg_type != NLMSG_ERROR ||
+	    !(nlh->nlmsg_flags & NLM_F_ACK_TLVS))
+		return;
+
+	off = NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(struct nlmsgerr));
+	left = nlh->nlmsg_len - off;
+	na = (const void *)(rbuf + off);
+
+	while (left >= (int)NLA_HDRLEN) {
+		if ((na->nla_type & NLA_TYPE_MASK) == NLMSGERR_ATTR_MSG) {
+			strncpy(last_extack, (const char *)na + NLA_HDRLEN,
+				sizeof(last_extack) - 1);
+			last_extack[sizeof(last_extack) - 1] = '\0';
+			return;
+		}
+		left -= NLA_ALIGN4(na->nla_len);
+		na = (const void *)((const char *)na + NLA_ALIGN4(na->nla_len));
+	}
+}
+
+/* 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)");
+
+	last_extack[0] = '\0';
+	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;
+		parse_extack(rbuf);
+	} 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);
+}
+
+/*
+ * Enable exactly one NFS version in this netns. NFSD_CMD_VERSION_SET clears
+ * every version first, so one nest is enough to leave the server v4-only.
+ * It refuses once a serv exists, so call it before any listener.
+ */
+static int version_set_only(uint32_t major, uint32_t minor)
+{
+	char attrs[64];
+	struct nlattr *nest = (void *)attrs;
+	int inner = NLA_HDRLEN;
+
+	inner = put_attr(attrs, inner, NFSD_A_VERSION_MAJOR,
+			 &major, sizeof(major));
+	inner = put_attr(attrs, inner, NFSD_A_VERSION_MINOR,
+			 &minor, sizeof(minor));
+	inner = put_attr(attrs, inner, NFSD_A_VERSION_ENABLED, NULL, 0);
+	nest->nla_type = NFSD_A_SERVER_PROTO_VERSION | NLA_F_NESTED;
+	nest->nla_len = inner;
+
+	return genl_request(NFSD_CMD_VERSION_SET, attrs, NLA_ALIGN4(inner));
+}
+
+/* Fetch the current listeners; returns count (>=0) or -errno. */
+static int listener_get(struct listener_ent *out, int max)
+{
+	char rbuf[8192];
+	int n = genl_request_reply(NFSD_CMD_LISTENER_GET, rbuf, sizeof(rbuf));
+
+	if (n < 0)
+		return n;
+	return parse_listener_get(rbuf, n, out, max);
+}
+
+/*
+ * Every listener these tests create comes from put_listener_af(), so the
+ * address is always loopback. Match on it too: without that, a reply that
+ * gave the right transport and port on the wrong address (0.0.0.0, say)
+ * would pass.
+ */
+static struct listener_ent *find_listener(struct listener_ent *e, int n,
+					  const char *xprt, int family,
+					  uint16_t port)
+{
+	int i;
+
+	for (i = 0; i < n; i++) {
+		if (e[i].family != family || e[i].port != port ||
+		    strcmp(e[i].xprt, xprt))
+			continue;
+		if (family == AF_INET6) {
+			if (memcmp(&e[i].a6, &in6addr_loopback, sizeof(e[i].a6)))
+				continue;
+		} else if (e[i].a4.s_addr != htonl(INADDR_LOOPBACK)) {
+			continue;
+		}
+		return &e[i];
+	}
+	return NULL;
+}
+
+/* Start (@n > 0) or stop (@n == 0) nfsd threads in this netns. */
+static int threads_set(int n)
+{
+	char attrs[64];
+	uint32_t v = n;
+	int off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, &v, sizeof(v));
+
+	return genl_request(NFSD_CMD_THREADS_SET, attrs, off);
+}
+
+/* ------------------- per-netns local rpcbind stub ------------------- */
+
+/*
+ * Creating a listener registers with rpcbind: nfsd_nl_listener_set_doit()
+ * passes no SVC_SOCK_ANONYMOUS for the first entry of a request, so
+ * pmap_register is true in svc_setup_socket(). The fixture's server has v3
+ * enabled, and nfsd_version3 does not set vs_rpcb_optnl, so a failure there
+ * comes back out of svc_register() and takes the listener down with it.
+ * With nothing listening, every attempt first waits out the local rpcbind
+ * timeout. 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 leave every IPv6 listener unregistered.
+ *
+ * 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.
+ *
+ * In RPCB_STUB_SILENT mode a SET or an UNSET is read and nothing is written
+ * back, so the kernel waits out its own timeout. That is the only mode that
+ * makes rpcb_register_call() report a call that got no answer, which is what
+ * the per-net failure count records. The NULL procedure is still answered:
+ * rpcb_create_af_local() builds its client without RPC_CLNT_CREATE_NOPING, so
+ * rpc_create() pings, and a ping that goes unanswered drops the kernel onto
+ * the loopback rpcb_create_local_net() client, which never reaches this stub.
+ *
+ * The stub also keeps counters and the mode in a page shared with the test, so
+ * a test can assert that the kernel never talked to rpcbind at all, or that it
+ * dropped the local rpcbind client and had to reconnect.
+ *
+ * The mode lives there rather than in the child so that a test can change it
+ * with a serv already up. Killing and restarting the stub would close the
+ * connection the kernel holds, and rpcb_register_call() issues UNSET over
+ * AF_LOCAL with RPC_TASK_NOCONNECT, so the next call would fail at once with
+ * -ENOTCONN instead of waiting out a timeout.
+ */
+#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, RPCB_STUB_SILENT };
+
+struct rpcb_stub_stats {
+	unsigned int conns;		/* connections accepted */
+	unsigned int calls;		/* calls received */
+	unsigned int mode;		/* RPCB_STUB_*, read on every call */
+};
+
+static volatile struct rpcb_stub_stats *rpcb_stats;	/* MAP_SHARED */
+
+static int rpcb_stats_alloc(void)
+{
+	void *p = mmap(NULL, sizeof(*rpcb_stats), PROT_READ | PROT_WRITE,
+		       MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+	if (p == MAP_FAILED)
+		return -1;
+	rpcb_stats = p;
+	return 0;
+}
+
+/*
+ * The stub bumps these before it replies and the kernel waits for that reply,
+ * so whatever a netlink request provoked is visible once it returns.
+ */
+static int rpcb_calls(void)
+{
+	return rpcb_stats ? (int)rpcb_stats->calls : 0;
+}
+
+static int rpcb_conns(void)
+{
+	return rpcb_stats ? (int)rpcb_stats->conns : 0;
+}
+
+/* Takes effect on the stub's next call; the caller has not sent one yet. */
+static void rpcb_stub_set_mode(int mode)
+{
+	rpcb_stats->mode = mode;
+}
+
+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)
+{
+	unsigned int len, nrep = 6, mode = rpcb_stats->mode;
+	uint32_t mark, call[6], rep[7];
+	size_t replen;
+
+	if (rpcb_stub_read(fd, &mark, sizeof(mark)))
+		return -1;
+	len = ntohl(mark) & 0x7fffffff;
+	if (len < sizeof(call) || len > 4096)
+		return -1;
+	if (rpcb_stub_read(fd, call, sizeof(call)))
+		return -1;
+
+	/* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */
+	for (len -= sizeof(call); len; ) {
+		char sink[256];
+		unsigned int n = len > sizeof(sink) ? sizeof(sink) : len;
+
+		if (rpcb_stub_read(fd, sink, n))
+			return -1;
+		len -= n;
+	}
+
+	if (rpcb_stats)
+		rpcb_stats->calls++;
+
+	rep[0] = call[0];		/* xid */
+	rep[1] = htonl(1);		/* REPLY */
+	rep[2] = htonl(0);		/* MSG_ACCEPTED */
+	rep[3] = htonl(0);		/* verifier flavor AUTH_NULL */
+	rep[4] = htonl(0);		/* verifier length */
+	rep[5] = htonl(0);		/* SUCCESS */
+
+	if (ntohl(call[3]) != RPCB_PROGRAM) {
+		rep[5] = htonl(1);	/* PROG_UNAVAIL */
+	} else {
+		unsigned int proc = ntohl(call[5]);
+
+		switch (proc) {
+		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 */
+		}
+
+		/*
+		 * Answer nothing, so the caller waits out its timeout. The
+		 * NULL procedure is answered even here: the kernel pings at
+		 * client creation, and a ping with no answer takes it off
+		 * this socket entirely.
+		 */
+		if (mode == RPCB_STUB_SILENT && proc != RPCB_PROC_NULL)
+			return 0;
+	}
+
+	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)
+{
+	struct pollfd pfd[1 + RPCB_STUB_MAXCONN];
+	nfds_t n = 1, i;
+
+	pfd[0].fd = lfd;
+
+	for (;;) {
+		/* stop polling the listener when full, or poll() spins */
+		pfd[0].events = n < 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;
+
+		if (poll(pfd, n, -1) < 0)
+			return;
+
+		if (pfd[0].revents & POLLIN) {
+			int c = accept(lfd, NULL, NULL);
+
+			if (c >= 0) {
+				pfd[n].fd = c;
+				pfd[n].events = POLLIN;
+				/*
+				 * poll() ran with the old n, so it did not
+				 * write this revents. The loop below reads it.
+				 */
+				pfd[n].revents = 0;
+				n++;
+				if (rpcb_stats)
+					rpcb_stats->conns++;
+			}
+		}
+
+		for (i = 1; i < n; i++) {
+			if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR)))
+				continue;
+			if (rpcb_stub_call(pfd[i].fd)) {
+				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;
+
+	rpcb_stats->mode = mode;
+
+	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);
+		_exit(0);
+	}
+
+	close(lfd);
+	return pid;
+}
+
+/* --------------------------- fixture --------------------------- */
+
+FIXTURE(nfsd_listener) {
+	pid_t rpcbd;
+};
+
+FIXTURE_SETUP(nfsd_listener)
+{
+	struct ifreq ifr = {0};
+	struct stat st;
+	int s;
+
+	if (geteuid() != 0)
+		SKIP(return, "must be run as root");
+	if (unshare(CLONE_NEWNET | CLONE_NEWNS) < 0)
+		SKIP(return, "unshare(NEWNET|NEWNS): %s", strerror(errno));
+	if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
+		SKIP(return, "mount(/ private): %s", strerror(errno));
+
+	/*
+	 * Keep the kernel's rpcbind client inside this namespace. The
+	 * abstract socket it tries first is per-netns, but the
+	 * "/var/run/rpcbind.sock" fallback is not, so hide the path.
+	 */
+	if (mount("tmpfs", "/run", "tmpfs", 0, NULL) < 0)
+		SKIP(return, "mount(tmpfs on /run): %s", strerror(errno));
+	if (lstat("/var/run", &st) == 0 && S_ISDIR(st.st_mode) &&
+	    mount("tmpfs", "/var/run", "tmpfs", 0, NULL) < 0)
+		SKIP(return, "mount(tmpfs on /var/run): %s", strerror(errno));
+
+	/* Bring loopback up so listener binds (127.0.0.1 / ::1) work. */
+	s = socket(AF_INET, SOCK_DGRAM, 0);
+	ASSERT_GE(s, 0);
+	strcpy(ifr.ifr_name, "lo");
+	ASSERT_EQ(0, ioctl(s, SIOCGIFFLAGS, &ifr));
+	ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
+	ASSERT_EQ(0, ioctl(s, SIOCSIFFLAGS, &ifr));
+	close(s);
+
+	nfsd_family = genl_resolve_nfsd();
+	if (nfsd_family < 0)
+		SKIP(return, "nfsd genl family not found (modprobe nfsd?)");
+
+	if (rpcb_stats_alloc() < 0)
+		SKIP(return, "mmap(rpcbind stub counters): %s", strerror(errno));
+
+	self->rpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);
+	if (self->rpcbd < 0)
+		SKIP(return, "cannot start the rpcbind stub: %s",
+		     strerror(errno));
+}
+
+FIXTURE_TEARDOWN(nfsd_listener)
+{
+	/*
+	 * A listener holds a reference to this netns, which outlives the test
+	 * process, so anything still up leaks it. Threads pin the listeners in
+	 * turn; dropping them destroys the serv and everything under it.
+	 */
+	if (nfsd_family >= 0 && listener_set(NULL, 0) == -EBUSY)
+		threads_set(0);
+
+	if (self->rpcbd > 0) {
+		kill(self->rpcbd, SIGKILL);
+		waitpid(self->rpcbd, NULL, 0);
+	}
+	if (rpcb_stats) {
+		munmap((void *)rpcb_stats, sizeof(*rpcb_stats));
+		rpcb_stats = NULL;
+	}
+}
+
+/* ===================== validation / negative ===================== */
+
+TEST_F(nfsd_listener, val_empty_list_ok)
+{
+	EXPECT_EQ(0, listener_set(NULL, 0));
+}
+
+TEST_F(nfsd_listener, val_too_many)
+{
+	static char attrs[1 << 20];
+	int i, off = 0;
+
+	for (i = 0; i < 1025; i++)		/* > NFSD_NL_LISTENER_MAX (1024) */
+		off = put_listener(attrs, off, "udp", TEST_PORT);
+	EXPECT_EQ(-E2BIG, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_addr)
+{
+	char attrs[64];
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 0 };
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_missing_transport)
+{
+	struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+	struct raw_listener r = { .xprt = NULL, .emit_addr = 1,
+				  .addr = &s4, .addr_len = sizeof(s4) };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+/*
+ * A name matching no transport class must be refused before nfsd_mutex is
+ * taken, so it never reaches svc_xprt_create_from_sa() and its
+ * request_module("svc%s", name) upcall.
+ *
+ * The errno cannot show that -- svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. The rpcbind traffic can:
+ * getting that far means nfsd_create_serv() ran, and svc_bind() pings
+ * rpcbind at client creation and then sweeps stale entries with
+ * svc_unregister(). A silent stub is the proof nothing was created.
+ */
+TEST_F(nfsd_listener, val_bad_transport)
+{
+	char attrs[64];
+	int off = put_listener(attrs, 0, "bogus_xprt", TEST_PORT);
+
+	ASSERT_EQ(0, rpcb_calls());
+	EXPECT_EQ(-EPROTONOSUPPORT, listener_set(attrs, off));
+	EXPECT_EQ(0, rpcb_calls());
+}
+
+TEST_F(nfsd_listener, val_addr_too_short)
+{
+	unsigned char tiny = 0;
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1,
+				  .addr = &tiny, .addr_len = 1 };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet_short)
+{
+	struct sockaddr_in s4 = { .sin_family = AF_INET, .sin_port = htons(TEST_PORT) };
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s4,
+				  .addr_len = sizeof(sa_family_t) + 2 };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_inet6_short)
+{
+	struct sockaddr_in6 s6 = { .sin6_family = AF_INET6, .sin6_port = htons(TEST_PORT) };
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &s6,
+				  .addr_len = sizeof(struct sockaddr_in) };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EINVAL, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_bad_family)
+{
+	struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+	struct raw_listener r = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+				  .addr_len = sizeof(struct sockaddr_in) };
+	char attrs[64];
+	int off = put_raw_listener(attrs, 0, &r);
+
+	EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+}
+
+TEST_F(nfsd_listener, val_second_entry_bad)
+{
+	struct sockaddr_storage ss = { .ss_family = AF_UNIX };
+	struct raw_listener bad = { .xprt = "tcp", .emit_addr = 1, .addr = &ss,
+				    .addr_len = sizeof(struct sockaddr_in) };
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[128];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	off = put_raw_listener(attrs, off, &bad);
+	/* The whole request is rejected during validation; nothing applied. */
+	EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
+	/*
+	 * Again the errno alone does not say so: svc_xprt_create_from_sa()
+	 * also returns -EAFNOSUPPORT, and the doit keeps the listeners it did
+	 * manage to create, so the well-formed tcp entry ahead of the bad one
+	 * would still be up.
+	 */
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/*
+ * A rejected request must leave the listeners that are already up alone.
+ * The errno alone does not show that: svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. What differs is how far the
+ * request gets -- without the check in nfsd_nl_validate_listeners(),
+ * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener
+ * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the
+ * name fails.
+ */
+TEST_F(nfsd_listener, val_reject_keeps_listeners)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char good[64], bad[64];
+	int og = put_listener(good, 0, "tcp", TEST_PORT);
+	int ob = put_listener(bad, 0, "bogus_xprt", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(good, og));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+
+	EXPECT_EQ(-EPROTONOSUPPORT, listener_set(bad, ob));
+
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+/* ===================== functional / round-trip ===================== */
+
+/* LISTENER_GET with no serv in this netns returns an empty list. */
+TEST_F(nfsd_listener, func_get_empty)
+{
+	struct listener_ent got[MAX_LISTENERS];
+
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+TEST_F(nfsd_listener, func_create_tcp)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_STREQ("", last_extack);		/* nothing to warn about */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_udp)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "udp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_multi)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[128];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	off = put_listener(attrs, off, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+	EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_idempotent)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_EQ(0, listener_set(attrs, off));		/* re-set same list */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_add)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64], two[128];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+	int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+	o2 = put_listener(two, o2, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, listener_set(two, o2));		/* add udp, keep tcp */
+	ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+	EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_remove_subset)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char both[128], one[64];
+	int ob = put_listener(both, 0, "tcp", TEST_PORT);
+	int oo = put_listener(one, 0, "tcp", TEST_PORT);
+
+	ob = put_listener(both, ob, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(both, ob));
+	ASSERT_EQ(0, listener_set(one, oo));		/* drop udp */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+/*
+ * LISTENER_GET cannot tell a destroyed serv from a live one with no
+ * permsocks: nfsd_nl_listener_get_doit() replies empty either way. The
+ * rpcbind client can. nfsd_destroy_serv() is the only path that reaches
+ * svc_xprt_destroy_all(..., unregister=true) -> svc_rpcb_cleanup() ->
+ * rpcb_put_local(), which drops the last user and shuts the local client
+ * down; the next serv then has to connect again. Leaving the serv in place
+ * would keep the first connection and the stub would see just the one.
+ */
+TEST_F(nfsd_listener, func_empty_destroys)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	int conns;
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	conns = rpcb_conns();
+	ASSERT_GT(conns, 0);
+
+	EXPECT_EQ(0, listener_set(NULL, 0));		/* empty -> destroy serv */
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_GT(rpcb_conns(), conns);
+}
+
+TEST_F(nfsd_listener, func_ipv6)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off, s;
+
+	s = socket(AF_INET6, SOCK_STREAM, 0);
+	if (s < 0)
+		SKIP(return, "IPv6 unavailable: %s", strerror(errno));
+	close(s);
+
+	off = put_listener_af(attrs, 0, "tcp", AF_INET6, TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET6, TEST_PORT));
+}
+
+/* ===================== rpcbind registration ===================== */
+
+/*
+ * A rpcbind that refuses the registration takes the listener down with it.
+ * svc_register() fails, so svc_setup_socket() fails, so no listener is
+ * created. -EACCES alone does not show that, since a bind can return it
+ * too, so read the listener set back as well.
+ */
+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);
+
+	rpcb_stub_set_mode(RPCB_STUB_REFUSE);
+
+	EXPECT_EQ(-EACCES, listener_set(attrs, off));
+	EXPECT_STRNE("", last_extack);
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/*
+ * A listener that cannot be created reports which one it was: the errno
+ * alone does not name the entry in a multi-listener request.
+ */
+TEST_F(nfsd_listener, sem_create_failure_extack)
+{
+	struct sockaddr_in s4 = { .sin_family = AF_INET,
+				  .sin_port = htons(TEST_PORT),
+				  .sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	int s;
+
+	/* squat on the port so the listener cannot bind */
+	s = socket(AF_INET, SOCK_STREAM, 0);
+	ASSERT_GE(s, 0);
+	ASSERT_EQ(0, bind(s, (struct sockaddr *)&s4, sizeof(s4)));
+
+	EXPECT_EQ(-EADDRINUSE, listener_set(attrs, off));
+	EXPECT_STRNE("", last_extack);
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+	close(s);
+}
+
+/* ============ one rpcbind attempt for each request ============ */
+
+/*
+ * Every listener used to register on its own, so a rpcbind that never
+ * answers cost one timeout for each entry. Ask for one listener, then for
+ * three, and compare what the stub saw. Three entries must not cost three
+ * times as much.
+ *
+ * The stub has to stay silent rather than refuse. A refusal is an answer,
+ * and rpcbind refuses one entry at a time, so the count ignores it.
+ */
+TEST_F(nfsd_listener, rpcb_stop_after_failure)
+{
+	int before, one, three, off;
+	char attrs[192];
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	listener_set(attrs, off);
+	one = rpcb_calls() - before;
+	ASSERT_GT(one, 0);
+
+	ASSERT_EQ(0, listener_set(attrs, 0));
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	listener_set(attrs, off);
+	three = rpcb_calls() - before;
+
+	/* the second and third entries must not reach rpcbind at all */
+	EXPECT_LE(three, one);
+}
+
+/*
+ * The entry that finds rpcbind silent is the one that pays for the
+ * discovery, and v3 has no vs_rpcb_optnl to discard the error, so it is the
+ * only entry whose listener would be lost. Nothing distinguishes it from the
+ * rest of the request, and a retry of the same request would fail the same
+ * entry again, so the set would stay short for as long as rpcbind was quiet.
+ *
+ * Ask for three listeners against a silent stub and require the whole set,
+ * a success, and a warning that says why.
+ */
+TEST_F(nfsd_listener, rpcb_silent_set_complete)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[192];
+	int off;
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	EXPECT_EQ(0, listener_set(attrs, off));
+
+	/* the first entry is not the odd one out */
+	EXPECT_EQ(3, listener_get(got, MAX_LISTENERS));
+	/* no errno reports this, so the ack has to */
+	EXPECT_STRNE("", last_extack);
+}
+
+/*
+ * The case that needs the count rather than a failed listener. NFSv4 sets
+ * vs_rpcb_optnl, so svc_generic_rpcbind_set() discards the error, every
+ * listener comes up, and nothing reports a failure. Without the fix each
+ * entry still waits for rpcbind on its own.
+ *
+ * Make the server v4-only, answer no SET, and require three things: the
+ * listeners come up, the ack warns that they are not registered, and the
+ * stub does not see one round trip for each entry.
+ */
+TEST_F(nfsd_listener, rpcb_v4_only_bounded)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	int before, one, three, off;
+	char attrs[192];
+
+	/* refuses once a serv exists, so this has to come first */
+	ASSERT_EQ(0, version_set_only(4, 1));
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	one = rpcb_calls() - before;
+	ASSERT_GT(one, 0);
+
+	/* start over, so the second measurement also builds a serv */
+	ASSERT_EQ(0, listener_set(attrs, 0));
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	three = rpcb_calls() - before;
+
+	/* the listeners are up even though rpcbind never answered */
+	EXPECT_EQ(3, listener_get(got, MAX_LISTENERS));
+	/* and the ack says they are unregistered, since no errno can */
+	EXPECT_STRNE("", last_extack);
+	EXPECT_LE(three, one);
+}
+
+/*
+ * The stop applies to one request only. After rpcbind starts answering,
+ * the next request must register without any other step.
+ */
+TEST_F(nfsd_listener, rpcb_retry_next_request)
+{
+	int before, after, off;
+	char attrs[192];
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	listener_set(attrs, off);
+	ASSERT_EQ(0, listener_set(attrs, 0));
+
+	/* rpcbind recovers */
+	rpcb_stub_set_mode(RPCB_STUB_ACCEPT);
+
+	before = rpcb_calls();
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	EXPECT_EQ(0, listener_set(attrs, off));
+	after = rpcb_calls();
+
+	/* a fresh request starts from a fresh reading and tries again */
+	EXPECT_GT(after, before);
+	EXPECT_STREQ("", last_extack);
+}
+
+/*
+ * The same rule on the way out. Removing a listener unregisters it, so a
+ * rpcbind that stops answering used to cost one timeout for each listener
+ * removed. Register one listener while the stub answers, silence the stub,
+ * remove it and count; then do the same with three.
+ *
+ * Both measurements also pay the svc_unregister() sweep that
+ * nfsd_destroy_serv() runs once the last listener is gone, so that cancels
+ * out of the comparison.
+ */
+TEST_F(nfsd_listener, rpcb_unreg_stop_after_failure)
+{
+	int before, one, three, off;
+	char attrs[192];
+
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+	before = rpcb_calls();
+	ASSERT_EQ(0, listener_set(NULL, 0));
+	one = rpcb_calls() - before;
+	ASSERT_GT(one, 0);
+
+	rpcb_stub_set_mode(RPCB_STUB_ACCEPT);
+	off = put_listener(attrs, 0, "tcp", TEST_PORT);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 1);
+	off = put_listener(attrs, off, "tcp", TEST_PORT + 2);
+	ASSERT_EQ(0, listener_set(attrs, off));
+
+	rpcb_stub_set_mode(RPCB_STUB_SILENT);
+	before = rpcb_calls();
+	ASSERT_EQ(0, listener_set(NULL, 0));
+	three = rpcb_calls() - before;
+
+	/* the second and third removals must not reach rpcbind at all */
+	EXPECT_LE(three, one);
+}
+
+/* ===================== threads / -EBUSY semantics ===================== */
+
+TEST_F(nfsd_listener, sem_busy_on_change)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64], two[128];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+	int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+	o2 = put_listener(two, o2, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, threads_set(1));			/* threads now running */
+	EXPECT_EQ(-EBUSY, listener_set(two, o2));	/* add refused */
+
+	/* refused means refused: the udp listener must not have been added */
+	EXPECT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+
+	threads_set(0);					/* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, threads_set(1));
+	EXPECT_EQ(-EBUSY, listener_set(NULL, 0));	/* remove refused */
+
+	/* the doit moves the permsocks to a temp list before it can fail */
+	EXPECT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+
+	threads_set(0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/nfsd/settings b/tools/testing/selftests/nfsd/settings
new file mode 100644
index 0000000000000..6091b45d226ba
--- /dev/null
+++ b/tools/testing/selftests/nfsd/settings
@@ -0,0 +1 @@
+timeout=120

23/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 25604
							output: 23
							thoughts: 653
						

Thoughts:
**Analyzing the Patch for KMSAN Fuzzing Potential**

Okay, let's break down this patch and figure out if it's worth setting up a KMSAN fuzzing session. The patch is primarily focused on NFS-related network changes.

First, it updates the `MAINTAINERS` file. That's not particularly interesting from a fuzzing perspective, so we can ignore that. The meat of the changes seems to be in `fs/nfsd/nfsctl.c`. Here, we see additions of `nfsd_nl_transport_supported` which looks like it validates transport names such as "tcp", "udp", and "rdma". There's also a `NFSD_NL_LISTENER_MAX` constant, setting a limit on the number of listeners, along with bounds checking in `nfsd_nl_validate_listeners()`. Looks like there are also additions to error handling: `bad_attr`, `bad_xprt`, `rpcb_failures`, `skipped_rpcb` are checked, along with `svc_rpcb_failure_count(serv)`. It also looks like the code is retrying `svc_xprt_create_from_sa` with `SVC_SOCK_ANONYMOUS` if rpcbind fails. Plus there's setting of extended ack messages via `NL_SET_ERR_MSG_FMT`. These error handling paths are definitely potential targets, especially if we can trigger specific failure conditions.

The `include/linux/sunrpc/clnt.h` and `include/linux/sunrpc/svc.h` files are updated with `atomic_t sv_rpcb_failures` to `struct svc_serv`, and prototype changes. This indicates related RPCbind changes are happening.

`net/sunrpc/rpcb_clnt.c` has a static timeout for the local rpcbind client and updates to error handling in `rpcb_register_call`. Returning `-EIO` if rpcbind isn't functional sounds interesting. Another potential target.

In `net/sunrpc/svc.c`, we're seeing updates to a bunch of functions: `svc_generic_rpcbind_set`, `svc_rpcb_failure_count`, `svc_register`, `__svc_unregister`, and `svc_unregister`. Definitely RPCbind interactions happening.

`net/sunrpc/svc_xprt.c` adds `svc_xprt_clear_rpcb_unreg` and updates `svc_delete_xprt`. It all looks like the code has some complexity related to RPCbind.

Finally, we've got updates to the selftests in `tools/testing/selftests/nfsd/...`. These tests might be useful in constructing a fuzzing harness.

Now, the important part: potential uninitialized memory risks. Let's dig in. I need to make sure `nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])` in `nfsd_nl_validate_listeners` is safe. Looks like it checks `tb[NFSD_A_SOCK_TRANSPORT_NAME]` before accessing its data. But, wait, is `nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])` NUL-terminated, or is there a bounds check? Need to verify. To be safe, I'll need to check the policy: `nfsd_sock_nl_policy`. Let's fire up `grepper` and see what that policy looks like. If the policy ensures the data is safe, then this patch looks fairly safe. But if it doesn't, we might have an interesting target!



24/3 2026/09/01 00:21 tool grepper
0m Args:
{
  "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=2067=int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n--\nfs/nfsd/nfsctl.c-2118-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2119:\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\nfs/nfsd/nfsctl.c-2120-\t\t\tcontinue;\n--\nfs/nfsd/nfsctl.c-2176-\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\nfs/nfsd/nfsctl.c:2177:\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\nfs/nfsd/nfsctl.c-2178-\t\t\tcontinue;\n"
}

25/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 27903
							output: 37
							thoughts: 25
						

Thoughts:
Here's a summary of my thought process, as requested:

**Diving into `netlink.c` for NFSd**

Okay, time to dig in. Based on what I've been seeing, my hunch is that the issue might be related to how we're handling netlink communication within the NFS server. Let's start by focusing on `fs/nfsd/netlink.c` – that's the obvious place to begin. My immediate target is around lines 35-50. I'm thinking that area likely handles the initial setup or perhaps the early message handling for netlink. I need to refresh my memory on the interactions there, perhaps there is some initialization being done. I need to see how the connection is being established and if there are any obvious race conditions or unexpected behaviors in that specific block of code that could be causing the observed problem. Let's see what we can find.



26/3 2026/09/01 00:21 tool read-file
0m Args:
{
  "File": "fs/nfsd/netlink.c",
  "FirstLine": 35,
  "LineCount": 20
}

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  50:\t\t[NFSD_A_SVC_EXPORT_ANON_UID] = { .type = NLA_U32, },\n  51:\t\t[NFSD_A_SVC_EXPORT_ANON_GID] = { .type = NLA_U32, },\n  52:\t\t[NFSD_A_SVC_EXPORT_FSLOCATIONS] = NLA_POLICY_NESTED(nfsd_fslocations_nl_policy),\n  53:\t\t[NFSD_A_SVC_EXPORT_UUID] = { .type = NLA_BINARY, },\n  54:\t\t[NFSD_A_SVC_EXPORT_SECINFO] = NLA_POLICY_NESTED(nfsd_auth_flavor_nl_policy),\n"
}

27/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 28756
							output: 40
							thoughts: 55
						

Thoughts:
Okay, here's my interpretation of those thoughts, presented as my own mental process, given my expertise:

**Checking the NFS Daemon's Network Socket Transportation**

Alright, so `NFSD_A_SOCK_TRANSPORT_NAME` is defined as `NLA_NUL_STRING`. That's interesting, but not entirely unexpected. Now, I need to understand what this means in the broader context of the NFS daemon's network interface. Let's see... the `NLA_NUL_STRING` suggests it's likely a null-terminated string, and `NFSD_A_SOCK_TRANSPORT_NAME` probably refers to the name associated with a specific socket or transport within the NFS daemon.

To get the full picture, I should probably check the `fs/nfsd/nfsctl.c` file, specifically around the `nfsd_nl_listener_set_doit` function. That function likely handles setting up or configuring the network listener for the NFS daemon. Looking there will tell me *how* this transport name is used, how it's set, and what it relates to in terms of the actual socket creation or configuration. It's the logical next step to understand the connection between the definition and its purpose within the NFS daemon's networking infrastructure. I'm especially keen to see how the code handles different network protocols or potentially the transition to different transports. Understanding this could be key to debugging or modifying how the NFS daemon handles network connections.



28/3 2026/09/01 00:21 tool read-file
0m Args:
{
  "File": "fs/nfsd/nfsctl.c",
  "FirstLine": 2160,
  "LineCount": 80
}

Results:
{
  "Contents": "2160:\t\t * remaining listeners and recreate the list.\n2161:\t\t */\n2162:\t\tif (delete)\n2163:\t\t\tsvc_xprt_destroy_all(serv, net, false);\n2164:\t\n2165:\t\trpcb_failures = svc_rpcb_failure_count(serv);\n2166:\t\n2167:\t\t/* walk list of addrs again, open any that still don't exist */\n2168:\t\tnlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info-\u003enlhdr,\n2169:\t\t\t\t\t GENL_HDRLEN, rem) {\n2170:\t\t\tstruct nlattr *tb[NFSD_A_SOCK_MAX + 1];\n2171:\t\t\tconst char *xcl_name;\n2172:\t\t\tstruct sockaddr *sa;\n2173:\t\t\tint flags, ret;\n2174:\t\n2175:\t\t\t/* validated up front in nfsd_nl_validate_listeners() */\n2176:\t\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\n2177:\t\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\n2178:\t\t\t\tcontinue;\n2179:\t\n2180:\t\t\txcl_name = nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME]);\n2181:\t\t\tsa = nla_data(tb[NFSD_A_SOCK_ADDR]);\n2182:\t\n2183:\t\t\txprt = svc_find_listener(serv, xcl_name, net, sa);\n2184:\t\t\tif (xprt) {\n2185:\t\t\t\tif (delete)\n2186:\t\t\t\t\tWARN_ONCE(1, \"Transport type=%s already exists\\n\",\n2187:\t\t\t\t\t\t  xcl_name);\n2188:\t\t\t\tsvc_xprt_put(xprt);\n2189:\t\t\t\tcontinue;\n2190:\t\t\t}\n2191:\t\n2192:\t\t\tflags = skipped_rpcb ? SVC_SOCK_ANONYMOUS : 0;\n2193:\t\t\tret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, flags,\n2194:\t\t\t\t\t\t      current_cred());\n2195:\t\n2196:\t\t\tif (!skipped_rpcb \u0026\u0026\n2197:\t\t\t    svc_rpcb_failure_count(serv) != rpcb_failures) {\n2198:\t\t\t\tskipped_rpcb = true;\n2199:\t\t\t\tif (ret \u003c 0)\n2200:\t\t\t\t\tret = svc_xprt_create_from_sa(serv, xcl_name,\n2201:\t\t\t\t\t\t\t\t      net, sa,\n2202:\t\t\t\t\t\t\t\t      SVC_SOCK_ANONYMOUS,\n2203:\t\t\t\t\t\t\t\t      current_cred());\n2204:\t\t\t}\n2205:\t\n2206:\t\t\t/* always save the latest error */\n2207:\t\t\tif (ret \u003c 0) {\n2208:\t\t\t\tbad_attr = attr;\n2209:\t\t\t\tbad_xprt = xcl_name;\n2210:\t\t\t\terr = ret;\n2211:\t\t\t}\n2212:\t\t}\n2213:\t\n2214:\t\t/*\n2215:\t\t * The ack carries the errno of the last entry that failed. Point at\n2216:\t\t * that entry as well, since several entries can share a transport\n2217:\t\t * name and the errno alone cannot tell them apart.\n2218:\t\t */\n2219:\t\tif (err) {\n2220:\t\t\tNL_SET_BAD_ATTR(info-\u003eextack, bad_attr);\n2221:\t\t\tif (skipped_rpcb)\n2222:\t\t\t\tNL_SET_ERR_MSG_FMT(info-\u003eextack,\n2223:\t\t\t\t\t\t   \"cannot create %s listener; rpcbind did not answer\",\n2224:\t\t\t\t\t\t   bad_xprt);\n2225:\t\t\telse\n2226:\t\t\t\tNL_SET_ERR_MSG_FMT(info-\u003eextack,\n2227:\t\t\t\t\t\t   \"cannot create %s listener\",\n2228:\t\t\t\t\t\t   bad_xprt);\n2229:\t\t} else if (skipped_rpcb) {\n2230:\t\t\tNL_SET_ERR_MSG(info-\u003eextack,\n2231:\t\t\t\t       \"rpcbind did not answer, some listeners are not registered\");\n2232:\t\t}\n2233:\t\n2234:\t\tif (!serv-\u003esv_nrthreads \u0026\u0026 list_empty(\u0026nn-\u003enfsd_serv-\u003esv_permsocks))\n2235:\t\t\tnfsd_destroy_serv(net);\n2236:\t\n2237:\tout_unlock_mtx:\n2238:\t\tmutex_unlock(\u0026nfsd_mutex);\n2239:\t\n"
}

29/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 30447
							output: 20
							thoughts: 37
						

Thoughts:
**Investigating the Allocation and Initialization of `svc_serv`**

Okay, let's dive in. I need to understand how the `svc_serv` structure is being handled from the ground up. Specifically, I'm concerned with its allocation and initialization process when a new service is created. My immediate action is to locate the code responsible for these tasks. So, I'll be searching through the codebase for functions like `svc_create` – that's the obvious candidate – or anything related to its creation and setup. Also, it might be connected with failure handling. So, let's also search for something like `sv_rpcb_failures`. This should lead me to the core logic, or at least a significant clue. Understanding this critical aspect will be key to troubleshooting any related issues and optimizing performance. I'll need to scrutinize the calling context, memory management, and how this structure is integrated within the broader RPC framework.



30/3 2026/09/01 00:21 tool grepper
0m Args:
{
  "Expression": "sv_rpcb_failures"
}

Results:
{
  "Output": "include/linux/sunrpc/svc.h=72=struct svc_serv {\n--\ninclude/linux/sunrpc/svc.h-80-\tunsigned int\t\tsv_xdrsize;\t/* XDR buffer size */\ninclude/linux/sunrpc/svc.h:81:\tatomic_t\t\tsv_rpcb_failures; /* unanswered rpcbind calls */\ninclude/linux/sunrpc/svc.h-82-\tstruct list_head\tsv_permsocks;\t/* all permanent sockets */\n--\nnet/sunrpc/svc.c=1210=unsigned int svc_rpcb_failure_count(struct svc_serv *serv)\nnet/sunrpc/svc.c-1211-{\nnet/sunrpc/svc.c:1212:\treturn atomic_read(\u0026serv-\u003esv_rpcb_failures);\nnet/sunrpc/svc.c-1213-}\n--\nnet/sunrpc/svc.c=1226=int svc_register(struct svc_serv *serv, struct net *net,\n--\nnet/sunrpc/svc.c-1267-\tif (noanswer)\nnet/sunrpc/svc.c:1268:\t\tatomic_inc(\u0026serv-\u003esv_rpcb_failures);\nnet/sunrpc/svc.c-1269-\n"
}

31/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 31029
							output: 20
							thoughts: 20
						

Thoughts:
Here's my take on those thoughts:

**Analyzing `svc_create` in `net/sunrpc/svc.c`**

Okay, time to dive into `net/sunrpc/svc.c` and specifically, the `svc_create` function. I need to refresh my memory on how the RPC service creation process is handled at the core level. I want to check how the service is initialized, resource allocation, and any potential points of contention or inefficiencies. I'm looking for things like memory leaks, unnecessary copies, or poorly optimized locking strategies in this crucial initialization phase. This is a foundational function, so any issues here could ripple outwards and impact overall RPC performance and stability. I need to understand how this function interfaces with the lower-level network stack and the upper-level RPC protocol implementation. Let's see what's what.



32/3 2026/09/01 00:21 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "kzalloc.*svc_serv"
}

Results:
{
  "error": "no matches"
}

33/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 31261
							output: 31
							thoughts: 20
						

Thoughts:
Here's my thought process, as it stands:

**Locating the RPC Service Creation Function**

Alright, time to get down to business. I need to track down the `svc_create` function. I know it's related to the SunRPC system, so that narrows the search considerably. Thinking practically, the most logical place for a function responsible for *creating* a service within the kernel's SunRPC implementation would be in the `net/sunrpc` directory. Within that, I'm going to bet it's in a file that's pretty central to the service handling. I'd wager it's likely to be named something like `svc.c`, which is a common naming convention for service-related code. So, the likely location is `net/sunrpc/svc.c`. Let's get to it!



34/3 2026/09/01 00:21 tool grepper
0m Args:
{
  "Expression": "svc_create",
  "PathPrefix": "net/sunrpc/svc.c"
}

Results:
{
  "Output": "net/sunrpc/svc.c=391=static struct svc_serv *\nnet/sunrpc/svc.c:392:__svc_create(struct svc_program *prog, int nprogs, struct svc_stat *stats,\nnet/sunrpc/svc.c-393-\t     unsigned int bufsize, int npools, int (*threadfn)(void *data))\n--\nnet/sunrpc/svc.c-464-/**\nnet/sunrpc/svc.c:465: * svc_create - Create an RPC service\nnet/sunrpc/svc.c-466- * @prog: the RPC program the new service will handle\n--\nnet/sunrpc/svc.c-471- */\nnet/sunrpc/svc.c:472:struct svc_serv *svc_create(struct svc_program *prog, unsigned int bufsize,\nnet/sunrpc/svc.c-473-\t\t\t    int (*threadfn)(void *data))\nnet/sunrpc/svc.c-474-{\nnet/sunrpc/svc.c:475:\treturn __svc_create(prog, 1, NULL, bufsize, 1, threadfn);\nnet/sunrpc/svc.c-476-}\nnet/sunrpc/svc.c:477:EXPORT_SYMBOL_GPL(svc_create);\nnet/sunrpc/svc.c-478-\nnet/sunrpc/svc.c-479-/**\nnet/sunrpc/svc.c:480: * svc_create_pooled - Create an RPC service with pooled threads\nnet/sunrpc/svc.c-481- * @prog:  Array of RPC programs the new service will handle\n--\nnet/sunrpc/svc.c-488- */\nnet/sunrpc/svc.c:489:struct svc_serv *svc_create_pooled(struct svc_program *prog,\nnet/sunrpc/svc.c-490-\t\t\t\t   unsigned int nprogs,\n--\nnet/sunrpc/svc.c-500-\nnet/sunrpc/svc.c:501:\tserv = __svc_create(prog, nprogs, stats, bufsize, npools, threadfn);\nnet/sunrpc/svc.c-502-\tif (!serv)\n--\nnet/sunrpc/svc.c-509-}\nnet/sunrpc/svc.c:510:EXPORT_SYMBOL_GPL(svc_create_pooled);\nnet/sunrpc/svc.c-511-\n"
}

35/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 32115
							output: 36
							thoughts: 21
						

Thoughts:
Here's a first-person summary, tailored as requested:

**Diving into `__svc_create`**

Okay, time to dissect `__svc_create` within `net/sunrpc/svc.c`. I'm familiar with the overall RPC infrastructure, so I'm expecting this function to be a key element in establishing a new service endpoint. Given the naming convention, the double underscore suggests this is likely an internal function, not directly exposed to user space. Therefore, it's probably the foundation upon which higher-level service creation is built. I'll need to pay close attention to the data structures it manipulates, especially anything related to `svc_rqst`, `svc_serv`, and any configuration settings it might be pulling from somewhere. I expect to see allocation of resources, initialization of critical fields within the RPC service descriptor, and perhaps some network socket setup if it handles transport directly. I also want to understand how it handles concurrency and locking, considering this is a network service. This is where the core of the RPC service magic happens, so I need to read through this line by line. Let's get to it.



36/3 2026/09/01 00:21 tool read-file
0m Args:
{
  "File": "net/sunrpc/svc.c",
  "FirstLine": 390,
  "LineCount": 40
}

Results:
{
  "Contents": " 390:\t */\n 391:\tstatic struct svc_serv *\n 392:\t__svc_create(struct svc_program *prog, int nprogs, struct svc_stat *stats,\n 393:\t\t     unsigned int bufsize, int npools, int (*threadfn)(void *data))\n 394:\t{\n 395:\t\tstruct svc_serv\t*serv;\n 396:\t\tunsigned int vers;\n 397:\t\tunsigned int xdrsize;\n 398:\t\tunsigned int i;\n 399:\t\n 400:\t\tif (!(serv = kzalloc_obj(*serv)))\n 401:\t\t\treturn NULL;\n 402:\t\tserv-\u003esv_name      = prog-\u003epg_name;\n 403:\t\tserv-\u003esv_programs  = prog;\n 404:\t\tserv-\u003esv_nprogs    = nprogs;\n 405:\t\tserv-\u003esv_stats     = stats;\n 406:\t\tif (bufsize \u003e RPCSVC_MAXPAYLOAD)\n 407:\t\t\tbufsize = RPCSVC_MAXPAYLOAD;\n 408:\t\tserv-\u003esv_max_payload = bufsize? bufsize : 4096;\n 409:\t\tserv-\u003esv_max_mesg  = roundup(serv-\u003esv_max_payload + PAGE_SIZE, PAGE_SIZE);\n 410:\t\tserv-\u003esv_threadfn = threadfn;\n 411:\t\txdrsize = 0;\n 412:\t\tfor (i = 0; i \u003c nprogs; i++) {\n 413:\t\t\tstruct svc_program *progp = \u0026prog[i];\n 414:\t\n 415:\t\t\tprogp-\u003epg_lovers = progp-\u003epg_nvers-1;\n 416:\t\t\tfor (vers = 0; vers \u003c progp-\u003epg_nvers ; vers++)\n 417:\t\t\t\tif (progp-\u003epg_vers[vers]) {\n 418:\t\t\t\t\tprogp-\u003epg_hivers = vers;\n 419:\t\t\t\t\tif (progp-\u003epg_lovers \u003e vers)\n 420:\t\t\t\t\t\tprogp-\u003epg_lovers = vers;\n 421:\t\t\t\t\tif (progp-\u003epg_vers[vers]-\u003evs_xdrsize \u003e xdrsize)\n 422:\t\t\t\t\t\txdrsize = progp-\u003epg_vers[vers]-\u003evs_xdrsize;\n 423:\t\t\t\t}\n 424:\t\t}\n 425:\t\tserv-\u003esv_xdrsize   = xdrsize;\n 426:\t\tINIT_LIST_HEAD(\u0026serv-\u003esv_tempsocks);\n 427:\t\tINIT_LIST_HEAD(\u0026serv-\u003esv_permsocks);\n 428:\t\ttimer_setup(\u0026serv-\u003esv_temptimer, NULL, 0);\n 429:\t\tspin_lock_init(\u0026serv-\u003esv_lock);\n"
}

37/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 33142
							output: 40
							thoughts: 888
						

Thoughts:
**Analyzing Variable Initialization and Potential Race Conditions**

Okay, so `kzalloc_obj(*serv)` should zero out the entire `struct svc_serv`, which means `sv_rpcb_failures` starts at zero, as expected. Good. Now, let's step through the other code changes in this patch, making sure we're on top of the initialization.

First, `rpcb_register_call`: the `result` variable is definitely initialized to 0. That's fine. We're looking for any potential for uninitialized variable reads or races.

Next up, `svc_register`. Yep, `error` is properly initialized to 0. Nothing there to worry about.

`svc_xprt_clear_rpcb_unreg`... This one looks straightforward; it iterates through a list, clearing a bit. No initialization concerns I can spot here.

Finally, `nfsd_nl_listener_set_doit`. Alright, here we go... It looks like `bad_attr`, `bad_xprt`, and `skipped_rpcb` are all initialized as expected to `NULL`, `NULL`, and `false`. The `rpcb_failures` is initialized using `svc_rpcb_failure_count(serv)` as expected. The logic seems to rely on the initialization. It appears that `bad_attr` and `bad_xprt` are only assigned when `err` is non-zero within the loop. Hmm... *is* `err` initialized to zero *before* the loop, though? Gotta be sure. Let me quickly jump over to `fs/nfsd/nfsctl.c` and confirm that. I don't want to miss a subtle race or initialization bug here.



38/3 2026/09/01 00:21 tool read-file
0m Args:
{
  "File": "fs/nfsd/nfsctl.c",
  "FirstLine": 2065,
  "LineCount": 55
}

Results:
{
  "Contents": "2065:\t * Return 0 on success or a negative errno.\n2066:\t */\n2067:\tint nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)\n2068:\t{\n2069:\t\tstruct net *net = genl_info_net(info);\n2070:\t\tconst struct nlattr *bad_attr = NULL;\n2071:\t\tstruct svc_xprt *xprt, *tmp;\n2072:\t\tconst char *bad_xprt = NULL;\n2073:\t\tunsigned int rpcb_failures;\n2074:\t\tconst struct nlattr *attr;\n2075:\t\tbool skipped_rpcb = false;\n2076:\t\tstruct svc_serv *serv;\n2077:\t\tLIST_HEAD(permsocks);\n2078:\t\tstruct nfsd_net *nn;\n2079:\t\tbool delete = false;\n2080:\t\tint err, rem;\n2081:\t\n2082:\t\t/*\n2083:\t\t * Validate the entire listener list before making any changes, so a\n2084:\t\t * malformed request fails cleanly without creating a serv or touching\n2085:\t\t * the existing listeners.\n2086:\t\t */\n2087:\t\terr = nfsd_nl_validate_listeners(info);\n2088:\t\tif (err)\n2089:\t\t\treturn err;\n2090:\t\n2091:\t\tmutex_lock(\u0026nfsd_mutex);\n2092:\t\n2093:\t\terr = nfsd_create_serv(net);\n2094:\t\tif (err) {\n2095:\t\t\tmutex_unlock(\u0026nfsd_mutex);\n2096:\t\t\treturn err;\n2097:\t\t}\n2098:\t\n2099:\t\tnn = net_generic(net, nfsd_net_id);\n2100:\t\tserv = nn-\u003enfsd_serv;\n2101:\t\n2102:\t\tspin_lock_bh(\u0026serv-\u003esv_lock);\n2103:\t\n2104:\t\t/* Move all of the old listener sockets to a temp list */\n2105:\t\tlist_splice_init(\u0026serv-\u003esv_permsocks, \u0026permsocks);\n2106:\t\n2107:\t\t/*\n2108:\t\t * Walk the list of server_socks from userland and move any that match\n2109:\t\t * back to sv_permsocks\n2110:\t\t */\n2111:\t\tnlmsg_for_each_attr_type(attr, NFSD_A_SERVER_SOCK_ADDR, info-\u003enlhdr,\n2112:\t\t\t\t\t GENL_HDRLEN, rem) {\n2113:\t\t\tstruct nlattr *tb[NFSD_A_SOCK_MAX + 1];\n2114:\t\t\tconst char *xcl_name;\n2115:\t\t\tstruct sockaddr *sa;\n2116:\t\n2117:\t\t\t/* validated up front in nfsd_nl_validate_listeners() */\n2118:\t\t\tif (nla_parse_nested(tb, NFSD_A_SOCK_MAX, attr,\n2119:\t\t\t\t\t     nfsd_sock_nl_policy, info-\u003eextack) \u003c 0)\n"
}

39/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 35306
							output: 40
							thoughts: 68
						

Thoughts:
**Analyzing the NFS Daemon Creation**

Okay, so I'm looking at this `nfsd_create_serv` function, and I'm tracking the return value, `err`. Right, it returns 0 on success, so if everything goes smoothly, `err` will be 0. Now, let's see how that propagates through the subsequent logic. Specifically, I'm diving into lines 2102-2160, and I need to zoom in on 2120-2165. This is where the magic happens, or potentially fails. I'm going to carefully dissect these lines to understand what happens to `err` if `nfsd_create_serv` *did* succeed (i.e., `err` is 0). It's all about how these subsequent operations handle that success condition and what actions get taken.



40/3 2026/09/01 00:21 tool read-file
0m Args:
{
  "File": "fs/nfsd/nfsctl.c",
  "FirstLine": 2120,
  "LineCount": 45
}

Results:
{
  "Contents": "2120:\t\t\t\tcontinue;\n2121:\t\n2122:\t\t\txcl_name = nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME]);\n2123:\t\t\tsa = nla_data(tb[NFSD_A_SOCK_ADDR]);\n2124:\t\n2125:\t\t\t/* Put back any matching sockets */\n2126:\t\t\tlist_for_each_entry_safe(xprt, tmp, \u0026permsocks, xpt_list) {\n2127:\t\t\t\t/* This shouldn't be possible */\n2128:\t\t\t\tif (WARN_ON_ONCE(xprt-\u003expt_net != net)) {\n2129:\t\t\t\t\tlist_move(\u0026xprt-\u003expt_list, \u0026serv-\u003esv_permsocks);\n2130:\t\t\t\t\tcontinue;\n2131:\t\t\t\t}\n2132:\t\n2133:\t\t\t\t/* If everything matches, put it back */\n2134:\t\t\t\tif (!strcmp(xprt-\u003expt_class-\u003excl_name, xcl_name) \u0026\u0026\n2135:\t\t\t\t    rpc_cmp_addr_port(sa, (struct sockaddr *)\u0026xprt-\u003expt_local)) {\n2136:\t\t\t\t\tlist_move(\u0026xprt-\u003expt_list, \u0026serv-\u003esv_permsocks);\n2137:\t\t\t\t\tbreak;\n2138:\t\t\t\t}\n2139:\t\t\t}\n2140:\t\t}\n2141:\t\n2142:\t\t/*\n2143:\t\t * If there are listener transports remaining on the permsocks list,\n2144:\t\t * it means we were asked to remove a listener.\n2145:\t\t */\n2146:\t\tif (!list_empty(\u0026permsocks)) {\n2147:\t\t\tlist_splice_init(\u0026permsocks, \u0026serv-\u003esv_permsocks);\n2148:\t\t\tdelete = true;\n2149:\t\t}\n2150:\t\tspin_unlock_bh(\u0026serv-\u003esv_lock);\n2151:\t\n2152:\t\t/* Do not remove listeners while there are active threads. */\n2153:\t\tif (serv-\u003esv_nrthreads) {\n2154:\t\t\terr = -EBUSY;\n2155:\t\t\tgoto out_unlock_mtx;\n2156:\t\t}\n2157:\t\n2158:\t\t/*\n2159:\t\t * Since we can't delete an arbitrary llist entry, destroy the\n2160:\t\t * remaining listeners and recreate the list.\n2161:\t\t */\n2162:\t\tif (delete)\n2163:\t\t\tsvc_xprt_destroy_all(serv, net, false);\n2164:\t\n"
}

41/3 2026/09/01 00:21 llm kmsan-evaluator
0m Model:
gemini-3.7-flash

Args:
null

Results:
null

Tokens:
							input: 36297
							output: 265
							thoughts: 720
						

Thoughts:
**My KMSAN Analysis**

Okay, let's break this down. From what I see here, the core of the patch revolves around enhancing the generic-netlink listener management within NFSD. It seems we're improving input validation, specifically for transport names, and implementing a limit on the number of listeners. We're also making the handling of rpcbind failures more robust through a failure counter (`sv_rpcb_failures`) and adding some self-tests to boot.

Now, my main concern is whether this introduces any potential uninitialized memory reads that would require KMSAN. Let's trace it carefully. The code's structure appears pretty straightforward. The `err` variable is initialized to 0, or exited via a -EBUSY. A negative return value in the second loop sets `err`, `bad_attr` and `bad_xprt`. If `err` is non-zero, then those bad variables are valid, which looks right.

Thinking about KMSAN specifically, we need to consider these points: Are we sending kernel structures to userspace, or using uninitialized padding in the network? It looks like we're not. We're dealing with netlink extack messages and return codes. These are going to be initialized strings and standard helpers. No raw structs or uninitialized memory are going out.

Next, are there any conditionals or branches that are influenced by uninitialized variables? Nope. Everything is initialized: `skipped_rpcb`, `rpcb_failures`, the aforementioned `err`, `bad_attr`, `bad_xprt`, `noanswer`, `error`, and `ret`.

And finally, how about data structures? Are we allocating things and leaving uninitialized parts around that could be read later? Here, `atomic_t sv_rpcb_failures` in `svc_serv` is allocated with `kzalloc_obj(*serv)` so that's zero-initialized. I see that there are bounds checks with `NFSD_NL_LISTENER_MAX`, and that the transport names are validated.

Based on all of this, it looks like there's no need for KMSAN here. Standard testing and KASAN should be sufficient to catch any issues. No new kernel data structures are exposed to userspace. All new local variables and fields (like `sv_rpcb_failures`) are properly initialized and control flow depends solely on initialized variables. Any potential issues with concurrency, refcounting, memory leaks, or out-of-bounds access would be detected by standard sanitizers. No uninitialized memory usage exists, so KMSAN isn't needed.

I've covered all the bases. I'm confident in saying that `NeedsKMSAN=false` for this. Now, let's move forward and update the results.



42/3 2026/09/01 00:21 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch set enhances NFSD generic netlink listener management by validating transport names early, limiting the maximum number of listeners per request, tracking rpcbind failures via an atomic counter in struct svc_serv, and adding comprehensive kselftests.\n\nKMSAN vs KASAN applicability analysis:\n1. No uninitialized memory or structure padding is copied to user space: netlink extack strings and return codes use constant strings or formatted messages with valid attributes.\n2. The newly added struct member (sv_rpcb_failures in struct svc_serv) is allocated using kzalloc_obj (*serv) in __svc_create(), ensuring it is always zero-initialized.\n3. All local variables and branching conditions (e.g. skipped_rpcb, bad_attr, bad_xprt, rpcb_failures, noanswer) are initialized before use.\n4. Any potential issues with listener lists, locking, or socket teardown are standard lifecycle and synchronization concerns covered by KASAN and LOCKDEP.\n\nTherefore, this patch series does not introduce or expose any uninitialized memory risks and does not warrant a dedicated KMSAN session.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch set enhances NFSD generic netlink listener management by validating transport names early, limiting the maximum number of listeners per request, tracking rpcbind failures via an atomic counter in struct svc_serv, and adding comprehensive kselftests.\n\nKMSAN vs KASAN applicability analysis:\n1. No uninitialized memory or structure padding is copied to user space: netlink extack strings and return codes use constant strings or formatted messages with valid attributes.\n2. The newly added struct member (sv_rpcb_failures in struct svc_serv) is allocated using kzalloc_obj (*serv) in __svc_create(), ensuring it is always zero-initialized.\n3. All local variables and branching conditions (e.g. skipped_rpcb, bad_attr, bad_xprt, rpcb_failures, noanswer) are initialized before use.\n4. Any potential issues with listener lists, locking, or socket teardown are standard lifecycle and synchronization concerns covered by KASAN and LOCKDEP.\n\nTherefore, this patch series does not introduce or expose any uninitialized memory risks and does not warrant a dedicated KMSAN session.",
  "NeedsKMSAN": false
}

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