BPF List
 help / color / mirror / Atom feed
* [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero
@ 2026-05-29  8:13 Eduard Zingerman
  2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
                   ` (2 more replies)
  0 siblings, 3 replies; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-29  8:13 UTC (permalink / raw)
  To: bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei,
	Eduard Zingerman

YiFei Zhu reported [1] the verifier regression after switch to cnum
based scalars representation. When the following sequence of
instructions is processed:

    1: ... rX setup with [negative, positive] bounds ...
    2: if rX == 0 goto ...
    3: if rX > C  goto ...
    4: ... code relying on rX being in range [1, C] ...

The cnum-based implementation only infers that rX range is [0, C]
at instruction (4). The pre-cnum signed/unsigned ranges based
representation could always deduct from 'rX != 0' that
umin bound is 1.

This patch introduces a workaround forking the verifier state when a
register with sign-crossing range is compared to zero.

[1] https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/

---
Eduard Zingerman (2):
      bpf: fork state when comparing sign crossing ranges with zero
      selftests/bpf: test fork on zero comparison with wrapping ranges

 kernel/bpf/verifier.c                              | 71 ++++++++++++++++++++++
 .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 +++++++++++++++++++++
 2 files changed, 139 insertions(+)
---
base-commit: e42e53ae23b7d41df22ccd7788192bf578f24da2
change-id: 20260529-cnum-split-at-zero-3c03db9234d3

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

* [PATCH bpf 1/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:13 [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
@ 2026-05-29  8:13 ` Eduard Zingerman
  2026-05-29  8:58   ` bot+bpf-ci
                     ` (2 more replies)
  2026-05-29  8:13 ` [PATCH bpf 2/2] selftests/bpf: test fork on zero comparison with wrapping ranges Eduard Zingerman
  2026-05-29 22:44 ` [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
  2 siblings, 3 replies; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-29  8:13 UTC (permalink / raw)
  To: bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei,
	Eduard Zingerman

YiFei Zhu reported the verifier regression after switch to cnum based
scalar representation. When the following sequence of instructions is
processed:

    1: ... rX setup with [negative, positive] bounds ...
    2: if rX == 0 goto ...
    3: if rX > C  goto ...
    4: ... code relying on rX being in range [1, C] ...

The comparison at (2) is processed by cnum{32,64}_intersect(), which
can't punch a hole in [negative, positive] range and over approximates
by leaving rX range unchanged, leading to a deduction of range [0, C]
at (4). The pre-cnum signed/unsigned ranges based representation could
always deduct from 'rX != 0' that umin bound is 1.

This patch introduces a workaround: when'if rX ==/!= 0 goto ...' is
processed and rX has [negative, positive] range, fork the verifier
state and use current and forked states to split rX representation
in negative and non-negative parts.

The fork is placed before the branch is evaluated, so that the forked
state re-enters check_cond_jmp_op() and gets mark_ptr_or_null_regs(),
sync_linked_regs(), precision tracking processing w/o additional code
changes.

Reported-by: YiFei Zhu <zhuyifei@google.com>
Closes: https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
---
 kernel/bpf/verifier.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 71 insertions(+)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c8d980fdd709..7998e8da5e55 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -15965,6 +15965,73 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
 	}
 }
 
+/*
+ * This is a workaround for a specific pattern:
+ *
+ *   1: ... rX setup with [negative, positive] bounds ...
+ *   2: if rX == 0 goto ...
+ *   3: if rX > C  goto ...
+ *   4: ... code relying on rX being in range [1, C] ...
+ *
+ * The comparison at (2) is processed by cnum{32,64}_intersect(),
+ * which can't punch a hole in [negative, positive] range and
+ * over approximates by leaving rX range unchanged,
+ * leading to a deduction of range [0, C] at (4).
+ *
+ * The workaround is to fork verifier state when:
+ * - 'if rX ==/!= 0 goto ...' is processed
+ * - rX has [negative, positive] range
+ * The current and forked states are used to split rX representation
+ * in negative and non-negative parts.
+ */
+static int maybe_fork_cmp_with_zero(struct bpf_verifier_env *env,
+				    struct bpf_insn *insn,
+				    struct bpf_reg_state *src_reg,
+				    struct bpf_reg_state *dst_reg)
+{
+	struct bpf_verifier_state *fork, *cur = env->cur_state;
+	struct bpf_reg_state *fork_dst_reg;
+	bool is_jmp32 = BPF_CLASS(insn->code) == BPF_JMP32;
+	u8 opcode = BPF_OP(insn->code);
+	bool swapped = false;
+	u32 fork_dst_regno;
+
+	if (opcode != BPF_JEQ && opcode != BPF_JNE)
+		return 0;
+
+	if (!is_reg_const(src_reg, is_jmp32)) {
+		swap(src_reg, dst_reg);
+		swapped = true;
+	}
+
+	if (!is_reg_const(src_reg, is_jmp32) || reg_const_value(src_reg, is_jmp32) != 0)
+		return 0;
+
+	bool cross_sign32 = is_jmp32 &&
+		dst_reg->r32.size < S32_MAX &&
+		cnum32_smin(dst_reg->r32) < 0 && cnum32_smax(dst_reg->r32) > 0;
+	bool cross_sign64 = !is_jmp32 &&
+		dst_reg->r64.size < S64_MAX &&
+		cnum64_smin(dst_reg->r64) < 0 && cnum64_smax(dst_reg->r64) > 0;
+	if (!cross_sign32 && !cross_sign64)
+		return 0;
+
+	fork = push_stack(env, env->insn_idx, env->insn_idx, cur->speculative);
+	if (!fork)
+		return -ENOMEM;
+
+	fork_dst_regno = swapped ? insn->src_reg : insn->dst_reg;
+	fork_dst_reg = &fork->frame[fork->curframe]->regs[fork_dst_regno];
+	if (is_jmp32) {
+		cnum32_intersect_with_srange(&dst_reg->r32, S32_MIN, -1);
+		cnum32_intersect_with_srange(&fork_dst_reg->r32, 0, S32_MAX);
+	} else {
+		cnum64_intersect_with_srange(&dst_reg->r64, S64_MIN, -1);
+		cnum64_intersect_with_srange(&fork_dst_reg->r64, 0, S64_MAX);
+	}
+	return 0;
+}
+
 static int check_cond_jmp_op(struct bpf_verifier_env *env,
 			     struct bpf_insn *insn, int *insn_idx)
 {
@@ -16038,6 +16105,10 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
 			insn_flags |= INSN_F_DST_REG_STACK;
 	}
 
