Skip to contentSkip to conversation

bpf: static stack liveness data flow analysis

PATCH In review Eduard Zingerman 6 participants 30 messages Apr 10 – May 11

Subsystems (4)
lore
Share & tools
lore

Eduard Zingerman

author maintainer
#0 lore raw

v4 cover bpf: static stack liveness data flow analysis

This patch set converts current dynamic stack slot liveness tracking mechanism to a static data flow analysis. The result is used during state pruning (clean_verifier_state): to zero out dead stack slots, enabling more aggressive state equivalence and pruning. To improve analysis precision live stack slot tracking is converted to 4-byte granularity.

The key ideas and the bulk of the execution behind the series belong to Alexei Starovoitov. I contributed to patch set integration with existing liveness tracking mechanism.

Due to complexity of the changes the bisectability property of the patch set is not preserved. Some selftests may fail between intermediate patches of the series.

Analysis consists of two passes:
- A forward fixed-point analysis that tracks which frame's FP each register value is derived from, and at what byte offset. This is needed because a callee can receive a pointer to its caller's stack frame (e.g. r1 = fp-16 at the call site), then do *(u64 *)(r1 + 0) inside the callee - a cross-frame stack access that the callee's local liveness must attribute to the caller's stack.
- A backward dataflow pass within each callee subprog that computes live_in = (live_out \ def) ∪ use for both local and non-local (ancestor) stack slots. The result of the analysis for callee is propagated up to the callsite.

The key idea making such analysis possible is that limited and conservative argument tracking pass is sufficient to recover most of the offsets / stack pointer arguments.

Changelog:
v3 -> v4:
liveness.c:
- fill_from_stack(): correct conservative stack mask for imprecise result, instead of picking frames from pointer register (Alexei, sashiko).
- spill_to_stack(): join with existing values instead of overwriting when dst has multiple offsets (cnt > 1) or imprecise offset (cnt == 0) (Alexei, sashiko).
- analyze_subprog(): big change, now each analyze_subprog() is called with a fresh func_instance, once read/write marks are collected the instance is joined with the one accumulated for (callsite, depth) and update_instance() is called.
This handles several issues:
- Avoids stale must_write marks when same func_instance is reused by analyze_subprog() several times.
- Handles potential calls multiple calls for mark_stack_write() within single instruction.
(Alexei, sashiko).
- analyze_subprog(): added complexity limit to avoid exponential analysis time blowup for crafted programs with lots of nested function calls (Alexei, sashiko).
- the patch "bpf: record arg tracking results in bpf_liveness masks"

    is reinstated, it was accidentally squashed during v1->v2
    transition.

verifier.c:
- clean_live_states() is replaced by a direct call to

    clean_verifier_state(), bpf_verifier_state->cleaned is dropped.

verifier_live_stack.c:
- added selftests for arg tracking changes.

v2 -> v3:
liveness.c:
- record_stack_access(): handle S64_MIN (unknown read) with imprecise offset. Test case can't be created with existing helpers/kfuncs (sashiko).
- fmt_subprog(): handle NULL name (subprogs without BTF info).
- print_instance(): use u64 for pos/insn_pos avoid truncation

    (bot+bpf-ci).
  - compute_subprog_args(): return error if
    'env->callsite_at_stack[idx] = kvmalloc_objs(...)' fails
    (sashiko).

- clear_overlapping_stack_slots(): avoid integer promoting issues by adding explicit (int) cast (sashiko).

bpf_verifier.h, verifier.c, liveness.c:
- Fixes in comments and commit messages (bot+bpf-ci).

v1 -> v2:
liveness.c:
- Removed func_instance->callsites and replaced it with explicit spine passed through analys_subprog() calls (sashiko).
- Fixed BPF_LOAD_ACQ handling in arg_track_xfer: don't clear dst register tracking (sashiko).
- Various error threading nits highlighted by bots

    (sashiko, bot+bpf-ci).

- Massaged fmt_spis_mask() to be more concise (Alexei)

verifier.c:
- Move subprog_info[i].name assignment from add_subprog_and_kfunc to check_btf_func (sashiko, bot+bpf-ci).
- Fixed inverse usage of msb/lsb halves by patch
"bpf: make liveness.c track stack with 4-byte granularity" (sashiko, bot+bpf-ci).

v1: https://lore.kernel.org/bpf/[email protected]/
v2: https://lore.kernel.org/bpf/[email protected]/
v3: https://lore.kernel.org/bpf/[email protected]/

Verification performance impact:

========= selftests: master vs patch-set =========

File                     Program        Insns (A)  Insns (B)  Insns    (DIFF)
-----------------------  -------------  ---------  ---------  ---------------
xdp_synproxy_kern.bpf.o  syncookie_tc       20363      22910  +2547 (+12.51%)
xdp_synproxy_kern.bpf.o  syncookie_xdp      20450      23001  +2551 (+12.47%)
Total progs: 4490
Old success: 2856
New success: 2856
total_insns diff min:  -80.26%
total_insns diff max:   12.51%

0 -> value: 0
value -> 0: 0
total_insns abs max old: 837,487
total_insns abs max new: 837,487

 -85 .. -75  %: 1
 -50 .. -40  %: 1
 -35 .. -25  %: 1
 -20 .. -10  %: 5
 -10 .. 0    %: 18
   0 .. 5    %: 4458
   5 .. 15   %: 6

========= scx: master vs patch-set =========

File            Program    Insns (A)  Insns (B)  Insns   (DIFF)
--------------  ---------  ---------  ---------  --------------
scx_qmap.bpf.o  qmap_init      20230      19022  -1208 (-5.97%)
Total progs: 376
Old success: 351
New success: 351
total_insns diff min:  -27.15%
total_insns diff max:    0.50%

0 -> value: 0
value -> 0: 0
total_insns abs max old: 236,251
total_insns abs max new: 233,669

 -30 .. -20  %: 8
 -20 .. -10  %: 2
 -10 .. 0    %: 21
   0 .. 5    %: 345

========= meta: master vs patch-set =========

File                                                                                       Program                        Insns (A)  Insns (B)  Insns      (DIFF)
-----------------------------------------------------------------------------------------  -----------------------------  ---------  ---------  -----------------
...
third-party-scx-backports-6.9-1.0.13.1-scheds-rust-scx_layered-bpf_skel_genskel-bpf.bpf.o  layered_dispatch                   13944      13104      -840 (-6.02%)
third-party-scx-backports-6.9-1.0.13.2-scheds-rust-scx_layered-bpf_skel_genskel-bpf.bpf.o  layered_dispatch                   13944      13104      -840 (-6.02%)
third-party-scx-gefe21962f49a-__scx_layered_bpf_skel_genskel-bpf.bpf.o                     layered_dispatch                   13825      12985      -840 (-6.08%)
third-party-scx-v1.0.16-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_enqueue                       15501      13602    -1899 (-12.25%)
third-party-scx-v1.0.16-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_select_cpu                    19814      16231    -3583 (-18.08%)
third-party-scx-v1.0.17-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_enqueue                       15501      13602    -1899 (-12.25%)
third-party-scx-v1.0.17-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_select_cpu                    19814      16231    -3583 (-18.08%)
third-party-scx-v1.0.17-__scx_layered_bpf_skel_genskel-bpf.bpf.o                           layered_dispatch                   13976      13151      -825 (-5.90%)
third-party-scx-v1.0.18-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_dispatch                     260628     237930    -22698 (-8.71%)
third-party-scx-v1.0.18-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_enqueue                       13437      12225     -1212 (-9.02%)
third-party-scx-v1.0.18-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                              lavd_select_cpu                    17744      14730    -3014 (-16.99%)
third-party-scx-v1.0.19-10-6b1958477-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                 lavd_cpu_offline                   19676      18418     -1258 (-6.39%)
third-party-scx-v1.0.19-10-6b1958477-__scx_lavd_bpf_skel_genskel-bpf.bpf.o                 lavd_cpu_online                    19674      18416     -1258 (-6.39%)

...

Total progs: 1540
Old success: 1492
New success: 1493
total_insns diff min:  -75.83%
total_insns diff max:   73.60%

0 -> value: 0
value -> 0: 0
total_insns abs max old: 434,763
total_insns abs max new: 666,036

 -80 .. -70  %: 2
 -55 .. -50  %: 7
 -50 .. -45  %: 10
 -45 .. -35  %: 4
 -35 .. -25  %: 4
 -25 .. -20  %: 8
 -20 .. -15  %: 15
 -15 .. -10  %: 11
 -10 .. -5   %: 45
  -5 .. 0    %: 112
   0 .. 5    %: 1316
   5 .. 15   %: 2
  15 .. 25   %: 1
  25 .. 35   %: 1
  55 .. 65   %: 1
  70 .. 75   %: 1

========= cilium: master vs patch-set =========

File             Program                            Insns (A)  Insns (B)  Insns     (DIFF)
---------------  ---------------------------------  ---------  ---------  ----------------
bpf_host.o       cil_host_policy                        45801      32027  -13774 (-30.07%)
bpf_host.o       cil_to_netdev                         100287      69042  -31245 (-31.16%)
bpf_host.o       tail_handle_ipv4_cont_from_host        60911      20962  -39949 (-65.59%)
bpf_host.o       tail_handle_ipv4_from_netdev           59735      33155  -26580 (-44.50%)
bpf_host.o       tail_handle_ipv6_cont_from_host        23529      17036   -6493 (-27.60%)
bpf_host.o       tail_handle_ipv6_from_host             11906      10303   -1603 (-13.46%)
bpf_host.o       tail_handle_ipv6_from_netdev           29778      23743   -6035 (-20.27%)
bpf_host.o       tail_handle_snat_fwd_ipv4              61616      67463    +5847 (+9.49%)
bpf_host.o       tail_handle_snat_fwd_ipv6              30802      22806   -7996 (-25.96%)
bpf_host.o       tail_ipv4_host_policy_ingress          20017      10528   -9489 (-47.40%)
bpf_host.o       tail_ipv6_host_policy_ingress          20693      17301   -3392 (-16.39%)
bpf_host.o       tail_nodeport_nat_egress_ipv4          16455      13684   -2771 (-16.84%)
bpf_host.o       tail_nodeport_nat_ingress_ipv4         36174      20080  -16094 (-44.49%)
bpf_host.o       tail_nodeport_nat_ingress_ipv6         48039      25779  -22260 (-46.34%)
bpf_lxc.o        tail_handle_ipv4                       13765      10001   -3764 (-27.34%)
bpf_lxc.o        tail_handle_ipv4_cont                  96891      68725  -28166 (-29.07%)
bpf_lxc.o        tail_handle_ipv6_cont                  21809      17697   -4112 (-18.85%)
bpf_lxc.o        tail_ipv4_ct_egress                    15949      17746   +1797 (+11.27%)
bpf_lxc.o        tail_nodeport_nat_egress_ipv4          16183      13432   -2751 (-17.00%)
bpf_lxc.o        tail_nodeport_nat_ingress_ipv4         18532      10697   -7835 (-42.28%)
bpf_overlay.o    tail_handle_inter_cluster_revsnat      15708      11099   -4609 (-29.34%)
bpf_overlay.o    tail_handle_ipv4                      105672      76108  -29564 (-27.98%)
bpf_overlay.o    tail_handle_ipv6                       15733      19944   +4211 (+26.77%)
bpf_overlay.o    tail_handle_snat_fwd_ipv4              19327      26468   +7141 (+36.95%)
bpf_overlay.o    tail_handle_snat_fwd_ipv6              20817      12556   -8261 (-39.68%)
bpf_overlay.o    tail_nodeport_nat_egress_ipv4          16175      12184   -3991 (-24.67%)
bpf_overlay.o    tail_nodeport_nat_ingress_ipv4         20760      11951   -8809 (-42.43%)
bpf_wireguard.o  tail_handle_ipv4                       27466      28909    +1443 (+5.25%)
bpf_wireguard.o  tail_nodeport_nat_egress_ipv4          15937      12094   -3843 (-24.11%)
bpf_wireguard.o  tail_nodeport_nat_ingress_ipv4         20624      11993   -8631 (-41.85%)
bpf_xdp.o        tail_lb_ipv4                           42673      60855  +18182 (+42.61%)
bpf_xdp.o        tail_lb_ipv6                           87903     108585  +20682 (+23.53%)
bpf_xdp.o        tail_nodeport_nat_ingress_ipv4         28787      20991   -7796 (-27.08%)
bpf_xdp.o        tail_nodeport_nat_ingress_ipv6        207593     152012  -55581 (-26.77%)
Total progs: 134
Old success: 134
New success: 134
total_insns diff min:  -65.59%
total_insns diff max:   42.61%

0 -> value: 0
value -> 0: 0
total_insns abs max old: 207,593
total_insns abs max new: 152,012

 -70 .. -60  %: 1
 -50 .. -40  %: 7
 -40 .. -30  %: 9
 -30 .. -25  %: 9
 -25 .. -20  %: 12
 -20 .. -15  %: 7
 -15 .. -10  %: 14
 -10 .. -5   %: 6
  -5 .. 0    %: 16
   0 .. 5    %: 42
   5 .. 15   %: 5
  15 .. 25   %: 2
  25 .. 35   %: 2
  35 .. 45   %: 2

---
Alexei Starovoitov (5):
bpf: Add spis_*() helpers for 4-byte stack slot bitmasks selftests/bpf: update existing tests due to liveness changes selftests/bpf: adjust verifier_log buffers selftests/bpf: add new tests for static stack liveness analysis
bpf: poison dead stack slots

Eduard Zingerman (9):
bpf: share several utility functions as internal API
bpf: save subprogram name in bpf_subprog_info
bpf: make liveness.c track stack with 4-byte granularity
bpf: 4-byte precise clean_verifier_state
bpf: prepare liveness internal API for static analysis pass
bpf: introduce forward arg-tracking dataflow analysis
bpf: record arg tracking results in bpf_liveness masks
bpf: simplify liveness to use (callsite, depth) keyed func_instances
bpf: change logging scheme for live stack analysis

 include/linux/bpf_verifier.h                       |   65 +-
 kernel/bpf/liveness.c                              | 2091 +++++++++++++----
 kernel/bpf/log.c                                   |    9 +-
 kernel/bpf/verifier.c                              |  311 ++-
 tools/testing/selftests/bpf/prog_tests/verifier.c  |    2 +
 .../selftests/bpf/prog_tests/verifier_log.c        |    6 +-
 .../selftests/bpf/progs/exceptions_assert.c        |    6 +-
 tools/testing/selftests/bpf/progs/uninit_stack.c   |    1 +
 tools/testing/selftests/bpf/progs/verifier_align.c |   10 +-
 .../selftests/bpf/progs/verifier_div_mod_bounds.c  |   18 +-
 .../selftests/bpf/progs/verifier_live_stack.c      | 2411 +++++++++++++++++++-
 .../selftests/bpf/progs/verifier_liveness_exp.c    |  139 ++
 .../selftests/bpf/progs/verifier_scalar_ids.c      |   26 +-
 .../selftests/bpf/progs/verifier_spill_fill.c      |   10 +-
 .../bpf/progs/verifier_subprog_precision.c         |    8 +-
 15 files changed, 4438 insertions(+), 675 deletions(-)

---
base-commit: 6a14beefab457f267b8cedc6ac697a9562ec1244
change-id: 20260408-patch-set-29efd8c0f810

author maintainer
#1 lore raw

PATCH v4 1/14 bpf: share several utility functions as internal API

Namely:
- bpf_subprog_is_global
- bpf_vlog_alignment

---
 include/linux/bpf_verifier.h |  2 ++
 kernel/bpf/log.c             |  4 ++--
 kernel/bpf/verifier.c        | 10 +++++-----
 3 files changed, 9 insertions(+), 7 deletions(-)
include/linux/bpf_verifier.h +2 -0
index 36bfd96d4563..15f7f9f35be9 100644
@@ -1121,12 +1121,14 @@ void print_verifier_state(struct bpf_verifier_env *env, const struct bpf_verifie
 			  u32 frameno, bool print_all);
 void print_insn_state(struct bpf_verifier_env *env, const struct bpf_verifier_state *vstate,
 		      u32 frameno);
+u32 bpf_vlog_alignment(u32 pos);
 
 struct bpf_subprog_info *bpf_find_containing_subprog(struct bpf_verifier_env *env, int off);
 int bpf_jmp_offset(struct bpf_insn *insn);
 struct bpf_iarray *bpf_insn_successors(struct bpf_verifier_env *env, u32 idx);
 void bpf_fmt_stack_mask(char *buf, ssize_t buf_sz, u64 stack_mask);
 bool bpf_calls_callback(struct bpf_verifier_env *env, int insn_idx);
