Cover LOCKD_CMD_SERVER_SET / SERVER_GET, in particular that each of the
three attributes can be set on its own. Without the preceding fix, four
of the eight tests fail with EINVAL -- every one that omits the grace
time, which is what nfsdctl sends for a [lockd] port with no
grace-time.

Also pinned down here:
- an empty SERVER_SET is a no-op, not an error
- a gracetime over nlm_grace_period_max is still rejected, and the
  rejected request applies none of the ports it came with
- the settings are per-netns

Everything runs under unshare(CLONE_NEWNET): the values are per-netns,
and a SERVER_SET in init_net would overwrite the host's module-wide
nlm_grace_period/nlm_tcpport/nlm_udpport.

nfsd_netlink.h grows genl_resolve() and *_to() request helpers that take
a family id, so the plumbing can drive the lockd family too;
genl_resolve_nfsd() and the existing helpers are thin wrappers and the
other tests are unchanged.

Assisted-by: LLM
Signed-off-by: Jeff Layton <[email protected]>
---
 tools/testing/selftests/nfsd/.gitignore           |   1 +
 tools/testing/selftests/nfsd/Makefile             |   1 +
 tools/testing/selftests/nfsd/nfsd_lockd_netlink.c | 268 ++++++++++++++++++++++
 tools/testing/selftests/nfsd/nfsd_netlink.h       |  90 ++++++--
 4 files changed, 345 insertions(+), 15 deletions(-)

diff --git a/tools/testing/selftests/nfsd/.gitignore 
b/tools/testing/selftests/nfsd/.gitignore
index 2347491c634d..7ac844d0fd57 100644
--- a/tools/testing/selftests/nfsd/.gitignore
+++ b/tools/testing/selftests/nfsd/.gitignore
@@ -1,3 +1,4 @@
+nfsd_lockd_netlink
 nfsd_netlink_listener
 nfsd_netns_isolation
 nfsd_netns_stress
diff --git a/tools/testing/selftests/nfsd/Makefile 
b/tools/testing/selftests/nfsd/Makefile
index b29bf642c0ad..e74bb3424d45 100644
--- a/tools/testing/selftests/nfsd/Makefile
+++ b/tools/testing/selftests/nfsd/Makefile
@@ -2,6 +2,7 @@
 CFLAGS += $(KHDR_INCLUDES) -Wall
 
 TEST_GEN_PROGS := nfsd_netlink_listener
+TEST_GEN_PROGS += nfsd_lockd_netlink
 TEST_GEN_PROGS += nfsd_netns_isolation
 TEST_GEN_PROGS += nfsd_netns_stress
 
