Re: [PATCH v2 bpf-next 02/13] bpf: Migrate release_on_unlock logic to non-owning ref semantics

Dave Marchevsky
2022-12-17T09:22:05Z

On 12/17/22 3:24 AM, Dave Marchevsky wrote:
> This patch introduces non-owning reference semantics to the verifier,
> specifically linked_list API kfunc handling. release_on_unlock logic for
> refs is refactored - with small functional changes - to implement these
> semantics, and bpf_list_push_{front,back} are migrated to use them.
>
> When a list node is pushed to a list, the program still has a pointer to
> the node:
>
> n = bpf_obj_new(typeof(*n));
>
> bpf_spin_lock(&l);
> bpf_list_push_back(&l, n);
> /* n still points to the just-added node */
> bpf_spin_unlock(&l);
>
> What the verifier considers n to be after the push, and thus what can be
> done with n, are changed by this patch.
>
> Common properties both before/after this patch:
> * After push, n is only a valid reference to the node until end of
> critical section
> * After push, n cannot be pushed to any list
> * After push, the program can read the node's fields using n
>
> Before:
> * After push, n retains the ref_obj_id which it received on
> bpf_obj_new, but the associated bpf_reference_state's
> release_on_unlock field is set to true
> * release_on_unlock field and associated logic is used to implement
> "n is only a valid ref until end of critical section"
> * After push, n cannot be written to, the node must be removed from
> the list before writing to its fields
> * After push, n is marked PTR_UNTRUSTED
>
> After:
> * After push, n's ref is released and ref_obj_id set to 0. The
> bpf_reg_state's non_owning_ref_lock struct is populated with the
> currently active lock
> * non_owning_ref_lock and logic is used to implement "n is only a
> valid ref until end of critical section"
> * n can be written to (except for special fields e.g. bpf_list_node,
> timer, ...)
> * No special type flag is added to n after push
>
> Summary of specific implementation changes to achieve the above:
>
> * release_on_unlock field, ref_set_release_on_unlock helper, and logic
> to "release on unlock" based on that field are removed
>
> * The anonymous active_lock struct used by bpf_verifier_state is
> pulled out into a named struct bpf_active_lock.
>
> * A non_owning_ref_lock field of type bpf_active_lock is added to
> bpf_reg_state's PTR_TO_BTF_ID union
>
> * Helpers are added to use non_owning_ref_lock to implement non-owning
> ref semantics as described above
> * invalidate_non_owning_refs - helper to clobber all non-owning refs
> matching a particular bpf_active_lock identity. Replaces
> release_on_unlock logic in process_spin_lock.
> * ref_set_non_owning_lock - set non_owning_ref_lock for a reg based
> on current verifier state
> * ref_convert_owning_non_owning - convert owning reference w/
> specified ref_obj_id to non-owning references. Setup
> non_owning_ref_lock for each reg with that ref_obj_id and 0 out
> its ref_obj_id
>
> * New KF_RELEASE_NON_OWN flag is added, to be used in conjunction with
> KF_RELEASE to indicate that the release arg reg should be converted
> to non-owning ref
> * Plain KF_RELEASE would clobber all regs with ref_obj_id matching
> the release arg reg's. KF_RELEASE_NON_OWN's logic triggers first -
> doing ref_convert_owning_non_owning on the ref first, which
> prevents the regs from being clobbered by 0ing out their
> ref_obj_ids. The bpf_reference_state itself is still released via
> release_reference as a result of the KF_RELEASE flag.
> * KF_RELEASE | KF_RELEASE_NON_OWN are added to
> bpf_list_push_{front,back}
>
> After these changes, linked_list's "release on unlock" logic continues
> to function as before, except for the semantic differences noted above.
> The patch immediately following this one makes minor changes to
> linked_list selftests to account for the differing behavior.
>
> Signed-off-by: Dave Marchevsky <davemarchevsky@fb.com>
> ---
> diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> index 53d175cbaa02..cb417ffbbb84 100644
> --- a/include/linux/bpf_verifier.h
> +++ b/include/linux/bpf_verifier.h
> @@ -43,6 +43,22 @@ enum bpf_reg_liveness {
> REG_LIVE_DONE = 0x8, /* liveness won't be updating this register anymore */
> };

[...]

> struct bpf_reg_state {
> /* Ordering of fields matters. See states_equal() */
> enum bpf_reg_type type;
> @@ -68,6 +84,7 @@ struct bpf_reg_state {
> struct {
> struct btf *btf;
> u32 btf_id;
> + struct bpf_active_lock non_owning_ref_lock;
> };
>

I think it's possible for this to be a pointer by just pointing to
struct bpf_verifier_state's active_lock. Why?

* There can currently only be one active_lock at a time
* non-owning refs are only valid in the critical section

So if a verifier_state has an active_lock, any non-owning ref must've been
obtained under that lock, and any non-owning ref not obtained under that
lock must have been invalidated previously.

This will keep bpf_reg_state size down. Will give it a shot for v3,
wanted to leave it in current state for v2 so logic in this patch
is easier to reason about.

Actually, if above logic is correct, then only valid states for
non_owning_ref_lock are "empty / null" and "same as current verifier_state",
in which case this can go back to being a bool. But for non-spin_unlock
invalidation points (e.g. rbtree_remove), we may want to keep additional
info around to avoid invalidating everything, which would require
re-introducing a non_owning_ref identity.

> u32 mem_size; /* for PTR_TO_MEM | PTR_TO_MEM_OR_NULL */
> @@ -223,11 +240,6 @@ struct bpf_reference_state {
> * exiting a callback function.
> */
> int callback_ref;
> - /* Mark the reference state to release the registers sharing the same id
> - * on bpf_spin_unlock (for nodes that we will lose ownership to but are
> - * safe to access inside the critical section).
> - */
> - bool release_on_unlock;
> };
>
> /* state of the program:
> @@ -328,21 +340,8 @@ struct bpf_verifier_state {
> u32 branches;
> u32 insn_idx;
> u32 curframe;
> - /* For every reg representing a map value or allocated object pointer,
> - * we consider the tuple of (ptr, id) for them to be unique in verifier
> - * context and conside them to not alias each other for the purposes of
> - * tracking lock state.
> - */
> - struct {
> - /* This can either be reg->map_ptr or reg->btf. If ptr is NULL,
> - * there's no active lock held, and other fields have no
> - * meaning. If non-NULL, it indicates that a lock is held and
> - * id member has the reg->id of the register which can be >= 0.
> - */
> - void *ptr;
> - /* This will be reg->id */
> - u32 id;
> - } active_lock;
> +
> + struct bpf_active_lock active_lock;
> bool speculative;
> bool active_rcu_lock; …