summaryrefslogtreecommitdiff
path: root/kernel
diff options
context:
space:
mode:
authorEduard Zingerman <eddyz87@gmail.com>2026-10-09 03:47:28 -0700
committerAlexei Starovoitov <ast@kernel.org>2026-10-09 20:29:13 +0000
commit8212c1c2986dfd06498558f672ff3ec964b924c5 (patch)
tree248152bb553127caef9904138fd21d14b2dbcde1 /kernel
parent13cffb88318085117c988106d4f5e439646f3ba6 (diff)
downloadlinux-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.c28
-rw-r--r--kernel/bpf/verifier.c66
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