heap-use-after-free in ivshmem-doorbell process_msg_connect
## Host environment
- Operating system: Linux
- OS/kernel version: 5.15.0-94-generic #104-Ubuntu SMP Tue Jan 9 15:25:40 UTC 2024
- Architecture: x86_64
- QEMU flavor: qemu-system-x86_64
- QEMU version: v11.0.0-1699-g345d23d774 (git master, affected since v10.0.0)
- QEMU command line: qemu-system-x86_64 -M pc -m 256M -accel tcg -display none -serial none -monitor none -nographic -chardev socket,id=char0,path=/tmp/ivshmem.sock -device ivshmem-doorbell,id=ivshmem0,chardev=char0,vectors=2 -qmp unix:/tmp/qmp.sock,server,nowait -no-shutdown
## Description of problem
I used AI/LLM tools for initial discovery and confirmed the bug manually
with ASAN and source analysis.
hw/misc/ivshmem-pci.c: ivshmem_exit() frees s->peers (line 968) and
s->msi_vectors (line 975) without clearing the chr_fe handlers set in
ivshmem_common_realize (line 907). The handlers are only removed later
in object_finalize -> release_chr -> qemu_chr_fe_deinit. s->peers and
s->nb_peers are not zeroed after free, so process_msg still enters the
resize_peers path on freed memory.
If a message arrives between exit and finalize, ivshmem_read() fires
and process_msg_connect() dereferences freed s->peers[posn]. This is
deterministic, not a race — ivshmem_exit() returns to the main loop
and pending chardev data is processed in the same iteration. 100%
reproduction rate.
Five UAF paths after s->peers is freed:
1. process_msg -> resize_peers -> g_renew(Peer, s->peers, n):
realloc on freed pointer (ASAN catches as double-free)
2. process_msg_connect -> peer->eventfds[vector]:
event_notifier_init_fd writes attacker-controlled fd to
peer->eventfds[vector] — if eventfds pointer is heap residual,
this is an arbitrary address write of a controlled int value
3. setup_interrupt -> watch_vector_notifier: registers fd handler
with eventfd read from freed memory, opaque points to freed
s->msi_vectors[vector]
4. msi_vectors double UAF: s->msi_vectors not zeroed after free,
assert(!s->msi_vectors[vector].pdev) reads freed memory,
s->msi_vectors[vector].pdev = PCI_DEVICE(s) writes freed memory
5. ivshmem_add_eventfd: registers &s->peers[posn].eventfds[i] in
MMIO eventfd list — dangling pointer in ivshmem_mmio
Non-ASAN crash confirms arbitrary address write:
event_notifier_init_fd(e=0x555555dbba03 <memory_region_get_priority>,
fd=17) at event_notifier-posix.c:30
peer->eventfds contained a heap residual pointing to QEMU code
segment. Writing fd=17 to read-only code causes SIGSEGV; the same
write would succeed against writable .data/.bss function pointers.
ASAN output (qemu-system-x86_64 built with -fsanitize=address):
```
==2779831==ERROR: AddressSanitizer: heap-use-after-free on address 0x51100008cbd0
READ of size 4 at 0x51100008cbd0 thread T0
#0 process_msg_connect ../hw/misc/ivshmem-pci.c:538
#1 process_msg ../hw/misc/ivshmem-pci.c:587
#2 ivshmem_read ../hw/misc/ivshmem-pci.c:619
#3 tcp_chr_read ../chardev/char-socket.c:511
#4 g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x55c43)
#5 glib_pollfds_poll ../util/main-loop.c:290
#6 os_host_main_loop_wait ../util/main-loop.c:313
#7 main_loop_wait ../util/main-loop.c:592
#8 qemu_main_loop ../system/runstate.c:950
#9 qemu_default_main ../system/main.c:50
#10 main ../system/main.c:93
#11 __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#12 __libc_start_main_impl ../csu/libc-start.c:392
#13 _start (qemu-system-x86_64+0xb59844)
0x51100008cbd0 is located 80 bytes inside of 256-byte region
[0x51100008cb80,0x51100008cc80)
freed by thread T0 here:
#0 __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127
#1 ivshmem_exit ../hw/misc/ivshmem-pci.c:968
#2 pci_qdev_unrealize ../hw/pci/pci.c:1484
#3 device_set_realized ../hw/core/qdev.c:603
previously allocated by thread T0 here:
#0 __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
#1 g_realloc (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5ed7f)
#2 resize_peers ../hw/misc/ivshmem-pci.c:414
#3 ivshmem_common_realize ../hw/misc/ivshmem-pci.c:888
SUMMARY: AddressSanitizer: heap-use-after-free ../hw/misc/ivshmem-pci.c:538 in process_msg_connect
```
Note: the ASAN evidence above was captured using `qom-set realized=false`
for reproducibility; in a real attack the guest triggers ACPI
hot-unplug via port 0xae08, which follows the same free path through
pci_qdev_unrealize.
Repro:
1. ivshmem-server -F -S /tmp/ivshmem.sock -l 1M -n 2
2. qemu-system-x86_64 -M pc -m 256M -accel tcg \
-chardev socket,id=char0,path=/tmp/ivshmem.sock \
-device ivshmem-doorbell,id=ivshmem0,chardev=char0,vectors=2 \
-qmp unix:/tmp/qmp.sock,server,nowait -no-shutdown
3. Connect second peer (another QEMU or ivshmem-client)
4. QMP: device_del ivshmem0
5. Server sends peer message -> ivshmem_read on freed s->peers -> crash
Impact: confirmed denial of service (UAF crash on unmodified QEMU).
Theoretical: arbitrary code execution if freed heap contents can be
controlled via heap spray to redirect the event_notifier_init_fd
write target to a writable function pointer. The non-ASAN crash
demonstrates the write primitive reaches a code-segment address
(heap residual); with heap control, a .data/.bss target is reachable.
Guest can trigger via ACPI _EJ0 hot-unplug without QMP access.
[ivshmem_uaf_asan.py](/uploads/01db5e12d4bb35c7208beab62db85cb0/ivshmem_uaf_asan.py)
issue