From nobody Tue Jun 16 12:47:54 2026 Received: from mail-pj1-f50.google.com (mail-pj1-f50.google.com [209.85.216.50]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 28AE22C3266 for ; Mon, 20 Apr 2026 06:06:40 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.216.50 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776665203; cv=none; b=QuL27PffmHWoUSIGjI16zlTAljww+iaJifFBlqI1ANbg296FizhAU8HaDQZ04EpO1sEIB6pYqpOUevtZhFGKZ0KRpilUDogFb8JLuaqTairfpK8c/chKGYynU+5FyeoBre0ukdZ4b37QyAO6C3rmuD0Qj1ak/nOT2cRKLOmw4CU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776665203; c=relaxed/simple; bh=o9VyHHljdlNryQxgGpX/rIFtgqHL6AXLc8VCdQZtJUw=; h=From:To:Cc:Subject:Date:Message-Id:MIME-Version; b=j4lmH5KDrCfMvtddF6YkVh3mcgpmZwLZmj+33kiNk3wRtEoMLBQBEBoscaa/3d6QrbPyqP5QgNyAMGcVQX1ZZCMcy1qiAKbmNVFLZ71eNuONTGeic51Y67KwKDproVaLgPSX1O69kmdb6nBR0hY6d8c33D3PWoEoz0m7CNwl21w= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=snu.ac.kr; spf=pass smtp.mailfrom=snu.ac.kr; dkim=pass (1024-bit key) header.d=snu.ac.kr header.i=@snu.ac.kr header.b=k2SKip8x; arc=none smtp.client-ip=209.85.216.50 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=snu.ac.kr Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=snu.ac.kr Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=snu.ac.kr header.i=@snu.ac.kr header.b="k2SKip8x" Received: by mail-pj1-f50.google.com with SMTP id 98e67ed59e1d1-35e576110adso1747598a91.0 for ; Sun, 19 Apr 2026 23:06:40 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=snu.ac.kr; s=google; t=1776665199; x=1777269999; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=sbHCUNdavsi9m4FkYItkV2uwfkFJaRWF7u5PuSyGeu8=; b=k2SKip8xU1lDGEOVv9gHsepsT/agF+sar6U9mGEnC7datNI8gbFlifYZK3c5+DEpLL Yq3lgPs6uCO5FUbYM2CF3AwmDD4LrDKmO+PQh1V0BmzAdYb9c848zoSVPOnjDFDR+UEp BvbTiu4D7xnNPIH9+TYPy6S6cIevgCYkgEtzI= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776665199; x=1777269999; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=sbHCUNdavsi9m4FkYItkV2uwfkFJaRWF7u5PuSyGeu8=; b=jhJg8QDOxbqjm3AIJGiABw3f+Vl1c3Avvluw9eG3RiNo19FbyXw4Ql2UlJWyu5P1Cv uBAyS8QsZB0uF2V5hcWrcxiF8V99XtafqZ/OfRV1eM0H3mX+SpFCU3e5hsnA7fSsXKKb 7tWVCQZr4Z95JbYiCHqoF4IwMS7Nfs18W4KaJGtei1/D3qYwMvxCxHP7Hci38kYXpc2g Y0n7CdLGVxwhiuz+uPX5S2G7ZUCr0eA/UIH+/XPL3x0y9i9yV2BRpeRJrF9e8BsV5Uiq oxykgxdmWY6HFf2mHJKfrxlIhUcVwu6EE0UEokhw1EHD3PWFLEpwfu2vBwk7wD5062Xo Qvvw== X-Forwarded-Encrypted: i=1; AFNElJ+7TJ817kEI1+xxTjPTdiFI4Kv59L3mSRzhnttu7J6TUlsI30qg9b1KVv3Gd7pWJ6GDnc5Ne8YSf2wOv9g=@vger.kernel.org X-Gm-Message-State: AOJu0Yw3lxZa4YS1dV1JgAXrf6BEcH7BJ57/7Ymg8So9SvhH+Inbai4s ODK/czWWQjMyNcduUld2cmjtH+/gKmXoO/5j5tWLQ9h3ROHB1SS3Wtwmp+3wik5rpAk= X-Gm-Gg: AeBDietwnEJdLfIr4tp7gjCBynSqiP3Zk9lrIqzo1j0HRSgtgMmVaRANOKWLztn4opH gc3tV8wQdhND3U0vNXh0AAYi1+C5BXghQlQHoF5dDD4+Ifu58N4Mbcl0tmnU6NzXjMEUUc6jYur 1raeGIQMfrOAu4VvEuF58yvq+8eng1ympic82BMklqdN9mTo72WYOmUaojdMf28WeCe59hplmHA 2a0rVVUZ8Mvf8Rpn9/8qERJxCms3afesz9KUP+KaxkU68yHd9A4+CMxp6xFAC+1VvzkiOb1dhgH 9tLpHtL5WfT9D7aaFh5EXyyIEna7PZjhxSVgECgtN+Js55DeR+95h2ki6HmMVaHLEzcC7FtHdbm RpapSfMV/C4vLKUE1s4VqxukpTC1iV8/5H1Zq6gz8C/K52E4y0MNU4lg6w3rpetxDOa3fyfEgWz 3gWTublK6Qy4ve1ZX2nVIt0+fcm7bsAr9jK42E5YJuphA0ESrpzRotYKsdjexfJWQJsKx2rQ== X-Received: by 2002:a17:90b:1e10:b0:35f:c6bf:2bba with SMTP id 98e67ed59e1d1-36140299a0dmr12086964a91.11.1776665199511; Sun, 19 Apr 2026 23:06:39 -0700 (PDT) Received: from nunu.. (nunu.snu.ac.kr. [147.46.112.82]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-3614195a8f0sm9082277a91.12.2026.04.19.23.06.37 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 19 Apr 2026 23:06:39 -0700 (PDT) From: Sangyun Kim To: Mike Isely , Mauro Carvalho Chehab , Hans Verkuil , Edward Adam Davis Cc: linux-media@vger.kernel.org, linux-kernel@vger.kernel.org, stable@vger.kernel.org Subject: [PATCH] media: pvrusb2: fix disconnect and teardown races Date: Mon, 20 Apr 2026 15:06:21 +0900 Message-Id: <20260420060621.1627352-1-sangyun.kim@snu.ac.kr> X-Mailer: git-send-email 2.34.1 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 =3D !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 =3D 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 =3D=3D 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 --- 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/us= b/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; =20 =20 -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 =3D 0; - mutex_lock(&pvr2_context_mutex); + if (fl) { + if (mp->destroying_flag) + return 0; if (!mp->notify_flag) { signal_flag =3D (pvr2_context_notify_first =3D=3D NULL); mp->notify_prev =3D 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 =3D 0; + + mutex_lock(&pvr2_context_mutex); + signal_flag =3D 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) =20 static void pvr2_context_destroy(struct pvr2_context *mp) { + int signal_flag =3D 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 =3D !0; + pvr2_context_set_notify_locked(mp, 0); if (mp->exist_next) { mp->exist_next->exist_prev =3D 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 =3D !0; } mutex_unlock(&pvr2_context_mutex); - kfree(mp); + if (signal_flag) + wake_up(&pvr2_context_sync_data); + pvr2_context_put(mp); } =20 =20 @@ -209,6 +231,7 @@ struct pvr2_context *pvr2_context_create( pvr2_trace(PVR2_TRACE_CTXT,"pvr2_context %p (create)",mp); mp->setup_func =3D setup_func; mutex_init(&mp->mutex); + refcount_set(&mp->refcount, 1); mutex_lock(&pvr2_context_mutex); mp->exist_prev =3D pvr2_context_exist_last; mp->exist_next =3D NULL; @@ -256,25 +279,41 @@ static void pvr2_context_enter(struct pvr2_context *m= p) static void pvr2_context_exit(struct pvr2_context *mp) { int destroy_flag =3D 0; + int signal_flag =3D 0; if (!(mp->mc_first || !mp->disconnect_flag)) { destroy_flag =3D !0; } mutex_unlock(&mp->mutex); - if (destroy_flag) pvr2_context_notify(mp); + if (destroy_flag) { + mutex_lock(&pvr2_context_mutex); + signal_flag =3D pvr2_context_set_notify_locked(mp, !0); + mutex_unlock(&pvr2_context_mutex); + if (signal_flag) + wake_up(&pvr2_context_sync_data); + } } =20 =20 void pvr2_context_disconnect(struct pvr2_context *mp) { + int signal_flag =3D 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 =3D !0; + if (!pvr2_context_shutok()) + signal_flag =3D 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); } =20 =20 void pvr2_channel_init(struct pvr2_channel *cp,struct pvr2_context *mp) { + refcount_inc(&mp->refcount); pvr2_context_enter(mp); cp->hdw =3D mp->hdw; cp->mc_head =3D mp; @@ -318,6 +357,7 @@ void pvr2_channel_done(struct pvr2_channel *cp) } cp->hdw =3D NULL; pvr2_context_exit(mp); + pvr2_context_put(mp); } =20 =20 diff --git a/drivers/media/usb/pvrusb2/pvrusb2-context.h b/drivers/media/us= b/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 =20 #include +#include #include #include =20 @@ -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; =20 /* Called after pvr2_context initialization is complete */ void (*setup_func)(struct pvr2_context *); --=20 2.34.1