diff --git a/tools/testing/selftests/arm64/abi/.gitignore b/tools/testing/selftests/arm64/abi/.gitignore index 39129a9907c7..491a80db9dff 100644 --- a/tools/testing/selftests/arm64/abi/.gitignore +++ b/tools/testing/selftests/arm64/abi/.gitignore @@ -1,5 +1,6 @@ hwcap ptrace seccomp_ptrace_x0_bypass +seccomp_ret_trace_x0_bypass syscall-abi tpidr2 diff --git a/tools/testing/selftests/arm64/abi/Makefile b/tools/testing/selftests/arm64/abi/Makefile index 5a16db379bd4..a01d3806eba8 100644 --- a/tools/testing/selftests/arm64/abi/Makefile +++ b/tools/testing/selftests/arm64/abi/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2021 ARM Limited -TEST_GEN_PROGS := hwcap ptrace syscall-abi tpidr2 seccomp_ptrace_x0_bypass +TEST_GEN_PROGS := hwcap ptrace syscall-abi tpidr2 seccomp_ptrace_x0_bypass seccomp_ret_trace_x0_bypass include ../../lib.mk diff --git a/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c b/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c new file mode 100644 index 000000000000..3a69f53f4f88 --- /dev/null +++ b/tools/testing/selftests/arm64/abi/seccomp_ret_trace_x0_bypass.c @@ -0,0 +1,204 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +}