[PATCH v3 3/3] media: dvb-usb: refcount dvb_usb_device to fix disconnect UAF on open chardevs

From: YuanZhe Liu

Date: Mon Oct 26 2026 - 22:01:23 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 reference has to cover the whole lifetime of a frontend, not
just individual calls: closing the last file descriptor after
disconnect runs __dvb_frontend_free() -> fe->ops.release(), which
still talks to the demodulator over i2c. Therefore:

* dvb_usb_adapter_frontend_init() takes a reference per registered
frontend, saves the driver's release callback and installs
dvb_usb_fe_release() as fe->ops.release; the wrapper calls the
original callback and then drops the reference. The DVB core
invokes fe->ops.release() exactly once, when the frontend object
is finally freed, so the object stays alive until the last fd is
closed and dvb_unregister_frontend()/dvb_frontend_detach() have
run. The failed-registration path drops the reference itself.
* usb_get_dev() / usb_put_dev() tie the underlying "struct
usb_device" to the same lifetime, so late USB helpers either
work or fail cleanly on a disconnected device instead of
dereferencing freed memory.
* dvb_usb_fe_wakeup() / dvb_usb_fe_sleep() pin the device around
the power-control + fe_init/fe_sleep calls;
* dvb_usb_generic_rw() and ttusb2_i2c_xfer() pin the device for
the duration of the USB/i2c work, check ->dead right after
taking their mutex and skip the hardware access if the device is
gone.

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.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@xxxxxxxxxxxxxxx
Co-developed-by: Bin Yu <byu@xxxxxxxxxxxxx>
Signed-off-by: Bin Yu <byu@xxxxxxxxxxxxx>
Co-developed-by: Cheng Wen <wencheng@xxxxxxxxxxxxx>
Signed-off-by: Cheng Wen <wencheng@xxxxxxxxxxxxx>
Signed-off-by: YuanZhe Liu <25031212351@xxxxxxxxxxxxxxxxx>
---
drivers/media/usb/dvb-usb/dvb-usb-dvb.c | 42 +++++++++++++++++++++---
drivers/media/usb/dvb-usb/dvb-usb-init.c | 41 +++++++++++++++++++++--
drivers/media/usb/dvb-usb/dvb-usb-urb.c | 21 ++++++++++--
drivers/media/usb/dvb-usb/dvb-usb.h | 14 ++++++++
drivers/media/usb/dvb-usb/ttusb2.c | 18 ++++++++--
5 files changed, 124 insertions(+), 12 deletions(-)

diff --git a/drivers/media/usb/dvb-usb/dvb-usb-dvb.c b/drivers/media/usb/dvb-usb/dvb-usb-dvb.c
index 029dad86a059..2554894f0523 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb-dvb.c
+++ b/drivers/media/usb/dvb-usb/dvb-usb-dvb.c
@@ -251,26 +251,47 @@ 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 (!dvb_usb_device_get(adap->dev))
+ return -ENODEV;

- if (adap->fe_adap[fe->id].fe_sleep)
+ 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;
+}
+
+static void dvb_usb_fe_release(struct dvb_frontend *fe)
+{
+ struct dvb_usb_adapter *adap = fe->dvb->priv;
+
+ if (adap->fe_adap[fe->id].fe_release)
+ adap->fe_adap[fe->id].fe_release(fe);
+ dvb_usb_device_put(adap->dev);
}

int dvb_usb_adapter_frontend_init(struct dvb_usb_adapter *adap)
@@ -304,10 +325,20 @@ int dvb_usb_adapter_frontend_init(struct dvb_usb_adapter *adap)
adap->fe_adap[i].fe->ops.init = dvb_usb_fe_wakeup;
adap->fe_adap[i].fe_sleep = adap->fe_adap[i].fe->ops.sleep;
adap->fe_adap[i].fe->ops.sleep = dvb_usb_fe_sleep;
+ adap->fe_adap[i].fe_release = adap->fe_adap[i].fe->ops.release;
+
+ /*
+ * A frontend file descriptor can outlive USB disconnect.
+ * Hold a reference for the whole frontend lifetime: the
+ * release callback and the frontend thread then still run
+ * against a live struct dvb_usb_device.
+ */
+ dvb_usb_device_get(adap->dev);

if (dvb_register_frontend(&adap->dvb_adap, adap->fe_adap[i].fe)) {
err("Frontend %d registration failed.", i);
dvb_frontend_detach(adap->fe_adap[i].fe);
+ dvb_usb_device_put(adap->dev);
adap->fe_adap[i].fe = NULL;
/* In error case, do not try register more FEs,
* still leaving already registered FEs alive. */
@@ -317,6 +348,9 @@ int dvb_usb_adapter_frontend_init(struct dvb_usb_adapter *adap)
return 0;
}

+ /* from now on the frontend owns the reference */
+ adap->fe_adap[i].fe->ops.release = dvb_usb_fe_release;
+
/* only attach the tuner if the demod is there */
if (adap->props.fe[i].tuner_attach != NULL)
adap->props.fe[i].tuner_attach(adap);
diff --git a/drivers/media/usb/dvb-usb/dvb-usb-init.c b/drivers/media/usb/dvb-usb/dvb-usb-init.c
index 6d2672bcc69b..8756ce18f1d7 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb-init.c
+++ b/drivers/media/usb/dvb-usb/dvb-usb-init.c
@@ -136,9 +136,36 @@ 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);
+
+ usb_put_dev(d->udev);
+ 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 +175,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 +184,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,8 +331,12 @@ 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;

+ /* keep the usb_device alive for as long as the object itself */
+ usb_get_dev(udev);
+
usb_set_intfdata(intf, d);

ret = dvb_usb_init(d, adapter_nums);
@@ -321,7 +353,10 @@ int dvb_usb_device_init(struct usb_interface *intf,

error:
usb_set_intfdata(intf, NULL);
- kfree(d);
+ if (d->udev)
+ dvb_usb_device_put(d);
+ else
+ kfree(d);
return ret;
}
EXPORT_SYMBOL(dvb_usb_device_init);
diff --git a/drivers/media/usb/dvb-usb/dvb-usb-urb.c b/drivers/media/usb/dvb-usb/dvb-usb-urb.c
index 2aabf90d8697..b8f51104092f 100644
--- a/drivers/media/usb/dvb-usb/dvb-usb-urb.c
+++ b/drivers/media/usb/dvb-usb/dvb-usb-urb.c
@@ -17,13 +17,28 @@ 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;
+
+ if (d->dead) {
+ mutex_unlock(&d->usb_mutex);
+ ret = -ENODEV;
+ goto out;
+ }

deb_xfer(">>> ");
debug_dump(wbuf,wlen,deb_xfer);
@@ -57,6 +72,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 550006a8d86f..53647f9cf34d 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>

@@ -373,6 +374,7 @@ struct dvb_usb_fe_adapter {

int (*fe_init) (struct dvb_frontend *);
int (*fe_sleep) (struct dvb_frontend *);
+ void (*fe_release)(struct dvb_frontend *fe);

struct usb_data_stream stream;

@@ -429,6 +431,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 +470,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 +513,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 acde6149d278..0e63511c0b3e 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.34.1