--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test for SECCOMP_RET_TRACE argument modification bypass
+ * via stale orig_x0 during filter re-evaluation.
+ *
+ * On arm64, syscall_get_arguments() reads the first argument from
+ * regs->orig_x0. When a seccomp filter returns SECCOMP_RET_TRACE,
+ * ptrace may modify regs->regs[0] while orig_x0 remains unchanged.
+ * The kernel then re-evaluates the filter; if it sees the stale
+ * orig_x0, it may incorrectly allow a syscall that the tracer intended
+ * to block.
+ *
+ * This test installs a filter that:
+ * - TRACEs write() when fd == 2
+ * - returns ERRNO(EPERM) when fd == 1
+ * - allows all other syscalls
+ *
+ * The child calls write(2, ...). The parent catches the SECCOMP stop,
+ * changes x0 (fd) from 2 to 1, and resumes the child.
+ *
+ * If re-evaluation sees the old fd=2 (stale orig_x0), the filter
+ * returns TRACE again; because recheck_after_trace is true, the kernel
+ * allows the syscall to proceed. write(1, ...) succeeds, child exits 0.
+ * -> test FAIL (bypass detected).
+ *
+ * If re-evaluation sees the new fd=1 (synced orig_x0), the filter
+ * returns ERRNO(EPERM), write fails, child exits 1.
+ * -> test PASS (no bypass).
+ *
+ * No special privileges required beyond CAP_SYS_PTRACE.
+ */
+#include <errno.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/prctl.h>
+#include <sys/ptrace.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <linux/elf.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+#include <linux/ptrace.h>
+
+#include "kselftest.h"
+
+#ifndef __NR_write
+#define __NR_write 64
+#endif
+
+#define PTRACE_EVENT_MASK(status) ((status) >> 16)
+
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define ARG0_OFFSET (offsetof(struct seccomp_data, args))
+#else
+#define ARG0_OFFSET (offsetof(struct seccomp_data, args) + 4)
+#endif
+
+static int do_child(void)
+{
+ long ret;
+
+ if (ptrace(PTRACE_TRACEME, 0, NULL, NULL))
+ _exit(2);
+
+ raise(SIGSTOP); /* synchronize with parent */
+
+ /*
+ * Filter:
+ * if syscall == write:
+ * if fd == 2 -> TRACE
+ * if fd == 1 -> ERRNO(EPERM)
+ * else -> ALLOW
+ * else -> ALLOW
+ */
+ struct sock_filter filter[] = {
+ /* Load syscall number */
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
+ /* If not write, allow */
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 5),
+ /* Load first argument (fd) */
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ARG0_OFFSET),
+ /* fd == 2 ? */
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 0, 1),
+ /* Yes: TRACE */
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRACE),
+ /* fd == 1 ? */
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 0, 1),
+ /* Yes: ERRNO(EPERM) */
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
+ /* Other fd: ALLOW */
+ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
+ };
+
+ struct sock_fprog prog = {
+ .len = ARRAY_SIZE(filter),
+ .filter = filter,
+ };
+
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+ _exit(3);
+ if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
+ _exit(4);
+
+ /*
+ * write(2, ...) triggers TRACE, parent changes fd to 1.
+ * If re-eval sees fd=1 -> ERRNO -> write fails, ret = -EPERM.
+ * If re-eval sees fd=2 -> TRACE again -> allowed -> write succeeds.
+ */
+ ret = syscall(__NR_write, 2, "", 0);
+ _exit(ret == 0 ? 0 : 1);
+}
+
+int main(void)
+{
+ struct user_pt_regs regs;
+ struct iovec iov = { .iov_base = ®s, .iov_len = sizeof(regs) };
+ pid_t child;
+ int status;
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ child = fork();
+ if (child < 0)
+ ksft_exit_fail_msg("fork failed: %s", strerror(errno));
+
+ if (!child)
+ return do_child();
+
+ /* 1. Wait for initial SIGSTOP */
+ if (waitpid(child, &status, 0) != child)
+ ksft_exit_fail_msg("waitpid SIGSTOP");
+ if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP)
+ ksft_exit_fail_msg("unexpected initial stop");
+
+ /* 2. Enable SECCOMP ptrace events */
+ if (ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESECCOMP))
+ ksft_exit_fail_msg("PTRACE_SETOPTIONS");
+
+ /* 3. Continue child to hit SECCOMP stop */
+ if (ptrace(PTRACE_CONT, child, 0, 0))
+ ksft_exit_fail_msg("PTRACE_CONT");
+
+ /* 4. Wait for SECCOMP stop */
+ while (1) {
+ if (waitpid(child, &status, 0) != child)
+ ksft_exit_fail_msg("waitpid SECCOMP");
+ if (WIFEXITED(status)) {
+ ksft_test_result_fail("child exited before SECCOMP stop\n");
+ goto out;
+ }
+ if (WIFSIGNALED(status)) {
+ ksft_test_result_fail("child killed unexpectedly\n");
+ goto out;
+ }
+ if (WIFSTOPPED(status) &&
+ WSTOPSIG(status) == SIGTRAP &&
+ PTRACE_EVENT_MASK(status) == PTRACE_EVENT_SECCOMP)
+ break;
+ ptrace(PTRACE_CONT, child, 0, WSTOPSIG(status));
+ }
+
+ /* 5. Modify x0 (fd) from 2 to 1 */
+ if (ptrace(PTRACE_GETREGSET, child, NT_PRSTATUS, &iov))
+ ksft_exit_fail_perror("GETREGSET");
+ if (regs.regs[8] != __NR_write || regs.regs[0] != 2) {
+ ksft_test_result_fail("unexpected regs: syscall=%llu, x0=%llu\n",
+ regs.regs[8], regs.regs[0]);
+ goto out;
+ }
+ regs.regs[0] = 1;
+ if (ptrace(PTRACE_SETREGSET, child, NT_PRSTATUS, &iov))
+ ksft_exit_fail_perror("SETREGSET");
+
+ /* 6. Resume child */
+ if (ptrace(PTRACE_CONT, child, 0, 0))
+ ksft_exit_fail_perror("PTRACE_CONT");
+
+ /* 7. Reap child – must exit normally */
+ if (waitpid(child, &status, 0) != child)
+ ksft_exit_fail_msg("final waitpid");
+
+ if (!WIFEXITED(status)) {
+ ksft_test_result_fail("child did not exit normally\n");
+ goto out;
+ }
+
+ if (WEXITSTATUS(status) != 0)
+ ksft_test_result_pass("seccomp correctly denied modified syscall\n");
+ else
+ ksft_test_result_fail("write succeeded, orig_x0 bypass likely\n");
+
+out:
+ if (child > 0) {
+ kill(child, SIGKILL);
+ waitpid(child, NULL, 0);
+ }
+ ksft_print_cnts();
+ return ksft_get_fail_cnt() ? EXIT_FAILURE : EXIT_SUCCESS;
+}