vmm: avoid pause deadlock on CPU hotplug MMIO - #8092
Merged
rbradford merged 1 commit intoApr 24, 2026
Merged
Conversation
1 task
# TL;DR In cloud-hypervisor#7990 we replaced the old deadlock with another deadlock. This commit finally resolves (hopefully) all dead locks on that code path by not holding `CpuManager::vcpu_states` while waiting for vCPU pause acknowledgements. A vCPU can receive the pause kick while servicing the ACPI CPU hotplug MMIO device, and that MMIO path also needs `vcpu_states`. Holding the mutex across the wait phase deadlocks pause against that MMIO access. # Problem `signal_vcpus()` used to lock `CpuManager::vcpu_states` for the whole function, signal every vCPU, and then wait for each vCPU to acknowledge the kick. That lock scope is too wide. A vCPU is allowed to observe the kick in userspace rather than returning directly from `KVM_RUN`. During boot, `vcpu0` can be in an MMIO access on the ACPI CPU hotplug device when pause arrives. `AcpiCpuHotplugController::read()` and `write()` both lock `vcpu_states` to inspect or update the selected vCPU state. The deadlock looks like this: VMM thread vCPU thread ---------- ---------- lock(vcpu_states) signal_vcpus() wait for ack ---------------------> receives pause kick enters ACPI CPU hotplug MMIO lock(vcpu_states) [blocks] wait for ack <--------------------- cannot set vcpu_run_interrupted The VMM thread waits for `vcpu_run_interrupted` to flip, but the vCPU cannot reach the pause acknowledgement path because it is sleeping on the same mutex. The debug logs matched that cycle exactly: signal delivery kept working, `vcpu0` stayed in one unmatched `run()` invocation, the stuck thread sampled in `futex_do_wait`, and the backtrace pointed at `AcpiCpuHotplugController::read()`. # Reproducer This was reproducible by continuously issuing `pause()` / `resume()` from while a Linux guest was still booting. That boot-tim window reliably exercises the ACPI CPU hotplug MMIO access that participates in the deadlock. Once the guest had finished booting, the problem became much harder to trigger (as there is no MMIO operation without explicit CPU plugging). # Solution Keep the existing two-phase behavior so all vCPUs are still signalled before the wait phase, but narrow the lifetime of the `vcpu_states` mutex. Reacquire it only long enough to access one `VcpuState` at a time in each phase. That preserves the original pause semantics and the fast signal-all / wait-all structure, while removing the lock inversion with the ACPI CPU hotplug MMIO path. This also remains safe if a vCPU is hot-removed while pause is in progress. Hot-remove does not shrink `vcpu_states`; it stops the thread and clears the `VcpuState` handle in place. `signal_vcpus()` can therefore snapshot the vector length up front, and if a vCPU disappears between the signal and wait phases, `wait_until_signal_acknowledged()` will observe `handle.is_none()` and return successfully. The interruption handshake itself lives in atomics inside each `VcpuState`. The outer mutex is only needed to reach the state objects, not to keep the acknowledgement protocol correct. Dropping the mutex between iterations therefore does not weaken the pause protocol, but it does allow MMIO handlers and other `vcpu_states` users to make forward progress while the VMM waits for the kick to be observed. On-behalf-of: SAP [email protected] Signed-off-by: Philipp Schuster <[email protected]>
phip1611
force-pushed
the
upstream-fix-cpu-deadlock
branch
from
April 24, 2026 09:22
05f5caa to
2482f56
Compare
rbradford
enabled auto-merge
April 24, 2026 09:38
phip1611
added a commit
to phip1611/cloud-hypervisor
that referenced
this pull request
Apr 28, 2026
This once and for all finally resolves the deadlock that was only partially addressed in cloud-hypervisor#7990 (dfe78a3 and following) and cloud-hypervisor#8092 (2482f56). The first one laid the important basic plumbing but actually just nararrowd down the deadlock. The second PR tightened the new race window further - but never resolved it. This commit removes the Mutex around `Vec<VcpuState>`, which was recently introduced in cloud-hypervisor#7990 (dfe78a3). The deadlock was: - CpuManager::pause() -> VcpuState::wait_until_signal_acknowleded() - Mutex is held - vCPU could not do MMIO on AcpiCpuHotplugController and therefore never acknowledge the signal. The Mutex around `Vec<VcpuState>` was introduced because: - CpuManager sometimes needs access to all vCPU states (the whole vec) or individual ones - AcpiPciHotplugController needs access to individual vCPU states The mutating operations caused the Mutex in the first place where: - setting `handle: Option<std::thread::Handle>` - from `None` -> `Some` in `CpuManager::start_vcpu()` - from `Some` -> `None`: - in `AcpiCpuHotplugController::remove_vcpu()` - in `CpuManager::shutdown()` - the two bools `inserting` and `removing`, which were guest-visible and used to check if guest ACKed CPU hotplug events I resolved that by moving the Mutex around the Option to the thread handle and switching the two raw bools to `AtomicBool`. After comprehensive analysis, I am confident this is a proper and clean solution that doesn't introduce any new (critical) races. On-behalf-of: SAP [email protected] Signed-off-by: Philipp Schuster <[email protected]>
1 task
phip1611
added a commit
to phip1611/cloud-hypervisor
that referenced
this pull request
May 15, 2026
TL;DR: fix deadlock
CpuManager::pause() signals all vCPUs and then waits until each run loop
acknowledges the signal through a shared bool. That can deadlock against
ACPI CPU hotplug MMIO:
VMM thread vCPU thread
---------- ----------
pause()
signal_vcpus()
read-lock selected vCPU state
wait for acknowledgement -------> exits KVM_RUN on signal
enters ACPI CPU hotplug MMIO
write-locks same state [blocks]
keeps waiting <------ cannot finish MMIO or ack signal
This was the case before v51 but also the partial fixes introduced in
v51 [0] [1]. Since the previous commit introduces per-vcpu-locking, we
now can refactor wait_until_signal_acknowledged() in a way that it
locks and releases the lock again on every loop iteration, allowing
forward progress on the MMIO path.
This change is needed as AcpiCpuHotplugManager::remove_vcpu() needs
mutable access to the underlying structure. Otherwise, the existing
read lock would be sufficient.
[0] cloud-hypervisor#7990
[1] cloud-hypervisor#8092
On-behalf-of: SAP [email protected]
Signed-off-by: Philipp Schuster <[email protected]>
phip1611
added a commit
to phip1611/cloud-hypervisor
that referenced
this pull request
May 15, 2026
TL;DR: fix deadlock
CpuManager::pause() signals all vCPUs and then waits until each run loop
acknowledges the signal through a shared bool. That can deadlock against
ACPI CPU hotplug MMIO:
VMM thread vCPU thread
---------- ----------
pause()
signal_vcpus()
read-lock selected vCPU state
wait for acknowledgement -------> exits KVM_RUN on signal
enters ACPI CPU hotplug MMIO
write-locks same state [blocks]
keeps waiting <------ cannot finish MMIO or ack signal
This was the case before v51 but also the partial fixes introduced in
v51 [0] [1]. Since the previous commit introduces per-vcpu-locking, we
now can refactor wait_until_signal_acknowledged() in a way that it
locks and releases the lock again on every loop iteration, allowing
forward progress on the MMIO path.
This change is needed as AcpiCpuHotplugManager::remove_vcpu() needs
mutable access to the underlying structure. Otherwise, the existing
read lock would be sufficient.
[0] cloud-hypervisor#7990
[1] cloud-hypervisor#8092
On-behalf-of: SAP [email protected]
Signed-off-by: Philipp Schuster <[email protected]>
phip1611
added a commit
to phip1611/cloud-hypervisor
that referenced
this pull request
May 18, 2026
TL;DR: fix deadlock
CpuManager::pause() signals all vCPUs and then waits until each run loop
acknowledges the signal through a shared bool. That can deadlock against
ACPI CPU hotplug MMIO:
VMM thread vCPU thread
---------- ----------
pause()
signal_vcpus()
read-lock selected vCPU state
wait for acknowledgement -------> exits KVM_RUN on signal
enters ACPI CPU hotplug MMIO
write-locks same state [blocks]
keeps waiting <------ cannot finish MMIO or ack signal
This was the case before v51 but also the partial fixes introduced in
v51 [0] [1]. Since the previous commit introduces per-vcpu-locking, we
now can refactor wait_until_signal_acknowledged() in a way that it
locks and releases the lock again on every loop iteration, allowing
forward progress on the MMIO path.
This change is needed as AcpiCpuHotplugManager::remove_vcpu() needs
mutable access to the underlying structure. Otherwise, the existing
read lock would be sufficient.
[0] cloud-hypervisor#7990
[1] cloud-hypervisor#8092
On-behalf-of: SAP [email protected]
Signed-off-by: Philipp Schuster <[email protected]>
phip1611
added a commit
to phip1611/cloud-hypervisor
that referenced
this pull request
May 18, 2026
TL;DR: fix deadlock
CpuManager::pause() signals all vCPUs and then waits until each run loop
acknowledges the signal through a shared bool. That can deadlock against
ACPI CPU hotplug MMIO:
VMM thread vCPU thread
---------- ----------
pause()
signal_vcpus()
read-lock selected vCPU state
wait for acknowledgement -------> exits KVM_RUN on signal
enters ACPI CPU hotplug MMIO
write-locks same state [blocks]
keeps waiting <------ cannot finish MMIO or ack signal
This was the case before v51 but also the partial fixes introduced in
v51 [0] [1]. Since the previous commit introduces per-vcpu-locking, we
now can refactor wait_until_signal_acknowledged() in a way that it
locks and releases the lock again on every loop iteration, allowing
forward progress on the MMIO path.
This change is needed as AcpiCpuHotplugManager::remove_vcpu() needs
mutable access to the underlying structure. Otherwise, the existing
read lock would be sufficient.
[0] cloud-hypervisor#7990
[1] cloud-hypervisor#8092
On-behalf-of: SAP [email protected]
Signed-off-by: Philipp Schuster <[email protected]>
phip1611
added a commit
to phip1611/cloud-hypervisor
that referenced
this pull request
Jun 2, 2026
TL;DR: fix deadlock
CpuManager::pause() signals all vCPUs and then waits until each run loop
acknowledges the signal through a shared bool. That can deadlock against
ACPI CPU hotplug MMIO:
VMM thread vCPU thread
---------- ----------
pause()
signal_vcpus()
read-lock selected vCPU state
wait for acknowledgement -------> exits KVM_RUN on signal
enters ACPI CPU hotplug MMIO
write-locks same state [blocks]
keeps waiting <------ cannot finish MMIO or ack signal
This was the case before v51 but also the partial fixes introduced in
v51 [0] [1]. Since the previous commit introduces per-vcpu-locking, we
now can refactor wait_until_signal_acknowledged() in a way that it
locks and releases the lock again on every loop iteration, allowing
forward progress on the MMIO path.
This change is needed as AcpiCpuHotplugManager::remove_vcpu() needs
mutable access to the underlying structure. Otherwise, the existing
read lock would be sufficient.
[0] cloud-hypervisor#7990
[1] cloud-hypervisor#8092
On-behalf-of: SAP [email protected]
Signed-off-by: Philipp Schuster <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix of #7990 - 7990 introduced another race condition causing a deadlock. I hope the situation is now resolved once and for all.
TL;DR
In #7990 we replaced the old deadlock with another deadlock. This commit finally resolves (hopefully) all dead locks on that code path by not holding
CpuManager::vcpu_stateswhile waiting for vCPU pause acknowledgements. A vCPU can receive the pause kick while servicing the ACPI CPU hotplug MMIO device, and that MMIO path also needsvcpu_states. Holding the mutex across the wait phase deadlocks pause against that MMIO access.Problem
signal_vcpus()used to lockCpuManager::vcpu_statesfor the whole function, signal every vCPU, and then wait for each vCPU to acknowledge the kick.That lock scope is too wide. A vCPU is allowed to observe the kick in userspace rather than returning directly from
KVM_RUN. During boot,vcpu0can be in an MMIO access on the ACPI CPU hotplug device when pause arrives.AcpiCpuHotplugController::read()andwrite()both lockvcpu_statesto inspect or update the selected vCPU state.The deadlock looks like this:
The VMM thread waits for
vcpu_run_interruptedto flip, but the vCPU cannot reach the pause acknowledgement path because it is sleeping on the same mutex.The debug logs matched that cycle exactly: signal delivery kept working,
vcpu0stayed in one unmatchedrun()invocation, the stuck thread sampled infutex_do_wait, and the backtrace pointed atAcpiCpuHotplugController::read().Reproducer
This was reproducible by continuously issuing
pause()/resume()from while a Linux guest was still booting. That boot-tim window reliably exercises the ACPI CPU hotplug MMIO access that participates in the deadlock. Once the guest had finished booting, the problem became much harder to trigger (as there is no MMIO operation without explicit CPU plugging).Solution
Keep the existing two-phase behavior so all vCPUs are still signalled before the wait phase, but narrow the lifetime of the
vcpu_statesmutex. Reacquire it only long enough to access oneVcpuStateat a time in each phase.That preserves the original pause semantics and the fast signal-all / wait-all structure, while removing the lock inversion with the ACPI CPU hotplug MMIO path.
This also remains safe if a vCPU is hot-removed while pause is in progress. Hot-remove does not shrink
vcpu_states; it stops the thread and clears theVcpuStatehandle in place.signal_vcpus()can therefore snapshot the vector length up front, and if a vCPU disappears between the signal and wait phases,wait_until_signal_acknowledged()will observehandle.is_none()and return successfully.The interruption handshake itself lives in atomics inside each
VcpuState. The outer mutex is only needed to reach the state objects, not to keep the acknowledgement protocol correct. Dropping the mutex between iterations therefore does not weaken the pause protocol, but it does allow MMIO handlers and othervcpu_statesusers to make forward progress while the VMM waits for the kick to be observed.