[PATCH 3/3] media: dvb-usb: refcount dvb_usb_device to fix disconnect UAF on open chardevs
From: Yuanzhe Liu
Date: Wed Sep 23 2026 - 05:39:20 EST
On USB unbind (physical disconnect or usbfs USBDEVFS_DISCONNECT_CLAIM /
USBDEVFS_RESET / USBDEVFS_SETCONFIGURATION), dvb_usb_device_exit() ->
dvb_usb_exit() tears the whole "struct dvb_usb_device" down while
userspace may still hold open /dev/dvb/adapter*/frontend* file
descriptors and while the kdvb-ad-X-fe-Y thread is still running.
The post-disconnect paths then trip over the freed object:
* i2c_transfer(&d->i2c_adap) from a frontend's ->release() /
->init() reads i2c_adap.bus_lock etc. out of freed memory
(KASAN reports A/D in the bug report);
* dvb_usb_generic_rw() / ttusb2_i2c_xfer() lock/unlock
&d->usb_mutex / &d->i2c_mutex inside freed memory
(KASAN UAF writes, UBSAN qspinlock splats, and a NULL dereference
where "d" was gone entirely);
* dvb_usb_fe_wakeup() / dvb_usb_fe_sleep() call
dvb_usb_device_power_ctrl() -> ttusb2_power_ctrl() on freed "d".
struct dvb_usb_device has no refcount, so there is no way to keep it
alive until the last chardev user is gone. Add one:
* New fields "struct kref kref" and "int dead" in struct
dvb_usb_device.
* New helpers dvb_usb_device_get() / dvb_usb_device_put(); the
release function frees ->priv and "d" once the last reference
is dropped.
* dvb_usb_device_init() initialises the kref; dvb_usb_exit() no
longer frees "d" but marks it ->dead, keeps doing all the
subsystem teardown (remote, adapters, i2c, priv_destroy), and
drops the framework reference. The object itself is freed by
the last put().
* The places that can outlive disconnect now pin the device:
- dvb_usb_fe_wakeup() / dvb_usb_fe_sleep() get/put around the
power-control + fe_init/fe_sleep calls;
- dvb_usb_generic_rw() gets a ref for the duration of the USB
I/O;
- ttusb2_i2c_xfer() gets a ref around its i2c work.
If the device is ->dead they skip the USB I/O and return -ENODEV /
the original error path instead of touching a gone udev.
Because the kref lives inside the object itself, every consumer that
wants to probe "is it dead?" does so while holding a ref, so the flag
can never be read from freed memory. With the object kept alive, the
mutexes and the i2c_adapter embedded in it are also still valid, which
is what the KASAN/UBSAN reports were complaining about.
Cc: stable@xxxxxxxxxxxxxxx
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Yuanzhe Liu <25031212351@xxxxxxxxxxxxxxxxx>
---
drivers/media/usb/dvb-usb/dvb-usb-dvb.c | 20 ++++++++++++---
drivers/media/usb/dvb-usb/dvb-usb-init.c | 32 ++++++++++++++++++++++--
drivers/media/usb/dvb-usb/dvb-usb-urb.c | 15 +++++++++--
drivers/media/usb/dvb-usb/dvb-usb.h | 13 ++++++++++
drivers/media/usb/dvb-usb/ttusb2.c | 18 ++++++++++---
5 files changed, 87 insertions(+), 11 deletions(-)
diff --git a/drivers/media/usb/dvb-usb/dvb-usb-dvb.c b/drivers/media/usb/dvb-usb/dvb-usb-dvb.c
index 029dad8..e9c15a4 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb-dvb.c
+++ b/drivers/media/usb/dvb-usb/dvb-usb-dvb.c
@@ -251,26 +251,38 @@ static int dvb_usb_fe_wakeup(struct dvb_frontend *fe)
{
struct dvb_usb_adapter *adap = fe->dvb->priv;
- dvb_usb_device_power_ctrl(adap->dev, 1);
+ if (!dvb_usb_device_get(adap->dev))
+ return -ENODEV;
+
+ if (!adap->dev->dead)
+ dvb_usb_device_power_ctrl(adap->dev, 1);
dvb_usb_set_active_fe(fe, 1);
- if (adap->fe_adap[fe->id].fe_init)
+ if (!adap->dev->dead && adap->fe_adap[fe->id].fe_init)
adap->fe_adap[fe->id].fe_init(fe);
+ dvb_usb_device_put(adap->dev);
return 0;
}
static int dvb_usb_fe_sleep(struct dvb_frontend *fe)
{
struct dvb_usb_adapter *adap = fe->dvb->priv;
+ int ret = 0;
- if (adap->fe_adap[fe->id].fe_sleep)
+ if (!dvb_usb_device_get(adap->dev))
+ return -ENODEV;
+
+ if (!adap->dev->dead && adap->fe_adap[fe->id].fe_sleep)
adap->fe_adap[fe->id].fe_sleep(fe);
dvb_usb_set_active_fe(fe, 0);
- return dvb_usb_device_power_ctrl(adap->dev, 0);
+ if (!adap->dev->dead)
+ ret = dvb_usb_device_power_ctrl(adap->dev, 0);
+ dvb_usb_device_put(adap->dev);
+ return ret;
}
int dvb_usb_adapter_frontend_init(struct dvb_usb_adapter *adap)
diff --git a/drivers/media/usb/dvb-usb/dvb-usb-init.c b/drivers/media/usb/dvb-usb/dvb-usb-init.c
index 6d2672b..3b68d91 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb-init.c
+++ b/drivers/media/usb/dvb-usb/dvb-usb-init.c
@@ -136,9 +136,35 @@ static int dvb_usb_adapter_exit(struct dvb_usb_device *d)
/* general initialization functions */
+static void dvb_usb_free_device(struct kref *ref)
+{
+ struct dvb_usb_device *d =
+ container_of(ref, struct dvb_usb_device, kref);
+
+ kfree(d->priv);
+ kfree(d);
+}
+
+struct dvb_usb_device *dvb_usb_device_get(struct dvb_usb_device *d)
+{
+ if (!d)
+ return NULL;
+ kref_get(&d->kref);
+ return d;
+}
+EXPORT_SYMBOL(dvb_usb_device_get);
+
+void dvb_usb_device_put(struct dvb_usb_device *d)
+{
+ if (d)
+ kref_put(&d->kref, dvb_usb_free_device);
+}
+EXPORT_SYMBOL(dvb_usb_device_put);
+
static int dvb_usb_exit(struct dvb_usb_device *d)
{
deb_info("state before exiting everything: %x\n", d->state);
+ d->dead = 1;
dvb_usb_remote_exit(d);
dvb_usb_adapter_exit(d);
dvb_usb_i2c_exit(d);
@@ -148,8 +174,8 @@ static int dvb_usb_exit(struct dvb_usb_device *d)
if (d->priv != NULL && d->props.priv_destroy != NULL)
d->props.priv_destroy(d);
- kfree(d->priv);
- kfree(d);
+ /* last put() frees the device */
+ dvb_usb_device_put(d);
return 0;
}
@@ -157,6 +183,7 @@ static int dvb_usb_init(struct dvb_usb_device *d, short *adapter_nums)
{
int ret = 0;
+ kref_init(&d->kref);
mutex_init(&d->data_mutex);
mutex_init(&d->usb_mutex);
mutex_init(&d->i2c_mutex);
@@ -303,6 +330,7 @@ int dvb_usb_device_init(struct usb_interface *intf,
info("found a '%s' in warm state.", desc->name);
d->udev = udev;
d->desc = desc;
+ d->dead = 0;
d->owner = owner;
usb_set_intfdata(intf, d);
diff --git a/drivers/media/usb/dvb-usb/dvb-usb-urb.c b/drivers/media/usb/dvb-usb/dvb-usb-urb.c
index 2aabf90..6f949b6 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb-urb.c
+++ b/drivers/media/usb/dvb-usb/dvb-usb-urb.c
@@ -17,13 +17,22 @@ int dvb_usb_generic_rw(struct dvb_usb_device *d, u8 *wbuf, u16 wlen, u8 *rbuf,
if (!d || wbuf == NULL || wlen == 0)
return -EINVAL;
+ if (!dvb_usb_device_get(d))
+ return -ENODEV;
+
if (d->props.generic_bulk_ctrl_endpoint == 0) {
err("endpoint for generic control not specified.");
- return -EINVAL;
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (d->dead) {
+ ret = -ENODEV;
+ goto out;
}
if ((ret = mutex_lock_interruptible(&d->usb_mutex)))
- return ret;
+ goto out;
deb_xfer(">>> ");
debug_dump(wbuf,wlen,deb_xfer);
@@ -57,6 +66,8 @@ int dvb_usb_generic_rw(struct dvb_usb_device *d, u8 *wbuf, u16 wlen, u8 *rbuf,
}
mutex_unlock(&d->usb_mutex);
+out:
+ dvb_usb_device_put(d);
return ret;
}
EXPORT_SYMBOL(dvb_usb_generic_rw);
diff --git a/drivers/media/usb/dvb-usb/dvb-usb.h b/drivers/media/usb/dvb-usb/dvb-usb.h
index 550006a..311cd3a 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb.h
+++ b/drivers/media/usb/dvb-usb/dvb-usb.h
@@ -14,6 +14,7 @@
#include <linux/input.h>
#include <linux/usb.h>
#include <linux/firmware.h>
+#include <linux/kref.h>
#include <linux/mutex.h>
#include <media/rc-core.h>
@@ -429,6 +430,12 @@ struct dvb_usb_adapter {
/**
* struct dvb_usb_device - object of a DVB USB device
* @props: copy of the struct dvb_usb_properties this device belongs to.
+ * @kref: refcount; the object is only freed once every user that can
+ * outlive USB disconnect (frontend file descriptors, the frontend
+ * thread, in-flight i2c/usb transfers) has dropped its reference.
+ * @dead: set once the USB device is gone; USB/i2c workers must check
+ * this while holding @kref and skip real hardware I/O. Protects
+ * access to @udev and to the mutexes below.
* @desc: pointer to the device's struct dvb_usb_device_description.
* @state: initialization and runtime state of the device.
*
@@ -462,6 +469,9 @@ struct dvb_usb_device {
struct dvb_usb_device_properties props;
const struct dvb_usb_device_description *desc;
+ struct kref kref;
+ int dead;
+
struct usb_device *udev;
#define DVB_USB_STATE_INIT 0x000
@@ -502,6 +512,9 @@ extern int dvb_usb_device_init(struct usb_interface *,
short *adapter_nums);
extern void dvb_usb_device_exit(struct usb_interface *);
+struct dvb_usb_device *dvb_usb_device_get(struct dvb_usb_device *d);
+void dvb_usb_device_put(struct dvb_usb_device *d);
+
/* the generic read/write method for device control */
extern int __must_check
dvb_usb_generic_rw(struct dvb_usb_device *, u8 *, u16, u8 *, u16, int);
diff --git a/drivers/media/usb/dvb-usb/ttusb2.c b/drivers/media/usb/dvb-usb/ttusb2.c
index acde614..0e63511 100644
--- a/drivers/media/usb/dvb-usb/ttusb2.c
+++ b/drivers/media/usb/dvb-usb/ttusb2.c
@@ -369,10 +369,20 @@ static int ttusb2_i2c_xfer(struct i2c_adapter *adap,struct i2c_msg msg[],int num
{
struct dvb_usb_device *d = i2c_get_adapdata(adap);
static u8 obuf[60], ibuf[60];
- int i, write_read, read;
+ int i = 0, write_read, read;
- if (mutex_lock_interruptible(&d->i2c_mutex) < 0)
- return -EAGAIN;
+ if (!dvb_usb_device_get(d))
+ return -ENODEV;
+
+ if (d->dead) {
+ i = -ENODEV;
+ goto out;
+ }
+
+ if (mutex_lock_interruptible(&d->i2c_mutex) < 0) {
+ i = -EAGAIN;
+ goto out;
+ }
if (num > 2)
warn("more than 2 i2c messages at a time is not handled yet. TODO.");
@@ -426,6 +436,8 @@ static int ttusb2_i2c_xfer(struct i2c_adapter *adap,struct i2c_msg msg[],int num
}
mutex_unlock(&d->i2c_mutex);
+out:
+ dvb_usb_device_put(d);
return i;
}
--
2.45.1.windows.1