* [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints
@ 2026-08-03 13:49 Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 1/5] " Himanshu Chauhan
` (5 more replies)
0 siblings, 6 replies; 9+ messages in thread
From: Himanshu Chauhan @ 2026-08-03 13:49 UTC (permalink / raw)
To: linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe, Himanshu Chauhan
This patchset adds support for hardware breakpoints and watchpoints in the
RISC-V architecture. The framework is built on top of the perf subsystem and
the SBI debug trigger extension (Sdtrig).
v6 extends v5 with two new patches:
- ptrace support: 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. A simpler PTRACE_GETHBPREGS/SETHBPREGS
request pair is also provided for direct single-trigger access without going
through the regset machinery. Thread flush/copy hooks are wired up so
per-task breakpoints are cleaned up across fork/exec.
- Extended selftest: the existing perf_event-based selftest is extended to
also exercise the new ptrace GETREGSET/SETREGSET regset path and the raw
PTRACE_GETHBPREGS/SETHBPREGS interface.
Single stepping is ready and test but to follow this patch set.
Virtualization of debug triggers are pending
The SBI debug trigger extension is specified in Chapter 19 of the SBI
specification:
https://github.com/riscv-non-isa/riscv-sbi-doc/releases/download/v3.0/riscv-sbi.pdf
The Sdtrig ISA is part of the RISC-V debug specification:
https://github.com/riscv/riscv-debug-spec
Changes from v5:
- Rebased to v7.2-rc6
- Simplified Macros in hw_breakpoint.h
- Took care of the review comments
- Added ptrace support for hardware break/watchpoints (new patch)
- PTRACE_GETREGSET/SETREGSET via NT_RISCV_HW_BREAK / NT_RISCV_HW_WATCH
- PTRACE_GETHBPREGS / SETHBPREGS for direct single-trigger access
- HAVE_MIXED_BREAKPOINTS_REGS selected (break/watch share trigger pool)
- flush_ptrace_hw_breakpoint / ptrace_hw_copy_thread wired up
- Extended selftest to cover ptrace-based hw break/watchpoint paths (new patch)
Changes from v4:
- Rebased to v7.2-rc4
- Fixed rv32 build error
- Added pr_fmt to print KBUILD_MODNAME
- Changed type of shmem_pa to phys_addr_t
- Use per_cpu_ptr_to_phys instead of __pa for per-cpu allocated memory
- Print successful registration/unregistration message when no error
- Added RISC-V DEBUGGING section in MAINTAINERS and added myself as maintainer
- Fixed warnings from checkpatch.pl --strict run
Changes from v3:
- Rebased to v7.1-rc3
- For watchpoints, check tdata1.hit via SBI_EXT_DBTR_TRIG_READ and keep
STVAL-based matching as fallback
- Improved watchpoint matching when STVAL reports the lowest accessed address
for wider memory accesses
- Program execute breakpoints with SIZE=0 (match any size) to avoid misses
with 16-bit/compressed instruction addresses
- Updated selftest to avoid deadlock by replacing unbounded sem_wait() with
sem_timedwait() timeout handling
- Updated selftest breakpoint function so it cannot be inlined or optimized away
Changes from v2:
- Rebased to v7.0-rc1
- Fixed warnings from checkpatch.pl --strict run
Changes from v1:
- The patch adding the SBI extension and function IDs is already merged; this
series builds on top of that
- Added breakpoint selftest in tools/testing/selftests/breakpoints/
How to use:
~~~~~~~~~~~
OpenSBI:
https://github.com/riscv-software-src/opensbi.git
QEMU:
https://github.com/qemu/qemu.git
Linux Kernel:
Apply these patches on top of v7.2-rc6.
How to test:
~~~~~~~~~~~
From the Linux kernel directory, first install the UAPI headers (required on a
fresh tree so the compiler can locate <asm/ptrace.h> and the new
NT_RISCV_HW_BREAK/WATCH definitions via KHDR_INCLUDES):
make headers
Then build the selftest:
make -C tools/testing/selftests/breakpoints/
This produces breakpoint_test_riscv under the same directory. Load it on the
target and run. Sample output:
# /apps/breakpoint_test_riscv
# [perf_event]: Breakpoint test passed!
# [perf_event]: Watchpoint test passed!
# [ptrace]: Breakpoint test passed!
# ptrace(PTRACE_GETREGSET): Number of watchpoints: 2
# ptrace(PTRACE_GETREGSet): addr: 0x82888 control: 0x8080
# [ptrace]: Watchpoint test passed!
# [hbpregs] breakpoint readback: addr=0x10472 type=4 len=4 ctrl=0
# [hbpregs]: Breakpoint test passed!
# [hbpregs] watchpoint readback: addr=0x82888 type=2 len=8 ctrl=0
# [hbpregs]: Watchpoint test passed!
Himanshu Chauhan (5):
riscv: Introduce support for hardware break/watchpoints
riscv: Add breakpoint and watchpoint test for riscv
riscv: ptrace support for hardware break/watchpoints
selftests/breakpoints: extend riscv test for ptrace hw
break/watchpoints
MAINTAINERS: Add entry for RISC-V Debugging
MAINTAINERS | 9 +
arch/riscv/Kconfig | 2 +
arch/riscv/include/asm/hw_breakpoint.h | 299 +++++++
arch/riscv/include/asm/kdebug.h | 3 +-
arch/riscv/include/asm/processor.h | 18 +
arch/riscv/include/uapi/asm/ptrace.h | 53 ++
arch/riscv/kernel/Makefile | 1 +
arch/riscv/kernel/hw_breakpoint.c | 677 ++++++++++++++++
arch/riscv/kernel/process.c | 5 +
arch/riscv/kernel/ptrace.c | 507 ++++++++++++
arch/riscv/kernel/traps.c | 6 +
include/uapi/linux/elf.h | 4 +
tools/include/uapi/linux/elf.h | 2 +
tools/testing/selftests/breakpoints/Makefile | 5 +
.../breakpoints/breakpoint_test_riscv.c | 765 ++++++++++++++++++
15 files changed, 2355 insertions(+), 1 deletion(-)
create mode 100644 arch/riscv/include/asm/hw_breakpoint.h
create mode 100644 arch/riscv/kernel/hw_breakpoint.c
create mode 100644 tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v6 1/5] riscv: Introduce support for hardware break/watchpoints
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
@ 2026-08-03 13:49 ` Himanshu Chauhan
2026-08-05 13:20 ` Chen Pei
2026-08-03 13:49 ` [PATCH v6 2/5] riscv: Add breakpoint and watchpoint test for riscv Himanshu Chauhan
` (4 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Himanshu Chauhan @ 2026-08-03 13:49 UTC (permalink / raw)
To: linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe, Himanshu Chauhan
RISC-V hardware breakpoint framework is built on top of perf subsystem
and uses SBI debug trigger extension to
install/uninstall/update/enable/disable hardware triggers as specified
in Sdtrig ISA extension.
Signed-off-by: Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
---
arch/riscv/Kconfig | 1 +
arch/riscv/include/asm/hw_breakpoint.h | 289 +++++++++++
arch/riscv/include/asm/kdebug.h | 3 +-
arch/riscv/kernel/Makefile | 1 +
arch/riscv/kernel/hw_breakpoint.c | 678 +++++++++++++++++++++++++
arch/riscv/kernel/traps.c | 6 +
6 files changed, 977 insertions(+), 1 deletion(-)
create mode 100644 arch/riscv/include/asm/hw_breakpoint.h
create mode 100644 arch/riscv/kernel/hw_breakpoint.c
diff --git a/arch/riscv/Kconfig b/arch/riscv/Kconfig
index f7028caaeae0..a624dacdaf12 100644
--- a/arch/riscv/Kconfig
+++ b/arch/riscv/Kconfig
@@ -172,6 +172,7 @@ config RISCV
select HAVE_FUNCTION_ERROR_INJECTION
select HAVE_GCC_PLUGINS
select HAVE_GENERIC_VDSO if MMU
+ select HAVE_HW_BREAKPOINT if PERF_EVENTS
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
new file mode 100644
index 000000000000..4df1bfe0507e
--- /dev/null
+++ b/arch/riscv/include/asm/hw_breakpoint.h
@@ -0,0 +1,289 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2026 Qualcomm Technologies, Inc.
+ */
+
+#ifndef __RISCV_HW_BREAKPOINT_H
+#define __RISCV_HW_BREAKPOINT_H
+
+struct task_struct;
+
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+
+#include <uapi/linux/hw_breakpoint.h>
+
+/* Maximum number of hardware breakpoints supported */
+#define RISCV_HW_BP_NUM_MAX 32
+
+#if __riscv_xlen == 64
+#define cpu_to_le cpu_to_le64
+#define le_to_cpu le64_to_cpu
+#elif __riscv_xlen == 32
+#define cpu_to_le cpu_to_le32
+#define le_to_cpu le32_to_cpu
+#else
+#error "Unexpected __riscv_xlen"
+#endif
+
+#define CLEAR_DBTR_BIT(_target, _bit) ((_target) &= ~BIT(_bit))
+#define SET_DBTR_BIT(_target, _bit) ((_target) |= BIT(_bit))
+
+#define RISCV_DBTR_EXEC BIT(0)
+#define RISCV_DBTR_LOAD BIT(1)
+#define RISCV_DBTR_STORE BIT(2)
+#define RISCV_DBTR_LDST (RISCV_DBTR_LOAD | RISCV_DBTR_STORE)
+
+enum {
+ RISCV_DBTR_TRIG_NONE = 0,
+ RISCV_DBTR_TRIG_LEGACY,
+ RISCV_DBTR_TRIG_MCONTROL,
+ RISCV_DBTR_TRIG_ICOUNT,
+ RISCV_DBTR_TRIG_ITRIGGER,
+ RISCV_DBTR_TRIG_ETRIGGER,
+ RISCV_DBTR_TRIG_MCONTROL6,
+};
+
+/* Trigger Data 1 */
+#define RISCV_DBTR_TDATA1_DATA_BIT 0
+#if __riscv_xlen == 64
+#define RISCV_DBTR_TDATA1_DMODE_BIT 59
+#define RISCV_DBTR_TDATA1_TYPE_BIT 60
+#elif __riscv_xlen == 32
+#define RISCV_DBTR_TDATA1_DMODE_BIT 27
+#define RISCV_DBTR_TDATA1_TYPE_BIT 28
+#else
+#error "Unknown __riscv_xlen"
+#endif
+
+#if __riscv_xlen == 64
+#define RISCV_DBTR_TDATA1_DATA_BIT_MASK GENMASK(58, RISCV_DBTR_TDATA1_DATA_BIT)
+#elif __riscv_xlen == 32
+#define RISCV_DBTR_TDATA1_DATA_BIT_MASK GENMASK(26, RISCV_DBTR_TDATA1_DATA_BIT)
+#else
+#error "Unknown __riscv_xlen"
+#endif
+#define RISCV_DBTR_TDATA1_DMODE_BIT_MASK BIT(RISCV_DBTR_TDATA1_DMODE_BIT)
+#define RISCV_DBTR_TDATA1_TYPE_BIT_MASK \
+ GENMASK(RISCV_DBTR_TDATA1_TYPE_BIT + 3, RISCV_DBTR_TDATA1_TYPE_BIT)
+
+/* MC - Match Control Type Register */
+#define RISCV_DBTR_MC_LOAD_BIT 0
+#define RISCV_DBTR_MC_STORE_BIT 1
+#define RISCV_DBTR_MC_EXEC_BIT 2
+#define RISCV_DBTR_MC_U_BIT 3
+#define RISCV_DBTR_MC_S_BIT 4
+#define RISCV_DBTR_MC_RES2_BIT 5
+#define RISCV_DBTR_MC_M_BIT 6
+#define RISCV_DBTR_MC_MATCH_BIT 7
+#define RISCV_DBTR_MC_CHAIN_BIT 11
+#define RISCV_DBTR_MC_ACTION_BIT 12
+#define RISCV_DBTR_MC_SIZELO_BIT 16
+#define RISCV_DBTR_MC_TIMING_BIT 18
+#define RISCV_DBTR_MC_SELECT_BIT 19
+#define RISCV_DBTR_MC_HIT_BIT 20
+#if __riscv_xlen >= 64
+#define RISCV_DBTR_MC_SIZEHI_BIT 21
+#endif
+#if __riscv_xlen == 64
+#define RISCV_DBTR_MC_MASKMAX_BIT 53
+#define RISCV_DBTR_MC_DMODE_BIT 59
+#define RISCV_DBTR_MC_TYPE_BIT 60
+#elif __riscv_xlen == 32
+#define RISCV_DBTR_MC_MASKMAX_BIT 21
+#define RISCV_DBTR_MC_DMODE_BIT 27
+#define RISCV_DBTR_MC_TYPE_BIT 28
+#else
+#error "Unknown riscv xlen"
+#endif
+
+#define RISCV_DBTR_MC_LOAD_BIT_MASK BIT(RISCV_DBTR_MC_LOAD_BIT)
+#define RISCV_DBTR_MC_STORE_BIT_MASK BIT(RISCV_DBTR_MC_STORE_BIT)
+#define RISCV_DBTR_MC_EXEC_BIT_MASK BIT(RISCV_DBTR_MC_EXEC_BIT)
+#define RISCV_DBTR_MC_U_BIT_MASK BIT(RISCV_DBTR_MC_U_BIT)
+#define RISCV_DBTR_MC_S_BIT_MASK BIT(RISCV_DBTR_MC_S_BIT)
+#define RISCV_DBTR_MC_RES2_BIT_MASK BIT(RISCV_DBTR_MC_RES2_BIT)
+#define RISCV_DBTR_MC_M_BIT_MASK BIT(RISCV_DBTR_MC_M_BIT)
+#define RISCV_DBTR_MC_MATCH_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC_MATCH_BIT + 3, RISCV_DBTR_MC_MATCH_BIT)
+#define RISCV_DBTR_MC_CHAIN_BIT_MASK BIT(RISCV_DBTR_MC_CHAIN_BIT)
+#define RISCV_DBTR_MC_ACTION_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC_ACTION_BIT + 3, RISCV_DBTR_MC_ACTION_BIT)
+#define RISCV_DBTR_MC_SIZELO_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC_SIZELO_BIT + 1, RISCV_DBTR_MC_SIZELO_BIT)
+#define RISCV_DBTR_MC_TIMING_BIT_MASK BIT(RISCV_DBTR_MC_TIMING_BIT)
+#define RISCV_DBTR_MC_SELECT_BIT_MASK BIT(RISCV_DBTR_MC_SELECT_BIT)
+#define RISCV_DBTR_MC_HIT_BIT_MASK BIT(RISCV_DBTR_MC_HIT_BIT)
+#if __riscv_xlen >= 64
+#define RISCV_DBTR_MC_SIZEHI_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC_SIZEHI_BIT + 1, RISCV_DBTR_MC_SIZEHI_BIT)
+#endif
+#define RISCV_DBTR_MC_MASKMAX_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC_MASKMAX_BIT + 5, RISCV_DBTR_MC_MASKMAX_BIT)
+#define RISCV_DBTR_MC_DMODE_BIT_MASK BIT(RISCV_DBTR_MC_DMODE_BIT)
+#define RISCV_DBTR_MC_TYPE_BIT_MASK GENMASK(RISCV_DBTR_MC_TYPE_BIT + 3, RISCV_DBTR_MC_TYPE_BIT)
+
+/* MC6 - Match Control 6 Type Register */
+#define RISCV_DBTR_MC6_LOAD_BIT 0
+#define RISCV_DBTR_MC6_STORE_BIT 1
+#define RISCV_DBTR_MC6_EXEC_BIT 2
+#define RISCV_DBTR_MC6_U_BIT 3
+#define RISCV_DBTR_MC6_S_BIT 4
+#define RISCV_DBTR_MC6_RES2_BIT 5
+#define RISCV_DBTR_MC6_M_BIT 6
+#define RISCV_DBTR_MC6_MATCH_BIT 7
+#define RISCV_DBTR_MC6_CHAIN_BIT 11
+#define RISCV_DBTR_MC6_ACTION_BIT 12
+#define RISCV_DBTR_MC6_SIZE_BIT 16
+#define RISCV_DBTR_MC6_TIMING_BIT 20
+#define RISCV_DBTR_MC6_SELECT_BIT 21
+#define RISCV_DBTR_MC6_HIT_BIT 22
+#define RISCV_DBTR_MC6_VU_BIT 23
+#define RISCV_DBTR_MC6_VS_BIT 24
+#if __riscv_xlen == 64
+#define RISCV_DBTR_MC6_DMODE_BIT 59
+#define RISCV_DBTR_MC6_TYPE_BIT 60
+#elif __riscv_xlen == 32
+#define RISCV_DBTR_MC6_DMODE_BIT 27
+#define RISCV_DBTR_MC6_TYPE_BIT 28
+#else
+#error "Unknown riscv xlen"
+#endif
+
+#define RISCV_DBTR_MC6_LOAD_BIT_MASK BIT(RISCV_DBTR_MC6_LOAD_BIT)
+#define RISCV_DBTR_MC6_STORE_BIT_MASK BIT(RISCV_DBTR_MC6_STORE_BIT)
+#define RISCV_DBTR_MC6_EXEC_BIT_MASK BIT(RISCV_DBTR_MC6_EXEC_BIT)
+#define RISCV_DBTR_MC6_U_BIT_MASK BIT(RISCV_DBTR_MC6_U_BIT)
+#define RISCV_DBTR_MC6_S_BIT_MASK BIT(RISCV_DBTR_MC6_S_BIT)
+#define RISCV_DBTR_MC6_RES2_BIT_MASK BIT(RISCV_DBTR_MC6_RES2_BIT)
+#define RISCV_DBTR_MC6_M_BIT_MASK BIT(RISCV_DBTR_MC6_M_BIT)
+#define RISCV_DBTR_MC6_MATCH_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC6_MATCH_BIT + 3, RISCV_DBTR_MC6_MATCH_BIT)
+#define RISCV_DBTR_MC6_CHAIN_BIT_MASK BIT(RISCV_DBTR_MC6_CHAIN_BIT)
+#define RISCV_DBTR_MC6_ACTION_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC6_ACTION_BIT + 3, RISCV_DBTR_MC6_ACTION_BIT)
+#define RISCV_DBTR_MC6_SIZE_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC6_SIZE_BIT + 3, RISCV_DBTR_MC6_SIZE_BIT)
+#define RISCV_DBTR_MC6_TIMING_BIT_MASK BIT(RISCV_DBTR_MC6_TIMING_BIT)
+#define RISCV_DBTR_MC6_SELECT_BIT_MASK BIT(RISCV_DBTR_MC6_SELECT_BIT)
+#define RISCV_DBTR_MC6_HIT_BIT_MASK BIT(RISCV_DBTR_MC6_HIT_BIT)
+#define RISCV_DBTR_MC6_VU_BIT_MASK BIT(RISCV_DBTR_MC6_VU_BIT)
+#define RISCV_DBTR_MC6_VS_BIT_MASK BIT(RISCV_DBTR_MC6_VS_BIT)
+#define RISCV_DBTR_MC6_DMODE_BIT_MASK BIT(RISCV_DBTR_MC6_DMODE_BIT)
+#define RISCV_DBTR_MC6_TYPE_BIT_MASK \
+ GENMASK(RISCV_DBTR_MC6_TYPE_BIT + 3, RISCV_DBTR_MC6_TYPE_BIT)
+
+#define RISCV_DBTR_SET_TDATA1_TYPE(_t1, _type) \
+ ({ \
+ typeof(_t1) (td1t1) = (_t1); \
+ (td1t1) &= ~RISCV_DBTR_TDATA1_TYPE_BIT_MASK; \
+ (td1t1) |= (((unsigned long)(_type) \
+ << RISCV_DBTR_TDATA1_TYPE_BIT) \
+ & RISCV_DBTR_TDATA1_TYPE_BIT_MASK); \
+ (td1t1); \
+ })
+
+#define RISCV_DBTR_SET_MC_TYPE(_t1, _type) \
+ ({ \
+ typeof(_t1) (mct1) = (_t1); \
+ (mct1) &= ~RISCV_DBTR_MC_TYPE_BIT_MASK; \
+ (mct1) |= (((unsigned long)(_type) \
+ << RISCV_DBTR_MC_TYPE_BIT) \
+ & RISCV_DBTR_MC_TYPE_BIT_MASK); \
+ (mct1); \
+ })
+
+#define RISCV_DBTR_SET_MC6_TYPE(_t1, _type) \
+ ({ \
+ typeof(_t1) (mc6t1) = (_t1); \
+ (mc6t1) &= ~RISCV_DBTR_MC6_TYPE_BIT_MASK; \
+ (mc6t1) |= (((unsigned long)(_type) \
+ << RISCV_DBTR_MC6_TYPE_BIT) \
+ & RISCV_DBTR_MC6_TYPE_BIT_MASK); \
+ (mc6t1); \
+ })
+
+#define RISCV_DBTR_SET_MC_EXEC_BIT(_t1) \
+ SET_DBTR_BIT(_t1, RISCV_DBTR_MC_EXEC_BIT)
+
+#define RISCV_DBTR_SET_MC_LOAD_BIT(_t1) \
+ SET_DBTR_BIT(_t1, RISCV_DBTR_MC_LOAD_BIT)
+
+#define RISCV_DBTR_SET_MC_STORE_BIT(_t1) \
+ SET_DBTR_BIT(_t1, RISCV_DBTR_MC_STORE_BIT)
+
+#define RISCV_DBTR_SET_MC_SIZELO(_t1, _val) \
+ ({ \
+ typeof(_t1) (mcslt1) = (_t1); \
+ mcslt1 &= ~RISCV_DBTR_MC_SIZELO_BIT_MASK; \
+ mcslt1 |= (((_val) << RISCV_DBTR_MC_SIZELO_BIT) \
+ & RISCV_DBTR_MC_SIZELO_BIT_MASK); \
+ (mcslt1); \
+ })
+
+#if __riscv_xlen >= 64
+#define RISCV_DBTR_SET_MC_SIZEHI(_t1, _val) \
+ ({ \
+ typeof(_t1) (mcsht1) = (_t1); \
+ mcsht1 &= ~RISCV_DBTR_MC_SIZEHI_BIT_MASK; \
+ mcsht1 |= (((_val) << RISCV_DBTR_MC_SIZEHI_BIT) \
+ & RISCV_DBTR_MC_SIZEHI_BIT_MASK); \
+ (mcsht1); \
+ })
+#else
+/* SIZEHI does not exist in the rv32 mcontrol layout; nothing to set. */
+#define RISCV_DBTR_SET_MC_SIZEHI(_t1, _val) ((void)(_val), (_t1))
+#endif
+
+#define RISCV_DBTR_SET_MC6_EXEC_BIT(_t1) \
+ SET_DBTR_BIT(_t1, RISCV_DBTR_MC6_EXEC_BIT)
+
+#define RISCV_DBTR_SET_MC6_LOAD_BIT(_t1) \
+ SET_DBTR_BIT(_t1, RISCV_DBTR_MC6_LOAD_BIT)
+
+#define RISCV_DBTR_SET_MC6_STORE_BIT(_t1) \
+ SET_DBTR_BIT(_t1, RISCV_DBTR_MC6_STORE_BIT)
+
+#define RISCV_DBTR_SET_MC6_SIZE(_t1, _val) \
+ ({ \
+ typeof(_t1) (mc6szt1) = (_t1); \
+ (mc6szt1) &= ~RISCV_DBTR_MC6_SIZE_BIT_MASK; \
+ (mc6szt1) |= (((_val) << RISCV_DBTR_MC6_SIZE_BIT) \
+ & RISCV_DBTR_MC6_SIZE_BIT_MASK); \
+ (mc6szt1); \
+ })
+
+struct arch_hw_breakpoint {
+ unsigned long address;
+ unsigned long len;
+ unsigned int type;
+
+ /* Trigger configuration data */
+ unsigned long tdata1;
+ unsigned long tdata2;
+ unsigned long tdata3;
+};
+
+struct perf_event_attr;
+struct notifier_block;
+struct perf_event;
+struct pt_regs;
+
+int hw_breakpoint_slots(int type);
+int arch_check_bp_in_kernelspace(struct arch_hw_breakpoint *hw);
+int hw_breakpoint_arch_parse(struct perf_event *bp,
+ const struct perf_event_attr *attr,
+ struct arch_hw_breakpoint *hw);
+int hw_breakpoint_exceptions_notify(struct notifier_block *unused,
+ unsigned long val, void *data);
+
+void arch_enable_hw_breakpoint(struct perf_event *bp);
+void arch_update_hw_breakpoint(struct perf_event *bp);
+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);
+
+#else
+
+#endif /* CONFIG_HAVE_HW_BREAKPOINT */
+#endif /* __RISCV_HW_BREAKPOINT_H */
diff --git a/arch/riscv/include/asm/kdebug.h b/arch/riscv/include/asm/kdebug.h
index 85ac00411f6e..53e989781aa1 100644
--- a/arch/riscv/include/asm/kdebug.h
+++ b/arch/riscv/include/asm/kdebug.h
@@ -6,7 +6,8 @@
enum die_val {
DIE_UNUSED,
DIE_TRAP,
- DIE_OOPS
+ DIE_OOPS,
+ DIE_DEBUG
};
#endif
diff --git a/arch/riscv/kernel/Makefile b/arch/riscv/kernel/Makefile
index cabb99cadfb6..590a280762c9 100644
--- a/arch/riscv/kernel/Makefile
+++ b/arch/riscv/kernel/Makefile
@@ -100,6 +100,7 @@ obj-$(CONFIG_DYNAMIC_FTRACE) += mcount-dyn.o
obj-$(CONFIG_PERF_EVENTS) += perf_callchain.o
obj-$(CONFIG_HAVE_PERF_REGS) += perf_regs.o
+obj-$(CONFIG_HAVE_HW_BREAKPOINT) += hw_breakpoint.o
obj-$(CONFIG_RISCV_SBI) += sbi.o sbi_ecall.o
ifeq ($(CONFIG_RISCV_SBI), y)
obj-$(CONFIG_SMP) += sbi-ipi.o
diff --git a/arch/riscv/kernel/hw_breakpoint.c b/arch/riscv/kernel/hw_breakpoint.c
new file mode 100644
index 000000000000..fc54a1a897c5
--- /dev/null
+++ b/arch/riscv/kernel/hw_breakpoint.c
@@ -0,0 +1,678 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Qualcomm Technologies, Inc.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/hw_breakpoint.h>
+#include <linux/perf_event.h>
+#include <linux/spinlock.h>
+#include <linux/percpu.h>
+#include <linux/kdebug.h>
+#include <linux/bitops.h>
+#include <linux/cpu.h>
+#include <linux/cpuhotplug.h>
+
+#include <asm/sbi.h>
+
+/* Registered per-cpu bp/wp */
+static DEFINE_PER_CPU(struct perf_event *, pcpu_hw_bp_events[RISCV_HW_BP_NUM_MAX]);
+static DEFINE_PER_CPU(unsigned long, ecall_lock_flags);
+static DEFINE_PER_CPU(raw_spinlock_t, ecall_lock);
+
+/* Per-cpu shared memory between S and M mode */
+static union sbi_dbtr_shmem_entry __percpu *sbi_dbtr_shmem;
+
+/* number of debug triggers on this cpu . */
+static int dbtr_total_num __ro_after_init;
+static int dbtr_type __ro_after_init;
+static int dbtr_init __ro_after_init;
+
+#define MEM_HI(_m) ((unsigned long)upper_32_bits(_m))
+#define MEM_LO(_m) ((unsigned long)lower_32_bits(_m))
+
+static int arch_smp_setup_sbi_shmem(unsigned int cpu)
+{
+ union sbi_dbtr_shmem_entry *dbtr_shmem;
+ phys_addr_t shmem_pa;
+ struct sbiret ret;
+
+ dbtr_shmem = per_cpu_ptr(sbi_dbtr_shmem, cpu);
+ if (!dbtr_shmem) {
+ pr_err("Invalid per-cpu shared memory for debug triggers\n");
+ return -ENODEV;
+ }
+
+ shmem_pa = per_cpu_ptr_to_phys(dbtr_shmem);
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_SETUP_SHMEM,
+ MEM_LO(shmem_pa), MEM_HI(shmem_pa), 0, 0, 0, 0);
+
+ if (ret.error) {
+ pr_warn("%s: failed to setup shared memory. error: %ld\n",
+ __func__, ret.error);
+ return sbi_err_map_linux_errno(ret.error);
+ }
+
+ pr_info("CPU %d: HW Breakpoint shared memory registered.\n", cpu);
+
+ return 0;
+}
+
+static int arch_smp_teardown_sbi_shmem(unsigned int cpu)
+{
+ struct sbiret ret;
+
+ /* Disable shared memory */
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_SETUP_SHMEM,
+ SBI_SHMEM_DISABLE, SBI_SHMEM_DISABLE, 0, 0, 0, 0);
+
+ if (ret.error)
+ pr_warn("%s: failed to disable shared memory. error: %ld\n",
+ __func__, ret.error);
+ else
+ pr_info("CPU %d: HW Breakpoint shared memory disabled.\n", cpu);
+
+ return 0;
+}
+
+static void init_sbi_dbtr(void)
+{
+ unsigned long tdata1;
+ struct sbiret ret;
+
+ if (sbi_probe_extension(SBI_EXT_DBTR) <= 0) {
+ pr_warn("SBI_EXT_DBTR is not supported\n");
+ dbtr_total_num = 0;
+ goto done;
+ }
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_NUM_TRIGGERS,
+ 0, 0, 0, 0, 0, 0);
+ if (ret.error) {
+ pr_warn("Failed to detect triggers\n");
+ dbtr_total_num = 0;
+ goto done;
+ }
+
+ tdata1 = 0;
+ tdata1 = RISCV_DBTR_SET_TDATA1_TYPE(tdata1, RISCV_DBTR_TRIG_MCONTROL6);
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_NUM_TRIGGERS,
+ tdata1, 0, 0, 0, 0, 0);
+ if (ret.error) {
+ pr_warn("Failed to detect mcontrol6 triggers\n");
+ } else if (!ret.value) {
+ pr_warn("Type 6 triggers not available\n");
+ } else {
+ dbtr_total_num = ret.value;
+ dbtr_type = RISCV_DBTR_TRIG_MCONTROL6;
+ pr_warn("Mcontrol6 trigger available.\n");
+ goto done;
+ }
+
+ /* fallback to type 2 triggers if type 6 is not available */
+
+ tdata1 = 0;
+ tdata1 = RISCV_DBTR_SET_TDATA1_TYPE(tdata1, RISCV_DBTR_TRIG_MCONTROL);
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_NUM_TRIGGERS,
+ tdata1, 0, 0, 0, 0, 0);
+ if (ret.error) {
+ pr_warn("Failed to detect mcontrol triggers\n");
+ } else if (!ret.value) {
+ pr_warn("Type 2 triggers not available\n");
+ } else {
+ dbtr_total_num = ret.value;
+ dbtr_type = RISCV_DBTR_TRIG_MCONTROL;
+ goto done;
+ }
+
+done:
+ dbtr_init = 1;
+}
+
+int hw_breakpoint_slots(int type)
+{
+ /*
+ * We can be called early, so don't rely on
+ * static variables being initialised.
+ */
+
+ if (!dbtr_init)
+ init_sbi_dbtr();
+
+ return dbtr_total_num;
+}
+
+int arch_check_bp_in_kernelspace(struct arch_hw_breakpoint *hw)
+{
+ unsigned int len;
+ unsigned long va;
+
+ va = hw->address;
+ len = hw->len;
+
+ return (va >= TASK_SIZE) && ((va + len - 1) >= TASK_SIZE);
+}
+
+static int rv_init_mcontrol_trigger(const struct perf_event_attr *attr,
+ struct arch_hw_breakpoint *hw)
+{
+ switch (attr->bp_type) {
+ case HW_BREAKPOINT_X:
+ hw->type = RISCV_DBTR_EXEC;
+ RISCV_DBTR_SET_MC_EXEC_BIT(hw->tdata1);
+ break;
+ case HW_BREAKPOINT_R:
+ hw->type = RISCV_DBTR_LOAD;
+ RISCV_DBTR_SET_MC_LOAD_BIT(hw->tdata1);
+ break;
+ case HW_BREAKPOINT_W:
+ hw->type = RISCV_DBTR_STORE;
+ RISCV_DBTR_SET_MC_STORE_BIT(hw->tdata1);
+ break;
+ case HW_BREAKPOINT_RW:
+ hw->type = RISCV_DBTR_LDST;
+ RISCV_DBTR_SET_MC_LOAD_BIT(hw->tdata1);
+ RISCV_DBTR_SET_MC_STORE_BIT(hw->tdata1);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (attr->bp_type == HW_BREAKPOINT_X) {
+ /*
+ * Userspace debuggers can request execute breakpoints with
+ * bp_len == 2 for compressed/non-aligned instruction
+ * addresses. Program execute triggers with "match any size"
+ * to avoid missing valid instruction fetches.
+ */
+ hw->len = 0;
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZELO(hw->tdata1, 0);
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZEHI(hw->tdata1, 0);
+ } else {
+ switch (attr->bp_len) {
+ case HW_BREAKPOINT_LEN_1:
+ hw->len = 1;
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZELO(hw->tdata1, 1);
+ break;
+ case HW_BREAKPOINT_LEN_2:
+ hw->len = 2;
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZELO(hw->tdata1, 2);
+ break;
+ case HW_BREAKPOINT_LEN_4:
+ hw->len = 4;
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZELO(hw->tdata1, 3);
+ break;
+#if __riscv_xlen >= 64
+ case HW_BREAKPOINT_LEN_8:
+ hw->len = 8;
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZELO(hw->tdata1, 1);
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZEHI(hw->tdata1, 1);
+ break;
+#endif
+ /* Set to match any size */
+ default:
+ hw->len = 0;
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZELO(hw->tdata1, 0);
+ hw->tdata1 = RISCV_DBTR_SET_MC_SIZEHI(hw->tdata1, 0);
+ break;
+ }
+ }
+
+ hw->tdata1 = RISCV_DBTR_SET_MC_TYPE(hw->tdata1, RISCV_DBTR_TRIG_MCONTROL);
+
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_DMODE_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_TIMING_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_SELECT_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_ACTION_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_CHAIN_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_MATCH_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_M_BIT);
+
+ SET_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_S_BIT);
+ SET_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC_U_BIT);
+
+ return 0;
+}
+
+static int rv_init_mcontrol6_trigger(const struct perf_event_attr *attr,
+ struct arch_hw_breakpoint *hw)
+{
+ switch (attr->bp_type) {
+ case HW_BREAKPOINT_X:
+ hw->type = RISCV_DBTR_EXEC;
+ RISCV_DBTR_SET_MC6_EXEC_BIT(hw->tdata1);
+ break;
+ case HW_BREAKPOINT_R:
+ hw->type = RISCV_DBTR_LOAD;
+ RISCV_DBTR_SET_MC6_LOAD_BIT(hw->tdata1);
+ break;
+ case HW_BREAKPOINT_W:
+ hw->type = RISCV_DBTR_STORE;
+ RISCV_DBTR_SET_MC6_STORE_BIT(hw->tdata1);
+ break;
+ case HW_BREAKPOINT_RW:
+ hw->type = RISCV_DBTR_LDST;
+ RISCV_DBTR_SET_MC6_STORE_BIT(hw->tdata1);
+ RISCV_DBTR_SET_MC6_LOAD_BIT(hw->tdata1);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (attr->bp_type == HW_BREAKPOINT_X) {
+ /* See rv_init_mcontrol_trigger() for rationale. */
+ hw->len = 0;
+ hw->tdata1 = RISCV_DBTR_SET_MC6_SIZE(hw->tdata1, 0);
+ } else {
+ switch (attr->bp_len) {
+ case HW_BREAKPOINT_LEN_1:
+ hw->len = 1;
+ hw->tdata1 = RISCV_DBTR_SET_MC6_SIZE(hw->tdata1, 1);
+ break;
+ case HW_BREAKPOINT_LEN_2:
+ hw->len = 2;
+ hw->tdata1 = RISCV_DBTR_SET_MC6_SIZE(hw->tdata1, 2);
+ break;
+ case HW_BREAKPOINT_LEN_4:
+ hw->len = 4;
+ hw->tdata1 = RISCV_DBTR_SET_MC6_SIZE(hw->tdata1, 3);
+ break;
+#if __riscv_xlen >= 64
+ case HW_BREAKPOINT_LEN_8:
+ hw->len = 8;
+ hw->tdata1 = RISCV_DBTR_SET_MC6_SIZE(hw->tdata1, 5);
+ break;
+#endif
+ /* Set to match any size */
+ default:
+ hw->len = 0;
+ hw->tdata1 = RISCV_DBTR_SET_MC6_SIZE(hw->tdata1, 0);
+ }
+ }
+
+ hw->tdata1 = RISCV_DBTR_SET_MC6_TYPE(hw->tdata1, RISCV_DBTR_TRIG_MCONTROL6);
+
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_DMODE_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_TIMING_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_SELECT_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_ACTION_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_CHAIN_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_MATCH_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_M_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_VS_BIT);
+ CLEAR_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_VU_BIT);
+
+ SET_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_S_BIT);
+ SET_DBTR_BIT(hw->tdata1, RISCV_DBTR_MC6_U_BIT);
+
+ return 0;
+}
+
+int hw_breakpoint_arch_parse(struct perf_event *bp,
+ const struct perf_event_attr *attr,
+ struct arch_hw_breakpoint *hw)
+{
+ int ret;
+
+ /* Breakpoint address */
+ hw->address = attr->bp_addr;
+ hw->tdata2 = attr->bp_addr;
+ hw->tdata3 = 0x0;
+
+ switch (dbtr_type) {
+ case RISCV_DBTR_TRIG_MCONTROL:
+ ret = rv_init_mcontrol_trigger(attr, hw);
+ break;
+ case RISCV_DBTR_TRIG_MCONTROL6:
+ ret = rv_init_mcontrol6_trigger(attr, hw);
+ break;
+ default:
+ pr_warn("Unsupported trigger type\n");
+ ret = -EOPNOTSUPP;
+ break;
+ }
+
+ return ret;
+}
+
+/*
+ * HW Breakpoint/watchpoint handler
+ */
+static int hw_breakpoint_handler(struct die_args *args)
+{
+ int ret = NOTIFY_DONE;
+ struct arch_hw_breakpoint *bp;
+ struct perf_event *event;
+ int i;
+
+ for (i = 0; i < dbtr_total_num; i++) {
+ event = this_cpu_read(pcpu_hw_bp_events[i]);
+ if (!event)
+ continue;
+
+ bp = counter_arch_bp(event);
+ switch (bp->type) {
+ /* Breakpoint */
+ case RISCV_DBTR_EXEC:
+ if (bp->address == args->regs->epc) {
+ perf_bp_event(event, args->regs);
+ ret = NOTIFY_STOP;
+ }
+ break;
+
+ /* Watchpoint */
+ case RISCV_DBTR_LOAD:
+ case RISCV_DBTR_STORE:
+ case RISCV_DBTR_LDST:
+ {
+ unsigned long stval = args->regs->badaddr;
+ unsigned long bp_start = bp->address;
+ unsigned long bp_len = bp->len ?: 1;
+ unsigned long bp_end = bp_start + bp_len - 1;
+ unsigned long stval_end = stval + sizeof(long) - 1;
+ unsigned long tdata1;
+ bool hit = false;
+ struct sbiret sret;
+ union sbi_dbtr_shmem_entry *shmem;
+
+ if (bp_end < bp_start)
+ bp_end = ~0UL;
+ if (stval_end < stval)
+ stval_end = ~0UL;
+
+ /*
+ * Prefer tdata1.hit from SBI trigger readout whenever
+ * possible. Fall back to address-based matching if HIT
+ * isn't observed/supported.
+ */
+ raw_spin_lock_irqsave(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+ shmem = this_cpu_ptr(sbi_dbtr_shmem);
+ sret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_READ,
+ i, 1, 0, 0, 0, 0);
+ if (!sret.error) {
+ tdata1 = le_to_cpu(shmem->data.tdata1);
+
+ if (dbtr_type == RISCV_DBTR_TRIG_MCONTROL)
+ hit = !!(tdata1 & RISCV_DBTR_MC_HIT_BIT_MASK);
+ else if (dbtr_type == RISCV_DBTR_TRIG_MCONTROL6)
+ hit = !!(tdata1 & RISCV_DBTR_MC6_HIT_BIT_MASK);
+ }
+ raw_spin_unlock_irqrestore(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+
+ /*
+ * Sdtrig may report STVAL as the lowest accessed
+ * address while the watchpoint can match a higher byte
+ * in the same access.
+ */
+ if (hit ||
+ (stval >= bp_start && stval <= bp_end) ||
+ (bp_start >= stval && bp_start <= stval_end)) {
+ perf_bp_event(event, args->regs);
+ ret = NOTIFY_STOP;
+ }
+ break;
+ }
+
+ default:
+ pr_warn("Unknown type: %u\n", bp->type);
+ break;
+ }
+ }
+
+ return ret;
+}
+
+int hw_breakpoint_exceptions_notify(struct notifier_block *unused,
+ unsigned long val, void *data)
+{
+ if (val != DIE_DEBUG)
+ return NOTIFY_DONE;
+
+ return hw_breakpoint_handler(data);
+}
+
+/* atomic: counter->ctx->lock is held */
+int arch_install_hw_breakpoint(struct perf_event *event)
+{
+ struct arch_hw_breakpoint *bp = counter_arch_bp(event);
+ union sbi_dbtr_shmem_entry *shmem = this_cpu_ptr(sbi_dbtr_shmem);
+ struct sbi_dbtr_data_msg *xmit;
+ struct sbi_dbtr_id_msg *recv;
+ struct perf_event **slot;
+ unsigned long idx;
+ struct sbiret ret;
+ int err = 0;
+
+ raw_spin_lock_irqsave(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+
+ xmit = &shmem->data;
+ recv = &shmem->id;
+ xmit->tdata1 = cpu_to_le(bp->tdata1);
+ xmit->tdata2 = cpu_to_le(bp->tdata2);
+ xmit->tdata3 = cpu_to_le(bp->tdata3);
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_INSTALL,
+ 1, 0, 0, 0, 0, 0);
+
+ if (ret.error) {
+ pr_warn("Failed to install trigger\n");
+ err = sbi_err_map_linux_errno(ret.error);
+ goto done;
+ }
+
+ idx = le_to_cpu(recv->idx);
+ if (idx >= dbtr_total_num) {
+ pr_warn("Invalid trigger index %lu\n", idx);
+ err = -EINVAL;
+ goto done;
+ }
+
+ slot = this_cpu_ptr(&pcpu_hw_bp_events[idx]);
+ if (*slot) {
+ pr_warn("Slot %lu is in use\n", idx);
+ err = -EBUSY;
+ goto done;
+ }
+
+ /* Save the event - to be looked up in handler */
+ *slot = event;
+
+done:
+ raw_spin_unlock_irqrestore(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+ return err;
+}
+
+/* atomic: counter->ctx->lock is held */
+void arch_uninstall_hw_breakpoint(struct perf_event *event)
+{
+ struct sbiret ret;
+ int i;
+
+ raw_spin_lock_irqsave(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+
+ for (i = 0; i < dbtr_total_num; i++) {
+ struct perf_event **slot = this_cpu_ptr(&pcpu_hw_bp_events[i]);
+
+ if (*slot == event) {
+ *slot = NULL;
+ break;
+ }
+ }
+
+ if (i == dbtr_total_num) {
+ pr_warn("Breakpoint not installed.\n");
+ goto out;
+ }
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_UNINSTALL,
+ i, 1, 0, 0, 0, 0);
+
+ if (ret.error) {
+ pr_warn("Failed to uninstall trigger %d.\n", i);
+ goto out;
+ }
+
+ out:
+ raw_spin_unlock_irqrestore(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+}
+
+void arch_enable_hw_breakpoint(struct perf_event *event)
+{
+ struct sbiret ret;
+ int i;
+ struct perf_event **slot;
+
+ raw_spin_lock_irqsave(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+
+ for (i = 0; i < dbtr_total_num; i++) {
+ slot = this_cpu_ptr(&pcpu_hw_bp_events[i]);
+
+ if (*slot == event)
+ break;
+ }
+
+ if (i == dbtr_total_num) {
+ pr_warn("Breakpoint not installed.\n");
+ goto out;
+ }
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_ENABLE,
+ i, 1, 0, 0, 0, 0);
+
+ if (ret.error) {
+ pr_warn("Failed to install trigger %d\n", i);
+ goto out;
+ }
+
+ out:
+ raw_spin_unlock_irqrestore(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+}
+EXPORT_SYMBOL_GPL(arch_enable_hw_breakpoint);
+
+void arch_update_hw_breakpoint(struct perf_event *event)
+{
+ struct arch_hw_breakpoint *bp = counter_arch_bp(event);
+ union sbi_dbtr_shmem_entry *shmem = this_cpu_ptr(sbi_dbtr_shmem);
+ struct sbi_dbtr_data_msg *xmit;
+ struct perf_event **slot;
+ struct sbiret ret;
+ int i;
+
+ for (i = 0; i < dbtr_total_num; i++) {
+ slot = this_cpu_ptr(&pcpu_hw_bp_events[i]);
+
+ if (*slot == event)
+ break;
+ }
+
+ if (i == dbtr_total_num) {
+ pr_warn("Breakpoint not installed.\n");
+ return;
+ }
+
+ raw_spin_lock_irqsave(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+
+ xmit = &shmem->data;
+ xmit->tdata1 = cpu_to_le(bp->tdata1);
+ xmit->tdata2 = cpu_to_le(bp->tdata2);
+ xmit->tdata3 = cpu_to_le(bp->tdata3);
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_UPDATE,
+ i, 1, 0, 0, 0, 0);
+ if (ret.error)
+ pr_warn("Failed to update trigger %d.\n", i);
+
+ raw_spin_unlock_irqrestore(this_cpu_ptr(&ecall_lock),
+ *this_cpu_ptr(&ecall_lock_flags));
+}
+EXPORT_SYMBOL_GPL(arch_update_hw_breakpoint);
+
+void arch_disable_hw_breakpoint(struct perf_event *event)
+{
+ struct perf_event **slot;
+ struct sbiret ret;
+ int i;
+
+ for (i = 0; i < dbtr_total_num; i++) {
+ slot = this_cpu_ptr(&pcpu_hw_bp_events[i]);
+
+ if (*slot == event)
+ break;
+ }
+
+ if (i == dbtr_total_num) {
+ pr_warn("Breakpoint not installed.\n");
+ return;
+ }
+
+ ret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_DISABLE,
+ i, 1, 0, 0, 0, 0);
+
+ if (ret.error) {
+ pr_warn("Failed to uninstall trigger %d.\n", i);
+ return;
+ }
+}
+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;
+ int rc = 0;
+
+ for_each_possible_cpu(cpu)
+ raw_spin_lock_init(&per_cpu(ecall_lock, cpu));
+
+ if (!dbtr_init)
+ init_sbi_dbtr();
+
+ if (dbtr_total_num) {
+ pr_info("Total number of type %d triggers: %u\n",
+ dbtr_type, dbtr_total_num);
+ } else {
+ pr_info("No hardware triggers available\n");
+ goto out;
+ }
+
+ /* Allocate per-cpu shared memory */
+ sbi_dbtr_shmem = __alloc_percpu(sizeof(*sbi_dbtr_shmem) * dbtr_total_num,
+ PAGE_SIZE);
+
+ if (!sbi_dbtr_shmem) {
+ pr_warn("Failed to allocate shared memory.\n");
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ /* Hotplug handler to register/unregister shared memory with SBI */
+ rc = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
+ "riscv/hw_breakpoint:prepare",
+ arch_smp_setup_sbi_shmem,
+ arch_smp_teardown_sbi_shmem);
+
+ if (rc < 0) {
+ pr_warn("Failed to setup CPU hotplug state\n");
+ free_percpu(sbi_dbtr_shmem);
+ return rc;
+ }
+ out:
+ return rc;
+}
+arch_initcall(arch_hw_breakpoint_init);
diff --git a/arch/riscv/kernel/traps.c b/arch/riscv/kernel/traps.c
index 8c62c771a656..029fd66a285e 100644
--- a/arch/riscv/kernel/traps.c
+++ b/arch/riscv/kernel/traps.c
@@ -286,6 +286,12 @@ void handle_break(struct pt_regs *regs)
if (probe_breakpoint_handler(regs))
return;
+#ifdef CONFIG_HAVE_HW_BREAKPOINT
+ if (notify_die(DIE_DEBUG, "EBREAK", regs, 0, regs->cause, SIGTRAP)
+ == NOTIFY_STOP)
+ return;
+#endif
+
current->thread.bad_cause = regs->cause;
if (user_mode(regs))
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v6 2/5] riscv: Add breakpoint and watchpoint test for riscv
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 1/5] " Himanshu Chauhan
@ 2026-08-03 13:49 ` Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints Himanshu Chauhan
` (3 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: Himanshu Chauhan @ 2026-08-03 13:49 UTC (permalink / raw)
To: linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe, Himanshu Chauhan
Add self test for riscv architecture. It uses ptrace to ptrace framework
to set/unset break/watchpoint and uses signals to check triggers.
Also add $(KHDR_INCLUDES) to CFLAGS for the riscv test build so the
UAPI headers exercised by the test can be located during compilation.
Signed-off-by: Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
---
tools/testing/selftests/breakpoints/Makefile | 5 +
.../breakpoints/breakpoint_test_riscv.c | 219 ++++++++++++++++++
2 files changed, 224 insertions(+)
create mode 100644 tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
diff --git a/tools/testing/selftests/breakpoints/Makefile b/tools/testing/selftests/breakpoints/Makefile
index 0b8f5acf7c78..b32c56b3c1db 100644
--- a/tools/testing/selftests/breakpoints/Makefile
+++ b/tools/testing/selftests/breakpoints/Makefile
@@ -12,5 +12,10 @@ ifneq (,$(filter $(ARCH),aarch64 arm64))
TEST_GEN_PROGS += breakpoint_test_arm64
endif
+ifneq (,$(filter $(ARCH),riscv))
+CFLAGS += -static $(KHDR_INCLUDES)
+TEST_GEN_PROGS += breakpoint_test_riscv
+endif
+
include ../lib.mk
diff --git a/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c b/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
new file mode 100644
index 000000000000..0649940b709e
--- /dev/null
+++ b/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
@@ -0,0 +1,219 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2026 Qualcomm Technologies, Inc.
+ *
+ * Author: Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
+ */
+
+#define _GNU_SOURCE
+#include <linux/perf_event.h> /* Definition of PERF_* constants */
+#include <linux/hw_breakpoint.h> /* Definition of HW_* constants */
+#include <sys/syscall.h> /* Definition of SYS_* constants */
+#include <unistd.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <time.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <sys/mman.h>
+#include <string.h>
+#include <semaphore.h>
+#include <errno.h>
+
+#ifndef noinline
+#define noinline __attribute__((noinline))
+#endif
+
+static int gfd;
+sem_t ib_mtx, wp_mtx;
+static int bp_triggered, wp_triggered;
+static int test_func_sink;
+static const int wait_timeout_sec = 5;
+
+int setup_bp(bool is_x, void *addr, int sig)
+{
+ struct perf_event_attr pe;
+ int fd;
+
+ memset(&pe, 0, sizeof(struct perf_event_attr));
+ pe.type = PERF_TYPE_BREAKPOINT;
+ pe.size = sizeof(struct perf_event_attr);
+
+ pe.config = 0;
+ pe.bp_type = is_x ? HW_BREAKPOINT_X : HW_BREAKPOINT_W;
+ pe.bp_addr = (unsigned long)addr;
+ pe.bp_len = sizeof(long);
+
+ pe.sample_period = 1;
+ pe.sample_type = PERF_SAMPLE_IP;
+ pe.wakeup_events = 1;
+
+ pe.disabled = 1;
+ pe.exclude_kernel = 1;
+ pe.exclude_hv = 1;
+
+ fd = syscall(SYS_perf_event_open, &pe, 0, -1, -1, 0);
+ if (fd < 0) {
+ printf("Failed to open event: %llx\n", pe.config);
+ return -1;
+ }
+
+ fcntl(fd, F_SETFL, O_RDWR | O_NONBLOCK | O_ASYNC);
+ fcntl(fd, F_SETSIG, sig);
+ fcntl(fd, F_SETOWN, getpid());
+
+ ioctl(fd, PERF_EVENT_IOC_RESET, 0);
+
+ return fd;
+}
+
+static void sig_handler_bp(int signum, siginfo_t *oh, void *uc)
+{
+ int ret;
+
+ bp_triggered++;
+
+ printf("Breakpoint triggered!\n");
+ ioctl(gfd, PERF_EVENT_IOC_DISABLE, 0);
+ ret = sem_post(&ib_mtx);
+ if (ret) {
+ printf("Failed to report BP success\n");
+ return;
+ }
+}
+
+static void sig_handler_wp(int signum, siginfo_t *oh, void *uc)
+{
+ int ret;
+
+ printf("Watchpoint triggered!\n");
+ ioctl(gfd, PERF_EVENT_IOC_DISABLE, 0);
+ wp_triggered++;
+
+ ret = sem_post(&wp_mtx);
+
+ if (ret) {
+ printf("Failed to report WP success\n");
+ return;
+ }
+}
+
+/*
+ * Keep a real instruction address for HW execute breakpoints: prevent inlining
+ * and force a visible side effect so the function can't be optimized away.
+ */
+static noinline void test_func(void)
+{
+ test_func_sink++;
+ __asm__ __volatile__("" : : "g" (test_func_sink));
+}
+
+static int trigger_bp(void)
+{
+ struct sigaction sa;
+
+ memset(&sa, 0, sizeof(struct sigaction));
+ sa.sa_sigaction = (void *)sig_handler_bp;
+ sa.sa_flags = SA_SIGINFO;
+
+ if (sigaction(SIGIO, &sa, NULL) < 0) {
+ printf("Failed to setup signal handler\n");
+ return -1;
+ }
+
+ gfd = setup_bp(1, test_func, SIGIO);
+
+ if (gfd < 0) {
+ printf("Failed to setup breakpoint.\n");
+ return -1;
+ }
+
+ ioctl(gfd, PERF_EVENT_IOC_ENABLE, 0);
+
+ test_func();
+
+ ioctl(gfd, PERF_EVENT_IOC_DISABLE, 0);
+
+ close(gfd);
+
+ return 0;
+}
+
+static int trigger_wp(void)
+{
+ struct sigaction sa;
+ unsigned long test_data;
+
+ memset(&sa, 0, sizeof(struct sigaction));
+ sa.sa_sigaction = (void *)sig_handler_wp;
+ sa.sa_flags = SA_SIGINFO;
+
+ if (sigaction(SIGUSR1, &sa, NULL) < 0) {
+ printf("Failed to setup signal handler\n");
+ return -1;
+ }
+
+ gfd = setup_bp(0, &test_data, SIGUSR1);
+
+ if (gfd < 0) {
+ printf("Failed to setup watchpoint\n");
+ return -1;
+ }
+
+ ioctl(gfd, PERF_EVENT_IOC_ENABLE, 0);
+ test_data = 0xdeadbeef;
+ ioctl(gfd, PERF_EVENT_IOC_DISABLE, 0);
+
+ return 0;
+}
+
+static int wait_event(sem_t *sem, const char *name)
+{
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_REALTIME, &ts)) {
+ printf("%s: Failed to get current time\n", name);
+ return -1;
+ }
+
+ /*
+ * Deadlock fix: avoid blocking forever on sem_wait() if the breakpoint/
+ * watchpoint signal never arrives. Use a bounded wait and fail the test
+ * on timeout instead.
+ */
+ ts.tv_sec += wait_timeout_sec;
+ if (!sem_timedwait(sem, &ts))
+ return 0;
+
+ if (errno == ETIMEDOUT)
+ printf("%s: Timed out waiting for event\n", name);
+ else
+ printf("%s: sem_timedwait() failed with %d\n", name, errno);
+
+ return -1;
+}
+
+int main(int argc, char *argv[])
+{
+ sem_init(&ib_mtx, 0, 0);
+ if (trigger_bp() < 0)
+ return -1;
+ if (wait_event(&ib_mtx, "Breakpoint") < 0)
+ return -1;
+
+ if (bp_triggered)
+ printf("Breakpoint test passed!\n");
+
+ sem_init(&wp_mtx, 0, 0);
+ if (trigger_wp() < 0)
+ return -1;
+ if (wait_event(&wp_mtx, "Watchpoint") < 0)
+ return -1;
+
+ if (wp_triggered)
+ printf("Watchpoint test passed!\n");
+
+ return 0;
+}
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 1/5] " Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 2/5] riscv: Add breakpoint and watchpoint test for riscv Himanshu Chauhan
@ 2026-08-03 13:49 ` Himanshu Chauhan
2026-08-05 13:22 ` Chen Pei
2026-08-03 13:49 ` [PATCH v6 4/5] selftests/breakpoints: extend riscv test for ptrace hw break/watchpoints Himanshu Chauhan
` (2 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Himanshu Chauhan @ 2026-08-03 13:49 UTC (permalink / raw)
To: linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe, Himanshu Chauhan
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@oss.qualcomm.com>
---
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
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
/*
* 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;
+ }
+ }
+}
+
+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;
+}
+
+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 */
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v6 4/5] selftests/breakpoints: extend riscv test for ptrace hw break/watchpoints
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
` (2 preceding siblings ...)
2026-08-03 13:49 ` [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints Himanshu Chauhan
@ 2026-08-03 13:49 ` Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 5/5] MAINTAINERS: Add entry for RISC-V Debugging Himanshu Chauhan
2026-08-04 17:10 ` [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Jesse Taube
5 siblings, 0 replies; 9+ messages in thread
From: Himanshu Chauhan @ 2026-08-03 13:49 UTC (permalink / raw)
To: linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe, Himanshu Chauhan
Cover the new ptrace-based hardware breakpoint/watchpoint support
added for riscv: exercise the PTRACE_GETREGSET/SETREGSET regset path
as well as the raw PTRACE_GETHBPREGS/SETHBPREGS interface, alongside
the existing perf_event-based tests.
Also drop a leftover #if 0 block duplicating the HWDEBUG_* control
field macros already defined in uapi/asm/ptrace.h.
Signed-off-by: Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
---
.../breakpoints/breakpoint_test_riscv.c | 590 +++++++++++++++++-
1 file changed, 568 insertions(+), 22 deletions(-)
diff --git a/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c b/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
index 0649940b709e..ee6641531fa7 100644
--- a/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
+++ b/tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
@@ -9,6 +9,7 @@
#include <linux/perf_event.h> /* Definition of PERF_* constants */
#include <linux/hw_breakpoint.h> /* Definition of HW_* constants */
#include <sys/syscall.h> /* Definition of SYS_* constants */
+#include <asm/ptrace.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdio.h>
@@ -18,18 +19,572 @@
#include <fcntl.h>
#include <signal.h>
#include <sys/mman.h>
+#include <sys/ptrace.h>
+#include <sys/wait.h>
#include <string.h>
#include <semaphore.h>
#include <errno.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <linux/elf.h>
#ifndef noinline
#define noinline __attribute__((noinline))
#endif
+#include "kselftest.h"
+
+static int test_func_sink;
+
+/*
+ * Keep a real instruction address for HW execute breakpoints: prevent inlining
+ * and force a visible side effect so the function can't be optimized away.
+ */
+static noinline void test_func(void)
+{
+ test_func_sink++;
+ __asm__ __volatile__("" : : "g" (test_func_sink));
+}
+
+/*
+ * BREAKPOINT TEST USING PTRACE
+ */
+static int do_wp_child(void *addr, size_t size)
+{
+ if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_TRACEME) failed: %s\n",
+ strerror(errno));
+ _exit(1);
+ }
+
+ if (raise(SIGSTOP) != 0) {
+ ksft_print_msg(
+ "raise(SIGSTOP) failed: %s\n", strerror(errno));
+ _exit(1);
+ }
+
+ sleep(1);
+ switch (size) {
+ case 1:
+ *(uint8_t *)addr = 47;
+ break;
+ case 2:
+ *(uint16_t *)addr = 47;
+ break;
+ case 4:
+ *(uint32_t *)addr = 47;
+ break;
+ case 8:
+ *(uint64_t *)addr = 47;
+ break;
+ default:
+ ksft_print_msg("Unknown watchpoint access size %u\n", size);
+ break;
+ }
+
+ _exit(0);
+}
+
+static int do_bp_child(void (*bp_func)(void))
+{
+ if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_TRACEME) failed: %s\n",
+ strerror(errno));
+ _exit(1);
+ }
+
+ if (raise(SIGSTOP) != 0) {
+ ksft_print_msg(
+ "raise(SIGSTOP) failed: %s\n", strerror(errno));
+ _exit(1);
+ }
+
+ bp_func();
+}
+
+unsigned long var;
+
+static bool set_watchpoint(pid_t pid, int size)
+{
+ uint8_t *addr = (uint8_t *)&var;
+ unsigned int control = 0;
+ struct user_hwdebug_state dreg_state;
+ struct iovec iov;
+
+ /* Write watchpoint */
+ control = (HW_BREAKPOINT_W << 14) & ((0x7 << 14));
+ control |= (HW_BREAKPOINT_LEN_8 << 4) & ((0x1f << 4));
+ memset(&dreg_state, 0, sizeof(dreg_state));
+ dreg_state.dbg_regs[0].addr = (uintptr_t)(addr);
+ dreg_state.dbg_regs[0].control = control;
+ iov.iov_base = &dreg_state;
+ iov.iov_len = offsetof(struct user_hwdebug_state, dbg_regs) +
+ sizeof(dreg_state.dbg_regs[0]);
+
+ if (ptrace(PTRACE_SETREGSET, pid, NT_RISCV_HW_WATCH, &iov) == 0) {
+ memset(&iov, 0, sizeof(iov));
+ memset(&dreg_state, 0, sizeof(dreg_state));
+
+ iov.iov_base = &dreg_state;
+ iov.iov_len = offsetof(struct user_hwdebug_state, dbg_regs) +
+ sizeof(dreg_state.dbg_regs[0]);
+
+ if (ptrace(PTRACE_GETREGSET, pid, NT_RISCV_HW_WATCH, &iov) == 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_GETREGSET): Number of watchpoints: %u\n", dreg_state.info);
+ ksft_print_msg(
+ "ptrace(PTRACE_GETREGSet): addr: 0x%lx control: 0x%x\n", dreg_state.dbg_regs[0].addr, dreg_state.dbg_regs[0].control);
+ } else {
+ ksft_print_msg(
+ "ptrace(PTRACE_GETREGSET): Failed\n");
+ return false;
+ }
+
+ return true;
+ }
+
+ if (errno == EIO)
+ ksft_print_msg(
+ "ptrace(PTRACE_SETREGSET, NT_RISCV_HW_WATCH) not supported on this hardware: %s\n",
+ strerror(errno));
+ else
+ ksft_print_msg(
+ "ptrace(PTRACE_SETREGSET, NT_RISCV_HW_WATCH) failed: %s\n",
+ strerror(errno));
+ return false;
+}
+
+static bool set_breakpoint(pid_t pid, void (*bp_func)(void))
+{
+ struct user_hwdebug_state dreg_state;
+ struct iovec iov;
+ unsigned int control = 0;
+
+ control = (HW_BREAKPOINT_X << 14) & ((0x7 << 14));
+ control |= (HW_BREAKPOINT_LEN_8 << 4) & ((0x1f << 4));
+ memset(&dreg_state, 0, sizeof(dreg_state));
+ dreg_state.dbg_regs[0].addr = (uintptr_t)bp_func;
+ dreg_state.dbg_regs[0].control = control;
+ iov.iov_base = &dreg_state;
+ iov.iov_len = offsetof(struct user_hwdebug_state, dbg_regs) + sizeof(dreg_state.dbg_regs[0]);
+
+ if (ptrace(PTRACE_SETREGSET, pid, NT_RISCV_HW_BREAK, &iov) == 0)
+ return true;
+
+ if (errno == EIO)
+ ksft_print_msg(
+ "ptrace(PTRACE_SETREGSET, NT_RISCV_HW_BREAK) not supported on this hardware: %s\n", strerror(errno));
+ else
+ ksft_print_msg(
+ "ptrace(PTRACE_SETREGSET, NT_RISCV_HW_BREAK) failed: %s\n", strerror(errno));
+
+ return false;
+}
+
+static int run_ptrace_wp_test(void)
+{
+ pid_t pid = fork();
+ pid_t wpid;
+ siginfo_t siginfo;
+ int status;
+
+ if (pid == 0)
+ do_wp_child(&var, 8);
+
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg(
+ "waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg(
+ "child did not stop: %s\n", strerror(errno));
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGSTOP) {
+ ksft_print_msg("child did not stop with SIGSTOP\n");
+ return false;
+ }
+
+ if (!set_watchpoint(pid, 8))
+ return false;
+
+ if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_CONT) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ alarm(3);
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg(
+ "waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ alarm(0);
+ if (WIFEXITED(status)) {
+ ksft_print_msg("child exited prematurely\n");
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg("child did not stop\n");
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGTRAP) {
+ ksft_print_msg("child did not stop with SIGTRAP\n");
+ return false;
+ }
+ if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &siginfo) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_GETSIGINFO): %s\n",
+ strerror(errno));
+ return false;
+ }
+ if (siginfo.si_code != TRAP_HWBKPT) {
+ ksft_print_msg(
+ "Unexpected si_code %d\n", siginfo.si_code);
+ return false;
+ }
+
+ kill(pid, SIGKILL);
+ wpid = waitpid(pid, &status, 0);
+ if (wpid != pid) {
+ ksft_print_msg(
+ "waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+
+ ksft_print_msg("[ptrace]: Watchpoint test passed!\n");
+
+ return true;
+}
+
+static int run_ptrace_bp_test(void)
+{
+ pid_t pid = fork();
+ pid_t wpid;
+ siginfo_t siginfo;
+ int status;
+
+ if (pid == 0)
+ do_bp_child(test_func);
+
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg(
+ "waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg(
+ "child did not stop: %s\n", strerror(errno));
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGSTOP) {
+ ksft_print_msg("child did not stop with SIGSTOP\n");
+ return false;
+ }
+
+ if (!set_breakpoint(pid, test_func))
+ return false;
+
+ if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_CONT) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ alarm(3);
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg(
+ "waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ alarm(0);
+ if (WIFEXITED(status)) {
+ ksft_print_msg("child exited prematurely\n");
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg("child did not stop\n");
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGTRAP) {
+ ksft_print_msg("child did not stop with SIGTRAP\n");
+ return false;
+ }
+ if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &siginfo) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_GETSIGINFO): %s\n",
+ strerror(errno));
+ return false;
+ }
+ if (siginfo.si_code != TRAP_HWBKPT) {
+ ksft_print_msg(
+ "Unexpected si_code %d\n", siginfo.si_code);
+ return false;
+ }
+
+ kill(pid, SIGKILL);
+ wpid = waitpid(pid, &status, 0);
+ if (wpid != pid) {
+ ksft_print_msg(
+ "waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+
+ ksft_print_msg("[ptrace]: Breakpoint test passed!\n");
+
+ return true;
+}
+
+/*
+ * BREAKPOINT TEST USING PTRACE_SETHBPREGS / PTRACE_GETHBPREGS
+ */
+static bool set_hbpregs_watchpoint(pid_t pid)
+{
+ struct __riscv_hwdebug_state state;
+
+ memset(&state, 0, sizeof(state));
+ state.addr = (unsigned long)&var;
+ state.len = HW_BREAKPOINT_LEN_8;
+ state.type = HW_BREAKPOINT_W;
+ state.ctrl = 0; /* enabled */
+
+ if (ptrace(PTRACE_SETHBPREGS, pid, 0, &state) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_SETHBPREGS) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ /* Read back and verify */
+ memset(&state, 0, sizeof(state));
+ if (ptrace(PTRACE_GETHBPREGS, pid, 0, &state) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_GETHBPREGS) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ ksft_print_msg(
+ "[hbpregs] watchpoint readback: addr=0x%lx type=%lu len=%lu ctrl=%lu\n",
+ state.addr, state.type, state.len, state.ctrl);
+
+ return true;
+}
+
+static bool set_hbpregs_breakpoint(pid_t pid, void (*bp_func)(void))
+{
+ struct __riscv_hwdebug_state state;
+
+ memset(&state, 0, sizeof(state));
+ state.addr = (unsigned long)bp_func;
+ state.len = HW_BREAKPOINT_LEN_4;
+ state.type = HW_BREAKPOINT_X;
+ state.ctrl = 0; /* enabled */
+
+ if (ptrace(PTRACE_SETHBPREGS, pid, 0, &state) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_SETHBPREGS) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ /* Read back and verify */
+ memset(&state, 0, sizeof(state));
+ if (ptrace(PTRACE_GETHBPREGS, pid, 0, &state) != 0) {
+ ksft_print_msg(
+ "ptrace(PTRACE_GETHBPREGS) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ ksft_print_msg(
+ "[hbpregs] breakpoint readback: addr=0x%lx type=%lu len=%lu ctrl=%lu\n",
+ state.addr, state.type, state.len, state.ctrl);
+
+ return true;
+}
+
+static int run_hbpregs_wp_test(void)
+{
+ pid_t pid = fork();
+ pid_t wpid;
+ siginfo_t siginfo;
+ int status;
+
+ if (pid == 0)
+ do_wp_child(&var, 8);
+
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg("waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg("child did not stop: %s\n", strerror(errno));
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGSTOP) {
+ ksft_print_msg("child did not stop with SIGSTOP\n");
+ return false;
+ }
+
+ if (!set_hbpregs_watchpoint(pid))
+ return false;
+
+ if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
+ ksft_print_msg("ptrace(PTRACE_CONT) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ alarm(3);
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg("waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ alarm(0);
+ if (WIFEXITED(status)) {
+ ksft_print_msg("child exited prematurely\n");
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg("child did not stop\n");
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGTRAP) {
+ ksft_print_msg("child did not stop with SIGTRAP\n");
+ return false;
+ }
+ if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &siginfo) != 0) {
+ ksft_print_msg("ptrace(PTRACE_GETSIGINFO): %s\n",
+ strerror(errno));
+ return false;
+ }
+ if (siginfo.si_code != TRAP_HWBKPT) {
+ ksft_print_msg("Unexpected si_code %d\n", siginfo.si_code);
+ return false;
+ }
+
+ kill(pid, SIGKILL);
+ wpid = waitpid(pid, &status, 0);
+ if (wpid != pid) {
+ ksft_print_msg("waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+
+ ksft_print_msg("[hbpregs]: Watchpoint test passed!\n");
+ return true;
+}
+
+static int run_hbpregs_bp_test(void)
+{
+ pid_t pid = fork();
+ pid_t wpid;
+ siginfo_t siginfo;
+ int status;
+
+ if (pid == 0)
+ do_bp_child(test_func);
+
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg("waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg("child did not stop: %s\n", strerror(errno));
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGSTOP) {
+ ksft_print_msg("child did not stop with SIGSTOP\n");
+ return false;
+ }
+
+ if (!set_hbpregs_breakpoint(pid, test_func))
+ return false;
+
+ if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
+ ksft_print_msg("ptrace(PTRACE_CONT) failed: %s\n",
+ strerror(errno));
+ return false;
+ }
+
+ alarm(3);
+ wpid = waitpid(pid, &status, __WALL);
+ if (wpid != pid) {
+ ksft_print_msg("waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+ alarm(0);
+ if (WIFEXITED(status)) {
+ ksft_print_msg("child exited prematurely\n");
+ return false;
+ }
+ if (!WIFSTOPPED(status)) {
+ ksft_print_msg("child did not stop\n");
+ return false;
+ }
+ if (WSTOPSIG(status) != SIGTRAP) {
+ ksft_print_msg("child did not stop with SIGTRAP\n");
+ return false;
+ }
+ if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &siginfo) != 0) {
+ ksft_print_msg("ptrace(PTRACE_GETSIGINFO): %s\n",
+ strerror(errno));
+ return false;
+ }
+ if (siginfo.si_code != TRAP_HWBKPT) {
+ ksft_print_msg("Unexpected si_code %d\n", siginfo.si_code);
+ return false;
+ }
+
+ kill(pid, SIGKILL);
+ wpid = waitpid(pid, &status, 0);
+ if (wpid != pid) {
+ ksft_print_msg("waitpid() failed: %s\n", strerror(errno));
+ return false;
+ }
+
+ ksft_print_msg("[hbpregs]: Breakpoint test passed!\n");
+ return true;
+}
+
+static void run_hbpregs_tests(void)
+{
+ run_hbpregs_bp_test();
+ run_hbpregs_wp_test();
+}
+
+/*
+ * BREAKPOINT TEST USING PTRACE_SETHBPREGS / PTRACE_GETHBPREGS - END
+ */
+static void run_ptrace_tests(void)
+{
+ run_ptrace_bp_test();
+ run_ptrace_wp_test();
+}
+
+/*
+ * BREAKPOINT TEST USING PTRACE - END
+ */
+
+/*
+ * BREAKPOINT TEST USING perf events
+ */
static int gfd;
sem_t ib_mtx, wp_mtx;
static int bp_triggered, wp_triggered;
-static int test_func_sink;
static const int wait_timeout_sec = 5;
int setup_bp(bool is_x, void *addr, int sig)
@@ -56,7 +611,7 @@ int setup_bp(bool is_x, void *addr, int sig)
fd = syscall(SYS_perf_event_open, &pe, 0, -1, -1, 0);
if (fd < 0) {
- printf("Failed to open event: %llx\n", pe.config);
+ ksft_print_msg("Failed to open event: %llx\n", pe.config);
return -1;
}
@@ -75,11 +630,10 @@ static void sig_handler_bp(int signum, siginfo_t *oh, void *uc)
bp_triggered++;
- printf("Breakpoint triggered!\n");
ioctl(gfd, PERF_EVENT_IOC_DISABLE, 0);
ret = sem_post(&ib_mtx);
if (ret) {
- printf("Failed to report BP success\n");
+ ksft_print_msg("Failed to report BP success\n");
return;
}
}
@@ -88,28 +642,17 @@ static void sig_handler_wp(int signum, siginfo_t *oh, void *uc)
{
int ret;
- printf("Watchpoint triggered!\n");
ioctl(gfd, PERF_EVENT_IOC_DISABLE, 0);
wp_triggered++;
ret = sem_post(&wp_mtx);
if (ret) {
- printf("Failed to report WP success\n");
+ ksft_print_msg("Failed to report WP success\n");
return;
}
}
-/*
- * Keep a real instruction address for HW execute breakpoints: prevent inlining
- * and force a visible side effect so the function can't be optimized away.
- */
-static noinline void test_func(void)
-{
- test_func_sink++;
- __asm__ __volatile__("" : : "g" (test_func_sink));
-}
-
static int trigger_bp(void)
{
struct sigaction sa;
@@ -119,14 +662,14 @@ static int trigger_bp(void)
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGIO, &sa, NULL) < 0) {
- printf("Failed to setup signal handler\n");
+ ksft_print_msg("Failed to setup signal handler\n");
return -1;
}
gfd = setup_bp(1, test_func, SIGIO);
if (gfd < 0) {
- printf("Failed to setup breakpoint.\n");
+ ksft_print_msg("Failed to setup breakpoint.\n");
return -1;
}
@@ -151,14 +694,14 @@ static int trigger_wp(void)
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGUSR1, &sa, NULL) < 0) {
- printf("Failed to setup signal handler\n");
+ ksft_print_msg("Failed to setup signal handler\n");
return -1;
}
gfd = setup_bp(0, &test_data, SIGUSR1);
if (gfd < 0) {
- printf("Failed to setup watchpoint\n");
+ ksft_print_msg("Failed to setup watchpoint\n");
return -1;
}
@@ -204,7 +747,7 @@ int main(int argc, char *argv[])
return -1;
if (bp_triggered)
- printf("Breakpoint test passed!\n");
+ ksft_print_msg("[perf_event]: Breakpoint test passed!\n");
sem_init(&wp_mtx, 0, 0);
if (trigger_wp() < 0)
@@ -213,7 +756,10 @@ int main(int argc, char *argv[])
return -1;
if (wp_triggered)
- printf("Watchpoint test passed!\n");
+ ksft_print_msg("[perf_event]: Watchpoint test passed!\n");
+
+ run_ptrace_tests();
+ run_hbpregs_tests();
return 0;
}
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v6 5/5] MAINTAINERS: Add entry for RISC-V Debugging
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
` (3 preceding siblings ...)
2026-08-03 13:49 ` [PATCH v6 4/5] selftests/breakpoints: extend riscv test for ptrace hw break/watchpoints Himanshu Chauhan
@ 2026-08-03 13:49 ` Himanshu Chauhan
2026-08-04 17:10 ` [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Jesse Taube
5 siblings, 0 replies; 9+ messages in thread
From: Himanshu Chauhan @ 2026-08-03 13:49 UTC (permalink / raw)
To: linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe, Himanshu Chauhan
Added myself as maintainer for the RISC-V inline debugging with sdtrig extension
Signed-off-by: Himanshu Chauhan <himanshu.chauhan@oss.qualcomm.com>
---
MAINTAINERS | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 5114e6db7307..fc59ab4f7684 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23311,6 +23311,15 @@ F: drivers/irqchip/irq-riscv-rpmi-sysmsi.c
F: drivers/mailbox/riscv-sbi-mpxy-mbox.c
F: include/linux/mailbox/riscv-rpmi-message.h
+RISC-V DEBUGGING
+M: Himanshu Chauhan <himanshu@thechauhan.dev>
+L: linux-riscv@lists.infradead.org
+F: arch/riscv/include/asm/hw_breakpoint.h
+F: arch/riscv/include/asm/perf_event.h
+F: arch/riscv/kernel/hw_breakpoint.c
+F: arch/riscv/kernel/perf_*
+F: drivers/perf/
+
RISC-V SPACEMIT SoC Support
M: Yixun Lan <dlan@kernel.org>
L: linux-riscv@lists.infradead.org
--
2.43.0
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
` (4 preceding siblings ...)
2026-08-03 13:49 ` [PATCH v6 5/5] MAINTAINERS: Add entry for RISC-V Debugging Himanshu Chauhan
@ 2026-08-04 17:10 ` Jesse Taube
5 siblings, 0 replies; 9+ messages in thread
From: Jesse Taube @ 2026-08-04 17:10 UTC (permalink / raw)
To: Himanshu Chauhan
Cc: linux-riscv, qingfang.deng, alex, aou, palmer, pjw, shuah,
linux-kernel, thecharlesjenkins
On Mon, Aug 3, 2026 at 9:49 AM Himanshu Chauhan
<himanshu.chauhan@oss.qualcomm.com> wrote:
>
> This patchset adds support for hardware breakpoints and watchpoints in the
> RISC-V architecture. The framework is built on top of the perf subsystem and
> the SBI debug trigger extension (Sdtrig).
>
> v6 extends v5 with two new patches:
>
> - ptrace support: 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. A simpler PTRACE_GETHBPREGS/SETHBPREGS
> request pair is also provided for direct single-trigger access without going
> through the regset machinery. Thread flush/copy hooks are wired up so
> per-task breakpoints are cleaned up across fork/exec.
>
> - Extended selftest: the existing perf_event-based selftest is extended to
> also exercise the new ptrace GETREGSET/SETREGSET regset path and the raw
> PTRACE_GETHBPREGS/SETHBPREGS interface.
I would recoment addaping
`tools/testing/selftests/breakpoints/breakpoint_test_arm64.c` as a
test.
I set up a test repository here
https://github.com/Mr-Bossman/dbtr-buildroot for anyone else to test
this set.
Unfortunatly this set fails `perf test 15 16 17 18`. I would like
those tests to pass
before accepting this set. My set addresses the perf failures which is why it is
more complicated.
I would highly recommend basing your set off of it as it fixes many of
the issues that
I commented on in V5. The first two commits in my series are
refactoring duplicate code,
and can technically be dropped. Even if you don't go forward with using my set,
I would at least recommend taking a look at it, especially
`riscv: ptrace: Add hw breakpoint regset`
and
`riscv: ptrace: Add hw breakpoint support`
as they are significantly simpler.
https://github.com/Mr-Bossman/linux/commits/dev/jesse/sdtrig-rebase/
Thanks,
Jesse Taube
> Single stepping is ready and test but to follow this patch set.
> Virtualization of debug triggers are pending
>
> The SBI debug trigger extension is specified in Chapter 19 of the SBI
> specification:
> https://github.com/riscv-non-isa/riscv-sbi-doc/releases/download/v3.0/riscv-sbi.pdf
>
> The Sdtrig ISA is part of the RISC-V debug specification:
> https://github.com/riscv/riscv-debug-spec
>
> Changes from v5:
> - Rebased to v7.2-rc6
> - Simplified Macros in hw_breakpoint.h
> - Took care of the review comments
> - Added ptrace support for hardware break/watchpoints (new patch)
> - PTRACE_GETREGSET/SETREGSET via NT_RISCV_HW_BREAK / NT_RISCV_HW_WATCH
> - PTRACE_GETHBPREGS / SETHBPREGS for direct single-trigger access
> - HAVE_MIXED_BREAKPOINTS_REGS selected (break/watch share trigger pool)
> - flush_ptrace_hw_breakpoint / ptrace_hw_copy_thread wired up
> - Extended selftest to cover ptrace-based hw break/watchpoint paths (new patch)
>
> Changes from v4:
> - Rebased to v7.2-rc4
> - Fixed rv32 build error
> - Added pr_fmt to print KBUILD_MODNAME
> - Changed type of shmem_pa to phys_addr_t
> - Use per_cpu_ptr_to_phys instead of __pa for per-cpu allocated memory
> - Print successful registration/unregistration message when no error
> - Added RISC-V DEBUGGING section in MAINTAINERS and added myself as maintainer
> - Fixed warnings from checkpatch.pl --strict run
>
> Changes from v3:
> - Rebased to v7.1-rc3
> - For watchpoints, check tdata1.hit via SBI_EXT_DBTR_TRIG_READ and keep
> STVAL-based matching as fallback
> - Improved watchpoint matching when STVAL reports the lowest accessed address
> for wider memory accesses
> - Program execute breakpoints with SIZE=0 (match any size) to avoid misses
> with 16-bit/compressed instruction addresses
> - Updated selftest to avoid deadlock by replacing unbounded sem_wait() with
> sem_timedwait() timeout handling
> - Updated selftest breakpoint function so it cannot be inlined or optimized away
>
> Changes from v2:
> - Rebased to v7.0-rc1
> - Fixed warnings from checkpatch.pl --strict run
>
> Changes from v1:
> - The patch adding the SBI extension and function IDs is already merged; this
> series builds on top of that
> - Added breakpoint selftest in tools/testing/selftests/breakpoints/
>
> How to use:
> ~~~~~~~~~~~
> OpenSBI:
> https://github.com/riscv-software-src/opensbi.git
>
> QEMU:
> https://github.com/qemu/qemu.git
>
> Linux Kernel:
> Apply these patches on top of v7.2-rc6.
>
> How to test:
> ~~~~~~~~~~~
> From the Linux kernel directory, first install the UAPI headers (required on a
> fresh tree so the compiler can locate <asm/ptrace.h> and the new
> NT_RISCV_HW_BREAK/WATCH definitions via KHDR_INCLUDES):
>
> make headers
>
> Then build the selftest:
>
> make -C tools/testing/selftests/breakpoints/
>
> This produces breakpoint_test_riscv under the same directory. Load it on the
> target and run. Sample output:
>
> # /apps/breakpoint_test_riscv
> # [perf_event]: Breakpoint test passed!
> # [perf_event]: Watchpoint test passed!
> # [ptrace]: Breakpoint test passed!
> # ptrace(PTRACE_GETREGSET): Number of watchpoints: 2
> # ptrace(PTRACE_GETREGSet): addr: 0x82888 control: 0x8080
> # [ptrace]: Watchpoint test passed!
> # [hbpregs] breakpoint readback: addr=0x10472 type=4 len=4 ctrl=0
> # [hbpregs]: Breakpoint test passed!
> # [hbpregs] watchpoint readback: addr=0x82888 type=2 len=8 ctrl=0
> # [hbpregs]: Watchpoint test passed!
>
> Himanshu Chauhan (5):
> riscv: Introduce support for hardware break/watchpoints
> riscv: Add breakpoint and watchpoint test for riscv
> riscv: ptrace support for hardware break/watchpoints
> selftests/breakpoints: extend riscv test for ptrace hw
> break/watchpoints
> MAINTAINERS: Add entry for RISC-V Debugging
>
> MAINTAINERS | 9 +
> arch/riscv/Kconfig | 2 +
> arch/riscv/include/asm/hw_breakpoint.h | 299 +++++++
> arch/riscv/include/asm/kdebug.h | 3 +-
> arch/riscv/include/asm/processor.h | 18 +
> arch/riscv/include/uapi/asm/ptrace.h | 53 ++
> arch/riscv/kernel/Makefile | 1 +
> arch/riscv/kernel/hw_breakpoint.c | 677 ++++++++++++++++
> arch/riscv/kernel/process.c | 5 +
> arch/riscv/kernel/ptrace.c | 507 ++++++++++++
> arch/riscv/kernel/traps.c | 6 +
> include/uapi/linux/elf.h | 4 +
> tools/include/uapi/linux/elf.h | 2 +
> tools/testing/selftests/breakpoints/Makefile | 5 +
> .../breakpoints/breakpoint_test_riscv.c | 765 ++++++++++++++++++
> 15 files changed, 2355 insertions(+), 1 deletion(-)
> create mode 100644 arch/riscv/include/asm/hw_breakpoint.h
> create mode 100644 arch/riscv/kernel/hw_breakpoint.c
> create mode 100644 tools/testing/selftests/breakpoints/breakpoint_test_riscv.c
>
> --
> 2.43.0
>
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v6 1/5] riscv: Introduce support for hardware break/watchpoints
2026-08-03 13:49 ` [PATCH v6 1/5] " Himanshu Chauhan
@ 2026-08-05 13:20 ` Chen Pei
0 siblings, 0 replies; 9+ messages in thread
From: Chen Pei @ 2026-08-05 13:20 UTC (permalink / raw)
To: Himanshu Chauhan, linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe
Hi Himanshu,
Thanks for the series. Two small observations on the tdata1.hit
handling, based on my reading of the Sdtrig spec.
> + /*
> + * Prefer tdata1.hit from SBI trigger readout whenever
> + * possible. Fall back to address-based matching if HIT
> + * isn't observed/supported.
> + */
> + raw_spin_lock_irqsave(this_cpu_ptr(&ecall_lock),
> + *this_cpu_ptr(&ecall_lock_flags));
> + shmem = this_cpu_ptr(sbi_dbtr_shmem);
> + sret = sbi_ecall(SBI_EXT_DBTR, SBI_EXT_DBTR_TRIG_READ,
> + i, 1, 0, 0, 0, 0);
> + if (!sret.error) {
> + tdata1 = le_to_cpu(shmem->data.tdata1);
> +
> + if (dbtr_type == RISCV_DBTR_TRIG_MCONTROL)
> + hit = !!(tdata1 & RISCV_DBTR_MC_HIT_BIT_MASK);
> + else if (dbtr_type == RISCV_DBTR_TRIG_MCONTROL6)
> + hit = !!(tdata1 & RISCV_DBTR_MC6_HIT_BIT_MASK);
> + }
It looks like the hit bit is read here but not cleared afterwards.
The debug spec describes mcontrol.hit as:
"If this bit is implemented then it must become set when this
trigger fires and may become set when this trigger matches.
The trigger's user can set or clear it at any time."
and for mcontrol6 hit0/hit1:
"The TM updates this field when the trigger fires. After the
debugger has seen the update, it will normally write 0 to this
field so it can see future changes."
So the hardware only sets hit, and clearing seems to be left to the
trigger's user. Without a clear, a trigger that fired once would keep
hit=1 and might be reported as matching on later unrelated traps on
the same hart. Would it make sense to write tdata1 back with the hit
bit(s) cleared (e.g. via TRIG_UPDATE) once a hit has been observed?
A related minor point: for mcontrol6 with tinfo.version > 0, hit1 and
hit0 combine into a 2-bit field (0=false, 1=before, 2=after). If only
hit0 is tested, the "after" encoding (hit1=1, hit0=0) might be missed
and would then rely on the address-based fallback. Checking (and
clearing) both bits could make the hit detection a bit more robust.
Best regards,
Pei
--
Chen Pei <cp0613@linux.alibaba.com>
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints
2026-08-03 13:49 ` [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints Himanshu Chauhan
@ 2026-08-05 13:22 ` Chen Pei
0 siblings, 0 replies; 9+ messages in thread
From: Chen Pei @ 2026-08-05 13:22 UTC (permalink / raw)
To: Himanshu Chauhan, linux-riscv
Cc: qingfang.deng, alex, aou, palmer, pjw, shuah, linux-kernel,
thecharlesjenkins, jtaubepe
> +struct __riscv_hwdebug_state {
> + unsigned long addr;
> + unsigned long type;
> + unsigned long len;
> + unsigned long ctrl;
> +} __packed;
Hi Himanshu,
Two suggestions about the new UAPI structure added in this patch.
1) Consider __u64 instead of 'unsigned long': its size differs
between rv32 and rv64, so the layout would not be stable across
32-bit and 64-bit userspace (e.g. a future COMPAT path). __u64
keeps the layout identical for both.
2) With all members the same width there is no interior padding, so
__packed can be dropped.
The struct user_hwdebug_state above already uses fixed-width types;
defining this one the same way would be consistent.
Best regards,
Pei
--
Chen Pei <cp0613@linux.alibaba.com>
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-08-05 13:23 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-03 13:49 [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 1/5] " Himanshu Chauhan
2026-08-05 13:20 ` Chen Pei
2026-08-03 13:49 ` [PATCH v6 2/5] riscv: Add breakpoint and watchpoint test for riscv Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 3/5] riscv: ptrace support for hardware break/watchpoints Himanshu Chauhan
2026-08-05 13:22 ` Chen Pei
2026-08-03 13:49 ` [PATCH v6 4/5] selftests/breakpoints: extend riscv test for ptrace hw break/watchpoints Himanshu Chauhan
2026-08-03 13:49 ` [PATCH v6 5/5] MAINTAINERS: Add entry for RISC-V Debugging Himanshu Chauhan
2026-08-04 17:10 ` [PATCH v6 0/5] riscv: Introduce support for hardware break/watchpoints Jesse Taube
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox