diff options
| author | Eduard Zingerman <eddyz87@gmail.com> | 2026-10-09 03:47:28 -0700 |
|---|---|---|
| committer | Alexei Starovoitov <ast@kernel.org> | 2026-10-09 20:29:13 +0000 |
| commit | 8212c1c2986dfd06498558f672ff3ec964b924c5 (patch) | |
| tree | 248152bb553127caef9904138fd21d14b2dbcde1 /kernel | |
| parent | 13cffb88318085117c988106d4f5e439646f3ba6 (diff) | |
| download | linux-next-8212c1c2986dfd06498558f672ff3ec964b924c5.tar.gz linux-next-8212c1c2986dfd06498558f672ff3ec964b924c5.zip | |
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
Diffstat (limited to 'kernel')
| -rw-r--r-- | kernel/bpf/liveness.c | 28 | ||||
| -rw-r--r-- | kernel/bpf/verifier.c | 66 |
2 files changed, 87 insertions, 7 deletions
diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c index 52c39bbee6b1..a359e457e6c0 100644 --- a/kernel/bpf/liveness.c +++ b/kernel/bpf/liveness.c @@ -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 */ diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 8987c9a69d10..a5456a6e2694 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -14571,6 +14571,72 @@ out: 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 |
