Linux kernel -stable discussions
 help / color / mirror / Atom feed
* [PATCH] media: pvrusb2: fix disconnect and teardown races
@ 2026-04-20  6:06 Sangyun Kim
  2026-05-05 13:14 ` Hans Verkuil
  0 siblings, 1 reply; 2+ messages in thread
From: Sangyun Kim @ 2026-04-20  6:06 UTC (permalink / raw)
  To: Mike Isely, Mauro Carvalho Chehab, Hans Verkuil,
	Edward Adam Davis
  Cc: linux-media, linux-kernel, stable

pvr2_context_disconnect() queues a notification to the pvrusb2-context
kthread before it stores mp->disconnect_flag:

    pvr2_hdw_disconnect(mp->hdw);
    if (!pvr2_context_shutok())
        pvr2_context_notify(mp);
    mp->disconnect_flag = !0;

The context thread only destroys a context when disconnect_flag is set
and mc_first is NULL. If the notification wakes the thread before the
flag store becomes visible, the thread dequeues the context, runs
pvr2_context_check() with disconnect_flag still observed as 0, decides
that the destroy condition is not met yet, and goes back to sleep.
Nothing wakes the thread again once the flag is finally stored, so the
context stays on the global exist list forever and
pvr2_context_global_done() blocks module unload. commit 0a0b79ea55de
("media: pvrusb2: fix uaf in pvr2_context_set_notify") made this
liveness failure easier to hit by moving the notify earlier in the
disconnect path.

The same teardown sequence still contains a use-after-free.
pvr2_context_exit() inspects disconnect_flag after releasing mp->mutex
and may then call pvr2_context_notify(mp) after the context thread has
already freed the object via pvr2_context_destroy()/kfree(). The hdw
completion callback registered through pvr2_hdw_initialize() can race
the same way. Reordering the disconnect path alone closes the unload
hang, but it still leaves late notifiers able to touch freed memory.

Fix both problems together:

- Split pvr2_context_set_notify() into a locked helper
  (pvr2_context_set_notify_locked()) and a wrapper that acquires
  pvr2_context_mutex. This lets callers update several pieces of
  related state inside a single critical section without relocking.

- In pvr2_context_disconnect(), set disconnect_flag and enqueue the
  thread notification under pvr2_context_mutex. The context thread
  manipulates the notify list under the same mutex, so when it observes
  the queued entry it is guaranteed to observe disconnect_flag = 1 as
  well and the destroy condition evaluates correctly. This eliminates
  the original notify-before-flag liveness hole.

- Add a per-context refcount_t. pvr2_context_create() initialises it to
  1 (creator reference). pvr2_channel_init() and pvr2_channel_done()
  take and drop a reference around each channel's lifetime.
  pvr2_context_disconnect() takes a temporary reference across its body
  so the context cannot be freed while disconnect is still touching it.
  pvr2_context_destroy() no longer calls kfree() directly; it drops its
  reference via pvr2_context_put(), and whichever caller drops the last
  reference performs the actual kfree. This keeps the object alive
  until disconnect and the final channel teardown finish, regardless of
  how the context thread, channel close, and USB disconnect paths
  interleave.

- Add a destroying_flag that pvr2_context_destroy() sets under
  pvr2_context_mutex before unlinking the context from the notify and
  exist lists. pvr2_context_set_notify_locked() refuses to re-enqueue a
  context whose destroying_flag is set, so a late notifier arriving
  after destroy has started cannot resurrect the context on the notify
  list. The dequeue path (fl == 0) still proceeds unconditionally
  because destroy itself must be able to remove any still-queued entry.

- Update pvr2_context_exit() to enqueue through
  pvr2_context_set_notify_locked() after releasing mp->mutex. The
  caller (channel close or disconnect) always holds a reference, so the
  object is stable across the mp->mutex / pvr2_context_mutex hand-off
  and a concurrent destroy cannot free it under us. If destroy has
  already won the race, destroying_flag short-circuits the enqueue into
  a no-op.

Lock ordering: pvr2_context_mutex is only acquired after mp->mutex is
released; no path holds pvr2_context_mutex while acquiring mp->mutex,
so no AB/BA deadlock is introduced. wake_up() on
pvr2_context_sync_data is moved outside pvr2_context_mutex in every
path that grew a new locked section, matching the existing style.

Fixes: 0a0b79ea55de ("media: pvrusb2: fix uaf in pvr2_context_set_notify")
Cc: stable@vger.kernel.org
Signed-off-by: Sangyun Kim <sangyun.kim@snu.ac.kr>
---
 drivers/media/usb/pvrusb2/pvrusb2-context.c | 56 ++++++++++++++++++---
 drivers/media/usb/pvrusb2/pvrusb2-context.h |  3 ++
 2 files changed, 51 insertions(+), 8 deletions(-)

diff --git a/drivers/media/usb/pvrusb2/pvrusb2-context.c b/drivers/media/usb/pvrusb2/pvrusb2-context.c
index 93f5da65ead9..fb9bdbf5c886 100644
--- a/drivers/media/usb/pvrusb2/pvrusb2-context.c
+++ b/drivers/media/usb/pvrusb2/pvrusb2-context.c
@@ -27,11 +27,19 @@ static int pvr2_context_cleaned_flag;
 static struct task_struct *pvr2_context_thread_ptr;
 
 
-static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
+static void pvr2_context_put(struct pvr2_context *mp)
+{
+	if (refcount_dec_and_test(&mp->refcount))
+		kfree(mp);
+}
+
+static int pvr2_context_set_notify_locked(struct pvr2_context *mp, int fl)
 {
 	int signal_flag = 0;
-	mutex_lock(&pvr2_context_mutex);
+
 	if (fl) {
+		if (mp->destroying_flag)
+			return 0;
 		if (!mp->notify_flag) {
 			signal_flag = (pvr2_context_notify_first == NULL);
 			mp->notify_prev = pvr2_context_notify_last;
@@ -59,6 +67,15 @@ static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
 			}
 		}
 	}
+	return signal_flag;
+}
+
+static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
+{
+	int signal_flag = 0;
+
+	mutex_lock(&pvr2_context_mutex);
+	signal_flag = pvr2_context_set_notify_locked(mp, fl);
 	mutex_unlock(&pvr2_context_mutex);
 	if (signal_flag) wake_up(&pvr2_context_sync_data);
 }
@@ -66,10 +83,13 @@ static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
 
 static void pvr2_context_destroy(struct pvr2_context *mp)
 {
+	int signal_flag = 0;
+
 	pvr2_trace(PVR2_TRACE_CTXT,"pvr2_context %p (destroy)",mp);
 	pvr2_hdw_destroy(mp->hdw);
-	pvr2_context_set_notify(mp, 0);
 	mutex_lock(&pvr2_context_mutex);
+	mp->destroying_flag = !0;
+	pvr2_context_set_notify_locked(mp, 0);
 	if (mp->exist_next) {
 		mp->exist_next->exist_prev = mp->exist_prev;
 	} else {
@@ -83,10 +103,12 @@ static void pvr2_context_destroy(struct pvr2_context *mp)
 	if (!pvr2_context_exist_first) {
 		/* Trigger wakeup on control thread in case it is waiting
 		   for an exit condition. */
-		wake_up(&pvr2_context_sync_data);
+		signal_flag = !0;
 	}
 	mutex_unlock(&pvr2_context_mutex);
-	kfree(mp);
+	if (signal_flag)
+		wake_up(&pvr2_context_sync_data);
+	pvr2_context_put(mp);
 }
 
 
@@ -209,6 +231,7 @@ struct pvr2_context *pvr2_context_create(
 	pvr2_trace(PVR2_TRACE_CTXT,"pvr2_context %p (create)",mp);
 	mp->setup_func = setup_func;
 	mutex_init(&mp->mutex);
+	refcount_set(&mp->refcount, 1);
 	mutex_lock(&pvr2_context_mutex);
 	mp->exist_prev = pvr2_context_exist_last;
 	mp->exist_next = NULL;
@@ -256,25 +279,41 @@ static void pvr2_context_enter(struct pvr2_context *mp)
 static void pvr2_context_exit(struct pvr2_context *mp)
 {
 	int destroy_flag = 0;
+	int signal_flag = 0;
 	if (!(mp->mc_first || !mp->disconnect_flag)) {
 		destroy_flag = !0;
 	}
 	mutex_unlock(&mp->mutex);
-	if (destroy_flag) pvr2_context_notify(mp);
+	if (destroy_flag) {
+		mutex_lock(&pvr2_context_mutex);
+		signal_flag = pvr2_context_set_notify_locked(mp, !0);
+		mutex_unlock(&pvr2_context_mutex);
+		if (signal_flag)
+			wake_up(&pvr2_context_sync_data);
+	}
 }
 
 
 void pvr2_context_disconnect(struct pvr2_context *mp)
 {
+	int signal_flag = 0;
+
+	refcount_inc(&mp->refcount);
 	pvr2_hdw_disconnect(mp->hdw);
-	if (!pvr2_context_shutok())
-		pvr2_context_notify(mp);
+	mutex_lock(&pvr2_context_mutex);
 	mp->disconnect_flag = !0;
+	if (!pvr2_context_shutok())
+		signal_flag = pvr2_context_set_notify_locked(mp, !0);
+	mutex_unlock(&pvr2_context_mutex);
+	if (signal_flag)
+		wake_up(&pvr2_context_sync_data);
+	pvr2_context_put(mp);
 }
 
 
 void pvr2_channel_init(struct pvr2_channel *cp,struct pvr2_context *mp)
 {
+	refcount_inc(&mp->refcount);
 	pvr2_context_enter(mp);
 	cp->hdw = mp->hdw;
 	cp->mc_head = mp;
@@ -318,6 +357,7 @@ void pvr2_channel_done(struct pvr2_channel *cp)
 	}
 	cp->hdw = NULL;
 	pvr2_context_exit(mp);
+	pvr2_context_put(mp);
 }
 
 
diff --git a/drivers/media/usb/pvrusb2/pvrusb2-context.h b/drivers/media/usb/pvrusb2/pvrusb2-context.h
index 5840b2ce8f1e..4e06530eccb8 100644
--- a/drivers/media/usb/pvrusb2/pvrusb2-context.h
+++ b/drivers/media/usb/pvrusb2/pvrusb2-context.h
@@ -7,6 +7,7 @@
 #define __PVRUSB2_CONTEXT_H
 
 #include <linux/mutex.h>
+#include <linux/refcount.h>
 #include <linux/usb.h>
 #include <linux/workqueue.h>
 
@@ -33,9 +34,11 @@ struct pvr2_context {
 	struct pvr2_hdw *hdw;
 	struct pvr2_context_stream video_stream;
 	struct mutex mutex;
+	refcount_t refcount;
 	int notify_flag;
 	int initialized_flag;
 	int disconnect_flag;
+	int destroying_flag;
 
 	/* Called after pvr2_context initialization is complete */
 	void (*setup_func)(struct pvr2_context *);
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH] media: pvrusb2: fix disconnect and teardown races
  2026-04-20  6:06 [PATCH] media: pvrusb2: fix disconnect and teardown races Sangyun Kim
@ 2026-05-05 13:14 ` Hans Verkuil
  0 siblings, 0 replies; 2+ messages in thread
From: Hans Verkuil @ 2026-05-05 13:14 UTC (permalink / raw)
  To: Sangyun Kim, Mike Isely, Mauro Carvalho Chehab, Hans Verkuil,
	Edward Adam Davis
  Cc: linux-media, linux-kernel, stable

On 4/20/26 08:06, Sangyun Kim wrote:
> pvr2_context_disconnect() queues a notification to the pvrusb2-context
> kthread before it stores mp->disconnect_flag:
> 
>     pvr2_hdw_disconnect(mp->hdw);
>     if (!pvr2_context_shutok())
>         pvr2_context_notify(mp);
>     mp->disconnect_flag = !0;
> 
> The context thread only destroys a context when disconnect_flag is set
> and mc_first is NULL. If the notification wakes the thread before the
> flag store becomes visible, the thread dequeues the context, runs
> pvr2_context_check() with disconnect_flag still observed as 0, decides
> that the destroy condition is not met yet, and goes back to sleep.
> Nothing wakes the thread again once the flag is finally stored, so the
> context stays on the global exist list forever and
> pvr2_context_global_done() blocks module unload. commit 0a0b79ea55de
> ("media: pvrusb2: fix uaf in pvr2_context_set_notify") made this
> liveness failure easier to hit by moving the notify earlier in the
> disconnect path.
> 
> The same teardown sequence still contains a use-after-free.
> pvr2_context_exit() inspects disconnect_flag after releasing mp->mutex
> and may then call pvr2_context_notify(mp) after the context thread has
> already freed the object via pvr2_context_destroy()/kfree(). The hdw
> completion callback registered through pvr2_hdw_initialize() can race
> the same way. Reordering the disconnect path alone closes the unload
> hang, but it still leaves late notifiers able to touch freed memory.
> 
> Fix both problems together:
> 
> - Split pvr2_context_set_notify() into a locked helper
>   (pvr2_context_set_notify_locked()) and a wrapper that acquires
>   pvr2_context_mutex. This lets callers update several pieces of
>   related state inside a single critical section without relocking.
> 
> - In pvr2_context_disconnect(), set disconnect_flag and enqueue the
>   thread notification under pvr2_context_mutex. The context thread
>   manipulates the notify list under the same mutex, so when it observes
>   the queued entry it is guaranteed to observe disconnect_flag = 1 as
>   well and the destroy condition evaluates correctly. This eliminates
>   the original notify-before-flag liveness hole.
> 
> - Add a per-context refcount_t. pvr2_context_create() initialises it to
>   1 (creator reference). pvr2_channel_init() and pvr2_channel_done()
>   take and drop a reference around each channel's lifetime.
>   pvr2_context_disconnect() takes a temporary reference across its body
>   so the context cannot be freed while disconnect is still touching it.
>   pvr2_context_destroy() no longer calls kfree() directly; it drops its
>   reference via pvr2_context_put(), and whichever caller drops the last
>   reference performs the actual kfree. This keeps the object alive
>   until disconnect and the final channel teardown finish, regardless of
>   how the context thread, channel close, and USB disconnect paths
>   interleave.
> 
> - Add a destroying_flag that pvr2_context_destroy() sets under
>   pvr2_context_mutex before unlinking the context from the notify and
>   exist lists. pvr2_context_set_notify_locked() refuses to re-enqueue a
>   context whose destroying_flag is set, so a late notifier arriving
>   after destroy has started cannot resurrect the context on the notify
>   list. The dequeue path (fl == 0) still proceeds unconditionally
>   because destroy itself must be able to remove any still-queued entry.
> 
> - Update pvr2_context_exit() to enqueue through
>   pvr2_context_set_notify_locked() after releasing mp->mutex. The
>   caller (channel close or disconnect) always holds a reference, so the
>   object is stable across the mp->mutex / pvr2_context_mutex hand-off
>   and a concurrent destroy cannot free it under us. If destroy has
>   already won the race, destroying_flag short-circuits the enqueue into
>   a no-op.
> 
> Lock ordering: pvr2_context_mutex is only acquired after mp->mutex is
> released; no path holds pvr2_context_mutex while acquiring mp->mutex,
> so no AB/BA deadlock is introduced. wake_up() on
> pvr2_context_sync_data is moved outside pvr2_context_mutex in every
> path that grew a new locked section, matching the existing style.

