* [PATCH v3 bpf-next 1/3] riscv: bpf: Add BPF private stack support
2026-11-06 18:15 [PATCH v3 bpf-next 0/3] riscv: bpf: Add BPF private stack support Chen Pei
@ 2026-11-06 18:15 ` Chen Pei
2026-11-06 18:15 ` [PATCH v3 bpf-next 2/3] selftests/bpf: Enable private stack tests for riscv64 Chen Pei
2026-11-06 18:15 ` [PATCH v3 bpf-next 3/3] selftests/bpf: Test a private stack together with stack arguments Chen Pei
2 siblings, 0 replies; 4+ messages in thread
From: Chen Pei @ 2026-11-06 18:15 UTC (permalink / raw)
To: ast, daniel, andrii, memxor, bjorn, puranjay, alexei.starovoitov
Cc: eddyz87, martin.lau, song, yonghong.song, jolsa, emil,
ihor.solodrai, pulehui, pjw, palmer, aou, alex, luke.r.nels,
xi.wang, shuah, rgbi3307, guoren, bpf, linux-riscv,
linux-kselftest, linux-kernel
A program on a private stack keeps its BPF frames in a per-CPU area of
its own rather than on the kernel stack of whichever context entered it.
bpf_enable_priv_stack() grants that mode only once the JIT reports
bpf_jit_supports_private_stack(), which riscv64 did not: programs there
always ran on the kernel stack, and a struct_ops program that requires
one was rejected with -EACCES.
Allocate a per-CPU area of the verifier calculated depth plus a 16 byte
guard region at each end, and point BPF_REG_FP (S5) at the top of it
while leaving SP on the kernel stack. Every BPF stack access already
goes through S5, and S5 is callee-saved with each subprog deriving its
own, so nothing else in the JIT has to know about the private stack.
The outgoing stack argument area stays in the kernel frame: callers
write it relative to SP, callees relative to RV_REG_FP, which the
prologue sets to the caller's SP.
The per-CPU lookup is emitted after the tail call entry point: a program
using a private stack can still be entered as a tail call target, and
RV_TAILCALL_OFFSET is a constant added to the target's bpf_func, so what
is in front of it must be the same size in every program.
The base is materialized with absolute moves, as on arm64, x86-64 and
powerpc64: the vmalloc'd per-CPU area is beyond an auipc pair from the
JIT region, and riscv64 has no register holding the per-CPU offset, so
the address of __per_cpu_offset is loaded too. emit_imm() is a choice
there, not a requirement; emit_addr() as used by
insn_is_mov_percpu_addr() would need an extra_pass argument and an error
return in bpf_jit_build_prologue(), touching bpf_jit_comp32.c too. Both
constants are fixed before the offset convergence loop, so the prologue
length is stable across passes.
The guard value follows arm64, x86-64 and powerpc64, and the 16 byte
two-copy region at each end follows arm64 and powerpc64, since x86-64
uses an 8 byte guard with a single copy. A jited program has them
checked when it is freed, reporting the first corrupted CPU only;
failure paths release the area unchecked since the image never ran,
which also clears priv_stack_ptr for the later bpf_jit_free().
RV32 does not declare support and keeps the existing kernel stack
behaviour.
Signed-off-by: Chen Pei <cp0613@linux.alibaba.com>
Reviewed-by: Pu Lehui <pulehui@huawei.com>
---
arch/riscv/net/bpf_jit.h | 2 +
arch/riscv/net/bpf_jit_comp64.c | 42 +++++++++++++++--
arch/riscv/net/bpf_jit_core.c | 82 +++++++++++++++++++++++++++++----
3 files changed, 114 insertions(+), 12 deletions(-)
diff --git a/arch/riscv/net/bpf_jit.h b/arch/riscv/net/bpf_jit.h
index 21a190a6a6d9..dbe1ac8cd0de 100644
--- a/arch/riscv/net/bpf_jit.h
+++ b/arch/riscv/net/bpf_jit.h
@@ -14,6 +14,8 @@
#define RV_MAX_REG_ARGS 8
#define RV_EXTRA_STK_ARGS (RV_MAX_REG_ARGS - MAX_BPF_FUNC_REG_ARGS)
+#define PRIV_STACK_GUARD_SZ 16
+#define PRIV_STACK_GUARD_VAL 0xEB9F12345678eb9fULL
/* verify runtime detection extension status */
#define rv_ext_enabled(ext) \
diff --git a/arch/riscv/net/bpf_jit_comp64.c b/arch/riscv/net/bpf_jit_comp64.c
index 371405598faa..a7a7d2f9da85 100644
--- a/arch/riscv/net/bpf_jit_comp64.c
+++ b/arch/riscv/net/bpf_jit_comp64.c
@@ -2134,12 +2134,34 @@ int bpf_jit_emit_insn(const struct bpf_insn *insn, struct rv_jit_context *ctx,
return 0;
}
+static void emit_priv_stack_frame(struct rv_jit_context *ctx)
+{
+ u64 fp = (__force u64)ctx->prog->aux->priv_stack_ptr + PRIV_STACK_GUARD_SZ;
+
+ fp += round_up(ctx->prog->aux->stack_depth, STACK_ALIGN);
+ emit_imm(RV_REG_S5, fp, ctx);
+#ifdef CONFIG_SMP
+ emit_lw(RV_REG_T1, offsetof(struct thread_info, cpu), RV_REG_TP, ctx);
+ emit_imm(RV_REG_T2, (u64)__per_cpu_offset, ctx);
+ emit_sh3add(RV_REG_T1, RV_REG_T1, RV_REG_T2, ctx);
+ emit_ld(RV_REG_T1, 0, RV_REG_T1, ctx);
+ emit_add(RV_REG_S5, RV_REG_S5, RV_REG_T1, ctx);
+#endif
+}
+
void bpf_jit_build_prologue(struct rv_jit_context *ctx, bool is_subprog)
{
+ bool priv_stack = ctx->prog->aux->priv_stack_ptr;
int i, stack_adjust = 0, store_offset, bpf_stack_adjust = ctx->stack_arg_sz;
- bpf_stack_adjust += round_up(ctx->prog->aux->stack_depth, STACK_ALIGN);
- if (bpf_stack_adjust)
+ /*
+ * With a private stack only the outgoing stack argument area is left in
+ * this frame. It has to stay: the caller writes it relative to SP while
+ * the callee reads it relative to RV_REG_FP, the caller's SP set below.
+ */
+ if (!priv_stack)
+ bpf_stack_adjust += round_up(ctx->prog->aux->stack_depth, STACK_ALIGN);
+ if (bpf_stack_adjust || priv_stack)
mark_fp(ctx);
if (seen_reg(RV_REG_RA, ctx))
@@ -2217,7 +2239,16 @@ void bpf_jit_build_prologue(struct rv_jit_context *ctx, bool is_subprog)
emit_addi(RV_REG_FP, RV_REG_SP, stack_adjust, ctx);
- if (bpf_stack_adjust)
+ /*
+ * Point BPF_REG_FP at the top of this CPU's private stack. A program
+ * with one can still be entered as a tail call target, at
+ * RV_TAILCALL_OFFSET, so this has to come after that entry point; the
+ * offset is a constant added to the target's bpf_func, so everything in
+ * front of it must also be the same size in every program.
+ */
+ if (priv_stack)
+ emit_priv_stack_frame(ctx);
+ else if (bpf_stack_adjust)
emit_addi(RV_REG_S5, RV_REG_SP, bpf_stack_adjust, ctx);
ctx->stack_size = stack_adjust;
@@ -2320,3 +2351,8 @@ bool bpf_jit_supports_hmul(void)
{
return true;
}
+
+bool bpf_jit_supports_private_stack(void)
+{
+ return true;
+}
diff --git a/arch/riscv/net/bpf_jit_core.c b/arch/riscv/net/bpf_jit_core.c
index f2497ec7d378..138e9fa90963 100644
--- a/arch/riscv/net/bpf_jit_core.c
+++ b/arch/riscv/net/bpf_jit_core.c
@@ -9,6 +9,7 @@
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/memory.h>
+#include <linux/percpu.h>
#include <asm/text-patching.h>
#include <asm/cfi.h>
#include "bpf_jit.h"
@@ -44,6 +45,60 @@ bool bpf_jit_needs_zext(void)
return true;
}
+static u32 priv_stack_size(struct bpf_prog *prog)
+{
+ return round_up(prog->aux->stack_depth, STACK_ALIGN) +
+ 2 * PRIV_STACK_GUARD_SZ;
+}
+
+static int alloc_priv_stack(struct bpf_prog *prog)
+{
+ u32 size = priv_stack_size(prog);
+ u32 end = (size - PRIV_STACK_GUARD_SZ) / sizeof(u64);
+ void __percpu *ptr;
+ int cpu, i;
+
+ ptr = __alloc_percpu_gfp(size, STACK_ALIGN, GFP_KERNEL);
+ if (!ptr)
+ return -ENOMEM;
+
+ for_each_possible_cpu(cpu) {
+ u64 *stack = per_cpu_ptr(ptr, cpu);
+
+ for (i = 0; i < PRIV_STACK_GUARD_SZ / sizeof(u64); i++) {
+ stack[i] = PRIV_STACK_GUARD_VAL;
+ stack[end + i] = PRIV_STACK_GUARD_VAL;
+ }
+ }
+ prog->aux->priv_stack_ptr = ptr;
+ return 0;
+}
+
+static void priv_stack_check_guard(struct bpf_prog *prog)
+{
+ u32 end = (priv_stack_size(prog) - PRIV_STACK_GUARD_SZ) / sizeof(u64);
+ int cpu;
+
+ for_each_possible_cpu(cpu) {
+ u64 *stack = per_cpu_ptr(prog->aux->priv_stack_ptr, cpu);
+
+ if (stack[0] != PRIV_STACK_GUARD_VAL ||
+ stack[1] != PRIV_STACK_GUARD_VAL ||
+ stack[end] != PRIV_STACK_GUARD_VAL ||
+ stack[end + 1] != PRIV_STACK_GUARD_VAL) {
+ pr_err("bpf-jit: private stack overflow/underflow guard of prog %s corrupted on CPU %d\n",
+ bpf_jit_get_prog_name(prog), cpu);
+ return;
+ }
+ }
+}
+
+static void free_priv_stack(struct bpf_prog *prog)
+{
+ free_percpu(prog->aux->priv_stack_ptr);
+ prog->aux->priv_stack_ptr = NULL;
+}
+
struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_prog *prog)
{
unsigned int prog_size = 0, extable_size = 0;
@@ -84,12 +139,15 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
round_up((stack_arg_cnt - RV_EXTRA_STK_ARGS) * 8, STACK_ALIGN) : 0;
ctx->prog = prog;
+ if (prog->aux->jits_use_priv_stack && alloc_priv_stack(prog))
+ goto out_free_hdr;
+
ctx->offset = kvzalloc_objs(int, prog->len);
if (!ctx->offset)
- goto out_offset;
+ goto out_free_hdr;
if (build_body(ctx, extra_pass, NULL))
- goto out_offset;
+ goto out_free_hdr;
for (i = 0; i < prog->len; i++) {
prev_ninsns += 32;
@@ -104,7 +162,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
ctx->prologue_len = ctx->ninsns;
if (build_body(ctx, extra_pass, ctx->offset))
- goto out_offset;
+ goto out_free_hdr;
ctx->epilogue_offset = ctx->ninsns;
bpf_jit_build_epilogue(ctx);
@@ -124,7 +182,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
bpf_fill_ill_insns,
bpf_prog_was_classic(prog));
if (!jit_data->ro_header)
- goto out_offset;
+ goto out_free_hdr;
/*
* Use the image(RW) for writing the JITed instructions. But also save
@@ -190,10 +248,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
ctx->offset[i] = ctx->offset[i - 1];
ctx->offset[0] = ninsns_rvoff(ctx->prologue_len);
bpf_prog_update_insn_ptrs(prog, ctx->offset, jit_data->ro_image);
-out_offset:
- kvfree(ctx->offset);
- kfree(jit_data);
- prog->aux->jit_data = NULL;
+ goto out_free_ctx;
}
return prog;
@@ -209,7 +264,12 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
sizeof(jit_data->header->size));
bpf_jit_binary_pack_free(jit_data->ro_header, jit_data->header);
}
- goto out_offset;
+ free_priv_stack(prog);
+out_free_ctx:
+ kvfree(ctx->offset);
+ kfree(jit_data);
+ prog->aux->jit_data = NULL;
+ return prog;
}
u64 bpf_jit_alloc_exec_limit(void)
@@ -261,6 +321,10 @@ void bpf_jit_free(struct bpf_prog *prog)
prog->bpf_func = (void *)prog->bpf_func - cfi_get_offset();
hdr = bpf_jit_binary_pack_hdr(prog);
bpf_jit_binary_pack_free(hdr, NULL);
+ if (prog->aux->priv_stack_ptr) {
+ priv_stack_check_guard(prog);
+ free_priv_stack(prog);
+ }
WARN_ON_ONCE(!bpf_prog_kallsyms_verify_off(prog));
}
--
2.50.1
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v3 bpf-next 2/3] selftests/bpf: Enable private stack tests for riscv64
2026-11-06 18:15 [PATCH v3 bpf-next 0/3] riscv: bpf: Add BPF private stack support Chen Pei
2026-11-06 18:15 ` [PATCH v3 bpf-next 1/3] " Chen Pei
@ 2026-11-06 18:15 ` Chen Pei
2026-11-06 18:15 ` [PATCH v3 bpf-next 3/3] selftests/bpf: Test a private stack together with stack arguments Chen Pei
2 siblings, 0 replies; 4+ messages in thread
From: Chen Pei @ 2026-11-06 18:15 UTC (permalink / raw)
To: ast, daniel, andrii, memxor, bjorn, puranjay, alexei.starovoitov
Cc: eddyz87, martin.lau, song, yonghong.song, jolsa, emil,
ihor.solodrai, pulehui, pjw, palmer, aou, alex, luke.r.nels,
xi.wang, shuah, rgbi3307, guoren, bpf, linux-riscv,
linux-kselftest, linux-kernel
The RV64 JIT now supports the private stack. Allow the existing
verifier and struct_ops private-stack tests to run on riscv64 and add
the expected RV64 JITed instruction patterns.
Signed-off-by: Chen Pei <cp0613@linux.alibaba.com>
Reviewed-by: Pu Lehui <pulehui@huawei.com>
---
.../bpf/prog_tests/struct_ops_private_stack.c | 3 +-
.../bpf/progs/verifier_private_stack.c | 41 ++++++++++++++++++-
2 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/struct_ops_private_stack.c b/tools/testing/selftests/bpf/prog_tests/struct_ops_private_stack.c
index 2b3ec2b79091..103179ff8733 100644
--- a/tools/testing/selftests/bpf/prog_tests/struct_ops_private_stack.c
+++ b/tools/testing/selftests/bpf/prog_tests/struct_ops_private_stack.c
@@ -6,7 +6,8 @@
#include "struct_ops_private_stack_recur.skel.h"
#include "struct_ops_private_stack_large.skel.h"
-#if defined(__x86_64__) || defined(__aarch64__) || defined(__powerpc64__)
+#if defined(__x86_64__) || defined(__aarch64__) || defined(__powerpc64__) || \
+ (defined(__riscv) && __riscv_xlen == 64)
static void test_private_stack(void)
{
struct struct_ops_private_stack *skel;
diff --git a/tools/testing/selftests/bpf/progs/verifier_private_stack.c b/tools/testing/selftests/bpf/progs/verifier_private_stack.c
index aa15361bcca5..3cde7b1af126 100644
--- a/tools/testing/selftests/bpf/progs/verifier_private_stack.c
+++ b/tools/testing/selftests/bpf/progs/verifier_private_stack.c
@@ -8,7 +8,8 @@
/* From include/linux/filter.h */
#define MAX_BPF_STACK 512
-#if defined(__TARGET_ARCH_x86) || defined(__TARGET_ARCH_arm64)
+#if defined(__TARGET_ARCH_x86) || defined(__TARGET_ARCH_arm64) || \
+ (defined(__TARGET_ARCH_riscv) && __riscv_xlen == 64)
struct elem {
struct bpf_timer t;
@@ -42,6 +43,14 @@ __jited(" mov x0, #0x2a")
__jited(" str x0, [x27]")
__jited("...")
__jited(" ldp x25, x27, [sp], {{.*}}")
+__arch_riscv64
+__jited(" addi sp, sp, -0x20")
+__jited("...")
+__jited(" sd s5, 0x10(sp)")
+__jited("...")
+__jited(" sd a0, -0x100(s5)")
+__jited("...")
+__jited(" ld s5, 0x10(sp)")
__naked void private_stack_single_prog(void)
{
asm volatile (" \
@@ -60,6 +69,10 @@ __jited(" subq $0x8, %rsp")
__arch_arm64
__jited(" mov x25, sp")
__jited(" sub sp, sp, #0x10")
+__arch_riscv64
+__jited(" addi sp, sp, -0x30")
+__jited("...")
+__jited(" addi s5, sp, 0x10")
__naked void no_private_stack_nested(void)
{
asm volatile (" \
@@ -153,6 +166,14 @@ __jited(" str x0, [x27]")
__jited(" bl {{.*}}")
__jited("...")
__jited(" ldp x25, x27, [sp], {{.*}}")
+__arch_riscv64
+__jited(" addi sp, sp, -0x20")
+__jited("...")
+__jited(" sd s5, 0x8(sp)")
+__jited("...")
+__jited(" sd a0, -0x200(s5)")
+__jited("...")
+__jited(" ld s5, 0x8(sp)")
__naked void private_stack_nested_1(void)
{
asm volatile (" \
@@ -223,6 +244,16 @@ __jited(" bl 0x{{.*}}")
__jited(" mov x8, x0")
__jited(" mov x8, #0x0")
__jited(" ldp x25, x27, [sp], {{.*}}")
+__arch_riscv64
+__jited("func #1")
+__jited("...")
+__jited(" addi sp, sp, -0x20")
+__jited("...")
+__jited(" sd s5, 0x8(sp)")
+__jited("...")
+__jited(" sd a0, -0x200(s5)")
+__jited("...")
+__jited(" ld s5, 0x8(sp)")
__naked void private_stack_callback(void)
{
asm volatile (" \
@@ -452,6 +483,10 @@ __arch_arm64
__jited(" mrs x10, TPIDR_EL{{[0-1]}}")
__jited(" add x27, x27, x10")
__jited(" add x25, x27, {{.*}}")
+__arch_riscv64
+__jited(" addi sp, sp, -0x20")
+__jited("...")
+__jited(" sd s5, 0x8(sp)")
int private_stack_async_callback_1(void)
{
struct bpf_timer *arr_timer;
@@ -481,6 +516,10 @@ __arch_x86_64
__jited(" subq $0x100, %rsp")
__arch_arm64
__jited(" sub sp, sp, #0x100")
+__arch_riscv64
+__jited(" addi sp, sp, -0x120")
+__jited("...")
+__jited(" addi s5, sp, 0x100")
int private_stack_async_callback_2(void)
{
struct bpf_timer *arr_timer;
--
2.50.1
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v3 bpf-next 3/3] selftests/bpf: Test a private stack together with stack arguments
2026-11-06 18:15 [PATCH v3 bpf-next 0/3] riscv: bpf: Add BPF private stack support Chen Pei
2026-11-06 18:15 ` [PATCH v3 bpf-next 1/3] " Chen Pei
2026-11-06 18:15 ` [PATCH v3 bpf-next 2/3] selftests/bpf: Enable private stack tests for riscv64 Chen Pei
@ 2026-11-06 18:15 ` Chen Pei
2 siblings, 0 replies; 4+ messages in thread
From: Chen Pei @ 2026-11-06 18:15 UTC (permalink / raw)
To: ast, daniel, andrii, memxor, bjorn, puranjay, alexei.starovoitov
Cc: eddyz87, martin.lau, song, yonghong.song, jolsa, emil,
ihor.solodrai, pulehui, pjw, palmer, aou, alex, luke.r.nels,
xi.wang, shuah, rgbi3307, guoren, bpf, linux-riscv,
linux-kselftest, linux-kernel
Add two asm programs to verifier_stack_arg.c, inside the block that is
already gated on x86, arm64 and riscv64. A raw_tp program with a 64 byte
frame calls a ten-argument subprog with a 64 byte frame of its own, and
BPF_PROG_TEST_RUN runs the pair without attaching it. Args 9 and 10 stay
in the kernel frame and arg 10 points into the caller's frame, so the two
have to agree on where the outgoing area is. The callee returns what the
caller stored plus arg 9, so a wrong area shows up as a wrong retval
rather than as a crash.
Suggested-by: Alexei Starovoitov <alexei.starovoitov@gmail.com>
Signed-off-by: Chen Pei <cp0613@linux.alibaba.com>
---
.../selftests/bpf/progs/verifier_stack_arg.c | 49 +++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_stack_arg.c b/tools/testing/selftests/bpf/progs/verifier_stack_arg.c
index 146cdf81bbc4..e8108e768b89 100644
--- a/tools/testing/selftests/bpf/progs/verifier_stack_arg.c
+++ b/tools/testing/selftests/bpf/progs/verifier_stack_arg.c
@@ -975,6 +975,55 @@ __naked void stack_arg_precision_fallback(void)
);
}
+/* Writes both ends of its frame, then reads args 9, 10 and the frame of the caller. */
+static __naked __noinline __used
+long priv_stack_10args(long a, long b, long c, long d, long e, long f, long g, long h,
+ long i, long *j)
+{
+ asm volatile (
+ "r1 = 0;"
+ "*(u64 *)(r10 - 8) = r1;"
+ "*(u64 *)(r10 - 64) = r1;"
+ "r0 = *(u64 *)(r11 + 32);"
+ "r1 = *(u64 *)(r11 + 40);"
+ "r1 = *(u64 *)(r1 + 0);"
+ "r0 += r1;"
+ "exit;"
+ ::: __clobber_all
+ );
+}
+
+/*
+ * A frame of 64 bytes in a raw_tp prog goes to the private stack.
+ * arm64 and riscv64 pass args 9 and 10 on the kernel stack. On x86-64
+ * both frames stay there as well.
+ */
+SEC("raw_tp")
+__description("stack_arg: private stack in the caller and in the callee")
+__success __retval(890)
+__naked void stack_arg_priv_stack(void)
+{
+ asm volatile (
+ "r1 = 800;"
+ "*(u64 *)(r10 - 64) = r1;"
+ "r1 = r10;"
+ "r1 += -64;"
+ "*(u64 *)(r11 - 40) = r1;"
+ "*(u64 *)(r11 - 32) = 90;"
+ "*(u64 *)(r11 - 24) = 8;"
+ "*(u64 *)(r11 - 16) = 7;"
+ "*(u64 *)(r11 - 8) = 6;"
+ "r1 = 1;"
+ "r2 = 2;"
+ "r3 = 3;"
+ "r4 = 4;"
+ "r5 = 5;"
+ "call priv_stack_10args;"
+ "exit;"
+ ::: __clobber_all
+ );
+}
+
#else
/* No BTF tells about this store. The insn is what JIT has to deal with. */
--
2.50.1
^ permalink raw reply [flat|nested] 4+ messages in thread