> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c
[ ... ]
> +__bpf_kfunc struct bpf_list_node *bpf_list_del(struct bpf_list_node *node)
> +{
> + struct bpf_list_node_kern *knode = (struct bpf_list_node_kern *)node;
> +
> + if (unlikely(!knode))
> + return NULL;
> +
> + if (WARN_ON_ONCE(!READ_ONCE(knode->owner)))
> + return NULL;
> +
> + list_del_init(&knode->list_head);
> + WRITE_ONCE(knode->owner, NULL);
> +
> + return node;
> +}
Unlike bpf_list_pop_front/bpf_list_pop_back which take a
bpf_list_head argument, bpf_list_del takes only a bpf_list_node.
The list_head argument is what triggers the verifier's lock check
in __process_kf_arg_ptr_to_graph_root() via
check_reg_allocation_locked().
Since bpf_list_del has no list_head argument, the verifier path
goes through __process_kf_arg_ptr_to_graph_node() which has no
lock check. Can a BPF program call bpf_list_del without holding
the bpf_spin_lock?
Consider the following sequence:
bpf_spin_lock(&obj->lock);
ref = bpf_list_front(&obj->head);
owned = bpf_refcount_acquire(ref);
bpf_spin_unlock(&obj->lock);
/* owned is PTR_TO_BTF_ID | MEM_ALLOC, still valid */
bpf_list_del(&owned->node); /* no lock held */
bpf_refcount_acquire returns an owning reference that survives
lock release. The verifier allows passing the node field of this
owning reference to bpf_list_del without any lock enforcement.
If another CPU concurrently operates on the same list under the
lock, the unprotected list_del_init here would race with those
operations and could corrupt the list.
For reference, bpf_rbtree_remove avoids this by taking both
root and node arguments, where the root triggers the lock check:
kernel/bpf/verifier.c:__process_kf_arg_ptr_to_graph_root() {
...
if (check_reg_allocation_locked(env, reg)) {
verbose(env, "bpf_spin_lock at off=%d must be held ...\n",
rec->spin_lock_off, head_type_name);
return -EINVAL;
}
...
}
Would it make sense to add a similar lock check in
__process_kf_arg_ptr_to_graph_node() for kfuncs that only
take a node argument?
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -13089,6 +13093,9 @@ __process_kf_arg_ptr_to_graph_node(struct
> bpf_verifier_env *env,
> return -EINVAL;
> }
>
> + if (!*node_field)
> + *node_field = field;
> +
> field = *node_field;
>
> et = btf_type_by_id(field->graph_root.btf,
> field->graph_root.value_btf_id);
This is where the node_field is set for bpf_list_del. For push
operations, *node_field was already set by the list_head argument
processing in __process_kf_arg_ptr_to_graph_root (which includes
the lock check). For bpf_list_del, *node_field starts as NULL and
is set here from the node's own field, bypassing the root path
entirely and with it the lock enforcement.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/22391130139
AI-authorship-score: medium
AI-authorship-explanation: Formulaic commit message style and brute-force
linear scan suggest possible AI generation, but coherent cross-file BPF
verifier integration shows domain understanding.
issues-found: 1
issue-severity-score: high
issue-severity-explanation: Missing bpf_spin_lock enforcement in the verifier
for bpf_list_del allows concurrent unprotected list_del_init, which can corrupt
the linked list and crash the kernel.