From: Michal Hocko <mhocko@suse.com>
To: "shaikh.kamal" <shaikhkamal2012@gmail.com>
Cc: Lorenzo Stoakes <ljs@kernel.org>,
"Liam R. Howlett" <liam@infradead.org>,
Vlastimil Babka <vbabka@kernel.org>,
Mike Rapoport <rppt@kernel.org>,
Suren Baghdasaryan <surenb@google.com>,
David Rientjes <rientjes@google.com>,
Shakeel Butt <shakeel.butt@linux.dev>,
Paolo Bonzini <pbonzini@redhat.com>,
linux-mm@kvack.org, linux-kernel@vger.kernel.org,
kvm@vger.kernel.org, linux-rt-devel@lists.linux.dev,
seanjc@google.com,
syzbot+c3178b6b512446632bac@syzkaller.appspotmail.com,
kernel test robot <lkp@intel.com>
Subject: Re: [PATCH v3] mm/mmu_notifier: Add async OOM cleanup via call_srcu()
Date: Mon, 27 Jul 2026 10:01:11 +0200 [thread overview]
Message-ID: <amcQRxA3WlyWIRho@tiehlicka> (raw)
In-Reply-To: <20260722140803.11421-1-shaikhkamal2012@gmail.com>
On Wed 22-07-26 19:38:03, shaikh.kamal wrote:
> When an mm undergoes OOM kill, the OOM reaper unmaps memory while
> holding the mmap_lock in a non-blocking context set up by
> mmu_notifier_invalidate_range_start_nonblock(). MMU notifier
> subscribers (notably KVM) acquire sleeping locks in their
> invalidate callbacks, which deadlocks on PREEMPT_RT where
> spinlock_t is a sleeping rt_mutex:
>
> BUG: sleeping function called from invalid context at
> kernel/locking/spinlock_rt.c:48
> in_atomic(): 0, irqs_disabled(): 0, non_block: 1, pid: 40,
> name: oom_reaper
> Call Trace:
> rt_spin_lock
> kvm_mmu_notifier_invalidate_range_start
> __mmu_notifier_invalidate_range_start
> zap_vma_for_reaping
> __oom_reap_task_mm
zap_vma_for_reaping does rely on the notifier to be non blocking.
mmu_notifier_invalidate_range_start_nonblock. Why
kvm_mmu_notifier_invalidate_range_start cannot use raw spinlock instead?
> Implement the asynchronous cleanup design proposed by Paolo
> Bonzini in v1 review: a new optional after_oom_unregister
> callback in struct mmu_notifier_ops, invoked after the SRCU grace
> period via call_srcu() so that no readers can still reference the
> subscription when cleanup runs.
>
> The flow is:
>
> 1. The OOM reaper calls mmu_notifier_oom_enter() from
> __oom_reap_task_mm(), before the non-blocking VMA zap loop.
> 2. mmu_notifier_oom_enter() walks the subscription list and, for
> each subscriber that provides after_oom_unregister, detaches
> the subscription from the active list and schedules a
> call_srcu() callback. The reaper's subsequent invalidations
> therefore never invoke the subscriber's callbacks.
> 3. The deferred callback invokes after_oom_unregister once the
> grace period has elapsed and all in-flight readers have
> finished.
> 4. Subsystems waiting to free structures referenced by the
> callback can call the new mmu_notifier_barrier() helper, which
> wraps srcu_barrier() to wait for all outstanding callbacks
> scheduled this way.
It is not really clear how/why this is safe wrt. __zap_vma_range. It was
my understanding that notifiers _have_ to run before ptes are freed.
[...]
> If the GFP_ATOMIC allocation in mmu_notifier_oom_enter() fails,
> the after_oom_unregister callback for that subscription is
> skipped rather than retried, since retrying could sleep and
> reintroduce the deadlock this patch fixes. The subscription is
> still cleaned up later via the normal unregister path.
Relying on allocation from effectivelly OOM callback is a bad idea.
This will only work for constrained OOM contexts and not reliably
even for those.
> Fixes: 52ac8b358b0c ("KVM: Block memslot updates across range_start() and range_end()")
> Reported-by: syzbot+c3178b6b512446632bac@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=c3178b6b512446632bac
> Suggested-by: Paolo Bonzini <pbonzini@redhat.com>
> Link: https://lore.kernel.org/all/CABgObfZQM0Eq1=vzm812D+CAcjOaE1f1QAUqGo5rTzXgLnR9cQ@mail.gmail.com/
> Reported-by: kernel test robot <lkp@intel.com>
> Closes: https://lore.kernel.org/oe-kbuild-all/202605031109.uxckW5L3-lkp@intel.com/
>
> Signed-off-by: shaikh.kamal <shaikhkamal2012@gmail.com>
> ---
> Changes in v3:
> - Rebase onto v7.2-rc3; v2 was based on stale v7.0 which the
> kernel test robot could not apply to current trees
> - Add missing static inline stub for mmu_notifier_oom_enter() in
> the !CONFIG_MMU_NOTIFIER section of include/linux/mmu_notifier.h,
> fixing allnoconfig build failures reported by the kernel test
> robot
> - Use kmalloc_obj() per current allocation idiom
> - Add Fixes: tag identifying the commit that introduced
> mn_invalidate_lock
>
> Changes in v2:
> - Complete redesign per Paolo's v1 review: moved from a
> KVM-internal locking change to a new mm/mmu_notifier
> after_oom_unregister callback with call_srcu() async cleanup
> (hence the subject prefix change from KVM: to mm/)
> - Add mmu_notifier_barrier() (srcu_barrier wrapper) for teardown
> synchronization in kvm_destroy_vm()
> - Move call site to __oom_reap_task_mm(); use hlist_del_init() to
> keep hlist_unhashed() correct and avoid use-after-free on the
> stack-allocated oom_list head
>
> v2: https://lore.kernel.org/all/20260429222548.25475-1-shaikhkamal2012@gmail.com/
> v1: https://lore.kernel.org/all/20260209161527.31978-1-shaikhkamal2012@gmail.com/
>
> include/linux/mmu_notifier.h | 14 ++++
> mm/mmu_notifier.c | 122 +++++++++++++++++++++++++++++++++++
> mm/oom_kill.c | 3 +
> virt/kvm/kvm_main.c | 27 +++++++-
> 4 files changed, 165 insertions(+), 1 deletion(-)
>
> diff --git a/include/linux/mmu_notifier.h b/include/linux/mmu_notifier.h
> index a11a44eef521..a1820a487854 100644
> --- a/include/linux/mmu_notifier.h
> +++ b/include/linux/mmu_notifier.h
> @@ -88,6 +88,14 @@ struct mmu_notifier_ops {
> void (*release)(struct mmu_notifier *subscription,
> struct mm_struct *mm);
>
> + /*
> + * Any mmu notifier that defines this is automatically unregistered
> + * when its mm is the subject of an OOM kill. after_oom_unregister()
> + * is invoked after all other outstanding callbacks have terminated.
> + */
> + void (*after_oom_unregister)(struct mmu_notifier *subscription,
> + struct mm_struct *mm);
> +
> /*
> * clear_flush_young is called after the VM is
> * test-and-clearing the young/accessed bitflag in the
> @@ -424,6 +432,8 @@ bool __mmu_notifier_clear_young(struct mm_struct *mm,
> unsigned long start, unsigned long end);
> bool __mmu_notifier_test_young(struct mm_struct *mm,
> unsigned long address);
> +void mmu_notifier_oom_enter(struct mm_struct *mm);
> +void mmu_notifier_barrier(void);
> extern int __mmu_notifier_invalidate_range_start(struct mmu_notifier_range *r);
> extern void __mmu_notifier_invalidate_range_end(struct mmu_notifier_range *r);
> extern void __mmu_notifier_arch_invalidate_secondary_tlbs(struct mm_struct *mm,
> @@ -643,6 +653,10 @@ static inline void mmu_notifier_synchronize(void)
> {
> }
>
> +static inline void mmu_notifier_oom_enter(struct mm_struct *mm)
> +{
> +}
> +
> #endif /* CONFIG_MMU_NOTIFIER */
>
> #endif /* _LINUX_MMU_NOTIFIER_H */
> diff --git a/mm/mmu_notifier.c b/mm/mmu_notifier.c
> index 245b74f39f91..f30bf95f17c7 100644
> --- a/mm/mmu_notifier.c
> +++ b/mm/mmu_notifier.c
> @@ -49,6 +49,37 @@ struct mmu_notifier_subscriptions {
> struct hlist_head deferred_list;
> };
>
> +/*
> + * Callback structure for asynchronous OOM cleanup.
> + * Used with call_srcu() to defer after_oom_unregister callbacks
> + * until after SRCU grace period completes.
> + */
> +struct mmu_notifier_oom_callback {
> + struct rcu_head rcu;
> + struct mmu_notifier *subscription;
> + struct mm_struct *mm;
> +};
> +
> +/*
> + * Callback function invoked after SRCU grace period.
> + * Safely calls after_oom_unregister once all readers have finished.
> + */
> +static void mmu_notifier_oom_callback_fn(struct rcu_head *rcu)
> +{
> + struct mmu_notifier_oom_callback *cb =
> + container_of(rcu, struct mmu_notifier_oom_callback, rcu);
> +
> + /* Safe - all SRCU readers have finished */
> + cb->subscription->ops->after_oom_unregister(cb->subscription, cb->mm);
> +
> + /* Release mm reference taken when callback was scheduled */
> + WARN_ON_ONCE(atomic_read(&cb->mm->mm_count) <= 0);
> + mmdrop(cb->mm);
> +
> + /* Free callback structure */
> + kfree(cb);
> +}
> +
> /*
> * This is a collision-retry read-side/write-side 'lock', a lot like a
> * seqcount, however this allows multiple write-sides to hold it at
> @@ -385,6 +416,84 @@ void __mmu_notifier_release(struct mm_struct *mm)
> mn_hlist_release(subscriptions, mm);
> }
>
> +void mmu_notifier_oom_enter(struct mm_struct *mm)
> +{
> + struct mmu_notifier_subscriptions *subscriptions =
> + mm->notifier_subscriptions;
> + struct mmu_notifier *subscription;
> + struct hlist_node *tmp;
> + HLIST_HEAD(oom_list);
> + int id;
> +
> + if (!subscriptions)
> + return;
> +
> + id = srcu_read_lock(&srcu);
> +
> + /*
> + * Prevent further calls to the MMU notifier, except for
> + * release and after_oom_unregister.
> + */
> + spin_lock(&subscriptions->lock);
> + hlist_for_each_entry_safe(subscription, tmp,
> + &subscriptions->list, hlist) {
> + if (!subscription->ops->after_oom_unregister)
> + continue;
> +
> + /*
> + * after_oom_unregister and alloc_notifier are incompatible,
> + * because there could be other references to allocated
> + * notifiers.
> + */
> + if (WARN_ON(subscription->ops->alloc_notifier))
> + continue;
> +
> + hlist_del_init_rcu(&subscription->hlist);
> + hlist_add_head(&subscription->hlist, &oom_list);
> + }
> + spin_unlock(&subscriptions->lock);
> + hlist_for_each_entry(subscription, &oom_list, hlist)
> + if (subscription->ops->release)
> + subscription->ops->release(subscription, mm);
> +
> + srcu_read_unlock(&srcu, id);
> +
> + if (hlist_empty(&oom_list))
> + return;
> +
> + hlist_for_each_entry_safe(subscription, tmp,
> + &oom_list, hlist) {
> + struct mmu_notifier_oom_callback *cb;
> + /*
> + * Remove from stack-based oom_list and reset hlist to unhashed state.
> + * This sets subscription->hlist.pprev = NULL, so future callers of
> + * mmu_notifier_unregister() (e.g. kvm_destroy_vm) will see
> + * hlist_unhashed() == true and take the safe path, avoiding
> + * use-after-free on the stack-allocated oom_list head.
> + */
> + hlist_del_init(&subscription->hlist);
> +
> + /*
> + * GFP_ATOMIC failure is exceedingly rare. We cannot sleep
> + * here (would reintroduce the deadlock this patch fixes)
> + * and cannot call after_oom_unregister synchronously
> + * without first waiting for SRCU readers. The subscriber
> + * will not receive after_oom_unregister but cleanup will
> + * eventually happen via the unregister path.
> + */
> + cb = kmalloc_obj(*cb, GFP_ATOMIC);
> + if (!cb)
> + continue;
> +
> + cb->subscription = subscription;
> + cb->mm = mm;
> + mmgrab(mm);
> +
> + /* Schedule callback - returns immediately */
> + call_srcu(&srcu, &cb->rcu, mmu_notifier_oom_callback_fn);
> + }
> +}
> +
> /*
> * If no young bitflag is supported by the hardware, ->clear_flush_young can
> * unmap the address and return 1 or 0 depending if the mapping previously
> @@ -1144,3 +1253,16 @@ void mmu_notifier_synchronize(void)
> synchronize_srcu(&srcu);
> }
> EXPORT_SYMBOL_GPL(mmu_notifier_synchronize);
> +
> +/**
> + * mmu_notifier_barrier - Wait for all pending MMU notifier callbacks
> + *
> + * Waits for all call_srcu() callbacks scheduled by mmu_notifier_oom_enter()
> + * to complete. Used by subsystems during cleanup to prevent use-after-free
> + * when destroying structures accessed by the callbacks.
> + */
> +void mmu_notifier_barrier(void)
> +{
> + srcu_barrier(&srcu);
> +}
> +EXPORT_SYMBOL_GPL(mmu_notifier_barrier);
> diff --git a/mm/oom_kill.c b/mm/oom_kill.c
> index 5f372f6e26fa..66adcd03f36a 100644
> --- a/mm/oom_kill.c
> +++ b/mm/oom_kill.c
> @@ -516,6 +516,9 @@ static bool __oom_reap_task_mm(struct mm_struct *mm)
> bool ret = true;
> MA_STATE(mas, &mm->mm_mt, ULONG_MAX, ULONG_MAX);
>
> + /* Notify MMU notifiers about the OOM event */
> + mmu_notifier_oom_enter(mm);
> +
> /*
> * Tell all users of get_user/copy_from_user etc... that the content
> * is no longer stable. No barriers really needed because unmapping
> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> index e44c20c04961..79a4df8a337a 100644
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
> @@ -875,6 +875,24 @@ static void kvm_mmu_notifier_release(struct mmu_notifier *mn,
> srcu_read_unlock(&kvm->srcu, idx);
> }
>
> +static void kvm_mmu_notifier_after_oom_unregister(struct mmu_notifier *mn,
> + struct mm_struct *mm)
> +{
> + struct kvm *kvm;
> +
> + kvm = mmu_notifier_to_kvm(mn);
> +
> + /*
> + * At this point the unregister has completed and all other callbacks
> + * have terminated. Clean up any unbalanced invalidation counts.
> + */
> + WARN_ON(rcuwait_active(&kvm->mn_memslots_update_rcuwait));
> + if (kvm->mn_active_invalidate_count)
> + kvm->mn_active_invalidate_count = 0;
> + else
> + WARN_ON(kvm->mmu_invalidate_in_progress);
> +}
> +
> static const struct mmu_notifier_ops kvm_mmu_notifier_ops = {
> .invalidate_range_start = kvm_mmu_notifier_invalidate_range_start,
> .invalidate_range_end = kvm_mmu_notifier_invalidate_range_end,
> @@ -882,6 +900,7 @@ static const struct mmu_notifier_ops kvm_mmu_notifier_ops = {
> .clear_young = kvm_mmu_notifier_clear_young,
> .test_young = kvm_mmu_notifier_test_young,
> .release = kvm_mmu_notifier_release,
> + .after_oom_unregister = kvm_mmu_notifier_after_oom_unregister,
> };
>
> static int kvm_init_mmu_notifier(struct kvm *kvm)
> @@ -1273,7 +1292,13 @@ static void kvm_destroy_vm(struct kvm *kvm)
> kvm->buses[i] = NULL;
> }
> kvm_coalesced_mmio_free(kvm);
> - mmu_notifier_unregister(&kvm->mmu_notifier, kvm->mm);
> + if (hlist_unhashed(&kvm->mmu_notifier.hlist)) {
> + /* Subscription removed by OOM. Wait for async callback. */
> + mmu_notifier_barrier();
> + mmdrop(kvm->mm);
> + } else {
> + mmu_notifier_unregister(&kvm->mmu_notifier, kvm->mm);
> + }
> /*
> * At this point, pending calls to invalidate_range_start()
> * have completed but no more MMU notifiers will run, so
>
> base-commit: a13c140cc289c0b7b3770bce5b3ad42ab35074aa
> --
> 2.43.0
--
Michal Hocko
SUSE Labs
prev parent reply other threads:[~2026-07-27 8:01 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-22 14:08 [PATCH v3] mm/mmu_notifier: Add async OOM cleanup via call_srcu() shaikh.kamal
2026-07-22 14:22 ` sashiko-bot
2026-07-24 8:59 ` Lorenzo Stoakes (ARM)
2026-07-27 8:01 ` Michal Hocko [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=amcQRxA3WlyWIRho@tiehlicka \
--to=mhocko@suse.com \
--cc=kvm@vger.kernel.org \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=linux-rt-devel@lists.linux.dev \
--cc=ljs@kernel.org \
--cc=lkp@intel.com \
--cc=pbonzini@redhat.com \
--cc=rientjes@google.com \
--cc=rppt@kernel.org \
--cc=seanjc@google.com \
--cc=shaikhkamal2012@gmail.com \
--cc=shakeel.butt@linux.dev \
--cc=surenb@google.com \
--cc=syzbot+c3178b6b512446632bac@syzkaller.appspotmail.com \
--cc=vbabka@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.