[PATCH] usb: gadget: f_uvc: fix NULL pointer dereference during unbind race

From: Jimmy Hu

Date: Tue Feb 24 2026 - 03:40:10 EST


Commit b81ac4395bbe ("usb: gadget: uvc: allow for application to cleanly
shutdown") introduced two stages of synchronization waits totaling 1500ms
in uvc_function_unbind() to prevent several types of kernel panics.
However, this timing-based approach is insufficient during power
management (PM) transitions.

When the PM subsystem starts freezing user space processes, the
wait_event_interruptible_timeout() is aborted early, which allows the
unbind thread to proceed and nullify the gadget pointer
(cdev->gadget = NULL):

[ 814.123447][ T947] configfs-gadget.g1 gadget.0: uvc: uvc_function_unbind()
[ 814.178583][ T3173] PM: suspend entry (deep)
[ 814.192487][ T3173] Freezing user space processes
[ 814.197668][ T947] configfs-gadget.g1 gadget.0: uvc: uvc_function_unbind no clean disconnect, wait for release

When the PM subsystem resumes or aborts the suspend and tasks are
restarted, the V4L2 release path is executed and attempts to access the
already nullified gadget pointer, triggering a kernel panic:

[ 814.292597][ C0] PM: pm_system_irq_wakeup: 479 triggered dhdpcie_host_wake
[ 814.386727][ T3173] Restarting tasks ...
[ 814.403522][ T4558] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000030
[ 814.404021][ T4558] pc : usb_gadget_deactivate+0x14/0xf4
[ 814.404031][ T4558] lr : usb_function_deactivate+0x54/0x94
[ 814.404078][ T4558] Call trace:
[ 814.404080][ T4558] usb_gadget_deactivate+0x14/0xf4
[ 814.404083][ T4558] usb_function_deactivate+0x54/0x94
[ 814.404087][ T4558] uvc_function_disconnect+0x1c/0x5c
[ 814.404092][ T4558] uvc_v4l2_release+0x44/0xac
[ 814.404095][ T4558] v4l2_release+0xcc/0x130

The fix introduces a 'func_unbinding' flag in struct uvc_device to protect
critical sections:
1. In uvc_function_disconnect(), it prevents accessing the nullified
cdev->gadget pointer.
2. In uvc_v4l2_release(), it ensures uvcg_free_buffers() is skipped
if unbind is already in progress, avoiding races with concurrent
bind operations or use-after-free on the video queue memory.

Fixes: b81ac4395bbe ("usb: gadget: uvc: allow for application to cleanly shutdown")
Cc: <stable@xxxxxxxxxxxxxxx>
Signed-off-by: Jimmy Hu <hhhuuu@xxxxxxxxxx>
---
drivers/usb/gadget/function/f_uvc.c | 7 +++++++
drivers/usb/gadget/function/uvc.h | 1 +
drivers/usb/gadget/function/uvc_v4l2.c | 6 ++++++
3 files changed, 14 insertions(+)

diff --git a/drivers/usb/gadget/function/f_uvc.c b/drivers/usb/gadget/function/f_uvc.c
index a96476507d2f..f8c609ad1a43 100644
--- a/drivers/usb/gadget/function/f_uvc.c
+++ b/drivers/usb/gadget/function/f_uvc.c
@@ -413,6 +413,11 @@ uvc_function_disconnect(struct uvc_device *uvc)
{
int ret;

+ if (uvc->func_unbinding) {
+ pr_info("uvc: unbinding, skipping function deactivate\n");
+ return;
+ }
+
if ((ret = usb_function_deactivate(&uvc->func)) < 0)
uvcg_info(&uvc->func, "UVC disconnect failed with %d\n", ret);
}
@@ -659,6 +664,7 @@ uvc_function_bind(struct usb_configuration *c, struct usb_function *f)
int ret = -EINVAL;

uvcg_info(f, "%s()\n", __func__);
+ uvc->func_unbinding = false;

opts = fi_to_f_uvc_opts(f->fi);
/* Sanity check the streaming endpoint module parameters. */
@@ -994,6 +1000,7 @@ static void uvc_function_unbind(struct usb_configuration *c,
long wait_ret = 1;

uvcg_info(f, "%s()\n", __func__);
+ uvc->func_unbinding = true;

kthread_cancel_work_sync(&video->hw_submit);

diff --git a/drivers/usb/gadget/function/uvc.h b/drivers/usb/gadget/function/uvc.h
index 676419a04976..7ca56ff737a4 100644
--- a/drivers/usb/gadget/function/uvc.h
+++ b/drivers/usb/gadget/function/uvc.h
@@ -155,6 +155,7 @@ struct uvc_device {
enum uvc_state state;
struct usb_function func;
struct uvc_video video;
+ bool func_unbinding;
bool func_connected;
wait_queue_head_t func_connected_queue;

diff --git a/drivers/usb/gadget/function/uvc_v4l2.c b/drivers/usb/gadget/function/uvc_v4l2.c
index fd4b998ccd16..a8a15b584648 100644
--- a/drivers/usb/gadget/function/uvc_v4l2.c
+++ b/drivers/usb/gadget/function/uvc_v4l2.c
@@ -594,7 +594,13 @@ static void uvc_v4l2_disable(struct uvc_device *uvc)
{
uvc_function_disconnect(uvc);
uvcg_video_disable(&uvc->video);
+ if (uvc->func_unbinding) {
+ pr_info("uvc: unbinding, skipping buffer cleanup\n");
+ goto skip_buffer_cleanup;
+ }
uvcg_free_buffers(&uvc->video.queue);
+
+skip_buffer_cleanup:
uvc->func_connected = false;
wake_up_interruptible(&uvc->func_connected_queue);
}

base-commit: 24d479d26b25bce5faea3ddd9fa8f3a6c3129ea7
--
2.53.0.371.g1d285c8824-goog