mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Cong Wang <xiyou.wangcong@gmail.com>
To: Kees Cook <kees@kernel.org>, linux-kernel@vger.kernel.org
Cc: Andy Lutomirski <luto@amacapital.net>,
	Will Drewry <wad@chromium.org>,
	Christian Brauner <brauner@kernel.org>,
	Cong Wang <cwang@multikernel.io>
Subject: [RFC PATCH v2 2/3] selftests/seccomp: add seccomp_notif_inject coverage
Date: Thu, 14 May 2026 21:27:37 -0700	[thread overview]
Message-ID: <20260515042738.1723296-3-xiyou.wangcong@gmail.com> (raw)
In-Reply-To: <20260515042738.1723296-1-xiyou.wangcong@gmail.com>

From: Cong Wang <cwang@multikernel.io>

Add a standalone selftest binary for SECCOMP_IOCTL_NOTIF_INJECT
covering positive end-to-end injection on each of the v1 syscalls
(openat, bind, write) and the negative paths (unsupported syscall,
trapped/inject syscall mismatch, double inject, CONTINUE+INJECTED
conflict, INJECTED without prior attach).

The end-to-end tests fork a child that issues a filtered syscall
with one set of arguments and verify that, after the supervisor
attaches a substitute via SECCOMP_IOCTL_NOTIF_INJECT and replies
with SECCOMP_USER_NOTIF_FLAG_INJECTED, the kernel acts on the
supervisor-supplied bytes rather than the child's user mm.

Lives in its own file rather than seccomp_bpf.c since the feature
is unrelated to the BPF filter machinery.

Assisted-by: Claude:claude-opus-4.6
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 tools/testing/selftests/seccomp/.gitignore    |   1 +
 tools/testing/selftests/seccomp/Makefile      |   2 +-
 .../selftests/seccomp/seccomp_notif_inject.c  | 434 ++++++++++++++++++
 3 files changed, 436 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/seccomp/seccomp_notif_inject.c

diff --git a/tools/testing/selftests/seccomp/.gitignore b/tools/testing/selftests/seccomp/.gitignore
index dec678577f9c..096f77a0b136 100644
--- a/tools/testing/selftests/seccomp/.gitignore
+++ b/tools/testing/selftests/seccomp/.gitignore
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 seccomp_bpf
 seccomp_benchmark
+seccomp_notif_inject
diff --git a/tools/testing/selftests/seccomp/Makefile b/tools/testing/selftests/seccomp/Makefile
index 584fba487037..8a4ca92f1af3 100644
--- a/tools/testing/selftests/seccomp/Makefile
+++ b/tools/testing/selftests/seccomp/Makefile
@@ -3,5 +3,5 @@ CFLAGS += -Wl,-no-as-needed -Wall $(KHDR_INCLUDES)
 LDFLAGS += -lpthread
 LDLIBS += -lcap
 
-TEST_GEN_PROGS := seccomp_bpf seccomp_benchmark
+TEST_GEN_PROGS := seccomp_bpf seccomp_benchmark seccomp_notif_inject
 include ../lib.mk
diff --git a/tools/testing/selftests/seccomp/seccomp_notif_inject.c b/tools/testing/selftests/seccomp/seccomp_notif_inject.c
new file mode 100644
index 000000000000..3d5bb97b8df9
--- /dev/null
+++ b/tools/testing/selftests/seccomp/seccomp_notif_inject.c
@@ -0,0 +1,434 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Selftest for SECCOMP_IOCTL_NOTIF_INJECT.
+ *
+ * Exercises end-to-end syscall injection via the listener-fd
+ * supervisor pattern: the child issues a filtered syscall, the
+ * supervisor describes a substitute syscall in a kernel buffer, the
+ * kernel runs the substitute on the child's behalf using kernel-mode
+ * helpers (filp_open/kernel_bind/kernel_write), and the result lands
+ * as the child's syscall return value. The trapped task's user mm is
+ * never re-read for the substituted syscall.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../kselftest_harness.h"
+
+#ifndef __NR_seccomp
+#define __NR_seccomp 317
+#endif
+
+static int seccomp_install(int nr)
+{
+	struct sock_filter filter[] = {
+		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_USER_NOTIF),
+		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog prog = {
+		.len = (unsigned short)ARRAY_SIZE(filter),
+		.filter = filter,
+	};
+
+	return syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER,
+		       SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
+}
+
+/* ----------------------------------------------------------------
+ * openat injection.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_openat)
+{
+	char tmp_real[] = "/tmp/seccomp-inject-XXXXXX";
+	int real_fd, listener, status;
+	pid_t pid;
+
+	real_fd = mkstemp(tmp_real);
+	ASSERT_GE(real_fd, 0);
+	ASSERT_EQ(write(real_fd, "real-data", 9), 9);
+	ASSERT_EQ(close(real_fd), 0);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_openat);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		char readback[16] = {0};
+		int fd;
+
+		fd = openat(AT_FDCWD, "/this/path/does/not/exist", O_RDONLY);
+		if (fd < 0)
+			_exit(10);
+		if (read(fd, readback, sizeof(readback) - 1) <= 0)
+			_exit(11);
+		_exit(memcmp(readback, "real-data", 9) == 0 ? 0 : 12);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+	struct seccomp_notif_inject inj = {0};
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+	EXPECT_EQ(req.data.nr, __NR_openat);
+
+	inj.id = req.id;
+	inj.nr = __NR_openat;
+	inj.args[0] = AT_FDCWD;
+	inj.args[1] = 0;
+	inj.args[2] = O_RDONLY;
+	inj.args[3] = 0;
+	inj.buf = (uintptr_t)tmp_real;
+	inj.buf_size = strlen(tmp_real) + 1;
+	inj.args_in_buf_mask = 1U << 1;
+	ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0) {
+		TH_LOG("INJECT failed: %s", strerror(errno));
+	}
+
+	resp.id = req.id;
+	resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(WEXITSTATUS(status), 0) {
+		TH_LOG("child exit status: %d", WEXITSTATUS(status));
+	}
+
+	unlink(tmp_real);
+	close(listener);
+}
+
+/* ----------------------------------------------------------------
+ * write injection.
+ * Child issues write(fd, "agent-data", 10); supervisor injects
+ * write(fd, "kernel-bytes", 12); verify file content matches the
+ * kernel-injected bytes.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_write)
+{
+	char path[] = "/tmp/seccomp-inject-write-XXXXXX";
+	static const char inject_bytes[] = "kernel-bytes";
+	int file_fd, listener, status;
+	char file_content[32];
+	pid_t pid;
+
+	file_fd = mkstemp(path);
+	ASSERT_GE(file_fd, 0);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_write);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ssize_t n;
+
+		n = write(file_fd, "agent-data", 10);
+		_exit(n > 0 ? 0 : 10);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+	struct seccomp_notif_inject inj = {0};
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+	EXPECT_EQ(req.data.nr, __NR_write);
+
+	inj.id = req.id;
+	inj.nr = __NR_write;
+	inj.args[0] = req.data.args[0];   /* fd, pass-through */
+	inj.args[1] = 0;
+	inj.args[2] = strlen(inject_bytes);
+	inj.buf = (uintptr_t)inject_bytes;
+	inj.buf_size = strlen(inject_bytes);
+	inj.args_in_buf_mask = 1U << 1;
+	ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0);
+
+	resp.id = req.id;
+	resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+
+	memset(file_content, 0, sizeof(file_content));
+	ASSERT_EQ(lseek(file_fd, 0, SEEK_SET), 0);
+	ASSERT_EQ(read(file_fd, file_content, sizeof(file_content) - 1),
+		  (ssize_t)strlen(inject_bytes));
+	EXPECT_EQ(memcmp(file_content, inject_bytes, strlen(inject_bytes)), 0) {
+		TH_LOG("file content: '%s'", file_content);
+	}
+
+	close(file_fd);
+	unlink(path);
+	close(listener);
+}
+
+/* ----------------------------------------------------------------
+ * bind injection.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_bind)
+{
+	struct sockaddr_un real_addr = { .sun_family = AF_UNIX };
+	char real_path[] = "/tmp/seccomp-inject-bind-XXXXXX";
+	int listener, status;
+	pid_t pid;
+
+	mktemp(real_path);
+	strcpy(real_addr.sun_path, real_path);
+	unlink(real_addr.sun_path);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_bind);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		struct sockaddr_un fake = { .sun_family = AF_UNIX };
+		int s;
+
+		strcpy(fake.sun_path, "/tmp/seccomp-inject-bind-fake");
+		s = socket(AF_UNIX, SOCK_STREAM, 0);
+		if (s < 0)
+			_exit(10);
+		if (bind(s, (struct sockaddr *)&fake, sizeof(fake)) < 0)
+			_exit(11);
+		_exit(0);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+	struct seccomp_notif_inject inj = {0};
+	struct stat st;
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+	EXPECT_EQ(req.data.nr, __NR_bind);
+
+	inj.id = req.id;
+	inj.nr = __NR_bind;
+	inj.args[0] = req.data.args[0];   /* sockfd, pass-through */
+	inj.args[1] = 0;
+	inj.args[2] = sizeof(real_addr);
+	inj.buf = (uintptr_t)&real_addr;
+	inj.buf_size = sizeof(real_addr);
+	inj.args_in_buf_mask = 1U << 1;
+	ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0);
+
+	resp.id = req.id;
+	resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	EXPECT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(WEXITSTATUS(status), 0);
+
+	/*
+	 * The kernel-injected path should exist; the agent's intended
+	 * path should not.
+	 */
+	EXPECT_EQ(stat(real_path, &st), 0);
+	EXPECT_EQ(stat("/tmp/seccomp-inject-bind-fake", &st), -1);
+
+	unlink(real_path);
+	close(listener);
+}
+
+/* ----------------------------------------------------------------
+ * Negative paths.
+ * ----------------------------------------------------------------
+ */
+TEST(notif_inject_unsupported_syscall)
+{
+	int listener, status;
+	pid_t pid;
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_close);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		close(99);
+		_exit(0);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+	struct seccomp_notif_inject inj = {0};
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+	/* close() is not in the injectable whitelist. */
+	inj.id = req.id;
+	inj.nr = __NR_close;
+	inj.args[0] = 99;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), -1);
+	EXPECT_EQ(errno, EOPNOTSUPP);
+
+	/* Cleanly deny so the child can exit. */
+	resp.id = req.id;
+	resp.error = -EBADF;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	close(listener);
+}
+
+TEST(notif_inject_syscall_mismatch)
+{
+	int listener, status;
+	pid_t pid;
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_openat);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		openat(AT_FDCWD, "/nonexistent", O_RDONLY);
+		_exit(0);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+	struct seccomp_notif_inject inj = {0};
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+	/* Trapped on openat, but supervisor tries to inject bind. */
+	inj.id = req.id;
+	inj.nr = __NR_bind;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), -1);
+	EXPECT_EQ(errno, ESRCH);
+
+	resp.id = req.id;
+	resp.error = -EACCES;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	close(listener);
+}
+
+TEST(notif_inject_double)
+{
+	char tmp_real[] = "/tmp/seccomp-inject-d-XXXXXX";
+	int real_fd, listener, status;
+	pid_t pid;
+
+	real_fd = mkstemp(tmp_real);
+	ASSERT_GE(real_fd, 0);
+	close(real_fd);
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_openat);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		openat(AT_FDCWD, "/nonexistent", O_RDONLY);
+		_exit(0);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+	struct seccomp_notif_inject inj = {0};
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+	inj.id = req.id;
+	inj.nr = __NR_openat;
+	inj.args[0] = AT_FDCWD;
+	inj.args[1] = 0;
+	inj.args[2] = O_RDONLY;
+	inj.buf = (uintptr_t)tmp_real;
+	inj.buf_size = strlen(tmp_real) + 1;
+	inj.args_in_buf_mask = 1U << 1;
+	ASSERT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), 0);
+
+	/* Second INJECT for the same id is rejected. */
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_INJECT, &inj), -1);
+	EXPECT_EQ(errno, EEXIST);
+
+	resp.id = req.id;
+	resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+
+	unlink(tmp_real);
+	close(listener);
+}
+
+TEST(notif_inject_continue_pinned_conflict)
+{
+	int listener, status;
+	pid_t pid;
+
+	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+	listener = seccomp_install(__NR_openat);
+	ASSERT_GE(listener, 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		openat(AT_FDCWD, "/nonexistent", O_RDONLY);
+		_exit(0);
+	}
+
+	struct seccomp_notif req = {0};
+	struct seccomp_notif_resp resp = {0};
+
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req), 0);
+
+	/* CONTINUE | INJECTED is rejected. */
+	resp.id = req.id;
+	resp.flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE |
+		     SECCOMP_USER_NOTIF_FLAG_INJECTED;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	/* INJECTED without prior INJECT ioctl is rejected too. */
+	resp.flags = SECCOMP_USER_NOTIF_FLAG_INJECTED;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), -1);
+	EXPECT_EQ(errno, EINVAL);
+
+	/* Cleanly deny so the child exits. */
+	resp.flags = 0;
+	resp.error = -ENOENT;
+	EXPECT_EQ(ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp), 0);
+	EXPECT_EQ(waitpid(pid, &status, 0), pid);
+	close(listener);
+}
+
+TEST_HARNESS_MAIN
-- 
2.43.0


  parent reply	other threads:[~2026-05-15  4:27 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-15  4:27 [RFC PATCH v2 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify Cong Wang
2026-05-15  4:27 ` [RFC PATCH v2 1/3] seccomp: add " Cong Wang
2026-05-15  4:27 ` Cong Wang [this message]
2026-05-15  4:27 ` [RFC PATCH v2 3/3] Documentation: seccomp: document SECCOMP_IOCTL_NOTIF_INJECT Cong Wang
2026-05-26 18:06 ` [RFC PATCH v2 0/3] seccomp: SECCOMP_IOCTL_NOTIF_INJECT for race-free unotify Cong Wang
2026-05-26 19:03 ` Andy Lutomirski
2026-05-28 17:41   ` Cong Wang
2026-05-28 18:15     ` Andy Lutomirski
2026-05-29  5:07       ` Cong Wang

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=20260515042738.1723296-3-xiyou.wangcong@gmail.com \
    --to=xiyou.wangcong@gmail.com \
    --cc=brauner@kernel.org \
    --cc=cwang@multikernel.io \
    --cc=kees@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=luto@amacapital.net \
    --cc=wad@chromium.org \
    /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®