Skip to content

Fix host panic from cross-thread hv_vcpu_destroy - #252

Merged
jserv merged 1 commit into
mainfrom
fix-panic
Jul 30, 2026
Merged

Fix host panic from cross-thread hv_vcpu_destroy#252
jserv merged 1 commit into
mainfrom
fix-panic

Conversation

@jserv

@jserv jserv commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

HVF vCPUs are thread-affine: only the creating thread may run or destroy a vCPU. guest_destroy could call hv_vcpu_destroy on a worker's vCPU from the main thread when the worker was still live past the join cap, freeing the kernel vCPU object out from under its owning thread. The owner's next kernel-side access read the freed, zone-poisoned struct at offset 0x8 and panicked the host (data abort, translation fault, seen twice with an identical backtrace on macOS 26.5.2 / xnu-12377).

Close every teardown-vs-live-vCPU window:

  • thread_destroy_all_vcpus destroys only the main vCPU (owned by the calling thread) and reports any live worker via a live_workers_left out-param instead of destroying it cross-thread. Every active non-main slot counts as live, including a worker still in bring-up (active, vcpu_valid not yet published) that is about to enter its own vCPU.
  • guest_destroy defers all remaining HVF teardown (vcpu destroy, hv_vm_unmap, hv_vm_destroy, munmap of the slab) to process exit when a worker is live -- the one operation that safely stops a foreign vCPU. It is always terminal: main() and the fork-child return straight into process exit after it.
  • Every worker-exit and bring-up-failure path destroys the vCPU under thread_lock and only then clears vcpu_valid / publishes vm_exited, so a teardown scan or a parent wait4 reap can never observe an active-or-reapable slot whose vCPU is still live.

Also resolves the tracked guest_destroy-vs-worker teardown data race (a TSAN-caught unmap racing a live worker): the deferral returns before any teardown write. The residual stall is confirmed to come from uninterruptible host syscalls (blocking fcntl file locks, fsync); the deferral is the correct handling.

Close #247


Summary by cubic

Fixes host panics from cross-thread hv_vcpu_destroy by ensuring teardown never destroys a foreign vCPU and defers VM teardown when any worker is live or in bring-up. Also resolves the TSAN unmap‑vs‑worker race in guest_destroy.

  • Bug Fixes

    • thread_destroy_all_vcpus destroys only the main vCPU and reports live_workers_left when any non‑main slot is active (including bring‑up with vcpu_valid=false); slots that already published vm_exited are not counted live.
    • guest_destroy defers hv_vm_unmap/hv_vm_destroy/slab munmap (and any cross‑thread vCPU destroy) to process exit if a worker is live; the main vCPU handle is cleared in all cases.
    • Worker exit and bring‑up failure paths destroy their own vCPU under thread_lock before clearing vcpu_valid/publishing vm_exited; table‑scan interrupts (thread_interrupt_all, thread_quiesce_siblings, PTRACE_INTERRUPT) now call hv_vcpus_exit while holding the lock.
    • cleanup_main_resources always runs host‑only cleanup even when teardown is deferred, so mounts and temp ELF files are not orphaned.
    • Tests: added test-teardown-live-vcpu-host and test-teardown-live-vcpu (spin/bringup), wired into make check and tests/test-matrix.sh.
  • Migration

    • guest_destroy is terminal. If any worker remains live, HV teardown is deferred to process exit; do not reuse the process to start another VM.

Written for commit eeec2f4. Summary will update on new commits.

Review in cubic

@jserv
jserv requested review from Max042004 and doanbaotrung July 30, 2026 00:58
cubic-dev-ai[bot]

This comment was marked as resolved.

@doanbaotrung

doanbaotrung commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Note

This is a race condition fix, so 260 clean runs is strong evidence but not mathematical proof of absence

elfuse fix-panic (commit 9f98e59) verification

summary + full per-iteration logs for both the elfuse-native stress test (200 runs) and the mup-level multithreaded-guest test (60 runs).

elfuse-fix-panic-verification.log

elfuse fix-panic verification
==============================
Date: Thu Jul 30 19:55:19 +07 2026
Host: macOS 26.5.2, Darwin Kernel Version 25.5.0: Tue Jun  9 22:28:24 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6020
elfuse commit: 9f98e595424a6e93173c9a7b9689daf13ec1fda1
elfuse subject: Fix host panic from cross-thread hv_vcpu_destroy

1) elfuse's own host-side regression test (accounting-only, no HVF calls):
   ---
test-teardown-live-vcpu-host: thread_destroy_all_vcpus accounting
  all checks passed
   exit=0

2) elfuse's own live-HVF integration test, 100x each mode (200 runs total):
   See elfuse-teardown-stress.log for full per-iteration detail.
