Re: [RFC PATCH] riscv: add userspace interface to voluntarily release vector state

From: daichengrong

Date: Thu Mar 19 2026 - 03:50:03 EST




On 3/17/26 12:41, Samuel Holland wrote:
> Hi,
>
> On 2026-03-16 9:22 PM, daichengrong wrote:
>> Vector registers in RVV can be large, and saving/restoring them on
>> context switches introduces overhead. Some workloads only use
>> vector instructions in short phases, after which the vector state
>> does not need to be preserved.
>>
>> This patch introduces a userspace-controlled mechanism:
>>
>> - Userspace can declare that it no longer needs the vector state.
>> - Kernel will skip saving/restoring vector registers during context
>> switch while the declaration is active.
>> - If the thread executes vector instructions after releasing its
>> vector state, the kernel will revoke the declaration automatically.
>>
>> This reduces unnecessary vector context switch overhead and improves
>> performance in workloads with intermittent vector usage.
>>
>> This is an RFC patch to solicit feedback on the API design and
>> implementation approach.
>>
>> Signed-off-by: daichengrong <daichengrong@xxxxxxxxxxx>
>> ---
>> arch/riscv/include/asm/processor.h | 1 +
>> arch/riscv/include/asm/syscall.h | 2 ++
>> arch/riscv/include/asm/vector.h | 7 +++++--
>> arch/riscv/kernel/process.c | 1 +
>> arch/riscv/kernel/sys_riscv.c | 12 ++++++++++++
>> scripts/syscall.tbl | 1 +
>> 6 files changed, 22 insertions(+), 2 deletions(-)
>>
>> diff --git a/arch/riscv/include/asm/processor.h b/arch/riscv/include/asm/processor.h
>> index 4c3dd94d0f63..b59f1456918b 100644
>> --- a/arch/riscv/include/asm/processor.h
>> +++ b/arch/riscv/include/asm/processor.h
>> @@ -113,6 +113,7 @@ struct thread_struct {
>> unsigned long envcfg;
>> unsigned long sum;
>> u32 riscv_v_flags;
>> + unsigned long riscv_v_release_flags;
>> u32 vstate_ctrl;
>> struct __riscv_v_ext_state vstate;
>> unsigned long align_ctl;
>> diff --git a/arch/riscv/include/asm/syscall.h b/arch/riscv/include/asm/syscall.h
>> index 8067e666a4ca..f6be37b01a67 100644
>> --- a/arch/riscv/include/asm/syscall.h
>> +++ b/arch/riscv/include/asm/syscall.h
>> @@ -121,4 +121,6 @@ asmlinkage long sys_riscv_flush_icache(uintptr_t, uintptr_t, uintptr_t);
>>
>> asmlinkage long sys_riscv_hwprobe(struct riscv_hwprobe *, size_t, size_t,
>> unsigned long *, unsigned int);
>> +// asmlinkage long sys_riscv_release_vector_register(uintptr_t);
>> +asmlinkage long sys_riscv_release_vector_register(void);
>> #endif /* _ASM_RISCV_SYSCALL_H */
>> diff --git a/arch/riscv/include/asm/vector.h b/arch/riscv/include/asm/vector.h
>> index 00cb9c0982b1..4bccccc20cc3 100644
>> --- a/arch/riscv/include/asm/vector.h
>> +++ b/arch/riscv/include/asm/vector.h
>> @@ -309,6 +309,7 @@ static inline void riscv_v_vstate_save(struct __riscv_v_ext_state *vstate,
>> if (__riscv_v_vstate_check(regs->status, DIRTY)) {
>> __riscv_v_vstate_save(vstate, vstate->datap);
>> __riscv_v_vstate_clean(regs);
>> + WRITE_ONCE(current->thread.riscv_v_release_flags, 0);
>> }
>> }
>>
>> @@ -325,8 +326,10 @@ static inline void riscv_v_vstate_set_restore(struct task_struct *task,
>> struct pt_regs *regs)
>> {
>> if (riscv_v_vstate_query(regs)) {
>> - set_tsk_thread_flag(task, TIF_RISCV_V_DEFER_RESTORE);
>> - riscv_v_vstate_on(regs);
>> + if (!READ_ONCE(current->thread.riscv_v_release_flags)) {
>> + set_tsk_thread_flag(task, TIF_RISCV_V_DEFER_RESTORE);
>> + riscv_v_vstate_on(regs);
>> + }
>
> This is a security bug, because it leaks the previous task's vector registers.
> >> }
>> }
>>
>> diff --git a/arch/riscv/kernel/process.c b/arch/riscv/kernel/process.c
>> index aacb23978f93..f1f36a3c7914 100644
>> --- a/arch/riscv/kernel/process.c
>> +++ b/arch/riscv/kernel/process.c
>> @@ -279,6 +279,7 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
>> p->thread.ra = (unsigned long)ret_from_fork_user_asm;
>> }
>> p->thread.riscv_v_flags = 0;
>> + p->thread.riscv_v_release_flags = 0;
>> if (has_vector() || has_xtheadvector())
>> riscv_v_thread_alloc(p);
>> p->thread.sp = (unsigned long)childregs; /* kernel sp */
>> diff --git a/arch/riscv/kernel/sys_riscv.c b/arch/riscv/kernel/sys_riscv.c
>> index 22fc9b3268be..934ddc06858d 100644
>> --- a/arch/riscv/kernel/sys_riscv.c
>> +++ b/arch/riscv/kernel/sys_riscv.c
>> @@ -8,6 +8,7 @@
>> #include <linux/syscalls.h>
>> #include <asm/cacheflush.h>
>> #include <asm-generic/mman-common.h>
>> +#include <asm/vector.h>
>>
>> static long riscv_sys_mmap(unsigned long addr, unsigned long len,
>> unsigned long prot, unsigned long flags,
>> @@ -78,6 +79,17 @@ SYSCALL_DEFINE3(riscv_flush_icache, uintptr_t, start, uintptr_t, end,
>> return 0;
>> }
>>
>> +SYSCALL_DEFINE0(riscv_release_vector_register)
>> +{
>> + struct pt_regs *regs = task_pt_regs(current);
>> +
>> + if (__riscv_v_vstate_check(regs->status, DIRTY))
>> + __riscv_v_vstate_clean(regs);
>> +
>> + WRITE_ONCE(current->thread.riscv_v_release_flags, 1);
>
> To avoid leaking register state at context switch, you must either:
> 1) set the vector registers to some safe contents (e.g. the initial state) or
> 2) set VS=off
>
> So if RVV is used rarely enough that you are willing to pay the cost of a trap
> when you next use it, this function can be as simple as:
>
> riscv_v_vstate_off(task_pt_regs(current));
>
Yes, I agree that this is a security issue, as it could potentially
leak vector register contents from a previous task.

