From: "Alexei Starovoitov" <alexei.starovoitov@gmail.com>
To: "Yusheng Zheng" <yunwei356@gmail.com>, <bpf@vger.kernel.org>
Cc: "Daniel Borkmann" <daniel@iogearbox.net>,
"Andrii Nakryiko" <andrii@kernel.org>,
"Eduard Zingerman" <eddyz87@gmail.com>,
"Kumar Kartikeya Dwivedi" <memxor@gmail.com>,
"Martin KaFai Lau" <martin.lau@linux.dev>,
"Song Liu" <song@kernel.org>,
"Yonghong Song" <yonghong.song@linux.dev>,
"Jiri Olsa" <jolsa@kernel.org>,
"John Fastabend" <john.fastabend@gmail.com>,
"Emil Tsalapatis" <emil@etsalapatis.com>,
"Ihor Solodrai" <ihor.solodrai@linux.dev>, <x86@kernel.org>,
"Thomas Gleixner" <tglx@kernel.org>,
"Ingo Molnar" <mingo@redhat.com>,
"Borislav Petkov" <bp@alien8.de>,
"Dave Hansen" <dave.hansen@linux.intel.com>,
"H . Peter Anvin" <hpa@zytor.com>,
"Leon Hwang" <leon.hwang@linux.dev>,
"Puranjay Mohan" <puranjay@kernel.org>,
"Hao Sun" <sunhao.th@gmail.com>
Subject: Re: [RFC PATCH bpf-next 0/7] bpf: Inline kfuncs that have a BPF body
Date: Sat, 17 Oct 2026 00:42:21 +0000 [thread overview]
Message-ID: <DM6P2S5E5F3M.3RB3ECL75G1PJ@gmail.com> (raw)
In-Reply-To: <20261005142219.33451-1-yunwei356@gmail.com>
On Mon, Oct 05, 2026 at 07:22 AM Yusheng Zheng <yunwei356@gmail.com> wrote:
> 1. Is it acceptable that the verifier replaces calls with their
> bodies before the analysis and restores them afterwards?
yes.
> 2. Should kfuncs bring native code through an emit callback, or
> should the JIT only copy compiled kfuncs? The table shows what
> copying alone costs.
Copy only.
> 3. Native code is trusted like the rest of the JIT, and the selftests
> compare it with the bodies on random inputs. Is that enough?
> 4. The bodies are written by hand. Should they rather be compiled from
> the kfunc's C source, as in the inlinable kfuncs RFC [3]?
Both questions go away when C is the only implementation.
As I suggested to Leon: keep C as the fallback, let KF_FASTCALL be
the signal, check the compiled kfunc with the insn decoder and copy it
without 'ret'.
llvm keeps r1-r5 that are not arguments live across bpf_fastcall.
When JIT can inline, the verifier removes spill/fill. When it cannot,
they stay and C is called. That works on all archs from day one.
The arguments are in rdi, rsi, rdx already, so memcpy is enough.
No need to re-encode the insns.
clang compiles bpf_rol64() into
mov %esi,%ecx; mov %rdi,%rax; rol %cl,%rax
rcx is R4 and it's not an argument, hence push/pop around it.
Then no need for bodies, emit, capture of __k constants, kfunc_bind(),
register renaming, body table in btf, hooks in do_check() and
const_fold, insn_kfuncs/ with two Kconfigs, etc.
What is missing: arm64, byte ops like setcc, selftests,
no push/pop when the register is dead.
Something like this (compile tested only):
diff --git a/arch/x86/net/bpf_jit_comp.c b/arch/x86/net/bpf_jit_comp.c
index 004e2dd..7a5667f 100644
--- a/arch/x86/net/bpf_jit_comp.c
+++ b/arch/x86/net/bpf_jit_comp.c
@@ -15,8 +15,11 @@
#include <linux/memory.h>
#include <linux/sort.h>
#include <linux/execmem.h>
+#include <linux/ftrace.h>
+#include <linux/kallsyms.h>
#include <asm/extable.h>
#include <asm/ftrace.h>
+#include <asm/insn.h>
#include <asm/set_memory.h>
#include <asm/nospec-branch.h>
#include <asm/text-patching.h>
@@ -2501,6 +2504,154 @@ static void adjust_cmp_imm(u32 *op, s32 *imm)
}
}
+/* The longest kfunc to inline, in bytes */
+#define KFUNC_INLINE_MAX 64
+
+static u32 x86_reg_bit(u32 bpf_reg)
+{
+ return BIT(reg2hex[bpf_reg] | is_ereg(bpf_reg) << 3);
+}
+
+/*
+ * Return the mask of x86 registers named by an insn of a kfunc or -EINVAL if
+ * the insn cannot be copied. Only 32 and 64-bit moves and ALU ops can be:
+ * no control flow, prefixes, rip-relative addressing or implicit destination.
+ */
+static int kfunc_insn_regs(struct insn *insn)
+{
+ u8 rex = insn->rex_prefix.bytes[0], modrm = insn->modrm.bytes[0], sib = insn->sib.bytes[0];
+ u8 ext = X86_MODRM_REG(modrm), idx = X86_SIB_INDEX(sib) | X86_REX_X(rex) << 2;
+ u32 op = insn->opcode.bytes[0];
+ int regs = 0;
+
+ if (insn->prefixes.nbytes || insn->rex_prefix.nbytes > 1 || insn->opcode.nbytes > 2 ||
+ insn_is_avx_or_xop(insn) || insn_rip_relative(insn))
+ return -EINVAL;
+ if (insn->opcode.nbytes == 2)
+ op = op << 8 | insn->opcode.bytes[1];
+
+ switch (op) {
+ case 0x01: case 0x03: /* add */
+ case 0x09: case 0x0b: /* or */
+ case 0x21: case 0x23: /* and */
+ case 0x29: case 0x2b: /* sub */
+ case 0x31: case 0x33: /* xor */
+ case 0x39: case 0x3b: /* cmp */
+ case 0x63: /* movsxd */
+ case 0x69: case 0x6b: case 0x0faf: /* imul */
+ case 0x85: /* test */
+ case 0x89: case 0x8b: /* mov */
+ case 0x8d: /* lea */
+ case 0x0f40 ... 0x0f4f: /* cmovcc */
+ case 0x0fb7: case 0x0fbf: /* movzx, movsx */
+ regs = BIT(ext | X86_REX_R(rex) << 1);
+ break;
+ case 0x81: case 0x83: /* ALU ops with imm */
+ case 0xc1: case 0xd1: case 0xd3: /* shifts and rotates */
+ break;
+ case 0xc7: /* mov imm */
+ if (ext)
+ return -EINVAL;
+ break;
+ case 0xf7: /* test, not, neg */
+ if (ext > 3)
+ return -EINVAL;
+ break;
+ case 0xb8 ... 0xbf: /* mov imm */
+ case 0x0fc8 ... 0x0fcf: /* bswap */
+ return BIT((op & 7) | X86_REX_B(rex) << 3);
+ default:
+ return -EINVAL;
+ }
+ if (!insn->sib.nbytes)
+ return regs | BIT(X86_MODRM_RM(modrm) | X86_REX_B(rex) << 3);
+ if (idx != 4)
+ regs |= BIT(idx);
+ return regs | BIT(X86_SIB_BASE(sib) | X86_REX_B(rex) << 3);
+}
+
+/* @op is 0x50 for push and 0x58 for pop of x86 register @reg */
+static void emit_push_pop(u8 **pprog, u8 op, int reg)
+{
+ u8 *prog = *pprog;
+
+ if (reg > 7)
+ EMIT1(0x41);
+ EMIT1(op | (reg & 7));
+ *pprog = prog;
+}
+
+/*
+ * Inline a KF_FASTCALL kfunc: copy what the compiler generated for it without
+ * ENDBR, fentry and 'ret'. bpf_fastcall lets it clobber only R0 and the
+ * arguments. Push/pop the other caller saved registers that it uses.
+ */
+static int emit_kfunc_inline(u8 **pprog, unsigned long addr, const struct btf_func_model *m)
+{
+ /* a call clobbers r10 and r11, hence nothing is live there */
+ u32 scratch = x86_reg_bit(BPF_REG_AX) | x86_reg_bit(AUX_REG);
+ u32 save = x86_reg_bit(X86_REG_R9), used = 0;
+ u8 *p = (u8 *)addr, *start, *prog = *pprog;
+ unsigned long size, off;
+ struct insn insn;
+ int i, regs;
+
+ if (m->nr_args > MAX_BPF_FUNC_REG_ARGS)
+ return -EINVAL;
+ for (i = 0; i < m->nr_args; i++)
+ if (m->arg_size[i] > 8 || (m->arg_flags[i] & BTF_FMODEL_ARENA_ARG))
+ return -EINVAL;
+ for (i = BPF_REG_0; i <= BPF_REG_5; i++) {
+ if (i ? i <= m->nr_args : m->ret_size)
+ scratch |= x86_reg_bit(i);
+ else
+ save |= x86_reg_bit(i);
+ }
+
+ if (!kallsyms_lookup_size_offset(addr, &size, &off) || off)
+ return -EINVAL;
+ if (is_endbr((u32 *)p))
+ p += ENDBR_INSN_SIZE;
+ if (ftrace_location(addr) == (unsigned long)p)
+ p += MCOUNT_INSN_SIZE;
+
+ for (start = p;; p += insn.length) {
+ if (p - start > KFUNC_INLINE_MAX || p >= (u8 *)addr + size ||
+ insn_decode_kernel(&insn, p))
+ return -EINVAL;
+ if (insn.opcode.bytes[0] == RET_INSN_OPCODE ||
+ (insn.opcode.bytes[0] == JMP32_INSN_OPCODE &&
+ p + insn.length + insn.immediate.value == (u8 *)x86_return_thunk))
+ break;
+ regs = kfunc_insn_regs(&insn);
+ if (regs < 0)
+ return regs;
+ used |= regs;
+ }
+ if (used & ~(scratch | save))
+ return -EINVAL;
+ save &= used;
+
+ for (i = 0; i < 16; i++)
+ if (save & BIT(i))
+ emit_push_pop(&prog, 0x50, i);
+ memcpy(prog, start, p - start);
+ prog += p - start;
+ for (i = 15; i >= 0; i--)
+ if (save & BIT(i))
+ emit_push_pop(&prog, 0x58, i);
+
+ *pprog = prog;
+ return 0;
+}
+
+bool bpf_jit_inlines_kfunc_call(unsigned long addr, const struct btf_func_model *m)
+{
+ u8 buf[BPF_MAX_INSN_SIZE], *prog = buf;
+
+ return !emit_kfunc_inline(&prog, addr, m);
+}
+
static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *addrs, u8 *image,
u8 *rw_image, int oldproglen, struct jit_context *ctx, bool jmp_padding)
{
@@ -3424,6 +3575,12 @@ populate_extable:
fm = bpf_jit_find_kfunc_model(bpf_prog, insn);
if (!fm)
return -EINVAL;
+ if (fm->ret_flags & BTF_FMODEL_JIT_INLINE) {
+ err = emit_kfunc_inline(&prog, (unsigned long)func, fm);
+ if (err)
+ return err;
+ break;
+ }
err = emit_kfunc_arena_args(bpf_prog, fm, &prog);
if (err < 0)
return err;
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 5414437..91b2c0e 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -1256,6 +1256,9 @@ struct bpf_prog_offload {
/* The argument is nullable. */
#define BTF_FMODEL_NULLABLE_ARG BIT(3)
+/* In ret_flags: the JIT inlines calls of this kfunc. */
+#define BTF_FMODEL_JIT_INLINE BIT(4)
+
struct btf_func_model {
u8 ret_size;
u8 ret_flags;
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 9339c61..cd170bf 100644
--- a/include/linux/filter.h
+++ b/include/linux/filter.h
@@ -1239,6 +1239,7 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
void bpf_jit_compile(struct bpf_prog *prog);
bool bpf_jit_needs_zext(void);
bool bpf_jit_inlines_helper_call(s32 imm);
+bool bpf_jit_inlines_kfunc_call(unsigned long addr, const struct btf_func_model *m);
bool bpf_jit_supports_subprog_tailcalls(void);
bool bpf_jit_supports_percpu_insn(void);
bool bpf_jit_supports_kfunc_call(void);
diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
index 642f0ab..49f331d 100644
--- a/kernel/bpf/core.c
+++ b/kernel/bpf/core.c
@@ -3295,6 +3295,15 @@ bool __weak bpf_jit_inlines_helper_call(s32 imm)
return false;
}
+/* Return TRUE if the JIT backend inlines the KF_FASTCALL kfunc at @addr
+ * without clobbering registers that bpf_fastcall contract doesn't allow.
+ * Otherwise the kfunc is called and spills/fills around the call stay.
+ */
+bool __weak bpf_jit_inlines_kfunc_call(unsigned long addr, const struct btf_func_model *m)
+{
+ return false;
+}
+
/* Return TRUE if the JIT backend supports mixing bpf2bpf and tailcalls. */
bool __weak bpf_jit_supports_subprog_tailcalls(void)
{
diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 439370d..4a1c08f 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -3383,6 +3383,16 @@ __bpf_kfunc void *bpf_rdonly_cast(const void *obj__ign, u32 btf_id__k)
return (void *)obj__ign;
}
+__bpf_kfunc u64 bpf_rol64(u64 x, u32 n)
+{
+ return rol64(x, n);
+}
+
+__bpf_kfunc u64 bpf_select64(u64 cond, u64 a, u64 b)
+{
+ return cond ? a : b;
+}
+
__bpf_kfunc void bpf_rcu_read_lock(void)
{
rcu_read_lock();
@@ -5147,6 +5157,8 @@ BTF_ID(func, bpf_cgroup_release_dtor)
BTF_KFUNCS_START(common_btf_ids)
BTF_ID_FLAGS(func, bpf_cast_to_kern_ctx, KF_FASTCALL)
BTF_ID_FLAGS(func, bpf_rdonly_cast, KF_FASTCALL | KF_PERFMON)
+BTF_ID_FLAGS(func, bpf_rol64, KF_FASTCALL)
+BTF_ID_FLAGS(func, bpf_select64, KF_FASTCALL)
BTF_ID_FLAGS(func, bpf_rcu_read_lock)
BTF_ID_FLAGS(func, bpf_rcu_read_unlock)
BTF_ID_FLAGS(func, bpf_dynptr_slice, KF_RET_NULL)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 62f8951..84b641d 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3151,6 +3151,8 @@ int bpf_add_kfunc_call(struct bpf_verifier_env *env, u32 func_id, u16 offset)
meta.func_proto = kfunc.proto;
meta.func_name = kfunc.name;
meta.kfunc_flags = kfunc.flags ? *kfunc.flags : 0;
+ if ((meta.kfunc_flags & KF_FASTCALL) && bpf_jit_inlines_kfunc_call(addr, &func_model))
+ func_model.ret_flags |= BTF_FMODEL_JIT_INLINE;
tab = krealloc(tab, struct_size(tab, descs, tab->nr_descs + 1), GFP_KERNEL_ACCOUNT);
if (!tab)
@@ -19257,6 +19259,13 @@ bool bpf_verifier_inlines_helper_call(struct bpf_verifier_env *env, s32 imm)
}
}
+/* True if bpf_fixup_kfunc_call() replaces calls to the kfunc */
+static bool verifier_inlines_kfunc_call(u32 func_id)
+{
+ return func_id == special_kfunc_list[KF_bpf_cast_to_kern_ctx] ||
+ func_id == special_kfunc_list[KF_bpf_rdonly_cast];
+}
+
/* If @call is a kfunc or helper call, fills @cs and returns true,
* otherwise returns false.
*/
@@ -19285,14 +19294,18 @@ bool bpf_get_call_summary(struct bpf_verifier_env *env, struct bpf_insn *call,
}
if (bpf_pseudo_kfunc_call(call)) {
+ struct bpf_kfunc_desc *desc;
int err;
err = bpf_fetch_kfunc_arg_meta(env, call->imm, call->off, &meta);
if (err < 0)
/* error would be reported later */
return false;
+ desc = find_kfunc_desc(env->prog, call->imm, call->off);
cs->arg_slot_cnt = btf_proto_slots(meta.btf, meta.func_proto);
- cs->fastcall = meta.kfunc_flags & KF_FASTCALL;
+ cs->fastcall = (meta.kfunc_flags & KF_FASTCALL) &&
+ (verifier_inlines_kfunc_call(meta.func_id) ||
+ (desc && (desc->func_model.ret_flags & BTF_FMODEL_JIT_INLINE)));
cs->is_void = btf_type_is_void(btf_type_by_id(meta.btf, meta.func_proto->type));
return true;
}
@@ -22945,8 +22958,7 @@ int bpf_fixup_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
__fixup_collection_insert_kfunc(&env->insn_aux_data[insn_idx], struct_meta_reg,
node_offset_reg, insn, insn_buf, cnt);
- } else if (desc->func_id == special_kfunc_list[KF_bpf_cast_to_kern_ctx] ||
- desc->func_id == special_kfunc_list[KF_bpf_rdonly_cast]) {
+ } else if (verifier_inlines_kfunc_call(desc->func_id)) {
insn_buf[0] = BPF_MOV64_REG(BPF_REG_0, BPF_REG_1);
*cnt = 1;
} else if (desc->func_id == special_kfunc_list[KF_bpf_session_is_return] &&
prev parent reply other threads:[~2026-10-17 0:42 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-05 14:22 [RFC PATCH bpf-next 0/7] bpf: Inline kfuncs that have a BPF body Yusheng Zheng
2026-10-05 14:22 ` [RFC PATCH bpf-next 1/7] bpf: Let kfunc sets give kfuncs " Yusheng Zheng
2026-10-05 14:38 ` sashiko-bot
2026-10-05 15:16 ` bot+bpf-ci
2026-10-05 14:22 ` [RFC PATCH bpf-next 2/7] bpf: Verify calls of kfuncs with a body through the body Yusheng Zheng
2026-10-05 14:41 ` sashiko-bot
2026-10-05 14:22 ` [RFC PATCH bpf-next 3/7] bpf, x86: Inline native code for kfuncs that have a body Yusheng Zheng
2026-10-05 14:22 ` [RFC PATCH bpf-next 4/7] bpf: Add kfuncs with bodies for common operations Yusheng Zheng
2026-10-05 14:39 ` sashiko-bot
2026-10-05 14:22 ` [RFC PATCH bpf-next 5/7] bpf, x86: Add native code for some inline kfuncs Yusheng Zheng
2026-10-05 14:22 ` [RFC PATCH bpf-next 6/7] selftests/bpf: Test " Yusheng Zheng
2026-10-05 14:22 ` [RFC PATCH bpf-next 7/7] Documentation/bpf: Describe " Yusheng Zheng
2026-10-05 15:16 ` bot+bpf-ci
2026-10-17 0:42 ` Alexei Starovoitov [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=DM6P2S5E5F3M.3RB3ECL75G1PJ@gmail.com \
--to=alexei.starovoitov@gmail.com \
--cc=andrii@kernel.org \
--cc=bp@alien8.de \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=dave.hansen@linux.intel.com \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=hpa@zytor.com \
--cc=ihor.solodrai@linux.dev \
--cc=john.fastabend@gmail.com \
--cc=jolsa@kernel.org \
--cc=leon.hwang@linux.dev \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=mingo@redhat.com \
--cc=puranjay@kernel.org \
--cc=song@kernel.org \
--cc=sunhao.th@gmail.com \
--cc=tglx@kernel.org \
--cc=x86@kernel.org \
--cc=yonghong.song@linux.dev \
--cc=yunwei356@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox