Check that one namespace's NFSD settings and running server stay out of
another's, now that the control plane is serialized per namespace.

  max_blksize_is_per_netns
        max_block_size is reachable only through a per-netns nfsd
        filesystem but used to live in a module-wide variable. Read it in
        a throwaway namespace, change it in a second, read it again in a
        third; the two reads must agree. No value is hardcoded -- the
        default is derived from the size of memory, so the test picks a
        target that differs from whatever this machine reports.

  *_busy_is_per_netns, pool_stats_readable_with_foreign_server,
  listener_get_does_not_show_foreign_listeners
        max_block_size, VERSION_SET and nfsv4leasetime all refuse with
        -EBUSY once that namespace has a serv. A peer namespace holds a
        server up while the test pokes its own, which must not be
        affected. LISTENER_GET must not report the peer's listener.

  expkey_flush_with_foreign_server
        writing /proc/net/rpc/nfsd.fh/flush is the one userspace path that
        reaches nfsd_file_cache_purge(), and so the one that takes the
        file cache lock with nothing else held.

Servers are created with NFSD_A_SERVER_SOCK_USERSPACE_RPCBIND so nothing
calls out to rpcbind, and restricted to NFSv4.1 so nfsd_needs_lockd()
stays false. Each namespace mounts its own nfsd filesystem on a private
tmpfs; the module creates /proc/fs/nfs, not /proc/fs/nfsd, so there is no
mountpoint to borrow.

Assisted-by: LLM
Signed-off-by: Jeff Layton <[email protected]>
---
 tools/testing/selftests/nfsd/.gitignore            |   1 +
 tools/testing/selftests/nfsd/Makefile              |   1 +
 .../testing/selftests/nfsd/nfsd_netns_isolation.c  | 468 +++++++++++++++++++++
 tools/testing/selftests/nfsd/settings              |   2 +-
 4 files changed, 471 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/nfsd/.gitignore 
b/tools/testing/selftests/nfsd/.gitignore
index 19e6dec04d8e..0304b80eb844 100644
--- a/tools/testing/selftests/nfsd/.gitignore
+++ b/tools/testing/selftests/nfsd/.gitignore
@@ -1 +1,2 @@
 nfsd_netlink_listener
+nfsd_netns_isolation
diff --git a/tools/testing/selftests/nfsd/Makefile 
b/tools/testing/selftests/nfsd/Makefile
index 15ac65549d25..2b7c44c3fb00 100644
--- a/tools/testing/selftests/nfsd/Makefile
+++ b/tools/testing/selftests/nfsd/Makefile
@@ -2,5 +2,6 @@
 CFLAGS += $(KHDR_INCLUDES) -Wall
 
 TEST_GEN_PROGS := nfsd_netlink_listener
+TEST_GEN_PROGS += nfsd_netns_isolation
 
 include ../lib.mk
diff --git a/tools/testing/selftests/nfsd/nfsd_netns_isolation.c 
b/tools/testing/selftests/nfsd/nfsd_netns_isolation.c
new file mode 100644
index 000000000000..b39ea6a908fd
--- /dev/null
+++ b/tools/testing/selftests/nfsd/nfsd_netns_isolation.c
@@ -0,0 +1,468 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Namespace-isolation tests for the NFSD control plane.
+ *
+ * NFSD's per-namespace settings used to sit behind one module-wide mutex,
+ * and one of them -- the maximum READ/WRITE payload -- was a module-wide
+ * variable reachable through a per-netns file. These tests pin down the
+ * boundary: what one namespace does to its own server must not be visible
+ * to, or block, another.
+ *
+ * Every namespace here gets a private net + mount namespace, a tmpfs on
+ * /mnt so nothing escapes, and its own nfsd filesystem mounted on
+ * /mnt/nfsd. The module creates /proc/fs/nfs, not /proc/fs/nfsd, so the
+ * mount has to be made by hand; it is also what ties a running server to
+ * this test, since nfsd_umount() stops the threads.
+ *
+ * Servers are created with NFSD_A_SERVER_SOCK_USERSPACE_RPCBIND so nothing
+ * ever calls out to rpcbind, and restricted to NFSv4.1 so that
+ * nfsd_needs_lockd() stays false and no lockd instance is started.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include "../kselftest_harness.h"
+#include "nfsd_netlink.h"
+
+#define NFSD_MNT               "/mnt/nfsd"
+#define TEST_PORT              20049
+
+/* netns_enter() could not build a usable namespace; not a test failure. */
+#define NETNS_NO_SETUP         INT_MIN
+
+/*
+ * Build a private net + mount namespace with an nfsd filesystem on
+ * /mnt/nfsd and loopback up. Returns 0, or NETNS_NO_SETUP when the
+ * environment will not allow it.
+ */
+static int netns_enter(void)
+{
+       struct ifreq ifr = {0};
+       int s;
+
+       if (unshare(CLONE_NEWNET | CLONE_NEWNS) < 0)
+               return NETNS_NO_SETUP;
+       if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
+               return NETNS_NO_SETUP;
+
+       /*
+        * Everything below is created inside this mount namespace only, so
+        * the mkdir cannot leave anything behind on the host.
+        */
+       if (mount("tmpfs", "/mnt", "tmpfs", 0, NULL) < 0)
+               return NETNS_NO_SETUP;
+       if (mkdir(NFSD_MNT, 0755) < 0)
+               return NETNS_NO_SETUP;
+       if (mount("nfsd", NFSD_MNT, "nfsd", 0, NULL) < 0)
+               return NETNS_NO_SETUP;
+
+       s = socket(AF_INET, SOCK_DGRAM, 0);
+       if (s < 0)
+               return NETNS_NO_SETUP;
+       strcpy(ifr.ifr_name, "lo");
+       if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) {
+               close(s);
+               return NETNS_NO_SETUP;
+       }
+       ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
+       if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) {
+               close(s);
+               return NETNS_NO_SETUP;
+       }
+       close(s);
+
+       nfsd_family = genl_resolve_nfsd();
+       if (nfsd_family < 0)
+               return NETNS_NO_SETUP;
+       return 0;
+}
+
+/* ------------------- nfsdfs file access ------------------- */
+
+static int nfsd_file_read(const char *name, char *buf, size_t len)
+{
+       char path[128];
+       int fd, n;
+
+       snprintf(path, sizeof(path), "%s/%s", NFSD_MNT, name);
+       fd = open(path, O_RDONLY);
+       if (fd < 0)
+               return -errno;
+       n = read(fd, buf, len - 1);
+       close(fd);
+       if (n < 0)
+               return -errno;
+       buf[n] = '\0';
+       return n;
+}
+
+static int nfsd_file_read_int(const char *name)
+{
+       char buf[64];
+       int n = nfsd_file_read(name, buf, sizeof(buf));
+
+       if (n < 0)
+               return n;
+       return atoi(buf);
+}
+
+static int nfsd_file_write_int(const char *name, int val)
+{
+       char path[128], buf[64];
+       int fd, n, len;
+
+       snprintf(path, sizeof(path), "%s/%s", NFSD_MNT, name);
+       len = snprintf(buf, sizeof(buf), "%d\n", val);
+       fd = open(path, O_WRONLY);
+       if (fd < 0)
+               return -errno;
+       n = write(fd, buf, len);
+       close(fd);
+       return n < 0 ? -errno : 0;
+}
+
+/* ------------------- server lifecycle ------------------- */
+
+/*
+ * Bring up a v4.1-only server on a loopback listener, owning rpcbind
+ * registration in userspace so the kernel never issues an rpcbind call.
+ */
+static int server_start(uint16_t port, int nthreads)
+{
+       char attrs[128];
+       int off;
+       int ret;
+
+       ret = version_set_only(4, 1);
+       if (ret)
+               return ret;
+
+       off = put_listener(attrs, 0, "tcp", port);
+       off = put_attr(attrs, off, NFSD_A_SERVER_SOCK_USERSPACE_RPCBIND,
+                      NULL, 0);
+       ret = listener_set(attrs, off);
+       if (ret)
+               return ret;
+
+       return threads_set(nthreads);
+}
+
+static void server_stop(void)
+{
+       threads_set(0);
+       listener_set(NULL, 0);
+}
+
+/* ------------------- run a callback in a fresh namespace ------------------- 
*/
+
+/*
+ * Fork a child into its own namespace, run @fn there and hand back what it
+ * returned. Used for the checks that only need one namespace at a time.
+ */
+static int netns_run(int (*fn)(long), long arg)
+{
+       int p[2], ret = -EIO;
+       pid_t pid;
+
+       if (pipe(p) < 0)
+               return -errno;
+
+       pid = fork();
+       if (pid < 0) {
+               close(p[0]);
+               close(p[1]);
+               return -errno;
+       }
+       if (pid == 0) {
+               int r = netns_enter();
+
+               if (r == 0)
+                       r = fn(arg);
+               if (write(p[1], &r, sizeof(r)) != sizeof(r))
+                       _exit(1);
+               _exit(0);
+       }
+
+       close(p[1]);
+       if (read(p[0], &ret, sizeof(ret)) != sizeof(ret))
+               ret = -EIO;
+       close(p[0]);
+       waitpid(pid, NULL, 0);
+       return ret;
+}
+
+/* ------------------- a peer namespace held open ------------------- */
+
+/*
+ * A second namespace running a server for as long as the test needs it.
+ * The peer reports readiness on a pipe and waits for a byte before tearing
+ * down, so the test can be sure the server is up while it pokes its own
+ * namespace.
+ */
+struct peer {
+       pid_t pid;
+       int wake;               /* write here to let the peer exit */
+       int ready;              /* peer writes its status here */
+};
+
+static int peer_start(struct peer *pr, uint16_t port)
+{
+       int wake[2], ready[2];
+       pid_t ppid = getpid();
+       char status;
+
+       if (pipe(wake) < 0)
+               return -errno;
+       if (pipe(ready) < 0) {
+               close(wake[0]);
+               close(wake[1]);
+               return -errno;
+       }
+
+       /* peer_stop() handles a dead peer itself; do not die of SIGPIPE first. 
*/
+       signal(SIGPIPE, SIG_IGN);
+
+       pr->pid = fork();
+       if (pr->pid < 0) {
+               close(wake[0]); close(wake[1]);
+               close(ready[0]); close(ready[1]);
+               return -errno;
+       }
+       if (pr->pid == 0) {
+               char c;
+               int r;
+
+               /* Hold no writer of our own, so read() below sees EOF. */
+               close(wake[1]);
+               close(ready[0]);
+               /*
+                * A parent that dies without reaching peer_stop() would leave
+                * this namespace and its server pinned by a process blocked
+                * forever in read().
+                */
+               prctl(PR_SET_PDEATHSIG, SIGKILL);
+               if (getppid() != ppid)
+                       _exit(1);
+
+               r = netns_enter();
+               if (r == 0)
+                       r = server_start(port, 1);
+               status = r == NETNS_NO_SETUP ? 'S' : (r ? 'E' : 'R');
+               if (write(ready[1], &status, 1) != 1)
+                       _exit(1);
+               /* Hold the namespace open until the test is done with it. */
+               if (read(wake[0], &c, 1) == 1 && status == 'R')
+                       server_stop();
+               _exit(0);
+       }
+
+       close(wake[0]);
+       close(ready[1]);
+       pr->wake = wake[1];
+       pr->ready = ready[0];
+
+       if (read(pr->ready, &status, 1) != 1)
+               status = 'E';
+       if (status == 'S')
+               return NETNS_NO_SETUP;
+       return status == 'R' ? 0 : -EIO;
+}
+
+static void peer_stop(struct peer *pr)
+{
+       char c = 'x';
+
+       if (pr->pid <= 0)
+               return;
+       if (write(pr->wake, &c, 1) != 1)
+               kill(pr->pid, SIGKILL);
+       close(pr->wake);
+       close(pr->ready);
+       waitpid(pr->pid, NULL, 0);
+       pr->pid = 0;
+}
+
+/* ===================== max_block_size is per-namespace ===================== 
*/
+
+static int read_max_blksize(long unused)
+{
+       (void)unused;
+       return nfsd_file_read_int("max_block_size");
+}
+
+static int write_max_blksize(long val)
+{
+       int ret = nfsd_file_write_int("max_block_size", (int)val);
+
+       if (ret)
+               return ret;
+       /* Report what stuck, so the caller knows the write was accepted. */
+       return nfsd_file_read_int("max_block_size");
+}
+
+/*
+ * max_block_size is reachable only through a per-netns nfsd filesystem, so
+ * a write in one namespace must not be visible in another. Read it in a
+ * throwaway namespace, change it in a second, then read it again in a
+ * third: the two reads have to agree.
+ *
+ * No value is hardcoded. The default is derived from the size of memory,
+ * so the test picks a target that differs from whatever this machine uses.
+ */
+TEST(max_blksize_is_per_netns)
+{
+       int before, after, wrote, target;
+
+       before = netns_run(read_max_blksize, 0);
+       if (before == NETNS_NO_SETUP)
+               SKIP(return, "cannot set up a private nfsd namespace");
+       ASSERT_GE(before, 0);
+
+       /* Any legal value that is not the one this machine already reports. */
+       target = (before == 262144) ? 131072 : 262144;
+
+       wrote = netns_run(write_max_blksize, target);
+       ASSERT_EQ(target, wrote)
+               TH_LOG("second namespace did not accept max_block_size=%d",
+                      target);
+
+       after = netns_run(read_max_blksize, 0);
+       ASSERT_GE(after, 0);
+       EXPECT_EQ(before, after)
+               TH_LOG("max_block_size leaked between namespaces: %d -> %d",
+                      before, after);
+}
+
+/* ===================== a busy namespace does not busy others 
===================== */
+
+FIXTURE(nfsd_peer) {
+       struct peer pr;
+};
+
+FIXTURE_SETUP(nfsd_peer)
+{
+       int ret;
+
+       if (geteuid() != 0)
+               SKIP(return, "must be run as root");
+
+       /* The peer namespace comes first; it must not be ours. */
+       memset(&self->pr, 0, sizeof(self->pr));
+       ret = peer_start(&self->pr, TEST_PORT);
+       if (ret == NETNS_NO_SETUP)
+               SKIP(return, "cannot set up a private nfsd namespace");
+       if (ret)
+               SKIP(return, "peer namespace could not start a server: %d", 
ret);
+
+       /* Now put this process in a namespace of its own, with no server. */
+       if (netns_enter() != 0)
+               SKIP(return, "cannot set up a private nfsd namespace");
+}
+
+FIXTURE_TEARDOWN(nfsd_peer)
+{
+       if (nfsd_family >= 0)
+               server_stop();
+       peer_stop(&self->pr);
+}
+
+/*
+ * write_maxblksize() refuses with -EBUSY while that namespace has a serv.
+ * The check is on nn->nfsd_serv, so a server belonging to someone else must
+ * not trip it.
+ */
+TEST_F(nfsd_peer, maxblksize_busy_is_per_netns)
+{
+       int cur = nfsd_file_read_int("max_block_size");
+
+       ASSERT_GE(cur, 0);
+       EXPECT_EQ(0, nfsd_file_write_int("max_block_size",
+                                        cur == 262144 ? 131072 : 262144))
+               TH_LOG("max_block_size refused while another netns has a 
server");
+}
+
+/*
+ * NFSD_CMD_VERSION_SET returns -EBUSY once the namespace has a serv. A
+ * server in the peer namespace must not reach us.
+ */
+TEST_F(nfsd_peer, version_set_busy_is_per_netns)
+{
+       EXPECT_EQ(0, version_set_only(4, 1))
+               TH_LOG("VERSION_SET refused while another netns has a server");
+}
+
+/*
+ * The grace and lease times are per-namespace too, and gated on the same
+ * nn->nfsd_serv check.
+ */
+TEST_F(nfsd_peer, leasetime_busy_is_per_netns)
+{
+       EXPECT_EQ(0, nfsd_file_write_int("nfsv4leasetime", 60))
+               TH_LOG("nfsv4leasetime refused while another netns has a 
server");
+}
+
+/*
+ * pool_stats runs its seq_file under the mutex that also guards the serv.
+ * Reading it here must not be affected by the peer's server, and must
+ * report this namespace, which has no threads at all.
+ */
+TEST_F(nfsd_peer, pool_stats_readable_with_foreign_server)
+{
+       char buf[4096];
+       int n = nfsd_file_read("pool_stats", buf, sizeof(buf));
+
+       ASSERT_GE(n, 0)
+               TH_LOG("pool_stats unreadable: %s", strerror(-n));
+       EXPECT_NE(NULL, strstr(buf, "packets-arrived"));
+}
+
+/*
+ * LISTENER_GET reports this namespace's listeners. With a server running
+ * next door and none here, the list must come back empty rather than
+ * showing the peer's.
+ */
+TEST_F(nfsd_peer, listener_get_does_not_show_foreign_listeners)
+{
+       char rbuf[8192];
+       int n = genl_request_reply(NFSD_CMD_LISTENER_GET, rbuf, sizeof(rbuf));
+
+       ASSERT_GT(n, 0);
+       EXPECT_EQ(NULL, memmem(rbuf, n, "tcp", 4))
+               TH_LOG("LISTENER_GET leaked a listener from another netns");
+}
+
+/*
+ * The file cache is one host-wide object, but the flush that reaches it is
+ * driven from a per-netns file. Doing it here while the peer has a server
+ * up must be harmless -- this is the path that takes the cache lock with
+ * nothing else held.
+ */
+TEST_F(nfsd_peer, expkey_flush_with_foreign_server)
+{
+       int fd = open("/proc/net/rpc/nfsd.fh/flush", O_WRONLY);
+
+       if (fd < 0)
+               SKIP(return, "no /proc/net/rpc/nfsd.fh/flush: %s",
+                    strerror(errno));
+       EXPECT_EQ(2, write(fd, "1\n", 2));
+       close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/nfsd/settings 
b/tools/testing/selftests/nfsd/settings
index 6091b45d226b..694d70710ff0 100644
--- a/tools/testing/selftests/nfsd/settings
+++ b/tools/testing/selftests/nfsd/settings
@@ -1 +1 @@
-timeout=120
+timeout=300

-- 
2.55.0


Reply via email to