[PATCH v7 3/6] riscv: clarify vector state semantics on syscall and context switch

Andy Chiu tchiu at tenstorrent.com
Wed Nov 11 10:43:25 PST 2026


From: daichengrong <daichengrong at iscas.ac.cn>

The RISC-V vector specification states that executing a system call
causes all caller-saved vector registers (v0-v31, vl, vtype) and vstart
to become unspecified.

Currently, after calling riscv_v_vstate_discard(), the vector state
may still be marked as DIRTY, which can mislead the context switch
logic into treating the registers as containing valid user data.

This patch clarifies and tightens the kernel-side semantics:

1. On syscall entry, the kernel checks the vector state via sstatus
   and set it to INIT if not already, indicating that the vector
   registers no longer contain meaningful user data. Context
   invalidation is required on restore.

   If the state is already set to INIT, it means that the user has not
   touched vector since the last invalidation. So no further action is
   required on restore.

2. During context switch, the vector state is saved only if the state is
   DIRTY. (no change)

3. On restore, if invalidation is required, the vector registers are
   overwritten with a known initial value and the state is set to INIT.

4. Ptrace writes (PTRACE_SETREGSET) during a syscall stop are silently
   dropped (returning 0). Returning an error like EINVAL would break
   debbugers like GDB, which disables the optional regset on receiving
   such error. On the otherhand, ptrace reads at a syscall stop always
   returns the nulled context.

Performance improvements on Blackhole x280:

Latency of getpid() with different status.VS upon syscall entry in ns:
status.VS	Before Patch	After Patch	Improvement
DIRTY		235.9		242.4		+6.5 (+2.7%) # regress
INIT		234.8		174.4		-60.4 (-25.7%)
OFF		178.2		174.5		-3.7 (-2.1%)

Context switch latencies in us:
Metric		Before Patch	After Patch	Improvement
Mean Latency	14.19		11.62		-2.57 (-18.1%)
Median Latency	13.51		11.01		-2.50 (-18.5%)
Max Latency	27.76		21.92		-5.84 (-21.0%)
Min Latency	8.99		8.12		-0.87 (-9.7%)

The metrics on context switch latencies are obtained by running the
following command 100 times:
$ taskset 0x2 ./lat_ctx_v -N 10000 -s 512 2

The program lat_ctx_v is a modified lat_ctx where benchmark processes
(parent/child) do a `vsetvli` to dirtify VS before making each write
syscall.

CC: gdb at sourceware.org
Signed-off-by: daichengrong <daichengrong at iscas.ac.cn>
Co-developed-by: Andy Chiu <tchiu at tenstorrent.com>
Signed-off-by: Andy Chiu <tchiu at tenstorrent.com>
---
Changelog v7:
 - Fold the next patch from v6 into this to prevent exposing breaks in
   signal and ptrace.
Changelog v4:
 - Provide performance meassurement (Olof)
 - Aggregate riscv_v_{enable,disable} for better performance (reduces to
   ~240 ns/call when the user touches vector)
 - Do not invalidate again if user doesn't touch vector
Changelog v3:
 - rename vstate_on to vstate_init to prevent confusion
 - set context as clean at first-use trap to return zero'ed context
 - reduce context nulling operations by defering __vstate_discard to
   exit_to_user_mode_prepare.
---
 arch/riscv/include/asm/vector.h | 45 ++++++++++++++++++---------------
 arch/riscv/kernel/ptrace.c      |  7 ++++-
 arch/riscv/kernel/signal.c      |  7 ++++-
 arch/riscv/kernel/vector.c      | 29 +++++++++++++++++++--
 4 files changed, 64 insertions(+), 24 deletions(-)

diff --git a/arch/riscv/include/asm/vector.h b/arch/riscv/include/asm/vector.h
index 7cd20f661596..e83f9e0046a9 100644
--- a/arch/riscv/include/asm/vector.h
+++ b/arch/riscv/include/asm/vector.h
@@ -40,6 +40,15 @@
 	_res;								\
 })
 
+#define __riscv_v_vstate_check_gt(_val, TYPE) ({			\
+	bool _res;							\
+	if (has_xtheadvector())						\
+		_res = ((_val) & SR_VS_THEAD) > SR_VS_##TYPE##_THEAD;	\
+	else								\
+		_res = ((_val) & SR_VS) > SR_VS_##TYPE;			\
+	_res;								\
+})
+
 extern unsigned long riscv_v_vsize;
 int riscv_v_setup_vsize(void);
 bool insn_is_vector(u32 insn_buf);
@@ -101,7 +110,7 @@ static inline void riscv_v_vstate_off(struct pt_regs *regs)
 	regs->status = __riscv_v_vstate_or(regs->status, OFF);
 }
 
-static inline void riscv_v_vstate_on(struct pt_regs *regs)
+static inline void riscv_v_vstate_init(struct pt_regs *regs)
 {
 	regs->status = __riscv_v_vstate_or(regs->status, INITIAL);
 }
@@ -294,16 +303,6 @@ static inline void __riscv_v_vstate_discard(void)
 		: "=&r" (vl) : "r" (vtype_inval));
 }
 
-static inline void riscv_v_vstate_discard(struct pt_regs *regs)
-{
-	if (riscv_v_vstate_query(regs)) {
-		riscv_v_enable();
-		__riscv_v_vstate_discard();
-		riscv_v_disable();
-		__riscv_v_vstate_dirty(regs);
-	}
-}
-
 static inline void riscv_v_vstate_save(struct __riscv_v_ext_state *vstate,
 				       struct pt_regs *regs)
 {
@@ -319,21 +318,26 @@ static inline void riscv_v_vstate_restore(struct __riscv_v_ext_state *vstate,
 					  struct pt_regs *regs)
 {
 	WARN_ON_ONCE(preemptible());
-
-	if (riscv_v_vstate_query(regs)) {
-		riscv_v_enable();
+	riscv_v_enable();
+	if (__riscv_v_vstate_check(regs->status, INITIAL))
+		__riscv_v_vstate_discard();
+	else if (__riscv_v_vstate_check(regs->status, CLEAN))
 		__riscv_v_vstate_restore(vstate, vstate->datap);
-		riscv_v_disable();
-		__riscv_v_vstate_clean(regs);
-	}
+	riscv_v_disable();
 }
 
 static inline void riscv_v_vstate_set_restore(struct task_struct *task,
 					      struct pt_regs *regs)
 {
-	if (riscv_v_vstate_query(regs)) {
+	if (riscv_v_vstate_query(regs))
 		set_tsk_thread_flag(task, TIF_RISCV_V_DEFER_RESTORE);
-		riscv_v_vstate_on(regs);
+}
+
+static inline void riscv_v_vstate_discard(struct pt_regs *regs)
+{
+	if (__riscv_v_vstate_check_gt(regs->status, INITIAL)) {
+		riscv_v_vstate_set_restore(current, regs);
+		riscv_v_vstate_init(regs);
 	}
 }
 
@@ -404,6 +408,7 @@ static inline void __switch_to_vector(struct task_struct *prev,
 			riscv_preempt_v_set_restore(next);
 		}
 	} else {
+		/* VS is never DIRTY at this point, there's no need to alter vstate here */
 		riscv_v_vstate_set_restore(next, task_pt_regs(next));
 	}
 }
@@ -429,7 +434,7 @@ static inline bool riscv_v_vstate_ctrl_user_allowed(void) { return false; }
 #define riscv_v_vstate_restore(vstate, regs)	do {} while (0)
 #define __switch_to_vector(__prev, __next)	do {} while (0)
 #define riscv_v_vstate_off(regs)		do {} while (0)
-#define riscv_v_vstate_on(regs)			do {} while (0)
+#define riscv_v_vstate_init(regs)		do {} while (0)
 #define riscv_v_thread_free(tsk)		do {} while (0)
 #define  riscv_v_setup_ctx_cache()		do {} while (0)
 #define riscv_v_thread_alloc(tsk)		do {} while (0)
