| // 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 <asm/unistd.h> |
| |
| #include "kselftest.h" |
| |
| #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; |
| } |