Hello,

I would like to report a reproducible kernel NULL pointer dereference in the
QXL DRM driver's release/fence cleanup path. I reproduced it on a Linux
7.1.5-based x86_64 kernel in QEMU.

Summary
=======

A userspace DRM_IOCTL_QXL_EXECBUFFER request can make 
qxl_process_single_command()
allocate a qxl_release and then fail while looking up a relocation's GEM handle.
The error path calls qxl_release_free() before dma_fence_init() has run. In the
affected code, qxl_release_alloc() allocates the object with kmalloc() and only
initializes selected fields. qxl_release_free() then calls
dma_fence_was_initialized(), which reads the uninitialized fence flags. When
the stale INITIALIZED bit is set, cleanup treats the fence as initialized and
calls dma_fence_signal(). The fence lock selection can then return the
uninitialized NULL extern_lock, leading to a NULL dereference in
_raw_spin_lock_irqsave().

Trigger and call path
=====================

The reproducing userspace sequence opens /dev/dri/card0, issues
DRM_IOCTL_QXL_ALLOC, then issues DRM_IOCTL_QXL_EXECBUFFER with a QXL_CMD_DRAW
command and a BO relocation whose src_handle does not exist (the test used
0xdeadbeef). The command and relocation lengths/types are otherwise valid.
The invalid handle makes the GEM lookup return -EINVAL and takes the normal
error-cleanup path:

  ioctl(DRM_IOCTL_QXL_EXECBUFFER)
    -> qxl_execbuffer_ioctl()
    -> qxl_process_single_command()
    -> qxl_alloc_release_reserved()
    -> qxlhw_handle_to_bo() [invalid GEM handle, returns -EINVAL]
    -> qxl_release_free() [before dma_fence_init()]
    -> dma_fence_was_initialized() [reads uninitialized flags]
    -> dma_fence_signal()
    -> dma_fence_spinlock() [selects NULL extern_lock]
    -> _raw_spin_lock_irqsave(NULL)

Relevant source code
====================

qxl_release_alloc() allocates the release with kmalloc() and initializes only
selected fields; the embedded dma_fence state is not zeroed:

  release = kmalloc(size, GFP_KERNEL);
  if (!release)
          return -ENOMEM;
  release->base.ops = NULL;
  release->type = type;
  release->release_offset = 0;
  release->surface_release_id = 0;
  INIT_LIST_HEAD(&release->bos);

In qxl_process_single_command(), a missing GEM handle makes
qxlhw_handle_to_bo() return -EINVAL. The caller jumps to cleanup before reaching
qxl_release_fence_buffer_objects(), the later code path that can initialize the
fence with dma_fence_init():

  ret = qxlhw_handle_to_bo(file_priv, reloc.src_handle, release,
                           &reloc_info[i].src_bo);
  if (ret)
          goto out_free_bos;

  ...
  qxl_release_fence_buffer_objects(release);

out_free_bos:
out_free_release:
  if (ret)
          qxl_release_free(qdev, release);

The lookup returns -EINVAL when drm_gem_object_lookup() cannot find the handle:

  gobj = drm_gem_object_lookup(file_priv, handle);
  if (!gobj)
          return -EINVAL;

The cleanup decision is the critical bug. dma_fence_was_initialized() tests
only a bit in fence->flags, and qxl_release_free() uses that result to decide
whether to signal the fence:

  if (dma_fence_was_initialized(&release->base)) {
          qxl_release_free_list(release);
          dma_fence_signal(&release->base);
          dma_fence_put(&release->base);
  } else {
          qxl_release_free_list(release);
          kfree(release);
  }

The helper itself is only a bit test; its kernel comment says this is valid
only when the fence backing memory was zero-initialized:

  return fence &&
         test_bit(DMA_FENCE_FLAG_INITIALIZED_BIT, &fence->flags);

Once dma_fence_signal() runs, it obtains the lock from the fence fields:

  return test_bit(DMA_FENCE_FLAG_INLINE_LOCK_BIT, &fence->flags) ?
         &fence->inline_lock : fence->extern_lock;

In the reproducing run, stale flags made the first branch run although the
fence had never been initialized. Its extern_lock was NULL, so
dma_fence_signal() reached the NULL lock operation in the crash trace.

The relevant userspace operations were:

  openat(AT_FDCWD, "/dev/dri/card0", O_RDWR)
  ioctl(fd, DRM_IOCTL_QXL_ALLOC, ...)
  ioctl(fd, DRM_IOCTL_QXL_EXECBUFFER, ...)

Crash evidence
==============

Immediately before the failure, the diagnostic values were:

  release=ff11000100a3fb40 base=ff11000100a3fb40
  ops=0000000000000000 flags=1
  extern_lock=0000000000000000 selected_lock=0000000000000000
  qdev_lock=ff1100010155f2f0

The kernel reported:

  BUG: kernel NULL pointer dereference, address: 0000000000000000
  #PF: supervisor write access in kernel mode
  RIP: _raw_spin_lock_irqsave+0x73/0xc0
  CR2: 0000000000000000
  Call Trace:
    dma_fence_signal
    qxl_release_free
    qxl_execbuffer_ioctl
    drm_ioctl_kernel
    drm_ioctl
    __x64_sys_ioctl

This is a kernel Oops, not merely an ioctl returning an error. Replacing the
release allocation with kzalloc() in my QEMU test caused the same invalid
handle request to return -EINVAL without the NULL dereference. I have not
verified this reproducer on physical QXL hardware.

Proposed fix
============

The following change zero-initializes the release object so that the
pre-initialization cleanup path cannot mistake stale fence flags for an
initialized dma_fence:

--- a/drivers/gpu/drm/qxl/qxl_release.c
+++ b/drivers/gpu/drm/qxl/qxl_release.c
@@ -91,8 +91,8 @@ qxl_release_alloc(struct qxl_device *qdev, int type,
        int handle;
        size_t size = sizeof(*release);
 
-       release = kmalloc(size, GFP_KERNEL);
+       release = kzalloc(size, GFP_KERNEL);
        if (!release) {
                DRM_ERROR("Out of memory\n");
                return -ENOMEM;

GPT-6-Luna assisted with the initial analysis and drafting this proposed fix.
I am responsible for the report and its technical claims. I can provide the
complete reproducer and full kernel log directly if needed.

Regards,

Yuhang Pan
[email protected]

Reply via email to