
Vulnerability Details for CVE-2022-41218
This vulnerability is a race condition vulnerability that occurs in drivers/media/dvb-core/dmxdev.c. Therefore, a race condition may occur in the usb device driver using this dmxdev.
This vulnerability could be used to trigger 3 UAFs.
First, the order of the exploit is as follows(I debugged using drivers/media/usb/ttusb-dec/ttusb_dec.c):
cpu0 cpu1 cpu 2
1. dvb_dvr_open()
2. ttusb_dec_disconnect()
ttusb_dec_exit_dvb()
dvb_dmxdev_release()
wait_event(dmxdev->dvr_dvbdev->wait_queue, …)
3. dvb_demux_open()
dvb_demux_ioctl()
dvb_usercopy()
copy_from_user() <- userfaultfd stuck
4. dvb_dvr_release()
5. vfree(dmxdev->filter)
6. copy_from_user() <- userfaultfd release
dvb_demux_do_ioctl()
struct dmxdev *dmxdev = dmxdevfilter->dev; <- UAF!!
The detailed exploit flow is as follows:
open() dvr0 among the device nodes. This calls dvb_dvr_open() to cause dvbdev->users++ to run.
In this case, when ttusb_dec_disconnect() is executed by removing the usb device, the wait_event(dmxdev->dvr_dvbdev->wait_queue, …) condition is caught.
Physically remove the USB device.
In this case, while the ttusb_dec_disconnect() function is being executed, it waits at wait_event(dmxdev->dvr_dvbdev->wait_queue, …).
open() the demux0 node so that the dvb_demux_open() function is called.
This function also executes dvbdev->users++ similarly to the dvb_dvr_open() function in step 1.
However, this improper reference counting occurs because the dvb_dmxdev_release() function that is currently waiting has already passed the check for dvbdev->users:
void dvb_dmxdev_release(struct dmxdev *dmxdev)
{
dmxdev->exit = 1;
if (dmxdev->dvbdev->users > 1) { // This check is bypassed. improper reference counting.
wait_event(dmxdev->dvbdev->wait_queue,
dmxdev->dvbdev->users == 1);
}
if (dmxdev->dvr_dvbdev->users > 1) {
wait_event(dmxdev->dvr_dvbdev->wait_queue, // Currently wait()ing here.
dmxdev->dvr_dvbdev->users == 1);
}
dvb_unregister_device(dmxdev->dvbdev);
dvb_unregister_device(dmxdev->dvr_dvbdev);
vfree(dmxdev->filter);
dmxdev->filter = NULL;
dmxdev->demux->close(dmxdev->demux);
}
It then calls ioctl() on demux0. When calling, the 3rd argument submits the user-space address set by userfaultfd(or FUSE fs). Now while ioctl is running it will hang at copy_from_user() of dvb_usercopy():
int dvb_usercopy(struct file *file,
unsigned int cmd, unsigned long arg,
int (*func)(struct file *file,
unsigned int cmd, void *arg))
{
char sbuf[128];
void *mbuf = NULL;
void *parg = NULL;
int err = -EINVAL;
/* Copy arguments into temp kernel buffer */
switch (_IOC_DIR(cmd)) {
case _IOC_NONE:
/*
* For this command, the pointer is actually an integer
* argument.
*/
parg = (void *) arg;
break;
case _IOC_READ: /* some v4l ioctls are marked wrong ... */
case _IOC_WRITE:
case (_IOC_WRITE | _IOC_READ):
if (_IOC_SIZE(cmd) <= sizeof(sbuf)) {
parg = sbuf;
} else {
/* too big to allocate from stack */
mbuf = kmalloc(_IOC_SIZE(cmd), GFP_KERNEL);
if (NULL == mbuf)
return -ENOMEM;
parg = mbuf;
}
err = -EFAULT;
if (copy_from_user(parg, (void __user *)arg, _IOC_SIZE(cmd))) // here
goto out;
break;
}
close() the fd in the thread that open()ed the dvr0 node.
Then dvb_dvr_release() is called, and dvbdev->users-- and wake_up(&dvbdev->wait_queue) are executed to wake up the dvb_dmxdev_release() function, which is the .disconnect flow.
In the .disconnect flow, dvb_dmxdev_release() function, vfree(dmxdev->filter) is executed.
Release userfaultfd from the demux0 ioctl thread that set userfaultfd.
This will cause the UAF to occur by reading the dmxdev->filter address you just released from .disconnect.
UAF can now be used in all cases of dvb_demux_do_ioctl(), and even LPE is possible when linked with eBPF.