Huge commit log, weird patch. All the hallmarks of AI slop.

Rejected.

Regards,

	Hans

> 
> Fixes: 0a0b79ea55de ("media: pvrusb2: fix uaf in pvr2_context_set_notify")
> Cc: stable@vger.kernel.org
> Signed-off-by: Sangyun Kim <sangyun.kim@snu.ac.kr>
> ---
>  drivers/media/usb/pvrusb2/pvrusb2-context.c | 56 ++++++++++++++++++---
>  drivers/media/usb/pvrusb2/pvrusb2-context.h |  3 ++
>  2 files changed, 51 insertions(+), 8 deletions(-)
> 
> diff --git a/drivers/media/usb/pvrusb2/pvrusb2-context.c b/drivers/media/usb/pvrusb2/pvrusb2-context.c
> index 93f5da65ead9..fb9bdbf5c886 100644
> --- a/drivers/media/usb/pvrusb2/pvrusb2-context.c
> +++ b/drivers/media/usb/pvrusb2/pvrusb2-context.c
> @@ -27,11 +27,19 @@ static int pvr2_context_cleaned_flag;
>  static struct task_struct *pvr2_context_thread_ptr;
>  
>  
> -static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
> +static void pvr2_context_put(struct pvr2_context *mp)
> +{
> +	if (refcount_dec_and_test(&mp->refcount))
> +		kfree(mp);
> +}
> +
> +static int pvr2_context_set_notify_locked(struct pvr2_context *mp, int fl)
>  {
>  	int signal_flag = 0;
> -	mutex_lock(&pvr2_context_mutex);
> +
>  	if (fl) {
> +		if (mp->destroying_flag)
> +			return 0;
>  		if (!mp->notify_flag) {
>  			signal_flag = (pvr2_context_notify_first == NULL);
>  			mp->notify_prev = pvr2_context_notify_last;
> @@ -59,6 +67,15 @@ static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
>  			}
>  		}
>  	}
> +	return signal_flag;
> +}
> +
> +static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
> +{
> +	int signal_flag = 0;
> +
> +	mutex_lock(&pvr2_context_mutex);
> +	signal_flag = pvr2_context_set_notify_locked(mp, fl);
>  	mutex_unlock(&pvr2_context_mutex);
>  	if (signal_flag) wake_up(&pvr2_context_sync_data);
>  }
> @@ -66,10 +83,13 @@ static void pvr2_context_set_notify(struct pvr2_context *mp, int fl)
>  
>  static void pvr2_context_destroy(struct pvr2_context *mp)
>  {
> +	int signal_flag = 0;
> +
>  	pvr2_trace(PVR2_TRACE_CTXT,"pvr2_context %p (destroy)",mp);
>  	pvr2_hdw_destroy(mp->hdw);
> -	pvr2_context_set_notify(mp, 0);
>  	mutex_lock(&pvr2_context_mutex);
> +	mp->destroying_flag = !0;
> +	pvr2_context_set_notify_locked(mp, 0);
>  	if (mp->exist_next) {
>  		mp->exist_next->exist_prev = mp->exist_prev;
>  	} else {
> @@ -83,10 +103,12 @@ static void pvr2_context_destroy(struct pvr2_context *mp)
>  	if (!pvr2_context_exist_first) {
>  		/* Trigger wakeup on control thread in case it is waiting
>  		   for an exit condition. */
> -		wake_up(&pvr2_context_sync_data);
> +		signal_flag = !0;
>  	}
>  	mutex_unlock(&pvr2_context_mutex);
> -	kfree(mp);
> +	if (signal_flag)
> +		wake_up(&pvr2_context_sync_data);
> +	pvr2_context_put(mp);
>  }
>  
>  
> @@ -209,6 +231,7 @@ struct pvr2_context *pvr2_context_create(
>  	pvr2_trace(PVR2_TRACE_CTXT,"pvr2_context %p (create)",mp);
>  	mp->setup_func = setup_func;
>  	mutex_init(&mp->mutex);
> +	refcount_set(&mp->refcount, 1);
>  	mutex_lock(&pvr2_context_mutex);
>  	mp->exist_prev = pvr2_context_exist_last;
>  	mp->exist_next = NULL;
> @@ -256,25 +279,41 @@ static void pvr2_context_enter(struct pvr2_context *mp)
>  static void pvr2_context_exit(struct pvr2_context *mp)
>  {
>  	int destroy_flag = 0;
> +	int signal_flag = 0;
>  	if (!(mp->mc_first || !mp->disconnect_flag)) {
>  		destroy_flag = !0;
>  	}
>  	mutex_unlock(&mp->mutex);
> -	if (destroy_flag) pvr2_context_notify(mp);
> +	if (destroy_flag) {
> +		mutex_lock(&pvr2_context_mutex);
> +		signal_flag = pvr2_context_set_notify_locked(mp, !0);
> +		mutex_unlock(&pvr2_context_mutex);
> +		if (signal_flag)
> +			wake_up(&pvr2_context_sync_data);
> +	}
>  }
>  
>  
>  void pvr2_context_disconnect(struct pvr2_context *mp)
>  {
> +	int signal_flag = 0;
> +
> +	refcount_inc(&mp->refcount);
>  	pvr2_hdw_disconnect(mp->hdw);
> -	if (!pvr2_context_shutok())
> -		pvr2_context_notify(mp);
> +	mutex_lock(&pvr2_context_mutex);
>  	mp->disconnect_flag = !0;
> +	if (!pvr2_context_shutok())
> +		signal_flag = pvr2_context_set_notify_locked(mp, !0);
> +	mutex_unlock(&pvr2_context_mutex);
> +	if (signal_flag)
> +		wake_up(&pvr2_context_sync_data);
> +	pvr2_context_put(mp);
>  }
>  
>  
>  void pvr2_channel_init(struct pvr2_channel *cp,struct pvr2_context *mp)
>  {
> +	refcount_inc(&mp->refcount);
>  	pvr2_context_enter(mp);
>  	cp->hdw = mp->hdw;
>  	cp->mc_head = mp;
> @@ -318,6 +357,7 @@ void pvr2_channel_done(struct pvr2_channel *cp)
>  	}
>  	cp->hdw = NULL;
>  	pvr2_context_exit(mp);
> +	pvr2_context_put(mp);
>  }
>  
>  
> diff --git a/drivers/media/usb/pvrusb2/pvrusb2-context.h b/drivers/media/usb/pvrusb2/pvrusb2-context.h
> index 5840b2ce8f1e..4e06530eccb8 100644
> --- a/drivers/media/usb/pvrusb2/pvrusb2-context.h
> +++ b/drivers/media/usb/pvrusb2/pvrusb2-context.h
> @@ -7,6 +7,7 @@
>  #define __PVRUSB2_CONTEXT_H
>  
>  #include <linux/mutex.h>
> +#include <linux/refcount.h>
>  #include <linux/usb.h>
>  #include <linux/workqueue.h>
>  
> @@ -33,9 +34,11 @@ struct pvr2_context {
>  	struct pvr2_hdw *hdw;
>  	struct pvr2_context_stream video_stream;
>  	struct mutex mutex;
> +	refcount_t refcount;
>  	int notify_flag;
>  	int initialized_flag;
>  	int disconnect_flag;
> +	int destroying_flag;
>  
>  	/* Called after pvr2_context initialization is complete */
>  	void (*setup_func)(struct pvr2_context *);


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-05-05 13:14 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-20  6:06 [PATCH] media: pvrusb2: fix disconnect and teardown races Sangyun Kim
2026-05-05 13:14 ` Hans Verkuil

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox