KVM: mmu_notifier: make mn_invalidate_lock non-sleeping for non-blocking invalidations
Subsystems (7)
PATCH 1/1 KVM: mmu_notifier: make mn_invalidate_lock non-sleeping for non-blocking invalidations
mmu_notifier_invalidate_range_start() may be invoked via mmu_notifier_invalidate_range_start_nonblock(), e.g. from oom_reaper(), where sleeping is explicitly forbidden.
KVM's mmu_notifier invalidate_range_start currently takes mn_invalidate_lock using spin_lock(). On PREEMPT_RT, spin_lock() maps to rt_mutex and may sleep, triggering:
BUG: sleeping function called from invalid context
This violates the MMU notifier contract regardless of PREEMPT_RT; RT kernels merely make the issue deterministic.
Fix by converting mn_invalidate_lock to a raw spinlock so that invalidate_range_start() remains non-sleeping while preserving the existing serialization between invalidate_range_start() and invalidate_range_end().
--- include/linux/kvm_host.h | 2 +- virt/kvm/kvm_main.c | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-)
include/linux/kvm_host.h +1 -1
@@ -797,7 +797,7 @@ struct kvm {atomic_t nr_memslots_dirty_logging;/* Used to wait for completion of MMU notifiers. */ -spinlock_t mn_invalidate_lock; +raw_spinlock_t mn_invalidate_lock;unsigned long mn_active_invalidate_count;struct rcuwait mn_memslots_update_rcuwait;
virt/kvm/kvm_main.c +9 -9
@@ -747,9 +747,9 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,* * Pairs with the decrement in range_end(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock); kvm->mn_active_invalidate_count++; -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * Invalidate pfn caches _before_ invalidating the secondary MMUs, i.e.
@@ -817,11 +817,11 @@ static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn,kvm_handle_hva_range (kvm, &hva_range);/* Pairs with the increment in range_start(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);if (!WARN_ON_ONCE (!kvm->mn_active_invalidate_count)) --kvm->mn_active_invalidate_count; wake = !kvm->mn_active_invalidate_count; -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * There can only be one waiter, since the wait happens under
@@ -1129,7 +1129,7 @@ static struct kvm *kvm_create_vm(unsigned long type, const char *fdname)mutex_init (&kvm->irq_lock);mutex_init (&kvm->slots_lock);mutex_init (&kvm->slots_arch_lock); -spin_lock_init (&kvm->mn_invalidate_lock); +raw_spin_lock_init (&kvm->mn_invalidate_lock);rcuwait_init (&kvm->mn_memslots_update_rcuwait);xa_init (&kvm->vcpu_array);#ifdef CONFIG_KVM_GENERIC_MEMORY_ATTRIBUTES
@@ -1635,17 +1635,17 @@ static void kvm_swap_active_memslots(struct kvm *kvm, int as_id)* progress, otherwise the locking in invalidate_range_start and * invalidate_range_end will be unbalanced. */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);prepare_to_rcuwait (&kvm->mn_memslots_update_rcuwait);while (kvm->mn_active_invalidate_count) {set_current_state (TASK_UNINTERRUPTIBLE ); -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);schedule (); -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock); }finish_rcuwait (&kvm->mn_memslots_update_rcuwait);rcu_assign_pointer (kvm->memslots[as_id], slots); -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * Acquired in kvm_set_memslot. Must be released before synchronize
Signature · 1 line
2.43.0
Quoted 13 lines · shaikh.kamal
mmu_notifier_invalidate_range_start()may be invoked viammu_notifier_invalidate_range_start_nonblock(), e.g. fromoom_reaper(), where sleeping is explicitly forbidden.KVM's
mmu_notifierinvalidate_range_startcurrently takesmn_invalidate_lockusingspin_lock(). OnPREEMPT_RT,spin_lock()maps tort_mutexand may sleep, triggering:BUG: sleeping function called from invalid contextThis violates the MMU notifier contract regardless of
PREEMPT_RT; RT kernels merely make the issue deterministic.
Quoted 6 lines
Fix by converting
mn_invalidate_lockto a raw spinlock so thatinvalidate_range_start()remains non-sleeping while preserving the existing serialization betweeninvalidate_range_start()andinvalidate_range_end().Signed-off-by shaikh.kamal <[email protected]>
I don't see any down side doing this, but…
virt/kvm/kvm_main.c static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, · line 752
Quoted 13 lines
virt/kvm/kvm_main.c +2
-2@@ -747,9 +747,9 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,* * Pairs with the decrement in range_end(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock); kvm->mn_active_invalidate_count++; -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);
atomic_inc(mn_active_invalidate_count)
virt/kvm/kvm_main.c static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn, · line 823
Quoted 12 lines
virt/kvm/kvm_main.c +1
-1/* * Invalidate pfn caches _before_ invalidating the secondary MMUs, i.e. @@ -817,11 +817,11 @@ static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn,kvm_handle_hva_range (kvm, &hva_range);/* Pairs with the increment in range_start(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);if (!WARN_ON_ONCE (!kvm->mn_active_invalidate_count)) --kvm->mn_active_invalidate_count; wake = !kvm->mn_active_invalidate_count;
wake = atomic_dec_return_safe(mn_active_invalidate_count); WARN_ON_ONCE(wake < 0); wake = !wake;
Quoted 8 lines
virt/kvm/kvm_main.c +1
-1-spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * There can only be one waiter, since the wait happens under @@ -1129,7 +1129,7 @@ static struct kvm *kvm_create_vm(unsigned long type, const char *fdname)@@ -1635,17 +1635,17 @@ static void kvm_swap_active_memslots(struct kvm *kvm, int as_id)* progress, otherwise the locking in invalidate_range_start and
virt/kvm/kvm_main.c static void kvm_swap_active_memslots(struct kvm *kvm, int as_id) · line 1643
Quoted 10 lines
virt/kvm/kvm_main.c +2
-2* invalidate_range_end will be unbalanced. */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);prepare_to_rcuwait (&kvm->mn_memslots_update_rcuwait);while (kvm->mn_active_invalidate_count) {set_current_state (TASK_UNINTERRUPTIBLE ); -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);schedule ();
And this I don't understand. The lock protects the rcuwait assignment which would be needed if multiple waiters are possible. But this goes away after the unlock and schedule() here. So these things could be moved outside of the locked section which limits it only to the mn_active_invalidate_count value.
virt/kvm/kvm_main.c static void kvm_swap_active_memslots(struct kvm *kvm, int as_id) · line 1651
Quoted 10 lines
virt/kvm/kvm_main.c +2
-2-spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock); }finish_rcuwait (&kvm->mn_memslots_update_rcuwait);rcu_assign_pointer (kvm->memslots[as_id], slots); -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * Acquired in kvm_set_memslot. Must be released before synchronize
Sebastian
Quoted 5 lines · Sebastian Andrzej Siewior
Quoted 4 lines · shaikh.kamal
mmu_notifier_invalidate_range_start()may be invoked viammu_notifier_invalidate_range_start_nonblock(), e.g. fromoom_reaper(), where sleeping is explicitly forbidden.
Quoted 7 lines
Quoted 7 lines
KVM's
mmu_notifierinvalidate_range_startcurrently takesmn_invalidate_lockusingspin_lock(). OnPREEMPT_RT,spin_lock()maps tort_mutexand may sleep, triggering:BUG: sleeping function called from invalid contextThis violates the MMU notifier contract regardless of
PREEMPT_RT;
I highly doubt that. kvm.mmu_lock is also a spinlock, and KVM has been taking that in invalidate_range_start() since
e930bffe95e1 ("KVM: Synchronize guest physical memory map to host virtual memory map")
which was a full decade before mmu_notifiers even added the blockable concept in
93065ac753e4 ("mm, oom: distinguish blockable mode for mmu notifiers")
and even predate the current concept of a "raw" spinlock introduced by
c2f21ce2e312 ("locking: Implement new raw_spinlock")
Quoted 1 line
Quoted 1 line
RT kernels merely make the issue deterministic.
No, RT kernels change the rules, because suddenly a non-sleeping locking becomes sleepable.
Quoted 4 lines
Quoted 4 lines
Fix by converting
mn_invalidate_lockto a raw spinlock so thatinvalidate_range_start()remains non-sleeping while preserving the existing serialization betweeninvalidate_range_start()andinvalidate_range_end().
This is insufficient. To actually "fix" this in KVM mmu_lock would need to be turned into a raw lock on all KVM architectures. I suspect the only reason there haven't been bug reports is because no one trips an OOM kill on VM while running with CONFIG_DEBUG_ATOMIC_SLEEP=y.
That combination is required because since commit
8931a454aea0 ("KVM: Take mmu_lock when handling MMU notifier iff the hva hits a memslot")
KVM only acquires mmu_lock if the to-be-invalidated range overlaps a memslot, i.e. affects memory that may be mapped into the guest.
E.g. this hack to simulate a non-blockable invalidation
virt/kvm/kvm_main.c +3 -1
@@ -739,7 +739,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, .handler = kvm_mmu_unmap_gfn_range, .on_lock = kvm_mmu_invalidate_begin, .flush_on_ret = true, - .may_block = mmu_notifier_range_blockable(range), + .may_block = false,//mmu_notifier_range_blockable(range), };trace_kvm_unmap_hva_range (range->start, range->end);
@@ -768,6 +768,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ gfn_to_pfn_cache_invalidate_start (kvm, range->start, range->end); +non_block_start ();/* * If one or more memslots were found and thus zapped, notify arch code * that guest memory has been reclaimed. This needs to be done *after*
@@ -775,6 +776,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ if (kvm_handle_hva_range (kvm, &hva_range).found_memslot)kvm_arch_guest_memory_reclaimed (kvm); +non_block_end ();return 0 ; }
immediately triggers
BUG: sleeping function called from invalid context at kernel/locking/spinlock_rt.c:241 in_atomic(): 0, irqs_disabled(): 0, non_block: 1, pid: 4992, name: qemu preempt_count: 0, expected: 0 RCU nest depth: 0, expected: 0 CPU: 6 UID: 1000 PID: 4992 Comm: qemu Not tainted 6.19.0-rc6-4d0917ffc392-x86_enter_mmio_stack_uaf_no_null-rt #1 PREEMPT_RT Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 0.0.0 02/06/2015 Call Trace: <TASK> dump_stack_lvl+0x51/0x60 __might_resched+0x10e/0x160 rt_write_lock+0x49/0x310 kvm_mmu_notifier_invalidate_range_start+0x10b/0x390 [kvm] __mmu_notifier_invalidate_range_start+0x9b/0x230 do_wp_page+0xce1/0xf30 __handle_mm_fault+0x380/0x3a0 handle_mm_fault+0xde/0x290 __get_user_pages+0x20d/0xbe0 get_user_pages_unlocked+0xf6/0x340 hva_to_pfn+0x295/0x420 [kvm] __kvm_faultin_pfn+0x5d/0x90 [kvm] kvm_mmu_faultin_pfn+0x31b/0x6e0 [kvm] kvm_tdp_page_fault+0xb6/0x160 [kvm] kvm_mmu_do_page_fault+0xee/0x1f0 [kvm] kvm_mmu_page_fault+0x8d/0x600 [kvm] vmx_handle_exit+0x18c/0x5a0 [kvm_intel] kvm_arch_vcpu_ioctl_run+0xc70/0x1c90 [kvm] kvm_vcpu_ioctl+0x2d7/0x9a0 [kvm] __x64_sys_ioctl+0x8a/0xd0 do_syscall_64+0x5e/0x11b0 entry_SYSCALL_64_after_hwframe+0x4b/0x53 </TASK> kvm: emulating exchange as write
It's not at all clear to me that switching mmu_lock to a raw lock would be a net positive for PREEMPT_RT. OOM-killing a KVM guest in a PREEMPT_RT seems like a comically rare scenario. Whereas contending mmu_lock in normal operation is relatively common (assuming there are even use cases for running VMs with a PREEMPT_RT host kernel).
In fact, the only reason the splat happens is because mmu_notifiers somewhat artificially forces an atomic context via non_block_start() since commit
ba170f76b69d ("mm, notifier: Catch sleeping/blocking for !blockable")
Given the massive amount of churn in KVM that would be required to fully eliminate the splat, and that it's not at all obvious that it would be a good change overall, at least for now:
NAK
I'm not fundamentally opposed to such a change, but there needs to be a _lot_ more analysis and justification beyond "fix CONFIG_DEBUG_ATOMIC_SLEEP=y".
Quoted 48 lines
virt/kvm/kvm_main.c static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, · line 752
Quoted 13 lines
virt/kvm/kvm_main.c +2
-2@@ -747,9 +747,9 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,* * Pairs with the decrement in range_end(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock); kvm->mn_active_invalidate_count++; -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);
atomic_inc(mn_active_invalidate_count)virt/kvm/kvm_main.c static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn, · line 823
Quoted 12 lines
virt/kvm/kvm_main.c +1
-1/* * Invalidate pfn caches _before_ invalidating the secondary MMUs, i.e. @@ -817,11 +817,11 @@ static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn,kvm_handle_hva_range (kvm, &hva_range);/* Pairs with the increment in range_start(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);if (!WARN_ON_ONCE (!kvm->mn_active_invalidate_count)) --kvm->mn_active_invalidate_count; wake = !kvm->mn_active_invalidate_count;wake = atomic_dec_return_safe(mn_active_invalidate_count); WARN_ON_ONCE(wake < 0); wake = !wake;Quoted 16 lines
virt/kvm/kvm_main.c +2
-3-spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * There can only be one waiter, since the wait happens under @@ -1129,7 +1129,7 @@ static struct kvm *kvm_create_vm(unsigned long type, const char *fdname)@@ -1635,17 +1635,17 @@ static void kvm_swap_active_memslots(struct kvm *kvm, int as_id)* progress, otherwise the locking in invalidate_range_start and * invalidate_range_end will be unbalanced. */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);prepare_to_rcuwait (&kvm->mn_memslots_update_rcuwait);while (kvm->mn_active_invalidate_count) {set_current_state (TASK_UNINTERRUPTIBLE ); -spin_unlock (&kvm->mn_invalidate_lock);
Quoted 8 lines
virt/kvm/kvm_main.c static void kvm_swap_active_memslots(struct kvm *kvm, int as_id) · line 1643
Quoted 2 lines
virt/kvm/kvm_main.c +1
-0+raw_spin_unlock (&kvm->mn_invalidate_lock);schedule ();And this I don't understand. The lock protects the rcuwait assignment which would be needed if multiple waiters are possible. But this goes away after the unlock and
schedule()here. So these things could be moved outside of the locked section which limits it only to themn_active_invalidate_countvalue.
The implementation is essentially a deliberately unfair rwswem. The "write" side in kvm_swap_active_memslots() subtly protect this code:
rcu_assign_pointer(kvm->memslots[as_id], slots);
and the "read" side protects the kvm->memslot lookups in kvm_handle_hva_range().
KVM optimizes its mmu_notifier invalidation path to only take action if the to-be-invalidated range overlaps one or more memslots, i.e. affects memory that be can be mapped into the guest. The wrinkle with those optimizations is that KVM needs to prevent changes to the memslots between invalidation start() and end(), otherwise the accounting can become imbalanced, e.g. mmu_invalidate_in_progress will underflow or be left elevated and essentially hang the VM (among other bad things).
So simply making mn_active_invalidate_count an atomic won't suffice, because KVM needs to block start() to ensure start()+end() see the exact same set of memslots.
Quoted 128 lines · Sean Christopherson
Quoted 5 lines · Sebastian Andrzej Siewior
Quoted 4 lines · shaikh.kamal
mmu_notifier_invalidate_range_start()may be invoked viammu_notifier_invalidate_range_start_nonblock(), e.g. fromoom_reaper(), where sleeping is explicitly forbidden.Quoted 7 lines
Quoted 7 lines
KVM's
mmu_notifierinvalidate_range_startcurrently takesmn_invalidate_lockusingspin_lock(). OnPREEMPT_RT,spin_lock()maps tort_mutexand may sleep, triggering:BUG: sleeping function called from invalid contextThis violates the MMU notifier contract regardless of
PREEMPT_RT;I highly doubt that. kvm.mmu_lock is also a spinlock, and KVM has been taking that in
invalidate_range_start()since
e930bffe95e1("KVM: Synchronize guest physical memory map to host virtual memory map")which was a full decade before
mmu_notifierseven added the blockable concept in
93065ac753e4("mm, oom: distinguish blockable mode for mmu notifiers")and even predate the current concept of a "raw" spinlock introduced by
c2f21ce2e312("locking: Implement newraw_spinlock")Quoted 1 line
Quoted 1 line
RT kernels merely make the issue deterministic.
No, RT kernels change the rules, because suddenly a non-sleeping locking becomes sleepable.
Quoted 4 lines
Quoted 4 lines
Fix by converting
mn_invalidate_lockto a raw spinlock so thatinvalidate_range_start()remains non-sleeping while preserving the existing serialization betweeninvalidate_range_start()andinvalidate_range_end().This is insufficient. To actually "fix" this in KVM
mmu_lockwould need to be turned into a raw lock on all KVM architectures. I suspect the only reason there haven't been bug reports is because no one trips an OOM kill on VM while running withCONFIG_DEBUG_ATOMIC_SLEEP=y.That combination is required because since commit
8931a454aea0("KVM: Takemmu_lockwhen handling MMU notifier iff the hva hits a memslot")KVM only acquires
mmu_lockif the to-be-invalidated range overlaps a memslot, i.e. affects memory that may be mapped into the guest.E.g. this hack to simulate a non-blockable invalidation
virt/kvm/kvm_main.c +3
-1@@ -739,7 +739,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, .handler = kvm_mmu_unmap_gfn_range, .on_lock = kvm_mmu_invalidate_begin, .flush_on_ret = true, - .may_block = mmu_notifier_range_blockable(range), + .may_block = false,//mmu_notifier_range_blockable(range), };trace_kvm_unmap_hva_range (range->start, range->end);@@ -768,6 +768,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ gfn_to_pfn_cache_invalidate_start (kvm, range->start, range->end); +non_block_start ();/* * If one or more memslots were found and thus zapped, notify arch code * that guest memory has been reclaimed. This needs to be done *after* @@ -775,6 +776,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ if (kvm_handle_hva_range (kvm, &hva_range).found_memslot)kvm_arch_guest_memory_reclaimed (kvm); +non_block_end ();return 0 ; }immediately triggers
BUG: sleeping function called from invalid context at kernel/locking/spinlock_rt.c:241 in_atomic(): 0, irqs_disabled(): 0, non_block: 1, pid: 4992, name: qemu preempt_count: 0, expected: 0 RCU nest depth: 0, expected: 0 CPU: 6 UID: 1000 PID: 4992 Comm: qemu Not tainted 6.19.0-rc6-4d0917ffc392-x86_enter_mmio_stack_uaf_no_null-rt #1 PREEMPT_RT Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 0.0.0 02/06/2015 Call Trace: <TASK> dump_stack_lvl+0x51/0x60 __might_resched+0x10e/0x160 rt_write_lock+0x49/0x310 kvm_mmu_notifier_invalidate_range_start+0x10b/0x390 [kvm] __mmu_notifier_invalidate_range_start+0x9b/0x230 do_wp_page+0xce1/0xf30 __handle_mm_fault+0x380/0x3a0 handle_mm_fault+0xde/0x290 __get_user_pages+0x20d/0xbe0 get_user_pages_unlocked+0xf6/0x340 hva_to_pfn+0x295/0x420 [kvm] __kvm_faultin_pfn+0x5d/0x90 [kvm] kvm_mmu_faultin_pfn+0x31b/0x6e0 [kvm] kvm_tdp_page_fault+0xb6/0x160 [kvm] kvm_mmu_do_page_fault+0xee/0x1f0 [kvm] kvm_mmu_page_fault+0x8d/0x600 [kvm] vmx_handle_exit+0x18c/0x5a0 [kvm_intel] kvm_arch_vcpu_ioctl_run+0xc70/0x1c90 [kvm] kvm_vcpu_ioctl+0x2d7/0x9a0 [kvm] __x64_sys_ioctl+0x8a/0xd0 do_syscall_64+0x5e/0x11b0 entry_SYSCALL_64_after_hwframe+0x4b/0x53 </TASK> kvm: emulating exchange as writeIt's not at all clear to me that switching
mmu_lockto a raw lock would be a net positive forPREEMPT_RT. OOM-killing a KVM guest in aPREEMPT_RTseems like a comically rare scenario. Whereas contendingmmu_lockin normal operation is relatively common (assuming there are even use cases for running VMs with aPREEMPT_RThost kernel).In fact, the only reason the splat happens is because
mmu_notifierssomewhat artificially forces an atomic context vianon_block_start()since commit
ba170f76b69d("mm, notifier: Catch sleeping/blocking for !blockable")
Quoted 9 lines
Given the massive amount of churn in KVM that would be required to fully eliminate the splat, and that it's not at all obvious that it would be a good change overall, at least for now:
NAK
I'm not fundamentally opposed to such a change, but there needs to be a _lot_ more analysis and justification beyond "fix
CONFIG_DEBUG_ATOMIC_SLEEP=y".
Hi Sean,
Thanks for the detailed explanation and for spelling out the border issue.
Understood on both points:
1. The changelog wording was too strong; PREEMPT_RT changes spin_lock() semantics, and the splat is fundamentally due to spinlocks becoming sleepable there.
2. Converting only mm_invalidate_lock to raw is insufficient since KVM can still take the mmu_lock (and other sleeping locks RT) in invalidate_range_start() when the invalidation hits a memslot.
Given the above, it shounds like "convert locks to raw" is not the right direction without sinificat rework and justification.
Would an acceptable direction be to handle the !blockable notifier case by deferring the heavyweight invalidation work(anything that take mmu_lock/may sleep on RT) to a context that may block(e.g. queued work), while keeping start()/end() accounting consisting with memslot changes ? if so, I can protoptype a patch along those lines and share for feedback.
Alternatively, if you think this needs to be addressed in mmu_notifiers(eg. how non_block_start() is applied), I'm happy to redirect my efforts there-Please advise.
Quoted 74 lines
Quoted 48 lines
virt/kvm/kvm_main.c static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, · line 752
Quoted 13 lines
virt/kvm/kvm_main.c +2
-2@@ -747,9 +747,9 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,* * Pairs with the decrement in range_end(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock); kvm->mn_active_invalidate_count++; -spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);
atomic_inc(mn_active_invalidate_count)virt/kvm/kvm_main.c static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn, · line 823
Quoted 12 lines
virt/kvm/kvm_main.c +1
-1/* * Invalidate pfn caches _before_ invalidating the secondary MMUs, i.e. @@ -817,11 +817,11 @@ static void kvm_mmu_notifier_invalidate_range_end(struct mmu_notifier *mn,kvm_handle_hva_range (kvm, &hva_range);/* Pairs with the increment in range_start(). */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);if (!WARN_ON_ONCE (!kvm->mn_active_invalidate_count)) --kvm->mn_active_invalidate_count; wake = !kvm->mn_active_invalidate_count;wake = atomic_dec_return_safe(mn_active_invalidate_count); WARN_ON_ONCE(wake < 0); wake = !wake;Quoted 16 lines
virt/kvm/kvm_main.c +2
-3-spin_unlock (&kvm->mn_invalidate_lock); +raw_spin_unlock (&kvm->mn_invalidate_lock);/* * There can only be one waiter, since the wait happens under @@ -1129,7 +1129,7 @@ static struct kvm *kvm_create_vm(unsigned long type, const char *fdname)@@ -1635,17 +1635,17 @@ static void kvm_swap_active_memslots(struct kvm *kvm, int as_id)* progress, otherwise the locking in invalidate_range_start and * invalidate_range_end will be unbalanced. */ -spin_lock (&kvm->mn_invalidate_lock); +raw_spin_lock (&kvm->mn_invalidate_lock);prepare_to_rcuwait (&kvm->mn_memslots_update_rcuwait);while (kvm->mn_active_invalidate_count) {set_current_state (TASK_UNINTERRUPTIBLE ); -spin_unlock (&kvm->mn_invalidate_lock);Quoted 8 lines
virt/kvm/kvm_main.c static void kvm_swap_active_memslots(struct kvm *kvm, int as_id) · line 1643
Quoted 2 lines
virt/kvm/kvm_main.c +1
-0+raw_spin_unlock (&kvm->mn_invalidate_lock);schedule ();And this I don't understand. The lock protects the rcuwait assignment which would be needed if multiple waiters are possible. But this goes away after the unlock and
schedule()here. So these things could be moved outside of the locked section which limits it only to themn_active_invalidate_countvalue.The implementation is essentially a deliberately unfair rwswem. The "write" side in
kvm_swap_active_memslots()subtly protect this code:rcu_assign_pointer(kvm->memslots[as_id], slots);and the "read" side protects the
kvm->memslotlookups inkvm_handle_hva_range().KVM optimizes its
mmu_notifierinvalidation path to only take action if the to-be-invalidated range overlaps one or more memslots, i.e. affects memory that be can be mapped into the guest. The wrinkle with those optimizations is that KVM needs to prevent changes to the memslots between invalidationstart()andend(), otherwise the accounting can become imbalanced, e.g.mmu_invalidate_in_progresswill underflow or be left elevated and essentially hang the VM (among other bad things).So simply making
mn_active_invalidate_countan atomic won't suffice, because KVM needs to blockstart()to ensure start()+end() see the exact same set of memslots.
Sean Christopherson
maintainerQuoted 31 lines · shaikh kamaluddin
Quoted 12 lines · Sean Christopherson
On Wed, Feb 11, 2026, Sebastian Andrzej Siewior wrote:
It's not at all clear to me that switchingmmu_lockto a raw lock would be a net positive forPREEMPT_RT. OOM-killing a KVM guest in aPREEMPT_RTseems like a comically rare scenario. Whereas contendingmmu_lockin normal operation is relatively common (assuming there are even use cases for running VMs with aPREEMPT_RThost kernel).In fact, the only reason the splat happens is because
mmu_notifierssomewhat artificially forces an atomic context vianon_block_start()since commit
ba170f76b69d("mm, notifier: Catch sleeping/blocking for !blockable")Quoted 9 lines
Given the massive amount of churn in KVM that would be required to fully eliminate the splat, and that it's not at all obvious that it would be a good change overall, at least for now:
NAK
I'm not fundamentally opposed to such a change, but there needs to be a _lot_ more analysis and justification beyond "fix
CONFIG_DEBUG_ATOMIC_SLEEP=y".Hi Sean,
Thanks for the detailed explanation and for spelling out the border issue.
Understood on both points:
1. The changelog wording was too strong;PREEMPT_RTchangesspin_lock()semantics, and the splat is fundamentally due to spinlocks becoming sleepable there.
2. Converting onlymm_invalidate_lockto raw is insufficient since KVM can still take themmu_lock(and other sleeping locks
Quoted 8 lines
RT) in
invalidate_range_start()when the invalidation hits a memslot.
Given the above, it shounds like "convert locks to raw" is not the right direction without sinificat rework and justification.
Would an acceptable direction be to handle the !blockable notifier case by deferring the heavyweight invalidationwork(anything that take mmu_lock/may sleep on RT)to a context that may block(e.g. queued work), while keeping start()/end() accounting consisting with memslot changes ?
No, because the _only_ case where the invalidation is non-blockable is when the kernel is OOM-killing. Deferring the invalidations when we're OOM is likely to make the problem *worse*.
That's the crux of my NAK. We'd be making KVM and kernel behavior worse to "fix" a largely hypothetical issue (OOM-killing a KVM guest in a RT kernel).
Paolo Bonzini
maintainerQuoted 158 lines · shaikh kamaluddin
Quoted 128 lines · Sean Christopherson
Quoted 5 lines · Sebastian Andrzej Siewior
Quoted 4 lines · shaikh.kamal
mmu_notifier_invalidate_range_start()may be invoked viammu_notifier_invalidate_range_start_nonblock(), e.g. fromoom_reaper(), where sleeping is explicitly forbidden.Quoted 7 lines
Quoted 7 lines
KVM's
mmu_notifierinvalidate_range_startcurrently takesmn_invalidate_lockusingspin_lock(). OnPREEMPT_RT,spin_lock()maps tort_mutexand may sleep, triggering:BUG: sleeping function called from invalid contextThis violates the MMU notifier contract regardless of
PREEMPT_RT;I highly doubt that. kvm.mmu_lock is also a spinlock, and KVM has been taking that in
invalidate_range_start()since
e930bffe95e1("KVM: Synchronize guest physical memory map to host virtual memory map")which was a full decade before
mmu_notifierseven added the blockable concept in
93065ac753e4("mm, oom: distinguish blockable mode for mmu notifiers")and even predate the current concept of a "raw" spinlock introduced by
c2f21ce2e312("locking: Implement newraw_spinlock")Quoted 1 line
Quoted 1 line
RT kernels merely make the issue deterministic.
No, RT kernels change the rules, because suddenly a non-sleeping locking becomes sleepable.
Quoted 4 lines
Quoted 4 lines
Fix by converting
mn_invalidate_lockto a raw spinlock so thatinvalidate_range_start()remains non-sleeping while preserving the existing serialization betweeninvalidate_range_start()andinvalidate_range_end().This is insufficient. To actually "fix" this in KVM
mmu_lockwould need to be turned into a raw lock on all KVM architectures. I suspect the only reason there haven't been bug reports is because no one trips an OOM kill on VM while running withCONFIG_DEBUG_ATOMIC_SLEEP=y.That combination is required because since commit
8931a454aea0("KVM: Takemmu_lockwhen handling MMU notifier iff the hva hits a memslot")KVM only acquires
mmu_lockif the to-be-invalidated range overlaps a memslot, i.e. affects memory that may be mapped into the guest.E.g. this hack to simulate a non-blockable invalidation
virt/kvm/kvm_main.c +3
-1@@ -739,7 +739,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, .handler = kvm_mmu_unmap_gfn_range, .on_lock = kvm_mmu_invalidate_begin, .flush_on_ret = true, - .may_block = mmu_notifier_range_blockable(range), + .may_block = false,//mmu_notifier_range_blockable(range), };trace_kvm_unmap_hva_range (range->start, range->end);@@ -768,6 +768,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ gfn_to_pfn_cache_invalidate_start (kvm, range->start, range->end); +non_block_start ();/* * If one or more memslots were found and thus zapped, notify arch code * that guest memory has been reclaimed. This needs to be done *after* @@ -775,6 +776,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ if (kvm_handle_hva_range (kvm, &hva_range).found_memslot)kvm_arch_guest_memory_reclaimed (kvm); +non_block_end ();return 0 ; }immediately triggers
BUG: sleeping function called from invalid context at kernel/locking/spinlock_rt.c:241 in_atomic(): 0, irqs_disabled(): 0, non_block: 1, pid: 4992, name: qemu preempt_count: 0, expected: 0 RCU nest depth: 0, expected: 0 CPU: 6 UID: 1000 PID: 4992 Comm: qemu Not tainted 6.19.0-rc6-4d0917ffc392-x86_enter_mmio_stack_uaf_no_null-rt #1 PREEMPT_RT Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 0.0.0 02/06/2015 Call Trace: <TASK> dump_stack_lvl+0x51/0x60 __might_resched+0x10e/0x160 rt_write_lock+0x49/0x310 kvm_mmu_notifier_invalidate_range_start+0x10b/0x390 [kvm] __mmu_notifier_invalidate_range_start+0x9b/0x230 do_wp_page+0xce1/0xf30 __handle_mm_fault+0x380/0x3a0 handle_mm_fault+0xde/0x290 __get_user_pages+0x20d/0xbe0 get_user_pages_unlocked+0xf6/0x340 hva_to_pfn+0x295/0x420 [kvm] __kvm_faultin_pfn+0x5d/0x90 [kvm] kvm_mmu_faultin_pfn+0x31b/0x6e0 [kvm] kvm_tdp_page_fault+0xb6/0x160 [kvm] kvm_mmu_do_page_fault+0xee/0x1f0 [kvm] kvm_mmu_page_fault+0x8d/0x600 [kvm] vmx_handle_exit+0x18c/0x5a0 [kvm_intel] kvm_arch_vcpu_ioctl_run+0xc70/0x1c90 [kvm] kvm_vcpu_ioctl+0x2d7/0x9a0 [kvm] __x64_sys_ioctl+0x8a/0xd0 do_syscall_64+0x5e/0x11b0 entry_SYSCALL_64_after_hwframe+0x4b/0x53 </TASK> kvm: emulating exchange as writeIt's not at all clear to me that switching
mmu_lockto a raw lock would be a net positive forPREEMPT_RT. OOM-killing a KVM guest in aPREEMPT_RTseems like a comically rare scenario. Whereas contendingmmu_lockin normal operation is relatively common (assuming there are even use cases for running VMs with aPREEMPT_RThost kernel).In fact, the only reason the splat happens is because
mmu_notifierssomewhat artificially forces an atomic context vianon_block_start()since commit
ba170f76b69d("mm, notifier: Catch sleeping/blocking for !blockable")Quoted 9 lines
Given the massive amount of churn in KVM that would be required to fully eliminate the splat, and that it's not at all obvious that it would be a good change overall, at least for now:
NAK
I'm not fundamentally opposed to such a change, but there needs to be a _lot_ more analysis and justification beyond "fix
CONFIG_DEBUG_ATOMIC_SLEEP=y".Hi Sean,
Thanks for the detailed explanation and for spelling out the border issue.
Understood on both points:
1. The changelog wording was too strong;PREEMPT_RTchangesspin_lock()semantics, and the splat is fundamentally due to spinlocks becoming sleepable there.
2. Converting onlymm_invalidate_lockto raw is insufficient since KVM can still take themmu_lock(and other sleeping locks RT) ininvalidate_range_start()when the invalidation hits a memslot.
Given the above, it shounds like "convert locks to raw" is not the right direction without sinificat rework and justification.
Would an acceptable direction be to handle the !blockable notifier case by deferring the heavyweight invalidationwork(anything that take mmu_lock/may sleep on RT)to a context that may block(e.g. queued work), while keeping start()/end() accounting consisting with memslot changes ? if so, I can protoptype a patch along those lines and share for feedback.
Quoted 3 lines
Alternatively, if you think this needs to be addressed in mmu_notifiers(eg. how
non_block_start()is applied), I'm happy to redirect my efforts there-Please advise.
Have you considered a "OOM entered" callback for MMU notifiers? KVM's MMU notifier can just remove itself for example, in fact there is code in kvm_destroy_vm() to do that even if invalidations are unbalanced.
Paolo
Quoted 160 lines · Paolo Bonzini
Quoted 155 lines · shaikh kamaluddin
Quoted 125 lines · Sean Christopherson
Quoted 5 lines · Sebastian Andrzej Siewior
Quoted 4 lines · shaikh.kamal
mmu_notifier_invalidate_range_start()may be invoked viammu_notifier_invalidate_range_start_nonblock(), e.g. fromoom_reaper(), where sleeping is explicitly forbidden.Quoted 7 lines
Quoted 7 lines
KVM's
mmu_notifierinvalidate_range_startcurrently takesmn_invalidate_lockusingspin_lock(). OnPREEMPT_RT,spin_lock()maps tort_mutexand may sleep, triggering:BUG: sleeping function called from invalid contextThis violates the MMU notifier contract regardless of
PREEMPT_RT;I highly doubt that. kvm.mmu_lock is also a spinlock, and KVM has been taking that in
invalidate_range_start()since
e930bffe95e1("KVM: Synchronize guest physical memory map to host virtual memory map")which was a full decade before
mmu_notifierseven added the blockable concept in
93065ac753e4("mm, oom: distinguish blockable mode for mmu notifiers")and even predate the current concept of a "raw" spinlock introduced by
c2f21ce2e312("locking: Implement newraw_spinlock")Quoted 1 line
Quoted 1 line
RT kernels merely make the issue deterministic.
No, RT kernels change the rules, because suddenly a non-sleeping locking becomes sleepable.
Quoted 4 lines
Quoted 4 lines
Fix by converting
mn_invalidate_lockto a raw spinlock so thatinvalidate_range_start()remains non-sleeping while preserving the existing serialization betweeninvalidate_range_start()andinvalidate_range_end().This is insufficient. To actually "fix" this in KVM
mmu_lockwould need to be turned into a raw lock on all KVM architectures. I suspect the only reason there haven't been bug reports is because no one trips an OOM kill on VM while running withCONFIG_DEBUG_ATOMIC_SLEEP=y.That combination is required because since commit
8931a454aea0("KVM: Takemmu_lockwhen handling MMU notifier iff the hva hits a memslot")KVM only acquires
mmu_lockif the to-be-invalidated range overlaps a memslot, i.e. affects memory that may be mapped into the guest.E.g. this hack to simulate a non-blockable invalidation
virt/kvm/kvm_main.c +3
-1@@ -739,7 +739,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn, .handler = kvm_mmu_unmap_gfn_range, .on_lock = kvm_mmu_invalidate_begin, .flush_on_ret = true, - .may_block = mmu_notifier_range_blockable(range), + .may_block = false,//mmu_notifier_range_blockable(range), };trace_kvm_unmap_hva_range (range->start, range->end);@@ -768,6 +768,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ gfn_to_pfn_cache_invalidate_start (kvm, range->start, range->end); +non_block_start ();/* * If one or more memslots were found and thus zapped, notify arch code * that guest memory has been reclaimed. This needs to be done *after* @@ -775,6 +776,7 @@ static int kvm_mmu_notifier_invalidate_range_start(struct mmu_notifier *mn,*/ if (kvm_handle_hva_range (kvm, &hva_range).found_memslot)kvm_arch_guest_memory_reclaimed (kvm); +non_block_end ();return 0 ; }immediately triggers
BUG: sleeping function called from invalid context at kernel/locking/spinlock_rt.c:241 in_atomic(): 0, irqs_disabled(): 0, non_block: 1, pid: 4992, name: qemu preempt_count: 0, expected: 0 RCU nest depth: 0, expected: 0 CPU: 6 UID: 1000 PID: 4992 Comm: qemu Not tainted 6.19.0-rc6-4d0917ffc392-x86_enter_mmio_stack_uaf_no_null-rt #1 PREEMPT_RT Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 0.0.0 02/06/2015 Call Trace: <TASK> dump_stack_lvl+0x51/0x60 __might_resched+0x10e/0x160 rt_write_lock+0x49/0x310 kvm_mmu_notifier_invalidate_range_start+0x10b/0x390 [kvm] __mmu_notifier_invalidate_range_start+0x9b/0x230 do_wp_page+0xce1/0xf30 __handle_mm_fault+0x380/0x3a0 handle_mm_fault+0xde/0x290 __get_user_pages+0x20d/0xbe0 get_user_pages_unlocked+0xf6/0x340 hva_to_pfn+0x295/0x420 [kvm] __kvm_faultin_pfn+0x5d/0x90 [kvm] kvm_mmu_faultin_pfn+0x31b/0x6e0 [kvm] kvm_tdp_page_fault+0xb6/0x160 [kvm] kvm_mmu_do_page_fault+0xee/0x1f0 [kvm] kvm_mmu_page_fault+0x8d/0x600 [kvm] vmx_handle_exit+0x18c/0x5a0 [kvm_intel] kvm_arch_vcpu_ioctl_run+0xc70/0x1c90 [kvm] kvm_vcpu_ioctl+0x2d7/0x9a0 [kvm] __x64_sys_ioctl+0x8a/0xd0 do_syscall_64+0x5e/0x11b0 entry_SYSCALL_64_after_hwframe+0x4b/0x53 </TASK> kvm: emulating exchange as writeIt's not at all clear to me that switching
mmu_lockto a raw lock would be a net positive forPREEMPT_RT. OOM-killing a KVM guest in aPREEMPT_RTseems like a comically rare scenario. Whereas contendingmmu_lockin normal operation is relatively common (assuming there are even use cases for running VMs with aPREEMPT_RThost kernel).In fact, the only reason the splat happens is because
mmu_notifierssomewhat artificially forces an atomic context vianon_block_start()since commit
ba170f76b69d("mm, notifier: Catch sleeping/blocking for !blockable")Quoted 9 lines
Given the massive amount of churn in KVM that would be required to fully eliminate the splat, and that it's not at all obvious that it would be a good change overall, at least for now:
NAK
I'm not fundamentally opposed to such a change, but there needs to be a _lot_ more analysis and justification beyond "fix
CONFIG_DEBUG_ATOMIC_SLEEP=y".Hi Sean,
Thanks for the detailed explanation and for spelling out the border issue.
Understood on both points:
1. The changelog wording was too strong;PREEMPT_RTchangesspin_lock()semantics, and the splat is fundamentally due to spinlocks becoming sleepable there.
2. Converting onlymm_invalidate_lockto raw is insufficient since KVM can still take themmu_lock(and other sleeping locks RT) ininvalidate_range_start()when the invalidation hits a memslot.
Given the above, it shounds like "convert locks to raw" is not the right direction without sinificat rework and justification.
Would an acceptable direction be to handle the !blockable notifier case by deferring the heavyweight invalidationwork(anything that take mmu_lock/may sleep on RT)to a context that may block(e.g. queued work), while keeping start()/end() accounting consisting with memslot changes ? if so, I can protoptype a patch along those lines and share for feedback.Quoted 3 lines
Alternatively, if you think this needs to be addressed in mmu_notifiers(eg. how
non_block_start()is applied), I'm happy to redirect my efforts there-Please advise.
Quoted 6 lines
Have you considered a "OOM entered" callback for MMU notifiers? KVM's MMU notifier can just remove itself for example, in fact there is code in
kvm_destroy_vm()to do that even if invalidations are unbalanced.Paolo
Thanks for the suggestion! That's a much cleaner approach than what I was considering.
If I understand correctly, the idea would be:
1. Add a new MMU notifier callback (e.g., .oom_entered or .release_on_oom)
2. Have KVM implement it to unregister the notifier when OOM reaper starts
3. Leverage the existing kvm_destroy_vm() logic that already handles unbalanced invalidations
This avoids the whole "convert locks to raw" problem and the complexity of deferring work.
I have questions on Testing part:
------------------------------------
I tried to reproduce the bug scenario using the virtme-ng then running
the stress-ng putting memory pressure on VM, but not able to reproduce
the scenario.
I tried this way ..
vng -v -r ./arch/x86/boot/bzImage
VM is up, then running the stress-ng as below
stress-ng --vm 2 --vm-bytes 95% --timeout 20s & sleep 5 & dmesg | tail -30 | grep "sleeping function" OOM Killer is triggered, but exact bug not able to reproduce, Please
suggest how to reproduce this bug, even we need to verify after code
changes which you have suggested.
Shaikh Kamal
Paolo Bonzini
maintainerQuoted 8 lines · shaikh kamaluddin
Quoted 8 lines
Quoted 3 lines
Alternatively, if you think this needs to be addressed in mmu_notifiers(eg. how
non_block_start()is applied), I'm happy to redirect my efforts there-Please advise.Have you considered a "OOM entered" callback for MMU notifiers? KVM's MMU notifier can just remove itself for example, in fact there is code in
kvm_destroy_vm()to do that even if invalidations are unbalanced.
Quoted 8 lines
Quoted 2 lines
Paolo
Thanks for the suggestion! That's a much cleaner approach than what I was considering.
If I understand correctly, the idea would be:
1. Add a new MMU notifier callback (e.g., .oom_entered or .release_on_oom)
2. Have KVM implement it to unregister the notifier when OOM reaper starts
3. Leverage the existingkvm_destroy_vm()logic that already handles unbalanced invalidations
Yes pretty much. Essentially, move the existing logic to the new callback and invoke it from kvm_destroy_vm().
Quoted 14 lines
This avoids the whole "convert locks to raw" problem and the complexity of deferring work.
I have questions on Testing part:
------------------------------------
I tried to reproduce the bug scenario using the virtme-ng then running
the stress-ng putting memory pressure on VM, but not able to reproduce
the scenario.
I tried this way ..
vng -v -r./arch/x86/boot/bzImage
VM is up, then running the stress-ng as below
stress-ng --vm 2 --vm-bytes 95% --timeout 20s & sleep 5 & dmesg | tail -30 | grep "sleeping function" OOM Killer is triggered, but exact bug not able to reproduce, Please
suggest how to reproduce this bug, even we need to verify after code
changes which you have suggested.
I don't know, sorry. But with this new approach there will always be a call to the new callback from the OOM killer, so it's easier to test.
Thanks,
Paolo
Quoted 13 lines · Paolo Bonzini
Quoted 12 lines · shaikh kamaluddin
Quoted 10 lines
Quoted 3 lines
Alternatively, if you think this needs to be addressed in mmu_notifiers(eg. how
non_block_start()is applied), I'm happy to redirect my efforts there-Please advise.Have you considered a "OOM entered" callback for MMU notifiers? KVM's MMU notifier can just remove itself for example, in fact there is code in
kvm_destroy_vm()to do that even if invalidations are unbalanced.Paolo
Thanks for the suggestion! That's a much cleaner approach than what I was considering.
Quoted 8 lines
Quoted 4 lines
If I understand correctly, the idea would be:
1. Add a new MMU notifier callback (e.g., .oom_entered or .release_on_oom)
2. Have KVM implement it to unregister the notifier when OOM reaper starts
3. Leverage the existingkvm_destroy_vm()logic that already handles unbalanced invalidationsYes pretty much. Essentially, move the existing logic to the new callback and invoke it from
kvm_destroy_vm().
Hi Paolo,
Thank you for the suggestion to use an oom_enter callback approach. I've implemented v2 based on your guidance and have successfully validated it.
Implementation Summary:
-------------------------------------
Following your recommendation, I've added a new oom_enter callback to the mmu_notifier_ops structure. The implementation:
1. Added oom_enter callback to struct mmu_notifier_ops in include/linux/mmu_notifier.h
2. Implemented __mmu_notifier_oom_enter() in mm/mmu_notifier.c to invoke registered callbacks
3. Called mmu_notifier_oom_enter(mm) from __oom_kill_process in mm/oom_kill.c before any invalidations
4. As per your suggestion, move existing kvm_destroy_vm() logic that already handles unbalanced invalidation to the new helper function kvm_mmu_notifier_detach() and invoke it from the kvm_destroy_vm()
Key Design Decision:
------------------------------
Implementation point no 4, while testing, Issue I was encountering is a recursive locking problem with the srcu lock, which is being acquired twice in the same context. This happens during the __mmu_notifier_oom_enter() and __synchronize_srcu() calls, leading to a potential deadlock. Please find below log snippet while launching the Guest VM
------------------------------------------------------------------------------------------------OOM_REAPER: START reaping:func:__mmu_notifier_oom_enter
[ 399.841599][T10882] OOM_REAPER: START reaping:func:__mmu_notifier_oom_enter
[ 399.841608][T10882] KVM: oom_enter callback invoked for VM:kvm_mmu_notifier_oom_enter
[ 399.841608][T10882] KVM: oom_enter callback invoked for VM:kvm_mmu_notifier_oom_enter
[ 399.841961][T10882]
[ 399.841961][T10882]
[ 399.841962][T10882] ============================================
[ 399.841962][T10882] ============================================
[ 399.841964][T10882] WARNING: possible recursive locking detected
[ 399.841964][T10882] WARNING: possible recursive locking detected
[ 399.841966][T10882] 7.0.0-rc2-00467-g4ae12d8bd9a8-dirty #12 Not tainted
[ 399.841966][T10882] 7.0.0-rc2-00467-g4ae12d8bd9a8-dirty #12 Not tainted
[ 399.841969][T10882] --------------------------------------------
[ 399.841969][T10882] --------------------------------------------
[ 399.841971][T10882] qemu-system-x86/10882 is trying to acquire lock:
[ 399.841971][T10882] qemu-system-x86/10882 is trying to acquire lock:
[ 399.841974][T10882] ffffffff8db05598 (srcu){.+.+}-{0:0}, at: __synchronize_srcu+0x83/0x380
[ 399.841974][T10882] ffffffff8db05598 (srcu){.+.+}-{0:0}, at: __synchronize_srcu+0x83/0x380
[ 399.841991][T10882]
[ 399.841991][T10882] but task is already holding lock:
[ 399.841991][T10882]
[ 399.841991][T10882] but task is already holding lock:
[ 399.841992][T10882] ffffffff8db05598 (srcu){.+.+}-{0:0}, at: __mmu_notifier_oom_enter+0x93/0x1f0
[ 399.841992][T10882] ffffffff8db05598 (srcu){.+.+}-{0:0}, at: __mmu_notifier_oom_enter+0x93/0x1f0
[ 399.842005][T10882]
[ 399.842005][T10882] other info that might help us debug this:
[ 399.842005][T10882]
[ 399.842005][T10882] other info that might help us debug this:
[ 399.842006][T10882] Possible unsafe locking scenario:
[ 399.842006][T10882]
[ 399.842006][T10882] Possible unsafe locking scenario:
[ 399.842006][T10882]
[ 399.842008][T10882] CPU0
[ 399.842008][T10882] CPU0
[ 399.842009][T10882] ----
[ 399.842009][T10882] ----
[ 399.842010][T10882] lock(srcu);
[ 399.842010][T10882] lock(srcu);
[ 399.842014][T10882] lock(srcu);
[ 399.842014][T10882] lock(srcu);
[ 399.842017][T10882]
[ 399.842017][T10882] *** DEADLOCK ***
[ 399.842017][T10882]
[ 399.842017][T10882]
[ 399.842017][T10882] *** DEADLOCK ***
[ 399.842017][T10882]
[ 399.842018][T10882] May be due to missing lock nesting notation
[ 399.842018][T10882]
[ 399.842018][T10882] May be due to missing lock nesting notation-------------------------------------------------------------------------------------------------------------------
Then defered the kvm_mmu_notifier_detach() using workqueue, then above issue got fixed.
Testing:
-------------
I've validated the v2 approach with:
Kernel: v7.0-rc2 with PREEMPT_RT and DEBUG_ATOMIC_SLEEP enabled
Test: Triggered OOM conditions that killed a QEMU process with active KVM VM
Use these commands for generating scenario:
1. vng -v -r ./arch/x86/boot/bzImage --qemu-opts='-m 2G -cpu EPYC,+svm,+npt,+tsc,+invtsc -s ' After successfully booting the virtme-ng(QEMU) ------> Act Host VM
2. chmod 666 /dev/kvm
3. dmesg -c > /dev/null
4. launching Guest VM using this command $qemu-system-x86_64 -enable-kvm -m 1000M -mem-prealloc \
-monitor none -serial none -display none -nographic & sleep 10
Results:
-------------------
1. oom_enter callback was successfully invoked
2 No SRCU deadlock warnings
3 No "sleeping function called from invalid context" warnings 4.OOM reaper completed successfully
5. Process was reaped without errors
Question:
Before I send the v2 patch series, I want to confirm this approach aligns with your expectations. Specifically:
Defered this coommon helper kvm_mmu_notifier_detach() for mmu_nottifier_unregister() and unbalanced invalidation using workque is good design? Are there any specific test cases or scenarios you'd like me to validate?
I can send the complete v2 patch series once you confirm this approach is on the right track.
Thanks again for the guidance!
Shaikh Kamal
Quoted 22 lines
Quoted 14 lines
This avoids the whole "convert locks to raw" problem and the complexity of deferring work.
I have questions on Testing part:
------------------------------------
I tried to reproduce the bug scenario using the virtme-ng then running
the stress-ng putting memory pressure on VM, but not able to reproduce
the scenario.
I tried this way ..
vng -v -r./arch/x86/boot/bzImage
VM is up, then running the stress-ng as below
stress-ng --vm 2 --vm-bytes 95% --timeout 20s & sleep 5 & dmesg | tail -30 | grep "sleeping function" OOM Killer is triggered, but exact bug not able to reproduce, Please
suggest how to reproduce this bug, even we need to verify after code
changes which you have suggested.I don't know, sorry. But with this new approach there will always be a call to the new callback from the OOM killer, so it's easier to test.
Thanks,
Paolo
Paolo Bonzini
maintainerIl mer 25 mar 2026, 06:19 shaikh kamaluddin
<[email protected]> ha scritto:
Quoted 5 lines
1. Added
oom_entercallback to structmmu_notifier_opsininclude/linux/mmu_notifier.h
2. Implemented__mmu_notifier_oom_enter()inmm/mmu_notifier.cto invoke registered callbacks
3. Calledmmu_notifier_oom_enter(mm)from__oom_kill_processinmm/oom_kill.cbefore any invalidations
4. As per your suggestion, move existingkvm_destroy_vm()logic that already handles unbalanced invalidation to the new helper functionkvm_mmu_notifier_detach()and invoke it from thekvm_destroy_vm()
This is not fully clear to me... It could be caused by a recursive locking, or also a false positive. It's hard to say without seeing the full backtrace, but seeing "lock(srcu)" is suspicious.
I wouldn't have expected deferral to be necessary; and it seems to me that, if you defer removal to some time after the OOM reaper starts, you'd have the same problem as before with sleeping spinlocks.
Can you post the original patch without deferral?
Paolo
Quoted 5 lines
Key Design Decision:
------------------------------
Implementation point no 4, while testing, Issue I was encountering is a recursive locking problem with the srcu lock, which is being acquired twice in the same context. This happens during the__mmu_notifier_oom_enter()and__synchronize_srcu()calls, leading to a potential deadlock. Please find below log snippet while launching the Guest VM
Quoted 12 lines · Paolo Bonzini
Il mer 25 mar 2026, 06:19 shaikh kamaluddin
<[email protected]> ha scritto:Quoted 5 lines
1. Added
oom_entercallback to structmmu_notifier_opsininclude/linux/mmu_notifier.h
2. Implemented__mmu_notifier_oom_enter()inmm/mmu_notifier.cto invoke registered callbacks
3. Calledmmu_notifier_oom_enter(mm)from__oom_kill_processinmm/oom_kill.cbefore any invalidations
4. As per your suggestion, move existingkvm_destroy_vm()logic that already handles unbalanced invalidation to the new helper functionkvm_mmu_notifier_detach()and invoke it from thekvm_destroy_vm()This is not fully clear to me... It could be caused by a recursive locking, or also a false positive. It's hard to say without seeing the full backtrace, but seeing "lock(srcu)" is suspicious.
Quoted 8 lines
I wouldn't have expected deferral to be necessary; and it seems to me that, if you defer removal to some time after the OOM reaper starts, you'd have the same problem as before with sleeping spinlocks.
Can you post the original patch without deferral?
Paolo
Hi Paolo,
Here's the current implementation without deferral as you requested.
As you suspected, it causes an SRCU deadlock. The callback calls kvm_mmu_notifier_detach() which attempts mmu_notifier_unregister() while __mmu_notifier_oom_enter is holding SRCU.
Kernel log shows:
WARNING: possible recursive locking detected lock(srcu) at __synchronize_srcu
already holding lock at __mmu_notifier_oom_enter
Should the callback simply set a flag (kvm->oom_reaping) and have invalidate_range_start check this flag to return early?
Current implementation (diff attached):
include/linux/mmu_notifier.h +25 -0
@@ -88,6 +88,23 @@ struct mmu_notifier_ops {void (*release)(struct mmu_notifier *subscription,struct mm_struct *mm); +/* +* Called when the OOM reaper is about to reap this mm. +* This is invoked before any invalidation attempts and allows +* the subscriber to handle the fact that OOM reclaim will proceed +* in non-blockable mode. +* +* This callback is optional and is called in atomic context. +* It must not sleep or use any locks that may block. +* +* Common use case: unregister the MMU notifier to avoid being +* called back in non-blockable invalidation context where +* sleeping locks cannot be used. +* +* This is called with a reference held on the mm_struct. +*/ +void (*oom_enter)(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
@@ -375,6 +392,7 @@ mmu_interval_check_retry(struct mmu_interval_notifier *interval_sub,extern void __mmu_notifier_subscriptions_destroy (struct mm_struct *mm);extern void __mmu_notifier_release (struct mm_struct *mm); +extern void __mmu_notifier_oom_enter (struct mm_struct *mm);extern int __mmu_notifier_clear_flush_young (struct mm_struct *mm,unsigned long start,unsigned long end);
@@ -402,6 +420,13 @@ static inline void mmu_notifier_release(struct mm_struct *mm)__mmu_notifier_release (mm); } +static inline void mmu_notifier_oom_enter (struct mm_struct *mm) +{ +if (mm_has_notifiers (mm)) +__mmu_notifier_oom_enter (mm); + +} +static inline int mmu_notifier_clear_flush_young (struct mm_struct *mm,unsigned long start,unsigned long end)
mm/mmu_notifier.c +21 -0
@@ -359,6 +359,27 @@ 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 *subscription; +int id; +pr_info ("Entering :func:%s\n" , __func__); +if (!mm->notifier_subscriptions) +return ; + + id =srcu_read_lock (&srcu); +hlist_for_each_entry_rcu (subscription, + &mm->notifier_subscriptions->list, hlist, +rcu_read_lock_held (&srcu)) { +if (subscription->ops->oom_enter) + subscription->ops->oom_enter (subscription, mm); + + } +srcu_read_unlock (&srcu, id); +pr_info ("Done:%s\n" , __func__); + +} +/* * 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
mm/oom_kill.c +3 -0
@@ -947,6 +947,9 @@ static void __oom_kill_process(struct task_struct *victim, const char *message) mm = victim->mm;mmgrab (mm); +/* Notify MMU notifiers about the OOM event */ +mmu_notifier_oom_enter (mm); +/* Raise event before sending signal: task reaper must see this */ count_vm_event (OOM_KILL );memcg_memory_event_mm (mm,MEMCG_OOM_KILL );
virt/kvm/kvm_main.c +41 -18
77 lines not on this page · raw message
Thanks,
Kamal
Quoted 6 lines
Quoted 5 lines
Key Design Decision:
------------------------------
Implementation point no 4, while testing, Issue I was encountering is a recursive locking problem with the srcu lock, which is being acquired twice in the same context. This happens during the__mmu_notifier_oom_enter()and__synchronize_srcu()calls, leading to a potential deadlock. Please find below log snippet while launching the Guest VM
Paolo Bonzini
maintainerQuoted 8 lines · shaikh kamaluddin
(quoted diff) +8
-0+void __mmu_notifier_oom_enter(struct mm_struct *mm) +{ + struct mmu_notifier *subscription; + int id; + pr_info("Entering :func:%s\n", __func__); + if (!mm->notifier_subscriptions) + return; +
Review
Quoted 10 lines
(quoted diff) +10
-0+ id = srcu_read_lock(&srcu); + hlist_for_each_entry_rcu(subscription, + &mm->notifier_subscriptions->list, hlist, + rcu_read_lock_held(&srcu)) { + if(subscription->ops->oom_enter) + subscription->ops->oom_enter(subscription, mm); + + } + srcu_read_unlock(&srcu, id); + pr_info("Done:%s\n", __func__);
Yeah, calling mmu_notifier_unregister() won't work from within this function.
One possibility is for the new method to be something like this:
void (*after_oom_unregister)(struct mmu_notifier *subscription);
So it only has to do
kvm->mn_registered = false; /* or xchg, it's the same */ 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);or something like that. See the attached sketch, feel free to reuse it as you see fit.
Paolo
arch/x86/kvm/svm/nested.c +5 -2
@@ -1973,12 +1973,15 @@ static gpa_t svm_translate_nested_gpa(struct kvm_vcpu *vcpu, gpa_t gpa, struct x86_exception *exception, u64 pte_access) { +struct vcpu_svm *svm =to_svm (vcpu);struct kvm_mmu *mmu = vcpu->arch.mmu;BUG_ON (!mmu_is_nested (vcpu)); -/* NPT walks are always user-walks */ - access |=PFERR_USER_MASK ; +/* Non-GMET walks are always user-walks */ +if (!(svm->nested.ctl.nested_ctl &SVM_NESTED_CTL_GMET_ENABLE )) + access |=PFERR_USER_MASK ; +return mmu->gva_to_gpa (vcpu, mmu, gpa, access, exception); }
arch/x86/kvm/vmx/nested.c +9 -0
@@ -7444,6 +7444,15 @@ static gpa_t vmx_translate_nested_gpa(struct kvm_vcpu *vcpu, gpa_t gpa,struct kvm_mmu *mmu = vcpu->arch.mmu;BUG_ON (!mmu_is_nested (vcpu)); + +/* +* MBEC differentiates based on the effective U/S bit of +* the guest page tables; not the processor CPL. +*/ + access &= ~PFERR_USER_MASK ; +if ((pte_access &ACC_USER_MASK ) && (access &PFERR_GUEST_FINAL_MASK )) + access |=PFERR_USER_MASK ; +return mmu->gva_to_gpa (vcpu, mmu, gpa, access, exception); }
include/linux/mmu_notifier.h +13 -0
@@ -212,6 +212,14 @@ struct mmu_notifier_ops {*/ struct mmu_notifier *(*alloc_notifier)(struct mm_struct *mm);void (*free_notifier)(struct mmu_notifier *subscription); + +/* +* 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); };/*
@@ -287,6 +295,7 @@ mmu_notifier_get(const struct mmu_notifier_ops *ops, struct mm_struct *mm) }void mmu_notifier_put (struct mmu_notifier *subscription);void mmu_notifier_synchronize (void ); +void mmu_notifier_oom_enter (struct mm_struct *mm);extern int mmu_notifier_register (struct mmu_notifier *subscription,struct mm_struct *mm);
@@ -661,6 +670,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 */
mm/mmu_notifier.c +50 -0
56 lines not on this page · raw message
v2 cover mm/mmu_notifier: Add async OOM cleanup via call_srcu()
This series implements the after_oom_unregister callback design proposed by Paolo in v1 review [1].
The current OOM notifier path calls synchronize_srcu() inline from mmu_notifier_oom_enter(), which can deadlock on PREEMPT_RT when locks such as siglock are held. This series moves the cleanup to an asynchronous context using call_srcu(), allowing the OOM path to proceed without waiting for an SRCU grace period.
Subscribers opt in via a new after_oom_unregister callback in struct mmu_notifier_ops.
KVM is the first (and currently only) user.
Changes since v1 [1]:
- Implement after_oom_unregister callback in struct mmu_notifier_ops as proposed by Paolo
- Add mmu_notifier_oom_enter() to detach subscriptions and schedule cleanup via call_srcu()
- Add mmu_notifier_barrier() (srcu_barrier wrapper) so consumers can wait for pending callbacks during teardown
- Move call site from __oom_kill_process() to __oom_reap_task_mm() to fix KASAN vmalloc-out-of-bounds observed in v1
- Use hlist_del_init() to keep hlist_unhashed() correct for the kvm_destroy_vm() detection path, avoiding use-after-free on the stack-allocated oom_list head
- Add KVM after_oom_unregister implementation to clear mn_active_invalidate_count
- Update kvm_destroy_vm() to detect detached subscriptions via hlist_unhashed() and use mmu_notifier_barrier() + mmdrop() instead of mmu_notifier_unregister()
- Remove pr_err() on GFP_ATOMIC failure per checkpatch; the trade-off is documented inline
Testing
-------
Developed and tested under virtme-ng with PREEMPT_RT, KASAN, and lockdep enabled.
Test setup:
- simple_kvm.c: minimal userspace program that opens /dev/kvm, creates a VM, registers memory, creates a vCPU, and sleeps
- CONFIG_DEBUG_VM-only debugfs interface (not part of this submission) at /sys/kernel/debug/oom_reap_task to invoke __oom_reap_task_mm() on a target task
Test sequence:
$ ./simple_kvm & $ echo $! | sudo tee /sys/kernel/debug/oom_reap_task
Observed with patch applied:
- __oom_reap_task_mm() completes
- mmu_notifier_oom_enter() detaches the KVM subscription
- call_srcu() callback runs after (SRCU grace period)
- KVM after_oom_unregister clears mn_active_invalidate_count
- mmu_notifier_barrier() returns cleanly
- No KASAN reports, no kernel BUGs, lockdep clean
Stress runs (20 iterations) showed consistent results.
Reproducing the syzbot-reported issue
-------------------------------------
The issue reported by syzbot is reproducible on an unpatched PREEMPT_RT kernel, triggering a "sleeping function called from invalid context" warning in kvm_mmu_notifier_invalidate_range_start(). With this patch applied, the warning is no longer observed..
Known limitations
-----------------
Failure of GFP_ATOMIC allocation in mmu_notifier_oom_enter() causes the corresponding after_oom_unregister callback to be skipped. The OOM path cannot sleep without reintroducing the deadlock this series fixes, and synchronous execution would require waiting for SRCU readers. Cleanup still occurs later via the normal unregister path. A mempool-backed allocator could address this in the future.
[1] https://lore.kernel.org/all/CABgObfZQM0Eq1=vzm812D+CAcjOaE1f1QAUqGo5rTzXgLnR9cQ@mail.gmail.com
shaikh.kamal (1):
mm/mmu_notifier: Add async OOM cleanup via call_srcu()
include/linux/mmu_notifier.h | 10 +++ mm/mmu_notifier.c | 123 +++++++++++++++++++++++++++++++++++ mm/oom_kill.c | 3 + virt/kvm/kvm_main.c | 27 +++++++- 4 files changed, 162 insertions(+), 1 deletion(-)
--
2.43.0
Re: [PATCH v2 0/1] mm/mmu_notifier: Add async OOM cleanup via call_srcu()
Quoted 72 lines · shaikh.kamal
This series implements the
after_oom_unregistercallback design proposed by Paolo in v1 review [1].The current OOM notifier path calls
synchronize_srcu()inline frommmu_notifier_oom_enter(), which can deadlock onPREEMPT_RTwhen locks such as siglock are held. This series moves the cleanup to an asynchronous context usingcall_srcu(), allowing the OOM path to proceed without waiting for an SRCU grace period.Subscribers opt in via a new
after_oom_unregistercallback in structmmu_notifier_ops.KVM is the first (and currently only) user.
Changes since v1 [1]:
- Implementafter_oom_unregistercallback in structmmu_notifier_opsas proposed by Paolo
- Addmmu_notifier_oom_enter()to detach subscriptions and schedule cleanup viacall_srcu()
- Addmmu_notifier_barrier()(srcu_barrierwrapper) so consumers can wait for pending callbacks during teardown
- Move call site from__oom_kill_process()to__oom_reap_task_mm()to fix KASAN vmalloc-out-of-bounds observed in v1
- Usehlist_del_init()to keephlist_unhashed()correct for thekvm_destroy_vm()detection path, avoiding use-after-free on the stack-allocatedoom_listhead
- Add KVMafter_oom_unregisterimplementation to clearmn_active_invalidate_count
- Updatekvm_destroy_vm()to detect detached subscriptions viahlist_unhashed()and usemmu_notifier_barrier()+mmdrop()instead ofmmu_notifier_unregister()
- Removepr_err()onGFP_ATOMICfailure per checkpatch; the trade-off is documented inlineTesting
-------Developed and tested under virtme-ng with
PREEMPT_RT, KASAN, and lockdep enabled.Test setup:
-simple_kvm.c: minimal userspace program that opens/dev/kvm, creates a VM, registers memory, creates a vCPU, and sleeps
- CONFIG_DEBUG_VM-only debugfs interface (not part of this submission) at/sys/kernel/debug/oom_reap_taskto invoke__oom_reap_task_mm()on a target taskTest sequence:
$ ./simple_kvm & $ echo $! | sudo tee /sys/kernel/debug/oom_reap_taskObserved with patch applied:
-__oom_reap_task_mm()completes
-mmu_notifier_oom_enter()detaches the KVM subscription
-call_srcu()callback runs after (SRCU grace period)
- KVMafter_oom_unregisterclearsmn_active_invalidate_count
-mmu_notifier_barrier()returns cleanly
- No KASAN reports, no kernel BUGs, lockdep cleanStress runs (20 iterations) showed consistent results.
Reproducing the syzbot-reported issue
-------------------------------------
The issue reported by syzbot is reproducible on an unpatchedPREEMPT_RTkernel, triggering a "sleeping function called from invalid context" warning inkvm_mmu_notifier_invalidate_range_start(). With this patch applied, the warning is no longer observed..Known limitations
-----------------
Quoted 8 lines
Failure of
GFP_ATOMICallocation inmmu_notifier_oom_enter()causes the correspondingafter_oom_unregistercallback to be skipped. The OOM path cannot sleep without reintroducing the deadlock this series fixes, and synchronous execution would require waiting for SRCU readers. Cleanup still occurs later via the normal unregister path. A mempool-backed allocator could address this in the future.
Hi Paolo, Lorenzo, Sean,
Gentle ping on v2, which implements the after_oom_unregister approach Paolo suggested on v1:
https://lore.kernel.org/all/[email protected]/
The one outstanding issue was a build failure on !CONFIG_MMU_NOTIFIER (arc/hexagon allnoconfig) from the kernel test robot, now fixed by adding the missing mmu_notifier_oom_enter() stub:
https://lore.kernel.org/all/[email protected]/
So it builds clean and is ready for review whenever you have bandwidth.
Thanks,
Kamal
Quoted 18 lines
[1] https://lore.kernel.org/all/CABgObfZQM0Eq1=vzm812D+CAcjOaE1f1QAUqGo5rTzXgLnR9cQ@mail.gmail.com
Reported-by [email protected]Tested-by Shaikh Kamaluddin <[email protected]>shaikh.kamal (1):
mm/mmu_notifier: Add async OOM cleanup viacall_srcu()include/linux/mmu_notifier.h | 10 +++ mm/mmu_notifier.c | 123 +++++++++++++++++++++++++++++++++++ mm/oom_kill.c | 3 + virt/kvm/kvm_main.c | 27 +++++++- 4 files changed, 162 insertions(+), 1 deletion(-)--
2.43.0
PATCH v2 1/1 mm/mmu_notifier: Add async OOM cleanup via call_srcu()
In reply to Paolo BonziniWhen an mm undergoes OOM kill, the OOM reaper unmaps memory while holding the mmap_lock. MMU notifier subscribers (notably KVM) need to be informed so they can tear down their secondary mappings. The current synchronous unregister path can deadlock on PREEMPT_RT because synchronize_srcu() is called from contexts that cannot safely sleep.
This patch implements 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().
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.
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.
after_oom_unregister is mutually exclusive with alloc_notifier because allocated notifiers can have additional outstanding references that the OOM path cannot safely drop.
KVM is updated to provide after_oom_unregister, which clears mn_active_invalidate_count, and to detect via hlist_unhashed() in kvm_destroy_vm() when its subscription was already detached by the OOM path; in that case it calls mmu_notifier_barrier() and drops the mm reference rather than calling mmu_notifier_unregister().
--- include/linux/mmu_notifier.h | 10 +++ mm/mmu_notifier.c | 123 +++++++++++++++++++++++++++++++++++ mm/oom_kill.c | 3 + virt/kvm/kvm_main.c | 27 +++++++- 4 files changed, 162 insertions(+), 1 deletion(-)
include/linux/mmu_notifier.h +10 -0
@@ -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
@@ -375,6 +383,8 @@ mmu_interval_check_retry(struct mmu_interval_notifier *interval_sub,extern void __mmu_notifier_subscriptions_destroy (struct mm_struct *mm);extern void __mmu_notifier_release (struct mm_struct *mm); +void mmu_notifier_oom_enter (struct mm_struct *mm); +extern void mmu_notifier_barrier (void );extern int __mmu_notifier_clear_flush_young (struct mm_struct *mm,unsigned long start,unsigned long end);
mm/mmu_notifier.c +123 -0
138 lines not on this page · raw message
mm/oom_kill.c +3 -0
@@ -519,6 +519,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
virt/kvm/kvm_main.c +26 -1
@@ -885,6 +885,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,
@@ -892,6 +910,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)
@@ -1280,7 +1299,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
--
2.43.0