bpf: Derive global subprog stack liveness from BTF signatures

Stack liveness currently analyzes global callees in the context of their
callers. The resulting read/write marks describe only the current body,
but freplace can replace it after the caller has been verified.

A no-op callee taking a stack pointer can be replaced with one writing
through that pointer, leaving the call's may_write marks incomplete.
SCEV could then preserve a stack counter recurrence across a call that
resets it, invalidating a loop termination proof.

Instead, derive global call effects from BTF argument types instead,
same is it done on the main verification pass.

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Link: https://patch.msgid.link/20261009-scev-minimal-rebase-v3-10-bcba9bc53ac9@gmail.com
This commit is contained in:
Eduard Zingerman 2026-10-09 03:47:28 -07:00 committed by Alexei Starovoitov
parent 13cffb8831
commit 8212c1c298
No known key found for this signature in database
3 changed files with 90 additions and 7 deletions

View File

@ -1744,6 +1744,9 @@ bpf_helper_stack_access_bytes(struct bpf_verifier_env *env,
struct arg_access_info
bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env,
struct bpf_insn *insn, int arg, int insn_idx);
struct arg_access_info
bpf_global_subprog_stack_access_bytes(struct bpf_verifier_env *env,
struct bpf_insn *insn, int arg, int insn_idx);
int bpf_compute_subprog_arg_access(struct bpf_verifier_env *env);
int bpf_stack_liveness_init(struct bpf_verifier_env *env);

View File

@ -1715,6 +1715,8 @@ static int record_arg_access(struct bpf_verifier_env *env,
info = bpf_helper_stack_access_bytes(env, insn, arg_idx, insn_idx);
} else if (bpf_pseudo_kfunc_call(insn)) {
info = bpf_kfunc_stack_access_bytes(env, insn, arg_idx, insn_idx);
} else if (bpf_pseudo_call(insn)) {
info = bpf_global_subprog_stack_access_bytes(env, insn, arg_idx, insn_idx);
} else {
for (int f = 0; f <= depth; f++) {
err = mark_stack_read_all(env, instance, f, insn_idx);
@ -1736,7 +1738,7 @@ static int record_arg_access(struct bpf_verifier_env *env,
return err;
}
/* Record stack access for a given 'at' state of helper/kfunc 'insn' */
/* Record stack access for a given 'at' state of helper/kfunc/global subprog 'insn' */
static int record_call_access(struct bpf_verifier_env *env,
struct func_instance *instance,
struct arg_track *at,
@ -1744,12 +1746,19 @@ static int record_call_access(struct bpf_verifier_env *env,
{
struct bpf_insn *insn = &env->prog->insnsi[insn_idx];
struct bpf_call_summary cs;
int r, err, arg_slot_cnt = 5;
int r, err, callee, arg_slot_cnt = 5;
if (bpf_pseudo_call(insn))
return 0;
if (bpf_is_callx(insn))
if (bpf_pseudo_call(insn)) {
/*
* analyze_subprog() handles static callees. For global callees,
* derive effects from argument types so may_write marks remain
* conservative under freplace.
*/
callee = bpf_find_subprog(env, insn_idx + insn->imm + 1);
if (callee < 0 || !bpf_subprog_is_global(env, callee))
return 0;
arg_slot_cnt = MAX_BPF_FUNC_ARGS;
} else if (bpf_is_callx(insn))
/*
* The callee is not known statically. Assume that all arg
* slots are passed and let record_arg_access() conservatively
@ -2282,7 +2291,12 @@ static int analyze_subprog(struct bpf_verifier_env *env,
if (bpf_pseudo_call(insn)) {
target = idx + insn->imm + 1;
callee = bpf_find_subprog(env, target);
if (callee < 0)
/*
* Global subprograms can be freplaced, so conservatively derive
* their stack effects from their BTF signatures instead of
* analyzing them in the context of their callers.
*/
if (callee < 0 || bpf_subprog_is_global(env, callee))
continue;
/* Build entry args: R1-R5 and stack args from at_in at call site */

View File

@ -14571,6 +14571,72 @@ bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn
return info;
}
/*
* Called before dead code elimination, some subprograms may be genuinely unreachable.
* Suppress diagnostics so callers can use a conservative result on error.
* The main pass would report errors for reachable subprograms.
*/
static bool try_btf_prepare_func_args(struct bpf_verifier_env *env, int subprog)
{
struct bpf_prog *prog = env->prog;
u32 log_level = env->log.level;
int err;
if (!prog->aux->func_info || !prog->aux->func_info[subprog].type_id)
return false;
env->log.level = 0;
err = btf_prepare_func_args(env, subprog);
env->log.level = log_level;
return !err;
}
/*
* Derive stack effects for a global subprog's argument slot @arg (0-based)
* from its declared type, matching the main pass's argument checks.
*/
struct arg_access_info
bpf_global_subprog_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn,
int arg, int insn_idx)
{
struct arg_access_info info = {
.size = U32_MAX,
.may_read = true,
.may_write = true,
};
struct bpf_subprog_info *sub;
int callee;
callee = bpf_find_subprog(env, insn_idx + insn->imm + 1);
if (callee < 0)
return info;
sub = subprog_info(env, callee);
if (!try_btf_prepare_func_args(env, callee))
return info;
switch (base_type(sub->args[arg].arg_type)) {
case ARG_UNUSED: /* Not an argument slot. */
case ARG_ANYTHING: /* A scalar, the callee can't dereference it. */
return (struct arg_access_info) {};
case ARG_PTR_TO_BTF_ID:
/*
* Trusted arguments cannot be stack pointers.
* Untrusted arguments use ARG_IGNORE:
* probe reads need no caller stack state, writes are prohibited.
*/
return (struct arg_access_info) {};
case ARG_PTR_TO_MEM:
/* mem_size is zero for untrusted pointers of unknown size. */
if (sub->args[arg].mem_size) {
info.size = sub->args[arg].mem_size;
info.must_write = true;
}
return info;
default:
return info;
}
}
/* check special kfuncs and return:
* 1 - not fall-through to 'else' branch, continue verification
* 0 - fall-through to 'else' branch