bpf: Consolidate helper and kfunc PTR_TO_BTF_ID argument matching

Keep check_reg_type() focused on register admission. Helpers currently
match BTF-ID arguments there, while kfuncs use
process_kf_arg_ptr_to_btf_id(). Both paths ultimately call
btf_struct_ids_match().

Introduce process_arg_ptr_to_btf_id() for helpers, kfuncs, and global
subprograms. Callers provide the expected BTF and ID and retain their
call-specific metadata handling. Group the helper compatible-ID, poison,
and bpf_kptr_xchg() handling in a helper-only block in the
ARG_PTR_TO_BTF_ID case, leaving the common matcher outside it.

Derive strict matching from the generated argument type. This limits
KF_RELEASE strictness to the argument marked OBJ_RELEASE while
preserving the bpf_sk_release() exception and kfunc no-cast-alias rule.

Remove the post-admission BTF and nullability switch from
check_reg_type(), leaving it responsible for register admission. The
compatibility tables already limit helper MEM_ALLOC inputs to
ARG_PTR_TO_SPIN_LOCK and ARG_KPTR_XCHG_DEST, while the kptr source is
admitted only for bpf_kptr_xchg(). Drop the redundant helper-ID
whitelist, pointer-offset check, and constant-offset assertion.

The bpf_kptr_xchg() source match now follows offset validation, so a
source within a referenced object reports the release zero-offset error
before the kptr type error. Update the affected selftests and use
call-neutral wording for BTF mismatch diagnostics.

Signed-off-by: Amery Hung <ameryhung@gmail.com>
Link: https://lore.kernel.org/r/20260911220415.1396439-18-ameryhung@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Amery Hung 2026-09-11 15:04:09 -07:00 committed by Alexei Starovoitov
parent a583d56e8d
commit b472f03e31
6 changed files with 94 additions and 143 deletions

View File

@ -8248,6 +8248,10 @@ static int resolve_func_arg_type(struct bpf_verifier_env *env,
struct bpf_reg_state *reg, u32 arg,
struct bpf_call_arg_meta *meta, int insn_idx,
enum bpf_arg_type *arg_type, u32 *arg_size);
static int process_arg_ptr_to_btf_id(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
argno_t argno, enum bpf_arg_type arg_type,
const struct btf *arg_btf, u32 arg_btf_id,
struct bpf_call_arg_meta *meta, int insn_idx);
struct bpf_reg_types {
const enum bpf_reg_type types[10];
@ -8450,13 +8454,12 @@ static const char *bpf_diag_expected_reg_types(struct bpf_verifier_env *env,
}
static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *reg, argno_t argno,
enum bpf_arg_type arg_type, const u32 *arg_btf_id,
struct bpf_call_arg_meta *meta)
enum bpf_arg_type arg_type, struct bpf_call_arg_meta *meta)
{
enum bpf_reg_type expected, type = reg->type;
const struct bpf_reg_types *compatible;
const char *actual, *accepted;
int i, j, err;
int i, j;
compatible = compatible_reg_types[base_type(arg_type)];
if (!compatible) {
@ -8512,90 +8515,6 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
return -EACCES;
found:
if (base_type(reg->type) != PTR_TO_BTF_ID)
return 0;
if (compatible == &mem_types)
return 0;
switch ((int)reg->type) {
case PTR_TO_BTF_ID:
case PTR_TO_BTF_ID | PTR_TRUSTED:
case PTR_TO_BTF_ID | PTR_TRUSTED | PTR_MAYBE_NULL:
case PTR_TO_BTF_ID | MEM_RCU:
case PTR_TO_BTF_ID | PTR_MAYBE_NULL:
case PTR_TO_BTF_ID | PTR_MAYBE_NULL | MEM_RCU:
{
/* For bpf_sk_release, it needs to match against first member
* 'struct sock_common', hence make an exception for it. This
* allows bpf_sk_release to work for multiple socket types.
*/
bool strict_type_match = arg_type_is_release(arg_type) &&
!is_helper_call(meta, BPF_FUNC_sk_release);
if (!arg_btf_id) {
if (!compatible->btf_id) {
verifier_bug(env, "missing arg compatible BTF ID");
return -EFAULT;
}
arg_btf_id = compatible->btf_id;
}
if (is_helper_call(meta, BPF_FUNC_kptr_xchg)) {
if (map_kptr_match_type(env, meta->kptr_field, reg, reg_from_argno(argno)))
return -EACCES;
} else {
if (arg_btf_id == BPF_PTR_POISON) {
verbose(env, "verifier internal error:");
verbose(env, "%s has non-overwritten BPF_PTR_POISON type\n",
reg_arg_name(env, argno));
return -EACCES;
}
err = __check_ptr_off_reg(env, reg, argno, true);
if (err)
return err;
if (!btf_struct_ids_match(&env->log, reg->btf, reg->btf_id,
reg->var_off.value, btf_vmlinux, *arg_btf_id,
strict_type_match, !type_is_alloc(reg->type))) {
verbose(env, "%s is of type %s but %s is expected\n",
reg_arg_name(env, argno),
btf_type_name(reg->btf, reg->btf_id),
btf_type_name(btf_vmlinux, *arg_btf_id));
return -EACCES;
}
}
break;
}
case PTR_TO_BTF_ID | MEM_ALLOC:
case PTR_TO_BTF_ID | MEM_PERCPU | MEM_ALLOC:
case PTR_TO_BTF_ID | MEM_ALLOC | NON_OWN_REF:
case PTR_TO_BTF_ID | MEM_ALLOC | NON_OWN_REF | MEM_RCU:
if (!is_helper_call(meta, BPF_FUNC_spin_lock) &&
!is_helper_call(meta, BPF_FUNC_spin_unlock) &&
!is_helper_call(meta, BPF_FUNC_kptr_xchg)) {
verifier_bug(env, "unimplemented handling of MEM_ALLOC");
return -EFAULT;
}
/* Check if local kptr in src arg matches kptr in dst arg */
if (is_helper_call(meta, BPF_FUNC_kptr_xchg)) {
int regno = reg_from_argno(argno);
if (regno == BPF_REG_2 &&
map_kptr_match_type(env, meta->kptr_field, reg, regno))
return -EACCES;
}
break;
case PTR_TO_BTF_ID | MEM_PERCPU:
case PTR_TO_BTF_ID | MEM_PERCPU | MEM_RCU:
case PTR_TO_BTF_ID | MEM_PERCPU | PTR_TRUSTED:
/* Handled by helper specific checks */
break;
default:
verifier_bug(env, "invalid PTR_TO_BTF_ID register for type match");
return -EFAULT;
}
return 0;
}
@ -8872,7 +8791,6 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
enum bpf_arg_type arg_type = fn->arg_type[arg];
int regno = reg_from_argno(argno);
enum bpf_reg_type type = reg->type;
const u32 *arg_btf_id = NULL;
u32 arg_size = arg_type & MEM_FIXED_SIZE ? fn->arg_size[arg] : 0;
u32 key_size;
int err = 0;
@ -8924,12 +8842,7 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
if (err)
return err;
/* arg_btf_id and arg_size are in a union. */
if (base_type(arg_type) == ARG_PTR_TO_BTF_ID ||
base_type(arg_type) == ARG_PTR_TO_SPIN_LOCK)
arg_btf_id = fn->arg_btf_id[arg];
err = check_reg_type(env, reg, argno, arg_type, arg_btf_id, meta);
err = check_reg_type(env, reg, argno, arg_type, meta);
if (err)
return err;
@ -9027,6 +8940,43 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
arg_type & MEM_WRITE ? BPF_WRITE : BPF_READ,
false, meta, NULL);
break;
case ARG_PTR_TO_BTF_ID:
case ARG_PTR_TO_BTF_ID_SOCK_COMMON:
{
const u32 *arg_btf_id = fn->arg_btf_id[arg];
if (!meta->btf) {
const struct bpf_reg_types *compatible;
if (base_type(reg->type) != PTR_TO_BTF_ID)
break;
if (is_helper_call(meta, BPF_FUNC_kptr_xchg))
return map_kptr_match_type(env, meta->kptr_field, reg, regno) ?
-EACCES : 0;
if (!arg_btf_id) {
compatible = compatible_reg_types[base_type(arg_type)];
if (!compatible->btf_id) {
verifier_bug(env, "missing arg compatible BTF ID");
return -EFAULT;
}
arg_btf_id = compatible->btf_id;
}
if (arg_btf_id == BPF_PTR_POISON) {
verbose(env, "verifier internal error:");
verbose(env, "%s has non-overwritten BPF_PTR_POISON type\n",
reg_arg_name(env, argno));
return -EACCES;
}
}
err = process_arg_ptr_to_btf_id(env, reg, argno, arg_type, btf_vmlinux,
*arg_btf_id, meta, insn_idx);
if (err < 0)
return err;
break;
}
case ARG_PTR_TO_PERCPU_BTF_ID:
if (!reg->btf_id) {
verbose(env, "Helper has invalid btf_id in %s\n", reg_arg_name(env, argno));
@ -9973,8 +9923,12 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
continue;
}
err = check_reg_type(env, reg, argno, arg->arg_type, &arg->btf_id, &meta);
err = check_reg_type(env, reg, argno, arg->arg_type, &meta);
err = err ?: check_func_arg_reg_off(env, reg, argno, arg->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,
&meta, env->insn_idx);
if (err)
return err;
} else {
@ -12511,32 +12465,28 @@ static int gen_kfunc_arg_proto(struct bpf_verifier_env *env, struct bpf_call_arg
return check_arg_prog_aux(env, proto) ? 0 : -EINVAL;
}
static int process_kf_arg_ptr_to_btf_id(struct bpf_verifier_env *env,
struct bpf_reg_state *reg,
const struct btf_type *ref_t,
const char *ref_tname, u32 ref_id,
struct bpf_call_arg_meta *meta,
int arg, argno_t argno)
static int process_arg_ptr_to_btf_id(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
argno_t argno, enum bpf_arg_type arg_type,
const struct btf *arg_btf, u32 arg_btf_id,
struct bpf_call_arg_meta *meta, int insn_idx)
{
const struct btf_type *reg_ref_t;
bool strict_type_match = false;
bool taking_projection, struct_same, strict_type_match = false;
const struct btf_type *arg_t, *reg_t;
const char *arg_tname, *reg_tname;
const struct btf *reg_btf;
const char *reg_ref_tname;
bool taking_projection;
bool struct_same;
u32 reg_ref_id;
u32 reg_btf_id;
if (base_type(reg->type) == PTR_TO_BTF_ID) {
reg_btf = reg->btf;
reg_ref_id = reg->btf_id;
reg_btf_id = reg->btf_id;
} else {
reg_btf = btf_vmlinux;
reg_ref_id = *reg2btf_ids[base_type(reg->type)];
reg_btf_id = *reg2btf_ids[base_type(reg->type)];
}
/* Enforce strict type matching for calls to kfuncs that are acquiring
* or releasing a reference, or are no-cast aliases. We do _not_
* enforce strict matching for kfuncs by default,
/*
* Enforce strict type matching for arguments that release a reference,
* or are no-cast aliases. We do _not_ enforce strict matching by default,
* as we want to enable BPF programs to pass types that are bitwise
* equivalent without forcing them to explicitly cast with something
* like bpf_cast_to_kern_ctx().
@ -12558,27 +12508,30 @@ static int process_kf_arg_ptr_to_btf_id(struct bpf_verifier_env *env,
* btf_struct_ids_match() to walk the struct at the 0th offset, and
* resolve types.
*/
if ((is_kfunc_release(meta) && reg_is_referenced(env, reg)) ||
btf_type_ids_nocast_alias(&env->log, reg_btf, reg_ref_id, meta->btf, ref_id))
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,
arg_btf, arg_btf_id)))
strict_type_match = true;
WARN_ON_ONCE(is_kfunc_release(meta) && !tnum_is_const(reg->var_off));
arg_t = btf_type_skip_modifiers(arg_btf, arg_btf_id, &arg_btf_id);
arg_tname = btf_name_by_offset(arg_btf, arg_t->name_off);
reg_t = btf_type_skip_modifiers(reg_btf, reg_btf_id, &reg_btf_id);
reg_tname = btf_name_by_offset(reg_btf, reg_t->name_off);
struct_same = btf_struct_ids_match(&env->log, reg_btf, reg_btf_id,
reg->var_off.value, arg_btf, arg_btf_id,
strict_type_match, !type_is_alloc(reg->type));
reg_ref_t = btf_type_skip_modifiers(reg_btf, reg_ref_id, &reg_ref_id);
reg_ref_tname = btf_name_by_offset(reg_btf, reg_ref_t->name_off);
struct_same = btf_struct_ids_match(&env->log, reg_btf, reg_ref_id, reg->var_off.value,
meta->btf, ref_id, strict_type_match,
!type_is_alloc(reg->type));
/* If kfunc is accepting a projection type (ie. __sk_buff), it cannot
* actually use it -- it must cast to the underlying type. So we allow
* caller to pass in the underlying type.
*/
taking_projection = btf_is_projection_of(ref_tname, reg_ref_tname);
taking_projection = meta->btf && btf_is_projection_of(arg_tname, reg_tname);
if (!taking_projection && !struct_same) {
verbose(env, "kernel function %s %s expected pointer to %s %s but %s has a pointer to %s %s\n",
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),
btf_type_str(ref_t), ref_tname, reg_arg_name(env, argno),
btf_type_str(reg_ref_t), reg_ref_tname);
btf_type_str(arg_t), arg_tname,
reg_arg_name(env, argno), btf_type_str(reg_t), reg_tname);
return -EINVAL;
}
return 0;
@ -13076,7 +13029,7 @@ static bool check_css_task_iter_allowlist(struct bpf_verifier_env *env)
static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
int insn_idx)
{
const char *func_name = meta->func_name, *ref_tname;
const char *func_name = meta->func_name;
struct bpf_func_state *caller = cur_func(env);
struct bpf_reg_state *regs = cur_regs(env);
const struct btf *btf = meta->btf;
@ -13098,7 +13051,7 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_call_arg_me
for (i = 0; i < nargs; i++) {
struct bpf_reg_state *reg = get_func_arg_reg(caller, regs, i);
enum bpf_arg_type arg_type = meta->fn->arg_type[i];
const struct btf_type *t, *ref_t;
const struct btf_type *t;
argno_t argno = argno_from_arg(i + 1);
int regno = reg_from_argno(argno);
u32 ref_id = args[i].type;
@ -13114,10 +13067,8 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_call_arg_me
t = btf_type_skip_modifiers(btf, args[i].type, NULL);
if (btf_type_is_ptr(t)) {
ref_t = btf_type_skip_modifiers(btf, t->type, &ref_id);
ref_tname = btf_name_by_offset(btf, ref_t->name_off);
}
if (btf_type_is_ptr(t))
btf_type_skip_modifiers(btf, t->type, &ref_id);
ret = check_func_arg_nullability(env, reg, argno, arg_type, meta, insn_idx);
if (ret < 0)
@ -13433,8 +13384,8 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_call_arg_me
}
}
ret = process_kf_arg_ptr_to_btf_id(env, reg, ref_t, ref_tname,
ref_id, meta, i, argno);
ret = process_arg_ptr_to_btf_id(env, reg, argno, arg_type, btf,
ref_id, meta, insn_idx);
if (ret < 0)
return ret;
break;

View File

@ -13,13 +13,13 @@ struct {
const char *prog_name;
const char *err_msg;
} test_bpf_nf_fail_tests[] = {
{ "alloc_release", "kernel function bpf_ct_release R1 expected pointer to STRUCT nf_conn but" },
{ "insert_insert", "kernel function bpf_ct_insert_entry R1 expected pointer to STRUCT nf_conn___init but" },
{ "lookup_insert", "kernel function bpf_ct_insert_entry R1 expected pointer to STRUCT nf_conn___init but" },
{ "set_timeout_after_insert", "kernel function bpf_ct_set_timeout R1 expected pointer to STRUCT nf_conn___init but" },
{ "set_status_after_insert", "kernel function bpf_ct_set_status R1 expected pointer to STRUCT nf_conn___init but" },
{ "change_timeout_after_alloc", "kernel function bpf_ct_change_timeout R1 expected pointer to STRUCT nf_conn but" },
{ "change_status_after_alloc", "kernel function bpf_ct_change_status R1 expected pointer to STRUCT nf_conn but" },
{ "alloc_release", "bpf_ct_release R1 expected pointer to STRUCT nf_conn but" },
{ "insert_insert", "bpf_ct_insert_entry R1 expected pointer to STRUCT nf_conn___init but" },
{ "lookup_insert", "bpf_ct_insert_entry R1 expected pointer to STRUCT nf_conn___init but" },
{ "set_timeout_after_insert", "bpf_ct_set_timeout R1 expected pointer to STRUCT nf_conn___init but" },
{ "set_status_after_insert", "bpf_ct_set_status R1 expected pointer to STRUCT nf_conn___init but" },
{ "change_timeout_after_alloc", "bpf_ct_change_timeout R1 expected pointer to STRUCT nf_conn but" },
{ "change_status_after_alloc", "bpf_ct_change_status R1 expected pointer to STRUCT nf_conn but" },
{ "write_not_allowlisted_field", "no write support to nf_conn at off" },
{ "lookup_null_bpf_tuple", "Possibly NULL pointer passed to trusted R2" },
{ "lookup_null_bpf_opts", "Possibly NULL pointer passed to trusted R4" },

View File

@ -291,7 +291,7 @@ int reject_bad_type_xchg(struct __sk_buff *ctx)
}
SEC("?tc")
__failure __msg("invalid kptr access, R2 type=trusted_ptr_prog_test_ref_kfunc")
__failure __msg("R2 must have zero offset when passed to release func")
int reject_member_of_ref_xchg(struct __sk_buff *ctx)
{
struct prog_test_ref_kfunc *ref_ptr;

View File

@ -128,7 +128,7 @@ int BPF_PROG(path_d_path_kfunc_untrusted_from_current)
}
SEC("lsm.s/file_open")
__failure __msg("kernel function bpf_path_d_path R1 expected pointer to STRUCT path but R1 has a pointer to STRUCT file")
__failure __msg("bpf_path_d_path R1 expected pointer to STRUCT path but R1 has a pointer to STRUCT file")
int BPF_PROG(path_d_path_kfunc_type_mismatch, struct file *file)
{
bpf_path_d_path((struct path *)&file->f_task_work, buf, sizeof(buf));

View File

@ -152,7 +152,7 @@
},
.prog_type = BPF_PROG_TYPE_SCHED_CLS,
.result = REJECT,
.errstr = "kernel function bpf_kfunc_call_memb1_release R1 expected pointer",
.errstr = "bpf_kfunc_call_memb1_release R1 expected pointer",
.fixup_kfunc_btf_id = {
{ "bpf_kfunc_call_memb_acquire", 1 },
{ "bpf_kfunc_call_memb1_release", 5 },

View File

@ -342,7 +342,7 @@
.prog_type = BPF_PROG_TYPE_SCHED_CLS,
.fixup_map_kptr = { 1 },
.result = REJECT,
.errstr = "invalid kptr access, R2 type=ptr_prog_test_ref_kfunc expected=ptr_prog_test_member",
.errstr = "R2 must have zero offset when passed to release func",
},
{
"map_kptr: ref: reference state created and released on xchg",