// SPDX-License-Identifier: GPL-2.0+ /* * Ptrace test for hw breakpoints * * Based on tools/testing/selftests/breakpoints/breakpoint_test.c * * This test forks and the parent then traces the child doing various * types of ptrace enabled breakpoints * * Copyright (C) 2018 Michael Neuling, IBM Corporation. */ #include <sys/ptrace.h> #include <unistd.h> #include <stddef.h> #include <sys/user.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/syscall.h> #include <linux/limits.h> #include "ptrace.h" #include "reg.h" #define SPRN_PVR 0x11F #define PVR_8xx 0x00500000 bool is_8xx; /* * Use volatile on all global var so that compiler doesn't * optimise their load/stores. Otherwise selftest can fail. */ static volatile __u64 glvar; #define DAWR_MAX_LEN 512 static volatile __u8 big_var[DAWR_MAX_LEN] __attribute__((aligned(512))); #define A_LEN 6 #define B_LEN 6 struct gstruct { __u8 a[A_LEN]; /* double word aligned */ __u8 b[B_LEN]; /* double word unaligned */ }; static volatile struct gstruct gstruct __attribute__((aligned(512))); static volatile char cwd[PATH_MAX] __attribute__((aligned(8))); static void get_dbginfo(pid_t child_pid, struct ppc_debug_info *dbginfo) { if (ptrace(PPC_PTRACE_GETHWDBGINFO, child_pid, NULL, dbginfo)) { perror("Can't get breakpoint info"); exit(-1); } } static bool dawr_present(struct ppc_debug_info *dbginfo) { return !!(dbginfo->features & PPC_DEBUG_FEATURE_DATA_BP_DAWR); } static void write_var(int len) { volatile __u8 *pcvar; volatile __u16 *psvar; volatile __u32 *pivar; volatile __u64 *plvar; switch (len) { case 1: pcvar = (volatile __u8 *)&glvar; *pcvar = 0xff; break; case 2: psvar = (volatile __u16 *)&glvar; *psvar = 0xffff; break; case 4: pivar = (volatile __u32 *)&glvar; *pivar = 0xffffffff; break; case 8: plvar = (volatile __u64 *)&glvar; *plvar = 0xffffffffffffffffLL; break; } } static void read_var(int len) { __u8 cvar __attribute__((unused)); __u16 svar __attribute__((unused)); __u32 ivar __attribute__((unused)); __u64 lvar __attribute__((unused)); switch (len) { case 1: cvar = (volatile __u8)glvar; break; case 2: svar = (volatile __u16)glvar; break; case 4: ivar = (volatile __u32)glvar; break; case 8: lvar = (volatile __u64)glvar; break; } } static void test_workload(void) { __u8 cvar __attribute__((unused)); __u32 ivar __attribute__((unused)); int len = 0; if (ptrace(PTRACE_TRACEME, 0, NULL, 0)) { perror("Child can't be traced?"); exit(-1); } /* Wake up father so that it sets up the first test */ kill(getpid(), SIGUSR1); /* PTRACE_SET_DEBUGREG, WO test */ for (len = 1; len <= sizeof(glvar); len <<= 1) write_var(len); /* PTRACE_SET_DEBUGREG, RO test */ for (len = 1; len <= sizeof(glvar); len <<= 1) read_var(len); /* PTRACE_SET_DEBUGREG, RW test */ for (len = 1; len <= sizeof(glvar); len <<= 1) { if (rand() % 2) read_var(len); else write_var(len); } /* PTRACE_SET_DEBUGREG, Kernel Access Userspace test */ syscall(__NR_getcwd, &cwd, PATH_MAX); /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, WO test */ write_var(1); /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, RO test */ read_var(1); /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, RW test */ if (rand() % 2) write_var(1); else read_var(1); /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, Kernel Access Userspace test */ syscall(__NR_getcwd, &cwd, PATH_MAX); /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED, WO test */ gstruct.a[rand() % A_LEN] = 'a'; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED, RO test */ cvar = gstruct.a[rand() % A_LEN]; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED, RW test */ if (rand() % 2) gstruct.a[rand() % A_LEN] = 'a'; else cvar = gstruct.a[rand() % A_LEN]; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, WO test */ gstruct.b[rand() % B_LEN] = 'b'; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, RO test */ cvar = gstruct.b[rand() % B_LEN]; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, RW test */ if (rand() % 2) gstruct.b[rand() % B_LEN] = 'b'; else cvar = gstruct.b[rand() % B_LEN]; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, DAR OUTSIDE, RW test */ if (rand() % 2) *((int *)(gstruct.a + 4)) = 10; else ivar = *((int *)(gstruct.a + 4)); /* PPC_PTRACE_SETHWDEBUG. DAWR_MAX_LEN. RW test */ if (rand() % 2) big_var[rand() % DAWR_MAX_LEN] = 'a'; else cvar = big_var[rand() % DAWR_MAX_LEN]; /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DW ALIGNED, WO test */ gstruct.a[rand() % A_LEN] = 'a'; /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DW UNALIGNED, RO test */ cvar = gstruct.b[rand() % B_LEN]; /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DAWR Overlap, WO test */ gstruct.a[rand() % A_LEN] = 'a'; /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DAWR Overlap, RO test */ cvar = gstruct.a[rand() % A_LEN]; } static void check_success(pid_t child_pid, const char *name, const char *type, unsigned long saddr, int len) { int status; siginfo_t siginfo; unsigned long eaddr = (saddr + len - 1) | 0x7; saddr &= ~0x7; /* Wait for the child to SIGTRAP */ wait(&status); ptrace(PTRACE_GETSIGINFO, child_pid, NULL, &siginfo); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP || (unsigned long)siginfo.si_addr < saddr || (unsigned long)siginfo.si_addr > eaddr) { printf("%s, %s, len: %d: Fail\n", name, type, len); exit(-1); } printf("%s, %s, len: %d: Ok\n", name, type, len); if (!is_8xx) { /* * For ptrace registered watchpoint, signal is generated * before executing load/store. Singlestep the instruction * and then continue the test. */ ptrace(PTRACE_SINGLESTEP, child_pid, NULL, 0); wait(NULL); } } static void ptrace_set_debugreg(pid_t child_pid, unsigned long wp_addr) { if (ptrace(PTRACE_SET_DEBUGREG, child_pid, 0, wp_addr)) { perror("PTRACE_SET_DEBUGREG failed"); exit(-1); } } static int ptrace_sethwdebug(pid_t child_pid, struct ppc_hw_breakpoint *info) { int wh = ptrace(PPC_PTRACE_SETHWDEBUG, child_pid, 0, info); if (wh <= 0) { perror("PPC_PTRACE_SETHWDEBUG failed"); exit(-1); } return wh; } static void ptrace_delhwdebug(pid_t child_pid, int wh) { if (ptrace(PPC_PTRACE_DELHWDEBUG, child_pid, 0, wh) < 0) { perror("PPC_PTRACE_DELHWDEBUG failed"); exit(-1); } } #define DABR_READ_SHIFT 0 #define DABR_WRITE_SHIFT 1 #define DABR_TRANSLATION_SHIFT 2 static int test_set_debugreg(pid_t child_pid) { unsigned long wp_addr = (unsigned long)&glvar; char *name = "PTRACE_SET_DEBUGREG"; int len; /* PTRACE_SET_DEBUGREG, WO test*/ wp_addr &= ~0x7UL; wp_addr |= (1UL << DABR_WRITE_SHIFT); wp_addr |= (1UL << DABR_TRANSLATION_SHIFT); for (len = 1; len <= sizeof(glvar); len <<= 1) { ptrace_set_debugreg(child_pid, wp_addr); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "WO", wp_addr, len); } /* PTRACE_SET_DEBUGREG, RO test */ wp_addr &= ~0x7UL; wp_addr |= (1UL << DABR_READ_SHIFT); wp_addr |= (1UL << DABR_TRANSLATION_SHIFT); for (len = 1; len <= sizeof(glvar); len <<= 1) { ptrace_set_debugreg(child_pid, wp_addr); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RO", wp_addr, len); } /* PTRACE_SET_DEBUGREG, RW test */ wp_addr &= ~0x7UL; wp_addr |= (1Ul << DABR_READ_SHIFT); wp_addr |= (1UL << DABR_WRITE_SHIFT); wp_addr |= (1UL << DABR_TRANSLATION_SHIFT); for (len = 1; len <= sizeof(glvar); len <<= 1) { ptrace_set_debugreg(child_pid, wp_addr); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RW", wp_addr, len); } ptrace_set_debugreg(child_pid, 0); return 0; } static int test_set_debugreg_kernel_userspace(pid_t child_pid) { unsigned long wp_addr = (unsigned long)cwd; char *name = "PTRACE_SET_DEBUGREG"; /* PTRACE_SET_DEBUGREG, Kernel Access Userspace test */ wp_addr &= ~0x7UL; wp_addr |= (1Ul << DABR_READ_SHIFT); wp_addr |= (1UL << DABR_WRITE_SHIFT); wp_addr |= (1UL << DABR_TRANSLATION_SHIFT); ptrace_set_debugreg(child_pid, wp_addr); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "Kernel Access Userspace", wp_addr, 8); ptrace_set_debugreg(child_pid, 0); return 0; } static void get_ppc_hw_breakpoint(struct ppc_hw_breakpoint *info, int type, unsigned long addr, int len) { info->version = 1; info->trigger_type = type; info->condition_mode = PPC_BREAKPOINT_CONDITION_NONE; info->addr = (__u64)addr; info->addr2 = (__u64)addr + len; info->condition_value = 0; if (!len) info->addr_mode = PPC_BREAKPOINT_MODE_EXACT; else info->addr_mode = PPC_BREAKPOINT_MODE_RANGE_INCLUSIVE; } static void test_sethwdebug_exact(pid_t child_pid) { struct ppc_hw_breakpoint info; unsigned long wp_addr = (unsigned long)&glvar; char *name = "PPC_PTRACE_SETHWDEBUG, MODE_EXACT"; int len = 1; /* hardcoded in kernel */ int wh; /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, WO test */ get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr, 0); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "WO", wp_addr, len); ptrace_delhwdebug(child_pid, wh); /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, RO test */ get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_READ, wp_addr, 0); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RO", wp_addr, len); ptrace_delhwdebug(child_pid, wh); /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, RW test */ get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_RW, wp_addr, 0); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RW", wp_addr, len); ptrace_delhwdebug(child_pid, wh); } static void test_sethwdebug_exact_kernel_userspace(pid_t child_pid) { struct ppc_hw_breakpoint info; unsigned long wp_addr = (unsigned long)&cwd; char *name = "PPC_PTRACE_SETHWDEBUG, MODE_EXACT"; int len = 1; /* hardcoded in kernel */ int wh; /* PPC_PTRACE_SETHWDEBUG, MODE_EXACT, Kernel Access Userspace test */ get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr, 0); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "Kernel Access Userspace", wp_addr, len); ptrace_delhwdebug(child_pid, wh); } static void test_sethwdebug_range_aligned(pid_t child_pid) { struct ppc_hw_breakpoint info; unsigned long wp_addr; char *name = "PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED"; int len; int wh; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED, WO test */ wp_addr = (unsigned long)&gstruct.a; len = A_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "WO", wp_addr, len); ptrace_delhwdebug(child_pid, wh); /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED, RO test */ wp_addr = (unsigned long)&gstruct.a; len = A_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_READ, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RO", wp_addr, len); ptrace_delhwdebug(child_pid, wh); /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW ALIGNED, RW test */ wp_addr = (unsigned long)&gstruct.a; len = A_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_RW, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RW", wp_addr, len); ptrace_delhwdebug(child_pid, wh); } static void test_multi_sethwdebug_range(pid_t child_pid) { struct ppc_hw_breakpoint info1, info2; unsigned long wp_addr1, wp_addr2; char *name1 = "PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DW ALIGNED"; char *name2 = "PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DW UNALIGNED"; int len1, len2; int wh1, wh2; wp_addr1 = (unsigned long)&gstruct.a; wp_addr2 = (unsigned long)&gstruct.b; len1 = A_LEN; len2 = B_LEN; get_ppc_hw_breakpoint(&info1, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr1, len1); get_ppc_hw_breakpoint(&info2, PPC_BREAKPOINT_TRIGGER_READ, wp_addr2, len2); /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DW ALIGNED, WO test */ wh1 = ptrace_sethwdebug(child_pid, &info1); /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DW UNALIGNED, RO test */ wh2 = ptrace_sethwdebug(child_pid, &info2); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name1, "WO", wp_addr1, len1); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name2, "RO", wp_addr2, len2); ptrace_delhwdebug(child_pid, wh1); ptrace_delhwdebug(child_pid, wh2); } static void test_multi_sethwdebug_range_dawr_overlap(pid_t child_pid) { struct ppc_hw_breakpoint info1, info2; unsigned long wp_addr1, wp_addr2; char *name = "PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DAWR Overlap"; int len1, len2; int wh1, wh2; wp_addr1 = (unsigned long)&gstruct.a; wp_addr2 = (unsigned long)&gstruct.a; len1 = A_LEN; len2 = A_LEN; get_ppc_hw_breakpoint(&info1, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr1, len1); get_ppc_hw_breakpoint(&info2, PPC_BREAKPOINT_TRIGGER_READ, wp_addr2, len2); /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DAWR Overlap, WO test */ wh1 = ptrace_sethwdebug(child_pid, &info1); /* PPC_PTRACE_SETHWDEBUG 2, MODE_RANGE, DAWR Overlap, RO test */ wh2 = ptrace_sethwdebug(child_pid, &info2); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "WO", wp_addr1, len1); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RO", wp_addr2, len2); ptrace_delhwdebug(child_pid, wh1); ptrace_delhwdebug(child_pid, wh2); } static void test_sethwdebug_range_unaligned(pid_t child_pid) { struct ppc_hw_breakpoint info; unsigned long wp_addr; char *name = "PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED"; int len; int wh; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, WO test */ wp_addr = (unsigned long)&gstruct.b; len = B_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "WO", wp_addr, len); ptrace_delhwdebug(child_pid, wh); /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, RO test */ wp_addr = (unsigned long)&gstruct.b; len = B_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_READ, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RO", wp_addr, len); ptrace_delhwdebug(child_pid, wh); /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, RW test */ wp_addr = (unsigned long)&gstruct.b; len = B_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_RW, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RW", wp_addr, len); ptrace_delhwdebug(child_pid, wh); } static void test_sethwdebug_range_unaligned_dar(pid_t child_pid) { struct ppc_hw_breakpoint info; unsigned long wp_addr; char *name = "PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, DAR OUTSIDE"; int len; int wh; /* PPC_PTRACE_SETHWDEBUG, MODE_RANGE, DW UNALIGNED, DAR OUTSIDE, RW test */ wp_addr = (unsigned long)&gstruct.b; len = B_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_WRITE, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RW", wp_addr, len); ptrace_delhwdebug(child_pid, wh); } static void test_sethwdebug_dawr_max_range(pid_t child_pid) { struct ppc_hw_breakpoint info; unsigned long wp_addr; char *name = "PPC_PTRACE_SETHWDEBUG, DAWR_MAX_LEN"; int len; int wh; /* PPC_PTRACE_SETHWDEBUG, DAWR_MAX_LEN, RW test */ wp_addr = (unsigned long)big_var; len = DAWR_MAX_LEN; get_ppc_hw_breakpoint(&info, PPC_BREAKPOINT_TRIGGER_RW, wp_addr, len); wh = ptrace_sethwdebug(child_pid, &info); ptrace(PTRACE_CONT, child_pid, NULL, 0); check_success(child_pid, name, "RW", wp_addr, len); ptrace_delhwdebug(child_pid, wh); } /* Set the breakpoints and check the child successfully trigger them */ static void run_tests(pid_t child_pid, struct ppc_debug_info *dbginfo, bool dawr) { test_set_debugreg(child_pid); test_set_debugreg_kernel_userspace(child_pid); test_sethwdebug_exact(child_pid); test_sethwdebug_exact_kernel_userspace(child_pid); if (dbginfo->features & PPC_DEBUG_FEATURE_DATA_BP_RANGE) { test_sethwdebug_range_aligned(child_pid); if (dawr || is_8xx) { test_sethwdebug_range_unaligned(child_pid); test_sethwdebug_range_unaligned_dar(child_pid); test_sethwdebug_dawr_max_range(child_pid); if (dbginfo->num_data_bps > 1) { test_multi_sethwdebug_range(child_pid); test_multi_sethwdebug_range_dawr_overlap(child_pid); } } } } static int ptrace_hwbreak(void) { pid_t child_pid; struct ppc_debug_info dbginfo; bool dawr; child_pid = fork(); if (!child_pid) { test_workload(); return 0; } wait(NULL); get_dbginfo(child_pid, &dbginfo); SKIP_IF_MSG(dbginfo.num_data_bps == 0, "No data breakpoints present"); dawr = dawr_present(&dbginfo); run_tests(child_pid, &dbginfo, dawr); /* Let the child exit first. */ ptrace(PTRACE_CONT, child_pid, NULL, 0); wait(NULL); /* * Testcases exits immediately with -1 on any failure. If * it has reached here, it means all tests were successful. */ return TEST_PASS; } int main(int argc, char **argv, char **envp) { is_8xx = mfspr(SPRN_PVR) == PVR_8xx; return test_harness(ptrace_hwbreak, "ptrace-hwbreak"); }