Skip to contentSkip to conversation

check bpf_func_state->callback_depth when pruning states

PATCH Superseded Eduard Zingerman 2 participants 12 messages Feb 12, 2024 – Feb 20, 2024

Subsystems (4)

v1 of 3 Superseded Latest is v3 →

lore
Share & tools
lore

Eduard Zingerman

author maintainer
#0 lore raw

cover check bpf_func_state->callback_depth when pruning states

This patch-set fixes bug in states pruning logic hit in mailing list discussion [0]. The details of the fix are in patch #2.
A change to the test case test_tcp_custom_syncookie.c is necessary, otherwise updated verifier won't be able to process it due to instruction complexity limit. This change is done in patch #1.

The main idea for the fix belongs to Yonghong Song, mine contribution is merely in review and test cases.

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

Eduard Zingerman (3):
selftests/bpf: update tcp_custom_syncookie to use scalar packet offset
bpf: check bpf_func_state->callback_depth when pruning states selftests/bpf: test case for callback_depth states pruning logic

 kernel/bpf/verifier.c                         |  3 +
 .../bpf/progs/test_tcp_custom_syncookie.c     | 83 ++++++++++++-------
 .../bpf/progs/verifier_iterating_callbacks.c  | 70 ++++++++++++++++
 3 files changed, 126 insertions(+), 30 deletions(-)
Signature · 1 line
2.43.0
author maintainer
#1 lore raw

PATCH 1/3 selftests/bpf: update tcp_custom_syncookie to use scalar packet offset

The next commit in a series fixes bug in bpf_loop() handling. That change makes tcp_custom_syncookie test too complex to verify.

This commit updates tcp_custom_syncookie.c:tcp_parse_option() to use explicit packet offset (ctx->off) for packet access instead of ever moving pointer (ctx->ptr), this reduces verification complexity:
- the tcp_parse_option() is passed as a callback to bpf_loop();
- suppose a checkpoint is created each time at function entry;
- the ctx->ptr is tracked by verifier as PTR_TO_PACKET;
- the ctx->ptr is incremented in tcp_parse_option(),
thus umax_value field tracked for it is incremented as well;
- on each next iteration of tcp_parse_option()
checkpoint from a previous iteration can't be reused for state pruning, because PTR_TO_PACKET registers are considered equivalent only if old->umax_value >= cur->umax_value;
- on the other hand, the ctx->off is a SCALAR,
subject to widen_imprecise_scalars();
- it's exact bounds are eventually forgotten and it is tracked as unknown scalar at entry to tcp_parse_option();
- hence checkpoints created at the start of the function eventually converge.

The change is similar to one applied in [0] to xdp_synproxy_kern.c.

[0] commit 977bc146d4eb ("selftests/bpf: track tcp payload offset as scalar in xdp_synproxy")

---
 .../bpf/progs/test_tcp_custom_syncookie.c     | 83 ++++++++++++-------
 1 file changed, 53 insertions(+), 30 deletions(-)
tools/testing/selftests/bpf/progs/test_tcp_custom_syncookie.c +53 -30
index a5501b29979a..c8e4553648bf 100644
@@ -10,6 +10,8 @@
 #include "test_siphash.h"
 #include "test_tcp_custom_syncookie.h"
 
+#define MAX_PACKET_OFF 0xffff
+
 /* Hash is calculated for each client and split into ISN and TS.
  *
  *       MSB                                   LSB
@@ -52,16 +54,15 @@ static siphash_key_t test_key_siphash = {
 
 struct tcp_syncookie {
 	struct __sk_buff *skb;
+	void *data;
 	void *data_end;
 	struct ethhdr *eth;
 	struct iphdr *ipv4;
 	struct ipv6hdr *ipv6;
 	struct tcphdr *tcp;
-	union {
-		char *ptr;
-		__be32 *ptr32;
-	};
+	__be32 *ptr32;
 	struct bpf_tcp_req_attrs attrs;
+	u32 off;
 	u32 cookie;
 	u64 first;
 };
@@ -70,6 +71,7 @@ bool handled_syn, handled_ack;
 
 static int tcp_load_headers(struct tcp_syncookie *ctx)
 {
+	ctx->data = (void *)(long)ctx->skb->data;
 	ctx->data_end = (void *)(long)ctx->skb->data_end;
 	ctx->eth = (struct ethhdr *)(long)ctx->skb->data;
 
@@ -134,6 +136,7 @@ static int tcp_reload_headers(struct tcp_syncookie *ctx)
 	if (bpf_skb_change_tail(ctx->skb, data_len + 60 - ctx->tcp->doff * 4, 0))
 		goto err;
 
+	ctx->data = (void *)(long)ctx->skb->data;
 	ctx->data_end = (void *)(long)ctx->skb->data_end;
 	ctx->eth = (struct ethhdr *)(long)ctx->skb->data;
 	if (ctx->ipv4) {
@@ -195,47 +198,68 @@ static int tcp_validate_header(struct tcp_syncookie *ctx)
 	return -1;
 }
 
-static int tcp_parse_option(__u32 index, struct tcp_syncookie *ctx)
+static __always_inline void *next(struct tcp_syncookie *ctx, __u32 sz)
 {
-	char opcode, opsize;
+	__u64 off = ctx->off;
+	__u8 *data;
 
-	if (ctx->ptr + 1 > ctx->data_end)
-		goto stop;
+	/* Verifier forbids access to packet when offset exceeds MAX_PACKET_OFF */
+	if (off > MAX_PACKET_OFF - sz)
+		return NULL;
+
+	data = ctx->data + off;
+	barrier_var(data);
+	if (data + sz >= ctx->data_end)
+		return NULL;
 
-	opcode = *ctx->ptr++;
+	ctx->off += sz;
+	return data;
+}
 
-	if (opcode == TCPOPT_EOL)
+static int tcp_parse_option(__u32 index, struct tcp_syncookie *ctx)
+{
+	__u8 *opcode, *opsize, *wscale;
+	__u32 *tsval, *tsecr;
+	__u16 *mss;
+	__u32 off;
+
+	off = ctx->off;
+	opcode = next(ctx, 1);
+	if (!opcode)
 		goto stop;
 
-	if (opcode == TCPOPT_NOP)
+	if (*opcode == TCPOPT_EOL)
+		goto stop;
+
+	if (*opcode == TCPOPT_NOP)
 		goto next;
 
-	if (ctx->ptr + 1 > ctx->data_end)
+	opsize = next(ctx, 1);
+	if (!opsize)
 		goto stop;
 
-	opsize = *ctx->ptr++;
-
-	if (opsize < 2)
+	if (*opsize < 2)
 		goto stop;
 
-	switch (opcode) {
+	switch (*opcode) {
 	case TCPOPT_MSS:
-		if (opsize == TCPOLEN_MSS && ctx->tcp->syn &&
-		    ctx->ptr + (TCPOLEN_MSS - 2) < ctx->data_end)
-			ctx->attrs.mss = get_unaligned_be16(ctx->ptr);
+		mss = next(ctx, 2);
+		if (*opsize == TCPOLEN_MSS && ctx->tcp->syn && mss)
+			ctx->attrs.mss = get_unaligned_be16(mss);
 		break;
 	case TCPOPT_WINDOW:
-		if (opsize == TCPOLEN_WINDOW && ctx->tcp->syn &&
-		    ctx->ptr + (TCPOLEN_WINDOW - 2) < ctx->data_end) {
+		wscale = next(ctx, 1);
+		if (*opsize == TCPOLEN_WINDOW && ctx->tcp->syn && wscale) {
 			ctx->attrs.wscale_ok = 1;
-			ctx->attrs.snd_wscale = *ctx->ptr;
+			ctx->attrs.snd_wscale = *wscale;
 		}
 		break;
 	case TCPOPT_TIMESTAMP:
-		if (opsize == TCPOLEN_TIMESTAMP &&
-		    ctx->ptr + (TCPOLEN_TIMESTAMP - 2) < ctx->data_end) {
-			ctx->attrs.rcv_tsval = get_unaligned_be32(ctx->ptr);
-			ctx->attrs.rcv_tsecr = get_unaligned_be32(ctx->ptr + 4);
+		tsval = next(ctx, 4);
+		tsecr = next(ctx, 4);
+		if (*opsize == TCPOLEN_TIMESTAMP && tsval && tsecr) {
+			ctx->attrs.rcv_tsval = get_unaligned_be32(tsval);
+			ctx->attrs.rcv_tsecr = get_unaligned_be32(tsecr);
 
 			if (ctx->tcp->syn && ctx->attrs.rcv_tsecr)
 				ctx->attrs.tstamp_ok = 0;
@@ -244,13 +268,12 @@ static int tcp_parse_option(__u32 index, struct tcp_syncookie *ctx)
 		}
 		break;
 	case TCPOPT_SACK_PERM:
-		if (opsize == TCPOLEN_SACK_PERM && ctx->tcp->syn &&
-		    ctx->ptr + (TCPOLEN_SACK_PERM - 2) < ctx->data_end)
+		if (*opsize == TCPOLEN_SACK_PERM && ctx->tcp->syn)
 			ctx->attrs.sack_ok = 1;
 		break;
 	}
 
-	ctx->ptr += opsize - 2;
+	ctx->off = off + *opsize;
 next:
 	return 0;
 stop:
@@ -259,7 +282,7 @@ static int tcp_parse_option(__u32 index, struct tcp_syncookie *ctx)
 
 static void tcp_parse_options(struct tcp_syncookie *ctx)
 {
-	ctx->ptr = (char *)(ctx->tcp + 1);
+	ctx->off = (__u8 *)(ctx->tcp + 1) - (__u8 *)ctx->data,
 
 	bpf_loop(40, tcp_parse_option, ctx, 0);
 }
Signature · 1 line
2.43.0
reviewer Acked-by on v1 1/3
#2 lore raw

Re: [PATCH bpf-next 1/3] selftests/bpf: update tcp_custom_syncookie to use scalar packet offset

Quoted 22 lines · Eduard Zingerman

The next commit in a series fixes bug in bpf_loop() handling. That change makes tcp_custom_syncookie test too complex to verify.

This commit updates tcp_custom_syncookie.c:tcp_parse_option() to use explicit packet offset (ctx->off) for packet access instead of ever moving pointer (ctx->ptr), this reduces verification complexity:
- the tcp_parse_option() is passed as a callback to bpf_loop();
- suppose a checkpoint is created each time at function entry;
- the ctx->ptr is tracked by verifier as PTR_TO_PACKET;
- the ctx->ptr is incremented in tcp_parse_option(),
thus umax_value field tracked for it is incremented as well;
- on each next iteration of tcp_parse_option()
checkpoint from a previous iteration can't be reused for state pruning, because PTR_TO_PACKET registers are considered equivalent only if old->umax_value >= cur->umax_value;
- on the other hand, the ctx->off is a SCALAR,
subject to widen_imprecise_scalars();
- it's exact bounds are eventually forgotten and it is tracked as unknown scalar at entry to tcp_parse_option();
- hence checkpoints created at the start of the function eventually converge.

Quoted 5 lines

The change is similar to one applied in [0] to xdp_synproxy_kern.c.

[0] commit 977bc146d4eb ("selftests/bpf: track tcp payload offset as scalar in xdp_synproxy")

author maintainer
#3 lore raw

PATCH 2/3 bpf: check bpf_func_state->callback_depth when pruning states

In reply to Eduard Zingerman

When comparing current and cached states verifier should consider bpf_func_state->callback_depth. Current state cannot be pruned against cached state, when current states has more iterations left compared to cached state. Current state has more iterations left when it's callback_depth is smaller.

Below is an example illustrating this bug, minimized from mailing list discussion [0].
The example is not a safe program: if loop_cb point (1) is followed by loop_cb point (2), then division by zero is possible at point (4).

    struct ctx {
    	__u64 a;
    	__u64 b;
    	__u64 c;
    };

    static void loop_cb(int i, struct ctx *ctx)
    {
    	/* assume that generated code is "fallthrough-first":
    	 * if ... == 1 goto
    	 * if ... == 2 goto
    	 * <default>
    	 */
    	switch (bpf_get_prandom_u32()) {
    	case 1:  /* 1 */ ctx->a = 42; return 0; break;
    	case 2:  /* 2 */ ctx->b = 42; return 0; break;
    	default: /* 3 */ ctx->c = 42; return 0; break;
    	}
    }

    SEC("tc")
    __failure
    __flag(BPF_F_TEST_STATE_FREQ)
    int test(struct __sk_buff *skb)
    {
    	struct ctx ctx = { 7, 7, 7 };

    	bpf_loop(2, loop_cb, &ctx, 0);              /* 0 */
    	/* assume generated checks are in-order: .a first */
    	if (ctx.a == 42 && ctx.b == 42 && ctx.c == 7)
    		asm volatile("r0 /= 0;":::"r0");    /* 4 */
    	return 0;
    }

Prior to this commit verifier built the following checkpoint tree for this example (notation: (code point #) {<ctx->a>,<ctx->b>,<ctx->c>}):

- (0) {7P,7,7}
- (3) {7P,7,7}
- (0) {7P,7,42} (checkpoint #1):
- (3) {7P,7,42}
- (0) {7P,7,42} -> to end
- (2) {7P,7,42}
- (0) {7P,42,42} -> to end
- (1) {7P,7,42} (checkpoint #2)
- (0) {42P,7P,42} -> to end
- (2) {7P,7,7}
- (0) {7P,42,7} safe (checkpoint #1)
- (1) {7,7,7} safe (checkpoint #2)

Here checkpoint #2 has callback_depth of 1, meaning that it would never reach state {42,42,7}.
While the last branch of the tree has callback_depth of 0, and thus could yet explore the state {42,42,7} if not pruned prematurely. This commit makes disallows such premature pruning.

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

---
 kernel/bpf/verifier.c | 3 +++
 1 file changed, 3 insertions(+)
kernel/bpf/verifier.c +3 -0
index ddaf09db1175..df99fcdbaa05 100644
@@ -16715,6 +16715,9 @@ static bool func_states_equal(struct bpf_verifier_env *env, struct bpf_func_stat
 {
 	int i;
 
+	if (old->callback_depth > cur->callback_depth)
+		return false;
+
 	for (i = 0; i < MAX_BPF_REG; i++)
 		if (!regsafe(env, &old->regs[i], &cur->regs[i],
 			     &env->idmap_scratch, exact))
Signature · 1 line
2.43.0
reviewer
#4 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

Quoted 36 lines · Eduard Zingerman

When comparing current and cached states verifier should consider bpf_func_state->callback_depth. Current state cannot be pruned against cached state, when current states has more iterations left compared to cached state. Current state has more iterations left when it's callback_depth is smaller.

Below is an example illustrating this bug, minimized from mailing list discussion [0].
The example is not a safe program: if loop_cb point (1) is followed by loop_cb point (2), then division by zero is possible at point (4).

     struct ctx {
     	__u64 a;
     	__u64 b;
     	__u64 c;
     };

     static void loop_cb(int i, struct ctx *ctx)
     {
     	/* assume that generated code is "fallthrough-first":
     	 * if ... == 1 goto
     	 * if ... == 2 goto
     	 * <default>
     	 */
     	switch (bpf_get_prandom_u32()) {
     	case 1:  /* 1 */ ctx->a = 42; return 0; break;
     	case 2:  /* 2 */ ctx->b = 42; return 0; break;
     	default: /* 3 */ ctx->c = 42; return 0; break;
     	}
     }

     SEC("tc")
     __failure
     __flag(BPF_F_TEST_STATE_FREQ)
     int test(struct __sk_buff *skb)
     {
Quoted 9 lines
     	struct ctx ctx = { 7, 7, 7 };

     	bpf_loop(2, loop_cb, &ctx, 0);              /* 0 */
     	/* assume generated checks are in-order: .a first */
     	if (ctx.a == 42 && ctx.b == 42 && ctx.c == 7)
     		asm volatile("r0 /= 0;":::"r0");    /* 4 */
     	return 0;
     }

The change LGTM. But the below description seems not very clear to me.

Quoted 4 lines

Prior to this commit verifier built the following checkpoint tree for this example (notation: (code point #) {<ctx->a>,<ctx->b>,<ctx->c>}):

- (0) {7P,7,7}

Why we have '7P' here?

Quoted 1 line

- (3) {7P,7,7}

So here when (3) is hit, we have callback_depth  = 1, right?

Quoted 1 line

- (0) {7P,7,42} (checkpoint #1):

So for below (3)/(2)/(1) we have callback_depth = 2, right?

Quoted 7 lines

- (3) {7P,7,42}
- (0) {7P,7,42} -> to end
- (2) {7P,7,42}
- (0) {7P,42,42} -> to end
- (1) {7P,7,42} (checkpoint #2)
- (0) {42P,7P,42} -> to end
- (2) {7P,7,7}

So now we back to callback_depth = 1.

Quoted 5 lines

- (0) {7P,42,7} safe (checkpoint #1)
- (1) {7,7,7} safe (checkpoint #2)

Here checkpoint #2 has callback_depth of 1, meaning that it would never reach state {42,42,7}.

It would be good to specify which 'checkpoint #2' has callback_depth of 1.

Quoted 2 lines

While the last branch of the tree has callback_depth of 0, and thus could yet explore the state {42,42,7} if not pruned prematurely.

which 'last branch'?

Quoted 1 line

This commit makes disallows such premature pruning.

It would be good if the commit message mentions what will change for the above digram if this commit is applied, so people can understand why this commit helps.

Quoted 23 lines

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

---
  kernel/bpf/verifier.c | 3 +++
  1 file changed, 3 insertions(+)
kernel/bpf/verifier.c +3 -0
index ddaf09db1175..df99fcdbaa05 100644
@@ -16715,6 +16715,9 @@ static bool func_states_equal(struct bpf_verifier_env *env, struct bpf_func_stat
  {
  	int i;
 
+	if (old->callback_depth > cur->callback_depth)
+		return false;
+
  	for (i = 0; i < MAX_BPF_REG; i++)
  		if (!regsafe(env, &old->regs[i], &cur->regs[i],
  			     &env->idmap_scratch, exact))
author maintainer
#5 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

On Mon, 2024-02-12 at 17:20 -0800, Yonghong Song wrote:
[...]

Hi Yonghong,

Thank you for the feedback, I put updated description at the end of the email, below are the answers to your questions.

Quoted 3 lines
Quoted 1 line

- (0) {7P,7,7}

Why we have '7P' here?

Precision is propagated because of the check in the "-> to end" branch, made it more clear in the updated description.

Quoted 3 lines
Quoted 1 line

- (3) {7P,7,7}

So here when (3) is hit, we have callback_depth  = 1, right?

Yes, made callback depth explicit.

Quoted 3 lines
Quoted 1 line

- (0) {7P,7,42} (checkpoint #1):

So for below (3)/(2)/(1) we have callback_depth = 2, right?

Yes.

Quoted 9 lines
Quoted 7 lines

- (3) {7P,7,42}
- (0) {7P,7,42} -> to end
- (2) {7P,7,42}
- (0) {7P,42,42} -> to end
- (1) {7P,7,42} (checkpoint #2)
- (0) {42P,7P,42} -> to end
- (2) {7P,7,7}

So now we back to callback_depth = 1.

Yes.

[...]

Quoted 4 lines
Quoted 2 lines

While the last branch of the tree has callback_depth of 0, and thus could yet explore the state {42,42,7} if not pruned prematurely.

which 'last branch'?

Gave it a name.

Quoted 3 lines

It would be good if the commit message mentions what will change for the above digram if this commit is applied, so people can understand why this commit helps.

Added.

--- 8< ---------------------------------

    struct ctx {
    	__u64 a;
    	__u64 b;
    	__u64 c;
    };

    static void loop_cb(int i, struct ctx *ctx)
    {
    	/* assume that generated code is "fallthrough-first":
    	 * if ... == 1 goto
    	 * if ... == 2 goto
    	 * <default>
    	 */
    	switch (bpf_get_prandom_u32()) {
    	case 1:  /* 1 */ ctx->a = 42; return 0; break;
    	case 2:  /* 2 */ ctx->b = 42; return 0; break;
    	default: /* 3 */ ctx->c = 42; return 0; break;
    	}
    }

    SEC("tc")
    __failure
    __flag(BPF_F_TEST_STATE_FREQ)
    int test(struct __sk_buff *skb)
    {
    	struct ctx ctx = { 7, 7, 7 };

    	/* 0 */ bpf_loop(2, loop_cb, &ctx, 0);
    	if (/* 4 */ ctx.a == 42 && ctx.b == 42 && ctx.c == 7)
    		/* 5 */ asm volatile("r0 /= 0;":::"r0");
    	/* 6 */ return 0;
    }

Prior to this commit verifier built the following checkpoint tree for this example:

.------------------------------------- checkpoint / state name

 |    .-------------------------------- code point number
 |    |   .---------------------------- stack state {ctx.a,ctx.b,ctx.c}
 |    |   |        .------------------- callback depth in frame #0
 v    v   v        v

- (0) {7P,7P,7},depth=0
- (3) {7P,7,7},depth=1
(a) - (0) {7P,7,42},depth=1
- (3) {7P,7,42},depth=2
- (0) {7P,7,42},depth=2 loop terminates because of depth limit
- (4) {7P,7,42},depth=0 predicted false, ctx.a marked precise
- (6) exit
- (2) {7P,7,42},depth=2
- (0) {7P,42,42},depth=2 loop terminates because of depth limit
- (4) {7P,42,42},depth=0 predicted false, ctx.a marked precise
- (6) exit
(b) - (1) {7P,7P,42},depth=2
- (0) {42P,7P,42},depth=2 loop terminates because of depth limit
- (4) {42P,7P,42},depth=0 predicted false, ctx.{a,b} marked precise
- (6) exit
- (2) {7P,7,7},depth=1
- (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
(c) - (1) {7,7,7},depth=1 considered safe, pruned using checkpoint (b)

Here checkpoint (b) has callback_depth of 2, meaning that it would never reach state {42,42,7}.
While checkpoint (c) has callback_depth of 1, and thus could yet explore the state {42,42,7} if not pruned prematurely. This commit makes forbids such premature pruning, allowing verifier to explore states sub-tree starting at (c):

(c) - (1) {7,7,7P},depth=1
- (0) {42P,7,7P},depth=1
...
- (2) {42,7,7},depth=2
- (0) {42,42,7},depth=2 loop terminates because of depth limit
- (4) {42,42,7},depth=0 predicted true, ctx.{a,b,c} marked precise
- (5) division by zero

--------------------------------- >8 ---

Wdyt?

author maintainer
#6 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

Updated diagram with a few fixes, line numbers would be removed in the final version.

--- 8< ---------------------------------

.------------------------------------- Checkpoint / State name

 |    .-------------------------------- Code point number
 |    |   .---------------------------- Stack state {ctx.a,ctx.b,ctx.c}
 |    |   |        .------------------- Callback depth in frame #0
 v    v   v        v

1 - (0) {7P,7P,7},depth=0
2 - (3) {7P,7P,7},depth=1
3 - (0) {7P,7P,42},depth=1
(a) - (3) {7P,7,42},depth=2
4 - (0) {7P,7,42},depth=2 loop terminates because of depth limit
5 - (4) {7P,7,42},depth=0 predicted false, ctx.a marked precise
6 - (6) exit
7 - (2) {7P,7,42},depth=2
8 - (0) {7P,42,42},depth=2 loop terminates because of depth limit
9 - (4) {7P,42,42},depth=0 predicted false, ctx.a marked precise
10 - (6) exit
(b) - (1) {7P,7P,42},depth=2
11 - (0) {42P,7P,42},depth=2 loop terminates because of depth limit
12 - (4) {42P,7P,42},depth=0 predicted false, ctx.{a,b} marked precise
13 - (6) exit
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)

Here checkpoint (b) has callback_depth of 2, meaning that it would never reach state {42,42,7}.
While checkpoint (c) has callback_depth of 1, and thus could yet explore the state {42,42,7} if not pruned prematurely. This commit makes forbids such premature pruning, allowing verifier to explore states sub-tree starting at (c):

(c) - (1) {7,7,7P},depth=1
16 - (0) {42P,7,7P},depth=1

         ...

17 - (2) {42,7,7},depth=2
18 - (0) {42,42,7},depth=2 loop terminates because of depth limit
19 - (4) {42,42,7},depth=0 predicted true, ctx.{a,b,c} marked precise
20 - (5) division by zero

--------------------------------- >8 ---

reviewer
#7 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

Quoted 20 lines · Eduard Zingerman

Updated diagram with a few fixes, line numbers would be removed in the final version.

--- 8< ---------------------------------

.------------------------------------- Checkpoint / State name

  |    .-------------------------------- Code point number
  |    |   .---------------------------- Stack state {ctx.a,ctx.b,ctx.c}
  |    |   |        .------------------- Callback depth in frame #0
  v    v   v        v

1 - (0) {7P,7P,7},depth=0
2 - (3) {7P,7P,7},depth=1
3 - (0) {7P,7P,42},depth=1
(a) - (3) {7P,7,42},depth=2
4 - (0) {7P,7,42},depth=2 loop terminates because of depth limit
5 - (4) {7P,7,42},depth=0 predicted false, ctx.a marked precise
6 - (6) exit
7 - (2) {7P,7,42},depth=2
8 - (0) {7P,42,42},depth=2 loop terminates because of depth limit
9 - (4) {7P,42,42},depth=0 predicted false, ctx.a marked precise

Quoted 8 lines

10 - (6) exit
(b) - (1) {7P,7P,42},depth=2
11 - (0) {42P,7P,42},depth=2 loop terminates because of depth limit
12 - (4) {42P,7P,42},depth=0 predicted false, ctx.{a,b} marked precise
13 - (6) exit
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)

For the above line
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)
I would change to
(c) - (1) {7P,7P,7},depth=1
- (0) {42P, 7P, 7},depth = 1 considered safe, pruned using checkpoint (11)

For
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
I suspect for line 15, the pruning uses checking point at line (8).

Other than the above, the diagram LGTM.

Quoted 17 lines

Here checkpoint (b) has callback_depth of 2, meaning that it would never reach state {42,42,7}.
While checkpoint (c) has callback_depth of 1, and thus could yet explore the state {42,42,7} if not pruned prematurely. This commit makes forbids such premature pruning, allowing verifier to explore states sub-tree starting at (c):

(c) - (1) {7,7,7P},depth=1
16 - (0) {42P,7,7P},depth=1

          ...

17 - (2) {42,7,7},depth=2
18 - (0) {42,42,7},depth=2 loop terminates because of depth limit
19 - (4) {42,42,7},depth=0 predicted true, ctx.{a,b,c} marked precise
20 - (5) division by zero

--------------------------------- >8 ---

author maintainer
#8 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

Quoted 24 lines · Yonghong Song
Quoted 15 lines

.------------------------------------- Checkpoint / State name

  |    .-------------------------------- Code point number
  |    |   .---------------------------- Stack state {ctx.a,ctx.b,ctx.c}
  |    |   |        .------------------- Callback depth in frame #0
  v    v   v        v

1 - (0) {7P,7P,7},depth=0
2 - (3) {7P,7P,7},depth=1
3 - (0) {7P,7P,42},depth=1
(a) - (3) {7P,7,42},depth=2
4 - (0) {7P,7,42},depth=2 loop terminates because of depth limit
5 - (4) {7P,7,42},depth=0 predicted false, ctx.a marked precise
6 - (6) exit
7 - (2) {7P,7,42},depth=2
8 - (0) {7P,42,42},depth=2 loop terminates because of depth limit
9 - (4) {7P,42,42},depth=0 predicted false, ctx.a marked precise

Quoted 8 lines

10 - (6) exit
(b) - (1) {7P,7P,42},depth=2
11 - (0) {42P,7P,42},depth=2 loop terminates because of depth limit
12 - (4) {42P,7P,42},depth=0 predicted false, ctx.{a,b} marked precise
13 - (6) exit
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)

Quoted 5 lines

For the above line
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)
I would change to
(c) - (1) {7P,7P,7},depth=1
- (0) {42P, 7P, 7},depth = 1 considered safe, pruned using checkpoint (11)

At that point:
- there is a checkpoint at (1) with state {7P,7P,42}
- verifier is at (1) in state {7,7,7}
Thus, verifier won't proceed to (0) because {7,7,7} is states_equal to {7P,7P,42}.

Quoted 4 lines

For
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
I suspect for line 15, the pruning uses checking point at line (8).

Right, because checkpoints for a particular insn form a stack. My bad.

Quoted 1 line

Other than the above, the diagram LGTM.

Thank you for the feedback, I'll post v2 shortly.

reviewer
#9 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

Quoted 30 lines · Eduard Zingerman
Quoted 23 lines · Yonghong Song
Quoted 15 lines
   .------------------------------------- Checkpoint / State name
   |    .-------------------------------- Code point number
   |    |   .---------------------------- Stack state {ctx.a,ctx.b,ctx.c}
   |    |   |        .------------------- Callback depth in frame #0
   v    v   v        v

1 - (0) {7P,7P,7},depth=0
2 - (3) {7P,7P,7},depth=1
3 - (0) {7P,7P,42},depth=1
(a) - (3) {7P,7,42},depth=2
4 - (0) {7P,7,42},depth=2 loop terminates because of depth limit
5 - (4) {7P,7,42},depth=0 predicted false, ctx.a marked precise
6 - (6) exit
7 - (2) {7P,7,42},depth=2
8 - (0) {7P,42,42},depth=2 loop terminates because of depth limit
9 - (4) {7P,42,42},depth=0 predicted false, ctx.a marked precise

Quoted 8 lines

10 - (6) exit
(b) - (1) {7P,7P,42},depth=2
11 - (0) {42P,7P,42},depth=2 loop terminates because of depth limit
12 - (4) {42P,7P,42},depth=0 predicted false, ctx.{a,b} marked precise
13 - (6) exit
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)

Quoted 5 lines

For the above line
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)
I would change to
(c) - (1) {7P,7P,7},depth=1
- (0) {42P, 7P, 7},depth = 1 considered safe, pruned using checkpoint (11)

Quoted 4 lines

At that point:
- there is a checkpoint at (1) with state {7P,7P,42}
- verifier is at (1) in state {7,7,7}
Thus, verifier won't proceed to (0) because {7,7,7} is states_equal to {7P,7P,42}.

Okay, I think the above example has BPF_F_TEST_STATE_FREQ set as in Patch 3. It will be great if you can explicitly mention this (BPF_F_TEST_STATE_FREQ) in the commit message. With this flag,
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)
is correct.

But then for
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
The state
14 - (2) {7P,7,7},depth=1
will have state equal to
7 - (2) {7P,7,42},depth=2
right?

Quoted 9 lines
Quoted 5 lines

For
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
I suspect for line 15, the pruning uses checking point at line (8).

Right, because checkpoints for a particular insn form a stack. My bad.

Quoted 1 line

Other than the above, the diagram LGTM.

Thank you for the feedback, I'll post v2 shortly.

author maintainer
#10 lore raw

Re: [PATCH bpf-next 2/3] bpf: check bpf_func_state->callback_depth when pruning states

Quoted 31 lines · Yonghong Song
Quoted 30 lines · Eduard Zingerman
Quoted 28 lines · Yonghong Song
Quoted 15 lines
   .------------------------------------- Checkpoint / State name
   |    .-------------------------------- Code point number
   |    |   .---------------------------- Stack state {ctx.a,ctx.b,ctx.c}
   |    |   |        .------------------- Callback depth in frame #0
   v    v   v        v

1 - (0) {7P,7P,7},depth=0
2 - (3) {7P,7P,7},depth=1
3 - (0) {7P,7P,42},depth=1
(a) - (3) {7P,7,42},depth=2
4 - (0) {7P,7,42},depth=2 loop terminates because of depth limit
5 - (4) {7P,7,42},depth=0 predicted false, ctx.a marked precise
6 - (6) exit
7 - (2) {7P,7,42},depth=2
8 - (0) {7P,42,42},depth=2 loop terminates because of depth limit
9 - (4) {7P,42,42},depth=0 predicted false, ctx.a marked precise

Quoted 8 lines

10 - (6) exit
(b) - (1) {7P,7P,42},depth=2
11 - (0) {42P,7P,42},depth=2 loop terminates because of depth limit
12 - (4) {42P,7P,42},depth=0 predicted false, ctx.{a,b} marked precise
13 - (6) exit
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)

For the above line
(c) - (1) {7P,7P,7},depth=1 considered safe, pruned using checkpoint (b)
I would change to
(c) - (1) {7P,7P,7},depth=1
- (0) {42P, 7P, 7},depth = 1 considered safe, pruned using checkpoint (11)

Quoted 7 lines
Quoted 4 lines

At that point:
- there is a checkpoint at (1) with state {7P,7P,42}
- verifier is at (1) in state {7,7,7}
Thus, verifier won't proceed to (0) because {7,7,7} is states_equal to {7P,7P,42}.

Okay, I think the above example has BPF_F_TEST_STATE_FREQ set as in Patch 3. It will be great if you can explicitly mention this (BPF_F_TEST_STATE_FREQ) in the commit message.

Will do.

[...]

Quoted 8 lines

But then for
14 - (2) {7P,7,7},depth=1
15 - (0) {7P,42,7},depth=1 considered safe, pruned using checkpoint (a)
The state
14 - (2) {7P,7,7},depth=1
will have state equal to

    7        - (2) {7P,7,42},depth=2

right?

I think you are correct.

author maintainer
#11 lore raw

PATCH 3/3 selftests/bpf: test case for callback_depth states pruning logic

In reply to Eduard Zingerman

The test case was minimized from mailing list discussion [0]. It is equivalent to the following C program:

    struct iter_limit_bug_ctx { __u64 a; __u64 b; __u64 c; };

    static __naked void iter_limit_bug_cb(void)
    {
    	switch (bpf_get_prandom_u32()) {
    	case 1:  ctx->a = 42; break;
    	case 2:  ctx->b = 42; break;
    	default: ctx->c = 42; break;
    	}
    }

    int iter_limit_bug(struct __sk_buff *skb)
    {
    	struct iter_limit_bug_ctx ctx = { 7, 7, 7 };

    	bpf_loop(2, iter_limit_bug_cb, &ctx, 0);
    	if (ctx.a == 42 && ctx.b == 42 && ctx.c == 7)
    	  asm volatile("r1 /= 0;":::"r1");
    	return 0;
    }

The main idea is that each loop iteration changes one of the state variables in a non-deterministic manner. Hence it is premature to prune the states that have two iterations left comparing them to states with one iteration left.
E.g. {{7,7,7}, callback_depth=0} can reach state {42,42,7}, while {{7,7,7}, callback_depth=1} can't.

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

---
 .../bpf/progs/verifier_iterating_callbacks.c  | 70 +++++++++++++++++++
 1 file changed, 70 insertions(+)
tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c +70 -0
index 5905e036e0ea..a955a6358206 100644
@@ -239,4 +239,74 @@ int bpf_loop_iter_limit_nested(void *unused)
 	return 1000 * a + b + c;
 }
 
+struct iter_limit_bug_ctx {
+	__u64 a;
+	__u64 b;
+	__u64 c;
+};
+
+static __naked void iter_limit_bug_cb(void)
+{
+	/* This is the same as C code below, but written
+	 * in assembly to control which branches are fall-through.
+	 *
+	 *   switch (bpf_get_prandom_u32()) {
+	 *   case 1:  ctx->a = 42; break;
+	 *   case 2:  ctx->b = 42; break;
+	 *   default: ctx->c = 42; break;
+	 *   }
+	 */
+	asm volatile (
+	"r9 = r2;"
+	"call %[bpf_get_prandom_u32];"
+	"r1 = r0;"
+	"r2 = 42;"
+	"r0 = 0;"
+	"if r1 == 0x1 goto 1f;"
+	"if r1 == 0x2 goto 2f;"
+	"*(u64 *)(r9 + 16) = r2;"
+	"exit;"
+	"1: *(u64 *)(r9 + 0) = r2;"
+	"exit;"
+	"2: *(u64 *)(r9 + 8) = r2;"
+	"exit;"
+	:
+	: __imm(bpf_get_prandom_u32)
+	: __clobber_all
+	);
+}
+
+SEC("tc")
+__failure
+__flag(BPF_F_TEST_STATE_FREQ)
+int iter_limit_bug(struct __sk_buff *skb)
+{
+	struct iter_limit_bug_ctx ctx = { 7, 7, 7 };
+
+	bpf_loop(2, iter_limit_bug_cb, &ctx, 0);
+
+	/* This is the same as C code below,
+	 * written in assembly to guarantee checks order.
+	 *
+	 *   if (ctx.a == 42 && ctx.b == 42 && ctx.c == 7)
+	 *     asm volatile("r1 /= 0;":::"r1");
+	 */
+	asm volatile (
+	"r1 = *(u64 *)%[ctx_a];"
+	"if r1 != 42 goto 1f;"
+	"r1 = *(u64 *)%[ctx_b];"
+	"if r1 != 42 goto 1f;"
+	"r1 = *(u64 *)%[ctx_c];"
+	"if r1 != 7 goto 1f;"
+	"r1 /= 0;"
+	"1:"
+	:
+	: [ctx_a]"m"(ctx.a),
+	  [ctx_b]"m"(ctx.b),
+	  [ctx_c]"m"(ctx.c)
+	: "r1"
+	);
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";
Signature · 1 line
2.43.0