* [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