summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEduard Zingerman <eddyz87@gmail.com>2026-10-09 03:47:20 -0700
committerAlexei Starovoitov <ast@kernel.org>2026-10-09 20:29:12 +0000
commit830e4372de5c8e1e0bbea925e74717b8fc259dfd (patch)
tree5ccbe0e302b19c88401e10785a11c4ac939837c5
parent5b6beb95f7b4bde8333c588543a2400e52805cba (diff)
downloadlinux-next-830e4372de5c8e1e0bbea925e74717b8fc259dfd.tar.gz
linux-next-830e4372de5c8e1e0bbea925e74717b8fc259dfd.zip
bpf: Describe helper stack accesses with arg_access_info
Adjust bpf_helper_stack_access_bytes() to return results as a struct arg_access_info. The result is based on bpf_func_proto->argX_type value as in the following table: | Argument flags | allow_uninit_stack | Meaning | |----------------------+--------------------+----------------------| | MEM_WRITE | true | may_read, must_write | | MEM_WRITE+MEM_UNINIT | true | must_write | | MEM_WRITE | false | may_read, must_write | | MEM_WRITE+MEM_UNINIT | false | may_read, may_write | | 0 | true/false | may_read | MEM_WRITE now produces must_write when allow_uninit_stack is true: the verifier discards previous stack knowledge over the affected range. Previously, liveness only treated MEM_UNINIT this way. Also use resolve_map_arg_type() to account for bpf_map_peek_elem() treating its buffer as an input for bloom filters. Annotate ringbuf dynptr submit and discard arguments with MEM_WRITE. Their existing OBJ_RELEASE handling already invalidates the dynptr, but the prototypes need to describe the write as well as the release. 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-2-bcba9bc53ac9@gmail.com
-rw-r--r--include/linux/bpf_verifier.h6
-rw-r--r--kernel/bpf/liveness.c5
-rw-r--r--kernel/bpf/ringbuf.c4
-rw-r--r--kernel/bpf/verifier.c81
4 files changed, 49 insertions, 47 deletions
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index d4f32acba236..5c99c024c309 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1731,9 +1731,9 @@ struct arg_access_info {
u8 must_write:1; /* Prior verifier state is destroyed throughout size. */
};
-s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env,
- struct bpf_insn *insn, int arg,
- int insn_idx);
+struct arg_access_info
+bpf_helper_stack_access_bytes(struct bpf_verifier_env *env,
+ struct bpf_insn *insn, int arg, int insn_idx);
s64 bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env,
struct bpf_insn *insn, int arg,
int insn_idx);
diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c
index 7b317fb5ca82..353b52031577 100644
--- a/kernel/bpf/liveness.c
+++ b/kernel/bpf/liveness.c
@@ -1604,7 +1604,7 @@ static int record_load_store_access(struct bpf_verifier_env *env,
return 0;
}
-/* Adapt the signed helper/kfunc access size until both producers are converted. */
+/* Adapt the signed kfunc access size until its producer is converted. */
static struct arg_access_info stack_access_info(s64 bytes)
{
bool write = bytes < 0 && bytes != S64_MIN && -bytes < U32_MAX;
@@ -1634,8 +1634,7 @@ static int record_arg_access(struct bpf_verifier_env *env,
return 0;
if (bpf_helper_call(insn)) {
- info = stack_access_info(bpf_helper_stack_access_bytes(env, insn,
- arg_idx, insn_idx));
+ info = bpf_helper_stack_access_bytes(env, insn, arg_idx, insn_idx);
} else if (bpf_pseudo_kfunc_call(insn)) {
info = stack_access_info(bpf_kfunc_stack_access_bytes(env, insn,
arg_idx, insn_idx));
diff --git a/kernel/bpf/ringbuf.c b/kernel/bpf/ringbuf.c
index 3f1013d80544..447863ece26a 100644
--- a/kernel/bpf/ringbuf.c
+++ b/kernel/bpf/ringbuf.c
@@ -722,7 +722,7 @@ BPF_CALL_2(bpf_ringbuf_submit_dynptr, struct bpf_dynptr_kern *, ptr, u64, flags)
const struct bpf_func_proto bpf_ringbuf_submit_dynptr_proto = {
.func = bpf_ringbuf_submit_dynptr,
.ret_type = RET_VOID,
- .arg1_type = ARG_PTR_TO_DYNPTR | DYNPTR_TYPE_RINGBUF | OBJ_RELEASE,
+ .arg1_type = ARG_PTR_TO_DYNPTR | DYNPTR_TYPE_RINGBUF | OBJ_RELEASE | MEM_WRITE,
.arg2_type = ARG_ANYTHING,
};
@@ -741,7 +741,7 @@ BPF_CALL_2(bpf_ringbuf_discard_dynptr, struct bpf_dynptr_kern *, ptr, u64, flags
const struct bpf_func_proto bpf_ringbuf_discard_dynptr_proto = {
.func = bpf_ringbuf_discard_dynptr,
.ret_type = RET_VOID,
- .arg1_type = ARG_PTR_TO_DYNPTR | DYNPTR_TYPE_RINGBUF | OBJ_RELEASE,
+ .arg1_type = ARG_PTR_TO_DYNPTR | DYNPTR_TYPE_RINGBUF | OBJ_RELEASE | MEM_WRITE,
.arg2_type = ARG_ANYTHING,
};
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 353bde9ae227..bbb81ba0f2dc 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -14358,35 +14358,32 @@ int bpf_fetch_kfunc_arg_meta(struct bpf_verifier_env *env,
}
/*
- * Determine how many bytes a helper accesses through a stack pointer at
- * argument position @arg (0-based, corresponding to R1-R5).
- *
- * Returns:
- * > 0 known read access size in bytes
- * 0 doesn't read anything directly
- * S64_MIN unknown
- * < 0 known write access of (-return) bytes
+ * Describe a helper's stack access through argument @arg (0-based, R1-R5).
+ * must_write means the verifier destroys prior state throughout size bytes.
+ * In other words, must_write follows the verifier's model of the
+ * function's behaviour, which is safe to use for is_state_visited() pruning.
*/
-s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn,
- int arg, int insn_idx)
+struct arg_access_info
+bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn,
+ int arg, int insn_idx)
{
struct bpf_insn_aux_data *aux = &env->insn_aux_data[insn_idx];
+ struct arg_access_info info = {
+ .size = U32_MAX,
+ .may_read = true,
+ .may_write = true,
+ };
const struct bpf_func_proto *fn;
+ enum bpf_access_type access_type;
enum bpf_arg_type at;
- bool full_write;
- s64 size;
+ bool exact_size = true;
+ u64 size = U32_MAX;
if (bpf_get_helper_proto(env, insn->imm, &fn) < 0)
- return S64_MIN;
+ return info;
at = fn->arg_type[arg];
- /*
- * Generic outputs may leave bytes untouched. Keep prior initialization
- * live when the caller cannot read uninitialized bytes. Constructors of
- * special objects, such as dynptrs, still define their storage.
- */
- full_write = (at & MEM_UNINIT) &&
- (!arg_type_is_raw_mem(at) || env->allow_uninit_stack);
+ access_type = func_arg_access_type(at);
switch (base_type(at)) {
case ARG_PTR_TO_MAP_KEY:
@@ -14409,6 +14406,10 @@ s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn
i = aux->const_reg_vals[map_reg];
if (i < env->used_map_cnt) {
+ /* Bloom-filter peek reads the value buffer. */
+ if (!is_key && insn->imm == BPF_FUNC_map_peek_elem &&
+ env->used_maps[i]->map_type == BPF_MAP_TYPE_BLOOM_FILTER)
+ access_type = BPF_READ;
size = is_key ? env->used_maps[i]->key_size
: env->used_maps[i]->value_size;
goto out;
@@ -14418,7 +14419,12 @@ scan_all_maps:
* Map pointer is not known at this call site (e.g. different
* maps on merged paths). Conservatively return the largest
* key_size or value_size across all maps used by the program.
+ * This is only an upper bound, so it cannot establish a definite
+ * write. Map-dependent argument types can also turn an output
+ * into an input, so conservatively retain a read dependency.
*/
+ exact_size = false;
+ access_type |= BPF_READ;
val = 0;
for (i = 0; i < env->used_map_cnt; i++) {
struct bpf_map *map = env->used_maps[i];
@@ -14434,7 +14440,7 @@ scan_all_maps:
}
}
if (!val)
- return S64_MIN;
+ return info;
size = val;
goto out;
}
@@ -14448,18 +14454,12 @@ scan_all_maps:
int size_reg = BPF_REG_1 + arg + 1;
if (aux->const_reg_mask & BIT(size_reg)) {
- size = (s64)aux->const_reg_vals[size_reg];
+ size = aux->const_reg_vals[size_reg];
goto out;
}
- /*
- * Size arg is const on each path but differs across merged
- * paths. Reads may extend anywhere up to the frame top.
- */
- if (full_write)
- return 0;
- return S64_MIN;
}
- return S64_MIN;
+ /* Preserve access directions even when the extent is unknown. */
+ goto out;
case ARG_PTR_TO_DYNPTR:
size = BPF_DYNPTR_SIZE;
break;
@@ -14469,18 +14469,21 @@ scan_all_maps:
* doesn't access stack. The callback subprog does and it's
* analyzed separately.
*/
- return 0;
+ return (struct arg_access_info) {};
default:
- return S64_MIN;
+ return info;
}
out:
- /*
- * Other accesses keep the previous state live, including untouched bytes
- * of an unprivileged generic output.
- */
- if (full_write)
- return -size;
- return size;
+ info.size = min_t(u64, size, U32_MAX);
+ info.may_read = !!(access_type & BPF_READ);
+ info.may_write = !!(access_type & BPF_WRITE);
+ info.must_write = info.may_write && exact_size && info.size != U32_MAX;
+ /* Generic unprivileged outputs retain prior initialization state. */
+ if (!env->allow_uninit_stack && arg_type_is_raw_mem(at)) {
+ info.may_read = true;
+ info.must_write = false;
+ }
+ return info;
}
/*