diff --git a/arch/riscv/kernel/ptrace.c b/arch/riscv/kernel/ptrace.c
index 88442ac4f6ae..d329518cceb6 100644
--- a/arch/riscv/kernel/ptrace.c
+++ b/arch/riscv/kernel/ptrace.c
@@ -218,13 +218,18 @@ static int riscv_vr_set(struct task_struct *target,
 	int ret;
 	struct __riscv_v_ext_state *vstate = &target->thread.vstate;
 	struct __riscv_v_regset_state ptrace_vstate;
+	struct pt_regs *regs = task_pt_regs(target);
 
 	if (!(has_vector() || has_xtheadvector()))
 		return -EINVAL;
 
-	if (!riscv_v_vstate_query(task_pt_regs(target)))
+	if (!riscv_v_vstate_query(regs))
 		return -ENODATA;
 
+	/* Silently drop the modification to tracee as no vreg lives across a syscall */
+	if (__riscv_v_vstate_check(regs->status, INITIAL))
+		return 0;
+
 	/* Copy rest of the vstate except datap */
 	memset(&ptrace_vstate, 0, sizeof(ptrace_vstate));
 	ret = user_regset_copyin(&pos, &count, &kbuf, &ubuf, &ptrace_vstate, 0,
diff --git a/arch/riscv/kernel/signal.c b/arch/riscv/kernel/signal.c
index 50c77bcf2b09..dc785aca1cc4 100644
--- a/arch/riscv/kernel/signal.c
+++ b/arch/riscv/kernel/signal.c
@@ -119,10 +119,15 @@ static long __restore_v_state(struct pt_regs *regs, void __user *sc_vec)
 	/*
 	 * Mark the vstate as clean prior performing the actual copy,
 	 * to avoid getting the vstate incorrectly clobbered by the
-	 *  discarded vector state.
+	 * discarded vector state.
+	 *
+	 * This also allows user to modify vregs through the signal
+	 * interface at a syscall stop. e.g. to support user space
+	 * context switching.
 	 */
 	preempt_disable();
 	riscv_v_vstate_set_restore(current, regs);
+	__riscv_v_vstate_clean(regs);
 	preempt_enable();
 
 	/* Copy everything of __sc_riscv_v_state except datap. */
diff --git a/arch/riscv/kernel/vector.c b/arch/riscv/kernel/vector.c
index 924baa71af7e..6fd541f5d5cb 100644
--- a/arch/riscv/kernel/vector.c
+++ b/arch/riscv/kernel/vector.c
@@ -29,15 +29,40 @@ static struct kmem_cache *riscv_v_kernel_cachep;
 unsigned long riscv_v_vsize __read_mostly;
 EXPORT_SYMBOL_GPL(riscv_v_vsize);
 
+/*
+ * Context memory is not coherent to register when sstatus.vs is set to INITIAL. This function
+ * take the INITIAL state into consideration and reflect the nulled state into context memory.
+ * Assume the target task is not actively running when tsk != current
+ */
 void riscv_v_ucontext_save(struct task_struct *tsk)
 {
 	struct __riscv_v_ext_state *vstate = &tsk->thread.vstate;
 	struct pt_regs *regs = task_pt_regs(tsk);
 
+	/*
+	 * Do not set vstate as clean when it is INITIAL, otherwise we lose track of the nulled
+	 * state in ptrace.
+	 */
 	if (tsk == current) {
 		get_cpu_vector_context();
-		riscv_v_vstate_save(vstate, regs);
+		if (__riscv_v_vstate_check(regs->status, INITIAL)) {
+			riscv_v_enable();
+			__riscv_v_vstate_discard();
+			__riscv_v_vstate_save(vstate, vstate->datap);
+			riscv_v_disable();
+		} else {
+			riscv_v_vstate_save(vstate, regs);
+		}
 		put_cpu_vector_context();
+	} else if (__riscv_v_vstate_check(regs->status, INITIAL)) {
+		/*
+		 * If we are not current and VS == INITIAL, null out the context memory for tsk
+		 * using kernel mode vector.
+		 */
+		kernel_vector_begin();
+		__riscv_v_vstate_discard();
+		__riscv_v_vstate_save(vstate, vstate->datap);
+		kernel_vector_end();
 	}
 }
 
@@ -233,7 +258,7 @@ bool riscv_v_first_use_handler(struct pt_regs *regs)
 		return true;
 	}
 
-	riscv_v_vstate_on(regs);
+	__riscv_v_vstate_clean(regs);
 	riscv_v_vstate_set_restore(current, regs);
 
 	return true;
-- 
2.43.0




More information about the linux-riscv mailing list