BPF List
 help / color / mirror / Atom feed
From: Daniel Borkmann <daniel@iogearbox.net>
To: ast@kernel.org
Cc: memxor@gmail.com, eddyz87@gmail.com, a.s.protopopov@gmail.com,
	info@starlabs.sg, bpf@vger.kernel.org,
	James Burton <jamesburton@meta.com>,
	Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
Subject: [PATCH bpf 4/6] bpf: Reject indirect jumps that leave their subprogram
Date: Wed,  9 Sep 2026 22:40:33 +0200	[thread overview]
Message-ID: <20260909204035.24289-4-daniel@iogearbox.net> (raw)
In-Reply-To: <20260909204035.24289-1-daniel@iogearbox.net>

The jump table of a subprog is collected in compute_subprog_jts() from the
insn_array maps of the program, and a map is attributed to the subprog that
contains its first entry. check_indirect_jump() instead resolves the targets
from the map the gotox register actually points to, bounded only by the
index range of that register, and never relates them back to the subprog
of the gotox.

The two disagree, so bpf_insn_successors() reports a subset of the edges the
BPF program can take and a gotox can enter a subprog the CFG never walked.
The x86 epilogue there pops the callee saved registers of its own subprog and
leaves the ones pushed by the current prologue unrestored, handing rbx, r13,
r14 and r15 to the kernel with the values the BPF program left in them.

Close both ends in check_indirect_jump(): confine the resolved targets to the
subprog of the gotox, and require each of them to be present in the jump table
the CFG walked, that is, in the successor set bpf_insn_successors() reported
for this instruction. The latter is the invariant that actually has to hold,
the former is kept because it names the problem the BPF program has.

Fixes: 493d9e0d6083 ("bpf, x86: add support for indirect jumps")
Reported-by: James Burton <jamesburton@meta.com>
Reported-by: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 include/linux/bpf_verifier.h                  |  1 +
 kernel/bpf/cfg.c                              | 35 +++++-----
 kernel/bpf/verifier.c                         | 65 +++++++++++++++++++
 .../selftests/bpf/progs/verifier_gotox.c      |  2 +-
 4 files changed, 85 insertions(+), 18 deletions(-)

diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 301a47d2b272..baf2e17d7019 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -826,6 +826,7 @@ struct bpf_subprog_info {
 	bool keep_fastcall_stack: 1;
 	bool changes_pkt_data: 1;
 	bool might_sleep: 1;
+	bool jt_spans_subprogs: 1;
 	u8 arg_cnt:4;
 
 	enum priv_stack_mode priv_stack_mode;
diff --git a/kernel/bpf/cfg.c b/kernel/bpf/cfg.c
index 8aee94689229..879587af8d08 100644
--- a/kernel/bpf/cfg.c
+++ b/kernel/bpf/cfg.c
@@ -315,6 +315,11 @@ static int compute_subprog_jts(struct bpf_verifier_env *env)
 			kvfree(jt_cur);
 			continue;
 		}
+		if (jt_cur->items[jt_cur->cnt - 1] >= (subprog + 1)->start) {
+			subprog->jt_spans_subprogs = true;
+			kvfree(jt_cur);
+			continue;
+		}
 
 		old_cnt = subprog->jt ? subprog->jt->cnt : 0;
 		jt = bpf_iarray_realloc(subprog->jt, old_cnt + jt_cur->cnt);
@@ -346,6 +351,7 @@ static void free_subprog_jts(struct bpf_verifier_env *env)
 	for (i = 0; i < ARRAY_SIZE(env->subprog_info); i++) {
 		kvfree(env->subprog_info[i].jt);
 		env->subprog_info[i].jt = NULL;
+		env->subprog_info[i].jt_spans_subprogs = false;
 	}
 	env->cfg.subprog_jts_ready = false;
 }
@@ -354,9 +360,8 @@ static struct bpf_iarray *
 create_jt(int t, struct bpf_verifier_env *env)
 {
 	struct bpf_subprog_info *subprog;
-	int subprog_start, subprog_end;
 	struct bpf_iarray *jt;
-	int i, err;
+	int subprog_start, err;
 
 	if (!env->cfg.subprog_jts_ready) {
 		err = compute_subprog_jts(env);
@@ -366,7 +371,17 @@ create_jt(int t, struct bpf_verifier_env *env)
 
 	subprog = bpf_find_containing_subprog(env, t);
 	subprog_start = subprog->start;
-	subprog_end = (subprog + 1)->start;
+
+	if (subprog->jt_spans_subprogs) {
+		verbose(env, "jump table of subprog starting at %u spans multiple subprogs\n",
+			subprog_start);
+		bpf_diag_program_structure(
+			env, subprog_start, "jump table spans subprograms",
+			"Keep every entry of a jump table inside one subprogram.",
+			"A jump table found for the subprogram that starts at instruction %u reaches past its end at instruction %u.",
+			subprog_start, (subprog + 1)->start);
+		return ERR_PTR(-EINVAL);
+	}
 
 	if (!subprog->jt) {
 		verbose(env, "no jump tables found for subprog starting at %u\n", subprog_start);
@@ -383,20 +398,6 @@ create_jt(int t, struct bpf_verifier_env *env)
 		return ERR_PTR(-ENOMEM);
 	memcpy(jt->items, subprog->jt->items, subprog->jt->cnt << 2);
 
-	for (i = 0; i < jt->cnt; i++) {
-		if (jt->items[i] < subprog_start || jt->items[i] >= subprog_end) {
-			verbose(env, "jump table for insn %d points outside of the subprog [%u,%u]\n",
-					t, subprog_start, subprog_end);
-			bpf_diag_program_structure(
-				env, t, "jump table target out of range",
-				"Keep every jump-table target inside the same subprogram.",
-				"The jump table for instruction %d points outside subprogram range [%u,%u).",
-				t, subprog_start, subprog_end);
-			kvfree(jt);
-			return ERR_PTR(-EINVAL);
-		}
-	}
-
 	return jt;
 }
 
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 72a3f5998dd2..45234e2fbee6 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -18165,11 +18165,56 @@ static int indirect_jump_min_max_index(struct bpf_verifier_env *env,
 	return 0;
 }
 
+/* 'jt' is sorted and free of duplicates, see sort_insn_array_uniq() */
+static bool jt_contains(const struct bpf_iarray *jt, u32 target)
+{
+	int l = 0, r = jt->cnt - 1, m;
+
+	while (l <= r) {
+		m = l + (r - l) / 2;
+		if (jt->items[m] == target)
+			return true;
+		if (jt->items[m] < target)
+			l = m + 1;
+		else
+			r = m - 1;
+	}
+	return false;
+}
+
+static int reject_gotox_out_of_subprog(struct bpf_verifier_env *env, u32 target,
+				       u32 subprog_start, u32 subprog_end)
+{
+	verbose(env, "indirect jump from insn %d to %u leaves the subprog [%u,%u)\n",
+		     env->insn_idx, target, subprog_start, subprog_end);
+	bpf_diag_program_structure(
+		env, env->insn_idx, "indirect jump leaves subprogram",
+		"Keep every reachable jump-table target inside the subprogram of the indirect jump.",
+		"Instruction %d can jump indirectly to instruction %u, which is outside its own subprogram [%u,%u).",
+		env->insn_idx, target, subprog_start, subprog_end);
+	return -EINVAL;
+}
+
+static int reject_gotox_without_cfg_edge(struct bpf_verifier_env *env, u32 target)
+{
+	verbose(env, "indirect jump from insn %d to %u is not in the jump table of the subprog\n",
+		     env->insn_idx, target);
+	bpf_diag_program_structure(
+		env, env->insn_idx, "indirect jump target without CFG edge",
+		"Resolve indirect jumps through a jump table whose entries all fall inside the subprogram of the jump.",
+		"Instruction %d can jump indirectly to instruction %u, which is not part of the jump table of its subprogram.",
+		env->insn_idx, target);
+	return -EINVAL;
+}
+
 /* gotox *dst_reg */
 static int check_indirect_jump(struct bpf_verifier_env *env, struct bpf_insn *insn)
 {
 	struct bpf_verifier_state *other_branch;
+	struct bpf_subprog_info *subprog;
+	u32 subprog_start, subprog_end;
 	struct bpf_reg_state *dst_reg;
+	struct bpf_iarray *jt;
 	struct bpf_map *map;
 	u32 min_index, max_index;
 	int err = 0;
@@ -18212,6 +18257,26 @@ static int check_indirect_jump(struct bpf_verifier_env *env, struct bpf_insn *in
 		return -EINVAL;
 	}
 
+	subprog = bpf_find_containing_subprog(env, env->insn_idx);
+	if (verifier_bug_if(!subprog, env, "no subprog contains insn %d", env->insn_idx))
+		return -EFAULT;
+	subprog_start = subprog->start;
+	subprog_end = (subprog + 1)->start;
+
+	jt = env->insn_aux_data[env->insn_idx].jt;
+	if (verifier_bug_if(!jt, env, "no jump table for insn %d", env->insn_idx))
+		return -EFAULT;
+
+	for (i = 0; i < n; i++) {
+		u32 target = env->gotox_tmp_buf->items[i];
+
+		if (target < subprog_start || target >= subprog_end)
+			return reject_gotox_out_of_subprog(env, target, subprog_start,
+							   subprog_end);
+		if (!jt_contains(jt, target))
+			return reject_gotox_without_cfg_edge(env, target);
+	}
+
 	for (i = 0; i < n - 1; i++) {
 		mark_indirect_target(env, env->gotox_tmp_buf->items[i]);
 		other_branch = push_stack(env, env->gotox_tmp_buf->items[i],
diff --git a/tools/testing/selftests/bpf/progs/verifier_gotox.c b/tools/testing/selftests/bpf/progs/verifier_gotox.c
index 5b18c9a27717..3567b29e2378 100644
--- a/tools/testing/selftests/bpf/progs/verifier_gotox.c
+++ b/tools/testing/selftests/bpf/progs/verifier_gotox.c
@@ -318,7 +318,7 @@ __used static int test_subprog(void)
 }
 
 SEC("socket")
-__failure __msg("jump table for insn 4 points outside of the subprog [0,10]")
+__failure __msg("jump table of subprog starting at 0 spans multiple subprogs")
 __naked void jump_table_outside_subprog(void)
 {
 	asm volatile ("						\
-- 
2.43.0


  parent reply	other threads:[~2026-09-09 20:40 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-09 20:40 [PATCH bpf 1/6] bpf: Avoid quadratic successor rescan in bpf_compute_scc Daniel Borkmann
2026-09-09 20:40 ` [PATCH bpf 2/6] bpf: Bound the number of indirect jump edges in a program Daniel Borkmann
2026-09-09 20:57   ` sashiko-bot
2026-09-10 11:15     ` Daniel Borkmann
2026-09-10 11:44   ` Anton Protopopov
2026-09-09 20:40 ` [PATCH bpf 3/6] bpf: Cache the jump table of a subprogram during CFG discovery Daniel Borkmann
2026-09-09 21:34   ` bot+bpf-ci
2026-09-10 11:21     ` Daniel Borkmann
2026-09-10 11:46   ` Anton Protopopov
2026-09-10 21:02   ` Eduard Zingerman
2026-09-09 20:40 ` Daniel Borkmann [this message]
2026-09-09 21:50   ` [PATCH bpf 4/6] bpf: Reject indirect jumps that leave their subprogram bot+bpf-ci
2026-09-10 12:10   ` Anton Protopopov
2026-09-10 19:37   ` Eduard Zingerman
2026-09-09 20:40 ` [PATCH bpf 5/6] selftests/bpf: Add tests for the indirect jump edge limit Daniel Borkmann
2026-09-09 21:34   ` bot+bpf-ci
2026-09-10 12:14   ` Anton Protopopov
2026-09-09 20:40 ` [PATCH bpf 6/6] selftests/bpf: Add tests for indirect jumps across subprograms Daniel Borkmann
2026-09-09 21:34   ` bot+bpf-ci
2026-09-10 12:22   ` Anton Protopopov
2026-09-10 18:54 ` [PATCH bpf 1/6] bpf: Avoid quadratic successor rescan in bpf_compute_scc Eduard Zingerman

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=20260909204035.24289-4-daniel@iogearbox.net \
    --to=daniel@iogearbox.net \
    --cc=a.s.protopopov@gmail.com \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=eddyz87@gmail.com \
    --cc=gnq25@mails.tsinghua.edu.cn \
    --cc=info@starlabs.sg \
    --cc=jamesburton@meta.com \
    --cc=memxor@gmail.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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox