* [PATCH v7 1/2] riscv: kprobes: reject probes inside LR/SC sequences
2026-10-19 13:41 [PATCH v7 0/2] riscv: kprobes: reject probes inside LR/SC sequences Xiaofeng Yuan
@ 2026-10-19 13:41 ` Xiaofeng Yuan
2026-10-19 13:41 ` [PATCH v7 2/2] riscv: kprobes: add KUnit test for LR/SC sequence rejection Xiaofeng Yuan
1 sibling, 0 replies; 3+ messages in thread
From: Xiaofeng Yuan @ 2026-10-19 13:41 UTC (permalink / raw)
To: Paul Walmsley, Palmer Dabbelt, Albert Ou
Cc: Alexandre Ghiti, Nam Cao, Naveen N Rao, David S . Miller,
Masami Hiramatsu, linux-riscv, linux-kernel, linux-trace-kernel,
Xiaofeng Yuan
A trap taken in the middle of an LR/SC sequence clears the load
reservation, so an SC following the probed instruction always fails and
the enclosing retry loop re-enters the breakpoint, livelocking the CPU.
Reject probes of the LR/SC instructions themselves and of any address
between them.
arm64's kprobes rejects its equivalent exclusive load/store instructions
and, in arm_kprobe_decode_insn(), walks backward from the probe address
(bounded by the enclosing symbol) to reject any address inside an
exclusive sequence. RISC-V cannot walk backward because
variable-length instructions make instruction boundaries ambiguous, so
walk forward from the enclosing symbol, a known instruction boundary, up
to the probe address and track whether an LR is still outstanding.
A Zalrsc-constrained LR/SC retry loop holds at most 16 instructions: the
LR, the SC, and up to MAX_LRSC_GAP instructions between them. An LR
MAX_LRSC_GAP or more instructions ahead of the probe cannot start a
sequence containing it. Count instructions, not bytes: compressed
instructions would make a byte window ambiguous.
The LR/SC instructions themselves are rejected in
riscv_probe_decode_insn(), and an address inside a sequence is rejected
in arch_check_kprobe().
Signed-off-by: Xiaofeng Yuan <yuanxiaofeng@eswincomputing.com>
Acked-by: Masami Hiramatsu (Google) <mhiramat@kernel.org>
---
arch/riscv/include/asm/insn.h | 63 ++++++++++++++++++++-----
arch/riscv/kernel/probes/decode-insn.c | 2 +
arch/riscv/kernel/probes/kprobes.c | 65 +++++++++++++++++++++-----
3 files changed, 107 insertions(+), 23 deletions(-)
diff --git a/arch/riscv/include/asm/insn.h b/arch/riscv/include/asm/insn.h
index ad348d8b8..6fdd61e6e 100644
--- a/arch/riscv/include/asm/insn.h
+++ b/arch/riscv/include/asm/insn.h
@@ -10,6 +10,8 @@
#define RV_INSN_FUNCT3_MASK GENMASK(14, 12)
#define RV_INSN_FUNCT3_OPOFF 12
+#define RV_INSN_FUNCT5_MASK GENMASK(31, 27)
+#define RV_INSN_FUNCT5_OPOFF 27
#define RV_INSN_OPCODE_MASK GENMASK(6, 0)
#define RV_INSN_OPCODE_OPOFF 0
#define RV_INSN_FUNCT12_OPOFF 20
@@ -141,6 +143,7 @@
#define RVG_OPCODE_JALR 0x67
#define RVG_OPCODE_JAL 0x6f
#define RVG_OPCODE_SYSTEM 0x73
+#define RVG_OPCODE_AMO 0x2f
#define RVG_SYSTEM_CSR_OFF 20
#define RVG_SYSTEM_CSR_MASK GENMASK(11, 0)
@@ -175,6 +178,8 @@
#define RVG_FUNCT3_BGE 0x5
#define RVG_FUNCT3_BLTU 0x6
#define RVG_FUNCT3_BGEU 0x7
+#define RVG_FUNCT3_AMO_W 0x2
+#define RVG_FUNCT3_AMO_D 0x3
/* parts of funct3 code for C extension*/
#define RVC_FUNCT3_C_BEQZ 0x6
@@ -263,6 +268,53 @@ __RISCV_INSN_FUNCS(ebreak, RVG_MASK_EBREAK, RVG_MATCH_EBREAK)
__RISCV_INSN_FUNCS(sret, RVG_MASK_SRET, RVG_MATCH_SRET)
__RISCV_INSN_FUNCS(fence, RVG_MASK_FENCE, RVG_MATCH_FENCE);
+/*
+ * LR/SC are identified by the AMO opcode plus the funct3 width field
+ * (.w/.d) and the funct5 field (funct7[6:2]) distinguishing LR from SC.
+ * The aq/rl bits (funct7[1:0]) are ignored. For LR the rs2 field must
+ * encode x0.
+ */
+#define RVG_AMO_FUNCT5_LR 0x02
+#define RVG_AMO_FUNCT5_SC 0x03
+
+#define RV_X_MASK(X, s, mask) (((X) >> (s)) & (mask))
+#define RV_X(X, s, n) RV_X_MASK(X, s, ((1 << (n)) - 1))
+
+#define RV_EXTRACT_FUNCT3(x) \
+ ({typeof(x) x_ = (x); \
+ (RV_X_MASK(x_, RV_INSN_FUNCT3_OPOFF, \
+ RV_INSN_FUNCT3_MASK >> RV_INSN_FUNCT3_OPOFF)); })
+
+#define RV_EXTRACT_FUNCT5(x) \
+ ({typeof(x) x_ = (x); \
+ (RV_X_MASK(x_, RV_INSN_FUNCT5_OPOFF, \
+ RV_INSN_FUNCT5_MASK >> RV_INSN_FUNCT5_OPOFF)); })
+
+#define RV_EXTRACT_RS2_REG(x) \
+ ({typeof(x) x_ = (x); \
+ (RV_X_MASK(x_, RVG_RS2_OPOFF, RVG_RS2_MASK)); })
+
+static __always_inline bool riscv_insn_is_lr(u32 code)
+{
+ u32 funct5 = RV_EXTRACT_FUNCT5(code);
+ u32 funct3 = RV_EXTRACT_FUNCT3(code);
+
+ return (code & RV_INSN_OPCODE_MASK) == RVG_OPCODE_AMO &&
+ funct5 == RVG_AMO_FUNCT5_LR &&
+ (funct3 == RVG_FUNCT3_AMO_W || funct3 == RVG_FUNCT3_AMO_D) &&
+ !RV_EXTRACT_RS2_REG(code);
+}
+
+static __always_inline bool riscv_insn_is_sc(u32 code)
+{
+ u32 funct5 = RV_EXTRACT_FUNCT5(code);
+ u32 funct3 = RV_EXTRACT_FUNCT3(code);
+
+ return (code & RV_INSN_OPCODE_MASK) == RVG_OPCODE_AMO &&
+ funct5 == RVG_AMO_FUNCT5_SC &&
+ (funct3 == RVG_FUNCT3_AMO_W || funct3 == RVG_FUNCT3_AMO_D);
+}
+
/* special case to catch _any_ system instruction */
static __always_inline bool riscv_insn_is_system(u32 code)
{
@@ -432,8 +484,6 @@ static __always_inline bool riscv_insn_is_c_jalr(u32 code)
#define RV_IMM_SIGN(x) (-(((x) >> 31) & 1))
#define RVC_IMM_SIGN(x) (-(((x) >> 12) & 1))
-#define RV_X_MASK(X, s, mask) (((X) >> (s)) & (mask))
-#define RV_X(X, s, n) RV_X_MASK(X, s, ((1 << (n)) - 1))
#define RVC_LW_IMM(x) ((RV_X(x, 6, 1) << 2) | \
(RV_X(x, 10, 3) << 3) | \
(RV_X(x, 5, 1) << 6))
@@ -454,19 +504,10 @@ static __always_inline bool riscv_insn_is_c_jalr(u32 code)
#define RVC_RS2(insn) RV_X(insn, SH_RS2C, 5)
#define RVC_X(X, s, mask) RV_X_MASK(X, s, mask)
-#define RV_EXTRACT_FUNCT3(x) \
- ({typeof(x) x_ = (x); \
- (RV_X_MASK(x_, RV_INSN_FUNCT3_OPOFF, \
- RV_INSN_FUNCT3_MASK >> RV_INSN_FUNCT3_OPOFF)); })
-
#define RV_EXTRACT_RS1_REG(x) \
({typeof(x) x_ = (x); \
(RV_X_MASK(x_, RVG_RS1_OPOFF, RVG_RS1_MASK)); })
-#define RV_EXTRACT_RS2_REG(x) \
- ({typeof(x) x_ = (x); \
- (RV_X_MASK(x_, RVG_RS2_OPOFF, RVG_RS2_MASK)); })
-
#define RV_EXTRACT_RD_REG(x) \
({typeof(x) x_ = (x); \
(RV_X_MASK(x_, RVG_RD_OPOFF, RVG_RD_MASK)); })
diff --git a/arch/riscv/kernel/probes/decode-insn.c b/arch/riscv/kernel/probes/decode-insn.c
index 433d9035b..e6a889467 100644
--- a/arch/riscv/kernel/probes/decode-insn.c
+++ b/arch/riscv/kernel/probes/decode-insn.c
@@ -23,6 +23,8 @@ riscv_probe_decode_insn(probe_opcode_t *addr, struct arch_probe_insn *api)
*/
RISCV_INSN_REJECTED(system, insn);
RISCV_INSN_REJECTED(fence, insn);
+ RISCV_INSN_REJECTED(lr, insn);
+ RISCV_INSN_REJECTED(sc, insn);
/*
* Simulate instructions list:
diff --git a/arch/riscv/kernel/probes/kprobes.c b/arch/riscv/kernel/probes/kprobes.c
index a76eab297..273b26ac2 100644
--- a/arch/riscv/kernel/probes/kprobes.c
+++ b/arch/riscv/kernel/probes/kprobes.c
@@ -7,11 +7,13 @@
#include <linux/slab.h>
#include <linux/stop_machine.h>
#include <linux/vmalloc.h>
+#include <linux/unaligned.h>
#include <asm/ptrace.h>
#include <linux/uaccess.h>
#include <asm/sections.h>
#include <asm/cacheflush.h>
#include <asm/bug.h>
+#include <asm/insn.h>
#include <asm/text-patching.h>
#include "decode-insn.h"
@@ -49,35 +51,74 @@ static void __kprobes arch_simulate_insn(struct kprobe *p, struct pt_regs *regs)
post_kprobe_handler(p, kcb, regs);
}
-static bool __kprobes arch_check_kprobe(unsigned long addr)
+/*
+ * A trap taken in the middle of an LR/SC sequence clears the load
+ * reservation, so an SC following the probed instruction would always
+ * fail and the enclosing retry loop would re-enter the breakpoint.
+ * Reject probes inside such a sequence.
+ *
+ * RISC-V instruction boundaries cannot be recovered by walking
+ * backwards - a 32-bit instruction whose upper halfword looks like a
+ * compressed instruction is ambiguous - so walk forward from the
+ * enclosing symbol, a known instruction boundary, up to the probe address
+ * and track whether an LR is still outstanding. A Zalrsc-constrained
+ * LR/SC retry loop holds at most 16 instructions: the LR, the SC, and
+ * up to MAX_LRSC_GAP instructions between them. The walk counts only
+ * the instructions between the LR and the probe, so an LR MAX_LRSC_GAP
+ * or more instructions ahead of the probe cannot start a sequence
+ * containing it. Count instructions, not bytes: compressed instructions
+ * would make a byte window ambiguous.
+ */
+#define MAX_LRSC_GAP 14
+
+/*
+ * Verify that @addr points at the start of an instruction and is not
+ * inside an LR/SC sequence. Returns 0 on success, -EILSEQ if @addr is
+ * not an instruction boundary, or -EINVAL if the probe would land
+ * inside an LR/SC sequence.
+ */
+static int __kprobes arch_check_kprobe(unsigned long addr)
{
- unsigned long tmp, offset;
+ unsigned long pc, offset;
+ int outstanding_lr = -1;
- /* start iterating at the closest preceding symbol */
if (!kallsyms_lookup_size_offset(addr, NULL, &offset))
- return false;
+ return -EILSEQ;
- tmp = addr - offset;
+ /* pc starts at the closest preceding symbol, a known instruction boundary */
+ pc = addr - offset;
- while (tmp <= addr) {
- if (tmp == addr)
- return true;
+ while (pc < addr) {
+ u32 insn = get_unaligned((u32 *)pc);
+ unsigned int len = GET_INSN_LENGTH(insn);
- tmp += GET_INSN_LENGTH(*(u16 *)tmp);
+ if (riscv_insn_is_lr(insn))
+ outstanding_lr = 0;
+ else if (riscv_insn_is_sc(insn))
+ outstanding_lr = -1;
+ else if (outstanding_lr >= 0 && ++outstanding_lr >= MAX_LRSC_GAP)
+ outstanding_lr = -1;
+
+ pc += len;
}
- return false;
+ if (pc != addr)
+ return -EILSEQ;
+
+ return outstanding_lr >= 0 ? -EINVAL : 0;
}
int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
u16 *insn = (u16 *)p->addr;
+ int ret;
if ((unsigned long)insn & 0x1)
return -EILSEQ;
- if (!arch_check_kprobe((unsigned long)p->addr))
- return -EILSEQ;
+ ret = arch_check_kprobe((unsigned long)p->addr);
+ if (ret)
+ return ret;
/* copy instruction */
p->opcode = (kprobe_opcode_t)(*insn++);
--
2.34.1
^ permalink raw reply related [flat|nested] 3+ messages in thread* [PATCH v7 2/2] riscv: kprobes: add KUnit test for LR/SC sequence rejection
2026-10-19 13:41 [PATCH v7 0/2] riscv: kprobes: reject probes inside LR/SC sequences Xiaofeng Yuan
2026-10-19 13:41 ` [PATCH v7 1/2] " Xiaofeng Yuan
@ 2026-10-19 13:41 ` Xiaofeng Yuan
1 sibling, 0 replies; 3+ messages in thread
From: Xiaofeng Yuan @ 2026-10-19 13:41 UTC (permalink / raw)
To: Paul Walmsley, Palmer Dabbelt, Albert Ou
Cc: Alexandre Ghiti, Nam Cao, Naveen N Rao, David S . Miller,
Masami Hiramatsu, linux-riscv, linux-kernel, linux-trace-kernel,
Xiaofeng Yuan
Extend the kprobes KUnit test with a function containing an LR/SC
sequence and assert that register_kprobe() accepts a probe placed right
before or right after the sequence, and rejects a probe placed on the
LR, on an instruction inside the sequence, or on the SC.
Add a second, never-executed function that pins the 16-instruction
tracking window: the 15th instruction after the LR (the last that can
still be inside the loop) must be rejected, and the 16th must be
accepted because it is either the SC (rejected during decoding) or a
plain instruction outside the loop.
Signed-off-by: Xiaofeng Yuan <yuanxiaofeng@eswincomputing.com>
---
.../kernel/tests/kprobes/test-kprobes-asm.S | 60 +++++++++++++++++++
.../riscv/kernel/tests/kprobes/test-kprobes.c | 34 +++++++++++
.../riscv/kernel/tests/kprobes/test-kprobes.h | 6 ++
3 files changed, 100 insertions(+)
diff --git a/arch/riscv/kernel/tests/kprobes/test-kprobes-asm.S b/arch/riscv/kernel/tests/kprobes/test-kprobes-asm.S
index 15e50934b..7924dd55a 100644
--- a/arch/riscv/kernel/tests/kprobes/test-kprobes-asm.S
+++ b/arch/riscv/kernel/tests/kprobes/test-kprobes-asm.S
@@ -100,6 +100,49 @@ test_kprobes_branch_addr8:
.option pop
SYM_FUNC_END(test_kprobes_branch)
+/*
+ * This function is never executed; it only provides the probe addresses
+ * in test_kprobes_lrsc_allowed_addresses[] and
+ * test_kprobes_lrsc_rejected_addresses[] for test_kprobe_lrsc(). The
+ * lr.w/sc.w use the invalid address 0(x0) and would fault if executed,
+ * so do not add this function to test_kprobes_functions[].
+ */
+SYM_FUNC_START(test_kprobes_lrsc)
+.Llrsc_before:
+ nop
+1:
+.Llrsc_lr:
+ lr.w t0, 0(x0)
+.Llrsc_inside:
+ addi t0, t0, 1
+.Llrsc_sc:
+ sc.w t0, t0, 0(x0)
+ bnez t0, 1b
+.Llrsc_after:
+ ret
+SYM_FUNC_END(test_kprobes_lrsc)
+
+/*
+ * Also never executed; it pins the 16-instruction LR/SC tracking window.
+ * The LR is followed by 13 nops. The 15th instruction (counting the LR
+ * as the 1st) is the last one that can still be inside the loop and must
+ * be rejected. The 16th is accepted: it is either the SC, rejected later
+ * during decoding, or a plain instruction, which cannot lie inside a
+ * 16-instruction loop because that loop's 16th instruction must be the SC.
+ */
+SYM_FUNC_START(test_kprobes_lrsc_gap)
+.Llrsc_gap_lr:
+ lr.w t0, 0(x0)
+ .rept 13
+ nop
+ .endr
+.Llrsc_gap_pos15:
+ nop
+.Llrsc_gap_pos16:
+ nop
+ ret
+SYM_FUNC_END(test_kprobes_lrsc_gap)
+
#ifdef CONFIG_RISCV_ISA_C
SYM_FUNC_START(test_kprobes_c_j)
@@ -253,3 +296,20 @@ SYM_DATA_START(test_kprobes_functions)
#endif /* CONFIG_RISCV_ISA_C */
RISCV_PTR 0
SYM_DATA_END(test_kprobes_functions)
+
+.section .rodata
+SYM_DATA_START(test_kprobes_lrsc_allowed_addresses)
+ RISCV_PTR .Llrsc_before
+ RISCV_PTR .Llrsc_after
+ RISCV_PTR .Llrsc_gap_pos16
+ RISCV_PTR 0
+SYM_DATA_END(test_kprobes_lrsc_allowed_addresses)
+
+.section .rodata
+SYM_DATA_START(test_kprobes_lrsc_rejected_addresses)
+ RISCV_PTR .Llrsc_lr
+ RISCV_PTR .Llrsc_inside
+ RISCV_PTR .Llrsc_sc
+ RISCV_PTR .Llrsc_gap_pos15
+ RISCV_PTR 0
+SYM_DATA_END(test_kprobes_lrsc_rejected_addresses)
diff --git a/arch/riscv/kernel/tests/kprobes/test-kprobes.c b/arch/riscv/kernel/tests/kprobes/test-kprobes.c
index 027424a3f..dd2fb0749 100644
--- a/arch/riscv/kernel/tests/kprobes/test-kprobes.c
+++ b/arch/riscv/kernel/tests/kprobes/test-kprobes.c
@@ -43,8 +43,42 @@ static void test_kprobe_riscv(struct kunit *test)
kfree(kp);
}
+static void test_kprobe_lrsc(struct kunit *test)
+{
+ int i;
+
+ /* probes outside the LR/SC sequence must be accepted */
+ for (i = 0; test_kprobes_lrsc_allowed_addresses[i]; i++) {
+ struct kprobe kp = {};
+ int ret;
+
+ kp.pre_handler = kprobe_dummy_handler;
+ kp.addr = test_kprobes_lrsc_allowed_addresses[i];
+
+ ret = register_kprobe(&kp);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ if (!ret)
+ unregister_kprobe(&kp);
+ }
+
+ /* probes on or inside the LR/SC sequence must be rejected */
+ for (i = 0; test_kprobes_lrsc_rejected_addresses[i]; i++) {
+ struct kprobe kp = {};
+ int ret;
+
+ kp.pre_handler = kprobe_dummy_handler;
+ kp.addr = test_kprobes_lrsc_rejected_addresses[i];
+
+ ret = register_kprobe(&kp);
+ KUNIT_EXPECT_LT(test, ret, 0);
+ if (!ret)
+ unregister_kprobe(&kp);
+ }
+}
+
static struct kunit_case kprobes_testcases[] = {
KUNIT_CASE(test_kprobe_riscv),
+ KUNIT_CASE(test_kprobe_lrsc),
{}
};
diff --git a/arch/riscv/kernel/tests/kprobes/test-kprobes.h b/arch/riscv/kernel/tests/kprobes/test-kprobes.h
index 537f44aa9..93393e77a 100644
--- a/arch/riscv/kernel/tests/kprobes/test-kprobes.h
+++ b/arch/riscv/kernel/tests/kprobes/test-kprobes.h
@@ -19,6 +19,12 @@ extern void *test_kprobes_addresses[];
/* array of functions that return KPROBE_TEST_MAGIC */
extern long (*test_kprobes_functions[])(void);
+/* probe points that must be accepted, terminated by NULL */
+extern void *test_kprobes_lrsc_allowed_addresses[];
+
+/* probe points that must be rejected, terminated by NULL */
+extern void *test_kprobes_lrsc_rejected_addresses[];
+
#endif /* __ASSEMBLER__ */
#endif /* TEST_KPROBES_H */
--
2.34.1
^ permalink raw reply related [flat|nested] 3+ messages in thread