Re: [PATCH bpf-next v3 2/6] bpf: Add verifier support for dynptrs and implement malloc dynptrs

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Thu, Apr 28, 2022 at 2:12 PM Joanne Koong <joannelkoong@xxxxxxxxx> wrote:
>
> This patch adds the bulk of the verifier work for supporting dynamic
> pointers (dynptrs) in bpf. This patch implements malloc-type dynptrs
> through 2 new APIs (bpf_dynptr_alloc and bpf_dynptr_put) that can be
> called by a bpf program. Malloc-type dynptrs are dynptrs that dynamically
> allocate memory on behalf of the program.
>
> A bpf_dynptr is opaque to the bpf program. It is a 16-byte structure
> defined internally as:
>
> struct bpf_dynptr_kern {
>     void *data;
>     u32 size;
>     u32 offset;
> } __aligned(8);
>
> The upper 8 bits of *size* is reserved (it contains extra metadata about
> read-only status and dynptr type); consequently, a dynptr only supports
> memory less than 16 MB.
>
> The 2 new APIs for malloc-type dynptrs are:
>
> long bpf_dynptr_alloc(u32 size, u64 flags, struct bpf_dynptr *ptr);
> void bpf_dynptr_put(struct bpf_dynptr *ptr);
>
> Please note that there *must* be a corresponding bpf_dynptr_put for
> every bpf_dynptr_alloc (even if the alloc fails). This is enforced
> by the verifier.
>
> In the verifier, dynptr state information will be tracked in stack
> slots. When the program passes in an uninitialized dynptr
> (ARG_PTR_TO_DYNPTR | MEM_UNINIT), the stack slots corresponding
> to the frame pointer where the dynptr resides at are marked STACK_DYNPTR.
>
> For helper functions that take in initialized dynptrs (eg
> bpf_dynptr_read + bpf_dynptr_write which are added later in this
> patchset), the verifier enforces that the dynptr has been initialized
> properly by checking that their corresponding stack slots have been marked
> as STACK_DYNPTR. Dynptr release functions (eg bpf_dynptr_put) will clear the
> stack slots. The verifier enforces at program exit that there are no
> referenced dynptrs that haven't been released.
>
> Signed-off-by: Joanne Koong <joannelkoong@xxxxxxxxx>
> ---
>  include/linux/bpf.h            |  60 ++++++++-
>  include/linux/bpf_verifier.h   |  21 +++
>  include/uapi/linux/bpf.h       |  30 +++++
>  kernel/bpf/helpers.c           |  75 +++++++++++
>  kernel/bpf/verifier.c          | 225 ++++++++++++++++++++++++++++++++-
>  scripts/bpf_doc.py             |   2 +
>  tools/include/uapi/linux/bpf.h |  30 +++++
>  7 files changed, 440 insertions(+), 3 deletions(-)
>

[...]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 4565684839f1..16b7ea54a7e0 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -187,6 +187,11 @@ struct bpf_verifier_stack_elem {
>                                           POISON_POINTER_DELTA))
>  #define BPF_MAP_PTR(X)         ((struct bpf_map *)((X) & ~BPF_MAP_PTR_UNPRIV))
>
> +/* forward declarations */

it's kind of obvious from C syntax, this comment doesn't really add value

> +static bool arg_type_is_mem_size(enum bpf_arg_type type);
> +static int acquire_reference_state(struct bpf_verifier_env *env, int insn_idx);
> +static int release_reference(struct bpf_verifier_env *env, int ref_obj_id);
> +

[...]

>  static struct bpf_func_state *func(struct bpf_verifier_env *env,
>                                    const struct bpf_reg_state *reg)
>  {
> @@ -646,6 +672,134 @@ static void mark_verifier_state_scratched(struct bpf_verifier_env *env)
>         env->scratched_stack_slots = ~0ULL;
>  }
>
> +#define DYNPTR_TYPE_FLAG_MASK          DYNPTR_TYPE_MALLOC

can this be put near where DYNPTR_TYPE_MALLOC is defined? It's quite
easy to forget to update this if it's somewhere far away

> +
> +static int arg_to_dynptr_type(enum bpf_arg_type arg_type)
> +{
> +       switch (arg_type & DYNPTR_TYPE_FLAG_MASK) {
> +       case DYNPTR_TYPE_MALLOC:
> +               return BPF_DYNPTR_TYPE_MALLOC;
> +       default:
> +               return BPF_DYNPTR_TYPE_INVALID;
> +       }
> +}
> +

[...]

> +static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
> +{
> +       struct bpf_func_state *state = func(env, reg);
> +       int spi, i;
> +
> +       spi = get_spi(reg->off);
> +
> +       if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS))
> +               return -EINVAL;
> +
> +       for (i = 0; i < BPF_REG_SIZE; i++) {
> +               state->stack[spi].slot_type[i] = STACK_INVALID;
> +               state->stack[spi - 1].slot_type[i] = STACK_INVALID;
> +       }
> +
> +       /* Invalidate any slices associated with this dynptr */
> +       if (dynptr_type_refcounted(state->stack[spi].spilled_ptr.dynptr.type)) {
> +               release_reference(env, state->stack[spi].spilled_ptr.id);
> +               state->stack[spi].spilled_ptr.id = 0;
> +               state->stack[spi - 1].spilled_ptr.id = 0;
> +       }
> +
> +       state->stack[spi].spilled_ptr.dynptr.type = 0;
> +       state->stack[spi - 1].spilled_ptr.dynptr.type = 0;
> +
> +       state->stack[spi].spilled_ptr.dynptr.first_slot = false;

nit: given you marked slots as STACK_INVALID, we shouldn't even look
into spilled_ptr.dynptr, so this zeroing out isn't necessary, right?

> +
> +       return 0;
> +}
> +
> +static bool is_dynptr_reg_valid_uninit(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
> +{
> +       struct bpf_func_state *state = func(env, reg);
> +       int spi = get_spi(reg->off);
> +       int i;
> +
> +       if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS))
> +               return true;

hm... if spi bounds are invalid, shouldn't that be an error?...

> +
> +       for (i = 0; i < BPF_REG_SIZE; i++) {
> +               if (state->stack[spi].slot_type[i] == STACK_DYNPTR ||
> +                   state->stack[spi - 1].slot_type[i] == STACK_DYNPTR)
> +                       return false;
> +       }
> +
> +       return true;
> +}
> +
> +static bool is_dynptr_reg_valid_init(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
> +                                    enum bpf_arg_type arg_type)
> +{
> +       struct bpf_func_state *state = func(env, reg);
> +       int spi = get_spi(reg->off);
> +       int i;
> +
> +       if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS) ||
> +           !state->stack[spi].spilled_ptr.dynptr.first_slot)
> +               return false;
> +
> +       for (i = 0; i < BPF_REG_SIZE; i++) {
> +           if (state->stack[spi].slot_type[i] != STACK_DYNPTR ||
> +               state->stack[spi - 1].slot_type[i] != STACK_DYNPTR)
> +                   return false;
> +       }

minor, but seems like it's pretty common operation to set or check all
those "microslots" to STACK_DYNPTR, would two small helpers be useful
for this (e.g., is_stack_dynptr() and set_stack_dynptr() or something
along those lines)?

> +
> +       /* ARG_PTR_TO_DYNPTR takes any type of dynptr */
> +       if (arg_type == ARG_PTR_TO_DYNPTR)
> +               return true;
> +
> +       return state->stack[spi].spilled_ptr.dynptr.type == arg_to_dynptr_type(arg_type);
> +}
> +

[...]

> @@ -5837,6 +6011,35 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
>                 bool zero_size_allowed = (arg_type == ARG_CONST_SIZE_OR_ZERO);
>
>                 err = check_mem_size_reg(env, reg, regno, zero_size_allowed, meta);
> +       } else if (arg_type_is_dynptr(arg_type)) {
> +               /* Can't pass in a dynptr at a weird offset */
> +               if (reg->off % BPF_REG_SIZE) {
> +                       verbose(env, "cannot pass in non-zero dynptr offset\n");
> +                       return -EINVAL;
> +               }
> +
> +               if (arg_type & MEM_UNINIT)  {
> +                       if (!is_dynptr_reg_valid_uninit(env, reg)) {
> +                               verbose(env, "Arg #%d dynptr has to be an uninitialized dynptr\n",
> +                                       arg + BPF_REG_1);
> +                               return -EINVAL;
> +                       }
> +
> +                       meta->uninit_dynptr_regno = arg + BPF_REG_1;

do we need a check that meta->uninit_dynptr_regno isn't already set?
I.e., prevent two uninit dynptr in a helper?

> +               } else if (!is_dynptr_reg_valid_init(env, reg, arg_type)) {
> +                       const char *err_extra = "";
> +
> +                       switch (arg_type & DYNPTR_TYPE_FLAG_MASK) {
> +                       case DYNPTR_TYPE_MALLOC:
> +                               err_extra = "malloc ";
> +                               break;
> +                       default:
> +                               break;
> +                       }
> +                       verbose(env, "Expected an initialized %sdynptr as arg #%d\n",
> +                               err_extra, arg + BPF_REG_1);
> +                       return -EINVAL;
> +               }
>         } else if (arg_type_is_alloc_size(arg_type)) {
>                 if (!tnum_is_const(reg->var_off)) {
>                         verbose(env, "R%d is not a known constant'\n",

[...]



[Index of Archives]     [Linux Samsung SoC]     [Linux Rockchip SoC]     [Linux Actions SoC]     [Linux for Synopsys ARC Processors]     [Linux NFS]     [Linux NILFS]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]


  Powered by Linux