bpf: static stack liveness data flow analysis
Subsystems (4)
Eduard Zingerman
maintainerv4 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: 0total_insns abs max old: 837,487total_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: 0total_insns abs max old: 236,251total_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: 0total_insns abs max old: 434,763total_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: 0total_insns abs max old: 207,593total_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
Eduard Zingerman
maintainerPATCH 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
@@ -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
@@ -847,7 +847,7 @@ void print_verifier_state(struct bpf_verifier_env *env, const struct bpf_verifiemark_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_stif (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
@@ -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
Eduard Zingerman
maintainerPATCH v4 2/14 bpf: save subprogram name in bpf_subprog_info
In reply to Eduard ZingermanSubprogram 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
@@ -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
@@ -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
Eduard Zingerman
maintainerPATCH v4 3/14 bpf: Add spis_*() helpers for 4-byte stack slot bitmasks
In reply to Eduard ZingermanFrom: 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
@@ -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
Eduard Zingerman
maintainerPATCH v4 4/14 bpf: make liveness.c track stack with 4-byte granularity
In reply to Eduard ZingermanConvert 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
@@ -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
Eduard Zingerman
maintainerPATCH v4 5/14 bpf: 4-byte precise clean_verifier_state
In reply to Eduard ZingermanMigrate 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
@@ -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
Eduard Zingerman
maintainerPATCH v4 6/14 bpf: prepare liveness internal API for static analysis pass
In reply to Eduard ZingermanMove 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
@@ -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, sint 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 *ibool 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 *ireturn 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
Eduard Zingerman
maintainerPATCH v4 7/14 bpf: introduce forward arg-tracking dataflow analysis
In reply to Eduard ZingermanThe 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
@@ -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
@@ -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
[email protected]
AI reviewRe: [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
[ ... ]
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
[ ... ]
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 dereferenceThis 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
Eduard Zingerman
maintainerRe: [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[ ... ]
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 timesdo_check()was called */[ ... ]
Quoted 4 lines
kernel/bpf/liveness.c +0
-0[ ... ]
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_stackbe NULL here?bpf_compute_subprog_arg_access()allocatescallsite_at_stackwithkvzalloc_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 NULLat_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 dereferenceThis appears to be a bisection hazard. The next commit in the series,
0458daba63a9("bpf: record arg tracking results inbpf_livenessmasks"), 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 intocompute_live_registers()until the next patch, or move thecallsite_at_stackpopulation 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.mdCI run summary: https://github.com/kernel-patches/bpf/actions/runs/24264407165
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[ ... ]
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[ ... ]
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_stackbe NULL here?bpf_compute_subprog_arg_access()allocatescallsite_at_stackwithkvzalloc_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 NULLat_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 dereferenceThis appears to be a bisection hazard. The next commit in the series,
0458daba63a9("bpf: record arg tracking results inbpf_livenessmasks"), 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 intocompute_live_registers()until the next patch, or move thecallsite_at_stackpopulation into this commit?Yeap, should be moved.
No point. The whole thing is not really bisectable.
Eduard Zingerman
maintainerPATCH v4 8/14 bpf: record arg tracking results in bpf_liveness masks
In reply to Eduard ZingermanAfter 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
@@ -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
Eduard Zingerman
maintainerPATCH v4 9/14 bpf: simplify liveness to use (callsite, depth) keyed func_instances
In reply to Eduard ZingermanRework 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
@@ -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
Re: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances
Quoted 49 lines · Eduard Zingerman
Rework
func_instanceidentification and remove the dynamic liveness API, completing the transition to fully static stack liveness analysis.Replace callchain-based
func_instancekeys 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_instancestores just callsite, depth.Drop
must_write_accpropagation. Previously,must_writemarks were accumulated across successors and propagated to the caller viapropagate_to_outer_instance(). Instead, callee entry liveness (live_beforeat subprog start) is pulled directly back to the caller's callsite inanalyze_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_writemarks from one call may be stale for another. To handle this,analyze_subprog()records into afresh_instance()when the instance was already visited (must_write_initialized), thenmerge_instances()combines the results:may_readis unioned,must_writeis intersected. This ensures only slots written on ALL paths through all call sites are marked as guaranteed writes.
This replacescommit_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 beyondMAX_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 byanalyze_subprog()at depth 0 independently.Update
lookup_instance()(used byis_live_beforequeries) to search for thefunc_instancewith 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 tobpf_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 indo_check()
- Thebpf_update_live_stack()call inprepare_func_exit()
Quoted 8 lines
mark_stack_read()andmark_stack_write()become static functions inliveness.c, called only from the static analysis pass. Thefunc_instance->updatedandmust_write_droppedflags are removed. Removespis_single_slot(),spis_one_bit()helpers frombpf_verifier.has they are no longer used.Signed-off-by Alexei Starovoitov <[email protected]>Signed-off-by Eduard Zingerman <[email protected]>
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
[...]
Eduard Zingerman
maintainerRe: [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?
[...]
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
[...]
[email protected]
AI reviewRe: [PATCH bpf-next v4 09/14] bpf: simplify liveness to use (callsite, depth) keyed func_instances
In reply to Eduard ZingermanThe 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
[ ... ]
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
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[ ... ]
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.
Eduard Zingerman
maintainerPATCH v4 10/14 bpf: change logging scheme for live stack analysis
In reply to Eduard ZingermanInstead 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
@@ -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
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 -16Print 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?
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) exitFor 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..-512Signed-off-by Eduard Zingerman <[email protected]>--- 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@@ -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@@ -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_spreturn 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
Eduard Zingerman
maintainerRe: [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 -16Print 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, fromprint_instances(), which prints full functions with the use/def slots, and to a lesser extent fromcompute_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_LEVEL3orBPF_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?
[...]
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 -16Print 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, fromprint_instances(), which prints full functions with the use/def slots, and to a lesser extent fromcompute_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()andprint_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_LEVEL3orBPF_LOG_LIVENESS?Had this been a normal compiler or jit engine, I'd vote for
BPF_LOG_LIVENESSlog 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 behindBPF_LOG_LEVEL2is 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
[...]
Eduard Zingerman
maintainerRe: [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_LEVEL3orBPF_LOG_LIVENESS?
Quoted 6 lines
Quoted 3 lines
Had this been a normal compiler or jit engine, I'd vote for
BPF_LOG_LIVENESSlog 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 behindBPF_LOG_LEVEL2is 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?
Kumar Kartikeya Dwivedi
maintainerRe: [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_LEVEL3orBPF_LOG_LIVENESS?Quoted 6 lines
Quoted 3 lines
Had this been a normal compiler or jit engine, I'd vote for
BPF_LOG_LIVENESSlog 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_LEVEL2is 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 behindBPF_LOG_LEVEL2is 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.
Eduard Zingerman
maintainerRe: [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_LEVEL3orBPF_LOG_LIVENESS?Quoted 6 lines
Quoted 3 lines
Had this been a normal compiler or jit engine, I'd vote for
BPF_LOG_LIVENESSlog 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_LEVEL2is 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 behindBPF_LOG_LEVEL2is 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.
Eduard Zingerman
maintainerPATCH v4 11/14 selftests/bpf: update existing tests due to liveness changes
In reply to Eduard ZingermanFrom: 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Eduard Zingerman
maintainerPATCH v4 12/14 selftests/bpf: adjust verifier_log buffers
In reply to Eduard ZingermanFrom: 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
@@ -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
Eduard Zingerman
maintainerPATCH v4 13/14 selftests/bpf: add new tests for static stack liveness analysis
In reply to Eduard ZingermanFrom: 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
@@ -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
Eduard Zingerman
maintainerPATCH v4 14/14 bpf: poison dead stack slots
In reply to Eduard ZingermanFrom: 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
@@ -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
@@ -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_verifiefor (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
@@ -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
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