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. NFSv4 carries no-udp and NFSv3 does not.
- 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_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.

Assisted-by: LLM
Signed-off-by: Jeff Layton <[email protected]>
---
 .../testing/selftests/nfsd/nfsd_netlink_listener.c | 284 ++++++++++++++++++++-
 1 file changed, 281 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c 
b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
index 106360f87b99..56ac2c11c07f 100644
--- a/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
+++ b/tools/testing/selftests/nfsd/nfsd_netlink_listener.c
@@ -46,6 +46,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 +176,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 +210,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 +400,111 @@ 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;
+
+       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 +1404,162 @@ 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));
+
+       v4 = find_rpcb(&r, NFS_PROGRAM, 4);
+       if (v4)
+               EXPECT_EQ(NFSD_RPCBIND_FLAGS_NO_UDP,
+                         v4->flags & NFSD_RPCBIND_FLAGS_NO_UDP);
+
+       v3 = find_rpcb(&r, NFS_PROGRAM, 3);
+       if (v3)
+               EXPECT_EQ(0, v3->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));
+}
+
+/*
+ * 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);
+}
+
 /* ===================== threads / -EBUSY semantics ===================== */
 
 TEST_F(nfsd_listener, sem_busy_on_change)

-- 
2.55.0


Reply via email to