[PATCH v3] drm/qxl: fix use-after-free in qxl_irq_handler on PCI

From: Óscar Megía López

Date: Mon Jul 27 2026 - 07:08:01 EST


while :; do
echo [pci qxl id] > /sys/bus/pci/drivers/qxl/unbind
echo [pci qxl id] > /sys/bus/pci/drivers/qxl/bind
done

After a few seconds, it reports:

==================================================================
BUG: KASAN: slab-use-after-free in qxl_irq_handler+0x269/0x2b0
Read of size 8 at addr ffff888001c6cd48 by task swapper/0/0

CPU: 0 UID: 0 PID: 0 Comm: swapper/0 Not tainted
7.1.0-10963-g1a3746ccbb0a #31 PREEMPT(lazy)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009),
BIOS Arch Linux 1.17.0-2-2 04/01/2014
Call Trace:
<IRQ>
dump_stack_lvl+0x4d/0x70
print_report+0x14b/0x4b0
? __pfx__raw_spin_lock_irqsave+0x10/0x10
? profile_tick+0x56/0x90
? tick_nohz_handler+0x23c/0x5c0
kasan_report+0x117/0x140
? qxl_irq_handler+0x269/0x2b0
? qxl_irq_handler+0x269/0x2b0
? __pfx_qxl_irq_handler+0x10/0x10
qxl_irq_handler+0x269/0x2b0
? __pfx_qxl_irq_handler+0x10/0x10
? __pfx_qxl_irq_handler+0x10/0x10
__handle_irq_event_percpu+0x116/0x450
? __pfx__raw_spin_lock+0x10/0x10
handle_irq_event+0xa6/0x1c0
handle_fasteoi_irq+0x271/0xb10
? __pfx_handle_fasteoi_irq+0x10/0x10
__common_interrupt+0x60/0x130
common_interrupt+0x7a/0x90
</IRQ>
<TASK>
asm_common_interrupt+0x26/0x40
RIP: 0010:pv_native_safe_halt+0xf/0x20
Code: 42 de 00 c3 cc cc cc cc 0f 1f 00 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 f3 0f 1e fa eb 07 0f 00 2d a3 cf 20 00
fb f4 <c3> cc cc cc cc 66 2e 0f 1f 84 00 00 00 00 00 66 90
90 90 90 90 90
RSP: 0018:ffffffffb8207e48 EFLAGS: 00000206
RAX: ffff8880b296f000 RBX: ffffffffb82146c0 RCX: 0000000000000001
RDX: 0000000000000001 RSI: 0000000000000004 RDI: 0000000000067a04
RBP: fffffbfff70428d8 R08: ffffffffb7247e1d R09: 1ffff1100d846202
R10: ffffed100d846203 R11: ffffed100d846203 R12: 0000000000000000
R13: 0000000000000000 R14: 1ffffffff7040fcd R15: dffffc0000000000
? ct_kernel_exit.constprop.0+0x9d/0xc0
default_idle+0x9/0x10
default_idle_call+0x37/0x60
do_idle+0x3a8/0x5d0
? __pfx___schedule+0x10/0x10
? __pfx_do_idle+0x10/0x10
cpu_startup_entry+0x4e/0x60
rest_init+0x11a/0x120
start_kernel+0x382/0x390
x86_64_start_reservations+0x24/0x30
x86_64_start_kernel+0xd6/0xe0
common_startup_64+0x13e/0x158
</TASK>

The qxl_pci_remove() function does not call free_irq(), allowing the IRQ
handler to fire after the device has been torn down, accessing freed
memory (qdev->ram_header, qdev->io_base).

I followed these steps to unload driver at link.

Added Disable the device from generating IRQs, Release the IRQ (free_irq())
at the start of qxl_pci_remove() to ensure no IRQs fire
after teardown begins.

Added at end Disable the device.

Fix: Added goto fini in ttm_device_fini() on error.
Bug: qxl_ttm_init never calls ttm_device_fini on failure
If qxl_ttm_init_mem_type() fails after ttm_device_init() succeeded,
ttm_glob_use_count stays incremented. All subsequent bind/unbind cycles
see refcount > 0 and skip ttm_pool_mgr_{init,fini}() entirely.
The global pool types are never finalized, and the list_lru_destroy fix
in ttm_pool_mgr_fini never runs.

Added free_irq on qxl_probe unload.

Set to NULL after free on qxl_device_fini.

