| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * Copyright (c) 2018-2025 Dmitry V. Levin <ldv@strace.io> |
| * All rights reserved. |
| * |
| * Check whether PTRACE_SET_SYSCALL_INFO semantics implemented in the kernel |
| * matches userspace expectations. |
| */ |
| |
| #include "kselftest_harness.h" |
| #include <err.h> |
| #include <fcntl.h> |
| #include <signal.h> |
| #include <asm/unistd.h> |
| #include <linux/types.h> |
| #include <linux/ptrace.h> |
| |
| #if defined(_MIPS_SIM) && _MIPS_SIM == _MIPS_SIM_NABI32 |
| /* |
| * MIPS N32 is the only architecture where __kernel_ulong_t |
| * does not match the bitness of syscall arguments. |
| */ |
| typedef unsigned long long kernel_ulong_t; |
| #else |
| typedef __kernel_ulong_t kernel_ulong_t; |
| #endif |
| |
| struct si_entry { |
| int nr; |
| kernel_ulong_t args[6]; |
| }; |
| struct si_exit { |
| unsigned int is_error; |
| int rval; |
| }; |
| |
| static unsigned int ptrace_stop; |
| static pid_t tracee_pid; |
| |
| static int |
| kill_tracee(pid_t pid) |
| { |
| if (!pid) |
| return 0; |
| |
| int saved_errno = errno; |
| |
| int rc = kill(pid, SIGKILL); |
| |
| errno = saved_errno; |
| return rc; |
| } |
| |
| static long |
| sys_ptrace(int request, pid_t pid, unsigned long addr, unsigned long data) |
| { |
| return syscall(__NR_ptrace, request, pid, addr, data); |
| } |
| |
| #define LOG_KILL_TRACEE(fmt, ...) \ |
| do { \ |
| kill_tracee(tracee_pid); \ |
| TH_LOG("wait #%d: " fmt, \ |
| ptrace_stop, ##__VA_ARGS__); \ |
| } while (0) |
| |
| static void |
| check_psi_entry(struct __test_metadata *_metadata, |
| const struct ptrace_syscall_info *info, |
| const struct si_entry *exp_entry, |
| const char *text) |
| { |
| unsigned int i; |
| int exp_nr = exp_entry->nr; |
| #if defined __s390__ || defined __s390x__ |
| /* s390 is the only architecture that has 16-bit syscall numbers */ |
| exp_nr &= 0xffff; |
| #endif |
| |
| ASSERT_EQ(PTRACE_SYSCALL_INFO_ENTRY, info->op) { |
| LOG_KILL_TRACEE("%s: entry stop mismatch", text); |
| } |
| ASSERT_TRUE(info->arch) { |
| LOG_KILL_TRACEE("%s: entry stop mismatch", text); |
| } |
| ASSERT_TRUE(info->instruction_pointer) { |
| LOG_KILL_TRACEE("%s: entry stop mismatch", text); |
| } |
| ASSERT_TRUE(info->stack_pointer) { |
| LOG_KILL_TRACEE("%s: entry stop mismatch", text); |
| } |
| ASSERT_EQ(exp_nr, info->entry.nr) { |
| LOG_KILL_TRACEE("%s: syscall nr mismatch", text); |
| } |
| for (i = 0; i < ARRAY_SIZE(exp_entry->args); ++i) { |
| ASSERT_EQ(exp_entry->args[i], info->entry.args[i]) { |
| LOG_KILL_TRACEE("%s: syscall arg #%u mismatch", |
| text, i); |
| } |
| } |
| } |
| |
| static void |
| check_psi_exit(struct __test_metadata *_metadata, |
| const struct ptrace_syscall_info *info, |
| const struct si_exit *exp_exit, |
| const char *text) |
| { |
| ASSERT_EQ(PTRACE_SYSCALL_INFO_EXIT, info->op) { |
| LOG_KILL_TRACEE("%s: exit stop mismatch", text); |
| } |
| ASSERT_TRUE(info->arch) { |
| LOG_KILL_TRACEE("%s: exit stop mismatch", text); |
| } |
| ASSERT_TRUE(info->instruction_pointer) { |
| LOG_KILL_TRACEE("%s: exit stop mismatch", text); |
| } |
| ASSERT_TRUE(info->stack_pointer) { |
| LOG_KILL_TRACEE("%s: exit stop mismatch", text); |
| } |
| ASSERT_EQ(exp_exit->is_error, info->exit.is_error) { |
| LOG_KILL_TRACEE("%s: exit stop mismatch", text); |
| } |
| ASSERT_EQ(exp_exit->rval, info->exit.rval) { |
| LOG_KILL_TRACEE("%s: exit stop mismatch", text); |
| } |
| } |
| |
| TEST(set_syscall_info) |
| { |
| const pid_t tracer_pid = getpid(); |
| const kernel_ulong_t dummy[] = { |
| (kernel_ulong_t) 0xdad0bef0bad0fed0ULL, |
| (kernel_ulong_t) 0xdad1bef1bad1fed1ULL, |
| (kernel_ulong_t) 0xdad2bef2bad2fed2ULL, |
| (kernel_ulong_t) 0xdad3bef3bad3fed3ULL, |
| (kernel_ulong_t) 0xdad4bef4bad4fed4ULL, |
| (kernel_ulong_t) 0xdad5bef5bad5fed5ULL, |
| }; |
| int splice_in[2], splice_out[2]; |
| |
| ASSERT_EQ(0, pipe(splice_in)); |
| ASSERT_EQ(0, pipe(splice_out)); |
| ASSERT_EQ(sizeof(dummy), write(splice_in[1], dummy, sizeof(dummy))); |
| |
| const struct { |
| struct si_entry entry[2]; |
| struct si_exit exit[2]; |
| } si[] = { |
| /* change scno, keep non-error rval */ |
| { |
| { |
| { |
| __NR_gettid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| __NR_getppid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 0, tracer_pid }, { 0, tracer_pid } |
| } |
| }, |
| |
| /* set scno to -1, keep error rval */ |
| { |
| { |
| { |
| __NR_chdir, |
| { |
| (uintptr_t) ".", |
| dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| -1, |
| { |
| (uintptr_t) ".", |
| dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 1, -ENOSYS }, { 1, -ENOSYS } |
| } |
| }, |
| |
| /* keep scno, change non-error rval */ |
| { |
| { |
| { |
| __NR_getppid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| __NR_getppid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 0, tracer_pid }, { 0, tracer_pid + 1 } |
| } |
| }, |
| |
| /* change arg1, keep non-error rval */ |
| { |
| { |
| { |
| __NR_chdir, |
| { |
| (uintptr_t) "", |
| dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| __NR_chdir, |
| { |
| (uintptr_t) ".", |
| dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 0, 0 }, { 0, 0 } |
| } |
| }, |
| |
| /* set scno to -1, change error rval to non-error */ |
| { |
| { |
| { |
| __NR_gettid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| -1, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 1, -ENOSYS }, { 0, tracer_pid } |
| } |
| }, |
| |
| /* change scno, change non-error rval to error */ |
| { |
| { |
| { |
| __NR_chdir, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| __NR_getppid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 0, tracer_pid }, { 1, -EISDIR } |
| } |
| }, |
| |
| /* change scno and all args, change non-error rval */ |
| { |
| { |
| { |
| __NR_gettid, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| __NR_splice, |
| { |
| splice_in[0], 0, splice_out[1], 0, |
| sizeof(dummy), SPLICE_F_NONBLOCK |
| } |
| } |
| }, { |
| { 0, sizeof(dummy) }, { 0, sizeof(dummy) + 1 } |
| } |
| }, |
| |
| /* change arg1, no exit stop */ |
| { |
| { |
| { |
| __NR_exit_group, |
| { |
| dummy[0], dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| }, { |
| __NR_exit_group, |
| { |
| 0, dummy[1], dummy[2], |
| dummy[3], dummy[4], dummy[5] |
| } |
| } |
| }, { |
| { 0, 0 }, { 0, 0 } |
| } |
| }, |
| }; |
| |
| long rc; |
| unsigned int i; |
| |
| tracee_pid = fork(); |
| |
| ASSERT_LE(0, tracee_pid) { |
| TH_LOG("fork: %m"); |
| } |
| |
| if (tracee_pid == 0) { |
| /* get the pid before PTRACE_TRACEME */ |
| tracee_pid = getpid(); |
| ASSERT_EQ(0, sys_ptrace(PTRACE_TRACEME, 0, 0, 0)) { |
| TH_LOG("PTRACE_TRACEME: %m"); |
| } |
| ASSERT_EQ(0, kill(tracee_pid, SIGSTOP)) { |
| /* cannot happen */ |
| TH_LOG("kill SIGSTOP: %m"); |
| } |
| for (i = 0; i < ARRAY_SIZE(si); ++i) { |
| rc = syscall(si[i].entry[0].nr, |
| si[i].entry[0].args[0], |
| si[i].entry[0].args[1], |
| si[i].entry[0].args[2], |
| si[i].entry[0].args[3], |
| si[i].entry[0].args[4], |
| si[i].entry[0].args[5]); |
| if (si[i].exit[1].is_error) { |
| if (rc != -1 || errno != -si[i].exit[1].rval) |
| break; |
| } else { |
| if (rc != si[i].exit[1].rval) |
| break; |
| } |
| } |
| /* |
| * Something went wrong, but in this state tracee |
| * cannot reliably issue syscalls, so just crash. |
| */ |
| *(volatile unsigned char *) (uintptr_t) i = 42; |
| /* unreachable */ |
| _exit(i + 1); |
| } |
| |
| for (ptrace_stop = 0; ; ++ptrace_stop) { |
| struct ptrace_syscall_info info = { |
| .op = 0xff /* invalid PTRACE_SYSCALL_INFO_* op */ |
| }; |
| const size_t size = sizeof(info); |
| const int expected_entry_size = |
| (void *) &info.entry.args[6] - (void *) &info; |
| const int expected_exit_size = |
| (void *) (&info.exit.is_error + 1) - |
| (void *) &info; |
| int status; |
| |
| ASSERT_EQ(tracee_pid, wait(&status)) { |
| /* cannot happen */ |
| LOG_KILL_TRACEE("wait: %m"); |
| } |
| if (WIFEXITED(status)) { |
| tracee_pid = 0; /* the tracee is no more */ |
| ASSERT_EQ(0, WEXITSTATUS(status)) { |
| LOG_KILL_TRACEE("unexpected exit status %u", |
| WEXITSTATUS(status)); |
| } |
| break; |
| } |
| ASSERT_FALSE(WIFSIGNALED(status)) { |
| tracee_pid = 0; /* the tracee is no more */ |
| LOG_KILL_TRACEE("unexpected signal %u", |
| WTERMSIG(status)); |
| } |
| ASSERT_TRUE(WIFSTOPPED(status)) { |
| /* cannot happen */ |
| LOG_KILL_TRACEE("unexpected wait status %#x", status); |
| } |
| |
| ASSERT_LT(ptrace_stop, ARRAY_SIZE(si) * 2) { |
| LOG_KILL_TRACEE("ptrace stop overflow"); |
| } |
| |
| switch (WSTOPSIG(status)) { |
| case SIGSTOP: |
| ASSERT_EQ(0, ptrace_stop) { |
| LOG_KILL_TRACEE("unexpected signal stop"); |
| } |
| ASSERT_EQ(0, sys_ptrace(PTRACE_SETOPTIONS, tracee_pid, |
| 0, PTRACE_O_TRACESYSGOOD)) { |
| LOG_KILL_TRACEE("PTRACE_SETOPTIONS: %m"); |
| } |
| break; |
| |
| case SIGTRAP | 0x80: |
| ASSERT_LT(0, ptrace_stop) { |
| LOG_KILL_TRACEE("unexpected syscall stop"); |
| } |
| ASSERT_LT(0, (rc = sys_ptrace(PTRACE_GET_SYSCALL_INFO, |
| tracee_pid, size, |
| (uintptr_t) &info))) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO #1: %m"); |
| } |
| if (ptrace_stop & 1) { |
| /* entering syscall */ |
| const struct si_entry *exp_entry = |
| &si[ptrace_stop / 2].entry[0]; |
| const struct si_entry *set_entry = |
| &si[ptrace_stop / 2].entry[1]; |
| |
| /* check ptrace_syscall_info before the changes */ |
| ASSERT_EQ(expected_entry_size, rc) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO #1" |
| ": entry stop mismatch"); |
| } |
| check_psi_entry(_metadata, &info, exp_entry, |
| "PTRACE_GET_SYSCALL_INFO #1"); |
| |
| /* apply the changes */ |
| info.entry.nr = set_entry->nr; |
| for (i = 0; i < ARRAY_SIZE(set_entry->args); ++i) |
| info.entry.args[i] = set_entry->args[i]; |
| ASSERT_EQ(0, sys_ptrace(PTRACE_SET_SYSCALL_INFO, |
| tracee_pid, size, |
| (uintptr_t) &info)) { |
| LOG_KILL_TRACEE("PTRACE_SET_SYSCALL_INFO: %m"); |
| } |
| |
| /* check ptrace_syscall_info after the changes */ |
| memset(&info, 0, sizeof(info)); |
| info.op = 0xff; |
| ASSERT_LT(0, (rc = sys_ptrace(PTRACE_GET_SYSCALL_INFO, |
| tracee_pid, size, |
| (uintptr_t) &info))) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO: %m"); |
| } |
| ASSERT_EQ(expected_entry_size, rc) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO #2" |
| ": entry stop mismatch"); |
| } |
| check_psi_entry(_metadata, &info, set_entry, |
| "PTRACE_GET_SYSCALL_INFO #2"); |
| } else { |
| /* exiting syscall */ |
| const struct si_exit *exp_exit = |
| &si[ptrace_stop / 2 - 1].exit[0]; |
| const struct si_exit *set_exit = |
| &si[ptrace_stop / 2 - 1].exit[1]; |
| |
| /* check ptrace_syscall_info before the changes */ |
| ASSERT_EQ(expected_exit_size, rc) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO #1" |
| ": exit stop mismatch"); |
| } |
| check_psi_exit(_metadata, &info, exp_exit, |
| "PTRACE_GET_SYSCALL_INFO #1"); |
| |
| /* apply the changes */ |
| info.exit.is_error = set_exit->is_error; |
| info.exit.rval = set_exit->rval; |
| ASSERT_EQ(0, sys_ptrace(PTRACE_SET_SYSCALL_INFO, |
| tracee_pid, size, |
| (uintptr_t) &info)) { |
| LOG_KILL_TRACEE("PTRACE_SET_SYSCALL_INFO: %m"); |
| } |
| |
| /* check ptrace_syscall_info after the changes */ |
| memset(&info, 0, sizeof(info)); |
| info.op = 0xff; |
| ASSERT_LT(0, (rc = sys_ptrace(PTRACE_GET_SYSCALL_INFO, |
| tracee_pid, size, |
| (uintptr_t) &info))) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO #2: %m"); |
| } |
| ASSERT_EQ(expected_exit_size, rc) { |
| LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO #2" |
| ": exit stop mismatch"); |
| } |
| check_psi_exit(_metadata, &info, set_exit, |
| "PTRACE_GET_SYSCALL_INFO #2"); |
| } |
| break; |
| |
| default: |
| LOG_KILL_TRACEE("unexpected stop signal %u", |
| WSTOPSIG(status)); |
| abort(); |
| } |
| |
| ASSERT_EQ(0, sys_ptrace(PTRACE_SYSCALL, tracee_pid, 0, 0)) { |
| LOG_KILL_TRACEE("PTRACE_SYSCALL: %m"); |
| } |
| } |
| |
| ASSERT_EQ(ptrace_stop, ARRAY_SIZE(si) * 2); |
| } |
| |
| TEST_HARNESS_MAIN |