Coordinated Disclosure Timeline

Summary

There is a use-after-free vulnerability in the Qualcomm kgsl driver.

Product

msm_kernel

Tested Version

v5.4 before May 2022

Details

Issue 1: Use-after-free in Qualcomm kgsl driver (GHSL-2022-037)

The source code references are to the msm-5.4.r5 branch.

The structure kgsl_timeline contains a list of kgsl_timeline_fence objects in kgsl_timeline::fences. The kgsl_timeline_fence objects can be added to the fences using kgsl_timeline_add_fence. While kgsl_timeline_fence are dma_fence objects that are reference counted objects, kgsl_timeline::fences does not increase the refcount on the kgsl_timeline_fence objects that it holds. In particular, the kgsl_ioctl_timeline_fence_get ioctl can be used to create and add kgsl_timeline_fence to kgsl_timeline::fences and return a file handle back to the user.

long kgsl_ioctl_timeline_fence_get(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
{
    ...

	fence = kgsl_timeline_fence_alloc(timeline, param->seqno);  //<------ allocate and add `fence` to `timeline`, refcount = 1

	if (IS_ERR(fence)) {
		kgsl_timeline_put(timeline);
		return PTR_ERR(fence);
	}

	fd = get_unused_fd_flags(O_CLOEXEC);
	if (fd < 0) {
		ret = fd;
		goto out;
	}

	sync_file = sync_file_create(fence);   //<--------- `sync_file` increases the refcount of `fence`, refcount = 2
	if (sync_file) {
		fd_install(fd, sync_file->file);
		param->handle = fd;                //<--------- file handle returns to user
	} else {
		put_unused_fd(fd);
		ret = -ENOMEM;
	}

out:
	dma_fence_put(fence);               //<------ refcount of `fence` is 1.
	kgsl_timeline_put(timeline);

	return ret;
}

If the user closes the file descriptor, then the refcount of fence will decrease to zero, which will trigger timeline_fence_release:

static void timeline_fence_release(struct dma_fence *fence)
{
	struct kgsl_timeline_fence *f = to_timeline_fence(fence);
	struct kgsl_timeline *timeline = f->timeline;
	struct kgsl_timeline_fence *cur, *temp;
	unsigned long flags;

	spin_lock_irqsave(&timeline->fence_lock, flags);

	/* If the fence is still on the active list, remove it */
	list_for_each_entry_safe(cur, temp, &timeline->fences, node) {
		if (f != cur)
			continue;

		list_del_init(&f->node);
		break;
	}
	spin_unlock_irqrestore(&timeline->fence_lock, flags);
	trace_kgsl_timeline_fence_release(f->timeline->id, fence->seqno);

	kgsl_timeline_put(f->timeline);
	dma_fence_free(fence);
}

Although timeline_fence_release removes fence from timeline->fences, there is a time window between the triggering of timeline_fence_release and its removal from timeline->fences where another thread may get hold of a copy of fence from timeline->fences and use it after dma_fence_free is called. This will trigger a use-after-free. For example, if another thread calls kgsl_ioctl_timeline_destroy and the code marked as “a” below is triggered before its removal from timeline->fences, then a use-after-free will occur:

long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
{
	struct kgsl_device *device = dev_priv->device;
	struct kgsl_timeline_fence *fence, *tmp;
	struct kgsl_timeline *timeline;
	struct list_head temp;
	u32 *param = data;

	if (*param == 0)
		return -ENODEV;

	spin_lock(&device->timelines_lock);
	timeline = idr_find(&device->timelines, *param);

	if (timeline == NULL) {
		spin_unlock(&device->timelines_lock);
		return -ENODEV;
	}

	/*
	 * Validate that the id given is owned by the dev_priv
	 * instance that is passed in. If not, abort.
	 */
	if (timeline->dev_priv != dev_priv) {
		spin_unlock(&device->timelines_lock);
		return -EINVAL;
	}

	idr_remove(&device->timelines, timeline->id);
	spin_unlock(&device->timelines_lock);

	INIT_LIST_HEAD(&temp);

	spin_lock(&timeline->fence_lock);                           //<----------- a. triggers before timeline_fence_release removes `fence` from `timeline->fences`
	list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
		dma_fence_get(&fence->base);
	list_replace_init(&timeline->fences, &temp);
	spin_unlock(&timeline->fence_lock);
                                                            //<------------- `timeline_fence_release` tries to remove `fence` from `timeline->fences` but couldn't find it
                                                            //                it then proceeds to and free `fence`
	spin_lock_irq(&timeline->lock);
	list_for_each_entry_safe(fence, tmp, &temp, node) {     //<------------  `use-after-free`
		dma_fence_set_error(&fence->base, -ENOENT);
		dma_fence_signal_locked(&fence->base);
		dma_fence_put(&fence->base);
	}
	spin_unlock_irq(&timeline->lock);

	kgsl_timeline_put(timeline);

	return timeline ? 0 : -ENODEV;
}

The issue can also be triggered using the kgsl_ioctl_timeline_wait as follows.

In the kgsl_timelines_to_fence_array function, kgsl_timeline_fence objects are created and added to a kgsl_timeline by calling the kgsl_timeline_fence_alloc function:

struct dma_fence *kgsl_timelines_to_fence_array(struct kgsl_device *device,
		u64 timelines, u32 count, u64 usize, bool any)
{
    ...
	for (i = 0; i < count; i++) {
        ...
		fences[i] = kgsl_timeline_fence_alloc(timeline, val.seqno);  //<------ kgsl_timeline_fence created and adds to timeline
        ...

		uptr += usize;
	}
    ...
err:
	for (i = 0; i < count; i++) {
		if (!IS_ERR_OR_NULL(fences[i]))
			dma_fence_put(fences[i]);          //<-------- this will free fences
	}
    ...
}

However, if there is an error, the created kgsl_timeline_fence objects will have their references removed, causing the timeline_fence_release method to be called to free them. Although timeline_fence_release will remove the kgsl_timeline_fence from timeline before freeing them, it is possible that another thread will get hold of a reference of the kgsl_timeline_fence before timeline_fence_release removes them, causing a use-after-free. For example, the kgsl_ioctl_timeline_destroy can race with the kgsl_ioctl_timeline_wait and get a reference of the kgsl_timeline_fence created in kgsl_timelines_to_fence_array (called by kgsl_ioctl_timeline_wait) before it gets freed by an error path in kgsl_ioctl_timeline_wait in the follow order of events:

Thread 1: kgsl_ioctl_timeline_wait
            kgsl_timelines_to_fence_array
              kgsl_timeline_fence_alloc
                dma_fence_put
                  timeline_fence_release
--------------------------------------------
Thread 2:  (kgsl_ioctl_timeline_destroy)
              	list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
		          dma_fence_get(&fence->base);
------------------------------------------------
Thread 1:  (timeline_fence_release)
             dma_fence_free(fence)
------------------------------------------------
Thread 2:  	(kgsl_ioctl_timeline_destroy)
	          list_for_each_entry_safe(fence, tmp, &temp, node) {
		        dma_fence_set_error(&fence->base, -ENOENT);        //<------ use-after-free
		        dma_fence_signal_locked(&fence->base);
		        dma_fence_put(&fence->base);
	          }

In the above scenario, a use-after-free can happen.

Impact

This vulnerability can be exploited to gain arbitrary kernel read and write, enabling untrusted apps on Android devices to gain root privileges.

  1. https://android.googlesource.com/kernel/msm/+/refs/heads/android-msm-coral-4.14-android12/drivers/media/platform/msm/npu/npu_mgr.c#831
  2. https://android.googlesource.com/kernel/msm/+/refs/heads/android-msm-coral-4.14-android12/drivers/media/platform/msm/npu/npu_mgr.c#688
  3. https://android.googlesource.com/kernel/msm/+/refs/heads/android-msm-coral-4.14-android12/drivers/media/platform/msm/npu/npu_dev.c#1579

CVE

Credit

These issues were discovered and reported by GHSL team member @m-y-mo (Man Yue Mo).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2022-037 in any communication regarding these issues.