From: Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
To: Paul Walmsley <pjw@kernel.org>,
Palmer Dabbelt <palmer@dabbelt.com>,
Albert Ou <aou@eecs.berkeley.edu>,
linux-riscv@lists.infradead.org
Cc: Alexandre Ghiti <alex@ghiti.fr>,
Jesse Taube <jtaubepe@redhat.com>,
Oleg Nesterov <oleg@redhat.com>, Kees Cook <kees@kernel.org>,
Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>,
Mark Rutland <mark.rutland@arm.com>,
Alexander Shishkin <alexander.shishkin@linux.intel.com>,
Jiri Olsa <jolsa@kernel.org>, Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
Shuah Khan <shuah@kernel.org>,
linux-kernel@vger.kernel.org, linux-mm@kvack.org,
linux-perf-users@vger.kernel.org,
linux-kselftest@vger.kernel.org,
Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
Subject: [PATCH v8 3/8] riscv: add ptrace requests for hardware breakpoints
Date: Sat, 7 Nov 2026 08:09:12 +0530 [thread overview]
Message-ID: <20261107023917.40606-4-himanshu.chauhan@oss.qualcomm.com> (raw)
In-Reply-To: <20261107023917.40606-1-himanshu.chauhan@oss.qualcomm.com>
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 <himanshu.chauhan@oss.qualcomm.com>
---
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 <uapi/linux/hw_breakpoint.h>
+/* 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 <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>
#include <asm/errata_list.h>
@@ -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 <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,
@@ -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
next prev parent reply other threads:[~2026-11-07 2:39 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-07 2:39 [PATCH v8 0/8] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
2026-11-07 2:39 ` [PATCH v8 1/8] " Himanshu Chauhan
2026-11-07 2:39 ` [PATCH v8 2/8] riscv: Add breakpoint and watchpoint test for riscv Himanshu Chauhan
2026-11-07 2:39 ` Himanshu Chauhan [this message]
2026-11-07 7:04 ` [PATCH v8 3/8] riscv: add ptrace requests for hardware breakpoints kernel test robot
2026-11-07 2:39 ` [PATCH v8 4/8] riscv: add regset access " Himanshu Chauhan
2026-11-07 2:39 ` [PATCH v8 5/8] selftests/breakpoints: extend riscv test for ptrace hw break/watchpoints Himanshu Chauhan
2026-11-07 2:39 ` [PATCH v8 6/8] riscv: Add shared instruction fetch and decode helpers Himanshu Chauhan
2026-11-07 2:39 ` [PATCH v8 7/8] riscv: Add software supported single stepping with mc/mc6 triggers Himanshu Chauhan
2026-11-07 2:39 ` [PATCH v8 8/8] perf tests: keep bp_signal memory accesses visible Himanshu Chauhan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261107023917.40606-4-himanshu.chauhan@oss.qualcomm.com \
--to=himanshu.chauhan@oss.qualcomm.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alex@ghiti.fr \
--cc=alexander.shishkin@linux.intel.com \
--cc=aou@eecs.berkeley.edu \
--cc=irogers@google.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=jtaubepe@redhat.com \
--cc=kees@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=linux-riscv@lists.infradead.org \
--cc=mark.rutland@arm.com \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=oleg@redhat.com \
--cc=palmer@dabbelt.com \
--cc=peterz@infradead.org \
--cc=pjw@kernel.org \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®