diff --git a/tools/testing/selftests/nfsd/nfsd_lockd_netlink.c 
b/tools/testing/selftests/nfsd/nfsd_lockd_netlink.c
new file mode 100644
index 000000000000..47a1dee5a1f5
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_lockd_netlink.c
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression tests for lockd's generic-netlink configuration interface
+ * (LOCKD_CMD_SERVER_SET / LOCKD_CMD_SERVER_GET).
+ *
+ * All three attributes are optional in the spec and each is applied on its
+ * own by the kernel, but SERVER_SET used to demand a grace time and reject
+ * anything else with -EINVAL. That made the tcp and udp ports unsettable by
+ * themselves, which is exactly what nfsdctl asks for when /etc/nfs.conf has
+ * a [lockd] port but no grace-time -- "nfsdctl autostart" then failed before
+ * it had configured anything.
+ *
+ * Every test runs in a private network namespace. The settings are per-netns,
+ * and a SERVER_SET in init_net would also overwrite the host's module-wide
+ * nlm_grace_period/nlm_tcpport/nlm_udpport.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <sched.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/lockd_netlink.h>
+
+#include "../kselftest_harness.h"
+#include "nfsd_netlink.h"
+
+/* fs/lockd/svc.c: nlm_grace_period_max */
+#define GRACE_MAX              240
+
+#define TEST_TCP_PORT          32531
+#define TEST_UDP_PORT          32532
+
+struct lockd_cfg {
+       uint32_t gracetime;
+       uint16_t tcp_port;
+       uint16_t udp_port;
+};
+
+static int lockd_family = -1;
+
+static int lockd_set(const char *attrs, int len)
+{
+       return genl_request_to(lockd_family, LOCKD_CMD_SERVER_SET, attrs, len);
+}
+
+static int lockd_get(struct lockd_cfg *cfg)
+{
+       const struct nlattr *grace, *tcp, *udp;
+       char rbuf[4096];
+       int n;
+
+       n = genl_request_reply_attrs_to(lockd_family, LOCKD_CMD_SERVER_GET,
+                                       NULL, 0, rbuf, sizeof(rbuf));
+       if (n < 0)
+               return n;
+
+       grace = genl_find_attr(rbuf, n, LOCKD_A_SERVER_GRACETIME);
+       tcp = genl_find_attr(rbuf, n, LOCKD_A_SERVER_TCP_PORT);
+       udp = genl_find_attr(rbuf, n, LOCKD_A_SERVER_UDP_PORT);
+       if (!grace || !tcp || !udp)
+               return -ENOENT;
+
+       cfg->gracetime = nla_u32(grace);
+       cfg->tcp_port = nla_u16(tcp);
+       cfg->udp_port = nla_u16(udp);
+       return 0;
+}
+
+/* ------------------- SERVER_SET request builders ------------------- */
+
+static int put_gracetime(char *buf, int off, uint32_t grace)
+{
+       return put_attr(buf, off, LOCKD_A_SERVER_GRACETIME, &grace,
+                       sizeof(grace));
+}
+
+static int put_tcp_port(char *buf, int off, uint16_t tcp)
+{
+       return put_attr(buf, off, LOCKD_A_SERVER_TCP_PORT, &tcp, sizeof(tcp));
+}
+
+static int put_udp_port(char *buf, int off, uint16_t udp)
+{
+       return put_attr(buf, off, LOCKD_A_SERVER_UDP_PORT, &udp, sizeof(udp));
+}
+
+static int put_ports(char *buf, int off, uint16_t tcp, uint16_t udp)
+{
+       off = put_tcp_port(buf, off, tcp);
+       return put_udp_port(buf, off, udp);
+}
+
+FIXTURE(lockd_netlink) {
+};
+
+FIXTURE_SETUP(lockd_netlink)
+{
+       if (geteuid() != 0)
+               SKIP(return, "must be run as root");
+       if (unshare(CLONE_NEWNET) < 0)
+               SKIP(return, "unshare(NEWNET): %s", strerror(errno));
+
+       lockd_family = genl_resolve(LOCKD_FAMILY_NAME, 
sizeof(LOCKD_FAMILY_NAME));
+       if (lockd_family < 0)
+               SKIP(return, "lockd netlink family not registered");
+}
+
+FIXTURE_TEARDOWN(lockd_netlink)
+{
+       /* Nothing to undo: the settings die with the namespace. */
+}
+
+/* A fresh namespace starts out with everything at zero. */
+TEST_F(lockd_netlink, defaults_are_zero)
+{
+       struct lockd_cfg cfg;
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(0, cfg.gracetime);
+       EXPECT_EQ(0, cfg.tcp_port);
+       EXPECT_EQ(0, cfg.udp_port);
+}
+
+/*
+ * The regression: ports on their own, no grace time. This is the request
+ * nfsdctl builds from a [lockd] section that only sets the ports.
+ */
+TEST_F(lockd_netlink, set_ports_without_gracetime)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_ports(attrs, 0, TEST_TCP_PORT, TEST_UDP_PORT);
+
+       ASSERT_EQ(0, lockd_set(attrs, off))
+               TH_LOG("SERVER_SET rejected a request with no gracetime");
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(TEST_TCP_PORT, cfg.tcp_port);
+       EXPECT_EQ(TEST_UDP_PORT, cfg.udp_port);
+       EXPECT_EQ(0, cfg.gracetime);
+}
+
+/* The mirror image: a grace time with no ports. */
+TEST_F(lockd_netlink, set_gracetime_without_ports)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_gracetime(attrs, 0, 90);
+
+       ASSERT_EQ(0, lockd_set(attrs, off));
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(90, cfg.gracetime);
+       EXPECT_EQ(0, cfg.tcp_port);
+       EXPECT_EQ(0, cfg.udp_port);
+}
+
+/* One attribute at a time leaves the others alone. */
+TEST_F(lockd_netlink, attributes_are_set_independently)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_tcp_port(attrs, 0, TEST_TCP_PORT);
+       ASSERT_EQ(0, lockd_set(attrs, off));
+
+       off = put_udp_port(attrs, 0, TEST_UDP_PORT);
+       ASSERT_EQ(0, lockd_set(attrs, off))
+               TH_LOG("SERVER_SET rejected a UDP-only request");
+
+       off = put_gracetime(attrs, 0, 30);
+       ASSERT_EQ(0, lockd_set(attrs, off));
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(30, cfg.gracetime);
+       EXPECT_EQ(TEST_TCP_PORT, cfg.tcp_port)
+               TH_LOG("a later SET clobbered the tcp port");
+       EXPECT_EQ(TEST_UDP_PORT, cfg.udp_port)
+               TH_LOG("a gracetime-only SET clobbered the udp port");
+}
+
+TEST_F(lockd_netlink, set_all_three)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_gracetime(attrs, 0, GRACE_MAX);
+       off = put_ports(attrs, off, TEST_TCP_PORT, TEST_UDP_PORT);
+
+       ASSERT_EQ(0, lockd_set(attrs, off));
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(GRACE_MAX, cfg.gracetime);
+       EXPECT_EQ(TEST_TCP_PORT, cfg.tcp_port);
+       EXPECT_EQ(TEST_UDP_PORT, cfg.udp_port);
+}
+
+/* An empty SERVER_SET has nothing to do, but is not an error. */
+TEST_F(lockd_netlink, empty_set_is_a_noop)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_gracetime(attrs, 0, 30);
+       off = put_ports(attrs, off, TEST_TCP_PORT, TEST_UDP_PORT);
+       ASSERT_EQ(0, lockd_set(attrs, off));
+
+       ASSERT_EQ(0, lockd_set(NULL, 0));
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(30, cfg.gracetime);
+       EXPECT_EQ(TEST_TCP_PORT, cfg.tcp_port);
+       EXPECT_EQ(TEST_UDP_PORT, cfg.udp_port);
+}
+
+/*
+ * The grace time is still range-checked, and the check runs before anything
+ * is stored: a rejected request must not apply the ports it came with.
+ */
+TEST_F(lockd_netlink, gracetime_above_max_rejected)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_gracetime(attrs, 0, GRACE_MAX + 1);
+       off = put_ports(attrs, off, TEST_TCP_PORT, TEST_UDP_PORT);
+
+       EXPECT_EQ(-EINVAL, lockd_set(attrs, off));
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(0, cfg.gracetime);
+       EXPECT_EQ(0, cfg.tcp_port)
+               TH_LOG("a rejected SERVER_SET applied the tcp port anyway");
+       EXPECT_EQ(0, cfg.udp_port);
+}
+
+/* Settings belong to the namespace that made them. */
+TEST_F(lockd_netlink, settings_are_per_netns)
+{
+       struct lockd_cfg cfg;
+       char attrs[64];
+       int off;
+
+       off = put_gracetime(attrs, 0, 30);
+       off = put_ports(attrs, off, TEST_TCP_PORT, TEST_UDP_PORT);
+       ASSERT_EQ(0, lockd_set(attrs, off));
+
+       if (unshare(CLONE_NEWNET) < 0)
+               SKIP(return, "second unshare(NEWNET): %s", strerror(errno));
+
+       ASSERT_EQ(0, lockd_get(&cfg));
+       EXPECT_EQ(0, cfg.gracetime)
+               TH_LOG("grace time leaked out of the namespace that set it");
+       EXPECT_EQ(0, cfg.tcp_port)
+               TH_LOG("tcp port leaked out of the namespace that set it");
+       EXPECT_EQ(0, cfg.udp_port);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/nfsd/nfsd_netlink.h 
b/tools/testing/selftests/nfsd/nfsd_netlink.h
index c66a980bfc01..bb9327109e8f 100644
--- a/tools/testing/selftests/nfsd/nfsd_netlink.h
+++ b/tools/testing/selftests/nfsd/nfsd_netlink.h
@@ -4,7 +4,9 @@
  *
  * Header-only: every helper is static inline, so each test binary gets its
  * own copy and there is nothing extra to link. nfsd_family must be set by
- * calling genl_resolve_nfsd() before any of the request helpers are used.
+ * calling genl_resolve_nfsd() before any of the request helpers are used;
+ * the *_to() variants take a family id instead, for the other families in
+ * the NFS server stack.
  */
 #ifndef __SELFTESTS_NFSD_NETLINK_H__
 #define __SELFTESTS_NFSD_NETLINK_H__
@@ -100,6 +102,45 @@ static inline int put_attr(char *buf, int off, uint16_t 
type,
        return off + NLA_ALIGN4(NLA_HDRLEN + len);
 }
 
+/* Payload accessors; the payload is only 4-byte aligned, so no direct load. */
+static inline uint32_t nla_u32(const struct nlattr *na)
+{
+       uint32_t v;
+
+       memcpy(&v, (const char *)na + NLA_HDRLEN, sizeof(v));
+       return v;
+}
+
+static inline uint16_t nla_u16(const struct nlattr *na)
+{
+       uint16_t v;
+
+       memcpy(&v, (const char *)na + NLA_HDRLEN, sizeof(v));
+       return v;
+}
+
+/* Find top-level attribute @type in a genl reply of @len bytes; NULL if 
absent. */
+static inline const struct nlattr *genl_find_attr(const char *rbuf, int len,
+                                                 uint16_t type)
+{
+       const struct nlmsghdr *nlh = (const void *)rbuf;
+       const struct nlattr *na;
+       int left;
+
+       if (len < (int)(NLMSG_HDRLEN + GENL_HDRLEN))
+               return NULL;
+       na = (const void *)(rbuf + NLMSG_HDRLEN + GENL_HDRLEN);
+       left = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
+
+       while (left >= (int)NLA_HDRLEN) {
+               if ((na->nla_type & NLA_TYPE_MASK) == type)
+                       return na;
+               left -= NLA_ALIGN4(na->nla_len);
+               na = (const void *)((const char *)na + NLA_ALIGN4(na->nla_len));
+       }
+       return NULL;
+}
+
 /* Build a genl message header into @buf; return the offset past it. */
 static inline int genl_hdr(char *buf, uint16_t type, uint16_t flags, uint8_t 
cmd)
 {
@@ -115,15 +156,16 @@ static inline int genl_hdr(char *buf, uint16_t type, 
uint16_t flags, uint8_t cmd
        return NLMSG_HDRLEN + GENL_HDRLEN;
 }
 
-/* Send an nfsd command with an ACK; return the ACK errno (<= 0). */
-static inline int genl_request(uint8_t cmd, const char *attrs, int attrs_len)
+/* Send a command to @family with an ACK; return the ACK errno (<= 0). */
+static inline int genl_request_to(uint16_t family, 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);
+       off = genl_hdr(buf, family, NLM_F_REQUEST | NLM_F_ACK, cmd);
        if (attrs_len) {
                memcpy(buf + off, attrs, attrs_len);
                off += attrs_len;
@@ -147,21 +189,27 @@ static inline int genl_request(uint8_t cmd, const char 
*attrs, int attrs_len)
        return ret;
 }
 
+static inline int genl_request(uint8_t cmd, const char *attrs, int attrs_len)
+{
+       return genl_request_to(nfsd_family, cmd, attrs, attrs_len);
+}
+
 /*
- * 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.
+ * Send a command to @family 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 inline int genl_request_reply_attrs(uint8_t cmd, const char *attrs,
-                                          int attrs_len, char *rbuf,
-                                          size_t rlen)
+static inline int genl_request_reply_attrs_to(uint16_t family, uint8_t cmd,
+                                             const char *attrs, int attrs_len,
+                                             char *rbuf, size_t rlen)
 {
        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);
+       off = genl_hdr(buf, family, NLM_F_REQUEST, cmd);
        if (attrs_len) {
                memcpy(buf + off, attrs, attrs_len);
                off += attrs_len;
@@ -182,13 +230,21 @@ static inline int genl_request_reply_attrs(uint8_t cmd, 
const char *attrs,
        return ret;
 }
 
+static inline int genl_request_reply_attrs(uint8_t cmd, const char *attrs,
+                                          int attrs_len, char *rbuf,
+                                          size_t rlen)
+{
+       return genl_request_reply_attrs_to(nfsd_family, cmd, attrs, attrs_len,
+                                          rbuf, rlen);
+}
+
 static inline 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 inline int genl_resolve_nfsd(void)
+/* Resolve a genl family id by name; -1 if not registered. */
+static inline int genl_resolve(const char *name, size_t namelen)
 {
        char buf[1024], rbuf[4096];
        struct nlmsghdr *nlh = (void *)buf;
@@ -198,8 +254,7 @@ static inline int genl_resolve_nfsd(void)
 
        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));
+       off = put_attr(buf, off, CTRL_ATTR_FAMILY_NAME, name, namelen);
        nlh->nlmsg_len = off;
 
        if (send(fd, buf, off, 0) < 0)
@@ -224,6 +279,11 @@ static inline int genl_resolve_nfsd(void)
        return id;
 }
 
+static inline int genl_resolve_nfsd(void)
+{
+       return genl_resolve(NFSD_FAMILY_NAME, sizeof(NFSD_FAMILY_NAME));
+}
+
 /* ------------------- listener request builders ------------------- */
 
 /* Fine-grained control for negative tests: any field can be 
omitted/malformed. */

-- 
2.55.0


Reply via email to