Re: [bpf-next v3 1/2] bpf: Offload kptr destructors that run from NMI
"Alexei Starovoitov" <[email protected]> Mon, 11 May 2026 08:51:53 -0700
| Newsgroups | dev.linux.lists.sashiko,org.kernel.vger.bpf |
|---|---|
| Message-ID | <[email protected]> |
On Sun May 10, 2026 at 6:49 PM PDT, Justin Suess wrote: > On Sun, May 10, 2026 at 03:38:08PM -0700, Alexei Starovoitov wrote: >> On Sun, May 10, 2026 at 8:14 AM Justin Suess <[email protected]> wrote: >> > >> > >> > Any help or guidance on this would be appreciated! >> >> sorry for the delay. Everyone was at lsfmmbpf for a week+. >> > > No worries! I hope it was an enjoyable trip and I look forward to > hearing about the conference. > >> All of the solutions so far are way too complicated. >> bpf_kptr_xchg() has to remain inlined as single atomic xchg >> without slowpath otherwise it ruins the concept >> and makes usage unpredictable. >> >> Let's step back. >> What is the issue you're trying to solve? >> >> the commit log say: >> >> > A BPF program attached to tp_btf/nmi_handler can delete map entries or >> > swap out referenced kptrs from NMI context. Today that runs the kptr >> > destructor inline. Destructors such as bpf_cpumask_release() can take >> > RCU-related locks, so running them from NMI can deadlock the system. >> >> and looking at selftest from patch 2 you do: >> >> old = bpf_kptr_xchg(&value->mask, old); >> if (old) >> bpf_cpumask_release(old); >> >> so? >> bpf_cpumask_release() is fine to call from any context, >> because bpf_mem_cache_free_rcu() is safe everywhere including NMI. >> > > My mistake on that. I picked a bad example for the test, but the test is > just exercising the nmi dtor path, and doesn't rely on the particular > type of kptr. I just picked one that was easy to acquire a reference to. > > This dtor is safe. task_struct dtor, cgroup dtor, crypto_ctx dtor are > not. I've annotated why here: > > crypto_ctx: > > __bpf_kfunc void bpf_crypto_ctx_release(struct bpf_crypto_ctx *ctx) > { > if (refcount_dec_and_test(&ctx->usage)) > call_rcu(&ctx->rcu, crypto_free_cb); /* UNSAFE: call_rcu */ > } > > __bpf_kfunc void bpf_crypto_ctx_release_dtor(void *ctx) > { > bpf_crypto_ctx_release(ctx); > } bpf_crypto_ctx_release() is only allowed in syscall prog types and dtor via hashmap free will execute in safe context as well. So not an issue. > task_struct: > > __bpf_kfunc void bpf_task_release(struct task_struct *p) > { > put_task_struct_rcu_user(p); > } > > __bpf_kfunc void bpf_task_release_dtor(void *p) > { > put_task_struct_rcu_user(p); > } > > void put_task_struct_rcu_user(struct task_struct *task) > { > if (refcount_dec_and_test(&task->rcu_users)) > call_rcu(&task->rcu, delayed_put_task_struct); /* UNSAFE: call_rcu > */ > } In theory. I don't think there is a reproducer. > cgroup_release_dtor is more complex, goes through ultimately through > callbacks to: > > static void css_release(struct percpu_ref *ref) > { > struct cgroup_subsys_state *css = > container_of(ref, struct cgroup_subsys_state, refcnt); > > INIT_WORK(&css->destroy_work, css_release_work_fn); > queue_work(cgroup_release_wq, &css->destroy_work); /* UNSAFE: > workqueue */ > } similar to task_struct. I don't think it's exploitable. > More generally, unless it's a BPF allocated object or doesn't rely on > locking/call_rcu or workqueues, the dtor is unsafe. > >> hashtab introduced dtor in bpf_mem_alloc, >> so bpf_obj_free_fields() and corresponding dtor's of kptr-s >> are called from valid context. >> >> What is the problematic sequence? > > So from the beginning stepping back. > > The problematic sequence: > > 1. ref kptr (i.e task_struct, cgroup, crypto_ctx) xchg'd into map. > > 2. in a tp_btf/nmi_handler (NMI CTX) program we drop the item from the map > with that referenced kptr field. > > 3. dtor runs in the nmi context > > 4. dtor runs call_rcu/queue_work/some bad thing in nmi, causing deadlock. > > You can see this demonstrated in my task_struct reproducer [1]. did you? That link points to your v2 with cpumask. I don't recall seeing task_struct repro. > It causes a deadlock by deliberately releasing the last reference to a > task_struct via a ref kptr in nmi, getting it to call_rcu and deadlock. > > The typical solution to this is to run the nmi unsafe code in non-nmi > context by offloading to NMI work, as you proposed. > > The problem is we need space to for the jobs we enqueue. The required > information to run the dtor is the dtor function and the original object > pointer. The number of dtors that can run in a single tp_btf/nmi_handler > prog is nearly unbounded. > > The other problem is even though bpf_mem_alloc is safe in NMI generally, > we cannot allocate in path that destroys an object. If the allocation > fails due to memory pressure, we leak the object. > > There are a few options, all with drawbacks. > > 1. Dynamically allocate the job. Non-starter, failing to allocate is > unrecoverable, memory pressure means we can't ever schedule the dtor to > run. > > 2. Store job ntrusively in the object : Requires a safe place to place > it within the object. Bad because not all objects have a space we can write to. > Non-starter. > > 3. Within the map slot (after actual kptr): Taken with my initial approach in v1. > Significant complexity and requires per-map changes. Feasible but very > complex and would need DCAS or locking to make updating the map slot and > our job information atomic. > > 4. Wrapping the kptr in a box and storing it in place of the kptr [2] : > Proposed by Mykyta. Would break direct load access to kptr objects. > > 5. Make every dtor nmi safe individually. This would require a lot of > duplicated code and require updating every destructor invididually. > Feasible technically, but seems brittle. > > 6. One that would be the least complex, would be forbidding xchg operations > that can run the dtor in NMI context. That would preserve the inlining fix, > but limit our usage of referenced kptrs in BPF programs that run in NMI context. > > The approach here: > > 7. Allocate a new spot for a free job every time we xchg into the map > and put it in a global list. When in NMI and we run a dtor, we pop a > job from that slot and use it to offload our work via irq_work. If > we're not in NMI we run normally. Downside is this breaks inlining for > ref kptrs. > > ... > > I may be missing something critical, but everything I've looked at > points to this problem being much more complex that it initially seemed. yes. it is complex. all 7 options are not good. I recall the whole thing started with desire to add bpf_put_file_dtor(). This was discussed with VFS maintainers and they didn't like the idea, since it needs a ton of work to make it safe: . umount notifier to make sure stashed file doesn't hold umount . potential circular refcnt issue if file to bpf map stashed into the same map . scm_rights-like facility with garbage collection So generic file stash is really no go.