* [PATCH bpf-next v1 00/12] Unify subprog argument checks
@ 2026-09-25 21:12 Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments Amery Hung
` (11 more replies)
0 siblings, 12 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Hi,
Helper and kfunc calls now validate arguments through check_func_args(),
while subprog calls still duplicate much of the same validation in
btf_check_func_arg_match(). This makes common argument rules and
diagnostics easy to change for one call kind without updating the other.
This series builds a temporary helper-style argument prototype from the
subprog's cached BTF metadata, then moves each supported argument class to
the common checker. The last patch removes the duplicate loop.
The series is organized as follows:
- Patch 1 fixes kfunc nullability diagnostics after wide arguments.
- Patches 2-3 identify subprog calls in common metadata and generate the
temporary argument prototype.
- Patches 4-9 move scalar, untrusted, context, arena, dynptr, and BTF-ID
arguments to the common path.
- Patches 10-11 make packet-change tracking common and move memory
arguments to the common path.
- Patch 12 removes the legacy loop and checks all subprog arguments through
check_func_args().
This follows the helper/kfunc argument-checking consolidation:
https://lore.kernel.org/bpf/20260911220415.1396439-1-ameryhung@gmail.com/
Amery Hung (12):
bpf: Fix kfunc nullability diagnostics after wide arguments
bpf: Identify subprog calls in argument metadata
bpf: Build argument prototypes for subprog calls
bpf: Check subprog scalar arguments in the common path
bpf: Check global subprog untrusted arguments in the common path
bpf: Check subprog context arguments in the common path
bpf: Check subprog arena arguments in the common path
bpf: Check subprog dynptr arguments in the common path
bpf: Check global subprog BTF-ID arguments in the common path
bpf: Track packet changes in call metadata
bpf: Check global subprog memory arguments in the common path
bpf: Check all subprog arguments in the common path
include/linux/bpf_verifier.h | 4 +-
kernel/bpf/verifier.c | 285 ++++++++----------
.../selftests/bpf/progs/aggregate_arg_func.c | 2 +-
.../testing/selftests/bpf/progs/dynptr_fail.c | 11 +-
.../selftests/bpf/progs/test_global_func5.c | 2 +-
.../bpf/progs/verifier_global_ptr_args.c | 7 +-
.../bpf/progs/verifier_global_subprogs.c | 2 +-
7 files changed, 136 insertions(+), 177 deletions(-)
--
2.52.0
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-26 8:48 ` Alexei Starovoitov
2026-09-25 21:12 ` [PATCH bpf-next v1 02/12] bpf: Identify subprog calls in argument metadata Amery Hung
` (10 subsequent siblings)
11 siblings, 1 reply; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
check_func_arg_nullability() indexes a kfunc BTF parameter using the ABI
slot number. The parameter and slot indexes diverge after a by-value
argument wider than one eightbyte, so a later NULL pointer is diagnosed
with an unrelated BTF type or an invalid type ID.
check_func_arg() already tracks the BTF parameter and ABI slot
separately. Pass the parameter index to check_func_arg_nullability() and
use it for the BTF lookup.
Fixes: ae6abae582b7 ("bpf: Recognize by-value struct and __int128 kfunc arguments")
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 5c8626215ee6..2792dcf91061 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -8919,7 +8919,7 @@ __printf(6, 7) static void bpf_diag_call_arg_fmt(struct bpf_verifier_env *env, u
}
static int check_func_arg_nullability(struct bpf_verifier_env *env,
- struct bpf_reg_state *reg, argno_t argno,
+ struct bpf_reg_state *reg, u32 arg, argno_t argno,
enum bpf_arg_type arg_type,
struct bpf_call_arg_meta *meta, int insn_idx)
{
@@ -8932,7 +8932,7 @@ static int check_func_arg_nullability(struct bpf_verifier_env *env,
if (meta->btf) {
u32 arg_btf_id;
- arg_btf_id = btf_params(meta->func_proto)[arg_idx_from_argno(argno)].type;
+ arg_btf_id = btf_params(meta->func_proto)[arg].type;
expected_type = bpf_diag_fmt(env, "value of type %s",
bpf_diag_fmt_btf_type(env, meta->btf, arg_btf_id));
}
@@ -9412,7 +9412,7 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
return 0;
}
- err = check_func_arg_nullability(env, reg, argno, arg_type, meta, insn_idx);
+ err = check_func_arg_nullability(env, reg, arg, argno, arg_type, meta, insn_idx);
if (err)
return err;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 02/12] bpf: Identify subprog calls in argument metadata
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 03/12] bpf: Build argument prototypes for subprog calls Amery Hung
` (9 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Prepare subprog calls to use the common check_func_args() path. That
checker distinguishes call kinds through bpf_call_arg_meta, but
btf_check_func_arg_match() leaves both BTF and the function ID zero even
though it validates a program-BTF signature.
Represent a subprog call with a non-NULL BTF and a zero function ID.
Define helpers as NULL BTF plus nonzero ID, and kfuncs as non-NULL BTF
plus nonzero ID. Requiring a nonzero kfunc ID also prevents unavailable
special-kfunc IDs from matching subprog metadata. The corresponding
subprog predicate is introduced later alongside its first use.
Setting BTF would expose checks that treat every BTF-backed call as a
kfunc. Restrict kfunc-only BTF parameter lookup, register admission,
release diagnostics, no-cast alias, and projection handling to actual
kfuncs.
The BTF is not modified here, but bpf_call_arg_meta::btf and several
downstream consumers use non-const pointers. Match those existing types
instead of broadening this series with a const-correctness cleanup.
No functional change is intended.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 36 +++++++++++++++++++++++-------------
1 file changed, 23 insertions(+), 13 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 2792dcf91061..43cae33d9921 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -8638,18 +8638,27 @@ static bool arg_type_is_scalar(enum bpf_arg_type type)
}
/*
- * A kfunc is named by a BTF ID, which can take the same numeric value as an
- * enum bpf_func_id. Only test meta->func_id against a BPF_FUNC_* once the call
- * is known to be to a helper; meta->btf is set only for a kfunc.
+ * A helper has no BTF and a nonzero function ID. A kfunc has both, while a
+ * BPF subprogram has BTF and a zero function ID.
*/
+static bool is_helper(const struct bpf_call_arg_meta *meta)
+{
+ return !meta->btf && meta->func_id;
+}
+
static bool is_helper_call(const struct bpf_call_arg_meta *meta, enum bpf_func_id func_id)
{
- return !meta->btf && meta->func_id == func_id;
+ return is_helper(meta) && meta->func_id == func_id;
+}
+
+static bool is_kfunc(const struct bpf_call_arg_meta *meta)
+{
+ return meta->btf && meta->func_id;
}
static bool is_kfunc_call(const struct bpf_call_arg_meta *meta, u32 btf_id)
{
- return meta->btf && meta->func_id == btf_id;
+ return is_kfunc(meta) && meta->func_id == btf_id;
}
static int resolve_map_arg_type(struct bpf_verifier_env *env,
@@ -8962,7 +8971,7 @@ static int check_func_arg_release(struct bpf_verifier_env *env, struct bpf_reg_s
verbose(env, "release function %s expects referenced PTR_TO_BTF_ID passed to %s\n",
meta->func_name, reg_arg_name(env, argno));
- if (meta->btf) {
+ if (is_kfunc(meta)) {
const struct btf_param *btf_arg;
const struct btf_type *t;
u32 ref_id;
@@ -9016,7 +9025,7 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
verifier_bug(env, "unsupported arg type %d", arg_type);
return -EFAULT;
}
- if (meta->btf && base_type(arg_type) == ARG_PTR_TO_BTF_ID &&
+ if (is_kfunc(meta) && base_type(arg_type) == ARG_PTR_TO_BTF_ID &&
(base_type(type) == PTR_TO_BTF_ID || reg2btf_ids[base_type(type)]))
goto found;
@@ -9039,7 +9048,7 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
if (base_type(arg_type) == ARG_PTR_TO_MEM)
type &= ~DYNPTR_TYPE_FLAG_MASK;
/* Allow allocated memory for kfunc ARG_PTR_TO_MEM but not helper. */
- if (meta->btf && base_type(arg_type) == ARG_PTR_TO_MEM &&
+ if (is_kfunc(meta) && base_type(arg_type) == ARG_PTR_TO_MEM &&
type_is_ptr_alloc_obj(type))
type = PTR_TO_MEM;
@@ -9362,7 +9371,8 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
struct bpf_call_arg_meta *meta,
int insn_idx)
{
- const struct btf_param *btf_arg = meta->btf ? &btf_params(meta->func_proto)[arg] : NULL;
+ const struct btf_param *btf_arg = is_kfunc(meta) ?
+ &btf_params(meta->func_proto)[arg] : NULL;
const struct bpf_func_proto *fn = meta->fn;
struct bpf_func_state *caller = cur_func(env);
struct bpf_reg_state *regs = cur_regs(env);
@@ -10788,7 +10798,7 @@ static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int calls
}
static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
- const struct btf *btf,
+ struct btf *btf,
struct bpf_reg_state *regs)
{
struct bpf_subprog_info *sub = subprog_info(env, subprog);
@@ -10800,8 +10810,8 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
u32 i;
int ret, err;
- /* Leave btf and func_id zero: this is neither a helper nor a kfunc. */
memset(&meta, 0, sizeof(meta));
+ meta.btf = btf;
meta.func_name = bpf_subprog_name(env, subprog);
ret = btf_prepare_func_args(env, subprog);
@@ -13781,7 +13791,7 @@ static int process_arg_ptr_to_btf_id(struct bpf_verifier_env *env, struct bpf_re
* resolve types.
*/
if ((arg_type_is_release(arg_type) && !is_helper_call(meta, BPF_FUNC_sk_release)) ||
- (meta->btf && btf_type_ids_nocast_alias(&env->log, reg_btf, reg_btf_id,
+ (is_kfunc(meta) && btf_type_ids_nocast_alias(&env->log, reg_btf, reg_btf_id,
arg_btf, arg_btf_id)))
strict_type_match = true;
@@ -13798,7 +13808,7 @@ static int process_arg_ptr_to_btf_id(struct bpf_verifier_env *env, struct bpf_re
* actually use it -- it must cast to the underlying type. So we allow
* caller to pass in the underlying type.
*/
- taking_projection = meta->btf && btf_is_projection_of(arg_tname, reg_tname);
+ taking_projection = is_kfunc(meta) && btf_is_projection_of(arg_tname, reg_tname);
if (!taking_projection && !struct_same) {
verbose(env, "%s %s expected pointer to %s %s but %s has a pointer to %s %s\n",
meta->func_name, reg_arg_name(env, argno),
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 03/12] bpf: Build argument prototypes for subprog calls
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 02/12] bpf: Identify subprog calls in argument metadata Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 04/12] bpf: Check subprog scalar arguments in the common path Amery Hung
` (8 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
The common argument checker consumes a parameter-indexed bpf_func_proto,
while BPF subprogram argument metadata is cached by ABI slot in compact
bpf_subprog_info entries. Embedding a full prototype in every fixed
subprogram entry would waste memory.
Embed a scratch prototype in the verifier environment. Fill it by
walking BTF parameters and the cached slots with separate cursors,
mirroring the kfunc representation for parameters that occupy multiple
slots. Keep the compact cache as the source of truth. The verifier
environment is already heap allocated, so this avoids a separate
allocation, failure path, and cleanup.
Keep the existing validation loop, but make it consume the generated
prototype in preparation for moving BPF subprogram calls to the common
argument checker. No functional change.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
include/linux/bpf_verifier.h | 3 +-
kernel/bpf/verifier.c | 103 ++++++++++++++++++++++++-----------
2 files changed, 73 insertions(+), 33 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 646e447cebd8..144da990ee8c 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -972,6 +972,7 @@ struct bpf_verifier_env {
const struct bpf_line_info *prev_linfo;
struct bpf_verifier_log log;
struct bpf_diag *diag;
+ struct bpf_func_proto bpf_subprog_scratch;
struct bpf_subprog_info subprog_info[BPF_MAX_SUBPROGS + 2]; /* max + 2 for the fake and exception subprogs */
/* subprog indices sorted in topological order: leaves first, callers last */
int subprog_topo_order[BPF_MAX_SUBPROGS + 2];
@@ -1638,6 +1639,7 @@ struct bpf_call_arg_meta {
struct btf *btf;
u32 func_id;
const struct bpf_func_proto *fn;
+ const struct btf_type *func_proto;
u8 release_regno;
u32 ret_btf_id;
u32 subprogno;
@@ -1650,7 +1652,6 @@ struct bpf_call_arg_meta {
/* Only set by kfunc */
bool r0_rdonly;
u32 kfunc_flags;
- const struct btf_type *func_proto;
const char *func_name;
struct arg_constant_desc arg_constant;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 43cae33d9921..65a1245e4edb 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -9940,20 +9940,20 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
* func model recorded, or the verifier would check an argument at a slot
* the JIT does not place it at.
*/
-static u32 kfunc_arg_slots(const struct btf_type *t)
+static u32 btf_arg_slots(const struct btf_type *t)
{
if (btf_type_is_int(t) || btf_type_is_struct(t))
return (t->size + BPF_REG_SIZE - 1) / BPF_REG_SIZE;
return 1;
}
-static u32 kfunc_proto_slots(const struct btf *btf, const struct btf_type *func_proto)
+static u32 btf_proto_slots(const struct btf *btf, const struct btf_type *func_proto)
{
const struct btf_param *args = btf_params(func_proto);
u32 i, nargs = btf_type_vlen(func_proto), slots_used = 0;
for (i = 0; i < nargs; i++)
- slots_used += kfunc_arg_slots(btf_type_skip_modifiers(btf, args[i].type, NULL));
+ slots_used += btf_arg_slots(btf_type_skip_modifiers(btf, args[i].type, NULL));
return slots_used;
}
@@ -9996,7 +9996,7 @@ static int check_func_args(struct bpf_verifier_env *env, struct bpf_call_arg_met
* count. Only a proto whose parameters take a slot each can name the
* argument a stack slot belongs to.
*/
- proto_slots = meta->btf ? kfunc_proto_slots(meta->btf, meta->func_proto) : nargs;
+ proto_slots = meta->btf ? btf_proto_slots(meta->btf, meta->func_proto) : nargs;
if (proto_slots > MAX_BPF_FUNC_REG_ARGS) {
err = check_outgoing_stack_args(env, caller, proto_slots, meta->func_name,
@@ -10012,7 +10012,7 @@ static int check_func_args(struct bpf_verifier_env *env, struct bpf_call_arg_met
nslots = 1;
if (args) {
t = btf_type_skip_modifiers(meta->btf, args[arg].type, NULL);
- nslots = kfunc_arg_slots(t);
+ nslots = btf_arg_slots(t);
}
if (meta->fn->arg_type[arg] == ARG_UNUSED)
@@ -10797,6 +10797,22 @@ static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int calls
return err;
}
+static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const struct btf *btf,
+ const struct btf_type *func_proto, struct bpf_func_proto *proto)
+{
+ const struct btf_param *args = btf_params(func_proto);
+ u32 arg, slot = 0;
+
+ memset(proto, 0, sizeof(*proto));
+ for (arg = 0; arg < btf_type_vlen(func_proto); arg++) {
+ const struct btf_type *t;
+
+ proto->arg_type[arg] = sub->args[slot].arg_type;
+ t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
+ slot += btf_arg_slots(t);
+ }
+}
+
static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
struct btf *btf,
struct bpf_reg_state *regs)
@@ -10804,10 +10820,11 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
struct bpf_subprog_info *sub = subprog_info(env, subprog);
struct bpf_func_state *caller = cur_func(env);
struct bpf_verifier_log *log = &env->log;
- const struct btf_param *args;
+ const struct btf_param *args, *stack_args;
const struct btf_type *func, *func_proto;
struct bpf_call_arg_meta meta;
- u32 i;
+ struct bpf_func_proto *fn;
+ u32 arg, slot, nslots;
int ret, err;
memset(&meta, 0, sizeof(meta));
@@ -10829,33 +10846,42 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
func = btf_type_by_id(btf, env->prog->aux->func_info[subprog].type_id);
func_proto = btf_type_by_id(btf, func->type);
args = btf_params(func_proto);
- if (sub->arg_slot_cnt != btf_type_vlen(func_proto))
- args = NULL;
+ stack_args = sub->arg_slot_cnt == btf_type_vlen(func_proto) ? args : NULL;
ret = check_outgoing_stack_args(env, caller, sub->arg_slot_cnt,
- bpf_subprog_name(env, subprog), btf, args);
+ bpf_subprog_name(env, subprog), btf, stack_args);
if (ret)
return ret;
+ fn = &env->bpf_subprog_scratch;
+ gen_subprog_arg_proto(sub, btf, func_proto, fn);
+ meta.fn = fn;
+ meta.func_proto = func_proto;
+
/* check that BTF function arguments match actual types that the
* verifier sees.
*/
- for (i = 0; i < sub->arg_slot_cnt; i++) {
- argno_t argno = argno_from_arg(i + 1);
- struct bpf_reg_state *reg = get_func_arg_reg(caller, regs, i);
- struct bpf_subprog_arg_info *arg = &sub->args[i];
+ for (arg = 0, slot = 0; arg < btf_type_vlen(func_proto); arg++, slot += nslots) {
+ struct bpf_reg_state *reg = get_func_arg_reg(caller, regs, slot);
+ enum bpf_arg_type arg_type = fn->arg_type[arg];
+ argno_t argno = argno_from_arg(slot + 1);
+ const struct btf_type *t;
+ u32 k;
+
+ t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
+ nslots = btf_arg_slots(t);
- if (arg->arg_type == ARG_SCALAR) {
+ if (arg_type == ARG_SCALAR) {
if (reg->type != SCALAR_VALUE) {
bpf_log(log, "%s is not a scalar\n", reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (arg->arg_type & PTR_UNTRUSTED) {
+ } else if (arg_type & PTR_UNTRUSTED) {
/*
* Anything is allowed for untrusted arguments, as these are
* read-only and probe read instructions would protect against
* invalid memory access.
*/
- } else if (arg->arg_type == ARG_PTR_TO_CTX) {
+ } else if (arg_type == ARG_PTR_TO_CTX) {
ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_CTX);
if (ret < 0)
return ret;
@@ -10867,11 +10893,12 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (base_type(arg->arg_type) == ARG_PTR_TO_MEM) {
+ } else if (base_type(arg_type) == ARG_PTR_TO_MEM) {
ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_MEM);
if (ret < 0)
return ret;
- if (check_mem_reg(env, reg, argno, arg->mem_size, BPF_READ | BPF_WRITE, NULL,
+ if (check_mem_reg(env, reg, argno, sub->args[slot].mem_size,
+ BPF_READ | BPF_WRITE, NULL,
NULL))
return -EINVAL;
/*
@@ -10884,13 +10911,13 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
reg_arg_name(env, argno), subprog);
return -EINVAL;
}
- if (!(arg->arg_type & PTR_MAYBE_NULL) &&
+ if (!(arg_type & PTR_MAYBE_NULL) &&
(type_may_be_null(reg->type) || bpf_register_is_null(reg))) {
bpf_log(log, "%s is expected to be non-NULL\n",
reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (base_type(arg->arg_type) == ARG_PTR_TO_ARENA) {
+ } else if (base_type(arg_type) == ARG_PTR_TO_ARENA) {
/*
* Can pass any value and the kernel won't crash, but
* only PTR_TO_ARENA or SCALAR make sense. Everything
@@ -10903,38 +10930,50 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (arg->arg_type == ARG_PTR_TO_DYNPTR) {
+ } else if (arg_type == ARG_PTR_TO_DYNPTR) {
ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_DYNPTR);
if (ret)
return ret;
ret = process_dynptr_func(env, reg, argno, env->insn_idx,
- arg->arg_type, &meta);
+ arg_type, &meta);
if (ret)
return ret;
- } else if (base_type(arg->arg_type) == ARG_PTR_TO_BTF_ID) {
+ } else if (base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
int err;
- if (bpf_register_is_null(reg) && type_may_be_null(arg->arg_type)) {
+ if (bpf_register_is_null(reg) && type_may_be_null(arg_type)) {
err = mark_arg_precision(env, argno);
if (err)
return err;
continue;
}
- err = check_reg_type(env, reg, argno, arg->arg_type, &meta);
- err = err ?: check_func_arg_reg_off(env, reg, argno, arg->arg_type);
+ err = check_reg_type(env, reg, argno, arg_type, &meta);
+ err = err ?: check_func_arg_reg_off(env, reg, argno, arg_type);
if (!err && base_type(reg->type) == PTR_TO_BTF_ID)
- err = process_arg_ptr_to_btf_id(env, reg, argno, arg->arg_type,
- btf_vmlinux, arg->btf_id,
+ err = process_arg_ptr_to_btf_id(env, reg, argno, arg_type,
+ btf_vmlinux, sub->args[slot].btf_id,
&meta, env->insn_idx);
if (err)
return err;
} else {
verifier_bug(env, "unrecognized %s type %d",
- reg_arg_name(env, argno), arg->arg_type);
+ reg_arg_name(env, argno), arg_type);
return -EFAULT;
}
+
+ for (k = 1; k < nslots; k++) {
+ argno_t extra_argno = argno_from_arg(slot + k + 1);
+ struct bpf_reg_state *extra_reg;
+
+ extra_reg = get_func_arg_reg(caller, regs, slot + k);
+ if (extra_reg->type != SCALAR_VALUE) {
+ bpf_log(log, "%s is not a scalar\n",
+ reg_arg_name(env, extra_argno));
+ return -EINVAL;
+ }
+ }
}
return 0;
@@ -14509,7 +14548,7 @@ s64 bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *
* pointer, and neither does a slot past the last parameter.
*/
for (i = 0, slot = 0; i < nargs && slot < arg; i++)
- slot += kfunc_arg_slots(btf_type_skip_modifiers(btf, args[i].type, NULL));
+ slot += btf_arg_slots(btf_type_skip_modifiers(btf, args[i].type, NULL));
if (i >= nargs || slot != arg)
return 0;
@@ -18792,7 +18831,7 @@ bool bpf_get_call_summary(struct bpf_verifier_env *env, struct bpf_insn *call,
if (err < 0)
/* error would be reported later */
return false;
- cs->arg_slot_cnt = kfunc_proto_slots(meta.btf, meta.func_proto);
+ cs->arg_slot_cnt = btf_proto_slots(meta.btf, meta.func_proto);
cs->fastcall = meta.kfunc_flags & KF_FASTCALL;
cs->is_void = btf_type_is_void(btf_type_by_id(meta.btf, meta.func_proto->type));
return true;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 04/12] bpf: Check subprog scalar arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (2 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 03/12] bpf: Build argument prototypes for subprog calls Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 22:01 ` bot+bpf-ci
2026-09-25 21:12 ` [PATCH bpf-next v1 05/12] bpf: Check global subprog untrusted " Amery Hung
` (7 subsequent siblings)
11 siblings, 1 reply; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
BPF subprogram scalar arguments are checked separately even though the
common argument checker enforces the same SCALAR_VALUE requirement.
Start the migration by routing the scalar branch through check_func_arg()
and update the verifier-log expectation.
Use the BTF parameter index for metadata and the ABI slot index for
register lookup. Route extra slots of by-value aggregates through
check_arg_extra_slot() so every occupied slot follows the common path.
Following patches can extend this guarded common-check branch as each
remaining BPF-subprogram-specific implementation is removed.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 19 ++++++-------------
.../selftests/bpf/progs/aggregate_arg_func.c | 2 +-
2 files changed, 7 insertions(+), 14 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 65a1245e4edb..a84143285b3f 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10871,10 +10871,9 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
nslots = btf_arg_slots(t);
if (arg_type == ARG_SCALAR) {
- if (reg->type != SCALAR_VALUE) {
- bpf_log(log, "%s is not a scalar\n", reg_arg_name(env, argno));
- return -EINVAL;
- }
+ ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
+ if (ret)
+ return ret;
} else if (arg_type & PTR_UNTRUSTED) {
/*
* Anything is allowed for untrusted arguments, as these are
@@ -10964,15 +10963,9 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
}
for (k = 1; k < nslots; k++) {
- argno_t extra_argno = argno_from_arg(slot + k + 1);
- struct bpf_reg_state *extra_reg;
-
- extra_reg = get_func_arg_reg(caller, regs, slot + k);
- if (extra_reg->type != SCALAR_VALUE) {
- bpf_log(log, "%s is not a scalar\n",
- reg_arg_name(env, extra_argno));
- return -EINVAL;
- }
+ ret = check_arg_extra_slot(env, caller, slot + k, &meta);
+ if (ret)
+ return ret;
}
}
diff --git a/tools/testing/selftests/bpf/progs/aggregate_arg_func.c b/tools/testing/selftests/bpf/progs/aggregate_arg_func.c
index d0a4f84a6fbf..2132b926a8f0 100644
--- a/tools/testing/selftests/bpf/progs/aggregate_arg_func.c
+++ b/tools/testing/selftests/bpf/progs/aggregate_arg_func.c
@@ -43,7 +43,7 @@ __naked int aggregate_arg_pair_asm(void)
}
SEC("tc")
-__failure __msg("R2 is not a scalar")
+__failure __msg("R2 type=fp expected=scalar")
__naked int aggregate_arg_pair_ptr_fail(void)
{
asm volatile (
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 05/12] bpf: Check global subprog untrusted arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (3 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 04/12] bpf: Check subprog scalar arguments in the common path Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 06/12] bpf: Check subprog context " Amery Hung
` (6 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Global subprogram arguments tagged __arg_untrusted deliberately
accept any caller value. The callee is verified separately with a
read-only untrusted pointer, whose accesses use protected probe-read
instructions.
Keep the untrusted type in the canonical bpf_subprog_info used to
prepare the callee state. Represent it as ARG_IGNORE only in the
temporary bpf_func_proto used for call-site checking, then extend the
guarded check_func_arg() path to cover it.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a84143285b3f..7932d81a5aa9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10805,9 +10805,18 @@ static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const stru
memset(proto, 0, sizeof(*proto));
for (arg = 0; arg < btf_type_vlen(func_proto); arg++) {
+ enum bpf_arg_type arg_type = sub->args[slot].arg_type;
const struct btf_type *t;
- proto->arg_type[arg] = sub->args[slot].arg_type;
+ if (arg_type & PTR_UNTRUSTED) {
+ /*
+ * An __arg_untrusted argument accepts any caller value. The
+ * callee treats it as read-only and uses probe-read instructions
+ * to protect against invalid memory access.
+ */
+ arg_type = ARG_IGNORE;
+ }
+ proto->arg_type[arg] = arg_type;
t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
slot += btf_arg_slots(t);
}
@@ -10870,16 +10879,10 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
nslots = btf_arg_slots(t);
- if (arg_type == ARG_SCALAR) {
+ if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE) {
ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
if (ret)
return ret;
- } else if (arg_type & PTR_UNTRUSTED) {
- /*
- * Anything is allowed for untrusted arguments, as these are
- * read-only and probe read instructions would protect against
- * invalid memory access.
- */
} else if (arg_type == ARG_PTR_TO_CTX) {
ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_CTX);
if (ret < 0)
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 06/12] bpf: Check subprog context arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (4 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 05/12] bpf: Check global subprog untrusted " Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 07/12] bpf: Check subprog arena " Amery Hung
` (5 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Subprog and helper/kfunc context arguments require PTR_TO_CTX with an
acceptable offset. Route subprog context arguments through
check_func_arg() and update the verifier-log expectation.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 15 ++-------------
.../selftests/bpf/progs/test_global_func5.c | 2 +-
2 files changed, 3 insertions(+), 14 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 7932d81a5aa9..a7b245e90f33 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10879,22 +10879,11 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
nslots = btf_arg_slots(t);
- if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE) {
+ if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE ||
+ arg_type == ARG_PTR_TO_CTX) {
ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
if (ret)
return ret;
- } else if (arg_type == ARG_PTR_TO_CTX) {
- ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_CTX);
- if (ret < 0)
- return ret;
- /* If function expects ctx type in BTF check that caller
- * is passing PTR_TO_CTX.
- */
- if (reg->type != PTR_TO_CTX) {
- bpf_log(log, "%s expects pointer to ctx\n",
- reg_arg_name(env, argno));
- return -EINVAL;
- }
} else if (base_type(arg_type) == ARG_PTR_TO_MEM) {
ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_MEM);
if (ret < 0)
diff --git a/tools/testing/selftests/bpf/progs/test_global_func5.c b/tools/testing/selftests/bpf/progs/test_global_func5.c
index 257c0569ff98..4573ecd96aa0 100644
--- a/tools/testing/selftests/bpf/progs/test_global_func5.c
+++ b/tools/testing/selftests/bpf/progs/test_global_func5.c
@@ -26,7 +26,7 @@ int f3(int val, struct __sk_buff *skb)
}
SEC("tc")
-__failure __msg("expects pointer to ctx")
+__failure __msg("R2 type=fp expected=ctx")
int global_func5(struct __sk_buff *skb)
{
return f1(skb) + f2(2, skb) + f3(3, skb);
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 07/12] bpf: Check subprog arena arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (5 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 06/12] bpf: Check subprog context " Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 08/12] bpf: Check subprog dynptr " Amery Hung
` (4 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
ARG_PTR_TO_ARENA accepts both arena pointers and scalars. The existing
BPF-subprogram contract includes a constant-zero scalar; it is an arena
address value rather than a conventional pointer rejected by
nullability checking.
Kfunc prototype generation already records this zero acceptance with
PTR_MAYBE_NULL for both arena suffixes; separate JIT metadata determines
whether the kernel callee receives the arena base or NULL. Apply the same
call-site representation to the temporary BPF-subprogram prototype, then
extend the guarded check_func_arg() path to cover arena arguments.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 18 ++++--------------
1 file changed, 4 insertions(+), 14 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a7b245e90f33..96749d9b3e26 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10815,6 +10815,8 @@ static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const stru
* to protect against invalid memory access.
*/
arg_type = ARG_IGNORE;
+ } else if (base_type(arg_type) == ARG_PTR_TO_ARENA) {
+ arg_type |= PTR_MAYBE_NULL;
}
proto->arg_type[arg] = arg_type;
t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
@@ -10880,7 +10882,8 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
nslots = btf_arg_slots(t);
if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE ||
- arg_type == ARG_PTR_TO_CTX) {
+ arg_type == ARG_PTR_TO_CTX ||
+ base_type(arg_type) == ARG_PTR_TO_ARENA) {
ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
if (ret)
return ret;
@@ -10908,19 +10911,6 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (base_type(arg_type) == ARG_PTR_TO_ARENA) {
- /*
- * Can pass any value and the kernel won't crash, but
- * only PTR_TO_ARENA or SCALAR make sense. Everything
- * else is a bug in the bpf program. Point it out to
- * the user at the verification time instead of
- * run-time debug nightmare.
- */
- if (reg->type != PTR_TO_ARENA && reg->type != SCALAR_VALUE) {
- bpf_log(log, "%s is not a pointer to arena or scalar.\n",
- reg_arg_name(env, argno));
- return -EINVAL;
- }
} else if (arg_type == ARG_PTR_TO_DYNPTR) {
ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_DYNPTR);
if (ret)
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 08/12] bpf: Check subprog dynptr arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (6 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 07/12] bpf: Check subprog arena " Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:26 ` sashiko-bot
2026-09-25 21:12 ` [PATCH bpf-next v1 09/12] bpf: Check global subprog BTF-ID " Amery Hung
` (3 subsequent siblings)
11 siblings, 1 reply; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Subprog and helper/kfunc dynptr arguments ultimately use the same
process_dynptr_func() validation. Route the subprog dynptr branch
through check_func_arg() so register type, offset, and dynptr state
are checked in the common order.
The existing wrong-register-type test passed a NULL local to a callee
that did not use the argument. Clang left the context pointer in R1,
while GCC materialized zero. The common checker rejected the values at
different stages and emitted different diagnostics.
Obtain a PTR_TO_BTF_ID from bpf_get_current_task_btf() and keep its
callee argument live. This makes both compilers exercise the intended
register-type mismatch.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 11 +----------
tools/testing/selftests/bpf/progs/dynptr_fail.c | 11 +++++------
2 files changed, 6 insertions(+), 16 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 96749d9b3e26..4a5c682ee73c 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10882,7 +10882,7 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
nslots = btf_arg_slots(t);
if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE ||
- arg_type == ARG_PTR_TO_CTX ||
+ arg_type == ARG_PTR_TO_CTX || arg_type == ARG_PTR_TO_DYNPTR ||
base_type(arg_type) == ARG_PTR_TO_ARENA) {
ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
if (ret)
@@ -10911,15 +10911,6 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (arg_type == ARG_PTR_TO_DYNPTR) {
- ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_DYNPTR);
- if (ret)
- return ret;
-
- ret = process_dynptr_func(env, reg, argno, env->insn_idx,
- arg_type, &meta);
- if (ret)
- return ret;
} else if (base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
int err;
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 9418dfe4d7b7..148cf4417322 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -2053,19 +2053,18 @@ __noinline long global_call_bpf_dynptr(const struct bpf_dynptr *dynptr)
/* Avoid leaving this global function empty to avoid having the compiler
* optimize away the call to this global function.
*/
+ __sink(dynptr);
__sink(ret);
return ret;
}
SEC("?raw_tp")
-__failure __msg("R1 expected pointer to stack or const struct bpf_dynptr")
+__failure __msg("R1 type=trusted_ptr_ expected=fp, dynptr_ptr")
int test_dynptr_reg_type(void *ctx)
{
- struct task_struct *current = NULL;
- /* R1 should be holding a PTR_TO_BTF_ID, so this shouldn't be a
- * reg->type that can be passed to a function accepting a
- * ARG_PTR_TO_DYNPTR | MEM_RDONLY. process_dynptr_func() should catch
- * this.
+ struct task_struct *current = bpf_get_current_task_btf();
+ /* R1 holds a PTR_TO_BTF_ID, which cannot be passed to a function
+ * accepting ARG_PTR_TO_DYNPTR | MEM_RDONLY.
*/
global_call_bpf_dynptr((const struct bpf_dynptr *)current);
return 0;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 09/12] bpf: Check global subprog BTF-ID arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (7 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 08/12] bpf: Check subprog dynptr " Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 10/12] bpf: Track packet changes in call metadata Amery Hung
` (2 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Route global ARG_PTR_TO_BTF_ID arguments through check_func_arg(). Use
vmlinux BTF for their type match and retain the global-subprogram rules
instead of applying kfunc-only trusted-pointer validation or runtime type
resolution.
The common nullability check now rejects non-nullable global BTF-ID
arguments before register-type matching. Update the corresponding
verifier log expectations.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 31 +++++--------------
.../bpf/progs/verifier_global_ptr_args.c | 4 +--
2 files changed, 10 insertions(+), 25 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 4a5c682ee73c..73a1c4877427 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -9512,7 +9512,7 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
case ARG_PTR_TO_BTF_ID_SOCK_COMMON:
{
const u32 *arg_btf_id = fn->arg_btf_id[arg];
- const struct btf *arg_btf = meta->btf ?: btf_vmlinux;
+ const struct btf *arg_btf = is_kfunc(meta) ? meta->btf : btf_vmlinux;
if (!meta->btf) {
const struct bpf_reg_types *compatible;
@@ -9540,8 +9540,8 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
}
}
- if (meta->btf && (!is_trusted_reg(env, reg) ||
- bpf_type_has_unsafe_modifiers(reg->type))) {
+ if (is_kfunc(meta) && (!is_trusted_reg(env, reg) ||
+ bpf_type_has_unsafe_modifiers(reg->type))) {
if (!(arg_type & MEM_RCU)) {
const char *actual_type, *arg_name, *expected_type;
@@ -10817,6 +10817,8 @@ static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const stru
arg_type = ARG_IGNORE;
} else if (base_type(arg_type) == ARG_PTR_TO_ARENA) {
arg_type |= PTR_MAYBE_NULL;
+ } else if (base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
+ proto->arg_btf_id[arg] = &sub->args[slot].btf_id;
}
proto->arg_type[arg] = arg_type;
t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
@@ -10883,7 +10885,8 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE ||
arg_type == ARG_PTR_TO_CTX || arg_type == ARG_PTR_TO_DYNPTR ||
- base_type(arg_type) == ARG_PTR_TO_ARENA) {
+ base_type(arg_type) == ARG_PTR_TO_ARENA ||
+ base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
if (ret)
return ret;
@@ -10911,24 +10914,6 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
reg_arg_name(env, argno));
return -EINVAL;
}
- } else if (base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
- int err;
-
- if (bpf_register_is_null(reg) && type_may_be_null(arg_type)) {
- err = mark_arg_precision(env, argno);
- if (err)
- return err;
- continue;
- }
-
- err = check_reg_type(env, reg, argno, arg_type, &meta);
- err = err ?: check_func_arg_reg_off(env, reg, argno, arg_type);
- if (!err && base_type(reg->type) == PTR_TO_BTF_ID)
- err = process_arg_ptr_to_btf_id(env, reg, argno, arg_type,
- btf_vmlinux, sub->args[slot].btf_id,
- &meta, env->insn_idx);
- if (err)
- return err;
} else {
verifier_bug(env, "unrecognized %s type %d",
reg_arg_name(env, argno), arg_type);
@@ -13073,7 +13058,7 @@ static int resolve_func_arg_type(struct bpf_verifier_env *env,
if (base_type(*arg_type) != ARG_PTR_TO_BTF_ID)
return 0;
- if (!meta->btf || arg_type_is_release(*arg_type) ||
+ if (!is_kfunc(meta) || arg_type_is_release(*arg_type) ||
base_type(reg->type) == PTR_TO_BTF_ID ||
reg2btf_ids[base_type(reg->type)])
return 0;
diff --git a/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c b/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c
index 10019af5eb74..f639e2767e35 100644
--- a/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c
+++ b/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c
@@ -87,7 +87,7 @@ __weak int subprog_trusted_task_nonnull(struct task_struct *task __arg_trusted)
SEC("?kprobe")
__failure __log_level(2)
-__msg("R1 type=scalar expected=ptr_, trusted_ptr_, rcu_ptr_")
+__msg("Possibly NULL pointer passed to trusted R1")
__msg("Caller passes invalid args into func#1 ('subprog_trusted_task_nonnull')")
int trusted_task_arg_nonnull_fail1(void *ctx)
{
@@ -96,7 +96,7 @@ int trusted_task_arg_nonnull_fail1(void *ctx)
SEC("?tp_btf/task_newtask")
__failure __log_level(2)
-__msg("R1 type=trusted_ptr_or_null_ expected=ptr_, trusted_ptr_, rcu_ptr_")
+__msg("Possibly NULL pointer passed to trusted R1")
__msg("Caller passes invalid args into func#1 ('subprog_trusted_task_nonnull')")
int trusted_task_arg_nonnull_fail2(void *ctx)
{
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 10/12] bpf: Track packet changes in call metadata
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (8 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 09/12] bpf: Check global subprog BTF-ID " Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-26 8:49 ` Alexei Starovoitov
2026-09-25 21:12 ` [PATCH bpf-next v1 11/12] bpf: Check global subprog memory arguments in the common path Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 12/12] bpf: Check all subprog " Amery Hung
11 siblings, 1 reply; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Helper, kfunc, and global subprog calls each determine whether a call
can invalidate packet pointers, but carry the result through
call-specific variables or look it up again where packet state is
cleared.
Record the effect in bpf_call_arg_meta. Initialize it from the helper
or kfunc identity and from the subprog propagated changes_pkt_data
flag. Keep the dynptr helper backing-type refinement and tail-call
handling on the common field, then have all three call paths clear
packet pointers from it.
No functional change is intended.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
include/linux/bpf_verifier.h | 1 +
kernel/bpf/verifier.c | 54 +++++++++++++++++++-----------------
2 files changed, 30 insertions(+), 25 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 144da990ee8c..590ac30a5691 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1640,6 +1640,7 @@ struct bpf_call_arg_meta {
u32 func_id;
const struct bpf_func_proto *fn;
const struct btf_type *func_proto;
+ bool pkt_changed;
u8 release_regno;
u32 ret_btf_id;
u32 subprogno;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 73a1c4877427..212cada61aa6 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10827,22 +10827,22 @@ static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const stru
}
static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
- struct btf *btf,
- struct bpf_reg_state *regs)
+ struct btf *btf, struct bpf_reg_state *regs,
+ struct bpf_call_arg_meta *meta)
{
struct bpf_subprog_info *sub = subprog_info(env, subprog);
struct bpf_func_state *caller = cur_func(env);
struct bpf_verifier_log *log = &env->log;
const struct btf_param *args, *stack_args;
const struct btf_type *func, *func_proto;
- struct bpf_call_arg_meta meta;
struct bpf_func_proto *fn;
u32 arg, slot, nslots;
int ret, err;
- memset(&meta, 0, sizeof(meta));
- meta.btf = btf;
- meta.func_name = bpf_subprog_name(env, subprog);
+ memset(meta, 0, sizeof(*meta));
+ meta->btf = btf;
+ meta->pkt_changed = sub->changes_pkt_data;
+ meta->func_name = bpf_subprog_name(env, subprog);
ret = btf_prepare_func_args(env, subprog);
if (ret) {
@@ -10867,8 +10867,8 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
fn = &env->bpf_subprog_scratch;
gen_subprog_arg_proto(sub, btf, func_proto, fn);
- meta.fn = fn;
- meta.func_proto = func_proto;
+ meta->fn = fn;
+ meta->func_proto = func_proto;
/* check that BTF function arguments match actual types that the
* verifier sees.
@@ -10887,7 +10887,7 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
arg_type == ARG_PTR_TO_CTX || arg_type == ARG_PTR_TO_DYNPTR ||
base_type(arg_type) == ARG_PTR_TO_ARENA ||
base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
- ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
+ ret = check_func_arg(env, arg, slot, 0, meta, env->insn_idx);
if (ret)
return ret;
} else if (base_type(arg_type) == ARG_PTR_TO_MEM) {
@@ -10921,7 +10921,7 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
}
for (k = 1; k < nslots; k++) {
- ret = check_arg_extra_slot(env, caller, slot + k, &meta);
+ ret = check_arg_extra_slot(env, caller, slot + k, meta);
if (ret)
return ret;
}
@@ -10938,7 +10938,8 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
* Only PTR_TO_CTX and SCALAR_VALUE states are recognized.
*/
static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog,
- struct bpf_reg_state *regs)
+ struct bpf_reg_state *regs,
+ struct bpf_call_arg_meta *meta)
{
struct bpf_prog *prog = env->prog;
struct btf *btf = prog->aux->btf;
@@ -10955,7 +10956,7 @@ static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog,
if (prog->aux->func_info_aux[subprog].unreliable)
return -EINVAL;
- err = btf_check_func_arg_match(env, subprog, btf, regs);
+ err = btf_check_func_arg_match(env, subprog, btf, regs, meta);
/* Compiler optimizations can remove arguments from static functions
* or mismatched type can be passed into a global function.
* In such cases mark the function as unreliable from BTF point of view.
@@ -10970,11 +10971,12 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
set_callee_state_fn set_callee_state_cb)
{
struct bpf_verifier_state *state = env->cur_state, *callback_state;
+ struct bpf_call_arg_meta meta;
struct bpf_func_state *caller, *callee;
int err;
caller = state->frame[state->curframe];
- err = btf_check_subprog_call(env, subprog, caller->regs);
+ err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
if (err == -EFAULT)
return err;
@@ -11100,6 +11102,7 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
int *insn_idx)
{
struct bpf_verifier_state *state = env->cur_state;
+ struct bpf_call_arg_meta meta;
struct bpf_func_state *caller;
int err, subprog, target_insn;
u32 i, nregs;
@@ -11111,7 +11114,7 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
return -EFAULT;
caller = state->frame[state->curframe];
- err = btf_check_subprog_call(env, subprog, caller->regs);
+ err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
if (err == -EFAULT)
return err;
if (bpf_subprog_is_global(env, subprog)) {
@@ -11154,7 +11157,7 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
subprog, sub_name);
sub_aux->called[in_sleepable_context(env)] = true;
returns_void = subprog_returns_void(env, subprog);
- if (env->subprog_info[subprog].changes_pkt_data)
+ if (meta.pkt_changed)
clear_all_pkt_pointers(env);
if (returns_void)
bpf_diag_record_scrub(env, &caller->regs[BPF_REG_0], BPF_DIAG_MOD_CALLER_SAVED);
@@ -11212,6 +11215,7 @@ static int check_func_callx(struct bpf_verifier_env *env, struct bpf_insn *insn,
int *insn_idx)
{
struct bpf_func_state *caller = cur_func(env);
+ struct bpf_call_arg_meta meta;
struct bpf_reg_state *reg;
const char *reason;
int err, subprog;
@@ -11247,7 +11251,7 @@ static int check_func_callx(struct bpf_verifier_env *env, struct bpf_insn *insn,
/* PTR_TO_FUNC is a pointer to a static subprog */
subprog = reg->subprogno;
- err = btf_check_subprog_call(env, subprog, caller->regs);
+ err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
if (err == -EFAULT)
return err;
@@ -12111,7 +12115,6 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
struct bpf_call_arg_meta meta;
const char *operation;
int insn_idx = *insn_idx_p;
- bool changes_data;
int i, err, func_id;
/* find function prototype */
@@ -12153,15 +12156,15 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
return -EINVAL;
}
+ memset(&meta, 0, sizeof(meta));
+ meta.pkt_changed = bpf_helper_changes_pkt_data(func_id);
+
/* With LD_ABS/IND some JITs save/restore skb from r1. */
- changes_data = bpf_helper_changes_pkt_data(func_id);
- if (changes_data && fn->arg1_type != ARG_PTR_TO_CTX) {
+ if (meta.pkt_changed && fn->arg1_type != ARG_PTR_TO_CTX) {
verifier_bug(env, "func %s#%d: r1 != ctx", func_id_name(func_id), func_id);
return -EFAULT;
}
- memset(&meta, 0, sizeof(meta));
-
err = check_func_proto(env, fn, &meta);
if (err) {
verifier_bug(env, "incorrect func proto %s#%d", func_id_name(func_id), func_id);
@@ -12332,7 +12335,7 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
/* this will trigger clear_all_pkt_pointers(), which will
* invalidate all dynptr slices associated with the skb
*/
- changes_data = true;
+ meta.pkt_changed = true;
break;
}
@@ -12617,11 +12620,11 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
return err;
env->insn_idx--;
} else {
- changes_data = false;
+ meta.pkt_changed = false;
}
}
- if (changes_data)
+ if (meta.pkt_changed)
clear_all_pkt_pointers(env);
return 0;
}
@@ -14753,6 +14756,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
}
if (err)
return err;
+ meta.pkt_changed = bpf_is_kfunc_pkt_changing(&meta);
desc_btf = meta.btf;
func_name = meta.func_name;
insn_aux = &env->insn_aux_data[insn_idx];
@@ -15180,7 +15184,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
}
}
- if (bpf_is_kfunc_pkt_changing(&meta))
+ if (meta.pkt_changed)
clear_all_pkt_pointers(env);
proto_slots = kfunc_abi_slots(&desc->func_model);
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 11/12] bpf: Check global subprog memory arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (9 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 10/12] bpf: Track packet changes in call metadata Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 12/12] bpf: Check all subprog " Amery Hung
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
Route fixed-size global ARG_PTR_TO_MEM arguments through
check_func_arg(). Preserve their read-write access check, nullable
contract, and support for BTF-defined allocated memory.
Recognize subprog calls explicitly in the common checker. Global
subprog stack liveness can prove that bytes in an argument are unused
by the callee. Retain the existing allowance for those poisoned stack
bytes when call metadata is present, and update the nullability log
expectation.
The verifier also rejects packet pointers when the callee may change
packet data, because the callee sees PTR_TO_MEM and cannot invalidate
packet bounds. Use pkt_changed from common call metadata to retain this
rule in the common fixed-memory path.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 57 +++++++++----------
.../bpf/progs/verifier_global_ptr_args.c | 3 +-
.../bpf/progs/verifier_global_subprogs.c | 2 +-
3 files changed, 29 insertions(+), 33 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 212cada61aa6..97c125850c7a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -7702,6 +7702,11 @@ static int check_mem_size_reg(struct bpf_verifier_env *env,
return err;
}
+static bool is_subprog(const struct bpf_call_arg_meta *meta)
+{
+ return meta->btf && !meta->func_id;
+}
+
static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
argno_t argno, u32 mem_size, enum bpf_access_type access_type,
struct bpf_call_arg_meta *meta, bool *known_memory)
@@ -7720,10 +7725,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
}
/*
- * Only a global subprog (meta == NULL) may read poisoned stack slots:
+ * Only a global subprog may read poisoned stack slots:
* its static stack liveness proved the callee body skips them.
*/
- size = (!meta && base_type(reg->type) == PTR_TO_STACK) ? -(int)mem_size : mem_size;
+ size = (is_subprog(meta) &&
+ base_type(reg->type) == PTR_TO_STACK) ? -(int)mem_size : mem_size;
if (access_type & BPF_READ)
err = check_helper_mem_access(env, reg, argno, size, BPF_READ, true, meta,
@@ -9047,8 +9053,8 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
type &= ~PTR_MAYBE_NULL;
if (base_type(arg_type) == ARG_PTR_TO_MEM)
type &= ~DYNPTR_TYPE_FLAG_MASK;
- /* Allow allocated memory for kfunc ARG_PTR_TO_MEM but not helper. */
- if (is_kfunc(meta) && base_type(arg_type) == ARG_PTR_TO_MEM &&
+ /* Allow allocated memory for BTF-defined ARG_PTR_TO_MEM but not helpers. */
+ if (!is_helper(meta) && base_type(arg_type) == ARG_PTR_TO_MEM &&
type_is_ptr_alloc_obj(type))
type = PTR_TO_MEM;
@@ -9678,6 +9684,17 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
bpf_diag_reg_type_plain(env, reg->type));
return err;
}
+ /*
+ * PTR_TO_PACKET gets passed as PTR_TO_MEM, preventing us from adjusting
+ * bounds tracking information.
+ */
+ if (is_subprog(meta) && meta->pkt_changed &&
+ (reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg))) {
+ verbose(env,
+ "cannot pass packet pointer %s to %s(): function may change packet data\n",
+ reg_arg_name(env, argno), meta->func_name);
+ return -EINVAL;
+ }
if (arg_type & MEM_ALIGNED)
err = check_ptr_alignment(env, reg, 0, arg_size, true);
break;
@@ -10817,6 +10834,9 @@ static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const stru
arg_type = ARG_IGNORE;
} else if (base_type(arg_type) == ARG_PTR_TO_ARENA) {
arg_type |= PTR_MAYBE_NULL;
+ } else if (base_type(arg_type) == ARG_PTR_TO_MEM) {
+ proto->arg_size[arg] = sub->args[slot].mem_size;
+ arg_type |= MEM_FIXED_SIZE | MEM_WRITE;
} else if (base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
proto->arg_btf_id[arg] = &sub->args[slot].btf_id;
}
@@ -10832,7 +10852,6 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
{
struct bpf_subprog_info *sub = subprog_info(env, subprog);
struct bpf_func_state *caller = cur_func(env);
- struct bpf_verifier_log *log = &env->log;
const struct btf_param *args, *stack_args;
const struct btf_type *func, *func_proto;
struct bpf_func_proto *fn;
@@ -10874,7 +10893,6 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
* verifier sees.
*/
for (arg = 0, slot = 0; arg < btf_type_vlen(func_proto); arg++, slot += nslots) {
- struct bpf_reg_state *reg = get_func_arg_reg(caller, regs, slot);
enum bpf_arg_type arg_type = fn->arg_type[arg];
argno_t argno = argno_from_arg(slot + 1);
const struct btf_type *t;
@@ -10886,34 +10904,11 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE ||
arg_type == ARG_PTR_TO_CTX || arg_type == ARG_PTR_TO_DYNPTR ||
base_type(arg_type) == ARG_PTR_TO_ARENA ||
- base_type(arg_type) == ARG_PTR_TO_BTF_ID) {
+ base_type(arg_type) == ARG_PTR_TO_BTF_ID ||
+ base_type(arg_type) == ARG_PTR_TO_MEM) {
ret = check_func_arg(env, arg, slot, 0, meta, env->insn_idx);
if (ret)
return ret;
- } else if (base_type(arg_type) == ARG_PTR_TO_MEM) {
- ret = check_func_arg_reg_off(env, reg, argno, ARG_PTR_TO_MEM);
- if (ret < 0)
- return ret;
- if (check_mem_reg(env, reg, argno, sub->args[slot].mem_size,
- BPF_READ | BPF_WRITE, NULL,
- NULL))
- return -EINVAL;
- /*
- * PTR_TO_PACKET get passed as PTR_TO_MEM, preventing
- * us from adjusting bounds tracking info.
- */
- if ((reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg)) &&
- sub->changes_pkt_data) {
- bpf_log(log, "%s is a packet pointer, but func#%d may change packet data\n",
- reg_arg_name(env, argno), subprog);
- return -EINVAL;
- }
- if (!(arg_type & PTR_MAYBE_NULL) &&
- (type_may_be_null(reg->type) || bpf_register_is_null(reg))) {
- bpf_log(log, "%s is expected to be non-NULL\n",
- reg_arg_name(env, argno));
- return -EINVAL;
- }
} else {
verifier_bug(env, "unrecognized %s type %d",
reg_arg_name(env, argno), arg_type);
diff --git a/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c b/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c
index f639e2767e35..03507eeae3cb 100644
--- a/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c
+++ b/tools/testing/selftests/bpf/progs/verifier_global_ptr_args.c
@@ -389,7 +389,8 @@ __weak int subprog_pkt_ptr_changes_data(struct __sk_buff *skb __arg_ctx,
SEC("?tc")
__failure __log_level(2)
-__msg("R2 is a packet pointer, but func#{{[0-9]+}} may change packet data")
+__msg("cannot pass packet pointer R2")
+__msg("function may change packet data")
__msg("Caller passes invalid args into func#{{[0-9]+}} ('subprog_pkt_ptr_changes_data')")
int pkt_ptr_to_global_mem_arg_changes_data(struct __sk_buff *skb)
{
diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
index 27fbe54e8795..574b26b5a7df 100644
--- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
+++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
@@ -195,7 +195,7 @@ int arg_tag_nonnull_ptr_good(void *ctx)
SEC("?raw_tp")
__failure __log_level(2)
-__msg("R1 is expected to be non-NULL")
+__msg("Possibly NULL pointer passed to trusted R1")
int arg_tag_nonnull_ptr_null_bad(void *ctx)
{
int y = 74;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH bpf-next v1 12/12] bpf: Check all subprog arguments in the common path
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
` (10 preceding siblings ...)
2026-09-25 21:12 ` [PATCH bpf-next v1 11/12] bpf: Check global subprog memory arguments in the common path Amery Hung
@ 2026-09-25 21:12 ` Amery Hung
11 siblings, 0 replies; 17+ messages in thread
From: Amery Hung @ 2026-09-25 21:12 UTC (permalink / raw)
To: bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team
All supported BPF-subprogram argument types now pass through
check_func_arg() from one guarded branch in the legacy validation loop.
Replace that loop and its outgoing-stack validation with
check_func_args().
The scratch prototype is indexed by BTF parameter and the call metadata
carries the BTF function prototype. The existing BTF argument iteration
therefore maps parameters to ABI slots for both kfunc and BPF subprogram
calls, including additional slots occupied by by-value aggregates.
The common checker can return non-EFAULT errors other than -EINVAL, as
the existing BTF-ID path already did. This is compatible with subprog
call handling: only -EFAULT is fatal. Any other error marks BTF
unreliable. Static subprogs can then fall back to inline verification,
while global subprogs reject the call.
Remove the caller-register plumbing that the dedicated loop required.
Signed-off-by: Amery Hung <ameryhung@gmail.com>
---
kernel/bpf/verifier.c | 62 +++++++------------------------------------
1 file changed, 10 insertions(+), 52 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 97c125850c7a..d6a94dc5e644 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10846,16 +10846,13 @@ static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const stru
}
}
-static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
- struct btf *btf, struct bpf_reg_state *regs,
+static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog, struct btf *btf,
struct bpf_call_arg_meta *meta)
{
struct bpf_subprog_info *sub = subprog_info(env, subprog);
struct bpf_func_state *caller = cur_func(env);
- const struct btf_param *args, *stack_args;
const struct btf_type *func, *func_proto;
struct bpf_func_proto *fn;
- u32 arg, slot, nslots;
int ret, err;
memset(meta, 0, sizeof(*meta));
@@ -10877,63 +10874,24 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
func = btf_type_by_id(btf, env->prog->aux->func_info[subprog].type_id);
func_proto = btf_type_by_id(btf, func->type);
- args = btf_params(func_proto);
- stack_args = sub->arg_slot_cnt == btf_type_vlen(func_proto) ? args : NULL;
- ret = check_outgoing_stack_args(env, caller, sub->arg_slot_cnt,
- bpf_subprog_name(env, subprog), btf, stack_args);
- if (ret)
- return ret;
fn = &env->bpf_subprog_scratch;
gen_subprog_arg_proto(sub, btf, func_proto, fn);
meta->fn = fn;
meta->func_proto = func_proto;
- /* check that BTF function arguments match actual types that the
- * verifier sees.
- */
- for (arg = 0, slot = 0; arg < btf_type_vlen(func_proto); arg++, slot += nslots) {
- enum bpf_arg_type arg_type = fn->arg_type[arg];
- argno_t argno = argno_from_arg(slot + 1);
- const struct btf_type *t;
- u32 k;
-
- t = btf_type_skip_modifiers(btf, args[arg].type, NULL);
- nslots = btf_arg_slots(t);
-
- if (arg_type == ARG_SCALAR || arg_type == ARG_IGNORE ||
- arg_type == ARG_PTR_TO_CTX || arg_type == ARG_PTR_TO_DYNPTR ||
- base_type(arg_type) == ARG_PTR_TO_ARENA ||
- base_type(arg_type) == ARG_PTR_TO_BTF_ID ||
- base_type(arg_type) == ARG_PTR_TO_MEM) {
- ret = check_func_arg(env, arg, slot, 0, meta, env->insn_idx);
- if (ret)
- return ret;
- } else {
- verifier_bug(env, "unrecognized %s type %d",
- reg_arg_name(env, argno), arg_type);
- return -EFAULT;
- }
-
- for (k = 1; k < nslots; k++) {
- ret = check_arg_extra_slot(env, caller, slot + k, meta);
- if (ret)
- return ret;
- }
- }
-
- return 0;
+ return check_func_args(env, meta, env->insn_idx);
}
-/* Compare BTF of a function call with given bpf_reg_state.
+/*
+ * Check that call-site argument states match a subprog's BTF signature.
+ *
* Returns:
* EFAULT - there is a verifier bug. Abort verification.
- * EINVAL - there is a type mismatch or BTF is not available.
+ * Other errors - there is a type mismatch or BTF is not available.
* 0 - BTF matches with what bpf_reg_state expects.
- * Only PTR_TO_CTX and SCALAR_VALUE states are recognized.
*/
static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog,
- struct bpf_reg_state *regs,
struct bpf_call_arg_meta *meta)
{
struct bpf_prog *prog = env->prog;
@@ -10951,7 +10909,7 @@ static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog,
if (prog->aux->func_info_aux[subprog].unreliable)
return -EINVAL;
- err = btf_check_func_arg_match(env, subprog, btf, regs, meta);
+ err = btf_check_func_arg_match(env, subprog, btf, meta);
/* Compiler optimizations can remove arguments from static functions
* or mismatched type can be passed into a global function.
* In such cases mark the function as unreliable from BTF point of view.
@@ -10971,7 +10929,7 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
int err;
caller = state->frame[state->curframe];
- err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
+ err = btf_check_subprog_call(env, subprog, &meta);
if (err == -EFAULT)
return err;
@@ -11109,7 +11067,7 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
return -EFAULT;
caller = state->frame[state->curframe];
- err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
+ err = btf_check_subprog_call(env, subprog, &meta);
if (err == -EFAULT)
return err;
if (bpf_subprog_is_global(env, subprog)) {
@@ -11246,7 +11204,7 @@ static int check_func_callx(struct bpf_verifier_env *env, struct bpf_insn *insn,
/* PTR_TO_FUNC is a pointer to a static subprog */
subprog = reg->subprogno;
- err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
+ err = btf_check_subprog_call(env, subprog, &meta);
if (err == -EFAULT)
return err;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* Re: [PATCH bpf-next v1 08/12] bpf: Check subprog dynptr arguments in the common path
2026-09-25 21:12 ` [PATCH bpf-next v1 08/12] bpf: Check subprog dynptr " Amery Hung
@ 2026-09-25 21:26 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-09-25 21:26 UTC (permalink / raw)
To: Amery Hung; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] bpf: multi-line comment violates BPF subsystem formatting style
--
commit 45344ee9671457679fb9b5565dcee5c8036d67b0
Author: Amery Hung <ameryhung@gmail.com>
bpf: Check subprog dynptr arguments in the common path
This commit updates the verifier to check subprog and helper/kfunc
dynptr arguments through the common check_func_arg() path. It also modifies
a selftest to ensure both Clang and GCC exercise the intended register-type
mismatch correctly.
> diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
> index 9418dfe4d7b7f..148cf44173221 100644
> --- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
> +++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
[ ... ]
> SEC("?raw_tp")
> -__failure __msg("R1 expected pointer to stack or const struct bpf_dynptr")
> +__failure __msg("R1 type=trusted_ptr_ expected=fp, dynptr_ptr")
> int test_dynptr_reg_type(void *ctx)
> {
> - struct task_struct *current = NULL;
> - /* R1 should be holding a PTR_TO_BTF_ID, so this shouldn't be a
> - * reg->type that can be passed to a function accepting a
> - * ARG_PTR_TO_DYNPTR | MEM_RDONLY. process_dynptr_func() should catch
> - * this.
> + struct task_struct *current = bpf_get_current_task_btf();
> + /* R1 holds a PTR_TO_BTF_ID, which cannot be passed to a function
> + * accepting ARG_PTR_TO_DYNPTR | MEM_RDONLY.
[Severity: Low]
This isn't a bug, but the BPF subsystem follows the modern kernel
multi-line comment style where the opening /* must be on its own line.
Should this comment block be updated to match the required formatting?
> */
> global_call_bpf_dynptr((const struct bpf_dynptr *)current);
> return 0;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260925211256.1834061-1-ameryhung@gmail.com?part=8
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH bpf-next v1 04/12] bpf: Check subprog scalar arguments in the common path
2026-09-25 21:12 ` [PATCH bpf-next v1 04/12] bpf: Check subprog scalar arguments in the common path Amery Hung
@ 2026-09-25 22:01 ` bot+bpf-ci
0 siblings, 0 replies; 17+ messages in thread
From: bot+bpf-ci @ 2026-09-25 22:01 UTC (permalink / raw)
To: ameryhung, bpf
Cc: alexei.starovoitov, andrii, daniel, eddyz87, memxor, ameryhung,
kernel-team, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4009 bytes --]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 4b683502c959..0f86feefbeb2 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -10872,10 +10872,9 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
> nslots = btf_arg_slots(t);
>
> if (arg_type == ARG_SCALAR) {
> - if (reg->type != SCALAR_VALUE) {
> - bpf_log(log, "%s is not a scalar\n", reg_arg_name(env, argno));
> - return -EINVAL;
> - }
> + ret = check_func_arg(env, arg, slot, 0, &meta, env->insn_idx);
> + if (ret)
> + return ret;
Does this change inadvertently create diagnostics on the tolerated path?
For static subprogs, callx targets and callbacks, btf_check_subprog_call()
failures are tolerated (only -EFAULT is fatal):
err = btf_check_subprog_call(env, subprog, caller->regs);
if (err == -EFAULT)
return err;
The comment in btf_check_subprog_call() explains this is because "Compiler
optimizations can remove arguments from static functions".
Before this patch, such a mismatch only logged the single line "R%d is not
a scalar". Now the same situation runs through code that writes a full
diagnostic straight into the verifier log:
- If the register is NOT_INIT (for example, the compiler dropped an
argument), __check_reg_arg() calls bpf_diag_unreadable_reg(), which calls
bpf_diag_header(env, REGISTER_TYPE_SAFETY, "unreadable register"). That
prints "Verification failed: Register Type Safety: Unreadable register"
and the suggestion "Initialize R%d on every path before this instruction."
- If the register holds a pointer, check_reg_type() calls
bpf_diag_call_arg_fmt() -> bpf_diag_call_type(), which prints
"Verification failed: Call Type Safety: Invalid call argument".
Nothing suppresses the output on the tolerated path, so verification
carries on after the log has already reported a failure.
Example: static __noinline int sub(int x) called with r1 = r10, or called
after clang removed the argument so that R1 is NOT_INIT. At log_level 2 a
program that loads successfully has this block in its log. At log_level 1,
the level libbpf uses when it reloads a failed program to fetch the log,
pop_stack() only rewinds the log to the push position of the branch being
resumed. So a block printed by such a call on the path that finally fails
(for example before its first branch) stays in the log ahead of the real
error. A user reading the first "Verification failed:" block is sent to a
harmless call site, with advice to initialize a register the verifier
deliberately does not require.
The ctx, dynptr and BTF-ID branches could already print diagnostics in this
tolerated path. This patch adds the same behaviour for scalars, the most
common static-subprog argument type and the exact case the tolerance was
written for.
Should diagnostics be muted, or the log rewound, when
btf_check_subprog_call() fails for a non-global subprog?
> } else if (arg_type & PTR_UNTRUSTED) {
> /*
> * Anything is allowed for untrusted arguments, as these are
[ ... ]
> @@ -10965,15 +10964,9 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
> }
>
> for (k = 1; k < nslots; k++) {
> - argno_t extra_argno = argno_from_arg(slot + k + 1);
> - struct bpf_reg_state *extra_reg;
> -
> - extra_reg = get_func_arg_reg(caller, regs, slot + k);
> - if (extra_reg->type != SCALAR_VALUE) {
> - bpf_log(log, "%s is not a scalar\n",
> - reg_arg_name(env, extra_argno));
> - return -EINVAL;
> - }
> + ret = check_arg_extra_slot(env, caller, slot + k, &meta);
> + if (ret)
> + return ret;
This extra-slot routing through check_arg_extra_slot() has the same
diagnostic behaviour as the scalar branch above.
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/36191749180
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments
2026-09-25 21:12 ` [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments Amery Hung
@ 2026-09-26 8:48 ` Alexei Starovoitov
0 siblings, 0 replies; 17+ messages in thread
From: Alexei Starovoitov @ 2026-09-26 8:48 UTC (permalink / raw)
To: Amery Hung, bpf; +Cc: andrii, daniel, eddyz87, memxor, kernel-team
On Fri, Sep 25, 2026 at 02:12 PM Amery Hung <ameryhung@gmail.com> wrote:
> - arg_btf_id = btf_params(meta->func_proto)[arg_idx_from_argno(argno)].type;
> + arg_btf_id = btf_params(meta->func_proto)[arg].type;
check_func_arg_release() and process_iter_arg() index the BTF params
with arg_idx_from_argno(argno) too.
For kfunc(struct two_u64 v, struct foo *p) 'p' is param 1 in slot 2.
check_func_arg_release() reads btf_params()[2] past the end of
the proto and process_iter_arg() passes 2 to btf_check_iter_arg().
Let's fix all three in one go?
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH bpf-next v1 10/12] bpf: Track packet changes in call metadata
2026-09-25 21:12 ` [PATCH bpf-next v1 10/12] bpf: Track packet changes in call metadata Amery Hung
@ 2026-09-26 8:49 ` Alexei Starovoitov
0 siblings, 0 replies; 17+ messages in thread
From: Alexei Starovoitov @ 2026-09-26 8:49 UTC (permalink / raw)
To: Amery Hung, bpf; +Cc: andrii, daniel, eddyz87, memxor, kernel-team
On Fri, Sep 25, 2026 at 02:12 PM Amery Hung <ameryhung@gmail.com> wrote:
> @@ -10970,11 +10971,12 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
> set_callee_state_fn set_callee_state_cb)
> {
> struct bpf_verifier_state *state = env->cur_state, *callback_state;
> + struct bpf_call_arg_meta meta;
> struct bpf_func_state *caller, *callee;
> int err;
>
> caller = state->frame[state->curframe];
> - err = btf_check_subprog_call(env, subprog, caller->regs);
> + err = btf_check_subprog_call(env, subprog, caller->regs, &meta);
Nothing reads this meta. Same in check_func_callx().
The only place that needs pkt_changed in meta is the subprog
pkt pointer check that the next patch adds to check_func_arg().
can btf_check_func_arg_match() keep its local meta and set
meta.pkt_changed = sub->changes_pkt_data there?
Then check_func_call() keeps reading subprog_info[subprog] and
helper and kfunc paths don't need to change.
^ permalink raw reply [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-09-26 8:49 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-25 21:12 [PATCH bpf-next v1 00/12] Unify subprog argument checks Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 01/12] bpf: Fix kfunc nullability diagnostics after wide arguments Amery Hung
2026-09-26 8:48 ` Alexei Starovoitov
2026-09-25 21:12 ` [PATCH bpf-next v1 02/12] bpf: Identify subprog calls in argument metadata Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 03/12] bpf: Build argument prototypes for subprog calls Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 04/12] bpf: Check subprog scalar arguments in the common path Amery Hung
2026-09-25 22:01 ` bot+bpf-ci
2026-09-25 21:12 ` [PATCH bpf-next v1 05/12] bpf: Check global subprog untrusted " Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 06/12] bpf: Check subprog context " Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 07/12] bpf: Check subprog arena " Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 08/12] bpf: Check subprog dynptr " Amery Hung
2026-09-25 21:26 ` sashiko-bot
2026-09-25 21:12 ` [PATCH bpf-next v1 09/12] bpf: Check global subprog BTF-ID " Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 10/12] bpf: Track packet changes in call metadata Amery Hung
2026-09-26 8:49 ` Alexei Starovoitov
2026-09-25 21:12 ` [PATCH bpf-next v1 11/12] bpf: Check global subprog memory arguments in the common path Amery Hung
2026-09-25 21:12 ` [PATCH bpf-next v1 12/12] bpf: Check all subprog " Amery Hung
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).