root/tools/testing/selftests/ptrace/set_syscall_info.c
// 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