From: David Vernet <void@manifault.com>
To: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Cc: bpf@vger.kernel.org, Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andrii@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Martin KaFai Lau <martin.lau@kernel.org>,
Joanne Koong <joannelkoong@gmail.com>
Subject: Re: [PATCH bpf-next v1 3/7] bpf: Rework process_dynptr_func
Date: Thu, 17 Nov 2022 15:11:57 -0600 [thread overview]
Message-ID: <Y3ajnSVA20j3o/16@maniforge.lan> (raw)
In-Reply-To: <20221115000130.1967465-4-memxor@gmail.com>
On Tue, Nov 15, 2022 at 05:31:26AM +0530, Kumar Kartikeya Dwivedi wrote:
> Recently, user ringbuf support introduced a PTR_TO_DYNPTR register type
> for use in callback state, because in case of user ringbuf helpers,
> there is no dynptr on the stack that is passed into the callback. To
> reflect such a state, a special register type was created.
>
> However, some checks have been bypassed incorrectly during the addition
> of this feature. First, for arg_type with MEM_UNINIT flag which
> initialize a dynptr, they must be rejected for such register type.
> Secondly, in the future, there are plans to add dynptr helpers that
> operate on the dynptr itself and may change its offset and other
> properties.
>
> In all of these cases, PTR_TO_DYNPTR shouldn't be allowed to be passed
> to such helpers, however the current code simply returns 0.
>
> The rejection for helpers that release the dynptr is already handled.
>
> For fixing this, we take a step back and rework existing code in a way
> that will allow fitting in all classes of helpers and have a coherent
> model for dealing with the variety of use cases in which dynptr is used.
>
> First, for ARG_PTR_TO_DYNPTR, it can either be set alone or together
> with a DYNPTR_TYPE_* constant that denotes the only type it accepts.
>
> Next, helpers which initialize a dynptr use MEM_UNINIT to indicate this
> fact. To make the distinction clear, use MEM_RDONLY flag to indicate
> that the helper only operates on the memory pointed to by the dynptr,
> not the dynptr itself. In C parlance, it would be equivalent to taking
> the dynptr as a point to const argument.
>
> When either of these flags are not present, the helper is allowed to
> mutate both the dynptr itself and also the memory it points to.
> Currently, the read only status of the memory is not tracked in the
> dynptr, but it would be trivial to add this support inside dynptr state
> of the register.
>
> With these changes and renaming PTR_TO_DYNPTR to CONST_PTR_TO_DYNPTR to
> better reflect its usage, it can no longer be passed to helpers that
> initialize a dynptr, i.e. bpf_dynptr_from_mem, bpf_ringbuf_reserve_dynptr.
>
> A note to reviewers is that in code that does mark_stack_slots_dynptr,
> and unmark_stack_slots_dynptr, we implicitly rely on the fact that
> PTR_TO_STACK reg is the only case that can reach that code path, as one
> cannot pass CONST_PTR_TO_DYNPTR to helpers that don't set MEM_RDONLY. In
> both cases such helpers won't be setting that flag.
>
> The next patch will add a couple of selftest cases to make sure this
> doesn't break.
>
> Fixes: 205715673844 ("bpf: Add bpf_user_ringbuf_drain() helper")
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
> ---
Overall this LGTM. Left some comments / nits before stamping.
> include/linux/bpf.h | 4 +-
> include/uapi/linux/bpf.h | 8 +-
> kernel/bpf/btf.c | 7 +-
> kernel/bpf/helpers.c | 18 +-
> kernel/bpf/verifier.c | 220 +++++++++++++-----
> scripts/bpf_doc.py | 1 +
> tools/include/uapi/linux/bpf.h | 8 +-
> .../selftests/bpf/prog_tests/user_ringbuf.c | 10 +-
> 8 files changed, 195 insertions(+), 81 deletions(-)
>
> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index 798aec816970..dfe45f6caa4f 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h
> @@ -709,7 +709,7 @@ enum bpf_reg_type {
> PTR_TO_MEM, /* reg points to valid memory region */
> PTR_TO_BUF, /* reg points to a read/write buffer */
> PTR_TO_FUNC, /* reg points to a bpf program function */
> - PTR_TO_DYNPTR, /* reg points to a dynptr */
> + CONST_PTR_TO_DYNPTR, /* reg points to a const struct bpf_dynptr */
> __BPF_REG_TYPE_MAX,
>
> /* Extended reg_types. */
> @@ -2761,7 +2761,7 @@ void bpf_dynptr_init(struct bpf_dynptr_kern *ptr, void *data,
> enum bpf_dynptr_type type, u32 offset, u32 size);
> void bpf_dynptr_set_null(struct bpf_dynptr_kern *ptr);
> int bpf_dynptr_check_size(u32 size);
> -u32 bpf_dynptr_get_size(struct bpf_dynptr_kern *ptr);
> +u32 bpf_dynptr_get_size(const struct bpf_dynptr_kern *ptr);
>
> #ifdef CONFIG_BPF_LSM
> void bpf_cgroup_atype_get(u32 attach_btf_id, int cgroup_atype);
> diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
> index fb4c911d2a03..b7543efd6284 100644
> --- a/include/uapi/linux/bpf.h
> +++ b/include/uapi/linux/bpf.h
> @@ -5290,7 +5290,7 @@ union bpf_attr {
> * Return
> * Nothing. Always succeeds.
> *
> - * long bpf_dynptr_read(void *dst, u32 len, struct bpf_dynptr *src, u32 offset, u64 flags)
> + * long bpf_dynptr_read(void *dst, u32 len, const struct bpf_dynptr *src, u32 offset, u64 flags)
> * Description
> * Read *len* bytes from *src* into *dst*, starting from *offset*
> * into *src*.
> @@ -5300,7 +5300,7 @@ union bpf_attr {
> * of *src*'s data, -EINVAL if *src* is an invalid dynptr or if
> * *flags* is not 0.
> *
> - * long bpf_dynptr_write(struct bpf_dynptr *dst, u32 offset, void *src, u32 len, u64 flags)
> + * long bpf_dynptr_write(const struct bpf_dynptr *dst, u32 offset, void *src, u32 len, u64 flags)
> * Description
> * Write *len* bytes from *src* into *dst*, starting from *offset*
> * into *dst*.
> @@ -5310,7 +5310,7 @@ union bpf_attr {
> * of *dst*'s data, -EINVAL if *dst* is an invalid dynptr or if *dst*
> * is a read-only dynptr or if *flags* is not 0.
> *
> - * void *bpf_dynptr_data(struct bpf_dynptr *ptr, u32 offset, u32 len)
> + * void *bpf_dynptr_data(const struct bpf_dynptr *ptr, u32 offset, u32 len)
> * Description
> * Get a pointer to the underlying dynptr data.
> *
> @@ -5411,7 +5411,7 @@ union bpf_attr {
> * Drain samples from the specified user ring buffer, and invoke
> * the provided callback for each such sample:
> *
> - * long (\*callback_fn)(struct bpf_dynptr \*dynptr, void \*ctx);
> + * long (\*callback_fn)(const struct bpf_dynptr \*dynptr, void \*ctx);
> *
> * If **callback_fn** returns 0, the helper will continue to try
> * and drain the next sample, up to a maximum of
> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index d02ae2f4249b..ba3b50895f6b 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
> @@ -6568,14 +6568,15 @@ static int btf_check_func_arg_match(struct bpf_verifier_env *env,
> }
>
> if (arg_dynptr) {
> - if (reg->type != PTR_TO_STACK) {
> - bpf_log(log, "arg#%d pointer type %s %s not to stack\n",
> + if (reg->type != PTR_TO_STACK &&
> + reg->type != CONST_PTR_TO_DYNPTR) {
> + bpf_log(log, "arg#%d pointer type %s %s not to stack or dynptr\n",
> i, btf_type_str(ref_t),
> ref_tname);
> return -EINVAL;
> }
Should we bring this check into process_dynptr_func()? It's kind of
unfortunate that we have divergent logic between helpers and kfuncs,
when it comes to checking types. Let's at least be uniform here?
> - if (process_dynptr_func(env, regno, ARG_PTR_TO_DYNPTR, NULL))
> + if (process_dynptr_func(env, regno, ARG_PTR_TO_DYNPTR | MEM_RDONLY, NULL))
> return -EINVAL;
> continue;
> }
> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> index 283f55bbeb70..714f5c9d0c1f 100644
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c
> @@ -1398,7 +1398,7 @@ static const struct bpf_func_proto bpf_kptr_xchg_proto = {
> #define DYNPTR_SIZE_MASK 0xFFFFFF
> #define DYNPTR_RDONLY_BIT BIT(31)
>
> -static bool bpf_dynptr_is_rdonly(struct bpf_dynptr_kern *ptr)
> +static bool bpf_dynptr_is_rdonly(const struct bpf_dynptr_kern *ptr)
> {
> return ptr->size & DYNPTR_RDONLY_BIT;
> }
> @@ -1408,7 +1408,7 @@ static void bpf_dynptr_set_type(struct bpf_dynptr_kern *ptr, enum bpf_dynptr_typ
> ptr->size |= type << DYNPTR_TYPE_SHIFT;
> }
>
> -u32 bpf_dynptr_get_size(struct bpf_dynptr_kern *ptr)
> +u32 bpf_dynptr_get_size(const struct bpf_dynptr_kern *ptr)
> {
> return ptr->size & DYNPTR_SIZE_MASK;
> }
> @@ -1432,7 +1432,7 @@ void bpf_dynptr_set_null(struct bpf_dynptr_kern *ptr)
> memset(ptr, 0, sizeof(*ptr));
> }
>
> -static int bpf_dynptr_check_off_len(struct bpf_dynptr_kern *ptr, u32 offset, u32 len)
> +static int bpf_dynptr_check_off_len(const struct bpf_dynptr_kern *ptr, u32 offset, u32 len)
> {
> u32 size = bpf_dynptr_get_size(ptr);
>
> @@ -1477,7 +1477,7 @@ static const struct bpf_func_proto bpf_dynptr_from_mem_proto = {
> .arg4_type = ARG_PTR_TO_DYNPTR | DYNPTR_TYPE_LOCAL | MEM_UNINIT,
> };
>
> -BPF_CALL_5(bpf_dynptr_read, void *, dst, u32, len, struct bpf_dynptr_kern *, src,
> +BPF_CALL_5(bpf_dynptr_read, void *, dst, u32, len, const struct bpf_dynptr_kern *, src,
> u32, offset, u64, flags)
> {
> int err;
> @@ -1500,12 +1500,12 @@ static const struct bpf_func_proto bpf_dynptr_read_proto = {
> .ret_type = RET_INTEGER,
> .arg1_type = ARG_PTR_TO_UNINIT_MEM,
> .arg2_type = ARG_CONST_SIZE_OR_ZERO,
> - .arg3_type = ARG_PTR_TO_DYNPTR,
> + .arg3_type = ARG_PTR_TO_DYNPTR | MEM_RDONLY,
> .arg4_type = ARG_ANYTHING,
> .arg5_type = ARG_ANYTHING,
> };
>
> -BPF_CALL_5(bpf_dynptr_write, struct bpf_dynptr_kern *, dst, u32, offset, void *, src,
> +BPF_CALL_5(bpf_dynptr_write, const struct bpf_dynptr_kern *, dst, u32, offset, void *, src,
> u32, len, u64, flags)
> {
> int err;
> @@ -1526,14 +1526,14 @@ static const struct bpf_func_proto bpf_dynptr_write_proto = {
> .func = bpf_dynptr_write,
> .gpl_only = false,
> .ret_type = RET_INTEGER,
> - .arg1_type = ARG_PTR_TO_DYNPTR,
> + .arg1_type = ARG_PTR_TO_DYNPTR | MEM_RDONLY,
> .arg2_type = ARG_ANYTHING,
> .arg3_type = ARG_PTR_TO_MEM | MEM_RDONLY,
> .arg4_type = ARG_CONST_SIZE_OR_ZERO,
> .arg5_type = ARG_ANYTHING,
> };
>
> -BPF_CALL_3(bpf_dynptr_data, struct bpf_dynptr_kern *, ptr, u32, offset, u32, len)
> +BPF_CALL_3(bpf_dynptr_data, const struct bpf_dynptr_kern *, ptr, u32, offset, u32, len)
> {
> int err;
>
> @@ -1554,7 +1554,7 @@ static const struct bpf_func_proto bpf_dynptr_data_proto = {
> .func = bpf_dynptr_data,
> .gpl_only = false,
> .ret_type = RET_PTR_TO_DYNPTR_MEM_OR_NULL,
> - .arg1_type = ARG_PTR_TO_DYNPTR,
> + .arg1_type = ARG_PTR_TO_DYNPTR | MEM_RDONLY,
> .arg2_type = ARG_ANYTHING,
> .arg3_type = ARG_CONST_ALLOC_SIZE_OR_ZERO,
> };
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 41ef7e4b73e4..c484e632b0cd 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -565,7 +565,7 @@ static const char *reg_type_str(struct bpf_verifier_env *env,
> [PTR_TO_BUF] = "buf",
> [PTR_TO_FUNC] = "func",
> [PTR_TO_MAP_KEY] = "map_key",
> - [PTR_TO_DYNPTR] = "dynptr_ptr",
> + [CONST_PTR_TO_DYNPTR] = "dynptr",
> };
>
> if (type & PTR_MAYBE_NULL) {
> @@ -699,6 +699,27 @@ static bool dynptr_type_refcounted(enum bpf_dynptr_type type)
> return type == BPF_DYNPTR_TYPE_RINGBUF;
> }
>
> +static void __mark_dynptr_regs(struct bpf_reg_state *reg1,
> + struct bpf_reg_state *reg2,
> + enum bpf_dynptr_type type);
> +
> +static void __mark_reg_not_init(const struct bpf_verifier_env *env,
> + struct bpf_reg_state *reg);
> +
> +static void mark_dynptr_stack_regs(struct bpf_reg_state *sreg1,
> + struct bpf_reg_state *sreg2,
> + enum bpf_dynptr_type type)
> +{
> + __mark_dynptr_regs(sreg1, sreg2, type);
> +}
> +
> +static void mark_dynptr_cb_reg(struct bpf_reg_state *reg1,
> + enum bpf_dynptr_type type)
> +{
> + __mark_dynptr_regs(reg1, NULL, type);
> +}
> +
> +
> static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
> enum bpf_arg_type arg_type, int insn_idx)
> {
> @@ -720,9 +741,8 @@ static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_
> if (type == BPF_DYNPTR_TYPE_INVALID)
> return -EINVAL;
>
> - state->stack[spi].spilled_ptr.dynptr.first_slot = true;
> - state->stack[spi].spilled_ptr.dynptr.type = type;
> - state->stack[spi - 1].spilled_ptr.dynptr.type = type;
> + mark_dynptr_stack_regs(&state->stack[spi].spilled_ptr,
> + &state->stack[spi - 1].spilled_ptr, type);
>
> if (dynptr_type_refcounted(type)) {
> /* The id is used to track proper releasing */
> @@ -730,8 +750,8 @@ static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_
> if (id < 0)
> return id;
>
> - state->stack[spi].spilled_ptr.id = id;
> - state->stack[spi - 1].spilled_ptr.id = id;
> + state->stack[spi].spilled_ptr.ref_obj_id = id;
> + state->stack[spi - 1].spilled_ptr.ref_obj_id = id;
> }
>
> return 0;
> @@ -753,25 +773,23 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
> }
>
> /* 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.first_slot = false;
> - state->stack[spi].spilled_ptr.dynptr.type = 0;
> - state->stack[spi - 1].spilled_ptr.dynptr.type = 0;
> + if (dynptr_type_refcounted(state->stack[spi].spilled_ptr.dynptr.type))
> + WARN_ON_ONCE(release_reference(env, state->stack[spi].spilled_ptr.ref_obj_id));
>
> + __mark_reg_not_init(env, &state->stack[spi].spilled_ptr);
> + __mark_reg_not_init(env, &state->stack[spi - 1].spilled_ptr);
> 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;
> + int spi, i;
> +
> + if (reg->type == CONST_PTR_TO_DYNPTR)
> + return false;
>
> + spi = get_spi(reg->off);
> if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS))
> return true;
>
> @@ -787,9 +805,14 @@ static bool is_dynptr_reg_valid_uninit(struct bpf_verifier_env *env, struct bpf_
> static bool is_dynptr_reg_valid_init(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 spi;
> int i;
>
> + /* This already represents first slot of initialized bpf_dynptr */
> + if (reg->type == CONST_PTR_TO_DYNPTR)
> + return true;
> +
> + spi = get_spi(reg->off);
> if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS) ||
> !state->stack[spi].spilled_ptr.dynptr.first_slot)
> return false;
> @@ -808,15 +831,19 @@ static bool is_dynptr_type_expected(struct bpf_verifier_env *env, struct bpf_reg
> {
> struct bpf_func_state *state = func(env, reg);
> enum bpf_dynptr_type dynptr_type;
> - int spi = get_spi(reg->off);
> + int spi;
>
> /* ARG_PTR_TO_DYNPTR takes any type of dynptr */
> if (arg_type == ARG_PTR_TO_DYNPTR)
> return true;
>
> dynptr_type = arg_to_dynptr_type(arg_type);
> -
> - return state->stack[spi].spilled_ptr.dynptr.type == dynptr_type;
> + if (reg->type == CONST_PTR_TO_DYNPTR) {
> + return reg->dynptr.type == dynptr_type;
> + } else {
> + spi = get_spi(reg->off);
> + return state->stack[spi].spilled_ptr.dynptr.type == dynptr_type;
> + }
> }
>
> /* The reg state of a pointer or a bounded scalar was saved when
> @@ -1324,9 +1351,6 @@ static const int caller_saved[CALLER_SAVED_REGS] = {
> BPF_REG_0, BPF_REG_1, BPF_REG_2, BPF_REG_3, BPF_REG_4, BPF_REG_5
> };
>
> -static void __mark_reg_not_init(const struct bpf_verifier_env *env,
> - struct bpf_reg_state *reg);
> -
> /* This helper doesn't clear reg->id */
> static void ___mark_reg_known(struct bpf_reg_state *reg, u64 imm)
> {
> @@ -1389,6 +1413,26 @@ static void mark_reg_known_zero(struct bpf_verifier_env *env,
> __mark_reg_known_zero(regs + regno);
> }
>
> +static void __mark_dynptr_regs(struct bpf_reg_state *reg1,
> + struct bpf_reg_state *reg2,
> + enum bpf_dynptr_type type)
> +{
> + /* reg->type has no meaning for STACK_DYNPTR, but when we set reg for
> + * callback arguments, it does need to be CONST_PTR_TO_DYNPTR, so simply
> + * set it unconditionally as it is ignored for STACK_DYNPTR anyway.
> + */
> + __mark_reg_known_zero(reg1);
> + reg1->type = CONST_PTR_TO_DYNPTR;
> + reg1->dynptr.type = type;
> + reg1->dynptr.first_slot = true;
> + if (!reg2)
> + return;
> + __mark_reg_known_zero(reg2);
> + reg2->type = CONST_PTR_TO_DYNPTR;
> + reg2->dynptr.type = type;
> + reg2->dynptr.first_slot = false;
> +}
> +
> static void mark_ptr_not_null_reg(struct bpf_reg_state *reg)
> {
> if (base_type(reg->type) == PTR_TO_MAP_VALUE) {
> @@ -5692,20 +5736,62 @@ static int process_kptr_func(struct bpf_verifier_env *env, int regno,
> return 0;
> }
>
> +/* Implementation details:
I don't think "Implementation details" is necessary to include in the
function header (and thank you for adding this header btw).
> + *
> + * There are two register types representing a bpf_dynptr, one is PTR_TO_STACK
> + * which points to a stack slot, and the other is CONST_PTR_TO_DYNPTR.
> + *
> + * In both cases we deal with the first 8 bytes, but need to mark the next 8
> + * bytes as STACK_DYNPTR in case of PTR_TO_STACK. In case of
> + * CONST_PTR_TO_DYNPTR, we are guaranteed to get the beginning of the object.
> + *
> + * Mutability of bpf_dynptr is at two levels, one is at the level of struct
> + * bpf_dynptr itself, i.e. whether the helper is receiving a pointer to struct
> + * bpf_dynptr or pointer to const struct bpf_dynptr. In the former case, it can
> + * mutate the view of the dynptr and also possibly destroy it. In the latter
> + * case, it cannot mutate the bpf_dynptr itself but it can still mutate the
> + * memory that dynptr points to.
> + *
> + * The verifier will keep track both levels of mutation (bpf_dynptr's in
> + * reg->type and the memory's in reg->dynptr.type), but there is no support for
> + * readonly dynptr view yet, hence only the first case is tracked and checked.
> + *
> + * This is consistent with how C applies the const modifier to a struct object,
> + * where the pointer itself inside bpf_dynptr becomes const but not what it
> + * points to.
> + *
> + * Helpers which do not mutate the bpf_dynptr set MEM_RDONLY in their argument
> + * type, and declare it as 'const struct bpf_dynptr *' in their prototype.
> + */
> int process_dynptr_func(struct bpf_verifier_env *env, int regno,
> - enum bpf_arg_type arg_type,
> - struct bpf_call_arg_meta *meta)
> + enum bpf_arg_type arg_type, struct bpf_call_arg_meta *meta)
> {
> struct bpf_reg_state *regs = cur_regs(env), *reg = ®s[regno];
> int argno = regno - 1;
>
> - /* We only need to check for initialized / uninitialized helper
> - * dynptr args if the dynptr is not PTR_TO_DYNPTR, as the
> - * assumption is that if it is, that a helper function
> - * initialized the dynptr on behalf of the BPF program.
> + if ((arg_type & (MEM_UNINIT | MEM_RDONLY)) == (MEM_UNINIT | MEM_RDONLY)) {
> + verbose(env, "verifier internal error: misconfigured dynptr helper type flags\n");
> + return -EFAULT;
> + }
> +
> + /* MEM_UNINIT and MEM_RDONLY are exclusive, when applied to a
s/applied to a/applied to an
> + * ARG_PTR_TO_DYNPTR (or ARG_PTR_TO_DYNPTR | DYNPTR_TYPE_*):
> + *
> + * MEM_UNINIT - Points to memory that is an appropriate candidate for
> + * constructing a mutable bpf_dynptr object.
> + *
> + * Currently, this is only possible with PTR_TO_STACK
> + * pointing to a region of atleast 16 bytes which doesn't
s/atleast/at least
> + * contain an existing bpf_dynptr.
> + *
> + * MEM_RDONLY - Points to a initialized bpf_dynptr that will not be
> + * mutated or destroyed. However, the memory it points to
> + * may be mutated.
> + *
> + * None - Points to a initialized dynptr that can be mutated and
> + * destroyed, including mutation of the memory it points
> + * to.
> */
> - if (base_type(reg->type) == PTR_TO_DYNPTR)
> - return 0;
> if (arg_type & MEM_UNINIT) {
> if (!is_dynptr_reg_valid_uninit(env, reg)) {
> verbose(env, "Dynptr has to be an uninitialized dynptr\n");
Not your change, but this is an awkwardly phrased error message. IMO
"dynptr must be initialized" is more succinct. Feel free to ignore if
you'd like, I'm happy to submit a separate patch to change it as some
point.
> @@ -5722,6 +5808,12 @@ int process_dynptr_func(struct bpf_verifier_env *env, int regno,
>
> meta->uninit_dynptr_regno = regno;
> } else {
> + /* For the reg->type == PTR_TO_STACK case, bpf_dynptr is never const */
> + if (reg->type == CONST_PTR_TO_DYNPTR && !(arg_type & MEM_RDONLY)) {
> + verbose(env, "cannot pass pointer to const bpf_dynptr, the helper mutates it\n");
> + return -EINVAL;
> + }
> +
> if (!is_dynptr_reg_valid_init(env, reg)) {
> verbose(env,
> "Expected an initialized dynptr as arg #%d\n",
> @@ -5729,6 +5821,8 @@ int process_dynptr_func(struct bpf_verifier_env *env, int regno,
> return -EINVAL;
> }
>
> + /* Fold MEM_RDONLY, only inspect arg_type */
> + arg_type &= ~MEM_RDONLY;
This seems brittle. Can is_dynptr_type_expected() just check the base
type?
> if (!is_dynptr_type_expected(env, reg, arg_type)) {
> const char *err_extra = "";
>
> @@ -5874,7 +5968,7 @@ static const struct bpf_reg_types kptr_types = { .types = { PTR_TO_MAP_VALUE } }
> static const struct bpf_reg_types dynptr_types = {
> .types = {
> PTR_TO_STACK,
> - PTR_TO_DYNPTR | DYNPTR_TYPE_LOCAL,
> + CONST_PTR_TO_DYNPTR,
> }
> };
>
> @@ -6050,12 +6144,15 @@ int check_func_arg_reg_off(struct bpf_verifier_env *env,
> return __check_ptr_off_reg(env, reg, regno, fixed_off_ok);
> }
>
> -static u32 stack_slot_get_id(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
> +static u32 dynptr_ref_obj_id(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 spi;
>
> - return state->stack[spi].spilled_ptr.id;
> + if (reg->type == CONST_PTR_TO_DYNPTR)
> + return reg->ref_obj_id;
> + spi = get_spi(reg->off);
> + return state->stack[spi].spilled_ptr.ref_obj_id;
> }
>
> static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
> @@ -6119,11 +6216,17 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg,
> if (arg_type_is_release(arg_type)) {
> if (arg_type_is_dynptr(arg_type)) {
> struct bpf_func_state *state = func(env, reg);
> - int spi = get_spi(reg->off);
> + int spi;
>
> - if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS) ||
> - !state->stack[spi].spilled_ptr.id) {
> - verbose(env, "arg %d is an unacquired reference\n", regno);
Can we add a comment here explaining why only PTR_TO_STACK dynptrs are
expected to be released? I know we have such comments elsewhere already,
but if we're going to have logic like this which is hard-coded against
assumptions of what types of dynptrs can be used in which contexts /
helpers, I think it is important to be verbose in calling that out as
it's not obvious from the code itself why this is the case.
> + if (reg->type == PTR_TO_STACK) {
> + spi = get_spi(reg->off);
> + if (!is_spi_bounds_valid(state, spi, BPF_DYNPTR_NR_SLOTS) ||
> + !state->stack[spi].spilled_ptr.ref_obj_id) {
> + verbose(env, "arg %d is an unacquired reference\n", regno);
> + return -EINVAL;
> + }
> + } else {
> + verbose(env, "cannot release unowned const bpf_dynptr\n");
> return -EINVAL;
> }
> } else if (!reg->ref_obj_id && !register_is_null(reg)) {
> @@ -7091,11 +7194,10 @@ static int set_user_ringbuf_callback_state(struct bpf_verifier_env *env,
> {
> /* bpf_user_ringbuf_drain(struct bpf_map *map, void *callback_fn, void
> * callback_ctx, u64 flags);
> - * callback_fn(struct bpf_dynptr_t* dynptr, void *callback_ctx);
> + * callback_fn(const struct bpf_dynptr_t* dynptr, void *callback_ctx);
> */
> __mark_reg_not_init(env, &callee->regs[BPF_REG_0]);
> - callee->regs[BPF_REG_1].type = PTR_TO_DYNPTR | DYNPTR_TYPE_LOCAL;
> - __mark_reg_known_zero(&callee->regs[BPF_REG_1]);
> + mark_dynptr_cb_reg(&callee->regs[BPF_REG_1], BPF_DYNPTR_TYPE_LOCAL);
> callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
>
> /* unused */
> @@ -7474,7 +7576,13 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
>
> regs = cur_regs(env);
>
> + /* This can only be set for PTR_TO_STACK, as CONST_PTR_TO_DYNPTR cannot
> + * be reinitialized by any dynptr helper. Hence, mark_stack_slots_dynptr
> + * is safe to do directly.
> + */
> if (meta.uninit_dynptr_regno) {
> + if (WARN_ON_ONCE(regs[meta.uninit_dynptr_regno].type == CONST_PTR_TO_DYNPTR))
I think we tend to do "verifier internal error" logs for situations like
this rather than WARN_ON_ONCE(), right?
> + return -EFAULT;
> /* we write BPF_DW bits (8 bytes) at a time */
> for (i = 0; i < BPF_DYNPTR_SIZE; i += 8) {
> err = check_mem_access(env, insn_idx, meta.uninit_dynptr_regno,
> @@ -7492,15 +7600,22 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
>
> if (meta.release_regno) {
> err = -EINVAL;
> - if (arg_type_is_dynptr(fn->arg_type[meta.release_regno - BPF_REG_1]))
> + /* This can only be set for PTR_TO_STACK, as CONST_PTR_TO_DYNPTR cannot
> + * be released by any dynptr helper. Hence, unmark_stack_slots_dynptr
> + * is safe to do directly.
> + */
> + if (arg_type_is_dynptr(fn->arg_type[meta.release_regno - BPF_REG_1])) {
> + if (WARN_ON_ONCE(regs[meta.release_regno].type == CONST_PTR_TO_DYNPTR))
Same here r.e. using verifier log rather than WARN_ON_ONCE(). Also, can
we bring this comment inside the if statement to match the alignemnt of
the one you added below?
> + return -EFAULT;
> err = unmark_stack_slots_dynptr(env, ®s[meta.release_regno]);
> - else if (meta.ref_obj_id)
> + } else if (meta.ref_obj_id) {
> err = release_reference(env, meta.ref_obj_id);
> - /* meta.ref_obj_id can only be 0 if register that is meant to be
> - * released is NULL, which must be > R0.
> - */
> - else if (register_is_null(®s[meta.release_regno]))
> + } else if (register_is_null(®s[meta.release_regno])) {
> + /* meta.ref_obj_id can only be 0 if register that is meant to be
> + * released is NULL, which must be > R0.
> + */
> err = 0;
> + }
> if (err) {
> verbose(env, "func %s#%d reference has not been acquired before\n",
> func_id_name(func_id), func_id);
> @@ -7574,11 +7689,8 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
> return -EFAULT;
> }
>
> - if (base_type(reg->type) != PTR_TO_DYNPTR)
> - /* Find the id of the dynptr we're
> - * tracking the reference of
> - */
> - meta.ref_obj_id = stack_slot_get_id(env, reg);
> + /* Find the id of the dynptr we're tracking the reference of */
Not your change, but IMO this comment does not add any value.
> + meta.ref_obj_id = dynptr_ref_obj_id(env, reg);
> break;
> }
> }
> diff --git a/scripts/bpf_doc.py b/scripts/bpf_doc.py
> index fdb0aff8cb5a..e8d90829f23e 100755
> --- a/scripts/bpf_doc.py
> +++ b/scripts/bpf_doc.py
> @@ -752,6 +752,7 @@ class PrinterHelpers(Printer):
> 'struct bpf_timer',
> 'struct mptcp_sock',
> 'struct bpf_dynptr',
> + 'const struct bpf_dynptr',
> 'struct iphdr',
> 'struct ipv6hdr',
> }
> diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h
> index fb4c911d2a03..b7543efd6284 100644
> --- a/tools/include/uapi/linux/bpf.h
> +++ b/tools/include/uapi/linux/bpf.h
> @@ -5290,7 +5290,7 @@ union bpf_attr {
> * Return
> * Nothing. Always succeeds.
> *
> - * long bpf_dynptr_read(void *dst, u32 len, struct bpf_dynptr *src, u32 offset, u64 flags)
> + * long bpf_dynptr_read(void *dst, u32 len, const struct bpf_dynptr *src, u32 offset, u64 flags)
> * Description
> * Read *len* bytes from *src* into *dst*, starting from *offset*
> * into *src*.
> @@ -5300,7 +5300,7 @@ union bpf_attr {
> * of *src*'s data, -EINVAL if *src* is an invalid dynptr or if
> * *flags* is not 0.
> *
> - * long bpf_dynptr_write(struct bpf_dynptr *dst, u32 offset, void *src, u32 len, u64 flags)
> + * long bpf_dynptr_write(const struct bpf_dynptr *dst, u32 offset, void *src, u32 len, u64 flags)
> * Description
> * Write *len* bytes from *src* into *dst*, starting from *offset*
> * into *dst*.
> @@ -5310,7 +5310,7 @@ union bpf_attr {
> * of *dst*'s data, -EINVAL if *dst* is an invalid dynptr or if *dst*
> * is a read-only dynptr or if *flags* is not 0.
> *
> - * void *bpf_dynptr_data(struct bpf_dynptr *ptr, u32 offset, u32 len)
> + * void *bpf_dynptr_data(const struct bpf_dynptr *ptr, u32 offset, u32 len)
> * Description
> * Get a pointer to the underlying dynptr data.
> *
> @@ -5411,7 +5411,7 @@ union bpf_attr {
> * Drain samples from the specified user ring buffer, and invoke
> * the provided callback for each such sample:
> *
> - * long (\*callback_fn)(struct bpf_dynptr \*dynptr, void \*ctx);
> + * long (\*callback_fn)(const struct bpf_dynptr \*dynptr, void \*ctx);
> *
> * If **callback_fn** returns 0, the helper will continue to try
> * and drain the next sample, up to a maximum of
> diff --git a/tools/testing/selftests/bpf/prog_tests/user_ringbuf.c b/tools/testing/selftests/bpf/prog_tests/user_ringbuf.c
> index 02b18d018b36..39882580cb90 100644
> --- a/tools/testing/selftests/bpf/prog_tests/user_ringbuf.c
> +++ b/tools/testing/selftests/bpf/prog_tests/user_ringbuf.c
> @@ -668,13 +668,13 @@ static struct {
> const char *expected_err_msg;
> } failure_tests[] = {
> /* failure cases */
> - {"user_ringbuf_callback_bad_access1", "negative offset dynptr_ptr ptr"},
> - {"user_ringbuf_callback_bad_access2", "dereference of modified dynptr_ptr ptr"},
> - {"user_ringbuf_callback_write_forbidden", "invalid mem access 'dynptr_ptr'"},
> + {"user_ringbuf_callback_bad_access1", "negative offset dynptr ptr"},
> + {"user_ringbuf_callback_bad_access2", "dereference of modified dynptr ptr"},
> + {"user_ringbuf_callback_write_forbidden", "invalid mem access 'dynptr'"},
> {"user_ringbuf_callback_null_context_write", "invalid mem access 'scalar'"},
> {"user_ringbuf_callback_null_context_read", "invalid mem access 'scalar'"},
> - {"user_ringbuf_callback_discard_dynptr", "arg 1 is an unacquired reference"},
> - {"user_ringbuf_callback_submit_dynptr", "arg 1 is an unacquired reference"},
> + {"user_ringbuf_callback_discard_dynptr", "cannot release unowned const bpf_dynptr"},
> + {"user_ringbuf_callback_submit_dynptr", "cannot release unowned const bpf_dynptr"},
> {"user_ringbuf_callback_invalid_return", "At callback return the register R0 has value"},
> };
>
> --
> 2.38.1
>
next prev parent reply other threads:[~2022-11-17 21:12 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2022-11-15 0:01 [PATCH bpf-next v1 0/7] Dynptr refactorings Kumar Kartikeya Dwivedi
2022-11-15 0:01 ` [PATCH bpf-next v1 1/7] bpf: Refactor ARG_PTR_TO_DYNPTR checks into process_dynptr_func Kumar Kartikeya Dwivedi
2022-11-15 4:15 ` Joanne Koong
2022-11-15 0:01 ` [PATCH bpf-next v1 2/7] bpf: Propagate errors from process_* checks in check_func_arg Kumar Kartikeya Dwivedi
2022-11-15 3:53 ` Joanne Koong
2022-11-15 0:01 ` [PATCH bpf-next v1 3/7] bpf: Rework process_dynptr_func Kumar Kartikeya Dwivedi
2022-11-16 18:04 ` Joanne Koong
2022-11-17 21:11 ` David Vernet [this message]
2022-11-20 18:06 ` Kumar Kartikeya Dwivedi
2022-11-20 18:16 ` David Vernet
2022-11-15 0:01 ` [PATCH bpf-next v1 4/7] bpf: Rework check_func_arg_reg_off Kumar Kartikeya Dwivedi
2022-11-15 18:24 ` Joanne Koong
2022-11-17 23:42 ` David Vernet
2022-11-20 18:41 ` Kumar Kartikeya Dwivedi
2022-11-21 5:39 ` David Vernet
2022-11-15 0:01 ` [PATCH bpf-next v1 5/7] bpf: Move PTR_TO_STACK alignment check to process_dynptr_func Kumar Kartikeya Dwivedi
2022-11-15 18:29 ` Joanne Koong
2022-11-17 23:49 ` David Vernet
2022-11-20 19:10 ` Kumar Kartikeya Dwivedi
2022-11-20 19:40 ` Alexei Starovoitov
2022-11-20 21:02 ` Kumar Kartikeya Dwivedi
2022-11-21 7:27 ` David Vernet
2022-12-07 20:41 ` Kumar Kartikeya Dwivedi
2022-11-15 0:01 ` [PATCH bpf-next v1 6/7] bpf: Use memmove for bpf_dynptr_{read,write} Kumar Kartikeya Dwivedi
2022-11-17 23:51 ` David Vernet
2022-11-15 0:01 ` [PATCH bpf-next v1 7/7] selftests/bpf: Add test for dynptr reinit in user_ringbuf callback Kumar Kartikeya Dwivedi
2022-11-15 18:36 ` Joanne Koong
2022-11-15 19:41 ` Kumar Kartikeya Dwivedi
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=Y3ajnSVA20j3o/16@maniforge.lan \
--to=void@manifault.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=joannelkoong@gmail.com \
--cc=martin.lau@kernel.org \
--cc=memxor@gmail.com \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox