Skip to content

pci_func_send_intx_irq: use-after-free race with rvvm_machine_free during in-flight IRQ dispatch #208

Description

@SolAstrius

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

mov    rcx, [rdx+0x8]

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:

  1. 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.
  2. shutting_down flag on the bus/machinepci_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.
  3. Drain threadpool before freeing PCI statervvm_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".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions