bpf: record arg tracking results in bpf_liveness masks

After arg tracking reaches a fixed point, perform a single linear scan
over the converged at_in[] state and translate each memory access into
liveness read/write masks on the func_instance:

- Load/store instructions: FP-derived pointer's frame and offset(s)
  are converted to half-slot masks targeting
  per_frame_masks->{may_read,must_write}

- Helper/kfunc calls: record_call_access() queries
  bpf_helper_stack_access_bytes() / bpf_kfunc_stack_access_bytes()
  for each FP-derived argument to determine access size and direction.
  Unknown access size (S64_MIN) conservatively marks all slots from
  fp_off to fp+0 as read.

- Imprecise pointers (frame == ARG_IMPRECISE): conservatively mark
  all slots in every frame covered by the pointer's frame bitmask
  as fully read.

- Static subprog calls with unresolved arguments: conservatively mark
  all frames as fully read.

Instead of a call to clean_live_states(), start cleaning the current
state continuously as registers and stack become dead since the static
analysis provides complete liveness information. This makes
clean_live_states() and bpf_verifier_state->cleaned unnecessary.

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260410-patch-set-v4-8-5d4eecb343db@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Eduard Zingerman
2026-04-10 13:55:59 -07:00
committed by Alexei Starovoitov
parent bf0c571f7f
commit fed53dbcdb
3 changed files with 245 additions and 63 deletions

View File

@@ -1421,6 +1421,215 @@ static void arg_track_xfer(struct bpf_verifier_env *env, struct bpf_insn *insn,
}
}
/*
* Record access_bytes from helper/kfunc or load/store insn.
* access_bytes > 0: stack read
* access_bytes < 0: stack write
* access_bytes == S64_MIN: unknown conservative, mark [0..slot] as read
* access_bytes == 0: no access
*
*/
static int record_stack_access_off(struct bpf_verifier_env *env,
struct func_instance *instance, s64 fp_off,
s64 access_bytes, u32 frame, u32 insn_idx)
{
s32 slot_hi, slot_lo;
spis_t mask;
if (fp_off >= 0)
/*
* out of bounds stack access doesn't contribute
* into actual stack liveness. It will be rejected
* by the main verifier pass later.
*/
return 0;
if (access_bytes == S64_MIN) {
/* helper/kfunc read unknown amount of bytes from fp_off until fp+0 */
slot_hi = (-fp_off - 1) / STACK_SLOT_SZ;
mask = SPIS_ZERO;
spis_or_range(&mask, 0, slot_hi);
return mark_stack_read(instance, frame, insn_idx, mask);
}
if (access_bytes > 0) {
/* Mark any touched slot as use */
slot_hi = (-fp_off - 1) / STACK_SLOT_SZ;
slot_lo = max_t(s32, (-fp_off - access_bytes) / STACK_SLOT_SZ, 0);
mask = SPIS_ZERO;
spis_or_range(&mask, slot_lo, slot_hi);
return mark_stack_read(instance, frame, insn_idx, mask);
} else if (access_bytes < 0) {
/* Mark only fully covered slots as def */
access_bytes = -access_bytes;
slot_hi = (-fp_off) / STACK_SLOT_SZ - 1;
slot_lo = max_t(s32, (-fp_off - access_bytes + STACK_SLOT_SZ - 1) / STACK_SLOT_SZ, 0);
if (slot_lo <= slot_hi) {
mask = SPIS_ZERO;
spis_or_range(&mask, slot_lo, slot_hi);
bpf_mark_stack_write(env, frame, mask);
}
}
return 0;
}
/*
* 'arg' is FP-derived argument to helper/kfunc or load/store that
* reads (positive) or writes (negative) 'access_bytes' into 'use' or 'def'.
*/
static int record_stack_access(struct bpf_verifier_env *env,
struct func_instance *instance,
const struct arg_track *arg,
s64 access_bytes, u32 frame, u32 insn_idx)
{
int i, err;
if (access_bytes == 0)
return 0;
if (arg->off_cnt == 0) {
if (access_bytes > 0 || access_bytes == S64_MIN)
return mark_stack_read(instance, frame, insn_idx, SPIS_ALL);
return 0;
}
if (access_bytes != S64_MIN && access_bytes < 0 && arg->off_cnt != 1)
/* multi-offset write cannot set stack_def */
return 0;
for (i = 0; i < arg->off_cnt; i++) {
err = record_stack_access_off(env, instance, arg->off[i], access_bytes, frame, insn_idx);
if (err)
return err;
}
return 0;
}
/*
* When a pointer is ARG_IMPRECISE, conservatively mark every frame in
* the bitmask as fully used.
*/
static int record_imprecise(struct func_instance *instance, u32 mask, u32 insn_idx)
{
int depth = instance->callchain.curframe;
int f, err;
for (f = 0; mask; f++, mask >>= 1) {
if (!(mask & 1))
continue;
if (f <= depth) {
err = mark_stack_read(instance, f, insn_idx, SPIS_ALL);
if (err)
return err;
}
}
return 0;
}
/* Record load/store access for a given 'at' state of 'insn'. */
static int record_load_store_access(struct bpf_verifier_env *env,
struct func_instance *instance,
struct arg_track *at, int insn_idx)
{
struct bpf_insn *insn = &env->prog->insnsi[insn_idx];
int depth = instance->callchain.curframe;
s32 sz = bpf_size_to_bytes(BPF_SIZE(insn->code));
u8 class = BPF_CLASS(insn->code);
struct arg_track resolved, *ptr;
int oi;
switch (class) {
case BPF_LDX:
ptr = &at[insn->src_reg];
break;
case BPF_STX:
if (BPF_MODE(insn->code) == BPF_ATOMIC) {
if (insn->imm == BPF_STORE_REL)
sz = -sz;
if (insn->imm == BPF_LOAD_ACQ)
ptr = &at[insn->src_reg];
else
ptr = &at[insn->dst_reg];
} else {
ptr = &at[insn->dst_reg];
sz = -sz;
}
break;
case BPF_ST:
ptr = &at[insn->dst_reg];
sz = -sz;
break;
default:
return 0;
}
/* Resolve offsets: fold insn->off into arg_track */
if (ptr->off_cnt > 0) {
resolved.off_cnt = ptr->off_cnt;
resolved.frame = ptr->frame;
for (oi = 0; oi < ptr->off_cnt; oi++) {
resolved.off[oi] = arg_add(ptr->off[oi], insn->off);
if (resolved.off[oi] == OFF_IMPRECISE) {
resolved.off_cnt = 0;
break;
}
}
ptr = &resolved;
}
if (ptr->frame >= 0 && ptr->frame <= depth)
return record_stack_access(env, instance, ptr, sz, ptr->frame, insn_idx);
if (ptr->frame == ARG_IMPRECISE)
return record_imprecise(instance, ptr->mask, insn_idx);
/* ARG_NONE: not derived from any frame pointer, skip */
return 0;
}
/* Record stack access for a given 'at' state of helper/kfunc 'insn' */
static int record_call_access(struct bpf_verifier_env *env,
struct func_instance *instance,
struct arg_track *at,
int insn_idx)
{
struct bpf_insn *insn = &env->prog->insnsi[insn_idx];
int depth = instance->callchain.curframe;
struct bpf_call_summary cs;
int r, err = 0, num_params = 5;
if (bpf_pseudo_call(insn))
return 0;
if (bpf_get_call_summary(env, insn, &cs))
num_params = cs.num_params;
for (r = BPF_REG_1; r < BPF_REG_1 + num_params; r++) {
int frame = at[r].frame;
s64 bytes;
if (!arg_is_fp(&at[r]))
continue;
if (bpf_helper_call(insn)) {
bytes = bpf_helper_stack_access_bytes(env, insn, r - 1, insn_idx);
} else if (bpf_pseudo_kfunc_call(insn)) {
bytes = bpf_kfunc_stack_access_bytes(env, insn, r - 1, insn_idx);
} else {
for (int f = 0; f <= depth; f++) {
err = mark_stack_read(instance, f, insn_idx, SPIS_ALL);
if (err)
return err;
}
return 0;
}
if (bytes == 0)
continue;
if (frame >= 0 && frame <= depth)
err = record_stack_access(env, instance, &at[r], bytes, frame, insn_idx);
else if (frame == ARG_IMPRECISE)
err = record_imprecise(instance, at[r].mask, insn_idx);
if (err)
return err;
}
return 0;
}
/*
* For a calls_callback helper, find the callback subprog and determine
* which caller register maps to which callback register for FP passthrough.
@@ -1665,6 +1874,40 @@ redo:
if (changed)
goto redo;
/* Record memory accesses using converged at_in (RPO skips dead code) */
for (p = po_end - 1; p >= po_start; p--) {
int idx = env->cfg.insn_postorder[p];
int i = idx - start;
struct bpf_insn *insn = &insns[idx];
reset_stack_write_marks(env, instance);
err = record_load_store_access(env, instance, at_in[i], idx);
if (err)
goto err_free;
if (insn->code == (BPF_JMP | BPF_CALL)) {
err = record_call_access(env, instance, at_in[i], idx);
if (err)
goto err_free;
}
if (bpf_pseudo_call(insn) || bpf_calls_callback(env, idx)) {
kvfree(env->callsite_at_stack[idx]);
env->callsite_at_stack[idx] =
kvmalloc_objs(*env->callsite_at_stack[idx],
MAX_ARG_SPILL_SLOTS, GFP_KERNEL_ACCOUNT);
if (!env->callsite_at_stack[idx]) {
err = -ENOMEM;
goto err_free;
}
memcpy(env->callsite_at_stack[idx],
at_stack_in[i], sizeof(struct arg_track) * MAX_ARG_SPILL_SLOTS);
}
err = commit_stack_write_marks(env, instance, idx);
if (err)
goto err_free;
}
info->at_in = at_in;
at_in = NULL;
info->len = len;

View File

@@ -1804,7 +1804,6 @@ static int copy_verifier_state(struct bpf_verifier_state *dst_state,
return err;
dst_state->speculative = src->speculative;
dst_state->in_sleepable = src->in_sleepable;
dst_state->cleaned = src->cleaned;
dst_state->curframe = src->curframe;
dst_state->branches = src->branches;
dst_state->parent = src->parent;
@@ -20254,8 +20253,6 @@ static int clean_verifier_state(struct bpf_verifier_env *env,
{
int i, err;
if (env->cur_state != st)
st->cleaned = true;
err = bpf_live_stack_query_init(env, st);
if (err)
return err;
@@ -20268,37 +20265,6 @@ static int clean_verifier_state(struct bpf_verifier_env *env,
return 0;
}
/* the parentage chains form a tree.
* the verifier states are added to state lists at given insn and
* pushed into state stack for future exploration.
* when the verifier reaches bpf_exit insn some of the verifier states
* stored in the state lists have their final liveness state already,
* but a lot of states will get revised from liveness point of view when
* the verifier explores other branches.
* Example:
* 1: *(u64)(r10 - 8) = 1
* 2: if r1 == 100 goto pc+1
* 3: *(u64)(r10 - 8) = 2
* 4: r0 = *(u64)(r10 - 8)
* 5: exit
* when the verifier reaches exit insn the stack slot -8 in the state list of
* insn 2 is not yet marked alive. Then the verifier pops the other_branch
* of insn 2 and goes exploring further. After the insn 4 read, liveness
* analysis would propagate read mark for -8 at insn 2.
*
* Since the verifier pushes the branch states as it sees them while exploring
* the program the condition of walking the branch instruction for the second
* time means that all states below this branch were already explored and
* their final liveness marks are already propagated.
* Hence when the verifier completes the search of state list in is_state_visited()
* we can call this clean_live_states() function to clear dead the registers and stack
* slots to simplify state merging.
*
* Important note here that walking the same branch instruction in the callee
* doesn't meant that the states are DONE. The verifier has to compare
* the callsites
*/
/* Find id in idset and increment its count, or add new entry */
static void idset_cnt_inc(struct bpf_idset *idset, u32 id)
{
@@ -20362,33 +20328,6 @@ static void clear_singular_ids(struct bpf_verifier_env *env,
}));
}
static int clean_live_states(struct bpf_verifier_env *env, int insn,
struct bpf_verifier_state *cur)
{
struct bpf_verifier_state_list *sl;
struct list_head *pos, *head;
int err;
head = explored_state(env, insn);
list_for_each(pos, head) {
sl = container_of(pos, struct bpf_verifier_state_list, node);
if (sl->state.branches)
continue;
if (sl->state.insn_idx != insn ||
!same_callsites(&sl->state, cur))
continue;
if (sl->state.cleaned)
/* all regs in this state in all frames were already marked */
continue;
if (incomplete_read_marks(env, &sl->state))
continue;
err = clean_verifier_state(env, &sl->state);
if (err)
return err;
}
return 0;
}
static bool regs_exact(const struct bpf_reg_state *rold,
const struct bpf_reg_state *rcur,
struct bpf_idmap *idmap)
@@ -21089,7 +21028,8 @@ static int is_state_visited(struct bpf_verifier_env *env, int insn_idx)
env->insn_processed - env->prev_insn_processed >= 8)
add_new_state = true;
err = clean_live_states(env, insn_idx, cur);
/* keep cleaning the current state as registers/stack become dead */
err = clean_verifier_state(env, cur);
if (err)
return err;