Summary
Observed a SIGSEGV inside pci_func_send_intx_irq on a threadpool worker thread, matching a use-after-free pattern: the worker was firing an INTx IRQ through a pci_func_t* whose owning machine was being torn down concurrently.
Reproducibility: intermittent, tens-of-minutes-into-a-session timing. Symptoms only appear when the emulator host repeatedly stops-and-frees machines while other machines are running. In my host (ScalarEvolution Minecraft mod), destroying a computer block while a VM is live reliably reproduces it within a few cycles.
Stack
C [librvvm.so+0x23b18] pci_func_send_intx_irq
C [librvvm.so+0x4471f] threadpool_worker
C [libc.so.6+0x9dd53] pthread start / clone
Faulting instruction
RDX = 0x00007fd351443300, si_addr = 0x00007fd351443308, si_code = SEGV_ACCERR.
ACCERR (not MAPERR) means the memory is mapped but with bad permissions — consistent with the allocator having returned the freed chunk and remapped it as a guard/PROT_NONE page while a pointer to it was still in flight.
Proposed cause
pci_func_send_intx_irq takes a pci_func_t* and dereferences a field at offset +0x8 without synchronizing against teardown. A threadpool worker that was already inside the dispatch when rvvm_machine_free started walking PCI devices will touch a freed struct.
Suggested fix directions
Any of the following would close the window:
- Refcount on
pci_func_t — threadpool jobs take a ref; teardown waits for outstanding refs to drain before freeing. Cleanest but largest surface-area change.
shutting_down flag on the bus/machine — pci_func_send_intx_irq early-returns if the machine is being torn down. Needs to be paired with an mfence or atomic load, but much cheaper than refcounting.
- Drain threadpool before freeing PCI state —
rvvm_machine_free blocks on threadpool_wait_all before any pci_func_t is freed. Simplest if the threadpool supports a "drain now" primitive.
Repro environment
- Using
pufit/RVVM staging (a close fork of LekKit upstream), tip 47b2d5b.
- Linux x86_64, glibc 2.42, Java 21 (host wraps RVVM via JNI).
- Machine config: 3 harts, 512 MiB RAM, HDA + rtl8169 + NVMe + flash. Crash reproduces regardless of guest OS; seen with both buildroot and Alpine riscv64.
Happy to minimize this into a standalone C repro if that'd help — it would roughly look like "spawn N machines with PCI devices in one thread, call rvvm_machine_free on them concurrently from another thread while IRQs are still being fired".
Summary
Observed a SIGSEGV inside
pci_func_send_intx_irqon a threadpool worker thread, matching a use-after-free pattern: the worker was firing an INTx IRQ through apci_func_t*whose owning machine was being torn down concurrently.Reproducibility: intermittent, tens-of-minutes-into-a-session timing. Symptoms only appear when the emulator host repeatedly stops-and-frees machines while other machines are running. In my host (ScalarEvolution Minecraft mod), destroying a computer block while a VM is live reliably reproduces it within a few cycles.
Stack
Faulting instruction
RDX = 0x00007fd351443300,si_addr = 0x00007fd351443308,si_code = SEGV_ACCERR.ACCERR(notMAPERR) means the memory is mapped but with bad permissions — consistent with the allocator having returned the freed chunk and remapped it as a guard/PROT_NONEpage while a pointer to it was still in flight.Proposed cause
pci_func_send_intx_irqtakes apci_func_t*and dereferences a field at offset +0x8 without synchronizing against teardown. A threadpool worker that was already inside the dispatch whenrvvm_machine_freestarted walking PCI devices will touch a freed struct.Suggested fix directions
Any of the following would close the window:
pci_func_t— threadpool jobs take a ref; teardown waits for outstanding refs to drain before freeing. Cleanest but largest surface-area change.shutting_downflag on the bus/machine —pci_func_send_intx_irqearly-returns if the machine is being torn down. Needs to be paired with an mfence or atomic load, but much cheaper than refcounting.rvvm_machine_freeblocks onthreadpool_wait_allbefore anypci_func_tis freed. Simplest if the threadpool supports a "drain now" primitive.Repro environment
pufit/RVVMstaging (a close fork of LekKit upstream), tip 47b2d5b.Happy to minimize this into a standalone C repro if that'd help — it would roughly look like "spawn N machines with PCI devices in one thread, call
rvvm_machine_freeon them concurrently from another thread while IRQs are still being fired".