+	err = maybe_fork_cmp_with_zero(env, insn, src_reg, dst_reg);
+	if (err)
+		return err;
+
 	if (insn_flags) {
 		err = bpf_push_jmp_history(env, this_branch, insn_flags, 0, 0, 0);
 		if (err)

-- 
2.53.0

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

* [PATCH bpf 2/2] selftests/bpf: test fork on zero comparison with wrapping ranges
  2026-05-29  8:13 [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
  2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
@ 2026-05-29  8:13 ` Eduard Zingerman
  2026-05-29 17:03   ` Emil Tsalapatis
  2026-05-29 22:44 ` [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
  2 siblings, 1 reply; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-29  8:13 UTC (permalink / raw)
  To: bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei,
	Eduard Zingerman

Add tests verifying that the verifier correctly narrows a
sign-boundary-crossing range after comparing with zero.
- fork_on_zero_cmp_k - BPF_K conditional instruction;
- fork_on_zero_cmp_x -
  BPF_X conditional instruction with src being zero.
- fork_on_zero_cmp_x_swapped -
  BPF_X conditional instruction with dst being zero.

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
---
 .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 ++++++++++++++++++++++
 1 file changed, 68 insertions(+)

diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/testing/selftests/bpf/progs/verifier_bounds.c
index bc038ac2df98..f97b0e109326 100644
--- a/tools/testing/selftests/bpf/progs/verifier_bounds.c
+++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c
@@ -2294,4 +2294,72 @@ __naked void range_within_cnum_cross_both_boundaries(void)
 	: __clobber_all);
 }
 
+SEC("socket")
+__success
+__naked void fork_on_zero_cmp_k(void)
+{
+	asm volatile ("							\
+	call %[bpf_get_prandom_u32];					\
+	r0 &= 0xff;			/* r0 ∈ [0, 255] */		\
+	r1 = 8;								\
+	r1 -= r0;			/* r1 ∈ [-247, 8] */		\
+	if r1 == 0 goto 1f;						\
+	if r1 > 8 goto 1f;		/* r1 ∈ [1, 8] */		\
+	if r1 < 1 goto 2f;		/* must be dead */		\
+	if r1 > 8 goto 2f;		/* must be dead */		\
+	goto 1f;							\
+2:	r0 /= 0;			/* unreachable */		\
+1:	r0 = 0;								\
+	exit;								\
+	"
+	:: __imm(bpf_get_prandom_u32)
+	: __clobber_all);
+}
+
+SEC("socket")
+__success
+__naked void fork_on_zero_cmp_x(void)
+{
+	asm volatile ("							\
+	call %[bpf_get_prandom_u32];					\
+	r0 &= 0xff;			/* r0 ∈ [0, 255] */		\
+	r1 = 8;								\
+	r1 -= r0;			/* r1 ∈ [-247, 8] */		\
+	r2 = 0;								\
+	if r1 == r2 goto 1f;						\
+	if r1 > 8 goto 1f;		/* r1 ∈ [1, 8] */		\
+	if r1 < 1 goto 2f;		/* must be dead */		\
+	if r1 > 8 goto 2f;		/* must be dead */		\
+	goto 1f;							\
+2:	r0 /= 0;			/* unreachable */		\
+1:	r0 = 0;								\
+	exit;								\
+	"
+	:: __imm(bpf_get_prandom_u32)
+	: __clobber_all);
+}
+
+SEC("socket")
+__success
+__naked void fork_on_zero_cmp_x_swapped(void)
+{
+	asm volatile ("							\
+	call %[bpf_get_prandom_u32];					\
+	r0 &= 0xff;			/* r0 ∈ [0, 255] */		\
+	r1 = 8;								\
+	r1 -= r0;			/* r1 ∈ [-247, 8] */		\
+	r2 = 0;								\
+	if r2 == r1 goto 1f;						\
+	if r1 > 8 goto 1f;		/* r1 ∈ [1, 8] */		\
+	if r1 < 1 goto 2f;		/* must be dead */		\
+	if r1 > 8 goto 2f;		/* must be dead */		\
+	goto 1f;							\
+2:	r0 /= 0;			/* unreachable */		\
+1:	r0 = 0;								\
+	exit;								\
+	"
+	:: __imm(bpf_get_prandom_u32)
+	: __clobber_all);
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.53.0

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

* Re: [PATCH bpf 1/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
@ 2026-05-29  8:58   ` bot+bpf-ci
  2026-05-29 18:22     ` Eduard Zingerman
  2026-05-29  8:59   ` sashiko-bot
  2026-05-29 16:57   ` Emil Tsalapatis
  2 siblings, 1 reply; 14+ messages in thread
From: bot+bpf-ci @ 2026-05-29  8:58 UTC (permalink / raw)
  To: eddyz87, bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei,
	eddyz87, ast, andrii, daniel, martin.lau, eddyz87, yonghong.song,
	clm, ihor.solodrai

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

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 7fb88e1cd7c4..a2ee316f7ec0 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -16297,6 +16297,73 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s

[ ... ]

> +static int maybe_fork_cmp_with_zero(struct bpf_verifier_env *env,
> +				    struct bpf_insn *insn,
> +				    struct bpf_reg_state *src_reg,
> +				    struct bpf_reg_state *dst_reg)
> +{
> +	struct bpf_verifier_state *fork, *cur = env->cur_state;
> +	struct bpf_reg_state *fork_dst_reg;

[ ... ]

> +	fork = push_stack(env, env->insn_idx, env->insn_idx, cur->speculative);
> +	if (!fork)
> +		return -ENOMEM;

Can this catch a push_stack() failure?  push_stack() returns an
ERR_PTR() on every failure path rather than NULL:

kernel/bpf/verifier.c:push_stack() {
	...
	elem = kzalloc_obj(elem, GFP_KERNEL);
	if (!elem)
		goto err;
	...
	if (env->stack_size > BPF_COMPLEXITY_LIMIT_JMP_SEQ) {
		...
		return ERR_PTR(-E2BIG);
	}
	...
err:
	...
	return ERR_PTR(-ENOMEM);
}

Since an ERR_PTR is non-NULL, would 'if (!fork)' be false on failure,
letting execution continue with an error value in fork?

> +	fork_dst_regno = swapped ? insn->src_reg : insn->dst_reg;
> +	fork_dst_reg = &fork->frame[fork->curframe]->regs[fork_dst_regno];

If fork holds an ERR_PTR here, does this dereference an invalid pointer
such as (void *)-12, for example under memory pressure or when the
BPF_COMPLEXITY_LIMIT_JMP_SEQ limit is hit?

The other push_stack() callers test with IS_ERR() and return
PTR_ERR(), including the other_branch push in check_cond_jmp_op()
itself:

	other_branch = push_stack(env, *insn_idx + insn->off + 1,
				  *insn_idx, false);
	if (IS_ERR(other_branch))
		return PTR_ERR(other_branch);

Would the same IS_ERR()/PTR_ERR() pattern be more appropriate here, so
that the -E2BIG complexity limit is also honored?

> +	if (is_jmp32) {
> +		cnum32_intersect_with_srange(&dst_reg->r32, S32_MIN, -1);
> +		cnum32_intersect_with_srange(&fork_dst_reg->r32, 0, S32_MAX);
> +	} else {
> +		cnum64_intersect_with_srange(&dst_reg->r64, S64_MIN, -1);
> +		cnum64_intersect_with_srange(&fork_dst_reg->r64, 0, S64_MAX);
> +	}
> +	return 0;
> +}
> +
>  static int check_cond_jmp_op(struct bpf_verifier_env *env,
>  			     struct bpf_insn *insn, int *insn_idx)
>  {

I could not find a later commit in this range that changes this check.

On the commit message:

There does not appear to be a Fixes: tag.  The commit message describes
this as a "verifier regression after switch to cnum based scalar
representation", where cnum{32,64}_intersect() over approximates a
[negative, positive] range when comparing with zero.

Commit bbc631085503 ("bpf: replace min/max fields with struct
cnum{32,64}") introduced the cnum based scalar representation and the
cnum{32,64}_intersect() helpers, and is an ancestor of this fix.

Should this include:

  Fixes: bbc631085503 ("bpf: replace min/max fields with struct cnum{32,64}")


---
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/26626969604

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

* Re: [PATCH bpf 1/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
  2026-05-29  8:58   ` bot+bpf-ci
@ 2026-05-29  8:59   ` sashiko-bot
  2026-05-29 18:23     ` Eduard Zingerman
  2026-05-29 16:57   ` Emil Tsalapatis
  2 siblings, 1 reply; 14+ messages in thread
From: sashiko-bot @ 2026-05-29  8:59 UTC (permalink / raw)
  To: Eduard Zingerman; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Inconsistent verifier state and bypassed linked register synchronization.
- [Medium] Missing scalar type check applies zero-comparison workaround to pointer offsets.
--

commit dc22ab232b73a45e1aacc8d291fb5640c3a0d950
Author: Eduard Zingerman <eddyz87@gmail.com>

bpf: fork state when comparing sign crossing ranges with zero

This commit introduces a workaround for the verifier when evaluating a
comparison of a sign-crossing scalar against zero. It forks the verifier state
to process negative and non-negative branches separately, restoring precision
lost in the cnum-based scalar representation.

Link: https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index c8d980fdd7099..7998e8da5e554 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -15965,6 +15965,73 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
> +static int maybe_fork_cmp_with_zero(struct bpf_verifier_env *env,
> +				    struct bpf_insn *insn,
> +				    struct bpf_reg_state *src_reg,
> +				    struct bpf_reg_state *dst_reg)
> +{
[ ... ]
> +	if (!is_reg_const(src_reg, is_jmp32) || reg_const_value(src_reg, is_jmp32) != 0)
> +		return 0;
> +

[Severity: Medium]
Does this code apply the scalar workaround to pointer types?

It appears that dst_reg is not checked for SCALAR_VALUE. If dst_reg is a 
valid pointer with a variable offset (e.g., PTR_TO_MAP_VALUE) that happens 
to cross zero, and the program compares it against zero (if (ptr == 0)), 
the verifier might artificially restrict the pointer's offset and fork the state.

Since valid pointers are not zero, both forks will follow the fall-through 
path, meaning this doesn't create a safety vulnerability but could lead to 
unnecessary state exploration and semantically incorrect behavior. Should we 
verify dst_reg->type == SCALAR_VALUE here?

> +	bool cross_sign32 = is_jmp32 &&
> +		dst_reg->r32.size < S32_MAX &&
> +		cnum32_smin(dst_reg->r32) < 0 && cnum32_smax(dst_reg->r32) > 0;
> +	bool cross_sign64 = !is_jmp32 &&
> +		dst_reg->r64.size < S64_MAX &&
> +		cnum64_smin(dst_reg->r64) < 0 && cnum64_smax(dst_reg->r64) > 0;
> +	if (!cross_sign32 && !cross_sign64)
> +		return 0;
> +
> +	fork = push_stack(env, env->insn_idx, env->insn_idx, cur->speculative);
> +	if (!fork)
> +		return -ENOMEM;
> +
> +	fork_dst_regno = swapped ? insn->src_reg : insn->dst_reg;
> +	fork_dst_reg = &fork->frame[fork->curframe]->regs[fork_dst_regno];
> +	if (is_jmp32) {
> +		cnum32_intersect_with_srange(&dst_reg->r32, S32_MIN, -1);
> +		cnum32_intersect_with_srange(&fork_dst_reg->r32, 0, S32_MAX);
> +	} else {
> +		cnum64_intersect_with_srange(&dst_reg->r64, S64_MIN, -1);
> +		cnum64_intersect_with_srange(&fork_dst_reg->r64, 0, S64_MAX);
> +	}

[Severity: Medium]
Are we leaving dst_reg and fork_dst_reg in an inconsistent state by not
calling reg_bounds_sync()?

Because dst_reg->r32 or r64 are modified directly here without syncing, 
the bounds are narrowed but var_off and the other bounds remain wide.

Additionally, because the dst_reg bounds are forced to be strictly negative 
here, is_branch_taken() later in check_cond_jmp_op() might evaluate 
the branch statically (pred == 0). If it does, check_cond_jmp_op() will 
return early and completely bypass sync_linked_regs() for the current branch.

While this makes the verifier strictly more conservative for the linked
registers (since wider bounds fail faster), it could cause linked registers
to lose precision and lead to potential spurious rejections.

> +	return 0;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260529-cnum-split-at-zero-v1-0-986c03752226@gmail.com?part=1

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

* Re: [PATCH bpf 1/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
  2026-05-29  8:58   ` bot+bpf-ci
  2026-05-29  8:59   ` sashiko-bot
@ 2026-05-29 16:57   ` Emil Tsalapatis
  2 siblings, 0 replies; 14+ messages in thread
From: Emil Tsalapatis @ 2026-05-29 16:57 UTC (permalink / raw)
  To: Eduard Zingerman, bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei

On Fri May 29, 2026 at 4:13 AM EDT, Eduard Zingerman wrote:
> YiFei Zhu reported the verifier regression after switch to cnum based
> scalar representation. When the following sequence of instructions is
> processed:
>
>     1: ... rX setup with [negative, positive] bounds ...
>     2: if rX == 0 goto ...
>     3: if rX > C  goto ...
>     4: ... code relying on rX being in range [1, C] ...
>
> The comparison at (2) is processed by cnum{32,64}_intersect(), which
> can't punch a hole in [negative, positive] range and over approximates
> by leaving rX range unchanged, leading to a deduction of range [0, C]
> at (4). The pre-cnum signed/unsigned ranges based representation could
> always deduct from 'rX != 0' that umin bound is 1.
>
> This patch introduces a workaround: when'if rX ==/!= 0 goto ...' is
> processed and rX has [negative, positive] range, fork the verifier
> state and use current and forked states to split rX representation
> in negative and non-negative parts.
>
> The fork is placed before the branch is evaluated, so that the forked
> state re-enters check_cond_jmp_op() and gets mark_ptr_or_null_regs(),
> sync_linked_regs(), precision tracking processing w/o additional code
> changes.
>
> Reported-by: YiFei Zhu <zhuyifei@google.com>
> Closes: https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
> Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>

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

Minor nit below.

> ---
>  kernel/bpf/verifier.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 71 insertions(+)
>
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index c8d980fdd709..7998e8da5e55 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -15965,6 +15965,73 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
>  	}
>  }
>  
> +/*
> + * This is a workaround for a specific pattern:
> + *
> + *   1: ... rX setup with [negative, positive] bounds ...
> + *   2: if rX == 0 goto ...
> + *   3: if rX > C  goto ...
> + *   4: ... code relying on rX being in range [1, C] ...
> + *
> + * The comparison at (2) is processed by cnum{32,64}_intersect(),
> + * which can't punch a hole in [negative, positive] range and
> + * over approximates by leaving rX range unchanged,
> + * leading to a deduction of range [0, C] at (4).
> + *
> + * The workaround is to fork verifier state when:
> + * - 'if rX ==/!= 0 goto ...' is processed
> + * - rX has [negative, positive] range
> + * The current and forked states are used to split rX representation
> + * in negative and non-negative parts.
> + */
> +static int maybe_fork_cmp_with_zero(struct bpf_verifier_env *env,
> +				    struct bpf_insn *insn,
> +				    struct bpf_reg_state *src_reg,
> +				    struct bpf_reg_state *dst_reg)
> +{
> +	struct bpf_verifier_state *fork, *cur = env->cur_state;
> +	struct bpf_reg_state *fork_dst_reg;
> +	bool is_jmp32 = BPF_CLASS(insn->code) == BPF_JMP32;
> +	u8 opcode = BPF_OP(insn->code);
> +	bool swapped = false;
> +	u32 fork_dst_regno;
> +
> +	if (opcode != BPF_JEQ && opcode != BPF_JNE)
> +		return 0;
> +
> +	if (!is_reg_const(src_reg, is_jmp32)) {
> +		swap(src_reg, dst_reg);
> +		swapped = true;
> +	}
> +
> +	if (!is_reg_const(src_reg, is_jmp32) || reg_const_value(src_reg, is_jmp32) != 0)
> +		return 0;
> +
> +	bool cross_sign32 = is_jmp32 &&
> +		dst_reg->r32.size < S32_MAX &&
> +		cnum32_smin(dst_reg->r32) < 0 && cnum32_smax(dst_reg->r32) > 0;
> +	bool cross_sign64 = !is_jmp32 &&
> +		dst_reg->r64.size < S64_MAX &&
> +		cnum64_smin(dst_reg->r64) < 0 && cnum64_smax(dst_reg->r64) > 0;

Nit: Hoist the bool definitions to the beginning of the function?

> +	if (!cross_sign32 && !cross_sign64)
> +		return 0;
> +
> +	fork = push_stack(env, env->insn_idx, env->insn_idx, cur->speculative);
> +	if (!fork)
> +		return -ENOMEM;
> +
> +	fork_dst_regno = swapped ? insn->src_reg : insn->dst_reg;
> +	fork_dst_reg = &fork->frame[fork->curframe]->regs[fork_dst_regno];
> +	if (is_jmp32) {
> +		cnum32_intersect_with_srange(&dst_reg->r32, S32_MIN, -1);
> +		cnum32_intersect_with_srange(&fork_dst_reg->r32, 0, S32_MAX);
> +	} else {
> +		cnum64_intersect_with_srange(&dst_reg->r64, S64_MIN, -1);
> +		cnum64_intersect_with_srange(&fork_dst_reg->r64, 0, S64_MAX);
> +	}
> +	return 0;
> +}
> +
>  static int check_cond_jmp_op(struct bpf_verifier_env *env,
>  			     struct bpf_insn *insn, int *insn_idx)
>  {
> @@ -16038,6 +16105,10 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
>  			insn_flags |= INSN_F_DST_REG_STACK;
>  	}
>  
> +	err = maybe_fork_cmp_with_zero(env, insn, src_reg, dst_reg);
> +	if (err)
> +		return err;
> +
>  	if (insn_flags) {
>  		err = bpf_push_jmp_history(env, this_branch, insn_flags, 0, 0, 0);
>  		if (err)


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

* Re: [PATCH bpf 2/2] selftests/bpf: test fork on zero comparison with wrapping ranges
  2026-05-29  8:13 ` [PATCH bpf 2/2] selftests/bpf: test fork on zero comparison with wrapping ranges Eduard Zingerman
@ 2026-05-29 17:03   ` Emil Tsalapatis
  0 siblings, 0 replies; 14+ messages in thread
From: Emil Tsalapatis @ 2026-05-29 17:03 UTC (permalink / raw)
  To: Eduard Zingerman, bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei

On Fri May 29, 2026 at 4:13 AM EDT, Eduard Zingerman wrote:
> Add tests verifying that the verifier correctly narrows a
> sign-boundary-crossing range after comparing with zero.
> - fork_on_zero_cmp_k - BPF_K conditional instruction;
> - fork_on_zero_cmp_x -
>   BPF_X conditional instruction with src being zero.
> - fork_on_zero_cmp_x_swapped -
>   BPF_X conditional instruction with dst being zero.
>

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

> Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
> ---
>  .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 ++++++++++++++++++++++
>  1 file changed, 68 insertions(+)
>
> diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/testing/selftests/bpf/progs/verifier_bounds.c
> index bc038ac2df98..f97b0e109326 100644
> --- a/tools/testing/selftests/bpf/progs/verifier_bounds.c
> +++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c
> @@ -2294,4 +2294,72 @@ __naked void range_within_cnum_cross_both_boundaries(void)
>  	: __clobber_all);
>  }
>  
> +SEC("socket")
> +__success
> +__naked void fork_on_zero_cmp_k(void)
> +{
> +	asm volatile ("							\
> +	call %[bpf_get_prandom_u32];					\
> +	r0 &= 0xff;			/* r0 ∈ [0, 255] */		\
> +	r1 = 8;								\
> +	r1 -= r0;			/* r1 ∈ [-247, 8] */		\
> +	if r1 == 0 goto 1f;						\
> +	if r1 > 8 goto 1f;		/* r1 ∈ [1, 8] */		\
> +	if r1 < 1 goto 2f;		/* must be dead */		\
> +	if r1 > 8 goto 2f;		/* must be dead */		\
> +	goto 1f;							\
> +2:	r0 /= 0;			/* unreachable */		\
> +1:	r0 = 0;								\
> +	exit;								\
> +	"
> +	:: __imm(bpf_get_prandom_u32)
> +	: __clobber_all);
> +}
> +
> +SEC("socket")
> +__success
> +__naked void fork_on_zero_cmp_x(void)
> +{
> +	asm volatile ("							\
> +	call %[bpf_get_prandom_u32];					\
> +	r0 &= 0xff;			/* r0 ∈ [0, 255] */		\
> +	r1 = 8;								\
> +	r1 -= r0;			/* r1 ∈ [-247, 8] */		\
> +	r2 = 0;								\
> +	if r1 == r2 goto 1f;						\
> +	if r1 > 8 goto 1f;		/* r1 ∈ [1, 8] */		\
> +	if r1 < 1 goto 2f;		/* must be dead */		\
> +	if r1 > 8 goto 2f;		/* must be dead */		\
> +	goto 1f;							\
> +2:	r0 /= 0;			/* unreachable */		\
> +1:	r0 = 0;								\
> +	exit;								\
> +	"
> +	:: __imm(bpf_get_prandom_u32)
> +	: __clobber_all);
> +}
> +
> +SEC("socket")
> +__success
> +__naked void fork_on_zero_cmp_x_swapped(void)
> +{
> +	asm volatile ("							\
> +	call %[bpf_get_prandom_u32];					\
> +	r0 &= 0xff;			/* r0 ∈ [0, 255] */		\
> +	r1 = 8;								\
> +	r1 -= r0;			/* r1 ∈ [-247, 8] */		\
> +	r2 = 0;								\
> +	if r2 == r1 goto 1f;						\
> +	if r1 > 8 goto 1f;		/* r1 ∈ [1, 8] */		\
> +	if r1 < 1 goto 2f;		/* must be dead */		\
> +	if r1 > 8 goto 2f;		/* must be dead */		\
> +	goto 1f;							\
> +2:	r0 /= 0;			/* unreachable */		\
> +1:	r0 = 0;								\
> +	exit;								\
> +	"
> +	:: __imm(bpf_get_prandom_u32)
> +	: __clobber_all);
> +}
> +
>  char _license[] SEC("license") = "GPL";


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

