All of lore.kernel.org
 help / color / mirror / Atom feed
From: Shung-Hsi Yu <shung-hsi.yu@suse.com>
To: Andrii Nakryiko <andrii@kernel.org>
Cc: bpf@vger.kernel.org, ast@kernel.org, daniel@iogearbox.net,
	martin.lau@kernel.org, kernel-team@meta.com
Subject: Re: [PATCH bpf-next 01/13] bpf: generalize reg_set_min_max() to handle non-const register comparisons
Date: Fri, 3 Nov 2023 16:33:06 +0800	[thread overview]
Message-ID: <ZUSwQtfjCsKpbWcL@u94a> (raw)
In-Reply-To: <ZUSmxI9EoWjUyO_t@u94a>

On Fri, Nov 03, 2023 at 03:52:36PM +0800, Shung-Hsi Yu wrote:
> On Thu, Nov 02, 2023 at 05:08:10PM -0700, Andrii Nakryiko wrote:
> > Generalize bounds adjustment logic of reg_set_min_max() to handle not
> > just register vs constant case, but in general any register vs any
> > register cases. For most of the operations it's trivial extension based
> > on range vs range comparison logic, we just need to properly pick
> > min/max of a range to compare against min/max of the other range.
> > 
> > For BPF_JSET we keep the original capabilities, just make sure JSET is
> > integrated in the common framework. This is manifested in the
> > internal-only BPF_KSET + BPF_X "opcode" to allow for simpler and more
>                     ^ typo?
> 
> Two more comments below
> 
> > uniform rev_opcode() handling. See the code for details. This allows to
> > reuse the same code exactly both for TRUE and FALSE branches without
> > explicitly handling both conditions with custom code.
> > 
> > Note also that now we don't need a special handling of BPF_JEQ/BPF_JNE
> > case none of the registers are constants. This is now just a normal
> > generic case handled by reg_set_min_max().
> > 
> > To make tnum handling cleaner, tnum_with_subreg() helper is added, as
> > that's a common operator when dealing with 32-bit subregister bounds.
> > This keeps the overall logic much less noisy when it comes to tnums.
> > 
> > Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
> > ---
> >  include/linux/tnum.h  |   4 +
> >  kernel/bpf/tnum.c     |   7 +-
> >  kernel/bpf/verifier.c | 327 ++++++++++++++++++++----------------------
> >  3 files changed, 165 insertions(+), 173 deletions(-)
> > 
> > diff --git a/include/linux/tnum.h b/include/linux/tnum.h
> > index 1c3948a1d6ad..3c13240077b8 100644
> > --- a/include/linux/tnum.h
> > +++ b/include/linux/tnum.h
> > @@ -106,6 +106,10 @@ int tnum_sbin(char *str, size_t size, struct tnum a);
> >  struct tnum tnum_subreg(struct tnum a);
> >  /* Returns the tnum with the lower 32-bit subreg cleared */
> >  struct tnum tnum_clear_subreg(struct tnum a);
> > +/* Returns the tnum with the lower 32-bit subreg in *reg* set to the lower
> > + * 32-bit subreg in *subreg*
> > + */
> > +struct tnum tnum_with_subreg(struct tnum reg, struct tnum subreg);
> >  /* Returns the tnum with the lower 32-bit subreg set to value */
> >  struct tnum tnum_const_subreg(struct tnum a, u32 value);
> >  /* Returns true if 32-bit subreg @a is a known constant*/
> > diff --git a/kernel/bpf/tnum.c b/kernel/bpf/tnum.c
> > index 3d7127f439a1..f4c91c9b27d7 100644
> > --- a/kernel/bpf/tnum.c
> > +++ b/kernel/bpf/tnum.c
> > @@ -208,7 +208,12 @@ struct tnum tnum_clear_subreg(struct tnum a)
> >  	return tnum_lshift(tnum_rshift(a, 32), 32);
> >  }
> >  
> > +struct tnum tnum_with_subreg(struct tnum reg, struct tnum subreg)
> > +{
> > +	return tnum_or(tnum_clear_subreg(reg), tnum_subreg(subreg));
> > +}
> > +
> >  struct tnum tnum_const_subreg(struct tnum a, u32 value)
> >  {
> > -	return tnum_or(tnum_clear_subreg(a), tnum_const(value));
> > +	return tnum_with_subreg(a, tnum_const(value));
> >  }
> > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > index 2197385d91dc..52934080042c 100644
> > --- a/kernel/bpf/verifier.c
> > +++ b/kernel/bpf/verifier.c
> > @@ -14379,218 +14379,211 @@ static int is_branch_taken(struct bpf_reg_state *reg1, struct bpf_reg_state *reg
> >  	return is_scalar_branch_taken(reg1, reg2, opcode, is_jmp32);
> >  }
> >  
> > -/* Adjusts the register min/max values in the case that the dst_reg and
> > - * src_reg are both SCALAR_VALUE registers (or we are simply doing a BPF_K
> > - * check, in which case we havea fake SCALAR_VALUE representing insn->imm).
> > - * Technically we can do similar adjustments for pointers to the same object,
> > - * but we don't support that right now.
> > +/* Opcode that corresponds to a *false* branch condition.
> > + * E.g., if r1 < r2, then reverse (false) condition is r1 >= r2
> >   */
> > -static void reg_set_min_max(struct bpf_reg_state *true_reg1,
> > -			    struct bpf_reg_state *true_reg2,
> > -			    struct bpf_reg_state *false_reg1,
> > -			    struct bpf_reg_state *false_reg2,
> > -			    u8 opcode, bool is_jmp32)
> > +static u8 rev_opcode(u8 opcode)
> 
> Nit: rev_opcode and flip_opcode seems like a possible source of confusing
> down the line. Flip and reverse are often interchangable, i.e. "flip the
> order" and "reverse the order" is the same thing.
> 
> Maybe "neg_opcode" or "neg_cond_opcode"?
> 
> Or do it the otherway around, keep rev_opcode but rename flip_opcode.
> 
> One more comment about BPF_JSET below
> 
> >  {
> > -	struct tnum false_32off, false_64off;
> > -	struct tnum true_32off, true_64off;
> > -	u64 uval;
> > -	u32 uval32;
> > -	s64 sval;
> > -	s32 sval32;
> > -
> > -	/* If either register is a pointer, we can't learn anything about its
> > -	 * variable offset from the compare (unless they were a pointer into
> > -	 * the same object, but we don't bother with that).
> > +	switch (opcode) {
> > +	case BPF_JEQ:		return BPF_JNE;
> > +	case BPF_JNE:		return BPF_JEQ;
> > +	/* JSET doesn't have it's reverse opcode in BPF, so add
> > +	 * BPF_X flag to denote the reverse of that operation
> >  	 */
> > -	if (false_reg1->type != SCALAR_VALUE || false_reg2->type != SCALAR_VALUE)
> > -		return;
> > -
> > -	/* we expect right-hand registers (src ones) to be constants, for now */
> > -	if (!is_reg_const(false_reg2, is_jmp32)) {
> > -		opcode = flip_opcode(opcode);
> > -		swap(true_reg1, true_reg2);
> > -		swap(false_reg1, false_reg2);
> > +	case BPF_JSET:		return BPF_JSET | BPF_X;
> > +	case BPF_JSET | BPF_X:	return BPF_JSET;
> > +	case BPF_JGE:		return BPF_JLT;
> > +	case BPF_JGT:		return BPF_JLE;
> > +	case BPF_JLE:		return BPF_JGT;
> > +	case BPF_JLT:		return BPF_JGE;
> > +	case BPF_JSGE:		return BPF_JSLT;
> > +	case BPF_JSGT:		return BPF_JSLE;
> > +	case BPF_JSLE:		return BPF_JSGT;
> > +	case BPF_JSLT:		return BPF_JSGE;
> > +	default:		return 0;
> >  	}
> > -	if (!is_reg_const(false_reg2, is_jmp32))
> > -		return;
> > +}
> >  
> > -	false_32off = tnum_subreg(false_reg1->var_off);
> > -	false_64off = false_reg1->var_off;
> > -	true_32off = tnum_subreg(true_reg1->var_off);
> > -	true_64off = true_reg1->var_off;
> > -	uval = false_reg2->var_off.value;
> > -	uval32 = (u32)tnum_subreg(false_reg2->var_off).value;
> > -	sval = (s64)uval;
> > -	sval32 = (s32)uval32;
> > +/* Refine range knowledge for <reg1> <op> <reg>2 conditional operation. */
> > +static void regs_refine_cond_op(struct bpf_reg_state *reg1, struct bpf_reg_state *reg2,
> > +				u8 opcode, bool is_jmp32)
> > +{
> > +	struct tnum t;
> >  
> >  	switch (opcode) {
> > -	/* JEQ/JNE comparison doesn't change the register equivalence.
> > -	 *
> > -	 * r1 = r2;
> > -	 * if (r1 == 42) goto label;
> > -	 * ...
> > -	 * label: // here both r1 and r2 are known to be 42.
> > -	 *
> > -	 * Hence when marking register as known preserve it's ID.
> > -	 */
> >  	case BPF_JEQ:
> >  		if (is_jmp32) {
> > -			__mark_reg32_known(true_reg1, uval32);
> > -			true_32off = tnum_subreg(true_reg1->var_off);
> > +			reg1->u32_min_value = max(reg1->u32_min_value, reg2->u32_min_value);
> > +			reg1->u32_max_value = min(reg1->u32_max_value, reg2->u32_max_value);
> > +			reg1->s32_min_value = max(reg1->s32_min_value, reg2->s32_min_value);
> > +			reg1->s32_max_value = min(reg1->s32_max_value, reg2->s32_max_value);
> > +			reg2->u32_min_value = reg1->u32_min_value;
> > +			reg2->u32_max_value = reg1->u32_max_value;
> > +			reg2->s32_min_value = reg1->s32_min_value;
> > +			reg2->s32_max_value = reg1->s32_max_value;
> > +
> > +			t = tnum_intersect(tnum_subreg(reg1->var_off), tnum_subreg(reg2->var_off));
> > +			reg1->var_off = tnum_with_subreg(reg1->var_off, t);
> > +			reg2->var_off = tnum_with_subreg(reg2->var_off, t);
> >  		} else {
> > -			___mark_reg_known(true_reg1, uval);
> > -			true_64off = true_reg1->var_off;
> > +			reg1->umin_value = max(reg1->umin_value, reg2->umin_value);
> > +			reg1->umax_value = min(reg1->umax_value, reg2->umax_value);
> > +			reg1->smin_value = max(reg1->smin_value, reg2->smin_value);
> > +			reg1->smax_value = min(reg1->smax_value, reg2->smax_value);
> > +			reg2->umin_value = reg1->umin_value;
> > +			reg2->umax_value = reg1->umax_value;
> > +			reg2->smin_value = reg1->smin_value;
> > +			reg2->smax_value = reg1->smax_value;
> > +
> > +			reg1->var_off = tnum_intersect(reg1->var_off, reg2->var_off);
> > +			reg2->var_off = reg1->var_off;
> >  		}
> >  		break;
> >  	case BPF_JNE:
> > +		/* we don't derive any new information for inequality yet */
> > +		break;
> > +	case BPF_JSET:
> > +	case BPF_JSET | BPF_X: { /* BPF_JSET and its reverse, see rev_opcode() */
> > +		u64 val;
> > +
> > +		if (!is_reg_const(reg2, is_jmp32))
> > +			swap(reg1, reg2);
> > +		if (!is_reg_const(reg2, is_jmp32))
> > +			break;
> > +
> > +		val = reg_const_value(reg2, is_jmp32);
> > +		/* BPF_JSET (i.e., TRUE branch, *not* BPF_JSET | BPF_X)
> > +		 * requires single bit to learn something useful. E.g., if we
> > +		 * know that `r1 & 0x3` is true, then which bits (0, 1, or both)
> > +		 * are actually set? We can learn something definite only if
> > +		 * it's a single-bit value to begin with.
> > +		 *
> > +		 * BPF_JSET | BPF_X (i.e., negation of BPF_JSET) doesn't have
> > +		 * this restriction. I.e., !(r1 & 0x3) means neither bit 0 nor
> > +		 * bit 1 is set, which we can readily use in adjustments.
> > +		 */
> > +		if (!(opcode & BPF_X) && !is_power_of_2(val))
> > +			break;
> > +
> >  		if (is_jmp32) {
> > -			__mark_reg32_known(false_reg1, uval32);
> > -			false_32off = tnum_subreg(false_reg1->var_off);
> > +			if (opcode & BPF_X)
> > +				t = tnum_and(tnum_subreg(reg1->var_off), tnum_const(~val));
> > +			else
> > +				t = tnum_or(tnum_subreg(reg1->var_off), tnum_const(val));
> > +			reg1->var_off = tnum_with_subreg(reg1->var_off, t);
> >  		} else {
> > -			___mark_reg_known(false_reg1, uval);
> > -			false_64off = false_reg1->var_off;
> > +			if (opcode & BPF_X)
> > +				reg1->var_off = tnum_and(reg1->var_off, tnum_const(~val));
> > +			else
> > +				reg1->var_off = tnum_or(reg1->var_off, tnum_const(val));
> >  		}
> >  		break;
> 
> Since you're already adding a tnum helper, I think we can add one more
> for BPF_JSET here
> 
> 	struct tnum tnum_neg(struct tnum a)
> 	{
> 		return TNUM(~a.value, a.mask);
> 	}

Didn't think it through well enough, with the above we might end up with a
invalid tnum because the unknown bits gets negated as well, need to mask the
unknown bits out.

 	struct tnum tnum_neg(struct tnum a)
 	{
 		return TNUM(~a.value & ~a.mask, a.mask);
 	}

> So instead of getting a value out of tnum then putting the value back
> into tnum again
> 
>     u64 val;
>     val = reg_const_value(reg2, is_jmp32);
>     tnum_ops(..., tnum_const(val or ~val);
> 
> Keep the value in tnum and process it as-is if possible
> 
>     tnum_ops(..., reg2->var_off or tnum_neg(reg2->var_off));
> 
> And with that hopefully make this fragment short enough that we don't
> mind duplicate a bit of code to seperate the BPF_JSET case from the
> BPF_JSET | BPF_X case. IMO a conditional is_power_of_2 check followed by
> two level of branching is a bit too much to follow, it is better to have
> them seperated just like how you're doing it for the others already.
> 
> I.e. something like the follow
> 
> 	case BPF_JSET: {
> 		if (!is_reg_const(reg2, is_jmp32))
> 			swap(reg1, reg2);
> 		if (!is_reg_const(reg2, is_jmp32))
> 			break;
> 		/* comment */
> 		if (!is_power_of_2(reg_const_value(reg2, is_jmp32))
> 			break;
> 
> 		if (is_jmp32) {
> 			t = tnum_or(tnum_subreg(reg1->var_off), tnum_subreg(reg2->var_off));
> 			reg1->var_off = tnum_with_subreg(reg1->var_off, t);
> 		} else {
> 			reg1->var_off = tnum_or(reg1->var_off, reg2->var_off);
> 		}
> 		break;
> 	}
> 	case BPF_JSET | BPF_X: {
> 		if (!is_reg_const(reg2, is_jmp32))
> 			swap(reg1, reg2);
> 		if (!is_reg_const(reg2, is_jmp32))
> 			break;
> 
> 		if (is_jmp32) {
> 			/* a slightly long line ... */
> 			t = tnum_and(tnum_subreg(reg1->var_off), tnum_neg(tnum_subreg(reg2->var_off)));
> 			reg1->var_off = tnum_with_subreg(reg1->var_off, t);
> 		} else {
> 			reg1->var_off = tnum_and(reg1->var_off, tnum_neg(reg2->var_off));
> 		}
> 		break;
> 	}
> 
> > ...

  reply	other threads:[~2023-11-03  8:33 UTC|newest]

Thread overview: 39+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-11-03  0:08 [PATCH bpf-next 00/13] BPF register bounds range vs range support Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 01/13] bpf: generalize reg_set_min_max() to handle non-const register comparisons Andrii Nakryiko
2023-11-03  7:52   ` Shung-Hsi Yu
2023-11-03  8:33     ` Shung-Hsi Yu [this message]
2023-11-03 20:39     ` Andrii Nakryiko
2023-11-03 20:48       ` Andrii Nakryiko
2023-11-06  2:22         ` Shung-Hsi Yu
2023-11-03 16:20   ` Eduard Zingerman
2023-11-03 20:39     ` Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 02/13] bpf: generalize is_scalar_branch_taken() logic Andrii Nakryiko
2023-11-03  0:13   ` Andrii Nakryiko
2023-11-03 16:47   ` Eduard Zingerman
2023-11-03 20:59     ` Andrii Nakryiko
2023-11-03 21:02       ` Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 03/13] bpf: enhance BPF_JEQ/BPF_JNE is_branch_taken logic Andrii Nakryiko
2023-11-03 17:28   ` Eduard Zingerman
2023-11-09  8:39   ` Shung-Hsi Yu
2023-11-03  0:08 ` [PATCH bpf-next 04/13] bpf: add register bounds sanity checks and sanitization Andrii Nakryiko
2023-11-03  2:13   ` Andrii Nakryiko
2023-11-03 17:56   ` Eduard Zingerman
2023-11-03 21:11     ` Andrii Nakryiko
2023-11-03 21:39       ` Eduard Zingerman
2023-11-09  8:30   ` Shung-Hsi Yu
2023-11-03  0:08 ` [PATCH bpf-next 05/13] bpf: remove redundant s{32,64} -> u{32,64} deduction logic Andrii Nakryiko
2023-11-03 22:16   ` Eduard Zingerman
2023-11-09  8:43   ` Shung-Hsi Yu
2023-11-03  0:08 ` [PATCH bpf-next 06/13] bpf: make __reg{32,64}_deduce_bounds logic more robust Andrii Nakryiko
2023-11-03 22:27   ` Eduard Zingerman
2023-11-09  9:02   ` Shung-Hsi Yu
2023-11-03  0:08 ` [PATCH bpf-next 07/13] selftests/bpf: BPF register range bounds tester Andrii Nakryiko
2023-11-03 19:19   ` Alexei Starovoitov
2023-11-03 21:12     ` Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 08/13] selftests/bpf: adjust OP_EQ/OP_NE handling to use subranges for branch taken Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 09/13] selftests/bpf: add range x range test to reg_bounds Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 10/13] selftests/bpf: add randomized reg_bounds tests Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 11/13] selftests/bpf: set BPF_F_TEST_SANITY_SCRIPT by default Andrii Nakryiko
2023-11-03 22:35   ` Eduard Zingerman
2023-11-03  0:08 ` [PATCH bpf-next 12/13] veristat: add ability to set BPF_F_TEST_SANITY_STRICT flag with -r flag Andrii Nakryiko
2023-11-03  0:08 ` [PATCH bpf-next 13/13] selftests/bpf: add iter test requiring range x range logic Andrii Nakryiko

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ZUSwQtfjCsKpbWcL@u94a \
    --to=shung-hsi.yu@suse.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=kernel-team@meta.com \
    --cc=martin.lau@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.