Re: [PATCH v5 bpf-next 17/23] bpf: generalize reg_set_min_max() to handle two sets of two registers

Andrii Nakryiko
2023-10-31T17:50:45Z

On Tue, Oct 31, 2023 at 9:24 AM Alexei Starovoitov
<alexei.starovoitov@gmail.com> wrote:
>
> On Mon, Oct 30, 2023 at 11:03 PM Andrii Nakryiko
> <andrii.nakryiko@gmail.com> wrote:
> >
> > On Mon, Oct 30, 2023 at 7:02 PM Alexei Starovoitov
> > <alexei.starovoitov@gmail.com> wrote:
> > >
> > > On Fri, Oct 27, 2023 at 11:13:40AM -0700, Andrii Nakryiko wrote:
> > > > static void reg_set_min_max(struct bpf_reg_state *true_reg1,
> > > > + struct bpf_reg_state *true_reg2,
> > > > struct bpf_reg_state *false_reg1,
> > > > - u64 val, u32 val32,
> > > > + struct bpf_reg_state *false_reg2,
> > > > u8 opcode, bool is_jmp32)
> > > > {
> > > > - struct tnum false_32off = tnum_subreg(false_reg1->var_off);
> > > > - struct tnum false_64off = false_reg1->var_off;
> > > > - struct tnum true_32off = tnum_subreg(true_reg1->var_off);
> > > > - struct tnum true_64off = true_reg1->var_off;
> > > > - s64 sval = (s64)val;
> > > > - s32 sval32 = (s32)val32;
> > > > -
> > > > - /* If the dst_reg is a pointer, we can't learn anything about its
> > > > - * variable offset from the compare (unless src_reg were a pointer into
> > > > - * the same object, but we don't bother with that.
> > > > - * Since false_reg1 and true_reg1 have the same type by construction, we
> > > > - * only need to check one of them for pointerness.
> > > > + struct tnum false_32off, false_64off;
> > > > + struct tnum true_32off, true_64off;
> > > > + u64 val;
> > > > + u32 val32;
> > > > + 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).
> > > > */
> > > > - if (__is_pointer_value(false, false_reg1))
> > >
> > > The removal of the above check, but not the comment was surprising and concerning,
> > > so I did a bit of git-archaeology.
> > > It was added in commit f1174f77b50c ("bpf/verifier: rework value tracking")
> > > back in 2017 !
> > > and in that commit reg_set_min_max() was always called with reg == scalar.
> > > It looked like premature check. Then I spotted a comment in that commit:
> > > * this is only legit if both are scalars (or pointers to the same
> > > * object, I suppose, but we don't support that right now), because
> > > * otherwise the different base pointers mean the offsets aren't
> > > * comparable.
> > > so the intent back then was to generalize reg_set_min_max() to be used with pointers too,
> > > but we never got around to do that and the comment now reads:
> >
> > Yeah, it shouldn't be too hard to "generalize" to pointer vs pointer,
> > if we ensure they point to exactly the same thing (I haven't thought
> > much about how), because beyond that it's still basically SCALAR
> > offsets. But I figured it's out of scope for these changes :)
> >
> > > * this is only legit if both are scalars (or pointers to the same
> > > * object, I suppose, see the PTR_MAYBE_NULL related if block below),
> > > * because otherwise the different base pointers mean the offsets aren't
> > > * comparable.
> > >
> > > So please remove is_pointer check and remove the comment,
> >
> > So I'm a bit confused. I did remove __is_pointer_value() check, but I
> > still need to guard against having pointers, which is why I have:
> >
> > if (false_reg1->type != SCALAR_VALUE || false_reg2->type != SCALAR_VALUE).
> > return;
> >
> > I think I need this check, because reg_set_min_max() can be called
> > from check_cond_jmp_op() with pointer regs, and we shouldn't try to
> > adjust them. Or am I missing something? And the comment I have here
> > now:
>
> I don't see a code path where reg_set_min_max() is called
> with pointers. At least not in the current code base.
> Are you saying somewhere in your later patch it happens?
>

Hm.. no, it's all in this patch. Check check_cond_jmp_op(). We at
least allow `(reg_is_pkt_pointer_any(dst_reg) &&
reg_is_pkt_pointer_any(src_reg)` case to get into is_branch_taken(),
which, btw, does handle pointer x pointer, pointer x scalar, and
scalar x scalar cases. Then, we go straight to reg_set_min_max(), both
for BPF_X and BPF_K cases. So reg_set_min_max() has to guard itself
against pointers.

> Then the question is whether to do this is_scalar check inside
> reg_set_min_max() or outside. Both options are probably fine.

Given we have two separate calls to reg_set_min_max(), BPF_X and
BPF_K, it seems cleaner to do it once at the beginning of
reg_set_min_max(). And if in the future we do support pointer
variants, I'd handle them inside reg_set_min_max(), just like
is_branch_taken() handles different situations in one place
transparently to the caller.

>
> >
> > + /* 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).
> > */
> >
> > is trying to explain that we don't really adjust two pointers.
> >
> > > and fixup the comment in check_cond_jmp_op() where reg_set_min_max().
> >
> > I have this locally for now, please let me know if this is fine or you
> > had something else in mind:
> >
> > -/* Adjusts the register min/max values in the case that the dst_reg is the
> > - * variable register that we are working on, and src_reg is a constant or we're
> > - * simply doing a BPF_K check.
> > - * In JEQ/JNE cases we also adjust the var_off values.
> > +/* 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.
>
> Looks fine. I'm trying to say that we had such comment forever and
> it never lead to actually doing the work.
> So I'd just remove the last sentence about pointers ...

Ah, ok, yep, sure.

>
> > */
> > static void reg_set_min_max(struct bpf_reg_state *true_reg1,
> > struct bpf_reg_state *true_reg2,
> > @@ -14884,13 +14885,6 @@ static int check_cond_jmp_op(struct
> > bpf_verifier_env *env,
> > return -EFAULT;
> > other_branch_regs = other_branch->frame[other_branch->curframe]->regs;
> >
> > - /* detect if we are comparing against a constant value so we can adjust
> > - * our min/max values for our dst register.
> > - * this is only legit if both are scalars (or pointers to the same
> > - * object, I suppose, see the PTR_MAYBE_NULL related if block below),
> > - * because otherwise the different base pointers mean the offsets aren't
> > - * comparable.
> > - */
>
> ... and removing this comment is good thing too.
> In general the comments should be in front of the function body
> (as you're doing) instead of the callsite.

yep, sounds good