mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jeff Layton <jlayton@kernel.org>
To: Chuck Lever <cel@kernel.org>, NeilBrown <neil@brown.name>,
	 Olga Kornievskaia <okorniev@redhat.com>,
	Dai Ngo <Dai.Ngo@oracle.com>,  Tom Talpey <tom@talpey.com>,
	Shuah Khan <shuah@kernel.org>
Cc: linux-nfs@vger.kernel.org, linux-kernel@vger.kernel.org,
	 linux-kselftest@vger.kernel.org,
	Jeff Layton <jlayton@kernel.org>
Subject: [PATCH v2 7/8] selftests/nfsd: add cross-namespace isolation tests
Date: Tue, 22 Sep 2026 07:34:08 -0400	[thread overview]
Message-ID: <20260922-nfsd-per-net-mutex-v2-7-4a5da8243a73@kernel.org> (raw)
In-Reply-To: <20260922-nfsd-per-net-mutex-v2-0-4a5da8243a73@kernel.org>

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 <jlayton@kernel.org>
---
 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


  parent reply	other threads:[~2026-09-22 11:34 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-22 11:34 [PATCH v2 0/8] nfsd: reduce the scope of the nfsd_mutex Jeff Layton
2026-09-22 11:34 ` [PATCH v2 1/8] nfsd: clear NFSD_NET_UP before dropping the generic resource reference Jeff Layton
2026-09-22 11:34 ` [PATCH v2 2/8] nfsd: make max_blksize a per-namespace setting Jeff Layton
2026-09-22 11:34 ` [PATCH v2 3/8] nfsd: move the control plane to a per-namespace mutex Jeff Layton
2026-09-22 11:34 ` [PATCH v2 4/8] nfsd: rename nfsd_mutex to nfsd_global_mutex Jeff Layton
2026-09-22 11:34 ` [PATCH v2 5/8] nfsd: give the open file cache its own mutex Jeff Layton
2026-09-22 11:34 ` [PATCH v2 6/8] selftests/nfsd: factor the netlink plumbing into a shared header Jeff Layton
2026-09-22 11:34 ` Jeff Layton [this message]
2026-09-22 11:34 ` [PATCH v2 8/8] selftests/nfsd: add a cross-namespace control-plane soak Jeff Layton
2026-09-22 15:50 ` [PATCH v2 0/8] nfsd: reduce the scope of the nfsd_mutex Chuck Lever

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260922-nfsd-per-net-mutex-v2-7-4a5da8243a73@kernel.org \
    --to=jlayton@kernel.org \
    --cc=Dai.Ngo@oracle.com \
    --cc=cel@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-nfs@vger.kernel.org \
    --cc=neil@brown.name \
    --cc=okorniev@redhat.com \
    --cc=shuah@kernel.org \
    --cc=tom@talpey.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®