selftests/nfsd: exercise the userspace-rpcbind listener_set flag

Cover the flag that moves rpcbind registration to the caller.

rpcb_userspace_no_traffic is the one that matters. The errno says nothing
about whether the kernel talked to rpcbind, but the stub counters do: a
request that carries the flag must leave both at zero, which is what shows
that svc_bind() and svc_register() were both skipped.

The rest cover the reply and the ownership rule:

- rpcb_userspace_reply. The ack flag, a non-empty program list, and the
  listener that came up. nfslocalio is hidden, so it must be absent.
- rpcb_userspace_no_v4_udp. NFSv3 must be offered and must not carry
  no-udp; NFSv4 carries it, or the test skips where v4 is not built.
- rpcb_userspace_nfsacl_follows_nfs. CONFIG_NFSD_V3_ACL is invisible to the
  test, so assert only that nfsacl v3 implies nfs v3.
- rpcb_userspace_busy and rpcb_userspace_busy_reverse. Ownership cannot
  change under a live serv, in either direction.
- rpcb_userspace_unflagged_teardown and its reverse. Teardown destroys the
  serv, so it is exempt from that rule in both directions. The follow-on
  listener_set is what proves the old serv went away rather than merely
  losing its listeners.
- rpcb_userspace_portlist_busy. The legacy portlist add-fd write is
  refused against a flagged serv and still works against a kernel-owned
  one. nfsdfs is mounted in the test's netns, so the write cannot reach
  the host's nfsd.
- rpcb_userspace_teardown. Create and destroy a flagged serv three times,
  then confirm that a kernel-owned serv still reaches rpcbind. An
  unbalanced rpcb_put_local() would break the last step.
- rpcb_userspace_many_listeners. Forty listeners, which is more than
  GENLMSG_DEFAULT_SIZE would have held. A reply sized from that constant
  instead of from its contents returns -EMSGSIZE with every listener
  already up.

genl_request_reply() grew an attribute argument, because listener_set now
answers with a reply message rather than a bare ack.

listener_set_rpcb() checks nlmsg_len against the recv() count before
parsing. Forty listeners fit in its buffer, but a reply near
NFSD_NL_LISTENER_MAX would not, and the parse loops trust nlmsg_len.

Assisted-by: LLM
Signed-off-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260915-nfsd-norpcb-v3-5-3c60d49ade02@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
This commit is contained in:
Jeff Layton 2026-09-15 12:57:46 -04:00 committed by Chuck Lever
parent 95ac4b0c62
commit fcc202d324

View File

@ -20,6 +20,7 @@
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
@ -46,6 +47,9 @@
#include "../kselftest_harness.h"
#define NFS_PROGRAM 100003
#define NFS_ACL_PROGRAM 100227
#define NLA_ALIGN4(len) (((len) + 3) & ~3)
#define TEST_PORT 20049
#define MAX_LISTENERS 8
@ -173,15 +177,24 @@ static int genl_request(uint8_t cmd, const char *attrs, int attrs_len)
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)
/*
* Send a command with attributes and return the full reply message; -errno
* on failure. NLM_F_ACK is left off: the kernel reports an error either way,
* so the first message back is the reply whenever there is one.
*/
static int genl_request_reply_attrs(uint8_t cmd, const char *attrs,
int attrs_len, char *rbuf, size_t rlen)
{
char buf[256];
char buf[1 << 20];
struct nlmsghdr *nlh = (void *)buf;
int fd = genl_open();
int off, n, ret;
off = genl_hdr(buf, nfsd_family, NLM_F_REQUEST, cmd);
if (attrs_len) {
memcpy(buf + off, attrs, attrs_len);
off += attrs_len;
}
nlh->nlmsg_len = off;
if (send(fd, buf, off, 0) < 0)
@ -198,6 +211,11 @@ static int genl_request_reply(uint8_t cmd, char *rbuf, size_t rlen)
return ret;
}
static int genl_request_reply(uint8_t cmd, char *rbuf, size_t rlen)
{
return genl_request_reply_attrs(cmd, NULL, 0, rbuf, rlen);
}
/* Resolve the "nfsd" genl family id; -1 if not registered. */
static int genl_resolve_nfsd(void)
{
@ -383,6 +401,119 @@ static int version_set_only(uint32_t major, uint32_t minor)
return genl_request(NFSD_CMD_VERSION_SET, attrs, NLA_ALIGN4(inner));
}
/* ------------------- userspace-rpcbind ------------------- */
struct rpcb_ent {
uint32_t program;
uint32_t version;
uint32_t flags;
};
/* More listeners than GENLMSG_DEFAULT_SIZE would have held. */
#define RPCB_MANY_LISTENERS 40
struct rpcb_reply {
int acked; /* saw NFSD_A_SERVER_SOCK_USERSPACE_RPCBIND */
int nprog;
struct rpcb_ent prog[16];
int naddr; /* every addr nest, not just the stored ones */
int nlistener;
struct listener_ent listener[MAX_LISTENERS];
};
/* Append the userspace-rpcbind request flag. */
static int put_userspace_rpcbind(char *buf, int off)
{
return put_attr(buf, off, NFSD_A_SERVER_SOCK_USERSPACE_RPCBIND, NULL, 0);
}
static void parse_rpcb_nest(const struct nlattr *na, struct rpcb_ent *e)
{
const struct nlattr *in = (const void *)((const char *)na + NLA_HDRLEN);
int ileft = na->nla_len - NLA_HDRLEN;
memset(e, 0, sizeof(*e));
while (ileft >= (int)NLA_HDRLEN) {
const void *d = (const char *)in + NLA_HDRLEN;
switch (in->nla_type & NLA_TYPE_MASK) {
case NFSD_A_RPCBIND_PROGRAM:
e->program = *(const uint32_t *)d;
break;
case NFSD_A_RPCBIND_VERSION:
e->version = *(const uint32_t *)d;
break;
case NFSD_A_RPCBIND_FLAGS:
e->flags = *(const uint32_t *)d;
break;
}
ileft -= NLA_ALIGN4(in->nla_len);
in = (const void *)((const char *)in + NLA_ALIGN4(in->nla_len));
}
}
/*
* Send a listener_set that asks to own rpcbind, and parse the reply.
* Returns 0 on success or -errno.
*/
static int listener_set_rpcb(char *attrs, int off, struct rpcb_reply *out)
{
char rbuf[64 * 1024];
const struct nlmsghdr *nlh = (const void *)rbuf;
const struct nlattr *na;
int left, n;
off = put_userspace_rpcbind(attrs, off);
memset(out, 0, sizeof(*out));
n = genl_request_reply_attrs(NFSD_CMD_LISTENER_SET, attrs, off,
rbuf, sizeof(rbuf));
if (n < 0)
return n;
/*
* A reply at NFSD_NL_LISTENER_MAX outgrows rbuf, and the parse loops
* below trust nlmsg_len. Refuse a short read rather than walk off it.
*/
if (n < (int)(NLMSG_HDRLEN + GENL_HDRLEN) ||
nlh->nlmsg_len > (unsigned int)n)
return -EMSGSIZE;
out->nlistener = parse_listener_get(rbuf, n, out->listener,
MAX_LISTENERS);
na = (const void *)(rbuf + NLMSG_HDRLEN + GENL_HDRLEN);
left = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
while (left >= (int)NLA_HDRLEN) {
switch (na->nla_type & NLA_TYPE_MASK) {
case NFSD_A_SERVER_SOCK_USERSPACE_RPCBIND:
out->acked = 1;
break;
case NFSD_A_SERVER_SOCK_ADDR:
out->naddr++;
break;
case NFSD_A_SERVER_SOCK_RPCBIND:
if (out->nprog < (int)ARRAY_SIZE(out->prog))
parse_rpcb_nest(na, &out->prog[out->nprog++]);
break;
}
left -= NLA_ALIGN4(na->nla_len);
na = (const void *)((const char *)na + NLA_ALIGN4(na->nla_len));
}
return 0;
}
static struct rpcb_ent *find_rpcb(struct rpcb_reply *r, uint32_t prog,
uint32_t vers)
{
int i;
for (i = 0; i < r->nprog; i++)
if (r->prog[i].program == prog && r->prog[i].version == vers)
return &r->prog[i];
return NULL;
}
/* Fetch the current listeners; returns count (>=0) or -errno. */
static int listener_get(struct listener_ent *out, int max)
{
@ -1282,6 +1413,273 @@ TEST_F(nfsd_listener, rpcb_unreg_stop_after_failure)
EXPECT_LE(three, one);
}
/* ===================== userspace rpcbind ===================== */
/*
* The point of the flag: nfsd must make no rpcbind call at all. svc_bind()
* pings rpcbind at client creation and svc_register() calls it once per
* program and version, so a silent stub is what proves both were skipped.
*/
TEST_F(nfsd_listener, rpcb_userspace_no_traffic)
{
struct rpcb_reply r;
char attrs[64];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, rpcb_conns());
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
EXPECT_EQ(0, rpcb_conns());
EXPECT_EQ(0, rpcb_calls());
}
/*
* The reply has to tell the caller what to register. Without the program
* list it cannot know whether nfsacl is built in.
*/
TEST_F(nfsd_listener, rpcb_userspace_reply)
{
struct rpcb_reply r;
char attrs[64];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
EXPECT_EQ(1, r.acked);
EXPECT_GT(r.nprog, 0);
/* the listener came up and is named, so the caller knows the port */
ASSERT_EQ(1, r.nlistener);
EXPECT_NE(NULL, find_listener(r.listener, r.nlistener, "tcp",
AF_INET, TEST_PORT));
/* nfslocalio is hidden and must never be offered for registration */
EXPECT_EQ(NULL, find_rpcb(&r, 400122, 1));
}
/*
* nfsd_nl_validate_listeners() allows far more listeners than the default
* genl buffer holds, so the reply has to be sized from its contents. If it
* is not, the listeners all come up and the caller still sees -EMSGSIZE.
*/
TEST_F(nfsd_listener, rpcb_userspace_many_listeners)
{
char attrs[RPCB_MANY_LISTENERS * 64];
struct rpcb_reply r;
int off = 0, i;
for (i = 0; i < RPCB_MANY_LISTENERS; i++)
off = put_listener(attrs, off, "tcp", TEST_PORT + i);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
EXPECT_EQ(1, r.acked);
EXPECT_GT(r.nprog, 0);
EXPECT_EQ(RPCB_MANY_LISTENERS, r.naddr);
}
/*
* NFSv4 sets vs_need_cong_ctrl, so the kernel never registered it on UDP.
* The reply cannot filter it out, because the rule depends on the listener,
* so it must carry the flag instead.
*/
TEST_F(nfsd_listener, rpcb_userspace_no_v4_udp)
{
struct rpcb_ent *v4, *v3;
struct rpcb_reply r;
char attrs[64];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
/* v3 is always built in and on by default, so it must be offered */
v3 = find_rpcb(&r, NFS_PROGRAM, 3);
ASSERT_NE(NULL, v3);
EXPECT_EQ(0, v3->flags & NFSD_RPCBIND_FLAGS_NO_UDP);
v4 = find_rpcb(&r, NFS_PROGRAM, 4);
if (!v4)
SKIP(return, "NFSv4 is not enabled");
EXPECT_EQ(NFSD_RPCBIND_FLAGS_NO_UDP,
v4->flags & NFSD_RPCBIND_FLAGS_NO_UDP);
}
/*
* nfsacl is the value userland cannot derive: CONFIG_NFSD_V3_ACL is not
* visible over netlink. Only assert self-consistency -- if the kernel
* offers nfsacl v3 then it must also offer nfs v3, since both gate on the
* same enabled version.
*/
TEST_F(nfsd_listener, rpcb_userspace_nfsacl_follows_nfs)
{
struct rpcb_reply r;
char attrs[64];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
if (find_rpcb(&r, NFS_ACL_PROGRAM, 3))
EXPECT_NE(NULL, find_rpcb(&r, NFS_PROGRAM, 3));
}
/*
* svc_bind() decided whether to take the rpcb_users reference that teardown
* drops, so ownership cannot flip under a live serv.
*/
TEST_F(nfsd_listener, rpcb_userspace_busy)
{
struct rpcb_reply r;
char attrs[64], plain[64];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
int poff = put_listener(plain, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
/* same listeners, but now asking the kernel to own rpcbind */
EXPECT_EQ(-EBUSY, listener_set(plain, poff));
EXPECT_STRNE("", last_extack);
}
/* And the same the other way round. */
TEST_F(nfsd_listener, rpcb_userspace_busy_reverse)
{
struct rpcb_reply r;
char attrs[64], plain[64];
int poff = put_listener(plain, 0, "tcp", TEST_PORT);
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set(plain, poff));
EXPECT_EQ(-EBUSY, listener_set_rpcb(attrs, off, &r));
}
#define NFSDFS_DIR "/run/nfsdfs"
/* nfsdfs binds to the netns it is mounted in, so mount a private one */
static int mount_nfsdfs(void)
{
if (mkdir(NFSDFS_DIR, 0700) < 0 && errno != EEXIST)
return -1;
return mount("nfsd", NFSDFS_DIR, "nfsd", 0, NULL);
}
/* simple_transaction allows one write per open, so open every time */
static int write_portlist(int sock)
{
char buf[32];
int fd, len, ret = 0;
fd = open(NFSDFS_DIR "/portlist", O_WRONLY);
if (fd < 0)
return -errno;
len = snprintf(buf, sizeof(buf), "%d\n", sock);
if (write(fd, buf, len) < 0)
ret = -errno;
close(fd);
return ret;
}
static int tcp_listener(int port)
{
struct sockaddr_in sa = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = htons(port),
};
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return -1;
if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0 ||
listen(fd, 1) < 0) {
close(fd);
return -1;
}
return fd;
}
/*
* The legacy portlist interface cannot hand a listener to the rpcbind owner
* and svc_register() will not register it, so it has to refuse.
*/
TEST_F(nfsd_listener, rpcb_userspace_portlist_busy)
{
struct rpcb_reply r;
char attrs[64], plain[64];
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
int poff = put_listener(plain, 0, "tcp", TEST_PORT);
int sock;
if (mount_nfsdfs() < 0)
SKIP(return, "cannot mount nfsdfs: %s", strerror(errno));
sock = tcp_listener(TEST_PORT + 1);
ASSERT_GE(sock, 0);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
EXPECT_EQ(-EBUSY, write_portlist(sock));
/* the same write against a kernel-owned serv still works */
ASSERT_EQ(0, listener_set_rpcb(attrs, 0, &r)); /* destroys the serv */
ASSERT_EQ(0, listener_set(plain, poff));
EXPECT_EQ(0, write_portlist(sock));
close(sock);
}
/*
* rpcb_create_local() increments sn->rpcb_users and rpcb_put_local()
* decrements it. A serv that never took the reference must not drop it, or
* the next serv finds the count wrong. Cycle a few times, then confirm a
* kernel-owned serv can still reach rpcbind.
*/
TEST_F(nfsd_listener, rpcb_userspace_teardown)
{
struct rpcb_reply r;
char attrs[64], plain[64];
int off, poff, i;
for (i = 0; i < 3; i++) {
off = put_listener(attrs, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
/* empty list with no threads destroys the serv */
ASSERT_EQ(0, listener_set_rpcb(attrs, 0, &r));
ASSERT_EQ(0, rpcb_conns());
}
poff = put_listener(plain, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set(plain, poff));
EXPECT_GT(rpcb_conns(), 0);
}
/*
* Teardown destroys the serv, so it is not an ownership change. A caller
* that never learned about the flag has to be able to shut nfsd down.
*/
TEST_F(nfsd_listener, rpcb_userspace_unflagged_teardown)
{
struct listener_ent got[MAX_LISTENERS];
char attrs[64], plain[64];
struct rpcb_reply r;
int off = put_listener(attrs, 0, "tcp", TEST_PORT);
int poff = put_listener(plain, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set_rpcb(attrs, off, &r));
ASSERT_EQ(1, r.nlistener);
/* no flag, no listeners */
EXPECT_EQ(0, listener_set(NULL, 0));
EXPECT_EQ(0, listener_get(got, MAX_LISTENERS));
/* the serv is gone, so kernel ownership can be taken */
EXPECT_EQ(0, listener_set(plain, poff));
}
/* And the same the other way round. */
TEST_F(nfsd_listener, rpcb_userspace_flagged_teardown_of_kernel_serv)
{
char attrs[64], plain[64];
struct rpcb_reply r;
int poff = put_listener(plain, 0, "tcp", TEST_PORT);
ASSERT_EQ(0, listener_set(plain, poff));
EXPECT_EQ(0, listener_set_rpcb(attrs, 0, &r));
EXPECT_EQ(0, listener_set(plain, poff));
}
/* ===================== threads / -EBUSY semantics ===================== */
TEST_F(nfsd_listener, sem_busy_on_change)