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
This commit is contained in:
Eduard Zingerman 2026-10-09 03:47:20 -07:00 committed by Alexei Starovoitov
parent 5b6beb95f7
commit 830e4372de
No known key found for this signature in database
4 changed files with 49 additions and 47 deletions

View File

@ -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);

View File

@ -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));

View File

@ -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,
};

View File

@ -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 @@ s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn
* 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 @@ s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn
}
}
if (!val)
return S64_MIN;
return info;
size = val;
goto out;
}
@ -14448,18 +14454,12 @@ s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn
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 @@ s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn
* 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;
}
/*