[PATCH] media: em28xx: fix use-after-free in em28xx_unregister_media_device()

Mauricio Faria de Oliveira posted 1 patch 2 months ago
There is a newer version of this series
drivers/media/usb/em28xx/em28xx-video.c | 47 +++++++++++++++++++++++++++++++--
1 file changed, 45 insertions(+), 2 deletions(-)
[PATCH] media: em28xx: fix use-after-free in em28xx_unregister_media_device()
Posted by Mauricio Faria de Oliveira 2 months ago
During a short probe/disconnect test, the em28xx driver may load the
em28xx-v4l sub-driver asynchronously and em28xx-v4l may fail to init,
releasing memory that may be used in the em28xx's disconnect handler
if a video device was opened during init (eg, udev's v4l_id program).

In em28xx_v4l2_init() once video_register_device() returns, a struct
video_device (vdev) that is embedded in struct em28xx_v4l2 (v4l2) is
referenced by the list struct media_device.entities, which is looped
over when releasing resources in em28xx_usb_disconnect().

This list entry is only removed when the last reference to the video
device (struct video_device.dev) is put. Currently, this might occur
*after* the containing struct em28xx_v4l2 (v4l2) is released in the
error path of em28xx_v4l2_init() and *after* the disconnect handler
loops over the list -- causing an use-after-free. Specifically:

If the video device is opened right after em28xx_v4l2_init() calls
video_register_device(), which adds the memory reference to the list,
and continues open after em28xx_v4l2_init()'s error path releases the
containing struct em28xx_v4l2 as it does not find existing references,
the memory reference to the contained struct video_device is still in
the list -- but it has been released!

Now, if em28xx_usb_disconnect() is called, it proceeds to loop over the
list, accessing the struct list_head's pointers in the released memory,
and hits a use-after-free error.

In order to fix this, get references to struct em28xx_v4l2 (v4l2) when
initializing its struct video_device fields (vdev, vbi_dev, radio_dev),
and put them in the (added) release callback for struct video_device,
or if video_register_device() fails (no release callback).

Problematic code path/race condition:

em28xx_usb_probe
    em28xx_init_dev
        em28xx_media_device_init(dev, ...)
            mdev = kzalloc_obj(*mdev)
            dev->media_dev = mdev

    request_modules(dev)
        INIT_WORK(&dev->request_module_wk, request_module_async)
        schedule_work(&dev->request_module_wk)

    media_device_register(dev->media_dev)

request_module_async
    request_module("em28xx-v4l") // em28xx-video.c

module_init(em28xx_video_register)
    em28xx_register_extension()
        em28xx_v4l2_init()
            v4l2 = kzalloc_obj(*v4l2)
            video_register_device(&v4l2->vdev, ...)
                __video_register_device(vdev = &v4l2->vdev, ...)
                    vdev->dev.release = v4l2_device_release
                    device_register(&vdev->dev)
                    video_register_media_controller(vdev)
                        media_device_register_entity(..., &vdev->entity)
                            media_gobj_create(..., &entity->graph_obj)
                                list_add_tail(&gobj->list,
                                              &mdev->entities) // ref!

        (*) /usr/lib/udev/v4l_id: open("/dev/video0")
                // get_device(dev)

            v4l2_mc_create_media_graph(dev->media_dev) // fails.
                dev_err(..., "failed to create media graph\n")
                goto unregister_dev;

            unregister_dev:
                video_unregister_device(&v4l2->vdev);
                    device_unregister(dev = &vdev->dev);
                        put_device(dev) // if open() happens before this
                                        // no v4l2_device_release() yet.

                kref_put(&v4l2->ref, em28xx_free_v4l2) // frees v4l2!

em28xx_usb_disconnect
    em28xx_release_resources
        em28xx_unregister_media_device
            if (dev->media_dev)
                media_device_unregister(dev->media_dev)
                    list_for_each_entry_safe(entity, next,
                                             &mdev->entities, // ref!
                                             graph_obj.list)
                        // access to v4l2.vdev.entity.graph_object.list!

        (*) /usr/lib/udev/v4l_id: close("/dev/video0")
                // put_device(dev)
                //     v4l2_device_release()
                //         media_device_unregister_entity(&vdev->entity)
                //             media_gobj_destroy(&entity->graph_obj)
                //                 list_del(&gobj->list) // too late.

Reported-by: syzbot+07b93bb3189febcab189@syzkaller.appspotmail.com
Closes: https://syzbot.org/bug?extid=07b93bb3189febcab189
Signed-off-by: Mauricio Faria de Oliveira <mfo@igalia.com>
---
It's possible to synthetically reproduce this bug with wait-for/delays
in the driver's code paths exercised by syzbot's USB probe/disconnect.
If you're interested in it, just let me know and I'll send it as well.

Also tested/verified on next-20260410.

Before:

	...
	[   36.744362] em28xx 1-1:0.132: New device syz syz @ 480 Mbps (0413:6023, interface 132, class 132)
	...
	[   38.129591] em28xx 1-1:0.132: Registering V4L2 extension
	...
	[   38.200707] em28xx 1-1:0.132: failed to create media graph
	[   38.201237] DBG Step 1 em28xx_v4l2_init(): Waiting for dev->v4l2 to be opened...
	[   38.214578] DBG Step 2 em28xx_v4l2_open(): dev->v4l2 opened; waiting 5 seconds.
	[   39.099563] usb 1-1: USB disconnect, device number 2
	[   39.102683] em28xx 1-1:0.132: Disconnecting em28xx
	[   39.209877] DBG Step 1 em28xx_v4l2_init(): Done.
	[   39.211261] em28xx 1-1:0.132: V4L2 device video0 deregistered

	(differences below)

	[   39.212953] DBG Step 3 em28xx_free_v4l2(): Freeing dev->v4l2
	[   39.214128] DBG Step 3 em28xx_free_v4l2(): Done
	[   39.215233] DBG Step 4 em28xx_usb_disconnect(): Waiting for dev->v4l2 to be freed...
	[   39.217183] DBG Step 4 em28xx_usb_disconnect(): Done.
	[   39.218319] DBG Step 5 em28xx_unregister_media_device(): use-after-free expected...
	[   39.220381] ==================================================================
	[   39.222372] BUG: KASAN: slab-use-after-free in media_device_unregister+0x542/0x560
	[   39.223794] Read of size 8 at addr ffff888005f36168 by task kworker/1:1/43
	[   39.224872]
	[   39.225136] CPU: 1 UID: 0 PID: 43 Comm: kworker/1:1 Not tainted 7.0.0-rc5+ #28 PREEMPT(lazy)
	[   39.225149] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
	[   39.225156] Workqueue: usb_hub_wq hub_event
	[   39.225170] Call Trace:
	[   39.225176]  <TASK>
	[   39.225181]  dump_stack_lvl+0x4d/0x70
	[   39.225195]  print_report+0x170/0x4f3
	[   39.225230]  kasan_report+0xda/0x110
	[   39.225267]  media_device_unregister+0x542/0x560
	[   39.225275]  em28xx_unregister_media_device.part.0+0x4f/0xf0
	[   39.225305]  em28xx_release_resources+0xa1/0x1a0
	[   39.225314]  em28xx_usb_disconnect.cold+0x280/0x323
	[   39.225325]  usb_unbind_interface+0x154/0x690
	[   39.225350]  device_release_driver_internal+0x36b/0x500
	[   39.225370]  bus_remove_device+0x259/0x3e0
	[   39.225405]  device_del+0x364/0xce0
	[   39.225449]  usb_disable_device+0x270/0x4f0
	[   39.225457]  usb_disconnect+0x26a/0x920
	[   39.225465]  hub_event+0x966/0x3600
	[   39.225559]  process_one_work+0x617/0xf00
	[   39.225577]  worker_thread+0x422/0xbb0
	[   39.225611]  kthread+0x2cb/0x3a0
	[   39.225633]  ret_from_fork+0x357/0x540
	[   39.225663]  ret_from_fork_asm+0x1a/0x30
	[   39.225675]  </TASK>
	[   39.225678]
	[   39.277189] Allocated by task 113:
	[   39.277933]  kasan_save_stack+0x30/0x50
	[   39.278696]  kasan_save_track+0x14/0x30
	[   39.279186]  __kasan_kmalloc+0x7f/0x90
	[   39.280147]  em28xx_v4l2_init.cold+0xd2/0x3de6
	[   39.281702]  em28xx_init_extension+0xfb/0x1c0
	[   39.282763]  process_one_work+0x617/0xf00
	[   39.283524]  worker_thread+0x422/0xbb0
	[   39.284543]  kthread+0x2cb/0x3a0
	[   39.285461]  ret_from_fork+0x357/0x540
	[   39.286401]  ret_from_fork_asm+0x1a/0x30
	[   39.287180]
	[   39.287514] Freed by task 113:
	[   39.288117]  kasan_save_stack+0x30/0x50
	[   39.288921]  kasan_save_track+0x14/0x30
	[   39.289694]  kasan_save_free_info+0x3b/0x70
	[   39.290584]  __kasan_slab_free+0x47/0x70
	[   39.291494]  kfree+0x147/0x3b0
	[   39.292126]  em28xx_free_v4l2+0x9c/0xc0
	[   39.293059]  em28xx_v4l2_init.cold+0x2fe/0x3de6
	[   39.294157]  em28xx_init_extension+0xfb/0x1c0
	[   39.295216]  process_one_work+0x617/0xf00
	[   39.296028]  worker_thread+0x422/0xbb0
	[   39.296771]  kthread+0x2cb/0x3a0
	[   39.297605]  ret_from_fork+0x357/0x540
	[   39.298447]  ret_from_fork_asm+0x1a/0x30
	[   39.299269]
	...

After:

	...
	[   12.513485] em28xx 1-1:0.132: New device syz syz @ 480 Mbps (0413:6023, interface 132, class 132)
	...
	[   13.884186] em28xx 1-1:0.132: Registering V4L2 extension
	...
	[   13.946067] em28xx 1-1:0.132: failed to create media graph
	[   13.946522] DBG Step 1 em28xx_v4l2_init(): Waiting for dev->v4l2 to be opened...
	[   13.959775] DBG Step 2 em28xx_v4l2_open(): dev->v4l2 opened; waiting 5 seconds.
	[   14.915494] usb 1-1: USB disconnect, device number 2
	[   14.918849] em28xx 1-1:0.132: Disconnecting em28xx
	[   14.993575] DBG Step 1 em28xx_v4l2_init(): Done.
	[   14.994326] em28xx 1-1:0.132: V4L2 device video0 deregistered

	(differences below)

	[   14.996910] DBG Step 4 em28xx_usb_disconnect(): Waiting for dev->v4l2 to be freed...
	[   19.089597] DBG Step 2 em28xx_v4l2_open(): Done.
	[   19.091534] videodev: DBG v4l2_device_release(): removing dev->v4l2 media entity
	[   19.095550] DBG Step 3 em28xx_free_v4l2(): Freeing dev->v4l2
	[   19.099202] DBG Step 3 em28xx_free_v4l2(): Done
	[   20.113671] DBG Step 4 em28xx_usb_disconnect(): Done.
	[   20.115563] DBG Step 5 em28xx_unregister_media_device(): use-after-free expected...
	[   20.121025] em28xx 1-1:0.132: Freeing device

	(No use-after-free.)
---
 drivers/media/usb/em28xx/em28xx-video.c | 47 +++++++++++++++++++++++++++++++--
 1 file changed, 45 insertions(+), 2 deletions(-)

diff --git a/drivers/media/usb/em28xx/em28xx-video.c b/drivers/media/usb/em28xx/em28xx-video.c
index 4a0ce9c5ee4b4abde681cc373e1c6db2f3b3af70..870b16ba4fef04947acae89a0d63663e1485a34d 100644
--- a/drivers/media/usb/em28xx/em28xx-video.c
+++ b/drivers/media/usb/em28xx/em28xx-video.c
@@ -2495,6 +2495,8 @@ static int em28xx_v4l2_close(struct file *filp)
 	return 0;
 }
 
