Skip to contentSkip to conversation

bpf: Fix exception exit lock checking for subprogs

PATCH Applied Ihor Solodrai 4 participants 7 messages Mar 20 – Mar 21

Subsystems (4)
lore
Share & tools
lore

Ihor Solodrai

author reviewer
#0 lore raw

PATCH v2 1/2 bpf: Fix exception exit lock checking for subprogs

process_bpf_exit_full() passes check_lock = !curframe to check_resource_leak(), which is false in cases when bpf_throw() is called from a static subprog. This makes check_resource_leak() to skip validation of active_rcu_locks, active_preempt_locks, and active_irq_id on exception exits from subprogs.

At runtime bpf_throw() unwinds the stack via ORC without releasing any user-acquired locks, which may cause various issues as the result.

Fix by setting check_lock = true for exception exits regardless of curframe, since exceptions bypass all intermediate frame cleanup. Update the error message prefix to "bpf_throw" for exception exits to distinguish them from normal BPF_EXIT.

Fix reject_subprog_with_rcu_read_lock test which was previously passing for the wrong reason. Test program returned directly from the subprog call without closing the RCU section, so the error was triggered by the unclosed RCU lock on normal exit, not by bpf_throw. Update __msg annotations for affected tests to match the new "bpf_throw" error prefix.

The spin_lock case is not affected because they are already checked [1] at the call site in do_check_insn() before bpf_throw can run.

[1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/bpf/verifier.c?h=v7.0-rc4#n21098

Assisted-by Claude:claude-opus-4-6
Fixes f18b03fabaa9 ("bpf: Implement BPF exceptions")

---

v1->v2:
* fix reject_subprog_with_rcu_read_lock test (Kumar)
* add test case with irq lock (Kumar)
* remove reject_subprog_throw_rcu_lock test from patch #2, since it's essentially a duplicate now

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

This bug was discovered by an AI bot. You can find the original AI-generated investigation here:
https://github.com/theihor/bpf/commits/ai-tinkering-20260310-0745/

---
 kernel/bpf/verifier.c                               | 3 ++-
 tools/testing/selftests/bpf/progs/exceptions_fail.c | 9 ++++++---
 2 files changed, 8 insertions(+), 4 deletions(-)
kernel/bpf/verifier.c +2 -1
index df22bfc572e2..5c0e6809024f 100644
@@ -20911,7 +20911,8 @@ static int process_bpf_exit_full(struct bpf_verifier_env *env,
 	 * state when it exits.
 	 */
 	int err = check_resource_leak(env, exception_exit,
-				      !env->cur_state->curframe,
+				      exception_exit || !env->cur_state->curframe,
+				      exception_exit ? "bpf_throw" :
 				      "BPF_EXIT instruction in main prog");
 	if (err)
 		return err;
tools/testing/selftests/bpf/progs/exceptions_fail.c +6 -3
index 8a0fdff89927..d7f1c492e3dd 100644
@@ -8,6 +8,7 @@
 #include "bpf_experimental.h"
 
 extern void bpf_rcu_read_lock(void) __ksym;
+extern void bpf_rcu_read_unlock(void) __ksym;
 
 #define private(name) SEC(".bss." #name) __hidden __attribute__((aligned(8)))
 
@@ -131,7 +132,7 @@ int reject_subprog_with_lock(void *ctx)
 }
 
 SEC("?tc")
-__failure __msg("BPF_EXIT instruction in main prog cannot be used inside bpf_rcu_read_lock-ed region")
+__failure __msg("bpf_throw cannot be used inside bpf_rcu_read_lock-ed region")
 int reject_with_rcu_read_lock(void *ctx)
 {
 	bpf_rcu_read_lock();
@@ -147,11 +148,13 @@ __noinline static int throwing_subprog(struct __sk_buff *ctx)
 }
 
 SEC("?tc")
-__failure __msg("BPF_EXIT instruction in main prog cannot be used inside bpf_rcu_read_lock-ed region")
+__failure __msg("bpf_throw cannot be used inside bpf_rcu_read_lock-ed region")
 int reject_subprog_with_rcu_read_lock(void *ctx)
 {
 	bpf_rcu_read_lock();
-	return throwing_subprog(ctx);
+	throwing_subprog(ctx);
+	bpf_rcu_read_unlock();
+	return 0;
 }
 
 static bool rbless(struct bpf_rb_node *n1, const struct bpf_rb_node *n2)
Signature · 1 line
2.53.0
author reviewer
#1 lore raw

PATCH v2 2/2 selftests/bpf: Add tests for bpf_throw lock leak from subprogs

Add test cases to ensure the verifier correctly rejects bpf_throw from subprogs when RCU, preempt, or IRQ locks are held:

* reject_subprog_rcu_lock_throw: subprog acquires bpf_rcu_read_lock and then calls bpf_throw
* reject_subprog_throw_preempt_lock: always-throwing subprog called while caller holds bpf_preempt_disable
* reject_subprog_throw_irq_lock: always-throwing subprog called while caller holds bpf_local_irq_save

Assisted-by Claude:claude-opus-4-6
---
 .../selftests/bpf/progs/exceptions_fail.c     | 47 +++++++++++++++++++
 1 file changed, 47 insertions(+)
tools/testing/selftests/bpf/progs/exceptions_fail.c +47 -0
index d7f1c492e3dd..9ea1353488d7 100644
@@ -9,6 +9,10 @@
 
 extern void bpf_rcu_read_lock(void) __ksym;
 extern void bpf_rcu_read_unlock(void) __ksym;
+extern void bpf_preempt_disable(void) __ksym;
+extern void bpf_preempt_enable(void) __ksym;
+extern void bpf_local_irq_save(unsigned long *) __ksym;
+extern void bpf_local_irq_restore(unsigned long *) __ksym;
 
 #define private(name) SEC(".bss." #name) __hidden __attribute__((aligned(8)))
 
@@ -349,4 +353,47 @@ int reject_exception_throw_cb_diff(struct __sk_buff *ctx)
 	return 0;
 }
 
+__noinline static int always_throws(void)
+{
+	bpf_throw(0);
+	return 0;
+}
+
+__noinline static int rcu_lock_then_throw(void)
+{
+	bpf_rcu_read_lock();
+	bpf_throw(0);
+	return 0;
+}
+
+SEC("?tc")
+__failure __msg("bpf_throw cannot be used inside bpf_rcu_read_lock-ed region")
+int reject_subprog_rcu_lock_throw(void *ctx)
+{
+	rcu_lock_then_throw();
+	return 0;
+}
+
+SEC("?tc")
+__failure __msg("bpf_throw cannot be used inside bpf_preempt_disable-ed region")
+int reject_subprog_throw_preempt_lock(void *ctx)
+{
+	bpf_preempt_disable();
+	always_throws();
+	bpf_preempt_enable();
+	return 0;
+}
+
+SEC("?tc")
+__failure __msg("bpf_throw cannot be used inside bpf_local_irq_save-ed region")
+int reject_subprog_throw_irq_lock(void *ctx)
+{
+	unsigned long flags;
+
+	bpf_local_irq_save(&flags);
+	always_throws();
+	bpf_local_irq_restore(&flags);
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";
Signature · 1 line
2.53.0
reviewer Acked-by on v2 2/2
#2 lore raw

Re: [PATCH bpf v2 2/2] selftests/bpf: Add tests for bpf_throw lock leak from subprogs

Quoted 10 lines · Ihor Solodrai

Add test cases to ensure the verifier correctly rejects bpf_throw from subprogs when RCU, preempt, or IRQ locks are held:

* reject_subprog_rcu_lock_throw: subprog acquires bpf_rcu_read_lock and then calls bpf_throw
* reject_subprog_throw_preempt_lock: always-throwing subprog called while caller holds bpf_preempt_disable
* reject_subprog_throw_irq_lock: always-throwing subprog called while caller holds bpf_local_irq_save

Quoted 2 lines
Assisted-by Claude:claude-opus-4-6
maintainer Acked-by on v2 2/2
#3 lore raw

Re: [PATCH bpf v2 2/2] selftests/bpf: Add tests for bpf_throw lock leak from subprogs

In reply to Ihor Solodrai
Quoted 11 lines · Ihor Solodrai

Add test cases to ensure the verifier correctly rejects bpf_throw from subprogs when RCU, preempt, or IRQ locks are held:

* reject_subprog_rcu_lock_throw: subprog acquires bpf_rcu_read_lock and then calls bpf_throw
* reject_subprog_throw_preempt_lock: always-throwing subprog called while caller holds bpf_preempt_disable
* reject_subprog_throw_irq_lock: always-throwing subprog called while caller holds bpf_local_irq_save

Quoted 3 lines
Assisted-by Claude:claude-opus-4-6

---

Quoted 1 line

[...]

reviewer Acked-by on v2 1/2
#4 lore raw
In reply to Ihor Solodrai
Quoted 21 lines · Ihor Solodrai

process_bpf_exit_full() passes check_lock = !curframe to check_resource_leak(), which is false in cases when bpf_throw() is called from a static subprog. This makes check_resource_leak() to skip validation of active_rcu_locks, active_preempt_locks, and active_irq_id on exception exits from subprogs.

At runtime bpf_throw() unwinds the stack via ORC without releasing any user-acquired locks, which may cause various issues as the result.

Fix by setting check_lock = true for exception exits regardless of curframe, since exceptions bypass all intermediate frame cleanup. Update the error message prefix to "bpf_throw" for exception exits to distinguish them from normal BPF_EXIT.

Fix reject_subprog_with_rcu_read_lock test which was previously passing for the wrong reason. Test program returned directly from the subprog call without closing the RCU section, so the error was triggered by the unclosed RCU lock on normal exit, not by bpf_throw. Update __msg annotations for affected tests to match the new "bpf_throw" error prefix.

Quoted 8 lines

The spin_lock case is not affected because they are already checked [1] at the call site in do_check_insn() before bpf_throw can run.

[1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/bpf/verifier.c?h=v7.0-rc4#n21098

Assisted-by Claude:claude-opus-4-6
Fixes f18b03fabaa9 ("bpf: Implement BPF exceptions")
maintainer Acked-by on v2 1/2
#5 lore raw
In reply to Ihor Solodrai
Quoted 25 lines · Ihor Solodrai

process_bpf_exit_full() passes check_lock = !curframe to check_resource_leak(), which is false in cases when bpf_throw() is called from a static subprog. This makes check_resource_leak() to skip validation of active_rcu_locks, active_preempt_locks, and active_irq_id on exception exits from subprogs.

At runtime bpf_throw() unwinds the stack via ORC without releasing any user-acquired locks, which may cause various issues as the result.

Fix by setting check_lock = true for exception exits regardless of curframe, since exceptions bypass all intermediate frame cleanup. Update the error message prefix to "bpf_throw" for exception exits to distinguish them from normal BPF_EXIT.

Fix reject_subprog_with_rcu_read_lock test which was previously passing for the wrong reason. Test program returned directly from the subprog call without closing the RCU section, so the error was triggered by the unclosed RCU lock on normal exit, not by bpf_throw. Update __msg annotations for affected tests to match the new "bpf_throw" error prefix.

The spin_lock case is not affected because they are already checked [1] at the call site in do_check_insn() before bpf_throw can run.

Quoted 7 lines

[1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/bpf/verifier.c?h=v7.0-rc4#n21098

Assisted-by Claude:claude-opus-4-6
Fixes f18b03fabaa9 ("bpf: Implement BPF exceptions")

---

Quoted 1 line

[...]

#6 lore raw
In reply to Ihor Solodrai
This series was applied to bpf/bpf.git (master) by Alexei Starovoitov <[email protected]>:

Hello:

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

Quoted 10 lines · On Thu, 19 Mar 2026 17:08:08 -0700 you wrote:

process_bpf_exit_full() passes check_lock = !curframe to check_resource_leak(), which is false in cases when bpf_throw() is called from a static subprog. This makes check_resource_leak() to skip validation of active_rcu_locks, active_preempt_locks, and active_irq_id on exception exits from subprogs.

At runtime bpf_throw() unwinds the stack via ORC without releasing any user-acquired locks, which may cause various issues as the result.

[...]

Here is the summary with links:
- [bpf,v2,1/2] bpf: Fix exception exit lock checking for subprogs

    https://git.kernel.org/bpf/bpf/c/6c2128505f61
  - [bpf,v2,2/2] selftests/bpf: Add tests for bpf_throw lock leak from subprogs
    https://git.kernel.org/bpf/bpf/c/a1e5c46eaed3

You are awesome, thank you!

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