* Re: [PATCH bpf 1/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:58   ` bot+bpf-ci
@ 2026-05-29 18:22     ` Eduard Zingerman
  0 siblings, 0 replies; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-29 18:22 UTC (permalink / raw)
  To: bot+bpf-ci, bpf, ast
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei,
	martin.lau, clm, ihor.solodrai

On Fri, 2026-05-29 at 08:58 +0000, bot+bpf-ci@kernel.org wrote:
> > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > index 7fb88e1cd7c4..a2ee316f7ec0 100644
> > --- a/kernel/bpf/verifier.c
> > +++ b/kernel/bpf/verifier.c
> > @@ -16297,6 +16297,73 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
> 
> [ ... ]
> 
> > +static int maybe_fork_cmp_with_zero(struct bpf_verifier_env *env,
> > +				    struct bpf_insn *insn,
> > +				    struct bpf_reg_state *src_reg,
> > +				    struct bpf_reg_state *dst_reg)
> > +{
> > +	struct bpf_verifier_state *fork, *cur = env->cur_state;
> > +	struct bpf_reg_state *fork_dst_reg;
> 
> [ ... ]
> 
> > +	fork = push_stack(env, env->insn_idx, env->insn_idx, cur->speculative);
> > +	if (!fork)
> > +		return -ENOMEM;
> 
> Can this catch a push_stack() failure?  push_stack() returns an
> ERR_PTR() on every failure path rather than NULL:
> 
> kernel/bpf/verifier.c:push_stack() {
> 	...
> 	elem = kzalloc_obj(elem, GFP_KERNEL);
> 	if (!elem)
> 		goto err;
> 	...
> 	if (env->stack_size > BPF_COMPLEXITY_LIMIT_JMP_SEQ) {
> 		...
> 		return ERR_PTR(-E2BIG);
> 	}
> 	...
> err:
> 	...
> 	return ERR_PTR(-ENOMEM);
> }
> 
> Since an ERR_PTR is non-NULL, would 'if (!fork)' be false on failure,
> letting execution continue with an error value in fork?

Should be fixed.

> > +	fork_dst_regno = swapped ? insn->src_reg : insn->dst_reg;
> > +	fork_dst_reg = &fork->frame[fork->curframe]->regs[fork_dst_regno];
> 
> If fork holds an ERR_PTR here, does this dereference an invalid pointer
> such as (void *)-12, for example under memory pressure or when the
> BPF_COMPLEXITY_LIMIT_JMP_SEQ limit is hit?
> 
> The other push_stack() callers test with IS_ERR() and return
> PTR_ERR(), including the other_branch push in check_cond_jmp_op()
> itself:
> 
> 	other_branch = push_stack(env, *insn_idx + insn->off + 1,
> 				  *insn_idx, false);
> 	if (IS_ERR(other_branch))
> 		return PTR_ERR(other_branch);
> 
> Would the same IS_ERR()/PTR_ERR() pattern be more appropriate here, so
> that the -E2BIG complexity limit is also honored?
> 
> > +	if (is_jmp32) {
> > +		cnum32_intersect_with_srange(&dst_reg->r32, S32_MIN, -1);
> > +		cnum32_intersect_with_srange(&fork_dst_reg->r32, 0, S32_MAX);
> > +	} else {
> > +		cnum64_intersect_with_srange(&dst_reg->r64, S64_MIN, -1);
> > +		cnum64_intersect_with_srange(&fork_dst_reg->r64, 0, S64_MAX);
> > +	}
> > +	return 0;
> > +}
> > +
> >  static int check_cond_jmp_op(struct bpf_verifier_env *env,
> >  			     struct bpf_insn *insn, int *insn_idx)
> >  {
> 
> I could not find a later commit in this range that changes this check.
> 
> On the commit message:
> 
> There does not appear to be a Fixes: tag.  The commit message describes
> this as a "verifier regression after switch to cnum based scalar
> representation", where cnum{32,64}_intersect() over approximates a
> [negative, positive] range when comparing with zero.
> 
> Commit bbc631085503 ("bpf: replace min/max fields with struct
> cnum{32,64}") introduced the cnum based scalar representation and the
> cnum{32,64}_intersect() helpers, and is an ancestor of this fix.
> 
> Should this include:
> 
>   Fixes: bbc631085503 ("bpf: replace min/max fields with struct cnum{32,64}")

I don't think "fixes" tag is warranted.

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

* Re: [PATCH bpf 1/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:59   ` sashiko-bot
@ 2026-05-29 18:23     ` Eduard Zingerman
  0 siblings, 0 replies; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-29 18:23 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: bpf

On Fri, 2026-05-29 at 08:59 +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] Inconsistent verifier state and bypassed linked register synchronization.
> - [Medium] Missing scalar type check applies zero-comparison workaround to pointer offsets.
> --
> 
> commit dc22ab232b73a45e1aacc8d291fb5640c3a0d950
> Author: Eduard Zingerman <eddyz87@gmail.com>
> 
> bpf: fork state when comparing sign crossing ranges with zero
> 
> This commit introduces a workaround for the verifier when evaluating a
> comparison of a sign-crossing scalar against zero. It forks the verifier state
> to process negative and non-negative branches separately, restoring precision
> lost in the cnum-based scalar representation.
> 
> Link: https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
> 
> > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > index c8d980fdd7099..7998e8da5e554 100644
> > --- a/kernel/bpf/verifier.c
> > +++ b/kernel/bpf/verifier.c
> [ ... ]
> > @@ -15965,6 +15965,73 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
> > +static int maybe_fork_cmp_with_zero(struct bpf_verifier_env *env,
> > +				    struct bpf_insn *insn,
> > +				    struct bpf_reg_state *src_reg,
> > +				    struct bpf_reg_state *dst_reg)
> > +{
> [ ... ]
> > +	if (!is_reg_const(src_reg, is_jmp32) || reg_const_value(src_reg, is_jmp32) != 0)
> > +		return 0;
> > +
> 
> [Severity: Medium]
> Does this code apply the scalar workaround to pointer types?
> 
> It appears that dst_reg is not checked for SCALAR_VALUE. If dst_reg is a 
> valid pointer with a variable offset (e.g., PTR_TO_MAP_VALUE) that happens 
> to cross zero, and the program compares it against zero (if (ptr == 0)), 
> the verifier might artificially restrict the pointer's offset and fork the state.
> 
> Since valid pointers are not zero, both forks will follow the fall-through 
> path, meaning this doesn't create a safety vulnerability but could lead to 
> unnecessary state exploration and semantically incorrect behavior. Should we 
> verify dst_reg->type == SCALAR_VALUE here?

Should probably restrict to SCALAR_VALUE.

> > +	bool cross_sign32 = is_jmp32 &&
> > +		dst_reg->r32.size < S32_MAX &&
> > +		cnum32_smin(dst_reg->r32) < 0 && cnum32_smax(dst_reg->r32) > 0;
> > +	bool cross_sign64 = !is_jmp32 &&
> > +		dst_reg->r64.size < S64_MAX &&
> > +		cnum64_smin(dst_reg->r64) < 0 && cnum64_smax(dst_reg->r64) > 0;
> > +	if (!cross_sign32 && !cross_sign64)
> > +		return 0;
> > +
> > +	fork = push_stack(env, env->insn_idx, env->insn_idx, cur->speculative);
> > +	if (!fork)
> > +		return -ENOMEM;
> > +
> > +	fork_dst_regno = swapped ? insn->src_reg : insn->dst_reg;
> > +	fork_dst_reg = &fork->frame[fork->curframe]->regs[fork_dst_regno];
> > +	if (is_jmp32) {
> > +		cnum32_intersect_with_srange(&dst_reg->r32, S32_MIN, -1);
> > +		cnum32_intersect_with_srange(&fork_dst_reg->r32, 0, S32_MAX);
> > +	} else {
> > +		cnum64_intersect_with_srange(&dst_reg->r64, S64_MIN, -1);
> > +		cnum64_intersect_with_srange(&fork_dst_reg->r64, 0, S64_MAX);
> > +	}
> 
> [Severity: Medium]
> Are we leaving dst_reg and fork_dst_reg in an inconsistent state by not
> calling reg_bounds_sync()?
> 
> Because dst_reg->r32 or r64 are modified directly here without syncing, 
> the bounds are narrowed but var_off and the other bounds remain wide.
> 
> Additionally, because the dst_reg bounds are forced to be strictly negative 
> here, is_branch_taken() later in check_cond_jmp_op() might evaluate 
> the branch statically (pred == 0). If it does, check_cond_jmp_op() will 
> return early and completely bypass sync_linked_regs() for the current branch.
> 
> While this makes the verifier strictly more conservative for the linked
> registers (since wider bounds fail faster), it could cause linked registers
> to lose precision and lead to potential spurious rejections.

Missing reg_bounds_sync() is a good point.

> > +	return 0;
> > +}

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

* Re: [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29  8:13 [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
  2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
  2026-05-29  8:13 ` [PATCH bpf 2/2] selftests/bpf: test fork on zero comparison with wrapping ranges Eduard Zingerman
@ 2026-05-29 22:44 ` Eduard Zingerman
  2026-05-29 23:02   ` Ihor Solodrai
  2026-05-30  0:23   ` Eduard Zingerman
  2 siblings, 2 replies; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-29 22:44 UTC (permalink / raw)
  To: bpf, ast; +Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei

On Fri, 2026-05-29 at 01:13 -0700, Eduard Zingerman wrote:
> YiFei Zhu reported [1] the verifier regression after switch to cnum
> based scalars representation. When the following sequence of
> instructions is processed:
> 
>     1: ... rX setup with [negative, positive] bounds ...
>     2: if rX == 0 goto ...
>     3: if rX > C  goto ...
>     4: ... code relying on rX being in range [1, C] ...
> 
> The cnum-based implementation only infers that rX range is [0, C]
> at instruction (4). The pre-cnum signed/unsigned ranges based
> representation could always deduct from 'rX != 0' that
> umin bound is 1.
> 
> This patch introduces a workaround forking the verifier state when a
> register with sign-crossing range is compared to zero.
> 
> [1] https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
> 
> ---
> Eduard Zingerman (2):
>       bpf: fork state when comparing sign crossing ranges with zero
>       selftests/bpf: test fork on zero comparison with wrapping ranges
> 
>  kernel/bpf/verifier.c                              | 71 ++++++++++++++++++++++
>  .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 +++++++++++++++++++++
>  2 files changed, 139 insertions(+)
> ---
> base-commit: e42e53ae23b7d41df22ccd7788192bf578f24da2
> change-id: 20260529-cnum-split-at-zero-3c03db9234d3

I don't know why CI misses it:

https://github.com/kernel-patches/bpf/pull/12235

But I see two libarena tests failures with this series locally:

File                 Program                    Verdict  Duration (us)   Insns  States  Program size  Jited size
-------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
...
libarena_asan.bpf.o  asan_test_buddy_oob        failure         879905  209739    4158          3931           0
...
libarena_asan.bpf.o  test_buddy_alloc_multiple  failure         269851  110341    2774          3897           0
...
-------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------

Investigating.

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

* Re: [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29 22:44 ` [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
@ 2026-05-29 23:02   ` Ihor Solodrai
  2026-05-29 23:53     ` Emil Tsalapatis
  2026-05-30  0:23   ` Eduard Zingerman
  1 sibling, 1 reply; 14+ messages in thread
From: Ihor Solodrai @ 2026-05-29 23:02 UTC (permalink / raw)
  To: Eduard Zingerman, bpf, ast, Emil Tsalapatis
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei

On 5/29/26 3:44 PM, Eduard Zingerman wrote:
> On Fri, 2026-05-29 at 01:13 -0700, Eduard Zingerman wrote:
>> YiFei Zhu reported [1] the verifier regression after switch to cnum
>> based scalars representation. When the following sequence of
>> instructions is processed:
>>
>>     1: ... rX setup with [negative, positive] bounds ...
>>     2: if rX == 0 goto ...
>>     3: if rX > C  goto ...
>>     4: ... code relying on rX being in range [1, C] ...
>>
>> The cnum-based implementation only infers that rX range is [0, C]
>> at instruction (4). The pre-cnum signed/unsigned ranges based
>> representation could always deduct from 'rX != 0' that
>> umin bound is 1.
>>
>> This patch introduces a workaround forking the verifier state when a
>> register with sign-crossing range is compared to zero.
>>
>> [1] https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
>>
>> ---
>> Eduard Zingerman (2):
>>       bpf: fork state when comparing sign crossing ranges with zero
>>       selftests/bpf: test fork on zero comparison with wrapping ranges
>>
>>  kernel/bpf/verifier.c                              | 71 ++++++++++++++++++++++
>>  .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 +++++++++++++++++++++
>>  2 files changed, 139 insertions(+)
>> ---
>> base-commit: e42e53ae23b7d41df22ccd7788192bf578f24da2
>> change-id: 20260529-cnum-split-at-zero-3c03db9234d3
> 
> I don't know why CI misses it:
> 
> https://github.com/kernel-patches/bpf/pull/12235
> 
> But I see two libarena tests failures with this series locally:
> 
> File                 Program                    Verdict  Duration (us)   Insns  States  Program size  Jited size
> -------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
> ...
> libarena_asan.bpf.o  asan_test_buddy_oob        failure         879905  209739    4158          3931           0
> ...
> libarena_asan.bpf.o  test_buddy_alloc_multiple  failure         269851  110341    2774          3897           0
> ...
> -------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
> 
> Investigating.

Hi everyone,

Apparently libarena_asan tests are currently skipped on BPF CI,
because they require clang 22 [1], which is not yet enabled there.

I'll work on adding clang 22 to CI, but in the meanwhile please make
sure to test libarena locally with clang 22 if your changes are relevant.

[1] https://lore.kernel.org/bpf/20260426190338.4615-6-emil@etsalapatis.com/

cc: Emil


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

* Re: [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29 23:02   ` Ihor Solodrai
@ 2026-05-29 23:53     ` Emil Tsalapatis
  2026-06-10 11:07       ` Shung-Hsi Yu
  0 siblings, 1 reply; 14+ messages in thread
From: Emil Tsalapatis @ 2026-05-29 23:53 UTC (permalink / raw)
  To: Ihor Solodrai, Eduard Zingerman, bpf, ast, Emil Tsalapatis
  Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei

On Fri May 29, 2026 at 7:02 PM EDT, Ihor Solodrai wrote:
> On 5/29/26 3:44 PM, Eduard Zingerman wrote:
>> On Fri, 2026-05-29 at 01:13 -0700, Eduard Zingerman wrote:
>>> YiFei Zhu reported [1] the verifier regression after switch to cnum
>>> based scalars representation. When the following sequence of
>>> instructions is processed:
>>>
>>>     1: ... rX setup with [negative, positive] bounds ...
>>>     2: if rX == 0 goto ...
>>>     3: if rX > C  goto ...
>>>     4: ... code relying on rX being in range [1, C] ...
>>>
>>> The cnum-based implementation only infers that rX range is [0, C]
>>> at instruction (4). The pre-cnum signed/unsigned ranges based
>>> representation could always deduct from 'rX != 0' that
>>> umin bound is 1.
>>>
>>> This patch introduces a workaround forking the verifier state when a
>>> register with sign-crossing range is compared to zero.
>>>
>>> [1] https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
>>>
>>> ---
>>> Eduard Zingerman (2):
>>>       bpf: fork state when comparing sign crossing ranges with zero
>>>       selftests/bpf: test fork on zero comparison with wrapping ranges
>>>
>>>  kernel/bpf/verifier.c                              | 71 ++++++++++++++++++++++
>>>  .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 +++++++++++++++++++++
>>>  2 files changed, 139 insertions(+)
>>> ---
>>> base-commit: e42e53ae23b7d41df22ccd7788192bf578f24da2
>>> change-id: 20260529-cnum-split-at-zero-3c03db9234d3
>> 
>> I don't know why CI misses it:
>> 
>> https://github.com/kernel-patches/bpf/pull/12235
>> 
>> But I see two libarena tests failures with this series locally:
>> 
>> File                 Program                    Verdict  Duration (us)   Insns  States  Program size  Jited size
>> -------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
>> ...
>> libarena_asan.bpf.o  asan_test_buddy_oob        failure         879905  209739    4158          3931           0
>> ...
>> libarena_asan.bpf.o  test_buddy_alloc_multiple  failure         269851  110341    2774          3897           0
>> ...
>> -------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
>> 
>> Investigating.
>
> Hi everyone,
>
> Apparently libarena_asan tests are currently skipped on BPF CI,
> because they require clang 22 [1], which is not yet enabled there.
>
> I'll work on adding clang 22 to CI, but in the meanwhile please make
> sure to test libarena locally with clang 22 if your changes are relevant.
>
> [1] https://lore.kernel.org/bpf/20260426190338.4615-6-emil@etsalapatis.com/

We actually need Clang HEAD since that's the release with address
space-specific support, is it possible to add it to the CI instead of
22? Alternatively we would have to wait for the Clang 23 RC in July.

>
> cc: Emil


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

* Re: [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29 22:44 ` [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
  2026-05-29 23:02   ` Ihor Solodrai
@ 2026-05-30  0:23   ` Eduard Zingerman
  1 sibling, 0 replies; 14+ messages in thread
From: Eduard Zingerman @ 2026-05-30  0:23 UTC (permalink / raw)
  To: bpf, ast; +Cc: andrii, daniel, martin.lau, kernel-team, yonghong.song, zhuyifei

On Fri, 2026-05-29 at 15:44 -0700, Eduard Zingerman wrote:
> On Fri, 2026-05-29 at 01:13 -0700, Eduard Zingerman wrote:
> > YiFei Zhu reported [1] the verifier regression after switch to cnum
> > based scalars representation. When the following sequence of
> > instructions is processed:
> > 
> >     1: ... rX setup with [negative, positive] bounds ...
> >     2: if rX == 0 goto ...
> >     3: if rX > C  goto ...
> >     4: ... code relying on rX being in range [1, C] ...
> > 
> > The cnum-based implementation only infers that rX range is [0, C]
> > at instruction (4). The pre-cnum signed/unsigned ranges based
> > representation could always deduct from 'rX != 0' that
> > umin bound is 1.
> > 
> > This patch introduces a workaround forking the verifier state when a
> > register with sign-crossing range is compared to zero.
> > 
> > [1] https://lore.kernel.org/bpf/96c4a1aa4333d10b882a9b5093d2d982f9f106e3.camel@gmail.com/T/
> > 
> > ---
> > Eduard Zingerman (2):
> >       bpf: fork state when comparing sign crossing ranges with zero
> >       selftests/bpf: test fork on zero comparison with wrapping ranges
> > 
> >  kernel/bpf/verifier.c                              | 71 ++++++++++++++++++++++
> >  .../testing/selftests/bpf/progs/verifier_bounds.c  | 68 +++++++++++++++++++++
> >  2 files changed, 139 insertions(+)
> > ---
> > base-commit: e42e53ae23b7d41df22ccd7788192bf578f24da2
> > change-id: 20260529-cnum-split-at-zero-3c03db9234d3
> 
> I don't know why CI misses it:
> 
> https://github.com/kernel-patches/bpf/pull/12235
> 
> But I see two libarena tests failures with this series locally:
> 
> File                 Program                    Verdict  Duration (us)   Insns  States  Program size  Jited size
> -------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
> ...
> libarena_asan.bpf.o  asan_test_buddy_oob        failure         879905  209739    4158          3931           0
> ...
> libarena_asan.bpf.o  test_buddy_alloc_multiple  failure         269851  110341    2774          3897           0
> ...
> -------------------  -------------------------  -------  -------------  ------  ------  ------------  ----------
> 
> Investigating.

So, the gist is: suppose there is a loop:

  for (i = 0; i < SUFFICIENTLY_LARGE; i++) {
    x = ... range [-127, +128] ...;
    if (x != 0) {
      ...
    }
    ...
  }

With this patch-set the 'if (x != 0)' would pile up an additional
state on the jump stack (second half of the range), compared to
master. Because the loop is verified till the exit the additional
states would accumulate on the jump stack. Which means that
SUFFICIENTLY_LARGE can always be picked such that the program verifies
on master but fails to verify with this patch.

Two test cases in libarena_asan hit this wall because they have
large-enough bounded loops (the loops are declared with 'can_loop',
but 'zero' is declared with 'const', hence the trick doesn't work).

Remaining options are:
- explore a simple constraints engine
- revert cnums
- accept the possibility of such regression

I'll work on the constraints engine over the weekend.

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

* Re: [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero
  2026-05-29 23:53     ` Emil Tsalapatis
@ 2026-06-10 11:07       ` Shung-Hsi Yu
  0 siblings, 0 replies; 14+ messages in thread
From: Shung-Hsi Yu @ 2026-06-10 11:07 UTC (permalink / raw)
  To: Emil Tsalapatis, Ihor Solodrai
  Cc: Eduard Zingerman, bpf, ast, andrii, daniel, martin.lau,
	kernel-team, yonghong.song, zhuyifei

On Fri, May 29, 2026 at 07:53:08PM -0400, Emil Tsalapatis wrote:
> On Fri May 29, 2026 at 7:02 PM EDT, Ihor Solodrai wrote:
> > On 5/29/26 3:44 PM, Eduard Zingerman wrote:
> >> On Fri, 2026-05-29 at 01:13 -0700, Eduard Zingerman wrote:
[...]
> > Hi everyone,
> >
> > Apparently libarena_asan tests are currently skipped on BPF CI,
> > because they require clang 22 [1], which is not yet enabled there.
> >
> > I'll work on adding clang 22 to CI, but in the meanwhile please make
> > sure to test libarena locally with clang 22 if your changes are relevant.
> >
> > [1] https://lore.kernel.org/bpf/20260426190338.4615-6-emil@etsalapatis.com/
> 
> We actually need Clang HEAD since that's the release with address
> space-specific support, is it possible to add it to the CI instead of
> 22? Alternatively we would have to wait for the Clang 23 RC in July.

Theorecially possible, the LLVM infrastructre at apt.llvm.org does
provide LLVM 23 snapshots (e.g. [1]), the problem awhile back was that
the infrasture had issues[2,3] so can't be used to install anything
newer than 21.

Just check again, and [2] seem to have been resolved[4]. While the
problem with installation script[3] thats left could probably be worked
around by adding the apt repositories directly.

1: https://apt.llvm.org/unstable/pool/main/l/llvm-toolchain-snapshot/
2: https://github.com/libbpf/ci/commit/af3742c19566425bf8fa3bd8425ea65917c55401
3: https://github.com/llvm/llvm-project/issues/180163
4: https://github.com/llvm/llvm-project/issues/179148#issuecomment-3963668474

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

end of thread, other threads:[~2026-06-10 11:07 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-29  8:13 [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
2026-05-29  8:13 ` [PATCH bpf 1/2] " Eduard Zingerman
2026-05-29  8:58   ` bot+bpf-ci
2026-05-29 18:22     ` Eduard Zingerman
2026-05-29  8:59   ` sashiko-bot
2026-05-29 18:23     ` Eduard Zingerman
2026-05-29 16:57   ` Emil Tsalapatis
2026-05-29  8:13 ` [PATCH bpf 2/2] selftests/bpf: test fork on zero comparison with wrapping ranges Eduard Zingerman
2026-05-29 17:03   ` Emil Tsalapatis
2026-05-29 22:44 ` [PATCH bpf 0/2] bpf: fork state when comparing sign crossing ranges with zero Eduard Zingerman
2026-05-29 23:02   ` Ihor Solodrai
2026-05-29 23:53     ` Emil Tsalapatis
2026-06-10 11:07       ` Shung-Hsi Yu
2026-05-30  0:23   ` Eduard Zingerman

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