However, in the current implementation, once a task executes a system
call, its user vector state is immediately discarded by the kernel.
There is therefore no need for an additional system call to explicitly
drop vector state from user space.

The root cause lies in how the kernel tracks the state after discard.
Currently, after riscv_v_vstate_discard(), the vector state is marked
as DIRTY instead of INIT. This causes the context switch logic to
treat the registers as containing valid user data, resulting in
unnecessary save and restore operations on registers that no longer
hold meaningful user information.

Specifically, this may lead to restoring stale or constant values
into a different task, which is the underlying source of the potential
data leakage.

The patch[1] I recently submitted addresses
this by marking the vector state as INIT after discard. This ensures
that the scheduler correctly skips saving/restoring invalid vector data
and prevents leaking contents from a previous task.

>> + return 0;
>> +}
>> +
>> /* Not defined using SYSCALL_DEFINE0 to avoid error injection */
>> asmlinkage long __riscv_sys_ni_syscall(const struct pt_regs *__unused)
>> {
>> diff --git a/scripts/syscall.tbl b/scripts/syscall.tbl
>> index 7a42b32b6577..1d0a493b87c3 100644
>> --- a/scripts/syscall.tbl
>> +++ b/scripts/syscall.tbl
>> @@ -302,6 +302,7 @@
>>
>> 244 or1k or1k_atomic sys_or1k_atomic
>>
>> +257 riscv riscv_release_vector_register sys_riscv_release_vector_register
>> 258 riscv riscv_hwprobe sys_riscv_hwprobe
>> 259 riscv riscv_flush_icache sys_riscv_flush_icache
>
> You may also consider adding a flag to prctl(PR_RISCV_V_SET_CONTROL) instead of
> a new syscall. So it would look something like:
>
> prctl(PR_RISCV_V_SET_CONTROL,
> PR_RISCV_V_VSTATE_CTRL_ON | PR_RISCV_V_VSTATE_CTRL_RESET);
>
> (Arguably it is a bug that riscv_v_ctrl_set() doesn't call riscv_v_vstate_off()
> for PR_RISCV_V_VSTATE_CTRL_OFF already.)
>
> Regards,
> Samuel


[1] https://lists.infradead.org/pipermail/linux-riscv/2026-March/087677.html