Re: [bpf-next v3 1/2] bpf: Offload kptr destructors that run from NMI
Justin Suess <[email protected]> Sun, 10 May 2026 11:13:36 -0400
| Newsgroups | dev.linux.lists.sashiko,org.kernel.vger.bpf |
|---|---|
| Message-ID | <agCXEJKJh-JGMhjG@zenbox> |
On Thu, May 07, 2026 at 11:45:19PM +0000, [email protected] wrote: > Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: > - [High] TOCTOU race in `bpf_dtor_kptr_release_one()` can lead to NMI deadlocks. > - [Low] Memory leak on initialization failure in `bpf_dtor_kptr_init()`. > -- > > commit 488cdc390b90427a591d47211600c09addc95fcd > Author: Justin Suess <[email protected]> > Date: Thu May 7 17:54:52 2026 +0000 > > bpf: Offload kptr destructors that run from NMI > > This commit queues destructor-backed kptr teardowns to irq_work when > running from NMI context. It maintains an idle-slot surplus counter to > ensure offload slots are preallocated to prevent deadlocks. > > Closes: https://lore.kernel.org/bpf/[email protected]/ > > > diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c > > index 3b1f0ba02f613..d34fdb99eb8a9 100644 > > --- a/kernel/bpf/syscall.c > > +++ b/kernel/bpf/syscall.c > [ ... ] > > @@ -65,6 +69,111 @@ static DEFINE_SPINLOCK(map_idr_lock); > > +static void bpf_dtor_kptr_release_one(void) > > +{ > > + struct bpf_dtor_kptr_work *job; > > + long surplus; > > + > > + for (;;) { > > + surplus = atomic_long_read(&bpf_dtor_kptr_surplus); > > + if (surplus <= 0) > > + return; > > + > > + job = bpf_dtor_kptr_pop_idle(); > > Could this sequence lead to a race condition that drains the idle pool > too early? > > If multiple threads read a positive surplus value here, they might all call > bpf_dtor_kptr_pop_idle() before the cmpxchg executes. This could temporarily > drain the idle pool below the required number of reserved slots. > > If an NMI fires during this window and attempts to tear down a kptr, would > bpf_dtor_kptr_offload() find the idle pool empty? If so, it looks like it > would trigger the fallback warning and run the destructor inline from the > NMI context, which might reintroduce the deadlocks this patch aims to > prevent. > Yes this is real. But there's a bigger problem: the push side of pcpu_freelist is not interruptible. If we jump to NMI during this stage then the pcpu_freelist will be locked up because we were pushing to it, thus preventing us from popping. Thus we just deadlock again. I think we technically have exclusivity here on our percpu head (we can't be in NMI and process context stimultaniously on the cpu). So maybe it is safe to forcibly acquire that specific lock? I'm not 100% sure. llist won't work since we can't have multiple llist_del_first consumers, and llist_del_all would render the list null and unusable if we jump to it from NMI while we're borrowing it. Making percpu llists won't work either, since the cpu we allocate the job one might not be the one the dtor runs on, so we need percpu stealing, which then runs into the same multiple consumers problem if threads steal from same cpu head. So this code is not ready either way. I think 128 bit cmpxchg could technically solve this problem, but don't think we can rely on it for all architectures BPF supports. If anyone has any guidance on this it would be much appreciated. Been stuck on this for several weeks... Construct I need is a data structure with atomic push, pop, and move between sets. No specific ordering needed, but updates from producers must be immediately visible to all consumers. I've looked into Treiber stacks. Any help or guidance on this would be appreciated! > > + if (!job) > > + return; > > + > > + if (!atomic_long_try_cmpxchg(&bpf_dtor_kptr_surplus, &surplus, > > + surplus - 1)) { > > + pcpu_freelist_push(&bpf_dtor_kptr_idle, &job->fnode); > > + continue; > > + } > > + > > + bpf_mem_free(&bpf_global_ma, job); > > + return; > > + } > > +} > [ ... ] > > +static int __init bpf_dtor_kptr_init(void) > > +{ > > + int err; > > + > > + err = pcpu_freelist_init(&bpf_dtor_kptr_idle); > > + if (err) > > + return err; > > + > > + err = pcpu_freelist_init(&bpf_dtor_kptr_jobs); > > + if (err) > > + return err; > > Does this error path leak the per-cpu memory allocated for the idle list? > > If the second pcpu_freelist_init() fails, the function returns immediately. > Should there be a cleanup path here to free the already initialized > bpf_dtor_kptr_idle list? > > > + > > + return 0; > > +} > > +late_initcall(bpf_dtor_kptr_init); > > -- > Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1