Linux Kernel Selftest development
 help / color / mirror / Atom feed
* [PATCH] selftests/seccomp: Test a TSYNC filter arriving during a ptrace stop
@ 2026-10-15 16:13 Kees Cook
  0 siblings, 0 replies; only message in thread
From: Kees Cook @ 2026-10-15 16:13 UTC (permalink / raw)
  To: Oleg Nesterov
  Cc: Kees Cook, Jinjie Ruan, Andy Lutomirski, Will Drewry, Shuah Khan,
	linux-kselftest, linux-kernel, linux-hardening

Seccomp runs after ptrace at syscall entry so that it checks what the
tracer left. A thread can get its first filter by TSYNC from a sibling
while it is stopped at a syscall entry; the generic entry code used the
syscall work flags read before the stop, missed SYSCALL_WORK_SECCOMP,
and ran that syscall unfiltered until commit 4a3591287fb7f ("entry: Fix
seccomp bypass after ptrace with TSYNC").

Add a test based on the reproducer from that patch's thread, using pipes
instead of a sleep so the filter is known to be installed before the
tracer resumes the syscall.

Tests run under qemu on ARCH=x86_64 defconfig plus the seccomp selftest
config, with GCC 16.2.0: running the test 20 times all 20 runs pass at
v7.3-rc2 and fails in all 20 with the fix reverted.

Link: https://lore.kernel.org/all/157aa098-a263-4f4f-b0fc-108701480303@huawei.com/
Signed-off-by: Kees Cook <kees@kernel.org>
---
 tools/testing/selftests/seccomp/seccomp_bpf.c | 112 ++++++++++++++++++
 1 file changed, 112 insertions(+)

diff --git a/tools/testing/selftests/seccomp/seccomp_bpf.c b/tools/testing/selftests/seccomp/seccomp_bpf.c
index 0622bc2acad4..4da5ad1d7ab3 100644
--- a/tools/testing/selftests/seccomp/seccomp_bpf.c
+++ b/tools/testing/selftests/seccomp/seccomp_bpf.c
@@ -5041,6 +5041,118 @@ TEST(tsync_vs_dead_thread_leader)
 	EXPECT_EQ(0, status);
 }
 
+struct tsync_vs_ptrace_args {
+	int go[2];
+	int done[2];
+};
+
+static void *tsync_vs_ptrace_syscall_stop_sibling(void *_args)
+{
+	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_getppid, 0, 1),
+		BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ERRNO | ESRCH),
+		BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW),
+	};
+	struct sock_fprog prog = {
+		.len = (unsigned short)ARRAY_SIZE(filter),
+		.filter = filter,
+	};
+	struct tsync_vs_ptrace_args *args = _args;
+	char result = 'N';
+	char go;
+
+	/* Wait for the leader to stop at the entry of getppid(). */
+	if (read(args->go[0], &go, 1) == 1 &&
+	    seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC,
+		    &prog) == 0)
+		result = 'Y';
+	if (write(args->done[1], &result, 1) != 1)
+		exit(3);
+
+	return NULL;
+}
+
+/*
+ * Seccomp runs after ptrace at syscall entry, so that it sees what the
+ * tracer left. A thread's first filter can arrive by TSYNC from a sibling
+ * while the thread is stopped at a syscall entry, and it must still apply
+ * to that syscall once the tracer resumes it.
+ */
+TEST(tsync_vs_ptrace_syscall_stop)
+{
+	struct tsync_vs_ptrace_args args;
+	unsigned long msg;
+	char result;
+	int status;
+	pid_t pid;
+	long ret;
+
+	ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("Kernel does not support PR_SET_NO_NEW_PRIVS!");
+	}
+	ASSERT_EQ(0, pipe(args.go));
+	ASSERT_EQ(0, pipe(args.done));
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		pthread_t sibling;
+
+		if (pthread_create(&sibling, NULL,
+				   tsync_vs_ptrace_syscall_stop_sibling, &args))
+			exit(1);
+		/* Only this thread is traced; the sibling runs freely. */
+		if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
+			exit(2);
+		raise(SIGSTOP);
+
+		ret = syscall(__NR_getppid);
+		/* The filter installed during the entry stop must apply. */
+		if (ret != -1 || errno != ESRCH)
+			exit(4);
+		exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFSTOPPED(status));
+	ASSERT_EQ(SIGSTOP, WSTOPSIG(status));
+	ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, pid, NULL,
+			    PTRACE_O_TRACESYSGOOD | PTRACE_O_EXITKILL));
+
+	/* Run the child to its syscall entry stop for getppid(). */
+	do {
+		ASSERT_EQ(0, ptrace(PTRACE_SYSCALL, pid, NULL, 0));
+		ASSERT_EQ(pid, waitpid(pid, &status, 0));
+		ASSERT_TRUE(WIFSTOPPED(status)) {
+			TH_LOG("Child did not stop: status 0x%x", status);
+		}
+		ASSERT_EQ(SIGTRAP | 0x80, WSTOPSIG(status));
+		ASSERT_EQ(0, ptrace(PTRACE_GETEVENTMSG, pid, NULL, &msg));
+	} while (msg != PTRACE_EVENTMSG_SYSCALL_ENTRY ||
+		 get_syscall(_metadata, pid) != __NR_getppid);
+
+	/* Have the sibling install the first filter with TSYNC, and wait. */
+	ASSERT_EQ(1, write(args.go[1], "G", 1));
+	ASSERT_EQ(1, read(args.done[0], &result, 1));
+	ASSERT_EQ('Y', result) {
+		TH_LOG("Sibling could not install the filter with TSYNC");
+	}
+
+	ASSERT_EQ(0, ptrace(PTRACE_CONT, pid, NULL, 0));
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status)) {
+		TH_LOG("Child did not exit: status 0x%x", status);
+	}
+	EXPECT_EQ(0, WEXITSTATUS(status)) {
+		if (WEXITSTATUS(status) == 4)
+			TH_LOG("getppid() ran without the TSYNC filter");
+	}
+}
+
 #ifdef __x86_64__
 
 /*
-- 
2.55.0


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-15 16:13 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-15 16:13 [PATCH] selftests/seccomp: Test a TSYNC filter arriving during a ptrace stop Kees Cook

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox