Netdev List
 help / color / mirror / Atom feed
* [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces
@ 2026-08-10 17:38 Jeff Layton
  2026-08-10 17:38 ` [PATCH 1/7] NFSD: validate transport name in listener_set before serv creation Jeff Layton
                   ` (6 more replies)
  0 siblings, 7 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

syzbot keeps landing in nfsd_nl_listener_set_doit(), where a stall under
nfsd_mutex blocks every other NFSD netlink op. This is hardening rather than
a fix for any one report: it narrows what userland can push into that path
and shortens the worst stalls.

  1: reject transport names NFSD cannot instantiate, before nfsd_mutex is
     taken
  2: cap a listener_set request at 1024 entries
  3: stop svc_register() losing a registration error to a later program
  4: bound the local rpcbind client to a single 1s attempt
  5: report listener creation failures through extack
  6: listener_set validation tests
  7: a per-netns rpcbind stub, and the listener round-trip tests

Measured against a local rpcbind that accepts the connection and never
replies. The wait is paid per listener, since svc_xprt_create_from_sa()
passes flags of 0 and every listener therefore calls svc_register():

  per rpcbind call   10s AF_LOCAL, 60s loopback TCP  ->  1s
  per listener       20s / 120s                      ->  2s
  entries/request    bounded only by message size    ->  1024
  worst request      unbounded                       ->  ~34min

Three things this does not do:

- "rdma" is still accepted, so 1024 entries can still mean 1024
  request_module("svcrdma") upcalls under nfsd_mutex where svcrdma is
  unavailable. Not counted above.
- write_ports() reaches the same code with the same mutex held. It is
  legacy, so it is left alone.
- ~34min is still ~17x the hung-task threshold, so the reproducer should be
  expected to keep tripping the watchdog. The durable fix is to make rpcbind
  registration asynchronous so those RPCs stop running under nfsd_mutex at
  all. That needs behavioural changes we should discuss first, so it is a
  separate patchset.

Patch 3 is a flag day for CONFIG_NFS_LOCALIO=y: a registration failure now
aborts listener creation there too, matching CONFIG_NFS_LOCALIO=n. Details
in that patch.

Please consider these for v7.4.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
Jeff Layton (7):
      NFSD: validate transport name in listener_set before serv creation
      NFSD: cap the number of listeners accepted in listener_set
      SUNRPC: keep the first error in svc_register()
      SUNRPC: bound the local rpcbind client timeout to 1s
      NFSD: report listener creation failures through extack
      selftests/nfsd: exercise listener_set request validation
      selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips

 fs/nfsd/nfsctl.c                                   |  42 +-
 net/sunrpc/rpcb_clnt.c                             |  12 +
 net/sunrpc/svc.c                                   |   9 +-
 tools/testing/selftests/Makefile                   |   1 +
 tools/testing/selftests/nfsd/.gitignore            |   1 +
 tools/testing/selftests/nfsd/Makefile              |   6 +
 tools/testing/selftests/nfsd/config                |   4 +
 .../testing/selftests/nfsd/nfsd_netlink_listener.c | 920 +++++++++++++++++++++
 tools/testing/selftests/nfsd/settings              |   1 +
 9 files changed, 987 insertions(+), 9 deletions(-)
---
base-commit: 0b6d2c7e3abca8d17fddeecb6e4c32a8438ec2fb
change-id: 20260717-nfsd-nl-hang-10a3b3e93f2a

Best regards,
-- 
Jeff Layton <jlayton@kernel.org>


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 1/7] NFSD: validate transport name in listener_set before serv creation
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  2026-08-10 17:38 ` [PATCH 2/7] NFSD: cap the number of listeners accepted in listener_set Jeff Layton
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

nfsd_nl_listener_set_doit() holds nfsd_mutex across the whole listener
teardown/rebuild. NFSD_A_SOCK_TRANSPORT_NAME is only checked for
presence, not content, so an arbitrary name reaches
svc_xprt_create_from_sa(), where a name matching no registered class
triggers request_module("svc%s", name) -- a TASK_KILLABLE usermode-helper
upcall run under nfsd_mutex.

Vet the name against the classes NFSD can instantiate (tcp, udp, rdma) in
nfsd_nl_validate_listeners(), which runs before nfsd_mutex is taken.

This narrows the upcall rather than removing it. "rdma" is accepted
unconditionally, so on a kernel where svcrdma is not built it still
reaches request_module("svcrdma") under nfsd_mutex -- as it must for the
modular case, where autoloading is legitimate.

Link: https://syzkaller.appspot.com/bug?extid=c7eae0eb80858a2dba0f
Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 fs/nfsd/nfsctl.c | 24 ++++++++++++++++++++++--
 1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index 4e5e083d8477..e5844d8454b8 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -1975,14 +1975,31 @@ 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;
+}
+
 /**
  * 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
+ * a supported 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.
  *
  * Return: 0 if every entry is valid, or a negative errno otherwise.
@@ -2006,6 +2023,9 @@ static int nfsd_nl_validate_listeners(struct genl_info *info)
 		if (!tb[NFSD_A_SOCK_ADDR] || !tb[NFSD_A_SOCK_TRANSPORT_NAME])
 			return -EINVAL;
 
+		if (!nfsd_nl_transport_supported(nla_data(tb[NFSD_A_SOCK_TRANSPORT_NAME])))
+			return -EPROTONOSUPPORT;
+
 		sa = nla_data(tb[NFSD_A_SOCK_ADDR]);
 		if (nla_len(tb[NFSD_A_SOCK_ADDR]) < sizeof(sa->sa_family))
 			return -EINVAL;

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 2/7] NFSD: cap the number of listeners accepted in listener_set
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
  2026-08-10 17:38 ` [PATCH 1/7] NFSD: validate transport name in listener_set before serv creation Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  2026-08-10 17:38 ` [PATCH 3/7] SUNRPC: keep the first error in svc_register() Jeff Layton
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

nfsd_nl_listener_set_doit() matches each requested listener against the
existing set in a nested loop that is O(N * M) in the requested (N) and
existing (M) counts, run under sv_lock with bottom halves disabled. A
userland request with a very large listener list can therefore spin in
atomic context for a long time.

Reject requests carrying more than NFSD_NL_LISTENER_MAX (1024) entries in
nfsd_nl_validate_listeners(), before any lock is taken. The limit is far
above any realistic configuration.

M is not capped here: write_ports() can add listeners too, via
svc_addsock() and svc_xprt_create(). But each one costs a real socket, so M
is bounded by resources, where N was bounded only by the message size.
Capping N leaves ~1M iterations plus 1024 nla_parse_nested() calls under
sv_lock as the worst case.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 fs/nfsd/nfsctl.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index e5844d8454b8..66931caaaaed 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -1992,21 +1992,22 @@ static bool nfsd_nl_transport_supported(const char *name)
 	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 supported 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,
@@ -2015,6 +2016,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)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 3/7] SUNRPC: keep the first error in svc_register()
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
  2026-08-10 17:38 ` [PATCH 1/7] NFSD: validate transport name in listener_set before serv creation Jeff Layton
  2026-08-10 17:38 ` [PATCH 2/7] NFSD: cap the number of listeners accepted in listener_set Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  2026-08-10 17:38 ` [PATCH 4/7] SUNRPC: bound the local rpcbind client timeout to 1s Jeff Layton
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

