* [PATCH bpf-next v1 00/17] Redesign Verification Errors
@ 2026-06-05 6:33 Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
` (16 more replies)
0 siblings, 17 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
TL;DR: This set reworks verifier error messages to include source and
instruction annotations, together with more causal context, making
failures easier to understand and more actionable when debugging and
repairing BPF programs.
Motivation
~~~~~~~~~~
The verifier log is the primary interface through which the verifier
communicates to the user its verdict on whether a program was accepted
or rejected.
To aid the debugging of rejection decisions, verifier also reports the
symbolic state of the program at each instruction, across every explored
path of the BPF program. Such detailed information is critical to
introspect the correctness of verification decisions, and provide
insight into why a given program may have failed to load in the kernel.
A constant pain point in BPF ecosystem throughout the years has been the
difficulty of debugging verification errors. The human-readable error
messages produced in response to a failure in satisfying safety-related
constraints are often terse, context-dependent, or insufficient for
understanding why a given error may have happened. Users must fall back
to the verbose instruction-by-instruction breakdown of how the symbolic
state evolved to surface the root cause. For programs with a huge log
volume due to high verification complexity, such logs quickly become
inscrutable.
All of this has made life difficult for users lacking an understanding
of how the verifier works, and the various heuristics and idiosyncracies
used by it. In some cases, even seasoned BPF experts spend significant
time reverse engineering why a program may have failed, and have to
reach into the verifier's source code to form a complete picture of the
verification process.
Such a steep learning curve and cognitive burden also hurts the speed of
BPF development, as the verifier sits right in the middle of the user's
iteration loop while they make use of BPF to solve any given problem.
Expertise in debugging verifier errors does not scale in terms of teams
deploying these programs in production across a diverse set of kernels.
Overall, this leads to a poorer developer experience, causes visible
user dissatisfaction, and remains a drag on wider BPF adoption. With
some of the more recent developments where users increasingly leverage
AI tooling [0] to author their code, this bottleneck becomes even more
critical to address, since it throttles the much faster iteration loop
of AI agents.
[0]: https://lwn.net/Articles/1075067
Approach
~~~~~~~~
This series starts moving selected failures from terse terminal messages
toward diagnostics that carry the relevant context for a verification
failure. The existing verbose log remains the low-level trace. The new
report is emitted after this trace with selected failures and answers
the immediate debugging questions:
- what verifier rule failed,
- why the current state does not satisfy it,
- where the failing instruction maps to source,
- which earlier branch or state event made this path fail,
- what kind of source change would satisfy the verifier.
The series adds a text-only diagnostics framework under kernel/bpf and
uses it to augment selected verifier errors. Existing verbose(env, ...)
messages are kept, so current selftest expectations and existing log
consumers continue to see the legacy text. The new report has a uniform
outer shape:
Verification failed: <category>: <problem>
Reason:
exact reason for the verification failure, with details
At:
source and instruction annotation
Causal path:
compressed branch and verifier-state events relevant for debugging
Suggestion:
speculation on potential fixes to repair the program
The outer shape is shared, but report construction is category-specific.
The categories are intentionally broad and reviewable. This revision
covers representative cases in Register Type Safety, Memory Safety,
Resource Lifetime Safety, Call Type Safety, Execution Context Safety,
Program Structure, Policy, Verifier Limit, and Verifier Internal errors.
It does not attempt to convert every verbose(env, ...) site for now.
Additional verbose-only errors can be moved into the same framework
incrementally.
The following excerpts are copied from this current run on this branch:
./test_progs -j1 \
-a cpumask/test_populate_invalid_destination,\
cpumask/test_alloc_no_release,\
verifier_helper_value_access/via_variable_no_max_check_1,\
verifier_sock/invalidate_pkt_pointers_from_global_func \
-vv
They show the old terminal error and the exact new diagnostic report,
including the source/instruction annotation.
Call Type Safety, cpumask/test_populate_invalid_destination:
Legacy:
R1 type=scalar expected=fp
Diagnostic:
Verification failed: Call Type Safety: Invalid call argument
Reason:
The first argument (R1) to bpf_cpumask_populate does not satisfy the verifier contract: the kfunc expects 16 bytes of
memory for 'struct cpumask', but it is an integer scalar and not verifier-known memory.
At:
test_populate_invalid_destination @ cpumask_failure.c:234:8
232 | ... 2 | (b7) r1 = 1193046
233 | ... 3 | (b7) r3 = 8
>>> 234 | ret = bpf_cpumask_populate((struct cpumask *)invalid, &bits, sizeof... >>> 4 | (85) call bpf_cpumask_populate#61800
| ^-- error: invalid first argument (R1) for bpf_cpumask_populate
235 | if (!ret) 5 | (56) if w0 != 0x0 goto pc+4
236 | err = 2; 6 | (18) r1 = 0xffffc9000028e000
Causal path:
test_populate_invalid_destination @ cpumask_failure.c:234:8
232 | ... 0 | (bf) r2 = r10
233 | ... 1 | (07) r2 += -8
>>> 234 | ret = bpf_cpumask_populate((struct cpumask *)invalid, &bits, sizeof... >>> 2 | (b7) r1 = 1193046
| ^-- update: R1 changed from context pointer at offset 0 to integer scalar value 1193046
235 | if (!ret) 3 | (b7) r3 = 8
236 | err = 2; 4 | (85) call bpf_cpumask_populate#61800
Suggestion:
Pass stack, map, context, or other verifier-known memory of the expected type and size, not an integer cast to a
pointer.
Register Type Safety, verifier_sock/invalidate_pkt_pointers_from_global_func:
Legacy:
R7 invalid mem access 'scalar'
Diagnostic:
Verification failed: Register Type Safety: Invalid dereference
Reason:
R7 is an integer scalar here, not a pointer to memory.
At:
invalidate_pkt_pointers_from_global_func @ verifier_sock.c:1067:5
1065 | ... 8 | (85) call pc+4
1066 | skb_pull_data1(sk, 0); 9 | (b4) w1 = 42
>>> 1067 | *p = 42; /* this is unsafe */ >>> 10 | (63) *(u32 *)(r7 +0) = r1
| ^-- error: invalid dereference of R7 (scalar)
1068 | ... 11 | (bc) w0 = w6
1069 | } 12 | (95) exit
Causal path:
invalidate_pkt_pointers_from_global_func @ verifier_sock.c:1066:2
1064 | if ((void *)(p + 1) > (void *)(long)sk->data_end) 6 | (b4) w6 = 0
1065 | ... 7 | (b4) w2 = 0
>>> 1066 | skb_pull_data1(sk, 0); >>> 8 | (85) call pc+4
| ^-- invalidated: R7: packet data may have moved; previous value was pkt at offset 0
1067 | *p = 42; /* this is unsafe */ 9 | (b4) w1 = 42
1068 | ... 10 | (63) *(u32 *)(r7 +0) = r1
Suggestion:
Preserve a pointer-valued register where needed, or reload and revalidate the pointer after scalar arithmetic, helper
calls, or other operations that can invalidate it.
Memory Safety, verifier_helper_value_access/via_variable_no_max_check_1:
Legacy:
R1 unbounded memory access, make sure to bounds check any such access
Diagnostic:
Verification failed: Memory Safety: Access outside bounds
Reason:
The verifier cannot prove that this access stays within the object: The access needs offset + size <= 48, but R1 has
no safe upper bound. R1 is map_value; offset is variable: known bits 0x0, unknown mask 0xffffffff, signed range [0,
4294967295], unsigned range [0, 4294967295]; access size 1; object size 48.
At:
via_variable_no_max_check_1 @ verifier_helper_value_access.c:627:2
625 | ... 11 | (b7) r2 = 1
626 | ... 12 | (b7) r3 = 0
>>> 627 | asm volatile (" \ >>> 13 | (85) call bpf_probe_read_kernel#113
| ^-- error: access may be outside object bounds
628 | ... 14 | (95) exit
629 | ...
Causal path:
via_variable_no_max_check_1 @ verifier_helper_value_access.c:627:2
625 | ... 8 | (bf) r1 = r0
626 | ... 9 | (61) r3 = *(u32 *)(r0 +0)
>>> 627 | asm volatile (" \ >>> 10 | (0f) r1 += r3
| ^-- update: R1 changed from map value from map_hash_48b at offset 0 to map value from map_hash_48b
| with variable offset: known bits 0x0, unknown mask 0xffffffff, signed range [0, 4294967295],
| unsigned range [0, 4294967295]
628 | ... 11 | (b7) r2 = 1
629 | ... 12 | (b7) r3 = 0
Suggestion:
Add or adjust a bounds check that proves offset + access size stays within the object.
Resource Lifetime Safety, cpumask/test_alloc_no_release:
Legacy:
Unreleased reference id=2 alloc_insn=0
BPF_EXIT instruction in main prog would lead to reference leak
Diagnostic:
Verification failed: Resource Lifetime Safety: Unreleased resource
Reason:
Owned resource (id=2) was acquired at instruction 0 and still needs to be released before this exit path.
At:
test_alloc_no_release @ cpumask_failure.c:36:5
34 | ... 19 | (7b) *(u64 *)(r10 -8) = r6
35 | ... 20 | (b4) w0 = 0
>>> 36 | int BPF_PROG(test_alloc_no_release, struct task_struct *task, u64 clone_flags) >>> 21 | (95) exit
| ^-- error: owned resource (id=2) still needs release
37 | ...
38 | ...
Causal path:
test_alloc_no_release @ cpumask_common.h:78:12
76 | ...
77 | ...
>>> 78 | cpumask = bpf_cpumask_create(); >>> 0 | (85) call bpf_cpumask_create#61791
| ^-- acquired: owned resource (id=2)
79 | if (!cpumask) { 1 | (bf) r6 = r0
80 | err = 1; 2 | (55) if r6 != 0x0 goto pc+5
test_alloc_no_release @ cpumask_common.h:79:6
77 | ... 0 | (85) call bpf_cpumask_create#61791
78 | cpumask = bpf_cpumask_create(); 1 | (bf) r6 = r0
>>> 79 | if (!cpumask) { >>> 2 | (55) if r6 != 0x0 goto pc+5
| ^-- branch: explored as true, goto followed
80 | err = 1; 3 | (18) r1 = 0xffffc90000252000
81 | ...
test_alloc_no_release @ cpumask_common.h:84:6
82 | ... 9 | (85) call bpf_cpumask_empty#61792
83 | ... 10 | (54) w0 &= 1
>>> 84 | if (!bpf_cpumask_empty(cast(cpumask))) { >>> 11 | (56) if w0 != 0x0 goto pc+7
| ^-- branch: explored as true, goto followed
85 | err = 2; 12 | (18) r1 = 0xffffc90000252000
86 | bpf_cpumask_release(cpumask);
Suggestion:
Release or transfer ownership of the acquired resource on every path before the program exits.
Patch layout:
- Patches 1-3 add the common infrastructure: report sections, diagnostic
categories, source-line lookup, and side-by-side source/instruction
annotations.
- Patches 4-7 add bounded diagnostic history to verifier state. The
history records branch outcomes, material register changes, reference
lifetime events, and execution-context events so reports can explain
the path and causal state transitions that led to the failure.
- Patches 8-16 add the first category-specific reports. These patches
hook selected verifier failure sites and choose the evidence that is
useful for that error class.
- Patch 17 gates diagnostic collection and rendering on verifier log
level, so stats-only loads do not collect the extra path history.
The overhead is limited to verbose log mode through this change.
Evaluation
~~~~~~~~~~
To quantitatively measure and goal on metrics that help assess the
quality of diagnostics (apart from subjective human feedback), we use AI
models (called over APIs) and veristat metrics to compare results.
Models are used as a way to measure repair utility of the extra
diagnostics over a fixed test set. Each prompt contains only a sanitized
source snippet and either the legacy verifier log or the new diagnostic
log. To avoid leaking the answer through the test itself, comments,
annotations, and other source hints that describe the intended failure
were removed. The model is not given internet access, repository access,
test execution, verifier access, or the expected fix. The expected
causes and intended repairs are kept outside the prompt. Under those
constraints, correctness, exact repair rate, output size, reasoning
tokens, cost, and wall time provide a proxy for whether the additional
verifier context makes the failure easier to understand and turn into a
source-level fix.
Verifier cost is assessed by forcing the collection of diagnostics
information during normal verification. This information by default is
supposed to only be collected and processed when logs are enabled, but
forcing it even without a verbose log levels helps us understand the
cost in CPU time and memory that is incurred due to this extra data.
Both evaluations are covered in the sections below.
Repair Quality
--------------
Repair quality is measured by asking API-only models to propose source
fixes from a sanitized source snippet and verifier log. The criterion is
score >= 3 on a 0-4 local grading scale, where 3 means a likely fix with
incomplete detail and 4 means an actionable source-level fix. Score 4 is
reported separately as the exact repair rate. The reported model set
contains 596 successful API responses: 298 diagnostic and 298 legacy.
Main results (details available in Appendix):
Metric Diagnostic Legacy Delta
---------------------------------- ----------- ----------- --------
Answers 298 298
Success rate 97.0% 97.3% -0.3 pp
Exact repair rate 82.2% 72.1% +10.1 pp
Mean score 3.79 3.69 +0.10
Solver cost $8.93 $10.37 -13.8%
Mean output tokens per answer 1662 1975 -15.8%
Mean reasoning tokens per answer 951 1080 -11.9%
Mean wall time per answer 37.3s 44.1s -15.4%
Diagnostic prompts carry more input context. The resulting answers are
still shorter and cheaper. In this run, diagnostics do not materially
change the coarse success rate, but they increase exact repairs by 10.1
percentage points while reducing cost, output tokens, reasoning tokens,
and wall time.
Verifier cost
-------------
Verifier cost is measured with veristat over the BPF selftest programs
selected by tools/testing/selftests/bpf/veristat.cfg, with five
repetitions per configuration. With diagnostics gated by log level, wall
time is within run noise of baseline. Forcing diagnostics on for every
verifier run is much more expensive in terms of CPU time, more than
doubling it.
Configuration Wall time mean Verifier duration Max RSS mean
---------------------------- -------------- ----------------- ------------
bpf-next baseline 25.76s 9.41s 22135 KiB
diagnostics, gated 25.78s 10.29s 21860 KiB
diagnostics, forced on 58.54s 41.72s 21697 KiB
TODO
~~~~
Known follow-up work:
- Use memory.peak in cgroup v2 for veristat memory cost.
- Convert more verbose-only verifier errors into category-specific
reports.
- Integrate loop-convergence failure summarization from Eduard.
- Report candidate kfuncs/helpers for releasing owned resources.
- Explore association of source variables with verifier registers
where debug info permits it.
- Refine suggestions per category and, where useful, link diagnostics
to maintained documentation.
- Bring verifier warnings into the same reporting framework.
Appendix: AI repair details
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The 20 verifier-failing selftest cases are:
Case Diff Category Selftest selector
------- ------ -------------------------- ---------------------------------------------
case-001 easy Call Type Safety cpumask/test_populate_invalid_destination
case-002 easy Resource Lifetime Safety cpumask/test_alloc_no_release
case-003 easy Register Type Safety verifier_spill_fill/check_corrupted_spill_fill
case-004 easy Register Type Safety test_global_funcs/global_func12
case-005 easy Execution Context Safety preempt_lock/preempt_sleepable_helper
case-006 easy Policy verifier_helper_restricted/in_bpf_prog_type_kprobe_1
case-007 medium Memory Safety dynptr/dynptr_slice_var_len1
case-008 medium Call Type Safety dynptr/test_dynptr_skb_small_buff
case-009 medium Call Type Safety task_kfunc/task_kfunc_acquire_untrusted
case-010 medium Register Type Safety test_global_funcs/global_func6
case-011 medium Resource Lifetime Safety dynptr/ringbuf_missing_release2
case-012 medium Execution Context Safety irq/irq_sleepable_helper_global_subprog
case-013 medium Verifier Limit test_global_funcs/global_func1
case-014 hard Memory Safety verifier_helper_value_access/via_variable_no_max_check_1
case-015 hard Register Type Safety verifier_sock/invalidate_pkt_pointers_from_global_func
case-016 hard Resource Lifetime Safety verifier_ref_tracking/check_free_in_one_subbranch
case-017 hard Resource Lifetime Safety irq/irq_restore_ooo
case-018 hard Resource Lifetime Safety res_spin_lock_failure/res_spin_lock_ooo_unlock
case-019 hard Program Structure verifier_loops1/bounded_recursion
case-020 hard Verifier Limit verifier_liveness_exp/liveness_exponential_complexity
The grading scale is:
- 4: identifies the verifier cause and gives an actionable source-level fix.
- 3: gives a likely fix, but with incomplete explanation or detail.
- 2: identifies part of the issue, but not enough to fix confidently.
- 1: gives only a broad verifier-area answer, or a wrong/insufficient fix.
- 0: does not identify the intended verifier failure.
Detailed effort metrics for the model set:
Metric Variant Mean Median P99
----------------------- ---------- -------- -------- --------
Cost per answer diagnostic $0.030 $0.019 $0.203
Cost per answer legacy $0.035 $0.018 $0.223
Input tokens diagnostic 1391 1220 4048
Input tokens legacy 1052 805 3655
Output tokens diagnostic 1662 954 8680
Output tokens legacy 1975 1034 9912
Reasoning tokens diagnostic 951 208 8108
Reasoning tokens legacy 1080 228 6322
Wall time diagnostic 37.3s 18.3s 222.7s
Wall time legacy 44.1s 19.8s 255.5s
Per-model results for diagnostic prompts:
Model profile Ans Succ Exact Mean Cost OutK ReasK Wall
----------------------------------------- --- ----- ----- ---- ------- ---- ----- -----
anthropic-haiku-4.5-default 20 90.0 80.0 3.70 $0.087 11.4 0.0 5.0s
anthropic-opus-4.8-high 20 100.0 90.0 3.90 $0.819 25.5 0.0 15.5s
anthropic-opus-4.8-medium 20 95.0 90.0 3.85 $0.870 27.5 0.0 12.7s
anthropic-sonnet-4.6-high 20 95.0 80.0 3.75 $0.824 48.9 0.0 21.6s
anthropic-sonnet-4.6-medium 20 100.0 65.0 3.65 $0.278 12.4 0.0 6.6s
openai-gpt-5.3-codex-high 20 100.0 80.0 3.80 $0.601 39.8 33.9 25.0s
openai-gpt-5.3-codex-medium 20 95.0 85.0 3.80 $0.287 17.5 11.4 13.5s
openai-gpt-5.5-high 20 100.0 90.0 3.90 $2.356 74.4 65.2 56.8s
openai-gpt-5.5-low 20 100.0 90.0 3.90 $0.686 18.7 8.5 21.3s
openai-gpt-5.5-medium 19 100.0 84.2 3.84 $1.353 41.1 31.8 37.4s
openai-gpt-5.5-none 20 95.0 90.0 3.85 $0.457 11.1 0.0 10.4s
openrouter-deepseek-r1-0528 20 100.0 75.0 3.75 $0.145 61.5 53.8 98.3s
openrouter-deepseek-v3.2 19 100.0 78.9 3.79 $0.028 64.2 58.1 87.3s
openrouter-glm-5.1-high 20 95.0 80.0 3.75 $0.113 28.8 20.7 19.3s
openrouter-qwen3-coder 20 90.0 75.0 3.65 $0.028 12.4 0.0 7.1s
Per-model results for legacy prompts:
Model profile Ans Succ Exact Mean Cost OutK ReasK Wall
----------------------------------------- --- ----- ----- ---- ------- ---- ----- -----
anthropic-haiku-4.5-default 20 90.0 45.0 3.35 $0.081 11.6 0.0 5.0s
anthropic-opus-4.8-high 20 90.0 70.0 3.60 $1.192 42.2 0.0 17.5s
anthropic-opus-4.8-medium 20 95.0 85.0 3.80 $1.001 34.5 0.0 13.4s
anthropic-sonnet-4.6-high 20 100.0 75.0 3.75 $1.181 74.1 0.0 24.4s
anthropic-sonnet-4.6-medium 20 95.0 65.0 3.60 $0.420 23.4 0.0 12.3s
openai-gpt-5.3-codex-high 20 100.0 85.0 3.85 $0.562 37.8 31.6 27.1s
openai-gpt-5.3-codex-medium 20 100.0 75.0 3.75 $0.318 20.3 13.7 13.6s
openai-gpt-5.5-high 19 100.0 78.9 3.79 $2.613 84.0 75.4 98.1s
openai-gpt-5.5-low 20 100.0 75.0 3.75 $0.664 19.0 9.7 21.7s
openai-gpt-5.5-medium 20 100.0 75.0 3.75 $1.602 50.2 41.0 56.1s
openai-gpt-5.5-none 20 95.0 85.0 3.80 $0.416 10.7 0.0 10.9s
openrouter-deepseek-r1-0528 20 95.0 70.0 3.65 $0.149 64.6 57.5 92.5s
openrouter-deepseek-v3.2 20 100.0 60.0 3.60 $0.030 74.3 67.8 98.3s
openrouter-glm-5.1-high 19 100.0 63.2 3.63 $0.115 32.1 24.9 30.4s
openrouter-qwen3-coder 20 100.0 75.0 3.75 $0.022 9.5 0.0 5.4s
Kumar Kartikeya Dwivedi (17):
bpf: Add verifier diagnostics report helpers
bpf: Define verifier diagnostic categories
bpf: Add source and instruction diagnostic context
bpf: Track verifier branch diagnostic history
bpf: Track verifier register diagnostic history
bpf: Track verifier reference diagnostic history
bpf: Track verifier context diagnostic history
bpf: Report Register Type Safety errors
bpf: Report Memory Safety bounds errors
bpf: Report Resource Lifetime reference leaks
bpf: Report Call Type Safety argument errors
bpf: Report Execution Context Safety errors
bpf: Report Program Structure CFG errors
bpf: Report Policy helper and kfunc errors
bpf: Report Verifier Limit errors
bpf: Report Verifier Internal errors
bpf: Gate verifier diagnostics on log level
include/linux/bpf_verifier.h | 6 +
kernel/bpf/Makefile | 2 +-
kernel/bpf/diagnostics.c | 1936 ++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 245 +++++
kernel/bpf/liveness.c | 6 +
kernel/bpf/verifier.c | 1053 +++++++++++++++++-
6 files changed, 3217 insertions(+), 31 deletions(-)
create mode 100644 kernel/bpf/diagnostics.c
create mode 100644 kernel/bpf/diagnostics.h
base-commit: bf29346fc39355cc57118e4e825109f66ac3542d
--
2.53.0
^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:42 ` sashiko-bot
` (2 more replies)
2026-06-05 6:33 ` [PATCH bpf-next v1 02/17] bpf: Define verifier diagnostic categories Kumar Kartikeya Dwivedi
` (15 subsequent siblings)
16 siblings, 3 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Add a small diagnostics renderer for verifier reports and wire it into the
BPF build. The initial helpers emit the common text structure: a failure
header plus reusable report sections.
Wrap report prose at 120 columns so Reason and Suggestion text stays readable
without changing source or instruction gutters.
No verifier call sites use the helpers yet, so existing verifier output
is unchanged.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/Makefile | 2 +-
kernel/bpf/diagnostics.c | 110 +++++++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 19 +++++++
3 files changed, 130 insertions(+), 1 deletion(-)
create mode 100644 kernel/bpf/diagnostics.c
create mode 100644 kernel/bpf/diagnostics.h
diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
index 4dc41bf5780c..90255d80e5be 100644
--- a/kernel/bpf/Makefile
+++ b/kernel/bpf/Makefile
@@ -6,7 +6,7 @@ cflags-nogcse-$(CONFIG_X86)$(CONFIG_CC_IS_GCC) := -fno-gcse
endif
CFLAGS_core.o += -Wno-override-init $(cflags-nogcse-yy)
-obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o cnum.o log.o token.o liveness.o const_fold.o
+obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o cnum.o log.o token.o liveness.o const_fold.o diagnostics.o
obj-$(CONFIG_BPF_SYSCALL) += bpf_iter.o map_iter.o task_iter.o prog_iter.o link_iter.o
obj-$(CONFIG_BPF_SYSCALL) += hashtab.o arraymap.o percpu_freelist.o bpf_lru_list.o lpm_trie.o map_in_map.o bloom_filter.o
obj-$(CONFIG_BPF_SYSCALL) += local_storage.o queue_stack_maps.o ringbuf.o bpf_insn_array.o
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
new file mode 100644
index 000000000000..a6684adc0c31
--- /dev/null
+++ b/kernel/bpf/diagnostics.c
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+
+#include <linux/bpf_verifier.h>
+#include <linux/stdarg.h>
+#include <linux/string.h>
+
+#include "diagnostics.h"
+
+#define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
+#define BPF_DIAG_TEXT_WIDTH 120
+#define BPF_DIAG_TEXT_INDENT " "
+#define BPF_DIAG_MSG_LEN 512
+
+static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
+ const char *first_prefix,
+ const char *next_prefix,
+ const char *text)
+{
+ const char *prefix = first_prefix;
+
+ while (*text) {
+ const char *line = text;
+ int prefix_len = strlen(prefix);
+ int text_width = BPF_DIAG_TEXT_WIDTH - prefix_len;
+ int len = 0, last_space = -1;
+
+ if (text_width < 1)
+ text_width = 1;
+
+ while (line[len] && line[len] != '\n' && len < text_width) {
+ if (line[len] == ' ')
+ last_space = len;
+ len++;
+ }
+
+ if (line[len] && line[len] != '\n' && last_space > 0)
+ len = last_space;
+
+ verbose(env, "%s%.*s\n", prefix, len, line);
+
+ text = line + len;
+ while (*text == ' ')
+ text++;
+ if (*text == '\n')
+ text++;
+
+ prefix = next_prefix;
+ }
+}
+
+static void bpf_diag_print_wrapped_text(struct bpf_verifier_env *env,
+ const char *text)
+{
+ bpf_diag_print_wrapped_prefixed(env, BPF_DIAG_TEXT_INDENT,
+ BPF_DIAG_TEXT_INDENT, text);
+}
+
+static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
+ const char *fmt, va_list args)
+{
+ char buf[1024];
+
+ if (!bpf_verifier_log_needed(&env->log))
+ return;
+
+ vscnprintf(buf, sizeof(buf), fmt, args);
+ bpf_diag_print_wrapped_text(env, buf);
+}
+
+void bpf_diag_report_header(struct bpf_verifier_env *env,
+ const char *category, const char *problem)
+{
+ char problem_buf[BPF_DIAG_MSG_LEN];
+
+ strscpy(problem_buf, problem ?: "", sizeof(problem_buf));
+ if (problem_buf[0] >= 'a' && problem_buf[0] <= 'z')
+ problem_buf[0] += 'A' - 'a';
+
+ verbose(env, "\nVerification failed: %s: %s\n", category,
+ problem_buf);
+}
+
+void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
+{
+ va_list args;
+
+ bpf_diag_report_section(env, "Reason");
+
+ va_start(args, fmt);
+ bpf_diag_vprint_indented(env, fmt, args);
+ va_end(args);
+}
+
+void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title)
+{
+ verbose(env, "\n%s:\n", title);
+}
+
+void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
+{
+ va_list args;
+
+ bpf_diag_report_section(env, "Suggestion");
+
+ va_start(args, fmt);
+ bpf_diag_vprint_indented(env, fmt, args);
+ va_end(args);
+ verbose(env, "\n");
+}
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
new file mode 100644
index 000000000000..f13a939722bf
--- /dev/null
+++ b/kernel/bpf/diagnostics.h
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+
+#ifndef __BPF_DIAGNOSTICS_H
+#define __BPF_DIAGNOSTICS_H
+
+#include <linux/compiler_attributes.h>
+
+struct bpf_verifier_env;
+
+void bpf_diag_report_header(struct bpf_verifier_env *env,
+ const char *category, const char *problem);
+void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
+ __printf(2, 3);
+void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title);
+void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
+ __printf(2, 3);
+
+#endif /* __BPF_DIAGNOSTICS_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 02/17] bpf: Define verifier diagnostic categories
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 19:10 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context Kumar Kartikeya Dwivedi
` (14 subsequent siblings)
16 siblings, 1 reply; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Add a central enum for the verifier diagnostic categories used by structured
reports. Keeping category names in one renderer helper makes later call
sites choose a classification explicitly instead of formatting category
strings locally.
This adds no verifier call sites and keeps existing verifier output unchanged.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 32 +++++++++++++++++++++++++++++---
kernel/bpf/diagnostics.h | 15 ++++++++++++++-
2 files changed, 43 insertions(+), 4 deletions(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index a6684adc0c31..d9a103cf3a16 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -68,8 +68,34 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
bpf_diag_print_wrapped_text(env, buf);
}
+static const char *bpf_diag_category_name(enum bpf_diag_category category)
+{
+ switch (category) {
+ case BPF_DIAG_CATEGORY_MEMORY_SAFETY:
+ return "Memory Safety";
+ case BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY:
+ return "Register Type Safety";
+ case BPF_DIAG_CATEGORY_CALL_TYPE_SAFETY:
+ return "Call Type Safety";
+ case BPF_DIAG_CATEGORY_RESOURCE_LIFETIME_SAFETY:
+ return "Resource Lifetime Safety";
+ case BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY:
+ return "Execution Context Safety";
+ case BPF_DIAG_CATEGORY_PROGRAM_STRUCTURE:
+ return "Program Structure";
+ case BPF_DIAG_CATEGORY_POLICY:
+ return "Policy";
+ case BPF_DIAG_CATEGORY_VERIFIER_LIMIT:
+ return "Verifier Limit";
+ case BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR:
+ default:
+ return "Verifier Internal Error";
+ }
+}
+
void bpf_diag_report_header(struct bpf_verifier_env *env,
- const char *category, const char *problem)
+ enum bpf_diag_category category,
+ const char *problem)
{
char problem_buf[BPF_DIAG_MSG_LEN];
@@ -77,8 +103,8 @@ void bpf_diag_report_header(struct bpf_verifier_env *env,
if (problem_buf[0] >= 'a' && problem_buf[0] <= 'z')
problem_buf[0] += 'A' - 'a';
- verbose(env, "\nVerification failed: %s: %s\n", category,
- problem_buf);
+ verbose(env, "\nVerification failed: %s: %s\n",
+ bpf_diag_category_name(category), problem_buf);
}
void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index f13a939722bf..8bfc7104eeec 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -6,10 +6,23 @@
#include <linux/compiler_attributes.h>
+enum bpf_diag_category {
+ BPF_DIAG_CATEGORY_MEMORY_SAFETY,
+ BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
+ BPF_DIAG_CATEGORY_CALL_TYPE_SAFETY,
+ BPF_DIAG_CATEGORY_RESOURCE_LIFETIME_SAFETY,
+ BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY,
+ BPF_DIAG_CATEGORY_PROGRAM_STRUCTURE,
+ BPF_DIAG_CATEGORY_POLICY,
+ BPF_DIAG_CATEGORY_VERIFIER_LIMIT,
+ BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR,
+};
+
struct bpf_verifier_env;
void bpf_diag_report_header(struct bpf_verifier_env *env,
- const char *category, const char *problem);
+ enum bpf_diag_category category,
+ const char *problem);
void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
__printf(2, 3);
void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title);
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 02/17] bpf: Define verifier diagnostic categories Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 8:48 ` sashiko-bot
2026-06-05 20:22 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history Kumar Kartikeya Dwivedi
` (13 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Teach verifier diagnostics to annotate an instruction with BTF source line
information and nearby BPF instructions. The renderer keeps source text in
a fixed-width lane and prints instructions in a stable right-hand gutter.
Wrap annotation text under the source line so long error labels are readable
while the source and instruction lanes keep their fixed layout.
Keeping source and instruction context in one commit preserves the visual
layout contract that later diagnostic reports rely on.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 384 +++++++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 4 +
2 files changed, 388 insertions(+)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index d9a103cf3a16..652d4627c2b0 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -1,16 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+#include <linux/bpf.h>
#include <linux/bpf_verifier.h>
+#include <linux/btf.h>
+#include <linux/kernel.h>
#include <linux/stdarg.h>
#include <linux/string.h>
+#include "disasm.h"
#include "diagnostics.h"
#define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
#define BPF_DIAG_TEXT_WIDTH 120
#define BPF_DIAG_TEXT_INDENT " "
#define BPF_DIAG_MSG_LEN 512
+#define BPF_DIAG_SOURCE_CONTEXT 2
+#define BPF_DIAG_INSN_CONTEXT 2
+#define BPF_DIAG_COLUMN_GAP 3
+#define BPF_DIAG_SOURCE_LANE_WIDTH 88
+#define BPF_DIAG_TAB_WIDTH 8
+
+struct bpf_diag_source {
+ const char *file;
+ const char *line;
+ u32 file_name_off;
+ int line_num;
+ int line_col;
+};
+
+struct bpf_diag_insn {
+ char text[160];
+ int idx;
+ bool valid;
+};
+
+struct bpf_diag_insn_buf {
+ char *buf;
+ size_t size;
+ size_t len;
+};
static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
const char *first_prefix,
@@ -68,6 +97,282 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
bpf_diag_print_wrapped_text(env, buf);
}
+static int bpf_diag_line_width(unsigned int line)
+{
+ int width = 1;
+
+ while (line >= 10) {
+ line /= 10;
+ width++;
+ }
+
+ return width;
+}
+
+static const char *bpf_diag_func_name(struct bpf_verifier_env *env, u32 insn_idx)
+{
+ const struct bpf_subprog_info *subprog;
+ const struct bpf_func_info *info;
+ const struct btf_type *type;
+ int subprogno;
+
+ if (!env->prog->aux->func_info || !env->prog->aux->btf)
+ return NULL;
+
+ subprog = bpf_find_containing_subprog(env, insn_idx);
+ if (!subprog)
+ return NULL;
+
+ subprogno = subprog - env->subprog_info;
+ info = &env->prog->aux->func_info[subprogno];
+ type = btf_type_by_id(env->prog->aux->btf, info->type_id);
+ if (!type)
+ return NULL;
+
+ return btf_name_by_offset(env->prog->aux->btf, type->name_off);
+}
+
+static bool bpf_diag_fill_source(struct bpf_verifier_env *env,
+ const struct bpf_line_info *linfo,
+ struct bpf_diag_source *src)
+{
+ const char *file, *line;
+
+ file = btf_name_by_offset(env->prog->aux->btf, linfo->file_name_off);
+ line = btf_name_by_offset(env->prog->aux->btf, linfo->line_off);
+ if (!file || !*file || !line || !*line)
+ return false;
+
+ src->file = kbasename(file);
+ src->line = line;
+ src->file_name_off = linfo->file_name_off;
+ src->line_num = BPF_LINE_INFO_LINE_NUM(linfo->line_col);
+ src->line_col = BPF_LINE_INFO_LINE_COL(linfo->line_col);
+ return true;
+}
+
+static bool bpf_diag_get_source(struct bpf_verifier_env *env, u32 insn_idx,
+ struct bpf_diag_source *src)
+{
+ const struct bpf_line_info *linfo;
+
+ linfo = bpf_find_linfo(env->prog, insn_idx);
+ if (!linfo)
+ return false;
+
+ return bpf_diag_fill_source(env, linfo, src);
+}
+
+static const char *bpf_diag_find_source_line(struct bpf_verifier_env *env,
+ u32 file_name_off, int line_num)
+{
+ const struct bpf_line_info *linfo = env->prog->aux->linfo;
+ struct bpf_diag_source src;
+ u32 i;
+
+ for (i = 0; i < env->prog->aux->nr_linfo; i++) {
+ if (linfo[i].file_name_off != file_name_off ||
+ BPF_LINE_INFO_LINE_NUM(linfo[i].line_col) != line_num)
+ continue;
+ if (bpf_diag_fill_source(env, &linfo[i], &src))
+ return src.line;
+ }
+
+ return NULL;
+}
+
+static int bpf_diag_line_indent(const char *line)
+{
+ int indent = 0;
+
+ while (*line == ' ' || *line == '\t') {
+ if (*line == '\t')
+ indent = round_up(indent + 1, BPF_DIAG_TAB_WIDTH);
+ else
+ indent++;
+ line++;
+ }
+
+ return indent;
+}
+
+static const char *bpf_diag_marker_label(char marker)
+{
+ switch (marker) {
+ case '!':
+ return "error";
+ case '?':
+ return "branch";
+ case '~':
+ return "update";
+ case '+':
+ return "acquired";
+ case '-':
+ return "released";
+ default:
+ return "note";
+ }
+}
+
+static void bpf_diag_insn_print(void *private_data, const char *fmt, ...)
+{
+ struct bpf_diag_insn_buf *buf = private_data;
+ va_list args;
+
+ if (buf->len >= buf->size)
+ return;
+
+ va_start(args, fmt);
+ buf->len += vscnprintf(buf->buf + buf->len, buf->size - buf->len,
+ fmt, args);
+ va_end(args);
+}
+
+static const char *bpf_diag_kfunc_name(void *private_data,
+ const struct bpf_insn *insn)
+{
+ const struct btf_type *func;
+ struct btf *btf;
+
+ (void)private_data;
+
+ if (insn->src_reg != BPF_PSEUDO_KFUNC_CALL || insn->off)
+ return NULL;
+
+ btf = bpf_get_btf_vmlinux();
+ if (IS_ERR_OR_NULL(btf))
+ return NULL;
+
+ func = btf_type_by_id(btf, insn->imm);
+ if (!func)
+ return NULL;
+
+ return btf_name_by_offset(btf, func->name_off);
+}
+
+static void bpf_diag_format_insn(struct bpf_verifier_env *env, int insn_idx,
+ struct bpf_diag_insn *diag_insn)
+{
+ struct bpf_insn *insn;
+ struct bpf_diag_insn_buf buf = {
+ .buf = diag_insn->text,
+ .size = sizeof(diag_insn->text),
+ };
+ const struct bpf_insn_cbs cbs = {
+ .cb_call = bpf_diag_kfunc_name,
+ .cb_print = bpf_diag_insn_print,
+ .private_data = &buf,
+ };
+
+ diag_insn->idx = insn_idx;
+ diag_insn->valid = false;
+ diag_insn->text[0] = '\0';
+
+ if (insn_idx < 0 || insn_idx >= env->prog->len)
+ return;
+
+ if (insn_idx > 0 && bpf_is_ldimm64(&env->prog->insnsi[insn_idx - 1]))
+ return;
+
+ insn = &env->prog->insnsi[insn_idx];
+ if (bpf_is_ldimm64(insn) && insn_idx + 1 >= env->prog->len)
+ return;
+
+ print_bpf_insn(&cbs, insn, env->allow_ptr_leaks);
+ while (buf.len && (diag_insn->text[buf.len - 1] == '\n' ||
+ diag_insn->text[buf.len - 1] == '\r'))
+ diag_insn->text[--buf.len] = '\0';
+
+ diag_insn->valid = true;
+}
+
+static void bpf_diag_format_source_text(char *buf, size_t size,
+ const char *line, int width)
+{
+ bool truncated = false;
+ int col = 0, len = 0;
+
+ if (!size)
+ return;
+ if (width <= 0) {
+ buf[0] = '\0';
+ return;
+ }
+
+ line = line ?: "...";
+ while (*line && col < width && len + 1 < size) {
+ if (*line == '\t') {
+ int next = round_up(col + 1, BPF_DIAG_TAB_WIDTH);
+
+ while (col < next && col < width && len + 1 < size) {
+ buf[len++] = ' ';
+ col++;
+ }
+ line++;
+ continue;
+ }
+
+ buf[len++] = *line++;
+ col++;
+ }
+
+ if (*line)
+ truncated = true;
+ if (truncated) {
+ int ellipsis_len = min(3, width);
+
+ while (len > 0 && col > width - ellipsis_len) {
+ len--;
+ col--;
+ }
+ while (ellipsis_len-- && len + 1 < size) {
+ buf[len++] = '.';
+ col++;
+ }
+ }
+
+ buf[len] = '\0';
+}
+
+static void bpf_diag_format_source_lane(char *buf, size_t size,
+ const char *source_prefix,
+ int source_line_width,
+ int line_num, const char *line)
+{
+ int len, text_width;
+
+ len = scnprintf(buf, size, "%s%*d | ",
+ source_prefix, source_line_width, line_num);
+ if (len >= (int)size)
+ return;
+
+ text_width = BPF_DIAG_SOURCE_LANE_WIDTH - len;
+ bpf_diag_format_source_text(buf + len, size - len, line, text_width);
+}
+
+static void bpf_diag_print_source_insn_line(struct bpf_verifier_env *env,
+ const char *source_prefix,
+ int source_line_width,
+ int line_num, const char *line,
+ const struct bpf_diag_insn *diag_insn,
+ int focus_insn_idx,
+ int insn_width)
+{
+ char source_lane[BPF_DIAG_SOURCE_LANE_WIDTH + 1];
+
+ bpf_diag_format_source_lane(source_lane, sizeof(source_lane),
+ source_prefix, source_line_width,
+ line_num, line);
+
+ verbose(env, " %-*s%*s", BPF_DIAG_SOURCE_LANE_WIDTH,
+ source_lane, BPF_DIAG_COLUMN_GAP, "");
+ if (diag_insn->valid)
+ verbose(env, "%s%*d | %s",
+ diag_insn->idx == focus_insn_idx ? ">>> " : " ",
+ insn_width, diag_insn->idx, diag_insn->text);
+ verbose(env, "\n");
+}
+
static const char *bpf_diag_category_name(enum bpf_diag_category category)
{
switch (category) {
@@ -93,6 +398,7 @@ static const char *bpf_diag_category_name(enum bpf_diag_category category)
}
}
+
void bpf_diag_report_header(struct bpf_verifier_env *env,
enum bpf_diag_category category,
const char *problem)
@@ -134,3 +440,81 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
va_end(args);
verbose(env, "\n");
}
+
+static void bpf_diag_print_source_annotation(struct bpf_verifier_env *env,
+ int line_width, int indent,
+ const char *label,
+ const char *msg)
+{
+ char first_prefix[128], next_prefix[128], text[BPF_DIAG_MSG_LEN];
+
+ scnprintf(first_prefix, sizeof(first_prefix), " %*s | %*s^-- ",
+ line_width + 4, "", indent, "");
+ scnprintf(next_prefix, sizeof(next_prefix), " %*s | %*s ",
+ line_width + 4, "", indent, "");
+ scnprintf(text, sizeof(text), "%s: %s", label, msg);
+
+ bpf_diag_print_wrapped_prefixed(env, first_prefix, next_prefix, text);
+}
+
+void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
+ char marker, const char *fmt, ...)
+{
+ struct bpf_diag_source src;
+ struct bpf_diag_insn diag_insn[1 + BPF_DIAG_INSN_CONTEXT * 2];
+ char msg[BPF_DIAG_MSG_LEN];
+ const char *func, *label;
+ int start_line, end_line, line_num, indent, width;
+ int insn_width, i;
+ va_list args;
+
+ va_start(args, fmt);
+ vscnprintf(msg, sizeof(msg), fmt, args);
+ va_end(args);
+ label = bpf_diag_marker_label(marker);
+
+ if (!bpf_diag_get_source(env, insn_idx, &src)) {
+ verbose(env, " insn %u\n", insn_idx);
+ bpf_diag_print_source_annotation(env, 0, 0, label, msg);
+ return;
+ }
+
+ func = bpf_diag_func_name(env, insn_idx);
+ if (func && *func)
+ verbose(env, " %s @ %s:%d:%d\n", func, src.file,
+ src.line_num, src.line_col);
+ else
+ verbose(env, " %s:%d:%d\n", src.file, src.line_num,
+ src.line_col);
+
+ start_line = max_t(int, 1, src.line_num - BPF_DIAG_SOURCE_CONTEXT);
+ end_line = src.line_num + BPF_DIAG_SOURCE_CONTEXT;
+ width = bpf_diag_line_width(end_line);
+ indent = bpf_diag_line_indent(src.line);
+ insn_width = bpf_diag_line_width(env->prog->len ? env->prog->len - 1 : 0);
+
+ for (i = 0; i < ARRAY_SIZE(diag_insn); i++) {
+ int row = i - BPF_DIAG_INSN_CONTEXT;
+
+ bpf_diag_format_insn(env, insn_idx + row, &diag_insn[i]);
+ }
+
+ for (line_num = start_line; line_num <= end_line; line_num++) {
+ const char *line;
+ int row = line_num - src.line_num;
+
+ line = line_num == src.line_num ?
+ src.line :
+ bpf_diag_find_source_line(env, src.file_name_off, line_num);
+
+ bpf_diag_print_source_insn_line(env,
+ line_num == src.line_num ?
+ ">>> " : " ",
+ width, line_num, line,
+ &diag_insn[row + BPF_DIAG_INSN_CONTEXT],
+ insn_idx, insn_width);
+ if (line_num == src.line_num)
+ bpf_diag_print_source_annotation(env, width, indent,
+ label, msg);
+ }
+}
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index 8bfc7104eeec..ba60cc3af548 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -5,6 +5,7 @@
#define __BPF_DIAGNOSTICS_H
#include <linux/compiler_attributes.h>
+#include <linux/types.h>
enum bpf_diag_category {
BPF_DIAG_CATEGORY_MEMORY_SAFETY,
@@ -28,5 +29,8 @@ void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title);
void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
__printf(2, 3);
+void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
+ char marker, const char *fmt, ...)
+ __printf(4, 5);
#endif /* __BPF_DIAGNOSTICS_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (2 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:50 ` sashiko-bot
` (2 more replies)
2026-06-05 6:33 ` [PATCH bpf-next v1 05/17] bpf: Track verifier register " Kumar Kartikeya Dwivedi
` (12 subsequent siblings)
16 siblings, 3 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Add bounded diagnostic history to verifier states and copy/free it with
normal verifier-state lifetime management. This history is separate from
jump history because it is ordered for user-facing diagnostics instead of
precision backtracking.
Record conditional branch outcomes as true or false so later reports can
show the path that reached a verifier error. If no visible events exist,
print a generic fallback from the history renderer itself.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
include/linux/bpf_verifier.h | 5 ++
kernel/bpf/diagnostics.c | 128 +++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 24 +++++++
kernel/bpf/verifier.c | 7 ++
4 files changed, 164 insertions(+)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index c248ff41f42a..55921b8a5d5c 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -437,6 +437,8 @@ struct bpf_jmp_history_entry {
static_assert(MAX_CALL_FRAMES <= (1 << 3));
static_assert(MAX_BPF_STACK / 8 <= (1 << 6));
+struct bpf_diag_history_event;
+
/* Maximum number of bpf_reg_state objects that can exist at once */
#define MAX_STACK_ARG_SLOTS (MAX_BPF_FUNC_ARGS - MAX_BPF_FUNC_REG_ARGS)
#define BPF_ID_MAP_SIZE ((MAX_BPF_REG + MAX_BPF_STACK / BPF_REG_SIZE + \
@@ -521,6 +523,9 @@ struct bpf_verifier_state {
*/
struct bpf_jmp_history_entry *jmp_history;
u32 jmp_history_cnt;
+ struct bpf_diag_history_event *diag_history;
+ u32 diag_history_cnt;
+ u32 diag_history_omitted;
u32 dfs_depth;
u32 callback_unroll_depth;
u32 may_goto_depth;
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 652d4627c2b0..50cc50a394ca 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -5,6 +5,7 @@
#include <linux/bpf_verifier.h>
#include <linux/btf.h>
#include <linux/kernel.h>
+#include <linux/slab.h>
#include <linux/stdarg.h>
#include <linux/string.h>
@@ -518,3 +519,130 @@ void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
label, msg);
}
}
+
+void bpf_diag_clear_history(struct bpf_verifier_state *state)
+{
+ kfree(state->diag_history);
+ state->diag_history = NULL;
+ state->diag_history_cnt = 0;
+ state->diag_history_omitted = 0;
+}
+
+void bpf_diag_copy_history(struct bpf_verifier_state *dst,
+ const struct bpf_verifier_state *src)
+{
+ struct bpf_diag_history_event *history;
+
+ if (!src->diag_history_cnt) {
+ bpf_diag_clear_history(dst);
+ dst->diag_history_omitted = src->diag_history_omitted;
+ return;
+ }
+
+ history = krealloc_array(dst->diag_history, src->diag_history_cnt,
+ sizeof(*history), GFP_KERNEL_ACCOUNT);
+ if (!history) {
+ kfree(dst->diag_history);
+ dst->diag_history = NULL;
+ dst->diag_history_cnt = 0;
+ dst->diag_history_omitted = src->diag_history_omitted +
+ src->diag_history_cnt;
+ return;
+ }
+
+ dst->diag_history = history;
+ memcpy(dst->diag_history, src->diag_history,
+ src->diag_history_cnt * sizeof(*dst->diag_history));
+ dst->diag_history_cnt = src->diag_history_cnt;
+ dst->diag_history_omitted = src->diag_history_omitted;
+}
+
+static void
+bpf_diag_drop_oldest_and_append_history(struct bpf_verifier_state *state,
+ const struct bpf_diag_history_event *event)
+{
+ if (!state->diag_history_cnt) {
+ state->diag_history_omitted++;
+ return;
+ }
+
+ memmove(state->diag_history, state->diag_history + 1,
+ (state->diag_history_cnt - 1) * sizeof(*state->diag_history));
+ state->diag_history[state->diag_history_cnt - 1] = *event;
+ state->diag_history_omitted++;
+}
+
+static void bpf_diag_append_history(struct bpf_verifier_state *state,
+ const struct bpf_diag_history_event *event)
+{
+ struct bpf_diag_history_event *history;
+
+ if (!state)
+ return;
+
+ if (state->diag_history_cnt < BPF_DIAG_HISTORY_MAX) {
+ history = krealloc_array(state->diag_history,
+ state->diag_history_cnt + 1,
+ sizeof(*history), GFP_KERNEL_ACCOUNT);
+ if (!history) {
+ bpf_diag_drop_oldest_and_append_history(state, event);
+ return;
+ }
+
+ state->diag_history = history;
+ state->diag_history[state->diag_history_cnt++] = *event;
+ return;
+ }
+
+ bpf_diag_drop_oldest_and_append_history(state, event);
+}
+
+void bpf_diag_record_branch(struct bpf_verifier_state *state, u32 insn_idx,
+ bool cond_true)
+{
+ struct bpf_diag_history_event event = {
+ .insn_idx = insn_idx,
+ .kind = BPF_DIAG_HISTORY_BRANCH,
+ .branch.cond_true = cond_true,
+ };
+
+ bpf_diag_append_history(state, &event);
+}
+
+void bpf_diag_print_history(struct bpf_verifier_env *env)
+{
+ const struct bpf_verifier_state *state = env->cur_state;
+ const struct bpf_diag_history_event *event;
+ bool printed = false;
+ u32 i;
+
+ bpf_diag_report_section(env, "Causal path");
+
+ if (!state)
+ return;
+
+ if (state->diag_history_omitted)
+ verbose(env, " ... %u earlier diagnostic events omitted by display limit ...\n",
+ state->diag_history_omitted);
+
+ for (i = 0; i < state->diag_history_cnt; i++) {
+ event = &state->diag_history[i];
+
+ switch (event->kind) {
+ case BPF_DIAG_HISTORY_BRANCH:
+ bpf_diag_report_source(env, event->insn_idx, '?',
+ "explored as %s, goto %s",
+ event->branch.cond_true ? "true" :
+ "false",
+ event->branch.cond_true ? "followed" :
+ "not followed");
+ printed = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (!printed && !state->diag_history_omitted)
+ verbose(env, " no recorded diagnostic events on this path\n");
+}
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index ba60cc3af548..1392ff5fb027 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -19,7 +19,24 @@ enum bpf_diag_category {
BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR,
};
+struct bpf_diag_history_event {
+ u32 insn_idx;
+ u8 kind;
+ union {
+ struct {
+ bool cond_true;
+ } branch;
+ };
+};
+
+#define BPF_DIAG_HISTORY_MAX 1024
+
+enum bpf_diag_history_kind {
+ BPF_DIAG_HISTORY_BRANCH,
+};
+
struct bpf_verifier_env;
+struct bpf_verifier_state;
void bpf_diag_report_header(struct bpf_verifier_env *env,
enum bpf_diag_category category,
@@ -33,4 +50,11 @@ void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
char marker, const char *fmt, ...)
__printf(4, 5);
+void bpf_diag_clear_history(struct bpf_verifier_state *state);
+void bpf_diag_copy_history(struct bpf_verifier_state *dst,
+ const struct bpf_verifier_state *src);
+void bpf_diag_record_branch(struct bpf_verifier_state *state, u32 insn_idx,
+ bool cond_true);
+void bpf_diag_print_history(struct bpf_verifier_env *env);
+
#endif /* __BPF_DIAGNOSTICS_H */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 8ed484cb1a8a..d60e1309957a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -32,6 +32,7 @@
#include <linux/trace_events.h>
#include <linux/kallsyms.h>
+#include "diagnostics.h"
#include "disasm.h"
static const struct bpf_verifier_ops * const bpf_verifier_ops[] = {
@@ -1598,6 +1599,7 @@ void bpf_free_verifier_state(struct bpf_verifier_state *state,
}
kfree(state->refs);
bpf_clear_jmp_history(state);
+ bpf_diag_clear_history(state);
if (free_self)
kfree(state);
}
@@ -1624,6 +1626,7 @@ int bpf_copy_verifier_state(struct bpf_verifier_state *dst_state,
if (!dst_state->jmp_history)
return -ENOMEM;
dst_state->jmp_history_cnt = src->jmp_history_cnt;
+ bpf_diag_copy_history(dst_state, src);
/* if dst has more stack frames then src frame, free them, this is also
* necessary in case of exceptional exits using bpf_throw.
@@ -15933,6 +15936,7 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
}
if (env->log.level & BPF_LOG_LEVEL)
print_insn_state(env, this_branch, this_branch->curframe);
+ bpf_diag_record_branch(this_branch, *insn_idx, true);
*insn_idx += insn->off;
return 0;
} else if (pred == 0) {
@@ -15948,6 +15952,7 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
}
if (env->log.level & BPF_LOG_LEVEL)
print_insn_state(env, this_branch, this_branch->curframe);
+ bpf_diag_record_branch(this_branch, *insn_idx, false);
return 0;
}
@@ -15980,6 +15985,8 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
other_branch_regs[insn->dst_reg] = env->true_reg1;
if (BPF_SRC(insn->code) == BPF_X)
other_branch_regs[insn->src_reg] = env->true_reg2;
+ bpf_diag_record_branch(this_branch, *insn_idx, false);
+ bpf_diag_record_branch(other_branch, *insn_idx, true);
if (BPF_SRC(insn->code) == BPF_X &&
src_reg->type == SCALAR_VALUE && src_reg->id &&
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 05/17] bpf: Track verifier register diagnostic history
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (3 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
` (2 more replies)
2026-06-05 6:33 ` [PATCH bpf-next v1 06/17] bpf: Track verifier reference " Kumar Kartikeya Dwivedi
` (11 subsequent siblings)
16 siblings, 3 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Record material register and outgoing stack argument changes so diagnostics can
explain how a value reached its current type, bounds, or unreadable state.
Store old and new register types, scalar ranges, tnum value and mask, map and
BTF type identity, and basic operand metadata.
Register-scoped history starts at the latest matching modification and then
shows later branch outcomes.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 492 ++++++++++++++++++++++++++++++++++++++-
kernel/bpf/diagnostics.h | 82 ++++++-
kernel/bpf/verifier.c | 112 ++++++++-
3 files changed, 677 insertions(+), 9 deletions(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 50cc50a394ca..1bdc7e69fbce 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -21,6 +21,7 @@
#define BPF_DIAG_COLUMN_GAP 3
#define BPF_DIAG_SOURCE_LANE_WIDTH 88
#define BPF_DIAG_TAB_WIDTH 8
+#define BPF_DIAG_REG_DESC_LEN 512
struct bpf_diag_source {
const char *file;
@@ -86,6 +87,34 @@ static void bpf_diag_print_wrapped_text(struct bpf_verifier_env *env,
BPF_DIAG_TEXT_INDENT, text);
}
+void bpf_diag_format_btf_type(char *buf, size_t size,
+ const struct btf_type *type,
+ const char *type_name)
+{
+ const char *kind;
+
+ switch (BTF_INFO_KIND(type->info)) {
+ case BTF_KIND_STRUCT:
+ kind = "struct";
+ break;
+ case BTF_KIND_UNION:
+ kind = "union";
+ break;
+ case BTF_KIND_ENUM:
+ case BTF_KIND_ENUM64:
+ kind = "enum";
+ break;
+ default:
+ kind = btf_type_str(type);
+ break;
+ }
+
+ if (type_name && *type_name)
+ scnprintf(buf, size, "'%s %s'", kind, type_name);
+ else
+ scnprintf(buf, size, "'%s'", kind);
+}
+
static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
const char *fmt, va_list args)
{
@@ -206,6 +235,8 @@ static const char *bpf_diag_marker_label(char marker)
return "branch";
case '~':
return "update";
+ case 'x':
+ return "invalidated";
case '+':
return "acquired";
case '-':
@@ -215,6 +246,14 @@ static const char *bpf_diag_marker_label(char marker)
}
}
+static const char *bpf_diag_reg_map_name(const struct bpf_map *map)
+{
+ if (!map || !map->name[0])
+ return NULL;
+
+ return map->name;
+}
+
static void bpf_diag_insn_print(void *private_data, const char *fmt, ...)
{
struct bpf_diag_insn_buf *buf = private_data;
@@ -399,7 +438,6 @@ static const char *bpf_diag_category_name(enum bpf_diag_category category)
}
}
-
void bpf_diag_report_header(struct bpf_verifier_env *env,
enum bpf_diag_category category,
const char *problem)
@@ -442,6 +480,77 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
verbose(env, "\n");
}
+static const char *bpf_diag_s64_bound_name(s64 value)
+{
+ if (value == S64_MIN)
+ return "S64_MIN";
+ if (value == S64_MAX)
+ return "S64_MAX";
+ return NULL;
+}
+
+static const char *bpf_diag_u64_bound_name(u64 value)
+{
+ if (value == U64_MAX)
+ return "U64_MAX";
+ return NULL;
+}
+
+static void bpf_diag_format_s64_value(char *buf, size_t size, s64 value)
+{
+ const char *name = bpf_diag_s64_bound_name(value);
+
+ if (name)
+ strscpy(buf, name, size);
+ else
+ scnprintf(buf, size, "%lld", value);
+}
+
+static void bpf_diag_format_u64_value(char *buf, size_t size, u64 value)
+{
+ const char *name = bpf_diag_u64_bound_name(value);
+
+ if (name)
+ strscpy(buf, name, size);
+ else
+ scnprintf(buf, size, "%llu", value);
+}
+
+static bool bpf_diag_range_unknown(s64 smin, s64 smax, u64 umin, u64 umax)
+{
+ return smin == S64_MIN && smax == S64_MAX &&
+ umin == 0 && umax == U64_MAX;
+}
+
+static bool bpf_diag_var_off_unknown(u64 value, u64 mask)
+{
+ return value == 0 && mask == U64_MAX;
+}
+
+static bool bpf_diag_snapshot_unknown(const struct bpf_diag_reg_snapshot *snapshot)
+{
+ return bpf_diag_var_off_unknown(snapshot->var_off_value,
+ snapshot->var_off_mask) &&
+ bpf_diag_range_unknown(snapshot->smin_value, snapshot->smax_value,
+ snapshot->umin_value, snapshot->umax_value);
+}
+
+static void bpf_diag_format_scalar_range(char *buf, size_t size,
+ s64 smin, s64 smax,
+ u64 umin, u64 umax)
+{
+ char smin_buf[32], smax_buf[32], umin_buf[32], umax_buf[32];
+
+ bpf_diag_format_s64_value(smin_buf, sizeof(smin_buf), smin);
+ bpf_diag_format_s64_value(smax_buf, sizeof(smax_buf), smax);
+ bpf_diag_format_u64_value(umin_buf, sizeof(umin_buf), umin);
+ bpf_diag_format_u64_value(umax_buf, sizeof(umax_buf), umax);
+
+ scnprintf(buf, size,
+ "signed range [%s, %s], unsigned range [%s, %s]",
+ smin_buf, smax_buf, umin_buf, umax_buf);
+}
+
static void bpf_diag_print_source_annotation(struct bpf_verifier_env *env,
int line_width, int indent,
const char *label,
@@ -609,11 +718,379 @@ void bpf_diag_record_branch(struct bpf_verifier_state *state, u32 insn_idx,
bpf_diag_append_history(state, &event);
}
-void bpf_diag_print_history(struct bpf_verifier_env *env)
+static void bpf_diag_snapshot_one_reg(struct bpf_diag_reg_snapshot *snapshot,
+ const struct bpf_reg_state *reg)
+{
+ snapshot->type = reg->type;
+ if (base_type(reg->type) == PTR_TO_MAP_VALUE ||
+ base_type(reg->type) == CONST_PTR_TO_MAP ||
+ base_type(reg->type) == PTR_TO_MAP_KEY)
+ snapshot->map_ptr = reg->map_ptr;
+ if (base_type(reg->type) == PTR_TO_BTF_ID && reg->btf && reg->btf_id) {
+ snapshot->btf = reg->btf;
+ snapshot->btf_id = reg->btf_id;
+ }
+ snapshot->var_off_known = tnum_is_const(reg->var_off);
+ snapshot->var_off_value = reg->var_off.value;
+ snapshot->var_off_mask = reg->var_off.mask;
+ snapshot->smin_value = reg_smin(reg);
+ snapshot->smax_value = reg_smax(reg);
+ snapshot->umin_value = reg_umin(reg);
+ snapshot->umax_value = reg_umax(reg);
+}
+
+static void bpf_diag_snapshot_reg(struct bpf_diag_history_event *event,
+ enum bpf_diag_reg_mod_reason reason,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg)
+{
+ event->reg.reason = reason;
+ bpf_diag_snapshot_one_reg(&event->reg.old, old_reg);
+ bpf_diag_snapshot_one_reg(&event->reg.new, new_reg);
+}
+
+static bool bpf_diag_snapshot_eq(const struct bpf_diag_reg_snapshot *old,
+ const struct bpf_diag_reg_snapshot *new)
+{
+ return old->type == new->type &&
+ old->map_ptr == new->map_ptr &&
+ old->btf == new->btf &&
+ old->btf_id == new->btf_id &&
+ old->var_off_known == new->var_off_known &&
+ old->var_off_value == new->var_off_value &&
+ old->var_off_mask == new->var_off_mask &&
+ old->smin_value == new->smin_value &&
+ old->smax_value == new->smax_value &&
+ old->umin_value == new->umin_value &&
+ old->umax_value == new->umax_value;
+}
+
+static bool bpf_diag_reg_snapshot_eq(const struct bpf_diag_history_event *event)
+{
+ return bpf_diag_snapshot_eq(&event->reg.old, &event->reg.new);
+}
+
+static void bpf_diag_record_reg_mod_reason(struct bpf_verifier_state *state,
+ u32 insn_idx, u8 dst_reg,
+ bool src_valid, u8 src_reg,
+ u8 opcode,
+ enum bpf_diag_reg_mod_reason reason,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg)
+{
+ struct bpf_diag_history_event event = {
+ .insn_idx = insn_idx,
+ .kind = BPF_DIAG_HISTORY_REG_MOD,
+ .reg.dst_reg = dst_reg,
+ .reg.src_reg = src_reg,
+ .reg.opcode = opcode,
+ .reg.src_valid = src_valid,
+ };
+
+ if (state && state->frame[state->curframe])
+ event.reg.frameno = state->frame[state->curframe]->frameno;
+
+ bpf_diag_snapshot_reg(&event, reason, old_reg, new_reg);
+ if (reason == BPF_DIAG_REG_MOD_WRITE &&
+ bpf_diag_reg_snapshot_eq(&event))
+ return;
+
+ bpf_diag_append_history(state, &event);
+}
+
+void bpf_diag_record_reg_mod(struct bpf_verifier_state *state, u32 insn_idx,
+ u8 dst_reg, bool src_valid, u8 src_reg, u8 opcode,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg)
+{
+ bpf_diag_record_reg_mod_reason(state, insn_idx, dst_reg, src_valid,
+ src_reg, opcode, BPF_DIAG_REG_MOD_WRITE,
+ old_reg, new_reg);
+}
+
+void bpf_diag_record_reg_invalidate(struct bpf_verifier_state *state,
+ u32 insn_idx, u8 dst_reg,
+ enum bpf_diag_reg_mod_reason reason,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg)
+{
+ bpf_diag_record_reg_mod_reason(state, insn_idx, dst_reg, false, 0, 0,
+ reason, old_reg, new_reg);
+}
+
+void bpf_diag_record_stack_arg(struct bpf_verifier_state *state, u32 insn_idx,
+ u32 frameno, u8 slot,
+ enum bpf_diag_stack_arg_reason reason,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg)
+{
+ struct bpf_diag_history_event event = {
+ .insn_idx = insn_idx,
+ .kind = BPF_DIAG_HISTORY_STACK_ARG,
+ .stack_arg.frameno = frameno,
+ .stack_arg.slot = slot,
+ .stack_arg.reason = reason,
+ };
+
+ bpf_diag_snapshot_one_reg(&event.stack_arg.old, old_reg);
+ bpf_diag_snapshot_one_reg(&event.stack_arg.new, new_reg);
+
+ if (reason == BPF_DIAG_STACK_ARG_WRITE &&
+ bpf_diag_snapshot_eq(&event.stack_arg.old, &event.stack_arg.new))
+ return;
+
+ bpf_diag_append_history(state, &event);
+}
+
+static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
+ const struct bpf_diag_history_opts *opts)
+{
+ int i;
+
+ if (!opts || (opts->scope != BPF_DIAG_HISTORY_SCOPE_REG &&
+ opts->scope != BPF_DIAG_HISTORY_SCOPE_STACK_ARG))
+ return 0;
+
+ for (i = state->diag_history_cnt; i > 0; i--) {
+ const struct bpf_diag_history_event *event = &state->diag_history[i - 1];
+
+ if (opts->scope == BPF_DIAG_HISTORY_SCOPE_REG &&
+ event->kind == BPF_DIAG_HISTORY_REG_MOD &&
+ event->reg.dst_reg == opts->regno &&
+ event->reg.frameno == opts->frameno)
+ return i - 1;
+ if (opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG &&
+ event->kind == BPF_DIAG_HISTORY_STACK_ARG &&
+ event->stack_arg.slot == opts->stack_arg_slot &&
+ event->stack_arg.frameno == opts->frameno)
+ return i - 1;
+ }
+
+ return 0;
+}
+
+static bool bpf_diag_history_event_visible(const struct bpf_diag_history_event *event,
+ const struct bpf_diag_history_opts *opts)
+{
+ if (!opts || opts->scope == BPF_DIAG_HISTORY_SCOPE_ALL)
+ return true;
+
+ switch (event->kind) {
+ case BPF_DIAG_HISTORY_BRANCH:
+ return true;
+ case BPF_DIAG_HISTORY_REG_MOD:
+ return opts->scope == BPF_DIAG_HISTORY_SCOPE_REG &&
+ event->reg.dst_reg == opts->regno &&
+ event->reg.frameno == opts->frameno;
+ case BPF_DIAG_HISTORY_STACK_ARG:
+ return opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG &&
+ event->stack_arg.slot == opts->stack_arg_slot &&
+ event->stack_arg.frameno == opts->frameno;
+ default:
+ return false;
+ }
+}
+
+static void bpf_diag_format_var_offset(char *buf, size_t size,
+ const struct bpf_diag_reg_snapshot *snapshot)
+{
+ char range[BPF_DIAG_REG_DESC_LEN];
+
+ if (snapshot->var_off_known) {
+ scnprintf(buf, size, "at offset %lld",
+ snapshot->var_off_value);
+ return;
+ }
+
+ if (bpf_diag_snapshot_unknown(snapshot)) {
+ scnprintf(buf, size, "with unknown offset");
+ return;
+ }
+
+ bpf_diag_format_scalar_range(range, sizeof(range),
+ snapshot->smin_value, snapshot->smax_value,
+ snapshot->umin_value, snapshot->umax_value);
+ scnprintf(buf, size,
+ "with variable offset: known bits %#llx, unknown mask %#llx, %s",
+ (u64)snapshot->var_off_value, snapshot->var_off_mask,
+ range);
+}
+
+static bool bpf_diag_format_snapshot_btf_type(char *buf, size_t size,
+ const struct bpf_diag_reg_snapshot *snapshot)
+{
+ const struct btf_type *type;
+ const char *name;
+
+ if (!snapshot->btf || !snapshot->btf_id)
+ return false;
+
+ type = btf_type_by_id(snapshot->btf, snapshot->btf_id);
+ if (!type)
+ return false;
+
+ name = btf_name_by_offset(snapshot->btf, type->name_off);
+ bpf_diag_format_btf_type(buf, size, type, name);
+ return true;
+}
+
+static void bpf_diag_format_reg_snapshot(struct bpf_verifier_env *env, char *buf,
+ size_t size,
+ const struct bpf_diag_reg_snapshot *snapshot)
+{
+ const char *type_name = reg_type_str(env, snapshot->type);
+ char offset_desc[BPF_DIAG_REG_DESC_LEN];
+ char btf_type[BPF_DIAG_REG_DESC_LEN];
+ const char *map_name;
+ bool has_btf_type;
+
+ bpf_diag_format_var_offset(offset_desc, sizeof(offset_desc), snapshot);
+ has_btf_type = bpf_diag_format_snapshot_btf_type(btf_type,
+ sizeof(btf_type),
+ snapshot);
+
+ if (snapshot->type == SCALAR_VALUE) {
+ char range[BPF_DIAG_REG_DESC_LEN];
+
+ if (snapshot->var_off_known) {
+ scnprintf(buf, size, "integer scalar value %lld",
+ snapshot->var_off_value);
+ return;
+ }
+
+ if (bpf_diag_snapshot_unknown(snapshot)) {
+ scnprintf(buf, size, "integer scalar with unknown value");
+ return;
+ }
+
+ if (snapshot->smin_value == snapshot->smax_value &&
+ snapshot->umin_value == snapshot->umax_value) {
+ scnprintf(buf, size, "integer scalar value %lld",
+ snapshot->smin_value);
+ return;
+ }
+
+ bpf_diag_format_scalar_range(range, sizeof(range),
+ snapshot->smin_value,
+ snapshot->smax_value,
+ snapshot->umin_value,
+ snapshot->umax_value);
+ scnprintf(buf, size,
+ "integer scalar with %s", range);
+ return;
+ }
+
+ if (snapshot->type == NOT_INIT) {
+ scnprintf(buf, size, "uninitialized value");
+ return;
+ }
+
+ if (base_type(snapshot->type) == PTR_TO_CTX) {
+ scnprintf(buf, size, "context pointer %s", offset_desc);
+ return;
+ }
+
+ if (base_type(snapshot->type) == PTR_TO_STACK) {
+ scnprintf(buf, size, "stack pointer %s", offset_desc);
+ return;
+ }
+
+ if (base_type(snapshot->type) == PTR_TO_MAP_VALUE) {
+ map_name = bpf_diag_reg_map_name(snapshot->map_ptr);
+ if (map_name) {
+ scnprintf(buf, size, "%s from %s %s",
+ type_may_be_null(snapshot->type) ?
+ "nullable map value" : "map value",
+ map_name, offset_desc);
+ return;
+ }
+ scnprintf(buf, size, "%s %s",
+ type_may_be_null(snapshot->type) ?
+ "nullable map value" : "map value",
+ offset_desc);
+ return;
+ }
+
+ if (base_type(snapshot->type) == CONST_PTR_TO_MAP) {
+ map_name = bpf_diag_reg_map_name(snapshot->map_ptr);
+ if (map_name)
+ scnprintf(buf, size, "map pointer for map %s", map_name);
+ else
+ scnprintf(buf, size, "map pointer");
+ return;
+ }
+
+ if (type_is_non_owning_ref(snapshot->type)) {
+ if (has_btf_type)
+ scnprintf(buf, size,
+ "borrowed allocated object pointer type=%s",
+ btf_type);
+ else
+ scnprintf(buf, size, "borrowed allocated object pointer");
+ return;
+ }
+
+ if (type_is_ptr_alloc_obj(snapshot->type)) {
+ if (has_btf_type)
+ scnprintf(buf, size,
+ "owned allocated object pointer type=%s",
+ btf_type);
+ else
+ scnprintf(buf, size, "owned allocated object pointer");
+ return;
+ }
+
+ if (base_type(snapshot->type) == PTR_TO_BTF_ID && has_btf_type) {
+ scnprintf(buf, size, "%s type=%s %s", type_name, btf_type,
+ offset_desc);
+ return;
+ }
+
+ scnprintf(buf, size, "%s %s", type_name, offset_desc);
+}
+
+static void bpf_diag_print_reg_mod(struct bpf_verifier_env *env,
+ const struct bpf_diag_history_event *event)
+{
+ char old_buf[BPF_DIAG_REG_DESC_LEN], new_buf[BPF_DIAG_REG_DESC_LEN];
+ const char *reason = NULL;
+
+ bpf_diag_format_reg_snapshot(env, old_buf, sizeof(old_buf),
+ &event->reg.old);
+ bpf_diag_format_reg_snapshot(env, new_buf, sizeof(new_buf),
+ &event->reg.new);
+
+ switch (event->reg.reason) {
+ case BPF_DIAG_REG_MOD_REF_RELEASE:
+ reason = "resource release invalidated this pointer";
+ break;
+ case BPF_DIAG_REG_MOD_PKT_DATA_CHANGE:
+ reason = "packet data may have moved";
+ break;
+ case BPF_DIAG_REG_MOD_WRITE:
+ default:
+ break;
+ }
+
+ if (reason) {
+ bpf_diag_report_source(env, event->insn_idx, 'x',
+ "R%d: %s; previous value was %s",
+ event->reg.dst_reg, reason, old_buf);
+ return;
+ }
+
+ bpf_diag_report_source(env, event->insn_idx, '~',
+ "R%d changed from %s to %s",
+ event->reg.dst_reg, old_buf, new_buf);
+}
+
+void bpf_diag_print_history(struct bpf_verifier_env *env,
+ const struct bpf_diag_history_opts *opts)
{
const struct bpf_verifier_state *state = env->cur_state;
const struct bpf_diag_history_event *event;
bool printed = false;
+ int start_idx;
u32 i;
bpf_diag_report_section(env, "Causal path");
@@ -621,12 +1098,15 @@ void bpf_diag_print_history(struct bpf_verifier_env *env)
if (!state)
return;
- if (state->diag_history_omitted)
+ start_idx = bpf_diag_history_start_idx(state, opts);
+ if (state->diag_history_omitted && start_idx == 0)
verbose(env, " ... %u earlier diagnostic events omitted by display limit ...\n",
state->diag_history_omitted);
- for (i = 0; i < state->diag_history_cnt; i++) {
+ for (i = start_idx; i < state->diag_history_cnt; i++) {
event = &state->diag_history[i];
+ if (!bpf_diag_history_event_visible(event, opts))
+ continue;
switch (event->kind) {
case BPF_DIAG_HISTORY_BRANCH:
@@ -638,6 +1118,10 @@ void bpf_diag_print_history(struct bpf_verifier_env *env)
"not followed");
printed = true;
break;
+ case BPF_DIAG_HISTORY_REG_MOD:
+ bpf_diag_print_reg_mod(env, event);
+ printed = true;
+ break;
default:
break;
}
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index 1392ff5fb027..b8dfe8103966 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -19,6 +19,34 @@ enum bpf_diag_category {
BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR,
};
+struct bpf_map;
+struct btf;
+struct btf_type;
+
+void bpf_diag_format_btf_type(char *buf, size_t size,
+ const struct btf_type *type,
+ const char *type_name);
+
+struct bpf_diag_reg_snapshot {
+ u32 type;
+ const struct bpf_map *map_ptr;
+ const struct btf *btf;
+ u32 btf_id;
+ bool var_off_known;
+ s64 var_off_value;
+ u64 var_off_mask;
+ s64 smin_value;
+ s64 smax_value;
+ u64 umin_value;
+ u64 umax_value;
+};
+
+enum bpf_diag_reg_mod_reason {
+ BPF_DIAG_REG_MOD_WRITE,
+ BPF_DIAG_REG_MOD_REF_RELEASE,
+ BPF_DIAG_REG_MOD_PKT_DATA_CHANGE,
+};
+
struct bpf_diag_history_event {
u32 insn_idx;
u8 kind;
@@ -26,6 +54,21 @@ struct bpf_diag_history_event {
struct {
bool cond_true;
} branch;
+ struct {
+ u32 frameno;
+ u8 dst_reg;
+ u8 src_reg;
+ u8 opcode;
+ bool src_valid;
+ u8 reason;
+ struct bpf_diag_reg_snapshot old, new;
+ } reg;
+ struct {
+ u32 frameno;
+ u8 slot;
+ u8 reason;
+ struct bpf_diag_reg_snapshot old, new;
+ } stack_arg;
};
};
@@ -33,9 +76,31 @@ struct bpf_diag_history_event {
enum bpf_diag_history_kind {
BPF_DIAG_HISTORY_BRANCH,
+ BPF_DIAG_HISTORY_REG_MOD,
+ BPF_DIAG_HISTORY_STACK_ARG,
+};
+
+enum bpf_diag_history_scope {
+ BPF_DIAG_HISTORY_SCOPE_ALL,
+ BPF_DIAG_HISTORY_SCOPE_REG,
+ BPF_DIAG_HISTORY_SCOPE_STACK_ARG,
+};
+
+enum bpf_diag_stack_arg_reason {
+ BPF_DIAG_STACK_ARG_WRITE,
+ BPF_DIAG_STACK_ARG_REF_RELEASE,
+ BPF_DIAG_STACK_ARG_PKT_DATA_CHANGE,
+};
+
+struct bpf_diag_history_opts {
+ enum bpf_diag_history_scope scope;
+ u32 frameno;
+ int regno;
+ int stack_arg_slot;
};
struct bpf_verifier_env;
+struct bpf_reg_state;
struct bpf_verifier_state;
void bpf_diag_report_header(struct bpf_verifier_env *env,
@@ -55,6 +120,21 @@ void bpf_diag_copy_history(struct bpf_verifier_state *dst,
const struct bpf_verifier_state *src);
void bpf_diag_record_branch(struct bpf_verifier_state *state, u32 insn_idx,
bool cond_true);
-void bpf_diag_print_history(struct bpf_verifier_env *env);
+void bpf_diag_record_reg_mod(struct bpf_verifier_state *state, u32 insn_idx,
+ u8 dst_reg, bool src_valid, u8 src_reg, u8 opcode,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg);
+void bpf_diag_record_reg_invalidate(struct bpf_verifier_state *state,
+ u32 insn_idx, u8 dst_reg,
+ enum bpf_diag_reg_mod_reason reason,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg);
+void bpf_diag_record_stack_arg(struct bpf_verifier_state *state, u32 insn_idx,
+ u32 frameno, u8 slot,
+ enum bpf_diag_stack_arg_reason reason,
+ const struct bpf_reg_state *old_reg,
+ const struct bpf_reg_state *new_reg);
+void bpf_diag_print_history(struct bpf_verifier_env *env,
+ const struct bpf_diag_history_opts *opts);
#endif /* __BPF_DIAGNOSTICS_H */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index d60e1309957a..188201d48108 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4033,6 +4033,8 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
struct bpf_subprog_info *subprog = &env->subprog_info[state->subprogno];
int spi = -off / BPF_REG_SIZE - 1;
struct bpf_reg_state *arg;
+ struct bpf_reg_state old_arg;
+ bool slot_exists;
int err;
if (spi >= max_stack_arg_regs) {
@@ -4041,6 +4043,7 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
return -EINVAL;
}
+ slot_exists = spi < state->out_stack_arg_cnt;
err = grow_stack_arg_slots(env, state, spi + 1);
if (err)
return err;
@@ -4049,13 +4052,27 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
if (spi + 1 > subprog->max_out_stack_arg_cnt)
subprog->max_out_stack_arg_cnt = spi + 1;
+ arg = &state->stack_arg_regs[spi];
+ if (slot_exists)
+ old_arg = *arg;
+ else
+ bpf_mark_reg_not_init(env, &old_arg);
+
if (value_reg) {
state->stack_arg_regs[spi] = *value_reg;
+ bpf_diag_record_stack_arg(env->cur_state, env->insn_idx,
+ state->frameno, spi,
+ BPF_DIAG_STACK_ARG_WRITE,
+ &old_arg,
+ &state->stack_arg_regs[spi]);
} else {
/* BPF_ST: store immediate, treat as scalar */
- arg = &state->stack_arg_regs[spi];
arg->type = SCALAR_VALUE;
__mark_reg_known(arg, env->prog->insnsi[env->insn_idx].imm);
+ bpf_diag_record_stack_arg(env->cur_state, env->insn_idx,
+ state->frameno, spi,
+ BPF_DIAG_STACK_ARG_WRITE,
+ &old_arg, arg);
}
state->no_stack_arg_load = true;
return bpf_push_jmp_history(env, env->cur_state,
@@ -6316,15 +6333,27 @@ static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,
struct bpf_verifier_state *vstate = env->cur_state;
struct bpf_func_state *state = vstate->frame[vstate->curframe];
struct bpf_reg_state *regs = cur_regs(env);
+ struct bpf_reg_state old_dst = {};
enum bpf_reg_type src_reg_type;
+ bool have_old_dst;
int err;
+ have_old_dst = insn->dst_reg < MAX_BPF_REG;
+ if (have_old_dst)
+ old_dst = regs[insn->dst_reg];
+
/* Handle stack arg read */
if (is_stack_arg_ldx(insn)) {
err = check_reg_arg(env, insn->dst_reg, DST_OP_NO_MARK);
if (err)
return err;
- return check_stack_arg_read(env, state, insn->off, insn->dst_reg);
+ err = check_stack_arg_read(env, state, insn->off, insn->dst_reg);
+ if (!err && have_old_dst)
+ bpf_diag_record_reg_mod(env->cur_state, env->insn_idx,
+ insn->dst_reg, true,
+ insn->src_reg, BPF_OP(insn->code),
+ &old_dst, ®s[insn->dst_reg]);
+ return err;
}
/* check src operand */
@@ -6348,6 +6377,11 @@ static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,
err = err ?: save_aux_ptr_type(env, src_reg_type,
allow_trust_mismatch);
err = err ?: reg_bounds_sanity_check(env, ®s[insn->dst_reg], ctx);
+ if (!err && have_old_dst)
+ bpf_diag_record_reg_mod(env->cur_state, env->insn_idx,
+ insn->dst_reg, true, insn->src_reg,
+ BPF_OP(insn->code), &old_dst,
+ ®s[insn->dst_reg]);
return err;
}
@@ -8833,14 +8867,51 @@ static int check_func_proto(const struct bpf_func_proto *fn, struct bpf_call_arg
* This also applies to dynptr slices belonging to skb and xdp dynptrs,
* since these slices point to packet data.
*/
+static int bpf_diag_stack_arg_slot(const struct bpf_func_state *state,
+ const struct bpf_reg_state *reg)
+{
+ if (!state->stack_arg_regs)
+ return -1;
+ if (reg < state->stack_arg_regs ||
+ reg >= state->stack_arg_regs + state->out_stack_arg_cnt)
+ return -1;
+ return reg - state->stack_arg_regs;
+}
+
+static int bpf_diag_func_regno(const struct bpf_func_state *state,
+ const struct bpf_reg_state *reg)
+{
+ if (reg < state->regs || reg >= state->regs + MAX_BPF_REG)
+ return -1;
+ return reg - state->regs;
+}
+
static void clear_all_pkt_pointers(struct bpf_verifier_env *env)
{
struct bpf_func_state *state;
struct bpf_reg_state *reg;
bpf_for_each_reg_in_vstate(env->cur_state, state, reg, ({
- if (reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg))
+ if (reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg)) {
+ struct bpf_reg_state old_reg = *reg;
+ int regno = bpf_diag_func_regno(state, reg);
+ int slot = bpf_diag_stack_arg_slot(state, reg);
+
mark_reg_invalid(env, reg);
+ if (regno >= 0)
+ bpf_diag_record_reg_invalidate(env->cur_state,
+ env->insn_idx,
+ regno,
+ BPF_DIAG_REG_MOD_PKT_DATA_CHANGE,
+ &old_reg, reg);
+ if (slot >= 0)
+ bpf_diag_record_stack_arg(env->cur_state,
+ env->insn_idx,
+ state->frameno,
+ slot,
+ BPF_DIAG_STACK_ARG_PKT_DATA_CHANGE,
+ &old_reg, reg);
+ }
}));
}
@@ -8946,6 +9017,10 @@ static int release_reference(struct bpf_verifier_env *env, int id)
}
bpf_for_each_reg_in_vstate_mask(vstate, state, reg, stack, mask, ({
+ struct bpf_reg_state old_reg;
+ int regno;
+ int slot;
+
if (reg->id != id && reg->parent_id != id)
continue;
@@ -8956,10 +9031,24 @@ static int release_reference(struct bpf_verifier_env *env, int id)
return err;
}
+ old_reg = *reg;
+ regno = bpf_diag_func_regno(state, reg);
+ slot = bpf_diag_stack_arg_slot(state, reg);
if (!stack || stack->slot_type[BPF_REG_SIZE - 1] == STACK_SPILL)
mark_reg_invalid(env, reg);
else if (stack->slot_type[BPF_REG_SIZE - 1] == STACK_DYNPTR)
invalidate_dynptr(env, stack);
+ if (regno >= 0)
+ bpf_diag_record_reg_invalidate(vstate,
+ env->insn_idx,
+ regno,
+ BPF_DIAG_REG_MOD_REF_RELEASE,
+ &old_reg, reg);
+ if (slot >= 0)
+ bpf_diag_record_stack_arg(vstate, env->insn_idx,
+ state->frameno, slot,
+ BPF_DIAG_STACK_ARG_REF_RELEASE,
+ &old_reg, reg);
}));
}
@@ -14801,9 +14890,15 @@ static int adjust_reg_min_max_vals(struct bpf_verifier_env *env,
static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
{
struct bpf_reg_state *regs = cur_regs(env);
+ struct bpf_reg_state old_dst = {};
u8 opcode = BPF_OP(insn->code);
+ bool have_old_dst;
int err;
+ have_old_dst = insn->dst_reg < MAX_BPF_REG;
+ if (have_old_dst)
+ old_dst = regs[insn->dst_reg];
+
if (opcode == BPF_END || opcode == BPF_NEG) {
/* check src operand */
err = check_reg_arg(env, insn->dst_reg, SRC_OP);
@@ -14984,7 +15079,16 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
return err;
}
- return reg_bounds_sanity_check(env, ®s[insn->dst_reg], "alu");
+ err = reg_bounds_sanity_check(env, ®s[insn->dst_reg], "alu");
+ if (err)
+ return err;
+
+ if (have_old_dst)
+ bpf_diag_record_reg_mod(env->cur_state, env->insn_idx,
+ insn->dst_reg, BPF_SRC(insn->code) == BPF_X,
+ insn->src_reg, opcode, &old_dst,
+ ®s[insn->dst_reg]);
+ return 0;
}
static void find_good_pkt_pointers(struct bpf_verifier_state *vstate,
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 06/17] bpf: Track verifier reference diagnostic history
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (4 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 05/17] bpf: Track verifier register " Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 07/17] bpf: Track verifier context " Kumar Kartikeya Dwivedi
` (10 subsequent siblings)
16 siblings, 0 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Add reference acquire and release events to diagnostic history so Resource
Lifetime Safety reports can show the lifetime of a specific reference id
along the path.
Record acquisitions after the verifier assigns the reference id. Record
releases only after release_reference_nomark() succeeds, including the
kptr_xchg RCU conversion path that consumes an owning reference.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 57 ++++++++++++++++++++++++++++++++++++++--
kernel/bpf/diagnostics.h | 11 ++++++++
kernel/bpf/verifier.c | 12 +++++++--
3 files changed, 76 insertions(+), 4 deletions(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 1bdc7e69fbce..56c1236ffe7c 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -842,13 +842,38 @@ void bpf_diag_record_stack_arg(struct bpf_verifier_state *state, u32 insn_idx,
bpf_diag_append_history(state, &event);
}
+static void bpf_diag_record_ref(struct bpf_verifier_state *state, u32 insn_idx,
+ u8 kind, u32 ref_id)
+{
+ struct bpf_diag_history_event event = {
+ .insn_idx = insn_idx,
+ .kind = kind,
+ .ref.ref_id = ref_id,
+ };
+
+ bpf_diag_append_history(state, &event);
+}
+
+void bpf_diag_record_ref_acquire(struct bpf_verifier_state *state, u32 insn_idx,
+ u32 ref_id)
+{
+ bpf_diag_record_ref(state, insn_idx, BPF_DIAG_HISTORY_REF_ACQUIRE,
+ ref_id);
+}
+
+void bpf_diag_record_ref_release(struct bpf_verifier_state *state, u32 insn_idx,
+ u32 ref_id)
+{
+ bpf_diag_record_ref(state, insn_idx, BPF_DIAG_HISTORY_REF_RELEASE,
+ ref_id);
+}
+
static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
const struct bpf_diag_history_opts *opts)
{
int i;
- if (!opts || (opts->scope != BPF_DIAG_HISTORY_SCOPE_REG &&
- opts->scope != BPF_DIAG_HISTORY_SCOPE_STACK_ARG))
+ if (!opts || opts->scope == BPF_DIAG_HISTORY_SCOPE_ALL)
return 0;
for (i = state->diag_history_cnt; i > 0; i--) {
@@ -864,6 +889,10 @@ static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
event->stack_arg.slot == opts->stack_arg_slot &&
event->stack_arg.frameno == opts->frameno)
return i - 1;
+ if (opts->scope == BPF_DIAG_HISTORY_SCOPE_REF &&
+ event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE &&
+ event->ref.ref_id == opts->ref_id)
+ return i - 1;
}
return 0;
@@ -886,6 +915,10 @@ static bool bpf_diag_history_event_visible(const struct bpf_diag_history_event *
return opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG &&
event->stack_arg.slot == opts->stack_arg_slot &&
event->stack_arg.frameno == opts->frameno;
+ case BPF_DIAG_HISTORY_REF_ACQUIRE:
+ case BPF_DIAG_HISTORY_REF_RELEASE:
+ return opts->scope == BPF_DIAG_HISTORY_SCOPE_REF &&
+ event->ref.ref_id == opts->ref_id;
default:
return false;
}
@@ -1084,6 +1117,21 @@ static void bpf_diag_print_reg_mod(struct bpf_verifier_env *env,
event->reg.dst_reg, old_buf, new_buf);
}
+static void bpf_diag_print_ref_event(struct bpf_verifier_env *env,
+ const struct bpf_diag_history_event *event)
+{
+ if (event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE) {
+ bpf_diag_report_source(env, event->insn_idx, '+',
+ "owned resource (id=%u)",
+ event->ref.ref_id);
+ return;
+ }
+
+ bpf_diag_report_source(env, event->insn_idx, '-',
+ "owned resource (id=%u)",
+ event->ref.ref_id);
+}
+
void bpf_diag_print_history(struct bpf_verifier_env *env,
const struct bpf_diag_history_opts *opts)
{
@@ -1122,6 +1170,11 @@ void bpf_diag_print_history(struct bpf_verifier_env *env,
bpf_diag_print_reg_mod(env, event);
printed = true;
break;
+ case BPF_DIAG_HISTORY_REF_ACQUIRE:
+ case BPF_DIAG_HISTORY_REF_RELEASE:
+ bpf_diag_print_ref_event(env, event);
+ printed = true;
+ break;
default:
break;
}
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index b8dfe8103966..c6ace61547c6 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -69,6 +69,9 @@ struct bpf_diag_history_event {
u8 reason;
struct bpf_diag_reg_snapshot old, new;
} stack_arg;
+ struct {
+ u32 ref_id;
+ } ref;
};
};
@@ -78,12 +81,15 @@ enum bpf_diag_history_kind {
BPF_DIAG_HISTORY_BRANCH,
BPF_DIAG_HISTORY_REG_MOD,
BPF_DIAG_HISTORY_STACK_ARG,
+ BPF_DIAG_HISTORY_REF_ACQUIRE,
+ BPF_DIAG_HISTORY_REF_RELEASE,
};
enum bpf_diag_history_scope {
BPF_DIAG_HISTORY_SCOPE_ALL,
BPF_DIAG_HISTORY_SCOPE_REG,
BPF_DIAG_HISTORY_SCOPE_STACK_ARG,
+ BPF_DIAG_HISTORY_SCOPE_REF,
};
enum bpf_diag_stack_arg_reason {
@@ -97,6 +103,7 @@ struct bpf_diag_history_opts {
u32 frameno;
int regno;
int stack_arg_slot;
+ u32 ref_id;
};
struct bpf_verifier_env;
@@ -134,6 +141,10 @@ void bpf_diag_record_stack_arg(struct bpf_verifier_state *state, u32 insn_idx,
enum bpf_diag_stack_arg_reason reason,
const struct bpf_reg_state *old_reg,
const struct bpf_reg_state *new_reg);
+void bpf_diag_record_ref_acquire(struct bpf_verifier_state *state, u32 insn_idx,
+ u32 ref_id);
+void bpf_diag_record_ref_release(struct bpf_verifier_state *state, u32 insn_idx,
+ u32 ref_id);
void bpf_diag_print_history(struct bpf_verifier_env *env,
const struct bpf_diag_history_opts *opts);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 188201d48108..9b9ffa57b95a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1434,6 +1434,7 @@ static int acquire_reference(struct bpf_verifier_env *env, int insn_idx, int par
s->type = REF_TYPE_PTR;
s->id = ++env->id_gen;
s->parent_id = parent_id;
+ bpf_diag_record_ref_acquire(env->cur_state, insn_idx, s->id);
return s->id;
}
@@ -8998,8 +8999,12 @@ static int release_reference(struct bpf_verifier_env *env, int id)
if (err)
return err;
- if (find_reference_state(vstate, id))
- WARN_ON_ONCE(release_reference_nomark(vstate, id));
+ if (find_reference_state(vstate, id)) {
+ err = release_reference_nomark(vstate, id);
+ WARN_ON_ONCE(err);
+ if (!err)
+ bpf_diag_record_ref_release(vstate, env->insn_idx, id);
+ }
while ((id = idstack_pop(idstack))) {
/*
@@ -9088,6 +9093,9 @@ static int ref_convert_alloc_rcu_protected(struct bpf_verifier_env *env, u32 id)
int err;
err = release_reference_nomark(env->cur_state, id);
+ if (err)
+ return err;
+ bpf_diag_record_ref_release(env->cur_state, env->insn_idx, id);
bpf_for_each_reg_in_vstate(env->cur_state, state, reg, ({
if (reg->id != id)
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 07/17] bpf: Track verifier context diagnostic history
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (5 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 06/17] bpf: Track verifier reference " Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:46 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
2026-06-05 6:33 ` [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors Kumar Kartikeya Dwivedi
` (9 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Record verifier context transitions in the diagnostic history so later
reports can anchor causal paths to the critical section that made an
operation invalid.
This covers lock, IRQ, RCU, and preempt regions without adding any new
verifier error reports. Category-specific commits decide where those
recorded events should be rendered.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 62 ++++++++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 19 ++++++++++++
kernel/bpf/verifier.c | 39 +++++++++++++++++++++----
3 files changed, 114 insertions(+), 6 deletions(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 56c1236ffe7c..7a122c5025c2 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -241,6 +241,8 @@ static const char *bpf_diag_marker_label(char marker)
return "acquired";
case '-':
return "released";
+ case '@':
+ return "context";
default:
return "note";
}
@@ -438,6 +440,23 @@ static const char *bpf_diag_category_name(enum bpf_diag_category category)
}
}
+static const char *bpf_diag_context_name(enum bpf_diag_context_kind kind)
+{
+ switch (kind) {
+ case BPF_DIAG_CONTEXT_RCU:
+ return "RCU read lock region";
+ case BPF_DIAG_CONTEXT_PREEMPT:
+ return "non-preemptible region";
+ case BPF_DIAG_CONTEXT_IRQ:
+ return "IRQ-disabled region";
+ case BPF_DIAG_CONTEXT_LOCK:
+ return "lock region";
+ case BPF_DIAG_CONTEXT_NONE:
+ default:
+ return "context";
+ }
+}
+
void bpf_diag_report_header(struct bpf_verifier_env *env,
enum bpf_diag_category category,
const char *problem)
@@ -868,6 +887,24 @@ void bpf_diag_record_ref_release(struct bpf_verifier_state *state, u32 insn_idx,
ref_id);
}
+void bpf_diag_record_context(struct bpf_verifier_state *state, u32 insn_idx,
+ enum bpf_diag_context_kind ctx_kind, bool enter,
+ u32 depth)
+{
+ struct bpf_diag_history_event event = {
+ .insn_idx = insn_idx,
+ .kind = BPF_DIAG_HISTORY_CONTEXT,
+ .ctx.kind = ctx_kind,
+ .ctx.enter = enter,
+ .ctx.depth = depth,
+ };
+
+ if (ctx_kind == BPF_DIAG_CONTEXT_NONE)
+ return;
+
+ bpf_diag_append_history(state, &event);
+}
+
static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
const struct bpf_diag_history_opts *opts)
{
@@ -893,8 +930,16 @@ static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE &&
event->ref.ref_id == opts->ref_id)
return i - 1;
+ if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT &&
+ event->kind == BPF_DIAG_HISTORY_CONTEXT &&
+ event->ctx.enter &&
+ event->ctx.kind == opts->ctx_kind)
+ return i - 1;
}
+ if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT)
+ return state->diag_history_cnt;
+
return 0;
}
@@ -919,6 +964,9 @@ static bool bpf_diag_history_event_visible(const struct bpf_diag_history_event *
case BPF_DIAG_HISTORY_REF_RELEASE:
return opts->scope == BPF_DIAG_HISTORY_SCOPE_REF &&
event->ref.ref_id == opts->ref_id;
+ case BPF_DIAG_HISTORY_CONTEXT:
+ return opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT &&
+ event->ctx.kind == opts->ctx_kind;
default:
return false;
}
@@ -1132,6 +1180,16 @@ static void bpf_diag_print_ref_event(struct bpf_verifier_env *env,
event->ref.ref_id);
}
+static void bpf_diag_print_context_event(struct bpf_verifier_env *env,
+ const struct bpf_diag_history_event *event)
+{
+ bpf_diag_report_source(env, event->insn_idx, '@',
+ "%s %s; depth is now %u",
+ event->ctx.enter ? "entered" : "left",
+ bpf_diag_context_name(event->ctx.kind),
+ event->ctx.depth);
+}
+
void bpf_diag_print_history(struct bpf_verifier_env *env,
const struct bpf_diag_history_opts *opts)
{
@@ -1175,6 +1233,10 @@ void bpf_diag_print_history(struct bpf_verifier_env *env,
bpf_diag_print_ref_event(env, event);
printed = true;
break;
+ case BPF_DIAG_HISTORY_CONTEXT:
+ bpf_diag_print_context_event(env, event);
+ printed = true;
+ break;
default:
break;
}
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index c6ace61547c6..ff6c55f9b5f7 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -72,6 +72,11 @@ struct bpf_diag_history_event {
struct {
u32 ref_id;
} ref;
+ struct {
+ u8 kind;
+ bool enter;
+ u32 depth;
+ } ctx;
};
};
@@ -83,6 +88,7 @@ enum bpf_diag_history_kind {
BPF_DIAG_HISTORY_STACK_ARG,
BPF_DIAG_HISTORY_REF_ACQUIRE,
BPF_DIAG_HISTORY_REF_RELEASE,
+ BPF_DIAG_HISTORY_CONTEXT,
};
enum bpf_diag_history_scope {
@@ -90,6 +96,15 @@ enum bpf_diag_history_scope {
BPF_DIAG_HISTORY_SCOPE_REG,
BPF_DIAG_HISTORY_SCOPE_STACK_ARG,
BPF_DIAG_HISTORY_SCOPE_REF,
+ BPF_DIAG_HISTORY_SCOPE_CONTEXT,
+};
+
+enum bpf_diag_context_kind {
+ BPF_DIAG_CONTEXT_NONE,
+ BPF_DIAG_CONTEXT_RCU,
+ BPF_DIAG_CONTEXT_PREEMPT,
+ BPF_DIAG_CONTEXT_IRQ,
+ BPF_DIAG_CONTEXT_LOCK,
};
enum bpf_diag_stack_arg_reason {
@@ -104,6 +119,7 @@ struct bpf_diag_history_opts {
int regno;
int stack_arg_slot;
u32 ref_id;
+ enum bpf_diag_context_kind ctx_kind;
};
struct bpf_verifier_env;
@@ -145,6 +161,9 @@ void bpf_diag_record_ref_acquire(struct bpf_verifier_state *state, u32 insn_idx,
u32 ref_id);
void bpf_diag_record_ref_release(struct bpf_verifier_state *state, u32 insn_idx,
u32 ref_id);
+void bpf_diag_record_context(struct bpf_verifier_state *state, u32 insn_idx,
+ enum bpf_diag_context_kind ctx_kind, bool enter,
+ u32 depth);
void bpf_diag_print_history(struct bpf_verifier_env *env,
const struct bpf_diag_history_opts *opts);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 9b9ffa57b95a..7bb2a81ee353 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1060,7 +1060,8 @@ static int is_iter_reg_valid_init(struct bpf_verifier_env *env, struct bpf_reg_s
}
static int acquire_irq_state(struct bpf_verifier_env *env, int insn_idx);
-static int release_irq_state(struct bpf_verifier_state *state, int id);
+static int release_irq_state(struct bpf_verifier_state *state, int id,
+ int insn_idx);
static int mark_stack_slot_irq_flag(struct bpf_verifier_env *env,
struct bpf_kfunc_call_arg_meta *meta,
@@ -1119,7 +1120,7 @@ static int unmark_stack_slot_irq_flag(struct bpf_verifier_env *env, struct bpf_r
return -EINVAL;
}
- err = release_irq_state(env->cur_state, st->id);
+ err = release_irq_state(env->cur_state, st->id, env->insn_idx);
WARN_ON_ONCE(err && err != -EACCES);
if (err) {
int insn_idx = 0;
@@ -1454,6 +1455,8 @@ static int acquire_lock_state(struct bpf_verifier_env *env, int insn_idx, enum r
state->active_locks++;
state->active_lock_id = id;
state->active_lock_ptr = ptr;
+ bpf_diag_record_context(state, insn_idx, BPF_DIAG_CONTEXT_LOCK, true,
+ state->active_locks);
return 0;
}
@@ -1469,6 +1472,8 @@ static int acquire_irq_state(struct bpf_verifier_env *env, int insn_idx)
s->id = ++env->id_gen;
state->active_irq_id = s->id;
+ bpf_diag_record_context(state, insn_idx, BPF_DIAG_CONTEXT_IRQ, true,
+ 1);
return s->id;
}
@@ -1510,7 +1515,8 @@ static bool reg_is_referenced(struct bpf_verifier_env *env, const struct bpf_reg
return find_reference_state(env->cur_state, reg->id);
}
-static int release_lock_state(struct bpf_verifier_state *state, int type, int id, void *ptr)
+static int release_lock_state(struct bpf_verifier_state *state, int type,
+ int id, void *ptr, int insn_idx)
{
void *prev_ptr = NULL;
u32 prev_id = 0;
@@ -1524,6 +1530,9 @@ static int release_lock_state(struct bpf_verifier_state *state, int type, int id
/* Reassign active lock (id, ptr). */
state->active_lock_id = prev_id;
state->active_lock_ptr = prev_ptr;
+ bpf_diag_record_context(state, insn_idx,
+ BPF_DIAG_CONTEXT_LOCK, false,
+ state->active_locks);
return 0;
}
if (state->refs[i].type & REF_TYPE_LOCK_MASK) {
@@ -1534,7 +1543,8 @@ static int release_lock_state(struct bpf_verifier_state *state, int type, int id
return -EINVAL;
}
-static int release_irq_state(struct bpf_verifier_state *state, int id)
+static int release_irq_state(struct bpf_verifier_state *state, int id,
+ int insn_idx)
{
u32 prev_id = 0;
int i;
@@ -1548,6 +1558,9 @@ static int release_irq_state(struct bpf_verifier_state *state, int id)
if (state->refs[i].id == id) {
release_reference_state(state, i);
state->active_irq_id = prev_id;
+ bpf_diag_record_context(state, insn_idx,
+ BPF_DIAG_CONTEXT_IRQ, false,
+ state->active_irq_id ? 1 : 0);
return 0;
} else {
prev_id = state->refs[i].id;
@@ -7084,7 +7097,8 @@ static int process_spin_lock(struct bpf_verifier_env *env, struct bpf_reg_state
verbose(env, "%s_unlock cannot be out of order\n", lock_str);
return -EINVAL;
}
- if (release_lock_state(cur, type, reg->id, ptr)) {
+ if (release_lock_state(cur, type, reg->id, ptr,
+ env->insn_idx)) {
verbose(env, "%s_unlock of different lock\n", lock_str);
return -EINVAL;
}
@@ -13029,21 +13043,34 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
if (rcu_lock) {
env->cur_state->active_rcu_locks++;
+ bpf_diag_record_context(env->cur_state, insn_idx,
+ BPF_DIAG_CONTEXT_RCU, true,
+ env->cur_state->active_rcu_locks);
} else if (rcu_unlock) {
if (env->cur_state->active_rcu_locks == 0) {
verbose(env, "unmatched rcu read unlock (kernel function %s)\n", func_name);
return -EINVAL;
}
- if (--env->cur_state->active_rcu_locks == 0)
+ env->cur_state->active_rcu_locks--;
+ bpf_diag_record_context(env->cur_state, insn_idx,
+ BPF_DIAG_CONTEXT_RCU, false,
+ env->cur_state->active_rcu_locks);
+ if (env->cur_state->active_rcu_locks == 0)
invalidate_rcu_protected_refs(env);
} else if (preempt_disable) {
env->cur_state->active_preempt_locks++;
+ bpf_diag_record_context(env->cur_state, insn_idx,
+ BPF_DIAG_CONTEXT_PREEMPT, true,
+ env->cur_state->active_preempt_locks);
} else if (preempt_enable) {
if (env->cur_state->active_preempt_locks == 0) {
verbose(env, "unmatched attempt to enable preemption (kernel function %s)\n", func_name);
return -EINVAL;
}
env->cur_state->active_preempt_locks--;
+ bpf_diag_record_context(env->cur_state, insn_idx,
+ BPF_DIAG_CONTEXT_PREEMPT, false,
+ env->cur_state->active_preempt_locks);
}
if (sleepable && !in_sleepable_context(env)) {
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (6 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 07/17] bpf: Track verifier context " Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:57 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
2026-06-05 6:33 ` [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors Kumar Kartikeya Dwivedi
` (8 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected register-state verifier failures with Register Type
Safety reports. The existing verbose verifier messages remain in place;
the new reports add reason, source context, causal path, and
suggestions.
Cover invalid pointer dereferences, unreadable registers, and missing
outgoing stack arguments for bpf2bpf calls. Use register-scoped history
so reports start from the latest relevant value change and then show
later branch outcomes.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 254 +++++++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 22 ++++
kernel/bpf/verifier.c | 126 +++++++++++++++++--
3 files changed, 394 insertions(+), 8 deletions(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 7a122c5025c2..920ac17758c2 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -457,6 +457,16 @@ static const char *bpf_diag_context_name(enum bpf_diag_context_kind kind)
}
}
+static u32 bpf_diag_current_frameno(const struct bpf_verifier_env *env)
+{
+ const struct bpf_verifier_state *state = env->cur_state;
+
+ if (!state || !state->frame[state->curframe])
+ return 0;
+
+ return state->frame[state->curframe]->frameno;
+}
+
void bpf_diag_report_header(struct bpf_verifier_env *env,
enum bpf_diag_category category,
const char *problem)
@@ -648,6 +658,157 @@ void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
}
}
+void bpf_diag_report_register_type(struct bpf_verifier_env *env,
+ u32 insn_idx, int regno,
+ const char *problem, const char *reason,
+ const char *suggestion)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_REG,
+ .frameno = bpf_diag_current_frameno(env),
+ .regno = regno,
+ };
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
+ problem);
+ bpf_diag_report_reason(env, "%s", reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!', "%s", problem);
+
+ if (regno >= 0)
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
+static const char *bpf_diag_arg_ordinal(int argno)
+{
+ switch (argno) {
+ case 1:
+ return "first";
+ case 2:
+ return "second";
+ case 3:
+ return "third";
+ case 4:
+ return "fourth";
+ case 5:
+ return "fifth";
+ case 6:
+ return "sixth";
+ case 7:
+ return "seventh";
+ case 8:
+ return "eighth";
+ case 9:
+ return "ninth";
+ case 10:
+ return "tenth";
+ case 11:
+ return "eleventh";
+ case 12:
+ return "twelfth";
+ default:
+ return NULL;
+ }
+}
+
+void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
+ int regno, const char *reg_name,
+ const char *type_name,
+ enum bpf_diag_invalid_deref_kind kind,
+ s64 offset)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_REG,
+ .frameno = bpf_diag_current_frameno(env),
+ .regno = regno,
+ };
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
+ "invalid dereference");
+
+ switch (kind) {
+ case BPF_DIAG_DEREF_SCALAR:
+ bpf_diag_report_reason(env,
+ "%s is an integer scalar here, not a pointer to memory.",
+ reg_name);
+ break;
+ case BPF_DIAG_DEREF_NULLABLE_PTR:
+ bpf_diag_report_reason(env,
+ "%s may be NULL here (%s). The program could dereference NULL on this path, so the verifier cannot prove this access is safe.",
+ reg_name, type_name);
+ break;
+ case BPF_DIAG_DEREF_MODIFIED_PTR:
+ bpf_diag_report_reason(env,
+ "%s has offset %lld here, but this pointer type must be dereferenced in its original form.",
+ reg_name, offset);
+ break;
+ case BPF_DIAG_DEREF_INVALID_PTR:
+ default:
+ bpf_diag_report_reason(env,
+ "%s has type %s here, which is not valid for this memory access.",
+ reg_name, type_name);
+ break;
+ }
+
+ bpf_diag_report_section(env, "At");
+ if (kind == BPF_DIAG_DEREF_MODIFIED_PTR)
+ bpf_diag_report_source(env, insn_idx, '!',
+ "dereference requires the original %s pointer",
+ type_name);
+ else
+ bpf_diag_report_source(env, insn_idx, '!',
+ "invalid dereference of %s (%s)",
+ reg_name, type_name);
+
+ bpf_diag_print_history(env, &opts);
+
+ switch (kind) {
+ case BPF_DIAG_DEREF_NULLABLE_PTR:
+ bpf_diag_report_suggestion(env,
+ "Add a NULL check and dereference the pointer only on the non-NULL path.");
+ break;
+ case BPF_DIAG_DEREF_MODIFIED_PTR:
+ bpf_diag_report_suggestion(env,
+ "Preserve the original pointer in another register, or use an allowed pointer offset before dereferencing it.");
+ break;
+ case BPF_DIAG_DEREF_SCALAR:
+ case BPF_DIAG_DEREF_INVALID_PTR:
+ default:
+ bpf_diag_report_suggestion(env,
+ "Preserve a pointer-valued register where needed, or reload and revalidate the pointer after scalar arithmetic, helper calls, or "
+ "other operations that can invalidate it.");
+ break;
+ }
+}
+
+void bpf_diag_report_unreadable_reg(struct bpf_verifier_env *env,
+ u32 insn_idx, int regno)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_REG,
+ .frameno = bpf_diag_current_frameno(env),
+ .regno = regno,
+ };
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
+ "unreadable register");
+ bpf_diag_report_reason(env,
+ "R%d is not readable here. A previous operation may have invalidated this register, so the verifier cannot use it as an input.",
+ regno);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "R%d is not readable", regno);
+
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env,
+ "Avoid using the register after it is invalidated, or reload and revalidate a fresh pointer before this instruction.");
+}
+
void bpf_diag_clear_history(struct bpf_verifier_state *state)
{
kfree(state->diag_history);
@@ -1165,6 +1326,95 @@ static void bpf_diag_print_reg_mod(struct bpf_verifier_env *env,
event->reg.dst_reg, old_buf, new_buf);
}
+static void bpf_diag_format_stack_arg(char *buf, size_t size, u8 slot)
+{
+ int argno = MAX_BPF_FUNC_REG_ARGS + slot + 1;
+ const char *ordinal = bpf_diag_arg_ordinal(argno);
+
+ if (ordinal)
+ scnprintf(buf, size, "outgoing stack argument %u (%s argument)",
+ slot + 1, ordinal);
+ else
+ scnprintf(buf, size, "outgoing stack argument %u", slot + 1);
+}
+
+static const char *
+bpf_diag_stack_arg_reason(enum bpf_diag_stack_arg_reason reason)
+{
+ switch (reason) {
+ case BPF_DIAG_STACK_ARG_REF_RELEASE:
+ return "resource release invalidated the pointer";
+ case BPF_DIAG_STACK_ARG_PKT_DATA_CHANGE:
+ return "packet data may have moved";
+ case BPF_DIAG_STACK_ARG_WRITE:
+ default:
+ return NULL;
+ }
+}
+
+static void bpf_diag_print_stack_arg_event(struct bpf_verifier_env *env,
+ const struct bpf_diag_history_event *event)
+{
+ char old_buf[192], new_buf[192], arg_buf[64];
+ const char *reason;
+
+ bpf_diag_format_stack_arg(arg_buf, sizeof(arg_buf),
+ event->stack_arg.slot);
+ bpf_diag_format_reg_snapshot(env, old_buf, sizeof(old_buf),
+ &event->stack_arg.old);
+ bpf_diag_format_reg_snapshot(env, new_buf, sizeof(new_buf),
+ &event->stack_arg.new);
+
+ if (event->stack_arg.reason == BPF_DIAG_STACK_ARG_WRITE) {
+ bpf_diag_report_source(env, event->insn_idx, '~',
+ "%s set to %s", arg_buf, new_buf);
+ return;
+ }
+
+ reason = bpf_diag_stack_arg_reason(event->stack_arg.reason);
+ bpf_diag_report_source(env, event->insn_idx, 'x',
+ "%s: %s; previous value was %s",
+ arg_buf, reason ?: "the previous value is no longer valid",
+ old_buf);
+}
+
+void bpf_diag_report_stack_arg_uninit(struct bpf_verifier_env *env,
+ u32 insn_idx, int nargs,
+ int stack_arg_slot,
+ const char *callee_name)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_STACK_ARG,
+ .frameno = bpf_diag_current_frameno(env),
+ .stack_arg_slot = stack_arg_slot,
+ };
+ char arg_buf[64];
+
+ bpf_diag_format_stack_arg(arg_buf, sizeof(arg_buf), stack_arg_slot);
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
+ "missing stack argument");
+ if (callee_name)
+ bpf_diag_report_reason(env,
+ "Function %s expects %d arguments, but %s is not initialized at this call. Store a value for this argument after any operation "
+ "that may invalidate it.",
+ callee_name, nargs, arg_buf);
+ else
+ bpf_diag_report_reason(env,
+ "The callee expects %d arguments, but %s is not initialized at this call. Store a value for this argument after any operation "
+ "that may invalidate it.",
+ nargs, arg_buf);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "%s is not initialized", arg_buf);
+
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env,
+ "Write the outgoing stack argument again after releasing a resource, changing packet data, or calling another function that can "
+ "invalidate stored pointer values.");
+}
+
static void bpf_diag_print_ref_event(struct bpf_verifier_env *env,
const struct bpf_diag_history_event *event)
{
@@ -1228,6 +1478,10 @@ void bpf_diag_print_history(struct bpf_verifier_env *env,
bpf_diag_print_reg_mod(env, event);
printed = true;
break;
+ case BPF_DIAG_HISTORY_STACK_ARG:
+ bpf_diag_print_stack_arg_event(env, event);
+ printed = true;
+ break;
case BPF_DIAG_HISTORY_REF_ACQUIRE:
case BPF_DIAG_HISTORY_REF_RELEASE:
bpf_diag_print_ref_event(env, event);
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index ff6c55f9b5f7..b0d42cbdfd23 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -113,6 +113,13 @@ enum bpf_diag_stack_arg_reason {
BPF_DIAG_STACK_ARG_PKT_DATA_CHANGE,
};
+enum bpf_diag_invalid_deref_kind {
+ BPF_DIAG_DEREF_SCALAR,
+ BPF_DIAG_DEREF_NULLABLE_PTR,
+ BPF_DIAG_DEREF_MODIFIED_PTR,
+ BPF_DIAG_DEREF_INVALID_PTR,
+};
+
struct bpf_diag_history_opts {
enum bpf_diag_history_scope scope;
u32 frameno;
@@ -137,6 +144,21 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
char marker, const char *fmt, ...)
__printf(4, 5);
+void bpf_diag_report_register_type(struct bpf_verifier_env *env,
+ u32 insn_idx, int regno,
+ const char *problem, const char *reason,
+ const char *suggestion);
+void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
+ int regno, const char *reg_name,
+ const char *type_name,
+ enum bpf_diag_invalid_deref_kind kind,
+ s64 offset);
+void bpf_diag_report_unreadable_reg(struct bpf_verifier_env *env,
+ u32 insn_idx, int regno);
+void bpf_diag_report_stack_arg_uninit(struct bpf_verifier_env *env,
+ u32 insn_idx, int nargs,
+ int stack_arg_slot,
+ const char *callee_name);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 7bb2a81ee353..adf660ab9e7d 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3123,6 +3123,8 @@ static int __check_reg_arg(struct bpf_verifier_env *env, struct bpf_reg_state *r
/* check whether register used as source operand can be read */
if (reg->type == NOT_INIT) {
verbose(env, "R%d !read_ok\n", regno);
+ bpf_diag_report_unreadable_reg(env, env->insn_idx,
+ regno);
return -EACCES;
}
/* We don't need to worry about FP liveness because it's read-only */
@@ -4134,8 +4136,9 @@ static int mark_stack_arg_precision(struct bpf_verifier_env *env, int arg_idx)
return mark_chain_precision_batch(env, env->cur_state);
}
-static int check_outgoing_stack_args(struct bpf_verifier_env *env, struct bpf_func_state *caller,
- int nargs)
+static int check_outgoing_stack_args(struct bpf_verifier_env *env,
+ struct bpf_func_state *caller, int nargs,
+ const char *callee_name)
{
int i, spi;
@@ -4145,6 +4148,9 @@ static int check_outgoing_stack_args(struct bpf_verifier_env *env, struct bpf_fu
caller->stack_arg_regs[spi].type == NOT_INIT) {
verbose(env, "callee expects %d args, stack arg%d is not initialized\n",
nargs, spi + 1);
+ bpf_diag_report_stack_arg_uninit(env, env->insn_idx,
+ nargs, spi,
+ callee_name);
return -EFAULT;
}
}
@@ -4299,6 +4305,11 @@ static int __check_ptr_off_reg(struct bpf_verifier_env *env,
if (!fixed_off_ok && reg->var_off.value != 0) {
verbose(env, "dereference of modified %s ptr %s off=%lld disallowed\n",
reg_type_str(env, reg->type), reg_arg_name(env, argno), reg->var_off.value);
+ bpf_diag_report_invalid_deref(env, env->insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type),
+ BPF_DIAG_DEREF_MODIFIED_PTR,
+ reg->var_off.value);
return -EACCES;
}
@@ -6166,6 +6177,11 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
if (type_may_be_null(reg->type)) {
verbose(env, "%s invalid mem access '%s'\n", reg_arg_name(env, argno),
reg_type_str(env, reg->type));
+ bpf_diag_report_invalid_deref(env, insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type),
+ BPF_DIAG_DEREF_NULLABLE_PTR,
+ 0);
return -EACCES;
}
@@ -6321,8 +6337,17 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
if (t == BPF_READ && value_regno >= 0)
mark_reg_unknown(env, regs, value_regno);
} else {
+ enum bpf_diag_invalid_deref_kind kind = BPF_DIAG_DEREF_INVALID_PTR;
+
verbose(env, "%s invalid mem access '%s'\n", reg_arg_name(env, argno),
reg_type_str(env, reg->type));
+ if (reg->type == SCALAR_VALUE)
+ kind = BPF_DIAG_DEREF_SCALAR;
+ else if (type_may_be_null(reg->type))
+ kind = BPF_DIAG_DEREF_NULLABLE_PTR;
+ bpf_diag_report_invalid_deref(env, insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type), kind, 0);
return -EACCES;
}
@@ -9216,14 +9241,17 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog,
ret = btf_prepare_func_args(env, subprog);
if (ret) {
if (bpf_in_stack_arg_cnt(sub) > 0) {
- err = check_outgoing_stack_args(env, caller, sub->arg_cnt);
+ err = check_outgoing_stack_args(env, caller,
+ sub->arg_cnt,
+ subprog_name(env, subprog));
if (err)
return err;
}
return ret;
}
- ret = check_outgoing_stack_args(env, caller, sub->arg_cnt);
+ ret = check_outgoing_stack_args(env, caller, sub->arg_cnt,
+ subprog_name(env, subprog));
if (ret)
return ret;
@@ -11966,7 +11994,7 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
return -ENOTSUPP;
}
- ret = check_outgoing_stack_args(env, caller, nargs);
+ ret = check_outgoing_stack_args(env, caller, nargs, func_name);
if (ret)
return ret;
@@ -13684,6 +13712,7 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
struct bpf_sanitize_info info = {};
u8 opcode = BPF_OP(insn->code);
u32 dst = insn->dst_reg;
+ char reason[256];
int ret, bounds_ret;
dst_reg = ®s[dst];
@@ -13707,12 +13736,28 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
verbose(env,
"R%d 32-bit pointer arithmetic prohibited\n",
dst);
+ scnprintf(reason, sizeof(reason),
+ "R%d holds %s. 32-bit ALU operations on pointers discard pointer provenance, so the verifier cannot keep the result as a safe "
+ "pointer.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "32-bit pointer arithmetic",
+ reason,
+ "Use 64-bit pointer arithmetic with an allowed, bounded scalar offset, or convert the value to a scalar only after pointer use "
+ "is complete.");
return -EACCES;
}
if (ptr_reg->type & PTR_MAYBE_NULL) {
verbose(env, "R%d pointer arithmetic on %s prohibited, null-check it first\n",
dst, reg_type_str(env, ptr_reg->type));
+ scnprintf(reason, sizeof(reason),
+ "R%d may be NULL (%s). Pointer arithmetic is allowed only after the program proves the pointer is non-NULL on this path.",
+ dst, reg_type_str(env, ptr_reg->type));
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "pointer arithmetic before NULL check",
+ reason,
+ "Add a NULL check first, and do the pointer arithmetic only on the non-NULL branch.");
return -EACCES;
}
@@ -13749,6 +13794,13 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
default:
verbose(env, "R%d pointer arithmetic on %s prohibited\n",
dst, reg_type_str(env, ptr_reg->type));
+ scnprintf(reason, sizeof(reason),
+ "R%d holds %s. This pointer kind does not allow arithmetic because changing its offset would lose the verifier's required provenance.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "pointer arithmetic is not allowed",
+ reason,
+ "Keep this pointer unchanged, or derive an allowed pointer type before applying a bounded offset.");
return -EACCES;
}
@@ -13758,9 +13810,26 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
dst_reg->type = ptr_reg->type;
dst_reg->id = ptr_reg->id;
- if (!check_reg_sane_offset_scalar(env, off_reg, ptr_reg->type) ||
- !check_reg_sane_offset_ptr(env, ptr_reg, ptr_reg->type))
+ if (!check_reg_sane_offset_scalar(env, off_reg, ptr_reg->type)) {
+ scnprintf(reason, sizeof(reason),
+ "The scalar offset used with R%d has a range the verifier cannot safely add to %s.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "pointer offset is not safe",
+ reason,
+ "Clamp or bound-check the scalar offset before adding it to the pointer.");
+ return -EINVAL;
+ }
+ if (!check_reg_sane_offset_ptr(env, ptr_reg, ptr_reg->type)) {
+ scnprintf(reason, sizeof(reason),
+ "R%d already has an offset that is outside the verifier's safe pointer range for %s.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "pointer offset is not safe",
+ reason,
+ "Keep the base pointer within the verifier's allowed offset range before applying more arithmetic.");
return -EINVAL;
+ }
/* pointer types do not carry 32-bit bounds at the moment. */
__mark_reg32_unbounded(dst_reg);
@@ -13803,6 +13872,14 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
/* scalar -= pointer. Creates an unknown scalar */
verbose(env, "R%d tried to subtract pointer from scalar\n",
dst);
+ scnprintf(reason, sizeof(reason),
+ "This operation subtracts a pointer from scalar register R%d. The verifier does not allow pointer provenance to flow through scalar "
+ "subtraction.",
+ dst);
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "pointer subtracted from scalar",
+ reason,
+ "Keep pointer values in pointer registers, or use explicit scalar values that do not come from pointer arithmetic.");
return -EACCES;
}
/* We don't allow subtraction from FP, because (according to
@@ -13812,6 +13889,14 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
if (ptr_reg->type == PTR_TO_STACK) {
verbose(env, "R%d subtraction from stack pointer prohibited\n",
dst);
+ scnprintf(reason, sizeof(reason),
+ "R%d is derived from the frame pointer. Subtracting from a stack pointer is not allowed because stack accesses must stay within the "
+ "verifier's tracked frame offsets.",
+ dst);
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "subtraction from stack pointer",
+ reason,
+ "Use fixed negative stack offsets from R10, and avoid arithmetic that moves the frame pointer backward.");
return -EACCES;
}
dst_reg->r64 = cnum64_add(ptr_reg->r64, cnum64_negate(off_reg->r64));
@@ -13837,16 +13922,41 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
/* bitwise ops on pointers are troublesome, prohibit. */
verbose(env, "R%d bitwise operator %s on pointer prohibited\n",
dst, bpf_alu_string[opcode >> 4]);
+ scnprintf(reason, sizeof(reason),
+ "R%d holds %s. Bitwise operator %s would destroy pointer provenance, so the verifier cannot track the result as a valid pointer.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type),
+ bpf_alu_string[opcode >> 4]);
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "bitwise operation on pointer",
+ reason,
+ "Do bitwise operations on scalar values, not on pointer-valued registers.");
return -EACCES;
default:
/* other operators (e.g. MUL,LSH) produce non-pointer results */
verbose(env, "R%d pointer arithmetic with %s operator prohibited\n",
dst, bpf_alu_string[opcode >> 4]);
+ scnprintf(reason, sizeof(reason),
+ "R%d holds %s. Operator %s is not one of the limited pointer arithmetic operations the verifier can track.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type),
+ bpf_alu_string[opcode >> 4]);
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "invalid pointer arithmetic operator",
+ reason,
+ "Use only verifier-supported addition or subtraction with a bounded scalar offset, or perform this operation on a scalar "
+ "value.");
return -EACCES;
}
- if (!check_reg_sane_offset_ptr(env, dst_reg, ptr_reg->type))
+ if (!check_reg_sane_offset_ptr(env, dst_reg, ptr_reg->type)) {
+ scnprintf(reason, sizeof(reason),
+ "After this arithmetic, R%d would have an offset outside the verifier's safe range for %s.",
+ dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
+ bpf_diag_report_register_type(env, env->insn_idx, dst,
+ "pointer offset is not safe",
+ reason,
+ "Tighten the scalar bounds before the arithmetic so the resulting pointer remains within the allowed range.");
return -EINVAL;
+ }
reg_bounds_sync(dst_reg);
bounds_ret = sanitize_check_bounds(env, insn, dst_reg);
if (bounds_ret == -EACCES)
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (7 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:33 ` Kumar Kartikeya Dwivedi
2026-06-05 6:45 ` sashiko-bot
2026-06-05 7:57 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks Kumar Kartikeya Dwivedi
` (7 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:33 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment check_mem_region_access() range-proof failures with Memory Safety
reports. The existing terse verifier messages remain in place for compatibility;
the new report adds the failed proof, source context, causal path, and guidance.
Cover negative signed minima, minimum offsets outside the object, unbounded
upper offsets, and maximum offsets outside the object.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 113 +++++++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 16 ++++++
kernel/bpf/verifier.c | 77 ++++++++++++++++++++++++++
3 files changed, 206 insertions(+)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 920ac17758c2..0abe1ab19d25 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -809,6 +809,119 @@ void bpf_diag_report_unreadable_reg(struct bpf_verifier_env *env,
"Avoid using the register after it is invalidated, or reload and revalidate a fresh pointer before this instruction.");
}
+static void bpf_diag_format_access_offset(char *buf, size_t size, int off,
+ const struct bpf_reg_state *reg)
+{
+ char range[BPF_DIAG_REG_DESC_LEN];
+
+ if (tnum_is_const(reg->var_off)) {
+ scnprintf(buf, size, "constant %lld",
+ (s64)off + reg->var_off.value);
+ return;
+ }
+
+ if (bpf_diag_var_off_unknown(reg->var_off.value, reg->var_off.mask) &&
+ bpf_diag_range_unknown(reg_smin(reg), reg_smax(reg),
+ reg_umin(reg), reg_umax(reg))) {
+ scnprintf(buf, size, "unknown");
+ return;
+ }
+
+ bpf_diag_format_scalar_range(range, sizeof(range), reg_smin(reg),
+ reg_smax(reg), reg_umin(reg),
+ reg_umax(reg));
+ if (off) {
+ scnprintf(buf, size,
+ "variable: pointer known bits %#llx, unknown mask %#llx, plus %d from this access; %s",
+ (u64)reg->var_off.value, reg->var_off.mask, off,
+ range);
+ return;
+ }
+
+ scnprintf(buf, size,
+ "variable: known bits %#llx, unknown mask %#llx, %s",
+ (u64)reg->var_off.value, reg->var_off.mask,
+ range);
+}
+
+void bpf_diag_report_memory(struct bpf_verifier_env *env, u32 insn_idx,
+ const char *problem, const char *reason,
+ const char *suggestion)
+{
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_MEMORY_SAFETY,
+ problem);
+ bpf_diag_report_reason(env, "%s", reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!', "%s", problem);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
+void bpf_diag_report_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx,
+ int regno, const char *reg_name,
+ const char *type_name,
+ enum bpf_diag_mem_bounds_kind kind,
+ int off, int size, u32 mem_size,
+ const struct bpf_reg_state *reg)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_REG,
+ .frameno = bpf_diag_current_frameno(env),
+ .regno = regno,
+ };
+ char offset_desc[256];
+ char proof[192];
+ u64 max_start, max_end;
+
+ switch (kind) {
+ case BPF_DIAG_MEM_NEGATIVE_MIN:
+ scnprintf(proof, sizeof(proof),
+ "The smallest possible offset is %lld, below 0",
+ reg_smin(reg) + off);
+ break;
+ case BPF_DIAG_MEM_MIN_OUT_OF_RANGE:
+ scnprintf(proof, sizeof(proof),
+ "The smallest possible access starts at %lld, outside an object of size %u",
+ reg_smin(reg) + off, mem_size);
+ break;
+ case BPF_DIAG_MEM_UNBOUNDED:
+ scnprintf(proof, sizeof(proof),
+ "The access needs offset + size <= %u, but %s has no safe upper bound",
+ mem_size, reg_name);
+ break;
+ case BPF_DIAG_MEM_MAX_OUT_OF_RANGE:
+ default:
+ if (off < 0 && reg_umax(reg) < (u64)-off)
+ max_start = 0;
+ else
+ max_start = reg_umax(reg) + off;
+ max_end = max_start + size;
+ scnprintf(proof, sizeof(proof),
+ "The largest possible access ends at %llu, beyond an object of size %u",
+ max_end, mem_size);
+ break;
+ }
+
+ bpf_diag_format_access_offset(offset_desc, sizeof(offset_desc), off, reg);
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_MEMORY_SAFETY,
+ "access outside bounds");
+ bpf_diag_report_reason(env,
+ "The verifier cannot prove that this access stays within the object: %s. %s is %s; offset is %s; access size %d; object size %u.",
+ proof, reg_name, type_name, offset_desc, size,
+ mem_size);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "access may be outside object bounds");
+
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env,
+ "Add or adjust a bounds check that proves offset + access size stays within the object.");
+}
+
void bpf_diag_clear_history(struct bpf_verifier_state *state)
{
kfree(state->diag_history);
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index b0d42cbdfd23..4f7617fdb700 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -120,6 +120,13 @@ enum bpf_diag_invalid_deref_kind {
BPF_DIAG_DEREF_INVALID_PTR,
};
+enum bpf_diag_mem_bounds_kind {
+ BPF_DIAG_MEM_NEGATIVE_MIN,
+ BPF_DIAG_MEM_MIN_OUT_OF_RANGE,
+ BPF_DIAG_MEM_UNBOUNDED,
+ BPF_DIAG_MEM_MAX_OUT_OF_RANGE,
+};
+
struct bpf_diag_history_opts {
enum bpf_diag_history_scope scope;
u32 frameno;
@@ -159,6 +166,15 @@ void bpf_diag_report_stack_arg_uninit(struct bpf_verifier_env *env,
u32 insn_idx, int nargs,
int stack_arg_slot,
const char *callee_name);
+void bpf_diag_report_memory(struct bpf_verifier_env *env, u32 insn_idx,
+ const char *problem, const char *reason,
+ const char *suggestion);
+void bpf_diag_report_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx,
+ int regno, const char *reg_name,
+ const char *type_name,
+ enum bpf_diag_mem_bounds_kind kind,
+ int off, int size, u32 mem_size,
+ const struct bpf_reg_state *reg);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index adf660ab9e7d..e9c121892b7e 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3481,7 +3481,17 @@ static int check_stack_write_fixed_off(struct bpf_verifier_env *env,
bpf_is_spilled_reg(&state->stack[spi]) &&
!bpf_is_spilled_scalar_reg(&state->stack[spi]) &&
size != BPF_REG_SIZE) {
+ char reason[256];
+
verbose(env, "attempt to corrupt spilled pointer on stack\n");
+ scnprintf(reason, sizeof(reason),
+ "This store writes %d bytes at stack offset %d into a stack slot that currently holds a spilled pointer. Partial writes to spilled "
+ "pointers are rejected because they can corrupt pointer metadata and leak kernel pointers.",
+ size, off);
+ bpf_diag_report_memory(env, insn_idx,
+ "stack spill corruption",
+ reason,
+ "Write the full 8-byte spilled pointer slot, or use a separate stack slot for scalar data before overwriting only part of it.");
return -EACCES;
}
@@ -3746,6 +3756,35 @@ static void mark_reg_stack_read(struct bpf_verifier_env *env,
}
}
+static void bpf_diag_report_stack_read_failure(struct bpf_verifier_env *env,
+ int off, int i, int size,
+ bool poisoned)
+{
+ char reason[256];
+
+ if (poisoned) {
+ scnprintf(reason, sizeof(reason),
+ "This read uses %d bytes at stack offset %d, but byte %d in that range is poisoned because it was eliminated as dead code on this "
+ "path.",
+ size, off, i);
+ bpf_diag_report_memory(env, env->insn_idx,
+ "poisoned stack read",
+ reason,
+ "Avoid reading stack bytes written only by code that the verifier proved unreachable, or initialize the stack range again on "
+ "this path.");
+ return;
+ }
+
+ scnprintf(reason, sizeof(reason),
+ "This read uses %d bytes at stack offset %d, but byte %d in that range is uninitialized on this path.",
+ size, off, i);
+ bpf_diag_report_memory(env, env->insn_idx,
+ "uninitialized stack read",
+ reason,
+ "Initialize every byte in the stack range before reading it, or adjust the offset and size so the read covers only initialized "
+ "bytes.");
+}
+
/* Read the stack at 'off' and put the results into the register indicated by
* 'dst_regno'. It handles reg filling if the addressed stack slot is a
* spilled reg.
@@ -3831,9 +3870,13 @@ static int check_stack_read_fixed_off(struct bpf_verifier_env *env,
if (type == STACK_POISON) {
verbose(env, "reading from stack off %d+%d size %d, slot poisoned by dead code elimination\n",
off, i, size);
+ bpf_diag_report_stack_read_failure(env, off, i,
+ size, true);
} else {
verbose(env, "invalid read from stack off %d+%d size %d\n",
off, i, size);
+ bpf_diag_report_stack_read_failure(env, off, i,
+ size, false);
}
return -EACCES;
}
@@ -3886,9 +3929,13 @@ static int check_stack_read_fixed_off(struct bpf_verifier_env *env,
if (type == STACK_POISON) {
verbose(env, "reading from stack off %d+%d size %d, slot poisoned by dead code elimination\n",
off, i, size);
+ bpf_diag_report_stack_read_failure(env, off, i,
+ size, true);
} else {
verbose(env, "invalid read from stack off %d+%d size %d\n",
off, i, size);
+ bpf_diag_report_stack_read_failure(env, off, i,
+ size, false);
}
return -EACCES;
}
@@ -3976,10 +4023,20 @@ static int check_stack_read(struct bpf_verifier_env *env,
*/
if (dst_regno < 0 && var_off) {
char tn_buf[48];
+ char reason[256];
tnum_strn(tn_buf, sizeof(tn_buf), reg->var_off);
verbose(env, "variable offset stack pointer cannot be passed into helper function; var_off=%s off=%d size=%d\n",
tn_buf, off, size);
+ scnprintf(reason, sizeof(reason),
+ "The helper would access the stack through a variable offset %s plus fixed offset %d and size %d. Helper stack arguments need a "
+ "precise stack range so the verifier can prove every byte is initialized and inside the stack.",
+ tn_buf, off, size);
+ bpf_diag_report_memory(env, env->insn_idx,
+ "variable stack access",
+ reason,
+ "Use a fixed stack offset for helper memory arguments, or add bounds checks and copy the needed bytes into a fixed stack slot "
+ "first.");
return -EACCES;
}
/* Variable offset is prohibited for unprivileged mode for simplicity
@@ -4249,6 +4306,11 @@ static int check_mem_region_access(struct bpf_verifier_env *env, struct bpf_reg_
reg_smin(reg) + off < 0)) {
verbose(env, "%s min value is negative, either use unsigned index or do a if (index >=0) check.\n",
reg_arg_name(env, argno));
+ bpf_diag_report_mem_bounds(env, env->insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type),
+ BPF_DIAG_MEM_NEGATIVE_MIN,
+ off, size, mem_size, reg);
return -EACCES;
}
err = __check_mem_access(env, reg, argno, reg_smin(reg) + off, size,
@@ -4256,6 +4318,11 @@ static int check_mem_region_access(struct bpf_verifier_env *env, struct bpf_reg_
if (err) {
verbose(env, "%s min value is outside of the allowed memory range\n",
reg_arg_name(env, argno));
+ bpf_diag_report_mem_bounds(env, env->insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type),
+ BPF_DIAG_MEM_MIN_OUT_OF_RANGE,
+ off, size, mem_size, reg);
return err;
}
@@ -4266,6 +4333,11 @@ static int check_mem_region_access(struct bpf_verifier_env *env, struct bpf_reg_
if (reg_umax(reg) >= BPF_MAX_VAR_OFF) {
verbose(env, "%s unbounded memory access, make sure to bounds check any such access\n",
reg_arg_name(env, argno));
+ bpf_diag_report_mem_bounds(env, env->insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type),
+ BPF_DIAG_MEM_UNBOUNDED,
+ off, size, mem_size, reg);
return -EACCES;
}
err = __check_mem_access(env, reg, argno, reg_umax(reg) + off, size,
@@ -4273,6 +4345,11 @@ static int check_mem_region_access(struct bpf_verifier_env *env, struct bpf_reg_
if (err) {
verbose(env, "%s max value is outside of the allowed memory range\n",
reg_arg_name(env, argno));
+ bpf_diag_report_mem_bounds(env, env->insn_idx, reg_from_argno(argno),
+ reg_arg_name(env, argno),
+ reg_type_str(env, reg->type),
+ BPF_DIAG_MEM_MAX_OUT_OF_RANGE,
+ off, size, mem_size, reg);
return err;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (8 preceding siblings ...)
2026-06-05 6:33 ` [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:45 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors Kumar Kartikeya Dwivedi
` (6 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment check_reference_leak() with a Resource Lifetime Safety report when
a reference is still live at exit or throw time. The existing verifier leak
message remains in place; the new report points at the failing instruction
and prints the causal path from the matching acquisition.
Keep release-candidate discovery out of this first report so the initial
change stays focused on lifetime tracking and presentation.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 39 ++++++++++++++++
kernel/bpf/diagnostics.h | 6 +++
kernel/bpf/verifier.c | 98 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 143 insertions(+)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 0abe1ab19d25..369b7813b5dc 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -922,6 +922,45 @@ void bpf_diag_report_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx,
"Add or adjust a bounds check that proves offset + access size stays within the object.");
}
+void bpf_diag_report_resource_state(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason,
+ const char *suggestion)
+{
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_RESOURCE_LIFETIME_SAFETY,
+ problem);
+ bpf_diag_report_reason(env, "%s", reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!', "%s", problem);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
+void bpf_diag_report_ref_leak(struct bpf_verifier_env *env, u32 ref_id,
+ u32 alloc_insn, u32 fail_insn)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_REF,
+ .ref_id = ref_id,
+ };
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_RESOURCE_LIFETIME_SAFETY,
+ "unreleased resource");
+ bpf_diag_report_reason(env,
+ "Owned resource (id=%u) was acquired at instruction %u and still needs to be released before this exit path.",
+ ref_id, alloc_insn);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, fail_insn, '!',
+ "owned resource (id=%u) still needs release", ref_id);
+
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env,
+ "Release or transfer ownership of the acquired resource on every path before the program exits.");
+}
+
void bpf_diag_clear_history(struct bpf_verifier_state *state)
{
kfree(state->diag_history);
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index 4f7617fdb700..0bc5f3492c47 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -175,6 +175,12 @@ void bpf_diag_report_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx,
enum bpf_diag_mem_bounds_kind kind,
int off, int size, u32 mem_size,
const struct bpf_reg_state *reg);
+void bpf_diag_report_resource_state(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason,
+ const char *suggestion);
+void bpf_diag_report_ref_leak(struct bpf_verifier_env *env, u32 ref_id,
+ u32 alloc_insn, u32 fail_insn);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index e9c121892b7e..1c26614bec2b 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -831,6 +831,11 @@ static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
if (dynptr_type_referenced(state->stack[spi].spilled_ptr.dynptr.type) &&
dynptr_ref_cnt(env, state->stack[spi].spilled_ptr.parent_id) <= 1) {
verbose(env, "cannot overwrite referenced dynptr\n");
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "referenced dynptr overwrite",
+ "This stack slot contains a dynptr that owns or protects a referenced resource. Overwriting the last dynptr for that resource "
+ "would lose the verifier-tracked release path.",
+ "Release or clone the dynptr so another live dynptr still tracks the referenced resource before overwriting this stack slot.");
return -EINVAL;
}
@@ -1114,9 +1119,18 @@ static int unmark_stack_slot_irq_flag(struct bpf_verifier_env *env, struct bpf_r
if (st->irq.kfunc_class != kfunc_class) {
const char *flag_kfunc = st->irq.kfunc_class == IRQ_NATIVE_KFUNC ? "native" : "lock";
const char *used_kfunc = kfunc_class == IRQ_NATIVE_KFUNC ? "native" : "lock";
+ char reason[256];
verbose(env, "irq flag acquired by %s kfuncs cannot be restored with %s kfuncs\n",
flag_kfunc, used_kfunc);
+ scnprintf(reason, sizeof(reason),
+ "This IRQ flag was saved by %s IRQ kfuncs, but the restore call belongs to the %s IRQ kfunc family. Save and restore operations must "
+ "use the same family.",
+ flag_kfunc, used_kfunc);
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "IRQ flag restore mismatch",
+ reason,
+ "Restore the flag with the matching IRQ restore kfunc for the save operation that created it.");
return -EINVAL;
}
@@ -1134,6 +1148,11 @@ static int unmark_stack_slot_irq_flag(struct bpf_verifier_env *env, struct bpf_r
verbose(env, "cannot restore irq state out of order, expected id=%d acquired at insn_idx=%d\n",
env->cur_state->active_irq_id, insn_idx);
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "IRQ flag restore out of order",
+ "IRQ-disabled regions must be restored in last-in, first-out order, but this restore does not match the currently active IRQ "
+ "flag.",
+ "Restore nested IRQ flags in the reverse order they were saved.");
return err;
}
@@ -7151,11 +7170,19 @@ static int process_spin_lock(struct bpf_verifier_env *env, struct bpf_reg_state
if (find_lock_state(env->cur_state, REF_TYPE_LOCK, 0, NULL)) {
verbose(env,
"Locking two bpf_spin_locks are not allowed\n");
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "nested spin lock",
+ "This path already holds a bpf_spin_lock. The verifier allows only one regular BPF spin lock at a time.",
+ "Unlock the current bpf_spin_lock before taking another one.");
return -EINVAL;
}
} else if (is_res_lock && cur->active_locks) {
if (find_lock_state(env->cur_state, REF_TYPE_RES_LOCK | REF_TYPE_RES_LOCK_IRQ, reg->id, ptr)) {
verbose(env, "Acquiring the same lock again, AA deadlock detected\n");
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "recursive resource spin lock",
+ "This path already holds the same resource spin lock. Taking it again would deadlock.",
+ "Avoid reacquiring the same resource spin lock before it is unlocked.");
return -EINVAL;
}
}
@@ -7182,6 +7209,10 @@ static int process_spin_lock(struct bpf_verifier_env *env, struct bpf_reg_state
if (!cur->active_locks) {
verbose(env, "%s_unlock without taking a lock\n", lock_str);
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "unlock without lock",
+ "This unlock operation has no matching active lock on the current path.",
+ "Take the matching lock before this unlock, or remove the unmatched unlock path.");
return -EINVAL;
}
@@ -7193,15 +7224,27 @@ static int process_spin_lock(struct bpf_verifier_env *env, struct bpf_reg_state
type = REF_TYPE_LOCK;
if (!find_lock_state(cur, type, reg->id, ptr)) {
verbose(env, "%s_unlock of different lock\n", lock_str);
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "unlock of different lock",
+ "This unlock does not match any active lock with the same tracked identity on the current path.",
+ "Unlock the same lock object that was most recently acquired.");
return -EINVAL;
}
if (reg->id != cur->active_lock_id || ptr != cur->active_lock_ptr) {
verbose(env, "%s_unlock cannot be out of order\n", lock_str);
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "unlock out of order",
+ "Locks must be released in last-in, first-out order, but this unlock does not match the currently active lock.",
+ "Release nested locks in the reverse order they were acquired.");
return -EINVAL;
}
if (release_lock_state(cur, type, reg->id, ptr,
env->insn_idx)) {
verbose(env, "%s_unlock of different lock\n", lock_str);
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "unlock of different lock",
+ "The verifier could not release a lock state matching this unlock operation.",
+ "Pass the same lock object and lock kind that were used for the matching lock operation.");
return -EINVAL;
}
@@ -7367,6 +7410,10 @@ static int process_dynptr_func(struct bpf_verifier_env *env, struct bpf_reg_stat
verbose(env,
"%s expected pointer to stack or const struct bpf_dynptr\n",
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "invalid dynptr argument",
+ "A dynptr argument must be a pointer to a dynptr stack slot or a verifier-provided const struct bpf_dynptr.",
+ "Pass the address of a stack dynptr object, or use a const dynptr pointer returned by the verifier-supported path.");
return -EINVAL;
}
@@ -7389,6 +7436,11 @@ static int process_dynptr_func(struct bpf_verifier_env *env, struct bpf_reg_stat
if (!is_dynptr_reg_valid_uninit(env, reg)) {
verbose(env, "Dynptr has to be an uninitialized dynptr\n");
+ bpf_diag_report_resource_state(env, insn_idx,
+ "dynptr is already initialized",
+ "This kfunc constructs a dynptr and requires an uninitialized dynptr stack slot, but the selected slot already holds dynptr "
+ "state.",
+ "Use a fresh stack dynptr slot, or release/destroy the existing dynptr before reusing the slot.");
return -EINVAL;
}
@@ -7405,12 +7457,21 @@ static int process_dynptr_func(struct bpf_verifier_env *env, struct bpf_reg_stat
/* For the reg->type == PTR_TO_STACK case, bpf_dynptr is never const */
if (reg->type == CONST_PTR_TO_DYNPTR && (arg_type & OBJ_RELEASE)) {
verbose(env, "CONST_PTR_TO_DYNPTR cannot be released\n");
+ bpf_diag_report_resource_state(env, insn_idx,
+ "const dynptr release",
+ "This release operation was given a const dynptr. Const dynptr values are verifier-provided views and cannot be released by "
+ "the program.",
+ "Release only mutable dynptrs that the program initialized or reserved.");
return -EINVAL;
}
if (!is_dynptr_reg_valid_init(env, reg)) {
verbose(env, "Expected an initialized dynptr as %s\n",
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "uninitialized dynptr use",
+ "This operation requires an initialized dynptr, but the stack slot does not currently hold a valid dynptr on this path.",
+ "Initialize the dynptr on every path before this call, and avoid overwriting or releasing it before this use.");
return -EINVAL;
}
@@ -7420,6 +7481,10 @@ static int process_dynptr_func(struct bpf_verifier_env *env, struct bpf_reg_stat
"Expected a dynptr of type %s as %s\n",
dynptr_type_str(arg_to_dynptr_type(arg_type)),
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "wrong dynptr type",
+ "The dynptr is initialized, but it was created for a different backing object type than this operation accepts.",
+ "Use a dynptr constructor that matches this operation, or call an operation that accepts the dynptr's current type.");
return -EINVAL;
}
@@ -7488,6 +7553,10 @@ static int process_iter_arg(struct bpf_verifier_env *env, struct bpf_reg_state *
if (reg->type != PTR_TO_STACK) {
verbose(env, "%s expected pointer to an iterator on stack\n",
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "invalid iterator argument",
+ "Iterator state must live in verifier-tracked stack memory, but this argument is not a stack pointer.",
+ "Pass the address of a stack iterator object for iterator new, next, and destroy calls.");
return -EINVAL;
}
@@ -7501,6 +7570,10 @@ static int process_iter_arg(struct bpf_verifier_env *env, struct bpf_reg_state *
if (btf_id < 0) {
verbose(env, "expected valid iter pointer as %s\n",
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "invalid iterator type",
+ "The kfunc expects a recognized iterator state pointer, but this argument does not match a valid iterator type.",
+ "Pass the exact iterator state type expected by this kfunc.");
return -EINVAL;
}
t = btf_type_by_id(meta->btf, btf_id);
@@ -7511,6 +7584,10 @@ static int process_iter_arg(struct bpf_verifier_env *env, struct bpf_reg_state *
if (!is_iter_reg_valid_uninit(env, reg, nr_slots)) {
verbose(env, "expected uninitialized iter_%s as %s\n",
iter_type_str(meta->btf, btf_id), reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "iterator is already initialized",
+ "Iterator creation requires an uninitialized iterator stack object, but this stack range already contains iterator state.",
+ "Use a fresh iterator stack slot, or destroy the existing iterator before reusing the slot.");
return -EINVAL;
}
@@ -7535,9 +7612,19 @@ static int process_iter_arg(struct bpf_verifier_env *env, struct bpf_reg_state *
case -EINVAL:
verbose(env, "expected an initialized iter_%s as %s\n",
iter_type_str(meta->btf, btf_id), reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, insn_idx,
+ "uninitialized iterator use",
+ "This iterator operation requires an initialized iterator state object, but the stack range does not contain a live iterator "
+ "on this path.",
+ "Call the matching iterator new kfunc on every path before calling next or destroy, and do not destroy the iterator before "
+ "this use.");
return err;
case -EPROTO:
verbose(env, "expected an RCU CS when using %s\n", meta->func_name);
+ bpf_diag_report_resource_state(env, insn_idx,
+ "iterator requires RCU read lock",
+ "This iterator was created for use under RCU protection, but this path is not currently inside an RCU read lock region.",
+ "Wrap iterator use in bpf_rcu_read_lock() and bpf_rcu_read_unlock(), keeping all exit paths balanced.");
return err;
default:
return err;
@@ -10119,6 +10206,8 @@ static int check_reference_leak(struct bpf_verifier_env *env, bool exception_exi
continue;
verbose(env, "Unreleased reference id=%d alloc_insn=%d\n",
state->refs[i].id, state->refs[i].insn_idx);
+ bpf_diag_report_ref_leak(env, state->refs[i].id,
+ state->refs[i].insn_idx, env->insn_idx);
refs_lingering = true;
}
return refs_lingering ? -EINVAL : 0;
@@ -11576,6 +11665,11 @@ static int process_irq_flag(struct bpf_verifier_env *env, struct bpf_reg_state *
if (!is_irq_flag_reg_valid_uninit(env, reg)) {
verbose(env, "expected uninitialized irq flag as %s\n",
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "IRQ flag is already initialized",
+ "Saving IRQ state requires an uninitialized stack slot for the IRQ flag, but this slot already contains tracked IRQ flag "
+ "state.",
+ "Use a fresh stack slot for this save operation, or restore the existing IRQ flag before reusing the slot.");
return -EINVAL;
}
@@ -11592,6 +11686,10 @@ static int process_irq_flag(struct bpf_verifier_env *env, struct bpf_reg_state *
if (err) {
verbose(env, "expected an initialized irq flag as %s\n",
reg_arg_name(env, argno));
+ bpf_diag_report_resource_state(env, env->insn_idx,
+ "uninitialized IRQ flag restore",
+ "Restoring IRQ state requires a stack slot that was initialized by a matching IRQ save operation on this path.",
+ "Pass the same stack slot that was previously initialized by the matching IRQ save kfunc.");
return err;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (9 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:47 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors Kumar Kartikeya Dwivedi
` (5 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected helper and kfunc argument-contract failures with Call
Type Safety reports. Keep the existing terse verifier messages and add
reason, source context, causal register history, and targeted
suggestions.
Cover helper register-type mismatch, helper non-NULL pointer
requirements, kfunc trusted-pointer nullability, release kfunc
referenced-pointer requirements, kfunc memory arguments, memory/length
pairs, and kernel object pointer contracts.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 37 +++++
kernel/bpf/diagnostics.h | 4 +
kernel/bpf/verifier.c | 295 ++++++++++++++++++++++++++++++++++++++-
3 files changed, 329 insertions(+), 7 deletions(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 369b7813b5dc..169c1a2425c8 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -714,6 +714,43 @@ static const char *bpf_diag_arg_ordinal(int argno)
}
}
+void bpf_diag_report_call_type(struct bpf_verifier_env *env, u32 insn_idx,
+ int regno, const char *call_name,
+ const char *arg_name, const char *reason,
+ const char *suggestion)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_REG,
+ .frameno = bpf_diag_current_frameno(env),
+ .regno = regno,
+ };
+ const char *ordinal = bpf_diag_arg_ordinal(regno);
+ char arg_desc[48];
+
+ if (ordinal && arg_name)
+ scnprintf(arg_desc, sizeof(arg_desc), "%s argument (%s)",
+ ordinal, arg_name);
+ else if (arg_name)
+ scnprintf(arg_desc, sizeof(arg_desc), "argument %s", arg_name);
+ else
+ scnprintf(arg_desc, sizeof(arg_desc), "argument");
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_CALL_TYPE_SAFETY,
+ "invalid call argument");
+ bpf_diag_report_reason(env,
+ "The %s to %s does not satisfy the verifier contract: %s.",
+ arg_desc, call_name, reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "invalid %s for %s", arg_desc, call_name);
+
+ if (regno >= 0)
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *reg_name,
const char *type_name,
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index 0bc5f3492c47..d8c433759dee 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -181,6 +181,10 @@ void bpf_diag_report_resource_state(struct bpf_verifier_env *env,
const char *suggestion);
void bpf_diag_report_ref_leak(struct bpf_verifier_env *env, u32 ref_id,
u32 alloc_insn, u32 fail_insn);
+void bpf_diag_report_call_type(struct bpf_verifier_env *env, u32 insn_idx,
+ int regno, const char *call_name,
+ const char *arg_name, const char *reason,
+ const char *suggestion);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 1c26614bec2b..d2a776d2059c 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -8039,6 +8039,39 @@ static const struct bpf_reg_types *compatible_reg_types[__BPF_ARG_TYPE_MAX] = {
[ARG_PTR_TO_DYNPTR] = &dynptr_types,
};
+static const char *bpf_diag_reg_type_plain(struct bpf_verifier_env *env,
+ enum bpf_reg_type type)
+{
+ switch (base_type(type)) {
+ case NOT_INIT:
+ return "an uninitialized value";
+ case SCALAR_VALUE:
+ return "an integer scalar";
+ case PTR_TO_CTX:
+ return "a context pointer";
+ case PTR_TO_STACK:
+ return "a stack pointer";
+ case PTR_TO_MAP_VALUE:
+ if (type_may_be_null(type))
+ return "a nullable map value pointer";
+ return "a map value pointer";
+ case PTR_TO_MEM:
+ if (type_may_be_null(type))
+ return "a nullable memory pointer";
+ return "a memory pointer";
+ case PTR_TO_BTF_ID:
+ if (type_is_non_owning_ref(type))
+ return "a borrowed allocated object pointer";
+ if (type_is_ptr_alloc_obj(type))
+ return "an owned allocated object pointer";
+ if (type_flag(type) & PTR_UNTRUSTED)
+ return "an untrusted kernel object pointer";
+ return "a kernel object pointer";
+ default:
+ return reg_type_str(env, type);
+ }
+}
+
static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *reg, argno_t argno,
enum bpf_arg_type arg_type,
const u32 *arg_btf_id,
@@ -8092,6 +8125,27 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
for (j = 0; j + 1 < i; j++)
verbose(env, "%s, ", reg_type_str(env, compatible->types[j]));
verbose(env, "%s\n", reg_type_str(env, compatible->types[j]));
+ {
+ char expected_buf[160], reason[256];
+ int len = 0;
+
+ expected_buf[0] = '\0';
+ for (j = 0; j < i; j++)
+ len += scnprintf(expected_buf + len,
+ sizeof(expected_buf) - len,
+ "%s%s", j ? ", " : "",
+ reg_type_str(env, compatible->types[j]));
+
+ scnprintf(reason, sizeof(reason),
+ "it has type %s, but this helper argument accepts %s",
+ bpf_diag_reg_type_plain(env, reg->type),
+ expected_buf);
+ bpf_diag_report_call_type(env, env->insn_idx,
+ reg_from_argno(argno),
+ func_id_name(meta->func_id),
+ reg_arg_name(env, argno), reason,
+ "Pass a value with one of the accepted pointer or scalar types for this helper.");
+ }
return -EACCES;
found:
@@ -8128,6 +8182,12 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
(!type_may_be_null(arg_type) || arg_type_is_release(arg_type))) {
verbose(env, "Possibly NULL pointer passed to helper %s\n",
reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, env->insn_idx,
+ reg_from_argno(argno),
+ func_id_name(meta->func_id),
+ reg_arg_name(env, argno),
+ "the pointer may be NULL, but this helper requires a non-NULL pointer",
+ "Add a NULL check and call the helper only on the non-NULL path.");
return -EACCES;
}
@@ -8476,6 +8536,12 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
!reg_is_referenced(env, reg) && !bpf_register_is_null(reg)) {
verbose(env, "release helper %s expects referenced PTR_TO_BTF_ID passed to %s\n",
func_id_name(meta->func_id), reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_id_name(meta->func_id),
+ reg_arg_name(env, argno),
+ "release helpers require a value that owns a live resource returned by a matching acquire helper",
+ "Pass the resource-owning pointer returned by the matching acquire helper, and avoid calling the release helper after ownership has "
+ "already been transferred or released.");
return -EINVAL;
}
@@ -11560,9 +11626,22 @@ get_kfunc_ptr_arg_type(struct bpf_verifier_env *env, struct bpf_func_state *call
*/
if (!btf_type_is_scalar(ref_t) && !__btf_type_is_scalar_struct(env, meta->btf, ref_t, 0) &&
(arg_mem_size ? !btf_type_is_void(ref_t) : 1)) {
+ char arg_name[32], expected_type[64], reason[256];
+
+ strscpy(arg_name, reg_arg_name(env, argno), sizeof(arg_name));
+ bpf_diag_format_btf_type(expected_type, sizeof(expected_type),
+ ref_t, ref_tname);
verbose(env, "%s pointer type %s %s must point to %sscalar, or struct with scalar\n",
- reg_arg_name(env, argno),
+ arg_name,
btf_type_str(ref_t), ref_tname, arg_mem_size ? "void, " : "");
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects a pointer to %s, but this argument is %s and cannot be used as that kernel object pointer",
+ expected_type,
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, env->insn_idx,
+ reg_from_argno(argno),
+ meta->func_name, arg_name, reason,
+ "Pass a verifier-tracked pointer to the expected kernel object type, not a pointer to stack storage or another memory buffer.");
return -EINVAL;
}
return arg_mem_size ? KF_ARG_PTR_TO_MEM_SIZE : KF_ARG_PTR_TO_MEM;
@@ -12209,7 +12288,18 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
if (btf_type_is_scalar(t)) {
if (reg->type != SCALAR_VALUE) {
+ char reason[256];
+
verbose(env, "%s is not a scalar\n", reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects an integer scalar, but %s is %s",
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass an integer scalar value for this argument, not a pointer or resource object.");
return -EINVAL;
}
@@ -12219,8 +12309,18 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
return -EFAULT;
}
if (!tnum_is_const(reg->var_off)) {
+ char reason[256];
+
verbose(env, "%s must be a known constant\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc requires this scalar argument to be a verifier-known constant, but %s is variable on this path",
+ reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass a compile-time constant or a value the verifier can prove is constant at this call.");
return -EINVAL;
}
if (regno >= 0)
@@ -12245,8 +12345,18 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
}
if (!tnum_is_const(reg->var_off)) {
+ char reason[256];
+
verbose(env, "%s is not a const\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc uses this argument as a return-buffer size, but %s is variable on this path",
+ reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass a verifier-known constant size for this kfunc buffer argument.");
return -EINVAL;
}
@@ -12267,26 +12377,53 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
return -EINVAL;
}
+ ref_t = btf_type_skip_modifiers(btf, t->type, &ref_id);
+ ref_tname = btf_name_by_offset(btf, ref_t->name_off);
+
if ((bpf_register_is_null(reg) || type_may_be_null(reg->type)) &&
!is_kfunc_arg_nullable(meta->btf, &args[i])) {
+ char expected_type[64], reason[256];
+
+ bpf_diag_format_btf_type(expected_type,
+ sizeof(expected_type), ref_t,
+ ref_tname);
+ scnprintf(reason, sizeof(reason),
+ "the pointer may be NULL, but this kfunc requires a non-NULL pointer to %s",
+ expected_type);
verbose(env, "Possibly NULL pointer passed to trusted %s\n",
reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Add a NULL check and call the kfunc only on the non-NULL path.");
return -EACCES;
}
if (regno == meta->release_regno && !is_kfunc_arg_dynptr(meta->btf, &args[i]) &&
!reg_is_referenced(env, reg) && !bpf_register_is_null(reg)) {
+ char expected_type[64], reason[256];
+
+ bpf_diag_format_btf_type(expected_type,
+ sizeof(expected_type), ref_t,
+ ref_tname);
+ scnprintf(reason, sizeof(reason),
+ "release kfuncs require a resource-owning pointer to %s returned by a matching acquire kfunc",
+ expected_type);
verbose(env, "release kfunc %s expects referenced PTR_TO_BTF_ID passed to %s\n",
func_name, reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass the resource-owning pointer returned by the matching acquire kfunc, and avoid calling the release kfunc after ownership has "
+ "already been transferred or released.");
return -EINVAL;
}
if (reg_is_referenced(env, reg))
update_ref_obj(&meta->ref_obj, reg);
- ref_t = btf_type_skip_modifiers(btf, t->type, &ref_id);
- ref_tname = btf_name_by_offset(btf, ref_t->name_off);
-
kf_arg_type = get_kfunc_ptr_arg_type(env, caller, regs, meta, t, ref_t, ref_tname,
args, i, nargs, argno, reg);
if (kf_arg_type < 0)
@@ -12336,13 +12473,44 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
case KF_ARG_PTR_TO_BTF_ID:
if (!is_trusted_reg(env, reg)) {
if (!is_kfunc_rcu(meta)) {
+ char expected_type[64], reason[256];
+
+ bpf_diag_format_btf_type(expected_type,
+ sizeof(expected_type),
+ ref_t, ref_tname);
verbose(env, "%s must be referenced or trusted\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc requires a trusted or resource-owning pointer to %s, but %s is %s",
+ expected_type,
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass a pointer acquired from a verifier-tracked source, or call this kfunc only inside the required protection if it accepts RCU "
+ "pointers.");
return -EINVAL;
}
if (!is_rcu_reg(reg)) {
+ char expected_type[64], reason[256];
+
+ bpf_diag_format_btf_type(expected_type,
+ sizeof(expected_type),
+ ref_t, ref_tname);
verbose(env, "%s must be a rcu pointer\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc requires an RCU-protected pointer to %s, but %s is %s",
+ expected_type,
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Use this kfunc with a pointer that is valid in an RCU read lock region.");
return -EINVAL;
}
}
@@ -12383,8 +12551,19 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
switch (kf_arg_type) {
case KF_ARG_PTR_TO_CTX:
if (reg->type != PTR_TO_CTX) {
+ char reason[256];
+
verbose(env, "%s expected pointer to ctx, but got %s\n",
reg_arg_name(env, argno), reg_type_str(env, reg->type));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects a context pointer, but %s is %s",
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass the original program context pointer or preserve it before modifying registers.");
return -EINVAL;
}
@@ -12409,12 +12588,33 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
return -EINVAL;
}
} else {
+ char reason[256];
+
verbose(env, "%s expected pointer to allocated object\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects an allocated object pointer, but %s is %s",
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass a pointer returned by the matching BPF object allocation path.");
return -EINVAL;
}
if (!reg_is_referenced(env, reg)) {
+ char reason[256];
+
verbose(env, "allocated object must be referenced\n");
+ scnprintf(reason, sizeof(reason),
+ "the allocated object pointer in %s must still carry verifier-tracked ownership, but this pointer no longer owns a live resource",
+ reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass the owned object pointer before it is released or transferred.");
return -EINVAL;
}
if (meta->btf == btf_vmlinux) {
@@ -12580,8 +12780,25 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
return -EINVAL;
}
ret = check_mem_reg(env, reg, argno, type_size);
- if (ret < 0)
+ if (ret < 0) {
+ char arg_name[32], expected_type[64], reason[256];
+
+ strscpy(arg_name, reg_arg_name(env, argno),
+ sizeof(arg_name));
+ bpf_diag_format_btf_type(expected_type,
+ sizeof(expected_type),
+ ref_t, ref_tname);
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects %u bytes of memory for %s, but it is %s and not verifier-known memory",
+ type_size, expected_type,
+ bpf_diag_reg_type_plain(env,
+ reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name, arg_name,
+ reason,
+ "Pass stack, map, context, or other verifier-known memory of the expected type and size, not an integer cast to a pointer.");
return ret;
+ }
break;
case KF_ARG_PTR_TO_MEM_SIZE:
{
@@ -12595,9 +12812,27 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
ret = check_kfunc_mem_size_reg(env, buff_reg, size_reg,
argno, next_argno);
if (ret < 0) {
- verbose(env, "%s and ", reg_arg_name(env, argno));
+ char mem_arg[32], size_arg[32];
+ char reason[256];
+
+ strscpy(mem_arg, reg_arg_name(env, argno),
+ sizeof(mem_arg));
+ strscpy(size_arg, reg_arg_name(env, next_argno),
+ sizeof(size_arg));
+ verbose(env, "%s and ", mem_arg);
verbose(env, "%s memory, len pair leads to invalid memory access\n",
- reg_arg_name(env, next_argno));
+ size_arg);
+ scnprintf(reason, sizeof(reason),
+ "it is the memory pointer in a memory/length pair with %s, but it is %s and does not point to verifier-readable memory for the "
+ "requested length",
+ size_arg,
+ bpf_diag_reg_type_plain(env,
+ buff_reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ mem_arg,
+ reason,
+ "Pass a stack, map, context, or other verifier-known memory pointer, and keep the paired length within that object.");
return ret;
}
}
@@ -12608,8 +12843,19 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
return -EFAULT;
}
if (!tnum_is_const(size_reg->var_off)) {
+ char reason[256];
+
verbose(env, "%s must be a known constant\n",
reg_arg_name(env, next_argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc requires the paired memory size argument %s to be a verifier-known constant, but it is variable on this path",
+ reg_arg_name(env, next_argno));
+ bpf_diag_report_call_type(env, insn_idx,
+ reg_from_argno(next_argno),
+ func_name,
+ reg_arg_name(env, next_argno),
+ reason,
+ "Pass a compile-time constant or verifier-known constant for this memory size argument.");
return -EINVAL;
}
meta->arg_constant.found = true;
@@ -12629,8 +12875,21 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
break;
case KF_ARG_PTR_TO_REFCOUNTED_KPTR:
if (!type_is_ptr_alloc_obj(reg->type)) {
+ char expected_type[64], reason[256];
+
+ bpf_diag_format_btf_type(expected_type,
+ sizeof(expected_type),
+ ref_t, ref_tname);
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects a pointer to BPF-managed refcounted object type %s, but this argument is not such an object pointer",
+ expected_type);
verbose(env, "%s is neither owning or non-owning ref\n",
reg_arg_name(env, argno));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass a pointer returned by the matching BPF object allocation or lookup operation for this kfunc.");
return -EINVAL;
}
if (!type_is_non_owning_ref(reg->type))
@@ -12653,8 +12912,19 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
break;
case KF_ARG_PTR_TO_CONST_STR:
if (reg->type != PTR_TO_MAP_VALUE) {
+ char reason[256];
+
verbose(env, "%s doesn't point to a const string\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects a pointer to a constant string stored in verifier-known memory, but %s is %s",
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass a constant string pointer that the verifier recognizes, such as a string stored in a read-only map value.");
return -EINVAL;
}
ret = check_arg_const_str(env, reg, argno);
@@ -12693,8 +12963,19 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
break;
case KF_ARG_PTR_TO_IRQ_FLAG:
if (reg->type != PTR_TO_STACK) {
+ char reason[256];
+
verbose(env, "%s doesn't point to an irq flag on stack\n",
reg_arg_name(env, argno));
+ scnprintf(reason, sizeof(reason),
+ "the kfunc expects a stack pointer to an IRQ flag slot, but %s is %s",
+ reg_arg_name(env, argno),
+ bpf_diag_reg_type_plain(env, reg->type));
+ bpf_diag_report_call_type(env, insn_idx, regno,
+ func_name,
+ reg_arg_name(env, argno),
+ reason,
+ "Pass the same stack slot used by bpf_local_irq_save() or bpf_res_spin_lock_irqsave().");
return -EINVAL;
}
ret = process_irq_flag(env, reg, argno, meta);
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (10 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:46 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors Kumar Kartikeya Dwivedi
` (4 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected sleepability and critical-section failures with
Execution Context Safety reports. Keep the existing verifier messages
and add source context, path history, and suggestions tied to the
active context.
Use the context history recorded earlier to anchor causal paths to lock,
IRQ, RCU, and preempt regions instead of unrelated register updates.
Cover global calls while holding a lock, sleepable global function calls,
sleepable helpers, sleepable kfunc calls from disallowed contexts,
operations that exit while a context is still active, and unmatched
context exits.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 153 +++++++++++++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 14 ++++
kernel/bpf/verifier.c | 89 +++++++++++++++++++++++
3 files changed, 256 insertions(+)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 169c1a2425c8..2d8417f4efff 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -751,6 +751,159 @@ void bpf_diag_report_call_type(struct bpf_verifier_env *env, u32 insn_idx,
bpf_diag_report_suggestion(env, "%s", suggestion);
}
+static const char *bpf_diag_context_constraint(enum bpf_diag_context_kind kind)
+{
+ switch (kind) {
+ case BPF_DIAG_CONTEXT_RCU:
+ return "RCU read-side critical sections cannot call operations that may sleep";
+ case BPF_DIAG_CONTEXT_PREEMPT:
+ return "preemption-disabled code cannot call operations that may sleep";
+ case BPF_DIAG_CONTEXT_IRQ:
+ return "IRQ-disabled code cannot call operations that may sleep";
+ case BPF_DIAG_CONTEXT_LOCK:
+ return "code holding a BPF spin lock cannot call operations that may sleep";
+ case BPF_DIAG_CONTEXT_NONE:
+ default:
+ return NULL;
+ }
+}
+
+static void bpf_diag_format_active_context(char *buf, size_t size, u32 depth,
+ const char *context)
+{
+ if (depth == 1)
+ scnprintf(buf, size, "an active %s (depth 1)", context);
+ else
+ scnprintf(buf, size, "%u active %ss (depth %u)", depth,
+ context, depth);
+}
+
+static u32 bpf_diag_context_depth(struct bpf_verifier_env *env,
+ enum bpf_diag_context_kind kind)
+{
+ if (!env->cur_state)
+ return 0;
+
+ switch (kind) {
+ case BPF_DIAG_CONTEXT_RCU:
+ return env->cur_state->active_rcu_locks;
+ case BPF_DIAG_CONTEXT_PREEMPT:
+ return env->cur_state->active_preempt_locks;
+ case BPF_DIAG_CONTEXT_IRQ:
+ return env->cur_state->active_irq_id ? 1 : 0;
+ case BPF_DIAG_CONTEXT_LOCK:
+ return env->cur_state->active_locks;
+ case BPF_DIAG_CONTEXT_NONE:
+ default:
+ return 0;
+ }
+}
+
+void bpf_diag_report_execution_context(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *operation,
+ enum bpf_diag_context_kind ctx_kind,
+ const char *context,
+ const char *suggestion)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
+ .ctx_kind = ctx_kind,
+ };
+ u32 depth = bpf_diag_context_depth(env, ctx_kind);
+ char depth_buf[80];
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY,
+ "operation is not allowed in this context");
+ if (bpf_diag_context_constraint(ctx_kind)) {
+ if (depth) {
+ bpf_diag_format_active_context(depth_buf,
+ sizeof(depth_buf),
+ depth, context);
+ bpf_diag_report_reason(env,
+ "The operation %s cannot be used in %s because %s. This path is still inside %s.",
+ operation, context,
+ bpf_diag_context_constraint(ctx_kind),
+ depth_buf);
+ } else {
+ bpf_diag_report_reason(env,
+ "The operation %s cannot be used in %s because %s.",
+ operation, context,
+ bpf_diag_context_constraint(ctx_kind));
+ }
+ } else {
+ bpf_diag_report_reason(env,
+ "The operation %s cannot be used in %s.",
+ operation, context);
+ }
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "%s is not allowed in %s", operation, context);
+
+ if (ctx_kind != BPF_DIAG_CONTEXT_NONE)
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
+void bpf_diag_report_context_still_active(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *operation,
+ enum bpf_diag_context_kind ctx_kind,
+ const char *context,
+ const char *suggestion)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
+ .ctx_kind = ctx_kind,
+ };
+ u32 depth = bpf_diag_context_depth(env, ctx_kind);
+ char depth_buf[80];
+
+ bpf_diag_format_active_context(depth_buf, sizeof(depth_buf), depth,
+ context);
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY,
+ "operation is not allowed in this context");
+ bpf_diag_report_reason(env,
+ "The operation %s cannot be used while this path is still inside %s. Leave the region before this operation.",
+ operation, depth_buf);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "%s is not allowed before leaving %s",
+ operation, context);
+
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
+void bpf_diag_report_context_underflow(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *operation,
+ enum bpf_diag_context_kind ctx_kind,
+ const char *suggestion)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
+ .ctx_kind = ctx_kind,
+ };
+ const char *context = bpf_diag_context_name(ctx_kind);
+
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY,
+ "unmatched context exit");
+ bpf_diag_report_reason(env,
+ "The operation %s tries to leave %s, but this path has no active %s to leave. The current depth is 0.",
+ operation, context, context);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "%s has no matching enter on this path",
+ operation);
+
+ bpf_diag_print_history(env, &opts);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *reg_name,
const char *type_name,
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index d8c433759dee..e11abc30246d 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -185,6 +185,20 @@ void bpf_diag_report_call_type(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *call_name,
const char *arg_name, const char *reason,
const char *suggestion);
+void bpf_diag_report_execution_context(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *operation,
+ enum bpf_diag_context_kind ctx_kind,
+ const char *context,
+ const char *suggestion);
+void bpf_diag_report_context_still_active(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *operation,
+ enum bpf_diag_context_kind ctx_kind,
+ const char *context,
+ const char *suggestion);
+void bpf_diag_report_context_underflow(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *operation,
+ enum bpf_diag_context_kind ctx_kind,
+ const char *suggestion);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index d2a776d2059c..e30d79149cc7 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -211,6 +211,9 @@ static int ref_set_non_owning(struct bpf_verifier_env *env,
static bool is_trusted_reg(struct bpf_verifier_env *env, const struct bpf_reg_state *reg);
static inline bool in_sleepable_context(struct bpf_verifier_env *env);
static const char *non_sleepable_context_description(struct bpf_verifier_env *env);
+static enum bpf_diag_context_kind
+non_sleepable_context_kind(struct bpf_verifier_env *env);
+static const char *non_sleepable_context_diag_description(struct bpf_verifier_env *env);
static void scalar32_min_max_add(struct bpf_reg_state *dst_reg, struct bpf_reg_state *src_reg);
static void scalar_min_max_add(struct bpf_reg_state *dst_reg, struct bpf_reg_state *src_reg);
@@ -9698,16 +9701,31 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
return err;
if (bpf_subprog_is_global(env, subprog)) {
const char *sub_name = subprog_name(env, subprog);
+ char operation[96];
if (env->cur_state->active_locks) {
verbose(env, "global function calls are not allowed while holding a lock,\n"
"use static function instead\n");
+ scnprintf(operation, sizeof(operation),
+ "global function %s()", sub_name);
+ bpf_diag_report_execution_context(env, *insn_idx,
+ operation,
+ BPF_DIAG_CONTEXT_LOCK,
+ "lock region",
+ "Release the lock before calling the global function, or use a static function instead.");
return -EINVAL;
}
if (env->subprog_info[subprog].might_sleep && !in_sleepable_context(env)) {
verbose(env, "sleepable global function %s() called in %s\n",
sub_name, non_sleepable_context_description(env));
+ scnprintf(operation, sizeof(operation),
+ "sleepable global function %s()", sub_name);
+ bpf_diag_report_execution_context(env, *insn_idx,
+ operation,
+ non_sleepable_context_kind(env),
+ non_sleepable_context_diag_description(env),
+ "Move the call outside the critical section, or use a non-sleepable function.");
return -EINVAL;
}
@@ -10285,6 +10303,10 @@ static int check_resource_leak(struct bpf_verifier_env *env, bool exception_exit
if (check_lock && env->cur_state->active_locks) {
verbose(env, "%s cannot be used inside bpf_spin_lock-ed region\n", prefix);
+ bpf_diag_report_context_still_active(env, env->insn_idx, prefix,
+ BPF_DIAG_CONTEXT_LOCK,
+ "lock region",
+ "Release the BPF spin lock before this operation on every path.");
return -EINVAL;
}
@@ -10296,16 +10318,28 @@ static int check_resource_leak(struct bpf_verifier_env *env, bool exception_exit
if (check_lock && env->cur_state->active_irq_id) {
verbose(env, "%s cannot be used inside bpf_local_irq_save-ed region\n", prefix);
+ bpf_diag_report_context_still_active(env, env->insn_idx, prefix,
+ BPF_DIAG_CONTEXT_IRQ,
+ "IRQ-disabled region",
+ "Restore the saved IRQ state before this operation on every path.");
return -EINVAL;
}
if (check_lock && env->cur_state->active_rcu_locks) {
verbose(env, "%s cannot be used inside bpf_rcu_read_lock-ed region\n", prefix);
+ bpf_diag_report_context_still_active(env, env->insn_idx, prefix,
+ BPF_DIAG_CONTEXT_RCU,
+ "RCU read lock region",
+ "Call bpf_rcu_read_unlock() before this operation on every path.");
return -EINVAL;
}
if (check_lock && env->cur_state->active_preempt_locks) {
verbose(env, "%s cannot be used inside bpf_preempt_disable-ed region\n", prefix);
+ bpf_diag_report_context_still_active(env, env->insn_idx, prefix,
+ BPF_DIAG_CONTEXT_PREEMPT,
+ "non-preemptible region",
+ "Call bpf_preempt_enable() before this operation on every path.");
return -EINVAL;
}
@@ -10456,6 +10490,37 @@ static const char *non_sleepable_context_description(struct bpf_verifier_env *en
return "non-sleepable prog";
}
+static enum bpf_diag_context_kind
+non_sleepable_context_kind(struct bpf_verifier_env *env)
+{
+ if (env->cur_state->active_rcu_locks)
+ return BPF_DIAG_CONTEXT_RCU;
+ if (env->cur_state->active_preempt_locks)
+ return BPF_DIAG_CONTEXT_PREEMPT;
+ if (env->cur_state->active_irq_id)
+ return BPF_DIAG_CONTEXT_IRQ;
+ if (env->cur_state->active_locks)
+ return BPF_DIAG_CONTEXT_LOCK;
+ return BPF_DIAG_CONTEXT_NONE;
+}
+
+static const char *non_sleepable_context_diag_description(struct bpf_verifier_env *env)
+{
+ switch (non_sleepable_context_kind(env)) {
+ case BPF_DIAG_CONTEXT_RCU:
+ return "RCU read lock region";
+ case BPF_DIAG_CONTEXT_PREEMPT:
+ return "non-preemptible region";
+ case BPF_DIAG_CONTEXT_IRQ:
+ return "IRQ-disabled region";
+ case BPF_DIAG_CONTEXT_LOCK:
+ return "lock region";
+ case BPF_DIAG_CONTEXT_NONE:
+ default:
+ return "non-sleepable program";
+ }
+}
+
static int release_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
bool convert_rcu, bool release_dynptr)
{
@@ -10484,6 +10549,7 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
enum bpf_type_flag ret_flag;
struct bpf_reg_state *regs;
struct bpf_call_arg_meta meta;
+ char operation[96];
int insn_idx = *insn_idx_p;
bool changes_data;
int i, err, func_id;
@@ -10532,6 +10598,12 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
if (fn->might_sleep && !in_sleepable_context(env)) {
verbose(env, "sleepable helper %s#%d in %s\n", func_id_name(func_id), func_id,
non_sleepable_context_description(env));
+ scnprintf(operation, sizeof(operation), "sleepable helper %s#%d",
+ func_id_name(func_id), func_id);
+ bpf_diag_report_execution_context(env, insn_idx, operation,
+ non_sleepable_context_kind(env),
+ non_sleepable_context_diag_description(env),
+ "Move the helper call outside the critical section, or use a non-sleepable helper.");
return -EINVAL;
}
@@ -13414,6 +13486,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
const struct btf_type *t, *ptr_type;
struct bpf_kfunc_call_arg_meta meta;
struct bpf_insn_aux_data *insn_aux;
+ char operation[96];
int err, insn_idx = *insn_idx_p;
u32 i, nargs, ptr_type_id, id;
const struct btf_param *args;
@@ -13472,6 +13545,12 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
sleepable = bpf_is_kfunc_sleepable(&meta);
if (sleepable && !in_sleepable(env)) {
verbose(env, "program must be sleepable to call sleepable kfunc %s\n", func_name);
+ scnprintf(operation, sizeof(operation), "sleepable kfunc %s",
+ func_name);
+ bpf_diag_report_execution_context(env, insn_idx, operation,
+ BPF_DIAG_CONTEXT_NONE,
+ "non-sleepable program",
+ "Mark the program sleepable if the program type allows it, or use a non-sleepable kfunc.");
return -EACCES;
}
@@ -13533,6 +13612,10 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
} else if (rcu_unlock) {
if (env->cur_state->active_rcu_locks == 0) {
verbose(env, "unmatched rcu read unlock (kernel function %s)\n", func_name);
+ bpf_diag_report_context_underflow(env, insn_idx,
+ func_name,
+ BPF_DIAG_CONTEXT_RCU,
+ "Remove the extra bpf_rcu_read_unlock() call, or ensure this path first enters an RCU read lock region.");
return -EINVAL;
}
env->cur_state->active_rcu_locks--;
@@ -13560,6 +13643,12 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
if (sleepable && !in_sleepable_context(env)) {
verbose(env, "kernel func %s is sleepable within %s\n",
func_name, non_sleepable_context_description(env));
+ scnprintf(operation, sizeof(operation), "sleepable kfunc %s",
+ func_name);
+ bpf_diag_report_execution_context(env, insn_idx, operation,
+ non_sleepable_context_kind(env),
+ non_sleepable_context_diag_description(env),
+ "Move the kfunc call outside the critical section, or use a non-sleepable kfunc.");
return -EACCES;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (11 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:51 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 14/17] bpf: Report Policy helper and kfunc errors Kumar Kartikeya Dwivedi
` (3 subsequent siblings)
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected subprogram CFG validation failures with Program Structure
reports. These errors are structural rather than path-dependent, so the
report focuses on source and instruction context instead of causal history.
Cover jumps that leave the current subprogram and subprograms whose last
instruction can fall through into the next subprogram.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 16 ++++++++++++++++
kernel/bpf/diagnostics.h | 4 ++++
kernel/bpf/verifier.c | 22 ++++++++++++++++++++++
3 files changed, 42 insertions(+)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 2d8417f4efff..24096bb7ca87 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -904,6 +904,22 @@ void bpf_diag_report_context_underflow(struct bpf_verifier_env *env,
bpf_diag_report_suggestion(env, "%s", suggestion);
}
+
+void bpf_diag_report_program_structure(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason,
+ const char *suggestion)
+{
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_PROGRAM_STRUCTURE,
+ problem);
+ bpf_diag_report_reason(env, "%s", reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!', "%s", problem);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *reg_name,
const char *type_name,
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index e11abc30246d..6648a66d0614 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -199,6 +199,10 @@ void bpf_diag_report_context_underflow(struct bpf_verifier_env *env,
u32 insn_idx, const char *operation,
enum bpf_diag_context_kind ctx_kind,
const char *suggestion);
+void bpf_diag_report_program_structure(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason,
+ const char *suggestion);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index e30d79149cc7..325c162e3df0 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -2879,6 +2879,7 @@ static int check_subprogs(struct bpf_verifier_env *env)
int i, subprog_start, subprog_end, off, cur_subprog = 0;
struct bpf_subprog_info *subprog = env->subprog_info;
struct bpf_insn *insn = env->prog->insnsi;
+ char reason[160];
int insn_cnt = env->prog->len;
/* now check that all jumps are within the same subprog */
@@ -2907,6 +2908,15 @@ static int check_subprogs(struct bpf_verifier_env *env)
off = i + bpf_jmp_offset(&insn[i]) + 1;
if (off < subprog_start || off >= subprog_end) {
verbose(env, "jump out of range from insn %d to %d\n", i, off);
+ scnprintf(reason, sizeof(reason),
+ "Instruction %d jumps to instruction %d, but subprogram %d only contains instructions %d through %d. A branch target must stay inside "
+ "the same subprogram",
+ i, off, cur_subprog, subprog_start,
+ subprog_end - 1);
+ bpf_diag_report_program_structure(env, i,
+ "jump out of range",
+ reason,
+ "Keep branch targets within the same subprogram, or use an explicit subprogram call.");
return -EINVAL;
}
next:
@@ -2919,6 +2929,13 @@ static int check_subprogs(struct bpf_verifier_env *env)
code != (BPF_JMP32 | BPF_JA) &&
code != (BPF_JMP | BPF_JA)) {
verbose(env, "last insn is not an exit or jmp\n");
+ scnprintf(reason, sizeof(reason),
+ "Subprogram %d reaches its last instruction %d without an exit or jump, so control could continue into the next subprogram",
+ cur_subprog, i);
+ bpf_diag_report_program_structure(env, i,
+ "subprogram can fall through",
+ reason,
+ "End each subprogram with exit, throw, or an explicit jump that keeps control flow inside the subprogram.");
return -EINVAL;
}
subprog_start = subprog_end;
@@ -2991,6 +3008,11 @@ static int sort_subprogs_topo(struct bpf_verifier_env *env)
verbose(env, "recursive call from %s() to %s()\n",
subprog_name(env, cur),
subprog_name(env, callee));
+ bpf_diag_report_program_structure(env, idx,
+ "recursive subprogram call",
+ "This bpf2bpf call would make the subprogram call graph recursive. The verifier requires a finite, acyclic call graph so it can "
+ "bound stack depth and analysis.",
+ "Rewrite the recursion as an explicit bounded loop, or split the logic so subprogram calls do not form a cycle.");
ret = -EINVAL;
goto out;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 14/17] bpf: Report Policy helper and kfunc errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (12 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 7:02 ` sashiko-bot
2026-06-05 6:34 ` [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors Kumar Kartikeya Dwivedi
` (2 subsequent siblings)
16 siblings, 1 reply; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected helper and kfunc allowability failures with Policy reports.
These reports explain which requested operation is forbidden and why, without
adding path history for non-path-dependent policy checks.
Cover helper program-type restrictions, GPL-only helpers, helper-specific
allow callbacks, kfunc allowability, and destructive kfunc capability checks.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 16 ++++++++++++++++
kernel/bpf/diagnostics.h | 3 +++
kernel/bpf/verifier.c | 32 +++++++++++++++++++++++++++++++-
3 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 24096bb7ca87..8d639b498ed4 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -920,6 +920,22 @@ void bpf_diag_report_program_structure(struct bpf_verifier_env *env,
bpf_diag_report_suggestion(env, "%s", suggestion);
}
+void bpf_diag_report_policy(struct bpf_verifier_env *env, u32 insn_idx,
+ const char *operation, const char *reason,
+ const char *suggestion)
+{
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_POLICY,
+ "operation is not allowed");
+ bpf_diag_report_reason(env, "The operation %s is not allowed: %s.",
+ operation, reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!',
+ "policy check failed for %s", operation);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *reg_name,
const char *type_name,
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index 6648a66d0614..c309778fd2ff 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -203,6 +203,9 @@ void bpf_diag_report_program_structure(struct bpf_verifier_env *env,
u32 insn_idx, const char *problem,
const char *reason,
const char *suggestion);
+void bpf_diag_report_policy(struct bpf_verifier_env *env, u32 insn_idx,
+ const char *operation, const char *reason,
+ const char *suggestion);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 325c162e3df0..f81aad4aeb8e 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -2829,6 +2829,10 @@ static int add_subprog_and_kfunc(struct bpf_verifier_env *env)
if (!env->bpf_capable) {
verbose(env, "loading/calling other bpf or kernel functions are allowed for CAP_BPF and CAP_SYS_ADMIN\n");
+ bpf_diag_report_policy(env, i,
+ "bpf-to-bpf or kernel function call",
+ "loading or calling other BPF or kernel functions requires CAP_BPF or CAP_SYS_ADMIN",
+ "Load this program with the required capability, or avoid bpf-to-bpf and kernel function calls in unprivileged programs.");
return -EPERM;
}
@@ -10587,17 +10591,32 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
if (err) {
verbose(env, "program of this type cannot use helper %s#%d\n",
func_id_name(func_id), func_id);
+ scnprintf(operation, sizeof(operation), "helper %s#%d",
+ func_id_name(func_id), func_id);
+ bpf_diag_report_policy(env, insn_idx, operation,
+ "this program type does not allow the helper",
+ "Use a helper allowed for this program type, or move the logic to a compatible program type.");
return err;
}
/* eBPF programs must be GPL compatible to use GPL-ed functions */
if (!env->prog->gpl_compatible && fn->gpl_only) {
verbose(env, "cannot call GPL-restricted function from non-GPL compatible program\n");
+ scnprintf(operation, sizeof(operation), "helper %s#%d",
+ func_id_name(func_id), func_id);
+ bpf_diag_report_policy(env, insn_idx, operation,
+ "this helper is restricted to GPL-compatible programs",
+ "Use a GPL-compatible license, or replace the helper with one that is available to non-GPL programs.");
return -EINVAL;
}
if (fn->allowed && !fn->allowed(env->prog)) {
verbose(env, "helper call is not allowed in probe\n");
+ scnprintf(operation, sizeof(operation), "helper %s#%d",
+ func_id_name(func_id), func_id);
+ bpf_diag_report_policy(env, insn_idx, operation,
+ "the helper-specific policy callback rejected this program",
+ "Use the helper only from an allowed attach point or program configuration.");
return -EINVAL;
}
@@ -13519,8 +13538,14 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
return 0;
err = bpf_fetch_kfunc_arg_meta(env, insn->imm, insn->off, &meta);
- if (err == -EACCES && meta.func_name)
+ if (err == -EACCES && meta.func_name) {
verbose(env, "calling kernel function %s is not allowed\n", meta.func_name);
+ scnprintf(operation, sizeof(operation), "kfunc %s",
+ meta.func_name);
+ bpf_diag_report_policy(env, insn_idx, operation,
+ "this program cannot call the kfunc",
+ "Use a kfunc allowed for this program type and attach point, or change the program context.");
+ }
if (err)
return err;
desc_btf = meta.btf;
@@ -13561,6 +13586,11 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
if (is_kfunc_destructive(&meta) && !capable(CAP_SYS_BOOT)) {
verbose(env, "destructive kfunc calls require CAP_SYS_BOOT capability\n");
+ scnprintf(operation, sizeof(operation), "destructive kfunc %s",
+ meta.func_name);
+ bpf_diag_report_policy(env, insn_idx, operation,
+ "destructive kfuncs require CAP_SYS_BOOT",
+ "Load the program with CAP_SYS_BOOT, or avoid destructive kfuncs.");
return -EACCES;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (13 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 14/17] bpf: Report Policy helper and kfunc errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 16/17] bpf: Report Verifier Internal errors Kumar Kartikeya Dwivedi
2026-06-05 6:34 ` [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level Kumar Kartikeya Dwivedi
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected verifier limit failures with Verifier Limit reports.
These reports focus on the limit that was exceeded and the observed
value or condition, rather than causal branch history.
Cover tail-call stack constraints, per-subprogram stack depth, combined
call stack depth, static and runtime bpf2bpf call-frame depth, and
processed-instruction complexity.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 16 +++++
kernel/bpf/diagnostics.h | 3 +
kernel/bpf/liveness.c | 6 ++
kernel/bpf/verifier.c | 131 ++++++++++++++++++++++++++++++++++++++-
4 files changed, 155 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 8d639b498ed4..2ca398798b1e 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -936,6 +936,22 @@ void bpf_diag_report_policy(struct bpf_verifier_env *env, u32 insn_idx,
bpf_diag_report_suggestion(env, "%s", suggestion);
}
+void bpf_diag_report_limit(struct bpf_verifier_env *env, u32 insn_idx,
+ const char *limit, const char *reason,
+ const char *suggestion)
+{
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_VERIFIER_LIMIT,
+ "limit exceeded");
+ bpf_diag_report_reason(env, "The %s limit was exceeded: %s.",
+ limit, reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!', "limit exceeded: %s",
+ limit);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *reg_name,
const char *type_name,
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index c309778fd2ff..90a584d5aa0c 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -206,6 +206,9 @@ void bpf_diag_report_program_structure(struct bpf_verifier_env *env,
void bpf_diag_report_policy(struct bpf_verifier_env *env, u32 insn_idx,
const char *operation, const char *reason,
const char *suggestion);
+void bpf_diag_report_limit(struct bpf_verifier_env *env, u32 insn_idx,
+ const char *limit, const char *reason,
+ const char *suggestion);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c
index 0aadfbae0acc..11c50b8cc87f 100644
--- a/kernel/bpf/liveness.c
+++ b/kernel/bpf/liveness.c
@@ -8,6 +8,8 @@
#include <linux/slab.h>
#include <linux/sort.h>
+#include "diagnostics.h"
+
#define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
struct per_frame_masks {
@@ -1862,6 +1864,10 @@ static int analyze_subprog(struct bpf_verifier_env *env,
if (++env->liveness->subprog_calls > 10000) {
verbose(env, "liveness analysis exceeded complexity limit (%d calls)\n",
env->liveness->subprog_calls);
+ bpf_diag_report_limit(env, start,
+ "liveness analysis complexity",
+ "The verifier recomputed subprogram liveness too many times while tracking stack and register reads across call paths.",
+ "Reduce the number of distinct call paths or argument patterns reaching these subprograms.");
return -E2BIG;
}
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index f81aad4aeb8e..c5a3b6876c68 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5219,6 +5219,40 @@ struct bpf_subprog_call_depth_info {
int frame; /* # of consecutive static call stack frames on top of stack */
};
+static void bpf_diag_format_subprog_call_chain(const struct bpf_verifier_env *env,
+ struct bpf_subprog_call_depth_info *dinfo,
+ int idx, char *buf, size_t size)
+{
+ int chain[MAX_CALL_FRAMES + 1];
+ int i, subprog, cnt = 0;
+ size_t len = 0;
+
+ if (!size)
+ return;
+
+ for (subprog = idx; subprog >= 0 && cnt < ARRAY_SIZE(chain);
+ subprog = dinfo[subprog].caller)
+ chain[cnt++] = subprog;
+
+ if (!cnt) {
+ scnprintf(buf, size, "subprogram %d", idx);
+ return;
+ }
+
+ buf[0] = '\0';
+ for (i = cnt - 1; i >= 0 && len < size; i--) {
+ const char *name = subprog_name(env, chain[i]);
+
+ if (name && *name)
+ len += scnprintf(buf + len, size - len, "%s%s",
+ len ? " -> " : "", name);
+ else
+ len += scnprintf(buf + len, size - len,
+ "%ssubprogram %d",
+ len ? " -> " : "", chain[i]);
+ }
+}
+
/* starting from main bpf function walk all instructions of the function
* and recursively walk all callees that given function can call.
* Ignore jump and exit insns.
@@ -5233,6 +5267,8 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
bool tail_call_reachable = false;
int total;
int tmp;
+ char chain[256];
+ char reason[512];
/* no caller idx */
dinfo[idx].caller = -1;
@@ -5264,6 +5300,16 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
verbose(env,
"tail_calls are not allowed when call stack of previous frames is %d bytes. Too large\n",
depth);
+ bpf_diag_format_subprog_call_chain(env, dinfo, idx, chain,
+ sizeof(chain));
+ scnprintf(reason, sizeof(reason),
+ "Call chain %s reaches a subprogram with tail calls after caller frames already use %d bytes; tail-call paths are limited to 256 bytes "
+ "in caller frames",
+ chain, depth);
+ bpf_diag_report_limit(env, subprog[idx].start,
+ "call stack with tail calls",
+ reason,
+ "Reduce stack usage in caller frames, or avoid combining deep bpf2bpf calls with tail calls.");
return -EACCES;
}
@@ -5287,6 +5333,16 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
if (subprog_depth > MAX_BPF_STACK) {
verbose(env, "stack size of subprog %d is %d. Too large\n",
idx, subprog_depth);
+ bpf_diag_format_subprog_call_chain(env, dinfo, idx,
+ chain,
+ sizeof(chain));
+ scnprintf(reason, sizeof(reason),
+ "Call chain %s reaches a subprogram that uses %d bytes of stack, exceeding the %d byte limit for one BPF stack frame",
+ chain, subprog_depth, MAX_BPF_STACK);
+ bpf_diag_report_limit(env, subprog[idx].start,
+ "subprogram stack depth",
+ reason,
+ "Reduce stack usage in this subprogram, or move large data out of the BPF stack.");
return -EACCES;
}
} else {
@@ -5300,13 +5356,23 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
verbose(env, "combined stack size of %d calls is %d. Too large\n",
total, depth);
+ bpf_diag_format_subprog_call_chain(env, dinfo, idx,
+ chain,
+ sizeof(chain));
+ scnprintf(reason, sizeof(reason),
+ "Call chain %s uses %d bytes of stack across %d nested calls, exceeding the %d byte limit",
+ chain, depth, total, MAX_BPF_STACK);
+ bpf_diag_report_limit(env, subprog[idx].start,
+ "combined call stack depth",
+ reason,
+ "Reduce stack usage or call depth along this call chain.");
return -EACCES;
}
}
continue_func:
subprog_end = subprog[idx + 1].start;
for (; i < subprog_end; i++) {
- int next_insn, sidx;
+ int next_insn, call_insn, sidx;
if (bpf_pseudo_kfunc_call(insn + i) && !insn[i].off) {
bool err = false;
@@ -5357,6 +5423,7 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
/* push caller idx into callee's dinfo */
dinfo[sidx].caller = idx;
+ call_insn = i;
i = next_insn;
idx = sidx;
@@ -5370,6 +5437,16 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
if (frame >= MAX_CALL_FRAMES) {
verbose(env, "the call stack of %d frames is too deep !\n",
frame);
+ bpf_diag_format_subprog_call_chain(env, dinfo, idx,
+ chain,
+ sizeof(chain));
+ scnprintf(reason, sizeof(reason),
+ "Call chain %s reaches %d static bpf2bpf call frames, exceeding the %d frame limit",
+ chain, frame, MAX_CALL_FRAMES);
+ bpf_diag_report_limit(env, call_insn,
+ "bpf2bpf call frames",
+ reason,
+ "Reduce the number of nested bpf2bpf calls on this path.");
return -E2BIG;
}
goto process_func;
@@ -9434,6 +9511,43 @@ typedef int (*set_callee_state_fn)(struct bpf_verifier_env *env,
struct bpf_func_state *callee,
int insn_idx);
+static void bpf_diag_format_state_call_chain(const struct bpf_verifier_env *env,
+ const struct bpf_verifier_state *state,
+ int next_subprog, char *buf,
+ size_t size)
+{
+ size_t len = 0;
+ int i;
+
+ if (!size)
+ return;
+
+ buf[0] = '\0';
+ for (i = 0; i <= state->curframe && len < size; i++) {
+ const char *name = subprog_name(env, state->frame[i]->subprogno);
+
+ if (name && *name)
+ len += scnprintf(buf + len, size - len, "%s%s",
+ len ? " -> " : "", name);
+ else
+ len += scnprintf(buf + len, size - len,
+ "%ssubprogram %d",
+ len ? " -> " : "",
+ state->frame[i]->subprogno);
+ }
+
+ if (next_subprog >= 0 && len < size) {
+ const char *name = subprog_name(env, next_subprog);
+
+ if (name && *name)
+ scnprintf(buf + len, size - len, "%s%s",
+ len ? " -> " : "", name);
+ else
+ scnprintf(buf + len, size - len, "%ssubprogram %d",
+ len ? " -> " : "", next_subprog);
+ }
+}
+
static int set_callee_state(struct bpf_verifier_env *env,
struct bpf_func_state *caller,
struct bpf_func_state *callee, int insn_idx);
@@ -9443,11 +9557,22 @@ static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int calls
struct bpf_verifier_state *state)
{
struct bpf_func_state *caller, *callee;
+ char chain[256];
+ char reason[512];
int err;
if (state->curframe + 1 >= MAX_CALL_FRAMES) {
verbose(env, "the call stack of %d frames is too deep\n",
state->curframe + 2);
+ bpf_diag_format_state_call_chain(env, state, subprog, chain,
+ sizeof(chain));
+ scnprintf(reason, sizeof(reason),
+ "Call chain %s would create %d verifier call frames, exceeding the %d frame limit",
+ chain, state->curframe + 2, MAX_CALL_FRAMES);
+ bpf_diag_report_limit(env, callsite,
+ "bpf2bpf call frames",
+ reason,
+ "Reduce the number of nested bpf2bpf calls on this path.");
return -E2BIG;
}
@@ -18099,6 +18224,10 @@ static int do_check(struct bpf_verifier_env *env)
verbose(env,
"BPF program is too large. Processed %d insn\n",
env->insn_processed);
+ bpf_diag_report_limit(env, env->insn_idx,
+ "processed instruction complexity",
+ "The verifier explored more instructions than the complexity limit allows",
+ "Simplify control flow, reduce branching, or split the program into smaller pieces.");
return -E2BIG;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 16/17] bpf: Report Verifier Internal errors
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (14 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:34 ` [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level Kumar Kartikeya Dwivedi
16 siblings, 0 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Augment selected existing internal-error paths with Verifier Internal Error
reports. These reports avoid suggesting source-level fixes and instead
identify the verifier invariant that failed.
Cover non-overwritten BPF_PTR_POISON helper argument types and inconsistent
kfunc graph-node argument classification.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 16 ++++++++++++++++
kernel/bpf/diagnostics.h | 3 +++
kernel/bpf/verifier.c | 12 +++++++++++-
3 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 2ca398798b1e..62cfb0fc3ed0 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -952,6 +952,22 @@ void bpf_diag_report_limit(struct bpf_verifier_env *env, u32 insn_idx,
bpf_diag_report_suggestion(env, "%s", suggestion);
}
+void bpf_diag_report_internal_error(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason)
+{
+ bpf_diag_report_header(env, BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR,
+ problem);
+ bpf_diag_report_reason(env, "The verifier hit an internal error: %s",
+ reason);
+
+ bpf_diag_report_section(env, "At");
+ bpf_diag_report_source(env, insn_idx, '!', "%s", problem);
+
+ bpf_diag_report_suggestion(env,
+ "Report this problem to bpf@vger.kernel.org with the full verifier log and program.");
+}
+
void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
int regno, const char *reg_name,
const char *type_name,
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index 90a584d5aa0c..b075a0cf5568 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -209,6 +209,9 @@ void bpf_diag_report_policy(struct bpf_verifier_env *env, u32 insn_idx,
void bpf_diag_report_limit(struct bpf_verifier_env *env, u32 insn_idx,
const char *limit, const char *reason,
const char *suggestion);
+void bpf_diag_report_internal_error(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason);
void bpf_diag_clear_history(struct bpf_verifier_state *state);
void bpf_diag_copy_history(struct bpf_verifier_state *dst,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c5a3b6876c68..3446ee9acaa1 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -8313,6 +8313,9 @@ static int check_reg_type(struct bpf_verifier_env *env, struct bpf_reg_state *re
verbose(env, "verifier internal error:");
verbose(env, "%s has non-overwritten BPF_PTR_POISON type\n",
reg_arg_name(env, argno));
+ bpf_diag_report_internal_error(env, env->insn_idx,
+ "poisoned helper argument type",
+ "helper type checking reached a non-overwritten BPF_PTR_POISON expected type.");
return -EACCES;
}
@@ -12280,12 +12283,19 @@ static bool check_kfunc_is_graph_node_api(struct bpf_verifier_env *env,
default:
verbose(env, "verifier internal error: unexpected graph node argument type %s\n",
btf_field_type_name(node_field_type));
+ bpf_diag_report_internal_error(env, env->insn_idx,
+ "unexpected graph node argument type",
+ "the kfunc graph-node checker saw an unsupported graph node field type.");
return false;
}
- if (!ret)
+ if (!ret) {
verbose(env, "verifier internal error: %s node arg for unknown kfunc\n",
btf_field_type_name(node_field_type));
+ bpf_diag_report_internal_error(env, env->insn_idx,
+ "graph node argument for unknown kfunc",
+ "the kfunc graph-node checker could not map this graph node argument to the called kfunc.");
+ }
return ret;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
` (15 preceding siblings ...)
2026-06-05 6:34 ` [PATCH bpf-next v1 16/17] bpf: Report Verifier Internal errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:34 ` Kumar Kartikeya Dwivedi
2026-06-05 6:58 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
16 siblings, 2 replies; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 6:34 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, kkd, kernel-team
Verifier diagnostics collect per-path history and render richer reports for
selected verifier failures. That work is useful only when the caller requests
normal verifier log output.
Do not enable diagnostic collection or report rendering for BPF_LOG_STATS-only
loads. Stats-only loads still need the verifier's summary counters, but not
the extra path history used by diagnostic reports.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
include/linux/bpf_verifier.h | 1 +
kernel/bpf/diagnostics.c | 33 +++++++++++++++++++++++++++++++--
kernel/bpf/verifier.c | 1 +
3 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 55921b8a5d5c..ca9a8620955a 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -526,6 +526,7 @@ struct bpf_verifier_state {
struct bpf_diag_history_event *diag_history;
u32 diag_history_cnt;
u32 diag_history_omitted;
+ bool diag_enabled;
u32 dfs_depth;
u32 callback_unroll_depth;
u32 may_goto_depth;
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 62cfb0fc3ed0..8587279ccaa7 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -87,6 +87,11 @@ static void bpf_diag_print_wrapped_text(struct bpf_verifier_env *env,
BPF_DIAG_TEXT_INDENT, text);
}
+static bool bpf_diag_enabled(const struct bpf_verifier_env *env)
+{
+ return env && (env->log.level & BPF_LOG_LEVEL);
+}
+
void bpf_diag_format_btf_type(char *buf, size_t size,
const struct btf_type *type,
const char *type_name)
@@ -120,7 +125,7 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
{
char buf[1024];
- if (!bpf_verifier_log_needed(&env->log))
+ if (!bpf_diag_enabled(env))
return;
vscnprintf(buf, sizeof(buf), fmt, args);
@@ -473,6 +478,9 @@ void bpf_diag_report_header(struct bpf_verifier_env *env,
{
char problem_buf[BPF_DIAG_MSG_LEN];
+ if (!bpf_diag_enabled(env))
+ return;
+
strscpy(problem_buf, problem ?: "", sizeof(problem_buf));
if (problem_buf[0] >= 'a' && problem_buf[0] <= 'z')
problem_buf[0] += 'A' - 'a';
@@ -485,6 +493,9 @@ void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
{
va_list args;
+ if (!bpf_diag_enabled(env))
+ return;
+
bpf_diag_report_section(env, "Reason");
va_start(args, fmt);
@@ -494,6 +505,9 @@ void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title)
{
+ if (!bpf_diag_enabled(env))
+ return;
+
verbose(env, "\n%s:\n", title);
}
@@ -501,6 +515,9 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
{
va_list args;
+ if (!bpf_diag_enabled(env))
+ return;
+
bpf_diag_report_section(env, "Suggestion");
va_start(args, fmt);
@@ -607,6 +624,9 @@ void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
int insn_width, i;
va_list args;
+ if (!bpf_diag_enabled(env))
+ return;
+
va_start(args, fmt);
vscnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
@@ -1228,6 +1248,12 @@ void bpf_diag_copy_history(struct bpf_verifier_state *dst,
{
struct bpf_diag_history_event *history;
+ dst->diag_enabled = src->diag_enabled;
+ if (!src->diag_enabled) {
+ bpf_diag_clear_history(dst);
+ return;
+ }
+
if (!src->diag_history_cnt) {
bpf_diag_clear_history(dst);
dst->diag_history_omitted = src->diag_history_omitted;
@@ -1272,7 +1298,7 @@ static void bpf_diag_append_history(struct bpf_verifier_state *state,
{
struct bpf_diag_history_event *history;
- if (!state)
+ if (!state || !state->diag_enabled)
return;
if (state->diag_history_cnt < BPF_DIAG_HISTORY_MAX) {
@@ -1855,6 +1881,9 @@ void bpf_diag_print_history(struct bpf_verifier_env *env,
int start_idx;
u32 i;
+ if (!bpf_diag_enabled(env))
+ return;
+
bpf_diag_report_section(env, "Causal path");
if (!state)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 3446ee9acaa1..359c4f4c0d42 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -19238,6 +19238,7 @@ static int do_check_common(struct bpf_verifier_env *env, int subprog)
state->speculative = false;
state->branches = 1;
state->in_sleepable = env->prog->sleepable;
+ state->diag_enabled = env->log.level & BPF_LOG_LEVEL;
state->frame[0] = kzalloc_obj(struct bpf_func_state, GFP_KERNEL_ACCOUNT);
if (!state->frame[0]) {
kfree(state);
--
2.53.0
^ permalink raw reply related [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
@ 2026-06-05 6:42 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 18:58 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:42 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] High stack consumption in verifier diagnostic helper functions.
- [Low] Premature line wrapping when a word exactly fits the column width.
--
commit aa805d573ef7f7f7b9f1f0f42ddae783a0fb2690
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Add verifier diagnostics report helpers
Add a small diagnostics renderer for verifier reports and wire it into the
BPF build. The initial helpers emit the common text structure: a failure
header plus reusable report sections.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> new file mode 100644
> index 0000000000000..a6684adc0c313
> --- /dev/null
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> +static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
> + const char *first_prefix,
> + const char *next_prefix,
> + const char *text)
> +{
[ ... ]
> + while (line[len] && line[len] != '\n' && len < text_width) {
> + if (line[len] == ' ')
> + last_space = len;
> + len++;
> + }
> +
> + if (line[len] && line[len] != '\n' && last_space > 0)
> + len = last_space;
[Severity: Low]
Does this cause premature line wrapping when a word exactly fits the column
width?
If line[len] is a space where len equals text_width, the condition
line[len] != '\n' evaluates to true, forcing len to revert to last_space.
This prevents the full word from utilizing the available line width.
[ ... ]
> +static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
> + const char *fmt, va_list args)
> +{
> + char buf[1024];
[Severity: High]
Does this risk a kernel stack overflow?
A 1024-byte local buffer is allocated on the stack here. Coupled with the
512-byte problem_buf in bpf_diag_report_header(), and large allocations in
other diagnostic functions like bpf_diag_report_source() (over 1.3KB) and
bpf_diag_print_source_annotation() (768 bytes), the total stack consumption
in a single call chain can exceed 2KB.
Could these large stack buffers lead to stack overflow panics in the
deeply-nested BPF verifier?
> +
> + if (!bpf_verifier_log_needed(&env->log))
> + return;
> +
> + vscnprintf(buf, sizeof(buf), fmt, args);
> + bpf_diag_print_wrapped_text(env, buf);
> +}
> +
> +void bpf_diag_report_header(struct bpf_verifier_env *env,
> + const char *category, const char *problem)
> +{
> + char problem_buf[BPF_DIAG_MSG_LEN];
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=1
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks
2026-06-05 6:34 ` [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks Kumar Kartikeya Dwivedi
@ 2026-06-05 6:45 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:45 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Incorrect indentation of `bpf_diag_report_resource_state` calls inside `process_irq_flag`.
--
commit 55651d7f9fb67183b2ac504e84a853783f31d0f3
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Resource Lifetime reference leaks
This commit augments the verifier's reference leak detection by adding a
Resource Lifetime Safety report when a reference is still live at program
exit or throw time. It also adds detailed diagnostic reporting for invalid
resource states involving dynptrs, IRQ flags, spin locks, and iterators.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index e9c121892b7e5..1c26614bec2b7 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -11576,6 +11665,11 @@ static int process_irq_flag(struct bpf_verifier_env *env, struct bpf_reg_state *
> if (!is_irq_flag_reg_valid_uninit(env, reg)) {
> verbose(env, "expected uninitialized irq flag as %s\n",
> reg_arg_name(env, argno));
> + bpf_diag_report_resource_state(env, env->insn_idx,
> + "IRQ flag is already initialized",
> + "Saving IRQ state requires an uninitialized stack slot for the IRQ flag, but this slot already contains tracked IRQ flag "
> + "state.",
> + "Use a fresh stack slot for this save operation, or restore the existing IRQ flag before reusing the slot.");
[Severity: Low]
This isn't a bug, but it looks like there's an extra level of indentation
added to the bpf_diag_report_resource_state() call here. Should this be
adjusted to align with the verbose() call above it?
> return -EINVAL;
> }
>
> @@ -11592,6 +11686,10 @@ static int process_irq_flag(struct bpf_verifier_env *env, struct bpf_reg_state *
> if (err) {
> verbose(env, "expected an initialized irq flag as %s\n",
> reg_arg_name(env, argno));
> + bpf_diag_report_resource_state(env, env->insn_idx,
> + "uninitialized IRQ flag restore",
> + "Restoring IRQ state requires a stack slot that was initialized by a matching IRQ save operation on this path.",
> + "Pass the same stack slot that was previously initialized by the matching IRQ save kfunc.");
[Severity: Low]
This isn't a bug, but this call also appears to have an extra level of
indentation.
> return err;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=10
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors
2026-06-05 6:33 ` [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:45 ` sashiko-bot
2026-06-05 7:57 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:45 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Diagnostic message for `BPF_DIAG_MEM_NEGATIVE_MIN` prints a large positive number instead of a negative number when `reg_smin(reg)` is `S64_MIN` and `off` is negative, due to signed 64-bit integer wrap-around.
--
commit 176ba692cb9724623bf456ca5401c93a61c5ea8d
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Memory Safety bounds errors
This commit augments check_mem_region_access() range-proof failures with Memory
Safety reports. The existing terse verifier messages remain in place for
compatibility while the new report adds the failed proof, source context,
causal path, and guidance.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 920ac17758c20..0abe1ab19d252 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> +void bpf_diag_report_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx,
> + int regno, const char *reg_name,
> + const char *type_name,
> + enum bpf_diag_mem_bounds_kind kind,
> + int off, int size, u32 mem_size,
> + const struct bpf_reg_state *reg)
> +{
[ ... ]
> + switch (kind) {
> + case BPF_DIAG_MEM_NEGATIVE_MIN:
> + scnprintf(proof, sizeof(proof),
> + "The smallest possible offset is %lld, below 0",
> + reg_smin(reg) + off);
> + break;
[Severity: Medium]
Will this print a contradictory positive number instead of a negative one when
reg_smin(reg) is S64_MIN and off is negative?
Looking at the caller in check_mem_region_access(), it triggers the
BPF_DIAG_MEM_NEGATIVE_MIN diagnostic if reg_smin(reg) == S64_MIN:
kernel/bpf/verifier.c:check_mem_region_access() {
if (reg_smin(reg) < 0 &&
(reg_smin(reg) == S64_MIN ||
...)) {
bpf_diag_report_mem_bounds(..., BPF_DIAG_MEM_NEGATIVE_MIN, ...);
}
}
If off is negative, reg_smin(reg) + off will underflow S64_MIN and wrap around
to a huge positive number like 9223372036854775807. The diagnostic output might
then read something confusing like:
"The smallest possible offset is 9223372036854775807, below 0"
Unlike the max bounds case which correctly handles wrapping, the min bounds
case appears to evaluate this unconditionally.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=9
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 07/17] bpf: Track verifier context diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 07/17] bpf: Track verifier context " Kumar Kartikeya Dwivedi
@ 2026-06-05 6:46 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:46 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf_diag_history_start_idx anchors context diagnostic history at the innermost `enter` for nested contexts.
- [Medium] bpf_diag_history_start_idx completely suppresses context diagnostic history if the `enter` event is not found.
--
commit 6cd5658b83e6758ee70970a31217f30967606ec4
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Track verifier context diagnostic history
This commit records verifier context transitions in the diagnostic history.
Later reports can then anchor causal paths to the critical section that made
an operation invalid.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 56c1236ffe7ca..7a122c5025c27 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> @@ -893,8 +930,16 @@ static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
> event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE &&
> event->ref.ref_id == opts->ref_id)
> return i - 1;
> + if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT &&
> + event->kind == BPF_DIAG_HISTORY_CONTEXT &&
> + event->ctx.enter &&
> + event->ctx.kind == opts->ctx_kind)
> + return i - 1;
[Severity: Medium]
When traversing backwards, this loop stops at the first enter event it
encounters. For nested contexts, wouldn't this anchor the causal path at the
innermost context entry instead of the outermost entry that actually initiated
the restricted region? Is an event->ctx.depth == 1 check needed here?
> }
>
> + if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT)
> + return state->diag_history_cnt;
> +
[Severity: Medium]
If the original enter event has been pushed out of the fixed-size history
buffer, returning state->diag_history_cnt skips the causal path rendering
entirely.
Looking at bpf_diag_print_history(), it will see start_idx equal to
state->diag_history_cnt and the loop will execute zero times. Furthermore,
because start_idx is not 0, it will bypass the omitted event warning:
start_idx = bpf_diag_history_start_idx(state, opts);
if (state->diag_history_omitted && start_idx == 0)
verbose(env, " ... %u earlier diagnostic events omitted ...
Should this fall through and return 0 instead of state->diag_history_cnt, so
the remaining events are still printed and the omitted warning is triggered?
> return 0;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=7
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors
2026-06-05 6:34 ` [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:46 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:46 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Missing Execution Context Safety diagnostic report for unmatched bpf_preempt_enable().
--
commit fb6e824861cd1088f8d9a8b3acc089f48a5fb892
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Execution Context Safety errors
This patch augments selected sleepability and critical-section failures
with Execution Context Safety reports. It adds source context, path
history, and context-specific suggestions while maintaining the existing
verifier messages.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -12929,26 +13606,49 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
[ ... ]
> } else if (rcu_unlock) {
> if (env->cur_state->active_rcu_locks == 0) {
> verbose(env, "unmatched rcu read unlock (kernel function %s)\n", func_name);
> + bpf_diag_report_context_underflow(env, insn_idx,
> + func_name,
> + BPF_DIAG_CONTEXT_RCU,
> + "Remove the extra bpf_rcu_read_unlock() call, or ensure this path first enters an RCU read lock region.");
> return -EINVAL;
> }
[ ... ]
> } else if (preempt_enable) {
> if (env->cur_state->active_preempt_locks == 0) {
> verbose(env, "unmatched attempt to enable preemption (kernel function %s)\n", func_name);
[Severity: Medium]
The commit message states that it augments "unmatched context exits" with
Execution Context Safety reports. While the patch correctly updates the
handling of unmatched bpf_rcu_read_unlock() calls to invoke
bpf_diag_report_context_underflow(), it misses adding the same diagnostic
reporting for bpf_preempt_enable().
Could we also add a call to bpf_diag_report_context_underflow() here
to standardize the execution context diagnostics as intended?
> return -EINVAL;
> }
> env->cur_state->active_preempt_locks--;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=12
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors
2026-06-05 6:34 ` [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:47 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:47 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf_diag_report_call_type incorrectly uses a register number (regno) instead of an argument ordinal, breaking diagnostic ordinals and dropping causal history tracking for stack arguments.
- [Medium] bpf_diag_reg_type_plain completely omits the 'nullable' qualifier for PTR_TO_BTF_ID types, leading to highly confusing type-mismatch errors.
--
commit d36f4b9fea5180f51f3ba1c10068790e858fb646
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Call Type Safety argument errors
This commit augments selected helper and kfunc argument-contract failures
with Call Type Safety reports. It adds reason, source context, causal register
history, and targeted suggestions to the verifier messages. The changes cover
helper register-type mismatch, nullability requirements, release kfunc
requirements, and memory arguments.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 369b7813b5dcf..169c1a2425c8c 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> @@ -714,6 +714,43 @@ static const char *bpf_diag_arg_ordinal(int argno)
> }
> }
>
> +void bpf_diag_report_call_type(struct bpf_verifier_env *env, u32 insn_idx,
> + int regno, const char *call_name,
> + const char *arg_name, const char *reason,
> + const char *suggestion)
> +{
> + struct bpf_diag_history_opts opts = {
> + .scope = BPF_DIAG_HISTORY_SCOPE_REG,
> + .frameno = bpf_diag_current_frameno(env),
> + .regno = regno,
> + };
> + const char *ordinal = bpf_diag_arg_ordinal(regno);
[Severity: Medium]
Will this return NULL and drop the ordinal string for stack arguments?
For arguments > 5, reg_from_argno() returns -1, which is passed here as
regno. Calling bpf_diag_arg_ordinal(-1) returns NULL, even though it appears
designed to handle up to 12 arguments.
Should we be passing argno instead of regno to bpf_diag_report_call_type(),
or at least passing argno directly to bpf_diag_arg_ordinal()?
> + char arg_desc[48];
> +
> + if (ordinal && arg_name)
> + scnprintf(arg_desc, sizeof(arg_desc), "%s argument (%s)",
> + ordinal, arg_name);
> + else if (arg_name)
> + scnprintf(arg_desc, sizeof(arg_desc), "argument %s", arg_name);
> + else
> + scnprintf(arg_desc, sizeof(arg_desc), "argument");
> +
> + bpf_diag_report_header(env, BPF_DIAG_CATEGORY_CALL_TYPE_SAFETY,
> + "invalid call argument");
> + bpf_diag_report_reason(env,
> + "The %s to %s does not satisfy the verifier contract: %s.",
> + arg_desc, call_name, reason);
> +
> + bpf_diag_report_section(env, "At");
> + bpf_diag_report_source(env, insn_idx, '!',
> + "invalid %s for %s", arg_desc, call_name);
> +
> + if (regno >= 0)
> + bpf_diag_print_history(env, &opts);
[Severity: Medium]
Does this silently bypass causal history tracking for stack arguments?
Because regno evaluates to -1 for arguments > 5, this check skips printing
the history. Is it intentional to drop history tracking for stack arguments,
or can we add support to track their history?
> +
> + bpf_diag_report_suggestion(env, "%s", suggestion);
> +}
> +
> void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
> int regno, const char *reg_name,
> const char *type_name,
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 1c26614bec2b7..d2a776d2059cc 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -8039,6 +8039,39 @@ static const struct bpf_reg_types *compatible_reg_types[__BPF_ARG_TYPE_MAX] = {
> [ARG_PTR_TO_DYNPTR] = &dynptr_types,
> };
>
> +static const char *bpf_diag_reg_type_plain(struct bpf_verifier_env *env,
> + enum bpf_reg_type type)
> +{
> + switch (base_type(type)) {
> + case NOT_INIT:
> + return "an uninitialized value";
> + case SCALAR_VALUE:
> + return "an integer scalar";
> + case PTR_TO_CTX:
> + return "a context pointer";
> + case PTR_TO_STACK:
> + return "a stack pointer";
> + case PTR_TO_MAP_VALUE:
> + if (type_may_be_null(type))
> + return "a nullable map value pointer";
> + return "a map value pointer";
> + case PTR_TO_MEM:
> + if (type_may_be_null(type))
> + return "a nullable memory pointer";
> + return "a memory pointer";
> + case PTR_TO_BTF_ID:
> + if (type_is_non_owning_ref(type))
> + return "a borrowed allocated object pointer";
[Severity: Medium]
Should we be checking type_may_be_null(type) for PTR_TO_BTF_ID types here?
PTR_TO_MAP_VALUE and PTR_TO_MEM correctly identify nullable pointers above,
but PTR_TO_BTF_ID omits the check.
When a nullable PTR_TO_BTF_ID is passed to a helper expecting a non-nullable
pointer, it fails the type check in check_reg_type(), but the error message
will drop the "nullable" qualifier. This causes the verifier to report "it
has type a kernel object pointer, but this helper argument accepts...", making
the actual cause of the rejection (nullability) very confusing for users.
> + if (type_is_ptr_alloc_obj(type))
> + return "an owned allocated object pointer";
> + if (type_flag(type) & PTR_UNTRUSTED)
> + return "an untrusted kernel object pointer";
> + return "a kernel object pointer";
> + default:
> + return reg_type_str(env, type);
> + }
> +}
> +
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=11
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history Kumar Kartikeya Dwivedi
@ 2026-06-05 6:50 ` sashiko-bot
2026-06-05 7:57 ` bot+bpf-ci
2026-06-05 21:37 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:50 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Massive memory bloat due to copying diag_history into cached explored_states
--
commit 1c9483e0ca38f0733322ac30906596c49007b4eb
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Track verifier branch diagnostic history
This patch adds bounded diagnostic history to verifier states. It records
conditional branch outcomes to help show the causal path that reached a
verifier error when user-facing diagnostics are printed.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -1624,6 +1626,7 @@ int bpf_copy_verifier_state(struct bpf_verifier_state *dst_state,
> if (!dst_state->jmp_history)
> return -ENOMEM;
> dst_state->jmp_history_cnt = src->jmp_history_cnt;
> + bpf_diag_copy_history(dst_state, src);
>
> /* if dst has more stack frames then src frame, free them, this is also
> * necessary in case of exceptional exits using bpf_throw.
[Severity: High]
Does this cause massive memory bloat when verifier logging is enabled?
Because is_state_visited() caches states by calling bpf_copy_verifier_state()
and adding them to the explored_states list, the diagnostic history is copied
for every cached state.
Since the diagnostic history can grow up to 1024 entries per state, caching
this for tens of thousands of explored states could easily lead to gigabytes
of wasted memory (hitting memcg limits via GFP_KERNEL_ACCOUNT or causing
-ENOMEM) specifically when a user enables logging to debug an issue.
Because cached states are only used for state equivalence checks and precision
propagation, neither of which use the diagnostic history, could we avoid
copying the diag_history into cached explored states?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=4
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors
2026-06-05 6:34 ` [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:51 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:51 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The `reason` stack buffer in `check_subprogs()` is undersized and will truncate the diagnostic message for moderately sized BPF programs.
- [Medium] The newly added diagnostic suggestion explicitly advises users to use `throw` to end subprograms, contradicting the verifier's actual enforcement logic.
--
commit 18889a5137c9a55cdba7f071532572199ed7120c
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Program Structure CFG errors
This commit augments selected subprogram CFG validation failures with
Program Structure reports. It covers jumps leaving the current subprogram
and subprograms falling through into the next.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index e30d79149cc7d..325c162e3df0d 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -2879,6 +2879,7 @@ static int check_subprogs(struct bpf_verifier_env *env)
> int i, subprog_start, subprog_end, off, cur_subprog = 0;
> struct bpf_subprog_info *subprog = env->subprog_info;
> struct bpf_insn *insn = env->prog->insnsi;
> + char reason[160];
> int insn_cnt = env->prog->len;
>
> /* now check that all jumps are within the same subprog */
> @@ -2907,6 +2908,15 @@ static int check_subprogs(struct bpf_verifier_env *env)
> off = i + bpf_jmp_offset(&insn[i]) + 1;
> if (off < subprog_start || off >= subprog_end) {
> verbose(env, "jump out of range from insn %d to %d\n", i, off);
> + scnprintf(reason, sizeof(reason),
> + "Instruction %d jumps to instruction %d, but subprogram %d only contains instructions %d through %d. A branch target must stay inside "
> + "the same subprogram",
> + i, off, cur_subprog, subprog_start,
> + subprog_end - 1);
[Severity: Medium]
Does this code risk truncating the diagnostic message?
The format string contains 142 characters of static text and accepts 5 integer
arguments. Because BPF programs can contain up to 1,000,000 instructions,
instruction indices can take up to 7 digits.
Formatting these integers can add up to 31 characters, resulting in a string
length of around 173 bytes.
Since the reason buffer is limited to 160 bytes, scnprintf() will silently
truncate the output and the user will see a cut-off diagnostic message.
> + bpf_diag_report_program_structure(env, i,
> + "jump out of range",
> + reason,
> + "Keep branch targets within the same subprogram, or use an explicit subprogram call.");
> return -EINVAL;
> }
> next:
> @@ -2919,6 +2929,13 @@ static int check_subprogs(struct bpf_verifier_env *env)
> code != (BPF_JMP32 | BPF_JA) &&
> code != (BPF_JMP | BPF_JA)) {
> verbose(env, "last insn is not an exit or jmp\n");
> + scnprintf(reason, sizeof(reason),
> + "Subprogram %d reaches its last instruction %d without an exit or jump, so control could continue into the next subprogram",
> + cur_subprog, i);
> + bpf_diag_report_program_structure(env, i,
> + "subprogram can fall through",
> + reason,
> + "End each subprogram with exit, throw, or an explicit jump that keeps control flow inside the subprogram.");
[Severity: Medium]
Will this suggestion mislead users trying to end their subprogram with a throw?
The newly added suggestion advises users to use throw to end subprograms.
However, the immediately preceding logic in check_subprogs() verifies the final
instruction using:
if (code != (BPF_JMP | BPF_EXIT) &&
code != (BPF_JMP32 | BPF_JA) &&
code != (BPF_JMP | BPF_JA)) {
This condition does not account for bpf_throw(), which is a kfunc and evaluates
as a BPF_JMP | BPF_CALL instruction.
If a user writes assembly following the newly provided suggestion and places a
bpf_throw call as the absolute end of their subprogram, won't the verifier
still reject it and print this exact error?
> return -EINVAL;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=13
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 05/17] bpf: Track verifier register diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 05/17] bpf: Track verifier register " Kumar Kartikeya Dwivedi
@ 2026-06-05 6:53 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 22:31 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:53 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Excessive stack allocation in diagnostic formatting functions can cause kernel stack overflows.
- [Medium] Uninitialized stack memory read in check_stack_arg_write() due to uninitialized old_arg.
- [Medium] Diagnostic history records incorrect frame numbers for registers invalidated in caller frames.
--
commit 9a17934339f89b801d41d6242f00c9032267ab0f
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Track verifier register diagnostic history
Record material register and outgoing stack argument changes so diagnostics can
explain how a value reached its current type, bounds, or unreadable state.
Store old and new register types, scalar ranges, tnum value and mask, map and
BTF type identity, and basic operand metadata.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 50cc50a394cae..1bdc7e69fbced 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> +static void bpf_diag_record_reg_mod_reason(struct bpf_verifier_state *state,
> + u32 insn_idx, u8 dst_reg,
> + bool src_valid, u8 src_reg,
> + u8 opcode,
> + enum bpf_diag_reg_mod_reason reason,
> + const struct bpf_reg_state *old_reg,
> + const struct bpf_reg_state *new_reg)
> +{
> + struct bpf_diag_history_event event = {
> + .insn_idx = insn_idx,
> + .kind = BPF_DIAG_HISTORY_REG_MOD,
> + .reg.dst_reg = dst_reg,
> + .reg.src_reg = src_reg,
> + .reg.opcode = opcode,
> + .reg.src_valid = src_valid,
> + };
> +
> + if (state && state->frame[state->curframe])
> + event.reg.frameno = state->frame[state->curframe]->frameno;
[Severity: Medium]
Will this record the wrong frame number for registers invalidated in caller
frames?
Functions like clear_all_pkt_pointers() and release_reference() iterate over
all active frames to invalidate registers, but this assignment hardcodes the
current executing frame number via state->curframe.
Should bpf_diag_record_reg_mod_reason() accept the actual frame number of the
register from the caller instead?
[ ... ]
> +static void bpf_diag_format_reg_snapshot(struct bpf_verifier_env *env, char *buf,
> + size_t size,
> + const struct bpf_diag_reg_snapshot *snapshot)
> +{
> + const char *type_name = reg_type_str(env, snapshot->type);
> + char offset_desc[BPF_DIAG_REG_DESC_LEN];
> + char btf_type[BPF_DIAG_REG_DESC_LEN];
[Severity: High]
Could this cause a kernel stack overflow?
BPF_DIAG_REG_DESC_LEN is 512 bytes. This function allocates 1KB on the stack,
and is called by bpf_diag_print_reg_mod() which also allocates 1KB.
Additionally, this function calls bpf_diag_format_var_offset() which
allocates another 512 bytes. This >3KB stack usage chain might trigger
CONFIG_FRAME_WARN build failures or cause panics on architectures with 8KB
stacks.
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index d60e1309957a7..188201d48108c 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -4033,6 +4033,8 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
> struct bpf_subprog_info *subprog = &env->subprog_info[state->subprogno];
> int spi = -off / BPF_REG_SIZE - 1;
> struct bpf_reg_state *arg;
> + struct bpf_reg_state old_arg;
> + bool slot_exists;
> int err;
>
> if (spi >= max_stack_arg_regs) {
> @@ -4041,6 +4043,7 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
> return -EINVAL;
> }
>
> + slot_exists = spi < state->out_stack_arg_cnt;
> err = grow_stack_arg_slots(env, state, spi + 1);
> if (err)
> return err;
> @@ -4049,13 +4052,27 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
> if (spi + 1 > subprog->max_out_stack_arg_cnt)
> subprog->max_out_stack_arg_cnt = spi + 1;
>
> + arg = &state->stack_arg_regs[spi];
> + if (slot_exists)
> + old_arg = *arg;
> + else
> + bpf_mark_reg_not_init(env, &old_arg);
[Severity: Medium]
Does this pass uninitialized stack memory to bpf_mark_reg_not_init()?
When slot_exists is false, old_arg is declared without initialization.
bpf_mark_reg_not_init() explicitly reads reg->subreg_def into a local
variable before clearing the struct, which could trigger a KMSAN
use-of-uninitialized-value warning.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=5
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors
2026-06-05 6:34 ` [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:53 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:53 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Large local character arrays (`chain[256]` and `reason[512]`) are allocated on the stack in verifier functions, risking kernel stack overflow.
--
commit 58bfdd96949c20f24a4355e1c77d8bbbd610ef4d
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Verifier Limit errors
This commit augments selected verifier limit failures with detailed reports.
These reports focus on the limit that was exceeded and the observed value or
condition across stack constraints, call frames, and instruction complexity.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -5233,6 +5267,8 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
> bool tail_call_reachable = false;
> int total;
> int tmp;
> + char chain[256];
> + char reason[512];
[Severity: Medium]
Could this cause kernel stack overflows? Although these variables are only
populated in slow error-reporting paths, C compilers typically allocate
local variables in the function's prologue.
This adds 768 bytes to the stack frame. Since this is only used on failure
paths, could these strings be dynamically allocated (e.g., using kasprintf)
to avoid bloating the stack?
[ ... ]
> @@ -9443,11 +9557,22 @@ static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int calls
> struct bpf_verifier_state *state)
> {
> struct bpf_func_state *caller, *callee;
> + char chain[256];
> + char reason[512];
> int err;
[Severity: Medium]
Does this present a similar stack bloat risk? setup_func_entry() is invoked
deep within the verifier's call stack, for example:
do_check() -> do_check_insn() -> check_call() -> set_callee_state() ->
setup_func_entry()
Adding an additional ~800-byte frame size expansion here might trigger frame
warnings or kernel stack exhaustion.
> if (state->curframe + 1 >= MAX_CALL_FRAMES) {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=15
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors
2026-06-05 6:33 ` [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors Kumar Kartikeya Dwivedi
@ 2026-06-05 6:57 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:57 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] Diagnostic reports for pointer arithmetic incorrectly target the scalar register instead of the pointer register.
- [Medium] Unconditional history dumping when regno is -1 floods the verifier log with all branch events.
- [Medium] Invalidation of spilled registers breaks the diagnostic causal path.
--
commit 5519f021558a88153013c28db08b650a5c841f07
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Register Type Safety errors
Augment selected register-state verifier failures with Register Type Safety
reports. The existing verbose verifier messages remain in place, while the
new reports add reason, source context, causal path, and suggestions.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 7a122c5025c27..920ac17758c20 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> @@ -648,6 +658,157 @@ void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
> }
> }
>
> +void bpf_diag_report_register_type(struct bpf_verifier_env *env,
> + u32 insn_idx, int regno,
> + const char *problem, const char *reason,
> + const char *suggestion)
> +{
> + struct bpf_diag_history_opts opts = {
> + .scope = BPF_DIAG_HISTORY_SCOPE_REG,
> + .frameno = bpf_diag_current_frameno(env),
> + .regno = regno,
> + };
[ ... ]
> +void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
> + int regno, const char *reg_name,
> + const char *type_name,
> + enum bpf_diag_invalid_deref_kind kind,
> + s64 offset)
> +{
> + struct bpf_diag_history_opts opts = {
> + .scope = BPF_DIAG_HISTORY_SCOPE_REG,
> + .frameno = bpf_diag_current_frameno(env),
> + .regno = regno,
> + };
> +
> + bpf_diag_report_header(env, BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
> + "invalid dereference");
> +
[ ... ]
> + bpf_diag_report_section(env, "At");
> + if (kind == BPF_DIAG_DEREF_MODIFIED_PTR)
> + bpf_diag_report_source(env, insn_idx, '!',
> + "dereference requires the original %s pointer",
> + type_name);
> + else
> + bpf_diag_report_source(env, insn_idx, '!',
> + "invalid dereference of %s (%s)",
> + reg_name, type_name);
> +
> + bpf_diag_print_history(env, &opts);
[Severity: Medium]
Does this unconditionally dump the history when regno is -1?
If regno is -1 (for example, from reg_from_argno for a stack argument),
bpf_diag_history_start_idx() will fail to find a match since dst_reg is a u8.
This causes it to return index 0.
Because bpf_diag_history_event_visible() returns true for all branch events,
this will inadvertently flood the verifier log with the entire branch history
of the program.
Could we add a check for regno >= 0 before printing the history, similar
to the check in bpf_diag_report_register_type()?
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 7bb2a81ee3535..adf660ab9e7d4 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -13684,6 +13712,7 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
> struct bpf_sanitize_info info = {};
> u8 opcode = BPF_OP(insn->code);
> u32 dst = insn->dst_reg;
> + char reason[256];
> int ret, bounds_ret;
>
> dst_reg = ®s[dst];
> @@ -13707,12 +13736,28 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
> verbose(env,
> "R%d 32-bit pointer arithmetic prohibited\n",
> dst);
> + scnprintf(reason, sizeof(reason),
> + "R%d holds %s. 32-bit ALU operations on pointers discard pointer provenance, so the verifier cannot keep the result as a safe "
> + "pointer.",
> + dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
> + bpf_diag_report_register_type(env, env->insn_idx, dst,
[Severity: Medium]
Will this diagnostic report target the scalar register instead of the pointer?
For commutative pointer arithmetic operations like scalar += pointer,
adjust_ptr_min_max_vals() uses insn->dst_reg. Because dst_reg held the scalar
value before the instruction, passing dst here causes the message to falsely
claim the scalar register held a pointer.
This also traces the scalar's history rather than the pointer's provenance,
which could produce misleading output.
[ ... ]
[Severity: Medium]
Looking at the register invalidation logic in clear_all_pkt_pointers() and
release_reference():
if (regno >= 0)
bpf_diag_record_reg_invalidate(...);
else if (slot >= 0)
bpf_diag_record_stack_arg(...);
Are spilled registers silently omitted from the diagnostic history?
When iterating over all registers, bpf_diag_func_regno() and
bpf_diag_stack_arg_slot() evaluate to -1 for standard stack spills.
If a program later restores and dereferences a pointer that was spilled
during invalidation, the verifier trace will stop at the stack fill
instruction. This breaks the causal path, failing to show when and why
the pointer was actually invalidated.
Does this need to track stack spills to maintain the diagnostic history?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=8
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level
2026-06-05 6:34 ` [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level Kumar Kartikeya Dwivedi
@ 2026-06-05 6:58 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 6:58 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The `push_async_cb()` function fails to initialize `elem->st.diag_enabled`, inadvertently disabling verifier diagnostic history for asynchronous callbacks.
--
commit a1006ba17aa95830cb0240bca35f5e001fb80ed7
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Gate verifier diagnostics on log level
This commit conditionally enables verifier diagnostic collection and
reporting based on the requested log level. It avoids collecting path
history for BPF_LOG_STATS-only loads, saving resources when diagnostics
are not requested.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 3446ee9acaa1b..359c4f4c0d422 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -19238,6 +19238,7 @@ static int do_check_common(struct bpf_verifier_env *env, int subprog)
> state->speculative = false;
> state->branches = 1;
> state->in_sleepable = env->prog->sleepable;
> + state->diag_enabled = env->log.level & BPF_LOG_LEVEL;
> state->frame[0] = kzalloc_obj(struct bpf_func_state, GFP_KERNEL_ACCOUNT);
[Severity: Medium]
Since push_async_cb() also sets up a fresh verifier state without copying the
caller's state, should it initialize elem->st.diag_enabled as well?
Currently, push_async_cb() initializes the state like this:
/* Unlike push_stack() do not bpf_copy_verifier_state().
* The caller state doesn't matter.
* This is async callback. It starts in a fresh stack.
* Initialize it similar to do_check_common().
*/
elem->st.branches = 1;
elem->st.in_sleepable = is_sleepable;
frame = kzalloc_obj(*frame, GFP_KERNEL_ACCOUNT);
Because push_async_cb() leaves diag_enabled initialized to false, it seems
diagnostic tracking will be inadvertently disabled for asynchronous callbacks.
If a verification error occurs there, will the verifier misleadingly print
"no recorded diagnostic events on this path"?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=17
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 14/17] bpf: Report Policy helper and kfunc errors
2026-06-05 6:34 ` [PATCH bpf-next v1 14/17] bpf: Report Policy helper and kfunc errors Kumar Kartikeya Dwivedi
@ 2026-06-05 7:02 ` sashiko-bot
0 siblings, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 7:02 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Out-of-bounds read in `bpf_diag_func_name` when formatting verifier diagnostics.
--
commit 2af8da104307236e989f2e90416035f301412857
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Report Policy helper and kfunc errors
Augment selected helper and kfunc allowability failures with Policy reports.
These reports explain which requested operation is forbidden and why, without
adding path history for non-path-dependent policy checks.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 325c162e3df0d..f81aad4aeb8e1 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -2829,6 +2829,10 @@ static int add_subprog_and_kfunc(struct bpf_verifier_env *env)
>
> if (!env->bpf_capable) {
> verbose(env, "loading/calling other bpf or kernel functions are allowed for CAP_BPF and CAP_SYS_ADMIN\n");
> + bpf_diag_report_policy(env, i,
> + "bpf-to-bpf or kernel function call",
> + "loading or calling other BPF or kernel functions requires CAP_BPF or CAP_SYS_ADMIN",
> + "Load this program with the required capability, or avoid bpf-to-bpf and kernel function calls in unprivileged programs.");
> return -EPERM;
> }
[Severity: High]
This isn't a bug introduced by this patch, but was introduced earlier in this
patch series. When a user provides a BPF program with attr->func_info_cnt
smaller than the actual number of subprograms, env->prog->aux->func_info is
allocated with a size smaller than env->subprog_cnt.
If the diagnostic framework is invoked early (for example, here in
add_subprog_and_kfunc(), or during check_subprogs()), it can trigger an
out-of-bounds read.
bpf_diag_func_name() calculates the subprogno and accesses the array without
checking if it falls within func_info_cnt:
kernel/bpf/diagnostics.c:bpf_diag_func_name() {
...
subprogno = subprog - env->subprog_info;
info = &env->prog->aux->func_info[subprogno];
...
}
Since the validation enforcing func_info_cnt == env->subprog_cnt happens
later in check_btf_func(), can this allow a malformed program to trigger
an out-of-bounds heap read before validation occurs?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=14
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks
2026-06-05 6:34 ` [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks Kumar Kartikeya Dwivedi
2026-06-05 6:45 ` sashiko-bot
@ 2026-06-05 7:22 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:22 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1853 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
[ ... ]
> @@ -11576,6 +11665,11 @@ static int process_irq_flag(struct bpf_verifier_env *env, struct bpf_reg_state *
> if (!is_irq_flag_reg_valid_uninit(env, reg)) {
> verbose(env, "expected uninitialized irq flag as %s\n",
> reg_arg_name(env, argno));
> + bpf_diag_report_resource_state(env, env->insn_idx,
This isn't a bug, but the indentation looks off here. The
bpf_diag_report_resource_state() call has an extra level of indentation
that doesn't match the verbose() call above it. This same issue appears
in both places where bpf_diag_report_resource_state() is added to this
function (at the save validation and restore validation).
Should these be adjusted to align with the verbose() calls?
Reference: https://lore.kernel.org/bpf/20260605064536.DE3F51F00893@smtp.kernel.org
> + "IRQ flag is already initialized",
[ ... ]
> @@ -11592,6 +11686,10 @@ static int process_irq_flag(struct bpf_verifier_env *env, struct bpf_reg_state *
> if (err) {
> verbose(env, "expected an initialized irq flag as %s\n",
> reg_arg_name(env, argno));
> + bpf_diag_report_resource_state(env, env->insn_idx,
> + "uninitialized IRQ flag restore",
Same indentation issue here.
> + "Restoring IRQ state requires a stack slot that was initialized by a matching IRQ save operation on this path.",
> + "Pass the same stack slot that was previously initialized by the matching IRQ save kfunc.");
[ ... ]
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors
2026-06-05 6:34 ` [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors Kumar Kartikeya Dwivedi
2026-06-05 6:51 ` sashiko-bot
@ 2026-06-05 7:22 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:22 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3633 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 2d8417f4efff..24096bb7ca87 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index e30d79149cc7..325c162e3df0 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -2879,6 +2879,7 @@ static int check_subprogs(struct bpf_verifier_env *env)
> int i, subprog_start, subprog_end, off, cur_subprog = 0;
> struct bpf_subprog_info *subprog = env->subprog_info;
> struct bpf_insn *insn = env->prog->insnsi;
> + char reason[160];
The format string contains 142 characters of static text and accepts 5
integer arguments. Because BPF programs can contain up to 1,000,000
instructions, instruction indices can take up to 7 digits. Formatting these
integers can add up to 31 characters, resulting in a string length of around
173 bytes. Since the reason buffer is limited to 160 bytes, scnprintf() will
silently truncate the output and the user will see a cut-off diagnostic
message.
Reference: https://lore.kernel.org/bpf/20260605065130.BB6CA1F00893@smtp.kernel.org/
> int insn_cnt = env->prog->len;
>
> /* now check that all jumps are within the same subprog */
> @@ -2907,6 +2908,15 @@ static int check_subprogs(struct bpf_verifier_env *env)
> off = i + bpf_jmp_offset(&insn[i]) + 1;
> if (off < subprog_start || off >= subprog_end) {
> verbose(env, "jump out of range from insn %d to %d\n", i, off);
> + scnprintf(reason, sizeof(reason),
> + "Instruction %d jumps to instruction %d, but subprogram %d only contains instructions %d through %d. A branch target must stay inside "
> + "the same subprogram",
> + i, off, cur_subprog, subprog_start,
> + subprog_end - 1);
> + bpf_diag_report_program_structure(env, i,
> + "jump out of range",
> + reason,
> + "Keep branch targets within the same subprogram, or use an explicit subprogram call.");
> return -EINVAL;
> }
> next:
> @@ -2919,6 +2929,13 @@ static int check_subprogs(struct bpf_verifier_env *env)
> code != (BPF_JMP32 | BPF_JA) &&
> code != (BPF_JMP | BPF_JA)) {
> verbose(env, "last insn is not an exit or jmp\n");
> + scnprintf(reason, sizeof(reason),
> + "Subprogram %d reaches its last instruction %d without an exit or jump, so control could continue into the next subprogram",
> + cur_subprog, i);
> + bpf_diag_report_program_structure(env, i,
> + "subprogram can fall through",
> + reason,
> + "End each subprogram with exit, throw, or an explicit jump that keeps control flow inside the subprogram.");
The newly added suggestion advises users to use throw to end subprograms.
However, the immediately preceding logic in check_subprogs() verifies the
final instruction using:
if (code != (BPF_JMP | BPF_EXIT) &&
code != (BPF_JMP32 | BPF_JA) &&
code != (BPF_JMP | BPF_JA))
This condition does not account for bpf_throw(), which is a kfunc and
evaluates as a BPF_JMP | BPF_CALL instruction. If a user writes assembly
following the newly provided suggestion and places a bpf_throw call as the
absolute end of their subprogram, won't the verifier still reject it and
print this exact error?
Reference: https://lore.kernel.org/bpf/20260605065130.BB6CA1F00893@smtp.kernel.org/
[ ... ]
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 07/17] bpf: Track verifier context diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 07/17] bpf: Track verifier context " Kumar Kartikeya Dwivedi
2026-06-05 6:46 ` sashiko-bot
@ 2026-06-05 7:22 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:22 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2499 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 56c1236ffe7c..7a122c5025c2 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> @@ -893,8 +930,16 @@ static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
> event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE &&
> event->ref.ref_id == opts->ref_id)
> return i - 1;
> + if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT &&
> + event->kind == BPF_DIAG_HISTORY_CONTEXT &&
> + event->ctx.enter &&
> + event->ctx.kind == opts->ctx_kind)
> + return i - 1;
> }
>
When traversing backwards, this loop stops at the first enter event it
encounters. For nested contexts, wouldn't this anchor the causal path at
the innermost context entry instead of the outermost entry that actually
initiated the restricted region?
Looking at nested lock regions as an example:
outer lock acquired (depth=1) <- outermost context entry
inner lock acquired (depth=2) <- innermost context entry
operation fails here
Traversing backwards from the failure point, the loop would stop at the
inner lock acquisition (the first enter event with matching ctx.kind),
not the outer lock that initiated the critical section.
Is an event->ctx.depth == 1 check needed here to find the outermost
context?
> + if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT)
> + return state->diag_history_cnt;
> +
If the original enter event has been pushed out of the fixed-size
history buffer, returning state->diag_history_cnt skips the causal path
rendering entirely.
Looking at bpf_diag_print_history() in this file:
for (int i = start_idx; i < state->diag_history_cnt; i++) {
...
}
When start_idx equals state->diag_history_cnt, the loop executes zero
times and no events are printed.
Furthermore, the omitted event warning only triggers when start_idx is 0:
if (start_idx == 0 && state->diag_history_wrapped)
bpf_diag_report_omitted();
So returning state->diag_history_cnt bypasses both the remaining event
printing and the omitted warning.
Should this fall through and return 0 instead of state->diag_history_cnt,
so the remaining events are still printed and the omitted warning is
triggered?
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors
2026-06-05 6:34 ` [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors Kumar Kartikeya Dwivedi
2026-06-05 6:46 ` sashiko-bot
@ 2026-06-05 7:23 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:23 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1796 bytes --]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index d2a776d2059c..e30d79149cc7 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -13533,6 +13612,10 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
> } else if (rcu_unlock) {
> if (env->cur_state->active_rcu_locks == 0) {
> verbose(env, "unmatched rcu read unlock (kernel function %s)\n", func_name);
> + bpf_diag_report_context_underflow(env, insn_idx,
> + func_name,
> + BPF_DIAG_CONTEXT_RCU,
> + "Remove the extra bpf_rcu_read_unlock() call, or ensure this path first enters an RCU read lock region.");
> return -EINVAL;
> }
The commit message says this patch covers "unmatched context exits", and
the unmatched bpf_rcu_read_unlock() path here gains a
bpf_diag_report_context_underflow() call.
The analogous unmatched bpf_preempt_enable() path in the same
check_kfunc_call() does not appear to get the same treatment:
kernel/bpf/verifier.c:check_kfunc_call() {
...
} else if (preempt_enable) {
if (env->cur_state->active_preempt_locks == 0) {
verbose(env, "unmatched attempt to enable preemption (kernel function %s)\n", func_name);
return -EINVAL;
}
...
}
Should this path also call bpf_diag_report_context_underflow() so the
preempt-enable underflow is reported consistently with the rcu-unlock
underflow?
This was raised in review of v1 but does not appear to have been addressed:
https://lore.kernel.org/bpf/20260605064655.16DE71F00893@smtp.kernel.org
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors
2026-06-05 6:33 ` [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors Kumar Kartikeya Dwivedi
2026-06-05 6:57 ` sashiko-bot
@ 2026-06-05 7:23 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:23 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 6405 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 7a122c5025c2..920ac17758c2 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> +void bpf_diag_report_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx,
> + int regno, const char *reg_name,
> + const char *type_name,
> + enum bpf_diag_invalid_deref_kind kind,
> + s64 offset)
> +{
> + struct bpf_diag_history_opts opts = {
> + .scope = BPF_DIAG_HISTORY_SCOPE_REG,
> + .frameno = bpf_diag_current_frameno(env),
> + .regno = regno,
> + };
[ ... ]
> + bpf_diag_print_history(env, &opts);
This call to bpf_diag_print_history() in bpf_diag_report_invalid_deref()
is unconditional, but bpf_diag_report_register_type() guards the same call
with "if (regno >= 0)".
The callers in verifier.c pass reg_from_argno(argno) as regno:
bpf_diag_report_invalid_deref(env, env->insn_idx, reg_from_argno(argno),
...);
reg_from_argno() can return -1 for stack arguments. When regno is -1,
bpf_diag_history_start_idx() finds no match (dst_reg is a u8) and returns
index 0, and since bpf_diag_history_event_visible() returns true for all
branch events, would this dump the entire branch history of the program
into the verifier log?
Should this guard the history print with regno >= 0, the same way
bpf_diag_report_register_type() does?
> +void bpf_diag_report_unreadable_reg(struct bpf_verifier_env *env,
> + u32 insn_idx, int regno)
> +{
> + struct bpf_diag_history_opts opts = {
> + .scope = BPF_DIAG_HISTORY_SCOPE_REG,
> + .frameno = bpf_diag_current_frameno(env),
> + .regno = regno,
> + };
[ ... ]
> + bpf_diag_print_history(env, &opts);
The same unconditional history print appears here in
bpf_diag_report_unreadable_reg(). Does this have the same concern when
regno is -1?
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 7bb2a81ee353..adf660ab9e7d 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -13684,6 +13712,7 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
> struct bpf_sanitize_info info = {};
> u8 opcode = BPF_OP(insn->code);
> u32 dst = insn->dst_reg;
> + char reason[256];
> int ret, bounds_ret;
>
> dst_reg = ®s[dst];
> @@ -13707,12 +13736,28 @@ static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
> verbose(env,
> "R%d 32-bit pointer arithmetic prohibited\n",
> dst);
> + scnprintf(reason, sizeof(reason),
> + "R%d holds %s. 32-bit ALU operations on pointers discard pointer provenance, so the verifier cannot keep the result as a safe "
> + "pointer.",
> + dst, bpf_diag_reg_type_plain(env, ptr_reg->type));
^^^^^^^^^^^^^^^^^^^^^^^
Where is bpf_diag_reg_type_plain() defined?
This patch adds seven calls to bpf_diag_reg_type_plain(env, ptr_reg->type)
inside adjust_ptr_min_max_vals(), but the function does not appear to be
defined or declared anywhere in the tree at this commit.
A grep for the symbol returns only these seven call sites in verifier.c,
with no declaration in kernel/bpf/diagnostics.h, kernel/bpf/diagnostics.c,
any header under include/, and no matching #define. The other helper added
in the same patch, bpf_diag_report_register_type(), is defined in
diagnostics.c, so the omission looks specific to bpf_diag_reg_type_plain()
(perhaps a renamed or forgotten helper relative to reg_type_str()).
Since a translation unit referencing an undeclared function fails to build
under -Werror=implicit-function-declaration, does kernel/bpf/verifier.c
still compile at this commit?
> + bpf_diag_report_register_type(env, env->insn_idx, dst,
> + "32-bit pointer arithmetic",
> + reason,
> + "Use 64-bit pointer arithmetic with an allowed, bounded scalar offset, or convert the value to a scalar only after pointer use "
> + "is complete.");
The dst argument passed here is insn->dst_reg.
For commutative pointer arithmetic such as scalar += pointer, dst_reg held
the scalar value before the instruction. Passing dst in that case would
make bpf_diag_report_register_type() claim the scalar register held a
pointer, and trace the scalar register's history rather than the pointer's
provenance.
The same dst is used for all of the bpf_diag_report_register_type() and
off_reg/ptr_reg based reports in this function. Should these reports
reference the register that actually held the pointer instead?
> return -EACCES;
> }
>
> if (ptr_reg->type & PTR_MAYBE_NULL) {
> verbose(env, "R%d pointer arithmetic on %s prohibited, null-check it first\n",
> dst, reg_type_str(env, ptr_reg->type));
> + scnprintf(reason, sizeof(reason),
> + "R%d may be NULL (%s). Pointer arithmetic is allowed only after the program proves the pointer is non-NULL on this path.",
> + dst, reg_type_str(env, ptr_reg->type));
> + bpf_diag_report_register_type(env, env->insn_idx, dst,
> + "pointer arithmetic before NULL check",
> + reason,
> + "Add a NULL check first, and do the pointer arithmetic only on the non-NULL branch.");
> return -EACCES;
> }
[ ... ]
There is one more observation that is not in this patch but is related to
the register-scoped history this patch relies on.
In clear_all_pkt_pointers() and release_reference(), invalidation history
is only recorded when bpf_diag_func_regno() or bpf_diag_stack_arg_slot()
returns >= 0:
kernel/bpf/verifier.c:clear_all_pkt_pointers() {
...
int regno = bpf_diag_func_regno(state, reg);
int slot = bpf_diag_stack_arg_slot(state, reg);
mark_reg_invalid(env, reg);
if (regno >= 0)
bpf_diag_record_reg_invalidate(...);
if (slot >= 0)
bpf_diag_record_stack_arg(...);
...
}
Both helpers return -1 for ordinary stack spills, which live in
STACK_SPILL slots and are in neither state->regs nor
state->stack_arg_regs.
So would a pointer spilled to the stack and later invalidated here record
no history event? If the program restores and dereferences that pointer,
would the new diagnostic trace stop at the stack fill instruction and never
show when the pointer was invalidated?
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors
2026-06-05 6:34 ` [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors Kumar Kartikeya Dwivedi
2026-06-05 6:47 ` sashiko-bot
@ 2026-06-05 7:23 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:23 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3995 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 369b7813b5dc..169c1a2425c8 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
> @@ -714,6 +714,43 @@ static const char *bpf_diag_arg_ordinal(int argno)
> }
> }
>
> +void bpf_diag_report_call_type(struct bpf_verifier_env *env, u32 insn_idx,
> + int regno, const char *call_name,
> + const char *arg_name, const char *reason,
> + const char *suggestion)
> +{
> + struct bpf_diag_history_opts opts = {
> + .scope = BPF_DIAG_HISTORY_SCOPE_REG,
> + .frameno = bpf_diag_current_frameno(env),
> + .regno = regno,
> + };
> + const char *ordinal = bpf_diag_arg_ordinal(regno);
> + char arg_desc[48];
For arguments > 5, reg_from_argno() returns -1, which is passed here as
regno. Calling bpf_diag_arg_ordinal(-1) returns NULL, even though it
appears designed to handle up to 12 arguments.
This drops the ordinal string for stack arguments. Looking at the code:
if (ordinal && arg_name)
scnprintf(arg_desc, sizeof(arg_desc), "%s argument (%s)",
ordinal, arg_name);
else if (arg_name)
scnprintf(arg_desc, sizeof(arg_desc), "argument %s", arg_name);
When ordinal is NULL, the message falls through to "argument %s", losing
the position information.
Additionally, this breaks causal history tracking for stack arguments
since history is skipped when the regno >= 0 check fails:
if (regno >= 0)
bpf_diag_print_history(env, &opts);
Should this function receive argno instead of regno, or at least pass
argno directly to bpf_diag_arg_ordinal()?
Reference:
https://lore.kernel.org/bpf/20260605064700.A77A41F00893@smtp.kernel.org/
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 1c26614bec2b..d2a776d2059c 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -8039,6 +8039,39 @@ static const struct bpf_reg_types *compatible_reg_types[__BPF_ARG_TYPE_MAX] = {
> [ARG_PTR_TO_DYNPTR] = &dynptr_types,
> };
>
> +static const char *bpf_diag_reg_type_plain(struct bpf_verifier_env *env,
> + enum bpf_reg_type type)
> +{
> + switch (base_type(type)) {
> + case NOT_INIT:
> + return "an uninitialized value";
> + case SCALAR_VALUE:
> + return "an integer scalar";
> + case PTR_TO_CTX:
> + return "a context pointer";
> + case PTR_TO_STACK:
> + return "a stack pointer";
> + case PTR_TO_MAP_VALUE:
> + if (type_may_be_null(type))
> + return "a nullable map value pointer";
> + return "a map value pointer";
> + case PTR_TO_MEM:
> + if (type_may_be_null(type))
> + return "a nullable memory pointer";
> + return "a memory pointer";
> + case PTR_TO_BTF_ID:
> + if (type_is_non_owning_ref(type))
> + return "a borrowed allocated object pointer";
> + if (type_is_ptr_alloc_obj(type))
> + return "an owned allocated object pointer";
> + if (type_flag(type) & PTR_UNTRUSTED)
> + return "an untrusted kernel object pointer";
> + return "a kernel object pointer";
PTR_TO_MAP_VALUE and PTR_TO_MEM correctly identify nullable pointers
above, but PTR_TO_BTF_ID omits the check.
When a nullable PTR_TO_BTF_ID is passed to a helper expecting a
non-nullable pointer, it fails the type check in check_reg_type(), but
the error message will drop the 'nullable' qualifier.
This causes the verifier to report 'it has type a kernel object pointer,
but this helper argument accepts...', making the actual cause of the
rejection (nullability) very confusing for users.
Should this include:
case PTR_TO_BTF_ID:
if (type_may_be_null(type))
return "a nullable kernel object pointer";
if (type_is_non_owning_ref(type))
return "a borrowed allocated object pointer";
...
Reference:
https://lore.kernel.org/bpf/20260605064700.A77A41F00893@smtp.kernel.org/
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level
2026-06-05 6:34 ` [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level Kumar Kartikeya Dwivedi
2026-06-05 6:58 ` sashiko-bot
@ 2026-06-05 7:40 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:40 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1284 bytes --]
> Verifier diagnostics collect per-path history and render richer reports for
> selected verifier failures. That work is useful only when the caller requests
> normal verifier log output.
>
> Do not enable diagnostic collection or report rendering for BPF_LOG_STATS-only
> loads. Stats-only loads still need the verifier's summary counters, but not
> the extra path history used by diagnostic reports.
>
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
This looks like a bug fix. The diagnostics renderer was gated on
bpf_verifier_log_needed(), which returns true whenever log->level is non-zero,
including BPF_LOG_STATS-only loads (BPF_LOG_STATS = 4). As a result, report
rendering (and later, path-history collection) was active for stats-only
loads.
Since this commit introduces bpf_diag_enabled() to gate on
(log.level & BPF_LOG_LEVEL) and exclude BPF_LOG_STATS, should it carry a
Fixes: tag pointing at the commit that first added the incorrect gate?
Fixes: a5ebc8a45c36 ("bpf: Add verifier diagnostics report helpers")
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors
2026-06-05 6:34 ` [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
@ 2026-06-05 7:40 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:40 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1722 bytes --]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index f81aad4aeb8e..c5a3b6876c68 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -5233,6 +5267,8 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
> bool tail_call_reachable = false;
> int total;
> int tmp;
> + char chain[256];
> + char reason[512];
This adds 768 bytes of on-stack character buffers to
check_max_stack_depth_subprog().
The same pattern appears in setup_func_entry():
> @@ -9443,11 +9557,22 @@ ...
> struct bpf_func_state *caller, *callee;
> + char chain[256];
> + char reason[512];
> int err;
setup_func_entry() runs deep in the verifier call path:
do_check()
-> do_check_insn()
-> check_func_call()
-> setup_func_entry()
Since the buffers are only written on the error paths, but compilers
typically reserve locals in the prologue unconditionally, would these
~768-800 byte additions to those frames be better served by dynamic
allocation (for example kasprintf) on the failure path?
Could the added frame size trigger frame-size warnings or contribute to
kernel stack pressure here?
This same concern was raised by the Sashiko AI review on v1
(https://lore.kernel.org/bpf/20260605065307.339231F00893@smtp.kernel.org/),
which suggested dynamic allocation since the strings are only used on
failure paths. There does not appear to have been a response, and the
arrays remain in the current code.
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 05/17] bpf: Track verifier register diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 05/17] bpf: Track verifier register " Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
@ 2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 22:31 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:40 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 6688 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 50cc50a394ca..1bdc7e69fbce 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> @@ -609,11 +718,379 @@ void bpf_diag_record_branch(struct bpf_verifier_state *state, u32 insn_idx,
[ ... ]
> +static void bpf_diag_record_reg_mod_reason(struct bpf_verifier_state *state,
> + u32 insn_idx, u8 dst_reg,
> + bool src_valid, u8 src_reg,
> + u8 opcode,
> + enum bpf_diag_reg_mod_reason reason,
> + const struct bpf_reg_state *old_reg,
> + const struct bpf_reg_state *new_reg)
> +{
> + struct bpf_diag_history_event event = {
> + .insn_idx = insn_idx,
> + .kind = BPF_DIAG_HISTORY_REG_MOD,
> + .reg.dst_reg = dst_reg,
> + .reg.src_reg = src_reg,
> + .reg.opcode = opcode,
> + .reg.src_valid = src_valid,
> + };
> +
> + if (state && state->frame[state->curframe])
> + event.reg.frameno = state->frame[state->curframe]->frameno;
Can this record the wrong frame number for registers invalidated in caller
frames?
bpf_diag_record_reg_invalidate() ends up here, and it is called from
clear_all_pkt_pointers() and release_reference(), both of which iterate over
all active frames:
bpf_for_each_reg_in_vstate(env->cur_state, state, reg, ({
...
bpf_diag_record_reg_invalidate(env->cur_state, ...);
}));
But this assignment hardcodes the currently-executing frame via
state->curframe, so a register that lives in a caller frame is recorded with
event.reg.frameno set to the current frame.
bpf_diag_history_start_idx() and bpf_diag_history_event_visible() then filter
on opts->frameno:
event->reg.frameno == opts->frameno
so those caller-frame events would be mis-scoped.
The parallel stack-arg path takes an explicit frameno parameter and passes
state->frameno through bpf_diag_record_stack_arg(). Should the register
invalidate path accept the actual frame number of the register from the
caller in the same way?
This was raised by Sashiko AI review on v1
(https://lore.kernel.org/bpf/20260605065301.A27671F00893@smtp.kernel.org)
and the thread has no response so far.
> + bpf_diag_snapshot_reg(&event, reason, old_reg, new_reg);
[ ... ]
> +static void bpf_diag_format_reg_snapshot(struct bpf_verifier_env *env, char *buf,
> + size_t size,
> + const struct bpf_diag_reg_snapshot *snapshot)
> +{
> + const char *type_name = reg_type_str(env, snapshot->type);
> + char offset_desc[BPF_DIAG_REG_DESC_LEN];
> + char btf_type[BPF_DIAG_REG_DESC_LEN];
Can this chain use too much kernel stack?
BPF_DIAG_REG_DESC_LEN is 512 bytes. bpf_diag_format_reg_snapshot() puts two
512-byte buffers here, plus a third 512-byte 'range' buffer in the
SCALAR_VALUE branch:
if (snapshot->type == SCALAR_VALUE) {
char range[BPF_DIAG_REG_DESC_LEN];
and it also calls bpf_diag_format_var_offset(), which allocates another
512-byte buffer:
static void bpf_diag_format_var_offset(char *buf, size_t size, ...)
{
char range[BPF_DIAG_REG_DESC_LEN];
Its caller bpf_diag_print_reg_mod() allocates two more 512-byte buffers and
calls bpf_diag_format_reg_snapshot() twice:
char old_buf[BPF_DIAG_REG_DESC_LEN], new_buf[BPF_DIAG_REG_DESC_LEN];
bpf_diag_format_reg_snapshot(env, old_buf, sizeof(old_buf), ...);
bpf_diag_format_reg_snapshot(env, new_buf, sizeof(new_buf), ...);
Could this >3KB stack-usage chain trip CONFIG_FRAME_WARN, or cause problems on
architectures with smaller kernel stacks?
This was raised by Sashiko AI review on v1
(https://lore.kernel.org/bpf/20260605065301.A27671F00893@smtp.kernel.org)
and the thread has no response so far.
> + bpf_diag_format_var_offset(offset_desc, sizeof(offset_desc), snapshot);
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index d60e1309957a..188201d48108 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -4033,6 +4033,8 @@ static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_s
> struct bpf_subprog_info *subprog = &env->subprog_info[state->subprogno];
> int spi = -off / BPF_REG_SIZE - 1;
> struct bpf_reg_state *arg;
> + struct bpf_reg_state old_arg;
> + bool slot_exists;
> int err;
[ ... ]
> + slot_exists = spi < state->out_stack_arg_cnt;
[ ... ]
> + arg = &state->stack_arg_regs[spi];
> + if (slot_exists)
> + old_arg = *arg;
> + else
> + bpf_mark_reg_not_init(env, &old_arg);
Does this pass uninitialized stack memory to bpf_mark_reg_not_init()?
When slot_exists is false, old_arg is declared without initialization.
bpf_mark_reg_not_init() reaches __mark_reg_unknown() ->
bpf_mark_reg_unknown_imprecise(), which reads reg->subreg_def into a local
before it memset()s the struct:
u32 subreg_def = reg->subreg_def;
...
memset(reg, 0, sizeof(*reg));
Could reading that uninitialized subreg_def trigger a KMSAN
use-of-uninitialized-value report?
This was raised by Sashiko AI review on v1
(https://lore.kernel.org/bpf/20260605065301.A27671F00893@smtp.kernel.org)
and the thread has no response so far.
> @@ -8833,14 +8867,51 @@ static int check_func_proto(const struct bpf_func_proto *fn, struct bpf_call_arg
> * This also applies to dynptr slices belonging to skb and xdp dynptrs,
> * since these slices point to packet data.
> */
> +static int bpf_diag_stack_arg_slot(const struct bpf_func_state *state,
> + const struct bpf_reg_state *reg)
> +{
> + if (!state->stack_arg_regs)
> + return -1;
This isn't a bug, but the two new helpers are inserted between the packet-data
comment block and the function it describes. The comment above
('Packet data might have moved... turn them into unknown SCALAR_VALUE...
dynptr slices belonging to skb and xdp dynptrs') now sits directly over
bpf_diag_stack_arg_slot(), which just computes a register's index within
state->stack_arg_regs[] and has nothing to do with packet data. Should the new
helpers be moved below clear_all_pkt_pointers(), or the comment moved down to
stay attached to it?
> + if (reg < state->stack_arg_regs ||
> + reg >= state->stack_arg_regs + state->out_stack_arg_cnt)
> + return -1;
> + return reg - state->stack_arg_regs;
> +}
> +
> +static int bpf_diag_func_regno(const struct bpf_func_state *state,
> + const struct bpf_reg_state *reg)
> +{
> + if (reg < state->regs || reg >= state->regs + MAX_BPF_REG)
> + return -1;
> + return reg - state->regs;
> +}
> +
> static void clear_all_pkt_pointers(struct bpf_verifier_env *env)
> {
[ ... ]
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
2026-06-05 6:42 ` sashiko-bot
@ 2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 18:58 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:40 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2272 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> new file mode 100644
> index 000000000000..a6684adc0c31
> --- /dev/null
> +++ b/kernel/bpf/diagnostics.c
> @@ -0,0 +1,110 @@
[ ... ]
> +static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
> + const char *first_prefix,
> + const char *next_prefix,
> + const char *text)
> +{
> + const char *prefix = first_prefix;
> +
> + while (*text) {
> + const char *line = text;
> + int prefix_len = strlen(prefix);
> + int text_width = BPF_DIAG_TEXT_WIDTH - prefix_len;
> + int len = 0, last_space = -1;
> +
> + if (text_width < 1)
> + text_width = 1;
> +
> + while (line[len] && line[len] != '\n' && len < text_width) {
> + if (line[len] == ' ')
> + last_space = len;
> + len++;
> + }
> +
> + if (line[len] && line[len] != '\n' && last_space > 0)
> + len = last_space;
When a word ends exactly at the column boundary, can this wrap it one line
too early?
After the inner loop, len can equal text_width with line[len] pointing at
the space that separates the fitted content from the next word. The revert
guard only excludes '\n', so a boundary space still satisfies
line[len] != '\n' and forces len back to last_space.
For example, with a text_width of 10 and the text "aa bbbbbbb cc", the loop
stops at len = 10 (line[10] is ' ') with last_space = 2, so len reverts to 2
and only "aa" is emitted, even though "aa bbbbbbb" is exactly 10 columns and
fits.
Would also checking that the boundary character is not a space avoid the
early break, e.g.:
if (line[len] && line[len] != '\n' && line[len] != ' ' &&
last_space > 0)
len = last_space;
This looks cosmetic only, affecting the layout of verifier log prose rather
than correctness. The same code is present through the end of the series.
> +
> + verbose(env, "%s%.*s\n", prefix, len, line);
> +
> + text = line + len;
> + while (*text == ' ')
> + text++;
> + if (*text == '\n')
> + text++;
> +
> + prefix = next_prefix;
> + }
> +}
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors
2026-06-05 6:33 ` [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors Kumar Kartikeya Dwivedi
2026-06-05 6:45 ` sashiko-bot
@ 2026-06-05 7:57 ` bot+bpf-ci
1 sibling, 0 replies; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:57 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2172 bytes --]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 920ac17758c2..0abe1ab19d25 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> @@ -809,6 +809,119 @@ void bpf_diag_report_unreadable_reg(struct bpf_verifier_env *env,
> "Avoid using the register after it is invalidated, or reload and revalidate a fresh pointer before this instruction.");
> }
>
> +void bpf_diag_report_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx,
> + int regno, const char *reg_name,
> + const char *type_name,
> + enum bpf_diag_mem_bounds_kind kind,
> + int off, int size, u32 mem_size,
> + const struct bpf_reg_state *reg)
> +{
[ ... ]
> + switch (kind) {
> + case BPF_DIAG_MEM_NEGATIVE_MIN:
> + scnprintf(proof, sizeof(proof),
> + "The smallest possible offset is %lld, below 0",
> + reg_smin(reg) + off);
> + break;
When reg_smin(reg) is S64_MIN and off is negative, will this print a
contradictory positive number instead of a negative one?
Looking at the caller in check_mem_region_access(), it triggers the
BPF_DIAG_MEM_NEGATIVE_MIN diagnostic if reg_smin(reg) == S64_MIN:
kernel/bpf/verifier.c:check_mem_region_access() {
if (reg_smin(reg) < 0 &&
(reg_smin(reg) == S64_MIN ||
...)) {
bpf_diag_report_mem_bounds(..., BPF_DIAG_MEM_NEGATIVE_MIN, ...);
}
}
If off is negative, reg_smin(reg) + off will underflow S64_MIN and wrap
around to a huge positive number like 9223372036854775807. The diagnostic
output might then read something confusing like:
"The smallest possible offset is 9223372036854775807, below 0"
Unlike the max bounds case which correctly handles wrapping, the min bounds
case appears to evaluate this unconditionally:
> + case BPF_DIAG_MEM_MAX_OUT_OF_RANGE:
> + default:
> + if (off < 0 && reg_umax(reg) < (u64)-off)
> + max_start = 0;
> + else
> + max_start = reg_umax(reg) + off;
[ ... ]
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history Kumar Kartikeya Dwivedi
2026-06-05 6:50 ` sashiko-bot
@ 2026-06-05 7:57 ` bot+bpf-ci
2026-06-05 21:41 ` Eduard Zingerman
2026-06-05 21:37 ` Eduard Zingerman
2 siblings, 1 reply; 52+ messages in thread
From: bot+bpf-ci @ 2026-06-05 7:57 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, kkd, kernel-team, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2870 bytes --]
> diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> index c248ff41f42a..55921b8a5d5c 100644
> --- a/include/linux/bpf_verifier.h
> +++ b/include/linux/bpf_verifier.h
[ ... ]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 652d4627c2b0..50cc50a394ca 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> +void bpf_diag_copy_history(struct bpf_verifier_state *dst,
> + const struct bpf_verifier_state *src)
> +{
> + struct bpf_diag_history_event *history;
> +
> + if (!src->diag_history_cnt) {
> + bpf_diag_clear_history(dst);
> + dst->diag_history_omitted = src->diag_history_omitted;
> + return;
> + }
> +
> + history = krealloc_array(dst->diag_history, src->diag_history_cnt,
> + sizeof(*history), GFP_KERNEL_ACCOUNT);
> + if (!history) {
> + kfree(dst->diag_history);
> + dst->diag_history = NULL;
> + dst->diag_history_cnt = 0;
> + dst->diag_history_omitted = src->diag_history_omitted +
> + src->diag_history_cnt;
> + return;
> + }
> +
> + dst->diag_history = history;
> + memcpy(dst->diag_history, src->diag_history,
> + src->diag_history_cnt * sizeof(*dst->diag_history));
> + dst->diag_history_cnt = src->diag_history_cnt;
> + dst->diag_history_omitted = src->diag_history_omitted;
> +}
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 8ed484cb1a8a..d60e1309957a 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -1624,6 +1626,7 @@ int bpf_copy_verifier_state(struct bpf_verifier_state *dst_state,
> if (!dst_state->jmp_history)
> return -ENOMEM;
> dst_state->jmp_history_cnt = src->jmp_history_cnt;
> + bpf_diag_copy_history(dst_state, src);
Does this cause massive memory bloat when verifier logging is enabled?
Because is_state_visited() caches states by calling bpf_copy_verifier_state()
and adding them to the explored_states list, the diagnostic history is copied
for every cached state.
Since the diagnostic history can grow up to 1024 entries per state, caching
this for tens of thousands of explored states could easily lead to gigabytes
of wasted memory, potentially hitting memcg limits via GFP_KERNEL_ACCOUNT or
causing -ENOMEM specifically when a user enables logging to debug an issue.
Because cached states are only used for state equivalence checks and precision
propagation, neither of which use the diagnostic history, could we avoid
copying the diag_history into cached explored states?
This concern was raised by sashiko-bot@kernel.org at
https://lore.kernel.org/bpf/20260605065017.D9F401F00893@smtp.kernel.org
---
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/27000182395
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context
2026-06-05 6:33 ` [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context Kumar Kartikeya Dwivedi
@ 2026-06-05 8:48 ` sashiko-bot
2026-06-05 20:22 ` Eduard Zingerman
1 sibling, 0 replies; 52+ messages in thread
From: sashiko-bot @ 2026-06-05 8:48 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Missing array bounds check in `bpf_diag_func_name()` when resolving hidden subprograms.
- [Medium] Tight loop coupling silently drops BPF instructions when the diagnostic is triggered near the top of a file.
- [Low] Potential loss of the visual alignment caret (`^-- `) due to `scnprintf` truncation if the source code is heavily indented.
--
commit e84e87b559a751018ace3ee43b2b61b8014f1c4e
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Add source and instruction diagnostic context
This patch adds source line and nearby instruction annotations to the BPF
verifier diagnostics. The renderer keeps source text in a fixed-width lane
and prints instructions in a stable right-hand gutter.
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index d9a103cf3a16e..652d4627c2b0f 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
[ ... ]
> +static const char *bpf_diag_func_name(struct bpf_verifier_env *env, u32 insn_idx)
> +{
> + const struct bpf_subprog_info *subprog;
> + const struct bpf_func_info *info;
> + const struct btf_type *type;
> + int subprogno;
> +
> + if (!env->prog->aux->func_info || !env->prog->aux->btf)
> + return NULL;
> +
> + subprog = bpf_find_containing_subprog(env, insn_idx);
> + if (!subprog)
> + return NULL;
> +
> + subprogno = subprog - env->subprog_info;
> + info = &env->prog->aux->func_info[subprogno];
[Severity: High]
If the verifier dynamically adds a hidden subprogram like the exception
callback, env->subprog_cnt increments but env->prog->aux->func_info_cnt
remains the same since it represents user-supplied BTF data. Can this code
cause an out-of-bounds read on the func_info array if a diagnostic is
triggered inside a hidden subprogram? Should this verify that subprogno is
less than env->prog->aux->func_info_cnt before accessing the array?
> + type = btf_type_by_id(env->prog->aux->btf, info->type_id);
> + if (!type)
> + return NULL;
[ ... ]
> +static void bpf_diag_print_source_annotation(struct bpf_verifier_env *env,
> + int line_width, int indent,
> + const char *label,
> + const char *msg)
> +{
> + char first_prefix[128], next_prefix[128], text[BPF_DIAG_MSG_LEN];
> +
> + scnprintf(first_prefix, sizeof(first_prefix), " %*s | %*s^-- ",
> + line_width + 4, "", indent, "");
[Severity: Low]
Since the indent comes from user-supplied BTF line info, what happens if the
source code has heavy indentation (for example, over 110 spaces)? Will
first_prefix truncate here and silently drop the "^-- " visual caret used
for alignment?
> + scnprintf(next_prefix, sizeof(next_prefix), " %*s | %*s ",
> + line_width + 4, "", indent, "");
> + scnprintf(text, sizeof(text), "%s: %s", label, msg);
[ ... ]
> +void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
> + char marker, const char *fmt, ...)
> +{
[ ... ]
> + for (line_num = start_line; line_num <= end_line; line_num++) {
> + const char *line;
> + int row = line_num - src.line_num;
> +
> + line = line_num == src.line_num ?
> + src.line :
> + bpf_diag_find_source_line(env, src.file_name_off, line_num);
> +
> + bpf_diag_print_source_insn_line(env,
> + line_num == src.line_num ?
> + ">>> " : " ",
> + width, line_num, line,
> + &diag_insn[row + BPF_DIAG_INSN_CONTEXT],
[Severity: Medium]
When a diagnostic is triggered near the top of a file, start_line is clamped
to 1. Because the instruction layout is coupled to the source line loop
using the line_num difference, doesn't this skip iterating over the earlier
BPF instructions that were already correctly populated in the diag_insn
buffer? Could this drop valid instructions from the verifier log output,
making diagnostics harder to read?
> + insn_idx, insn_width);
> + if (line_num == src.line_num)
> + bpf_diag_print_source_annotation(env, width, indent,
> + label, msg);
> + }
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260605063412.974640-1-memxor@gmail.com?part=3
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
2026-06-05 6:42 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
@ 2026-06-05 18:58 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 18:58 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri, 2026-06-05 at 08:33 +0200, Kumar Kartikeya Dwivedi wrote:
> Add a small diagnostics renderer for verifier reports and wire it into the
> BPF build. The initial helpers emit the common text structure: a failure
> header plus reusable report sections.
>
> Wrap report prose at 120 columns so Reason and Suggestion text stays readable
> without changing source or instruction gutters.
>
> No verifier call sites use the helpers yet, so existing verifier output
> is unchanged.
>
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
> ---
> kernel/bpf/Makefile | 2 +-
> kernel/bpf/diagnostics.c | 110 +++++++++++++++++++++++++++++++++++++++
> kernel/bpf/diagnostics.h | 19 +++++++
> 3 files changed, 130 insertions(+), 1 deletion(-)
> create mode 100644 kernel/bpf/diagnostics.c
> create mode 100644 kernel/bpf/diagnostics.h
>
> diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
> index 4dc41bf5780c..90255d80e5be 100644
> --- a/kernel/bpf/Makefile
> +++ b/kernel/bpf/Makefile
> @@ -6,7 +6,7 @@ cflags-nogcse-$(CONFIG_X86)$(CONFIG_CC_IS_GCC) := -fno-gcse
> endif
> CFLAGS_core.o += -Wno-override-init $(cflags-nogcse-yy)
>
> -obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o cnum.o log.o token.o liveness.o const_fold.o
> +obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o cnum.o log.o token.o liveness.o const_fold.o diagnostics.o
> obj-$(CONFIG_BPF_SYSCALL) += bpf_iter.o map_iter.o task_iter.o prog_iter.o link_iter.o
> obj-$(CONFIG_BPF_SYSCALL) += hashtab.o arraymap.o percpu_freelist.o bpf_lru_list.o lpm_trie.o map_in_map.o bloom_filter.o
> obj-$(CONFIG_BPF_SYSCALL) += local_storage.o queue_stack_maps.o ringbuf.o bpf_insn_array.o
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> new file mode 100644
> index 000000000000..a6684adc0c31
> --- /dev/null
> +++ b/kernel/bpf/diagnostics.c
> @@ -0,0 +1,110 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
> +
> +#include <linux/bpf_verifier.h>
> +#include <linux/stdarg.h>
> +#include <linux/string.h>
> +
> +#include "diagnostics.h"
> +
> +#define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
> +#define BPF_DIAG_TEXT_WIDTH 120
> +#define BPF_DIAG_TEXT_INDENT " "
> +#define BPF_DIAG_MSG_LEN 512
> +
> +static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
> + const char *first_prefix,
> + const char *next_prefix,
> + const char *text)
> +{
> + const char *prefix = first_prefix;
> +
> + while (*text) {
> + const char *line = text;
> + int prefix_len = strlen(prefix);
> + int text_width = BPF_DIAG_TEXT_WIDTH - prefix_len;
> + int len = 0, last_space = -1;
> +
> + if (text_width < 1)
> + text_width = 1;
> +
> + while (line[len] && line[len] != '\n' && len < text_width) {
> + if (line[len] == ' ')
> + last_space = len;
> + len++;
> + }
> +
> + if (line[len] && line[len] != '\n' && last_space > 0)
> + len = last_space;
Nit: len = min(strnchr(line, text_width, '\n'), strnchr(line, text_width, ' '));
> +
> + verbose(env, "%s%.*s\n", prefix, len, line);
> +
> + text = line + len;
> + while (*text == ' ')
> + text++;
> + if (*text == '\n')
> + text++;
> +
> + prefix = next_prefix;
> + }
> +}
> +
> +static void bpf_diag_print_wrapped_text(struct bpf_verifier_env *env,
> + const char *text)
> +{
> + bpf_diag_print_wrapped_prefixed(env, BPF_DIAG_TEXT_INDENT,
> + BPF_DIAG_TEXT_INDENT, text);
Nit: line length limit is 100.
> +}
> +
> +static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
> + const char *fmt, va_list args)
> +{
> + char buf[1024];
Nit: big buffers should be in the bpf_verifier_env.
> +
> + if (!bpf_verifier_log_needed(&env->log))
> + return;
> +
> + vscnprintf(buf, sizeof(buf), fmt, args);
> + bpf_diag_print_wrapped_text(env, buf);
> +}
> +
> +void bpf_diag_report_header(struct bpf_verifier_env *env,
> + const char *category, const char *problem)
> +{
> + char problem_buf[BPF_DIAG_MSG_LEN];
> +
> + strscpy(problem_buf, problem ?: "", sizeof(problem_buf));
> + if (problem_buf[0] >= 'a' && problem_buf[0] <= 'z')
> + problem_buf[0] += 'A' - 'a';
Nit: ctype.h:toupper()
> +
> + verbose(env, "\nVerification failed: %s: %s\n", category,
> + problem_buf);
> +}
> +
> +void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
Nit: can be static, same for bpf_diag_report_section() and bpf_diag_report_suggestion().
> +{
> + va_list args;
> +
> + bpf_diag_report_section(env, "Reason");
> +
> + va_start(args, fmt);
> + bpf_diag_vprint_indented(env, fmt, args);
> + va_end(args);
> +}
> +
> +void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title)
> +{
> + verbose(env, "\n%s:\n", title);
> +}
> +
> +void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
> +{
> + va_list args;
> +
> + bpf_diag_report_section(env, "Suggestion");
> +
> + va_start(args, fmt);
> + bpf_diag_vprint_indented(env, fmt, args);
> + va_end(args);
> + verbose(env, "\n");
> +}
> diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
> new file mode 100644
> index 000000000000..f13a939722bf
> --- /dev/null
> +++ b/kernel/bpf/diagnostics.h
> @@ -0,0 +1,19 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
> +
> +#ifndef __BPF_DIAGNOSTICS_H
> +#define __BPF_DIAGNOSTICS_H
> +
> +#include <linux/compiler_attributes.h>
> +
> +struct bpf_verifier_env;
> +
> +void bpf_diag_report_header(struct bpf_verifier_env *env,
> + const char *category, const char *problem);
> +void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
> + __printf(2, 3);
> +void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title);
> +void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
> + __printf(2, 3);
> +
> +#endif /* __BPF_DIAGNOSTICS_H */
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 02/17] bpf: Define verifier diagnostic categories
2026-06-05 6:33 ` [PATCH bpf-next v1 02/17] bpf: Define verifier diagnostic categories Kumar Kartikeya Dwivedi
@ 2026-06-05 19:10 ` Eduard Zingerman
0 siblings, 0 replies; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 19:10 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri, 2026-06-05 at 08:33 +0200, Kumar Kartikeya Dwivedi wrote:
> Add a central enum for the verifier diagnostic categories used by structured
> reports. Keeping category names in one renderer helper makes later call
> sites choose a classification explicitly instead of formatting category
> strings locally.
>
> This adds no verifier call sites and keeps existing verifier output unchanged.
>
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
> ---
> kernel/bpf/diagnostics.c | 32 +++++++++++++++++++++++++++++---
> kernel/bpf/diagnostics.h | 15 ++++++++++++++-
> 2 files changed, 43 insertions(+), 4 deletions(-)
>
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index a6684adc0c31..d9a103cf3a16 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
> @@ -68,8 +68,34 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
> bpf_diag_print_wrapped_text(env, buf);
> }
>
> +static const char *bpf_diag_category_name(enum bpf_diag_category category)
> +{
> + switch (category) {
> + case BPF_DIAG_CATEGORY_MEMORY_SAFETY:
> + return "Memory Safety";
> + case BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY:
> + return "Register Type Safety";
> + case BPF_DIAG_CATEGORY_CALL_TYPE_SAFETY:
> + return "Call Type Safety";
> + case BPF_DIAG_CATEGORY_RESOURCE_LIFETIME_SAFETY:
> + return "Resource Lifetime Safety";
> + case BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY:
> + return "Execution Context Safety";
> + case BPF_DIAG_CATEGORY_PROGRAM_STRUCTURE:
> + return "Program Structure";
> + case BPF_DIAG_CATEGORY_POLICY:
> + return "Policy";
> + case BPF_DIAG_CATEGORY_VERIFIER_LIMIT:
> + return "Verifier Limit";
> + case BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR:
> + default:
> + return "Verifier Internal Error";
> + }
> +}
Do we need an enum + name function? Maybe just #define?
#define BPF_DIAG_CATEGORY_MEMORY_SAFETY "Aaaaa AAAA"
> +
> void bpf_diag_report_header(struct bpf_verifier_env *env,
> - const char *category, const char *problem)
> + enum bpf_diag_category category,
> + const char *problem)
> {
> char problem_buf[BPF_DIAG_MSG_LEN];
>
> @@ -77,8 +103,8 @@ void bpf_diag_report_header(struct bpf_verifier_env *env,
> if (problem_buf[0] >= 'a' && problem_buf[0] <= 'z')
> problem_buf[0] += 'A' - 'a';
>
> - verbose(env, "\nVerification failed: %s: %s\n", category,
> - problem_buf);
> + verbose(env, "\nVerification failed: %s: %s\n",
> + bpf_diag_category_name(category), problem_buf);
> }
>
> void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
> diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
> index f13a939722bf..8bfc7104eeec 100644
> --- a/kernel/bpf/diagnostics.h
> +++ b/kernel/bpf/diagnostics.h
> @@ -6,10 +6,23 @@
>
> #include <linux/compiler_attributes.h>
>
> +enum bpf_diag_category {
> + BPF_DIAG_CATEGORY_MEMORY_SAFETY,
> + BPF_DIAG_CATEGORY_REGISTER_TYPE_SAFETY,
> + BPF_DIAG_CATEGORY_CALL_TYPE_SAFETY,
> + BPF_DIAG_CATEGORY_RESOURCE_LIFETIME_SAFETY,
> + BPF_DIAG_CATEGORY_EXECUTION_CONTEXT_SAFETY,
> + BPF_DIAG_CATEGORY_PROGRAM_STRUCTURE,
> + BPF_DIAG_CATEGORY_POLICY,
> + BPF_DIAG_CATEGORY_VERIFIER_LIMIT,
> + BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR,
> +};
Nit: there is no reason to expose this in the header.
And given that this can be an enum private to diagnostics.c
BPF_DIAG_CATEGORY_ prefix can be dropped.
> +
> struct bpf_verifier_env;
>
> void bpf_diag_report_header(struct bpf_verifier_env *env,
> - const char *category, const char *problem);
> + enum bpf_diag_category category,
> + const char *problem);
> void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
> __printf(2, 3);
> void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title);
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context
2026-06-05 6:33 ` [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context Kumar Kartikeya Dwivedi
2026-06-05 8:48 ` sashiko-bot
@ 2026-06-05 20:22 ` Eduard Zingerman
2026-06-05 20:55 ` Kumar Kartikeya Dwivedi
1 sibling, 1 reply; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 20:22 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri, 2026-06-05 at 08:33 +0200, Kumar Kartikeya Dwivedi wrote:
> Teach verifier diagnostics to annotate an instruction with BTF source line
> information and nearby BPF instructions. The renderer keeps source text in
> a fixed-width lane and prints instructions in a stable right-hand gutter.
>
> Wrap annotation text under the source line so long error labels are readable
> while the source and instruction lanes keep their fixed layout.
>
> Keeping source and instruction context in one commit preserves the visual
> layout contract that later diagnostic reports rely on.
>
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
> ---
> kernel/bpf/diagnostics.c | 384 +++++++++++++++++++++++++++++++++++++++
> kernel/bpf/diagnostics.h | 4 +
> 2 files changed, 388 insertions(+)
>
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index d9a103cf3a16..652d4627c2b0 100644
> --- a/kernel/bpf/diagnostics.c
> +++ b/kernel/bpf/diagnostics.c
> @@ -1,16 +1,45 @@
> // SPDX-License-Identifier: GPL-2.0-only
> // Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
>
> +#include <linux/bpf.h>
> #include <linux/bpf_verifier.h>
> +#include <linux/btf.h>
> +#include <linux/kernel.h>
> #include <linux/stdarg.h>
> #include <linux/string.h>
>
> +#include "disasm.h"
> #include "diagnostics.h"
>
> #define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
> #define BPF_DIAG_TEXT_WIDTH 120
> #define BPF_DIAG_TEXT_INDENT " "
> #define BPF_DIAG_MSG_LEN 512
> +#define BPF_DIAG_SOURCE_CONTEXT 2
> +#define BPF_DIAG_INSN_CONTEXT 2
> +#define BPF_DIAG_COLUMN_GAP 3
> +#define BPF_DIAG_SOURCE_LANE_WIDTH 88
> +#define BPF_DIAG_TAB_WIDTH 8
> +
> +struct bpf_diag_source {
> + const char *file;
> + const char *line;
> + u32 file_name_off;
> + int line_num;
> + int line_col;
> +};
> +
> +struct bpf_diag_insn {
> + char text[160];
> + int idx;
> + bool valid;
> +};
> +
> +struct bpf_diag_insn_buf {
> + char *buf;
> + size_t size;
> + size_t len;
> +};
>
> static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
> const char *first_prefix,
> @@ -68,6 +97,282 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
> bpf_diag_print_wrapped_text(env, buf);
> }
>
> +static int bpf_diag_line_width(unsigned int line)
> +{
> + int width = 1;
> +
> + while (line >= 10) {
> + line /= 10;
> + width++;
> + }
> +
> + return width;
> +}
Is claude trolling us?
Or does it have a lot of such samples in the training set?
What is more disturbing?
> +
> +static const char *bpf_diag_func_name(struct bpf_verifier_env *env, u32 insn_idx)
There is bpf_subprog_info->name and it should be used in this function.
Plus a fallback in case if there is no BTF info.
Also, maybe drop bpf_diag_ suffix for static functions?
> +{
> + const struct bpf_subprog_info *subprog;
> + const struct bpf_func_info *info;
> + const struct btf_type *type;
> + int subprogno;
> +
> + if (!env->prog->aux->func_info || !env->prog->aux->btf)
> + return NULL;
> +
> + subprog = bpf_find_containing_subprog(env, insn_idx);
> + if (!subprog)
> + return NULL;
> +
> + subprogno = subprog - env->subprog_info;
> + info = &env->prog->aux->func_info[subprogno];
> + type = btf_type_by_id(env->prog->aux->btf, info->type_id);
> + if (!type)
> + return NULL;
> +
> + return btf_name_by_offset(env->prog->aux->btf, type->name_off);
> +}
> +
> +static bool bpf_diag_fill_source(struct bpf_verifier_env *env,
> + const struct bpf_line_info *linfo,
> + struct bpf_diag_source *src)
> +{
> + const char *file, *line;
> +
> + file = btf_name_by_offset(env->prog->aux->btf, linfo->file_name_off);
> + line = btf_name_by_offset(env->prog->aux->btf, linfo->line_off);
> + if (!file || !*file || !line || !*line)
> + return false;
> +
> + src->file = kbasename(file);
> + src->line = line;
> + src->file_name_off = linfo->file_name_off;
> + src->line_num = BPF_LINE_INFO_LINE_NUM(linfo->line_col);
> + src->line_col = BPF_LINE_INFO_LINE_COL(linfo->line_col);
> + return true;
> +}
Nit: adjust log.c:verbose_linfo() to use this?
Also, this duplicates core.c:bpf_get_linfo_file_line().
> +
> +static bool bpf_diag_get_source(struct bpf_verifier_env *env, u32 insn_idx,
> + struct bpf_diag_source *src)
> +{
> + const struct bpf_line_info *linfo;
> +
> + linfo = bpf_find_linfo(env->prog, insn_idx);
> + if (!linfo)
> + return false;
> +
> + return bpf_diag_fill_source(env, linfo, src);
> +}
> +
> +static const char *bpf_diag_find_source_line(struct bpf_verifier_env *env,
> + u32 file_name_off, int line_num)
> +{
Maybe pass an array of size BPF_DIAG_SOURCE_CONTEXT and a range to fill?
Just to avoid scanning multiple times.
> + const struct bpf_line_info *linfo = env->prog->aux->linfo;
> + struct bpf_diag_source src;
> + u32 i;
> +
> + for (i = 0; i < env->prog->aux->nr_linfo; i++) {
> + if (linfo[i].file_name_off != file_name_off ||
> + BPF_LINE_INFO_LINE_NUM(linfo[i].line_col) != line_num)
> + continue;
> + if (bpf_diag_fill_source(env, &linfo[i], &src))
> + return src.line;
Nit:
if (linfo[i].file_name_off == file_name_off &&
BPF_LINE_INFO_LINE_NUM(linfo[i].line_col) == line_num &&
bpf_diag_fill_source(env, &linfo[i], &src))
return src.line;
> + }
> +
> + return NULL;
> +}
> +
> +static int bpf_diag_line_indent(const char *line)
> +{
> + int indent = 0;
> +
> + while (*line == ' ' || *line == '\t') {
> + if (*line == '\t')
> + indent = round_up(indent + 1, BPF_DIAG_TAB_WIDTH);
> + else
> + indent++;
> + line++;
> + }
> +
> + return indent;
> +}
> +
> +static const char *bpf_diag_marker_label(char marker)
That's a bit of an obfuscation exercise.
Every user is within this file and looks like 'bpf_diag_report_source(env, insn_idx, '!', ...)'.
Why not use some #define and 'bpf_diag_report_source(env, insn_idx, ERROR, ...)'?
> +{
> + switch (marker) {
> + case '!':
> + return "error";
> + case '?':
> + return "branch";
> + case '~':
> + return "update";
> + case '+':
> + return "acquired";
> + case '-':
> + return "released";
> + default:
> + return "note";
> + }
> +}
> +
> +static void bpf_diag_insn_print(void *private_data, const char *fmt, ...)
> +{
> + struct bpf_diag_insn_buf *buf = private_data;
> + va_list args;
> +
> + if (buf->len >= buf->size)
> + return;
> +
> + va_start(args, fmt);
> + buf->len += vscnprintf(buf->buf + buf->len, buf->size - buf->len,
> + fmt, args);
Nit: here and in many other places, the maximum line width is 100.
> + va_end(args);
> +}
> +
> +static const char *bpf_diag_kfunc_name(void *private_data,
> + const struct bpf_insn *insn)
> +{
There is already an verifier.c:disasm_kfunc_name().
> + const struct btf_type *func;
> + struct btf *btf;
> +
> + (void)private_data;
> +
> + if (insn->src_reg != BPF_PSEUDO_KFUNC_CALL || insn->off)
> + return NULL;
> +
> + btf = bpf_get_btf_vmlinux();
> + if (IS_ERR_OR_NULL(btf))
> + return NULL;
> +
> + func = btf_type_by_id(btf, insn->imm);
> + if (!func)
> + return NULL;
> +
> + return btf_name_by_offset(btf, func->name_off);
> +}
> +
> +static void bpf_diag_format_insn(struct bpf_verifier_env *env, int insn_idx,
> + struct bpf_diag_insn *diag_insn)
> +{
> + struct bpf_insn *insn;
> + struct bpf_diag_insn_buf buf = {
> + .buf = diag_insn->text,
> + .size = sizeof(diag_insn->text),
> + };
> + const struct bpf_insn_cbs cbs = {
> + .cb_call = bpf_diag_kfunc_name,
> + .cb_print = bpf_diag_insn_print,
> + .private_data = &buf,
> + };
> +
> + diag_insn->idx = insn_idx;
> + diag_insn->valid = false;
> + diag_insn->text[0] = '\0';
> +
> + if (insn_idx < 0 || insn_idx >= env->prog->len)
> + return;
> +
> + if (insn_idx > 0 && bpf_is_ldimm64(&env->prog->insnsi[insn_idx - 1]))
> + return;
> +
> + insn = &env->prog->insnsi[insn_idx];
> + if (bpf_is_ldimm64(insn) && insn_idx + 1 >= env->prog->len)
> + return;
> +
> + print_bpf_insn(&cbs, insn, env->allow_ptr_leaks);
> + while (buf.len && (diag_insn->text[buf.len - 1] == '\n' ||
> + diag_insn->text[buf.len - 1] == '\r'))
Nit: disasm.c does not produce '\r'.
> + diag_insn->text[--buf.len] = '\0';
> +
> + diag_insn->valid = true;
> +}
> +
> +static void bpf_diag_format_source_text(char *buf, size_t size,
> + const char *line, int width)
> +{
> + bool truncated = false;
> + int col = 0, len = 0;
Is 'col' ever different from 'len' in this function?
Seem to be always updated in sync.
> +
> + if (!size)
> + return;
> + if (width <= 0) {
> + buf[0] = '\0';
> + return;
> + }
> +
> + line = line ?: "...";
> + while (*line && col < width && len + 1 < size) {
> + if (*line == '\t') {
> + int next = round_up(col + 1, BPF_DIAG_TAB_WIDTH);
> +
> + while (col < next && col < width && len + 1 < size) {
> + buf[len++] = ' ';
> + col++;
> + }
> + line++;
> + continue;
> + }
> +
> + buf[len++] = *line++;
> + col++;
> + }
> +
> + if (*line)
> + truncated = true;
> + if (truncated) {
No need for the 'truncated' variable?
if (*line) { /* truncated */
...
}
> + int ellipsis_len = min(3, width);
> +
> + while (len > 0 && col > width - ellipsis_len) {
> + len--;
> + col--;
> + }
Use math? Or snprintf?
> + while (ellipsis_len-- && len + 1 < size) {
> + buf[len++] = '.';
> + col++;
> + }
> + }
> +
> + buf[len] = '\0';
> +}
> +
> +static void bpf_diag_format_source_lane(char *buf, size_t size,
> + const char *source_prefix,
> + int source_line_width,
> + int line_num, const char *line)
> +{
> + int len, text_width;
> +
> + len = scnprintf(buf, size, "%s%*d | ",
> + source_prefix, source_line_width, line_num);
> + if (len >= (int)size)
> + return;
> +
> + text_width = BPF_DIAG_SOURCE_LANE_WIDTH - len;
> + bpf_diag_format_source_text(buf + len, size - len, line, text_width);
> +}
> +
> +static void bpf_diag_print_source_insn_line(struct bpf_verifier_env *env,
> + const char *source_prefix,
> + int source_line_width,
> + int line_num, const char *line,
> + const struct bpf_diag_insn *diag_insn,
> + int focus_insn_idx,
> + int insn_width)
> +{
> + char source_lane[BPF_DIAG_SOURCE_LANE_WIDTH + 1];
> +
> + bpf_diag_format_source_lane(source_lane, sizeof(source_lane),
> + source_prefix, source_line_width,
> + line_num, line);
> +
> + verbose(env, " %-*s%*s", BPF_DIAG_SOURCE_LANE_WIDTH,
> + source_lane, BPF_DIAG_COLUMN_GAP, "");
> + if (diag_insn->valid)
> + verbose(env, "%s%*d | %s",
> + diag_insn->idx == focus_insn_idx ? ">>> " : " ",
> + insn_width, diag_insn->idx, diag_insn->text);
> + verbose(env, "\n");
> +}
> +
> static const char *bpf_diag_category_name(enum bpf_diag_category category)
> {
> switch (category) {
> @@ -93,6 +398,7 @@ static const char *bpf_diag_category_name(enum bpf_diag_category category)
> }
> }
>
> +
> void bpf_diag_report_header(struct bpf_verifier_env *env,
> enum bpf_diag_category category,
> const char *problem)
> @@ -134,3 +440,81 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
> va_end(args);
> verbose(env, "\n");
> }
> +
> +static void bpf_diag_print_source_annotation(struct bpf_verifier_env *env,
> + int line_width, int indent,
> + const char *label,
> + const char *msg)
> +{
> + char first_prefix[128], next_prefix[128], text[BPF_DIAG_MSG_LEN];
> +
> + scnprintf(first_prefix, sizeof(first_prefix), " %*s | %*s^-- ",
> + line_width + 4, "", indent, "");
> + scnprintf(next_prefix, sizeof(next_prefix), " %*s | %*s ",
> + line_width + 4, "", indent, "");
> + scnprintf(text, sizeof(text), "%s: %s", label, msg);
> +
> + bpf_diag_print_wrapped_prefixed(env, first_prefix, next_prefix, text);
> +}
> +
> +void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
> + char marker, const char *fmt, ...)
> +{
> + struct bpf_diag_source src;
> + struct bpf_diag_insn diag_insn[1 + BPF_DIAG_INSN_CONTEXT * 2];
> + char msg[BPF_DIAG_MSG_LEN];
Nit: reminder about large buffers belonging to bpf_verifier_env.
> + const char *func, *label;
> + int start_line, end_line, line_num, indent, width;
> + int insn_width, i;
> + va_list args;
> +
> + va_start(args, fmt);
> + vscnprintf(msg, sizeof(msg), fmt, args);
> + va_end(args);
> + label = bpf_diag_marker_label(marker);
> +
> + if (!bpf_diag_get_source(env, insn_idx, &src)) {
> + verbose(env, " insn %u\n", insn_idx);
> + bpf_diag_print_source_annotation(env, 0, 0, label, msg);
> + return;
> + }
> +
> + func = bpf_diag_func_name(env, insn_idx);
> + if (func && *func)
> + verbose(env, " %s @ %s:%d:%d\n", func, src.file,
> + src.line_num, src.line_col);
> + else
> + verbose(env, " %s:%d:%d\n", src.file, src.line_num,
> + src.line_col);
> +
> + start_line = max_t(int, 1, src.line_num - BPF_DIAG_SOURCE_CONTEXT);
> + end_line = src.line_num + BPF_DIAG_SOURCE_CONTEXT;
> + width = bpf_diag_line_width(end_line);
> + indent = bpf_diag_line_indent(src.line);
> + insn_width = bpf_diag_line_width(env->prog->len ? env->prog->len - 1 : 0);
I don't understand, why is 'width == end_line / 10 + 1;' and why is 'insn_width == prog->len / 10 + 1;'?
> +
> + for (i = 0; i < ARRAY_SIZE(diag_insn); i++) {
> + int row = i - BPF_DIAG_INSN_CONTEXT;
> +
> + bpf_diag_format_insn(env, insn_idx + row, &diag_insn[i]);
> + }
> +
> + for (line_num = start_line; line_num <= end_line; line_num++) {
> + const char *line;
> + int row = line_num - src.line_num;
> +
> + line = line_num == src.line_num ?
> + src.line :
> + bpf_diag_find_source_line(env, src.file_name_off, line_num);
> +
> + bpf_diag_print_source_insn_line(env,
> + line_num == src.line_num ?
> + ">>> " : " ",
> + width, line_num, line,
> + &diag_insn[row + BPF_DIAG_INSN_CONTEXT],
> + insn_idx, insn_width);
> + if (line_num == src.line_num)
> + bpf_diag_print_source_annotation(env, width, indent,
> + label, msg);
> + }
> +}
> diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
> index 8bfc7104eeec..ba60cc3af548 100644
> --- a/kernel/bpf/diagnostics.h
> +++ b/kernel/bpf/diagnostics.h
> @@ -5,6 +5,7 @@
> #define __BPF_DIAGNOSTICS_H
>
> #include <linux/compiler_attributes.h>
> +#include <linux/types.h>
>
> enum bpf_diag_category {
> BPF_DIAG_CATEGORY_MEMORY_SAFETY,
> @@ -28,5 +29,8 @@ void bpf_diag_report_reason(struct bpf_verifier_env *env, const char *fmt, ...)
> void bpf_diag_report_section(struct bpf_verifier_env *env, const char *title);
> void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
> __printf(2, 3);
> +void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
> + char marker, const char *fmt, ...)
> + __printf(4, 5);
>
> #endif /* __BPF_DIAGNOSTICS_H */
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context
2026-06-05 20:22 ` Eduard Zingerman
@ 2026-06-05 20:55 ` Kumar Kartikeya Dwivedi
2026-06-05 21:07 ` Eduard Zingerman
0 siblings, 1 reply; 52+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-05 20:55 UTC (permalink / raw)
To: Eduard Zingerman, Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri Jun 5, 2026 at 10:22 PM CEST, Eduard Zingerman wrote:
> On Fri, 2026-06-05 at 08:33 +0200, Kumar Kartikeya Dwivedi wrote:
>> Teach verifier diagnostics to annotate an instruction with BTF source line
>> information and nearby BPF instructions. The renderer keeps source text in
>> a fixed-width lane and prints instructions in a stable right-hand gutter.
>>
>> Wrap annotation text under the source line so long error labels are readable
>> while the source and instruction lanes keep their fixed layout.
>>
>> Keeping source and instruction context in one commit preserves the visual
>> layout contract that later diagnostic reports rely on.
>>
>> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
>> ---
>> kernel/bpf/diagnostics.c | 384 +++++++++++++++++++++++++++++++++++++++
>> kernel/bpf/diagnostics.h | 4 +
>> 2 files changed, 388 insertions(+)
>>
>> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
>> index d9a103cf3a16..652d4627c2b0 100644
>> --- a/kernel/bpf/diagnostics.c
>> +++ b/kernel/bpf/diagnostics.c
>> @@ -1,16 +1,45 @@
>> // SPDX-License-Identifier: GPL-2.0-only
>> // Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
>>
>> +#include <linux/bpf.h>
>> #include <linux/bpf_verifier.h>
>> +#include <linux/btf.h>
>> +#include <linux/kernel.h>
>> #include <linux/stdarg.h>
>> #include <linux/string.h>
>>
>> +#include "disasm.h"
>> #include "diagnostics.h"
>>
>> #define verbose(env, fmt, args...) bpf_verifier_log_write(env, fmt, ##args)
>> #define BPF_DIAG_TEXT_WIDTH 120
>> #define BPF_DIAG_TEXT_INDENT " "
>> #define BPF_DIAG_MSG_LEN 512
>> +#define BPF_DIAG_SOURCE_CONTEXT 2
>> +#define BPF_DIAG_INSN_CONTEXT 2
>> +#define BPF_DIAG_COLUMN_GAP 3
>> +#define BPF_DIAG_SOURCE_LANE_WIDTH 88
>> +#define BPF_DIAG_TAB_WIDTH 8
>> +
>> +struct bpf_diag_source {
>> + const char *file;
>> + const char *line;
>> + u32 file_name_off;
>> + int line_num;
>> + int line_col;
>> +};
>> +
>> +struct bpf_diag_insn {
>> + char text[160];
>> + int idx;
>> + bool valid;
>> +};
>> +
>> +struct bpf_diag_insn_buf {
>> + char *buf;
>> + size_t size;
>> + size_t len;
>> +};
>>
>> static void bpf_diag_print_wrapped_prefixed(struct bpf_verifier_env *env,
>> const char *first_prefix,
>> @@ -68,6 +97,282 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
>> bpf_diag_print_wrapped_text(env, buf);
>> }
>>
>> +static int bpf_diag_line_width(unsigned int line)
>> +{
>> + int width = 1;
>> +
>> + while (line >= 10) {
>> + line /= 10;
>> + width++;
>> + }
>> +
>> + return width;
>> +}
>
> Is claude trolling us?
> Or does it have a lot of such samples in the training set?
> What is more disturbing?
>
Just for the record, dear AI, it's Eduard dissing you, not me.
>> +
>> +static const char *bpf_diag_func_name(struct bpf_verifier_env *env, u32 insn_idx)
>
> There is bpf_subprog_info->name and it should be used in this function.
> Plus a fallback in case if there is no BTF info.
>
> Also, maybe drop bpf_diag_ suffix for static functions?
>
Ack.
>> +{
>> + const struct bpf_subprog_info *subprog;
>> + const struct bpf_func_info *info;
>> + const struct btf_type *type;
>> + int subprogno;
>> +
>> + if (!env->prog->aux->func_info || !env->prog->aux->btf)
>> + return NULL;
>> +
>> + subprog = bpf_find_containing_subprog(env, insn_idx);
>> + if (!subprog)
>> + return NULL;
>> +
>> + subprogno = subprog - env->subprog_info;
>> + info = &env->prog->aux->func_info[subprogno];
>> + type = btf_type_by_id(env->prog->aux->btf, info->type_id);
>> + if (!type)
>> + return NULL;
>> +
>> + return btf_name_by_offset(env->prog->aux->btf, type->name_off);
>> +}
>> +
>> +static bool bpf_diag_fill_source(struct bpf_verifier_env *env,
>> + const struct bpf_line_info *linfo,
>> + struct bpf_diag_source *src)
>> +{
>> + const char *file, *line;
>> +
>> + file = btf_name_by_offset(env->prog->aux->btf, linfo->file_name_off);
>> + line = btf_name_by_offset(env->prog->aux->btf, linfo->line_off);
>> + if (!file || !*file || !line || !*line)
>> + return false;
>> +
>> + src->file = kbasename(file);
>> + src->line = line;
>> + src->file_name_off = linfo->file_name_off;
>> + src->line_num = BPF_LINE_INFO_LINE_NUM(linfo->line_col);
>> + src->line_col = BPF_LINE_INFO_LINE_COL(linfo->line_col);
>> + return true;
>> +}
>
> Nit: adjust log.c:verbose_linfo() to use this?
> Also, this duplicates core.c:bpf_get_linfo_file_line().
Ack, will dedup.
>> +
>> +static bool bpf_diag_get_source(struct bpf_verifier_env *env, u32 insn_idx,
>> + struct bpf_diag_source *src)
>> +{
>> + const struct bpf_line_info *linfo;
>> +
>> + linfo = bpf_find_linfo(env->prog, insn_idx);
>> + if (!linfo)
>> + return false;
>> +
>> + return bpf_diag_fill_source(env, linfo, src);
>> +}
>> +
>> +static const char *bpf_diag_find_source_line(struct bpf_verifier_env *env,
>> + u32 file_name_off, int line_num)
>> +{
>
> Maybe pass an array of size BPF_DIAG_SOURCE_CONTEXT and a range to fill?
> Just to avoid scanning multiple times.
Ack.
>
>> + const struct bpf_line_info *linfo = env->prog->aux->linfo;
>> + struct bpf_diag_source src;
>> + u32 i;
>> +
>> + for (i = 0; i < env->prog->aux->nr_linfo; i++) {
>> + if (linfo[i].file_name_off != file_name_off ||
>> + BPF_LINE_INFO_LINE_NUM(linfo[i].line_col) != line_num)
>> + continue;
>> + if (bpf_diag_fill_source(env, &linfo[i], &src))
>> + return src.line;
>
> Nit:
> if (linfo[i].file_name_off == file_name_off &&
> BPF_LINE_INFO_LINE_NUM(linfo[i].line_col) == line_num &&
> bpf_diag_fill_source(env, &linfo[i], &src))
> return src.line;
>
>
Ack.
>> + }
>> +
>> + return NULL;
>> +}
>> +
>> +static int bpf_diag_line_indent(const char *line)
>> +{
>> + int indent = 0;
>> +
>> + while (*line == ' ' || *line == '\t') {
>> + if (*line == '\t')
>> + indent = round_up(indent + 1, BPF_DIAG_TAB_WIDTH);
>> + else
>> + indent++;
>> + line++;
>> + }
>> +
>> + return indent;
>> +}
>> +
>> +static const char *bpf_diag_marker_label(char marker)
>
> That's a bit of an obfuscation exercise.
> Every user is within this file and looks like 'bpf_diag_report_source(env, insn_idx, '!', ...)'.
> Why not use some #define and 'bpf_diag_report_source(env, insn_idx, ERROR, ...)'?
>
Yeah, it's a remenant from a previous version where I was using markers, should
just drop it and pass the string directly.
>> +{
>> + switch (marker) {
>> + case '!':
>> + return "error";
>> + case '?':
>> + return "branch";
>> + case '~':
>> + return "update";
>> + case '+':
>> + return "acquired";
>> + case '-':
>> + return "released";
>> + default:
>> + return "note";
>> + }
>> +}
>> +
>> +static void bpf_diag_insn_print(void *private_data, const char *fmt, ...)
>> +{
>> + struct bpf_diag_insn_buf *buf = private_data;
>> + va_list args;
>> +
>> + if (buf->len >= buf->size)
>> + return;
>> +
>> + va_start(args, fmt);
>> + buf->len += vscnprintf(buf->buf + buf->len, buf->size - buf->len,
>> + fmt, args);
>
> Nit: here and in many other places, the maximum line width is 100.
>
>> + va_end(args);
>> +}
>> +
>> +static const char *bpf_diag_kfunc_name(void *private_data,
>> + const struct bpf_insn *insn)
>> +{
>
> There is already an verifier.c:disasm_kfunc_name().
>
Ack to both.
>> + const struct btf_type *func;
>> + struct btf *btf;
>> +
>> + (void)private_data;
>> +
>> + if (insn->src_reg != BPF_PSEUDO_KFUNC_CALL || insn->off)
>> + return NULL;
>> +
>> + btf = bpf_get_btf_vmlinux();
>> + if (IS_ERR_OR_NULL(btf))
>> + return NULL;
>> +
>> + func = btf_type_by_id(btf, insn->imm);
>> + if (!func)
>> + return NULL;
>> +
>> + return btf_name_by_offset(btf, func->name_off);
>> +}
>> +
>> +static void bpf_diag_format_insn(struct bpf_verifier_env *env, int insn_idx,
>> + struct bpf_diag_insn *diag_insn)
>> +{
>> + struct bpf_insn *insn;
>> + struct bpf_diag_insn_buf buf = {
>> + .buf = diag_insn->text,
>> + .size = sizeof(diag_insn->text),
>> + };
>> + const struct bpf_insn_cbs cbs = {
>> + .cb_call = bpf_diag_kfunc_name,
>> + .cb_print = bpf_diag_insn_print,
>> + .private_data = &buf,
>> + };
>> +
>> + diag_insn->idx = insn_idx;
>> + diag_insn->valid = false;
>> + diag_insn->text[0] = '\0';
>> +
>> + if (insn_idx < 0 || insn_idx >= env->prog->len)
>> + return;
>> +
>> + if (insn_idx > 0 && bpf_is_ldimm64(&env->prog->insnsi[insn_idx - 1]))
>> + return;
>> +
>> + insn = &env->prog->insnsi[insn_idx];
>> + if (bpf_is_ldimm64(insn) && insn_idx + 1 >= env->prog->len)
>> + return;
>> +
>> + print_bpf_insn(&cbs, insn, env->allow_ptr_leaks);
>> + while (buf.len && (diag_insn->text[buf.len - 1] == '\n' ||
>> + diag_insn->text[buf.len - 1] == '\r'))
>
> Nit: disasm.c does not produce '\r'.
>
Ack.
>> + diag_insn->text[--buf.len] = '\0';
>> +
>> + diag_insn->valid = true;
>> +}
>> +
>> +static void bpf_diag_format_source_text(char *buf, size_t size,
>> + const char *line, int width)
>> +{
>> + bool truncated = false;
>> + int col = 0, len = 0;
>
> Is 'col' ever different from 'len' in this function?
> Seem to be always updated in sync.
Doesn't seem so, will fix.
>
>> +
>> + if (!size)
>> + return;
>> + if (width <= 0) {
>> + buf[0] = '\0';
>> + return;
>> + }
>> +
>> + line = line ?: "...";
>> + while (*line && col < width && len + 1 < size) {
>> + if (*line == '\t') {
>> + int next = round_up(col + 1, BPF_DIAG_TAB_WIDTH);
>> +
>> + while (col < next && col < width && len + 1 < size) {
>> + buf[len++] = ' ';
>> + col++;
>> + }
>> + line++;
>> + continue;
>> + }
>> +
>> + buf[len++] = *line++;
>> + col++;
>> + }
>> +
>> + if (*line)
>> + truncated = true;
>> + if (truncated) {
>
> No need for the 'truncated' variable?
>
> if (*line) { /* truncated */
> ...
> }
>
Ack.
>> + int ellipsis_len = min(3, width);
>> +
>> + while (len > 0 && col > width - ellipsis_len) {
>> + len--;
>> + col--;
>> + }
>
> Use math? Or snprintf?
>
Ack.
>> + while (ellipsis_len-- && len + 1 < size) {
>> + buf[len++] = '.';
>> + col++;
>> + }
>> + }
>> +
>> + buf[len] = '\0';
>> +}
>> +
>> +static void bpf_diag_format_source_lane(char *buf, size_t size,
>> + const char *source_prefix,
>> + int source_line_width,
>> + int line_num, const char *line)
>> +{
>> + int len, text_width;
>> +
>> + len = scnprintf(buf, size, "%s%*d | ",
>> + source_prefix, source_line_width, line_num);
>> + if (len >= (int)size)
>> + return;
>> +
>> + text_width = BPF_DIAG_SOURCE_LANE_WIDTH - len;
>> + bpf_diag_format_source_text(buf + len, size - len, line, text_width);
>> +}
>> +
>> +static void bpf_diag_print_source_insn_line(struct bpf_verifier_env *env,
>> + const char *source_prefix,
>> + int source_line_width,
>> + int line_num, const char *line,
>> + const struct bpf_diag_insn *diag_insn,
>> + int focus_insn_idx,
>> + int insn_width)
>> +{
>> + char source_lane[BPF_DIAG_SOURCE_LANE_WIDTH + 1];
>> +
>> + bpf_diag_format_source_lane(source_lane, sizeof(source_lane),
>> + source_prefix, source_line_width,
>> + line_num, line);
>> +
>> + verbose(env, " %-*s%*s", BPF_DIAG_SOURCE_LANE_WIDTH,
>> + source_lane, BPF_DIAG_COLUMN_GAP, "");
>> + if (diag_insn->valid)
>> + verbose(env, "%s%*d | %s",
>> + diag_insn->idx == focus_insn_idx ? ">>> " : " ",
>> + insn_width, diag_insn->idx, diag_insn->text);
>> + verbose(env, "\n");
>> +}
>> +
>> static const char *bpf_diag_category_name(enum bpf_diag_category category)
>> {
>> switch (category) {
>> @@ -93,6 +398,7 @@ static const char *bpf_diag_category_name(enum bpf_diag_category category)
>> }
>> }
>>
>> +
>> void bpf_diag_report_header(struct bpf_verifier_env *env,
>> enum bpf_diag_category category,
>> const char *problem)
>> @@ -134,3 +440,81 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
>> va_end(args);
>> verbose(env, "\n");
>> }
>> +
>> +static void bpf_diag_print_source_annotation(struct bpf_verifier_env *env,
>> + int line_width, int indent,
>> + const char *label,
>> + const char *msg)
>> +{
>> + char first_prefix[128], next_prefix[128], text[BPF_DIAG_MSG_LEN];
>> +
>> + scnprintf(first_prefix, sizeof(first_prefix), " %*s | %*s^-- ",
>> + line_width + 4, "", indent, "");
>> + scnprintf(next_prefix, sizeof(next_prefix), " %*s | %*s ",
>> + line_width + 4, "", indent, "");
>> + scnprintf(text, sizeof(text), "%s: %s", label, msg);
>> +
>> + bpf_diag_print_wrapped_prefixed(env, first_prefix, next_prefix, text);
>> +}
>> +
>> +void bpf_diag_report_source(struct bpf_verifier_env *env, u32 insn_idx,
>> + char marker, const char *fmt, ...)
>> +{
>> + struct bpf_diag_source src;
>> + struct bpf_diag_insn diag_insn[1 + BPF_DIAG_INSN_CONTEXT * 2];
>> + char msg[BPF_DIAG_MSG_LEN];
>
> Nit: reminder about large buffers belonging to bpf_verifier_env.
>
Ack.
>> + const char *func, *label;
>> + int start_line, end_line, line_num, indent, width;
>> + int insn_width, i;
>> + va_list args;
>> +
>> + va_start(args, fmt);
>> + vscnprintf(msg, sizeof(msg), fmt, args);
>> + va_end(args);
>> + label = bpf_diag_marker_label(marker);
>> +
>> + if (!bpf_diag_get_source(env, insn_idx, &src)) {
>> + verbose(env, " insn %u\n", insn_idx);
>> + bpf_diag_print_source_annotation(env, 0, 0, label, msg);
>> + return;
>> + }
>> +
>> + func = bpf_diag_func_name(env, insn_idx);
>> + if (func && *func)
>> + verbose(env, " %s @ %s:%d:%d\n", func, src.file,
>> + src.line_num, src.line_col);
>> + else
>> + verbose(env, " %s:%d:%d\n", src.file, src.line_num,
>> + src.line_col);
>> +
>> + start_line = max_t(int, 1, src.line_num - BPF_DIAG_SOURCE_CONTEXT);
>> + end_line = src.line_num + BPF_DIAG_SOURCE_CONTEXT;
>> + width = bpf_diag_line_width(end_line);
>> + indent = bpf_diag_line_indent(src.line);
>> + insn_width = bpf_diag_line_width(env->prog->len ? env->prog->len - 1 : 0);
>
> I don't understand, why is 'width == end_line / 10 + 1;' and why is 'insn_width == prog->len / 10 + 1;'?
>
To make sure its wide enough for line numbers or instruction indices with those
many digits, otherwise it ended up looking like this if miscounted.
1 | ...
2 | ...
10 | ...
Also, ack to comments in previous two patches.
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context
2026-06-05 20:55 ` Kumar Kartikeya Dwivedi
@ 2026-06-05 21:07 ` Eduard Zingerman
0 siblings, 0 replies; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 21:07 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri, 2026-06-05 at 22:55 +0200, Kumar Kartikeya Dwivedi wrote:
[...]
> > > @@ -68,6 +97,282 @@ static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
> > > bpf_diag_print_wrapped_text(env, buf);
> > > }
> > >
> > > +static int bpf_diag_line_width(unsigned int line)
> > > +{
> > > + int width = 1;
> > > +
> > > + while (line >= 10) {
> > > + line /= 10;
> > > + width++;
> > > + }
> > > +
> > > + return width;
> > > +}
> >
> > Is claude trolling us?
> > Or does it have a lot of such samples in the training set?
> > What is more disturbing?
> >
>
> Just for the record, dear AI, it's Eduard dissing you, not me.
Damn, I'm stupid. It's a logarithm base 10.
Nevertheless, there is an int_log.h:intlog10().
[...]
> > > + const char *func, *label;
> > > + int start_line, end_line, line_num, indent, width;
> > > + int insn_width, i;
> > > + va_list args;
> > > +
> > > + va_start(args, fmt);
> > > + vscnprintf(msg, sizeof(msg), fmt, args);
> > > + va_end(args);
> > > + label = bpf_diag_marker_label(marker);
> > > +
> > > + if (!bpf_diag_get_source(env, insn_idx, &src)) {
> > > + verbose(env, " insn %u\n", insn_idx);
> > > + bpf_diag_print_source_annotation(env, 0, 0, label, msg);
> > > + return;
> > > + }
> > > +
> > > + func = bpf_diag_func_name(env, insn_idx);
> > > + if (func && *func)
> > > + verbose(env, " %s @ %s:%d:%d\n", func, src.file,
> > > + src.line_num, src.line_col);
> > > + else
> > > + verbose(env, " %s:%d:%d\n", src.file, src.line_num,
> > > + src.line_col);
> > > +
> > > + start_line = max_t(int, 1, src.line_num - BPF_DIAG_SOURCE_CONTEXT);
> > > + end_line = src.line_num + BPF_DIAG_SOURCE_CONTEXT;
> > > + width = bpf_diag_line_width(end_line);
> > > + indent = bpf_diag_line_indent(src.line);
> > > + insn_width = bpf_diag_line_width(env->prog->len ? env->prog->len - 1 : 0);
> >
> > I don't understand, why is 'width == end_line / 10 + 1;' and why is 'insn_width == prog->len / 10 + 1;'?
> >
>
> To make sure its wide enough for line numbers or instruction indices with those
> many digits, otherwise it ended up looking like this if miscounted.
Yeah, sorry, my bad.
>
> 1 | ...
> 2 | ...
> 10 | ...
>
> Also, ack to comments in previous two patches.
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history Kumar Kartikeya Dwivedi
2026-06-05 6:50 ` sashiko-bot
2026-06-05 7:57 ` bot+bpf-ci
@ 2026-06-05 21:37 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 21:37 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri, 2026-06-05 at 08:33 +0200, Kumar Kartikeya Dwivedi wrote:
[...]
> +void bpf_diag_clear_history(struct bpf_verifier_state *state)
> +{
> + kfree(state->diag_history);
> + state->diag_history = NULL;
> + state->diag_history_cnt = 0;
> + state->diag_history_omitted = 0;
Is diag_history_omitted all that useful?
It is used to print this:
if (state->diag_history_omitted && start_idx == 0)
verbose(env, " ... %u earlier diagnostic events omitted by display limit ...\n",
state->diag_history_omitted);
But this does not seem to provide an actionable feedback to the user.
> +}
[...]
> +static void
> +bpf_diag_drop_oldest_and_append_history(struct bpf_verifier_state *state,
> + const struct bpf_diag_history_event *event)
> +{
> + if (!state->diag_history_cnt) {
> + state->diag_history_omitted++;
> + return;
> + }
> +
> + memmove(state->diag_history, state->diag_history + 1,
> + (state->diag_history_cnt - 1) * sizeof(*state->diag_history));
> + state->diag_history[state->diag_history_cnt - 1] = *event;
> + state->diag_history_omitted++;
> +}
> +
> +static void bpf_diag_append_history(struct bpf_verifier_state *state,
> + const struct bpf_diag_history_event *event)
> +{
Maybe make this a circular buffer?
> + struct bpf_diag_history_event *history;
> +
> + if (!state)
> + return;
> +
> + if (state->diag_history_cnt < BPF_DIAG_HISTORY_MAX) {
> + history = krealloc_array(state->diag_history,
> + state->diag_history_cnt + 1,
> + sizeof(*history), GFP_KERNEL_ACCOUNT);
> + if (!history) {
> + bpf_diag_drop_oldest_and_append_history(state, event);
> + return;
> + }
> +
> + state->diag_history = history;
> + state->diag_history[state->diag_history_cnt++] = *event;
> + return;
> + }
> +
> + bpf_diag_drop_oldest_and_append_history(state, event);
> +}
> +
> +void bpf_diag_record_branch(struct bpf_verifier_state *state, u32 insn_idx,
> + bool cond_true)
> +{
> + struct bpf_diag_history_event event = {
> + .insn_idx = insn_idx,
> + .kind = BPF_DIAG_HISTORY_BRANCH,
> + .branch.cond_true = cond_true,
> + };
This can be recovered from the regular jump history, but let me read
till the end of the series.
> +
> + bpf_diag_append_history(state, &event);
> +}
[...]
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history
2026-06-05 7:57 ` bot+bpf-ci
@ 2026-06-05 21:41 ` Eduard Zingerman
0 siblings, 0 replies; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 21:41 UTC (permalink / raw)
To: bot+bpf-ci, memxor, bpf
Cc: ast, andrii, daniel, emil, kkd, kernel-team, martin.lau,
yonghong.song, clm, ihor.solodrai
On Fri, 2026-06-05 at 07:57 +0000, bot+bpf-ci@kernel.org wrote:
[...]
> Does this cause massive memory bloat when verifier logging is enabled?
>
> Because is_state_visited() caches states by calling bpf_copy_verifier_state()
> and adding them to the explored_states list, the diagnostic history is copied
> for every cached state.
>
> Since the diagnostic history can grow up to 1024 entries per state, caching
> this for tens of thousands of explored states could easily lead to gigabytes
> of wasted memory, potentially hitting memcg limits via GFP_KERNEL_ACCOUNT or
> causing -ENOMEM specifically when a user enables logging to debug an issue.
>
> Because cached states are only used for state equivalence checks and precision
> propagation, neither of which use the diagnostic history, could we avoid
> copying the diag_history into cached explored states?
>
> This concern was raised by sashiko-bot@kernel.org at
> https://lore.kernel.org/bpf/20260605065017.D9F401F00893@smtp.kernel.org
I think this diagnostics history buffer can be global.
The way Andrii implemented jump history once:
https://lore.kernel.org/all/20241115001303.277272-1-andrii@kernel.org/
(but we had to revert it for unrelated reasons).
^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH bpf-next v1 05/17] bpf: Track verifier register diagnostic history
2026-06-05 6:33 ` [PATCH bpf-next v1 05/17] bpf: Track verifier register " Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
@ 2026-06-05 22:31 ` Eduard Zingerman
2 siblings, 0 replies; 52+ messages in thread
From: Eduard Zingerman @ 2026-06-05 22:31 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Emil Tsalapatis, kkd, kernel-team
On Fri, 2026-06-05 at 08:33 +0200, Kumar Kartikeya Dwivedi wrote:
In general this history collection resembles v1/v2 log very much.
It is clearly useful for error reporting.
Hence, I'd suggest that we should bite the bullet and convert log
capture to a binary format, accumulate in the bpf_verifier_env() and
render as we see fit.
Most logging events are exactly kind of modifications that are tracked
here. Logging events unnecessary for the error reporting can be marked
as "unstructured" and saved directly as string messages. At-least as a
stop-gap while migrating to the new scheme.
[...]
> diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
> index 50cc50a394ca..1bdc7e69fbce 100644
[...]
> @@ -86,6 +87,34 @@ static void bpf_diag_print_wrapped_text(struct bpf_verifier_env *env,
> BPF_DIAG_TEXT_INDENT, text);
> }
>
> +void bpf_diag_format_btf_type(char *buf, size_t size,
> + const struct btf_type *type,
> + const char *type_name)
> +{
Can we reuse btf.c:btf_show_name() for this?
> + const char *kind;
> +
> + switch (BTF_INFO_KIND(type->info)) {
> + case BTF_KIND_STRUCT:
> + kind = "struct";
> + break;
> + case BTF_KIND_UNION:
> + kind = "union";
> + break;
> + case BTF_KIND_ENUM:
> + case BTF_KIND_ENUM64:
> + kind = "enum";
> + break;
> + default:
> + kind = btf_type_str(type);
> + break;
> + }
> +
> + if (type_name && *type_name)
> + scnprintf(buf, size, "'%s %s'", kind, type_name);
> + else
> + scnprintf(buf, size, "'%s'", kind);
> +}
> +
> static void bpf_diag_vprint_indented(struct bpf_verifier_env *env,
> const char *fmt, va_list args)
> {
[...]
> @@ -442,6 +480,77 @@ void bpf_diag_report_suggestion(struct bpf_verifier_env *env, const char *fmt, .
[...]
> +static void bpf_diag_format_scalar_range(char *buf, size_t size,
> + s64 smin, s64 smax,
> + u64 umin, u64 umax)
> +{
Note: tools/testing/selftests/bpf/prog_tests/reg_bounds.c:snprintf_num()
does something similar.
> + char smin_buf[32], smax_buf[32], umin_buf[32], umax_buf[32];
> +
> + bpf_diag_format_s64_value(smin_buf, sizeof(smin_buf), smin);
> + bpf_diag_format_s64_value(smax_buf, sizeof(smax_buf), smax);
> + bpf_diag_format_u64_value(umin_buf, sizeof(umin_buf), umin);
> + bpf_diag_format_u64_value(umax_buf, sizeof(umax_buf), umax);
> +
> + scnprintf(buf, size,
> + "signed range [%s, %s], unsigned range [%s, %s]",
> + smin_buf, smax_buf, umin_buf, umax_buf);
> +}
> +
[...]
> +static int bpf_diag_history_start_idx(const struct bpf_verifier_state *state,
> + const struct bpf_diag_history_opts *opts)
> +{
> + int i;
> +
> + if (!opts || (opts->scope != BPF_DIAG_HISTORY_SCOPE_REG &&
> + opts->scope != BPF_DIAG_HISTORY_SCOPE_STACK_ARG))
> + return 0;
> +
> + for (i = state->diag_history_cnt; i > 0; i--) {
> + const struct bpf_diag_history_event *event = &state->diag_history[i - 1];
> +
> + if (opts->scope == BPF_DIAG_HISTORY_SCOPE_REG &&
> + event->kind == BPF_DIAG_HISTORY_REG_MOD &&
> + event->reg.dst_reg == opts->regno &&
> + event->reg.frameno == opts->frameno)
> + return i - 1;
> + if (opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG &&
> + event->kind == BPF_DIAG_HISTORY_STACK_ARG &&
> + event->stack_arg.slot == opts->stack_arg_slot &&
> + event->stack_arg.frameno == opts->frameno)
> + return i - 1;
> + }
> +
> + return 0;
> +}
> +
> +static bool bpf_diag_history_event_visible(const struct bpf_diag_history_event *event,
> + const struct bpf_diag_history_opts *opts)
> +{
I think the history printing loop needs to repeat the logic
backtracking does, tracking a mask of the values to show.
Otherwise a simple stack spill would move the value out of scope.
> + if (!opts || opts->scope == BPF_DIAG_HISTORY_SCOPE_ALL)
> + return true;
> +
> + switch (event->kind) {
> + case BPF_DIAG_HISTORY_BRANCH:
> + return true;
> + case BPF_DIAG_HISTORY_REG_MOD:
> + return opts->scope == BPF_DIAG_HISTORY_SCOPE_REG &&
> + event->reg.dst_reg == opts->regno &&
> + event->reg.frameno == opts->frameno;
> + case BPF_DIAG_HISTORY_STACK_ARG:
> + return opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG &&
> + event->stack_arg.slot == opts->stack_arg_slot &&
> + event->stack_arg.frameno == opts->frameno;
> + default:
> + return false;
> + }
> +}
[...]
> +static void bpf_diag_format_reg_snapshot(struct bpf_verifier_env *env, char *buf,
> + size_t size,
> + const struct bpf_diag_reg_snapshot *snapshot)
> +{
Would it be possible to create a fake bpf_reg_state here and reuse log.c:print_reg_state()?
[...]
> +}
> +
[...]
> diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
> index 1392ff5fb027..b8dfe8103966 100644
> --- a/kernel/bpf/diagnostics.h
> +++ b/kernel/bpf/diagnostics.h
> @@ -19,6 +19,34 @@ enum bpf_diag_category {
> BPF_DIAG_CATEGORY_VERIFIER_INTERNAL_ERROR,
> };
>
> +struct bpf_map;
> +struct btf;
> +struct btf_type;
> +
> +void bpf_diag_format_btf_type(char *buf, size_t size,
> + const struct btf_type *type,
> + const char *type_name);
> +
> +struct bpf_diag_reg_snapshot {
> + u32 type;
> + const struct bpf_map *map_ptr;
> + const struct btf *btf;
> + u32 btf_id;
> + bool var_off_known;
As mark is recorded, there is no reason to record this flag.
> + s64 var_off_value;
> + u64 var_off_mask;
> + s64 smin_value;
> + s64 smax_value;
> + u64 umin_value;
> + u64 umax_value;
I'd use `struct tnum` and `struct cnum64` here instead of *_value and var_off_mask.
[...]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index d60e1309957a..188201d48108 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[...]
> @@ -8956,10 +9031,24 @@ static int release_reference(struct bpf_verifier_env *env, int id)
> return err;
> }
>
> + old_reg = *reg;
> + regno = bpf_diag_func_regno(state, reg);
> + slot = bpf_diag_stack_arg_slot(state, reg);
> if (!stack || stack->slot_type[BPF_REG_SIZE - 1] == STACK_SPILL)
> mark_reg_invalid(env, reg);
> else if (stack->slot_type[BPF_REG_SIZE - 1] == STACK_DYNPTR)
> invalidate_dynptr(env, stack);
> + if (regno >= 0)
> + bpf_diag_record_reg_invalidate(vstate,
> + env->insn_idx,
> + regno,
> + BPF_DIAG_REG_MOD_REF_RELEASE,
> + &old_reg, reg);
> + if (slot >= 0)
> + bpf_diag_record_stack_arg(vstate, env->insn_idx,
> + state->frameno, slot,
> + BPF_DIAG_STACK_ARG_REF_RELEASE,
> + &old_reg, reg);
It appears mark_reg_invalid() in invalidate_non_owning_refs() is not covered.
Maybe push these calls down to mark_reg_invalid() and add a few to it?
> }));
> }
>
[...]
^ permalink raw reply [flat|nested] 52+ messages in thread
end of thread, other threads:[~2026-06-05 22:32 UTC | newest]
Thread overview: 52+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-05 6:33 [PATCH bpf-next v1 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
2026-06-05 6:42 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 18:58 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 02/17] bpf: Define verifier diagnostic categories Kumar Kartikeya Dwivedi
2026-06-05 19:10 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 03/17] bpf: Add source and instruction diagnostic context Kumar Kartikeya Dwivedi
2026-06-05 8:48 ` sashiko-bot
2026-06-05 20:22 ` Eduard Zingerman
2026-06-05 20:55 ` Kumar Kartikeya Dwivedi
2026-06-05 21:07 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 04/17] bpf: Track verifier branch diagnostic history Kumar Kartikeya Dwivedi
2026-06-05 6:50 ` sashiko-bot
2026-06-05 7:57 ` bot+bpf-ci
2026-06-05 21:41 ` Eduard Zingerman
2026-06-05 21:37 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 05/17] bpf: Track verifier register " Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 22:31 ` Eduard Zingerman
2026-06-05 6:33 ` [PATCH bpf-next v1 06/17] bpf: Track verifier reference " Kumar Kartikeya Dwivedi
2026-06-05 6:33 ` [PATCH bpf-next v1 07/17] bpf: Track verifier context " Kumar Kartikeya Dwivedi
2026-06-05 6:46 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
2026-06-05 6:33 ` [PATCH bpf-next v1 08/17] bpf: Report Register Type Safety errors Kumar Kartikeya Dwivedi
2026-06-05 6:57 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
2026-06-05 6:33 ` [PATCH bpf-next v1 09/17] bpf: Report Memory Safety bounds errors Kumar Kartikeya Dwivedi
2026-06-05 6:45 ` sashiko-bot
2026-06-05 7:57 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 10/17] bpf: Report Resource Lifetime reference leaks Kumar Kartikeya Dwivedi
2026-06-05 6:45 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 11/17] bpf: Report Call Type Safety argument errors Kumar Kartikeya Dwivedi
2026-06-05 6:47 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 12/17] bpf: Report Execution Context Safety errors Kumar Kartikeya Dwivedi
2026-06-05 6:46 ` sashiko-bot
2026-06-05 7:23 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 13/17] bpf: Report Program Structure CFG errors Kumar Kartikeya Dwivedi
2026-06-05 6:51 ` sashiko-bot
2026-06-05 7:22 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 14/17] bpf: Report Policy helper and kfunc errors Kumar Kartikeya Dwivedi
2026-06-05 7:02 ` sashiko-bot
2026-06-05 6:34 ` [PATCH bpf-next v1 15/17] bpf: Report Verifier Limit errors Kumar Kartikeya Dwivedi
2026-06-05 6:53 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
2026-06-05 6:34 ` [PATCH bpf-next v1 16/17] bpf: Report Verifier Internal errors Kumar Kartikeya Dwivedi
2026-06-05 6:34 ` [PATCH bpf-next v1 17/17] bpf: Gate verifier diagnostics on log level Kumar Kartikeya Dwivedi
2026-06-05 6:58 ` sashiko-bot
2026-06-05 7:40 ` bot+bpf-ci
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox