public inbox for bpf@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH bpf-next v4 0/2] bpf: Relax 8 frame limitation for global subprogs
@ 2026-03-09 20:44 Emil Tsalapatis
  2026-03-09 20:44 ` [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks Emil Tsalapatis
  2026-03-09 20:44 ` [PATCH bpf-next v4 2/2] bpf: Add deep call stack selftests Emil Tsalapatis
  0 siblings, 2 replies; 8+ messages in thread
From: Emil Tsalapatis @ 2026-03-09 20:44 UTC (permalink / raw)
  To: bpf
  Cc: andrii, ast, daniel, eddyz87, martin.lau, memxor, song,
	yonghong.song, Emil Tsalapatis

The BPF verifier currently limits the maximum runtime call stack to
8 frames. Larger BPF programs like sched-ext schedulers routinely
fail verification because they exceed this limit, even as they use
very little actual stack space for each frame.

Relax the verifier to permit call stacks > 8 frames deep when the
call stacks include global subprogs. The old 8 stack frame limit now
only applies to call stacks composed entirely of static function calls.
This works because global functions are each verified in isolation, so
the verifier does not need to cross-reference verification state across
the function call boundary, which has been the reason for limiting the
call stack size in the first place.

This patch does not change the verification time limit of 8 stack
frames. Static functions that are inlined for verification purposes
still only go 8 frames deep to avoid changing the verifier's internal
data structures used for verification. These data structures only
support holding information on up to 8 stack frames.

This patch also does not adjust the actual maximum stack size of 512.

CHANGELOG
=========

v3 -> v4 (https://lore.kernel.org/bpf/20260303043106.406099-1-emil@etsalapatis.com/)
- Factor out temp call depth tracking info into its own struct (Eduard)
- Bring depth calculation loop in line with the other instances (Mykyta)
- Add comment on why selftest call stack is 16 bytes/frame (Eduard)
- Rename "cidx" to "caller" for clarity (Mykyta, Eduard)

v2 -> v3 (https://lore.kernel.org/bpf/20260210213606.475415-1-emil@etsalapatis.com/)
- Change logic to remove arbitrary limit on call depth (Eduard)
- Add additional selftests (Eduard)

v1 -> v2 (https://lore.kernel.org/bpf/20260202233716.835638-1-emil@etsalapatis.com)
- Adjust patch to only increase the runtime stack depth, leaving the
verification-time stack depth unchanged (Alexei)

Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>

Emil Tsalapatis (2):
  bpf: Only enforce 8 frame call stack limit for all-static stacks
  bpf: Add deep call stack selftests

 include/linux/bpf_verifier.h                  |  9 ++
 kernel/bpf/verifier.c                         | 52 ++++++++----
 .../bpf/prog_tests/test_global_funcs.c        |  2 +
 .../selftests/bpf/progs/test_global_func3.c   | 18 ++--
 .../bpf/progs/test_global_func_deep_stack.c   | 83 +++++++++++++++++++
 5 files changed, 137 insertions(+), 27 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c

-- 
2.49.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks
  2026-03-09 20:44 [PATCH bpf-next v4 0/2] bpf: Relax 8 frame limitation for global subprogs Emil Tsalapatis
@ 2026-03-09 20:44 ` Emil Tsalapatis
  2026-03-09 21:23   ` bot+bpf-ci
                     ` (2 more replies)
  2026-03-09 20:44 ` [PATCH bpf-next v4 2/2] bpf: Add deep call stack selftests Emil Tsalapatis
  1 sibling, 3 replies; 8+ messages in thread
From: Emil Tsalapatis @ 2026-03-09 20:44 UTC (permalink / raw)
  To: bpf
  Cc: andrii, ast, daniel, eddyz87, martin.lau, memxor, song,
	yonghong.song, Emil Tsalapatis

The BPF verifier currently enforces a call stack depth of 8 frames,
regardless of the actual stack space consumption of those frames. The
limit is necessary for static call stacks, because the bookkeeping data
structures used by the verifier when stepping into static functions
during verification only support 8 stack frames. However, this
limitation only matters for static stack frames: Global subprogs are
verified by themselves and do not require limiting the call depth.

Relax this limitation to only apply to static stack frames. Verification
now only fails when there is a sequence of 8 calls to non-global
subprogs. Calling into a global subprog resets the counter. This allows
deeper call stacks, provided all frames still fit in the stack.

The change does not increase the maximum size of the call stack, only
the maximum number of frames we can place in it.

Also change the progs/test_global_func3.c selftest to use static
functions, since with the new patch it would otherwise unexpectedly
pass verification.

Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
 include/linux/bpf_verifier.h                  |  9 ++++
 kernel/bpf/verifier.c                         | 52 ++++++++++++-------
 .../selftests/bpf/progs/test_global_func3.c   | 18 +++----
 3 files changed, 52 insertions(+), 27 deletions(-)

diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 090aa26d1c98..b45c3bb801c5 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -651,6 +651,12 @@ enum priv_stack_mode {
 	PRIV_STACK_ADAPTIVE,
 };
 
+struct bpf_subprog_call_depth_info {
+	int ret_insn; /* caller instruction where we return to. */
+	int caller; /* caller subprogram idx */
+	int frame; /* # of consecutive static call stack frames on top of stack */
+};
+
 struct bpf_subprog_info {
 	/* 'start' has to be the first field otherwise find_subprog() won't work */
 	u32 start; /* insn idx of function entry point */
@@ -678,6 +684,9 @@ struct bpf_subprog_info {
 
 	enum priv_stack_mode priv_stack_mode;
 	struct bpf_subprog_arg_info args[MAX_BPF_FUNC_REG_ARGS];
+
+	/* temporary state used for call frame depth calculation */
+	struct bpf_subprog_call_depth_info dinfo;
 };
 
 struct bpf_verifier_env;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 8e4f69918693..ccd4efec179d 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6733,9 +6733,11 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 	struct bpf_insn *insn = env->prog->insnsi;
 	int depth = 0, frame = 0, i, subprog_end, subprog_depth;
 	bool tail_call_reachable = false;
-	int ret_insn[MAX_CALL_FRAMES];
-	int ret_prog[MAX_CALL_FRAMES];
-	int j;
+	int total;
+	int tmp;
+
+	/* no caller idx */
+	subprog[idx].dinfo.caller = -1;
 
 	i = subprog[idx].start;
 	if (!priv_stack_supported)
@@ -6787,8 +6789,12 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 	} else {
 		depth += subprog_depth;
 		if (depth > MAX_BPF_STACK) {
+			total = 0;
+			for (tmp = idx; tmp >= 0; tmp = subprog[tmp].dinfo.caller)
+				total++;
+
 			verbose(env, "combined stack size of %d calls is %d. Too large\n",
-				frame + 1, depth);
+				total, depth);
 			return -EACCES;
 		}
 	}
@@ -6802,10 +6808,8 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 
 			if (!is_bpf_throw_kfunc(insn + i))
 				continue;
-			if (subprog[idx].is_cb)
-				err = true;
-			for (int c = 0; c < frame && !err; c++) {
-				if (subprog[ret_prog[c]].is_cb) {
+			for (tmp = idx; tmp >= 0 && !err; tmp = subprog[tmp].dinfo.caller) {
+				if (subprog[tmp].is_cb) {
 					err = true;
 					break;
 				}
@@ -6821,8 +6825,6 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 		if (!bpf_pseudo_call(insn + i) && !bpf_pseudo_func(insn + i))
 			continue;
 		/* remember insn and function to return to */
-		ret_insn[frame] = i + 1;
-		ret_prog[frame] = idx;
 
 		/* find the callee */
 		next_insn = i + insn[i].imm + 1;
@@ -6842,7 +6844,16 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 				return -EINVAL;
 			}
 		}
+
+		/* store caller info for after we return from callee */
+		subprog[idx].dinfo.frame = frame;
+		subprog[idx].dinfo.ret_insn = i + 1;
+
+		/* push caller idx into callee's dinfo */
+		subprog[sidx].dinfo.caller = idx;
+
 		i = next_insn;
+
 		idx = sidx;
 		if (!priv_stack_supported)
 			subprog[idx].priv_stack_mode = NO_PRIV_STACK;
@@ -6850,7 +6861,7 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 		if (subprog[idx].has_tail_call)
 			tail_call_reachable = true;
 
-		frame++;
+		frame = subprog_is_global(env, idx) ? 0 : frame + 1;
 		if (frame >= MAX_CALL_FRAMES) {
 			verbose(env, "the call stack of %d frames is too deep !\n",
 				frame);
@@ -6864,12 +6875,12 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 	 * tail call counter throughout bpf2bpf calls combined with tailcalls
 	 */
 	if (tail_call_reachable)
-		for (j = 0; j < frame; j++) {
-			if (subprog[ret_prog[j]].is_exception_cb) {
+		for (tmp = idx; tmp >= 0; tmp = subprog[tmp].dinfo.caller) {
+			if (subprog[tmp].is_exception_cb) {
 				verbose(env, "cannot tail call within exception cb\n");
 				return -EINVAL;
 			}
-			subprog[ret_prog[j]].tail_call_reachable = true;
+			subprog[tmp].tail_call_reachable = true;
 		}
 	if (subprog[0].tail_call_reachable)
 		env->prog->aux->tail_call_reachable = true;
@@ -6877,13 +6888,18 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
 	/* end of for() loop means the last insn of the 'subprog'
 	 * was reached. Doesn't matter whether it was JA or EXIT
 	 */
-	if (frame == 0)
+	if (frame == 0 && subprog[idx].dinfo.caller < 0)
 		return 0;
 	if (subprog[idx].priv_stack_mode != PRIV_STACK_ADAPTIVE)
 		depth -= round_up_stack_depth(env, subprog[idx].stack_depth);
-	frame--;
-	i = ret_insn[frame];
-	idx = ret_prog[frame];
+
+	/* pop caller idx from callee */
+	idx = subprog[idx].dinfo.caller;
+
+	/* retrieve caller state from its frame */
+	frame = subprog[idx].dinfo.frame;
+	i = subprog[idx].dinfo.ret_insn;
+
 	goto continue_func;
 }
 
diff --git a/tools/testing/selftests/bpf/progs/test_global_func3.c b/tools/testing/selftests/bpf/progs/test_global_func3.c
index 142b682d3c2f..974fd8c19561 100644
--- a/tools/testing/selftests/bpf/progs/test_global_func3.c
+++ b/tools/testing/selftests/bpf/progs/test_global_func3.c
@@ -5,56 +5,56 @@
 #include <bpf/bpf_helpers.h>
 #include "bpf_misc.h"
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f1(struct __sk_buff *skb)
 {
 	return skb->len;
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f2(int val, struct __sk_buff *skb)
 {
 	return f1(skb) + val;
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f3(int val, struct __sk_buff *skb, int var)
 {
 	return f2(var, skb) + val;
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f4(struct __sk_buff *skb)
 {
 	return f3(1, skb, 2);
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f5(struct __sk_buff *skb)
 {
 	return f4(skb);
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f6(struct __sk_buff *skb)
 {
 	return f5(skb);
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f7(struct __sk_buff *skb)
 {
 	return f6(skb);
 }
 
-__attribute__ ((noinline))
+static __attribute__ ((noinline))
 int f8(struct __sk_buff *skb)
 {
 	return f7(skb);
 }
 
 SEC("tc")
-__failure __msg("the call stack of 8 frames")
+__failure __msg("the call stack of 9 frames")
 int global_func3(struct __sk_buff *skb)
 {
 	return f8(skb);
-- 
2.49.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH bpf-next v4 2/2] bpf: Add deep call stack selftests
  2026-03-09 20:44 [PATCH bpf-next v4 0/2] bpf: Relax 8 frame limitation for global subprogs Emil Tsalapatis
  2026-03-09 20:44 ` [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks Emil Tsalapatis
@ 2026-03-09 20:44 ` Emil Tsalapatis
  2026-03-11  2:59   ` Yonghong Song
  1 sibling, 1 reply; 8+ messages in thread
From: Emil Tsalapatis @ 2026-03-09 20:44 UTC (permalink / raw)
  To: bpf
  Cc: andrii, ast, daniel, eddyz87, martin.lau, memxor, song,
	yonghong.song, Emil Tsalapatis

Add tests that demonstrate the verifier support for deep call stacks
while still enforcing maximum stack size limits.

Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
 .../bpf/prog_tests/test_global_funcs.c        |  2 +
 .../bpf/progs/test_global_func_deep_stack.c   | 83 +++++++++++++++++++
 2 files changed, 85 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c

diff --git a/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c b/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c
index e905cbaf6b3d..500446808908 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c
@@ -18,6 +18,7 @@
 #include "test_global_func15.skel.h"
 #include "test_global_func16.skel.h"
 #include "test_global_func17.skel.h"
+#include "test_global_func_deep_stack.skel.h"
 #include "test_global_func_ctx_args.skel.h"
 
 #include "bpf/libbpf_internal.h"
@@ -155,6 +156,7 @@ void test_test_global_funcs(void)
 	RUN_TESTS(test_global_func15);
 	RUN_TESTS(test_global_func16);
 	RUN_TESTS(test_global_func17);
+	RUN_TESTS(test_global_func_deep_stack);
 	RUN_TESTS(test_global_func_ctx_args);
 
 	if (test__start_subtest("ctx_arg_rewrite"))
diff --git a/tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c b/tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c
new file mode 100644
index 000000000000..733c3334b7b1
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Meta Platforms, Inc and affiliates. */
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+
+/*
+ * Macro tricks to tersely define for long non-recursive call chains. Add
+ * computation to the functions prevent tail recursion from reducing the
+ * stack size to 0.
+ */
+
+#define CAT(a, b) a ## b
+#define XCAT(a, b) CAT(a, b)
+
+#define F_0 \
+__attribute__((noinline)) \
+int f0(unsigned long a) { volatile long b = a + 16; if (a == 0) return 0; return b; }
+
+#define FN(n, prev) \
+__attribute__((noinline)) \
+int XCAT(f, n)(unsigned long a) { volatile long b = XCAT(f, prev)(a - 1); if (!b) return 0; return b + 1; }
+
+/* Call chain 33 levels deep. */
+#define F_1 F_0		FN(1, 0)
+#define F_2 F_1   	FN(2, 1)
+#define F_3 F_2   	FN(3, 2)
+#define F_4 F_3   	FN(4, 3)
+#define F_5 F_4   	FN(5, 4)
+#define F_6 F_5   	FN(6, 5)
+#define F_7 F_6   	FN(7, 6)
+#define F_8 F_7   	FN(8, 7)
+#define F_9 F_8   	FN(9, 8)
+#define F_10 F_9	FN(10, 9)
+#define F_11 F_10	FN(11, 10)
+#define F_12 F_11   	FN(12, 11)
+#define F_13 F_12   	FN(13, 12)
+#define F_14 F_13   	FN(14, 13)
+#define F_15 F_14   	FN(15, 14)
+#define F_16 F_15   	FN(16, 15)
+#define F_17 F_16   	FN(17, 16)
+#define F_18 F_17   	FN(18, 17)
+#define F_19 F_18   	FN(19, 18)
+#define F_20 F_19	FN(20, 19)
+#define F_21 F_20	FN(21, 20)
+#define F_22 F_21   	FN(22, 21)
+#define F_23 F_22   	FN(23, 22)
+#define F_24 F_23   	FN(24, 23)
+#define F_25 F_24   	FN(25, 24)
+#define F_26 F_25   	FN(26, 25)
+#define F_27 F_26   	FN(27, 26)
+#define F_28 F_27   	FN(28, 27)
+#define F_29 F_28   	FN(29, 28)
+#define F_30 F_29	FN(30, 29)
+#define F_31 F_30	FN(31, 30)
+#define F_32 F_31   	FN(32, 31)
+
+#define CAT2(a, b) a ## b
+#define XCAT2(a, b) CAT2(a, b)
+
+#define F(n) XCAT2(F_, n)
+
+F(32)
+
+/* Ensure that even 32 levels deep, the function verifies. */
+SEC("syscall")
+__success
+int global_func_deep_stack_success(struct __sk_buff *skb)
+{
+	return f31(55);
+}
+
+/* 
+ * Check we actually honor stack limits (33 * 16 = 528 > 512 = MAX_STACK_DEPTH). 
+ * The stack depth is 16 because the verifier calls round_up_stack_depth() on
+ * the size.
+ */
+SEC("syscall")
+__failure __msg("combined stack size of 34 calls")
+int global_func_deep_stack_fail(struct __sk_buff *skb)
+{
+	return f32(123);
+}
-- 
2.49.0


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks
  2026-03-09 20:44 ` [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks Emil Tsalapatis
@ 2026-03-09 21:23   ` bot+bpf-ci
  2026-03-09 21:37   ` Eduard Zingerman
  2026-03-10 14:04   ` Mykyta Yatsenko
  2 siblings, 0 replies; 8+ messages in thread
From: bot+bpf-ci @ 2026-03-09 21:23 UTC (permalink / raw)
  To: emil, bpf
  Cc: andrii, ast, daniel, eddyz87, martin.lau, memxor, song,
	yonghong.song, emil, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, clm, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 1341 bytes --]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 8e4f69918693..ccd4efec179d 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -6733,9 +6733,11 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  	struct bpf_insn *insn = env->prog->insnsi;
>  	int depth = 0, frame = 0, i, subprog_end, subprog_depth;
>  	bool tail_call_reachable = false;
> -	int ret_insn[MAX_CALL_FRAMES];
> -	int ret_prog[MAX_CALL_FRAMES];
> -	int j;
> +	int total;
> +	int tmp;
> +
> +	/* no caller idx */
> +	subprog[idx].dinfo.caller = -1;

The function header comment above this hunk still reads:

    Since recursion is prevented by check_cfg() this algorithm
    only needs a local stack of MAX_CALL_FRAMES to remember callsites

The local stack arrays (ret_insn[] and ret_prog[]) have been removed
in favor of per-subprog dinfo fields. Should this comment be updated
to reflect the new approach?

This was also noted during the v3 review:
https://lore.kernel.org/bpf/ad397307a9cd76c9739ee05f4642c7b6a881b7dfb8a2facefa6b2062ffe328ea@mail.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/22874415683

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks
  2026-03-09 20:44 ` [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks Emil Tsalapatis
  2026-03-09 21:23   ` bot+bpf-ci
@ 2026-03-09 21:37   ` Eduard Zingerman
  2026-03-10 19:07     ` Alexei Starovoitov
  2026-03-10 14:04   ` Mykyta Yatsenko
  2 siblings, 1 reply; 8+ messages in thread
From: Eduard Zingerman @ 2026-03-09 21:37 UTC (permalink / raw)
  To: Emil Tsalapatis, bpf
  Cc: andrii, ast, daniel, martin.lau, memxor, song, yonghong.song

On Mon, 2026-03-09 at 16:44 -0400, Emil Tsalapatis wrote:
> The BPF verifier currently enforces a call stack depth of 8 frames,
> regardless of the actual stack space consumption of those frames. The
> limit is necessary for static call stacks, because the bookkeeping data
> structures used by the verifier when stepping into static functions
> during verification only support 8 stack frames. However, this
> limitation only matters for static stack frames: Global subprogs are
> verified by themselves and do not require limiting the call depth.
> 
> Relax this limitation to only apply to static stack frames. Verification
> now only fails when there is a sequence of 8 calls to non-global
> subprogs. Calling into a global subprog resets the counter. This allows
> deeper call stacks, provided all frames still fit in the stack.
> 
> The change does not increase the maximum size of the call stack, only
> the maximum number of frames we can place in it.
> 
> Also change the progs/test_global_func3.c selftest to use static
> functions, since with the new patch it would otherwise unexpectedly
> pass verification.
> 
> Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
> ---

Acked-by: Eduard Zingerman <eddyz87@gmail.com>

[...]

> diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> index 090aa26d1c98..b45c3bb801c5 100644
> --- a/include/linux/bpf_verifier.h
> +++ b/include/linux/bpf_verifier.h
> @@ -651,6 +651,12 @@ enum priv_stack_mode {
>  	PRIV_STACK_ADAPTIVE,
>  };
>  
> +struct bpf_subprog_call_depth_info {
> +	int ret_insn; /* caller instruction where we return to. */
> +	int caller; /* caller subprogram idx */
> +	int frame; /* # of consecutive static call stack frames on top of stack */
> +};
> +
>  struct bpf_subprog_info {
>  	/* 'start' has to be the first field otherwise find_subprog() won't work */
>  	u32 start; /* insn idx of function entry point */
> @@ -678,6 +684,9 @@ struct bpf_subprog_info {
>  
>  	enum priv_stack_mode priv_stack_mode;
>  	struct bpf_subprog_arg_info args[MAX_BPF_FUNC_REG_ARGS];
> +
> +	/* temporary state used for call frame depth calculation */
> +	struct bpf_subprog_call_depth_info dinfo;

Nit: I think this adds an unnecessary nesting level (e.g. I'd had it
     either at `env` level as a separate array, or here w/o additional
     struct).

[...]

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks
  2026-03-09 20:44 ` [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks Emil Tsalapatis
  2026-03-09 21:23   ` bot+bpf-ci
  2026-03-09 21:37   ` Eduard Zingerman
@ 2026-03-10 14:04   ` Mykyta Yatsenko
  2 siblings, 0 replies; 8+ messages in thread
From: Mykyta Yatsenko @ 2026-03-10 14:04 UTC (permalink / raw)
  To: Emil Tsalapatis, bpf
  Cc: andrii, ast, daniel, eddyz87, martin.lau, memxor, song,
	yonghong.song, Emil Tsalapatis

Emil Tsalapatis <emil@etsalapatis.com> writes:

> The BPF verifier currently enforces a call stack depth of 8 frames,
> regardless of the actual stack space consumption of those frames. The
> limit is necessary for static call stacks, because the bookkeeping data
> structures used by the verifier when stepping into static functions
> during verification only support 8 stack frames. However, this
> limitation only matters for static stack frames: Global subprogs are
> verified by themselves and do not require limiting the call depth.
>
> Relax this limitation to only apply to static stack frames. Verification
> now only fails when there is a sequence of 8 calls to non-global
> subprogs. Calling into a global subprog resets the counter. This allows
> deeper call stacks, provided all frames still fit in the stack.
>
> The change does not increase the maximum size of the call stack, only
> the maximum number of frames we can place in it.
>
> Also change the progs/test_global_func3.c selftest to use static
> functions, since with the new patch it would otherwise unexpectedly
> pass verification.
>
> Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
> ---
Acked-by: Mykyta Yatsenko <yatsenko@meta.com>
>  include/linux/bpf_verifier.h                  |  9 ++++
>  kernel/bpf/verifier.c                         | 52 ++++++++++++-------
>  .../selftests/bpf/progs/test_global_func3.c   | 18 +++----
>  3 files changed, 52 insertions(+), 27 deletions(-)
>
> diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> index 090aa26d1c98..b45c3bb801c5 100644
> --- a/include/linux/bpf_verifier.h
> +++ b/include/linux/bpf_verifier.h
> @@ -651,6 +651,12 @@ enum priv_stack_mode {
>  	PRIV_STACK_ADAPTIVE,
>  };
>  
> +struct bpf_subprog_call_depth_info {
> +	int ret_insn; /* caller instruction where we return to. */
> +	int caller; /* caller subprogram idx */
> +	int frame; /* # of consecutive static call stack frames on top of stack */
> +};
> +
>  struct bpf_subprog_info {
>  	/* 'start' has to be the first field otherwise find_subprog() won't work */
>  	u32 start; /* insn idx of function entry point */
> @@ -678,6 +684,9 @@ struct bpf_subprog_info {
>  
>  	enum priv_stack_mode priv_stack_mode;
>  	struct bpf_subprog_arg_info args[MAX_BPF_FUNC_REG_ARGS];
> +
> +	/* temporary state used for call frame depth calculation */
> +	struct bpf_subprog_call_depth_info dinfo;
>  };
>  
>  struct bpf_verifier_env;
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 8e4f69918693..ccd4efec179d 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -6733,9 +6733,11 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  	struct bpf_insn *insn = env->prog->insnsi;
>  	int depth = 0, frame = 0, i, subprog_end, subprog_depth;
>  	bool tail_call_reachable = false;
> -	int ret_insn[MAX_CALL_FRAMES];
> -	int ret_prog[MAX_CALL_FRAMES];
> -	int j;
> +	int total;
> +	int tmp;
> +
> +	/* no caller idx */
> +	subprog[idx].dinfo.caller = -1;
>  
>  	i = subprog[idx].start;
>  	if (!priv_stack_supported)
> @@ -6787,8 +6789,12 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  	} else {
>  		depth += subprog_depth;
>  		if (depth > MAX_BPF_STACK) {
> +			total = 0;
> +			for (tmp = idx; tmp >= 0; tmp = subprog[tmp].dinfo.caller)
> +				total++;
> +
>  			verbose(env, "combined stack size of %d calls is %d. Too large\n",
> -				frame + 1, depth);
> +				total, depth);
>  			return -EACCES;
>  		}
>  	}
> @@ -6802,10 +6808,8 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  
>  			if (!is_bpf_throw_kfunc(insn + i))
>  				continue;
> -			if (subprog[idx].is_cb)
> -				err = true;
> -			for (int c = 0; c < frame && !err; c++) {
> -				if (subprog[ret_prog[c]].is_cb) {
> +			for (tmp = idx; tmp >= 0 && !err; tmp = subprog[tmp].dinfo.caller) {
> +				if (subprog[tmp].is_cb) {
>  					err = true;
>  					break;
>  				}
> @@ -6821,8 +6825,6 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  		if (!bpf_pseudo_call(insn + i) && !bpf_pseudo_func(insn + i))
>  			continue;
>  		/* remember insn and function to return to */
> -		ret_insn[frame] = i + 1;
> -		ret_prog[frame] = idx;
>  
>  		/* find the callee */
>  		next_insn = i + insn[i].imm + 1;
> @@ -6842,7 +6844,16 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  				return -EINVAL;
>  			}
>  		}
> +
> +		/* store caller info for after we return from callee */
> +		subprog[idx].dinfo.frame = frame;
> +		subprog[idx].dinfo.ret_insn = i + 1;
> +
> +		/* push caller idx into callee's dinfo */
> +		subprog[sidx].dinfo.caller = idx;
> +
>  		i = next_insn;
> +
>  		idx = sidx;
>  		if (!priv_stack_supported)
>  			subprog[idx].priv_stack_mode = NO_PRIV_STACK;
> @@ -6850,7 +6861,7 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  		if (subprog[idx].has_tail_call)
>  			tail_call_reachable = true;
>  
> -		frame++;
> +		frame = subprog_is_global(env, idx) ? 0 : frame + 1;
>  		if (frame >= MAX_CALL_FRAMES) {
>  			verbose(env, "the call stack of %d frames is too deep !\n",
>  				frame);
> @@ -6864,12 +6875,12 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  	 * tail call counter throughout bpf2bpf calls combined with tailcalls
>  	 */
>  	if (tail_call_reachable)
> -		for (j = 0; j < frame; j++) {
> -			if (subprog[ret_prog[j]].is_exception_cb) {
> +		for (tmp = idx; tmp >= 0; tmp = subprog[tmp].dinfo.caller) {
> +			if (subprog[tmp].is_exception_cb) {
>  				verbose(env, "cannot tail call within exception cb\n");
>  				return -EINVAL;
>  			}
> -			subprog[ret_prog[j]].tail_call_reachable = true;
> +			subprog[tmp].tail_call_reachable = true;
>  		}
>  	if (subprog[0].tail_call_reachable)
>  		env->prog->aux->tail_call_reachable = true;
> @@ -6877,13 +6888,18 @@ static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,
>  	/* end of for() loop means the last insn of the 'subprog'
>  	 * was reached. Doesn't matter whether it was JA or EXIT
>  	 */
> -	if (frame == 0)
> +	if (frame == 0 && subprog[idx].dinfo.caller < 0)
>  		return 0;
>  	if (subprog[idx].priv_stack_mode != PRIV_STACK_ADAPTIVE)
>  		depth -= round_up_stack_depth(env, subprog[idx].stack_depth);
> -	frame--;
> -	i = ret_insn[frame];
> -	idx = ret_prog[frame];
> +
> +	/* pop caller idx from callee */
> +	idx = subprog[idx].dinfo.caller;
> +
> +	/* retrieve caller state from its frame */
> +	frame = subprog[idx].dinfo.frame;
> +	i = subprog[idx].dinfo.ret_insn;
> +
>  	goto continue_func;
>  }
>  
> diff --git a/tools/testing/selftests/bpf/progs/test_global_func3.c b/tools/testing/selftests/bpf/progs/test_global_func3.c
> index 142b682d3c2f..974fd8c19561 100644
> --- a/tools/testing/selftests/bpf/progs/test_global_func3.c
> +++ b/tools/testing/selftests/bpf/progs/test_global_func3.c
> @@ -5,56 +5,56 @@
>  #include <bpf/bpf_helpers.h>
>  #include "bpf_misc.h"
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f1(struct __sk_buff *skb)
>  {
>  	return skb->len;
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f2(int val, struct __sk_buff *skb)
>  {
>  	return f1(skb) + val;
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f3(int val, struct __sk_buff *skb, int var)
>  {
>  	return f2(var, skb) + val;
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f4(struct __sk_buff *skb)
>  {
>  	return f3(1, skb, 2);
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f5(struct __sk_buff *skb)
>  {
>  	return f4(skb);
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f6(struct __sk_buff *skb)
>  {
>  	return f5(skb);
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f7(struct __sk_buff *skb)
>  {
>  	return f6(skb);
>  }
>  
> -__attribute__ ((noinline))
> +static __attribute__ ((noinline))
>  int f8(struct __sk_buff *skb)
>  {
>  	return f7(skb);
>  }
>  
>  SEC("tc")
> -__failure __msg("the call stack of 8 frames")
> +__failure __msg("the call stack of 9 frames")
>  int global_func3(struct __sk_buff *skb)
>  {
>  	return f8(skb);
> -- 
> 2.49.0

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks
  2026-03-09 21:37   ` Eduard Zingerman
@ 2026-03-10 19:07     ` Alexei Starovoitov
  0 siblings, 0 replies; 8+ messages in thread
From: Alexei Starovoitov @ 2026-03-10 19:07 UTC (permalink / raw)
  To: Eduard Zingerman
  Cc: Emil Tsalapatis, bpf, Andrii Nakryiko, Alexei Starovoitov,
	Daniel Borkmann, Martin KaFai Lau, Kumar Kartikeya Dwivedi,
	Song Liu, Yonghong Song

On Mon, Mar 9, 2026 at 2:37 PM Eduard Zingerman <eddyz87@gmail.com> wrote:
>
> On Mon, 2026-03-09 at 16:44 -0400, Emil Tsalapatis wrote:
> > The BPF verifier currently enforces a call stack depth of 8 frames,
> > regardless of the actual stack space consumption of those frames. The
> > limit is necessary for static call stacks, because the bookkeeping data
> > structures used by the verifier when stepping into static functions
> > during verification only support 8 stack frames. However, this
> > limitation only matters for static stack frames: Global subprogs are
> > verified by themselves and do not require limiting the call depth.
> >
> > Relax this limitation to only apply to static stack frames. Verification
> > now only fails when there is a sequence of 8 calls to non-global
> > subprogs. Calling into a global subprog resets the counter. This allows
> > deeper call stacks, provided all frames still fit in the stack.
> >
> > The change does not increase the maximum size of the call stack, only
> > the maximum number of frames we can place in it.
> >
> > Also change the progs/test_global_func3.c selftest to use static
> > functions, since with the new patch it would otherwise unexpectedly
> > pass verification.
> >
> > Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
> > ---
>
> Acked-by: Eduard Zingerman <eddyz87@gmail.com>
>
> [...]
>
> > diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> > index 090aa26d1c98..b45c3bb801c5 100644
> > --- a/include/linux/bpf_verifier.h
> > +++ b/include/linux/bpf_verifier.h
> > @@ -651,6 +651,12 @@ enum priv_stack_mode {
> >       PRIV_STACK_ADAPTIVE,
> >  };
> >
> > +struct bpf_subprog_call_depth_info {
> > +     int ret_insn; /* caller instruction where we return to. */
> > +     int caller; /* caller subprogram idx */
> > +     int frame; /* # of consecutive static call stack frames on top of stack */
> > +};
> > +
> >  struct bpf_subprog_info {
> >       /* 'start' has to be the first field otherwise find_subprog() won't work */
> >       u32 start; /* insn idx of function entry point */
> > @@ -678,6 +684,9 @@ struct bpf_subprog_info {
> >
> >       enum priv_stack_mode priv_stack_mode;
> >       struct bpf_subprog_arg_info args[MAX_BPF_FUNC_REG_ARGS];
> > +
> > +     /* temporary state used for call frame depth calculation */
> > +     struct bpf_subprog_call_depth_info dinfo;
>
> Nit: I think this adds an unnecessary nesting level (e.g. I'd had it
>      either at `env` level as a separate array, or here w/o additional
>      struct).

it's also not needed after analysis, right?
So let's not waste memory on it.

pw-bot: cr

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH bpf-next v4 2/2] bpf: Add deep call stack selftests
  2026-03-09 20:44 ` [PATCH bpf-next v4 2/2] bpf: Add deep call stack selftests Emil Tsalapatis
@ 2026-03-11  2:59   ` Yonghong Song
  0 siblings, 0 replies; 8+ messages in thread
From: Yonghong Song @ 2026-03-11  2:59 UTC (permalink / raw)
  To: Emil Tsalapatis, bpf
  Cc: andrii, ast, daniel, eddyz87, martin.lau, memxor, song



On 3/9/26 1:44 PM, Emil Tsalapatis wrote:
> Add tests that demonstrate the verifier support for deep call stacks
> while still enforcing maximum stack size limits.
>
> Acked-by: Eduard Zingerman <eddyz87@gmail.com>
> Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>

In subject, please use 'selftests/bpf: ...' instead of 'bpf: ...'.

> ---
>   .../bpf/prog_tests/test_global_funcs.c        |  2 +
>   .../bpf/progs/test_global_func_deep_stack.c   | 83 +++++++++++++++++++
>   2 files changed, 85 insertions(+)
>   create mode 100644 tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c
>
> diff --git a/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c b/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c
> index e905cbaf6b3d..500446808908 100644
> --- a/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c
> +++ b/tools/testing/selftests/bpf/prog_tests/test_global_funcs.c
> @@ -18,6 +18,7 @@
>   #include "test_global_func15.skel.h"
>   #include "test_global_func16.skel.h"
>   #include "test_global_func17.skel.h"
> +#include "test_global_func_deep_stack.skel.h"
>   #include "test_global_func_ctx_args.skel.h"
>   
>   #include "bpf/libbpf_internal.h"
> @@ -155,6 +156,7 @@ void test_test_global_funcs(void)
>   	RUN_TESTS(test_global_func15);
>   	RUN_TESTS(test_global_func16);
>   	RUN_TESTS(test_global_func17);
> +	RUN_TESTS(test_global_func_deep_stack);
>   	RUN_TESTS(test_global_func_ctx_args);
>   
>   	if (test__start_subtest("ctx_arg_rewrite"))
> diff --git a/tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c b/tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c
> new file mode 100644
> index 000000000000..733c3334b7b1
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/test_global_func_deep_stack.c
> @@ -0,0 +1,83 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/* Copyright (c) 2026 Meta Platforms, Inc and affiliates. */
> +#include <linux/bpf.h>
> +#include <bpf/bpf_helpers.h>
> +#include "bpf_misc.h"
> +
> +/*
> + * Macro tricks to tersely define for long non-recursive call chains. Add
> + * computation to the functions prevent tail recursion from reducing the
> + * stack size to 0.
> + */
> +
> +#define CAT(a, b) a ## b
> +#define XCAT(a, b) CAT(a, b)
> +
> +#define F_0 \
> +__attribute__((noinline)) \
> +int f0(unsigned long a) { volatile long b = a + 16; if (a == 0) return 0; return b; }
> +
> +#define FN(n, prev) \
> +__attribute__((noinline)) \
> +int XCAT(f, n)(unsigned long a) { volatile long b = XCAT(f, prev)(a - 1); if (!b) return 0; return b + 1; }
> +
> +/* Call chain 33 levels deep. */
> +#define F_1 F_0		FN(1, 0)
> +#define F_2 F_1   	FN(2, 1)

The above two misaligned.

> +#define F_3 F_2   	FN(3, 2)
> +#define F_4 F_3   	FN(4, 3)
> +#define F_5 F_4   	FN(5, 4)
> +#define F_6 F_5   	FN(6, 5)
> +#define F_7 F_6   	FN(7, 6)
> +#define F_8 F_7   	FN(8, 7)
> +#define F_9 F_8   	FN(9, 8)
> +#define F_10 F_9	FN(10, 9)
> +#define F_11 F_10	FN(11, 10)
> +#define F_12 F_11   	FN(12, 11)
> +#define F_13 F_12   	FN(13, 12)
> +#define F_14 F_13   	FN(14, 13)
> +#define F_15 F_14   	FN(15, 14)
> +#define F_16 F_15   	FN(16, 15)
> +#define F_17 F_16   	FN(17, 16)
> +#define F_18 F_17   	FN(18, 17)
> +#define F_19 F_18   	FN(19, 18)
> +#define F_20 F_19	FN(20, 19)
> +#define F_21 F_20	FN(21, 20)
> +#define F_22 F_21   	FN(22, 21)
> +#define F_23 F_22   	FN(23, 22)
> +#define F_24 F_23   	FN(24, 23)
> +#define F_25 F_24   	FN(25, 24)
> +#define F_26 F_25   	FN(26, 25)
> +#define F_27 F_26   	FN(27, 26)
> +#define F_28 F_27   	FN(28, 27)
> +#define F_29 F_28   	FN(29, 28)
> +#define F_30 F_29	FN(30, 29)
> +#define F_31 F_30	FN(31, 30)
> +#define F_32 F_31   	FN(32, 31)
> +
> +#define CAT2(a, b) a ## b
> +#define XCAT2(a, b) CAT2(a, b)
> +
> +#define F(n) XCAT2(F_, n)
> +
> +F(32)
> +
> +/* Ensure that even 32 levels deep, the function verifies. */
> +SEC("syscall")
> +__success
> +int global_func_deep_stack_success(struct __sk_buff *skb)
> +{
> +	return f31(55);
> +}
> +
> +/*
> + * Check we actually honor stack limits (33 * 16 = 528 > 512 = MAX_STACK_DEPTH).

In the above two lines, there are trailing empty space. Use scripts/checkpatch.pl
to check style issues.

> + * The stack depth is 16 because the verifier calls round_up_stack_depth() on
> + * the size.
> + */
> +SEC("syscall")
> +__failure __msg("combined stack size of 34 calls")
> +int global_func_deep_stack_fail(struct __sk_buff *skb)
> +{
> +	return f32(123);
> +}


^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-03-11  2:59 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-09 20:44 [PATCH bpf-next v4 0/2] bpf: Relax 8 frame limitation for global subprogs Emil Tsalapatis
2026-03-09 20:44 ` [PATCH bpf-next v4 1/2] bpf: Only enforce 8 frame call stack limit for all-static stacks Emil Tsalapatis
2026-03-09 21:23   ` bot+bpf-ci
2026-03-09 21:37   ` Eduard Zingerman
2026-03-10 19:07     ` Alexei Starovoitov
2026-03-10 14:04   ` Mykyta Yatsenko
2026-03-09 20:44 ` [PATCH bpf-next v4 2/2] bpf: Add deep call stack selftests Emil Tsalapatis
2026-03-11  2:59   ` Yonghong Song

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox