* [PATCH bpf v3 0/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS
@ 2025-01-06 17:15 Arthur Fabre
2025-01-06 17:15 ` [PATCH bpf v3 1/2] " Arthur Fabre
2025-01-06 17:15 ` [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return Arthur Fabre
0 siblings, 2 replies; 7+ messages in thread
From: Arthur Fabre @ 2025-01-06 17:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
kernel-team, Arthur Fabre
A BPF function can return before its exit instruction: LD_ABS, LD_IND,
and tail_call() can all cause it to return abnormally.
When such a function is called by another BPF function, the verifier
doesn't take this into account when calculating the bounds of the return
value, or pointers to the caller's stack.
---
Changes in v2:
- Handle LD_ABS and LD_IND, not just tail_call()
- Split tests out
- Use inline asm for tests
Changes in v3:
- Don't handle just r0, model abnormal exits as a branch that exits or
falls through.
- Try to use C as much as possible for the tests.
Arthur Fabre (2):
bpf: Account for early exit of bpf_tail_call() and LD_ABS
selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with
abnormal return
kernel/bpf/verifier.c | 84 +++++++++----
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../bpf/progs/verifier_abnormal_ret.c | 115 ++++++++++++++++++
3 files changed, 178 insertions(+), 23 deletions(-)
create mode 100644 tools/testing/selftests/bpf/progs/verifier_abnormal_ret.c
--
2.43.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH bpf v3 1/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS
2025-01-06 17:15 [PATCH bpf v3 0/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS Arthur Fabre
@ 2025-01-06 17:15 ` Arthur Fabre
2025-01-06 20:31 ` Eduard Zingerman
2025-01-06 17:15 ` [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return Arthur Fabre
1 sibling, 1 reply; 7+ messages in thread
From: Arthur Fabre @ 2025-01-06 17:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
kernel-team, Arthur Fabre
bpf_tail_call(), LD_ABS, and LD_IND can cause the current function to
return abnormally:
- On success, bpf_tail_call() will jump to the tail_called program, and
that program will return directly to the outer caller.
- On failure, LD_ABS or LD_IND return directly to the outer caller.
But the verifier doesn't account for these abnormal exits, so it assumes
the instructions following a bpf_tail_call() or LD_ABS are always
executed, and updates bounds info accordingly.
Before BPF to BPF calls that was ok: the whole BPF program would
terminate anyways, so it didn't matter that the verifier state didn't
match reality.
But if these instructions are used in a function call, the verifier will
propagate some of this incorrect bounds info to the caller. There are at
least two kinds of this:
- The callee's return value in the caller.
- References to the caller's stack passed into the caller.
For example, loading:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 1);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} tail_call_map SEC(".maps");
static __attribute__((noinline)) int callee(struct xdp_md *ctx)
{
bpf_tail_call(ctx, &tail_call_map, 0);
int ret;
asm volatile("%0 = 23" : "=r"(ret));
return ret;
}
static SEC("xdp") int caller(struct xdp_md *ctx)
{
int res = callee(ctx);
if (res == 23) {
return XDP_PASS;
}
return XDP_DROP;
}
The verifier logs:
func#0 @0
func#1 @6
0: R1=ctx() R10=fp0
; int res = callee(ctx); @ test.c:24
0: (85) call pc+5
caller:
R10=fp0
callee:
frame1: R1=ctx() R10=fp0
6: frame1: R1=ctx() R10=fp0
; bpf_tail_call(ctx, &tail_call_map, 0); @ test.c:15
6: (18) r2 = 0xffff8a9c82a75800 ; frame1: R2_w=map_ptr(map=tail_call_map,ks=4,vs=4)
8: (b4) w3 = 0 ; frame1: R3_w=0
9: (85) call bpf_tail_call#12
10: frame1:
; asm volatile("%0 = 23" : "=r"(ret)); @ test.c:18
10: (b7) r0 = 23 ; frame1: R0_w=23
; return ret; @ test.c:19
11: (95) exit
returning from callee:
frame1: R0_w=23 R10=fp0
to caller at 1:
R0_w=23 R10=fp0
from 11 to 1: R0_w=23 R10=fp0
; int res = callee(ctx); @ test.c:24
1: (bc) w1 = w0 ; R0_w=23 R1_w=23
2: (b4) w0 = 2 ; R0=2
; @ test.c:0
3: (16) if w1 == 0x17 goto pc+1
3: R1=23
; } @ test.c:29
5: (95) exit
processed 10 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1
And tracks R0_w=23 from the callee through to the caller.
This lets it completely prune the res != 23 branch, skipping over
instruction 4.
But this isn't sound: the bpf_tail_call() could make the callee return
before r0 = 23.
Aside from pruning incorrect branches, this can also be used to read and
write arbitrary memory by using r0 as a index.
Make the verifier track instructions that can return abnormally as a
branch that either exits, or falls through to the remaining
instructions.
This naturally checks for resource leaks, so we can remove the explicit
checks for tail_calls and LD_ABS.
Fixes: f4d7e40a5b71 ("bpf: introduce function calls (verification)")
Signed-off-by: Arthur Fabre <afabre@cloudflare.com>
Cc: stable@vger.kernel.org
---
kernel/bpf/verifier.c | 84 +++++++++++++++++++++++++++++++------------
1 file changed, 61 insertions(+), 23 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 77f56674aaa9..a0853e9866d8 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10488,13 +10488,20 @@ record_func_key(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
return 0;
}
-static int check_reference_leak(struct bpf_verifier_env *env, bool exception_exit)
+enum bpf_exit {
+ BPF_EXIT_INSN,
+ BPF_EXIT_EXCEPTION,
+ BPF_EXIT_TAIL_CALL,
+ BPF_EXIT_LD_ABS,
+};
+
+static int check_reference_leak(struct bpf_verifier_env *env, enum bpf_exit exit)
{
struct bpf_func_state *state = cur_func(env);
bool refs_lingering = false;
int i;
- if (!exception_exit && state->frameno)
+ if (exit != BPF_EXIT_EXCEPTION && state->frameno)
return 0;
for (i = 0; i < state->acquired_refs; i++) {
@@ -10507,16 +10514,32 @@ static int check_reference_leak(struct bpf_verifier_env *env, bool exception_exi
return refs_lingering ? -EINVAL : 0;
}
-static int check_resource_leak(struct bpf_verifier_env *env, bool exception_exit, bool check_lock, const char *prefix)
+static int check_resource_leak(struct bpf_verifier_env *env, enum bpf_exit exit, bool check_lock)
{
int err;
+ const char *prefix;
+
+ switch (exit) {
+ case BPF_EXIT_INSN:
+ prefix = "BPF_EXIT instruction";
+ break;
+ case BPF_EXIT_EXCEPTION:
+ prefix = "bpf_throw";
+ break;
+ case BPF_EXIT_TAIL_CALL:
+ prefix = "tail_call";
+ break;
+ case BPF_EXIT_LD_ABS:
+ prefix = "BPF_LD_[ABS|IND]";
+ break;
+ }
if (check_lock && cur_func(env)->active_locks) {
verbose(env, "%s cannot be used inside bpf_spin_lock-ed region\n", prefix);
return -EINVAL;
}
- err = check_reference_leak(env, exception_exit);
+ err = check_reference_leak(env, exit);
if (err) {
verbose(env, "%s would lead to reference leak\n", prefix);
return err;
@@ -10802,11 +10825,6 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
}
switch (func_id) {
- case BPF_FUNC_tail_call:
- err = check_resource_leak(env, false, true, "tail_call");
- if (err)
- return err;
- break;
case BPF_FUNC_get_local_storage:
/* check that flags argument in get_local_storage(map, flags) is 0,
* this is required because get_local_storage() can't return an error.
@@ -15963,14 +15981,6 @@ static int check_ld_abs(struct bpf_verifier_env *env, struct bpf_insn *insn)
if (err)
return err;
- /* Disallow usage of BPF_LD_[ABS|IND] with reference tracking, as
- * gen_ld_abs() may terminate the program at runtime, leading to
- * reference leak.
- */
- err = check_resource_leak(env, false, true, "BPF_LD_[ABS|IND]");
- if (err)
- return err;
-
if (regs[ctx_reg].type != PTR_TO_CTX) {
verbose(env,
"at the time of BPF_LD_ABS|IND R6 != pointer to skb\n");
@@ -18540,7 +18550,7 @@ static int do_check(struct bpf_verifier_env *env)
int prev_insn_idx = -1;
for (;;) {
- bool exception_exit = false;
+ enum bpf_exit exit;
struct bpf_insn *insn;
u8 class;
int err;
@@ -18760,7 +18770,7 @@ static int do_check(struct bpf_verifier_env *env)
} else if (insn->src_reg == BPF_PSEUDO_KFUNC_CALL) {
err = check_kfunc_call(env, insn, &env->insn_idx);
if (!err && is_bpf_throw_kfunc(insn)) {
- exception_exit = true;
+ exit = BPF_EXIT_EXCEPTION;
goto process_bpf_exit_full;
}
} else {
@@ -18770,6 +18780,21 @@ static int do_check(struct bpf_verifier_env *env)
return err;
mark_reg_scratched(env, BPF_REG_0);
+
+ if (insn->src_reg == 0 && insn->imm == BPF_FUNC_tail_call) {
+ /* Explore both cases: tail_call fails and we fallthrough,
+ * or it succeeds and we exit the current function.
+ */
+ if (!push_stack(env, env->insn_idx + 1, env->insn_idx, false))
+ return -ENOMEM;
+ /* bpf_tail_call() doesn't set r0 on failure / in the fallthrough case.
+ * But it does on success, so we have to mark it after queueing the
+ * fallthrough case, but before prepare_func_exit().
+ */
+ __mark_reg_unknown(env, &state->frame[state->curframe]->regs[BPF_REG_0]);
+ exit = BPF_EXIT_TAIL_CALL;
+ goto process_bpf_exit_full;
+ }
} else if (opcode == BPF_JA) {
if (BPF_SRC(insn->code) != BPF_K ||
insn->src_reg != BPF_REG_0 ||
@@ -18795,6 +18820,8 @@ static int do_check(struct bpf_verifier_env *env)
verbose(env, "BPF_EXIT uses reserved fields\n");
return -EINVAL;
}
+ exit = BPF_EXIT_INSN;
+
process_bpf_exit_full:
/* We must do check_reference_leak here before
* prepare_func_exit to handle the case when
@@ -18802,8 +18829,7 @@ static int do_check(struct bpf_verifier_env *env)
* function, for which reference_state must
* match caller reference state when it exits.
*/
- err = check_resource_leak(env, exception_exit, !env->cur_state->curframe,
- "BPF_EXIT instruction");
+ err = check_resource_leak(env, exit, !env->cur_state->curframe);
if (err)
return err;
@@ -18817,7 +18843,7 @@ static int do_check(struct bpf_verifier_env *env)
* exits. We also skip return code checks as
* they are not needed for exceptional exits.
*/
- if (exception_exit)
+ if (exit == BPF_EXIT_EXCEPTION)
goto process_bpf_exit;
if (state->curframe) {
@@ -18829,6 +18855,12 @@ static int do_check(struct bpf_verifier_env *env)
continue;
}
+ /* BPF_EXIT instruction is the only one that doesn't intrinsically
+ * set R0.
+ */
+ if (exit != BPF_EXIT_INSN)
+ goto process_bpf_exit;
+
err = check_return_code(env, BPF_REG_0, "R0");
if (err)
return err;
@@ -18857,7 +18889,13 @@ static int do_check(struct bpf_verifier_env *env)
err = check_ld_abs(env, insn);
if (err)
return err;
-
+ /* Explore both cases: LD_ABS|IND succeeds and we fallthrough,
+ * or it fails and we exit the current function.
+ */
+ if (!push_stack(env, env->insn_idx + 1, env->insn_idx, false))
+ return -ENOMEM;
+ exit = BPF_EXIT_LD_ABS;
+ goto process_bpf_exit_full;
} else if (mode == BPF_IMM) {
err = check_ld_imm(env, insn);
if (err)
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return
2025-01-06 17:15 [PATCH bpf v3 0/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS Arthur Fabre
2025-01-06 17:15 ` [PATCH bpf v3 1/2] " Arthur Fabre
@ 2025-01-06 17:15 ` Arthur Fabre
2025-01-06 20:34 ` Eduard Zingerman
1 sibling, 1 reply; 7+ messages in thread
From: Arthur Fabre @ 2025-01-06 17:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
kernel-team, Arthur Fabre
In all three cases where a callee can abnormally return (tail_call(),
LD_ABS, and LD_IND), test the verifier doesn't know the bounds of:
- r0 / what the callee returned.
- References to the caller's stack passed to the callee.
Additionally, ensure the tail_call fallthrough case can't access r0, as
bpf_tail_call() returns nothing on failure.
Signed-off-by: Arthur Fabre <afabre@cloudflare.com>
---
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../bpf/progs/verifier_abnormal_ret.c | 115 ++++++++++++++++++
2 files changed, 117 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/verifier_abnormal_ret.c
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index 3ee40ee9413a..6bed606544e3 100644
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -3,6 +3,7 @@
#include <test_progs.h>
#include "cap_helpers.h"
+#include "verifier_abnormal_ret.skel.h"
#include "verifier_and.skel.h"
#include "verifier_arena.skel.h"
#include "verifier_arena_large.skel.h"
@@ -133,6 +134,7 @@ static void run_tests_aux(const char *skel_name,
#define RUN(skel) run_tests_aux(#skel, skel##__elf_bytes, NULL)
+void test_verifier_abnormal_ret(void) { RUN(verifier_abnormal_ret); }
void test_verifier_and(void) { RUN(verifier_and); }
void test_verifier_arena(void) { RUN(verifier_arena); }
void test_verifier_arena_large(void) { RUN(verifier_arena_large); }
diff --git a/tools/testing/selftests/bpf/progs/verifier_abnormal_ret.c b/tools/testing/selftests/bpf/progs/verifier_abnormal_ret.c
new file mode 100644
index 000000000000..185c19ba3329
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/verifier_abnormal_ret.c
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include "../../../include/linux/filter.h"
+#include "bpf_misc.h"
+
+#define TEST(NAME, CALLEE) \
+ SEC("socket") \
+ __description("r0: " #NAME) \
+ __failure __msg("math between ctx pointer and register with unbounded min value") \
+ __naked int check_abnormal_ret_r0_##NAME(void) \
+ { \
+ asm volatile(" \
+ r6 = r1; \
+ r2 = r10; \
+ r2 += -8; \
+ call " #CALLEE "; \
+ r6 += r0; \
+ r0 = 0; \
+ exit; \
+ " : \
+ : \
+ : __clobber_all); \
+ } \
+ \
+ SEC("socket") \
+ __description("ref: " #NAME) \
+ __failure __msg("math between ctx pointer and register with unbounded min value") \
+ __naked int check_abnormal_ret_ref_##NAME(void) \
+ { \
+ asm volatile(" \
+ r6 = r1; \
+ r7 = r10; \
+ r7 += -8; \
+ r2 = r7; \
+ call " #CALLEE "; \
+ r0 = *(u64*)(r7 + 0); \
+ r6 += r0; \
+ exit; \
+ " : \
+ : \
+ : __clobber_all); \
+ }
+
+TEST(ld_abs, callee_ld_abs);
+TEST(ld_ind, callee_ld_ind);
+TEST(tail_call, callee_tail_call);
+
+static __naked __noinline __used
+int callee_ld_abs(void)
+{
+ asm volatile(" \
+ r6 = r1; \
+ r9 = r2; \
+ .8byte %[ld_abs]; \
+ *(u64*)(r9 + 0) = 1; \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm_insn(ld_abs, BPF_LD_ABS(BPF_W, 0))
+ : __clobber_all);
+}
+
+static __naked __noinline __used
+int callee_ld_ind(void)
+{
+ asm volatile(" \
+ r6 = r1; \
+ r7 = 1; \
+ r9 = r2; \
+ .8byte %[ld_ind]; \
+ *(u64*)(r9 + 0) = 1; \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm_insn(ld_ind, BPF_LD_IND(BPF_W, BPF_REG_7, 0))
+ : __clobber_all);
+}
+
+SEC("socket")
+__auxiliary __naked
+int dummy_prog(void)
+{
+ asm volatile("r0 = 1; exit;");
+}
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(int));
+ __array(values, void(void));
+} map_prog SEC(".maps") = {
+ .values = {
+ [0] = (void *)&dummy_prog,
+ },
+};
+
+static __noinline __used
+int callee_tail_call(struct __sk_buff *skb, __u64 *foo)
+{
+ bpf_tail_call(skb, &map_prog, 0);
+ *foo = 1;
+ return 0;
+}
+
+SEC("socket")
+__description("r0 not set by tail_call")
+__failure __msg("R0 !read_ok")
+int check_abnormal_ret_tail_call_fail(struct __sk_buff *skb)
+{
+ return bpf_tail_call(skb, &map_prog, 0);
+}
+
+char _license[] SEC("license") = "GPL";
--
2.43.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH bpf v3 1/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS
2025-01-06 17:15 ` [PATCH bpf v3 1/2] " Arthur Fabre
@ 2025-01-06 20:31 ` Eduard Zingerman
2025-01-08 20:44 ` Arthur Fabre
0 siblings, 1 reply; 7+ messages in thread
From: Eduard Zingerman @ 2025-01-06 20:31 UTC (permalink / raw)
To: Arthur Fabre, bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Song Liu, Yonghong Song,
KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa, kernel-team
On Mon, 2025-01-06 at 18:15 +0100, Arthur Fabre wrote:
> bpf_tail_call(), LD_ABS, and LD_IND can cause the current function to
> return abnormally:
> - On success, bpf_tail_call() will jump to the tail_called program, and
> that program will return directly to the outer caller.
> - On failure, LD_ABS or LD_IND return directly to the outer caller.
>
> But the verifier doesn't account for these abnormal exits, so it assumes
> the instructions following a bpf_tail_call() or LD_ABS are always
> executed, and updates bounds info accordingly.
>
> Before BPF to BPF calls that was ok: the whole BPF program would
> terminate anyways, so it didn't matter that the verifier state didn't
> match reality.
>
> But if these instructions are used in a function call, the verifier will
> propagate some of this incorrect bounds info to the caller. There are at
> least two kinds of this:
> - The callee's return value in the caller.
> - References to the caller's stack passed into the caller.
>
> For example, loading:
>
> #include <linux/bpf.h>
> #include <bpf/bpf_helpers.h>
>
> struct {
> __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
> __uint(max_entries, 1);
> __uint(key_size, sizeof(__u32));
> __uint(value_size, sizeof(__u32));
> } tail_call_map SEC(".maps");
>
> static __attribute__((noinline)) int callee(struct xdp_md *ctx)
> {
> bpf_tail_call(ctx, &tail_call_map, 0);
>
> int ret;
> asm volatile("%0 = 23" : "=r"(ret));
> return ret;
> }
>
> static SEC("xdp") int caller(struct xdp_md *ctx)
> {
> int res = callee(ctx);
> if (res == 23) {
> return XDP_PASS;
> }
> return XDP_DROP;
> }
>
> The verifier logs:
>
> func#0 @0
> func#1 @6
> 0: R1=ctx() R10=fp0
> ; int res = callee(ctx); @ test.c:24
> 0: (85) call pc+5
> caller:
> R10=fp0
> callee:
> frame1: R1=ctx() R10=fp0
> 6: frame1: R1=ctx() R10=fp0
> ; bpf_tail_call(ctx, &tail_call_map, 0); @ test.c:15
> 6: (18) r2 = 0xffff8a9c82a75800 ; frame1: R2_w=map_ptr(map=tail_call_map,ks=4,vs=4)
> 8: (b4) w3 = 0 ; frame1: R3_w=0
> 9: (85) call bpf_tail_call#12
> 10: frame1:
> ; asm volatile("%0 = 23" : "=r"(ret)); @ test.c:18
> 10: (b7) r0 = 23 ; frame1: R0_w=23
> ; return ret; @ test.c:19
> 11: (95) exit
> returning from callee:
> frame1: R0_w=23 R10=fp0
> to caller at 1:
> R0_w=23 R10=fp0
>
> from 11 to 1: R0_w=23 R10=fp0
> ; int res = callee(ctx); @ test.c:24
> 1: (bc) w1 = w0 ; R0_w=23 R1_w=23
> 2: (b4) w0 = 2 ; R0=2
> ; @ test.c:0
> 3: (16) if w1 == 0x17 goto pc+1
> 3: R1=23
> ; } @ test.c:29
> 5: (95) exit
> processed 10 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 1
>
> And tracks R0_w=23 from the callee through to the caller.
> This lets it completely prune the res != 23 branch, skipping over
> instruction 4.
>
> But this isn't sound: the bpf_tail_call() could make the callee return
> before r0 = 23.
>
> Aside from pruning incorrect branches, this can also be used to read and
> write arbitrary memory by using r0 as a index.
>
> Make the verifier track instructions that can return abnormally as a
> branch that either exits, or falls through to the remaining
> instructions.
>
> This naturally checks for resource leaks, so we can remove the explicit
> checks for tail_calls and LD_ABS.
>
> Fixes: f4d7e40a5b71 ("bpf: introduce function calls (verification)")
> Signed-off-by: Arthur Fabre <afabre@cloudflare.com>
> Cc: stable@vger.kernel.org
> ---
This patch is correct as far as I can tell.
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
[...]
> @@ -18770,6 +18780,21 @@ static int do_check(struct bpf_verifier_env *env)
> return err;
>
> mark_reg_scratched(env, BPF_REG_0);
> +
> + if (insn->src_reg == 0 && insn->imm == BPF_FUNC_tail_call) {
> + /* Explore both cases: tail_call fails and we fallthrough,
> + * or it succeeds and we exit the current function.
> + */
> + if (!push_stack(env, env->insn_idx + 1, env->insn_idx, false))
> + return -ENOMEM;
> + /* bpf_tail_call() doesn't set r0 on failure / in the fallthrough case.
> + * But it does on success, so we have to mark it after queueing the
> + * fallthrough case, but before prepare_func_exit().
> + */
> + __mark_reg_unknown(env, &state->frame[state->curframe]->regs[BPF_REG_0]);
> + exit = BPF_EXIT_TAIL_CALL;
> + goto process_bpf_exit_full;
> + }
Nit: it's a bit unfortunate, that this logic is inside do_check,
instead of check_helper_call() and check_ld_abs().
But it makes BPF_EXIT_* propagation simpler.
> } else if (opcode == BPF_JA) {
> if (BPF_SRC(insn->code) != BPF_K ||
> insn->src_reg != BPF_REG_0 ||
[...]
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return
2025-01-06 17:15 ` [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return Arthur Fabre
@ 2025-01-06 20:34 ` Eduard Zingerman
2025-01-08 20:46 ` Arthur Fabre
0 siblings, 1 reply; 7+ messages in thread
From: Eduard Zingerman @ 2025-01-06 20:34 UTC (permalink / raw)
To: Arthur Fabre, bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Song Liu, Yonghong Song,
KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa, kernel-team
On Mon, 2025-01-06 at 18:15 +0100, Arthur Fabre wrote:
> In all three cases where a callee can abnormally return (tail_call(),
> LD_ABS, and LD_IND), test the verifier doesn't know the bounds of:
>
> - r0 / what the callee returned.
> - References to the caller's stack passed to the callee.
>
> Additionally, ensure the tail_call fallthrough case can't access r0, as
> bpf_tail_call() returns nothing on failure.
>
> Signed-off-by: Arthur Fabre <afabre@cloudflare.com>
> ---
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
[...]
> +#define TEST(NAME, CALLEE) \
> + SEC("socket") \
> + __description("r0: " #NAME) \
> + __failure __msg("math between ctx pointer and register with unbounded min value") \
> + __naked int check_abnormal_ret_r0_##NAME(void) \
> + { \
> + asm volatile(" \
> + r6 = r1; \
> + r2 = r10; \
> + r2 += -8; \
> + call " #CALLEE "; \
> + r6 += r0; \
> + r0 = 0; \
> + exit; \
> + " : \
> + : \
> + : __clobber_all); \
> + } \
> + \
> + SEC("socket") \
> + __description("ref: " #NAME) \
> + __failure __msg("math between ctx pointer and register with unbounded min value") \
> + __naked int check_abnormal_ret_ref_##NAME(void) \
> + { \
> + asm volatile(" \
> + r6 = r1; \
> + r7 = r10; \
> + r7 += -8; \
> + r2 = r7; \
> + call " #CALLEE "; \
> + r0 = *(u64*)(r7 + 0); \
> + r6 += r0; \
> + exit; \
> + " : \
> + : \
> + : __clobber_all); \
> + }
Nit: I think having both cases is an overkill, as both effectively
test if branching occur.
[...]
> +struct {
> + __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
> + __uint(max_entries, 1);
> + __uint(key_size, sizeof(int));
> + __array(values, void(void));
> +} map_prog SEC(".maps") = {
> + .values = {
> + [0] = (void *)&dummy_prog,
> + },
> +};
> +
> +static __noinline __used
> +int callee_tail_call(struct __sk_buff *skb, __u64 *foo)
> +{
> + bpf_tail_call(skb, &map_prog, 0);
> + *foo = 1;
> + return 0;
> +}
Nit: I'd also add a test where invalid action is taken
after bpf_tail_call inside the callee,
just to make sure that both branches are explored.
> +
> +SEC("socket")
> +__description("r0 not set by tail_call")
> +__failure __msg("R0 !read_ok")
> +int check_abnormal_ret_tail_call_fail(struct __sk_buff *skb)
> +{
> + return bpf_tail_call(skb, &map_prog, 0);
> +}
> +
> +char _license[] SEC("license") = "GPL";
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf v3 1/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS
2025-01-06 20:31 ` Eduard Zingerman
@ 2025-01-08 20:44 ` Arthur Fabre
0 siblings, 0 replies; 7+ messages in thread
From: Arthur Fabre @ 2025-01-08 20:44 UTC (permalink / raw)
To: Eduard Zingerman, bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Song Liu, Yonghong Song,
KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa, kernel-team
On Mon Jan 6, 2025 at 9:31 PM CET, Eduard Zingerman wrote:
> On Mon, 2025-01-06 at 18:15 +0100, Arthur Fabre wrote:
[...]
> This patch is correct as far as I can tell.
>
> Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Thanks for the review!
> [...]
>
> > @@ -18770,6 +18780,21 @@ static int do_check(struct bpf_verifier_env *env)
> > return err;
> >
> > mark_reg_scratched(env, BPF_REG_0);
> > +
> > + if (insn->src_reg == 0 && insn->imm == BPF_FUNC_tail_call) {
> > + /* Explore both cases: tail_call fails and we fallthrough,
> > + * or it succeeds and we exit the current function.
> > + */
> > + if (!push_stack(env, env->insn_idx + 1, env->insn_idx, false))
> > + return -ENOMEM;
> > + /* bpf_tail_call() doesn't set r0 on failure / in the fallthrough case.
> > + * But it does on success, so we have to mark it after queueing the
> > + * fallthrough case, but before prepare_func_exit().
> > + */
> > + __mark_reg_unknown(env, &state->frame[state->curframe]->regs[BPF_REG_0]);
> > + exit = BPF_EXIT_TAIL_CALL;
> > + goto process_bpf_exit_full;
> > + }
>
> Nit: it's a bit unfortunate, that this logic is inside do_check,
> instead of check_helper_call() and check_ld_abs().
> But it makes BPF_EXIT_* propagation simpler.
Agreed, it's unfortunate to add more to do_check().
I tried passing exit / BPF_EXIT_* by pointer to check_helper_call() and
check_ld_abs(), but that still means we need a conditional in do_check()
to see if it's set:
if (exit != NULL)
oto process_bpf_exit_full;
Happy to switch to that if you think it's cleaner.
[...]
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return
2025-01-06 20:34 ` Eduard Zingerman
@ 2025-01-08 20:46 ` Arthur Fabre
0 siblings, 0 replies; 7+ messages in thread
From: Arthur Fabre @ 2025-01-08 20:46 UTC (permalink / raw)
To: Eduard Zingerman, bpf
Cc: Alexei Starovoitov, Daniel Borkmann, John Fastabend,
Andrii Nakryiko, Martin KaFai Lau, Song Liu, Yonghong Song,
KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa, kernel-team
On Mon Jan 6, 2025 at 9:34 PM CET, Eduard Zingerman wrote:
> On Mon, 2025-01-06 at 18:15 +0100, Arthur Fabre wrote:
[...]
> > +#define TEST(NAME, CALLEE) \
> > + SEC("socket") \
> > + __description("r0: " #NAME) \
> > + __failure __msg("math between ctx pointer and register with unbounded min value") \
> > + __naked int check_abnormal_ret_r0_##NAME(void) \
> > + { \
> > + asm volatile(" \
> > + r6 = r1; \
> > + r2 = r10; \
> > + r2 += -8; \
> > + call " #CALLEE "; \
> > + r6 += r0; \
> > + r0 = 0; \
> > + exit; \
> > + " : \
> > + : \
> > + : __clobber_all); \
> > + } \
> > + \
> > + SEC("socket") \
> > + __description("ref: " #NAME) \
> > + __failure __msg("math between ctx pointer and register with unbounded min value") \
> > + __naked int check_abnormal_ret_ref_##NAME(void) \
> > + { \
> > + asm volatile(" \
> > + r6 = r1; \
> > + r7 = r10; \
> > + r7 += -8; \
> > + r2 = r7; \
> > + call " #CALLEE "; \
> > + r0 = *(u64*)(r7 + 0); \
> > + r6 += r0; \
> > + exit; \
> > + " : \
> > + : \
> > + : __clobber_all); \
> > + }
>
> Nit: I think having both cases is an overkill, as both effectively
> test if branching occur.
Fair enough, I can drop the reference tests.
> [...]
>
> > +struct {
> > + __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
> > + __uint(max_entries, 1);
> > + __uint(key_size, sizeof(int));
> > + __array(values, void(void));
> > +} map_prog SEC(".maps") = {
> > + .values = {
> > + [0] = (void *)&dummy_prog,
> > + },
> > +};
> > +
> > +static __noinline __used
> > +int callee_tail_call(struct __sk_buff *skb, __u64 *foo)
> > +{
> > + bpf_tail_call(skb, &map_prog, 0);
> > + *foo = 1;
> > + return 0;
> > +}
>
> Nit: I'd also add a test where invalid action is taken
> after bpf_tail_call inside the callee,
> just to make sure that both branches are explored.
Good idea, I'll add that in and resend. Thanks for the feedback!
>
> > +
> > +SEC("socket")
> > +__description("r0 not set by tail_call")
> > +__failure __msg("R0 !read_ok")
> > +int check_abnormal_ret_tail_call_fail(struct __sk_buff *skb)
> > +{
> > + return bpf_tail_call(skb, &map_prog, 0);
> > +}
> > +
> > +char _license[] SEC("license") = "GPL";
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2025-01-08 20:46 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-01-06 17:15 [PATCH bpf v3 0/2] bpf: Account for early exit of bpf_tail_call() and LD_ABS Arthur Fabre
2025-01-06 17:15 ` [PATCH bpf v3 1/2] " Arthur Fabre
2025-01-06 20:31 ` Eduard Zingerman
2025-01-08 20:44 ` Arthur Fabre
2025-01-06 17:15 ` [PATCH bpf v3 2/2] selftests/bpf: Test r0 and ref lifetime after BPF-BPF call with abnormal return Arthur Fabre
2025-01-06 20:34 ` Eduard Zingerman
2025-01-08 20:46 ` Arthur Fabre
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox