* [PATCH bpf-next 0/2] bpf: Fix loop detection for re-arming async callbacks
@ 2026-09-23 14:01 Puranjay Mohan
2026-09-23 14:01 ` [PATCH bpf-next 1/2] bpf: Look at frame 0 when telling async callback entries apart Puranjay Mohan
2026-09-23 14:01 ` [PATCH bpf-next 2/2] selftests/bpf: Add timer tests for a re-arming callback with a loop Puranjay Mohan
0 siblings, 2 replies; 4+ messages in thread
From: Puranjay Mohan @ 2026-09-23 14:01 UTC (permalink / raw)
To: bpf
Cc: Puranjay Mohan, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song
is_state_visited() skips the infinite loop check when two states differ in
async_entry_cnt, since seeing the same state on a second entry into an
async callback is not a loop. It reads that from the innermost frame, but
push_async_cb() sets in_async_callback_fn and async_entry_cnt on the
callback's own frame, and setup_func_entry() copies neither, so a subprog
called by the callback carries neither.
A callback which re-arms itself and calls a subprog therefore has its two
entries compared at a loop inside that subprog, and is rejected:
infinite loop detected at insn 57
Patch 1 reads both from frame 0, which push_async_cb() makes the callback's
frame at any call depth.
Patch 2 covers both directions: a timer callback which re-arms through
bpf_timer_set_callback() and reaches a bounded loop through a static
subprog, which fails to load without patch 1, and a callback which never
returns, which must still be rejected either way. Nothing covered an async
callback before, so neither direction was tested.
Puranjay Mohan (2):
bpf: Look at frame 0 when telling async callback entries apart
selftests/bpf: Add timer tests for a re-arming callback with a loop
kernel/bpf/states.c | 4 +-
.../testing/selftests/bpf/prog_tests/timer.c | 33 ++++++++++++
tools/testing/selftests/bpf/progs/timer.c | 50 ++++++++++++++++++-
.../selftests/bpf/progs/timer_failure.c | 29 +++++++++++
4 files changed, 113 insertions(+), 3 deletions(-)
base-commit: 91f8613d95ad8cd99d8baf094806d1ef98bc6380
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH bpf-next 1/2] bpf: Look at frame 0 when telling async callback entries apart
2026-09-23 14:01 [PATCH bpf-next 0/2] bpf: Fix loop detection for re-arming async callbacks Puranjay Mohan
@ 2026-09-23 14:01 ` Puranjay Mohan
2026-09-24 5:56 ` Alexei Starovoitov
2026-09-23 14:01 ` [PATCH bpf-next 2/2] selftests/bpf: Add timer tests for a re-arming callback with a loop Puranjay Mohan
1 sibling, 1 reply; 4+ messages in thread
From: Puranjay Mohan @ 2026-09-23 14:01 UTC (permalink / raw)
To: bpf
Cc: Puranjay Mohan, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song
is_state_visited() skips the infinite loop check when two states differ
in async_entry_cnt, because seeing the same state on a second entry into
an async callback is not a loop. It reads that from the innermost frame,
but push_async_cb() sets in_async_callback_fn and async_entry_cnt on the
callback's own frame, and setup_func_entry() copies neither, so a subprog
called by the callback carries neither.
A callback which re-arms itself and calls a subprog therefore compares
the two entries at a loop inside that subprog, where the innermost frame
is the subprog's, and the state is rejected:
infinite loop detected at insn 60
Read the flag and the count from frame 0, which push_async_cb() makes the
callback's frame. A loop within a single entry still has a matching count
and is still caught, and frame 0 of a non-async state does not have the
flag set, so nothing else changes.
Fixes: bfc6bb74e4f1 ("bpf: Implement verifier support for validation of async callbacks.")
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
kernel/bpf/states.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index 66fb11b6c6a76..32e141aa6a117 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -1273,10 +1273,10 @@ int bpf_is_state_visited(struct bpf_verifier_env *env, int insn_idx)
continue;
if (sl->state.branches) {
- struct bpf_func_state *frame = sl->state.frame[sl->state.curframe];
+ struct bpf_func_state *frame = sl->state.frame[0];
if (frame->in_async_callback_fn &&
- frame->async_entry_cnt != cur->frame[cur->curframe]->async_entry_cnt) {
+ frame->async_entry_cnt != cur->frame[0]->async_entry_cnt) {
/* Different async_entry_cnt means that the verifier is
* processing another entry into async callback.
* Seeing the same state is not an indication of infinite
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH bpf-next 2/2] selftests/bpf: Add timer tests for a re-arming callback with a loop
2026-09-23 14:01 [PATCH bpf-next 0/2] bpf: Fix loop detection for re-arming async callbacks Puranjay Mohan
2026-09-23 14:01 ` [PATCH bpf-next 1/2] bpf: Look at frame 0 when telling async callback entries apart Puranjay Mohan
@ 2026-09-23 14:01 ` Puranjay Mohan
1 sibling, 0 replies; 4+ messages in thread
From: Puranjay Mohan @ 2026-09-23 14:01 UTC (permalink / raw)
To: bpf
Cc: Puranjay Mohan, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song
Existing timer callbacks either do not re-arm through
bpf_timer_set_callback(), which is what starts another async callback
entry, or do not reach a loop once they do. Cover that: loop_cb() re-arms
itself and reaches a bounded loop through sum_to(), which is static and
not inlined, so the loop is walked in a frame below the callback's rather
than in a separately verified global subprog. Without the preceding fix
the program is rejected with "infinite loop detected".
Telling async callback entries apart must not stop the verifier catching
a real loop in a callback, and no existing test covers that either, so
also check that a callback which never returns is still rejected.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
---
.../testing/selftests/bpf/prog_tests/timer.c | 33 ++++++++++++
tools/testing/selftests/bpf/progs/timer.c | 50 ++++++++++++++++++-
.../selftests/bpf/progs/timer_failure.c | 29 +++++++++++
3 files changed, 111 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/timer.c b/tools/testing/selftests/bpf/prog_tests/timer.c
index 09ff21e1ad2f0..178223190b8b7 100644
--- a/tools/testing/selftests/bpf/prog_tests/timer.c
+++ b/tools/testing/selftests/bpf/prog_tests/timer.c
@@ -262,6 +262,34 @@ static int timer_cancel_async(struct timer *timer_skel)
return 0;
}
+/*
+ * A timer callback which re-arms itself and reaches a loop through a call:
+ * the two entries into the callback meet at the loop, in a frame of its own.
+ */
+static int timer_loop_rearm(struct timer *timer_skel)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, topts);
+ int err, prog_fd, i;
+
+ err = timer__attach(timer_skel);
+ if (!ASSERT_OK(err, "timer_attach"))
+ return err;
+
+ timer_skel->bss->loop_rearm = 1;
+
+ prog_fd = bpf_program__fd(timer_skel->progs.test_loop_rearm);
+ err = bpf_prog_test_run_opts(prog_fd, &topts);
+ if (!ASSERT_OK(err, "test_run"))
+ return err;
+
+ for (i = 0; i < 100 && timer_skel->bss->loop_sum < 120 * 2; i++)
+ usleep(1000);
+
+ timer__detach(timer_skel);
+ ASSERT_EQ(timer_skel->bss->loop_sum, 120 * 2, "loop_sum");
+ return 0;
+}
+
static void test_timer(int (*timer_test_fn)(struct timer *timer_skel))
{
struct timer *timer_skel = NULL;
@@ -287,6 +315,11 @@ void serial_test_timer(void)
RUN_TESTS(timer_failure);
}
+void serial_test_timer_loop_rearm(void)
+{
+ test_timer(timer_loop_rearm);
+}
+
void serial_test_timer_stress(void)
{
test_timer(timer_stress);
diff --git a/tools/testing/selftests/bpf/progs/timer.c b/tools/testing/selftests/bpf/progs/timer.c
index d6d5fefcd9b13..832b94b8e8dff 100644
--- a/tools/testing/selftests/bpf/progs/timer.c
+++ b/tools/testing/selftests/bpf/progs/timer.c
@@ -6,6 +6,7 @@
#include <errno.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
+#include "bpf_experimental.h"
#define CLOCK_MONOTONIC 1
#define CLOCK_BOOTTIME 7
@@ -57,9 +58,12 @@ struct {
__type(key, int);
__type(value, struct elem);
} abs_timer SEC(".maps"), soft_timer_pinned SEC(".maps"), abs_timer_pinned SEC(".maps"),
- race_array SEC(".maps");
+ race_array SEC(".maps"), loop_array SEC(".maps");
__u64 bss_data;
+__u64 loop_sum;
+int loop_rearm; /* number of times loop_cb() re-arms itself */
+__u64 zero;
__u64 abs_data;
__u64 err;
__u64 ok;
@@ -139,6 +143,50 @@ static int timer_cb1(void *map, int *key, struct bpf_timer *timer)
return 0;
}
+/*
+ * Static and not inlined, so the loop is walked in a frame below the
+ * callback's rather than in a separately verified global subprog.
+ */
+static __noinline int sum_to(__u64 n)
+{
+ __u64 i, sum = 0;
+
+ for (i = zero; i < n && can_loop; i++)
+ sum += i;
+
+ return sum;
+}
+
+/* Re-arms itself and reaches a bounded loop through a call. */
+static int loop_cb(void *map, int *key, struct elem *val)
+{
+ loop_sum += sum_to(16);
+
+ if (loop_rearm > 0) {
+ loop_rearm--;
+ /* set_callback is what starts another async callback entry */
+ bpf_timer_set_callback(&val->t, loop_cb);
+ bpf_timer_start(&val->t, 0, 0);
+ }
+ return 0;
+}
+
+SEC("fentry/bpf_fentry_test1")
+int BPF_PROG2(test_loop_rearm, int, a)
+{
+ struct bpf_timer *timer;
+ int key = 0;
+
+ timer = bpf_map_lookup_elem(&loop_array, &key);
+ if (!timer)
+ return 0;
+
+ bpf_timer_init(timer, &loop_array, CLOCK_MONOTONIC);
+ bpf_timer_set_callback(timer, loop_cb);
+ bpf_timer_start(timer, 0, 0);
+ return 0;
+}
+
SEC("fentry/bpf_fentry_test1")
int BPF_PROG2(test1, int, a)
{
diff --git a/tools/testing/selftests/bpf/progs/timer_failure.c b/tools/testing/selftests/bpf/progs/timer_failure.c
index 5a2e9dabf1c6c..0538269101cac 100644
--- a/tools/testing/selftests/bpf/progs/timer_failure.c
+++ b/tools/testing/selftests/bpf/progs/timer_failure.c
@@ -66,3 +66,32 @@ long BPF_PROG2(test_bad_ret, int, a)
return 0;
}
+
+/*
+ * A real loop inside an async callback must still be rejected: both entries
+ * into the callback have the same async_entry_cnt, so telling entries apart
+ * does not apply here.
+ */
+static int timer_cb_infinite_loop(void *map, int *key, struct elem *val)
+{
+ for (;;) {}
+
+ return 0;
+}
+
+SEC("fentry/bpf_fentry_test1")
+__failure __msg("infinite loop detected")
+long BPF_PROG2(test_infinite_loop_cb, int, a)
+{
+ struct bpf_timer *timer;
+ int key = 0;
+
+ timer = bpf_map_lookup_elem(&timer_map, &key);
+ if (timer) {
+ bpf_timer_init(timer, &timer_map, CLOCK_BOOTTIME);
+ bpf_timer_set_callback(timer, timer_cb_infinite_loop);
+ bpf_timer_start(timer, 1000, 0);
+ }
+
+ return 0;
+}
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH bpf-next 1/2] bpf: Look at frame 0 when telling async callback entries apart
2026-09-23 14:01 ` [PATCH bpf-next 1/2] bpf: Look at frame 0 when telling async callback entries apart Puranjay Mohan
@ 2026-09-24 5:56 ` Alexei Starovoitov
0 siblings, 0 replies; 4+ messages in thread
From: Alexei Starovoitov @ 2026-09-24 5:56 UTC (permalink / raw)
To: Puranjay Mohan, bpf
Cc: Daniel Borkmann, Andrii Nakryiko, Martin KaFai Lau,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Song Liu,
Yonghong Song
On Wed, Sep 23, 2026 at 07:01 AM Puranjay Mohan <puranjay@kernel.org> wrote:
> - struct bpf_func_state *frame = sl->state.frame[sl->state.curframe];
> + struct bpf_func_state *frame = sl->state.frame[0];
>
> if (frame->in_async_callback_fn &&
> - frame->async_entry_cnt != cur->frame[cur->curframe]->async_entry_cnt) {
> + frame->async_entry_cnt != cur->frame[0]->async_entry_cnt) {
push_callback_call() has the same problem:
caller = state->frame[state->curframe];
...
callee->async_entry_cnt = caller->async_entry_cnt + 1;
When the callback re-arms itself from a subprog the caller is
frame 1 with async_entry_cnt == 0, so every entry into the callback
gets async_entry_cnt == 1 and the check above never skips.
A loop anywhere in such callback is still rejected with
"infinite loop detected".
pw-bot: cr
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-24 5:56 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-23 14:01 [PATCH bpf-next 0/2] bpf: Fix loop detection for re-arming async callbacks Puranjay Mohan
2026-09-23 14:01 ` [PATCH bpf-next 1/2] bpf: Look at frame 0 when telling async callback entries apart Puranjay Mohan
2026-09-24 5:56 ` Alexei Starovoitov
2026-09-23 14:01 ` [PATCH bpf-next 2/2] selftests/bpf: Add timer tests for a re-arming callback with a loop Puranjay Mohan
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox