mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Josef Bacik <josef@toxicpanda.com>
To: Christian Brauner <brauner@kernel.org>,
	 James Bottomley <James.Bottomley@HansenPartnership.com>,
	 Andrew Halaney <andrew@amutable.com>,
	Jeremy Bingham <jbingham@gmail.com>,
	 Tahera Fahimi <taherafahimi@linux.microsoft.com>,
	 Serge Hallyn <sergeh@kernel.org>
Cc: linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
	 Josef Bacik <josef@toxicpanda.com>
Subject: [PATCH RFC v3 90/90] selftests: add tests for cells
Date: Thu, 05 Nov 2026 15:03:22 +0000	[thread overview]
Message-ID: <20261105-b4-cells-v3-90-bc9095937a84@toxicpanda.com> (raw)
In-Reply-To: <20261105-b4-cells-v3-0-bc9095937a84@toxicpanda.com>

The tests build cells, run programs in them, kill them, and try the things
that are not supposed to work.

There are seven test programs, one per topic: the life of a cell,
spawning, the slots, ids, the boundary, the network and cgroups. Each of
them acts as a builder, and cell_test.h holds the code that they share.
cell_child is a statically linked program that is copied into the
otherwise empty tree of each cell. It reports what it finds from the
inside.

The tests cover:

  - the states of a cell, and that it is not reported dead early
  - empty pidfds, and what a new task does and does not inherit
  - capabilities: a task has what the builder named and never gains more
  - setns(), unshare() and related calls, refused from both sides
  - set-id bits and file capabilities
  - unix sockets between a cell and the host
  - the id window, ids that must not reach a filesystem of the host, mount
    options, overlayfs, and that the builder is told the block
  - device nodes and supplementary groups
  - the permission checks on the slots, and every allocation in the seal
    failing in turn
  - what can and cannot get into a budget, what a cell and its builder can
    do below it, and what a cell cannot do to the budget itself
  - a network that is set up from the outside, no network at all, the
    rights of a cell over its network, and that the network goes away with
    the cell
  - what seccomp does and does not cover
  - that the host runs no program and takes no core dump for a cell
  - an unprivileged builder, and a pod made of several cells
  - a kill, a close and a start racing with spawns in flight

The tests need root, cgroup2 with the memory and pids controllers, and
tmpfs. Some need more and are skipped otherwise. CELL_TEST_EXT4_IMG names
an ext4 image that can be frozen. CELL_TEST_9P_TAG names a writable 9p
share, for ->atomic_open(). /dev/vhost-net is used if it exists.

Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
 tools/testing/selftests/Makefile                  |    1 +
 tools/testing/selftests/cell/.gitignore           |    2 +
 tools/testing/selftests/cell/Makefile             |   14 +
 tools/testing/selftests/cell/cell_boundary_test.c |  535 +++++++
 tools/testing/selftests/cell/cell_cgroup_test.c   |  546 +++++++
 tools/testing/selftests/cell/cell_child.c         | 1626 +++++++++++++++++++++
 tools/testing/selftests/cell/cell_ids_test.c      |  762 ++++++++++
 tools/testing/selftests/cell/cell_life_test.c     |  446 ++++++
 tools/testing/selftests/cell/cell_net_test.c      |  915 ++++++++++++
 tools/testing/selftests/cell/cell_slots_test.c    |  748 ++++++++++
 tools/testing/selftests/cell/cell_spawn_test.c    |  559 +++++++
 tools/testing/selftests/cell/cell_test.h          |  586 ++++++++
 tools/testing/selftests/cell/cell_util.h          |  108 ++
 tools/testing/selftests/cell/config               |   35 +
 14 files changed, 6883 insertions(+)

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 07c3719f8b04..45b78a73fa2e 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -8,6 +8,7 @@ TARGETS += bpf
 TARGETS += breakpoints
 TARGETS += cachestat
 TARGETS += capabilities
+TARGETS += cell
 TARGETS += cgroup
 TARGETS += clone3
 TARGETS += connector
diff --git a/tools/testing/selftests/cell/.gitignore b/tools/testing/selftests/cell/.gitignore
new file mode 100644
index 000000000000..585ed8e41c1c
--- /dev/null
+++ b/tools/testing/selftests/cell/.gitignore
@@ -0,0 +1,2 @@
+cell_*_test
+cell_child
diff --git a/tools/testing/selftests/cell/Makefile b/tools/testing/selftests/cell/Makefile
new file mode 100644
index 000000000000..ccf1fa3c6565
--- /dev/null
+++ b/tools/testing/selftests/cell/Makefile
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+TEST_GEN_PROGS := cell_life_test cell_spawn_test cell_slots_test cell_ids_test
+TEST_GEN_PROGS += cell_boundary_test cell_net_test cell_cgroup_test
+TEST_GEN_FILES := cell_child
+
+include ../lib.mk
+
+# Runs inside a cell, whose tree holds nothing else.
+$(OUTPUT)/cell_child: cell_child.c cell_util.h
+	$(CC) $(CFLAGS) -static -o $@ $<
+
+$(TEST_GEN_PROGS): cell_test.h cell_util.h
diff --git a/tools/testing/selftests/cell/cell_boundary_test.c b/tools/testing/selftests/cell/cell_boundary_test.c
new file mode 100644
index 000000000000..483a5e642ebf
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_boundary_test.c
@@ -0,0 +1,535 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for cells, the boundary: nobody enters, a uid means nothing across it,
+ * and sockets carry bytes.
+ */
+#include "cell_test.h"
+
+TEST_F(cell, membership)
+{
+	RUN_IN_CELL("membership");
+}
+
+TEST_F(cell, nobody_enters)
+{
+	static const char * const ns[] = { "user", "mnt", "pid", "ipc", "uts", "cgroup" };
+	char path[64];
+	unsigned int i;
+	pid_t pid;
+	int pidfd, fd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+
+	for (i = 0; i < ARRAY_SIZE(ns); i++) {
+		snprintf(path, sizeof(path), "/proc/%d/ns/%s", pid, ns[i]);
+		fd = open(path, O_RDONLY);
+		ASSERT_GE(fd, 0);
+		EXPECT_EQ(setns(fd, 0), -1) TH_LOG("entered %s", ns[i]);
+		EXPECT_EQ(errno, EPERM) TH_LOG("%s", ns[i]);
+		close(fd);
+	}
+	EXPECT_EQ(setns(pidfd, CLONE_NEWIPC), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(pidfd);
+}
+
+/* Not while it is built either. Whoever got in could become the init of the cell. */
+TEST_F(cell, nobody_enters_while_it_is_built)
+{
+	int fd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+	fd = cell_open(self->c.cfd, CELL_OPEN_SLOT, "pids", NULL, 0);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(setns(fd, CLONE_NEWPID), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(fd);
+}
+
+static int drop_caps(void)
+{
+	struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 };
+	struct __user_cap_data_struct data[2] = {};
+
+	return syscall(SYS_capset, &hdr, data);
+}
+
+/* A one-id cell runs on the kuid of whoever made it. That must not count. */
+/* Have the kernel send @pid a SIGUSR1 on behalf of the caller. */
+static int sigio_to(pid_t pid)
+{
+	int sp[2], ret = -1;
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp))
+		return -1;
+	if (!fcntl(sp[0], F_SETOWN, pid) && !fcntl(sp[0], F_SETSIG, SIGUSR1) &&
+	    !fcntl(sp[0], F_SETFL, O_ASYNC) && write(sp[1], "x", 1) == 1)
+		ret = 0;
+	close(sp[0]);
+	close(sp[1]);
+	return ret;
+}
+
+#define V1ROOT	"/tmp/cell-test-v1"
+
+TEST_F(cell, equal_uid_means_nothing)
+{
+	int pidfd, status;
+	pid_t pid, plain, prober;
+	bool v1;
+
+	/* A cgroup v1 hierarchy with a group that uid 1000 may write to. */
+	mkdir(V1ROOT, 0755);
+	v1 = !mount("none", V1ROOT, "cgroup", 0, "none,name=celltest");
+	if (v1) {
+		ASSERT_EQ(mkdir(V1ROOT "/sub", 0755), 0);
+		ASSERT_EQ(chown(V1ROOT "/sub/cgroup.procs", 1000, 1000), 0);
+	} else {
+		TH_LOG("no cgroup v1, that part is skipped");
+	}
+
+	/* Made by uid 1000, with the privilege the mount work takes. */
+	ASSERT_EQ(prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), 0);
+	plain = fork();
+	ASSERT_GE(plain, 0);
+	if (!plain) {
+		if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000) ||
+		    drop_caps() || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		pause();
+		_exit(0);
+	}
+
+	ASSERT_EQ(setresgid(-1, 1000, -1), 0);
+	ASSERT_EQ(setresuid(-1, 1000, -1), 0);
+	self->c.cfd = cell_create(CELL_SELF, 0);
+	ASSERT_EQ(setresuid(-1, 0, -1), 0);
+	ASSERT_EQ(setresgid(-1, 0, -1), 0);
+	ASSERT_GE(self->c.cfd, 0);
+	close(self->c.cfd);
+	self->c.cfd = -1;
+
+	/* cell_build() makes its own: do all of it as euid 1000. */
+	prober = fork();
+	ASSERT_GE(prober, 0);
+	if (!prober) {
+		struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 };
+		struct __user_cap_data_struct data[2];
+		struct pollfd pfd = { .events = POLLIN };
+		struct sched_param sp = { 0 };
+		struct rlimit rl;
+		struct cellctx c;
+		cpu_set_t cpus;
+		pid_t victim;
+		char buf[32];
+		int step;
+
+		if (chown(CGROOT, 1000, 1000) || chown(CGROOT "/cgroup.procs", 1000, 1000))
+			_exit(10);
+		if (setgroups(2, (gid_t[]){ 2000, 2001 }))
+			_exit(11);
+		if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+			_exit(11);
+		if (syscall(SYS_capget, &hdr, data))
+			_exit(12);
+		data[0].effective = data[0].permitted;
+		data[1].effective = data[1].permitted;
+		if (syscall(SYS_capset, &hdr, data))
+			_exit(13);
+
+		step = cell_build(&c, &(struct build){ .ids = 1 });
+		if (step)
+			_exit(20 + step);
+		pidfd = spawn(c.cfd, &pid, "oneid", "1000", NULL);
+		if (pidfd < 0)
+			_exit(14);
+		if (cell_wait(c.cfd))
+			_exit(15);
+		close(pidfd);
+		cell_cleanup(&c);
+
+		/* It would lose a capability with its first exec, so it gets none. */
+		step = cell_build(&c, &(struct build){ .ids = 1, .caps = CAP(CHOWN),
+						       .no_seal = true });
+		if (step)
+			_exit(80 + step);
+		if (!cell_configure(c.cfd, CELL_CMD_SEAL, NULL, NULL, 0) || errno != EINVAL)
+			_exit(16);
+		cell_cleanup(&c);
+
+		step = cell_build(&c, &(struct build){ .ids = 1 });
+		if (step)
+			_exit(50 + step);
+		pidfd = spawn(c.cfd, &pid, "usr1", NULL, NULL);
+		if (pidfd < 0)
+			_exit(16);
+		if (host_uid(pid) != 1000)
+			_exit(17);
+
+		/* Now be nobody in particular, with the very same uid. */
+		if (drop_caps())
+			_exit(18);
+		if (kill(plain, 0))
+			_exit(19);		/* an equal uid at home counts */
+		if (kill(pid, 0) != -1 || errno != EPERM)
+			_exit(2);
+		if (kill(pid, SIGKILL) != -1 || errno != EPERM)
+			_exit(3);
+		if (setpriority(PRIO_PROCESS, pid, 5) != -1 || errno != EPERM)
+			_exit(4);
+		/* IOPRIO_WHO_PROCESS, IOPRIO_CLASS_IDLE */
+		if (syscall(SYS_ioprio_set, 1, plain, 3 << 13))
+			_exit(7);
+		if (syscall(SYS_ioprio_set, 1, pid, 3 << 13) != -1 || errno != EPERM)
+			_exit(8);
+		CPU_ZERO(&cpus);
+		CPU_SET(0, &cpus);
+		if (sched_setaffinity(plain, sizeof(cpus), &cpus))
+			_exit(100);
+		if (sched_setaffinity(pid, sizeof(cpus), &cpus) != -1 || errno != EPERM)
+			_exit(101);
+		if (sched_setscheduler(plain, SCHED_BATCH, &sp))
+			_exit(102);
+		if (sched_setscheduler(pid, SCHED_BATCH, &sp) != -1 || errno != EPERM)
+			_exit(103);
+		if (prlimit(plain, RLIMIT_NOFILE, NULL, &rl))
+			_exit(104);
+		if (prlimit(pid, RLIMIT_NOFILE, NULL, &rl) != -1 || errno != EPERM)
+			_exit(105);
+		if (v1) {
+			snprintf(buf, sizeof(buf), "%d", plain);
+			if (write_file(V1ROOT "/sub/cgroup.procs", buf))
+				_exit(106);
+			snprintf(buf, sizeof(buf), "%d", pid);
+			if (write_file(V1ROOT "/sub/cgroup.procs", buf) != -1 ||
+			    errno != EACCES)
+				_exit(107);
+		}
+		/* SIGIO: fatal at home, never sent across. It would end the cell. */
+		victim = fork();
+		if (victim < 0)
+			_exit(108);
+		if (!victim) {
+			pause();
+			_exit(0);
+		}
+		if (sigio_to(victim) || waitpid(victim, &step, 0) != victim ||
+		    !WIFSIGNALED(step) || WTERMSIG(step) != SIGUSR1)
+			_exit(109);
+		if (sigio_to(pid))
+			_exit(110);
+		pfd.fd = pidfd;
+		if (poll(&pfd, 1, 200) != 0)
+			_exit(111);
+		/*
+		 * Last, since a traced task takes its signals differently. The
+		 * capability LSM refuses this one too, for the user namespace.
+		 */
+		if (ptrace(PTRACE_SEIZE, plain, 0, 0))
+			_exit(112);
+		if (ptrace(PTRACE_SEIZE, pid, 0, 0) != -1 || errno != EPERM)
+			_exit(113);
+		/* The handle is what counts. */
+		if (cell_kill(c.cfd, 0))
+			_exit(5);
+		if (cell_wait(c.cfd) != SIGKILL)
+			_exit(6);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(prober, &status, 0), prober);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+
+	kill(plain, SIGKILL);
+	waitpid(plain, NULL, 0);
+	if (v1) {
+		EXPECT_EQ(rmdir(V1ROOT "/sub"), 0);
+		EXPECT_EQ(umount(V1ROOT), 0);
+	}
+	rmdir(V1ROOT);
+	EXPECT_EQ(chown(CGROOT, 0, 0), 0);
+	EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0);
+}
+
+/* A hard boundary: the uid of root on the host counts for no more than any other. */
+TEST_F(cell, uid_0_means_nothing_either)
+{
+	struct pollfd pfd = { .events = POLLIN };
+	char buf[32];
+	int ready[2];
+	pid_t pid;
+	bool v1;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(pipe(ready), 0);
+	pfd.fd = prepare(self->c.cfd, "usr1", NULL, NULL);
+	ASSERT_GE(pfd.fd, 0);
+	ASSERT_EQ(pidfd_configure(pfd.fd, PIDFD_SET_FD, "3", NULL, ready[1]), 0);
+	ASSERT_EQ(pidfd_configure(pfd.fd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	pid = pidfd_nr(pfd.fd);
+	ASSERT_GT(pid, 0);
+	close(ready[1]);
+	/* Until it has its handler an init does not get the signal anyway. */
+	ASSERT_EQ(read(ready[0], buf, 1), 1);
+	close(ready[0]);
+
+	/* It would end the cell. */
+	ASSERT_EQ(sigio_to(pid), 0);
+	EXPECT_EQ(poll(&pfd, 1, 200), 0);
+
+	mkdir(V1ROOT, 0755);
+	v1 = !mount("none", V1ROOT, "cgroup", 0, "none,name=celltest");
+	if (v1) {
+		ASSERT_EQ(mkdir(V1ROOT "/sub", 0755), 0);
+		snprintf(buf, sizeof(buf), "%d", pid);
+		EXPECT_EQ(write_file(V1ROOT "/sub/cgroup.procs", buf), -1);
+		EXPECT_EQ(errno, EACCES);
+		EXPECT_EQ(rmdir(V1ROOT "/sub"), 0);
+		EXPECT_EQ(umount(V1ROOT), 0);
+	} else {
+		TH_LOG("no cgroup v1, that part is skipped");
+	}
+	rmdir(V1ROOT);
+	close(pfd.fd);
+}
+
+TEST_F(cell, sockets_carry_bytes)
+{
+	struct sockaddr_un addr = { .sun_family = AF_UNIX };
+	char ctl[CMSG_SPACE(sizeof(int))] = {}, c = 'h';
+	struct iovec iov = { .iov_base = &c, .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = ctl, .msg_controllen = sizeof(ctl),
+	};
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+	int pidfd, sv[2], lsock, conn, zero = 0, cwd;
+	struct ucred uc;
+	socklen_t len;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+
+	/* A listener of the host's that the cell can reach by name. */
+	lsock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	cwd = open(".", O_RDONLY | O_DIRECTORY);
+	ASSERT_EQ(fchdir(self->c.raw), 0);
+	strcpy(addr.sun_path, "tmp/sock");
+	ASSERT_EQ(bind(lsock, (struct sockaddr *)&addr, sizeof(addr)), 0);
+	ASSERT_EQ(chmod("tmp/sock", 0777), 0);
+	ASSERT_EQ(fchdir(cwd), 0);
+	ASSERT_EQ(listen(lsock, 1), 0);
+	ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0);
+
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv), 0);
+	pidfd = prepare(self->c.cfd, "unix", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+
+	/* A file in flight from before the socket is given away. */
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+	memcpy(CMSG_DATA(cmsg), &zero, sizeof(int));
+	c = 'q';
+	ASSERT_EQ(sendmsg(sv[0], &msg, 0), 1);
+	c = 'h';
+
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, sv[1]), 0);
+	close(sv[1]);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+
+	/* From the moment it was given away. */
+	EXPECT_EQ(sendmsg(sv[0], &msg, 0), -1);
+	EXPECT_EQ(errno, EPERM);
+
+	ASSERT_EQ(read(sv[0], &c, 1), 1);
+	EXPECT_EQ(c, 'a');
+	ASSERT_EQ(read(sv[0], &c, 1), 1);
+	EXPECT_EQ(c, 'b');
+	ASSERT_EQ(write(sv[0], "h", 1), 1);
+
+	ASSERT_EQ(wait_for(lsock, pidfd), 0);
+	conn = accept(lsock, NULL, NULL);
+	ASSERT_GE(conn, 0);
+	len = sizeof(uc);
+	EXPECT_EQ(getsockopt(conn, SOL_SOCKET, SO_PEERCRED, &uc, &len), -1);
+	EXPECT_EQ(errno, ENODATA);
+	c = 'h';
+	EXPECT_EQ(sendmsg(conn, &msg, 0), -1);
+	EXPECT_EQ(errno, EPERM);
+	ASSERT_EQ(read(conn, &c, 1), 1);
+	EXPECT_EQ(c, 'c');
+
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(conn);
+	close(lsock);
+	close(sv[0]);
+	close(pidfd);
+	close(cwd);
+}
+
+/* See check_peer_race(). */
+TEST_F(cell, peer_is_checked_and_read_at_once)
+{
+	struct sockaddr_un addr = { .sun_family = AF_UNIX };
+	int lsock, cwd, pidfd, status;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+	lsock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	ASSERT_GE(lsock, 0);
+	cwd = open(".", O_RDONLY | O_DIRECTORY);
+	ASSERT_EQ(fchdir(self->c.raw), 0);
+	strcpy(addr.sun_path, "tmp/sock");
+	ASSERT_EQ(bind(lsock, (struct sockaddr *)&addr, sizeof(addr)), 0);
+	ASSERT_EQ(chmod("tmp/sock", 0777), 0);
+	ASSERT_EQ(fchdir(cwd), 0);
+	close(cwd);
+	ASSERT_EQ(listen(lsock, 128), 0);
+	ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		for (;;) {
+			int conn = accept(lsock, NULL, NULL);
+
+			if (conn >= 0)
+				close(conn);
+		}
+	}
+	pidfd = spawn(self->c.cfd, NULL, "peer-race", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+	kill(pid, SIGKILL);
+	waitpid(pid, &status, 0);
+	close(lsock);
+}
+
+#define HELPER_RAN	"/tmp/cell_test_helper_ran"
+#define CORE_SOCK	"/tmp/cell_test_core.sock"
+
+static int read_line(const char *path, char *buf, size_t len)
+{
+	int fd = open(path, O_RDONLY | O_CLOEXEC);
+	ssize_t n = fd < 0 ? -1 : read(fd, buf, len - 1);
+
+	close(fd);
+	if (n < 0)
+		return -1;
+	buf[n] = 0;
+	buf[strcspn(buf, "\n")] = 0;
+	return 0;
+}
+
+/* Has the kernel run the helper with @arg among its arguments? It may take a moment. */
+static char helper_log[4096];
+
+static bool helper_ran(const char *arg)
+{
+	char *buf = helper_log;
+	int i, fd;
+
+	for (i = 0; i < 20; i++) {
+		fd = open(HELPER_RAN, O_RDONLY | O_CLOEXEC);
+		memset(buf, 0, sizeof(helper_log));
+		if (fd >= 0 && read(fd, buf, sizeof(helper_log) - 1) > 0 && strstr(buf, arg)) {
+			close(fd);
+			return true;
+		}
+		close(fd);
+		usleep(50000);
+	}
+	return false;
+}
+
+/*
+ * There are things a task can do that make the kernel run a program: as root, on the
+ * host, in no namespace but the first. Not for a task of a cell.
+ */
+TEST_F(cell, host_runs_no_program_for_a_cell)
+{
+	char self_path[256] = {}, child[300], pattern[340], modprobe[256], core[256];
+	struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = CORE_SOCK };
+	bool request_key = false;
+	int pidfd, status, lsock;
+	struct pollfd pfd;
+	pid_t pid;
+
+	if (read_line("/proc/sys/kernel/modprobe", modprobe, sizeof(modprobe)) ||
+	    read_line("/proc/sys/kernel/core_pattern", core, sizeof(core)))
+		SKIP(return, "no modules or no core dumps");
+	ASSERT_GT(readlink("/proc/self/exe", self_path, sizeof(self_path) - 1), 0);
+	*strrchr(self_path, '/') = 0;
+	snprintf(child, sizeof(child), "%s/cell_child", self_path);
+	snprintf(pattern, sizeof(pattern), "|%s core %%p", child);
+	ASSERT_EQ(write_file("/proc/sys/kernel/modprobe", child), 0);
+	ASSERT_EQ(write_file("/proc/sys/kernel/core_pattern", pattern), 0);
+	/* The path of this one is fixed. Leave a real one alone. */
+	mkdir("/sbin", 0755);
+	request_key = !symlink(child, "/sbin/request-key");
+
+	/* They do tell: this is what it looks like for a task of the host. */
+	unlink(HELPER_RAN);
+	EXPECT_EQ(socket(AF_ECONET, SOCK_DGRAM, 0), -1);
+	EXPECT_TRUE(helper_ran("net-pf-19"));
+	if (request_key) {
+		syscall(SYS_request_key, "user", "cell:test", "make it", KEY_SPEC_PROCESS_KEYRING);
+		EXPECT_TRUE(helper_ran("request-key"));
+	}
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid)
+		raise(SIGSEGV);
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_TRUE(helper_ran(" core "));
+
+	unlink(HELPER_RAN);
+	EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new" }), 0);
+	pidfd = spawn(self->c.cfd, NULL, "helpers", NULL, NULL);
+	EXPECT_GE(pidfd, 0);
+	/* Killed by the signal, and nothing was dumped. */
+	EXPECT_EQ(cell_wait(self->c.cfd), SIGILL);
+	close(pidfd);
+	usleep(300000);
+	EXPECT_EQ(access(HELPER_RAN, F_OK), -1) {
+		helper_ran("\n\n");
+		TH_LOG("it ran: %s", helper_log);
+	}
+	cell_cleanup(&self->c);
+
+	/* A core dump can go to a socket as well, where a daemon of the host listens. */
+	unlink(CORE_SOCK);
+	lsock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	ASSERT_GE(lsock, 0);
+	ASSERT_EQ(bind(lsock, (struct sockaddr *)&addr, sizeof(addr)), 0);
+	ASSERT_EQ(listen(lsock, 4), 0);
+	pfd = (struct pollfd){ .fd = lsock, .events = POLLIN };
+	if (!write_file("/proc/sys/kernel/core_pattern", "@" CORE_SOCK)) {
+		pid = fork();
+		ASSERT_GE(pid, 0);
+		if (!pid)
+			raise(SIGSEGV);
+		EXPECT_EQ(poll(&pfd, 1, 5000), 1);
+		close(accept(lsock, NULL, NULL));
+		ASSERT_EQ(waitpid(pid, &status, 0), pid);
+
+		EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+		pidfd = spawn(self->c.cfd, NULL, "helpers", NULL, NULL);
+		EXPECT_GE(pidfd, 0);
+		EXPECT_EQ(cell_wait(self->c.cfd), SIGILL);
+		close(pidfd);
+		EXPECT_EQ(poll(&pfd, 1, 300), 0);
+	}
+	close(lsock);
+	unlink(CORE_SOCK);
+
+	unlink(HELPER_RAN);
+	if (request_key)
+		unlink("/sbin/request-key");
+	EXPECT_EQ(write_file("/proc/sys/kernel/modprobe", modprobe), 0);
+	EXPECT_EQ(write_file("/proc/sys/kernel/core_pattern", core), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_cgroup_test.c b/tools/testing/selftests/cell/cell_cgroup_test.c
new file mode 100644
index 000000000000..6bd4f21a3fec
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_cgroup_test.c
@@ -0,0 +1,546 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Cells: cgroups below the budget. Whoever built the cell lays them out, and so
+ * does the cell itself, as an init that manages cgroups will. The budget is what
+ * bounds the cell, so its own limits are not the cell's.
+ */
+#include "cell_test.h"
+
+static int at(const struct cellctx *c, const char *name, char *path, size_t len)
+{
+	return snprintf(path, len, "%s/%s", c->cg, name);
+}
+
+static int cg_mkdir(const struct cellctx *c, const char *name)
+{
+	char path[256];
+
+	at(c, name, path, sizeof(path));
+	return mkdir(path, 0755);
+}
+
+static int cg_write(const struct cellctx *c, const char *name, const char *val)
+{
+	char path[256];
+
+	at(c, name, path, sizeof(path));
+	return write_file(path, val);
+}
+
+static int cg_move(const struct cellctx *c, const char *name, pid_t pid)
+{
+	char path[256], nr[16];
+
+	snprintf(path, sizeof(path), "%s/%s/cgroup.procs", c->cg, name);
+	snprintf(nr, sizeof(nr), "%d", pid);
+	return write_file(path, nr);
+}
+
+/* Where a task is, below the budget of @c: "" for the budget itself. */
+static const char *cg_of(const struct cellctx *c, pid_t pid)
+{
+	static char buf[256];
+	const char *budget = c->cg + strlen(CGROOT);
+	char path[64];
+	FILE *f;
+
+	snprintf(path, sizeof(path), "/proc/%d/cgroup", pid);
+	f = fopen(path, "r");
+	if (!f)
+		return "?";
+	/* There may be legacy hierarchies about. */
+	while (fgets(buf, sizeof(buf), f)) {
+		if (!strncmp(buf, "0::", 3))
+			break;
+	}
+	fclose(f);
+	buf[strcspn(buf, "\n")] = 0;
+	if (strncmp(buf, "0::", 3) || strncmp(buf + 3, budget, strlen(budget)))
+		return buf;
+	return buf + 3 + strlen(budget);
+}
+
+/* How many cgroups there are below the budget. */
+static int cg_children(const struct cellctx *c)
+{
+	struct dirent *e;
+	int n = 0;
+	DIR *d;
+
+	d = opendir(c->cg);
+	if (!d)
+		return -1;
+	while ((e = readdir(d)))
+		n += e->d_type == DT_DIR && e->d_name[0] != '.';
+	closedir(d);
+	return n;
+}
+
+/* Has all of a cgroup come to a halt? */
+static int cg_frozen(const struct cellctx *c, const char *name)
+{
+	char path[256], buf[128] = {};
+	int fd, i;
+
+	snprintf(path, sizeof(path), "%s/%s/cgroup.events", c->cg, name);
+	for (i = 0; i < 100; i++) {
+		fd = open(path, O_RDONLY | O_CLOEXEC);
+		if (fd < 0 || read(fd, buf, sizeof(buf) - 1) <= 0)
+			return -1;
+		close(fd);
+		if (strstr(buf, "populated 1") && strstr(buf, "frozen 1"))
+			return 1;
+		usleep(20000);
+	}
+	return 0;
+}
+
+/* Controllers for the budgets to pass on. */
+static int controllers(void)
+{
+	return write_file(CGROOT "/cgroup.subtree_control", "+memory +pids");
+}
+
+struct inside {
+	int pidfd;
+	int ready;	/* the task writes to this */
+	int go;		/* and then waits for this */
+};
+
+/* Run @what with the budget as fd 3. @fd4, or else a pipe each way as fd 4 and 5. */
+static int start_inside(struct cellctx *c, struct inside *in, const char *what,
+			const char *arg, int fd4)
+{
+	int up[2] = { -1, -1 }, down[2] = { -1, -1 };
+
+	in->ready = in->go = -1;
+	in->pidfd = prepare(c->cfd, what, arg, NULL);
+	if (in->pidfd < 0 || pidfd_configure(in->pidfd, PIDFD_SET_FD, "3", NULL, c->cgfd))
+		return -1;
+	if (fd4 >= 0) {
+		if (pidfd_configure(in->pidfd, PIDFD_SET_FD, "4", NULL, fd4))
+			return -1;
+	} else {
+		if (pipe2(up, O_CLOEXEC) || pipe2(down, O_CLOEXEC) ||
+		    pidfd_configure(in->pidfd, PIDFD_SET_FD, "4", NULL, up[1]) ||
+		    pidfd_configure(in->pidfd, PIDFD_SET_FD, "5", NULL, down[0]))
+			return -1;
+		close(up[1]);
+		close(down[0]);
+		in->ready = up[0];
+		in->go = down[1];
+	}
+	return pidfd_configure(in->pidfd, PIDFD_CMD_START, NULL, NULL, 0);
+}
+
+static int wait_ready(struct inside *in)
+{
+	char c;
+
+	return read(in->ready, &c, 1) == 1 ? 0 : -1;
+}
+
+static void done_inside(struct inside *in)
+{
+	close(in->pidfd);
+	if (in->ready >= 0)
+		close(in->ready);
+	if (in->go >= 0)
+		close(in->go);
+}
+
+/* Run @fn as uid 1000, in a user namespace of its own, as who builds a one-id cell. */
+static int unprivileged(int (*fn)(void))
+{
+	int status, ret = -1;
+	pid_t pid;
+
+	if (chown(CGROOT, 1000, 1000) || chown(CGROOT "/cgroup.procs", 1000, 1000))
+		return -1;
+	pid = fork();
+	if (!pid) {
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(101);
+		if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+			_exit(102);
+		if (write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(103);
+		_exit(fn());
+	}
+	if (pid > 0 && waitpid(pid, &status, 0) == pid)
+		ret = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
+	if (chown(CGROOT, 0, 0) || chown(CGROOT "/cgroup.procs", 0, 0))
+		return -1;
+	return ret;
+}
+
+/*
+ * The budget has no children when the slot is filled. From then on whoever built
+ * the cell makes what it likes in there and moves the tasks of the cell about.
+ */
+TEST_F(cell, builder_lays_out_the_budget)
+{
+	pid_t pid;
+	int p;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+	EXPECT_EQ(cg_mkdir(&self->c, "a"), 0);
+	ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0);
+	EXPECT_EQ(cg_mkdir(&self->c, "b"), 0);
+
+	p = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(p, 0);
+	EXPECT_STREQ(cg_of(&self->c, pid), "");
+	EXPECT_EQ(cg_move(&self->c, "a", pid), 0);
+	EXPECT_STREQ(cg_of(&self->c, pid), "/a");
+	EXPECT_EQ(cg_move(&self->c, "b", pid), 0);
+	EXPECT_STREQ(cg_of(&self->c, pid), "/b");
+
+	/* Within the budget, and only what is the cell's. */
+	EXPECT_EQ(cg_move(&self->c, "..", pid), -1);
+	EXPECT_EQ(errno, EPERM);
+	EXPECT_EQ(cg_move(&self->c, "a", getpid()), -1);
+	EXPECT_EQ(errno, EPERM);
+	EXPECT_STREQ(cg_of(&self->c, pid), "/b");
+
+	/* All of it goes with the cell, whoever made it. */
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	cell_wait(self->c.cfd);
+	EXPECT_EQ(cg_children(&self->c), 0);
+	close(p);
+}
+
+/* Have a task of the cell say where it is, started in the cgroup @name. */
+static int run_in_cgroup(struct cellctx *c, const char *name, const char *seen)
+{
+	int cgfd = openat(c->cgfd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	int pidfd = prepare(c->cfd, "in-cgroup", seen, NULL), st = -99;
+
+	if (cgfd >= 0 && pidfd >= 0 &&
+	    !pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, cgfd) &&
+	    !pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0))
+		st = task_wait(pidfd);
+	close(pidfd);
+	close(cgfd);
+	return st;
+}
+
+/* One cgroup with a limit for each of what runs in the cell. */
+TEST_F(cell, builder_sets_limits_below_the_budget)
+{
+	pid_t pid;
+	int p;
+
+	ASSERT_EQ(controllers(), 0);
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(cg_mkdir(&self->c, "a"), 0);
+	p = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(p, 0);
+
+	/* A cgroup that passes a controller on has no tasks of its own. */
+	EXPECT_EQ(cg_write(&self->c, "cgroup.subtree_control", "+memory"), -1);
+	EXPECT_EQ(errno, EBUSY);
+	EXPECT_EQ(cg_move(&self->c, "a", pid), 0);
+	EXPECT_EQ(cg_write(&self->c, "cgroup.subtree_control", "+memory +pids"), 0);
+	EXPECT_EQ(cg_write(&self->c, "a/pids.max", "5"), 0);
+	EXPECT_EQ(cg_write(&self->c, "a/memory.max", "67108864"), 0);
+
+	/* That goes for a new task as well, which starts in the budget. */
+	EXPECT_EQ(spawn(self->c.cfd, NULL, "pause", NULL, NULL), -1);
+	EXPECT_EQ(errno, EBUSY);
+	/* Unless it is told where else. */
+	EXPECT_EQ(run_in_cgroup(&self->c, "a", "/a"), 0);
+	close(p);
+}
+
+/* Start @what in the cgroup @cgfd. Returns a pidfd, or -1 with what START had to say. */
+static int spawn_in(struct cellctx *c, int cgfd, const char *what, const char *arg)
+{
+	int pidfd = prepare(c->cfd, what, arg, NULL), e;
+
+	if (pidfd < 0)
+		return -1;
+	if (pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, cgfd) ||
+	    pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) {
+		e = errno;
+		close(pidfd);
+		errno = e;
+		return -1;
+	}
+	return pidfd;
+}
+
+/* It is within the budget of the cell, there, and can hold a task. Or there is no task. */
+TEST_F(cell, spawn_names_no_other_cgroup)
+{
+	struct cellctx other;
+	int init, fd, pidfd;
+
+	ASSERT_EQ(controllers(), 0);
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(cell_build(&other, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(cg_mkdir(&self->c, "a"), 0);
+	ASSERT_EQ(cg_mkdir(&self->c, "a/leaf"), 0);
+	ASSERT_EQ(cg_mkdir(&self->c, "gone"), 0);
+	ASSERT_EQ(cg_mkdir(&other, "theirs"), 0);
+	fd = openat(self->c.cgfd, "a/leaf", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	init = spawn_in(&self->c, fd, "pause", NULL);
+	ASSERT_GE(init, 0);
+	close(fd);
+
+	/* Above the budget, beside it, and in that of another cell. */
+	fd = open(CGROOT, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(fd);
+	EXPECT_EQ(spawn_in(&self->c, other.cgfd, "pause", NULL), -1);
+	EXPECT_EQ(errno, EPERM);
+	fd = openat(other.cgfd, "theirs", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(fd);
+
+	/* Removed since it was named, and since it was opened. */
+	fd = openat(self->c.cgfd, "gone", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	pidfd = prepare(self->c.cfd, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, fd), 0);
+	ASSERT_EQ(unlinkat(self->c.cgfd, "gone", AT_REMOVEDIR), 0);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, ENODEV);
+	close(pidfd);
+	EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1);
+	EXPECT_EQ(errno, ENOENT);
+	close(fd);
+
+	/* Passes a controller on. */
+	ASSERT_EQ(cg_write(&self->c, "cgroup.subtree_control", "+memory"), 0);
+	ASSERT_EQ(cg_write(&self->c, "a/cgroup.subtree_control", "+memory"), 0);
+	fd = openat(self->c.cgfd, "a", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	EXPECT_EQ(spawn_in(&self->c, fd, "pause", NULL), -1);
+	EXPECT_EQ(errno, EBUSY);
+	close(fd);
+
+	/* No cgroup at all. */
+	pidfd = prepare(self->c.cfd, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, fd), -1);
+	EXPECT_EQ(errno, EBADF);
+	close(fd);
+	/* The last word counts, and an O_PATH file will do. */
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, other.cgfd), 0);
+	fd = openat(self->c.cgfd, "a/leaf", O_PATH | O_CLOEXEC);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "cgroup", NULL, fd), 0);
+	close(fd);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_STREQ(cg_of(&self->c, pidfd_nr(pidfd)), "/a/leaf");
+	close(pidfd);
+
+	/* A kill finds them wherever they are. */
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	cell_wait(self->c.cfd);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	EXPECT_EQ(cg_children(&self->c), 0);
+	close(init);
+	cell_cleanup(&other);
+}
+
+/* What the ids of the cell own below the budget, as the host sees it. */
+TEST_F(cell, manages_its_cgroups)
+{
+	struct inside in;
+	struct stat st;
+	char path[256];
+	long first;
+
+	ASSERT_EQ(controllers(), 0);
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = CAP(CHOWN) }), 0);
+	ASSERT_EQ(fstat(self->c.cgfd, &st), 0);
+	first = st.st_uid;
+	ASSERT_GE(first, (long)CELL_WINDOW_FIRST);
+
+	ASSERT_EQ(start_inside(&self->c, &in, "cg-manage", "block", -1), 0);
+	ASSERT_EQ(wait_ready(&in), 0);
+
+	ASSERT_EQ(fstatat(self->c.cgfd, "init.scope", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, first);
+	ASSERT_EQ(fstatat(self->c.cgfd, "svc", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, first + 1000);
+	EXPECT_EQ(st.st_gid, first + 1000);
+	ASSERT_EQ(fstatat(self->c.cgfd, "svc/pids.max", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, first);
+
+	/* That is for a task of the cell to do. Nobody else names its ids. */
+	at(&self->c, "svc", path, sizeof(path));
+	EXPECT_EQ(chown(path, first + 5, first + 5), -1);
+	EXPECT_EQ(errno, EOVERFLOW);
+	EXPECT_EQ(chown(CGROOT, first, first), -1);
+	EXPECT_EQ(errno, EOVERFLOW);
+
+	/* The init has taken over. Another task goes where it made room for one. */
+	EXPECT_EQ(spawn(self->c.cfd, NULL, "pause", NULL, NULL), -1);
+	EXPECT_EQ(errno, EBUSY);
+	EXPECT_EQ(run_in_cgroup(&self->c, "init.scope", "/init.scope"), 0);
+	EXPECT_EQ(run_in_cgroup(&self->c, "svc", "/svc"), 0);
+
+	ASSERT_EQ(write(in.go, "g", 1), 1);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	done_inside(&in);
+
+	/* Nothing is left that belongs to them. */
+	EXPECT_EQ(cg_children(&self->c), 0);
+	ASSERT_EQ(fstat(self->c.cgfd, &st), 0);
+	EXPECT_EQ(st.st_uid, 0);
+}
+
+static int one_id_manages(void)
+{
+	struct build b = { .ids = 1 };
+	struct inside in;
+	struct cellctx c;
+	int st;
+
+	if (cell_build(&c, &b))
+		return 1;
+	if (start_inside(&c, &in, "cg-manage", "one", -1) || wait_ready(&in))
+		return 2;
+	if (write(in.go, "g", 1) != 1)
+		return 3;
+	st = cell_wait(c.cfd);
+	done_inside(&in);
+	cell_cleanup(&c);
+	return st ? 4 : 0;
+}
+
+TEST_F(cell, one_id_cell_manages_its_cgroups)
+{
+	ASSERT_EQ(controllers(), 0);
+	EXPECT_EQ(unprivileged(one_id_manages), 0);
+}
+
+/*
+ * Holding the cellfd is all it takes. What the init of the cell made belongs to
+ * ids that nobody outside of the cell has, whoever built it included.
+ */
+static int spawns_where_it_owns_nothing(void)
+{
+	struct build b = { .ids = 65536, .caps = CAP(CHOWN) };
+	struct inside in;
+	struct cellctx c;
+	struct stat st;
+	int ret = 0;
+
+	if (cell_build(&c, &b))
+		return 1;
+	if (start_inside(&c, &in, "cg-manage", "block", -1) || wait_ready(&in))
+		return 2;
+	/* It has not even a name for them. */
+	if (fstatat(c.cgfd, "svc", &st, 0) || st.st_uid != 65534)
+		ret = 3;
+	else if (!faccessat(c.cgfd, "svc/cgroup.procs", W_OK, 0))
+		ret = 4;
+	else if (run_in_cgroup(&c, "svc", "/svc"))
+		ret = 5;
+	if (write(in.go, "g", 1) != 1 || cell_wait(c.cfd))
+		ret = ret ?: 6;
+	done_inside(&in);
+	cell_cleanup(&c);
+	return ret;
+}
+
+TEST_F(cell, unprivileged_builder_spawns_where_the_cell_made_room)
+{
+	ASSERT_EQ(controllers(), 0);
+	EXPECT_EQ(unprivileged(spawns_where_it_owns_nothing), 0);
+}
+
+static int own_limits(unsigned int ids)
+{
+	struct build b = { .ids = ids };
+	struct inside in;
+	struct cellctx c;
+	char buf[16] = {};
+	int max, st;
+
+	if (cell_build(&c, &b))
+		return 1;
+	if (cg_write(&c, "pids.max", "7"))
+		return 2;
+	max = openat(c.cgfd, "pids.max", O_RDWR | O_CLOEXEC);
+	if (max < 0 || start_inside(&c, &in, "cg-own", NULL, max))
+		return 3;
+	st = cell_wait(c.cfd);
+	done_inside(&in);
+	if (pread(max, buf, sizeof(buf) - 1, 0) <= 0 || strcmp(buf, "7\n"))
+		st = 5;
+	close(max);
+	cell_cleanup(&c);
+	return st ? (st == 5 ? 5 : 4) : 0;
+}
+
+static int own_limits_one_id(void)
+{
+	return own_limits(1);
+}
+
+/* The limits of the budget stay with whoever is above. */
+TEST_F(cell, limits_of_the_budget_are_not_the_cells)
+{
+	ASSERT_EQ(controllers(), 0);
+	EXPECT_EQ(own_limits(65536), 0);
+}
+
+/* A cell of one id owns what its maker owns, and its maker made the budget. */
+TEST_F(cell, limits_of_the_budget_are_not_a_one_id_cells)
+{
+	ASSERT_EQ(controllers(), 0);
+	EXPECT_EQ(unprivileged(own_limits_one_id), 0);
+}
+
+/* What it is in the middle of making goes as well. */
+TEST_F_TIMEOUT(cell, killed_while_it_makes_cgroups, 300)
+{
+	struct inside in;
+	struct stat st;
+	int i;
+
+	for (i = 0; i < 10; i++) {
+		ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+		ASSERT_EQ(start_inside(&self->c, &in, "cg-churn", NULL, -1), 0);
+		ASSERT_EQ(wait_ready(&in), 0);
+		usleep(50000);
+		EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+		cell_wait(self->c.cfd);
+		EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+		EXPECT_EQ(cg_children(&self->c), 0);
+		ASSERT_EQ(fstat(self->c.cgfd, &st), 0);
+		EXPECT_EQ(st.st_uid, 0);
+		done_inside(&in);
+		cell_cleanup(&self->c);
+		self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1;
+		self->c.rootfd = self->c.layers = -1;
+	}
+}
+
+/* A task that the cell has frozen is killed like any other. */
+TEST_F(cell, kill_takes_what_the_cell_has_frozen)
+{
+	struct inside in;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(start_inside(&self->c, &in, "cg-freeze", NULL, -1), 0);
+	ASSERT_EQ(wait_ready(&in), 0);
+	EXPECT_EQ(cg_frozen(&self->c, "cold"), 1);
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	cell_wait(self->c.cfd);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	EXPECT_EQ(cg_children(&self->c), 0);
+	done_inside(&in);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_child.c b/tools/testing/selftests/cell/cell_child.c
new file mode 100644
index 000000000000..2f2d205ce9d3
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_child.c
@@ -0,0 +1,1626 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * What runs inside a cell for cell_test. Statically linked: the tree of the
+ * cell holds this file and nothing else.
+ *
+ * Usage: cell_child <check>. Complains on stderr, exits 0 if all is well.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <grp.h>
+#include <netinet/in.h>
+#include <pthread.h>
+#include <time.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/resource.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <linux/capability.h>
+#include <linux/keyctl.h>
+#include <linux/if_tun.h>
+#include <linux/netfilter/nfnetlink.h>
+#include <linux/netfilter/nf_tables.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <linux/sockios.h>
+#include <net/if.h>
+#include <net/route.h>
+
+#include "cell_util.h"
+
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+extern char **environ;
+static int failures;
+
+#define CHECK(cond, fmt, ...) do {						\
+	if (!(cond)) {								\
+		fprintf(stderr, "cell_child: %s:%d: " fmt "\n", __func__,	\
+			__LINE__, ##__VA_ARGS__);				\
+		failures++;							\
+	}									\
+} while (0)
+
+#define CHECK_ERRNO(call, err) do {						\
+	errno = 0;								\
+	long __r = (long)(call);						\
+	CHECK(__r == -1 && errno == (err), "%s: returned %ld, errno %d, wanted %d", \
+	      #call, __r, errno, (err));					\
+} while (0)
+
+static unsigned long long status_caps(const char *field)
+{
+	unsigned long long val = ~0ULL;
+	char line[256];
+	FILE *f = fopen("/proc/self/status", "r");
+
+	while (f && fgets(line, sizeof(line), f)) {
+		if (!strncmp(line, field, strlen(field)))
+			val = strtoull(line + strlen(field) + 1, NULL, 16);
+	}
+	if (f)
+		fclose(f);
+	return val;
+}
+
+static void check_caps_are(unsigned long long has, unsigned long long bound)
+{
+	CHECK(status_caps("CapPrm") == has, "permitted %llx", status_caps("CapPrm"));
+	CHECK(status_caps("CapEff") == has, "effective %llx", status_caps("CapEff"));
+	CHECK(status_caps("CapBnd") == bound, "bounding set %llx", status_caps("CapBnd"));
+	CHECK(status_caps("CapInh") == 0, "inheritable %llx", status_caps("CapInh"));
+	CHECK(status_caps("CapAmb") == 0, "ambient %llx", status_caps("CapAmb"));
+}
+
+static int set_caps_to(unsigned long long effective, unsigned long long permitted)
+{
+	struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 };
+	struct __user_cap_data_struct data[2] = {
+		{ .effective = effective, .permitted = permitted },
+		{ .effective = effective >> 32, .permitted = permitted >> 32 },
+	};
+
+	return syscall(SYS_capset, &hdr, data);
+}
+
+static int set_caps(unsigned long long caps)
+{
+	return set_caps_to(caps, caps);
+}
+
+/* Run @fn in a task of its own, which may do to itself what it likes. */
+static void in_a_fork(void (*fn)(unsigned long long), unsigned long long caps)
+{
+	int status = -1;
+	pid_t pid = fork();
+
+	if (!pid) {
+		failures = 0;
+		fn(caps);
+		_exit(!!failures);
+	}
+	CHECK(waitpid(pid, &status, 0) == pid && !status, "status %x", status);
+}
+
+#define CAP(x)	(1ULL << CAP_##x)
+
+static void become_somebody(unsigned long long caps)
+{
+	if (!(caps & CAP(SETGID))) {
+		CHECK_ERRNO(setgroups(0, NULL), EPERM);
+		CHECK_ERRNO(setgid(33), EPERM);
+	} else {
+		CHECK(!setgroups(0, NULL) && !setgid(33), "setgid: %d", errno);
+	}
+	if (!(caps & CAP(SETUID))) {
+		CHECK_ERRNO(setuid(33), EPERM);
+		return;
+	}
+	CHECK(!setuid(33), "setuid: %d", errno);
+	/* Which costs them, as ever. */
+	check_caps_are(0, caps);
+}
+
+/* Letting go takes nothing, CAP_SETPCAP included. */
+static void shrink_the_bounding_set(unsigned long long caps)
+{
+	CHECK(!prctl(PR_CAPBSET_DROP, CAP_CHOWN, 0, 0, 0), "drop: %d", errno);
+	check_caps_are(caps, caps & ~CAP(CHOWN));
+	CHECK(!prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN, 0, 0, 0), "drop: %d", errno);
+	check_caps_are(caps, caps & ~(CAP(CHOWN) | CAP(SYS_ADMIN)));
+	CHECK_ERRNO(prctl(PR_CAPBSET_DROP, 63, 0, 0, 0), EINVAL);
+}
+
+/*
+ * What the builder named and nothing else, through an exec and for good: a task
+ * can let go of a capability, and nothing brings one back.
+ */
+static void check_caps(char **argv)
+{
+	unsigned long long caps = strtoull(argv[2], NULL, 16);
+	const char *stage = argv[3] ?: "first";
+	int fd;
+
+	if (!strcmp(stage, "first")) {
+		check_caps_are(caps, caps);
+
+		fd = open("/tmp/c", O_CREAT | O_WRONLY, 0644);
+		CHECK(fd >= 0, "create: %d", errno);
+		if (caps & CAP(CHOWN))
+			CHECK(!fchown(fd, 33, 33), "chown: %d", errno);
+		else
+			CHECK_ERRNO(fchown(fd, 33, 33), EPERM);
+		close(fd);
+		/* What goes by a capability over the host stays out of reach. */
+		CHECK_ERRNO(mknod("/tmp/n", S_IFCHR | 0600, makedev(1, 3)), EPERM);
+		in_a_fork(become_somebody, caps);
+		in_a_fork(shrink_the_bounding_set, caps);
+		CHECK_ERRNO(set_caps(caps | CAP(SETFCAP)), EPERM);
+		stage = "again";
+	} else if (!strcmp(stage, "again")) {
+		/* Uid 0 ran a program. */
+		check_caps_are(caps, caps);
+		CHECK(!set_caps(0), "capset: %d", errno);
+		check_caps_are(0, caps);
+		stage = "dropped";
+	} else if (!strcmp(stage, "dropped")) {
+		/* It did so again, and has nothing to show for it. */
+		check_caps_are(0, caps);
+		CHECK_ERRNO(set_caps(caps ?: CAP(CHOWN)), EPERM);
+		stage = "gain";
+	} else {
+		/* Set-uid root, with a file capability. */
+		check_caps_are(0, caps);
+		return;
+	}
+	if (failures)
+		return;
+	execl(strcmp(stage, "gain") ? "/cell_child" : "/gain", "cell_child", "caps", argv[2],
+	      stage, NULL);
+	CHECK(0, "exec for %s: %d", stage, errno);
+}
+
+/* Nothing but what was given. */
+static void check_basic(void)
+{
+	struct cell_info info = { .mask = CELL_INFO_ID };
+	char buf[256];
+	int fd, nr = 0;
+
+	CHECK(getpid() == 1, "pid %d", getpid());
+	CHECK(getppid() == 0, "ppid %d", getppid());
+	CHECK(getuid() == 0 && geteuid() == 0, "uid %d euid %d", getuid(), geteuid());
+	CHECK(getgid() == 0 && getegid() == 0, "gid %d egid %d", getgid(), getegid());
+	CHECK(getgroups(0, NULL) == 0, "%d groups", getgroups(0, NULL));
+	CHECK(getsid(0) == 1, "sid %d", getsid(0));
+	CHECK(getpgrp() == 1, "pgrp %d", getpgrp());
+	CHECK(getcwd(buf, sizeof(buf)) && !strcmp(buf, "/"), "cwd %s", buf);
+	CHECK(!gethostname(buf, sizeof(buf)) && !strcmp(buf, "celltest"),
+	      "hostname %s", buf);
+	CHECK(umask(0) == 022, "umask");
+	CHECK(open("/dev/tty", O_RDWR) == -1, "has a controlling terminal");
+
+	for (fd = 0; fd < 1024; fd++) {
+		if (fcntl(fd, F_GETFD) != -1)
+			nr++;
+	}
+	CHECK(nr == 3, "%d open files", nr);
+
+	CHECK(environ[0] && !strcmp(environ[0], "CELL=yes") && !environ[1],
+	      "environment");
+
+	CHECK(prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) == 1, "no_new_privs is off");
+	check_caps_are(0, 0);
+
+	CHECK(!cell_info(CELL_SELF, &info, sizeof(info), 0) && info.id != 0,
+	      "own cell id %llu", (unsigned long long)info.id);
+
+	/* The "spawn" slot is empty, whatever the file descriptor is. */
+	CHECK_ERRNO(cell_create(CELL_SELF, 0), EPERM);
+	for (fd = -1; fd < 8; fd++)
+		CHECK_ERRNO(pidfd_open_empty(fd, 0), EPERM);
+	CHECK_ERRNO(pidfd_open_empty(-EBADF, 0), EPERM);
+}
+
+/* A task has no say in its namespaces. */
+static void check_membership(void)
+{
+	static const int flags[] = {
+		CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWNET,
+		CLONE_NEWIPC, CLONE_NEWUTS, CLONE_NEWCGROUP, 0x80 /* NEWTIME */,
+	};
+	static const char * const ns[] = {
+		"user", "mnt", "pid", "net", "ipc", "uts", "cgroup", "time",
+	};
+	char path[64];
+	unsigned int i;
+	int fd;
+
+	for (i = 0; i < ARRAY_SIZE(flags); i++)
+		CHECK_ERRNO(unshare(flags[i]), EPERM);
+
+	for (i = 0; i < ARRAY_SIZE(ns); i++) {
+		snprintf(path, sizeof(path), "/proc/self/ns/%s", ns[i]);
+		fd = open(path, O_RDONLY);
+		CHECK(fd >= 0, "open %s: %d", path, errno);
+		if (fd < 0)
+			continue;
+		CHECK_ERRNO(setns(fd, 0), EPERM);
+		close(fd);
+	}
+
+	CHECK_ERRNO(syscall(SYS_clone, CLONE_NEWUSER | SIGCHLD, 0, 0, 0, 0), EPERM);
+	CHECK_ERRNO(syscall(SYS_clone, CLONE_NEWNET | SIGCHLD, 0, 0, 0, 0), EPERM);
+	CHECK_ERRNO(syscall(SYS_open_tree, AT_FDCWD, "/", 2 /* OPEN_TREE_NAMESPACE */),
+		    EPERM);
+	CHECK_ERRNO(mount("none", "/tmp", "tmpfs", 0, NULL), EPERM);
+	CHECK_ERRNO(umount2("/proc", MNT_DETACH), EPERM);
+}
+
+/* Nothing a cell writes imparts privilege. */
+static void check_setid(void)
+{
+	/* VFS_CAP_REVISION_2, cap_net_raw permitted and effective. */
+	static const unsigned char cap[20] = {
+		0x01, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x00,
+	};
+	/* VFS_CAP_REVISION_3, the same, with uid 0 for the root. */
+	static const unsigned char cap3[24] = {
+		0x01, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00,
+	};
+	struct stat st;
+	char buf[64];
+	int fd;
+
+	fd = open("/tmp/a", O_CREAT | O_WRONLY, 06755);
+	CHECK(fd >= 0, "create: %d", errno);
+	CHECK(!fstat(fd, &st) && !(st.st_mode & (S_ISUID | S_ISGID)),
+	      "created with mode %o", st.st_mode);
+	CHECK(st.st_uid == 0 && st.st_gid == 0, "owned by %d:%d", st.st_uid, st.st_gid);
+
+	CHECK(!fchmod(fd, 06755), "fchmod: %d", errno);
+	CHECK(!fstat(fd, &st) && (st.st_mode & 07777) == 0755,
+	      "chmod gave mode %o", st.st_mode);
+	CHECK(!chmod("/tmp/a", 04711), "chmod: %d", errno);
+	CHECK(!stat("/tmp/a", &st) && (st.st_mode & 07777) == 0711,
+	      "chmod gave mode %o", st.st_mode);
+
+	CHECK_ERRNO(fsetxattr(fd, "security.capability", cap, sizeof(cap), 0), EPERM);
+	/*
+	 * Nor one for the root of the cell only. On disk that is uid 0, so whoever
+	 * else looks at the filesystem would take it for one of its own.
+	 */
+	CHECK_ERRNO(fsetxattr(fd, "security.capability", cap3, sizeof(cap3), 0), EPERM);
+	close(fd);
+
+	CHECK(!mknod("/tmp/f", S_IFIFO | 06644, 0), "mknod: %d", errno);
+	CHECK(!stat("/tmp/f", &st) && !(st.st_mode & (S_ISUID | S_ISGID)),
+	      "mknod gave mode %o", st.st_mode);
+
+	/*
+	 * A set-uid file that came with the tree. A store through a shared
+	 * mapping would leave the bits alone, so being able to write is enough
+	 * to lose them.
+	 */
+	CHECK(!stat("/su", &st) && (st.st_mode & 07777) == 06755, "mode %o", st.st_mode);
+	fd = open("/su", O_RDONLY);
+	CHECK(fd >= 0 && !fstat(fd, &st) && (st.st_mode & 07777) == 06755,
+	      "reading cost the bits: %o", st.st_mode);
+	CHECK(fgetxattr(fd, "security.capability", buf, sizeof(buf)) > 0,
+	      "reading cost the file capability: %d", errno);
+	close(fd);
+	fd = open("/su", O_RDWR);
+	CHECK(fd >= 0 && !fstat(fd, &st) && (st.st_mode & 07777) == 0755,
+	      "still mode %o when open for writing", st.st_mode);
+	CHECK_ERRNO(fgetxattr(fd, "security.capability", buf, sizeof(buf)), ENODATA);
+	close(fd);
+
+	/* On a directory setgid is about inheritance, not privilege. */
+	CHECK(!mkdir("/tmp/d", 0755) && !chmod("/tmp/d", 02755), "mkdir: %d", errno);
+	CHECK(!stat("/tmp/d", &st) && (st.st_mode & S_ISGID), "mode %o", st.st_mode);
+
+	/* Ids work as ever otherwise. */
+	CHECK(!chown("/tmp/a", 33, 33), "chown: %d", errno);
+	CHECK(!stat("/tmp/a", &st) && st.st_uid == 33 && st.st_gid == 33,
+	      "owned by %d:%d", st.st_uid, st.st_gid);
+}
+
+static int send_fd(int sock, int fd)
+{
+	char data = 'x', ctl[CMSG_SPACE(sizeof(int))] = {};
+	struct iovec iov = { .iov_base = &data, .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = ctl, .msg_controllen = sizeof(ctl),
+	};
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+	memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
+	return sendmsg(sock, &msg, 0);
+}
+
+static void check_peer_queries(int sock)
+{
+	struct ucred uc;
+	socklen_t len;
+	gid_t groups[8];
+	int pidfd;
+
+	len = sizeof(uc);
+	CHECK_ERRNO(getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &uc, &len), ENODATA);
+	len = sizeof(groups);
+	CHECK_ERRNO(getsockopt(sock, SOL_SOCKET, SO_PEERGROUPS, groups, &len), ENODATA);
+	len = sizeof(pidfd);
+	CHECK_ERRNO(getsockopt(sock, SOL_SOCKET, 77 /* SO_PEERPIDFD */, &pidfd, &len),
+		    ENODATA);
+}
+
+/*
+ * Between cells a socket carries bytes. fd 3 is one end of a socketpair the
+ * host made, /tmp/sock a listener of the host's.
+ */
+static void check_unix(void)
+{
+	struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = "/tmp/sock" };
+	char ctl[256], c;
+	struct iovec iov = { .iov_base = &c, .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = ctl, .msg_controllen = sizeof(ctl),
+	};
+	int one = 1, sock, sv[2];
+	struct ucred uc;
+	socklen_t len;
+
+	/*
+	 * The host queued a file on the socket while both ends were still its
+	 * own. Nobody could have refused that. It must not arrive.
+	 */
+	CHECK(recvmsg(3, &msg, MSG_PEEK) == 1 && c == 'q', "peek: %d", errno);
+	CHECK(msg.msg_controllen == 0, "peeked %zu bytes of ancillary data",
+	      (size_t)msg.msg_controllen);
+	msg.msg_controllen = sizeof(ctl);
+	CHECK(recvmsg(3, &msg, 0) == 1 && c == 'q', "recvmsg: %d", errno);
+	CHECK(msg.msg_controllen == 0, "got %zu bytes of ancillary data",
+	      (size_t)msg.msg_controllen);
+	CHECK(fcntl(4, F_GETFD) == -1, "a file arrived");
+	msg.msg_controllen = sizeof(ctl);
+
+	check_peer_queries(3);
+	CHECK_ERRNO(send_fd(3, 0), EPERM);
+	CHECK(write(3, "a", 1) == 1, "plain bytes: %d", errno);
+
+	/* The host sends a byte, with SO_PASSCRED on at this end. */
+	CHECK(!setsockopt(3, SOL_SOCKET, SO_PASSCRED, &one, sizeof(one)), "passcred");
+	CHECK(write(3, "b", 1) == 1, "plain bytes: %d", errno);
+	CHECK(recvmsg(3, &msg, 0) == 1 && c == 'h', "recvmsg: %d", errno);
+	CHECK(msg.msg_controllen == 0, "got %zu bytes of ancillary data",
+	      (size_t)msg.msg_controllen);
+
+	sock = socket(AF_UNIX, SOCK_STREAM, 0);
+	CHECK(!connect(sock, (struct sockaddr *)&addr, sizeof(addr)), "connect: %d", errno);
+	check_peer_queries(sock);
+	CHECK_ERRNO(send_fd(sock, 0), EPERM);
+	CHECK(write(sock, "c", 1) == 1, "plain bytes: %d", errno);
+	close(sock);
+
+	/* Within the cell everything is as it always was. */
+	CHECK(!socketpair(AF_UNIX, SOCK_STREAM, 0, sv), "socketpair");
+	len = sizeof(uc);
+	CHECK(!getsockopt(sv[0], SOL_SOCKET, SO_PEERCRED, &uc, &len) &&
+	      uc.pid == getpid() && uc.uid == 0, "SO_PEERCRED at home: %d", errno);
+	CHECK(send_fd(sv[0], 0) == 1, "SCM_RIGHTS at home: %d", errno);
+	msg.msg_controllen = sizeof(ctl);
+	CHECK(recvmsg(sv[1], &msg, 0) == 1 && msg.msg_controllen >= CMSG_LEN(sizeof(int)),
+	      "no fd arrived at home");
+}
+
+/* fd 3 is a directory of a filesystem of the host that is not idmapped. */
+static void check_dirfd(void)
+{
+	CHECK_ERRNO(openat(3, "x", O_CREAT | O_WRONLY, 0644), EOVERFLOW);
+	CHECK_ERRNO(mkdirat(3, "d", 0755), EOVERFLOW);
+	CHECK_ERRNO(fchownat(3, "world", 0, 0, 0), EOVERFLOW);
+}
+
+/* /data is a real filesystem, with a set-uid file that came with it. */
+static void check_data(const char *create)
+{
+	struct stat st;
+	int fd;
+
+	fd = open("/data/su", O_RDWR);
+	CHECK(fd >= 0, "open: %d", errno);
+	CHECK(!fstat(fd, &st) && !(st.st_mode & (S_ISUID | S_ISGID)),
+	      "still mode %o when open for writing", st.st_mode);
+	close(fd);
+
+	if (!create)
+		return;
+	/* With ->atomic_open() this opens under the lock of the directory. */
+	fd = open("/data/new", O_CREAT | O_EXCL | O_RDWR, 06755);
+	CHECK(fd >= 0, "create: %d", errno);
+	CHECK(!fstat(fd, &st) && !(st.st_mode & (S_ISUID | S_ISGID)),
+	      "created with mode %o", st.st_mode);
+	CHECK(write(fd, "x", 1) == 1, "write: %d", errno);
+	close(fd);
+	CHECK(!unlink("/data/new"), "unlink: %d", errno);
+}
+
+/* /data is an overlay, and writing to a file of the lower layer copies it up. */
+static void check_copy_up(void)
+{
+	struct stat st;
+	int fd;
+
+	CHECK(!stat("/data/file", &st) && !st.st_uid && !st.st_gid,
+	      "belongs to %u:%u", st.st_uid, st.st_gid);
+	fd = open("/data/file", O_WRONLY);
+	CHECK(fd >= 0, "open: %d", errno);
+	CHECK(write(fd, "upper", 5) == 5, "write: %d", errno);
+	fd = open("/data/new", O_CREAT | O_EXCL | O_WRONLY, 0644);
+	CHECK(fd >= 0, "create: %d", errno);
+}
+
+/*
+ * fd 3 is /dev/vhost-net. In its kthread mode vhost puts a kernel thread into
+ * the cgroups of the owner. A kill could not take that with it.
+ */
+static void check_vhost(void)
+{
+	unsigned char mode = 0;		/* VHOST_FORK_OWNER_KTHREAD */
+
+	CHECK(!ioctl(3, _IOW(0xAF, 0x84, unsigned char), &mode),
+	      "VHOST_SET_FORK_FROM_OWNER: %d", errno);
+	/* vhost makes ENOMEM of whatever went wrong with its worker. */
+	CHECK(ioctl(3, _IO(0xAF, 0x01) /* VHOST_SET_OWNER */) == -1,
+	      "VHOST_SET_OWNER worked");
+}
+
+/* The spawner ignores, handles and blocks signals. None of that is ours. */
+static void check_signals(void)
+{
+	struct sigaction sa;
+	sigset_t set;
+	int sig;
+
+	for (sig = 1; sig < _NSIG; sig++) {
+		/* The C library keeps a few to itself. */
+		if (sigaction(sig, NULL, &sa))
+			continue;
+		CHECK(sa.sa_handler == SIG_DFL, "signal %d is not at its default", sig);
+	}
+	CHECK(!sigprocmask(SIG_BLOCK, NULL, &set), "sigprocmask: %d", errno);
+	for (sig = 1; sig < _NSIG; sig++)
+		CHECK(sigismember(&set, sig) != 1, "signal %d is blocked", sig);
+	CHECK(!sigpending(&set), "sigpending: %d", errno);
+	for (sig = 1; sig < _NSIG; sig++)
+		CHECK(sigismember(&set, sig) != 1, "signal %d is pending", sig);
+}
+
+/* The spawner has a filter that fails getrusage() with EACCES. */
+static void check_seccomp(void)
+{
+	struct rusage ru;
+
+	CHECK_ERRNO(getrusage(RUSAGE_SELF, &ru), EACCES);
+}
+
+static void on_usr1(int sig)
+{
+	_exit(0);
+}
+
+/*
+ * An init only gets the signals it has a handler for, so without one nobody
+ * could tell a signal that was refused from one that was dropped.
+ */
+static void wait_for_usr1(void)
+{
+	signal(SIGUSR1, on_usr1);
+	/* Whoever wants to know that we are ready put a pipe there. */
+	if (write(3, "x", 1) < 0)
+		;
+	for (;;)
+		pause();
+}
+
+/* Spawned into a running cell: the parent is somebody outside. */
+static void check_second(void)
+{
+	CHECK(getpid() != 1 && getppid() == 0, "pid %d ppid %d", getpid(), getppid());
+	CHECK_ERRNO(ptrace(PTRACE_TRACEME, 0, 0, 0), EPERM);
+}
+
+static void check_oneid(const char *uid)
+{
+	CHECK((int)getuid() == atoi(uid), "uid %d", getuid());
+	check_caps_are(0, 0);
+	CHECK_ERRNO(setuid(getuid() + 1), EINVAL);
+	CHECK_ERRNO(setgroups(0, NULL), EPERM);
+	/* The groups of whoever made the cell, denials and all. */
+	CHECK(getgroups(0, NULL) == 2, "%d groups", getgroups(0, NULL));
+
+	/* The parent has the very same uid. It is not in this cell. */
+	CHECK(syscall(SYS_keyctl, 1 /* JOIN_SESSION_KEYRING */, NULL) > 0,
+	      "join session keyring: %d", errno);
+	CHECK_ERRNO(syscall(SYS_keyctl, 18 /* SESSION_TO_PARENT */), EPERM);
+	CHECK_ERRNO(ptrace(PTRACE_TRACEME, 0, 0, 0), EPERM);
+}
+
+static int write_str(const char *path, const char *val)
+{
+	int fd = open(path, O_WRONLY), ret;
+
+	if (fd < 0)
+		return -1;
+	ret = write(fd, val, strlen(val)) < 0 ? -1 : 0;
+	close(fd);
+	return ret;
+}
+
+/* As "echo val > path" does it, which truncates the file. */
+static int write_at(int dir, const char *path, const char *val)
+{
+	int fd = openat(dir, path, O_WRONLY | O_TRUNC), ret;
+
+	if (fd < 0)
+		return -1;
+	ret = write(fd, val, strlen(val)) < 0 ? -1 : 0;
+	close(fd);
+	return ret;
+}
+
+static void check_in_cgroup(const char *want)
+{
+	char buf[512] = {}, line[256];
+	int fd = open("/proc/self/cgroup", O_RDONLY);
+
+	/* There may be legacy hierarchies about. */
+	snprintf(line, sizeof(line), "0::%s\n", want);
+	CHECK(fd >= 0 && read(fd, buf, sizeof(buf) - 1) > 0 && strstr(buf, line),
+	      "in %s and not in %s", buf, want);
+	close(fd);
+}
+
+/* Tell whoever is outside, on fd 4, and wait for it to have had a look, on fd 5. */
+static void show_the_host(void)
+{
+	char c;
+
+	CHECK(write(4, "r", 1) == 1 && read(5, &c, 1) == 1, "the host went away");
+}
+
+/*
+ * fd 3 is the budget. What an init that manages cgroups does: get out of the
+ * root, enable controllers there, and make a cgroup with a limit per service.
+ */
+static void check_cg_manage(const char *ids)
+{
+	struct stat st;
+	int status;
+	pid_t pid;
+
+	CHECK(!mkdirat(3, "init.scope", 0755), "mkdir: %d", errno);
+	CHECK(!write_at(3, "init.scope/cgroup.procs", "0"), "move: %d", errno);
+	check_in_cgroup("/init.scope");
+	CHECK(!write_at(3, "cgroup.subtree_control", "+memory +pids"), "enable: %d", errno);
+
+	CHECK(!mkdirat(3, "svc", 0755), "mkdir: %d", errno);
+	CHECK(!write_at(3, "svc/pids.max", "5"), "pids.max: %d", errno);
+	CHECK(!write_at(3, "svc/memory.max", "67108864"), "memory.max: %d", errno);
+	pid = fork();
+	if (!pid) {
+		failures = 0;
+		CHECK(!write_at(3, "svc/cgroup.procs", "0"), "move: %d", errno);
+		check_in_cgroup("/svc");
+		_exit(!!failures);
+	}
+	CHECK(waitpid(pid, &status, 0) == pid && !status, "the service: %x", status);
+
+	CHECK(!mkdirat(3, "svc/deeper", 0755), "mkdir: %d", errno);
+	CHECK(!unlinkat(3, "svc/deeper", AT_REMOVEDIR), "rmdir: %d", errno);
+
+	/* Hand a cgroup to somebody in here. */
+	if (!strcmp(ids, "block")) {
+		CHECK(!fchownat(3, "svc", 1000, 1000, 0), "chown: %d", errno);
+		CHECK(!fchownat(3, "svc/cgroup.procs", 1000, 1000, 0), "chown: %d", errno);
+		CHECK(!fstatat(3, "svc", &st, 0) && st.st_uid == 1000 && st.st_gid == 1000,
+		      "belongs to %u:%u", st.st_uid, st.st_gid);
+		CHECK_ERRNO(fchownat(3, "svc", 70000, 70000, 0), EINVAL);
+	}
+	show_the_host();
+}
+
+/*
+ * fd 3 is the budget, fd 4 its pids.max, which somebody outside opened. The cell
+ * makes cgroups in the budget and puts tasks there. The rest of it is not the
+ * cell's, whoever it belongs to: that is what bounds the cell.
+ */
+static void check_cg_own(void)
+{
+	static const char * const not_ours[] = {
+		"pids.max", "memory.max", "memory.high", "cgroup.freeze", "cgroup.kill",
+		"cgroup.type", "cgroup.max.depth", "cgroup.max.descendants",
+	};
+	static const char * const ours[] = {
+		"cgroup.procs", "cgroup.threads", "cgroup.subtree_control",
+	};
+	struct timespec now[2] = { { .tv_nsec = UTIME_NOW }, { .tv_nsec = UTIME_NOW } };
+	unsigned int i;
+	int fd;
+
+	for (i = 0; i < ARRAY_SIZE(not_ours); i++) {
+		fd = openat(3, not_ours[i], O_WRONLY);
+		CHECK(fd == -1 && errno == EPERM, "%s: %d, %d", not_ours[i], fd, errno);
+		fd = openat(3, not_ours[i], O_WRONLY | O_TRUNC);
+		CHECK(fd == -1 && errno == EPERM, "%s: %d, %d", not_ours[i], fd, errno);
+	}
+	CHECK_ERRNO(ftruncate(4, 0), EPERM);
+	CHECK_ERRNO(write(4, "max", 3), EPERM);
+
+	for (i = 0; i < ARRAY_SIZE(ours); i++) {
+		fd = openat(3, ours[i], O_WRONLY);
+		CHECK(fd >= 0, "%s: %d", ours[i], errno);
+		close(fd);
+		/* Truncating a file is writing to it, and a shell does. */
+		fd = openat(3, ours[i], O_WRONLY | O_TRUNC);
+		CHECK(fd >= 0, "%s, truncating: %d", ours[i], errno);
+		close(fd);
+		/* To write to, and not to hand on. */
+		CHECK_ERRNO(fchmodat(3, ours[i], 0666, 0), EPERM);
+	}
+
+	CHECK_ERRNO(fchmodat(3, "pids.max", 0666, 0), EPERM);
+	CHECK_ERRNO(fchownat(3, "pids.max", getuid(), getgid(), 0), EPERM);
+	CHECK_ERRNO(utimensat(3, "pids.max", now, 0), EPERM);
+	CHECK_ERRNO(fchmod(3, 0777), EPERM);
+	CHECK_ERRNO(fchown(3, getuid(), getgid()), EPERM);
+	CHECK_ERRNO(fsetxattr(3, "user.cell", "c", 1, 0), EPERM);
+
+	/* What it makes is its own, limits and all. */
+	CHECK(!mkdirat(3, "mine", 0755), "mkdir: %d", errno);
+	CHECK(!write_at(3, "mine/cgroup.max.depth", "3"), "a limit below: %d", errno);
+	CHECK(!fchmodat(3, "mine", 0700, 0), "chmod below: %d", errno);
+	CHECK(!unlinkat(3, "mine", AT_REMOVEDIR), "rmdir: %d", errno);
+}
+
+/* fd 3 is the budget. Make and remove cgroups until killed. */
+static void cg_churn(void)
+{
+	char a[32], b[48];
+	int i;
+
+	for (i = 0; i < 3; i++) {
+		if (!fork())
+			break;
+	}
+	snprintf(a, sizeof(a), "c%d", getpid());
+	snprintf(b, sizeof(b), "%s/x", a);
+	/* Not before it is known to work. */
+	if (!i && (mkdirat(3, a, 0755) || write(4, "r", 1) != 1))
+		_exit(1);
+	for (;;) {
+		mkdirat(3, a, 0755);
+		mkdirat(3, b, 0755);
+		unlinkat(3, b, AT_REMOVEDIR);
+		unlinkat(3, a, AT_REMOVEDIR);
+	}
+}
+
+/* fd 3 is the budget. Freeze a cgroup with a task in it, and wait to be killed. */
+static void cg_freeze(void)
+{
+	CHECK(!mkdirat(3, "cold", 0755), "mkdir: %d", errno);
+	if (!fork()) {
+		write_at(3, "cold/cgroup.procs", "0");
+		for (;;)
+			pause();
+	}
+	usleep(100000);
+	CHECK(!write_at(3, "cold/cgroup.freeze", "1"), "freeze: %d", errno);
+	show_the_host();
+}
+
+/*
+ * All of cgroupfs is at /data, and /data/open and its files are there for
+ * anybody to write. It is not within the budget.
+ */
+static void check_outside_the_budget(void)
+{
+	CHECK_ERRNO(write_str("/data/open/cgroup.subtree_control", "+pids"), EPERM);
+	CHECK_ERRNO(write_str("/data/open/cgroup.procs", "0"), EPERM);
+	CHECK_ERRNO(write_str("/data/open/pids.max", "1"), EPERM);
+	CHECK_ERRNO(rmdir("/data/open/empty"), EPERM);
+	CHECK_ERRNO(chmod("/data/open", 0700), EPERM);
+	/* These may not get that far: the id of the caller cannot be stored here. */
+	CHECK(mkdir("/data/open/mine", 0755) && (errno == EPERM || errno == EOVERFLOW),
+	      "mkdir: %d", errno);
+	CHECK(rename("/data/open/empty", "/data/open/other") &&
+	      (errno == EPERM || errno == EOVERFLOW), "rename: %d", errno);
+	CHECK(chown("/data/open", 0, 0) && (errno == EPERM || errno == EOVERFLOW),
+	      "chown: %d", errno);
+	/* Neither of these asks cgroup anything. */
+	CHECK_ERRNO(setxattr("/data/open", "user.cell", "x", 1, 0), EPERM);
+	CHECK_ERRNO(setxattr("/data/open", "user.host", "x", 1, 0), EPERM);
+	CHECK_ERRNO(removexattr("/data/open", "user.host"), EPERM);
+	CHECK_ERRNO(utimensat(AT_FDCWD, "/data/open", NULL, 0), EPERM);
+	CHECK_ERRNO(open("/data/open/cgroup.procs", O_WRONLY), EPERM);
+	CHECK(open("/data/open/cgroup.procs", O_RDONLY) >= 0, "open to read: %d", errno);
+}
+
+/* A legacy hierarchy has no budget on it, and it can rename. */
+static void check_legacy_hierarchy(void)
+{
+	CHECK_ERRNO(rename("/data/open/empty", "/data/open/other"), EPERM);
+	CHECK_ERRNO(rmdir("/data/open/empty"), EPERM);
+	CHECK_ERRNO(write_str("/data/open/cgroup.procs", "0"), EPERM);
+	CHECK_ERRNO(write_str("/data/open/notify_on_release", "1"), EPERM);
+	CHECK_ERRNO(setxattr("/data/open", "user.cell", "x", 1, 0), EPERM);
+	CHECK(mkdir("/data/open/mine", 0755) && (errno == EPERM || errno == EOVERFLOW),
+	      "mkdir: %d", errno);
+}
+
+/*
+ * A cell of one id runs on the id of its maker, so it owns what its maker owns.
+ * File descriptor 3 is on a cgroup of the maker's that is not within the budget.
+ */
+static void check_outside_but_its_own(void)
+{
+	struct stat st;
+
+	CHECK(!fstat(3, &st) && st.st_uid == getuid(), "owner %u", st.st_uid);
+	CHECK_ERRNO(fchmod(3, 0700), EPERM);
+	CHECK_ERRNO(fchown(3, -1, getgid()), EPERM);
+	CHECK_ERRNO(futimens(3, NULL), EPERM);
+	CHECK_ERRNO(fsetxattr(3, "user.cell", "x", 1, 0), EPERM);
+	CHECK_ERRNO(fremovexattr(3, "user.host"), EPERM);
+	CHECK_ERRNO(mkdirat(3, "mine", 0755), EPERM);
+	CHECK_ERRNO(unlinkat(3, "empty", AT_REMOVEDIR), EPERM);
+	CHECK_ERRNO(openat(3, "cgroup.procs", O_WRONLY), EPERM);
+	CHECK_ERRNO(fchmodat(3, "cgroup.procs", 0666, 0), EPERM);
+	/* File descriptor 4 was opened for writing by the maker. */
+	CHECK_ERRNO(write(4, "5", 1), EPERM);
+	/* File descriptor 5, if there is one, is the same on a legacy hierarchy. */
+	if (fstat(5, &st))
+		return;
+	CHECK(st.st_uid == getuid(), "owner %u", st.st_uid);
+	CHECK_ERRNO(renameat(5, "empty", 5, "other"), EPERM);
+	CHECK_ERRNO(mkdirat(5, "mine", 0755), EPERM);
+	CHECK_ERRNO(unlinkat(5, "empty", AT_REMOVEDIR), EPERM);
+	CHECK_ERRNO(fchmod(5, 0700), EPERM);
+	CHECK_ERRNO(fsetxattr(5, "user.cell", "x", 1, 0), EPERM);
+}
+
+#ifndef SO_PEERPIDFD
+#define SO_PEERPIDFD	77
+#endif
+
+static int race_sock = -1, race_stop, race_hits;
+
+static void *peer_race_ask(void *arg)
+{
+	while (!__atomic_load_n(&race_stop, __ATOMIC_RELAXED)) {
+		int s = __atomic_load_n(&race_sock, __ATOMIC_RELAXED), fd = -1;
+		socklen_t len = sizeof(int);
+
+		if (s >= 0 && !getsockopt(s, SOL_SOCKET, SO_PEERPIDFD, &fd, &len)) {
+			__atomic_fetch_add(&race_hits, 1, __ATOMIC_RELAXED);
+			close(fd);
+		}
+	}
+	return NULL;
+}
+
+/*
+ * Who the peer is must be checked and read in one go. One thread connects a
+ * fresh socket to a listener of the host's, over and over, while another asks
+ * each of them for a pidfd on its peer. There is no moment at which it gets one.
+ */
+static void check_peer_race(void)
+{
+	struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = "/tmp/sock" };
+	time_t end = time(NULL) + 8;
+	pthread_t ask[2];
+	int i, s, connected = 0;
+
+	for (i = 0; i < 2; i++)
+		CHECK(!pthread_create(&ask[i], NULL, peer_race_ask, NULL), "thread");
+	while (time(NULL) < end) {
+		s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+		if (s < 0)
+			break;
+		__atomic_store_n(&race_sock, s, __ATOMIC_RELAXED);
+		if (!connect(s, (struct sockaddr *)&addr, sizeof(addr)))
+			connected++;
+		__atomic_store_n(&race_sock, -1, __ATOMIC_RELAXED);
+		close(s);
+	}
+	__atomic_store_n(&race_stop, 1, __ATOMIC_RELAXED);
+	for (i = 0; i < 2; i++)
+		pthread_join(ask[i], NULL);
+	CHECK(connected > 100, "only %d connected", connected);
+	CHECK(!race_hits, "%d pidfds on a task of the host in %d connects",
+	      race_hits, connected);
+}
+
+static const struct sockaddr_in pod_addr = {
+	.sin_family = AF_INET,
+	.sin_port = __builtin_bswap16(7777),
+	.sin_addr = { __builtin_bswap32(INADDR_LOOPBACK) },
+};
+
+/* One container of a pod: leaves a file on the volume and serves on localhost. */
+static void pod_serve(void)
+{
+	int one = 1, fd, lsock, conn;
+	char c = 0;
+
+	fd = open("/data/from-a", O_CREAT | O_WRONLY, 0600);
+	CHECK(fd >= 0, "create on the volume: %d", errno);
+	close(fd);
+	lsock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	CHECK(lsock >= 0, "socket: %d", errno);
+	setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+	CHECK(!bind(lsock, (const struct sockaddr *)&pod_addr, sizeof(pod_addr)),
+	      "bind: %d", errno);
+	CHECK(!listen(lsock, 1), "listen: %d", errno);
+	/* Ready. */
+	CHECK(write(3, "r", 1) == 1, "ready: %d", errno);
+	conn = accept(lsock, NULL, NULL);
+	CHECK(conn >= 0, "accept: %d", errno);
+	CHECK(read(conn, &c, 1) == 1 && c == 'p', "read '%c': %d", c, errno);
+	CHECK(write(conn, "q", 1) == 1, "write: %d", errno);
+}
+
+/* Another one: finds the file, as its own root's, and talks to the first. */
+static void pod_connect(void)
+{
+	struct stat st;
+	char c = 0;
+	int sock;
+
+	CHECK(!stat("/data/from-a", &st), "stat on the volume: %d", errno);
+	CHECK(st.st_uid == 0 && st.st_gid == 0, "owner %u:%u", st.st_uid, st.st_gid);
+	sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	CHECK(sock >= 0, "socket: %d", errno);
+	CHECK(!connect(sock, (const struct sockaddr *)&pod_addr, sizeof(pod_addr)),
+	      "connect: %d", errno);
+	CHECK(write(sock, "p", 1) == 1, "write: %d", errno);
+	CHECK(read(sock, &c, 1) == 1 && c == 'q', "read '%c': %d", c, errno);
+}
+
+/* The network. */
+
+static bool has_cap(int cap)
+{
+	struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 };
+	struct __user_cap_data_struct data[2] = {};
+
+	return !syscall(SYS_capget, &hdr, data) && (data[cap / 32].effective & (1U << (cap % 32)));
+}
+
+static int dev_flags(const char *dev, int set, int clear)
+{
+	struct ifreq ifr = {};
+	int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0), was, r;
+
+	strcpy(ifr.ifr_name, dev);
+	r = ioctl(s, SIOCGIFFLAGS, &ifr);
+	was = (unsigned short)ifr.ifr_flags;
+	if (!r && (set || clear)) {
+		ifr.ifr_flags = (was | set) & ~clear;
+		r = ioctl(s, SIOCSIFFLAGS, &ifr);
+	}
+	r = r ? -errno : was;
+	close(s);
+	return r;
+}
+
+static int dev_set_addr(const char *dev, unsigned int ip)
+{
+	struct ifreq ifr = {};
+	struct sockaddr_in *sin = (void *)&ifr.ifr_addr;
+	int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0), r;
+
+	strcpy(ifr.ifr_name, dev);
+	sin->sin_family = AF_INET;
+	sin->sin_addr.s_addr = __builtin_bswap32(ip);
+	r = ioctl(s, SIOCSIFADDR, &ifr) ? -errno : 0;
+	close(s);
+	return r;
+}
+
+static int try_bind(int port)
+{
+	struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = __builtin_bswap16(port) };
+	int s = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0), r;
+
+	r = bind(s, (void *)&a, sizeof(a)) ? -errno : 0;
+	close(s);
+	return r;
+}
+
+static int try_connect(unsigned int ip, int port)
+{
+	struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = __builtin_bswap16(port),
+				 .sin_addr = { __builtin_bswap32(ip) } };
+	int s = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+
+	if (connect(s, (void *)&a, sizeof(a))) {
+		int err = errno;
+
+		close(s);
+		return -err;
+	}
+	return s;
+}
+
+/*
+ * One request on a netlink socket. Returns what the kernel made of it. NLM_F_ECHO, which
+ * means nothing here, stands for a plain write(): whoever says where a message is to go
+ * knows what it is writing to, and whoever opened the socket is then not asked about.
+ */
+static int nl_poke_on(int s, int type, int flags, const void *body, size_t len)
+{
+	struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
+	bool plain = flags & NLM_F_ECHO;
+	struct {
+		struct nlmsghdr h;
+		char body[64];
+	} req = { .h = { sizeof(req), type, NLM_F_REQUEST | NLM_F_ACK | (flags & ~NLM_F_ECHO) } };
+	char buf[4096];
+	struct nlmsghdr *h = (void *)buf;
+	int n;
+
+	if (body) {
+		memcpy(req.body, body, len);
+		req.h.nlmsg_len = NLMSG_LENGTH(len);
+	}
+	if ((plain ? write(s, &req, req.h.nlmsg_len) :
+		     sendto(s, &req, req.h.nlmsg_len, 0, (void *)&sa, sizeof(sa))) < 0)
+		return -errno;
+	n = recv(s, buf, sizeof(buf), 0);
+	if (n < (int)sizeof(*h))
+		return -EPROTO;
+	return h->nlmsg_type == NLMSG_ERROR ? ((struct nlmsgerr *)NLMSG_DATA(h))->error : 0;
+}
+
+static int nl_poke(int proto, int type, const void *body, size_t len)
+{
+	int s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, proto), r;
+
+	if (s < 0)
+		return -errno;
+	r = nl_poke_on(s, type, 0, body, len);
+	close(s);
+	return r;
+}
+
+/* Allowed or not is all that is asked: a request that is let through may well be nonsense. */
+#define MAY(right, r, refused, fmt, ...) \
+	CHECK((rights & (right)) ? (r) != (refused) : (r) == (refused), \
+	      "%s " fmt ": %d", (rights & (right)) ? "refused" : "allowed", ##__VA_ARGS__, (int)(r))
+
+/*
+ * What the cell may do to its network is what the builder named, @granted, and who in
+ * the cell may do it is whoever has the capability that it takes anywhere else.
+ */
+static void check_net_config(const char *dev, unsigned long long granted)
+{
+	static const struct { int type; unsigned long long right; } rtnl[] = {
+		{ RTM_SETLINK, CELL_NET_LINK }, { RTM_DELLINK, CELL_NET_LINK_NEW },
+		{ RTM_NEWADDR, CELL_NET_ADDR }, { RTM_DELADDR, CELL_NET_ADDR },
+		{ RTM_NEWROUTE, CELL_NET_ROUTE }, { RTM_DELROUTE, CELL_NET_ROUTE },
+		{ RTM_NEWNEIGH, CELL_NET_ROUTE }, { RTM_NEWRULE, CELL_NET_ROUTE },
+		{ RTM_NEWQDISC, CELL_NET_TC }, { RTM_NEWTFILTER, CELL_NET_TC },
+		/* Nobody has said what these are for. */
+		{ RTM_NEWNSID, 0 }, { RTM_NEWNETCONF, 0 }, { RTM_SETDCB, 0 },
+	};
+	struct ifinfomsg lo = { .ifi_index = 1 }, none = {};
+	struct nfgenmsg nfg = { .nfgen_family = AF_INET };
+	struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI };
+	unsigned long long rights = 0;
+	struct rtentry rt = {};
+	socklen_t len = 84;
+	char info[84] = "filter";
+	int one = 1, r, s;
+	unsigned int i;
+
+	if (has_cap(CAP_NET_ADMIN))
+		rights |= granted & ~CELL_NET_RAW;
+	if (has_cap(CAP_NET_RAW))
+		rights |= granted & CELL_NET_RAW;
+
+	for (i = 0; i < ARRAY_SIZE(rtnl); i++) {
+		r = nl_poke(NETLINK_ROUTE, rtnl[i].type, NULL, 0);
+		MAY(rtnl[i].right, r, -EPERM, "rtnetlink %d", rtnl[i].type);
+	}
+	/* The one request is for a device that is there and for one that is to be. */
+	r = nl_poke(NETLINK_ROUTE, RTM_NEWLINK, &lo, sizeof(lo));
+	MAY(CELL_NET_LINK, r, -EPERM, "RTM_NEWLINK for lo");
+	s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+	r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &none, sizeof(none));
+	MAY(CELL_NET_LINK_NEW, r, -EPERM, "RTM_NEWLINK for a new device");
+	close(s);
+
+	r = dev_flags(dev, IFF_UP, 0);
+	MAY(CELL_NET_LINK, r, -EPERM, "%s up", dev);
+	r = dev_flags(dev, 0, IFF_UP);
+	MAY(CELL_NET_LINK, r, -EPERM, "%s down", dev);
+	r = dev_set_addr(dev, 0x0a0900fe);
+	MAY(CELL_NET_ADDR, r, -EPERM, "address on %s", dev);
+	s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+	r = ioctl(s, SIOCADDRT, &rt) ? -errno : 0;
+	MAY(CELL_NET_ROUTE, r, -EPERM, "SIOCADDRT");
+
+	r = setsockopt(s, SOL_SOCKET, SO_MARK, &one, sizeof(one)) ? -errno : 0;
+	MAY(CELL_NET_SOCKOPT | CELL_NET_RAW, r, -EPERM, "SO_MARK");
+	r = setsockopt(s, IPPROTO_IP, IP_TRANSPARENT, &one, sizeof(one)) ? -errno : 0;
+	MAY(CELL_NET_SOCKOPT | CELL_NET_RAW, r, -EPERM, "IP_TRANSPARENT");
+
+	/* Both ways into netfilter, where it is built. */
+	r = nl_poke(NETLINK_NETFILTER, NFNL_SUBSYS_NFTABLES << 8 | NFT_MSG_GETGEN, &nfg,
+		    sizeof(nfg));
+	if (r != -EPROTONOSUPPORT) {
+		MAY(CELL_NET_FILTER, r, -EPERM, "nftables");
+		/* What these keep is not per network namespace. Not with any right. */
+		r = nl_poke(NETLINK_NETFILTER, NFNL_SUBSYS_OSF << 8 | 0 /* OSF_MSG_ADD */, &nfg,
+			    sizeof(nfg));
+		CHECK(r == -EPERM || ((rights & CELL_NET_FILTER) && r == -EINVAL), "osf: %d", r);
+		r = nl_poke(NETLINK_NETFILTER, NFNL_SUBSYS_CTHELPER << 8 | 0 /* NEW */, &nfg,
+			    sizeof(nfg));
+		CHECK(r == -EPERM || ((rights & CELL_NET_FILTER) && r == -EINVAL),
+		      "cthelper: %d", r);
+	}
+	r = getsockopt(s, IPPROTO_IP, 64 /* IPT_SO_GET_INFO */, info, &len) ? -errno : 0;
+	if (r != -ENOPROTOOPT)
+		MAY(CELL_NET_FILTER, r, -EPERM, "iptables");
+	close(s);
+
+	s = socket(AF_INET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMP);
+	MAY(CELL_NET_RAW, s < 0 ? -errno : 0, -EPERM, "raw socket");
+	close(s);
+	s = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0);
+	MAY(CELL_NET_RAW, s < 0 ? -errno : 0, -EPERM, "packet socket");
+	close(s);
+
+	r = write_str("/proc/sys/net/ipv4/ip_forward", "1") ? -errno : 0;
+	MAY(CELL_NET_SYSCTL, r, -EACCES, "net sysctl");
+
+	s = open("/tun", O_RDWR | O_CLOEXEC);
+	if (s >= 0) {
+		strcpy(ifr.ifr_name, "tap%d");
+		r = ioctl(s, TUNSETIFF, &ifr) ? -errno : 0;
+		MAY(CELL_NET_TUN, r, -EPERM, "TUNSETIFF");
+		close(s);
+	}
+
+	/* There is no right for these, so far or at all. */
+	r = try_bind(80);
+	CHECK(r == -EACCES, "port 80: %d", r);
+	r = nl_poke(NETLINK_XFRM, 0x10 /* XFRM_MSG_NEWSA */, NULL, 0);
+	CHECK(r == -EPERM || r == -EPROTONOSUPPORT, "xfrm: %d", r);
+}
+
+/* @arg: the rights in hex, then a device if it is not to be lo. */
+static void check_net_rights(const char *hex, const char *dev)
+{
+	unsigned long long granted = strtoull(hex, NULL, 16), caps = status_caps("CapPrm");
+	int s;
+
+	check_net_config(dev ?: "lo", granted);
+
+	/*
+	 * A request by netlink is also held against whoever opened the socket, lest
+	 * somebody has another write to a socket of its own making.
+	 */
+	CHECK(!set_caps_to(0, caps), "capset: %d", errno);
+	s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+	CHECK(!set_caps_to(caps, caps), "capset: %d", errno);
+	CHECK(nl_poke_on(s, RTM_SETLINK, NLM_F_ECHO, NULL, 0) == -EPERM, "a socket opened without");
+	close(s);
+	/* It does tell: the same on a socket of its own. */
+	if ((granted & CELL_NET_LINK) && has_cap(CAP_NET_ADMIN)) {
+		s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+		CHECK(nl_poke_on(s, RTM_SETLINK, NLM_F_ECHO, NULL, 0) != -EPERM, "its own socket");
+		close(s);
+	}
+
+	/* Who lets go of the capability is like anybody else in here. */
+	CHECK(!set_caps(0), "capset: %d", errno);
+	check_net_config(dev ?: "lo", granted);
+	CHECK(!has_cap(CAP_NET_ADMIN) && !has_cap(CAP_NET_RAW), "still has them");
+}
+
+/*
+ * The rights are over the network of the cell. Files 3 and 4 are an inet and a
+ * rtnetlink socket that somebody made in another one.
+ */
+static void check_net_foreign(void)
+{
+	struct ifreq ifr = { .ifr_name = "lo", .ifr_flags = IFF_UP };
+	int one = 1;
+
+	CHECK_ERRNO(setsockopt(3, SOL_SOCKET, SO_MARK, &one, sizeof(one)), EPERM);
+	CHECK_ERRNO(ioctl(3, SIOCSIFFLAGS, &ifr), EPERM);
+	CHECK(nl_poke_on(4, RTM_SETLINK, 0, NULL, 0) == -EPERM, "RTM_SETLINK elsewhere");
+	CHECK(nl_poke_on(4, RTM_NEWADDR, 0, NULL, 0) == -EPERM, "RTM_NEWADDR elsewhere");
+	/* They do work at home. */
+	check_net_config("lo", ~0ULL);
+}
+
+/* Nobody has set it up: there is a loopback device that is down, and that is that. */
+static void check_net_bare(void)
+{
+	struct if_nameindex *ni = if_nameindex();
+	int r;
+
+	CHECK(ni && ni[0].if_name && !strcmp(ni[0].if_name, "lo") && !ni[1].if_index,
+	      "more than lo");
+	r = dev_flags("lo", 0, 0);
+	CHECK(r >= 0 && !(r & IFF_UP), "lo: %x", r);
+	r = try_connect(INADDR_LOOPBACK, 7777);
+	CHECK(r == -ENETUNREACH, "connect: %d", r);
+	check_net_config("lo", 0);
+}
+
+/*
+ * The slot was left empty, so there is no network: no socket but a unix socket,
+ * and nothing to be learned about a network through any socket. File descriptor
+ * 3 is a socket of the host's that listens on its loopback device. What is handed
+ * to the cell works.
+ */
+static void check_net_none(void)
+{
+	static const int families[] = {
+		AF_INET, AF_INET6, AF_NETLINK, AF_PACKET, AF_ALG, AF_VSOCK, AF_KEY, AF_XDP,
+		AF_BLUETOOTH, AF_CAN, AF_TIPC,
+	};
+	static const unsigned long about_the_net[] = {
+		SIOCGIFCONF, SIOCGIFFLAGS, SIOCSIFFLAGS, SIOCGIFADDR, SIOCSIFADDR, SIOCGIFINDEX,
+		SIOCGIFNAME, SIOCGIFHWADDR, SIOCADDRT, SIOCDELRT, SIOCGARP, SIOCGIFBR,
+		SIOCBRADDBR, SIOCGIFVLAN, SIOCDEVPRIVATE, SIOCGSKNS, 0x8B01 /* SIOCGIWNAME */,
+	};
+	struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = "\0cell-test" };
+	int sp[2], socks[2], s, l, n, pending;
+	unsigned int i, j;
+	char c = 0;
+
+	for (i = 0; i < ARRAY_SIZE(families); i++) {
+		CHECK_ERRNO(socket(families[i], SOCK_DGRAM | SOCK_CLOEXEC, 0), EAFNOSUPPORT);
+		CHECK_ERRNO(socket(families[i], SOCK_STREAM | SOCK_CLOEXEC, 0), EAFNOSUPPORT);
+		CHECK_ERRNO(socket(families[i], SOCK_RAW | SOCK_CLOEXEC, 0), EAFNOSUPPORT);
+		CHECK_ERRNO(socketpair(families[i], SOCK_STREAM, 0, sp), EAFNOSUPPORT);
+	}
+	CHECK(!if_nameindex(), "there are devices to be listed");
+
+	/* Unix sockets are not the network. */
+	CHECK(!socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp), "socketpair: %d", errno);
+	l = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	CHECK(l >= 0 && !bind(l, (void *)&addr, sizeof(sa_family_t) + 10) && !listen(l, 1),
+	      "an abstract unix socket: %d", errno);
+	s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	CHECK(!connect(s, (void *)&addr, sizeof(sa_family_t) + 10), "connect to it: %d", errno);
+
+	/* What was handed over works, new sockets that come of it included. */
+	n = accept4(3, NULL, NULL, SOCK_CLOEXEC);
+	CHECK(n >= 0, "accept: %d", errno);
+	CHECK(read(n, &c, 1) == 1 && c == 'h' && write(n, "c", 1) == 1,
+	      "talk to the host: '%c', %d", c, errno);
+
+	/* About the socket: fine. About the network it is in: there is none. */
+	socks[0] = s;
+	socks[1] = n;
+	for (j = 0; j < ARRAY_SIZE(socks); j++) {
+		CHECK(!ioctl(socks[j], FIONREAD, &pending), "FIONREAD on %u: %d", j, errno);
+		CHECK(!ioctl(socks[j], SIOCGPGRP, &pending), "SIOCGPGRP on %u: %d", j, errno);
+		/* In among those that are about devices, by its number. */
+		if (j)
+			CHECK(!ioctl(socks[j], SIOCOUTQNSD, &pending), "SIOCOUTQNSD: %d", errno);
+		for (i = 0; i < ARRAY_SIZE(about_the_net); i++) {
+			struct ifreq ifr = { .ifr_name = "lo" };
+
+			errno = 0;
+			CHECK(ioctl(socks[j], about_the_net[i], &ifr) == -1 && errno == ENODEV,
+			      "ioctl %#lx on %u: %d", about_the_net[i], j, errno);
+		}
+	}
+}
+
+/* "new", and whoever built the cell has brought lo up before sealing it. */
+static void check_net_lo(void)
+{
+	int r = dev_flags("lo", 0, 0);
+
+	CHECK(r >= 0 && (r & IFF_UP), "lo: %x", r);
+	r = try_connect(INADDR_LOOPBACK, 7777);
+	CHECK(r == -ECONNREFUSED, "connect: %d", r);
+	check_net_config("lo", 0);
+}
+
+/*
+ * One end of a veth pair, with an address. The host listens at the other end.
+ * File descriptors 3 and 4 are for telling the host to add an address meanwhile.
+ */
+static void check_net_given(void)
+{
+	char c = 0;
+	int s;
+
+	s = try_connect(0x0a090001, 7777);
+	CHECK(s >= 0, "connect to the host: %d", s);
+	CHECK(write(s, "c", 1) == 1 && read(s, &c, 1) == 1 && c == 'h',
+	      "talk to the host: '%c', %d", c, errno);
+	close(s);
+	CHECK(!try_bind(8080), "port 8080");
+
+	CHECK(try_connect(0x0a090003, 9) == -ENETUNREACH ||
+	      try_connect(0x0a090003, 9) == -EHOSTUNREACH, "the address is there already");
+	CHECK(write(3, "a", 1) == 1 && read(4, &c, 1) == 1, "handshake: %d", errno);
+	s = try_connect(0x0a090003, 9);
+	CHECK(s == -ECONNREFUSED, "the address that was added from the outside: %d", s);
+
+	check_net_config("vc0", 0);
+}
+
+/* Low ports and ping are settings of the network namespace. */
+static void check_net_settings(const char *when)
+{
+	struct sockaddr_in lo = { .sin_family = AF_INET,
+				  .sin_addr = { __builtin_bswap32(INADDR_LOOPBACK) } };
+	struct {
+		unsigned char type, code;
+		unsigned short sum, id, seq;
+	} echo = { .type = 8 }, reply = { .type = 0xff };
+	bool after = !strcmp(when, "after");
+	int r, s;
+
+	r = try_bind(80);
+	CHECK(after ? !r : r == -EACCES, "port 80: %d", r);
+	s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_ICMP);
+	CHECK(after ? s >= 0 : (s == -1 && errno == EACCES), "ping socket: %d, %d", s, errno);
+	if (s >= 0) {
+		CHECK(sendto(s, &echo, sizeof(echo), 0, (void *)&lo, sizeof(lo)) == sizeof(echo),
+		      "echo: %d", errno);
+		CHECK(recv(s, &reply, sizeof(reply), 0) == sizeof(reply) && !reply.type,
+		      "echo reply: type %d, %d", reply.type, errno);
+		close(s);
+	}
+	/* They are not the cell's to change back. */
+	CHECK_ERRNO(write_str("/proc/sys/net/ipv4/ip_unprivileged_port_start", "1024"), EACCES);
+	CHECK_ERRNO(write_str("/proc/sys/net/ipv4/ping_group_range", "1 0"), EACCES);
+}
+
+/* Is there a network at all? */
+static void check_net_at_all(const char *want)
+{
+	int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+
+	if (!strcmp(want, "yes"))
+		CHECK(s >= 0, "socket: %d", errno);
+	else
+		CHECK(s == -1 && errno == EAFNOSUPPORT, "socket: %d, %d", s, errno);
+}
+
+/* The id that the network namespace of @dev has for the one its other end is in. */
+static int link_netnsid(const char *dev)
+{
+	struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
+	struct {
+		struct nlmsghdr h;
+		struct ifinfomsg ifi;
+	} req = {
+		.h = { sizeof(req), RTM_GETLINK, NLM_F_REQUEST },
+		.ifi = { .ifi_index = if_nametoindex(dev) },
+	};
+	int s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE), n, left;
+	struct nlmsghdr *h;
+	struct rtattr *a;
+	char buf[8192];
+
+	if (s < 0 || sendto(s, &req, sizeof(req), 0, (void *)&sa, sizeof(sa)) < 0)
+		return -errno;
+	n = recv(s, buf, sizeof(buf), 0);
+	close(s);
+	h = (void *)buf;
+	if (n < (int)sizeof(*h) || h->nlmsg_type != RTM_NEWLINK)
+		return -EPROTO;
+	left = h->nlmsg_len - NLMSG_LENGTH(sizeof(req.ifi));
+	for (a = IFLA_RTA(NLMSG_DATA(h)); RTA_OK(a, left); a = RTA_NEXT(a, left)) {
+		if (a->rta_type == IFLA_LINK_NETNSID)
+			return *(int *)RTA_DATA(a);
+	}
+	return -ENOENT;
+}
+
+/*
+ * A veth device whose peer is elsewhere gives the network namespace it is in an id for
+ * the other one, and a request can name that as the place for a device to go to, to be
+ * made in, or to be linked to. @dev is such a device, and the other end is the host's.
+ */
+static void check_net_no_way_out(const char *dev)
+{
+	struct {
+		struct ifinfomsg ifi;
+		struct nlattr a;
+		int nsid;
+		struct nlattr n;
+		char name[8];
+		struct nlattr li;
+		struct nlattr k;
+		char kind[8];
+	} req = {
+		.a = { NLA_HDRLEN + 4, IFLA_TARGET_NETNSID },
+		.n = { NLA_HDRLEN + 5, IFLA_IFNAME }, .name = "esc0",
+		.li = { 2 * NLA_HDRLEN + 8, IFLA_LINKINFO | NLA_F_NESTED },
+		.k = { NLA_HDRLEN + 5, IFLA_INFO_KIND }, .kind = "veth",
+	};
+	int s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE), r, nsid;
+
+	/* Anybody can look it up. Without one, what follows would say nothing. */
+	nsid = link_netnsid(dev);
+	CHECK(nsid >= 0, "no id for where the other end of %s is: %d", dev, nsid);
+	if (failures)
+		return;
+
+	req.nsid = nsid;
+	r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req));
+	CHECK(r == -EPERM, "a device made elsewhere: %d", r);
+	req.nsid = nsid + 77;
+	r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req));
+	CHECK(r == -EINVAL, "an id that there is not: %d", r);
+	req.nsid = nsid;
+
+	req.a.nla_type = IFLA_LINK_NETNSID;
+	r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req));
+	CHECK(r == -EPERM, "a device linked to elsewhere: %d", r);
+
+	req.a.nla_type = IFLA_TARGET_NETNSID;
+	req.ifi.ifi_index = if_nametoindex(dev);
+	r = nl_poke_on(s, RTM_SETLINK, 0, &req, offsetof(typeof(req), n));
+	CHECK(r == -EPERM, "%s moved elsewhere: %d", dev, r);
+	CHECK(if_nametoindex(dev), "%s is gone", dev);
+	close(s);
+
+	/* At home all of that works. */
+	req.a.nla_type = IFLA_UNSPEC;
+	req.ifi.ifi_index = 0;
+	s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+	r = nl_poke_on(s, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &req, sizeof(req));
+	CHECK(!r && if_nametoindex("esc0"), "a device made at home: %d", r);
+	close(s);
+}
+
+/* Attaches to a tap device, or makes it. With an owner or a group it is made to stay. */
+static int tun_attach_as(const char *dev, int owner, int group)
+{
+	struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI };
+	int fd = open("/bound", O_RDWR | O_CLOEXEC), r;
+
+	if (fd < 0)
+		fd = open("/tun", O_RDWR | O_CLOEXEC);
+	if (fd < 0)
+		return -errno;
+	strcpy(ifr.ifr_name, dev);
+	r = ioctl(fd, TUNSETIFF, &ifr) ? -errno : 0;
+	if (!r && owner >= 0 && ioctl(fd, TUNSETOWNER, owner))
+		r = -errno;
+	if (!r && group >= 0 && ioctl(fd, TUNSETGROUP, group))
+		r = -errno;
+	if (!r && (owner >= 0 || group >= 0) && ioctl(fd, TUNSETPERSIST, 1))
+		r = -errno;
+	close(fd);
+	return r;
+}
+
+static int tun_attach(const char *dev)
+{
+	return tun_attach_as(dev, -1, -1);
+}
+
+/*
+ * A tun device can have an owner and a group, who may attach to it without further ado.
+ * Those of tapu and tapg are the uid and the gid of whoever made this cell, which are also
+ * what a cell of one id runs on. They mean the maker. tapn has neither, which takes what
+ * it takes to make one.
+ */
+static void check_tun_owner(void)
+{
+	CHECK(tun_attach("tapu") == -EPERM, "the maker's, by uid: %d", tun_attach("tapu"));
+	CHECK(tun_attach("tapg") == -EPERM, "the maker's, by gid: %d", tun_attach("tapg"));
+	CHECK(tun_attach("tapn") == -EPERM, "nobody's: %d", tun_attach("tapn"));
+}
+
+/*
+ * Within a cell an owner is what it is anywhere. This one may make tun devices, and makes
+ * tapc for its uid 1000 and tapd for its gid 1000. taph was made on the outside, for the
+ * very same kuid, and that means nothing in here: it takes the right, as for a device of
+ * anybody else.
+ */
+static void check_tun_owner_within(void)
+{
+	int status;
+	pid_t pid;
+
+	CHECK(!tun_attach_as("tapc", 1000, -1), "making one for uid 1000: %d", errno);
+	CHECK(!tun_attach_as("tapd", -1, 1000), "making one for gid 1000: %d", errno);
+	CHECK(!tun_attach("taph"), "with the right");
+	if (failures)
+		return;
+
+	pid = fork();
+	if (!pid) {
+		if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+			_exit(1);
+		if (has_cap(CAP_NET_ADMIN))
+			_exit(2);
+		if (tun_attach("tapc"))
+			_exit(3);
+		if (tun_attach("tapd"))
+			_exit(5);
+		_exit(tun_attach("taph") == -EPERM ? 0 : 4);
+	}
+	CHECK(waitpid(pid, &status, 0) == pid && WIFEXITED(status) && !WEXITSTATUS(status),
+	      "as uid 1000: %x", status);
+}
+
+static void pause_for_good(void)
+{
+	for (;;)
+		pause();
+}
+
+static void fork_for_good(void)
+{
+	for (;;)
+		fork();
+}
+
+#define HELPER_RAN	"/tmp/cell_test_helper_ran"
+
+/* Run by the kernel, on the host: as modprobe, as request-key, or for a core dump. */
+static int note_helper(int argc, char **argv)
+{
+	int fd = open(HELPER_RAN, O_CREAT | O_WRONLY | O_APPEND, 0600), i;
+
+	for (i = 0; i < argc; i++)
+		dprintf(fd, "%s ", argv[i]);
+	dprintf(fd, "\n");
+	close(fd);
+	return 1;
+}
+
+/* What makes the kernel run a program, as root and outside of every namespace. */
+static void check_helpers(void)
+{
+	/* A protocol that nobody has: the kernel goes looking for a module. */
+	CHECK_ERRNO(socket(AF_ECONET, SOCK_DGRAM, 0), EAFNOSUPPORT);
+	/* A key that is not there, and something to tell whoever is to make it. */
+	CHECK_ERRNO(syscall(SYS_request_key, "user", "cell:test", "make it",
+			    KEY_SPEC_PROCESS_KEYRING), EPERM);
+	if (failures)
+		return;
+	/* And a crash. A real one: an init does not take a signal from itself. */
+	__builtin_trap();
+}
+
+int main(int argc, char **argv)
+{
+	const char *what = argc > 1 ? argv[1] : "";
+
+	if (!strcmp(what, "-q") || !strcmp(what, "core") || strstr(argv[0], "request-key"))
+		return note_helper(argc, argv);
+	if (!strcmp(what, "basic"))
+		check_basic();
+	else if (!strcmp(what, "helpers"))
+		check_helpers();
+	else if (!strcmp(what, "caps"))
+		check_caps(argv);
+	else if (!strcmp(what, "membership"))
+		check_membership();
+	else if (!strcmp(what, "setid"))
+		check_setid();
+	else if (!strcmp(what, "unix"))
+		check_unix();
+	else if (!strcmp(what, "dirfd"))
+		check_dirfd();
+	else if (!strcmp(what, "oneid"))
+		check_oneid(argv[2]);
+	else if (!strcmp(what, "pause"))
+		pause_for_good();
+	else if (!strcmp(what, "usr1"))
+		wait_for_usr1();
+	else if (!strcmp(what, "seccomp"))
+		check_seccomp();
+	else if (!strcmp(what, "signals"))
+		check_signals();
+	else if (!strcmp(what, "forkbomb"))
+		fork_for_good();
+	else if (!strcmp(what, "exit"))
+		return atoi(argv[2]);
+	else if (!strcmp(what, "in-cgroup"))
+		check_in_cgroup(argv[2]);
+	else if (!strcmp(what, "cg-manage"))
+		check_cg_manage(argv[2]);
+	else if (!strcmp(what, "cg-own"))
+		check_cg_own();
+	else if (!strcmp(what, "cg-churn"))
+		cg_churn();
+	else if (!strcmp(what, "cg-freeze"))
+		cg_freeze();
+	else if (!strcmp(what, "opens")) {
+		int fd = open(argv[2], O_RDWR);
+
+		if (!strcmp(argv[3], "yes"))
+			CHECK(fd >= 0, "%s: %d", argv[2], errno);
+		else
+			CHECK(fd == -1 && errno == EACCES, "%s: %d, %d", argv[2], fd, errno);
+	} else if (!strcmp(what, "exists"))
+		CHECK(!access(argv[2], F_OK), "%s: %d", argv[2], errno);
+	else if (!strcmp(what, "copy-up"))
+		check_copy_up();
+	else if (!strcmp(what, "vhost"))
+		check_vhost();
+	else if (!strcmp(what, "data"))
+		check_data(argv[2]);
+	else if (!strcmp(what, "second"))
+		check_second();
+	else if (!strcmp(what, "outside"))
+		check_outside_the_budget();
+	else if (!strcmp(what, "net-none"))
+		check_net_none();
+	else if (!strcmp(what, "net-at-all"))
+		check_net_at_all(argv[2]);
+	else if (!strcmp(what, "net-bare"))
+		check_net_bare();
+	else if (!strcmp(what, "net-lo"))
+		check_net_lo();
+	else if (!strcmp(what, "net-given"))
+		check_net_given();
+	else if (!strcmp(what, "net-settings"))
+		check_net_settings(argv[2]);
+	else if (!strcmp(what, "net-rights"))
+		check_net_rights(argv[2], argv[3]);
+	else if (!strcmp(what, "net-foreign"))
+		check_net_foreign();
+	else if (!strcmp(what, "tun-owner"))
+		check_tun_owner();
+	else if (!strcmp(what, "tun-owner-within"))
+		check_tun_owner_within();
+	else if (!strcmp(what, "net-no-way-out"))
+		check_net_no_way_out(argv[2]);
+	else if (!strcmp(what, "legacy"))
+		check_legacy_hierarchy();
+	else if (!strcmp(what, "outside-own"))
+		check_outside_but_its_own();
+	else if (!strcmp(what, "peer-race"))
+		check_peer_race();
+	else if (!strcmp(what, "pod-serve"))
+		pod_serve();
+	else if (!strcmp(what, "pod-connect"))
+		pod_connect();
+	else if (!strcmp(what, "uid"))
+		return ((int)getuid() == atoi(argv[2]) && (int)getgid() == atoi(argv[3])) ? 0 : 1;
+	else
+		return 99;
+
+	return failures ? 1 : 0;
+}
diff --git a/tools/testing/selftests/cell/cell_ids_test.c b/tools/testing/selftests/cell/cell_ids_test.c
new file mode 100644
index 000000000000..4178c80f8f63
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_ids_test.c
@@ -0,0 +1,762 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for cells, ids: the window, the blocks, and that nothing a cell writes
+ * imparts privilege.
+ */
+#include "cell_test.h"
+
+/* Root does not get a cell on kuid 0, least of all by default. */
+TEST_F(cell, no_cell_on_uid_0)
+{
+	EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 1 }), 4);
+	EXPECT_EQ(errno, EPERM);
+	cell_cleanup(&self->c);
+	EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 0 }), 18);
+	EXPECT_EQ(errno, EPERM);
+}
+
+/* The window is the kernel's. A one-id cell is not made of an id in it. */
+TEST_F(cell, no_cell_on_an_id_of_the_window)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		unsigned int ids = 1;
+		int cfd;
+
+		if (setresuid(CELL_WINDOW_FIRST + 0x1234, CELL_WINDOW_FIRST + 0x1234, -1))
+			_exit(1);
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0)
+			_exit(2);
+		if (cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)) != -1 ||
+		    errno != EPERM)
+			_exit(3);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+TEST_F(cell, window_is_the_kernels)
+{
+	char path[64];
+	int sync[2];
+	pid_t pid;
+	char c;
+
+	ASSERT_EQ(pipe(sync), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		if (unshare(CLONE_NEWUSER))
+			_exit(1);
+		if (write(sync[1], "x", 1) != 1)
+			_exit(1);
+		pause();
+		_exit(0);
+	}
+	ASSERT_EQ(read(sync[0], &c, 1), 1);
+
+	snprintf(path, sizeof(path), "/proc/%d/uid_map", pid);
+	EXPECT_EQ(write_file(path, "0 2147483648 1"), -1);
+	EXPECT_EQ(errno, EPERM);
+	EXPECT_EQ(write_file(path, "0 2147483000 1000"), -1);
+	EXPECT_EQ(errno, EPERM);
+	EXPECT_EQ(write_file(path, "0 4294900000 10"), -1);
+	EXPECT_EQ(errno, EPERM);
+	EXPECT_EQ(write_file(path, "0 2147483000 648"), 0);
+	snprintf(path, sizeof(path), "/proc/%d/gid_map", pid);
+	EXPECT_EQ(write_file(path, "0 3000000000 1"), -1);
+	EXPECT_EQ(errno, EPERM);
+
+	kill(pid, SIGKILL);
+	waitpid(pid, NULL, 0);
+}
+
+static int set_option(int fs, const char *key, const char *val)
+{
+	return sys_fsconfig(fs, FSCONFIG_SET_STRING, key, val, 0);
+}
+
+/* Nor is it for a mount option to put one on the files of a filesystem. */
+TEST_F(cell, no_mount_option_names_an_id_of_the_window)
+{
+	static const char * const types[] = { "tmpfs", "bpf", "devpts", "hugetlbfs" };
+	static const char * const keys[] = { "uid", "gid" };
+	unsigned int t, k, seen = 0;
+
+	for (t = 0; t < ARRAY_SIZE(types); t++) {
+		int fs = sys_fsopen(types[t], FSOPEN_CLOEXEC);
+
+		if (fs < 0)
+			continue;
+		seen++;
+		for (k = 0; k < ARRAY_SIZE(keys); k++) {
+			TH_LOG("%s %s", types[t], keys[k]);
+			EXPECT_EQ(set_option(fs, keys[k], "2147483648"), -1);
+			EXPECT_EQ(errno, EINVAL);
+			EXPECT_EQ(set_option(fs, keys[k], "4294901759"), -1);
+			EXPECT_EQ(errno, EINVAL);
+			EXPECT_EQ(set_option(fs, keys[k], "2147483647"), 0);
+		}
+		close(fs);
+	}
+	EXPECT_GE(seen, 2);
+}
+
+/* Is anything below @dirfd owned by an id of the window? */
+static int window_ids_below(int dirfd)
+{
+	struct dirent *e;
+	struct stat st;
+	int n = 0, sub;
+	DIR *d;
+
+	d = fdopendir(openat(dirfd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
+	if (!d)
+		return -1;
+	while ((e = readdir(d))) {
+		if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
+			continue;
+		if (fstatat(dirfd, e->d_name, &st, AT_SYMLINK_NOFOLLOW))
+			continue;
+		if (st.st_uid >= CELL_WINDOW_FIRST || st.st_gid >= CELL_WINDOW_FIRST)
+			n++;
+		if (!S_ISDIR(st.st_mode))
+			continue;
+		sub = openat(dirfd, e->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (sub >= 0) {
+			n += window_ids_below(sub);
+			close(sub);
+		}
+	}
+	closedir(d);
+	return n;
+}
+
+static int run_in(struct cellctx *c, const char *what, const char *arg)
+{
+	int pidfd = spawn(c->cfd, NULL, what, arg, NULL), st;
+
+	if (pidfd < 0)
+		return -99;
+	st = cell_wait(c->cfd);
+	close(pidfd);
+	return st;
+}
+
+/* An overlay that is idmapped for the cell stores the ids on disk, copying up included. */
+TEST_F(cell, overlay_for_a_cell)
+{
+	struct stat st;
+	int r;
+
+	r = cell_build(&self->c, &(struct build){ .ids = 65536, .overlay = OVL_IDMAPPED });
+	if (r == 22 && sys_fsopen("overlay", FSOPEN_CLOEXEC) < 0)
+		SKIP(return, "no overlayfs");
+	ASSERT_EQ(r, 0);
+	EXPECT_EQ(run_in(&self->c, "copy-up", NULL), 0);
+	ASSERT_EQ(fstatat(self->c.layers, "rw/upper/file", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, 0);
+	EXPECT_EQ(st.st_gid, 0);
+	ASSERT_EQ(fstatat(self->c.layers, "rw/upper/new", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, 0);
+	EXPECT_EQ(window_ids_below(self->c.layers), 0);
+}
+
+/*
+ * A stacking filesystem stores with the credentials of whoever mounted it, so the
+ * rule goes by the value of the id. Here a file of the lower layer belongs to an
+ * id of the window, and the upper layer is as the host has it.
+ */
+TEST_F(cell, copy_up_stores_no_id_of_the_window)
+{
+	struct stat st;
+	int r, fd;
+
+	r = cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+						  .overlay = OVL_LOWER_IDMAPPED });
+	if (r == 22 && sys_fsopen("overlay", FSOPEN_CLOEXEC) < 0)
+		SKIP(return, "no overlayfs");
+	ASSERT_EQ(r, 0);
+	ASSERT_EQ(fstatat(self->c.tree, "data/file", &st, 0), 0);
+	ASSERT_GE(st.st_uid, CELL_WINDOW_FIRST);
+
+	fd = openat(self->c.tree, "data/file", O_WRONLY | O_CLOEXEC);
+	EXPECT_EQ(fd, -1);
+	EXPECT_EQ(errno, EOVERFLOW);
+	EXPECT_EQ(fstatat(self->c.layers, "rw/upper/file", &st, 0), -1);
+	EXPECT_EQ(window_ids_below(self->c.layers), 0);
+
+	/* No cell is made of that to begin with. */
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EOVERFLOW);
+}
+
+/* Does a task of the cell with these ids get to open @path? */
+static int opens(struct cellctx *c, const char *path, unsigned int id, unsigned int group,
+		 const char *want)
+{
+	struct pidfd_cred cred = { .uid = id, .gid = id, .ngroups = !!group,
+				   .groups = (unsigned long)&group };
+	int pidfd = prepare(c->cfd, "opens", path, want), st = -99;
+
+	if (pidfd >= 0 &&
+	    !pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", &cred, sizeof(cred)) &&
+	    !pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0))
+		st = task_wait(pidfd);
+	close(pidfd);
+	return st;
+}
+
+#define KVM_GID		36
+
+/*
+ * A device is its number. Whoever builds a cell makes a node for it in the tree of
+ * the cell, and who in the cell may open that is a matter of the ids on disk, which
+ * the idmapping of the tree makes the ids of the cell. So several cells use one
+ * device, each by a node of its own, and none of them has an id of the host's.
+ */
+TEST_F(cell, device_by_a_node_of_its_own)
+{
+	struct cellctx cells[2];
+	struct stat st;
+	int init[2], i;
+
+	for (i = 0; i < 2; i++) {
+		ASSERT_EQ(cell_build(&cells[i], &(struct build){ .ids = 65536 }), 0);
+		/* /dev/null, for want of a /dev/kvm. */
+		ASSERT_EQ(mknodat(cells[i].raw, "kvm", S_IFCHR | 0660, makedev(1, 3)), 0);
+		ASSERT_EQ(fchownat(cells[i].raw, "kvm", 0, KVM_GID, 0), 0);
+		ASSERT_EQ(fchmodat(cells[i].raw, "kvm", 0660, 0), 0);
+		init[i] = spawn(cells[i].cfd, NULL, "pause", NULL, NULL);
+		ASSERT_GE(init[i], 0);
+	}
+	for (i = 0; i < 2; i++) {
+		EXPECT_EQ(opens(&cells[i], "/kvm", 0, 0, "yes"), 0);
+		EXPECT_EQ(opens(&cells[i], "/kvm", 1000, KVM_GID, "yes"), 0);
+		EXPECT_EQ(opens(&cells[i], "/kvm", 1000, 0, "no"), 0);
+		EXPECT_EQ(opens(&cells[i], "/kvm", 1000, KVM_GID + 1, "no"), 0);
+		/* Nothing of the window is on disk. */
+		ASSERT_EQ(fstatat(cells[i].raw, "kvm", &st, 0), 0);
+		EXPECT_EQ(st.st_gid, KVM_GID);
+		close(init[i]);
+		cell_cleanup(&cells[i]);
+	}
+}
+
+/*
+ * The node of the host's own /dev belongs to ids of the host, which nobody in a
+ * cell has, its root included. What is left is what the mode has for everybody.
+ */
+TEST_F(cell, device_by_the_node_of_the_host)
+{
+	struct stat was;
+	int init;
+
+	ASSERT_EQ(stat("/dev/full", &was), 0);
+	ASSERT_EQ(chown("/dev/full", 0, KVM_GID), 0);
+	ASSERT_EQ(chmod("/dev/full", 0660), 0);
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536,
+							.bind_file = "/dev/full" }), 0);
+	init = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(init, 0);
+
+	EXPECT_EQ(opens(&self->c, "/bound", 0, 0, "no"), 0);
+	EXPECT_EQ(opens(&self->c, "/bound", 1000, KVM_GID, "no"), 0);
+	ASSERT_EQ(chmod("/dev/full", 0666), 0);
+	EXPECT_EQ(opens(&self->c, "/bound", 1000, 0, "yes"), 0);
+
+	close(init);
+	EXPECT_EQ(chown("/dev/full", was.st_uid, was.st_gid), 0);
+	EXPECT_EQ(chmod("/dev/full", was.st_mode & 07777), 0);
+}
+
+TEST_F(cell, blocks_do_not_overlap)
+{
+	struct cellctx other;
+	long a, b;
+	pid_t pid;
+	int p1, p2;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(cell_build(&other, &(struct build){ .ids = 65536 }), 0);
+	p1 = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(p1, 0);
+	a = host_uid(pid);
+	p2 = spawn(other.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(p2, 0);
+	b = host_uid(pid);
+
+	EXPECT_GE(a, (long)CELL_WINDOW_FIRST);
+	EXPECT_LE(a, (long)CELL_WINDOW_LAST);
+	EXPECT_EQ(a % 65536, 0);
+	EXPECT_EQ(b % 65536, 0);
+	EXPECT_NE(a, b);
+
+	close(p1);
+	close(p2);
+	cell_cleanup(&other);
+}
+
+/* What cell_info() has for the first id of the block, or -1 if it has nothing. */
+static long ids_first(int cfd)
+{
+	struct cell_info info = { .mask = CELL_INFO_IDS_FIRST };
+
+	if (cell_info(cfd, &info, sizeof(info), 0))
+		return -2;
+	return info.mask & CELL_INFO_IDS_FIRST ? (long)info.ids_first : -1;
+}
+
+/*
+ * The kernel picks the block. Whoever builds the cell can ask which one it was, as
+ * soon as there is one, to say who in the cell is meant where a rule goes by an id:
+ * in a packet filter, in a routing rule.
+ */
+TEST_F(cell, builder_is_told_the_block)
+{
+	unsigned int ids = 65536, one = 1;
+	struct stat st;
+	int cfd, pidfd, status;
+	long first;
+	pid_t pid;
+
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	EXPECT_EQ(ids_first(cfd), -1);
+	ASSERT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), 0);
+	first = ids_first(cfd);
+	EXPECT_GE(first, (long)CELL_WINDOW_FIRST);
+	EXPECT_LE(first, (long)CELL_WINDOW_LAST);
+	EXPECT_EQ(first % 65536, 0);
+
+	/* It is what everything else says, later on. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	EXPECT_NE(ids_first(self->c.cfd), first);
+	first = ids_first(self->c.cfd);
+	ASSERT_EQ(stat(self->c.cg, &st), 0);
+	EXPECT_EQ((long)st.st_uid, first);
+	EXPECT_EQ((long)st.st_gid, first);
+	pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(host_uid(pid), first);
+	close(pidfd);
+	close(cfd);
+
+	/* The block goes back with the cell. */
+	ASSERT_EQ(cell_kill(self->c.cfd, 0), 0);
+	cell_wait(self->c.cfd);
+	EXPECT_EQ(ids_first(self->c.cfd), -1);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		/* One id is no block, and the builder knows its own. */
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0 || cell_configure(cfd, CELL_SET_BINARY, "ids", &one, sizeof(one)))
+			_exit(2);
+		if (ids_first(cfd) != -1)
+			_exit(3);
+		close(cfd);
+		/* No privilege is needed to be told. */
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0 || cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)))
+			_exit(4);
+		if (ids_first(cfd) < (long)CELL_WINDOW_FIRST)
+			_exit(5);
+		close(cfd);
+		/* But a name for it is: in here there is one for uid 1000 and no other. */
+		if (unshare(CLONE_NEWUSER) || write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(6);
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0 || cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)))
+			_exit(7);
+		_exit(ids_first(cfd) == -1 ? 0 : 8);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+/* There are only so many blocks, and no one user gets them all. */
+TEST_F(cell, blocks_are_counted)
+{
+	unsigned int ids = 65536;
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "1"), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		int a, b;
+
+		/*
+		 * They are counted by user, and a block goes on counting until
+		 * the user namespace it was lent to is freed, some time after
+		 * the cell is dead. So as a user that never had one.
+		 */
+		if (setresgid(54321, 54321, 54321) || setresuid(54321, 54321, 54321))
+			_exit(1);
+		a = cell_create(CELL_SELF, 0);
+		b = cell_create(CELL_SELF, 0);
+		if (a < 0 || b < 0)
+			_exit(2);
+		if (cell_configure(a, CELL_SET_BINARY, "ids", &ids, sizeof(ids)))
+			_exit(3);
+		if (cell_configure(b, CELL_SET_BINARY, "ids", &ids, sizeof(ids)) != -1 ||
+		    errno != ENOSPC)
+			_exit(4);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "4095"), 0);
+}
+
+TEST_F(cell, ids_never_reach_a_disk)
+{
+	int pidfd, host, fd;
+
+	/* A filesystem of the host's, wide open, and a way to it past the tree. */
+	fd = new_tmpfs();
+	ASSERT_GE(fd, 0);
+	host = openat(fd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(host, 0);
+	close(fd);
+	ASSERT_EQ(fchmod(host, 0777), 0);
+	fd = openat(host, "world", O_CREAT | O_WRONLY, 0666);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = prepare(self->c.cfd, "dirfd", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, host), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+
+	/* Nor by the hand of the host. */
+	EXPECT_EQ(fchownat(host, "world", CELL_WINDOW_FIRST + 5, -1, 0), -1);
+	EXPECT_EQ(errno, EOVERFLOW);
+	EXPECT_EQ(fchownat(host, "world", -1, CELL_WINDOW_LAST, 0), -1);
+	EXPECT_EQ(errno, EOVERFLOW);
+	EXPECT_EQ(fchownat(host, "world", CELL_WINDOW_FIRST - 1, CELL_WINDOW_FIRST - 1, 0), 0);
+	close(host);
+	close(pidfd);
+}
+
+/* A mount that was idmapped for a cell names its ids, so it holds on to them. */
+TEST_F(cell, idmapped_mount_pins_the_block)
+{
+	struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP };
+	unsigned int ids = 65536;
+	int a, b, mnt, i, ret = -1;
+
+	ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "1"), 0);
+	a = cell_create(CELL_SELF, 0);
+	ASSERT_GE(a, 0);
+	ASSERT_EQ(cell_configure(a, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), 0);
+	mnt = new_tmpfs();
+	ASSERT_GE(mnt, 0);
+	attr.userns_fd = a;
+	ASSERT_EQ(sys_mount_setattr(mnt, "", AT_EMPTY_PATH, &attr, sizeof(attr)), 0);
+
+	/* The cell is gone. The mount is not. */
+	ASSERT_EQ(cell_kill(a, 0), 0);
+	close(a);
+	usleep(500000);
+	b = cell_create(CELL_SELF, 0);
+	ASSERT_GE(b, 0);
+	EXPECT_EQ(cell_configure(b, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), -1);
+	EXPECT_EQ(errno, ENOSPC);
+
+	close(mnt);
+	for (i = 0; i < 100 && ret; i++) {
+		usleep(50000);
+		ret = cell_configure(b, CELL_SET_BINARY, "ids", &ids, sizeof(ids));
+	}
+	EXPECT_EQ(ret, 0);
+	close(b);
+	ASSERT_EQ(write_file("/proc/sys/user/max_cell_id_blocks", "4095"), 0);
+}
+
+/* Every capability there is but the one that no cell can have. */
+static __u64 all_caps(void)
+{
+	char buf[16] = {};
+	int fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY);
+
+	if (fd < 0 || read(fd, buf, sizeof(buf) - 1) <= 0)
+		return 0;
+	close(fd);
+	return ((2ULL << atoi(buf)) - 1) & ~CAP(SETFCAP);
+}
+
+/* A program that is set-uid root and comes with cap_net_raw, for the cell to run. */
+static int add_gain(struct cellctx *c)
+{
+	int fd;
+
+	if (copy_child(c->raw, "gain"))
+		return -1;
+	fd = openat(c->raw, "gain", O_RDONLY);
+	if (fd < 0 || fchmod(fd, 06755) ||
+	    fsetxattr(fd, "security.capability", fscap_v2, sizeof(fscap_v2), 0))
+		return -1;
+	return close(fd);
+}
+
+/* Being uid 0 of a cell comes with nothing. The builder names what it comes with. */
+TEST_F(cell, capabilities_are_what_the_builder_named)
+{
+	__u64 caps[] = {
+		0, CAP(CHOWN), CAP(SETUID) | CAP(SETGID), CAP(SETPCAP) | CAP(CHOWN),
+		CAP(NET_RAW), all_caps(),
+	};
+	struct cellctx c;
+	char hex[32];
+	unsigned int i;
+	int init, pidfd;
+
+	ASSERT_NE(caps[ARRAY_SIZE(caps) - 1], 0);
+	for (i = 0; i < ARRAY_SIZE(caps); i++) {
+		snprintf(hex, sizeof(hex), "%llx", caps[i]);
+		ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536, .caps = caps[i] }), 0);
+		ASSERT_EQ(add_gain(&c), 0);
+		pidfd = spawn(c.cfd, NULL, "caps", hex, NULL);
+		ASSERT_GE(pidfd, 0);
+		EXPECT_EQ(cell_wait(c.cfd), 0) {
+			TH_LOG("with %s", hex);
+		}
+		close(pidfd);
+		cell_cleanup(&c);
+	}
+
+	/* For every task that is started, not just the first. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = CAP(KILL) }), 0);
+	ASSERT_EQ(add_gain(&self->c), 0);
+	init = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(init, 0);
+	pidfd = spawn(self->c.cfd, NULL, "caps", "20", NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(task_wait(pidfd), 0);
+	close(pidfd);
+	close(init);
+}
+
+TEST_F(cell, files_impart_no_privilege)
+{
+	struct stat st;
+	char buf[64];
+	int fd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = CAP(CHOWN) }), 0);
+	fd = spawn(self->c.cfd, NULL, "setid", NULL, NULL);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(fd);
+
+	/* And this is what the host finds. */
+	ASSERT_EQ(fstatat(self->c.raw, "tmp/a", &st, 0), 0);
+	EXPECT_EQ(st.st_mode & (S_ISUID | S_ISGID), 0);
+	/* On disk the ids of the cell count from 0. No kuid of its block got there. */
+	EXPECT_EQ(st.st_uid, 33);
+	ASSERT_EQ(fstatat(self->c.raw, "su", &st, 0), 0);
+	EXPECT_EQ(st.st_mode & 07777, 0755);
+	EXPECT_EQ(st.st_uid, 0);
+	fd = openat(self->c.raw, "su", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(fgetxattr(fd, "security.capability", buf, sizeof(buf)), -1);
+	EXPECT_EQ(errno, ENODATA);
+	close(fd);
+	fd = openat(self->c.raw, "tmp/a", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(fgetxattr(fd, "security.capability", buf, sizeof(buf)), -1);
+	EXPECT_EQ(errno, ENODATA);
+	close(fd);
+}
+
+/* The kernel wrote the maps of a cell, once. Nobody writes them again. */
+TEST_F(cell, maps_are_the_kernels)
+{
+	static const char * const maps[] = { "uid_map", "gid_map" };
+	char path[64];
+	unsigned int i;
+	pid_t pid;
+	int p;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	p = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(p, 0);
+	for (i = 0; i < ARRAY_SIZE(maps); i++) {
+		snprintf(path, sizeof(path), "/proc/%d/%s", pid, maps[i]);
+		EXPECT_EQ(write_file(path, "0 0 1"), -1);
+		EXPECT_EQ(errno, EPERM);
+		EXPECT_EQ(write_file(path, "0 100000 65536"), -1);
+		EXPECT_EQ(errno, EPERM);
+	}
+	close(p);
+}
+
+static int mount_source(const char *fstype, const char *source, const char *opt,
+			const char *val)
+{
+	int fs, mnt;
+
+	fs = sys_fsopen(fstype, FSOPEN_CLOEXEC);
+	if (fs < 0)
+		return -1;
+	if (sys_fsconfig(fs, FSCONFIG_SET_STRING, "source", source, 0) ||
+	    (opt && sys_fsconfig(fs, FSCONFIG_SET_STRING, opt, val, 0)) ||
+	    sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) {
+		close(fs);
+		return -1;
+	}
+	mnt = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0);
+	close(fs);
+	return mnt;
+}
+
+static int make_su(int dirfd)
+{
+	int fd = openat(dirfd, "su", O_CREAT | O_WRONLY, 0755);
+
+	if (fd < 0 || fchmod(fd, 06755))
+		return -1;
+	close(fd);
+	return 0;
+}
+
+/*
+ * Dropping the bits changes the inode, so on a frozen filesystem the open has to
+ * wait. Needs CELL_TEST_EXT4_IMG to name an ext4 image.
+ */
+TEST_F(cell, drop_privs_waits_for_thaw)
+{
+	const char *img = getenv("CELL_TEST_EXT4_IMG");
+	int ctl, nr, loop, back, mnt, raw, pidfd;
+	struct pollfd pfd;
+	struct stat st;
+	char dev[32];
+
+	if (!img)
+		SKIP(return, "CELL_TEST_EXT4_IMG is not set");
+	ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
+	if (ctl < 0)
+		SKIP(return, "no loop devices");
+	nr = ioctl(ctl, LOOP_CTL_GET_FREE);
+	ASSERT_GE(nr, 0);
+	snprintf(dev, sizeof(dev), "/dev/loop%d", nr);
+	loop = open(dev, O_RDWR | O_CLOEXEC);
+	back = open(img, O_RDWR | O_CLOEXEC);
+	ASSERT_GE(loop, 0);
+	ASSERT_GE(back, 0);
+	ASSERT_EQ(ioctl(loop, LOOP_SET_FD, back), 0);
+
+	mnt = mount_source("ext4", dev, NULL, NULL);
+	ASSERT_GE(mnt, 0);
+	ASSERT_EQ(make_su(mnt), 0);
+	raw = sys_open_tree(mnt, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+	ASSERT_GE(raw, 0);
+	raw = openat(raw, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(raw, 0);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .data = mnt }), 0);
+	ASSERT_EQ(ioctl(raw, FIFREEZE, 0), 0);
+
+	pidfd = spawn(self->c.cfd, NULL, "data", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	pfd = (struct pollfd){ .fd = self->c.cfd, .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, 1500), 0);
+	ASSERT_EQ(fstatat(raw, "su", &st, 0), 0);
+	EXPECT_EQ(st.st_mode & 07777, 06755);
+
+	ASSERT_EQ(ioctl(raw, FITHAW, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	ASSERT_EQ(fstatat(raw, "su", &st, 0), 0);
+	EXPECT_EQ(st.st_mode & 07777, 0755);
+
+	close(pidfd);
+	close(raw);
+	close(mnt);
+	cell_cleanup(&self->c);
+	self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1;
+	ioctl(loop, LOOP_CLR_FD, 0);
+	close(loop);
+	close(back);
+	close(ctl);
+}
+
+/*
+ * A filesystem with ->atomic_open() opens a file it creates with the directory
+ * locked, a path tmpfs does not have. Needs CELL_TEST_9P_TAG to name a writable
+ * 9p share over virtio.
+ */
+TEST_F(cell, drop_privs_with_atomic_open)
+{
+	const char *tag = getenv("CELL_TEST_9P_TAG");
+	int status;
+	pid_t pid;
+
+	if (!tag)
+		SKIP(return, "CELL_TEST_9P_TAG is not set");
+
+	/* 9p cannot be idmapped, so a one-id cell, so not as root. */
+	ASSERT_EQ(prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct __user_cap_header_struct hdr = { .version = _LINUX_CAPABILITY_VERSION_3 };
+		struct __user_cap_data_struct data[2];
+		struct cellctx c;
+		int mnt, pidfd, step;
+
+		mnt = mount_source("9p", tag, "trans", "virtio");
+		if (mnt < 0)
+			_exit(77);
+		if (fchmodat(mnt, "", 0777, AT_EMPTY_PATH) && errno != ENOTSUP && errno != EINVAL)
+			_exit(9);
+		if (chown(CGROOT, 1000, 1000) || chown(CGROOT "/cgroup.procs", 1000, 1000))
+			_exit(10);
+		if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+			_exit(11);
+		if (syscall(SYS_capget, &hdr, data))
+			_exit(12);
+		data[0].effective = data[0].permitted;
+		data[1].effective = data[1].permitted;
+		if (syscall(SYS_capset, &hdr, data))
+			_exit(13);
+		unlinkat(mnt, "su", 0);
+		if (make_su(mnt))
+			_exit(14);
+
+		step = cell_build(&c, &(struct build){ .ids = 1, .data = mnt });
+		if (step)
+			_exit(20 + step);
+		pidfd = spawn(c.cfd, NULL, "data", "create", NULL);
+		if (pidfd < 0)
+			_exit(15);
+		if (cell_wait(c.cfd))
+			_exit(16);
+		cell_cleanup(&c);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(chown(CGROOT, 0, 0), 0);
+	EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0);
+	if (WIFEXITED(status) && WEXITSTATUS(status) == 77)
+		SKIP(return, "cannot mount the 9p share");
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_life_test.c b/tools/testing/selftests/cell/cell_life_test.c
new file mode 100644
index 000000000000..e00cbacebc49
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_life_test.c
@@ -0,0 +1,446 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for cells, the life of a cell: building one, killing one, handles, and
+ * the races between them.
+ */
+#include "cell_test.h"
+
+TEST_F(cell, life)
+{
+	struct cell_info info = { .mask = ~0ULL };
+	struct pollfd pfd;
+	pid_t pid;
+	int pidfd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_BUILDING);
+
+	/* Not before it is sealed. */
+	EXPECT_EQ(pidfd_open_empty(self->c.cfd, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+
+	/* Sealed is sealed. */
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_STRING, "hostname", "x", 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+
+	pidfd = spawn(self->c.cfd, &pid, "exit", "7", NULL);
+	ASSERT_GE(pidfd, 0);
+
+	pfd = (struct pollfd){ .fd = self->c.cfd, .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, 10000), 1);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	EXPECT_EQ(cell_wait(self->c.cfd), 7 << 8);
+	/* Repeatable. */
+	EXPECT_EQ(cell_wait(self->c.cfd), 7 << 8);
+
+	ASSERT_EQ(cell_info(self->c.cfd, &info, sizeof(info), 0), 0);
+	EXPECT_NE(info.id, 0);
+	EXPECT_EQ(info.parent_id, 0);
+	EXPECT_EQ(info.ids_count, 65536);
+	EXPECT_EQ(info.exit_status, 7 << 8);
+	EXPECT_NE(info.budget_cgroupid, 0);
+	/* All there is to know, but for the block of ids, which has gone back. */
+	EXPECT_EQ(info.mask, info.supported_mask & ~CELL_INFO_IDS_FIRST);
+
+	/* Nothing goes into a dead cell. */
+	EXPECT_EQ(pidfd_open_empty(self->c.cfd, 0), -1);
+	EXPECT_EQ(errno, ESRCH);
+	EXPECT_EQ(cg_populated(self->c.cg), 0);
+	close(pidfd);
+}
+
+/* A read of the cellfd waits for the end of the cell, if it may. */
+TEST_F(cell, read_waits_for_the_end)
+{
+	int pidfd, status = -1, fl;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+
+	fl = fcntl(self->c.cfd, F_GETFL);
+	ASSERT_EQ(fcntl(self->c.cfd, F_SETFL, fl | O_NONBLOCK), 0);
+	EXPECT_EQ(read(self->c.cfd, &status, sizeof(status)), -1);
+	EXPECT_EQ(errno, EAGAIN);
+	ASSERT_EQ(fcntl(self->c.cfd, F_SETFL, fl), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		usleep(200000);
+		_exit(!!cell_kill(self->c.cfd, 0));
+	}
+	/* Not for ever, should it not come. */
+	alarm(20);
+	EXPECT_EQ(read(self->c.cfd, &status, sizeof(status)), sizeof(status));
+	alarm(0);
+	EXPECT_EQ(status, SIGKILL);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	waitpid(pid, NULL, 0);
+	close(pidfd);
+}
+
+TEST_F(cell, bad_arguments)
+{
+	unsigned int ids;
+	char path[64];
+	int cfd;
+
+	EXPECT_EQ(cell_create(CELL_SELF, 1), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_create(0, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	EXPECT_EQ(fcntl(cfd, F_GETFD) & FD_CLOEXEC, FD_CLOEXEC);
+
+	/* A handle is passed on, not opened again. */
+	snprintf(path, sizeof(path), "/proc/self/fd/%d", cfd);
+	EXPECT_EQ(open(path, O_RDONLY), -1);
+	EXPECT_EQ(errno, ENXIO);
+	EXPECT_EQ(open(path, O_RDWR), -1);
+	EXPECT_EQ(errno, EPERM);
+
+	EXPECT_EQ(cell_configure(cfd, 99, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "nonsense", "x", 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "root", NULL, cfd), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_configure(0, CELL_CMD_SEAL, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	ids = 1000;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), -1);
+	EXPECT_EQ(errno, EINVAL);
+	ids = 65536;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "ids", &ids, sizeof(ids)), -1);
+	EXPECT_EQ(errno, EBUSY);
+	/* No root, no budget. */
+	EXPECT_EQ(cell_configure(cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	close(cfd);
+}
+
+TEST_F(cell, kill_while_building)
+{
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+	ASSERT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL);
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	/* Idempotent. */
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+}
+
+/* What cell_kill() takes, and that it works whatever state the cell is in. */
+TEST_F(cell, kill_in_every_state)
+{
+	struct pollfd pfd;
+	int fd, pidfd;
+
+	EXPECT_EQ(cell_kill(-1, 0), -1);
+	EXPECT_EQ(errno, EBADF);
+	fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(cell_kill(fd, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	close(fd);
+
+	/* Sealed, nothing spawned. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+	EXPECT_EQ(cell_kill(self->c.cfd, 1), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+	/* It is a system call of its own and no command of cell_configure(). */
+	EXPECT_EQ(cell_configure(self->c.cfd, 5, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	/* Dead. */
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	cell_cleanup(&self->c);
+
+	/* Running. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING);
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	/* Dying, or dead already. It does not wait. */
+	EXPECT_EQ(cell_kill(self->c.cfd, 0), 0);
+	pfd = (struct pollfd){ .fd = self->c.cfd, .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, 10000), 1);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	close(pidfd);
+}
+
+TEST_F(cell, kill_fork_bomb)
+{
+	int pidfd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(write_file(({ static char p[192];
+				snprintf(p, sizeof(p), "%s/pids.max", self->c.cg); p; }),
+			     "200") == 0 || errno == ENOENT, 1);
+	pidfd = spawn(self->c.cfd, NULL, "forkbomb", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	usleep(300000);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING);
+
+	ASSERT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL);
+	/* DEAD means gone. */
+	EXPECT_EQ(cg_populated(self->c.cg), 0);
+	close(pidfd);
+}
+
+/* A cell lives for as long as somebody holds a handle on it, and no longer. */
+TEST_F(cell, dies_with_its_last_handle)
+{
+	struct pollfd pfd;
+	int pidfd, second;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	pfd = (struct pollfd){ .fd = pidfd, .events = POLLIN };
+
+	/* Any handle will do, so a cell can be handed on. */
+	second = dup(self->c.cfd);
+	ASSERT_GE(second, 0);
+	close(self->c.cfd);
+	self->c.cfd = -1;
+	EXPECT_EQ(poll(&pfd, 1, 300), 0);
+
+	close(second);
+	EXPECT_EQ(poll(&pfd, 1, 10000), 1);
+	close(pidfd);
+}
+
+/* Whoever made a cell is killed, and had told nobody about it. */
+TEST_F(cell, dies_with_its_maker)
+{
+	char cg[sizeof(self->c.cg)] = {};
+	int p[2], i, populated = -1;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(p), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct build b = { .ids = 65536 };
+		struct cellctx c;
+
+		if (cell_build(&c, &b) ||
+		    spawn(c.cfd, NULL, "pause", NULL, NULL) < 0 ||
+		    write(p[1], c.cg, sizeof(c.cg)) != sizeof(c.cg))
+			_exit(1);
+		for (;;)
+			pause();
+	}
+	close(p[1]);
+	ASSERT_EQ(read(p[0], cg, sizeof(cg)), sizeof(cg));
+	close(p[0]);
+	EXPECT_EQ(cg_populated(cg), 1);
+
+	ASSERT_EQ(kill(pid, SIGKILL), 0);
+	ASSERT_EQ(waitpid(pid, NULL, 0), pid);
+	for (i = 0; i < 200 && populated; i++) {
+		usleep(50000);
+		populated = cg_populated(cg);
+	}
+	EXPECT_EQ(populated, 0);
+	for (i = 0; i < 100 && rmdir(cg) && errno == EBUSY; i++)
+		usleep(20000);
+}
+
+/* The name is taken, and there is nothing to fill the slot with yet. */
+TEST_F(cell, spawn_slot_is_reserved)
+{
+	unsigned int one = 1;
+	int cfd;
+
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_FLAG, "spawn", NULL, 0), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "spawn", "yes", 0), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "spawn", &one, sizeof(one)), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "spawn", NULL, cfd), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	close(cfd);
+}
+
+/*
+ * DEAD means gone. The init of a cell is still on its way out when the kernel
+ * learns of its exit, and the cell must not be reported dead before it is off
+ * the cpu for good: that is when it stops counting for cgroup.events.
+ */
+TEST_F(cell, dead_means_gone)
+{
+	int i, pidfd, early = 0;
+
+	for (i = 0; i < 60; i++) {
+		struct cellctx c;
+
+		ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536 }), 0);
+		pidfd = spawn(c.cfd, NULL, "exit", "0", NULL);
+		ASSERT_GE(pidfd, 0);
+		ASSERT_EQ(cell_wait(c.cfd), 0);
+		if (cg_populated(c.cg))
+			early++;
+		close(pidfd);
+		cell_cleanup(&c);
+	}
+	EXPECT_EQ(early, 0);
+}
+
+/* Make and break a good many, so that leaks and races get a chance. */
+TEST_F(cell, churn)
+{
+	int i;
+
+	for (i = 0; i < 40; i++) {
+		struct cellctx c;
+		int pidfd;
+
+		ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536 }), 0);
+		pidfd = spawn(c.cfd, NULL, i % 3 ? "pause" : "exit", "0", NULL);
+		ASSERT_GE(pidfd, 0);
+		if (i % 5 == 0) {
+			int second = prepare(c.cfd, "pause", NULL, NULL);
+
+			if (second >= 0)
+				close(second);
+		}
+		close(pidfd);
+		cell_cleanup(&c);
+	}
+}
+
+/* A kill against spawns in flight: whatever got in is gone when it says DEAD. */
+TEST_F(cell, kill_races_with_spawn)
+{
+	int i, j;
+
+	for (i = 0; i < 25; i++) {
+		pid_t spawners[3];
+		struct cellctx c;
+		int pidfd;
+
+		ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536 }), 0);
+		pidfd = spawn(c.cfd, NULL, "pause", NULL, NULL);
+		ASSERT_GE(pidfd, 0);
+
+		for (j = 0; j < 3; j++) {
+			spawners[j] = fork();
+			ASSERT_GE(spawners[j], 0);
+			if (!spawners[j]) {
+				for (;;) {
+					int p = j ? spawn(c.cfd, NULL, "pause", NULL, NULL) :
+						    prepare(c.cfd, "pause", NULL, NULL);
+
+					if (p < 0) {
+						_exit(errno == ESRCH || errno == ENOMEM ||
+						      errno == EINTR || errno == EBUSY ?
+						      0 : 1);
+					}
+					close(p);
+				}
+			}
+		}
+		usleep(1000 * (i % 7) * (i % 5));
+		ASSERT_EQ(cell_kill(c.cfd, 0), 0);
+		EXPECT_EQ(cell_wait(c.cfd), SIGKILL);
+		EXPECT_EQ(cg_populated(c.cg), 0);
+		for (j = 0; j < 3; j++) {
+			int status;
+
+			ASSERT_EQ(waitpid(spawners[j], &status, 0), spawners[j]);
+			EXPECT_EQ(status, 0);
+		}
+		EXPECT_EQ(cg_populated(c.cg), 0);
+		close(pidfd);
+		cell_cleanup(&c);
+	}
+}
+
+/* Starting an embryo while its handle goes away, from two processes. */
+TEST_F(cell, start_races_with_close)
+{
+	int i;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_GE(spawn(self->c.cfd, NULL, "pause", NULL, NULL), 0);
+
+	for (i = 0; i < 60; i++) {
+		int pidfd = prepare(self->c.cfd, "exit", "0", NULL);
+		int status;
+		pid_t pid;
+
+		ASSERT_GE(pidfd, 0);
+		pid = fork();
+		ASSERT_GE(pid, 0);
+		if (!pid) {
+			/* Both hold the one file. Only one start can win. */
+			int r = pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0);
+
+			_exit(!r || errno == EBUSY || errno == ESRCH ? 0 : 1);
+		}
+		if (i & 1)
+			pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0);
+		close(pidfd);
+		ASSERT_EQ(waitpid(pid, &status, 0), pid);
+		EXPECT_EQ(status, 0);
+	}
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING);
+}
+
+/* The last handle going away against everything else. */
+TEST_F(cell, close_races)
+{
+	int i;
+
+	for (i = 0; i < 40; i++) {
+		struct cellctx c;
+		int status;
+		pid_t pid;
+
+		ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536,
+							  .no_seal = i % 4 == 0 }), 0);
+		pid = fork();
+		ASSERT_GE(pid, 0);
+		if (!pid) {
+			int p = spawn(c.cfd, NULL, i % 3 ? "pause" : "exit", "0", NULL);
+
+			if (p >= 0)
+				close(p);
+			_exit(0);		/* drops its copy of the handle */
+		}
+		if (i % 2)
+			usleep(500 * (i % 9));
+		close(c.cfd);
+		c.cfd = -1;
+		ASSERT_EQ(waitpid(pid, &status, 0), pid);
+		cell_cleanup(&c);
+		EXPECT_EQ(access(c.cg, F_OK), -1);
+	}
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_net_test.c b/tools/testing/selftests/cell/cell_net_test.c
new file mode 100644
index 000000000000..297469260cba
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_net_test.c
@@ -0,0 +1,915 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Cells: the network. It is set up from the outside and handed to the cell, as
+ * the tree is. A cell configures nothing unless it was built to.
+ */
+#include "cell_test.h"
+#include <netinet/in.h>
+#include <linux/if_tun.h>
+#include <linux/if_link.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <linux/veth.h>
+
+#define HOST_IP		0x0a090001	/* 10.9.0.1 */
+#define CELL_IP		0x0a090002
+#define LATER_IP	0x0a090003
+
+static int set_cred(int pidfd, unsigned int id)
+{
+	struct pidfd_cred cred = { .uid = id, .gid = id };
+
+	return pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred", &cred, sizeof(cred));
+}
+
+/* Build a cell, run one thing in it as @id to its end. Returns its status. */
+static int run(const struct build *b, unsigned int id, const char *what, const char *arg)
+{
+	struct cellctx c;
+	int pidfd, st;
+
+	st = cell_build(&c, b);
+	if (st) {
+		cell_cleanup(&c);
+		return -100 - st;
+	}
+	pidfd = prepare(c.cfd, what, arg, NULL);
+	if (pidfd < 0 || (id && set_cred(pidfd, id)) ||
+	    pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0))
+		st = -99;
+	else
+		st = cell_wait(c.cfd);
+	close(pidfd);
+	cell_cleanup(&c);
+	return st;
+}
+
+static int dev_up(const char *dev, unsigned int ip)
+{
+	struct ifreq ifr = {};
+	struct sockaddr_in *sin = (void *)&ifr.ifr_addr;
+	int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0), r;
+
+	strcpy(ifr.ifr_name, dev);
+	sin->sin_family = AF_INET;
+	sin->sin_addr.s_addr = htonl(ip);
+	r = ioctl(s, SIOCSIFADDR, &ifr);
+	sin->sin_addr.s_addr = htonl(0xffffff00);
+	r = r ?: ioctl(s, SIOCSIFNETMASK, &ifr);
+	r = r ?: ioctl(s, SIOCGIFFLAGS, &ifr);
+	ifr.ifr_flags |= IFF_UP;
+	r = r ?: ioctl(s, SIOCSIFFLAGS, &ifr);
+	r = r ? -errno : 0;
+	close(s);
+	return r;
+}
+
+/* The same in the network namespace @net. Comes back. */
+static int dev_up_in(int net, const char *dev, unsigned int ip)
+{
+	int old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC), r;
+
+	if (setns(net, CLONE_NEWNET))
+		return -errno;
+	r = dev_up(dev, ip);
+	if (setns(old, CLONE_NEWNET))
+		return -errno;
+	close(old);
+	return r;
+}
+
+static int sysctl_in(int net, const char *path, const char *val)
+{
+	int old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC), r;
+
+	if (setns(net, CLONE_NEWNET))
+		return -errno;
+	r = write_file(path, val) ? -errno : 0;
+	if (setns(old, CLONE_NEWNET))
+		return -errno;
+	close(old);
+	return r;
+}
+
+/* A network namespace of the caller's own making, with nothing done to it. */
+static int make_netns(void)
+{
+	int old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC), net;
+
+	if (old < 0 || unshare(CLONE_NEWNET))
+		return -1;
+	net = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+	if (setns(old, CLONE_NEWNET))
+		return -1;
+	close(old);
+	return net;
+}
+
+static struct rtattr *put_attr(struct nlmsghdr *h, int type, const void *data, int len)
+{
+	struct rtattr *a = (void *)((char *)h + NLMSG_ALIGN(h->nlmsg_len));
+
+	a->rta_type = type;
+	a->rta_len = RTA_LENGTH(len);
+	if (data)
+		memcpy(RTA_DATA(a), data, len);
+	h->nlmsg_len = NLMSG_ALIGN(h->nlmsg_len) + RTA_ALIGN(a->rta_len);
+	return a;
+}
+
+static void end_nest(struct nlmsghdr *h, struct rtattr *a)
+{
+	a->rta_len = (char *)h + NLMSG_ALIGN(h->nlmsg_len) - (char *)a;
+}
+
+/* vh0 here, vc0 in the network namespace @net. */
+static int make_veth(int net)
+{
+	struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
+	struct {
+		struct nlmsghdr h;
+		struct ifinfomsg i;
+		char attrs[256];
+	} req = {};
+	struct rtattr *info, *data, *peer;
+	struct nlmsghdr *h;
+	char buf[1024];
+	int s, n;
+
+	req.h.nlmsg_type = RTM_NEWLINK;
+	req.h.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
+	req.h.nlmsg_len = NLMSG_LENGTH(sizeof(req.i));
+	put_attr(&req.h, IFLA_IFNAME, "vh0", 4);
+	info = put_attr(&req.h, IFLA_LINKINFO, NULL, 0);
+	put_attr(&req.h, IFLA_INFO_KIND, "veth", 5);
+	data = put_attr(&req.h, IFLA_INFO_DATA, NULL, 0);
+	peer = put_attr(&req.h, VETH_INFO_PEER, NULL, sizeof(struct ifinfomsg));
+	put_attr(&req.h, IFLA_IFNAME, "vc0", 4);
+	put_attr(&req.h, IFLA_NET_NS_FD, &net, sizeof(net));
+	end_nest(&req.h, peer);
+	end_nest(&req.h, data);
+	end_nest(&req.h, info);
+
+	s = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+	if (s < 0 || sendto(s, &req, req.h.nlmsg_len, 0, (void *)&sa, sizeof(sa)) < 0)
+		return -errno;
+	n = recv(s, buf, sizeof(buf), 0);
+	close(s);
+	h = (void *)buf;
+	if (n < (int)sizeof(*h) || h->nlmsg_type != NLMSG_ERROR)
+		return -EPROTO;
+	return ((struct nlmsgerr *)NLMSG_DATA(h))->error;
+}
+
+/*
+ * Nothing was said about the network, so there is none. What the cell is handed
+ * works all the same: here a socket that listens in the network of the host.
+ */
+TEST_F(cell, no_net_slot_no_network)
+{
+	static const unsigned int ids[] = { 0, 1000 };
+	struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(7778),
+				 .sin_addr = { htonl(INADDR_LOOPBACK) } };
+	int lsock, conn, pidfd, one = 1;
+	unsigned int i;
+	char c = 0;
+
+	ASSERT_EQ(dev_up("lo", INADDR_LOOPBACK), 0);
+	lsock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+	ASSERT_EQ(bind(lsock, (void *)&a, sizeof(a)), 0);
+	ASSERT_EQ(listen(lsock, 1), 0);
+
+	for (i = 0; i < ARRAY_SIZE(ids); i++) {
+		struct cellctx cell;
+
+		ASSERT_EQ(cell_build(&cell, &(struct build){ .ids = 65536 }), 0);
+		pidfd = prepare(cell.cfd, "net-none", NULL, NULL);
+		ASSERT_GE(pidfd, 0);
+		if (ids[i])
+			ASSERT_EQ(set_cred(pidfd, ids[i]), 0);
+		ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, lsock), 0);
+		ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+
+		conn = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+		ASSERT_EQ(connect(conn, (void *)&a, sizeof(a)), 0);
+		EXPECT_EQ(write(conn, "h", 1), 1);
+		ASSERT_EQ(wait_for(conn, pidfd), 0);
+		EXPECT_EQ(read(conn, &c, 1), 1);
+		EXPECT_EQ(c, 'c');
+		close(conn);
+
+		EXPECT_EQ(cell_wait(cell.cfd), 0);
+		close(pidfd);
+		cell_cleanup(&cell);
+	}
+	close(lsock);
+}
+
+static int seal(int cfd)
+{
+	return cell_configure(cfd, CELL_CMD_SEAL, NULL, NULL, 0);
+}
+
+static int run_sealed(struct cellctx *c, const char *what, const char *arg)
+{
+	int pidfd = spawn(c->cfd, NULL, what, arg, NULL), st;
+
+	if (pidfd < 0)
+		return -99;
+	st = cell_wait(c->cfd);
+	close(pidfd);
+	return st;
+}
+
+/* A seal that fails before there is a network namespace has decided nothing. */
+TEST_F(cell, failed_seal_leaves_the_net_slot_open)
+{
+	char max[32] = {};
+	int fd;
+
+	fd = open("/proc/sys/user/max_net_namespaces", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	ASSERT_GT(read(fd, max, sizeof(max) - 1), 0);
+	close(fd);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true }), 0);
+	ASSERT_EQ(write_file("/proc/sys/user/max_net_namespaces", "0"), 0);
+	EXPECT_EQ(seal(self->c.cfd), -1);
+	EXPECT_EQ(errno, ENOSPC);
+	ASSERT_EQ(write_file("/proc/sys/user/max_net_namespaces", max), 0);
+
+	ASSERT_EQ(cell_configure(self->c.cfd, CELL_SET_STRING, "net", "new", 0), 0);
+	ASSERT_EQ(seal(self->c.cfd), 0);
+	EXPECT_EQ(run_sealed(&self->c, "net-at-all", "yes"), 0);
+}
+
+/* Makes the next seal fail late, with EBUSY: somebody is in the mount namespace. */
+static pid_t sit_in(int rootfd)
+{
+	int ready[2];
+	pid_t pid;
+	char c;
+
+	if (pipe2(ready, O_CLOEXEC))
+		return -1;
+	pid = fork();
+	if (!pid) {
+		if (setns(rootfd, CLONE_NEWNS) || write(ready[1], "r", 1) != 1)
+			_exit(1);
+		for (;;)
+			pause();
+	}
+	close(ready[1]);
+	if (read(ready[0], &c, 1) != 1)
+		pid = -1;
+	close(ready[0]);
+	return pid;
+}
+
+static int seal_once_free(int cfd, pid_t sitter)
+{
+	int i, r = -1;
+
+	kill(sitter, SIGKILL);
+	waitpid(sitter, NULL, 0);
+	for (i = 0; i < 100 && (r = seal(cfd)) && errno == EBUSY; i++)
+		usleep(20000);
+	return r;
+}
+
+/* One that fails later has filled the slot in. No network is what it stays. */
+TEST_F(cell, failed_seal_then_still_no_network)
+{
+	pid_t sitter;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+							.keep_root = true }), 0);
+	sitter = sit_in(self->c.rootfd);
+	ASSERT_GT(sitter, 0);
+	EXPECT_EQ(seal(self->c.cfd), -1);
+	EXPECT_EQ(errno, EBUSY);
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_STRING, "net", "new", 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	ASSERT_EQ(seal_once_free(self->c.cfd, sitter), 0);
+	EXPECT_EQ(run_sealed(&self->c, "net-at-all", "no"), 0);
+}
+
+/* Unless the builder asks for it after all, to set it up. */
+TEST_F(cell, failed_seal_then_a_network_after_all)
+{
+	pid_t sitter;
+	int net;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+							.keep_root = true }), 0);
+	sitter = sit_in(self->c.rootfd);
+	ASSERT_GT(sitter, 0);
+	EXPECT_EQ(seal(self->c.cfd), -1);
+	EXPECT_EQ(errno, EBUSY);
+	net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0);
+	close(net);
+	ASSERT_EQ(seal_once_free(self->c.cfd, sitter), 0);
+	EXPECT_EQ(run_sealed(&self->c, "net-lo", NULL), 0);
+}
+
+/* "new" is one for whoever builds the cell to set up. Until then there is little to it. */
+TEST_F(cell, new_net_is_the_builders_to_set_up)
+{
+	int net, pidfd;
+
+	EXPECT_EQ(run(&(struct build){ .ids = 65536, .net_str = "new" }, 0, "net-bare", NULL), 0);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new",
+							.no_seal = true }), 0);
+	net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0);
+	ASSERT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0);
+	pidfd = spawn(self->c.cfd, NULL, "net-lo", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+	close(net);
+}
+
+/*
+ * What an orchestrator does: it makes the network namespace, puts one end of a
+ * veth pair in there, gives it an address, and goes on owning all of it. The cell
+ * uses it, and its root can change nothing about it.
+ */
+TEST_F(cell, net_is_set_up_from_the_outside)
+{
+	struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(7777),
+				 .sin_addr = { htonl(HOST_IP) } };
+	int net, lsock, conn, up[2], down[2], pidfd, one = 1, r;
+	char c = 0;
+
+	net = make_netns();
+	ASSERT_GE(net, 0);
+	r = make_veth(net);
+	if (r == -EOPNOTSUPP) {
+		close(net);
+		SKIP(return, "no veth");
+	}
+	ASSERT_EQ(r, 0);
+	ASSERT_EQ(dev_up("vh0", HOST_IP), 0);
+	ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0);
+	ASSERT_EQ(dev_up_in(net, "vc0", CELL_IP), 0);
+
+	lsock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+	ASSERT_EQ(bind(lsock, (void *)&a, sizeof(a)), 0);
+	ASSERT_EQ(listen(lsock, 1), 0);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net = net }), 0);
+	ASSERT_EQ(pipe2(up, O_CLOEXEC), 0);
+	ASSERT_EQ(pipe2(down, O_CLOEXEC), 0);
+	pidfd = prepare(self->c.cfd, "net-given", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, up[1]), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "4", NULL, down[0]), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+
+	ASSERT_EQ(wait_for(lsock, pidfd), 0);
+	conn = accept(lsock, NULL, NULL);
+	ASSERT_GE(conn, 0);
+	ASSERT_EQ(wait_for(conn, pidfd), 0);
+	EXPECT_EQ(read(conn, &c, 1), 1);
+	EXPECT_EQ(c, 'c');
+	EXPECT_EQ(write(conn, "h", 1), 1);
+
+	/* The cell is running. Whoever owns the namespace still gets at it. */
+	ASSERT_EQ(wait_for(up[0], pidfd), 0);
+	ASSERT_EQ(read(up[0], &c, 1), 1);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING);
+	EXPECT_EQ(dev_up_in(net, "lo", LATER_IP), 0);
+	ASSERT_EQ(write(down[1], "g", 1), 1);
+
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+	close(conn);
+	close(lsock);
+	close(net);
+}
+
+/* That of the host is no different: it is there to be used. */
+TEST_F(cell, host_net_is_used_and_not_configured)
+{
+	int net = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+
+	ASSERT_GE(net, 0);
+	ASSERT_EQ(dev_up("lo", INADDR_LOOPBACK), 0);
+	EXPECT_EQ(run(&(struct build){ .ids = 65536, .net = net }, 0, "net-lo", NULL), 0);
+	close(net);
+}
+
+/* What a pod expects takes no capability: it is set on the network namespace. */
+TEST_F(cell, low_ports_and_ping_are_settings)
+{
+	static const unsigned int ids[] = { 0, 1000 };
+	unsigned int i;
+	int net;
+
+	net = make_netns();
+	ASSERT_GE(net, 0);
+	ASSERT_EQ(dev_up_in(net, "lo", INADDR_LOOPBACK), 0);
+	for (i = 0; i < ARRAY_SIZE(ids); i++) {
+		EXPECT_EQ(run(&(struct build){ .ids = 65536, .net = net }, ids[i],
+			      "net-settings", "before"), 0);
+	}
+
+	ASSERT_EQ(sysctl_in(net, "/proc/sys/net/ipv4/ip_unprivileged_port_start", "0"), 0);
+	ASSERT_EQ(sysctl_in(net, "/proc/sys/net/ipv4/ping_group_range", "0 4294967294"), 0);
+	for (i = 0; i < ARRAY_SIZE(ids); i++) {
+		EXPECT_EQ(run(&(struct build){ .ids = 65536, .net = net }, ids[i],
+			      "net-settings", "after"), 0);
+	}
+	close(net);
+}
+
+/* Is there a device of that name in the network namespace? -1 if it cannot be entered. */
+static int dev_in(int net, const char *dev)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	if (!pid) {
+		if (setns(net, CLONE_NEWNET))
+			_exit(2);
+		_exit(!!if_nametoindex(dev));
+	}
+	if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ||
+	    WEXITSTATUS(status) > 1)
+		return -1;
+	return WEXITSTATUS(status);
+}
+
+/* A network namespace is taken apart in its own time. */
+static bool dev_goes(const char *dev)
+{
+	int i;
+
+	for (i = 0; i < 100 && if_nametoindex(dev); i++)
+		usleep(100000);
+	return !if_nametoindex(dev);
+}
+
+/*
+ * A cell that dies takes its network along: it lets go of the namespace, and that is
+ * the end of whatever is in there.
+ *
+ * Sealed is sealed, so the namespace is not to be had from the cell later on. Whoever
+ * wants to take the network apart in an orderly way keeps the file from when the cell
+ * was built. The namespace is then there for as long as that file is, with all that is
+ * in it and nobody left to use it.
+ */
+TEST_F(cell, net_goes_with_the_cell)
+{
+	struct cellctx c;
+	int net, pidfd, r;
+
+	/* Nobody has kept anything. */
+	ASSERT_EQ(cell_build(&c, &(struct build){ .ids = 65536, .no_seal = true,
+						  .net_str = "new" }), 0);
+	net = cell_open(c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	r = make_veth(net);
+	close(net);
+	if (r == -EOPNOTSUPP) {
+		cell_cleanup(&c);
+		SKIP(return, "no veth");
+	}
+	ASSERT_EQ(r, 0);
+	ASSERT_EQ(seal(c.cfd), 0);
+	pidfd = spawn(c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_NE(if_nametoindex("vh0"), 0);
+	ASSERT_EQ(cell_kill(c.cfd, 0), 0);
+	EXPECT_EQ(cell_wait(c.cfd), SIGKILL);
+	close(pidfd);
+	/* The cellfd is still open. It does not hold on to anything. */
+	EXPECT_TRUE(dev_goes("vh0"));
+	cell_cleanup(&c);
+
+	/* Somebody has kept the file. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+							.net_str = "new" }), 0);
+	net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	ASSERT_EQ(make_veth(net), 0);
+	ASSERT_EQ(seal(self->c.cfd), 0);
+	EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	pidfd = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	ASSERT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL);
+	close(pidfd);
+	EXPECT_EQ(cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+
+	/* Long enough for it to have gone, if it were going to. */
+	usleep(500000);
+	EXPECT_EQ(dev_in(net, "vc0"), 1);
+	EXPECT_NE(if_nametoindex("vh0"), 0);
+
+	close(net);
+	EXPECT_TRUE(dev_goes("vh0"));
+}
+
+#define NET_CAPS	(CAP(NET_ADMIN) | CAP(NET_RAW) | CAP(NET_BIND_SERVICE))
+#define ALL_RIGHTS	((CELL_NET_RAW << 1) - 1)
+
+static int run_rights(__u64 rights, __u64 caps, unsigned int id)
+{
+	struct build b = { .ids = 65536, .net_str = "new", .caps = caps, .net_rights = rights,
+			   .tun = true };
+	char hex[32];
+
+	snprintf(hex, sizeof(hex), "%llx", rights);
+	return run(&b, id, "net-rights", hex);
+}
+
+/*
+ * The network namespace of a cell is never the cell's, so no capability of a task of
+ * the cell counts for it. What such a task may do to it all the same, the builder
+ * names, one thing at a time.
+ */
+TEST_F(cell, net_rights_are_what_the_builder_named)
+{
+	__u64 right;
+
+	EXPECT_EQ(run_rights(0, NET_CAPS, 0), 0);
+	for (right = 1; right <= CELL_NET_RAW; right <<= 1) {
+		EXPECT_EQ(run_rights(right, NET_CAPS, 0), 0) {
+			TH_LOG("with %llx", right);
+		}
+	}
+	EXPECT_EQ(run_rights(ALL_RIGHTS, NET_CAPS, 0), 0);
+}
+
+/* The right says what, the capability says who in the cell. It takes both. */
+TEST_F(cell, net_rights_are_for_who_has_the_capability)
+{
+	EXPECT_EQ(run_rights(ALL_RIGHTS, 0, 0), 0);
+	EXPECT_EQ(run_rights(ALL_RIGHTS, CAP(NET_ADMIN), 0), 0);
+	EXPECT_EQ(run_rights(ALL_RIGHTS, CAP(NET_RAW), 0), 0);
+	/* Somebody else in there, who has left them behind with uid 0. */
+	EXPECT_EQ(run_rights(ALL_RIGHTS, NET_CAPS, 1000), 0);
+}
+
+/* What the "net-rights" slot takes, and from whom. */
+TEST_F(cell, net_rights_slot)
+{
+	struct cell_info info = { .mask = CELL_INFO_NET_RIGHTS };
+	__u64 rights[2] = { CELL_NET_LINK | CELL_NET_ADDR, 0 };
+	int cfd, status;
+	pid_t pid;
+
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	/* Rights over what? */
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 8), -1);
+	EXPECT_EQ(errno, EINVAL);
+	ASSERT_EQ(cell_configure(cfd, CELL_SET_STRING, "net", "new", 0), 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 4), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 16), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", (void *)1, 8), -1);
+	EXPECT_EQ(errno, EFAULT);
+	rights[1] = CELL_NET_RAW << 1;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8), -1);
+	EXPECT_EQ(errno, EINVAL);
+	ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0);
+	EXPECT_EQ(info.mask, CELL_INFO_NET_RIGHTS);
+	EXPECT_EQ(info.net_rights, 0);
+
+	/* The last word counts. */
+	rights[1] = ALL_RIGHTS;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8), 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 8), 0);
+	ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0);
+	EXPECT_EQ(info.net_rights, rights[0]);
+	close(cfd);
+
+	/* Until the seal. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new",
+							.net_rights = rights[0] }), 0);
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8), -1);
+	EXPECT_EQ(errno, EBUSY);
+
+	/* There used to be a network namespace that the cell owns. */
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "net", "own", 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	close(cfd);
+
+	/* Nobody hands out what is not theirs to do. */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		__u64 raw = CELL_NET_RAW, none = 0;
+
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		/* This one belongs to the user namespace of the host, where it is nobody. */
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0 || cell_configure(cfd, CELL_SET_STRING, "net", "new", 0))
+			_exit(2);
+		if (!cell_configure(cfd, CELL_SET_BINARY, "net-rights", rights, 8) ||
+		    errno != EPERM)
+			_exit(3);
+		if (!cell_configure(cfd, CELL_SET_BINARY, "net-rights", &raw, 8) || errno != EPERM)
+			_exit(4);
+		if (cell_configure(cfd, CELL_SET_BINARY, "net-rights", &none, 8))
+			_exit(5);
+		close(cfd);
+		/* And this one to a user namespace of its own. */
+		if (unshare(CLONE_NEWUSER) || write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(6);
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0 || cell_configure(cfd, CELL_SET_STRING, "net", "new", 0))
+			_exit(7);
+		_exit(cell_configure(cfd, CELL_SET_BINARY, "net-rights", &rights[1], 8) ? 8 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+/* They are rights over the network of the cell, and over no other. */
+TEST_F(cell, net_rights_end_at_the_network_of_the_cell)
+{
+	int inet, rtnl, pidfd;
+
+	inet = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+	rtnl = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+	ASSERT_GE(inet, 0);
+	ASSERT_GE(rtnl, 0);
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new",
+							.caps = NET_CAPS, .tun = true,
+							.net_rights = ALL_RIGHTS }), 0);
+	pidfd = prepare(self->c.cfd, "net-foreign", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, inet), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "4", NULL, rtnl), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+	close(inet);
+	close(rtnl);
+}
+
+/* Nor does a device leave for another, or come to be there. */
+TEST_F(cell, net_rights_move_nothing_out)
+{
+	int net, r;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+							.net_str = "new", .caps = NET_CAPS,
+							.net_rights = ALL_RIGHTS }), 0);
+	net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	r = make_veth(net);
+	close(net);
+	if (r == -EOPNOTSUPP)
+		SKIP(return, "no veth");
+	ASSERT_EQ(r, 0);
+	ASSERT_EQ(seal(self->c.cfd), 0);
+	r = spawn(self->c.cfd, NULL, "net-no-way-out", "vc0", NULL);
+	ASSERT_GE(r, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(r);
+	EXPECT_EQ(if_nametoindex("esc0"), 0);
+	EXPECT_EQ(if_nametoindex("vc0"), 0);
+}
+
+/* The builder hands it a device and the right to run it. The rest is up to the cell. */
+TEST_F(cell, net_rights_run_a_device_that_was_handed_over)
+{
+	char hex[32];
+	int net, r;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+							.net_str = "new", .caps = NET_CAPS,
+							.net_rights = CELL_NET_LINK |
+								      CELL_NET_ADDR }), 0);
+	net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	r = make_veth(net);
+	close(net);
+	if (r == -EOPNOTSUPP)
+		SKIP(return, "no veth");
+	ASSERT_EQ(r, 0);
+	ASSERT_EQ(seal(self->c.cfd), 0);
+	snprintf(hex, sizeof(hex), "%llx", CELL_NET_LINK | CELL_NET_ADDR);
+	r = spawn(self->c.cfd, NULL, "net-rights", hex, "vc0");
+	ASSERT_GE(r, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(r);
+}
+
+/*
+ * A network namespace that was made for a cell is not the cell's, so who owns it
+ * may enter it whatever the cell is up to, and may share it with another cell.
+ */
+TEST_F(cell, new_net_belongs_to_the_builder)
+{
+	int net, old, other, pidfd;
+	char path[64];
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net_str = "new" }), 0);
+	pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	snprintf(path, sizeof(path), "/proc/%d/ns/net", pid);
+	net = open(path, O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(net, 0);
+
+	old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+	EXPECT_EQ(setns(net, CLONE_NEWNET), 0);
+	ASSERT_EQ(setns(old, CLONE_NEWNET), 0);
+	close(old);
+
+	other = cell_create(CELL_SELF, 0);
+	ASSERT_GE(other, 0);
+	EXPECT_EQ(cell_configure(other, CELL_SET_FD, "net", NULL, net), 0);
+	close(other);
+	close(net);
+	close(pidfd);
+}
+
+/* A builder without privilege works in a user namespace of its own, which owns it. */
+TEST_F(cell, unprivileged_builder_sets_up_the_net)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(chown(CGROOT, 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct cellctx c;
+		pid_t helper;
+		int step, net;
+
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+			_exit(2);
+		if (write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(3);
+		step = cell_build(&c, &(struct build){ .ids = 1, .no_seal = true });
+		if (step)
+			_exit(20 + step);
+		net = cell_open(c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+		if (net < 0)
+			_exit(4);
+		/* There is no way back to the network namespace of the host. */
+		helper = fork();
+		if (!helper)
+			_exit(setns(net, CLONE_NEWNET) || dev_up("lo", INADDR_LOOPBACK) ? 1 : 0);
+		if (waitpid(helper, &step, 0) != helper || step)
+			_exit(5);
+		if (cell_configure(c.cfd, CELL_CMD_SEAL, NULL, NULL, 0))
+			_exit(6);
+		if (spawn(c.cfd, NULL, "net-lo", NULL, NULL) < 0)
+			_exit(7);
+		_exit(cell_wait(c.cfd) ? 8 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	EXPECT_EQ(chown(CGROOT, 0, 0), 0);
+	EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0);
+}
+
+#define TUN_NODE	"/tmp/cell-test-tun"
+
+/* A persistent tap device in the network namespace of the caller. -1: nobody's. */
+static int make_tap(const char *dev, long long uid, long long gid)
+{
+	struct ifreq ifr = { .ifr_flags = IFF_TAP | IFF_NO_PI };
+	int fd = open(TUN_NODE, O_RDWR | O_CLOEXEC), r = 0;
+
+	if (fd < 0)
+		return -errno;
+	strcpy(ifr.ifr_name, dev);
+	if (ioctl(fd, TUNSETIFF, &ifr) ||
+	    (uid >= 0 && ioctl(fd, TUNSETOWNER, (unsigned long)uid)) ||
+	    (gid >= 0 && ioctl(fd, TUNSETGROUP, (unsigned long)gid)) ||
+	    ioctl(fd, TUNSETPERSIST, 1))
+		r = -errno;
+	close(fd);
+	return r;
+}
+
+/* A cell of one id runs on the uid of its maker. What is the maker's is not the cell's. */
+TEST_F(cell, tun_device_of_the_maker_is_not_the_cells)
+{
+	int status;
+	pid_t pid;
+
+	unlink(TUN_NODE);
+	ASSERT_EQ(mknod(TUN_NODE, S_IFCHR | 0666, makedev(10, 200)), 0);
+	ASSERT_EQ(chmod(TUN_NODE, 0666), 0);
+	status = open(TUN_NODE, O_RDWR | O_CLOEXEC);
+	if (status < 0) {
+		unlink(TUN_NODE);
+		SKIP(return, "no tun");
+	}
+	close(status);
+	ASSERT_EQ(chown(CGROOT, 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct cellctx c;
+		pid_t helper;
+		int step, net;
+
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) || prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+			_exit(2);
+		if (write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(3);
+		step = cell_build(&c, &(struct build){ .ids = 1, .no_seal = true,
+						       .bind_file = TUN_NODE });
+		if (step)
+			_exit(20 + step);
+		net = cell_open(c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+		if (net < 0)
+			_exit(4);
+		helper = fork();
+		if (!helper)
+			_exit(setns(net, CLONE_NEWNET) || make_tap("tapu", 1000, -1) ||
+			      make_tap("tapg", -1, 1000) || make_tap("tapn", -1, -1) ? 1 : 0);
+		if (waitpid(helper, &step, 0) != helper || step)
+			_exit(5);
+		if (cell_configure(c.cfd, CELL_CMD_SEAL, NULL, NULL, 0))
+			_exit(6);
+		if (spawn(c.cfd, NULL, "tun-owner", NULL, NULL) < 0)
+			_exit(7);
+		_exit(cell_wait(c.cfd) ? 8 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	EXPECT_EQ(chown(CGROOT, 0, 0), 0);
+	EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0);
+	unlink(TUN_NODE);
+}
+
+/* An owner counts in the cell that named it, and in no other. Not even if it was meant to. */
+TEST_F(cell, tun_owner_counts_within_a_cell)
+{
+	struct cell_info info = { .mask = CELL_INFO_IDS_FIRST };
+	int net, old, r;
+
+	unlink(TUN_NODE);
+	ASSERT_EQ(mknod(TUN_NODE, S_IFCHR | 0666, makedev(10, 200)), 0);
+	r = open(TUN_NODE, O_RDWR | O_CLOEXEC);
+	if (r < 0) {
+		unlink(TUN_NODE);
+		SKIP(return, "no tun");
+	}
+	close(r);
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true, .tun = true,
+							.net_str = "new",
+							.caps = CAP(NET_ADMIN) | CAP(SETUID) |
+								CAP(SETGID),
+							.net_rights = CELL_NET_TUN }), 0);
+	ASSERT_EQ(cell_info(self->c.cfd, &info, sizeof(info), 0), 0);
+	ASSERT_EQ(info.mask, CELL_INFO_IDS_FIRST);
+	net = cell_open(self->c.cfd, CELL_OPEN_SLOT, "net", NULL, 0);
+	ASSERT_GE(net, 0);
+	old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(old, 0);
+	ASSERT_EQ(setns(net, CLONE_NEWNET), 0);
+	r = make_tap("taph", (long long)info.ids_first + 1000, -1);
+	ASSERT_EQ(setns(old, CLONE_NEWNET), 0);
+	close(old);
+	close(net);
+	unlink(TUN_NODE);
+	ASSERT_EQ(r, 0);
+	ASSERT_EQ(seal(self->c.cfd), 0);
+	EXPECT_EQ(run_sealed(&self->c, "tun-owner-within", NULL), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_slots_test.c b/tools/testing/selftests/cell/cell_slots_test.c
new file mode 100644
index 000000000000..58e57e8c1642
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_slots_test.c
@@ -0,0 +1,748 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for cells, the slots: what may fill them, who may fill them, and what
+ * sealing makes of them.
+ */
+#include "cell_test.h"
+
+TEST_F(cell, tree_must_be_for_the_cell)
+{
+	/* A writable tmpfs of the host's, not idmapped. */
+	EXPECT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_idmap = true }), 18);
+	EXPECT_EQ(errno, EOVERFLOW);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_BUILDING);
+}
+
+TEST_F(cell, nothing_propagates_in)
+{
+	char path[64], line[512];
+	pid_t pid;
+	int pidfd;
+	FILE *f;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .shared = true }), 0);
+	pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+
+	snprintf(path, sizeof(path), "/proc/%d/mountinfo", pid);
+	f = fopen(path, "r");
+	ASSERT_NE(f, NULL);
+	while (fgets(line, sizeof(line), f)) {
+		EXPECT_EQ(strstr(line, "shared:"), NULL) TH_LOG("%s", line);
+		EXPECT_EQ(strstr(line, "master:"), NULL) TH_LOG("%s", line);
+	}
+	fclose(f);
+	close(pidfd);
+}
+
+/* Making a mount namespace a cell's takes CAP_SYS_ADMIN over its owner. */
+/*
+ * Somebody who is still in the mount namespace puts another tree in the place of
+ * the one that is there, and leaves, while the cell is being sealed. The tree that
+ * the seal has looked at is the one that the tasks of the cell are in.
+ */
+TEST_F(cell, root_is_what_the_seal_looked_at)
+{
+	struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP };
+	int other, fd, p, i, r = -1, go[2], status;
+	pid_t pid;
+	char c;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+							.keep_root = true }), 0);
+	other = new_tmpfs();
+	ASSERT_GE(other, 0);
+	ASSERT_EQ(copy_child(other, "cell_child"), 0);
+	fd = openat(other, "other", O_CREAT | O_WRONLY, 0644);
+	ASSERT_GE(fd, 0);
+	close(fd);
+	attr.userns_fd = self->c.cfd;
+	ASSERT_EQ(sys_mount_setattr(other, "", AT_EMPTY_PATH, &attr, sizeof(attr)), 0);
+
+	ASSERT_EQ(pipe2(go, O_CLOEXEC), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		if (setns(self->c.rootfd, CLONE_NEWNS))
+			_exit(1);
+		if (read(go[0], &c, 1) != 1)
+			_exit(2);
+		usleep(100000);
+		if (umount2("/", MNT_DETACH))
+			_exit(3);
+		/* Once more, for the root of the namespace that this has laid bare. */
+		if (setns(self->c.rootfd, CLONE_NEWNS))
+			_exit(4);
+		if (sys_move_mount(other, "", AT_FDCWD, "/", MOVE_MOUNT_F_EMPTY_PATH))
+			_exit(5);
+		_exit(0);
+	}
+	usleep(50000);
+	ASSERT_EQ(write(go[1], "g", 1), 1);
+	for (i = 0; i < 200; i++) {
+		r = cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0);
+		if (!r || errno != EBUSY)
+			break;
+		usleep(10000);
+	}
+	EXPECT_EQ(r, 0);
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+
+	p = spawn(self->c.cfd, NULL, "exists", "/other", NULL);
+	ASSERT_GE(p, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(p);
+	close(other);
+}
+
+TEST_F(cell, root_takes_permission)
+{
+	int nsfd, status;
+	pid_t pid;
+
+	nsfd = sys_open_tree(AT_FDCWD, "/tmp", OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+	ASSERT_GE(nsfd, 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		int cfd;
+
+		if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+			_exit(1);
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0)
+			_exit(2);
+		if (cell_configure(cfd, CELL_SET_FD, "root", NULL, nsfd) != -1 || errno != EPERM)
+			_exit(3);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	close(nsfd);
+}
+
+/*
+ * Sharing a network namespace takes CAP_SYS_ADMIN over its owner, which is half
+ * of what setns() asks for. The other half is about the namespaces of the
+ * caller, and those do not change. So a caller without any privilege can share
+ * what a child of its made in a user namespace of its own, and cannot enter it.
+ */
+TEST_F(cell, net_takes_half_of_setns)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		int ready[2], hold[2], cfd, nsfd, host;
+		char path[64], c;
+		pid_t child;
+
+		host = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+		if (host < 0 || setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+			_exit(1);
+		/* /proc/<pid>/ns of the child is gated by ptrace access. */
+		if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) || pipe(ready) || pipe(hold))
+			_exit(2);
+		child = fork();
+		if (child < 0)
+			_exit(3);
+		if (!child) {
+			close(hold[1]);
+			if (unshare(CLONE_NEWUSER | CLONE_NEWNET) || write(ready[1], "r", 1) != 1)
+				_exit(1);
+			_exit(read(hold[0], &c, 1));
+		}
+		if (read(ready[0], &c, 1) != 1)
+			_exit(4);
+		snprintf(path, sizeof(path), "/proc/%d/ns/net", child);
+		nsfd = open(path, O_RDONLY | O_CLOEXEC);
+		if (nsfd < 0)
+			_exit(5);
+		if (setns(nsfd, CLONE_NEWNET) != -1 || errno != EPERM)
+			_exit(6);
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0)
+			_exit(7);
+		/* Not the host's, over whose owner it has nothing. */
+		if (cell_configure(cfd, CELL_SET_FD, "net", NULL, host) != -1 || errno != EPERM)
+			_exit(8);
+		if (cell_configure(cfd, CELL_SET_FD, "net", NULL, nsfd))
+			_exit(9);
+		close(hold[1]);
+		_exit(waitpid(child, &status, 0) == child && !status ? 0 : 10);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+/* A network namespace of the caller's own making, with lo up. */
+static int new_netns(void)
+{
+	struct ifreq ifr = { .ifr_name = "lo", .ifr_flags = IFF_UP };
+	int old, new = -1, sock;
+
+	old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+	if (old < 0 || unshare(CLONE_NEWNET))
+		return -1;
+	sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+	if (sock >= 0 && !ioctl(sock, SIOCSIFFLAGS, &ifr))
+		new = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+	close(sock);
+	if (setns(old, CLONE_NEWNET))
+		return -1;
+	close(old);
+	return new;
+}
+
+/*
+ * A pod is several cells and not one. Each has a tree, ids and a budget of its
+ * own. What they share is filled in from the same file descriptors: a network
+ * namespace that the builder made and still owns, and a volume. One that shows
+ * up later is one more cell. Nothing enters anything.
+ */
+TEST_F(cell, pod)
+{
+	int net, vol, vol_a, vol_b, old, ready[2], pidfd;
+	struct cellctx b;
+	char c;
+
+	net = new_netns();
+	ASSERT_GE(net, 0);
+	vol = new_tmpfs();
+	ASSERT_GE(vol, 0);
+	vol_a = sys_open_tree(vol, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+	vol_b = sys_open_tree(vol, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+	ASSERT_GE(vol_a, 0);
+	ASSERT_GE(vol_b, 0);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .net = net,
+							.data = vol_a }), 0);
+	ASSERT_EQ(pipe2(ready, O_CLOEXEC), 0);
+	pidfd = prepare(self->c.cfd, "pod-serve", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, ready[1]), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	close(pidfd);
+	close(ready[1]);
+	ASSERT_EQ(read(ready[0], &c, 1), 1);
+
+	/* The second one is made while the first is running. */
+	ASSERT_EQ(cell_build(&b, &(struct build){ .ids = 65536, .net = net,
+						  .data = vol_b }), 0);
+
+	/* The builder owns the network namespace, so it can still get in there. */
+	old = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(old, 0);
+	EXPECT_EQ(setns(net, CLONE_NEWNET), 0);
+	ASSERT_EQ(setns(old, CLONE_NEWNET), 0);
+	close(old);
+
+	pidfd = spawn(b.cfd, NULL, "pod-connect", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	close(pidfd);
+	EXPECT_EQ(cell_wait(b.cfd), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+
+	cell_cleanup(&b);
+	close(ready[0]);
+	close(net);
+	close(vol);
+}
+
+/* The budget is judged against whoever fills the slot. */
+TEST_F(cell, budget_takes_permission)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(mkdir(CGROOT "/notyours", 0755), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		int cfd, cg;
+
+		if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000))
+			_exit(1);
+		cfd = cell_create(CELL_SELF, 0);
+		if (cfd < 0)
+			_exit(2);
+		cg = open(CGROOT "/notyours", O_RDONLY | O_DIRECTORY);
+		if (cg < 0)
+			_exit(3);
+		if (cell_configure(cfd, CELL_SET_FD, "budget", NULL, cg) != -1 ||
+		    (errno != EPERM && errno != EACCES))
+			_exit(4);
+		_exit(0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	rmdir(CGROOT "/notyours");
+}
+
+/* A kill takes everything in the budget with it, so a budget is one cell's. */
+TEST_F(cell, budget_is_exclusive)
+{
+	char path[192], buf[32];
+	int cfd, cg, status;
+	struct stat st;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "budget", NULL, self->c.cgfd), -1);
+	EXPECT_EQ(errno, EBUSY);
+
+	/* Nor one that lies within. */
+	snprintf(path, sizeof(path), "%s/sub", self->c.cg);
+	ASSERT_EQ(mkdir(path, 0755), 0);
+	cg = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(cg, 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "budget", NULL, cg), -1);
+	EXPECT_EQ(errno, EBUSY);
+	close(cg);
+
+	/* Nor one that somebody is in. */
+	ASSERT_EQ(mkdir(CGROOT "/busy", 0755), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		pause();
+		_exit(0);
+	}
+	snprintf(buf, sizeof(buf), "%d", pid);
+	ASSERT_EQ(write_file(CGROOT "/busy/cgroup.procs", buf), 0);
+	cg = open(CGROOT "/busy", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(cg, 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_FD, "budget", NULL, cg), -1);
+	EXPECT_EQ(errno, EBUSY);
+	kill(pid, SIGKILL);
+	waitpid(pid, &status, 0);
+	close(cg);
+	close(cfd);
+	rmdir(CGROOT "/busy");
+
+	/* It comes back when the cell is gone, with nothing of the cell's left. */
+	ASSERT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	ASSERT_EQ(fstat(self->c.cgfd, &st), 0);
+	EXPECT_EQ(st.st_uid, 0);
+	ASSERT_EQ(fstatat(self->c.cgfd, "cgroup.procs", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, 0);
+	EXPECT_EQ(access(path, F_OK), -1);
+}
+
+TEST_F(cell, budget_is_handed_over)
+{
+	struct stat st;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(fstat(self->c.cgfd, &st), 0);
+	EXPECT_GE(st.st_uid, CELL_WINDOW_FIRST);
+	ASSERT_EQ(fstatat(self->c.cgfd, "cgroup.procs", &st, 0), 0);
+	EXPECT_GE(st.st_uid, CELL_WINDOW_FIRST);
+	/* Limits stay with whoever is above. */
+	ASSERT_EQ(fstatat(self->c.cgfd, "cgroup.max.depth", &st, 0), 0);
+	EXPECT_EQ(st.st_uid, 0);
+}
+
+/* Who the budget and what is handed over with it belong to, or -1 if not to one. */
+static long budget_owner(const struct cellctx *c)
+{
+	static const char * const names[] = {
+		".", "cgroup.procs", "cgroup.threads", "cgroup.subtree_control",
+	};
+	struct stat st;
+	long uid = -1;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(names); i++) {
+		if (fstatat(c->cgfd, names[i], &st, 0))
+			return -2;
+		if (i && (long)st.st_uid != uid)
+			return -1;
+		uid = st.st_uid;
+	}
+	return uid;
+}
+
+#define FAIL_NTH	"/proc/thread-self/fail-nth"
+
+/*
+ * Have one allocation after the other fail in a seal. It leaves the budget with
+ * whoever it belongs to, all of it, and the cell to be sealed another time.
+ */
+TEST_F_TIMEOUT(cell, seal_that_runs_out_of_memory, 900)
+{
+	char nth[16], left[16] = {};
+	int n, r, e, fd, failed = 0;
+
+	mkdir("/tmp/cell_test.debugfs", 0700);
+	mount("none", "/tmp/cell_test.debugfs", "debugfs", 0, NULL);
+	r = write_file("/tmp/cell_test.debugfs/failslab/ignore-gfp-wait", "N");
+	umount2("/tmp/cell_test.debugfs", MNT_DETACH);
+	rmdir("/tmp/cell_test.debugfs");
+	/* Opening it takes memory as well, so that is done with for good. */
+	fd = open(FAIL_NTH, O_RDWR | O_CLOEXEC);
+	if (r || fd < 0)
+		SKIP(return, "no fault injection");
+
+	for (n = 1; n < 2000; n++) {
+		ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .no_seal = true,
+								.net_str = "new" }), 0);
+		ASSERT_EQ(budget_owner(&self->c), 0);
+
+		snprintf(nth, sizeof(nth), "%d", n);
+		ASSERT_GT(pwrite(fd, nth, strlen(nth), 0), 0);
+		r = cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0);
+		e = errno;
+		ASSERT_GT(pread(fd, left, sizeof(left) - 1, 0), 0);
+		ASSERT_GT(pwrite(fd, "0", 1, 0), 0);
+
+		if (r) {
+			failed++;
+			EXPECT_EQ(e, ENOMEM) TH_LOG("allocation %d", n);
+			EXPECT_EQ(budget_owner(&self->c), 0) TH_LOG("allocation %d", n);
+			EXPECT_EQ(cell_state(self->c.cfd), CELL_BUILDING);
+			EXPECT_EQ(cell_configure(self->c.cfd, CELL_CMD_SEAL, NULL, NULL, 0), 0)
+				TH_LOG("allocation %d", n);
+		}
+		EXPECT_GE(budget_owner(&self->c), (long)CELL_WINDOW_FIRST)
+			TH_LOG("allocation %d", n);
+		cell_cleanup(&self->c);
+		self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1;
+		self->c.rootfd = self->c.layers = -1;
+		/* It got through without reaching the one that was to fail. */
+		if (!r && atoi(left) > 0)
+			break;
+	}
+	close(fd);
+	TH_LOG("%d allocations, %d failed seals", n - 1, failed);
+	EXPECT_GT(failed, 4);
+	EXPECT_LT(n, 2000);
+}
+
+TEST_F(cell, stays_in_its_budget)
+{
+	char buf[32];
+	pid_t pid;
+	int pidfd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = spawn(self->c.cfd, &pid, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+
+	snprintf(buf, sizeof(buf), "%d", pid);
+	EXPECT_EQ(write_file(CGROOT "/cgroup.procs", buf), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(pidfd);
+}
+
+/* Nothing but a spawn gets a task into a budget. */
+TEST_F(cell, nothing_enters_the_budget)
+{
+	struct cell_clone_args ca = { .flags = CLONE_INTO_CGROUP, .exit_signal = SIGCHLD };
+	char path[192], buf[32];
+	int pidfd, vhost;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+
+	snprintf(path, sizeof(path), "%s/cgroup.procs", self->c.cg);
+	snprintf(buf, sizeof(buf), "%d", getpid());
+	EXPECT_EQ(write_file(path, buf), -1);
+	EXPECT_EQ(errno, EPERM);
+
+	ca.cgroup = self->c.cgfd;
+	pid = syscall(SYS_clone3, &ca, sizeof(ca));
+	if (!pid)
+		_exit(0);
+	EXPECT_EQ(pid, -1);
+	EXPECT_EQ(errno, EPERM);
+
+	/* Nor the kernel on behalf of a task of the cell. */
+	vhost = open("/dev/vhost-net", O_RDWR | O_CLOEXEC);
+	if (vhost < 0) {
+		TH_LOG("no /dev/vhost-net, that part is skipped");
+		return;
+	}
+	pidfd = prepare(self->c.cfd, "vhost", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, vhost), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	/* With the device still open out here, the cell gets to be DEAD. */
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	EXPECT_EQ(cg_populated(self->c.cg), 0);
+	close(vhost);
+	close(pidfd);
+}
+
+/*
+ * Nobody in particular builds a cell. The mount work takes a user namespace and
+ * a mount namespace of one's own, and nothing else.
+ */
+/*
+ * A task of a cell changes nothing in cgroupfs outside of its budget, even if the
+ * builder gave it all of cgroupfs and somebody left a cgroup open to anybody.
+ * Enabling a controller makes files in the children that belong to whoever asked,
+ * and nothing asks whether their id can be stored: it would outlive the cell.
+ */
+TEST_F(cell, changes_nothing_outside_its_budget)
+{
+	struct stat st;
+	int pidfd;
+	char c = 0;
+
+	write_file(CGROOT "/cgroup.subtree_control", "+pids");
+	ASSERT_EQ(mkdir(CGROOT "/open", 0777), 0);
+	ASSERT_EQ(chmod(CGROOT "/open", 0777), 0);
+	ASSERT_EQ(chmod(CGROOT "/open/cgroup.subtree_control", 0666), 0);
+	ASSERT_EQ(chmod(CGROOT "/open/cgroup.procs", 0666), 0);
+	chmod(CGROOT "/open/pids.max", 0666);
+	ASSERT_EQ(mkdir(CGROOT "/open/kid", 0755), 0);
+	ASSERT_EQ(mkdir(CGROOT "/open/empty", 0755), 0);
+	ASSERT_EQ(setxattr(CGROOT "/open", "user.host", "h", 1, 0), 0);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .cgroupfs = true }), 0);
+	pidfd = spawn(self->c.cfd, NULL, "outside", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+
+	/* Nothing was made, so nothing is left that is owned by an id of the cell. */
+	EXPECT_EQ(stat(CGROOT "/open/kid/pids.max", &st), -1);
+	EXPECT_EQ(errno, ENOENT);
+	EXPECT_EQ(stat(CGROOT "/open/empty", &st), 0);
+	EXPECT_EQ(getxattr(CGROOT "/open", "user.host", &c, 1), 1);
+	EXPECT_EQ(c, 'h');
+	EXPECT_EQ(getxattr(CGROOT "/open", "user.cell", &c, 1), -1);
+
+	write_file(CGROOT "/open/cgroup.subtree_control", "-pids");
+	rmdir(CGROOT "/open/kid");
+	rmdir(CGROOT "/open/empty");
+	rmdir(CGROOT "/open");
+}
+
+#define V1ROOT	"/tmp/cell-test-v1"
+
+TEST_F(cell, changes_nothing_on_a_legacy_hierarchy)
+{
+	struct stat st;
+	int pidfd;
+
+	mkdir(V1ROOT, 0755);
+	if (mount("none", V1ROOT, "cgroup", 0, "none,name=celltest")) {
+		rmdir(V1ROOT);
+		SKIP(return, "no legacy hierarchies");
+	}
+	ASSERT_EQ(mkdir(V1ROOT "/open", 0777), 0);
+	ASSERT_EQ(chmod(V1ROOT "/open", 0777), 0);
+	ASSERT_EQ(chmod(V1ROOT "/open/cgroup.procs", 0666), 0);
+	ASSERT_EQ(chmod(V1ROOT "/open/notify_on_release", 0666), 0);
+	ASSERT_EQ(mkdir(V1ROOT "/open/empty", 0777), 0);
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .bind = V1ROOT }), 0);
+	pidfd = spawn(self->c.cfd, NULL, "legacy", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+	EXPECT_EQ(stat(V1ROOT "/open/empty", &st), 0);
+
+	cell_cleanup(&self->c);
+	self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1;
+	rmdir(V1ROOT "/open/other");
+	rmdir(V1ROOT "/open/empty");
+	EXPECT_EQ(rmdir(V1ROOT "/open"), 0);
+	EXPECT_EQ(umount(V1ROOT), 0);
+	rmdir(V1ROOT);
+}
+
+/* Nor does it help to own the cgroup, as a cell of one id does what its maker owns. */
+TEST_F(cell, changes_nothing_outside_its_budget_that_it_owns)
+{
+	struct stat st;
+	int status;
+	pid_t pid;
+	char c = 0;
+	bool v1;
+
+	ASSERT_EQ(chown(CGROOT, 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0);
+	ASSERT_EQ(mkdir(CGROOT "/theirs", 0755), 0);
+	ASSERT_EQ(mkdir(CGROOT "/theirs/empty", 0755), 0);
+	ASSERT_EQ(chown(CGROOT "/theirs", 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/theirs/empty", 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/theirs/cgroup.procs", 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/theirs/cgroup.max.depth", 1000, 1000), 0);
+	ASSERT_EQ(setxattr(CGROOT "/theirs", "user.host", "h", 1, 0), 0);
+	mkdir(V1ROOT, 0755);
+	v1 = !mount("none", V1ROOT, "cgroup", 0, "none,name=celltest");
+	if (v1) {
+		ASSERT_EQ(mkdir(V1ROOT "/theirs", 0755), 0);
+		ASSERT_EQ(mkdir(V1ROOT "/theirs/empty", 0755), 0);
+		ASSERT_EQ(chown(V1ROOT "/theirs", 1000, 1000), 0);
+		ASSERT_EQ(chown(V1ROOT "/theirs/empty", 1000, 1000), 0);
+	}
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct cellctx c;
+		int step, pidfd, dir, procs, legacy;
+
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) ||
+		    prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+			_exit(2);
+		if (write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(3);
+		step = cell_build(&c, &(struct build){ .ids = 1 });
+		if (step)
+			_exit(20 + step);
+		dir = open(CGROOT "/theirs", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		/* Not cgroup.procs, which has more reasons to refuse. */
+		procs = open(CGROOT "/theirs/cgroup.max.depth", O_WRONLY | O_CLOEXEC);
+		pidfd = prepare(c.cfd, "outside-own", NULL, NULL);
+		if (dir < 0 || procs < 0 || pidfd < 0 ||
+		    pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, dir) ||
+		    pidfd_configure(pidfd, PIDFD_SET_FD, "4", NULL, procs))
+			_exit(4);
+		legacy = open(V1ROOT "/theirs", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (v1 && (legacy < 0 ||
+			   pidfd_configure(pidfd, PIDFD_SET_FD, "5", NULL, legacy)))
+			_exit(6);
+		if (pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0))
+			_exit(7);
+		_exit(cell_wait(c.cfd) ? 5 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+
+	EXPECT_EQ(stat(CGROOT "/theirs", &st), 0);
+	EXPECT_EQ(st.st_mode & 07777, 0755);
+	EXPECT_EQ(stat(CGROOT "/theirs/empty", &st), 0);
+	EXPECT_EQ(getxattr(CGROOT "/theirs", "user.host", &c, 1), 1);
+	EXPECT_EQ(getxattr(CGROOT "/theirs", "user.cell", &c, 1), -1);
+
+	if (v1) {
+		EXPECT_EQ(stat(V1ROOT "/theirs/empty", &st), 0);
+		rmdir(V1ROOT "/theirs/other");
+		rmdir(V1ROOT "/theirs/mine");
+		rmdir(V1ROOT "/theirs/empty");
+		EXPECT_EQ(rmdir(V1ROOT "/theirs"), 0);
+		EXPECT_EQ(umount(V1ROOT), 0);
+	}
+	rmdir(V1ROOT);
+	rmdir(CGROOT "/theirs/mine");
+	rmdir(CGROOT "/theirs/empty");
+	EXPECT_EQ(rmdir(CGROOT "/theirs"), 0);
+	EXPECT_EQ(chown(CGROOT, 0, 0), 0);
+	EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0);
+}
+
+TEST_F(cell, unprivileged_builder)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(chown(CGROOT, 1000, 1000), 0);
+	ASSERT_EQ(chown(CGROOT "/cgroup.procs", 1000, 1000), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct cellctx c;
+		int step;
+
+		/* Dumpable, or /proc/self is root's. */
+		if (setgroups(0, NULL) || setresgid(1000, 1000, 1000) ||
+		    setresuid(1000, 1000, 1000) ||
+		    prctl(PR_SET_DUMPABLE, 1, 0, 0, 0))
+			_exit(1);
+		if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+			_exit(2);
+		if (write_file("/proc/self/setgroups", "deny") ||
+		    write_file("/proc/self/uid_map", "1000 1000 1") ||
+		    write_file("/proc/self/gid_map", "1000 1000 1"))
+			_exit(3);
+		step = cell_build(&c, &(struct build){ .ids = 1 });
+		if (step)
+			_exit(20 + step);
+		if (spawn(c.cfd, NULL, "exit", "0", NULL) < 0)
+			_exit(4);
+		_exit(cell_wait(c.cfd) ? 5 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	EXPECT_EQ(chown(CGROOT, 0, 0), 0);
+	EXPECT_EQ(chown(CGROOT "/cgroup.procs", 0, 0), 0);
+}
+
+/* What the "caps" slot takes. */
+TEST_F(cell, caps_slot)
+{
+	struct cell_info info = { .mask = CELL_INFO_CAPS };
+	__u64 caps[2] = { CAP(CHOWN) | CAP(KILL), 0 };
+	char buf[16] = {};
+	int cfd, fd, last;
+
+	fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	ASSERT_GT(read(fd, buf, sizeof(buf) - 1), 0);
+	close(fd);
+	last = atoi(buf);
+	ASSERT_LT(last, 63);
+
+	cfd = cell_create(CELL_SELF, 0);
+	ASSERT_GE(cfd, 0);
+	ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0);
+	EXPECT_EQ(info.mask & CELL_INFO_CAPS, CELL_INFO_CAPS);
+	EXPECT_EQ(info.caps, 0);
+
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", caps, 4), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", caps, 16), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", (void *)1, 8), -1);
+	EXPECT_EQ(errno, EFAULT);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_STRING, "caps", "chown", 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	/* Nothing a cell writes imparts privilege, whatever the builder thinks of it. */
+	caps[1] = caps[0] | CAP(SETFCAP);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1);
+	EXPECT_EQ(errno, EINVAL);
+	/* There is no such capability. */
+	caps[1] = caps[0] | 2ULL << last;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1);
+	EXPECT_EQ(errno, EINVAL);
+	caps[1] = caps[0] | 1ULL << 63;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1);
+	EXPECT_EQ(errno, EINVAL);
+	ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0);
+	EXPECT_EQ(info.caps, 0);
+
+	/* The last word counts. */
+	caps[1] = 1ULL << last;
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", &caps[1], 8), 0);
+	EXPECT_EQ(cell_configure(cfd, CELL_SET_BINARY, "caps", caps, 8), 0);
+	ASSERT_EQ(cell_info(cfd, &info, sizeof(info), 0), 0);
+	EXPECT_EQ(info.caps, caps[0]);
+	close(cfd);
+
+	/* Until the seal. */
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536, .caps = caps[0] }), 0);
+	caps[1] = 0;
+	EXPECT_EQ(cell_configure(self->c.cfd, CELL_SET_BINARY, "caps", &caps[1], 8), -1);
+	EXPECT_EQ(errno, EBUSY);
+	ASSERT_EQ(cell_info(self->c.cfd, &info, sizeof(info), 0), 0);
+	EXPECT_EQ(info.caps, caps[0]);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_spawn_test.c b/tools/testing/selftests/cell/cell_spawn_test.c
new file mode 100644
index 000000000000..e5f0250be4db
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_spawn_test.c
@@ -0,0 +1,559 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for cells, spawning: empty pidfds, and what a new task does and does
+ * not start out with.
+ */
+#include "cell_test.h"
+
+TEST_F(cell, nothing_inherited)
+{
+	/* Things a fork would have passed on. */
+	int junk = open("/etc/passwd", O_RDONLY);
+
+	ASSERT_GE(junk, 0);
+	ASSERT_EQ(chdir("/etc"), 0);
+	setenv("SECRET", "hunter2", 1);
+	RUN_IN_CELL("basic");
+	close(junk);
+}
+
+/* What pidfd_open() takes with PIDFD_EMPTY. */
+TEST_F(cell, empty_pidfd_open)
+{
+	int fd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+
+	/* Outside of a cell it makes sense, and is not done yet. */
+	EXPECT_EQ(pidfd_open_empty(-EBADF, 0), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+	EXPECT_EQ(pidfd_open_empty(-1, 0), -1);
+	EXPECT_EQ(errno, EBADF);
+	fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(pidfd_open_empty(fd, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	close(fd);
+	EXPECT_EQ(pidfd_open_empty(self->c.cfd, O_EXCL /* PIDFD_THREAD */), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	fd = pidfd_open_empty(self->c.cfd, 0);
+	ASSERT_GE(fd, 0);
+	EXPECT_EQ(fcntl(fd, F_GETFD), FD_CLOEXEC);
+	close(fd);
+	/* Nothing came of it, and the cell is none the worse. */
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+}
+
+/* The "Pid:" line of the fdinfo of a pidfd, or -2. */
+static int pidfd_fdinfo_pid(int pidfd)
+{
+	char path[64], line[128];
+	int nr = -2;
+	FILE *f;
+
+	snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", pidfd);
+	f = fopen(path, "r");
+	if (!f)
+		return -2;
+	while (fgets(line, sizeof(line), f)) {
+		if (sscanf(line, "Pid:\t%d", &nr) == 1)
+			break;
+	}
+	fclose(f);
+	return nr;
+}
+
+/* It is a pidfd like any other. There just is no task yet. */
+TEST_F(cell, empty_pidfd)
+{
+	struct pidfd_info_v0 info = {};
+	struct stat st, st_other;
+	struct pollfd pfd;
+	int pidfd, other;
+	siginfo_t si;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+
+	pidfd = prepare(self->c.cfd, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+
+	/* Nothing exists, and nothing has happened to the cell. */
+	EXPECT_EQ(cg_populated(self->c.cg), 0);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+
+	/* Whatever it is passed to finds no task. */
+	EXPECT_EQ(syscall(SYS_pidfd_send_signal, pidfd, 0, NULL, 0), -1);
+	EXPECT_EQ(errno, ESRCH);
+	EXPECT_EQ(syscall(SYS_pidfd_getfd, pidfd, 0, 0), -1);
+	EXPECT_EQ(errno, ESRCH);
+	EXPECT_EQ(ioctl(pidfd, PIDFD_GET_INFO_V0, &info), -1);
+	EXPECT_EQ(errno, ESRCH);
+	/* Nor the record of an exit: nothing has exited. */
+	info.mask = PIDFD_INFO_EXIT_V0;
+	EXPECT_EQ(ioctl(pidfd, PIDFD_GET_INFO_V0, &info), -1);
+	EXPECT_EQ(errno, ESRCH);
+	info.mask = 0;
+	/* -1 is for a task that is gone. */
+	EXPECT_EQ(pidfd_fdinfo_pid(pidfd), 0);
+	EXPECT_EQ(setns(pidfd, CLONE_NEWNET), -1);
+	EXPECT_EQ(errno, ESRCH);
+	EXPECT_EQ(waitid(P_PIDFD, pidfd, &si, WEXITED | WNOHANG), -1);
+	EXPECT_EQ(errno, ECHILD);
+	/* No task yet is not the same as no task any more. */
+	pfd = (struct pollfd){ .fd = pidfd, .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, 100), 0);
+	ASSERT_EQ(fstat(pidfd, &st), 0);
+
+	/* What may not be given. */
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, self->c.cfd), -1);
+	EXPECT_EQ(errno, EPERM);
+	other = open("/proc/self/ns/net", O_RDONLY);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, other), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(other);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, pidfd), -1);
+	EXPECT_EQ(errno, EPERM);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "nonsense", NULL, 0), -1);
+	EXPECT_EQ(errno, EINVAL);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", "unterminated", 12), -1);
+	EXPECT_EQ(errno, EINVAL);
+	/* An id the cell does not have. */
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred",
+				  &(struct pidfd_cred){ .uid = 70000 },
+				  sizeof(struct pidfd_cred)), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/x", 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+
+	/* Now there is one: in the cell, as the cell's root. */
+	pid = pidfd_nr(pidfd);
+	ASSERT_GT(pid, 0);
+	EXPECT_GE(host_uid(pid), (long)CELL_WINDOW_FIRST);
+	EXPECT_EQ(cg_populated(self->c.cg), 1);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING);
+	EXPECT_EQ(syscall(SYS_pidfd_send_signal, pidfd, 0, NULL, 0), 0);
+	EXPECT_EQ(poll(&pfd, 1, 100), 0);
+	EXPECT_EQ(pidfd_fdinfo_pid(pidfd), pid);
+
+	/* Another pidfd on it is the same inode, which did not change either. */
+	other = sys_pidfd_open(pid, 0);
+	ASSERT_GE(other, 0);
+	ASSERT_EQ(fstat(other, &st_other), 0);
+	EXPECT_EQ(st.st_ino, st_other.st_ino);
+	ASSERT_EQ(fstat(pidfd, &st_other), 0);
+	EXPECT_EQ(st.st_ino, st_other.st_ino);
+	/* But only the file pidfd_open() returned for PIDFD_EMPTY configures. */
+	EXPECT_EQ(pidfd_configure(other, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EPERM);
+	close(other);
+	close(pidfd);
+
+	/* A second one, into the running cell. */
+	pidfd = spawn(self->c.cfd, NULL, "second", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(task_wait(pidfd), 0);
+	close(pidfd);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_RUNNING);
+}
+
+/*
+ * An empty pidfd reserves nothing in the cell. Whichever is started first is
+ * the init, and one that is left empty keeps the cell from nothing.
+ */
+TEST_F(cell, empty_pidfd_reserves_nothing)
+{
+	int a, b, c;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+
+	/* "basic" insists on being pid 1, "second" on not being it. */
+	a = prepare(self->c.cfd, "second", NULL, NULL);
+	b = prepare(self->c.cfd, "pause", NULL, NULL);
+	c = prepare(self->c.cfd, "pause", NULL, NULL);
+	ASSERT_GE(a, 0);
+	ASSERT_GE(b, 0);
+	ASSERT_GE(c, 0);
+	ASSERT_EQ(pidfd_configure(b, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	ASSERT_EQ(pidfd_configure(a, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_EQ(task_wait(a), 0);
+
+	/* The cell dies with @c still there, and then nothing comes of @c. */
+	ASSERT_EQ(cell_kill(self->c.cfd, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL);
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_DEAD);
+	EXPECT_EQ(pidfd_configure(c, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, ESRCH);
+	close(a);
+	close(b);
+	close(c);
+}
+
+/*
+ * A start that fails before there is a task. The pidfd then says that there is
+ * none and never will be, every way it can be asked, and the cell is as it was.
+ */
+TEST_F(cell, start_fails_without_a_task)
+{
+	struct pidfd_info_v0 info = { .mask = PIDFD_INFO_EXIT_V0 };
+	struct pollfd pfd;
+	char path[192];
+	int pidfd;
+
+	write_file(CGROOT "/cgroup.subtree_control", "+pids");
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	snprintf(path, sizeof(path), "%s/pids.max", self->c.cg);
+	if (write_file(path, "0")) {
+		TH_LOG("no pids controller, skipped");
+		return;
+	}
+
+	pidfd = prepare(self->c.cfd, "pause", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EAGAIN);
+	EXPECT_EQ(cg_populated(self->c.cg), 0);
+
+	/* It can be tried only once. */
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, EBUSY);
+	pfd = (struct pollfd){ .fd = pidfd, .events = POLLIN };
+	EXPECT_EQ(poll(&pfd, 1, 1000), 1);
+	EXPECT_EQ(ioctl(pidfd, PIDFD_GET_INFO_V0, &info), -1);
+	EXPECT_EQ(errno, ESRCH);
+	EXPECT_EQ(pidfd_fdinfo_pid(pidfd), -1);
+	EXPECT_EQ(syscall(SYS_pidfd_send_signal, pidfd, 0, NULL, 0), -1);
+	EXPECT_EQ(errno, ESRCH);
+	close(pidfd);
+
+	/* Nothing ever ran, so the next one is the init of the cell. */
+	EXPECT_EQ(cell_state(self->c.cfd), CELL_SEALED);
+	ASSERT_EQ(write_file(path, "max"), 0);
+	pidfd = spawn(self->c.cfd, NULL, "basic", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+}
+
+/* The pidfd may be kept for long. What was configured is not kept with it. */
+static int usr1_seen;
+
+static void on_usr1(int sig)
+{
+	__atomic_fetch_add(&usr1_seen, 1, __ATOMIC_RELAXED);
+}
+
+/*
+ * A fork is started over if a signal is pending for whoever forks, and nobody gets
+ * to see that. A start is no different, and it has not been tried by then.
+ */
+TEST_F_TIMEOUT(cell, start_is_started_over_after_a_signal, 120)
+{
+	struct sigaction sa = { .sa_handler = on_usr1, .sa_flags = SA_RESTART };
+	pid_t me = getpid(), pest;
+	int init, p, i, seen, failed = 0;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	init = spawn(self->c.cfd, NULL, "pause", NULL, NULL);
+	ASSERT_GE(init, 0);
+	ASSERT_EQ(sigaction(SIGUSR1, &sa, NULL), 0);
+	pest = fork();
+	ASSERT_GE(pest, 0);
+	if (!pest) {
+		for (;;) {
+			kill(me, SIGUSR1);
+			usleep(300);
+		}
+	}
+
+	for (i = 0; i < 300; i++) {
+		p = prepare(self->c.cfd, "exit", "0", NULL);
+		ASSERT_GE(p, 0);
+		if (pidfd_configure(p, PIDFD_CMD_START, NULL, NULL, 0)) {
+			failed++;
+			TH_LOG("start %d: errno %d", i, errno);
+		}
+		close(p);
+	}
+	kill(pest, SIGKILL);
+	waitpid(pest, NULL, 0);
+	signal(SIGUSR1, SIG_DFL);
+
+	seen = __atomic_load_n(&usr1_seen, __ATOMIC_RELAXED);
+	TH_LOG("%d signals", seen);
+	EXPECT_GT(seen, 100);
+	EXPECT_EQ(failed, 0);
+	close(init);
+}
+
+TEST_F(cell, pidfd_keeps_no_file)
+{
+	int p[2], pidfd;
+	char c;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(pipe2(p, O_CLOEXEC), 0);
+	pidfd = prepare(self->c.cfd, "exit", "0", NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, p[1]), 0);
+	close(p[1]);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	/* The last writer is gone, though the pidfd is not. */
+	ASSERT_EQ(fcntl(p[0], F_SETFL, O_NONBLOCK), 0);
+	EXPECT_EQ(read(p[0], &c, 1), 0);
+	close(p[0]);
+	close(pidfd);
+}
+
+TEST_F(cell, exec_failure_is_reported)
+{
+	int pidfd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = prepare(self->c.cfd, "basic", NULL, NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/no/such/file", 0), 0);
+	EXPECT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	EXPECT_EQ(errno, ENOENT);
+	/* That was the cell's init. */
+	EXPECT_EQ(cell_wait(self->c.cfd), SIGKILL);
+	close(pidfd);
+}
+
+TEST_F(cell, cred)
+{
+	int pidfd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pidfd = prepare(self->c.cfd, "uid", "33", "44");
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_BINARY, "cred",
+				 &(struct pidfd_cred){ .uid = 33, .gid = 44 },
+				 sizeof(struct pidfd_cred)), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), 0);
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);
+	close(pidfd);
+}
+
+/* Send @fd over @sock, by SCM_RIGHTS. */
+static int send_one_fd(int sock, int fd)
+{
+	char buf[CMSG_SPACE(sizeof(int))] = {};
+	struct iovec iov = { .iov_base = "x", .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov, .msg_iovlen = 1,
+		.msg_control = buf, .msg_controllen = sizeof(buf),
+	};
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+	memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
+	return sendmsg(sock, &msg, 0) == 1 ? 0 : -1;
+}
+
+/*
+ * A socket that was to go to a task which then never started is nobody's but
+ * the host's, and goes on working as it did.
+ */
+TEST_F(cell, failed_start_leaves_a_socket_alone)
+{
+	int sp[2], pidfd;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp), 0);
+	ASSERT_EQ(send_one_fd(sp[0], sp[0]), 0);
+
+	pidfd = prepare(self->c.cfd, "exit", "0", NULL);
+	ASSERT_GE(pidfd, 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_FD, "3", NULL, sp[1]), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/no-such-file", 0), 0);
+	ASSERT_EQ(pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0), -1);
+	ASSERT_EQ(errno, ENOENT);
+	close(pidfd);
+
+	EXPECT_EQ(send_one_fd(sp[0], sp[0]), 0);
+	close(sp[0]);
+	close(sp[1]);
+}
+
+static void no_handler(int sig)
+{
+}
+
+/* Nothing is inherited: not what the spawner ignores, handles or blocks. */
+TEST_F(cell, signals_start_from_scratch)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		sigset_t set;
+
+		signal(SIGPIPE, SIG_IGN);
+		signal(SIGHUP, SIG_IGN);
+		signal(SIGCHLD, SIG_IGN);
+		signal(SIGUSR2, no_handler);
+		sigemptyset(&set);
+		sigaddset(&set, SIGUSR1);
+		sigaddset(&set, SIGTERM);
+		if (sigprocmask(SIG_BLOCK, &set, NULL) || raise(SIGUSR1))
+			_exit(1);
+		if (spawn(self->c.cfd, NULL, "signals", NULL, NULL) < 0)
+			_exit(2);
+		_exit(cell_wait(self->c.cfd) ? 3 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+/* From here on system call @nr fails with EACCES, for the caller and its issue. */
+static int seccomp_deny(int nr)
+{
+	struct sock_filter insns[] = {
+		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 1),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog prog = { .len = ARRAY_SIZE(insns), .filter = insns };
+
+	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+		return -1;
+	return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
+}
+
+/* What a confined task spawns is confined as it is itself. */
+TEST_F(cell, seccomp_filters_are_inherited)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		if (seccomp_deny(SYS_getrusage))
+			_exit(1);
+		if (spawn(self->c.cfd, NULL, "seccomp", NULL, NULL) < 0)
+			_exit(2);
+		_exit(cell_wait(self->c.cfd) ? 3 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+/*
+ * A listener on a filter stays with the spawner, outside. It can answer for the
+ * system calls of a task of the cell. It cannot put a file into it.
+ */
+TEST_F(cell, seccomp_listener_adds_no_file)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		struct sock_filter insns[] = {
+			BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
+			BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_getrusage, 0, 1),
+			BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
+			BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+		};
+		struct sock_fprog prog = { .len = ARRAY_SIZE(insns), .filter = insns };
+		struct seccomp_notif_addfd addfd = {};
+		struct seccomp_notif_resp resp = {};
+		struct seccomp_notif req = {};
+		int listener, round, task;
+		pid_t home;
+
+		if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+			_exit(1);
+		listener = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER,
+				   SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
+		if (listener < 0)
+			_exit(2);
+
+		/* First at home, where it works, then into the cell. */
+		for (round = 0; round < 2; round++) {
+			if (!round) {
+				home = fork();
+				if (home < 0)
+					_exit(3);
+				if (!home) {
+					struct rusage ru;
+
+					_exit(getrusage(RUSAGE_SELF, &ru) == -1 &&
+					      errno == EACCES ? 0 : 1);
+				}
+				task = syscall(SYS_pidfd_open, home, 0);
+			} else {
+				task = spawn(self->c.cfd, NULL, "seccomp", NULL, NULL);
+			}
+			if (task < 0)
+				_exit(4);
+
+			/* Nothing shows on a listener when the task is gone. */
+			if (wait_for(listener, task))
+				_exit(11);
+			close(task);
+			memset(&req, 0, sizeof(req));
+			if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req))
+				_exit(5);
+			addfd.id = req.id;
+			addfd.srcfd = self->c.cfd;
+			if (!round) {
+				if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd) < 0)
+					_exit(6);
+			} else if (ioctl(listener, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd) != -1 ||
+				   errno != EPERM) {
+				_exit(7);
+			}
+			resp.id = req.id;
+			resp.error = -EACCES;
+			if (ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp))
+				_exit(8);
+			if (!round && (waitpid(home, &status, 0) != home || status))
+				_exit(9);
+		}
+		_exit(cell_wait(self->c.cfd) ? 10 : 0);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+}
+
+/* The exec of an embryo is no system call. This is how a filter prevents it. */
+TEST_F(cell, seccomp_can_deny_a_start)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (!pid) {
+		if (seccomp_deny(__NR_pidfd_configure))
+			_exit(1);
+		if (spawn(self->c.cfd, NULL, "exit", "0", NULL) >= 0)
+			_exit(2);
+		_exit(errno == EACCES ? 0 : 3);
+	}
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_EQ(WIFEXITED(status) ? WEXITSTATUS(status) : -1, 0);
+	/* Nothing ever ran. */
+	EXPECT_EQ(cg_populated(self->c.cg), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/cell/cell_test.h b/tools/testing/selftests/cell/cell_test.h
new file mode 100644
index 000000000000..fdd3a55358dd
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_test.h
@@ -0,0 +1,586 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Cells: build them, run things in them, kill them, and try everything that is
+ * not supposed to work. This is what the tests share: how to build a cell, how
+ * to spawn into one, and the fixture.
+ *
+ * They need root, cgroup2 and tmpfs. cell_child must sit next to the binaries.
+ */
+#ifndef CELL_TEST_H
+#define CELL_TEST_H
+
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/auxv.h>
+#include <sys/mount.h>
+#include <grp.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <linux/capability.h>
+#include <linux/keyctl.h>
+#include <linux/filter.h>
+#include <linux/fs.h>
+#include <linux/loop.h>
+#include <linux/mount.h>
+#include <linux/seccomp.h>
+
+#include "../kselftest_harness.h"
+#include "cell_util.h"
+
+#ifndef OPEN_TREE_NAMESPACE
+#define OPEN_TREE_NAMESPACE	(1 << 1)
+#endif
+#ifndef MOUNT_ATTR_IDMAP
+#define MOUNT_ATTR_IDMAP	0x00100000
+#endif
+
+static inline int sys_fsopen(const char *fs, unsigned int flags)
+{
+	return syscall(SYS_fsopen, fs, flags);
+}
+
+static inline int sys_fsconfig(int fd, unsigned int cmd, const char *key, const void *val, int aux)
+{
+	return syscall(SYS_fsconfig, fd, cmd, key, val, aux);
+}
+
+static inline int sys_fsmount(int fd, unsigned int flags, unsigned int attr)
+{
+	return syscall(SYS_fsmount, fd, flags, attr);
+}
+
+static inline int sys_move_mount(int from, const char *fp, int to, const char *tp,
+				 unsigned int flags)
+{
+	return syscall(SYS_move_mount, from, fp, to, tp, flags);
+}
+
+static inline int sys_open_tree(int dfd, const char *path, unsigned int flags)
+{
+	return syscall(SYS_open_tree, dfd, path, flags);
+}
+
+static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags,
+			     struct mount_attr *attr, size_t size)
+{
+	return syscall(SYS_mount_setattr, dfd, path, flags, attr, size);
+}
+
+static inline int sys_pidfd_open(pid_t pid, unsigned int flags)
+{
+	return syscall(SYS_pidfd_open, pid, flags);
+}
+
+#define CGROOT	"/tmp/cell_test.cgroup"
+
+static int child_fd = -1;
+static int cg_seq;
+
+/* What goes into a cell. */
+#define CAP(x)	(1ULL << CAP_##x)
+
+struct build {
+	unsigned int	ids;		/* 0: leave alone */
+	__u64		caps;		/* likewise */
+	bool		no_idmap;	/* leave the tree as it is */
+	bool		shared;		/* make the tree a peer of something */
+	bool		no_seal;
+	int		data;		/* a detached mount to put at /data, or 0 */
+	int		net;		/* a network namespace to share, or 0 */
+	const char	*net_str;	/* or "new" */
+	__u64		net_rights;	/* CELL_NET_* */
+	bool		tun;		/* a node for /dev/net/tun, at /tun */
+	bool		keep_root;	/* leave a file descriptor on the mount namespace */
+	bool		cgroupfs;	/* all of cgroupfs at /data, not just the budget */
+	const char	*bind;		/* or this, as it is */
+	int		overlay;	/* or an overlay, see OVL_* */
+	const char	*bind_file;	/* a file of the host's, as it is, at /bound */
+};
+
+#define OVL_IDMAPPED		1	/* the overlay is idmapped for the cell, as it should be */
+#define OVL_LOWER_IDMAPPED	2	/* its lower layer is, and nothing else */
+
+struct cellctx {
+	int	cfd;
+	int	tree;		/* the detached tmpfs that becomes "/" */
+	int	raw;		/* the same filesystem as the host sees it */
+	int	cgfd;
+	int	rootfd;		/* see keep_root */
+	int	layers;		/* what an overlay is made of, as the host sees it */
+	char	cg[128];
+};
+
+static inline int copy_child(int dirfd, const char *name)
+{
+	static char buf[65536];
+	off_t off = 0;
+	ssize_t n;
+	int out;
+
+	out = openat(dirfd, name, O_CREAT | O_WRONLY, 0755);
+	if (out < 0)
+		return -1;
+	while ((n = pread(child_fd, buf, sizeof(buf), off)) > 0) {
+		if (write(out, buf, n) != n)
+			return -1;
+		off += n;
+	}
+	close(out);
+	return n;
+}
+
+static inline int new_tmpfs(void)
+{
+	int fs, mnt;
+
+	fs = sys_fsopen("tmpfs", FSOPEN_CLOEXEC);
+	if (fs < 0)
+		return -1;
+	if (sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0))
+		return -1;
+	mnt = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0);
+	close(fs);
+	return mnt;
+}
+
+/* Returns 0, or the step that failed with errno set. */
+/* VFS_CAP_REVISION_2, cap_net_raw permitted and effective. */
+static const unsigned char fscap_v2[20] = {
+	0x01, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0x00,
+};
+
+static inline int put_file(int dirfd, const char *name)
+{
+	int fd = openat(dirfd, name, O_CREAT | O_WRONLY, 0644);
+
+	if (fd < 0 || write(fd, "lower", 5) != 5)
+		return -1;
+	return close(fd);
+}
+
+/* An overlay at /data. Its layers are in a tmpfs of the host's, c->layers. */
+static inline int add_overlay(struct cellctx *c, int how)
+{
+	struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP, .userns_fd = c->cfd };
+	int lower, rw, upper, work, fs, mnt;
+
+	c->layers = new_tmpfs();
+	if (c->layers < 0 || mkdirat(c->layers, "lower", 0755) || mkdirat(c->layers, "rw", 0755) ||
+	    mkdirat(c->layers, "rw/upper", 0755) || mkdirat(c->layers, "rw/work", 0755) ||
+	    put_file(c->layers, "lower/file"))
+		return -1;
+	lower = sys_open_tree(c->layers, "lower", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+	rw = sys_open_tree(c->layers, "rw", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+	if (lower < 0 || rw < 0)
+		return -2;
+	if (how == OVL_LOWER_IDMAPPED &&
+	    sys_mount_setattr(lower, "", AT_EMPTY_PATH, &attr, sizeof(attr)))
+		return -3;
+	upper = openat(rw, "upper", O_PATH | O_DIRECTORY | O_CLOEXEC);
+	work = openat(rw, "work", O_PATH | O_DIRECTORY | O_CLOEXEC);
+	fs = sys_fsopen("overlay", FSOPEN_CLOEXEC);
+	if (fs < 0)
+		return -ENODEV;
+	if (sys_fsconfig(fs, FSCONFIG_SET_FD, "lowerdir+", NULL, lower) ||
+	    sys_fsconfig(fs, FSCONFIG_SET_FD, "upperdir", NULL, upper) ||
+	    sys_fsconfig(fs, FSCONFIG_SET_FD, "workdir", NULL, work) ||
+	    sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0))
+		return -4;
+	mnt = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0);
+	if (mnt < 0)
+		return -5;
+	if (how == OVL_IDMAPPED &&
+	    sys_mount_setattr(mnt, "", AT_EMPTY_PATH, &attr, sizeof(attr)))
+		return -6;
+	if (sys_move_mount(mnt, "", c->tree, "data", MOVE_MOUNT_F_EMPTY_PATH))
+		return -7;
+	close(mnt);
+	close(fs);
+	close(work);
+	close(upper);
+	close(rw);
+	close(lower);
+	return 0;
+}
+
+static inline int cell_build(struct cellctx *c, const struct build *b)
+{
+	struct mount_attr attr = { .attr_set = MOUNT_ATTR_IDMAP };
+	int pidns, fs, proc, nsfd;
+
+	memset(c, 0, sizeof(*c));
+	c->cfd = c->tree = c->raw = c->cgfd = c->rootfd = c->layers = -1;
+
+	snprintf(c->cg, sizeof(c->cg), CGROOT "/cell.%d.%d", getpid(), cg_seq++);
+	if (mkdir(c->cg, 0755))
+		return 1;
+	c->cgfd = open(c->cg, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	if (c->cgfd < 0)
+		return 2;
+
+	c->cfd = cell_create(CELL_SELF, 0);
+	if (c->cfd < 0)
+		return 3;
+	if (b->ids && cell_configure(c->cfd, CELL_SET_BINARY, "ids", &b->ids, sizeof(b->ids)))
+		return 4;
+	if (b->caps && cell_configure(c->cfd, CELL_SET_BINARY, "caps", &b->caps, sizeof(b->caps)))
+		return 4;
+
+	/* The tree, from scratch, one mount at a time. */
+	c->tree = new_tmpfs();
+	if (c->tree < 0)
+		return 5;
+	if (copy_child(c->tree, "cell_child") || mkdirat(c->tree, "proc", 0755) ||
+	    mkdirat(c->tree, "data", 0755) ||
+	    mkdirat(c->tree, "tmp", 01777) || fchmodat(c->tree, "tmp", 01777, 0))
+		return 6;
+	c->raw = sys_open_tree(c->tree, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+	if (c->raw < 0)
+		return 6;
+	if (b->bind_file && put_file(c->tree, "bound"))
+		return 6;
+	if (b->tun && (mknodat(c->tree, "tun", S_IFCHR | 0666, makedev(10, 200)) ||
+		       fchmodat(c->tree, "tun", 0666, 0)))
+		return 6;
+	pidns = openat(c->tree, "su", O_CREAT | O_WRONLY, 0755);
+	if (pidns < 0 || fchmod(pidns, 06755) ||
+	    fsetxattr(pidns, "security.capability", fscap_v2, sizeof(fscap_v2), 0))
+		return 6;
+	close(pidns);
+	if (!b->no_idmap && b->ids > 1) {
+		attr.userns_fd = c->cfd;
+		if (sys_mount_setattr(c->tree, "", AT_EMPTY_PATH, &attr, sizeof(attr)))
+			return 7;
+	}
+
+	pidns = cell_open(c->cfd, CELL_OPEN_SLOT, "pids", NULL, 0);
+	if (pidns < 0)
+		return 8;
+	fs = sys_fsopen("proc", FSOPEN_CLOEXEC);
+	if (fs < 0 || sys_fsconfig(fs, FSCONFIG_SET_FD, "pidns", NULL, pidns) ||
+	    sys_fsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0))
+		return 9;
+	proc = sys_fsmount(fs, FSMOUNT_CLOEXEC, 0);
+	if (proc < 0)
+		return 10;
+	if (sys_move_mount(proc, "", c->tree, "proc", MOVE_MOUNT_F_EMPTY_PATH))
+		return 11;
+	close(pidns);
+	close(fs);
+	close(proc);
+
+	if (b->data) {
+		if (b->ids > 1 &&
+		    sys_mount_setattr(b->data, "", AT_EMPTY_PATH, &attr, sizeof(attr)))
+			return 19;
+		if (sys_move_mount(b->data, "", c->tree, "data", MOVE_MOUNT_F_EMPTY_PATH))
+			return 20;
+	}
+
+	if (b->cgroupfs || b->bind) {
+		int dir = open(b->bind ?: CGROOT, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		int mnt = sys_open_tree(dir, "", AT_EMPTY_PATH | OPEN_TREE_CLONE |
+					OPEN_TREE_CLOEXEC);
+
+		if (mnt < 0 ||
+		    sys_move_mount(mnt, "", c->tree, "data", MOVE_MOUNT_F_EMPTY_PATH))
+			return 21;
+		close(mnt);
+		close(dir);
+	}
+
+	if (b->overlay && add_overlay(c, b->overlay))
+		return 22;
+
+	if (b->bind_file) {
+		int mnt = sys_open_tree(AT_FDCWD, b->bind_file,
+					OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+
+		if (mnt < 0 ||
+		    sys_move_mount(mnt, "", c->tree, "bound", MOVE_MOUNT_F_EMPTY_PATH))
+			return 23;
+		close(mnt);
+	}
+
+	if (b->shared) {
+		struct mount_attr sh = { .propagation = MS_SHARED };
+
+		if (sys_mount_setattr(c->tree, "", AT_EMPTY_PATH | AT_RECURSIVE, &sh, sizeof(sh)))
+			return 12;
+	}
+
+	nsfd = sys_open_tree(c->tree, "", AT_EMPTY_PATH | AT_RECURSIVE |
+			     OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+	if (nsfd < 0)
+		return 13;
+
+	if (cell_configure(c->cfd, CELL_SET_FD, "root", NULL, nsfd))
+		return 14;
+	if (b->keep_root)
+		c->rootfd = nsfd;
+	else
+		close(nsfd);
+	if (cell_configure(c->cfd, CELL_SET_FD, "budget", NULL, c->cgfd))
+		return 15;
+	if (cell_configure(c->cfd, CELL_SET_STRING, "hostname", "celltest", 0))
+		return 16;
+	if (b->net && cell_configure(c->cfd, CELL_SET_FD, "net", NULL, b->net))
+		return 17;
+	if (b->net_str && cell_configure(c->cfd, CELL_SET_STRING, "net", b->net_str, 0))
+		return 17;
+	if (b->net_rights && cell_configure(c->cfd, CELL_SET_BINARY, "net-rights", &b->net_rights,
+					    sizeof(b->net_rights)))
+		return 23;
+	if (!b->no_seal && cell_configure(c->cfd, CELL_CMD_SEAL, NULL, NULL, 0))
+		return 18;
+	return 0;
+}
+
+static inline void cell_cleanup(struct cellctx *c)
+{
+	int i;
+
+	if (c->cfd >= 0) {
+		cell_kill(c->cfd, 0);
+		close(c->cfd);
+	}
+	if (c->tree >= 0)
+		close(c->tree);
+	if (c->raw >= 0)
+		close(c->raw);
+	if (c->cgfd >= 0)
+		close(c->cgfd);
+	if (c->rootfd > 0)
+		close(c->rootfd);
+	if (c->layers > 0)
+		close(c->layers);
+	for (i = 0; i < 100 && rmdir(c->cg) && errno == EBUSY; i++)
+		usleep(20000);
+}
+
+/* <linux/pidfd.h> does not get along with <fcntl.h>. */
+struct pidfd_info_v0 {
+	__u64 mask, cgroupid;
+	__u32 pid, tgid, ppid, ruid, rgid, euid, egid, suid, sgid, fsuid, fsgid;
+	__s32 exit_code;
+};
+#define PIDFD_GET_INFO_V0	_IOWR(0xFF, 11, struct pidfd_info_v0)
+#define PIDFD_INFO_EXIT_V0	(1 << 3)
+
+/* The pid of what a pidfd is on, as the caller sees it, or -1. */
+static inline pid_t pidfd_nr(int pidfd)
+{
+	struct pidfd_info_v0 info = {};
+
+	if (ioctl(pidfd, PIDFD_GET_INFO_V0, &info))
+		return -1;
+	return info.pid;
+}
+
+/* argv as a block of NUL terminated strings: "cell_child\0<a>\0<b>\0<c>\0" */
+static inline int set_argv(int pidfd, const char *a, const char *b, const char *c)
+{
+	char blk[256];
+	int len = 0;
+
+	len += sprintf(blk + len, "cell_child") + 1;
+	if (a)
+		len += sprintf(blk + len, "%s", a) + 1;
+	if (b)
+		len += sprintf(blk + len, "%s", b) + 1;
+	if (c)
+		len += sprintf(blk + len, "%s", c) + 1;
+	return pidfd_configure(pidfd, PIDFD_SET_BINARY, "argv", blk, len);
+}
+
+/* There is no task, and so no pid, before it is started. */
+static inline int prepare(int cfd, const char *a, const char *b, const char *c)
+{
+	int pidfd = pidfd_open_empty(cfd, 0);
+	int null;
+
+	if (pidfd < 0)
+		return -1;
+	null = open("/dev/null", O_RDWR | O_CLOEXEC);
+	if (pidfd_configure(pidfd, PIDFD_SET_STRING, "exec", "/cell_child", 0) ||
+	    set_argv(pidfd, a, b, c) ||
+	    pidfd_configure(pidfd, PIDFD_SET_BINARY, "envp", "CELL=yes", 9) ||
+	    pidfd_configure(pidfd, PIDFD_SET_FD, "0", NULL, null) ||
+	    pidfd_configure(pidfd, PIDFD_SET_FD, "1", NULL, STDERR_FILENO) ||
+	    pidfd_configure(pidfd, PIDFD_SET_FD, "2", NULL, STDERR_FILENO)) {
+		close(pidfd);
+		return -1;
+	}
+	close(null);
+	return pidfd;
+}
+
+static inline int spawn(int cfd, pid_t *pid, const char *a, const char *b, const char *c)
+{
+	int pidfd = prepare(cfd, a, b, c);
+
+	if (pidfd >= 0 && pidfd_configure(pidfd, PIDFD_CMD_START, NULL, NULL, 0)) {
+		close(pidfd);
+		return -1;
+	}
+	if (pidfd >= 0 && pid)
+		*pid = pidfd_nr(pidfd);
+	return pidfd;
+}
+
+/*
+ * Wait for there to be something to read or to accept on @fd. Not for longer than
+ * there is the task of @pidfd to provide it: its death need not show on @fd.
+ */
+static inline int wait_for(int fd, int pidfd)
+{
+	struct pollfd p[2] = {
+		{ .fd = fd, .events = POLLIN }, { .fd = pidfd, .events = POLLIN },
+	};
+
+	if (poll(p, 2, 20000) <= 0)
+		return -1;
+	return p[0].revents & POLLIN ? 0 : -1;
+}
+
+/*
+ * How the task of @pidfd ended, or -99. A pidfd is readable once the task has
+ * exited. How it did is on record only once it has been reaped, which nobody has to
+ * do for a task that was spawned, and which is when the pidfd hangs up.
+ */
+static inline int task_wait(int pidfd)
+{
+	struct pidfd_info_v0 info = { .mask = PIDFD_INFO_EXIT_V0 };
+	struct pollfd p = { .fd = pidfd };
+
+	if (poll(&p, 1, 20000) != 1 || !(p.revents & POLLHUP))
+		return -99;
+	if (ioctl(pidfd, PIDFD_GET_INFO_V0, &info) || !(info.mask & PIDFD_INFO_EXIT_V0))
+		return -99;
+	return info.exit_code;
+}
+
+/* How the cell ended, once it is dead, or -2 if that takes too long. */
+static inline int cell_wait(int cfd)
+{
+	struct pollfd pfd = { .fd = cfd, .events = POLLIN };
+	int status = -1;
+
+	if (poll(&pfd, 1, 20000) != 1)
+		return -2;
+	if (read(cfd, &status, sizeof(status)) != sizeof(status))
+		return -1;
+	return status;
+}
+
+static inline unsigned int cell_state(int cfd)
+{
+	struct cell_info info = { .mask = CELL_INFO_STATE };
+
+	if (cell_info(cfd, &info, sizeof(info), 0) || !(info.mask & CELL_INFO_STATE))
+		return ~0U;
+	return info.state;
+}
+
+/* Not by cgroup.procs, which does not list a task that is on its way out. */
+static inline int cg_populated(const char *cg)
+{
+	char path[192], buf[256] = {};
+	int fd;
+
+	snprintf(path, sizeof(path), "%s/cgroup.events", cg);
+	fd = open(path, O_RDONLY);
+	if (fd < 0)
+		return -1;
+	if (read(fd, buf, sizeof(buf) - 1) < 0)
+		buf[0] = 0;
+	close(fd);
+	if (strstr(buf, "populated 0"))
+		return 0;
+	return strstr(buf, "populated 1") ? 1 : -1;
+}
+
+/* The kuid the host sees @pid run as. */
+static inline long host_uid(pid_t pid)
+{
+	char path[64];
+	struct stat st;
+
+	snprintf(path, sizeof(path), "/proc/%d", pid);
+	if (stat(path, &st))
+		return -1;
+	return st.st_uid;
+}
+
+static inline int write_file(const char *path, const char *val)
+{
+	int fd = open(path, O_WRONLY), ret;
+
+	if (fd < 0)
+		return -1;
+	ret = write(fd, val, strlen(val)) < 0 ? -1 : 0;
+	close(fd);
+	return ret;
+}
+
+FIXTURE(cell) {
+	struct cellctx c;
+};
+
+FIXTURE_SETUP(cell)
+{
+	char path[PATH_MAX], *slash;
+
+	if (geteuid())
+		SKIP(return, "needs root");
+	if (cell_create(-1, 0) == -1 && errno == ENOSYS)
+		SKIP(return, "no cells in this kernel");
+
+	/* Next to this binary. */
+	snprintf(path, sizeof(path) - 16, "%s", (char *)getauxval(AT_EXECFN));
+	slash = strrchr(path, '/');
+	strcpy(slash ? slash + 1 : path, "cell_child");
+	child_fd = open(path, O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(child_fd, 0);
+
+	mkdir(CGROOT, 0755);
+	if (mount("none", CGROOT, "cgroup2", 0, NULL) && errno != EBUSY)
+		SKIP(return, "no cgroup2");
+	self->c.cfd = self->c.tree = self->c.raw = self->c.cgfd = -1;
+}
+
+FIXTURE_TEARDOWN(cell)
+{
+	cell_cleanup(&self->c);
+	umount2(CGROOT, MNT_DETACH);
+	close(child_fd);
+}
+
+#define RUN_IN_CELL(what) do {							\
+	int pidfd;								\
+	ASSERT_EQ(cell_build(&self->c, &(struct build){ .ids = 65536 }), 0);	\
+	pidfd = spawn(self->c.cfd, NULL, what, NULL, NULL);			\
+	ASSERT_GE(pidfd, 0);							\
+	EXPECT_EQ(cell_wait(self->c.cfd), 0);					\
+	close(pidfd);								\
+} while (0)
+
+#endif /* CELL_TEST_H */
diff --git a/tools/testing/selftests/cell/cell_util.h b/tools/testing/selftests/cell/cell_util.h
new file mode 100644
index 000000000000..7a0946d87e5a
--- /dev/null
+++ b/tools/testing/selftests/cell/cell_util.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef CELL_UTIL_H
+#define CELL_UTIL_H
+
+#include <fcntl.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+#include <linux/types.h>
+
+#include "../../../../include/uapi/linux/cell.h"
+
+#ifndef CELL_SELF
+#define CELL_SELF		-10005
+#endif
+
+#ifndef __NR_cell_create
+#define __NR_cell_create	473
+#define __NR_cell_configure	474
+#define __NR_cell_info		475
+#define __NR_cell_open		476
+#define __NR_cell_kill		477
+#define __NR_pidfd_configure	478
+#endif
+
+/* <linux/pidfd.h> does not get along with <fcntl.h>. */
+#ifndef PIDFD_EMPTY
+#define PIDFD_EMPTY		O_NOCTTY
+
+enum pidfd_configure_cmd {
+	PIDFD_SET_STRING	= 0,
+	PIDFD_SET_BINARY	= 1,
+	PIDFD_SET_FD		= 2,
+	PIDFD_CMD_START		= 3,
+};
+
+struct pidfd_cred {
+	__u32 uid;
+	__u32 gid;
+	__u32 ngroups;
+	__u32 __spare;
+	__aligned_u64 groups;
+};
+#endif
+
+#ifndef CLONE_INTO_CGROUP
+#define CLONE_INTO_CGROUP	(1ULL << 33)
+#endif
+
+#define CELL_WINDOW_FIRST	0x80000000U
+#define CELL_WINDOW_LAST	0xfffeffffU
+
+struct cell_clone_args {
+	__aligned_u64 flags;
+	__aligned_u64 pidfd;
+	__aligned_u64 child_tid;
+	__aligned_u64 parent_tid;
+	__aligned_u64 exit_signal;
+	__aligned_u64 stack;
+	__aligned_u64 stack_size;
+	__aligned_u64 tls;
+	__aligned_u64 set_tid;
+	__aligned_u64 set_tid_size;
+	__aligned_u64 cgroup;
+};
+
+static inline int cell_create(int parent, unsigned int flags)
+{
+	return syscall(__NR_cell_create, parent, flags);
+}
+
+static inline int cell_configure(int cfd, unsigned int cmd, const char *key,
+				 const void *value, int aux)
+{
+	return syscall(__NR_cell_configure, cfd, cmd, key, value, aux);
+}
+
+static inline int cell_info(int cfd, struct cell_info *info, size_t size,
+			    unsigned int flags)
+{
+	return syscall(__NR_cell_info, cfd, info, size, flags);
+}
+
+static inline int cell_open(int cfd, unsigned int what, const char *key,
+			    const void *arg, unsigned int flags)
+{
+	return syscall(__NR_cell_open, cfd, what, key, arg, flags);
+}
+
+static inline int cell_kill(int cfd, unsigned int flags)
+{
+	return syscall(__NR_cell_kill, cfd, flags);
+}
+
+/* A pidfd that no task is attached to yet, for one in the cell @cfd. */
+static inline int pidfd_open_empty(int cfd, unsigned int flags)
+{
+	return syscall(SYS_pidfd_open, cfd, PIDFD_EMPTY | flags);
+}
+
+static inline int pidfd_configure(int pidfd, unsigned int cmd, const char *key,
+				  const void *value, int aux)
+{
+	return syscall(__NR_pidfd_configure, pidfd, cmd, key, value, aux);
+}
+
+#endif /* CELL_UTIL_H */
diff --git a/tools/testing/selftests/cell/config b/tools/testing/selftests/cell/config
new file mode 100644
index 000000000000..262912e589d6
--- /dev/null
+++ b/tools/testing/selftests/cell/config
@@ -0,0 +1,35 @@
+CONFIG_CELLS=y
+CONFIG_USER_NS=y
+CONFIG_PID_NS=y
+CONFIG_NET_NS=y
+CONFIG_VETH=y
+CONFIG_IPC_NS=y
+CONFIG_UTS_NS=y
+CONFIG_CGROUPS=y
+CONFIG_TMPFS=y
+CONFIG_TMPFS_XATTR=y
+CONFIG_OVERLAY_FS=y
+CONFIG_DEBUG_FS=y
+CONFIG_FAULT_INJECTION=y
+CONFIG_FAULT_INJECTION_DEBUG_FS=y
+CONFIG_FAILSLAB=y
+CONFIG_MEMCG=y
+CONFIG_CGROUP_PIDS=y
+CONFIG_IPV6=y
+CONFIG_NETFILTER=y
+CONFIG_NETFILTER_ADVANCED=y
+CONFIG_NETFILTER_NETLINK=y
+CONFIG_NETFILTER_NETLINK_OSF=y
+CONFIG_NETFILTER_XTABLES=y
+CONFIG_NF_CONNTRACK=y
+CONFIG_NF_CT_NETLINK=y
+CONFIG_NETFILTER_NETLINK_QUEUE=y
+CONFIG_NETFILTER_NETLINK_GLUE_CT=y
+CONFIG_NF_CT_NETLINK_HELPER=y
+CONFIG_NF_TABLES=y
+CONFIG_NF_TABLES_INET=y
+CONFIG_IP_NF_IPTABLES=y
+CONFIG_IP_NF_FILTER=y
+CONFIG_PACKET=y
+CONFIG_TUN=y
+CONFIG_XFRM_USER=y

-- 
2.55.0


      parent reply	other threads:[~2026-11-05 15:33 UTC|newest]

Thread overview: 93+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-05 14:56 [PATCH RFC v3 00/90] cells: a container as one kernel object Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 01/90] Documentation: add a design document for cells Josef Bacik
2026-11-05 17:06   ` Serge E. Hallyn
2026-11-05 18:23     ` Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 02/90] Documentation: add examples " Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 03/90] cells: reserve a window of ids Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 04/90] user_namespace: let a cell own a user namespace Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 05/90] user_namespace: refuse setns() into a cell Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 06/90] capability: give the owner no say over " Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 07/90] ucount: count the id blocks a user holds Josef Bacik
2026-11-05 14:56 ` [PATCH RFC v3 08/90] user_namespace: let the kernel map the ids of a cell Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 09/90] user_namespace: keep legacy maps out of the window Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 10/90] user: add uid_range_in_use() Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 11/90] keys: add key_user_range_in_use() Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 12/90] fs: add kuid_has_fsmapping() and kgid_has_fsmapping() Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 13/90] fs: never store an id that was lent to a cell Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 14/90] fs: take no id of a cell for a mount option Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 15/90] fs: let an idmapping pin the ids of a cell Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 16/90] cgroup: let a cgroup become the budget " Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 17/90] cgroup: add cgroup_cell_chown() Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 18/90] cgroup: give the budget of a cell back Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 19/90] cgroup: add cgroup_cell_kill() Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 20/90] cgroup: root a cgroup namespace at a given cgroup Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 21/90] fs: seal a mount namespace for a cell Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 22/90] nsfs: make open_namespace() available outside of fs/ Josef Bacik
2026-11-05 14:57 ` [PATCH RFC v3 23/90] nsproxy: build the nsproxy the tasks of a cell share Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 24/90] cells: add the cell object and cell_create() Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 25/90] cells: add cell_kill() Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 26/90] cells: add cell_configure() Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 27/90] cells: add the "budget" slot Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 28/90] cells: add the "root" slot Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 29/90] cells: add the "ids" slot, for a single id Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 30/90] cells: lend a cell a block of ids Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 31/90] cells: reserve the "spawn" slot Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 32/90] cells: add CELL_CMD_SEAL Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 33/90] cells: add cell_open() Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 34/90] pid: allocate a struct pid ahead of its numbers Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 35/90] fork: let copy_process() place a task in a cell Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 36/90] cgroup: fork a task of a cell into the budget Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 37/90] x86: give a task without an mm no user state Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 38/90] uprobes: cope with a new task that has no mm Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 39/90] fork: add tasks that start out with nothing Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 40/90] pidfd: add PIDFD_EMPTY Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 41/90] pidfd: add pidfd_configure() Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 42/90] pidfd: let pidfd_configure() place files Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 43/90] pidfd: let pidfd_configure() set credentials Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 44/90] pidfd: let pidfd_configure() name a cgroup to start in Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 45/90] ptrace: keep everybody off an embryo Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 46/90] cells: end a cell when its init exits Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 47/90] cells: refuse a task of a cell a change of namespaces Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 48/90] fs: refuse a task of a cell a new mount namespace Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 49/90] nsproxy: refuse to enter a namespace of a cell Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 50/90] cgroup: keep a task of a cell within the budget Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 51/90] cgroup: let nothing but a spawn into a budget Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 52/90] kernfs: let a filesystem refuse a change to a node Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 53/90] cgroup: let a cell change nothing outside its budget Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 54/90] cgroup: leave the limits of a budget to who is above Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 55/90] fs: let a filesystem hold ids that go with the cell Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 56/90] cgroup: let a cell make cgroups below its budget Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 57/90] resctrl: let a cell change nothing Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 58/90] cred: let no id count across cells Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 59/90] ptrace: refuse PTRACE_TRACEME " Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 60/90] keys: ignore an equal uid or gid " Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 61/90] fs: accept a cellfd for MOUNT_ATTR_IDMAP Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 62/90] fs: let a cell set no set-id bits Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 63/90] fs: drop privileges from what a cell may write to Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 64/90] cells: add the "caps" slot Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 65/90] capabilities: take nothing from a file in a cell Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 66/90] capabilities: let a cell shrink its bounding sets Josef Bacik
2026-11-05 15:02 ` [PATCH RFC v3 67/90] af_unix: deliver ancillary data only within a cell Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 68/90] af_unix: tell a sender that control data will not cross Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 69/90] net: do not identify a peer in another cell Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 70/90] seccomp: let a listener put no file into a cell Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 71/90] umh: run no program for a task of " Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 72/90] coredump: let no core dump leave " Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 73/90] cells: add the "net" slot Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 74/90] net: give a cell without a network no sockets Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 75/90] net: add net_may() Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 76/90] rtnetlink: use net_may() for requests that change state Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 77/90] net: use net_may() in the configuration ioctls Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 78/90] netfilter: use net_may() for configuration Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 79/90] tun: use net_may() to create and attach to devices Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 80/90] net: use net_may() for the net sysctls Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 81/90] net: use net_may() for privileged socket options Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 82/90] net: use net_may() for raw and packet sockets Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 83/90] cells: add the "net-rights" slot Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 84/90] net: honour the rights of a cell in net_may() Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 85/90] tun: ignore an equal uid or gid across cells Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 86/90] cells: add cell_info() Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 87/90] cells: add CONFIG_CELLS Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 88/90] arch: wire up the cell system calls Josef Bacik
2026-11-05 15:03 ` [PATCH RFC v3 89/90] MAINTAINERS: add an entry for cells Josef Bacik
2026-11-05 15:03 ` Josef Bacik [this message]

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=20261105-b4-cells-v3-90-bc9095937a84@toxicpanda.com \
    --to=josef@toxicpanda.com \
    --cc=James.Bottomley@HansenPartnership.com \
    --cc=andrew@amutable.com \
    --cc=brauner@kernel.org \
    --cc=jbingham@gmail.com \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=sergeh@kernel.org \
    --cc=taherafahimi@linux.microsoft.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®