Add support for the RISC-V PTRACE_GETHBPREGS and PTRACE_SETHBPREGS requests. A tracer can use these requests to read or program one hardware breakpoint or watchpoint slot at a time. Store the ptrace-owned breakpoints in the traced task and use perf hardware breakpoint events to program the trigger. Report hits as SIGTRAP/TRAP_HWBKPT. Clear the events when a thread is flushed or copied so breakpoints do not survive execve() or fork(). Signed-off-by: Himanshu Chauhan --- arch/riscv/include/asm/hw_breakpoint.h | 13 + arch/riscv/include/asm/processor.h | 15 ++ arch/riscv/include/uapi/asm/ptrace.h | 9 + arch/riscv/kernel/hw_breakpoint.c | 2 - arch/riscv/kernel/process.c | 2 + arch/riscv/kernel/ptrace.c | 321 +++++++++++++++++++++++++ 6 files changed, 360 insertions(+), 2 deletions(-) diff --git a/arch/riscv/include/asm/hw_breakpoint.h b/arch/riscv/include/asm/hw_breakpoint.h index dcce20ba3faa..6e8913d69df2 100644 --- a/arch/riscv/include/asm/hw_breakpoint.h +++ b/arch/riscv/include/asm/hw_breakpoint.h @@ -12,10 +12,17 @@ struct task_struct; #include +/* Maximum number of hardware breakpoints supported */ +#define RISCV_MAX_BP 16 + struct arch_hw_breakpoint { unsigned long address; unsigned long len; unsigned int type; + unsigned int match; + unsigned int chain; + unsigned int select; + unsigned int time; /* Trigger configuration data */ unsigned long tdata1; @@ -42,8 +49,14 @@ void arch_disable_hw_breakpoint(struct perf_event *bp); int arch_install_hw_breakpoint(struct perf_event *bp); void arch_uninstall_hw_breakpoint(struct perf_event *bp); void hw_breakpoint_pmu_read(struct perf_event *bp); +void clear_ptrace_hw_breakpoint(struct task_struct *tsk); +void flush_ptrace_hw_breakpoint(struct task_struct *tsk); +void ptrace_hw_copy_thread(struct task_struct *task); #else +static inline void flush_ptrace_hw_breakpoint(struct task_struct *task) { } +static inline void ptrace_hw_copy_thread(struct task_struct *task) { } + #endif /* CONFIG_HAVE_HW_BREAKPOINT */ #endif /* __RISCV_HW_BREAKPOINT_H */ diff --git a/arch/riscv/include/asm/processor.h b/arch/riscv/include/asm/processor.h index e9d339bcb7e9..a124ea48d805 100644 --- a/arch/riscv/include/asm/processor.h +++ b/arch/riscv/include/asm/processor.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -111,6 +112,19 @@ struct pt_regs; #define RISCV_PREEMPT_V_NEED_RESTORE 0x40000000 #define RISCV_PREEMPT_V_IN_SCHEDULE 0x20000000 +struct debug_info { +#ifdef CONFIG_HAVE_HW_BREAKPOINT + /* Have we suspended stepping by a debugger? */ + int suspended_step; + /* Allow breakpoints and watchpoints to be disabled for this thread. */ + int bp_disabled; + int wp_disabled; + /* Hardware breakpoints pinned to this task. */ + struct perf_event *hbp_break[RISCV_MAX_BP]; + struct perf_event *hbp_watch[RISCV_MAX_BP]; +#endif +}; + /* CPU-specific state of a task */ struct thread_struct { /* Callee-saved registers */ @@ -135,6 +149,7 @@ struct thread_struct { #ifdef CONFIG_RISCV_ISA_SSQOSID u32 srmcfg; #endif + struct debug_info debug; }; /* Whitelist the fstate from the task_struct for hardened usercopy */ diff --git a/arch/riscv/include/uapi/asm/ptrace.h b/arch/riscv/include/uapi/asm/ptrace.h index 3de2b7124aff..373a1c14f207 100644 --- a/arch/riscv/include/uapi/asm/ptrace.h +++ b/arch/riscv/include/uapi/asm/ptrace.h @@ -15,6 +15,8 @@ #define PTRACE_GETFDPIC_EXEC 0 #define PTRACE_GETFDPIC_INTERP 1 +#define PTRACE_GETHBPREGS 0x4210 +#define PTRACE_SETHBPREGS 0x4211 /* * User-mode register state for core dumps, ptrace, sigcontext @@ -164,6 +166,13 @@ struct user_cfi_state { __u64 shstk_ptr; }; +struct __riscv_hwdebug_state { + __u64 addr; + __u64 type; + __u64 len; + __u64 ctrl; +}; + #endif /* __ASSEMBLER__ */ #endif /* _UAPI_ASM_RISCV_PTRACE_H */ diff --git a/arch/riscv/kernel/hw_breakpoint.c b/arch/riscv/kernel/hw_breakpoint.c index 421ecd8549d9..4231170d4901 100644 --- a/arch/riscv/kernel/hw_breakpoint.c +++ b/arch/riscv/kernel/hw_breakpoint.c @@ -846,8 +846,6 @@ EXPORT_SYMBOL_GPL(arch_disable_hw_breakpoint); void hw_breakpoint_pmu_read(struct perf_event *bp) { } -void flush_ptrace_hw_breakpoint(struct task_struct *tsk) { } - static int __init arch_hw_breakpoint_init(void) { unsigned int cpu; diff --git a/arch/riscv/kernel/process.c b/arch/riscv/kernel/process.c index 34c1d498d1ca..083c45e657b6 100644 --- a/arch/riscv/kernel/process.c +++ b/arch/riscv/kernel/process.c @@ -204,6 +204,7 @@ void flush_thread(void) if (riscv_has_extension_unlikely(RISCV_ISA_EXT_SUPM)) envcfg_update_bits(current, ENVCFG_PMM, ENVCFG_PMM_PMLEN_0); #endif + flush_ptrace_hw_breakpoint(current); } void arch_release_task_struct(struct task_struct *tsk) @@ -283,6 +284,7 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) p->thread.riscv_v_flags = 0; if (has_vector() || has_xtheadvector()) riscv_v_thread_alloc(p); + ptrace_hw_copy_thread(p); p->thread.sp = (unsigned long)childregs; /* kernel sp */ return 0; } diff --git a/arch/riscv/kernel/ptrace.c b/arch/riscv/kernel/ptrace.c index a0055743d3ba..be076865d02d 100644 --- a/arch/riscv/kernel/ptrace.c +++ b/arch/riscv/kernel/ptrace.c @@ -18,8 +18,10 @@ #include #include #include +#include #include #include +#include enum riscv_regset { REGSET_X, @@ -380,6 +382,152 @@ static int riscv_cfi_set(struct task_struct *target, } #endif +#ifdef CONFIG_HAVE_HW_BREAKPOINT +enum riscv_hbp_type { + RISCV_HBP_BREAK, + RISCV_HBP_WATCH, +}; + +/* + * Handle hitting a HW-breakpoint. + */ +static void riscv_ptrace_hbptriggered(struct perf_event *bp, + struct perf_sample_data *data, + struct pt_regs *regs) +{ + struct arch_hw_breakpoint *bkpt = counter_arch_bp(bp); + + force_sig_fault(SIGTRAP, TRAP_HWBKPT, (void __user *)bkpt->address); +} + +/* + * Unregister breakpoints from this task and reset the pointers in + * the thread_struct. + */ +void flush_ptrace_hw_breakpoint(struct task_struct *tsk) +{ + int i; + struct thread_struct *t = &tsk->thread; + + for (i = 0; i < RISCV_MAX_BP; i++) { + if (t->debug.hbp_break[i]) { + unregister_hw_breakpoint(t->debug.hbp_break[i]); + t->debug.hbp_break[i] = NULL; + } + } + + for (i = 0; i < RISCV_MAX_BP; i++) { + if (t->debug.hbp_watch[i]) { + unregister_hw_breakpoint(t->debug.hbp_watch[i]); + t->debug.hbp_watch[i] = NULL; + } + } +} + +void ptrace_hw_copy_thread(struct task_struct *tsk) +{ + memset(&tsk->thread.debug, 0, sizeof(struct debug_info)); +} + +static struct perf_event *ptrace_hbp_get_event(enum riscv_hbp_type hbp_type, + struct task_struct *tsk, + unsigned long idx) +{ + struct perf_event *bp = ERR_PTR(-EINVAL); + + switch (hbp_type) { + case RISCV_HBP_BREAK: + if (idx >= RISCV_MAX_BP) + goto out; + idx = array_index_nospec(idx, RISCV_MAX_BP); + bp = tsk->thread.debug.hbp_break[idx]; + break; + case RISCV_HBP_WATCH: + if (idx >= RISCV_MAX_BP) + goto out; + idx = array_index_nospec(idx, RISCV_MAX_BP); + bp = tsk->thread.debug.hbp_watch[idx]; + break; + } + +out: + return bp; +} + +static int ptrace_hbp_set_event(enum riscv_hbp_type hbp_type, + struct task_struct *tsk, + unsigned long idx, + struct perf_event *bp) +{ + int err = -EINVAL; + + switch (hbp_type) { + case RISCV_HBP_BREAK: + if (idx >= RISCV_MAX_BP) + goto out; + idx = array_index_nospec(idx, RISCV_MAX_BP); + tsk->thread.debug.hbp_break[idx] = bp; + err = 0; + break; + case RISCV_HBP_WATCH: + if (idx >= RISCV_MAX_BP) + goto out; + idx = array_index_nospec(idx, RISCV_MAX_BP); + tsk->thread.debug.hbp_watch[idx] = bp; + err = 0; + break; + } + +out: + return err; +} + +static struct perf_event *ptrace_hbp_create(enum riscv_hbp_type hbp_type, + struct task_struct *tsk, + unsigned long idx) +{ + struct perf_event *bp; + struct perf_event_attr attr; + int err, type; + + switch (hbp_type) { + case RISCV_HBP_BREAK: + type = HW_BREAKPOINT_X; + break; + case RISCV_HBP_WATCH: + type = HW_BREAKPOINT_RW; + break; + default: + return ERR_PTR(-EINVAL); + } + + ptrace_breakpoint_init(&attr); + + /* + * Initialise fields to sane defaults + * (i.e. values that will pass validation). + */ + attr.bp_addr = 0; + attr.bp_len = HW_BREAKPOINT_LEN_4; + attr.bp_type = type; + attr.disabled = 1; + + bp = register_user_hw_breakpoint(&attr, riscv_ptrace_hbptriggered, NULL, tsk); + if (IS_ERR(bp)) + return bp; + + err = ptrace_hbp_set_event(hbp_type, tsk, idx, bp); + if (err) { + /* Drop the event if it cannot be published in the task slot. */ + unregister_hw_breakpoint(bp); + return ERR_PTR(err); + } + + return bp; +} + +#endif /* CONFIG_HAVE_HW_BREAKPOINT */ + static struct user_regset riscv_user_regset[] __ro_after_init = { [REGSET_X] = { USER_REGSET_NOTE_TYPE(PRSTATUS), @@ -549,12 +697,173 @@ void ptrace_disable(struct task_struct *child) { } +#ifdef CONFIG_HAVE_HW_BREAKPOINT +static int riscv_ptrace_bp_type_to_hbp(u64 type, enum riscv_hbp_type *hbp_type) +{ + switch (type) { + case HW_BREAKPOINT_X: + *hbp_type = RISCV_HBP_BREAK; + return 0; + case HW_BREAKPOINT_R: + case HW_BREAKPOINT_W: + case HW_BREAKPOINT_RW: + *hbp_type = RISCV_HBP_WATCH; + return 0; + default: + return -EINVAL; + } +} + +static int riscv_ptrace_bp_clear_event(struct task_struct *child, + enum riscv_hbp_type hbp_type, + unsigned long idx) +{ + struct perf_event *bp = ptrace_hbp_get_event(hbp_type, child, idx); + + if (IS_ERR(bp)) + return PTR_ERR(bp); + if (!bp) + return 0; + + unregister_hw_breakpoint(bp); + return ptrace_hbp_set_event(hbp_type, child, idx, NULL); +} + +static int riscv_ptrace_bp_get(struct task_struct *child, unsigned long idx, + struct __riscv_hwdebug_state *state) +{ + struct perf_event *bp; + + if (idx >= RISCV_MAX_BP) + return -EINVAL; + + idx = array_index_nospec(idx, RISCV_MAX_BP); + + /* + * PTRACE_GETHBPREGS is a compact view keyed only by index. The real + * storage is split by type, so search both arrays. + */ + bp = ptrace_hbp_get_event(RISCV_HBP_BREAK, child, idx); + if (IS_ERR(bp)) + return PTR_ERR(bp); + if (!bp) { + bp = ptrace_hbp_get_event(RISCV_HBP_WATCH, child, idx); + if (IS_ERR(bp)) + return PTR_ERR(bp); + } + if (!bp) + return -ENOENT; + + state->addr = bp->attr.bp_addr; + state->len = bp->attr.bp_len; + state->type = bp->attr.bp_type; + state->ctrl = bp->attr.disabled == 1; + + return 0; +} + +static int riscv_ptrace_bp_set(struct task_struct *child, unsigned long idx, + struct __riscv_hwdebug_state *state) +{ + struct perf_event *bp; + struct perf_event_attr attr; + enum riscv_hbp_type hbp_type, other_hbp_type; + int ret; + bool created = false; + + if (idx >= RISCV_MAX_BP) + return -EINVAL; + + idx = array_index_nospec(idx, RISCV_MAX_BP); + + ret = riscv_ptrace_bp_type_to_hbp(state->type, &hbp_type); + if (ret) + return ret; + + /* The legacy request exposes ctrl as a disabled/enabled boolean. */ + if (state->ctrl > 1) + return -EINVAL; + + other_hbp_type = hbp_type == RISCV_HBP_BREAK ? + RISCV_HBP_WATCH : RISCV_HBP_BREAK; + + bp = ptrace_hbp_get_event(hbp_type, child, idx); + if (IS_ERR(bp)) + return PTR_ERR(bp); + if (!bp) { + bp = ptrace_hbp_create(hbp_type, child, idx); + if (IS_ERR(bp)) + return PTR_ERR(bp); + created = true; + } + + attr = bp->attr; + + attr.bp_addr = state->addr; + attr.bp_len = state->len; + attr.bp_type = state->type; + /* Enable or disable as requested by ctrl (0 = enabled, 1 = disabled) */ + attr.disabled = state->ctrl == 1; + ret = modify_user_hw_breakpoint(bp, &attr); + if (ret) { + /* Drop only events allocated by this failed request. */ + if (created) + riscv_ptrace_bp_clear_event(child, hbp_type, idx); + return ret; + } + + /* + * If the same index changed from breakpoint to watchpoint, or back, + * clear the old opposite-type slot after the new event is valid so a + * failed update does not destroy the previous state. + */ + return riscv_ptrace_bp_clear_event(child, other_hbp_type, idx); +} + +static long riscv_ptrace_gethbpregs(struct task_struct *child, unsigned long idx, + unsigned long __user *datap) +{ + struct __riscv_hwdebug_state state; + long ret; + + ret = riscv_ptrace_bp_get(child, idx, &state); + if (ret) + return ret; + if (copy_to_user(datap, &state, sizeof(state))) + return -EFAULT; + + return 0; +} + +static long riscv_ptrace_sethbpregs(struct task_struct *child, unsigned long idx, + unsigned long __user *datap) +{ + struct __riscv_hwdebug_state state; + + if (copy_from_user(&state, datap, sizeof(state))) + return -EFAULT; + + return riscv_ptrace_bp_set(child, idx, &state); +} +#endif /* CONFIG_HAVE_HW_BREAKPOINT */ + long arch_ptrace(struct task_struct *child, long request, unsigned long addr, unsigned long data) { long ret = -EIO; +#ifdef CONFIG_HAVE_HW_BREAKPOINT + unsigned long __user *datap = (unsigned long __user *)data; +#endif switch (request) { +#ifdef CONFIG_HAVE_HW_BREAKPOINT + case PTRACE_GETHBPREGS: + ret = riscv_ptrace_gethbpregs(child, addr, datap); + break; + case PTRACE_SETHBPREGS: + ret = riscv_ptrace_sethbpregs(child, addr, datap); + break; +#endif default: ret = ptrace_request(child, request, addr, data); break; @@ -623,8 +932,20 @@ long compat_arch_ptrace(struct task_struct *child, compat_long_t request, compat_ulong_t caddr, compat_ulong_t cdata) { long ret = -EIO; +#ifdef CONFIG_HAVE_HW_BREAKPOINT + /* cdata is a compat user pointer for the legacy HBP requests. */ + unsigned long __user *datap = compat_ptr(cdata); +#endif switch (request) { +#ifdef CONFIG_HAVE_HW_BREAKPOINT + case PTRACE_GETHBPREGS: + ret = riscv_ptrace_gethbpregs(child, caddr, datap); + break; + case PTRACE_SETHBPREGS: + ret = riscv_ptrace_sethbpregs(child, caddr, datap); + break; +#endif default: ret = compat_ptrace_request(child, request, caddr, cdata); break; -- 2.43.0