root/tools/testing/selftests/ptrace/get_syscall_info.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (c) 2018 Dmitry V. Levin <ldv@altlinux.org>
 * All rights reserved.
 *
 * Check whether PTRACE_GET_SYSCALL_INFO semantics implemented in the kernel
 * matches userspace expectations.
 */

#include "kselftest_harness.h"
#include <err.h>
#include <signal.h>
#include <asm/unistd.h>
#include "linux/ptrace.h"

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(pid);                               \
                TH_LOG("wait #%d: " fmt,                        \
                       ptrace_stop, ##__VA_ARGS__);             \
        } while (0)

TEST(get_syscall_info)
{
        static const unsigned long args[][7] = {
                /* a sequence of architecture-agnostic syscalls */
                {
                        __NR_chdir,
                        (unsigned long) "",
                        0xbad1fed1,
                        0xbad2fed2,
                        0xbad3fed3,
                        0xbad4fed4,
                        0xbad5fed5
                },
                {
                        __NR_gettid,
                        0xcaf0bea0,
                        0xcaf1bea1,
                        0xcaf2bea2,
                        0xcaf3bea3,
                        0xcaf4bea4,
                        0xcaf5bea5
                },
                {
                        __NR_exit_group,
                        0,
                        0xfac1c0d1,
                        0xfac2c0d2,
                        0xfac3c0d3,
                        0xfac4c0d4,
                        0xfac5c0d5
                }
        };
        const unsigned long *exp_args;

        pid_t pid = fork();

        ASSERT_LE(0, pid) {
                TH_LOG("fork: %m");
        }

        if (pid == 0) {
                /* get the pid before PTRACE_TRACEME */
                pid = getpid();
                ASSERT_EQ(0, sys_ptrace(PTRACE_TRACEME, 0, 0, 0)) {
                        TH_LOG("PTRACE_TRACEME: %m");
                }
                ASSERT_EQ(0, kill(pid, SIGSTOP)) {
                        /* cannot happen */
                        TH_LOG("kill SIGSTOP: %m");
                }
                for (unsigned int i = 0; i < ARRAY_SIZE(args); ++i) {
                        syscall(args[i][0],
                                args[i][1], args[i][2], args[i][3],
                                args[i][4], args[i][5], args[i][6]);
                }
                /* unreachable */
                _exit(1);
        }

        const struct {
                unsigned int is_error;
                int rval;
        } *exp_param, exit_param[] = {
                { 1, -ENOENT }, /* chdir */
                { 0, pid }      /* gettid */
        };

        unsigned int ptrace_stop;

        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_none_size =
                        (void *) &info.entry - (void *) &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;
                long rc;

                ASSERT_EQ(pid, wait(&status)) {
                        /* cannot happen */
                        LOG_KILL_TRACEE("wait: %m");
                }
                if (WIFEXITED(status)) {
                        pid = 0;        /* the tracee is no more */
                        ASSERT_EQ(0, WEXITSTATUS(status));
                        break;
                }
                ASSERT_FALSE(WIFSIGNALED(status)) {
                        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);
                }

                switch (WSTOPSIG(status)) {
                case SIGSTOP:
                        ASSERT_EQ(0, ptrace_stop) {
                                LOG_KILL_TRACEE("unexpected signal stop");
                        }
                        ASSERT_EQ(0, sys_ptrace(PTRACE_SETOPTIONS, pid, 0,
                                                PTRACE_O_TRACESYSGOOD)) {
                                LOG_KILL_TRACEE("PTRACE_SETOPTIONS: %m");
                        }
                        ASSERT_LT(0, (rc = sys_ptrace(PTRACE_GET_SYSCALL_INFO,
                                                      pid, size,
                                                      (unsigned long) &info))) {
                                LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO: %m");
                        }
                        ASSERT_EQ(expected_none_size, rc) {
                                LOG_KILL_TRACEE("signal stop mismatch");
                        }
                        ASSERT_EQ(PTRACE_SYSCALL_INFO_NONE, info.op) {
                                LOG_KILL_TRACEE("signal stop mismatch");
                        }
                        ASSERT_TRUE(info.arch) {
                                LOG_KILL_TRACEE("signal stop mismatch");
                        }
                        ASSERT_TRUE(info.instruction_pointer) {
                                LOG_KILL_TRACEE("signal stop mismatch");
                        }
                        ASSERT_TRUE(info.stack_pointer) {
                                LOG_KILL_TRACEE("signal stop mismatch");
                        }
                        break;

                case SIGTRAP | 0x80:
                        ASSERT_LT(0, (rc = sys_ptrace(PTRACE_GET_SYSCALL_INFO,
                                                      pid, size,
                                                      (unsigned long) &info))) {
                                LOG_KILL_TRACEE("PTRACE_GET_SYSCALL_INFO: %m");
                        }
                        switch (ptrace_stop) {
                        case 1: /* entering chdir */
                        case 3: /* entering gettid */
                        case 5: /* entering exit_group */
                                exp_args = args[ptrace_stop / 2];
                                ASSERT_EQ(expected_entry_size, rc) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(PTRACE_SYSCALL_INFO_ENTRY, info.op) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_TRUE(info.arch) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_TRUE(info.instruction_pointer) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_TRUE(info.stack_pointer) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[0], info.entry.nr) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[1], info.entry.args[0]) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[2], info.entry.args[1]) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[3], info.entry.args[2]) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[4], info.entry.args[3]) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[5], info.entry.args[4]) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                ASSERT_EQ(exp_args[6], info.entry.args[5]) {
                                        LOG_KILL_TRACEE("entry stop mismatch");
                                }
                                break;
                        case 2: /* exiting chdir */
                        case 4: /* exiting gettid */
                                exp_param = &exit_param[ptrace_stop / 2 - 1];
                                ASSERT_EQ(expected_exit_size, rc) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                ASSERT_EQ(PTRACE_SYSCALL_INFO_EXIT, info.op) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                ASSERT_TRUE(info.arch) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                ASSERT_TRUE(info.instruction_pointer) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                ASSERT_TRUE(info.stack_pointer) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                ASSERT_EQ(exp_param->is_error,
                                          info.exit.is_error) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                ASSERT_EQ(exp_param->rval, info.exit.rval) {
                                        LOG_KILL_TRACEE("exit stop mismatch");
                                }
                                break;
                        default:
                                LOG_KILL_TRACEE("unexpected syscall stop");
                                abort();
                        }
                        break;

                default:
                        LOG_KILL_TRACEE("unexpected stop signal %#x",
                                        WSTOPSIG(status));
                        abort();
                }

                ASSERT_EQ(0, sys_ptrace(PTRACE_SYSCALL, pid, 0, 0)) {
                        LOG_KILL_TRACEE("PTRACE_SYSCALL: %m");
                }
        }

        ASSERT_EQ(ARRAY_SIZE(args) * 2, ptrace_stop);
}

TEST_HARNESS_MAIN