macOS: 26.5.2 (Darwin Kernel Version 25.5.0: Tue Jun  9 22:28:24 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6020)
finished: Thu Jul 30 19:46:53 +07 2026
=== overall fail=0 (0 = all 200 runs exited 42 as expected) ===

3) mup (this project) running a real multithreaded guest binary
   (fork-from-worker-thread) through the fixed elfuse, 60 iterations:
   See mup-mt-fork-exec-stress.log for full per-iteration detail.
iter 60 exit=0
finished: Thu Jul 30 19:54:42 +07 2026
=== overall fail=0 ===

4) macOS diagnostic/panic log check across the full test window: no entries.

Result: 260/260 total runs (200 elfuse-native + 60 mup-level) completed
cleanly with expected exit codes. No host panic, no diagnostic report,
no crash of any kind observed.

elfuse-teardown-stress.log

=== Stress run: 100x spin + 100x bringup ===
started: Thu Jul 30 19:46:25 +07 2026
elfuse commit: 9f98e595424a6e93173c9a7b9689daf13ec1fda1 (Fix host panic from cross-thread hv_vcpu_destroy)
macOS: 26.5.2 (Darwin Kernel Version 25.5.0: Tue Jun  9 22:28:24 PDT 2026; root:xnu-12377.121.10~1/RELEASE_ARM64_T6020)
finished: Thu Jul 30 19:46:53 +07 2026
=== overall fail=0 (0 = all 200 runs exited 42 as expected) ===

mup-mt-fork-exec-stress.log

=== test_mt_fork_exec stress: 40 iterations ===
started: Thu Jul 30 19:49:21 +07 2026
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 1 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 2 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 3 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 4 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 5 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 6 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 7 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 8 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 9 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 10 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 11 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 12 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 13 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 14 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 15 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 16 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 17 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 18 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 19 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 20 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 21 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 22 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 23 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 24 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 25 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 26 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 27 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 28 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 29 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 30 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 31 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 32 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 33 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 34 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 35 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 36 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 37 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 38 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 39 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 40 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 41 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 42 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 43 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 44 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 45 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 46 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 47 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 48 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 49 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 50 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 51 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 52 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 53 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 54 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 55 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 56 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 57 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 58 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 59 exit=0
[main] Starting test_mt_fork_exec
[thread] Forking from helper thread...
[thread] Child process exited with status 256
[main] Finished test_mt_fork_exec
iter 60 exit=0
finished: Thu Jul 30 19:54:42 +07 2026
=== overall fail=0 ===

HVF vCPUs are thread-affine: only the creating thread may run or destroy
a vCPU. guest_destroy could call hv_vcpu_destroy on a worker's vCPU from
the main thread when the worker was still live past the join cap, freeing
the kernel vCPU object out from under its owning thread. The owner's next
kernel-side access read the freed, zone-poisoned struct at offset 0x8 and
panicked the host (data abort, translation fault, seen twice with an
identical backtrace on macOS 26.5.2 / xnu-12377).

Close every teardown-vs-live-vCPU window:
- thread_destroy_all_vcpus destroys only the main vCPU (owned by the
  calling thread) and reports any live worker via a live_workers_left
  out-param instead of destroying it cross-thread. An active non-main
  slot counts as live -- including a worker still in bring-up (active,
  vcpu_valid not yet published) about to enter its own vCPU -- unless it
  has already published vm_exited (a vm-clone child that self-destroyed
  its vCPU and only stays active for wait4), which holds no live vCPU.
- guest_destroy defers all remaining HVF teardown (vcpu destroy,
  hv_vm_unmap, hv_vm_destroy, munmap of the slab) to process exit when a
  worker is live: the one operation that safely stops a foreign vCPU.
  It is always terminal: main() and the fork-child return straight into
  process exit after it.
- Every worker-exit and bring-up-failure path destroys the vCPU under
  thread_lock and only then clears vcpu_valid / publishes vm_exited, so
  a teardown scan or a parent wait4 reap can never observe an
  active-or-reapable slot whose vCPU is still live.
- The table-scan interrupt paths (thread_interrupt_all,
  thread_quiesce_siblings, PTRACE_INTERRUPT) issue hv_vcpus_exit under
  thread_lock rather than after releasing it, so a handle collected under
  the lock cannot be handed to HVF after a worker frees it -- the same
  freed-vCPU fault via hv_vcpus_exit instead of hv_vcpu_destroy.

Also resolves the tracked guest_destroy-vs-worker teardown data race (a
TSAN-caught unmap racing a live worker): the deferral returns before any
teardown write. The residual stall is confirmed to come from
uninterruptible host syscalls (blocking fcntl file locks, fsync); the
deferral is the correct handling.

Close #247
@jserv
jserv merged commit 543ae67 into main Jul 30, 2026
10 checks passed
@jserv
jserv deleted the fix-panic branch July 30, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kernel panic (data abort, near-null dereference) hit twice with an identical backtrace during normal elfuse/HVF guest execution

2 participants