Assisted-by: OpenCode:1.17.8-Big Pickle/DeepSeek V4 Flash
Fixes: 48bd85808443 ("drm/qxl: Convert to Linux IRQ interfaces")
Signed-off-by: Óscar Megía López <megia.oscar@xxxxxxxxx>
Link: https://www.kernel.org/doc/html/latest/PCI/pci.html
---
drivers/gpu/drm/qxl/qxl_drv.c | 9 +++++++++
drivers/gpu/drm/qxl/qxl_kms.c | 13 +++++++++++++
drivers/gpu/drm/qxl/qxl_ttm.c | 8 ++++++--
3 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/drivers/gpu/drm/qxl/qxl_drv.c b/drivers/gpu/drm/qxl/qxl_drv.c
index 1e6a2392d7c6..e109114ac691 100644
--- a/drivers/gpu/drm/qxl/qxl_drv.c
+++ b/drivers/gpu/drm/qxl/qxl_drv.c
@@ -128,6 +128,8 @@ qxl_pci_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
qxl_modeset_fini(qdev);
unload:
qxl_device_fini(qdev);
+ qxl_io_reset(qdev);
+ free_irq(pdev->irq, &qdev->ddev);
put_vga:
if (pci_is_vga(pdev) && pdev->revision < 5)
vga_put(pdev, VGA_RSRC_LEGACY_IO);
@@ -154,12 +156,19 @@ static void
qxl_pci_remove(struct pci_dev *pdev)
{
struct drm_device *dev = pci_get_drvdata(pdev);
+ struct qxl_device *qdev = to_qxl(dev);
+
+ qdev->ram_header->int_mask = 0;
+ outb(0, qdev->io_base + QXL_IO_UPDATE_IRQ);
+ free_irq(pdev->irq, dev);
+ cancel_work_sync(&qdev->client_monitors_config_work);

drm_kms_helper_poll_fini(dev);
drm_dev_unregister(dev);
drm_atomic_helper_shutdown(dev);
if (pci_is_vga(pdev) && pdev->revision < 5)
vga_put(pdev, VGA_RSRC_LEGACY_IO);
+ pci_disable_device(pdev);
}

static void
diff --git a/drivers/gpu/drm/qxl/qxl_kms.c b/drivers/gpu/drm/qxl/qxl_kms.c
index 461b7ab9ad5c..eb17d61eac11 100644
--- a/drivers/gpu/drm/qxl/qxl_kms.c
+++ b/drivers/gpu/drm/qxl/qxl_kms.c
@@ -313,11 +313,24 @@ void qxl_device_fini(struct qxl_device *qdev)
qxl_gem_fini(qdev);
qxl_bo_fini(qdev);
qxl_ring_free(qdev->command_ring);
+ qdev->command_ring = NULL;
qxl_ring_free(qdev->cursor_ring);
+ qdev->cursor_ring = NULL;
qxl_ring_free(qdev->release_ring);
+ qdev->release_ring = NULL;
io_mapping_free(qdev->surface_mapping);
+ qdev->surface_mapping = NULL;
io_mapping_free(qdev->vram_mapping);
+ qdev->vram_mapping = NULL;
iounmap(qdev->ram_header);
+ qdev->ram_header = NULL;
iounmap(qdev->rom);
qdev->rom = NULL;
+
+ idr_destroy(&qdev->release_idr);
+ idr_destroy(&qdev->surf_id_idr);
+ kfree(qdev->client_monitors_config);
+ qdev->client_monitors_config = NULL;
+
+ qdev->gc_work.func = NULL;
}
diff --git a/drivers/gpu/drm/qxl/qxl_ttm.c b/drivers/gpu/drm/qxl/qxl_ttm.c
index 5d495c4798a3..bdcc7560f3f1 100644
--- a/drivers/gpu/drm/qxl/qxl_ttm.c
+++ b/drivers/gpu/drm/qxl/qxl_ttm.c
@@ -207,13 +207,13 @@ int qxl_ttm_init(struct qxl_device *qdev)
r = qxl_ttm_init_mem_type(qdev, TTM_PL_VRAM, num_io_pages);
if (r) {
DRM_ERROR("Failed initializing VRAM heap.\n");
- return r;
+ goto fini;
}
r = qxl_ttm_init_mem_type(qdev, TTM_PL_PRIV,
qdev->surfaceram_size / PAGE_SIZE);
if (r) {
DRM_ERROR("Failed initializing Surfaces heap.\n");
- return r;
+ goto fini;
}
DRM_INFO("qxl: %uM of VRAM memory size\n",
(unsigned int)qdev->vram_size / (1024 * 1024));
@@ -222,6 +222,10 @@ int qxl_ttm_init(struct qxl_device *qdev)
DRM_INFO("qxl: %uM of Surface memory size\n",
(unsigned int)qdev->surfaceram_size / (1024 * 1024));
return 0;
+
+fini:
+ ttm_device_fini(&qdev->mman.bdev);
+ return r;
}

void qxl_ttm_fini(struct qxl_device *qdev)
--
2.55.0