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/[email protected]/ Signed-off-by: Kees Cook <[email protected]> --- 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

