From: Kumar Kartikeya Dwivedi <memxor@gmail.com>
To: bpf@vger.kernel.org
Cc: Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andrii@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Eduard Zingerman <eddyz87@gmail.com>,
Emil Tsalapatis <emil@etsalapatis.com>,
kkd@meta.com, kernel-team@meta.com
Subject: [PATCH bpf-next v2 10/17] bpf: Report Resource Lifetime reference leaks
Date: Fri, 19 Jun 2026 22:59:23 +0200 [thread overview]
Message-ID: <20260619205934.1312876-11-memxor@gmail.com> (raw)
In-Reply-To: <20260619205934.1312876-1-memxor@gmail.com>
Augment selected Resource Lifetime Safety failures with structured diagnostics
while preserving the existing verifier messages.
Report unreleased references from check_reference_leak() using
reference-scoped diagnostic history, and add state reports for dynptr,
iterator, lock, and IRQ-flag lifetime misuse.
IRQ restore mismatch and out-of-order diagnostics use IRQ context-scoped
history when an IRQ-disabled region is active, so retained save/restore context
is still visible after per-state history removal.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/diagnostics.c | 65 ++++++++++++++++++++++++++++
kernel/bpf/diagnostics.h | 11 +++++
kernel/bpf/verifier.c | 92 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 168 insertions(+)
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 933540eb105b..e9c58f84ec89 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -1702,6 +1702,71 @@ 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, "error", "%s", problem);
+
+ bpf_diag_report_suggestion(env, "%s", suggestion);
+}
+
+void bpf_diag_report_irq_resource_state(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason,
+ const char *suggestion,
+ u32 depth)
+{
+ struct bpf_diag_history_opts opts = {
+ .scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
+ .ctx_kind = BPF_DIAG_CONTEXT_IRQ,
+ .ctx_depth = depth,
+ };
+
+ 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, "error", "%s", problem);
+
+ if (depth)
+ bpf_diag_print_history(env, &opts);
+
+ 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, "error",
+ "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.");
+}
+
static void bpf_diag_format_var_offset(struct bpf_diag_reg_fmt *fmt,
char *buf, size_t size,
const struct bpf_diag_reg_snapshot *snapshot)
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index fea7731d431c..4e0bb27ea951 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -187,6 +187,17 @@ 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_irq_resource_state(struct bpf_verifier_env *env,
+ u32 insn_idx, const char *problem,
+ const char *reason,
+ const char *suggestion,
+ u32 depth);
+void bpf_diag_report_ref_leak(struct bpf_verifier_env *env, u32 ref_id,
+ u32 alloc_insn, u32 fail_insn);
void bpf_diag_record_branch(struct bpf_verifier_env *env, u32 insn_idx,
bool cond_true);
void bpf_diag_record_reg_mod(struct bpf_verifier_env *env, u32 insn_idx,
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index af04709c5178..db151e6b8949 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -835,6 +835,10 @@ 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;
}
@@ -1120,6 +1124,15 @@ static int unmark_stack_slot_irq_flag(struct bpf_verifier_env *env, struct bpf_r
verbose(env, "irq flag acquired by %s kfuncs cannot be restored with %s kfuncs\n",
flag_kfunc, used_kfunc);
+ bpf_diag_report_irq_resource_state(env, env->insn_idx,
+ "IRQ flag restore mismatch",
+ bpf_diag_scratch_printf(env,
+ 0,
+ "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),
+ "Restore the flag with the matching IRQ restore kfunc for the save operation that created it.",
+ env->cur_state->active_irq_id ? 1 : 0);
return -EINVAL;
}
@@ -1137,6 +1150,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_irq_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.",
+ env->cur_state->active_irq_id ? 1 : 0);
return err;
}
@@ -7258,11 +7276,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;
}
}
@@ -7289,6 +7315,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;
}
@@ -7300,14 +7330,26 @@ 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(env, 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",
+ "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;
}
@@ -7473,6 +7515,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;
}
@@ -7495,6 +7541,10 @@ 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;
}
@@ -7511,12 +7561,20 @@ 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;
}
@@ -7526,6 +7584,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;
}
@@ -7594,6 +7656,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;
}
@@ -7607,6 +7673,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);
@@ -7617,6 +7687,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;
}
@@ -7641,9 +7715,17 @@ 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;
@@ -10333,6 +10415,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;
@@ -11829,6 +11913,10 @@ 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;
}
@@ -11845,6 +11933,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
next prev parent reply other threads:[~2026-06-19 20:59 UTC|newest]
Thread overview: 34+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-19 20:59 [PATCH bpf-next v2 00/17] Redesign Verification Errors Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 01/17] bpf: Add verifier diagnostics report helpers Kumar Kartikeya Dwivedi
2026-06-19 21:09 ` sashiko-bot
2026-06-19 20:59 ` [PATCH bpf-next v2 02/17] bpf: Add source and instruction diagnostic context Kumar Kartikeya Dwivedi
2026-06-19 21:46 ` bot+bpf-ci
2026-06-19 20:59 ` [PATCH bpf-next v2 03/17] bpf: Add verifier diagnostic event log Kumar Kartikeya Dwivedi
2026-06-19 21:46 ` bot+bpf-ci
2026-06-19 20:59 ` [PATCH bpf-next v2 04/17] bpf: Prune verifier diagnostics on backtracking Kumar Kartikeya Dwivedi
2026-06-19 21:46 ` bot+bpf-ci
2026-06-19 20:59 ` [PATCH bpf-next v2 05/17] bpf: Track verifier register diagnostic events Kumar Kartikeya Dwivedi
2026-06-19 21:18 ` sashiko-bot
2026-06-19 23:35 ` Alexei Starovoitov
2026-06-19 20:59 ` [PATCH bpf-next v2 06/17] bpf: Track verifier reference " Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 07/17] bpf: Track verifier context " Kumar Kartikeya Dwivedi
2026-06-19 21:13 ` sashiko-bot
2026-06-19 21:19 ` Kumar Kartikeya Dwivedi
2026-06-19 21:46 ` bot+bpf-ci
2026-06-19 20:59 ` [PATCH bpf-next v2 08/17] bpf: Report Register Type Safety errors Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 09/17] bpf: Report Memory Safety bounds errors Kumar Kartikeya Dwivedi
2026-06-19 21:46 ` bot+bpf-ci
2026-06-19 23:40 ` Alexei Starovoitov
2026-06-19 20:59 ` Kumar Kartikeya Dwivedi [this message]
2026-06-19 21:12 ` [PATCH bpf-next v2 10/17] bpf: Report Resource Lifetime reference leaks sashiko-bot
2026-06-19 23:42 ` Alexei Starovoitov
2026-06-19 20:59 ` [PATCH bpf-next v2 11/17] bpf: Report Call Type Safety argument errors Kumar Kartikeya Dwivedi
2026-06-19 21:47 ` bot+bpf-ci
2026-06-19 20:59 ` [PATCH bpf-next v2 12/17] bpf: Report Execution Context Safety errors Kumar Kartikeya Dwivedi
2026-06-19 21:19 ` sashiko-bot
2026-06-19 23:44 ` Alexei Starovoitov
2026-06-19 20:59 ` [PATCH bpf-next v2 13/17] bpf: Report Program Structure CFG errors Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 14/17] bpf: Report Policy helper and kfunc errors Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 15/17] bpf: Report Verifier Limit errors Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 16/17] bpf: Report Verifier Internal errors Kumar Kartikeya Dwivedi
2026-06-19 20:59 ` [PATCH bpf-next v2 17/17] bpf: Gate verifier diagnostics on log level Kumar Kartikeya Dwivedi
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260619205934.1312876-11-memxor@gmail.com \
--to=memxor@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=kernel-team@meta.com \
--cc=kkd@meta.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.