svc_register() assigns each pg_rpcbind_set() result to the same "error"
and returns the last one. The break only leaves the version loop, so any
program after a failed one overwrites its error.

Keep the first error instead of the last.

This is a flag day for CONFIG_NFS_LOCALIO=y, which converges on the
CONFIG_NFS_LOCALIO=n behaviour (the Kconfig default):

- NFSv4 is unaffected: nfsd_version4 sets vs_rpcb_optnl, so
  svc_generic_rpcbind_set() returns 0 for it however __svc_register() went.
- nfsd_version3 does not set it, and nfsd_net_init() enables every
  supported version, so a v3-enabled server with no reachable rpcbind now
  fails to bring up any listener. svc_bind() does not catch that earlier:
  rpcb_create_local() falls through to rpcb_create_local_net(), which
  passes RPC_CLNT_CREATE_NOPING and returns 0 with nothing listening.
- A partial failure (nfsd registered, nfsacl not) tears the listener down
  but leaves the nfsd entry in rpcbind until the next svc_rpcb_setup()
  clears it.

Fixes: 642ee6b209c2 ("SUNRPC: Allow further customisation of RPC program registration")
Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 net/sunrpc/svc.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/net/sunrpc/svc.c b/net/sunrpc/svc.c
index 8297bad2b177..4f402bbf97ba 100644
--- a/net/sunrpc/svc.c
+++ b/net/sunrpc/svc.c
@@ -1208,13 +1208,16 @@ int svc_register(const struct svc_serv *serv, struct net *net,
 		struct svc_program *progp = &serv->sv_programs[p];
 
 		for (i = 0; i < progp->pg_nvers; i++) {
+			int ret;
 
-			error = progp->pg_rpcbind_set(net, progp, i,
+			ret = progp->pg_rpcbind_set(net, progp, i,
 					family, proto, port);
-			if (error < 0) {
+			if (ret < 0) {
 				printk(KERN_WARNING "svc: failed to register "
 					"%sv%u RPC service (errno %d).\n",
-					progp->pg_name, i, -error);
+					progp->pg_name, i, -ret);
+				if (!error)
+					error = ret;
 				break;
 			}
 		}

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 4/7] SUNRPC: bound the local rpcbind client timeout to 1s
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
                   ` (2 preceding siblings ...)
  2026-08-10 17:38 ` [PATCH 3/7] SUNRPC: keep the first error in svc_register() Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  2026-08-10 17:38 ` [PATCH 5/7] NFSD: report listener creation failures through extack Jeff Layton
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

The kernel's local rpcbind client runs on the transport defaults: a 10s
major timeout for AF_LOCAL, 60s for the loopback TCP fallback
(xprt_calc_majortimeo() returns to_initval when to_increment is 0).

Those calls are synchronous and run under nfsd_mutex, several per
operation: rpcb_create_local() attempts up to three client creations, and
svc_register() issues one call per program and version. A local rpcbind
that accepts the connection but never replies stalls each of them, and
the accumulated hold is enough to trip the hung-task watchdog on other
NFSD netlink ops (the holder waits killably and evades it):

  INFO: task hung in nfsd_nl_cache_flush_doit

The local rpcbind lives on loopback or an AF_LOCAL socket and answers in
microseconds, so bound its client to one attempt, 1s.

This shortens the stall rather than removing it, and it is not free.
Registration stays synchronous and stays fatal: rpcb_create_local()
failure aborts nfsd_create_serv() via svc_bind(), and svc_register()
failure makes svc_setup_socket() fail, so a rpcbind that is merely slow
to be scheduled can now fail server startup where it previously
succeeded. Making the registration asynchronous is the real fix.

Link: https://syzkaller.appspot.com/bug?extid=c7eae0eb80858a2dba0f
Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 net/sunrpc/rpcb_clnt.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/net/sunrpc/rpcb_clnt.c b/net/sunrpc/rpcb_clnt.c
index 6aa372188c86..0aa376b82a52 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;

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 5/7] NFSD: report listener creation failures through extack
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
                   ` (3 preceding siblings ...)
  2026-08-10 17:38 ` [PATCH 4/7] SUNRPC: bound the local rpcbind client timeout to 1s Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  2026-08-10 17:38 ` [PATCH 6/7] selftests/nfsd: exercise listener_set request validation Jeff Layton
  2026-08-10 17:38 ` [PATCH 7/7] selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips Jeff Layton
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

nfsd_nl_listener_set_doit() hands back the raw errno from
svc_xprt_create_from_sa() and sets no extack, so a failed LISTENER_SET
tells userland only "Connection refused". The usual cause is
svc_setup_socket() -> svc_register() failing because the local rpcbind is
not reachable, which is not guessable from the errno alone -- and since
"SUNRPC: keep the first error in svc_register()" that failure is fatal on
CONFIG_NFS_LOCALIO=y too.

Name the transport and the error. The message tracks err, which keeps the
last failure, so a multi-listener request reports the entry whose errno is
returned.

The rejections in nfsd_nl_validate_listeners() other than -E2BIG are still
bare; those are left alone here.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 fs/nfsd/nfsctl.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/fs/nfsd/nfsctl.c b/fs/nfsd/nfsctl.c
index 66931caaaaed..a6ef85e6b419 100644
--- a/fs/nfsd/nfsctl.c
+++ b/fs/nfsd/nfsctl.c
@@ -2184,8 +2184,12 @@ int nfsd_nl_listener_set_doit(struct sk_buff *skb, struct genl_info *info)
 		ret = svc_xprt_create_from_sa(serv, xcl_name, net, sa, 0,
 					      current_cred());
 		/* always save the latest error */
-		if (ret < 0)
+		if (ret < 0) {
+			NL_SET_ERR_MSG_FMT(info->extack,
+					   "cannot create %s listener: %d",
+					   xcl_name, ret);
 			err = ret;
+		}
 	}
 
 	if (!serv->sv_nrthreads && list_empty(&nn->nfsd_serv->sv_permsocks))

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 6/7] selftests/nfsd: exercise listener_set request validation
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
                   ` (4 preceding siblings ...)
  2026-08-10 17:38 ` [PATCH 5/7] NFSD: report listener creation failures through extack Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  2026-08-10 17:38 ` [PATCH 7/7] selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips Jeff Layton
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

Regression tests for the NFSD_CMD_LISTENER_SET checks that
nfsd_nl_validate_listeners() runs before nfsd_mutex is taken: bad or absent
transport name, missing address, truncated or unsupported sockaddr, bad
address family, a malformed entry behind a well-formed one, and more than
NFSD_NL_LISTENER_MAX entries. Plus a LISTENER_GET against an empty netns.

None of these reach nfsd_create_serv(), so nothing here creates a serv or
registers with rpcbind. Tests that do need one come next, with a stub.

Uses kselftest_harness.h so each test runs in its own net+mount namespace.
/run is masked there: unix_find_bsd() resolves by inode and takes no struct
net, so a "/var/run/rpcbind.sock" connect from this netns would otherwise
reach the rpcbind on the host -- and svc_rpcb_setup() opens by calling
svc_unregister(), which would clear the host's nfsd registrations.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 tools/testing/selftests/Makefile                   |   1 +
 tools/testing/selftests/nfsd/.gitignore            |   1 +
 tools/testing/selftests/nfsd/Makefile              |   6 +
 tools/testing/selftests/nfsd/config                |   4 +
 .../testing/selftests/nfsd/nfsd_netlink_listener.c | 488 +++++++++++++++++++++
 tools/testing/selftests/nfsd/settings              |   1 +
 6 files changed, 501 insertions(+)

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

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 7/7] selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips
  2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
                   ` (5 preceding siblings ...)
  2026-08-10 17:38 ` [PATCH 6/7] selftests/nfsd: exercise listener_set request validation Jeff Layton