+static void em28xx_vdev_release(struct video_device *vfd);
+
 static const struct v4l2_file_operations em28xx_v4l_fops = {
 	.owner         = THIS_MODULE,
 	.open          = em28xx_v4l2_open,
@@ -2552,7 +2554,7 @@ static const struct v4l2_ioctl_ops video_ioctl_ops = {
 static const struct video_device em28xx_video_template = {
 	.fops		= &em28xx_v4l_fops,
 	.ioctl_ops	= &video_ioctl_ops,
-	.release	= video_device_release_empty,
+	.release	= em28xx_vdev_release,
 	.tvnorms	= V4L2_STD_ALL,
 };
 
@@ -2581,7 +2583,7 @@ static const struct v4l2_ioctl_ops radio_ioctl_ops = {
 static struct video_device em28xx_radio_template = {
 	.fops		= &radio_fops,
 	.ioctl_ops	= &radio_ioctl_ops,
-	.release	= video_device_release_empty,
+	.release	= em28xx_vdev_release,
 };
 
 /* I2C possible address to saa7115, tvp5150, msp3400, tvaudio */
@@ -2619,6 +2621,44 @@ static void em28xx_vdev_init(struct em28xx *dev,
 		 dev_name(&dev->intf->dev), type_name);
 
 	video_set_drvdata(vfd, dev);
+
+#ifdef CONFIG_MEDIA_CONTROLLER
+	/*
+	 * The struct video_device is referenced by struct media_device.entities
+	 * list (with video_device.entity.graph_obj.list) after the registration
+	 * until the last reference to struct video_device.dev is released, when
+	 * it is removed from the list.
+	 *
+	 * Keep a reference on struct em28xx_v4l2 (contains struct video_device)
+	 * so to account for this memory reference and not free it while in use,
+	 * and release this reference in struct video_device.release() callback.
+	 * (Note: if registration fails, this reference must be released.)
+	 *
+	 * See video_register_device(vdev)
+	 *       device_register(&vdev->dev) // .release = v4l2_device_release()
+	 *       video_register_media_controller(vdev)
+	 *         media_device_register_entity(..., &vdev->entity)
+	 * and v4l2_device_release(vdev)
+	 *       media_device_unregister_entity(&vdev->entity)
+	 *       vdev->release(vdev)
+	 */
+	kref_get(&dev->v4l2->ref);
+#endif
+}
+
+static void em28xx_vdev_release(struct video_device *vfd)
+{
+#ifdef CONFIG_MEDIA_CONTROLLER
+	struct em28xx_v4l2 *v4l2;
+
+	/*
+	 * Find struct em28xx_v4l2 with its embedded struct v4l2_device,
+	 * as video_get_drvdata() returns dev and dev->v4l2 may be NULL
+	 * (e.g., after em28xx_v4l2_init()'s error path).
+	 */
+	v4l2 = container_of(vfd->v4l2_dev, struct em28xx_v4l2, v4l2_dev);
+	kref_put(&v4l2->ref, em28xx_free_v4l2);
+#endif
 }
 
 static void em28xx_tuner_setup(struct em28xx *dev, unsigned short tuner_addr)
@@ -2961,6 +3001,7 @@ static int em28xx_v4l2_init(struct em28xx *dev)
 	if (ret) {
 		dev_err(&dev->intf->dev,
 			"unable to register video device (error=%i).\n", ret);
+		kref_put(&dev->v4l2->ref, em28xx_free_v4l2);
 		goto unregister_dev;
 	}
 
@@ -2995,6 +3036,7 @@ static int em28xx_v4l2_init(struct em28xx *dev)
 		if (ret < 0) {
 			dev_err(&dev->intf->dev,
 				"unable to register vbi device\n");
+			kref_put(&dev->v4l2->ref, em28xx_free_v4l2);
 			goto unregister_dev;
 		}
 	}
@@ -3008,6 +3050,7 @@ static int em28xx_v4l2_init(struct em28xx *dev)
 		if (ret < 0) {
 			dev_err(&dev->intf->dev,
 				"can't register radio device\n");
+			kref_put(&dev->v4l2->ref, em28xx_free_v4l2);
 			goto unregister_dev;
 		}
 		dev_info(&dev->intf->dev,

---
base-commit: 66672af7a095d89f082c5327f3b15bc2f93d558e
change-id: 20260413-em28xx-v4l-uaf-1d196e53dc50

Best regards,
-- 
Mauricio Faria de Oliveira <mfo@igalia.com>