Re: [RFC bpf-next v1 2/8] bpf: no_caller_saved_registers attribute for helper calls

Andrii Nakryiko
2024-07-02T21:09:46Z

On Tue, Jul 2, 2024 at 1:38 PM Eduard Zingerman <eddyz87@gmail.com> wrote:
>
> On Mon, 2024-07-01 at 17:41 -0700, Andrii Nakryiko wrote:
>
> [...]
>

[...]

> > > +static bool verifier_inlines_helper_call(struct bpf_verifier_env *env, s32 imm)
> > > +{
> >
> > note that there is now also bpf_jit_inlines_helper_call()
>
> There is.
> I'd like authors of jit inline patches to explicitly opt-in
> for nocsr, vouching that inline patch follows nocsr contract.
> The idea is that people would extend call_csr_mask() as necessary.
>

you are defining a general framework with these changes, though, so
let's introduce a standard and simple way to do this. Say, in addition
to having arch-specific bpf_jit_inlines_helper_call() we can have
bpf_jit_supports_helper_nocsr() or something. And they should be
defined next to each other, so whenever one changes it's easier to
remember to change the other one.

I don't think requiring arm64 contributors to change the code of
call_csr_mask() is the right approach.

> >
> >
> > > + return false;
> > > +}
> > > +
> > > +/* If 'insn' is a call that follows no_caller_saved_registers contract
> > > + * and called function is inlined by current jit, return a mask with
> > > + * 1s corresponding to registers that are scratched by this call
> > > + * (depends on return type and number of return parameters).
> > > + * Otherwise return ALL_CALLER_SAVED_REGS mask.
> > > + */
> > > +static u32 call_csr_mask(struct bpf_verifier_env *env, struct bpf_insn *insn)
> > > +{
> > > + const struct bpf_func_proto *fn;
> > > +
> > > + if (bpf_helper_call(insn) &&
> > > + verifier_inlines_helper_call(env, insn->imm) &&
> >
> > strictly speaking, does nocsr have anything to do with inlining,
> > though? E.g., if we know for sure (however, that's a separate issue)
> > that helper implementation doesn't touch extra registers, why do we
> > need inlining to make use of nocsr?
>
> Technically, alternative for nocsr is for C version of the
> helper/kfunc itself has no_caller_saved_registers attribute.
> Grep shows a single function annotated as such in kernel tree:
> stackleak_track_stack().
> Or, maybe, for helpers/kfuncs implemented in assembly.

Yes, I suppose it's too dangerous to rely on the compiler to not use
some extra register. I guess worst case we can "inline" helper by
keeping call to it intact :)

> Whenever such helpers/kfuncs are added, this function could be extended.
>
> >
> > > + get_helper_proto(env, insn->imm, &fn) == 0 &&
> > > + fn->nocsr)
> > > + return ~get_helper_reg_mask(fn);
> > > +
> > > + return ALL_CALLER_SAVED_REGS;
> > > +}
> > > +
>
> [...]
>

[...]

> > > + if (i == 1)
> > > + return 0;
> > > + if (mark) {
> > > + s = find_containing_subprog(env, t);
> > > + /* can't happen */
> > > + if (WARN_ON_ONCE(s < 0))
> > > + return 0;
> > > + subprog = &env->subprog_info[s];
> > > + subprog->nocsr_stack_off = min(subprog->nocsr_stack_off, off);
> > > + }
> >
> > why not split pattern detection and all this other marking logic? You
> > can return "the size of csr pattern", meaning how many spills/fills
> > are there surrounding the call, no? Then all the marking can be done
> > (if necessary) by the caller.
>
> I'll try recording pattern size for function call,
> so no split would be necessary.
>

ack

> > The question is what to do with zero patter (no spills/fills for nocsr
> > call, is that valid case?)
>
> Zero pattern -- no spills/fills to remove, so nothing to do.
> I will add a test case, but it should be handled by current implementation fine.

yep, thanks

>
> [...]
>
> > > +/* Remove unnecessary spill/fill pairs, members of nocsr pattern.
> > > + * Do this as a separate pass to avoid interfering with helper/kfunc
> > > + * inlining logic in do_misc_fixups().
> > > + * See comment for match_and_mark_nocsr_pattern().
> > > + */
> > > +static int remove_nocsr_spills_fills(struct bpf_verifier_env *env)
> > > +{
> > > + struct bpf_subprog_info *subprogs = env->subprog_info;
> > > + int i, j, spills_num, cur_subprog = 0;
> > > + struct bpf_insn *insn = env->prog->insnsi;
> > > + int insn_cnt = env->prog->len;
> > > +
> > > + for (i = 0; i < insn_cnt; i++, insn++) {
> > > + spills_num = match_nocsr_pattern(env, i);
> >
> > we can probably afford a single-byte field somewhere in
> > bpf_insn_aux_data to remember "csr pattern size" instead of just a
> > true/false fact that it is nocsr call. And so we wouldn't need to do
> > pattern matching again here, we'll just have all the data.
>
> Makes sense.
>
> …