+bool bpf_subprog_is_global(const struct bpf_verifier_env *env, int subprog);
 
 int bpf_find_subprog(struct bpf_verifier_env *env, int off);
 int bpf_compute_const_regs(struct bpf_verifier_env *env);
kernel/bpf/log.c +2 -2
index 37d72b052192..f0902ecb7df6 100644
@@ -847,7 +847,7 @@ void print_verifier_state(struct bpf_verifier_env *env, const struct bpf_verifie
 		mark_verifier_state_clean(env);
 }
 
-static inline u32 vlog_alignment(u32 pos)
+u32 bpf_vlog_alignment(u32 pos)
 {
 	return round_up(max(pos + BPF_LOG_MIN_ALIGNMENT / 2, BPF_LOG_ALIGNMENT),
 			BPF_LOG_MIN_ALIGNMENT) - pos - 1;
@@ -859,7 +859,7 @@ void print_insn_state(struct bpf_verifier_env *env, const struct bpf_verifier_st
 	if (env->prev_log_pos && env->prev_log_pos == env->log.end_pos) {
 		/* remove new line character */
 		bpf_vlog_reset(&env->log, env->prev_log_pos - 1);
-		verbose(env, "%*c;", vlog_alignment(env->prev_insn_print_pos), ' ');
+		verbose(env, "%*c;", bpf_vlog_alignment(env->prev_insn_print_pos), ' ');
 	} else {
 		verbose(env, "%d:", env->insn_idx);
 	}
kernel/bpf/verifier.c +5 -5
index 84699a428077..dd81cfbad680 100644
@@ -421,7 +421,7 @@ static struct btf_record *reg_btf_record(const struct bpf_reg_state *reg)
 	return rec;
 }
 
-static bool subprog_is_global(const struct bpf_verifier_env *env, int subprog)
+bool bpf_subprog_is_global(const struct bpf_verifier_env *env, int subprog)
 {
 	struct bpf_func_info_aux *aux = env->prog->aux->func_info_aux;
 
@@ -4610,7 +4610,7 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
 			if (subprog < 0)
 				return -EFAULT;
 
-			if (subprog_is_global(env, subprog)) {
+			if (bpf_subprog_is_global(env, subprog)) {
 				/* check that jump history doesn't have any
 				 * extra instructions from subprog; the next
 				 * instruction after call to global subprog
@@ -6971,7 +6971,7 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 		if (subprog[idx].has_tail_call)
 			tail_call_reachable = true;
 
-		frame = subprog_is_global(env, idx) ? 0 : frame + 1;
+		frame = bpf_subprog_is_global(env, idx) ? 0 : frame + 1;
 		if (frame >= MAX_CALL_FRAMES) {
 			verbose(env, "the call stack of %d frames is too deep !\n",
 				frame);
@@ -11040,7 +11040,7 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 	err = btf_check_subprog_call(env, subprog, caller->regs);
 	if (err == -EFAULT)
 		return err;
-	if (subprog_is_global(env, subprog)) {
+	if (bpf_subprog_is_global(env, subprog)) {
 		const char *sub_name = subprog_name(env, subprog);
 
 		if (env->cur_state->active_locks) {
@@ -25186,7 +25186,7 @@ static int do_check_subprogs(struct bpf_verifier_env *env)
 again:
 	new_cnt = 0;
 	for (i = 1; i < env->subprog_cnt; i++) {
-		if (!subprog_is_global(env, i))
+		if (!bpf_subprog_is_global(env, i))
 			continue;
 
 		sub_aux = subprog_aux(env, i);
Signature · 1 line
2.53.0
author maintainer
#2 lore raw

PATCH v4 2/14 bpf: save subprogram name in bpf_subprog_info

In reply to Eduard Zingerman

Subprogram name can be computed from function info and BTF, but it is convenient to have the name readily available for logging purposes. Update comment saying that bpf_subprog_info->start has to be the first field, this is no longer true, relevant sites access .start field by it's name.

---
 include/linux/bpf_verifier.h | 2 +-
 kernel/bpf/verifier.c        | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)
include/linux/bpf_verifier.h +1 -1
index 15f7f9f35be9..cec6054d6333 100644
@@ -664,7 +664,7 @@ enum priv_stack_mode {
 };
 
 struct bpf_subprog_info {
-	/* 'start' has to be the first field otherwise find_subprog() won't work */
+	const char *name; /* name extracted from BTF */
 	u32 start; /* insn idx of function entry point */
 	u32 linfo_idx; /* The idx to the main_prog->aux->linfo */
 	u32 postorder_start; /* The idx to the env->cfg.insn_postorder */
kernel/bpf/verifier.c +1 -0
index dd81cfbad680..cd2a56308e59 100644
@@ -19682,6 +19682,7 @@ static int check_btf_func(struct bpf_verifier_env *env,
 			goto err_free;
 		}
 
+		env->subprog_info[i].name = btf_name_by_offset(btf, type->name_off);
 		bpfptr_add(&urecord, urec_size);
 	}
 
Signature · 1 line
2.53.0
author maintainer
#3 lore raw

PATCH v4 3/14 bpf: Add spis_*() helpers for 4-byte stack slot bitmasks

In reply to Eduard Zingerman

From: Alexei Starovoitov <[email protected]>

Add helper functions for manipulating u64[2] bitmasks that represent 4-byte stack slot liveness. The 512-byte BPF stack is divided into 128 4-byte slots, requiring 128 bits (two u64s) to track.

These will be used by the static stack liveness analysis in the next commit.

---
 include/linux/bpf_verifier.h | 67 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)
include/linux/bpf_verifier.h +67 -0
index cec6054d6333..f34e7a074a20 100644
@@ -224,6 +224,73 @@ enum bpf_stack_slot_type {
 
 #define BPF_REG_SIZE 8	/* size of eBPF register in bytes */
 
+/* 4-byte stack slot granularity for liveness analysis */
+#define BPF_HALF_REG_SIZE	4
+#define STACK_SLOTS		(MAX_BPF_STACK / BPF_HALF_REG_SIZE)	/* 128 */
+
+typedef struct {
+	u64 v[2];
+} spis_t;
+
+#define SPIS_ZERO	((spis_t){})
+#define SPIS_ALL	((spis_t){{ U64_MAX, U64_MAX }})
+
+static inline bool spis_is_zero(spis_t s)
+{
+	return s.v[0] == 0 && s.v[1] == 0;
+}
+
+static inline bool spis_equal(spis_t a, spis_t b)
+{
+	return a.v[0] == b.v[0] && a.v[1] == b.v[1];
+}
+
+static inline spis_t spis_or(spis_t a, spis_t b)
+{
+	return (spis_t){{ a.v[0] | b.v[0], a.v[1] | b.v[1] }};
+}
+
+static inline spis_t spis_and(spis_t a, spis_t b)
+{
+	return (spis_t){{ a.v[0] & b.v[0], a.v[1] & b.v[1] }};
+}
+
+static inline spis_t spis_xor(spis_t a, spis_t b)
+{
+	return (spis_t){{ a.v[0] ^ b.v[0], a.v[1] ^ b.v[1] }};
+}
+
+static inline spis_t spis_not(spis_t s)
+{
+	return (spis_t){{ ~s.v[0], ~s.v[1] }};
+}
+
+static inline bool spis_test_bit(spis_t s, u32 slot)
+{
+	return s.v[slot / 64] & BIT_ULL(slot % 64);
+}
+
+static inline void spis_or_range(spis_t *mask, u32 lo, u32 hi)
+{
+	u32 w;
+
+	for (w = lo; w <= hi && w < STACK_SLOTS; w++)
+		mask->v[w / 64] |= BIT_ULL(w % 64);
+}
+
+static inline spis_t spis_one_bit(u32 slot)
+{
+	if (slot < 64)
+		return (spis_t){{ BIT_ULL(slot), 0 }};
+	else
+		return (spis_t){{ 0, BIT_ULL(slot - 64) }};
+}
+
+static inline spis_t spis_single_slot(u32 spi)
+{
+	return spis_or(spis_one_bit(spi * 2), spis_one_bit(spi * 2 + 1));
+}
+
 #define BPF_REGMASK_ARGS ((1 << BPF_REG_1) | (1 << BPF_REG_2) | \
 			  (1 << BPF_REG_3) | (1 << BPF_REG_4) | \
 			  (1 << BPF_REG_5))
Signature · 1 line
2.53.0
author maintainer
#4 lore raw

PATCH v4 4/14 bpf: make liveness.c track stack with 4-byte granularity

In reply to Eduard Zingerman

Convert liveness bitmask type from u64 to spis_t, doubling the number of trackable stack slots from 64 to 128 to support 4-byte granularity.

Each 8-byte SPI now maps to two consecutive 4-byte sub-slots in the
bitmask: spi*2 half and spi*2+1 half. In verifier.c, check_stack_write_fixed_off() now reports 4-byte aligned writes of 4-byte writes as half-slot marks and 8-byte aligned 8-byte writes as two slots. Similar logic applied in check_stack_read_fixed_off().

Queries (is_live_before) are not yet migrated to half-slot granularity.

---
 include/linux/bpf_verifier.h |   4 +-
 kernel/bpf/liveness.c        | 122 ++++++++++++++++++++++++++++---------------
 kernel/bpf/verifier.c        |  55 +++++++++++--------
 3 files changed, 115 insertions(+), 66 deletions(-)
include/linux/bpf_verifier.h +2 -2
index f34e7a074a20..7b31d8024c61 100644
@@ -1217,8 +1217,8 @@ s64 bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env,
 int bpf_stack_liveness_init(struct bpf_verifier_env *env);
 void bpf_stack_liveness_free(struct bpf_verifier_env *env);
 int bpf_update_live_stack(struct bpf_verifier_env *env);
-int bpf_mark_stack_read(struct bpf_verifier_env *env, u32 frameno, u32 insn_idx, u64 mask);
-void bpf_mark_stack_write(struct bpf_verifier_env *env, u32 frameno, u64 mask);
+int bpf_mark_stack_read(struct bpf_verifier_env *env, u32 frameno, u32 insn_idx, spis_t mask);
+void bpf_mark_stack_write(struct bpf_verifier_env *env, u32 frameno, spis_t mask);
 int bpf_reset_stack_write_marks(struct bpf_verifier_env *env, u32 insn_idx);
 int bpf_commit_stack_write_marks(struct bpf_verifier_env *env);
 int bpf_live_stack_query_init(struct bpf_verifier_env *env, struct bpf_verifier_state *st);
kernel/bpf/liveness.c +79 -43

234 lines not on this page · raw message

kernel/bpf/verifier.c +34 -21

127 lines not on this page · raw message

Signature · 1 line
2.53.0
author maintainer
#5 lore raw

PATCH v4 5/14 bpf: 4-byte precise clean_verifier_state

In reply to Eduard Zingerman

Migrate clean_verifier_state() and its liveness queries from 8-byte SPI granularity to 4-byte half-slot granularity.

In __clean_func_state(), each SPI is cleaned in two independent halves:
- half_spi 2*i (lo): slot_type[0..3]
- half_spi 2*i+1 (hi): slot_type[4..7]

Slot types STACK_DYNPTR, STACK_ITER and STACK_IRQ_FLAG are never cleaned, as their slot type markers are required by destroy_if_dynptr_stack_slot(), is_iter_reg_valid_uninit() and is_irq_flag_reg_valid_uninit() for correctness.

When only the hi half is dead, spilled_ptr metadata is destroyed and the lo half's STACK_SPILL bytes are downgraded to STACK_MISC or STACK_ZERO. When only the lo half is dead, spilled_ptr is preserved because the hi half may still need it for state comparison.

---
 kernel/bpf/liveness.c | 14 ++++----
 kernel/bpf/verifier.c | 94 ++++++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 85 insertions(+), 23 deletions(-)
kernel/bpf/liveness.c +7 -7
index 9e36ea5f2eec..e2697cd9ae27 100644
@@ -732,13 +732,12 @@ int bpf_update_live_stack(struct bpf_verifier_env *env)
 	return 0;
 }
 
-static bool is_live_before(struct func_instance *instance, u32 insn_idx, u32 frameno, u32 spi)
+static bool is_live_before(struct func_instance *instance, u32 insn_idx, u32 frameno, u32 half_spi)
 {
 	struct per_frame_masks *masks;
 
 	masks = get_frame_masks(instance, frameno, insn_idx);
-	return masks && (spis_test_bit(masks->live_before, spi * 2) ||
-			 spis_test_bit(masks->live_before, spi * 2 + 1));
+	return masks && spis_test_bit(masks->live_before, half_spi);
 }
 
 int bpf_live_stack_query_init(struct bpf_verifier_env *env, struct bpf_verifier_state *st)
@@ -759,7 +758,7 @@ int bpf_live_stack_query_init(struct bpf_verifier_env *env, struct bpf_verifier_
 	return 0;
 }
 
-bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 spi)
+bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_spi)
 {
 	/*
 	 * Slot is alive if it is read before q->st->insn_idx in current func instance,
@@ -773,15 +772,16 @@ bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 spi)
 	bool alive;
 
 	curframe_instance = q->instances[q->curframe];
-	if (is_live_before(curframe_instance, q->insn_idx, frameno, spi))
+	alive = is_live_before(curframe_instance, q->insn_idx, frameno, half_spi);
+	if (alive)
 		return true;
 
 	for (i = frameno; i < q->curframe; i++) {
 		callsite = curframe_instance->callchain.callsites[i];
 		instance = q->instances[i];
 		alive = bpf_calls_callback(env, callsite)
-			? is_live_before(instance, callsite, frameno, spi)
-			: is_live_before(instance, callsite + 1, frameno, spi);
+			? is_live_before(instance, callsite, frameno, half_spi)
+			: is_live_before(instance, callsite + 1, frameno, half_spi);
 		if (alive)
 			return true;
 	}
kernel/bpf/verifier.c +78 -16

141 lines not on this page · raw message

Signature · 1 line
2.53.0
author maintainer
#6 lore raw

PATCH v4 6/14 bpf: prepare liveness internal API for static analysis pass

In reply to Eduard Zingerman

Move the updated check and reset from bpf_update_live_stack() into update_instance() itself, so callers outside the main loop can reuse it. Similarly, move write_insn_idx assignment out of reset_stack_write_marks() into its public caller, and thread insn_idx as a parameter to commit_stack_write_marks() instead of reading it from liveness->write_insn_idx. Drop the unused env parameter from alloc_frame_masks() and mark_stack_read().

---
 kernel/bpf/liveness.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)
kernel/bpf/liveness.c +23 -25
index e2697cd9ae27..a3af5972520f 100644
@@ -259,8 +259,7 @@ static struct per_frame_masks *get_frame_masks(struct func_instance *instance,
 	return &instance->frames[frame][relative_idx(instance, insn_idx)];
 }
 
-static struct per_frame_masks *alloc_frame_masks(struct bpf_verifier_env *env,
-						 struct func_instance *instance,
+static struct per_frame_masks *alloc_frame_masks(struct func_instance *instance,
 						 u32 frame, u32 insn_idx)
 {
 	struct per_frame_masks *arr;
@@ -298,13 +297,12 @@ static int ensure_cur_instance(struct bpf_verifier_env *env)
 }
 
 /* Accumulate may_read masks for @frame at @insn_idx */
-static int mark_stack_read(struct bpf_verifier_env *env,
-			   struct func_instance *instance, u32 frame, u32 insn_idx, spis_t mask)
+static int mark_stack_read(struct func_instance *instance, u32 frame, u32 insn_idx, spis_t mask)
 {
 	struct per_frame_masks *masks;
 	spis_t new_may_read;
 
-	masks = alloc_frame_masks(env, instance, frame, insn_idx);
+	masks = alloc_frame_masks(instance, frame, insn_idx);
 	if (IS_ERR(masks))
 		return PTR_ERR(masks);
 	new_may_read = spis_or(masks->may_read, mask);
@@ -321,17 +319,15 @@ int bpf_mark_stack_read(struct bpf_verifier_env *env, u32 frame, u32 insn_idx, s
 	int err;
 
 	err = ensure_cur_instance(env);
-	err = err ?: mark_stack_read(env, env->liveness->cur_instance, frame, insn_idx, mask);
+	err = err ?: mark_stack_read(env->liveness->cur_instance, frame, insn_idx, mask);
 	return err;
 }
 
-static void reset_stack_write_marks(struct bpf_verifier_env *env,
-				    struct func_instance *instance, u32 insn_idx)
+static void reset_stack_write_marks(struct bpf_verifier_env *env, struct func_instance *instance)
 {
 	struct bpf_liveness *liveness = env->liveness;
 	int i;
 
-	liveness->write_insn_idx = insn_idx;
 	for (i = 0; i <= instance->callchain.curframe; i++)
 		liveness->write_masks_acc[i] = SPIS_ZERO;
 }
@@ -345,7 +341,8 @@ int bpf_reset_stack_write_marks(struct bpf_verifier_env *env, u32 insn_idx)
 	if (err)
 		return err;
 
-	reset_stack_write_marks(env, liveness->cur_instance, insn_idx);
+	liveness->write_insn_idx = insn_idx;
+	reset_stack_write_marks(env, liveness->cur_instance);
 	return 0;
 }
 
@@ -355,7 +352,8 @@ void bpf_mark_stack_write(struct bpf_verifier_env *env, u32 frame, spis_t mask)
 }
 
 static int commit_stack_write_marks(struct bpf_verifier_env *env,
-				    struct func_instance *instance)
+				    struct func_instance *instance,
+				    u32 insn_idx)
 {
 	struct bpf_liveness *liveness = env->liveness;
 	u32 idx, frame, curframe;
@@ -366,13 +364,13 @@ static int commit_stack_write_marks(struct bpf_verifier_env *env,
 		return 0;
 
 	curframe = instance->callchain.curframe;
-	idx = relative_idx(instance, liveness->write_insn_idx);
+	idx = relative_idx(instance, insn_idx);
 	for (frame = 0; frame <= curframe; frame++) {
 		mask = liveness->write_masks_acc[frame];
 		/* avoid allocating frames for zero masks */
 		if (spis_is_zero(mask) && !instance->must_write_set[idx])
 			continue;
-		masks = alloc_frame_masks(env, instance, frame, liveness->write_insn_idx);
+		masks = alloc_frame_masks(instance, frame, insn_idx);
 		if (IS_ERR(masks))
 			return PTR_ERR(masks);
 		old_must_write = masks->must_write;
@@ -402,7 +400,7 @@ static int commit_stack_write_marks(struct bpf_verifier_env *env,
  */
 int bpf_commit_stack_write_marks(struct bpf_verifier_env *env)
 {
-	return commit_stack_write_marks(env, env->liveness->cur_instance);
+	return commit_stack_write_marks(env, env->liveness->cur_instance, env->liveness->write_insn_idx);
 }
 
 static char *fmt_callchain(struct bpf_verifier_env *env, struct callchain *callchain)
@@ -576,18 +574,17 @@ static int propagate_to_outer_instance(struct bpf_verifier_env *env,
 	if (IS_ERR(outer_instance))
 		return PTR_ERR(outer_instance);
 	callsite = callchain->callsites[callchain->curframe - 1];
-
-	reset_stack_write_marks(env, outer_instance, callsite);
+	reset_stack_write_marks(env, outer_instance);
 	for (frame = 0; frame < callchain->curframe; frame++) {
 		insn = get_frame_masks(instance, frame, this_subprog_start);
 		if (!insn)
 			continue;
 		bpf_mark_stack_write(env, frame, insn->must_write_acc);
-		err = mark_stack_read(env, outer_instance, frame, callsite, insn->live_before);
+		err = mark_stack_read(outer_instance, frame, callsite, insn->live_before);
 		if (err)
 			return err;
 	}
-	commit_stack_write_marks(env, outer_instance);
+	commit_stack_write_marks(env, outer_instance, callsite);
 	return 0;
 }
 
@@ -654,6 +651,9 @@ static int update_instance(struct bpf_verifier_env *env, struct func_instance *i
 	bool changed;
 	int err;
 
+	if (!instance->updated)
+		return 0;
+
 	this_subprog_start = callchain_subprog_start(callchain);
 	/*
 	 * If must_write marks were updated must_write_acc needs to be reset
@@ -699,6 +699,8 @@ static int update_instance(struct bpf_verifier_env *env, struct func_instance *i
 			return err;
 	}
 
+	instance->updated = false;
+	instance->must_write_dropped = false;
 	return 0;
 }
 
@@ -721,13 +723,9 @@ int bpf_update_live_stack(struct bpf_verifier_env *env)
 		if (IS_ERR(instance))
 			return PTR_ERR(instance);
 
-		if (instance->updated) {
-			err = update_instance(env, instance);
-			if (err)
-				return err;
-			instance->updated = false;
-			instance->must_write_dropped = false;
-		}
+		err = update_instance(env, instance);
+		if (err)
+			return err;
 	}
 	return 0;
 }
Signature · 1 line
2.53.0
author maintainer
#7 lore raw

PATCH v4 7/14 bpf: introduce forward arg-tracking dataflow analysis

In reply to Eduard Zingerman

The analysis is a basis for static liveness tracking mechanism introduced by the next two commits.

A forward fixed-point analysis that tracks which frame's FP each register value is derived from, and at what byte offset. This is needed because a callee can receive a pointer to its caller's stack frame (e.g. r1 = fp-16 at the call site), then do *(u64 *)(r1 + 0) inside the callee — a cross-frame stack access that the callee's local liveness must attribute to the caller's stack.

Each register holds an arg_track value from a three-level lattice:
- Precise {frame=N, off=[o1,o2,...]} — known frame index and up to 4 concrete byte offsets
- Offset-imprecise {frame=N, off_cnt=0} — known frame, unknown offset
- Fully-imprecise {frame=ARG_IMPRECISE, mask=bitmask} — unknown frame, mask says which frames might be involved

At CFG merge points the lattice moves toward imprecision (same frame+offset stays precise, same frame different offsets merges offset sets or becomes offset-imprecise, different frames become fully-imprecise with OR'd bitmask).

The analysis also tracks spills/fills to the callee's own stack (at_stack_in/out), so FP derived values spilled and reloaded.

This pass is run recursively per call site: when subprog A calls B with specific FP-derived arguments, B is re-analyzed with those entry args. The recursion follows analyze_subprog -> compute_subprog_args -> (for each call insn) -> analyze_subprog. Subprogs that receive no FP-derived args are skipped during recursion and analyzed independently at depth 0.

---
 include/linux/bpf_verifier.h |    4 +
 kernel/bpf/liveness.c        | 1059 ++++++++++++++++++++++++++++++++++++++++++
 kernel/bpf/verifier.c        |    5 +
 3 files changed, 1068 insertions(+)
include/linux/bpf_verifier.h +4 -0
index 7b31d8024c61..49b19118c326 100644
@@ -226,6 +226,7 @@ enum bpf_stack_slot_type {
 
 /* 4-byte stack slot granularity for liveness analysis */
 #define BPF_HALF_REG_SIZE	4
+#define STACK_SLOT_SZ		4
 #define STACK_SLOTS		(MAX_BPF_STACK / BPF_HALF_REG_SIZE)	/* 128 */
 
 typedef struct {
@@ -886,6 +887,8 @@ struct bpf_verifier_env {
 	} cfg;
 	struct backtrack_state bt;
 	struct bpf_jmp_history_entry *cur_hist_ent;
+	/* Per-callsite copy of parent's converged at_stack_in for cross-frame fills. */
+	struct arg_track **callsite_at_stack;
 	u32 pass_cnt; /* number of times do_check() was called */
 	u32 subprog_cnt;
 	/* number of instructions analyzed by the verifier */
@@ -1213,6 +1216,7 @@ s64 bpf_helper_stack_access_bytes(struct bpf_verifier_env *env,
 s64 bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env,
 				 struct bpf_insn *insn, int arg,
 				 int insn_idx);
+int bpf_compute_subprog_arg_access(struct bpf_verifier_env *env);
 
 int bpf_stack_liveness_init(struct bpf_verifier_env *env);
 void bpf_stack_liveness_free(struct bpf_verifier_env *env);
kernel/bpf/liveness.c +1059 -0

1,095 lines not on this page · raw message

kernel/bpf/verifier.c +5 -0
index 0731e99aa541..e7f03ec7e0e4 100644
@@ -26301,6 +26301,11 @@ static int compute_live_registers(struct bpf_verifier_env *env)
 	for (i = 0; i < insn_cnt; ++i)
 		compute_insn_live_regs(env, &insns[i], &state[i]);
 
+	/* Forward pass: resolve stack access through FP-derived pointers */
+	err = bpf_compute_subprog_arg_access(env);
+	if (err)
+		goto out;
+
 	changed = true;
 	while (changed) {
 		changed = false;
Signature · 1 line
2.53.0
#8 lore raw

Re: [PATCH bpf-next v4 07/14] bpf: introduce forward arg-tracking dataflow analysis

Can parent_stack be NULL here? bpf_compute_subprog_arg_access() allocates callsite_at_stack with kvzalloc_objs (zero-filled), but nothing in this commit ever writes to individual callsite_at_stack[]…
Quoted 4 lines
include/linux/bpf_verifier.h +0 -0
index 7b31d8024c61..49b19118c326 100644

[ ... ]

Quoted 7 lines
include/linux/bpf_verifier.h +2 -0
@@ -886,6 +887,8 @@ struct bpf_verifier_env {
 	} cfg;
 	struct backtrack_state bt;
 	struct bpf_jmp_history_entry *cur_hist_ent;
+	/* Per-callsite copy of parent's converged at_stack_in for cross-frame fills. */
+	struct arg_track **callsite_at_stack;
 	u32 pass_cnt; /* number of times do_check() was called */

[ ... ]

Quoted 4 lines
kernel/bpf/liveness.c +0 -0
index a3af5972520f..c5d6760454d6 100644

[ ... ]

Quoted 5 lines
kernel/bpf/liveness.c +5 -0
+static struct arg_track fill_from_stack(struct bpf_insn *insn,
+					struct arg_track *at_out, int reg,
+					struct arg_track *at_stack_out,
+					int depth)
+{

[ ... ]

Quoted 14 lines
kernel/bpf/liveness.c +14 -0
+	cnt = at_out[reg].off_cnt;
+	if (cnt == 0)
+		return imp;
+
+	for (i = 0; i < cnt; i++) {
+		s16 fp_off = arg_add(at_out[reg].off[i], insn->off);
+		int slot = fp_off_to_slot(fp_off);
+
+		if (slot < 0)
+			return imp;
+		result = __arg_track_join(result, at_stack_out[slot]);
+	}
+	return result;
+}

[ ... ]

Quoted 5 lines
kernel/bpf/liveness.c +5 -0
+	} else if (class == BPF_LDX) {
+		u32 sz = bpf_size_to_bytes(BPF_SIZE(insn->code));
+		bool src_is_local_fp = insn->src_reg == BPF_REG_FP || src->frame == depth ||
+				       (src->frame == ARG_IMPRECISE && (src->mask & BIT(depth)));
+

[ ... ]

kernel/bpf/liveness.c bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_sp · line 1377

Quoted 7 lines
kernel/bpf/liveness.c +7 -0
+		} else if (src->frame >= 0 && src->frame < depth &&
+			   BPF_MODE(insn->code) == BPF_MEM && sz == 8) {
+			struct arg_track *parent_stack =
+				env->callsite_at_stack[callsites[src->frame]];
+
+			*dst = fill_from_stack(insn, at_out, insn->src_reg,
+					       parent_stack, src->frame);

Can parent_stack be NULL here? bpf_compute_subprog_arg_access() allocates callsite_at_stack with kvzalloc_objs (zero-filled), but nothing in this commit ever writes to individual callsite_at_stack[] entries. They all remain NULL.

When a callee receives a parent-frame FP-derived pointer (e.g. r1 with frame=0 at depth 1) and does an 8-byte load from it, this path is taken. fill_from_stack() then dereferences the NULL at_stack_out:

  fill_from_stack()
    cnt = at_out[reg].off_cnt;   /* > 0 for precise args */
    ...
    result = __arg_track_join(result, at_stack_out[slot]);
                                      ^^^^^^^^^^^^^^^^
                                      NULL dereference

This appears to be a bisection hazard. The next commit in the series, 0458daba63a9 ("bpf: record arg tracking results in bpf_liveness masks"), adds the code that populates these entries:

  if (bpf_pseudo_call(insn) || bpf_calls_callback(env, idx)) {
      env->callsite_at_stack[idx] = kvmalloc_objs(...);
      memcpy(env->callsite_at_stack[idx], at_stack_in[i], ...);
  }

Would it make sense to either defer wiring up the bpf_compute_subprog_arg_access() call into compute_live_registers() until the next patch, or move the callsite_at_stack population into this commit?

kernel/bpf/liveness.c bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_sp · line 1378

Quoted 1 line
kernel/bpf/liveness.c +1 -0
+		} else if (src->frame == ARG_IMPRECISE &&

[ ... ]

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/24264407165

author maintainer
#9 lore raw

Re: [PATCH bpf-next v4 07/14] bpf: introduce forward arg-tracking dataflow analysis

Quoted 90 lines · On Fri, 2026-04-10 at 21:44 +0000, [email protected] wrote:
Quoted 4 lines
include/linux/bpf_verifier.h +0 -0
index 7b31d8024c61..49b19118c326 100644

[ ... ]

Quoted 7 lines
include/linux/bpf_verifier.h +2 -0
@@ -886,6 +887,8 @@ struct bpf_verifier_env {
  	} cfg;
  	struct backtrack_state bt;
  	struct bpf_jmp_history_entry *cur_hist_ent;
+	/* Per-callsite copy of parent's converged at_stack_in for cross-frame fills. */
+	struct arg_track **callsite_at_stack;

  u32 pass_cnt; /* number of times do_check() was called */

[ ... ]

Quoted 4 lines
kernel/bpf/liveness.c +0 -0
index a3af5972520f..c5d6760454d6 100644

[ ... ]

Quoted 5 lines
kernel/bpf/liveness.c +5 -0
+static struct arg_track fill_from_stack(struct bpf_insn *insn,
+					struct arg_track *at_out, int reg,
+					struct arg_track *at_stack_out,
+					int depth)
+{

[ ... ]

Quoted 14 lines
kernel/bpf/liveness.c +14 -0
+	cnt = at_out[reg].off_cnt;
+	if (cnt == 0)
+		return imp;
+
+	for (i = 0; i < cnt; i++) {
+		s16 fp_off = arg_add(at_out[reg].off[i], insn->off);
+		int slot = fp_off_to_slot(fp_off);
+
+		if (slot < 0)
+			return imp;
+		result = __arg_track_join(result, at_stack_out[slot]);
+	}
+	return result;
+}

[ ... ]

Quoted 5 lines
kernel/bpf/liveness.c +5 -0
+	} else if (class == BPF_LDX) {
+		u32 sz = bpf_size_to_bytes(BPF_SIZE(insn->code));
+		bool src_is_local_fp = insn->src_reg == BPF_REG_FP || src->frame == depth ||
+				       (src->frame == ARG_IMPRECISE && (src->mask & BIT(depth)));
+

[ ... ]

kernel/bpf/liveness.c bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_sp · line 1377

Quoted 7 lines
kernel/bpf/liveness.c +7 -0
+		} else if (src->frame >= 0 && src->frame < depth &&
+			   BPF_MODE(insn->code) == BPF_MEM && sz == 8) {
+			struct arg_track *parent_stack =
+				env->callsite_at_stack[callsites[src->frame]];
+
+			*dst = fill_from_stack(insn, at_out, insn->src_reg,
+					       parent_stack, src->frame);

Can parent_stack be NULL here? bpf_compute_subprog_arg_access() allocates callsite_at_stack with kvzalloc_objs (zero-filled), but nothing in this commit ever writes to individual callsite_at_stack[] entries. They all remain NULL.

When a callee receives a parent-frame FP-derived pointer (e.g. r1 with frame=0 at depth 1) and does an 8-byte load from it, this path is taken. fill_from_stack() then dereferences the NULL at_stack_out:

  fill_from_stack()
    cnt = at_out[reg].off_cnt; /* > 0 for precise args */
    ...
    result = __arg_track_join(result, at_stack_out[slot]); ^^^^^^^^^^^^^^^^ NULL dereference

This appears to be a bisection hazard. The next commit in the series, 0458daba63a9 ("bpf: record arg tracking results in bpf_liveness masks"), adds the code that populates these entries:

  if (bpf_pseudo_call(insn) || bpf_calls_callback(env, idx)) { env->callsite_at_stack[idx] = kvmalloc_objs(...); memcpy(env->callsite_at_stack[idx], at_stack_in[i], ...); }

Quoted 4 lines

Would it make sense to either defer wiring up the bpf_compute_subprog_arg_access() call into compute_live_registers() until the next patch, or move the callsite_at_stack population into this commit?

Yeap, should be moved.

Quoted 11 lines

kernel/bpf/liveness.c bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_sp · line 1378

Quoted 2 lines
kernel/bpf/liveness.c +1 -0
+		} else if (src->frame == ARG_IMPRECISE &&

[ ... ]

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/24264407165

#10 lore raw

Re: [PATCH bpf-next v4 07/14] bpf: introduce forward arg-tracking dataflow analysis

Quoted 92 lines · Eduard Zingerman
Quoted 90 lines · On Fri, 2026-04-10 at 21:44 +0000, [email protected] wrote:
Quoted 4 lines
include/linux/bpf_verifier.h +0 -0
index 7b31d8024c61..49b19118c326 100644

[ ... ]

Quoted 7 lines
include/linux/bpf_verifier.h +2 -0
@@ -886,6 +887,8 @@ struct bpf_verifier_env {
    } cfg;
    struct backtrack_state bt;
    struct bpf_jmp_history_entry *cur_hist_ent;
+   /* Per-callsite copy of parent's converged at_stack_in for cross-frame fills. */
+   struct arg_track **callsite_at_stack;
    u32 pass_cnt; /* number of times do_check() was called */

[ ... ]

Quoted 4 lines
kernel/bpf/liveness.c +0 -0
index a3af5972520f..c5d6760454d6 100644

[ ... ]

Quoted 5 lines
kernel/bpf/liveness.c +5 -0
+static struct arg_track fill_from_stack(struct bpf_insn *insn,
+                                   struct arg_track *at_out, int reg,
+                                   struct arg_track *at_stack_out,
+                                   int depth)
+{

[ ... ]

Quoted 14 lines
kernel/bpf/liveness.c +14 -0
+   cnt = at_out[reg].off_cnt;
+   if (cnt == 0)
+           return imp;
+
+   for (i = 0; i < cnt; i++) {
+           s16 fp_off = arg_add(at_out[reg].off[i], insn->off);
+           int slot = fp_off_to_slot(fp_off);
+
+           if (slot < 0)
+                   return imp;
+           result = __arg_track_join(result, at_stack_out[slot]);
+   }
+   return result;
+}

[ ... ]

Quoted 5 lines
kernel/bpf/liveness.c +5 -0
+   } else if (class == BPF_LDX) {
+           u32 sz = bpf_size_to_bytes(BPF_SIZE(insn->code));
+           bool src_is_local_fp = insn->src_reg == BPF_REG_FP || src->frame == depth ||
+                                  (src->frame == ARG_IMPRECISE && (src->mask & BIT(depth)));
+

[ ... ]

kernel/bpf/liveness.c bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_sp · line 1377

Quoted 7 lines
kernel/bpf/liveness.c +7 -0
+           } else if (src->frame >= 0 && src->frame < depth &&
+                      BPF_MODE(insn->code) == BPF_MEM && sz == 8) {
+                   struct arg_track *parent_stack =
+                           env->callsite_at_stack[callsites[src->frame]];
+
+                   *dst = fill_from_stack(insn, at_out, insn->src_reg,
+                                          parent_stack, src->frame);

Can parent_stack be NULL here? bpf_compute_subprog_arg_access() allocates callsite_at_stack with kvzalloc_objs (zero-filled), but nothing in this commit ever writes to individual callsite_at_stack[] entries. They all remain NULL.

When a callee receives a parent-frame FP-derived pointer (e.g. r1 with frame=0 at depth 1) and does an 8-byte load from it, this path is taken. fill_from_stack() then dereferences the NULL at_stack_out:

  fill_from_stack()
    cnt = at_out[reg].off_cnt;   /* > 0 for precise args */
    ...
    result = __arg_track_join(result, at_stack_out[slot]);
                                      ^^^^^^^^^^^^^^^^
                                      NULL dereference

This appears to be a bisection hazard. The next commit in the series, 0458daba63a9 ("bpf: record arg tracking results in bpf_liveness masks"), adds the code that populates these entries:

  if (bpf_pseudo_call(insn) || bpf_calls_callback(env, idx)) {
      env->callsite_at_stack[idx] = kvmalloc_objs(...);
      memcpy(env->callsite_at_stack[idx], at_stack_in[i], ...);
  }
Quoted 6 lines
Quoted 4 lines

Would it make sense to either defer wiring up the bpf_compute_subprog_arg_access() call into compute_live_registers() until the next patch, or move the callsite_at_stack population into this commit?

Yeap, should be moved.

No point. The whole thing is not really bisectable.

author maintainer
#11 lore raw

PATCH v4 8/14 bpf: record arg tracking results in bpf_liveness masks

In reply to Eduard Zingerman

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.

---
 include/linux/bpf_verifier.h |   1 -
 kernel/bpf/liveness.c        | 243 +++++++++++++++++++++++++++++++++++++++++++
 kernel/bpf/verifier.c        |  64 +-----------
 3 files changed, 245 insertions(+), 63 deletions(-)
include/linux/bpf_verifier.h +0 -1
index 49b19118c326..8e83a5e66fd8 100644
@@ -492,7 +492,6 @@ struct bpf_verifier_state {
 
 	bool speculative;
 	bool in_sleepable;
-	bool cleaned;
 
 	/* first and last insn idx of this verifier state */
 	u32 first_insn_idx;
kernel/bpf/liveness.c +243 -0

255 lines not on this page · raw message

kernel/bpf/verifier.c +2 -62

94 lines not on this page · raw message

Signature · 1 line
2.53.0
author maintainer
#12 lore raw

PATCH v4 9/14 bpf: simplify liveness to use (callsite, depth) keyed func_instances

In reply to Eduard Zingerman

Rework func_instance identification and remove the dynamic liveness API, completing the transition to fully static stack liveness analysis.

Replace callchain-based func_instance keys with (callsite, depth) pairs. The full callchain (all ancestor callsites) is no longer part of the hash key; only the immediate callsite and the call depth matter. This does not lose precision in practice and simplifies the data structure significantly: struct callchain is removed entirely, func_instance stores just callsite, depth.

Drop must_write_acc propagation. Previously, must_write marks were accumulated across successors and propagated to the caller via propagate_to_outer_instance(). Instead, callee entry liveness (live_before at subprog start) is pulled directly back to the caller's callsite in analyze_subprog() after each callee returns.

Since (callsite, depth) instances are shared across different call chains that invoke the same subprog at the same depth, must_write marks from one call may be stale for another. To handle this, analyze_subprog() records into a fresh_instance() when the instance was already visited (must_write_initialized), then merge_instances() combines the results: may_read is unioned, must_write is intersected. This ensures only slots written on ALL paths through all call sites are marked as guaranteed writes.
This replaces commit_stack_write_marks() logic.

Skip recursive descent into callees that receive no FP-derived arguments (has_fp_args() check). This is needed because global subprogram calls can push depth beyond MAX_CALL_FRAMES (max depth is 64 for global calls but only 8 frames are accommodated for FP passing). It also handles the case where a callback subprog cannot be determined by argument tracking: such callbacks will be processed by analyze_subprog() at depth 0 independently.

Update lookup_instance() (used by is_live_before queries) to search for the func_instance with maximal depth at the corresponding callsite, walking depth downward from frameno to 0. This accounts for the fact that instance depth no longer corresponds 1:1 to bpf_verifier_state->curframe, since skipped non-FP calls create gaps.

Remove the dynamic public liveness API from verifier.c:
- bpf_mark_stack_{read,write}(), bpf_reset/commit_stack_write_marks()
- bpf_update_live_stack(), bpf_reset_live_stack_callchain()
- All call sites in check_stack_{read,write}_fixed_off(), check_stack_range_initialized(), mark_stack_slot_obj_read(), mark/unmark_stack_slots_{dynptr,iter,irq_flag}()
- The per-instruction write mark accumulation in do_check()
- The bpf_update_live_stack() call in prepare_func_exit()

mark_stack_read() and mark_stack_write() become static functions in liveness.c, called only from the static analysis pass. The func_instance->updated and must_write_dropped flags are removed. Remove spis_single_slot(), spis_one_bit() helpers from bpf_verifier.h as they are no longer used.

---
 include/linux/bpf_verifier.h |  19 --
 kernel/bpf/liveness.c        | 660 ++++++++++++++++---------------------------
 kernel/bpf/verifier.c        |  79 +-----
 3 files changed, 254 insertions(+), 504 deletions(-)
include/linux/bpf_verifier.h +0 -19
index 8e83a5e66fd8..d7fbc0e1559b 100644
@@ -279,19 +279,6 @@ static inline void spis_or_range(spis_t *mask, u32 lo, u32 hi)
 		mask->v[w / 64] |= BIT_ULL(w % 64);
 }
 
-static inline spis_t spis_one_bit(u32 slot)
-{
-	if (slot < 64)
-		return (spis_t){{ BIT_ULL(slot), 0 }};
-	else
-		return (spis_t){{ 0, BIT_ULL(slot - 64) }};
-}
-
-static inline spis_t spis_single_slot(u32 spi)
-{
-	return spis_or(spis_one_bit(spi * 2), spis_one_bit(spi * 2 + 1));
-}
-
 #define BPF_REGMASK_ARGS ((1 << BPF_REG_1) | (1 << BPF_REG_2) | \
 			  (1 << BPF_REG_3) | (1 << BPF_REG_4) | \
 			  (1 << BPF_REG_5))
@@ -1219,13 +1206,7 @@ int bpf_compute_subprog_arg_access(struct bpf_verifier_env *env);
 
 int bpf_stack_liveness_init(struct bpf_verifier_env *env);
 void bpf_stack_liveness_free(struct bpf_verifier_env *env);
-int bpf_update_live_stack(struct bpf_verifier_env *env);
-int bpf_mark_stack_read(struct bpf_verifier_env *env, u32 frameno, u32 insn_idx, spis_t mask);
-void bpf_mark_stack_write(struct bpf_verifier_env *env, u32 frameno, spis_t mask);
-int bpf_reset_stack_write_marks(struct bpf_verifier_env *env, u32 insn_idx);
-int bpf_commit_stack_write_marks(struct bpf_verifier_env *env);
 int bpf_live_stack_query_init(struct bpf_verifier_env *env, struct bpf_verifier_state *st);
 bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 spi);
-void bpf_reset_live_stack_callchain(struct bpf_verifier_env *env);
 
 #endif /* _LINUX_BPF_VERIFIER_H */
kernel/bpf/liveness.c +250 -410

974 lines not on this page · raw message

kernel/bpf/verifier.c +4 -75

184 lines not on this page · raw message

Signature · 1 line
2.53.0
Tested-by on v4 9/14
#13 lore raw

Re: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances

Quoted 49 lines · Eduard Zingerman

Rework func_instance identification and remove the dynamic liveness API, completing the transition to fully static stack liveness analysis.

Replace callchain-based func_instance keys with (callsite, depth) pairs. The full callchain (all ancestor callsites) is no longer part of the hash key; only the immediate callsite and the call depth matter. This does not lose precision in practice and simplifies the data structure significantly: struct callchain is removed entirely, func_instance stores just callsite, depth.

Drop must_write_acc propagation. Previously, must_write marks were accumulated across successors and propagated to the caller via propagate_to_outer_instance(). Instead, callee entry liveness (live_before at subprog start) is pulled directly back to the caller's callsite in analyze_subprog() after each callee returns.

Since (callsite, depth) instances are shared across different call chains that invoke the same subprog at the same depth, must_write marks from one call may be stale for another. To handle this, analyze_subprog() records into a fresh_instance() when the instance was already visited (must_write_initialized), then merge_instances() combines the results: may_read is unioned, must_write is intersected. This ensures only slots written on ALL paths through all call sites are marked as guaranteed writes.
This replaces commit_stack_write_marks() logic.

Skip recursive descent into callees that receive no FP-derived arguments (has_fp_args() check). This is needed because global subprogram calls can push depth beyond MAX_CALL_FRAMES (max depth is 64 for global calls but only 8 frames are accommodated for FP passing). It also handles the case where a callback subprog cannot be determined by argument tracking: such callbacks will be processed by analyze_subprog() at depth 0 independently.

Update lookup_instance() (used by is_live_before queries) to search for the func_instance with maximal depth at the corresponding callsite, walking depth downward from frameno to 0. This accounts for the fact that instance depth no longer corresponds 1:1 to bpf_verifier_state->curframe, since skipped non-FP calls create gaps.

Remove the dynamic public liveness API from verifier.c:
- bpf_mark_stack_{read,write}(), bpf_reset/commit_stack_write_marks()
- bpf_update_live_stack(), bpf_reset_live_stack_callchain()
- All call sites in check_stack_{read,write}_fixed_off(), check_stack_range_initialized(), mark_stack_slot_obj_read(), mark/unmark_stack_slots_{dynptr,iter,irq_flag}()
- The per-instruction write mark accumulation in do_check()
- The bpf_update_live_stack() call in prepare_func_exit()

Quoted 8 lines

mark_stack_read() and mark_stack_write() become static functions in liveness.c, called only from the static analysis pass. The func_instance->updated and must_write_dropped flags are removed. Remove spis_single_slot(), spis_one_bit() helpers from bpf_verifier.h as they are no longer used.

I tested this series with Cilium's complexity test suite [1]. We have a lot of different configuration and some producing larger programs than covered in the cover letter.

Overall, the impact is quite good. It reduces the number of processed instructions by more than 60k instructions for some of our largest programs. It also increases the number of processed instructions by up to 20k in fewer other cases, but I think it's manageable because they are not the largest programs so we have some room. The mean diff over all programs is a reduction of a few thousands processed instructions.

For good measure, I also ran this patchset through a subset of Cilium's CI. All looks good.

For the whole series, but probably only needs to be applied for this patch:

1: https://pchaigno.github.io/test-verifier-complexity.html

[...]

author maintainer
#14 lore raw

Re: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances

On Fri, 2026-04-10 at 23:39 +0200, Paul Chaignon wrote:

[...]

Quoted 10 lines

I tested this series with Cilium's complexity test suite [1]. We have a lot of different configuration and some producing larger programs than covered in the cover letter.

Overall, the impact is quite good. It reduces the number of processed instructions by more than 60k instructions for some of our largest programs. It also increases the number of processed instructions by up to 20k in fewer other cases, but I think it's manageable because they are not the largest programs so we have some room. The mean diff over all programs is a reduction of a few thousands processed instructions.

Hi Paul,

Thank you for testing this series!
Are the regressing program open-source?
If so, could you please point to a few?

[...]

#15 lore raw

Re: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances

Quoted 15 lines · Eduard Zingerman

On Fri, 2026-04-10 at 23:39 +0200, Paul Chaignon wrote:

[...]

Quoted 10 lines

I tested this series with Cilium's complexity test suite [1]. We have a lot of different configuration and some producing larger programs than covered in the cover letter.

Overall, the impact is quite good. It reduces the number of processed instructions by more than 60k instructions for some of our largest programs. It also increases the number of processed instructions by up to 20k in fewer other cases, but I think it's manageable because they are not the largest programs so we have some room. The mean diff over all programs is a reduction of a few thousands processed instructions.

Quoted 5 lines

Hi Paul,

Thank you for testing this series!
Are the regressing program open-source?
If so, could you please point to a few?

Hi Eduard,

Yes, all programs tested are from Cilium OSS. [1] shows the overall results grouped by program (each line covers multiple configurations).

I've uploaded details for programs (a) tail_nodeport_nat_ingress_ipv4 and (b) tail_handle_ipv4_cont with configurations for which your patchset causes an increase in complexity. The object file at [2] contains program (a) compiled with the Cilium configuration from [3]. The impact of your patchset on that program is at [4]. For program (b), see [5, 6, 7].

These programs are quite large so not the easiest to dig into. Unfortunately, I'm not seeing anything obvious that differentiate the programs with a positive impact from those with a negative impact.

Don't hesitate if I can do anything to help! I can also share the verifier logs if that helps.

1: https://pchaigno.github.io/assets/static-liveness-complexity/complexity-results.png
2: https://pchaigno.github.io/assets/static-liveness-complexity/bpf_host-3.o
3: https://github.com/cilium/cilium/blob/12955851fc2f0c25f01d0df27a66c0ed6c275f59/bpf/complexity-tests/netnext/bpf_host/3.txt
4: https://pchaigno.github.io/assets/static-liveness-complexity/bpf_host-build3.png
5: https://pchaigno.github.io/assets/static-liveness-complexity/bpf_lxc-7.o
6: https://github.com/cilium/cilium/blob/12955851fc2f0c25f01d0df27a66c0ed6c275f59/bpf/complexity-tests/netnext/bpf_lxc/7.txt
7: https://pchaigno.github.io/assets/static-liveness-complexity/bpf_lxc-build7.png

Quoted 2 lines

[...]

#16 lore raw

Re: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances

In reply to Eduard Zingerman
The file header comment block still describes the old callchain-based architecture, but this commit removes struct callchain and replaces it with (callsite, depth) keying.
Quoted 4 lines
kernel/bpf/liveness.c +0 -0
index 2729ed965f62..1c8477069a43 100644

[ ... ]

kernel/bpf/liveness.c line 72

Quoted 3 lines
kernel/bpf/liveness.c +0 -0
@@ -85,158 +71,111 @@
 * - A hash table mapping call chains to function instances.
 */

The file header comment block still describes the old callchain-based architecture, but this commit removes struct callchain and replaces it with (callsite, depth) keying.

For example, the data structure summary near the end of the block says:

* - Call chain: for given verifier state, the call chain is
* a tuple of call instruction indexes leading to the current
* subprogram plus the subprogram entry point index.
* - Function instance: for a given call chain, ...
* - A hash table mapping call chains to function instances.

But the struct comment and the hash table inline comment were updated to reflect the new design:

* A function instance keyed by (callsite, depth).

DECLARE_HASHTABLE(func_instances, 8); /* maps (depth, callsite) to func_instance */

Should the file header comment be updated to match the new (callsite, depth) keying, or is leaving the "call chain" terminology intentional?

Quoted 8 lines
kernel/bpf/liveness.c +0 -6
-struct callchain {
-	u32 callsites[MAX_CALL_FRAMES];
-	u32 sp_starts[MAX_CALL_FRAMES];
-	u32 curframe;
-};
-
 struct per_frame_masks {

[ ... ]

kernel/bpf/liveness.c

Quoted 10 lines
kernel/bpf/liveness.c +7 -1
+/*
+ * A function instance keyed by (callsite, depth).
+ * Encapsulates read and write marks for each instruction in the function.
+ * Marks are tracked for each frame up to @depth.
+ */
 struct func_instance {
 	struct hlist_node hl_node;
-	struct callchain callchain;
+	u32 callsite;
+	u32 depth;

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/24264407165

#17 lore raw

Re: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances

Quoted 8 lines · On Fri, Apr 10, 2026 at 2:44 PM <[email protected]> wrote:
Quoted 5 lines
kernel/bpf/liveness.c +0 -0
index 2729ed965f62..1c8477069a43 100644

[ ... ]

Quoted 7 lines

kernel/bpf/liveness.c line 72

Quoted 3 lines
kernel/bpf/liveness.c +0 -0
@@ -85,158 +71,111 @@
 * - A hash table mapping call chains to function instances.
 */

The file header comment block still describes the old callchain-based architecture, but this commit removes struct callchain and replaces it with (callsite, depth) keying.

Not only that. Most of that comment is no longer valid. It needs to be completely rewritten,
so I deleted the whole thing while applying.

tbh I wouldn't follow up with a doc just yet. We might keep changing it.

author maintainer
#18 lore raw

PATCH v4 10/14 bpf: change logging scheme for live stack analysis

In reply to Eduard Zingerman

Instead of breadcrumbs like:

(d2,cs15) frame 0 insn 18 +live -16
(d2,cs15) frame 0 insn 17 +live -16

Print final accumulated stack use/def data per-func_instance per-instruction. printed func_instance's are ordered by callsite and depth. For example:

stack use/def subprog#0 shared_instance_must_write_overwrite (d0,cs0):

    0: (b7) r1 = 1
    1: (7b) *(u64 *)(r10 -8) = r1        ; def: fp0-8
    2: (7b) *(u64 *)(r10 -16) = r1       ; def: fp0-16
    3: (bf) r1 = r10
    4: (07) r1 += -8
    5: (bf) r2 = r10
    6: (07) r2 += -16
    7: (85) call pc+7                    ; use: fp0-8 fp0-16
    8: (bf) r1 = r10
    9: (07) r1 += -16

10: (bf) r2 = r10
11: (07) r2 += -8
12: (85) call pc+2 ; use: fp0-8 fp0-16
13: (b7) r0 = 0
14: (95) exit
stack use/def subprog#1 forwarding_rw (d1,cs7):
15: (85) call pc+1 ; use: fp0-8 fp0-16
16: (95) exit
stack use/def subprog#1 forwarding_rw (d1,cs12):
15: (85) call pc+1 ; use: fp0-8 fp0-16
16: (95) exit
stack use/def subprog#2 write_first_read_second (d2,cs15):
17: (7a) *(u64 *)(r1 +0) = 42
18: (79) r0 = *(u64 *)(r2 +0) ; use: fp0-8 fp0-16
19: (95) exit

For groups of three or more consecutive stack slots, abbreviate as follows:

25: (85) call bpf_loop#181 ; use: fp2-8..-512 fp1-8..-512 fp0-8..-512

---
 include/linux/bpf_verifier.h |   5 -
 kernel/bpf/liveness.c        | 241 +++++++++++++++++++++++++++++--------------
 2 files changed, 165 insertions(+), 81 deletions(-)
include/linux/bpf_verifier.h +0 -5
index d7fbc0e1559b..d3dc46aae2e7 100644
@@ -256,11 +256,6 @@ static inline spis_t spis_and(spis_t a, spis_t b)
 	return (spis_t){{ a.v[0] & b.v[0], a.v[1] & b.v[1] }};
 }
 
-static inline spis_t spis_xor(spis_t a, spis_t b)
-{
-	return (spis_t){{ a.v[0] ^ b.v[0], a.v[1] ^ b.v[1] }};
-}
-
 static inline spis_t spis_not(spis_t s)
 {
 	return (spis_t){{ ~s.v[0], ~s.v[1] }};
kernel/bpf/liveness.c +165 -76

289 lines not on this page · raw message

Signature · 1 line
2.53.0
#19 lore raw

Re: [PATCH bpf-next v4 10/14] bpf: change logging scheme for live stack analysis

Quoted 8 lines · Eduard Zingerman

Instead of breadcrumbs like:

(d2,cs15) frame 0 insn 18 +live -16
(d2,cs15) frame 0 insn 17 +live -16

Print final accumulated stack use/def data per-func_instance per-instruction. printed func_instance's are ordered by callsite and depth. For example:

Hi Eduard,

Sorry to revive an old thread.

We've started running a kernel with this patchset in Cilium's CI and noticed a big increase of log verbosity for BPF_LOG_LEVEL2. To get an idea, a full dump of all (uncompressed) verifier logs for our tested programs used to take ~8.4G. With this patchset, it now takes 15G. [1] helps a bit to reduce it, but still only getting to 12G.

The increase seems to come from print_subprog_arg_access(), which prints full functions with the results of the fixed-point analysis, from print_instances(), which prints full functions with the use/def slots, and to a lesser extent from compute_subprog_args(), which prints the fixed-point iterations.

I'm wondering if maybe there are other opportunities to reduce verbosity here (besides [1]). Maybe we don't need to print the fixed-point iterations if we're already printing the results? Or maybe we could put the more detailed liveness-related logs behind BPF_LOG_LEVEL3 or BPF_LOG_LIVENESS?

1: https://lore.kernel.org/bpf/e4d7b53d4963ef520541a782f5fc8108a168877c.1778176504.git.paul.chaignon@gmail.com/

Quoted 360 lines

stack use/def subprog#0 shared_instance_must_write_overwrite (d0,cs0):

    0: (b7) r1 = 1
    1: (7b) *(u64 *)(r10 -8) = r1        ; def: fp0-8
    2: (7b) *(u64 *)(r10 -16) = r1       ; def: fp0-16
    3: (bf) r1 = r10
    4: (07) r1 += -8
    5: (bf) r2 = r10
    6: (07) r2 += -16
    7: (85) call pc+7                    ; use: fp0-8 fp0-16
    8: (bf) r1 = r10
    9: (07) r1 += -16
   10: (bf) r2 = r10
   11: (07) r2 += -8
   12: (85) call pc+2                    ; use: fp0-8 fp0-16
   13: (b7) r0 = 0
   14: (95) exit
  stack use/def subprog#1 forwarding_rw (d1,cs7):
   15: (85) call pc+1                    ; use: fp0-8 fp0-16
   16: (95) exit
  stack use/def subprog#1 forwarding_rw (d1,cs12):
   15: (85) call pc+1                    ; use: fp0-8 fp0-16
   16: (95) exit
  stack use/def subprog#2 write_first_read_second (d2,cs15):
   17: (7a) *(u64 *)(r1 +0) = 42
   18: (79) r0 = *(u64 *)(r2 +0)         ; use: fp0-8 fp0-16
   19: (95) exit

For groups of three or more consecutive stack slots, abbreviate as follows:

   25: (85) call bpf_loop#181            ; use: fp2-8..-512 fp1-8..-512 fp0-8..-512
---
 include/linux/bpf_verifier.h |   5 -
 kernel/bpf/liveness.c        | 241 +++++++++++++++++++++++++++++--------------
 2 files changed, 165 insertions(+), 81 deletions(-)
include/linux/bpf_verifier.h +0 -5
index d7fbc0e1559b..d3dc46aae2e7 100644
@@ -256,11 +256,6 @@ static inline spis_t spis_and(spis_t a, spis_t b)
 	return (spis_t){{ a.v[0] & b.v[0], a.v[1] & b.v[1] }};
 }
 
-static inline spis_t spis_xor(spis_t a, spis_t b)
-{
-	return (spis_t){{ a.v[0] ^ b.v[0], a.v[1] ^ b.v[1] }};
-}
-
 static inline spis_t spis_not(spis_t s)
 {
 	return (spis_t){{ ~s.v[0], ~s.v[1] }};
kernel/bpf/liveness.c +165 -76
index 1c8477069a43..b8005094c6a6 100644
@@ -6,6 +6,7 @@
 #include <linux/hashtable.h>
 #include <linux/jhash.h>
 #include <linux/slab.h>
+#include <linux/sort.h>
 
 #define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
 
@@ -259,64 +260,6 @@ static int mark_stack_write(struct func_instance *instance, u32 frame, u32 insn_
 	return 0;
 }
 
-static char *fmt_instance(struct bpf_verifier_env *env, struct func_instance *instance)
-{
-	snprintf(env->tmp_str_buf, sizeof(env->tmp_str_buf),
-		 "(d%d,cs%d)", instance->depth, instance->callsite);
-	return env->tmp_str_buf;
-}
-
-/*
- * When both halves of an 8-byte SPI are set, print as "-8","-16",...
- * When only one half is set, print as "-4h","-8h",...
- */
-static void bpf_fmt_spis_mask(char *buf, ssize_t buf_sz, spis_t spis)
-{
-	bool first = true;
-	int spi, n;
-
-	buf[0] = '\0';
-
-	for (spi = 0; spi < STACK_SLOTS / 2 && buf_sz > 0; spi++) {
-		bool lo = spis_test_bit(spis, spi * 2);
-		bool hi = spis_test_bit(spis, spi * 2 + 1);
-
-		if (!lo && !hi)
-			continue;
-		n = snprintf(buf, buf_sz, "%s%d%s",
-			     first ? "" : ",",
-			     -(spi + 1) * BPF_REG_SIZE + (lo && !hi ? BPF_HALF_REG_SIZE : 0),
-			     lo && hi ? "" : "h");
-		first = false;
-		buf += n;
-		buf_sz -= n;
-	}
-}
-
-static void log_mask_change(struct bpf_verifier_env *env, struct func_instance *instance,
-			    char *pfx, u32 frame, u32 insn_idx,
-			    spis_t old, spis_t new)
-{
-	spis_t changed_bits, new_ones, new_zeros;
-
-	changed_bits = spis_xor(old, new);
-	new_ones = spis_and(new, changed_bits);
-	new_zeros = spis_and(spis_not(new), changed_bits);
-
-	if (spis_is_zero(changed_bits))
-		return;
-	bpf_log(&env->log, "%s frame %d insn %d ", fmt_instance(env, instance), frame, insn_idx);
-	if (!spis_is_zero(new_ones)) {
-		bpf_fmt_spis_mask(env->tmp_str_buf, sizeof(env->tmp_str_buf), new_ones);
-		bpf_log(&env->log, "+%s %s ", pfx, env->tmp_str_buf);
-	}
-	if (!spis_is_zero(new_zeros)) {
-		bpf_fmt_spis_mask(env->tmp_str_buf, sizeof(env->tmp_str_buf), new_zeros);
-		bpf_log(&env->log, "-%s %s", pfx, env->tmp_str_buf);
-	}
-	bpf_log(&env->log, "\n");
-}
-
 int bpf_jmp_offset(struct bpf_insn *insn)
 {
 	u8 code = insn->code;
@@ -392,7 +335,6 @@ __diag_pop();
 static inline bool update_insn(struct bpf_verifier_env *env,
 			       struct func_instance *instance, u32 frame, u32 insn_idx)
 {
-	struct bpf_insn_aux_data *aux = env->insn_aux_data;
 	spis_t new_before, new_after;
 	struct per_frame_masks *insn, *succ_insn;
 	struct bpf_iarray *succ;
@@ -419,13 +361,6 @@ static inline bool update_insn(struct bpf_verifier_env *env,
 	new_before = spis_or(spis_and(new_after, spis_not(insn->must_write)),
 			     insn->may_read);
 	changed |= !spis_equal(new_before, insn->live_before);
-	if (unlikely(env->log.level & BPF_LOG_LEVEL2) &&
-	    (!spis_is_zero(insn->may_read) || !spis_is_zero(insn->must_write) ||
-	     insn_idx == instance->subprog_start ||
-	     aux[insn_idx].prune_point)) {
-		log_mask_change(env, instance, "live",
-				frame, insn_idx, insn->live_before, new_before);
-	}
 	insn->live_before = new_before;
 	return changed;
 }
@@ -455,10 +390,6 @@ static void update_instance(struct bpf_verifier_env *env, struct func_instance *
 				changed |= update_insn(env, instance, frame, insn_postorder[i]);
 		}
 	} while (changed);
-
-	if (env->log.level & BPF_LOG_LEVEL2)
-		bpf_log(&env->log, "%s live stack update done in %d iterations\n",
-			fmt_instance(env, instance), cnt);
 }
 
 static bool is_live_before(struct func_instance *instance, u32 insn_idx, u32 frameno, u32 half_spi)
@@ -538,6 +469,166 @@ bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 half_sp
 	return false;
 }
 
+static char *fmt_subprog(struct bpf_verifier_env *env, int subprog)
+{
+	const char *name = env->subprog_info[subprog].name;
+
+	snprintf(env->tmp_str_buf, sizeof(env->tmp_str_buf),
+		 "subprog#%d%s%s", subprog, name ? " " : "", name ? name : "");
+	return env->tmp_str_buf;
+}
+
+static char *fmt_instance(struct bpf_verifier_env *env, struct func_instance *instance)
+{
+	snprintf(env->tmp_str_buf, sizeof(env->tmp_str_buf),
+		 "(d%d,cs%d)", instance->depth, instance->callsite);
+	return env->tmp_str_buf;
+}
+
+static int spi_off(int spi)
+{
+	return -(spi + 1) * BPF_REG_SIZE;
+}
+
+/*
+ * When both halves of an 8-byte SPI are set, print as "-8","-16",...
+ * When only one half is set, print as "-4h","-8h",...
+ * Runs of 3+ consecutive fully-set SPIs are collapsed: "fp0-8..-24"
+ */
+static char *fmt_spis_mask(struct bpf_verifier_env *env, int frame, bool first, spis_t spis)
+{
+	int buf_sz = sizeof(env->tmp_str_buf);
+	char *buf = env->tmp_str_buf;
+	int spi, n, run_start;
+
+	buf[0] = '\0';
+
+	for (spi = 0; spi < STACK_SLOTS / 2 && buf_sz > 0; spi++) {
+		bool lo = spis_test_bit(spis, spi * 2);
+		bool hi = spis_test_bit(spis, spi * 2 + 1);
+		const char *space = first ? "" : " ";
+
+		if (!lo && !hi)
+			continue;
+
+		if (!lo || !hi) {
+			/* half-spi */
+			n = scnprintf(buf, buf_sz, "%sfp%d%d%s",
+				      space, frame, spi_off(spi) + (lo ? STACK_SLOT_SZ : 0), "h");
+		} else if (spi + 2 < STACK_SLOTS / 2 &&
+			   spis_test_bit(spis, spi * 2 + 2) &&
+			   spis_test_bit(spis, spi * 2 + 3) &&
+			   spis_test_bit(spis, spi * 2 + 4) &&
+			   spis_test_bit(spis, spi * 2 + 5)) {
+			/* 3+ consecutive full spis */
+			run_start = spi;
+			while (spi + 1 < STACK_SLOTS / 2 &&
+			       spis_test_bit(spis, (spi + 1) * 2) &&
+			       spis_test_bit(spis, (spi + 1) * 2 + 1))
+				spi++;
+			n = scnprintf(buf, buf_sz, "%sfp%d%d..%d",
+				      space, frame, spi_off(run_start), spi_off(spi));
+		} else {
+			/* just a full spi */
+			n = scnprintf(buf, buf_sz, "%sfp%d%d", space, frame, spi_off(spi));
+		}
+		first = false;
+		buf += n;
+		buf_sz -= n;
+	}
+	return env->tmp_str_buf;
+}
+
+static void print_instance(struct bpf_verifier_env *env, struct func_instance *instance)
+{
+	int start = env->subprog_info[instance->subprog].start;
+	struct bpf_insn *insns = env->prog->insnsi;
+	struct per_frame_masks *masks;
+	int len = instance->insn_cnt;
+	int insn_idx, frame, i;
+	bool has_use, has_def;
+	u64 pos, insn_pos;
+
+	if (!(env->log.level & BPF_LOG_LEVEL2))
+		return;
+
+	verbose(env, "stack use/def %s ", fmt_subprog(env, instance->subprog));
+	verbose(env, "%s:\n", fmt_instance(env, instance));
+	for (i = 0; i < len; i++) {
+		insn_idx = start + i;
+		has_use = false;
+		has_def = false;
+		pos = env->log.end_pos;
+		verbose(env, "%3d: ", insn_idx);
+		bpf_verbose_insn(env, &insns[insn_idx]);
+		bpf_vlog_reset(&env->log, env->log.end_pos - 1); /* remove \n */
+		insn_pos = env->log.end_pos;
+		verbose(env, "%*c;", bpf_vlog_alignment(insn_pos - pos), ' ');
+		pos = env->log.end_pos;
+		verbose(env, " use: ");
+		for (frame = instance->depth; frame >= 0; --frame) {
+			masks = get_frame_masks(instance, frame, insn_idx);
+			if (!masks || spis_is_zero(masks->may_read))
+				continue;
+			verbose(env, "%s", fmt_spis_mask(env, frame, !has_use, masks->may_read));
+			has_use = true;
+		}
+		if (!has_use)
+			bpf_vlog_reset(&env->log, pos);
+		pos = env->log.end_pos;
+		verbose(env, " def: ");
+		for (frame = instance->depth; frame >= 0; --frame) {
+			masks = get_frame_masks(instance, frame, insn_idx);
+			if (!masks || spis_is_zero(masks->must_write))
+				continue;
+			verbose(env, "%s", fmt_spis_mask(env, frame, !has_def, masks->must_write));
+			has_def = true;
+		}
+		if (!has_def)
+			bpf_vlog_reset(&env->log, has_use ? pos : insn_pos);
+		verbose(env, "\n");
+		if (bpf_is_ldimm64(&insns[insn_idx]))
+			i++;
+	}
+}
+
+static int cmp_instances(const void *pa, const void *pb)
+{
+	struct func_instance *a = *(struct func_instance **)pa;
+	struct func_instance *b = *(struct func_instance **)pb;
+	int dcallsite = (int)a->callsite - b->callsite;
+	int ddepth = (int)a->depth - b->depth;
+
+	if (dcallsite)
+		return dcallsite;
+	if (ddepth)
+		return ddepth;
+	return 0;
+}
+
+/* print use/def slots for all instances ordered by callsite first, then by depth */
+static int print_instances(struct bpf_verifier_env *env)
+{
+	struct func_instance *instance, **sorted_instances;
+	struct bpf_liveness *liveness = env->liveness;
+	int i, bkt, cnt;
+
+	cnt = 0;
+	hash_for_each(liveness->func_instances, bkt, instance, hl_node)
+		cnt++;
+	sorted_instances = kvmalloc_objs(*sorted_instances, cnt, GFP_KERNEL_ACCOUNT);
+	if (!sorted_instances)
+		return -ENOMEM;
+	cnt = 0;
+	hash_for_each(liveness->func_instances, bkt, instance, hl_node)
+		sorted_instances[cnt++] = instance;
+	sort(sorted_instances, cnt, sizeof(*sorted_instances), cmp_instances, NULL);
+	for (i = 0; i < cnt; i++)
+		print_instance(env, sorted_instances[i]);
+	kvfree(sorted_instances);
+	return 0;
+}
+
 /*
  * Per-register tracking state for compute_subprog_args().
  * Tracks which frame's FP a value is derived from
@@ -1425,12 +1516,7 @@ static void print_subprog_arg_access(struct bpf_verifier_env *env,
 	if (!(env->log.level & BPF_LOG_LEVEL2))
 		return;
 
-	verbose(env, "subprog#%d %s:\n", subprog,
-		env->prog->aux->func_info
-		? btf_name_by_offset(env->prog->aux->btf,
-				     btf_type_by_id(env->prog->aux->btf,
-						    env->prog->aux->func_info[subprog].type_id)->name_off)
-		: "");
+	verbose(env, "%s:\n", fmt_subprog(env, subprog));
 	for (i = 0; i < len; i++) {
 		int idx = start + i;
 		bool has_extra = false;
@@ -1918,6 +2004,9 @@ int bpf_compute_subprog_arg_access(struct bpf_verifier_env *env)
 			goto out;
 	}
 
+	if (env->log.level & BPF_LOG_LEVEL2)
+		err = print_instances(env);
+
 out:
 	for (k = 0; k < insn_cnt; k++)
 		kvfree(env->callsite_at_stack[k]);
Signature · 2 lines
-- 
2.53.0
author maintainer
#20 lore raw

Re: [PATCH bpf-next v4 10/14] bpf: change logging scheme for live stack analysis

Quoted 20 lines · Paul Chaignon
Quoted 8 lines · Eduard Zingerman

Instead of breadcrumbs like:

  (d2,cs15) frame 0 insn 18 +live -16
  (d2,cs15) frame 0 insn 17 +live -16

Print final accumulated stack use/def data per-func_instance per-instruction. printed func_instance's are ordered by callsite and depth. For example:

Hi Eduard,

Sorry to revive an old thread.

We've started running a kernel with this patchset in Cilium's CI and noticed a big increase of log verbosity for BPF_LOG_LEVEL2. To get an idea, a full dump of all (uncompressed) verifier logs for our tested programs used to take ~8.4G. With this patchset, it now takes 15G. [1] helps a bit to reduce it, but still only getting to 12G.

Quoted 5 lines

The increase seems to come from print_subprog_arg_access(), which prints full functions with the results of the fixed-point analysis, from print_instances(), which prints full functions with the use/def slots, and to a lesser extent from compute_subprog_args(), which prints the fixed-point iterations.

Hi Paul,

Do you have a break-down? I'd expect most of the log to come from arg_track_log(), as it visits instructions multiple times (although, convergence might take only a few iterations). If it is a significant chunk, I think we can drop it completely.

As for print_subprog_arg_access() and print_instances(), as you suggest the output can be combined, such that the full function is printed only once per call depth. This combined print-out can also include results of registers liveness analysis.

Quoted 5 lines

I'm wondering if maybe there are other opportunities to reduce verbosity here (besides [1]). Maybe we don't need to print the fixed-point iterations if we're already printing the results? Or maybe we could put the more detailed liveness-related logs behind BPF_LOG_LEVEL3 or BPF_LOG_LIVENESS?

Had this been a normal compiler or jit engine, I'd vote for BPF_LOG_LIVENESS log channel, I'm not sure what our stance regarding user visible ABI here.

Regarding BPF_LOG_LEVEL3, I think that the original idea behind BPF_LOG_LEVEL2 is that it would serve as a "debug log" that regular users won't need to consume. What is the motivation behind collecting level 2 log on your CI? Is it to infer clues regarding programs hitting 1M instructions limit?

[...]

#21 lore raw

Re: [PATCH bpf-next v4 10/14] bpf: change logging scheme for live stack analysis

Quoted 27 lines · Eduard Zingerman
Quoted 20 lines · Paul Chaignon
Quoted 8 lines · Eduard Zingerman

Instead of breadcrumbs like:

  (d2,cs15) frame 0 insn 18 +live -16
  (d2,cs15) frame 0 insn 17 +live -16

Print final accumulated stack use/def data per-func_instance per-instruction. printed func_instance's are ordered by callsite and depth. For example:

Hi Eduard,

Sorry to revive an old thread.

We've started running a kernel with this patchset in Cilium's CI and noticed a big increase of log verbosity for BPF_LOG_LEVEL2. To get an idea, a full dump of all (uncompressed) verifier logs for our tested programs used to take ~8.4G. With this patchset, it now takes 15G. [1] helps a bit to reduce it, but still only getting to 12G.

Quoted 5 lines

The increase seems to come from print_subprog_arg_access(), which prints full functions with the results of the fixed-point analysis, from print_instances(), which prints full functions with the use/def slots, and to a lesser extent from compute_subprog_args(), which prints the fixed-point iterations.

Quoted 6 lines

Hi Paul,

Do you have a break-down? I'd expect most of the log to come from arg_track_log(), as it visits instructions multiple times (although, convergence might take only a few iterations). If it is a significant chunk, I think we can drop it completely.

That was also my first guess, but looking at a couple examples, it seems to converge fairly quickly. Some statistics to confirm this: if I filter my 12G of logs to only lines containing " -> " (so coming from arg_track_log()), then I only get 30M of logs remaining. So arg_track_log() looks really negligible here.

Quoted 5 lines

As for print_subprog_arg_access() and print_instances(), as you suggest the output can be combined, such that the full function is printed only once per call depth. This combined print-out can also include results of registers liveness analysis.

Yep, that makes sense. I'll try to find some time to look into that.

Quoted 10 lines
Quoted 6 lines

I'm wondering if maybe there are other opportunities to reduce verbosity here (besides [1]). Maybe we don't need to print the fixed-point iterations if we're already printing the results? Or maybe we could put the more detailed liveness-related logs behind BPF_LOG_LEVEL3 or BPF_LOG_LIVENESS?

Had this been a normal compiler or jit engine, I'd vote for BPF_LOG_LIVENESS log channel, I'm not sure what our stance regarding user visible ABI here.

IIUC, it was only released in an RC and the logs themselves are not really part of the ABI, no? Or is there some other concern I'm missing?

Quoted 6 lines

Regarding BPF_LOG_LEVEL3, I think that the original idea behind BPF_LOG_LEVEL2 is that it would serve as a "debug log" that regular users won't need to consume. What is the motivation behind collecting level 2 log on your CI? Is it to infer clues regarding programs hitting 1M instructions limit?

At the moment, we collect this (1) in case of failures due to the 1M limit and (2) to compute the maximum combined stack depth using the per-subprog stack depths and the callgraph. (1) is not expected to be happening often and isn't much of an issue. For (2), I'm planning to send a patch to have the verifier report the max stack depth itself.

Overall, this isn't an issue for us. We're just using a bit more memory and disk space. I just though the sudden increase was unexpected and thought I'd have a look :)

Quoted 2 lines

[...]

author maintainer
#22 lore raw

Re: [PATCH bpf-next v4 10/14] bpf: change logging scheme for live stack analysis

On Mon, 2026-05-11 at 19:45 +0200, Paul Chaignon wrote:

[...]

Quoted 6 lines
Quoted 6 lines
Quoted 5 lines

I'm wondering if maybe there are other opportunities to reduce verbosity here (besides [1]). Maybe we don't need to print the fixed-point iterations if we're already printing the results? Or maybe we could put the more detailed liveness-related logs behind BPF_LOG_LEVEL3 or BPF_LOG_LIVENESS?

Quoted 6 lines
Quoted 3 lines

Had this been a normal compiler or jit engine, I'd vote for BPF_LOG_LIVENESS log channel, I'm not sure what our stance regarding user visible ABI here.

IIUC, it was only released in an RC and the logs themselves are not really part of the ABI, no? Or is there some other concern I'm missing?

I mean the valid flag values themselves. I hope log is not an ABI, given the number of times we changed it. If BPF_LOG_LEVEL2 is considered a kernel development only thing, then splitting it into multiple channels would actually help the developer, imo.

Quoted 12 lines
Quoted 5 lines

Regarding BPF_LOG_LEVEL3, I think that the original idea behind BPF_LOG_LEVEL2 is that it would serve as a "debug log" that regular users won't need to consume. What is the motivation behind collecting level 2 log on your CI? Is it to infer clues regarding programs hitting 1M instructions limit?

At the moment, we collect this (1) in case of failures due to the 1M limit and (2) to compute the maximum combined stack depth using the per-subprog stack depths and the callgraph. (1) is not expected to be happening often and isn't much of an issue. For (2), I'm planning to send a patch to have the verifier report the max stack depth itself.

Quoted 3 lines

Overall, this isn't an issue for us. We're just using a bit more memory and disk space. I just though the sudden increase was unexpected and thought I'd have a look :)

For 1M instructions, I have the code to identify loop headers, so error reporting here can be changed as follows:
- count the number of times each loop header is visited
- when 1M instructions limit is hit, identify the "hottest" loop
- print info about the loop
- if loop is supposed to converge (e.g. it is an iterator based loop) print out the samples from states cache, noting which registers differ between samples.

Alongside your future patch to printout stack depth this should make log level 3 not needed for now, I think.

Kartikeya, do we plan to do something about 1M reporting or are we now pivoting towards rust2bpf vision?

#23 lore raw

Re: [PATCH bpf-next v4 10/14] bpf: change logging scheme for live stack analysis

Quoted 48 lines · Eduard Zingerman

On Mon, 2026-05-11 at 19:45 +0200, Paul Chaignon wrote:

[...]

Quoted 6 lines
Quoted 6 lines
Quoted 5 lines

I'm wondering if maybe there are other opportunities to reduce verbosity here (besides [1]). Maybe we don't need to print the fixed-point iterations if we're already printing the results? Or maybe we could put the more detailed liveness-related logs behind BPF_LOG_LEVEL3 or BPF_LOG_LIVENESS?

Quoted 6 lines
Quoted 3 lines

Had this been a normal compiler or jit engine, I'd vote for BPF_LOG_LIVENESS log channel, I'm not sure what our stance regarding user visible ABI here.

IIUC, it was only released in an RC and the logs themselves are not really part of the ABI, no? Or is there some other concern I'm missing?

I mean the valid flag values themselves. I hope log is not an ABI, given the number of times we changed it. If BPF_LOG_LEVEL2 is considered a kernel development only thing, then splitting it into multiple channels would actually help the developer, imo.

Quoted 12 lines
Quoted 5 lines

Regarding BPF_LOG_LEVEL3, I think that the original idea behind BPF_LOG_LEVEL2 is that it would serve as a "debug log" that regular users won't need to consume. What is the motivation behind collecting level 2 log on your CI? Is it to infer clues regarding programs hitting 1M instructions limit?

At the moment, we collect this (1) in case of failures due to the 1M limit and (2) to compute the maximum combined stack depth using the per-subprog stack depths and the callgraph. (1) is not expected to be happening often and isn't much of an issue. For (2), I'm planning to send a patch to have the verifier report the max stack depth itself.

Quoted 3 lines

Overall, this isn't an issue for us. We're just using a bit more memory and disk space. I just though the sudden increase was unexpected and thought I'd have a look :)

For 1M instructions, I have the code to identify loop headers, so error reporting here can be changed as follows:
- count the number of times each loop header is visited
- when 1M instructions limit is hit, identify the "hottest" loop
- print info about the loop
- if loop is supposed to converge (e.g. it is an iterator based loop) print out the samples from states cache, noting which registers differ between samples.

Quoted 5 lines

Alongside your future patch to printout stack depth this should make log level 3 not needed for now, I think.

Kartikeya, do we plan to do something about 1M reporting or are we now pivoting towards rust2bpf vision?

The main thing I'm changing is more context around an error (by relating it to the source), categorization, and mitigation information. For now all of this will just be appended to the existing logs at the end, once I share something we can discuss specifics.

I do think convergence failures can use more summarized output, right now, the huge volume of information makes it difficult for most users to figure out why the loop fails to converge. If you have something lying around for that, you should go ahead and share it. I was wondering if we could avoid emitting a huge volume of info upfront by identifying cases of failed loop convergence, instead of logging a lot by default and then pruning it later only when the 1M limit is hit.

Rust-BPF stuff is orthogonal; we will have and still need useful verification errors from the in-kernel verifier with or without the Rust frontend. It is just likely a lot of the errors will be caught ahead of time by encoding a lot of invariants into the Rust type system, but the specifics of the solutions there are too speculative right now.

author maintainer
#24 lore raw

Re: [PATCH bpf-next v4 10/14] bpf: change logging scheme for live stack analysis

Quoted 60 lines · Kumar Kartikeya Dwivedi
Quoted 48 lines · Eduard Zingerman

On Mon, 2026-05-11 at 19:45 +0200, Paul Chaignon wrote:

[...]

Quoted 6 lines
Quoted 6 lines
Quoted 5 lines

I'm wondering if maybe there are other opportunities to reduce verbosity here (besides [1]). Maybe we don't need to print the fixed-point iterations if we're already printing the results? Or maybe we could put the more detailed liveness-related logs behind BPF_LOG_LEVEL3 or BPF_LOG_LIVENESS?

Quoted 6 lines
Quoted 3 lines

Had this been a normal compiler or jit engine, I'd vote for BPF_LOG_LIVENESS log channel, I'm not sure what our stance regarding user visible ABI here.

IIUC, it was only released in an RC and the logs themselves are not really part of the ABI, no? Or is there some other concern I'm missing?

I mean the valid flag values themselves. I hope log is not an ABI, given the number of times we changed it. If BPF_LOG_LEVEL2 is considered a kernel development only thing, then splitting it into multiple channels would actually help the developer, imo.

Quoted 12 lines
Quoted 5 lines

Regarding BPF_LOG_LEVEL3, I think that the original idea behind BPF_LOG_LEVEL2 is that it would serve as a "debug log" that regular users won't need to consume. What is the motivation behind collecting level 2 log on your CI? Is it to infer clues regarding programs hitting 1M instructions limit?

At the moment, we collect this (1) in case of failures due to the 1M limit and (2) to compute the maximum combined stack depth using the per-subprog stack depths and the callgraph. (1) is not expected to be happening often and isn't much of an issue. For (2), I'm planning to send a patch to have the verifier report the max stack depth itself.

Quoted 3 lines

Overall, this isn't an issue for us. We're just using a bit more memory and disk space. I just though the sudden increase was unexpected and thought I'd have a look :)

For 1M instructions, I have the code to identify loop headers, so error reporting here can be changed as follows:
- count the number of times each loop header is visited
- when 1M instructions limit is hit, identify the "hottest" loop
- print info about the loop
- if loop is supposed to converge (e.g. it is an iterator based loop) print out the samples from states cache, noting which registers differ between samples.

Quoted 5 lines

Alongside your future patch to printout stack depth this should make log level 3 not needed for now, I think.

Kartikeya, do we plan to do something about 1M reporting or are we now pivoting towards rust2bpf vision?

The main thing I'm changing is more context around an error (by relating it to the source), categorization, and mitigation information. For now all of this will just be appended to the existing logs at the end, once I share something we can discuss specifics.

Quoted 7 lines

I do think convergence failures can use more summarized output, right now, the huge volume of information makes it difficult for most users to figure out why the loop fails to converge. If you have something lying around for that, you should go ahead and share it. I was wondering if we could avoid emitting a huge volume of info upfront by identifying cases of failed loop convergence, instead of logging a lot by default and then pruning it later only when the 1M limit is hit.

I don't have anything specifically for error reporting, just the code to detect the loop structure. But I think that 1M error summarization should be easy to bolt on top of it. And this is an avenue to start landing scev related things gradually. I'll take a look.

Quoted 6 lines

Rust-BPF stuff is orthogonal; we will have and still need useful verification errors from the in-kernel verifier with or without the Rust frontend. It is just likely a lot of the errors will be caught ahead of time by encoding a lot of invariants into the Rust type system, but the specifics of the solutions there are too speculative right now.

The reason I am asking is that Alexei wanted to eventually forgo 1M instructions limit completely.

author maintainer
#25 lore raw

PATCH v4 11/14 selftests/bpf: update existing tests due to liveness changes

In reply to Eduard Zingerman

From: Alexei Starovoitov <[email protected]>

The verifier cleans all dead registers and stack slots in the current state. Adjust expected output in tests or insert dummy stack/register reads. Also update verifier_live_stack tests to adhere to new logging scheme.

---
 .../selftests/bpf/progs/exceptions_assert.c        |  6 +-
 tools/testing/selftests/bpf/progs/uninit_stack.c   |  1 +
 tools/testing/selftests/bpf/progs/verifier_align.c | 10 +--
 .../selftests/bpf/progs/verifier_div_mod_bounds.c  | 18 +++--
 .../selftests/bpf/progs/verifier_live_stack.c      | 88 +++++++++-------------
 .../selftests/bpf/progs/verifier_scalar_ids.c      | 26 ++++---
 .../selftests/bpf/progs/verifier_spill_fill.c      |  8 +-
 .../bpf/progs/verifier_subprog_precision.c         |  8 +-
 8 files changed, 79 insertions(+), 86 deletions(-)
tools/testing/selftests/bpf/progs/exceptions_assert.c +3 -3
index 2027cfcb748c..e4abf4172fca 100644
@@ -59,7 +59,7 @@ check_assert(s64, >=, ge_neg, INT_MIN);
 
 SEC("?tc")
 __log_level(2) __failure
-__msg(": R0=0 R1=ctx() R2=scalar(smin=0xffffffff80000002,smax=smax32=0x7ffffffd,smin32=0x80000002) R10=fp0")
+__msg(": R1=ctx() R2=scalar(smin=0xffffffff80000002,smax=smax32=0x7ffffffd,smin32=0x80000002) R10=fp0")
 int check_assert_range_s64(struct __sk_buff *ctx)
 {
 	struct bpf_sock *sk = ctx->sk;
@@ -86,7 +86,7 @@ int check_assert_range_u64(struct __sk_buff *ctx)
 
 SEC("?tc")
 __log_level(2) __failure
-__msg(": R0=0 R1=ctx() R2=4096 R10=fp0")
+__msg(": R1=ctx() R2=4096 R10=fp0")
 int check_assert_single_range_s64(struct __sk_buff *ctx)
 {
 	struct bpf_sock *sk = ctx->sk;
@@ -114,7 +114,7 @@ int check_assert_single_range_u64(struct __sk_buff *ctx)
 
 SEC("?tc")
 __log_level(2) __failure
-__msg(": R1=pkt(r=64,imm=64) R2=pkt_end() R6=pkt(r=64) R10=fp0")
+__msg(": R6=pkt(r=64) R10=fp0")
 int check_assert_generic(struct __sk_buff *ctx)
 {
 	u8 *data_end = (void *)(long)ctx->data_end;
tools/testing/selftests/bpf/progs/uninit_stack.c +1 -0
index 046a204c8fc6..5db02323c89c 100644
@@ -76,6 +76,7 @@ __naked int helper_uninit_to_misc(void *ctx)
 		 * thus showing the stack state, matched by __msg().		\
 		 */					\
 		call %[dummy];				\
+		r1 = *(u64*)(r10 - 104);		\
 		r0 = 0;					\
 		exit;					\
 "
tools/testing/selftests/bpf/progs/verifier_align.c +5 -5
index 24553ce62881..3e52686515ca 100644
@@ -131,7 +131,7 @@ LBL ":"							\
 SEC("tc")
 __success __log_level(2)
 __flag(BPF_F_ANY_ALIGNMENT)
-__msg("6: R0=pkt(r=8,imm=8)")
+__msg("6: {{.*}} R2=pkt(r=8)")
 __msg("6: {{.*}} R3={{[^)]*}}var_off=(0x0; 0xff)")
 __msg("7: {{.*}} R3={{[^)]*}}var_off=(0x0; 0x1fe)")
 __msg("8: {{.*}} R3={{[^)]*}}var_off=(0x0; 0x3fc)")
@@ -205,7 +205,7 @@ __success __log_level(2)
 __msg("2: {{.*}} R5=pkt(r=0)")
 __msg("4: {{.*}} R5=pkt(r=0,imm=14)")
 __msg("5: {{.*}} R4=pkt(r=0,imm=14)")
-__msg("9: {{.*}} R2=pkt(r=18)")
+__msg("9: {{.*}} R5=pkt(r=18,imm=14)")
 __msg("10: {{.*}} R4={{[^)]*}}var_off=(0x0; 0xff){{.*}} R5=pkt(r=18,imm=14)")
 __msg("13: {{.*}} R4={{[^)]*}}var_off=(0x0; 0xffff)")
 __msg("14: {{.*}} R4={{[^)]*}}var_off=(0x0; 0xffff)")
@@ -254,7 +254,7 @@ __msg("11: {{.*}} R5=pkt(id=1,{{[^)]*}},var_off=(0x2; 0x7fc)")
  * offset is considered using reg->aux_off_align which
  * is 4 and meets the load's requirements.
  */
-__msg("15: {{.*}} R4={{[^)]*}}var_off=(0x2; 0x7fc){{.*}} R5={{[^)]*}}var_off=(0x2; 0x7fc)")
+__msg("15: {{.*}} R5={{[^)]*}}var_off=(0x2; 0x7fc)")
 /* Variable offset is added to R5 packet pointer,
  * resulting in auxiliary alignment of 4. To avoid BPF
  * verifier's precision backtracking logging
@@ -273,7 +273,7 @@ __msg("19: {{.*}} R5=pkt(id=2,{{[^)]*}}var_off=(0x2; 0x7fc)")
  * aligned, so the total offset is 4-byte aligned and
  * meets the load's requirements.
  */
-__msg("24: {{.*}} R4={{[^)]*}}var_off=(0x2; 0x7fc){{.*}} R5={{[^)]*}}var_off=(0x2; 0x7fc)")
+__msg("24: {{.*}} R5={{[^)]*}}var_off=(0x2; 0x7fc)")
 /* Constant offset is added to R5 packet pointer,
  * resulting in reg->off value of 14.
  */
@@ -296,7 +296,7 @@ __msg("31: {{.*}} R4={{[^)]*}}var_off=(0x2; 0xffc){{.*}} R5={{[^)]*}}var_off=(0x
  * the total offset is 4-byte aligned and meets the
  * load's requirements.
  */
-__msg("35: {{.*}} R4={{[^)]*}}var_off=(0x2; 0xffc){{.*}} R5={{[^)]*}}var_off=(0x2; 0xffc)")
+__msg("35: {{.*}} R5={{[^)]*}}var_off=(0x2; 0xffc)")
 __naked void packet_variable_offset(void)
 {
 	asm volatile ("					\
tools/testing/selftests/bpf/progs/verifier_div_mod_bounds.c +10 -8
index 4672af0b3268..e814a054d69a 100644
@@ -36,7 +36,7 @@ l0_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("UDIV32, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("w1 /= w2 {{.*}}; R1=0 R2=0")
+__msg("w1 /= w2 {{.*}}; R1=0")
 __naked void udiv32_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -81,7 +81,7 @@ l0_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("UDIV64, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("r1 /= r2 {{.*}}; R1=0 R2=0")
+__msg("r1 /= r2 {{.*}}; R1=0")
 __naked void udiv64_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -242,7 +242,7 @@ l1_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("SDIV32, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("w1 s/= w2 {{.*}}; R1=0 R2=0")
+__msg("w1 s/= w2 {{.*}}; R1=0")
 __naked void sdiv32_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -275,6 +275,7 @@ __naked void sdiv32_overflow_1(void)
 	w2 += 10;					\
 	if w1 s> w2 goto l0_%=;				\
 	w1 s/= -1;					\
+	r2 = r1;					\
 l0_%=:	r0 = 0;						\
 	exit;						\
 "	:
@@ -443,7 +444,7 @@ l1_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("SDIV64, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("r1 s/= r2 {{.*}}; R1=0 R2=0")
+__msg("r1 s/= r2 {{.*}}; R1=0")
 __naked void sdiv64_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -476,6 +477,7 @@ __naked void sdiv64_overflow_1(void)
 	r2 += 10;					\
 	if r1 s> r2 goto l0_%=;				\
 	r1 s/= -1;					\
+	r2 = r1;					\
 l0_%=:	r0 = 0;						\
 	exit;						\
 "	:
@@ -553,7 +555,7 @@ l0_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("UMOD32, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("w1 %= w2 {{.*}}; R1=scalar(smin=umin=smin32=umin32=1,smax=umax=smax32=umax32=9,var_off=(0x1; 0x8)) R2=0")
+__msg("w1 %= w2 {{.*}}; R1=scalar(smin=umin=smin32=umin32=1,smax=umax=smax32=umax32=9,var_off=(0x1; 0x8))")
 __naked void umod32_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -624,7 +626,7 @@ l0_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("UMOD64, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("r1 %= r2 {{.*}}; R1=scalar(smin=umin=smin32=umin32=1,smax=umax=smax32=umax32=9,var_off=(0x1; 0x8)) R2=0")
+__msg("r1 %= r2 {{.*}}; R1=scalar(smin=umin=smin32=umin32=1,smax=umax=smax32=umax32=9,var_off=(0x1; 0x8))")
 __naked void umod64_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -833,7 +835,7 @@ l1_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("SMOD32, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("w1 s%= w2 {{.*}}; R1=scalar(smin=0,smax=umax=0xffffffff,smin32=-8,smax32=10,var_off=(0x0; 0xffffffff)) R2=0")
+__msg("w1 s%= w2 {{.*}}; R1=scalar(smin=0,smax=umax=0xffffffff,smin32=-8,smax32=10,var_off=(0x0; 0xffffffff))")
 __naked void smod32_zero_divisor(void)
 {
 	asm volatile ("					\
@@ -1084,7 +1086,7 @@ l1_%=:	r0 = *(u64 *)(r1 + 0);				\
 SEC("socket")
 __description("SMOD64, zero divisor")
 __success __retval(0) __log_level(2)
-__msg("r1 s%= r2 {{.*}}; R1=scalar(smin=smin32=-8,smax=smax32=10) R2=0")
+__msg("r1 s%= r2 {{.*}}; R1=scalar(smin=smin32=-8,smax=smax32=10)")
 __naked void smod64_zero_divisor(void)
 {
 	asm volatile ("					\
tools/testing/selftests/bpf/progs/verifier_live_stack.c +35 -53

139 lines not on this page · raw message

tools/testing/selftests/bpf/progs/verifier_scalar_ids.c +17 -9
index a5b8753ce52c..70ae14d6084f 100644
@@ -264,13 +264,13 @@ void precision_many_frames__bar(void)
  */
 SEC("socket")
 __success __log_level(2)
-__msg("11: (0f) r2 += r1")
+__msg("12: (0f) r2 += r1")
 /* foo frame */
-__msg("frame1: regs=r1 stack= before 10: (bf) r2 = r10")
-__msg("frame1: regs=r1 stack= before 9: (25) if r1 > 0x7 goto pc+0")
-__msg("frame1: regs=r1 stack=-8,-16 before 8: (7b) *(u64 *)(r10 -16) = r1")
-__msg("frame1: regs=r1 stack=-8 before 7: (7b) *(u64 *)(r10 -8) = r1")
-__msg("frame1: regs=r1 stack= before 4: (85) call pc+2")
+__msg("frame1: regs=r1 stack= before 11: (bf) r2 = r10")
+__msg("frame1: regs=r1 stack= before 10: (25) if r1 > 0x7 goto pc+0")
+__msg("frame1: regs=r1 stack=-8,-16 before 9: (7b) *(u64 *)(r10 -16) = r1")
+__msg("frame1: regs=r1 stack=-8 before 8: (7b) *(u64 *)(r10 -8) = r1")
+__msg("frame1: regs=r1 stack= before 4: (85) call pc+3")
 /* main frame */
 __msg("frame0: regs=r1 stack=-8 before 3: (7b) *(u64 *)(r10 -8) = r1")
 __msg("frame0: regs=r1 stack= before 2: (bf) r1 = r0")
@@ -286,6 +286,7 @@ __naked void precision_stack(void)
 	"r1 = r0;"
 	"*(u64*)(r10 - 8) = r1;"
 	"call precision_stack__foo;"
+	"r0 = *(u64*)(r10 - 8);"
 	"r0 = 0;"
 	"exit;"
 	:
@@ -309,6 +310,8 @@ void precision_stack__foo(void)
 	 */
 	"r2 = r10;"
 	"r2 += r1;"
+	"r0 = *(u64*)(r10 - 8);"
+	"r0 = *(u64*)(r10 - 16);"
 	"exit"
 	::: __clobber_all);
 }
@@ -802,9 +805,9 @@ __success __log_level(2)
 /* The exit instruction should be reachable from two states,
  * use two matches and "processed .. insns" to ensure this.
  */
-__msg("15: (95) exit")
-__msg("15: (95) exit")
-__msg("processed 20 insns")
+__msg("16: (95) exit")
+__msg("16: (95) exit")
+__msg("processed 22 insns")
 __flag(BPF_F_TEST_STATE_FREQ)
 __naked void two_old_ids_one_cur_id(void)
 {
@@ -835,6 +838,11 @@ __naked void two_old_ids_one_cur_id(void)
 	"r2 = r10;"
 	"r2 += r6;"
 	"r2 += r7;"
+	/*
+	 * keep r8 and r9 live, otherwise r6->id and r7->id
+	 * will become singular and reset to zero before if r6 > r7
+	 */
+	"r9 += r8;"
 	"exit;"
 	:
 	: __imm(bpf_ktime_get_ns)
tools/testing/selftests/bpf/progs/verifier_spill_fill.c +4 -4
index 672e4446181e..c6ae64b99cd6 100644
@@ -650,7 +650,7 @@ __msg("mark_precise: frame0: last_idx 9 first_idx 7 subseq_idx -1")
 __msg("mark_precise: frame0: regs=r2 stack= before 8: (79) r2 = *(u64 *)(r10 -8)")
 __msg("mark_precise: frame0: regs= stack=-8 before 7: (bf) r1 = r6")
 /* note, fp-8 is precise, fp-16 is not yet precise, we'll get there */
-__msg("mark_precise: frame0: parent state regs= stack=-8:  R0=1 R1=ctx() R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=P1 fp-16=1")
+__msg("mark_precise: frame0: parent state regs= stack=-8:  R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=P1 fp-16=1")
 __msg("mark_precise: frame0: last_idx 6 first_idx 3 subseq_idx 7")
 __msg("mark_precise: frame0: regs= stack=-8 before 6: (05) goto pc+0")
 __msg("mark_precise: frame0: regs= stack=-8 before 5: (7b) *(u64 *)(r10 -16) = r0")
@@ -668,7 +668,7 @@ __msg("mark_precise: frame0: regs= stack=-16 before 9: (0f) r1 += r2")
 __msg("mark_precise: frame0: regs= stack=-16 before 8: (79) r2 = *(u64 *)(r10 -8)")
 __msg("mark_precise: frame0: regs= stack=-16 before 7: (bf) r1 = r6")
 /* now both fp-8 and fp-16 are precise, very good */
-__msg("mark_precise: frame0: parent state regs= stack=-16:  R0=1 R1=ctx() R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=P1 fp-16=P1")
+__msg("mark_precise: frame0: parent state regs= stack=-16:  R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=P1 fp-16=P1")
 __msg("mark_precise: frame0: last_idx 6 first_idx 3 subseq_idx 7")
 __msg("mark_precise: frame0: regs= stack=-16 before 6: (05) goto pc+0")
 __msg("mark_precise: frame0: regs= stack=-16 before 5: (7b) *(u64 *)(r10 -16) = r0")
@@ -726,7 +726,7 @@ __msg("9: (0f) r1 += r2")
 __msg("mark_precise: frame0: last_idx 9 first_idx 7 subseq_idx -1")
 __msg("mark_precise: frame0: regs=r2 stack= before 8: (61) r2 = *(u32 *)(r10 -8)")
 __msg("mark_precise: frame0: regs= stack=-8 before 7: (bf) r1 = r6")
-__msg("mark_precise: frame0: parent state regs= stack=-8:  R0=1 R1=ctx() R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=????P1 fp-16=????1")
+__msg("mark_precise: frame0: parent state regs= stack=-8:  R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=????P1 fp-16=????1")
 __msg("mark_precise: frame0: last_idx 6 first_idx 3 subseq_idx 7")
 __msg("mark_precise: frame0: regs= stack=-8 before 6: (05) goto pc+0")
 __msg("mark_precise: frame0: regs= stack=-8 before 5: (63) *(u32 *)(r10 -16) = r0")
@@ -743,7 +743,7 @@ __msg("mark_precise: frame0: regs= stack=-16 before 10: (73) *(u8 *)(r1 +0) = r2
 __msg("mark_precise: frame0: regs= stack=-16 before 9: (0f) r1 += r2")
 __msg("mark_precise: frame0: regs= stack=-16 before 8: (61) r2 = *(u32 *)(r10 -8)")
 __msg("mark_precise: frame0: regs= stack=-16 before 7: (bf) r1 = r6")
-__msg("mark_precise: frame0: parent state regs= stack=-16:  R0=1 R1=ctx() R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=????P1 fp-16=????P1")
+__msg("mark_precise: frame0: parent state regs= stack=-16:  R6=map_value(map=.data.two_byte_,ks=4,vs=2) R10=fp0 fp-8=????P1 fp-16=????P1")
 __msg("mark_precise: frame0: last_idx 6 first_idx 3 subseq_idx 7")
 __msg("mark_precise: frame0: regs= stack=-16 before 6: (05) goto pc+0")
 __msg("mark_precise: frame0: regs= stack=-16 before 5: (63) *(u32 *)(r10 -16) = r0")
tools/testing/selftests/bpf/progs/verifier_subprog_precision.c +4 -4
index 61886ed554de..d21d32f6a676 100644
@@ -282,7 +282,7 @@ __msg("mark_precise: frame0: regs=r0,r6 stack= before 10: (bf) r6 = r0")
 __msg("mark_precise: frame0: regs=r0 stack= before 9: (85) call bpf_loop")
 /* State entering callback body popped from states stack */
 __msg("from 9 to 17: frame1:")
-__msg("17: frame1: R1=scalar() R2=0 R10=fp0 cb")
+__msg("17: frame1: R10=fp0 cb")
 __msg("17: (b7) r0 = 0")
 __msg("18: (95) exit")
 __msg("returning from callee:")
@@ -411,7 +411,7 @@ __msg("mark_precise: frame0: regs=r6 stack= before 5: (b7) r1 = 1")
 __msg("mark_precise: frame0: regs=r6 stack= before 4: (b7) r6 = 3")
 /* State entering callback body popped from states stack */
 __msg("from 9 to 15: frame1:")
-__msg("15: frame1: R1=scalar() R2=0 R10=fp0 cb")
+__msg("15: frame1: R10=fp0 cb")
 __msg("15: (b7) r0 = 0")
 __msg("16: (95) exit")
 __msg("returning from callee:")
@@ -567,7 +567,7 @@ __msg("mark_precise: frame0: regs= stack=-8 before 5: (7b) *(u64 *)(r10 -8) = r6
 __msg("mark_precise: frame0: regs=r6 stack= before 4: (b7) r6 = 3")
 /* State entering callback body popped from states stack */
 __msg("from 10 to 17: frame1:")
-__msg("17: frame1: R1=scalar() R2=0 R10=fp0 cb")
+__msg("17: frame1: R10=fp0 cb")
 __msg("17: (b7) r0 = 0")
 __msg("18: (95) exit")
 __msg("returning from callee:")
@@ -681,7 +681,7 @@ __msg("mark_precise: frame0: last_idx 10 first_idx 7 subseq_idx -1")
 __msg("mark_precise: frame0: regs=r7 stack= before 9: (bf) r1 = r8")
 __msg("mark_precise: frame0: regs=r7 stack= before 8: (27) r7 *= 4")
 __msg("mark_precise: frame0: regs=r7 stack= before 7: (79) r7 = *(u64 *)(r10 -8)")
-__msg("mark_precise: frame0: parent state regs= stack=-8:  R0=2 R6=1 R8=map_value(map=.data.vals,ks=4,vs=16) R10=fp0 fp-8=P1")
+__msg("mark_precise: frame0: parent state regs= stack=-8:  R8=map_value(map=.data.vals,ks=4,vs=16) R10=fp0 fp-8=P1")
 __msg("mark_precise: frame0: last_idx 18 first_idx 0 subseq_idx 7")
 __msg("mark_precise: frame0: regs= stack=-8 before 18: (95) exit")
 __msg("mark_precise: frame1: regs= stack= before 17: (0f) r0 += r2")
Signature · 1 line
2.53.0
author maintainer
#26 lore raw

PATCH v4 12/14 selftests/bpf: adjust verifier_log buffers

In reply to Eduard Zingerman

From: Alexei Starovoitov <[email protected]>

The new liveness analysis in liveness.c adds verbose output at BPF_LOG_LEVEL2, making the verifier log for good_prog exceed the 1024-byte reference buffer. When the reference is truncated in fixed mode, the rolling mode captures the actual tail of the full log, which doesn't match the truncated reference.

The fix is to increase the buffer sizes in the test.

---
 tools/testing/selftests/bpf/prog_tests/verifier_log.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
tools/testing/selftests/bpf/prog_tests/verifier_log.c +3 -3
index aaa2854974c0..c01c0114af1b 100644
@@ -25,10 +25,10 @@ static bool check_prog_load(int prog_fd, bool expect_err, const char *tag)
 
 static struct {
 	/* strategically placed before others to avoid accidental modification by kernel */
-	char filler[1024];
-	char buf[1024];
+	char filler[16384];
+	char buf[16384];
 	/* strategically placed after buf[] to catch more accidental corruptions */
-	char reference[1024];
+	char reference[16384];
 } logs;
 static const struct bpf_insn *insns;
 static size_t insn_cnt;
Signature · 1 line
2.53.0
author maintainer
#27 lore raw

PATCH v4 13/14 selftests/bpf: add new tests for static stack liveness analysis

In reply to Eduard Zingerman

From: Alexei Starovoitov <[email protected]>

Add a bunch of new tests to verify the static stack liveness analysis.

---
 tools/testing/selftests/bpf/prog_tests/verifier.c  |    2 +
 .../selftests/bpf/progs/verifier_live_stack.c      | 2323 ++++++++++++++++++++
 .../selftests/bpf/progs/verifier_liveness_exp.c    |  139 ++
 3 files changed, 2464 insertions(+)
tools/testing/selftests/bpf/prog_tests/verifier.c +2 -0
index 1ac366fd4dae..e1f0a76219f3 100644
@@ -54,6 +54,7 @@
 #include "verifier_leak_ptr.skel.h"
 #include "verifier_linked_scalars.skel.h"
 #include "verifier_live_stack.skel.h"
+#include "verifier_liveness_exp.skel.h"
 #include "verifier_load_acquire.skel.h"
 #include "verifier_loops1.skel.h"
 #include "verifier_lwt.skel.h"
@@ -202,6 +203,7 @@ void test_verifier_ldsx(void)                  { RUN(verifier_ldsx); }
 void test_verifier_leak_ptr(void)             { RUN(verifier_leak_ptr); }
 void test_verifier_linked_scalars(void)       { RUN(verifier_linked_scalars); }
 void test_verifier_live_stack(void)           { RUN(verifier_live_stack); }
+void test_verifier_liveness_exp(void)         { RUN(verifier_liveness_exp); }
 void test_verifier_loops1(void)               { RUN(verifier_loops1); }
 void test_verifier_lwt(void)                  { RUN(verifier_lwt); }
 void test_verifier_map_in_map(void)           { RUN(verifier_map_in_map); }
tools/testing/selftests/bpf/progs/verifier_live_stack.c +2323 -0

2,344 lines not on this page · raw message

tools/testing/selftests/bpf/progs/verifier_liveness_exp.c new file +139 -0

139 lines not on this page · raw message

Signature · 1 line
2.53.0
author maintainer
#28 lore raw

PATCH v4 14/14 bpf: poison dead stack slots

In reply to Eduard Zingerman

From: Alexei Starovoitov <[email protected]>

As a sanity check poison stack slots that stack liveness determined to be dead, so that any read from such slots will cause program rejection. If stack liveness logic is incorrect the poison can cause valid program to be rejected, but it also will prevent unsafe program to be accepted.

Allow global subprogs "read" poisoned stack slots.
The static stack liveness determined that subprog doesn't read certain stack slots, but sizeof(arg_type) based global subprog validation isn't accurate enough to know which slots will actually be read by the callee, so it needs to check full sizeof(arg_type) at the caller.

---
 include/linux/bpf_verifier.h                       |  1 +
 kernel/bpf/log.c                                   |  5 +-
 kernel/bpf/verifier.c                              | 89 ++++++++++++++++------
 .../selftests/bpf/progs/verifier_spill_fill.c      |  2 +
 4 files changed, 70 insertions(+), 27 deletions(-)
include/linux/bpf_verifier.h +1 -0
index d3dc46aae2e7..05b9fe98b8f8 100644
@@ -220,6 +220,7 @@ enum bpf_stack_slot_type {
 	STACK_DYNPTR,
 	STACK_ITER,
 	STACK_IRQ_FLAG,
+	STACK_POISON,
 };
 
 #define BPF_REG_SIZE 8	/* size of eBPF register in bytes */
kernel/bpf/log.c +3 -2
index f0902ecb7df6..d5779a3426d9 100644
@@ -542,7 +542,8 @@ static char slot_type_char[] = {
 	[STACK_ZERO]	= '0',
 	[STACK_DYNPTR]	= 'd',
 	[STACK_ITER]	= 'i',
-	[STACK_IRQ_FLAG] = 'f'
+	[STACK_IRQ_FLAG] = 'f',
+	[STACK_POISON]	= 'p',
 };
 
 #define UNUM_MAX_DECIMAL U16_MAX
@@ -779,7 +780,7 @@ void print_verifier_state(struct bpf_verifier_env *env, const struct bpf_verifie
 
 		for (j = 0; j < BPF_REG_SIZE; j++) {
 			slot_type = state->stack[i].slot_type[j];
-			if (slot_type != STACK_INVALID)
+			if (slot_type != STACK_INVALID && slot_type != STACK_POISON)
 				valid = true;
 			types_buf[j] = slot_type_char[slot_type];
 		}
kernel/bpf/verifier.c +64 -25

181 lines not on this page · raw message

tools/testing/selftests/bpf/progs/verifier_spill_fill.c +2 -0
index c6ae64b99cd6..6bc721accbae 100644
@@ -780,6 +780,8 @@ __naked void stack_load_preserves_const_precision_subreg(void)
 		"r1 += r2;"
 		"*(u8 *)(r1 + 0) = r2;" /* this should be fine */
 
+		"r2 = *(u64 *)(r10 -8);" /* keep slots alive */
+		"r2 = *(u64 *)(r10 -16);"
 		"r0 = 0;"
 		"exit;"
 	:
Signature · 1 line
2.53.0
#29 lore raw
In reply to Eduard Zingerman
This series was applied to bpf/bpf-next.git (master) by Alexei Starovoitov <[email protected]>:

Hello:

This series was applied to bpf/bpf-next.git (master) by Alexei Starovoitov <[email protected]>:

Quoted 8 lines · On Fri, 10 Apr 2026 13:55:51 -0700 you wrote:

This patch set converts current dynamic stack slot liveness tracking mechanism to a static data flow analysis. The result is used during state pruning (clean_verifier_state): to zero out dead stack slots, enabling more aggressive state equivalence and pruning. To improve analysis precision live stack slot tracking is converted to 4-byte granularity.

[...]

Here is the summary with links:
- [bpf-next,v4,01/14] bpf: share several utility functions as internal API

    https://git.kernel.org/bpf/bpf-next/c/33dfc521c20d
  - [bpf-next,v4,02/14] bpf: save subprogram name in bpf_subprog_info
    https://git.kernel.org/bpf/bpf-next/c/cf3ee1ecf346
  - [bpf-next,v4,03/14] bpf: Add spis_*() helpers for 4-byte stack slot bitmasks
    https://git.kernel.org/bpf/bpf-next/c/2ad45b414b87
  - [bpf-next,v4,04/14] bpf: make liveness.c track stack with 4-byte granularity
    https://git.kernel.org/bpf/bpf-next/c/7ca5f68cda07
  - [bpf-next,v4,05/14] bpf: 4-byte precise clean_verifier_state
    https://git.kernel.org/bpf/bpf-next/c/be23266b4a08
  - [bpf-next,v4,06/14] bpf: prepare liveness internal API for static analysis pass
    https://git.kernel.org/bpf/bpf-next/c/8d3219f64d98
  - [bpf-next,v4,07/14] bpf: introduce forward arg-tracking dataflow analysis
    https://git.kernel.org/bpf/bpf-next/c/bf0c571f7feb
  - [bpf-next,v4,08/14] bpf: record arg tracking results in bpf_liveness masks
    https://git.kernel.org/bpf/bpf-next/c/fed53dbcdb61
  - [bpf-next,v4,09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances
    (no matching commit)
  - [bpf-next,v4,10/14] bpf: change logging scheme for live stack analysis
    https://git.kernel.org/bpf/bpf-next/c/2c167d91775b
  - [bpf-next,v4,11/14] selftests/bpf: update existing tests due to liveness changes
    https://git.kernel.org/bpf/bpf-next/c/b42eb55f6ca2
  - [bpf-next,v4,12/14] selftests/bpf: adjust verifier_log buffers
    https://git.kernel.org/bpf/bpf-next/c/957c30c06748
  - [bpf-next,v4,13/14] selftests/bpf: add new tests for static stack liveness analysis
    https://git.kernel.org/bpf/bpf-next/c/27417e5eb9cd
  - [bpf-next,v4,14/14] bpf: poison dead stack slots
    https://git.kernel.org/bpf/bpf-next/c/2cb27158adb3

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html