Re: [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints
From: Rui Qi
Date: Tue Sep 15 2026 - 05:05:42 EST
On 8/3/26 9:49 PM, Himanshu Chauhan wrote:
> Add ptrace support for hardware breakpoints and watchpoints on
> RISC-V. Debuggers can now set and query hardware debug triggers
> through the standard PTRACE_GETREGSET/SETREGSET interface using new
> NT_RISCV_HW_BREAK/WATCH note types, backed by
> register_user_hw_breakpoint()/modify_user_hw_breakpoint() and
> delivering SIGTRAP/TRAP_HWBKPT to the tracee when a trigger fires.
>
> For convenience, also add a simpler PTRACE_GETHBPREGS/SETHBPREGS
> request pair that lets a tracer read or write a single breakpoint or
> watchpoint directly, without going through the regset machinery.
> These request numbers live in the arch-specific ptrace range
> (0x4210/0x4211) so they don't collide with the generic
> PTRACE_PEEKDATA/PTRACE_PEEKUSR codes.
>
> Breakpoints and watchpoints share the same trigger pool on this
> architecture, so select HAVE_MIXED_BREAKPOINTS_REGS. Hook up
> thread flush/copy so per-task breakpoints are cleaned up and cleared
> across fork/exec.
>
> Signed-off-by: Himanshu Chauhan <himanshu.chauhan@xxxxxxxxxxxxxxxx>
> ---
> arch/riscv/Kconfig | 1 +
> arch/riscv/include/asm/hw_breakpoint.h | 10 +
> arch/riscv/include/asm/processor.h | 18 +
> arch/riscv/include/uapi/asm/ptrace.h | 50 +++
> arch/riscv/kernel/hw_breakpoint.c | 1 -
> arch/riscv/kernel/process.c | 5 +
> arch/riscv/kernel/ptrace.c | 507 +++++++++++++++++++++++++
> include/uapi/linux/elf.h | 4 +
> tools/include/uapi/linux/elf.h | 2 +
> 9 files changed, 597 insertions(+), 1 deletion(-)
>
> diff --git a/arch/riscv/Kconfig b/arch/riscv/Kconfig
> index a624dacdaf12..4d2913b49817 100644
> --- a/arch/riscv/Kconfig
> +++ b/arch/riscv/Kconfig
> @@ -173,6 +173,7 @@ config RISCV
> select HAVE_GCC_PLUGINS
> select HAVE_GENERIC_VDSO if MMU
> select HAVE_HW_BREAKPOINT if PERF_EVENTS
> + select HAVE_MIXED_BREAKPOINTS_REGS
RISC-V selects HAVE_MIXED_BREAKPOINTS_REGS unconditionally:
select HAVE_HW_BREAKPOINT if PERF_EVENTS
select HAVE_MIXED_BREAKPOINTS_REGS # unconditional
but that symbol has a direct dependency:
config HAVE_MIXED_BREAKPOINTS_REGS
bool
depends on HAVE_HW_BREAKPOINT # arch/Kconfig:401
and HAVE_HW_BREAKPOINT itself depends on
PERF_EVENTS (arch/Kconfig:399), which RISC-V only selects
conditionally (the line above).
So with PERF_EVENTS disabled — a valid config for minimal/embedded
RISC-V builds — HAVE_HW_BREAKPOINT is off, yet
HAVE_MIXED_BREAKPOINTS_REGS is forced on. Kconfig then warns about
unmet direct dependencies:
warning: (RISCV) selects HAVE_MIXED_BREAKPOINTS_REGS whose direct
dependencies (HAVE_HW_BREAKPOINT) are not met
The fix is to gate the select the same way as the line above it:
select HAVE_MIXED_BREAKPOINTS_REGS if PERF_EVENTS
(or "if HAVE_HW_BREAKPOINT" if you prefer the more direct
expression — both are equivalent here since HAVE_HW_BREAKPOINT
itself depends on PERF_EVENTS).
Thanks,
Rui Qi
> select HAVE_IRQ_TIME_ACCOUNTING
> select HAVE_KERNEL_BZIP2 if !EFI_ZBOOT
> select HAVE_KERNEL_GZIP if !EFI_ZBOOT
> diff --git a/arch/riscv/include/asm/hw_breakpoint.h b/arch/riscv/include/asm/hw_breakpoint.h
> index 4df1bfe0507e..94ebdd1ab9c3 100644
> --- a/arch/riscv/include/asm/hw_breakpoint.h
> +++ b/arch/riscv/include/asm/hw_breakpoint.h
> @@ -14,6 +14,7 @@ struct task_struct;
>
> /* Maximum number of hardware breakpoints supported */
> #define RISCV_HW_BP_NUM_MAX 32
> +#define RISCV_MAX_BP 16
>
> #if __riscv_xlen == 64
> #define cpu_to_le cpu_to_le64
> @@ -256,6 +257,10 @@ 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;
> @@ -282,8 +287,13 @@ 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 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 812517b2cec1..421bb6773015 100644
> --- a/arch/riscv/include/asm/processor.h
> +++ b/arch/riscv/include/asm/processor.h
> @@ -15,6 +15,7 @@
> #include <asm/ptrace.h>
> #include <asm/insn-def.h>
> #include <asm/alternative-macros.h>
> +#include <asm/hw_breakpoint.h>
> #include <asm/hwcap.h>
> #include <asm/usercfi.h>
>
> @@ -102,6 +103,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 */
> @@ -122,6 +136,10 @@ struct thread_struct {
> bool force_icache_flush;
> /* A forced icache flush is not needed if migrating to the previous cpu. */
> unsigned int prev_cpu;
> +#endif
> + struct debug_info debug;
> +#ifdef CONFIG_HAVE_HW_BREAKPOINT
> + struct perf_event *ptrace_bps[RISCV_HW_BP_NUM_MAX];
> #endif
> };
>
> diff --git a/arch/riscv/include/uapi/asm/ptrace.h b/arch/riscv/include/uapi/asm/ptrace.h
> index 3de2b7124aff..9b0acc0537fa 100644
> --- a/arch/riscv/include/uapi/asm/ptrace.h
> +++ b/arch/riscv/include/uapi/asm/ptrace.h
> @@ -10,11 +10,14 @@
>
> #include <linux/types.h>
> #include <linux/const.h>
> +#include <linux/bits.h>
>
> #define PTRACE_GETFDPIC 33
>
> #define PTRACE_GETFDPIC_EXEC 0
> #define PTRACE_GETFDPIC_INTERP 1
> +#define PTRACE_GETHBPREGS 0x4210
> +#define PTRACE_SETHBPREGS 0x4211
>
PTRACE_GETHBPREGS and PTRACE_SETHBPREGS are defined as 0x4210 and 0x4211
(arch/riscv/include/uapi/asm/ptrace.h), but those values are already
taken generically by PTRACE_SET_SYSCALL_USER_DISPATCH_CONFIG and
PTRACE_GET_SYSCALL_USER_DISPATCH_CONFIG (include/uapi/linux/ptrace.h),
and the whole 0x4200-0x4300 range is explicitly reserved for
architecture-independent additions.
Because sys_ptrace() calls arch_ptrace() before ptrace_request(), and
RISC-V's arch_ptrace() handles GETHBPREGS/SETHBPREGS under
CONFIG_HAVE_HW_BREAKPOINT before the default fall-through, a tracer
issuing the generic syscall-user-dispatch config requests gets routed
into the hwbreak handlers instead — and GET/SET land on the wrong
handler. So syscall-user-dispatch config is broken on RISC-V whenever
hardware breakpoints are enabled.
Thanks,
Rui Qi
> /*
> * User-mode register state for core dumps, ptrace, sigcontext
> @@ -164,6 +167,53 @@ struct user_cfi_state {
> __u64 shstk_ptr;
> };
>
> +/*
> + * bit[3:0] Match
> + * bit[8:4] Size
> + * bit[11:9] When
> + * bit[12] Select
> + * bit[13] Chain
> + * bit[16:14] Type
> + * bit[XLEN-1:17] Reserved
> +*/
> +#define HWDEBUG_MATCH_MASK __GENMASK(3, 0)
> +#define HWDEBUG_SIZE_MASK __GENMASK(8, 4)
> +#define HWDEBUG_WHEN_MASK __GENMASK(11, 9)
> +#define HWDEBUG_SELECT_MASK _BITUL(12)
> +#define HWDEBUG_CHAIN_MASK _BITUL(13)
> +#define HWDEBUG_TYPE_MASK __GENMASK(16, 14)
> +
> +#define HWDEBUG_MATCH(_ctrl) (((_ctrl) & HWDEBUG_MATCH_MASK) >> 0)
> +#define HWDEBUG_SIZE(_ctrl) (((_ctrl) & HWDEBUG_SIZE_MASK) >> 4)
> +#define HWDEBUG_WHEN(_ctrl) (((_ctrl) & HWDEBUG_WHEN_MASK) >> 9)
> +#define HWDEBUG_SELECT(_ctrl) (((_ctrl) & HWDEBUG_SELECT_MASK) >> 12)
> +#define HWDEBUG_CHAIN(_ctrl) (((_ctrl) & HWDEBUG_CHAIN_MASK) >> 13)
> +#define HWDEBUG_TYPE(_ctrl) (((_ctrl) & HWDEBUG_TYPE_MASK) >> 14)
> +
> +#define HWDEBUG_MK_MATCH(_match) ((_match << 0) & HWDEBUG_MATCH_MASK)
> +#define HWDEBUG_MK_SIZE(_sz) ((_sz << 4) & HWDEBUG_SIZE_MASK)
> +#define HWDEBUG_MK_WHEN(_when) ((_when << 9) & HWDEBUG_WHEN_MASK)
> +#define HWDEBUG_MK_SELECT(_sel) ((_sel << 12) & HWDEBUG_SELECT_MASK)
> +#define HWDEBUG_MK_CHAIN(_chain) ((_chain << 13) & HWDEBUG_CHAIN_MASK)
> +#define HWDEBUG_MK_TYPE(_type) ((_type << 14) & HWDEBUG_TYPE_MASK)
> +
> +struct user_hwdebug_state {
> + __u32 info;
> + __u32 pad;
> + struct {
> + __u64 addr;
> + __u32 control;
> + __u32 pad;
> + } dbg_regs[16];
> +};
> +
> +struct __riscv_hwdebug_state {
> + unsigned long addr;
> + unsigned long type;
> + unsigned long len;
> + unsigned long ctrl;
> +} __packed;
> +
> #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 fc54a1a897c5..b4fab41cd3f8 100644
> --- a/arch/riscv/kernel/hw_breakpoint.c
> +++ b/arch/riscv/kernel/hw_breakpoint.c
> @@ -630,7 +630,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)
> {
> diff --git a/arch/riscv/kernel/process.c b/arch/riscv/kernel/process.c
> index b2df7f72241a..b20b5fbeb4e3 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,10 @@ 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);
> +#ifdef CONFIG_HAVE_HW_BREAKPOINT
> + memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
> +#endif
> 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 f336a183667e..2d4ee51e6859 100644
> --- a/arch/riscv/kernel/ptrace.c
> +++ b/arch/riscv/kernel/ptrace.c
> @@ -18,8 +18,10 @@
> #include <linux/elf.h>
> #include <linux/regset.h>
> #include <linux/sched.h>
> +#include <linux/sched/signal.h>
> #include <linux/sched/task_stack.h>
> #include <asm/usercfi.h>
> +#include <linux/hw_breakpoint.h>
>
> enum riscv_regset {
> REGSET_X,
> @@ -35,6 +37,10 @@ enum riscv_regset {
> #ifdef CONFIG_RISCV_USER_CFI
> REGSET_CFI,
> #endif
> +#ifdef CONFIG_HAVE_HW_BREAKPOINT
> + REGSET_RISCV_HW_BREAK,
> + REGSET_RISCV_HW_WATCH,
> +#endif
> };
>
> static int riscv_gpr_get(struct task_struct *target,
> @@ -372,6 +378,397 @@ static int riscv_cfi_set(struct task_struct *target,
> }
> #endif
>
> +#ifdef CONFIG_HAVE_HW_BREAKPOINT
> +/*
> + * 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;
> + }
> + }
> +}
The direct request path and the regset path store their events in
different arrays:
- PTRACE_SETHBPREGS stores into thread.ptrace_bps[idx]
- The regset path (NT_RISCV_HW_BREAK/WATCH) stores into
thread.debug.hbp_break[idx] / hbp_watch[idx]
But flush_ptrace_hw_breakpoint() only unregisters the latter — it
walks hbp_break[] and hbp_watch[], calls unregister_hw_breakpoint()
and NULLs them, and never touches ptrace_bps[]
On exec, flush_thread() calls only flush_ptrace_hw_breakpoint()
and the generic perf exec path perf_event_remove_on_exec()
(kernel/events/core.c) skips events without attr->remove_on_exec — which
ptrace_breakpoint_init() never sets.
Net result: an event installed via the direct PTRACE_SETHBPREGS
request sits in ptrace_bps[], is not touched by
flush_ptrace_hw_breakpoint(), and is not removed by the generic exec
path either. So a normal, dumpable exec can keep the old image's
hardware triggers armed — pointing at addresses that, after exec,
may map to unrelated code or data in the new image (spurious
triggers or missed ones). The events only get cleaned at task exit
via perf_event_exit_task().
> +
> +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(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx)
> +{
> + struct perf_event *bp = ERR_PTR(-EINVAL);
> +
> + switch (note_type) {
> + case NT_RISCV_HW_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 NT_RISCV_HW_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(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx,
> + struct perf_event *bp)
> +{
> + int err = -EINVAL;
> +
> + switch (note_type) {
> + case NT_RISCV_HW_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 NT_RISCV_HW_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(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx)
> +{
> + struct perf_event *bp;
> + struct perf_event_attr attr;
> + int err, type;
> +
> + switch (note_type) {
> + case NT_RISCV_HW_BREAK:
> + type = HW_BREAKPOINT_X;
> + break;
> + case NT_RISCV_HW_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(note_type, tsk, idx, bp);
> + if (err)
> + return ERR_PTR(err);
> +
> + return bp;
> +}
> +
> +static int ptrace_hbp_fill_attr_ctrl(unsigned int note_type,
> + struct arch_hw_breakpoint *bpctrl,
> + struct perf_event_attr *attr)
> +{
> + int len, type;
> +
> + attr->disabled = 0;
> + type = bpctrl->type;
> + len = bpctrl->len;
> +
> + switch (note_type) {
> + case NT_RISCV_HW_BREAK:
> + if ((type & HW_BREAKPOINT_X) != type)
> + return -EINVAL;
> + break;
> + case NT_RISCV_HW_WATCH:
> + if ((type & HW_BREAKPOINT_RW) != type)
> + return -EINVAL;
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + attr->bp_len = len;
> + attr->bp_type = type;
> + attr->bp_addr = bpctrl->address;
> +
> + return 0;
> +}
> +
a slot programmed via the
NT_RISCV_HW_BREAK/WATCH regset cannot be disabled through the same ABI.
The control word (uapi/asm/ptrace.h) has no enabled bit, and
ptrace_hbp_fill_attr_ctrl() unconditionally sets attr->disabled = 0. A
debugger's natural disable attempt — writing a zero control word — fails:
Tracing that path:
1. decode_ctrl_reg(0, &bpctrl) leaves bpctrl->type == 0 and bpctrl->len
== 0;
2. ptrace_hbp_fill_attr_ctrl() sets attr->disabled = 0 (i.e. enabled),
and the validation checks ((type & HW_BREAKPOINT_X) != type) / ((type &
HW_BREAKPOINT_RW) != type) both pass for type == 0, since 0 is a subset
of any mask;
3.so attr->bp_type is set to 0, i.e. HW_BREAKPOINT_EMPTY, with
attr->disabled = 0;
4. modify_user_hw_breakpoint() then calls hw_breakpoint_arch_parse() ->
rv_init_mcontrol_trigger(), whose switch (attr->bp_type) hits default:
return -EINVAL; for bp_type == 0.
The modify_user_hw_breakpoint() call therefore fails, the previous
hardware breakpoint stays armed, and the write appears to "succeed" at
the regset layer only to fail silently underneath. A slot created via
the regset is thus reprogrammable, but not disableable, until exec or
task exit.
For contrast, arm64's ptrace_hbp_fill_attr_ctrl() reads a real
ctrl.enabled bit, computes disabled = !ctrl.enabled, and returns early
(attr->disabled = 1)
when the slot is disabled, so a zero/disabled control word is a
legitimate operation there. RISC-V has no equivalent path.
> +static int ptrace_hbp_get_resource_info(unsigned int note_type, u32 *info)
> +{
> + u8 num;
> +
> + switch (note_type) {
> + case NT_RISCV_HW_BREAK:
> + num = hw_breakpoint_slots(TYPE_INST);
> + break;
> + case NT_RISCV_HW_WATCH:
> + num = hw_breakpoint_slots(TYPE_DATA);
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + *info = num;
> +
> + return 0;
> +}
> +
> +static u32 encode_ctrl_reg(struct perf_event *bp)
> +{
> + struct arch_hw_breakpoint *bpctrl = counter_arch_bp(bp);
> + u32 ctrl = 0;
> +
> + /* Expose the generic UAPI bp_type values in ptrace control bits. */
> + ctrl |= HWDEBUG_MK_TYPE(bp->attr.bp_type);
> + ctrl |= HWDEBUG_MK_MATCH(bpctrl->match);
> + ctrl |= HWDEBUG_MK_SELECT(bpctrl->select);
> + ctrl |= HWDEBUG_MK_WHEN(bpctrl->time);
> + ctrl |= HWDEBUG_MK_SIZE(bp->attr.bp_len);
> + ctrl |= HWDEBUG_MK_CHAIN(bpctrl->chain);
> +
> + return ctrl;
> +}
> +
> +static int ptrace_hbp_get_ctrl(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx,
> + u32 *ctrl)
> +{
> + struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
> +
> + if (IS_ERR(bp))
> + return PTR_ERR(bp);
> +
> + *ctrl = bp ? encode_ctrl_reg(bp) : 0;
> + return 0;
> +}
> +
> +static int ptrace_hbp_get_addr(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx,
> + u64 *addr)
> +{
> + struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
> +
> + if (IS_ERR(bp))
> + return PTR_ERR(bp);
> +
> + *addr = bp ? counter_arch_bp(bp)->address : 0;
> + return 0;
> +}
> +
> +static struct perf_event *ptrace_hbp_get_initialised_bp(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx)
> +{
> + struct perf_event *bp = ptrace_hbp_get_event(note_type, tsk, idx);
> +
> + if (!bp)
> + bp = ptrace_hbp_create(note_type, tsk, idx);
> +
> + return bp;
> +}
> +
> +static void decode_ctrl_reg(u32 uctrl, struct arch_hw_breakpoint *bpctrl)
> +{
> + bpctrl->type = HWDEBUG_TYPE(uctrl);
> + bpctrl->match = HWDEBUG_MATCH(uctrl);
> + bpctrl->select = HWDEBUG_SELECT(uctrl);
> + bpctrl->time = HWDEBUG_WHEN(uctrl);
> + bpctrl->len = HWDEBUG_SIZE(uctrl);
> + bpctrl->chain = HWDEBUG_CHAIN(uctrl);
> +}
> +
> +static int ptrace_hbp_set_ctrl(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx,
> + u32 uctrl)
> +{
> + int err;
> + struct perf_event *bp;
> + struct perf_event_attr attr;
> + struct arch_hw_breakpoint bpctrl;
> +
> + bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx);
> + if (IS_ERR(bp)) {
> + err = PTR_ERR(bp);
> + return err;
> + }
> +
> + attr = bp->attr;
> + decode_ctrl_reg(uctrl, &bpctrl);
> + bpctrl.address = attr.bp_addr;
> + err = ptrace_hbp_fill_attr_ctrl(note_type, &bpctrl, &attr);
> + if (err)
> + return err;
> +
> + return modify_user_hw_breakpoint(bp, &attr);
> +}
> +
> +static int ptrace_hbp_set_addr(unsigned int note_type,
> + struct task_struct *tsk,
> + unsigned long idx,
> + u64 addr)
> +{
> + int err;
> + struct perf_event *bp;
> + struct perf_event_attr attr;
> +
> + bp = ptrace_hbp_get_initialised_bp(note_type, tsk, idx);
> + if (IS_ERR(bp)) {
> + err = PTR_ERR(bp);
> + return err;
> + }
> +
> + attr = bp->attr;
> + attr.bp_addr = addr;
> + err = modify_user_hw_breakpoint(bp, &attr);
> + return err;
> +}
> +
> +#define PTRACE_HBP_ADDR_SZ sizeof(u64)
> +#define PTRACE_HBP_CTRL_SZ sizeof(u32)
> +#define PTRACE_HBP_PAD_SZ sizeof(u32)
> +
> +static int riscv_hw_break_get(struct task_struct *target,
> + const struct user_regset *regset,
> + struct membuf to)
> +{
> + unsigned int note_type = regset->core_note_type;
> + int ret, idx, num_slots;
> + u32 info, ctrl;
> + u64 addr;
> +
> + /* Resource info: number of available slots */
> + ret = ptrace_hbp_get_resource_info(note_type, &info);
> + if (ret)
> + return ret;
> +
> + membuf_write(&to, &info, sizeof(info));
> + membuf_zero(&to, sizeof(u32));
> +
> + /* Emit one (address, ctrl, pad) entry per available slot */
> + num_slots = (int)info;
> + for (idx = 0; idx < num_slots; idx++) {
> + ret = ptrace_hbp_get_addr(note_type, target, idx, &addr);
> + if (ret)
> + return ret;
> + ret = ptrace_hbp_get_ctrl(note_type, target, idx, &ctrl);
> + if (ret)
> + return ret;
> + membuf_store(&to, addr);
> + membuf_store(&to, ctrl);
> + membuf_zero(&to, sizeof(u32));
> + }
> + return 0;
> +}
> +
> +static int riscv_hw_break_set(struct task_struct *target,
> + const struct user_regset *regset,
> + unsigned int pos, unsigned int count,
> + const void *kbuf, const void __user *ubuf)
> +{
> + unsigned int note_type = regset->core_note_type;
> + int ret, idx = 0, offset, limit;
> + u32 ctrl;
> + u64 addr;
> +
> + /* Resource info and pad */
> + offset = offsetof(struct user_hwdebug_state, dbg_regs);
> + user_regset_copyin_ignore(&pos, &count, &kbuf, &ubuf, 0, offset);
> +
> + /* (address, ctrl) registers */
> + limit = regset->n * regset->size;
> + while (count && offset < limit) {
> + if (count < PTRACE_HBP_ADDR_SZ)
> + return -EINVAL;
> +
> + ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &addr,
> + offset, offset + PTRACE_HBP_ADDR_SZ);
> + if (ret)
> + return ret;
> +
> + ret = ptrace_hbp_set_addr(note_type, target, idx, addr);
> + if (ret)
> + return ret;
> +
> + offset += PTRACE_HBP_ADDR_SZ;
> +
> + if (!count)
> + break;
> +
> + ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &ctrl,
> + offset, offset + PTRACE_HBP_CTRL_SZ);
> + if (ret)
> + return ret;
> +
> + ret = ptrace_hbp_set_ctrl(note_type, target, idx, ctrl);
> + if (ret)
> + return ret;
> +
> + offset += PTRACE_HBP_CTRL_SZ;
> +
> + user_regset_copyin_ignore(&pos, &count, &kbuf, &ubuf,
> + offset, offset + PTRACE_HBP_PAD_SZ);
> + offset += PTRACE_HBP_PAD_SZ;
> + idx++;
> + }
> +
> + return 0;
> +}
> +#endif /* CONFIG_HAVE_HW_BREAKPOINT */
> +
> static struct user_regset riscv_user_regset[] __ro_after_init = {
> [REGSET_X] = {
> USER_REGSET_NOTE_TYPE(PRSTATUS),
> @@ -421,6 +818,24 @@ static struct user_regset riscv_user_regset[] __ro_after_init = {
> .set = riscv_cfi_set,
> },
> #endif
> +#ifdef CONFIG_HAVE_HW_BREAKPOINT
> + [REGSET_RISCV_HW_BREAK] = {
> + USER_REGSET_NOTE_TYPE(RISCV_HW_BREAK),
> + .n = sizeof(struct user_hwdebug_state) / sizeof(u32),
> + .size = sizeof(u32),
> + .align = sizeof(u32),
> + .regset_get = riscv_hw_break_get,
> + .set = riscv_hw_break_set,
> + },
> + [REGSET_RISCV_HW_WATCH] = {
> + USER_REGSET_NOTE_TYPE(RISCV_HW_WATCH),
> + .n = sizeof(struct user_hwdebug_state) / sizeof(u32),
> + .size = sizeof(u32),
> + .align = sizeof(u32),
> + .regset_get = riscv_hw_break_get,
> + .set = riscv_hw_break_set,
> + },
> +#endif
> };
>
> static const struct user_regset_view riscv_user_native_view = {
> @@ -541,12 +956,104 @@ void ptrace_disable(struct task_struct *child)
> {
> }
>
> +#ifdef CONFIG_HAVE_HW_BREAKPOINT
> +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_HW_BP_NUM_MAX)
> + return -EINVAL;
> +
> + bp = child->thread.ptrace_bps[idx];
> + 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;
> +
> + if (idx >= RISCV_HW_BP_NUM_MAX)
> + return -EINVAL;
> +
> + bp = child->thread.ptrace_bps[idx];
> + if (bp)
> + attr = bp->attr;
> + else
> + ptrace_breakpoint_init(&attr);
> +
> + attr.bp_addr = state->addr;
> + attr.bp_len = state->len;
> + attr.bp_type = state->type;
> + /* Always register disabled; enable below if requested */
> + attr.disabled = 1;
> +
> + if (!bp) {
> + bp = register_user_hw_breakpoint(&attr, riscv_ptrace_hbptriggered, NULL, child);
> + if (IS_ERR(bp))
> + return PTR_ERR(bp);
> + child->thread.ptrace_bps[idx] = bp;
> + }
> +
> + /* Enable or disable as requested by ctrl (0 = enabled, 1 = disabled) */
> + attr.disabled = state->ctrl == 1;
> + return modify_user_hw_breakpoint(bp, &attr);
> +}
> +
> +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;
> diff --git a/include/uapi/linux/elf.h b/include/uapi/linux/elf.h
> index ee30dcd80901..1315ac35157c 100644
> --- a/include/uapi/linux/elf.h
> +++ b/include/uapi/linux/elf.h
> @@ -547,6 +547,10 @@ typedef struct elf64_shdr {
> #define NT_RISCV_TAGGED_ADDR_CTRL 0x902 /* RISC-V tagged address control (prctl()) */
> #define NN_RISCV_USER_CFI "LINUX"
> #define NT_RISCV_USER_CFI 0x903 /* RISC-V shadow stack state */
> +#define NN_RISCV_HW_BREAK "LINUX"
> +#define NT_RISCV_HW_BREAK 0x904 /* RISC-V hardware breakpoint registers */
> +#define NN_RISCV_HW_WATCH "LINUX"
> +#define NT_RISCV_HW_WATCH 0x905 /* RISCV-V hardware watchpoint registers */
> #define NN_LOONGARCH_CPUCFG "LINUX"
> #define NT_LOONGARCH_CPUCFG 0xa00 /* LoongArch CPU config registers */
> #define NN_LOONGARCH_CSR "LINUX"
> diff --git a/tools/include/uapi/linux/elf.h b/tools/include/uapi/linux/elf.h
> index 5834b83d7f9a..21f225502051 100644
> --- a/tools/include/uapi/linux/elf.h
> +++ b/tools/include/uapi/linux/elf.h
> @@ -460,6 +460,8 @@ typedef struct elf64_shdr {
> #define NT_RISCV_CSR 0x900 /* RISC-V Control and Status Registers */
> #define NT_RISCV_VECTOR 0x901 /* RISC-V vector registers */
> #define NT_RISCV_TAGGED_ADDR_CTRL 0x902 /* RISC-V tagged address control (prctl()) */
> +#define NT_RISCV_HW_BREAK 0x904
> +#define NT_RISCV_HW_WATCH 0x905
> #define NT_LOONGARCH_CPUCFG 0xa00 /* LoongArch CPU config registers */
> #define NT_LOONGARCH_CSR 0xa01 /* LoongArch control and status registers */
> #define NT_LOONGARCH_LSX 0xa02 /* LoongArch Loongson SIMD Extension registers */