@ 2026-08-10 17:38 ` Jeff Layton
  6 siblings, 0 replies; 8+ messages in thread
From: Jeff Layton @ 2026-08-10 17:38 UTC (permalink / raw)
  To: Chuck Lever, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	Trond Myklebust, Anna Schumaker, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, J. Bruce Fields,
	Shuah Khan
  Cc: linux-nfs, linux-kernel, netdev, Trond Myklebust, linux-kselftest,
	Jeff Layton

Creating a listener registers with rpcbind: svc_xprt_create_from_sa() passes
flags of 0, so pmap_register is true in svc_setup_socket(), and
nfsd_version3 is enabled by default and does not set vs_rpcb_optnl, so a
registration failure aborts listener creation. A fresh netns has no rpcbind,
and reaching the host's is not an option -- svc_rpcb_setup() opens by
calling svc_unregister(), which would clear the host's nfsd entries.

Serve it from within the namespace instead. The abstract AF_LOCAL name the
kernel tries first is per-netns (unix_find_abstract() takes a struct net),
so bind "\0/run/rpcbind.sock" and fork a minimal responder:

- arguments are never decoded; the NULL procedure gets an empty success and
  SET/UNSET get TRUE
- RPCBVERS_4 is answered as well as RPCBVERS_2, because
  __svc_rpcb_register6() turns a v4 refusal into -EAFNOSUPPORT and that
  would fail every IPv6 listener
- PR_SET_PDEATHSIG plus an explicit kill in FIXTURE_TEARDOWN, so no stub
  outlives its test

With that in place, add the tests that need a serv: create/add/remove and
LISTENER_GET round-trips (tcp, udp, multi, idempotent re-set, subset
removal, empty-list serv destroy, IPv6), the empty-list request, and the
-EBUSY refusal once THREADS_SET has started threads.

Two of the new tests exist to catch a revert rather than to describe the
interface, since neither is visible in the errno alone:

- val_reject_keeps_listeners. An unknown transport name ends in
  -EPROTONOSUPPORT either way, because svc_xprt_create_from_sa() returns
  that too. What differs is that without the up-front check
  nfsd_nl_listener_set_doit() has already destroyed the listeners that did
  not match by the time the name fails.
- sem_register_refused, which restarts the stub in a mode that answers
  RPCBPROC_SET with FALSE. rpcb_register_call() turns that into -EACCES,
  which must reach userland and leave no listener behind. On
  CONFIG_NFS_LOCALIO=y it does not, unless svc_register() keeps the first
  error: nfslocalio is last in nfsd_programs and its NULL and vs_hidden
  versions both report success, overwriting the failure.

Signed-off-by: Jeff Layton <jlayton@kernel.org>
Assisted-by: LLM
---
 .../testing/selftests/nfsd/nfsd_netlink_listener.c | 444 ++++++++++++++++++++-
 1 file changed, 438 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
index ae28c224255f..3e3307680d7d 100644
--- a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -3,30 +3,40 @@
  * Regression tests for the NFSD generic-netlink listener interface
  * (NFSD_CMD_LISTENER_SET / NFSD_CMD_LISTENER_GET).
  *
- * These cover the request validation that nfsd_nl_validate_listeners() does
- * before nfsd_mutex is taken: bad or absent transport name, missing address,
- * truncated or unsupported sockaddr, oversized list. None of them reach
- * nfsd_create_serv(), so nothing here creates a serv or talks to rpcbind.
+ * Three groups:
+ *   validation  - malformed/abusive LISTENER_SET requests are rejected by
+ *                 nfsd_nl_validate_listeners(), before nfsd_mutex is taken.
+ *   functional  - create/add/remove listeners and verify LISTENER_GET
+ *                 reflects the set (round-trip of transport + addr:port).
+ *   semantics   - once threads are running (THREADS_SET) a listener change
+ *                 is refused with -EBUSY.
  *
  * Each test runs in its own private net + mount namespace (unshare in
  * FIXTURE_SETUP). /run is masked there: a pathname AF_LOCAL connect is not
  * scoped by the network namespace, since unix_find_bsd() resolves by inode
  * and takes no struct net, so the kernel's rpcbind client would otherwise be
- * able to reach the rpcbind running on the host.
+ * able to reach the rpcbind running on the host. Anything that creates a
+ * serv is served by the per-netns rpcbind stub below instead.
  */
 #define _GNU_SOURCE
 #include <errno.h>
+#include <poll.h>
 #include <sched.h>
+#include <signal.h>
+#include <stddef.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <sys/mount.h>
+#include <sys/prctl.h>
 #include <sys/socket.h>
 #include <sys/ioctl.h>
 #include <sys/stat.h>
 #include <sys/time.h>
+#include <sys/un.h>
+#include <sys/wait.h>
 #include <net/if.h>
 #include <netinet/in.h>
 #include <linux/netlink.h>
@@ -36,8 +46,10 @@
 
 /* NFSD generic-netlink constants (from linux/nfsd_netlink.h). */
 #define NFSD_FAMILY_NAME		"nfsd"
+#define NFSD_CMD_THREADS_SET		2
 #define NFSD_CMD_LISTENER_SET		6
 #define NFSD_CMD_LISTENER_GET		7
+#define NFSD_A_SERVER_THREADS		1
 #define NFSD_A_SERVER_SOCK_ADDR		1	/* per-listener nest */
 #define NFSD_A_SOCK_ADDR		1	/* inside the nest */
 #define NFSD_A_SOCK_TRANSPORT_NAME	2	/* inside the nest */
@@ -327,10 +339,233 @@ static int listener_get(struct listener_ent *out, int max)
 	return parse_listener_get(rbuf, n, out, max);
 }
 
+static struct listener_ent *find_listener(struct listener_ent *e, int n,
+					  const char *xprt, int family,
+					  uint16_t port)
+{
+	int i;
+
+	for (i = 0; i < n; i++)
+		if (e[i].family == family && e[i].port == port &&
+		    !strcmp(e[i].xprt, xprt))
+			return &e[i];
+	return NULL;
+}
+
+/* Start (@n > 0) or stop (@n == 0) nfsd threads in this netns. */
+static int threads_set(int n)
+{
+	char attrs[64];
+	uint32_t v = n;
+	int off = put_attr(attrs, 0, NFSD_A_SERVER_THREADS, &v, sizeof(v));
+
+	return genl_request(NFSD_CMD_THREADS_SET, attrs, off);
+}
+
+/* ------------------- per-netns local rpcbind stub ------------------- */
+
+/*
+ * Creating a listener registers with rpcbind: svc_xprt_create_from_sa()
+ * passes flags of 0, so pmap_register is true in svc_setup_socket(), and
+ * nfsd_version3 is registerable by default and does not set vs_rpcb_optnl,
+ * so a registration failure aborts listener creation. The abstract AF_LOCAL
+ * name the kernel tries first is per-netns (unix_find_abstract() takes a
+ * struct net), so answer it here and stay out of the host's rpcbind.
+ *
+ * Arguments are never decoded. The NULL procedure gets an empty success and
+ * SET/UNSET get TRUE, for both RPCBVERS_2 and RPCBVERS_4. v4 has to be
+ * answered because __svc_rpcb_register6() turns a v4 refusal into
+ * -EAFNOSUPPORT, which would fail every IPv6 listener.
+ *
+ * In RPCB_STUB_REFUSE mode SET is answered FALSE instead, which
+ * rpcb_register_call() reports as -EACCES. UNSET is left alone: only
+ * svc_unregister() issues it, and it discards the result.
+ */
+#define RPCB_PROGRAM		100000
+#define RPCB_PROC_NULL		0
+#define RPCB_PROC_SET		1
+#define RPCB_PROC_UNSET		2
+#define RPCB_ABSTRACT_NAME	"/run/rpcbind.sock"
+#define RPCB_STUB_MAXCONN	4
+
+enum { RPCB_STUB_ACCEPT, RPCB_STUB_REFUSE };
+
+static int rpcb_stub_listen(void)
+{
+	struct sockaddr_un sun = { .sun_family = AF_UNIX };
+	size_t nlen = strlen(RPCB_ABSTRACT_NAME);
+	socklen_t alen;
+	int fd;
+
+	/* Abstract names are length-delimited, so the length must match. */
+	memcpy(sun.sun_path + 1, RPCB_ABSTRACT_NAME, nlen);
+	alen = offsetof(struct sockaddr_un, sun_path) + 1 + nlen;
+
+	fd = socket(AF_UNIX, SOCK_STREAM, 0);
+	if (fd < 0)
+		return -1;
+	if (bind(fd, (struct sockaddr *)&sun, alen) < 0 ||
+	    listen(fd, RPCB_STUB_MAXCONN) < 0) {
+		close(fd);
+		return -1;
+	}
+	return fd;
+}
+
+static int rpcb_stub_read(int fd, void *buf, size_t len)
+{
+	size_t done = 0;
+
+	while (done < len) {
+		ssize_t n = read(fd, (char *)buf + done, len - done);
+
+		if (n <= 0)
+			return -1;
+		done += n;
+	}
+	return 0;
+}
+
+/* Handle one record-marked RPC call. Returns -1 when the peer is done. */
+static int rpcb_stub_call(int fd, int mode)
+{
+	uint32_t mark, call[6], rep[7];
+	unsigned int len, nrep = 6;
+	size_t replen;
+
+	if (rpcb_stub_read(fd, &mark, sizeof(mark)))
+		return -1;
+	len = ntohl(mark) & 0x7fffffff;
+	if (len < sizeof(call) || len > 4096)
+		return -1;
+	if (rpcb_stub_read(fd, call, sizeof(call)))
+		return -1;
+
+	/* xid, msg_type, rpcvers, prog, vers, proc; the rest is discarded */
+	for (len -= sizeof(call); len; ) {
+		char sink[256];
+		unsigned int n = len > sizeof(sink) ? sizeof(sink) : len;
+
+		if (rpcb_stub_read(fd, sink, n))
+			return -1;
+		len -= n;
+	}
+
+	rep[0] = call[0];		/* xid */
+	rep[1] = htonl(1);		/* REPLY */
+	rep[2] = htonl(0);		/* MSG_ACCEPTED */
+	rep[3] = htonl(0);		/* verifier flavor AUTH_NULL */
+	rep[4] = htonl(0);		/* verifier length */
+	rep[5] = htonl(0);		/* SUCCESS */
+
+	if (ntohl(call[3]) != RPCB_PROGRAM) {
+		rep[5] = htonl(1);	/* PROG_UNAVAIL */
+	} else {
+		switch (ntohl(call[5])) {
+		case RPCB_PROC_NULL:
+			break;
+		case RPCB_PROC_SET:
+			rep[6] = htonl(mode == RPCB_STUB_REFUSE ? 0 : 1);
+			nrep = 7;
+			break;
+		case RPCB_PROC_UNSET:
+			rep[6] = htonl(1);	/* TRUE */
+			nrep = 7;
+			break;
+		default:
+			rep[5] = htonl(3);	/* PROC_UNAVAIL */
+		}
+	}
+
+	replen = nrep * sizeof(rep[0]);
+	mark = htonl(0x80000000 | replen);
+	if (write(fd, &mark, sizeof(mark)) != (ssize_t)sizeof(mark) ||
+	    write(fd, rep, replen) != (ssize_t)replen)
+		return -1;
+	return 0;
+}
+
+static void rpcb_stub_serve(int lfd, int mode)
+{
+	struct pollfd pfd[1 + RPCB_STUB_MAXCONN];
+	nfds_t n = 1, i;
+
+	pfd[0].fd = lfd;
+
+	for (;;) {
+		/* stop polling the listener when full, or poll() spins */
+		pfd[0].events = n < 1 + RPCB_STUB_MAXCONN ? POLLIN : 0;
+
+		if (poll(pfd, n, -1) < 0)
+			return;
+
+		if (pfd[0].revents & POLLIN) {
+			int c = accept(lfd, NULL, NULL);
+
+			if (c >= 0) {
+				pfd[n].fd = c;
+				pfd[n].events = POLLIN;
+				n++;
+			}
+		}
+
+		for (i = 1; i < n; i++) {
+			if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR)))
+				continue;
+			if (rpcb_stub_call(pfd[i].fd, mode)) {
+				close(pfd[i].fd);
+				pfd[i] = pfd[--n];
+			}
+		}
+	}
+}
+
+/* Returns the stub's pid, or -1. The socket is listening before we fork. */
+static pid_t rpcb_stub_start(int mode)
+{
+	int lfd = rpcb_stub_listen();
+	pid_t pid;
+
+	if (lfd < 0)
+		return -1;
+
+	pid = fork();
+	if (pid < 0) {
+		close(lfd);
+		return -1;
+	}
+	if (pid == 0) {
+		signal(SIGPIPE, SIG_IGN);
+		prctl(PR_SET_PDEATHSIG, SIGKILL);
+		if (getppid() == 1)		/* raced with parent exit */
+			_exit(0);
+		rpcb_stub_serve(lfd, mode);
+		_exit(0);
+	}
+
+	close(lfd);
+	return pid;
+}
+
+/*
+ * Swap the stub for one in @mode. Safe before the first request: no serv
+ * exists yet, so the kernel has not connected and the abstract name is free
+ * again once the old stub has been reaped.
+ */
+static int rpcb_stub_restart(pid_t *pid, int mode)
+{
+	if (*pid > 0) {
+		kill(*pid, SIGKILL);
+		waitpid(*pid, NULL, 0);
+	}
+	*pid = rpcb_stub_start(mode);
+	return *pid > 0 ? 0 : -1;
+}
+
 /* --------------------------- fixture --------------------------- */
 
 FIXTURE(nfsd_listener) {
-	int placeholder;
+	pid_t rpcbd;
 };
 
 FIXTURE_SETUP(nfsd_listener)
@@ -369,14 +604,28 @@ FIXTURE_SETUP(nfsd_listener)
 	nfsd_family = genl_resolve_nfsd();
 	if (nfsd_family < 0)
 		SKIP(return, "nfsd genl family not found (modprobe nfsd?)");
+
+	self->rpcbd = rpcb_stub_start(RPCB_STUB_ACCEPT);
+	if (self->rpcbd < 0)
+		SKIP(return, "cannot start the rpcbind stub: %s",
+		     strerror(errno));
 }
 
 FIXTURE_TEARDOWN(nfsd_listener)
 {
+	if (self->rpcbd > 0) {
+		kill(self->rpcbd, SIGKILL);
+		waitpid(self->rpcbd, NULL, 0);
+	}
 }
 
 /* ===================== validation / negative ===================== */
 
+TEST_F(nfsd_listener, val_empty_list_ok)
+{
+	EXPECT_EQ(0, listener_set(NULL, 0));
+}
+
 TEST_F(nfsd_listener, val_too_many)
 {
 	static char attrs[1 << 20];
@@ -477,6 +726,33 @@ TEST_F(nfsd_listener, val_second_entry_bad)
 	EXPECT_EQ(-EAFNOSUPPORT, listener_set(attrs, off));
 }
 
+/*
+ * A rejected request must leave the listeners that are already up alone.
+ * The errno alone does not show that: svc_xprt_create_from_sa() returns
+ * -EPROTONOSUPPORT for an unknown name too. What differs is how far the
+ * request gets -- without the check in nfsd_nl_validate_listeners(),
+ * nfsd_nl_listener_set_doit() has already moved the unmatched tcp listener
+ * off sv_permsocks and run svc_xprt_destroy_all() on it by the time the
+ * name fails.
+ */
+TEST_F(nfsd_listener, val_reject_keeps_listeners)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char good[64], bad[64];
+	int og = put_listener(good, 0, "tcp", TEST_PORT);
+	int ob = put_listener(bad, 0, "bogus_xprt", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(good, og));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+
+	EXPECT_EQ(-EPROTONOSUPPORT, listener_set(bad, ob));
+
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+/* ===================== functional / round-trip ===================== */
+
 /* LISTENER_GET with no serv in this netns returns an empty list. */
 TEST_F(nfsd_listener, func_get_empty)
 {
@@ -485,4 +761,160 @@ TEST_F(nfsd_listener, func_get_empty)
 	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
 }
 
+TEST_F(nfsd_listener, func_create_tcp)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+	EXPECT_EQ(htonl(INADDR_LOOPBACK), got[0].a4.s_addr);
+}
+
+TEST_F(nfsd_listener, func_create_udp)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "udp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_create_multi)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[128];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	off = put_listener(attrs, off, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+	EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_idempotent)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_EQ(0, listener_set(attrs, off));		/* re-set same list */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_add)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char one[64], two[128];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+	int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+	o2 = put_listener(two, o2, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, listener_set(two, o2));		/* add udp, keep tcp */
+	ASSERT_EQ(2, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 2, "tcp", AF_INET, TEST_PORT));
+	EXPECT_NE(NULL, find_listener(got, 2, "udp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_remove_subset)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char both[128], one[64];
+	int ob = put_listener(both, 0, "tcp", TEST_PORT);
+	int oo = put_listener(one, 0, "tcp", TEST_PORT);
+
+	ob = put_listener(both, ob, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(both, ob));
+	ASSERT_EQ(0, listener_set(one, oo));		/* drop udp */
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET, TEST_PORT));
+}
+
+TEST_F(nfsd_listener, func_empty_destroys)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(attrs, off));
+	EXPECT_EQ(0, listener_set(NULL, 0));		/* empty -> destroy serv */
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+TEST_F(nfsd_listener, func_ipv6)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off, s;
+
+	s = socket(AF_INET6, SOCK_STREAM, 0);
+	if (s < 0)
+		SKIP(return, "IPv6 unavailable: %s", strerror(errno));
+	close(s);
+
+	off = put_listener_af(attrs, 0, "tcp", AF_INET6, TEST_PORT);
+	ASSERT_EQ(0, listener_set(attrs, off));
+	ASSERT_EQ(1, listener_get(got, MAX_LISTENERS));
+	EXPECT_NE(NULL, find_listener(got, 1, "tcp", AF_INET6, TEST_PORT));
+	EXPECT_EQ(0, memcmp(&got[0].a6, &in6addr_loopback, sizeof(in6addr_loopback)));
+}
+
+/* ===================== rpcbind registration ===================== */
+
+/*
+ * A rpcbind that refuses the registration must fail listener creation,
+ * whatever CONFIG_NFS_LOCALIO is set to.
+ *
+ * The error has to survive svc_register()'s walk over sv_programs to get
+ * here. With CONFIG_NFS_LOCALIO=y the trailing nfslocalio program has only
+ * a NULL and a vs_hidden version, and svc_generic_rpcbind_set() reports 0
+ * for both, so an svc_register() that keeps the last result rather than the
+ * first hands back success and the listener comes up regardless.
+ */
+TEST_F(nfsd_listener, sem_register_refused)
+{
+	struct listener_ent got[MAX_LISTENERS];
+	char attrs[64];
+	int off = put_listener(attrs, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, rpcb_stub_restart(&self->rpcbd, RPCB_STUB_REFUSE));
+
+	EXPECT_EQ(-EACCES, listener_set(attrs, off));
+	EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
+}
+
+/* ===================== threads / -EBUSY semantics ===================== */
+
+TEST_F(nfsd_listener, sem_busy_on_change)
+{
+	char one[64], two[128];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+	int o2 = put_listener(two, 0, "tcp", TEST_PORT);
+
+	o2 = put_listener(two, o2, "udp", TEST_PORT);
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, threads_set(1));			/* threads now running */
+	EXPECT_EQ(-EBUSY, listener_set(two, o2));	/* add refused */
+	threads_set(0);					/* stop before netns exit */
+}
+
+TEST_F(nfsd_listener, sem_busy_on_remove)
+{
+	char one[64];
+	int o1 = put_listener(one, 0, "tcp", TEST_PORT);
+
+	ASSERT_EQ(0, listener_set(one, o1));
+	ASSERT_EQ(0, threads_set(1));
+	EXPECT_EQ(-EBUSY, listener_set(NULL, 0));	/* remove refused */
+	threads_set(0);
+}
+
 TEST_HARNESS_MAIN

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-08-10 17:39 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-10 17:38 [PATCH 0/7] nfsd/sunrpc: harden the netlink listener interfaces Jeff Layton
2026-08-10 17:38 ` [PATCH 1/7] NFSD: validate transport name in listener_set before serv creation Jeff Layton
2026-08-10 17:38 ` [PATCH 2/7] NFSD: cap the number of listeners accepted in listener_set Jeff Layton
2026-08-10 17:38 ` [PATCH 3/7] SUNRPC: keep the first error in svc_register() Jeff Layton
2026-08-10 17:38 ` [PATCH 4/7] SUNRPC: bound the local rpcbind client timeout to 1s Jeff Layton
2026-08-10 17:38 ` [PATCH 5/7] NFSD: report listener creation failures through extack Jeff Layton
2026-08-10 17:38 ` [PATCH 6/7] selftests/nfsd: exercise listener_set request validation Jeff Layton
2026-08-10 17:38 ` [PATCH 7/7] selftests/nfsd: add a per-netns rpcbind stub and the listener round-trips Jeff Layton

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox