blob: 2e0d86e0687e15dda924a5d5f5931b4860838b90 [file] [log] [blame]
// 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"
#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)
{
__u8 *pcvar;
__u16 *psvar;
__u32 *pivar;
__u64 *plvar;
switch (len) {
case 1:
pcvar = (__u8 *)&glvar;
*pcvar = 0xff;
break;
case 2:
psvar = (__u16 *)&glvar;
*psvar = 0xffff;
break;
case 4:
pivar = (__u32 *)&glvar;
*pivar = 0xffffffff;
break;
case 8:
plvar = (__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 = (__u8)glvar;
break;
case 2:
svar = (__u16)glvar;
break;
case 4:
ivar = (__u32)glvar;
break;
case 8:
lvar = (__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];
}
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_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);
}
}
}
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(dbginfo.num_data_bps == 0);
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)
{
int pvr = 0;
asm __volatile__ ("mfspr %0,%1" : "=r"(pvr) : "i"(SPRN_PVR));
if (pvr == PVR_8xx)
is_8xx = true;
return test_harness(ptrace_hwbreak, "ptrace-hwbreak");
}