Re: [PATCH bpf-next v2] bpf: Fix mmap_lock leak in irq_work path

From: Andrii Nakryiko

Date: Thu Jul 30 2026 - 20:04:36 EST


On Wed, Jul 29, 2026 at 10:49 PM Sanghyun Park
<sanghyun.park.cnu@xxxxxxxxx> wrote:
>
> stack_map_get_build_id_offset() introduced a per-CPU irq_work to defer
> mmap_read_unlock() from NMI context, and bpf_find_vma() later reused the
> same mmap_unlock_work. Both callers only check whether the work is busy
> before taking mmap_lock, so a nested caller can reuse the slot before the
> first caller queues it. Two read locks may then be acquired while only one
> deferred unlock runs, leaking a read lock and blocking exit_mmap().
>
> Reserve the per-CPU slot before mmap_read_trylock(). Use the same wrapper
> in stackmap and bpf_find_vma() so both callers release the reservation on
> trylock failure. Release it after the irq_work callback unlocks the mm.
>
> Fixes: bae77c5eb5b2 ("bpf: enable stackmap with build_id in nmi context")
> Reported-by: syzbot+cdd6c0925e12b0af60cc@xxxxxxxxxxxxxxxxxxxxxxxxx
> Closes: https://syzkaller.appspot.com/bug?extid=cdd6c0925e12b0af60cc
> Reported-by: sashiko-bot@xxxxxxxxxx
> Closes: https://lore.kernel.org/r/20260630033745.B80201F000E9@xxxxxxxxxxxxxxx
> Tested-by: Sun Jian <sun.jian.kdev@xxxxxxxxx>
> Reviewed-by: Puranjay Mohan <puranjay@xxxxxxxxxx>
> Acked-by: Ihor Solodrai <ihor.solodrai@xxxxxxxxx>
> Signed-off-by: Sanghyun Park <sanghyun.park.cnu@xxxxxxxxx>
> ---
> v2:
> - Drop irq_work_is_busy() and rely exclusively on active, as suggested by Ihor.
>
> v1: https://lore.kernel.org/bpf/20260722023004.1497923-2-sanghyun.park.cnu@xxxxxxxxx/
>
> kernel/bpf/mmap_unlock_work.h | 37 +++++++++++++++++++++++++++++++----
> kernel/bpf/stackmap.c | 3 +--
> kernel/bpf/task_iter.c | 7 +++----
> 3 files changed, 37 insertions(+), 10 deletions(-)
>
> diff --git a/kernel/bpf/mmap_unlock_work.h b/kernel/bpf/mmap_unlock_work.h
> index 5d18d7d85bef9..d6e2b65f6d052 100644
> --- a/kernel/bpf/mmap_unlock_work.h
> +++ b/kernel/bpf/mmap_unlock_work.h
> @@ -4,12 +4,14 @@
>
> #ifndef __MMAP_UNLOCK_WORK_H__
> #define __MMAP_UNLOCK_WORK_H__
> +#include <linux/atomic.h>
> #include <linux/irq_work.h>
>
> /* irq_work to run mmap_read_unlock() in irq_work */
> struct mmap_unlock_irq_work {
> struct irq_work irq_work;
> struct mm_struct *mm;
> + atomic_t active;
> };
>
> DECLARE_PER_CPU(struct mmap_unlock_irq_work, mmap_unlock_work);
> @@ -18,8 +20,8 @@ DECLARE_PER_CPU(struct mmap_unlock_irq_work, mmap_unlock_work);
> * We cannot do mmap_read_unlock() when the irq is disabled, because of
> * risk to deadlock with rq_lock. To look up vma when the irqs are
> * disabled, we need to run mmap_read_unlock() in irq_work. We use a
> - * percpu variable to do the irq_work. If the irq_work is already used
> - * by another lookup, we fall over.
> + * percpu variable to do the irq_work. The active flag reserves the slot
> + * before mmap_read_trylock() and until the irq_work callback consumes mm.
> */
> static inline bool bpf_mmap_unlock_get_irq_work(struct mmap_unlock_irq_work **work_ptr)
> {
> @@ -29,9 +31,9 @@ static inline bool bpf_mmap_unlock_get_irq_work(struct mmap_unlock_irq_work **wo

tbh, bpf_mmap_unlock_get_irq_work() name and its semantics of
returning true if we can't really do anything seems convoluted and
backwards.

maybe we should make bpf_mmap_unlock_get_irq_work() return `struct
mmap_unlock_irq_work *` which would be ERR_PTR(), and return
ERR_PTR(-EBUSY) if (active || irq_work_is_busy) (see below)? I'd
probably call it bpf_mmap_unlock_guard_get(), though. And then we will
have bpf_mmap_unlock_guard_put() (instead of
bpf_mmap_unlock_put_irq_work) with the same logic.

and so in code, we'll check

work = bpf_mmap_unlock_guard_get();
if (IS_ERR(work)) return /* womp womp */

....

bpf_mmap_unlock_guard_put();


thoughts?

> if (irqs_disabled()) {
> if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
> work = this_cpu_ptr(&mmap_unlock_work);
> - if (irq_work_is_busy(&work->irq_work)) {
> - /* cannot queue more up_read, fallback */
> + if (atomic_cmpxchg_acquire(&work->active, 0, 1)) {
> irq_work_busy = true;
> + work = NULL;
> }
> } else {
> /*

[...]

> @@ -1191,6 +1188,8 @@ static void do_mmap_read_unlock(struct irq_work *entry)
>
> work = container_of(entry, struct mmap_unlock_irq_work, irq_work);
> mmap_read_unlock_non_owner(work->mm);
> + work->mm = NULL;
> + bpf_mmap_unlock_put_irq_work(work);

we drop active to zero here, but irq_work_is_busy() can still return
true, I think we do need to check both

> }
>
> static int __init task_iter_init(void)
> --
> 2.48.1