Files
Eduard Zingerman 2fa579694d bpf: Record basic-block ends in insn_aux_data
SCEV needs information about the program's basic-block structure.
Piggyback on compute_predecessors() and set
insn_aux_data->bb_end for an instruction if:
- it has multiple successors, or
- its sole successor has multiple predecessors, or
- it is a jump instruction.

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-31-bcba9bc53ac9@gmail.com
2026-10-09 20:29:14 +00:00

617 lines
15 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <linux/slab.h>
#include <linux/sched/signal.h>
#include <linux/bpf_verifier.h>
static bool is_cfg_jump(struct bpf_insn *insn)
{
u8 class = BPF_CLASS(insn->code);
u8 opcode = BPF_OP(insn->code);
switch (class) {
case BPF_JMP:
case BPF_JMP32:
return opcode != BPF_CALL;
default:
return false;
}
}
static struct bpf_iarray **compute_predecessors(struct bpf_verifier_env *env)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
struct bpf_iarray *succ, *preds, **result;
struct bpf_prog *prog = env->prog;
u32 *num_preds, i, s, len = prog->len;
struct bpf_insn *insn;
size_t sz;
void *tmp;
num_preds = kvcalloc(prog->len, sizeof(u32), GFP_KERNEL_ACCOUNT);
if (!num_preds)
return NULL;
/*
* 'result' layout:
* - array of pointers (struct bpf_iarray *)[len]
* - struct bpf_iarray one after another
*/
sz = size_mul(len, sizeof(*result) + sizeof(**result));
for (i = 0; i < len; i++) {
insn = env->prog->insnsi + i;
succ = bpf_insn_successors(env, i);
sz = size_add(sz, array_size(succ->cnt, sizeof(*succ->items)));
iarray_for_each(s, succ) {
num_preds[s]++;
}
if (bpf_is_ldimm64(insn))
i++;
}
result = kvzalloc(sz, GFP_KERNEL_ACCOUNT);
if (!result) {
kvfree(num_preds);
return NULL;
}
tmp = (void *)&result[len];
for (i = 0; i < len; i++) {
result[i] = tmp;
tmp += struct_size(result[i], items, num_preds[i]);
}
for (i = 0; i < len; i++) {
insn = env->prog->insnsi + i;
succ = bpf_insn_successors(env, i);
iarray_for_each(s, succ) {
preds = result[s];
preds->items[preds->cnt++] = i;
}
if (succ->cnt > 1 ||
(succ->cnt == 1 && num_preds[succ->items[0]] > 1) ||
is_cfg_jump(insn))
aux[i].bb_end = true;
if (bpf_is_ldimm64(insn))
i++;
}
kvfree(num_preds);
return result;
}
static int idoms_intersect(struct bpf_verifier_env *env, int a, int b)
{
int *postorder_nums = env->cfg.postorder_nums;
int *idoms = env->idoms;
while (a != b) {
while (postorder_nums[a] < postorder_nums[b]) {
a = idoms[a];
}
while (postorder_nums[b] < postorder_nums[a]) {
b = idoms[b];
}
}
return a;
}
/* See "A Simple, Fast Dominance Algorithm" by Cooper et al. for details. */
static int compute_subprog_idoms(struct bpf_verifier_env *env, struct bpf_iarray **preds,
int subprog_idx)
{
struct bpf_subprog_info *subprog = &env->subprog_info[subprog_idx];
int start = subprog->start;
int po_first = subprog->postorder_start;
int po_last = (subprog + 1)->postorder_start - 1;
int *idoms = env->idoms;
int po_num, pred;
u32 work = 0;
bool changed;
idoms[start] = start;
changed = true;
do {
changed = false;
/*
* Iterate in reverse postorder, skipping the subprog entry:
* insn_postorder[po_last] is the subprogram entry.
*/
for (po_num = po_last - 1; po_num >= po_first; po_num--) {
int idx = env->cfg.insn_postorder[po_num];
int new_idom = -1;
iarray_for_each(pred, preds[idx]) {
if (++work % 1024 == 0) {
if (signal_pending(current))
return -EAGAIN;
cond_resched();
}
if (idoms[pred] == -1)
continue;
if (new_idom == -1)
new_idom = pred;
else
new_idom = idoms_intersect(env, pred, new_idom);
}
if (new_idom != -1 && idoms[idx] != new_idom) {
idoms[idx] = new_idom;
changed = true;
}
}
} while (changed);
idoms[start] = -1;
return 0;
}
int bpf_compute_idoms(struct bpf_verifier_env *env)
{
struct bpf_iarray **preds;
u32 len = env->prog->len;
int *idoms, i, err = 0;
preds = compute_predecessors(env);
if (!preds)
return -ENOMEM;
idoms = kvcalloc(len, sizeof(*idoms), GFP_KERNEL_ACCOUNT);
if (!idoms) {
kvfree(preds);
return -ENOMEM;
}
env->idoms = idoms;
for (i = 0; i < len; i++)
idoms[i] = -1;
for (i = 0; i < env->subprog_cnt; i++) {
err = compute_subprog_idoms(env, preds, i);
if (err)
break;
}
kvfree(preds);
return err;
}
struct dfs_state {
u32 traversed:1;
u32 next_succ:31;
};
struct loops_dfs {
struct dfs_state *state;
int *dfs_pos;
int *stack;
};
static void mark_irreducible(struct bpf_verifier_env *env, int h)
{
env->insn_aux_data[h].loop->irreducible = true;
}
static void add_backedge(struct bpf_verifier_env *env, int from, int h)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
struct bpf_loop *loop = aux[h].loop;
int cnt = loop->backedges_cnt;
if (cnt == MAX_BACKEDGES) {
loop->backedges_overflow = true;
return;
}
loop->backedges[cnt].from = from;
loop->backedges[cnt].latches_cnt = 0;
loop->backedges_cnt++;
}
static int add_exit(struct bpf_loop *loop, int from, int to)
{
if (loop->exits_overflow)
return 0;
if (loop->exits_cnt == MAX_LOOP_EXITS) {
loop->exits_overflow = true;
return 0;
}
if (!loop->exits) {
loop->exits = kvcalloc(MAX_LOOP_EXITS, sizeof(*loop->exits), GFP_KERNEL_ACCOUNT);
if (!loop->exits)
return -ENOMEM;
}
loop->exits[loop->exits_cnt] = (struct bpf_loop_exit) {
.from = from,
.to = to,
};
loop->exits_cnt++;
return 0;
}
static int mark_as_header(struct bpf_verifier_env *env, int h)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
if (!aux[h].loop) {
aux[h].loop = kvzalloc_obj(struct bpf_loop, GFP_KERNEL_ACCOUNT);
if (!aux[h].loop)
return -ENOMEM;
}
return 0;
}
static int assign_header(struct bpf_verifier_env *env, struct loops_dfs *dfs, int n, int h)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
int *dfs_pos = dfs->dfs_pos;
int err, nh;
err = mark_as_header(env, h);
if (err)
return err;
/* Don't encode self-loops, otherwise can't reflect loops nesting structure. */
if (n == h)
return 0;
/* Make sure that loop headers up the chain are sorted by dfs_pos. */
while (aux[n].loop_header != -1) {
nh = aux[n].loop_header;
if (nh == h)
return 0;
if (dfs_pos[nh] < dfs_pos[h]) {
aux[n].loop_header = h;
n = h;
h = nh;
} else {
n = nh;
}
}
aux[n].loop_header = h;
return 0;
}
static bool is_cond_jmp_insn(struct bpf_insn *insn)
{
u8 class = BPF_CLASS(insn->code);
u8 opcode = BPF_OP(insn->code);
if (class != BPF_JMP && class != BPF_JMP32)
return false;
switch (opcode) {
case BPF_JEQ:
case BPF_JGE:
case BPF_JGT:
case BPF_JLE:
case BPF_JLT:
case BPF_JNE:
case BPF_JSET:
case BPF_JSGE:
case BPF_JSGT:
case BPF_JSLE:
case BPF_JSLT:
return true;
default:
return false;
}
}
int bpf_loop_at_index(struct bpf_verifier_env *env, u32 idx)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
return aux[idx].loop ? idx : aux[idx].loop_header;
}
/*
* Collect conditional jumps within n's loop that dominate 'n' and have
* a successor outside the loop, nearest first.
*/
static void find_dominating_conditions(struct bpf_verifier_env *env, int n,
struct bpf_backedge *backedge)
{
struct bpf_insn *insns = env->prog->insnsi;
bool t_exits_loop, f_exits_loop;
int n_loop, t_loop, f_loop;
int *idoms = env->idoms;
n_loop = bpf_loop_at_index(env, n);
if (idoms_intersect(env, n, n_loop) != n_loop)
return;
while (n >= 0) {
if (is_cond_jmp_insn(&insns[n]) && bpf_loop_at_index(env, n) == n_loop) {
t_loop = bpf_loop_at_index(env, n + insns[n].off + 1);
f_loop = bpf_loop_at_index(env, n + 1);
t_exits_loop = t_loop != n_loop && !bpf_is_nested_loop(env, t_loop, n_loop);
f_exits_loop = f_loop != n_loop && !bpf_is_nested_loop(env, f_loop, n_loop);
if (t_exits_loop || f_exits_loop) {
if (backedge->latches_cnt == MAX_LATCHES)
return;
backedge->latches[backedge->latches_cnt++] = n;
}
}
if (n == n_loop)
break;
n = idoms[n];
}
}
/*
* As described in "A New Algorithm for Identifying Loops in Decompilation" by Wei et al,
* adapted to be non-recursive.
*/
static int compute_loops_in_subprog(struct bpf_verifier_env *env, struct loops_dfs *dfs,
int subprog_idx)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
struct dfs_state *state = dfs->state;
int start = env->subprog_info[subprog_idx].start;
int *dfs_pos = dfs->dfs_pos;
int *stack = dfs->stack;
int s, h, err, cur, stack_sz;
struct bpf_iarray *succ;
u32 i;
stack[0] = start;
state[start].traversed = true;
state[start].next_succ = 0;
dfs_pos[start] = 1;
stack_sz = 1;
i = 0;
do {
/*
* The algorithm should be very fast in practice,
* guard against pathological inputs, just in case.
*/
if ((++i % 1024) == 0) {
if (signal_pending(current))
return -EAGAIN;
cond_resched();
}
cur = stack[stack_sz - 1];
succ = bpf_insn_successors(env, cur);
if (state[cur].next_succ == succ->cnt) {
dfs_pos[cur] = 0;
stack_sz--;
continue;
}
s = succ->items[state[cur].next_succ];
if (!state[s].traversed) {
/* Case A: start -> ... -> cur -> s [unexplored] */
state[s].traversed = true;
state[s].next_succ = 0;
stack[stack_sz] = s;
dfs_pos[s] = stack_sz + 1;
stack_sz++;
continue;
}
/* 's' is fully explored at this point */
if (dfs_pos[s]) {
/*
* start -> ... -> s -> cur --.
* ^ |
* '----------'
* Case B: 's' is in the current DFS path.
*/
err = assign_header(env, dfs, cur, s);
if (err)
return err;
add_backedge(env, cur, s);
} else if (aux[s].loop_header == -1) {
/*
* start -> ... -> ... -> s -> ... -> end
* | ^
* '---> cur ---'
* Case C: 's' is explored, not in the current DFS path,
* and not a part of any loop.
*/
} else if (dfs_pos[aux[s].loop_header]) {
/*
* .----------------------.
* v |
* start -> ... -> h -> ... -> ... -> s --'
* | ^
* '---> cur ---'
* Case D: 's' is explored, not in current DFS path,
* but its innermost loop header is.
*/
err = assign_header(env, dfs, cur, aux[s].loop_header);
if (err)
return err;
} else {
/*
* case E: 's' is explored, not in current DFS path,
* its innermost loop header is not in current DFS path,
* hence 's' is another entry into the same loop.
*/
h = aux[s].loop_header;
mark_irreducible(env, h);
while (aux[h].loop_header != -1) {
h = aux[h].loop_header;
if (dfs_pos[h]) {
err = assign_header(env, dfs, cur, h);
if (err)
return err;
break;
}
mark_irreducible(env, h);
}
}
state[cur].next_succ++;
} while (stack_sz);
return 0;
}
bool bpf_is_nested_loop(struct bpf_verifier_env *env, int inner_header, int outer_header)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
int idx;
for (idx = inner_header; idx >= 0; idx = aux[idx].loop_header)
if (aux[idx].loop_header == outer_header)
return true;
return false;
}
int bpf_compute_loops(struct bpf_verifier_env *env)
{
int i, j, k, s, t, iloop, sloop, err = 0, len = env->prog->len;
struct bpf_insn_aux_data *aux = env->insn_aux_data;
struct bpf_verifier_log *log = &env->log;
struct bpf_backedge *backedge;
struct loops_dfs dfs = {};
struct bpf_iarray *succ;
struct bpf_loop *loop;
dfs.dfs_pos = kvcalloc(len, sizeof(int), GFP_KERNEL_ACCOUNT);
dfs.state = kvcalloc(len, sizeof(struct dfs_state), GFP_KERNEL_ACCOUNT);
dfs.stack = kvcalloc(len, sizeof(int), GFP_KERNEL_ACCOUNT);
if (!dfs.dfs_pos || !dfs.state || !dfs.stack) {
err = -ENOMEM;
goto out;
}
for (i = 0; i < len; i++)
aux[i].loop_header = -1;
for (i = 0; i < env->subprog_cnt; i++) {
err = compute_loops_in_subprog(env, &dfs, i);
if (err)
goto out;
}
/* find latches */
for (i = 0; i < len; i++) {
loop = aux[i].loop;
if (!loop)
continue;
for (j = 0; j < loop->backedges_cnt; j++) {
backedge = &loop->backedges[j];
/*
* In theory, the backedge->from and it's latch can reside in
* an inner loop of `i`, e.g.:
*
* 1: r7 += 1;
* 2: r6 += 1;
* if r7 == 2 goto 1b;
* if r6 < 2 goto 2b;
*
* For now, let's assume that latches are unknown for such cases.
* (GCC/LLVM handle this by inserting artificial cfg nodes).
*/
if (bpf_loop_at_index(env, backedge->from) != i)
continue;
find_dominating_conditions(env, backedge->from, backedge);
}
}
/* find exits */
for (i = 0; i < len; i++) {
iloop = aux[i].loop ? i : aux[i].loop_header;
if (iloop < 0)
continue;
succ = bpf_insn_successors(env, i);
iarray_for_each(s, succ) {
/*
* Nothing left to record once the innermost loop of 'i'
* overflowed: the walk below would stop at it right away.
*/
if (aux[iloop].loop->exits_overflow)
break;
sloop = aux[s].loop ? s : aux[s].loop_header;
if (iloop == sloop)
continue;
if (bpf_is_nested_loop(env, sloop, iloop))
continue;
/*
* At this point 'sloop' is either -1, an outer loop,
* or a loop in another branch of the loop hierarchy.
*/
for (t = iloop; t >= 0 && t != sloop; t = aux[t].loop_header) {
/*
* Account for the following configuration:
*
* tloop {
* iloop {
* ... i: goto s;
* }
* sloop {
* s:
* ...
* }
* }
*/
if (bpf_is_nested_loop(env, sloop, t))
break;
/*
* Record edges i -> s as exits from tloop when:
*
* sloop {
* tloop {
* iloop {
* ... i: goto s;
* }
* }
* s: ...
* }
*/
/* Enclosing loops inherit the overflow, see below */
if (aux[t].loop->exits_overflow)
break;
err = add_exit(aux[t].loop, i, s);
if (err)
goto out;
}
}
if (bpf_is_ldimm64(env->prog->insnsi + i))
i++;
}
/* A nested loop with a truncated exit list can't be abstracted by SCEV */
for (i = 0; i < len; i++) {
loop = aux[i].loop;
if (!loop || !loop->exits_overflow)
continue;
for (t = aux[i].loop_header; t >= 0; t = aux[t].loop_header)
aux[t].loop->exits_overflow = true;
}
if (env->log.level & BPF_LOG_LEVEL2) {
for (i = 0; i < len; i++) {
loop = aux[i].loop;
if (!loop)
continue;
bpf_log(log, "loop at %d", i);
if (aux[i].loop_header >= 0)
bpf_log(log, ", nested in %d", aux[i].loop_header);
if (loop->irreducible)
bpf_log(log, ", irreducible");
if (loop->exits_overflow)
bpf_log(log, ", too many exits");
bpf_log(log, "\n");
for (j = 0; j < loop->backedges_cnt; j++) {
backedge = &loop->backedges[j];
bpf_log(log, " backedge from %d", backedge->from);
for (k = 0; k < backedge->latches_cnt; k++) {
if (k == 0)
bpf_log(log, ", latch at ");
if (k == 1)
bpf_log(log, ", dominating exits at ");
if (k > 1)
bpf_log(log, ", ");
bpf_log(log, "%d", backedge->latches[k]);
}
bpf_log(log, "\n");
}
for (j = 0; j < loop->exits_cnt; j++)
bpf_log(log, " exit from %d to %d\n",
loop->exits[j].from, loop->exits[j].to);
}
}
out:
kvfree(dfs.dfs_pos);
kvfree(dfs.stack);
kvfree(dfs.state);
return err;
}