All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.