mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH RESEND wireless 0/4] wifi: rtl8xxxu: keep RX requests available across transient errors
@ 2026-09-12 22:09 kimwooseok
  2026-09-12 22:09 ` [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails kimwooseok
                   ` (3 more replies)
  0 siblings, 4 replies; 7+ messages in thread
From: kimwooseok @ 2026-09-12 22:09 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jes.Sorensen, linux-kernel

This resends the same patches after line wrapping damaged the previous
posting and prevented it from being applied. There are no code changes.

Previous posting:
https://lore.kernel.org/linux-wireless/CAH1Bc_wBUmmNyzF3c2H1aFBRBbAPSw0hR=1MnPOAcCXypVL_Dg@mail.gmail.com/

rtl8xxxu maintains a pool of 32 RX URBs. Successful completions return
their URBs to a pending list and schedule the RX worker when more than
eight URBs are waiting. The worker allocates fresh receive buffers and
resubmits the queued URBs in a batch.

Completion errors free the affected URBs, reducing the pool. Once eight
or fewer survive, their completions can leave all remaining requests on
the pending list without reaching the worker scheduling threshold. If no
request is in flight and no worker is pending or running, nothing triggers
further submissions, so RX remains stalled after the errors stop.

Temporary submission failures can leave requests in the same state, while
retaining an skb that the next submission attempt overwrites.

This series fixes buffer ownership on failed submission, unwinds
incomplete startup, and preserves requests through one delayed retry path.
The retry worker schedules submission even for a single request. A
separate error list provides a 100 ms batch delay from the first error
while preserving normal successful RX batching. Shutdown drains retry
work, submission work and active requests in that order.

Completion retries cover EPROTO, EILSEQ, ETIME, EOVERFLOW, ECOMM and ENOSR;
ENOMEM/EAGAIN from startup or worker submission uses the same retry path.
Cancellation and removal keep their release behavior. EPIPE endpoint-halt
recovery is outside this series.

With 24 injected EPROTO completions, the original driver stopped at eight
pending requests and zero in flight for about 30 seconds. Under the same
error budget, the patched driver retained all 32 requests and resumed RX
without restarting the interface.

Validation:
- ARM64 QEMU KUnit: 11/11 cases passed, covering the actual RX helpers,
  worker, buffer ownership, startup failures and shutdown.
- RTL8192EU on Raspberry Pi, kernel 6.18.46-thesis-test-rt+: 13/13 cases
  passed using a compatibility backport. The lab fixture substitutes
  completion statuses and injects submission and allocation failures.
  Each of the six completion statuses recovered after 32 completion
  errors followed by 96 submission failures. Error-window, pending-retry
  stop, startup failure and clean RX/down-up cases also passed.
- W=1 ARM64 allmodconfig and allyesconfig builds passed with incremental
  caches; the changed driver and KUnit objects rebuilt in both.
  Sparse added no diagnostics relative to baseline.
- Each intermediate production commit compiled, and sequential patch
  application reproduced the corresponding source trees.

kimwooseok (4):
  wifi: rtl8xxxu: free RX skb when URB submission fails
  wifi: rtl8xxxu: unwind incomplete receive startup
  wifi: rtl8xxxu: preserve RX requests across recoverable transfer
    errors
  wifi: rtl8xxxu: test RX ownership and recovery across failures

 .../wireless/realtek/rtl8xxxu/.kunitconfig    |  16 +
 drivers/net/wireless/realtek/rtl8xxxu/Kconfig |  11 +
 .../net/wireless/realtek/rtl8xxxu/Makefile    |   3 +
 drivers/net/wireless/realtek/rtl8xxxu/core.c  | 236 +++++++--
 .../net/wireless/realtek/rtl8xxxu/rtl8xxxu.h  |   3 +
 .../net/wireless/realtek/rtl8xxxu/rx-test.c   | 461 ++++++++++++++++++
 .../net/wireless/realtek/rtl8xxxu/rx-test.h   |  27 +
 7 files changed, 707 insertions(+), 50 deletions(-)
 create mode 100644 drivers/net/wireless/realtek/rtl8xxxu/.kunitconfig
 create mode 100644 drivers/net/wireless/realtek/rtl8xxxu/rx-test.c
 create mode 100644 drivers/net/wireless/realtek/rtl8xxxu/rx-test.h


base-commit: f71dd599a98182d6bc34dce39977f928068ecd64
-- 
2.48.1

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

* [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails
  2026-09-12 22:09 [PATCH RESEND wireless 0/4] wifi: rtl8xxxu: keep RX requests available across transient errors kimwooseok
@ 2026-09-12 22:09 ` kimwooseok
  2026-09-13  3:02   ` Ping-Ke Shih
  2026-09-12 22:09 ` [PATCH RESEND wireless 2/4] wifi: rtl8xxxu: unwind incomplete receive startup kimwooseok
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 7+ messages in thread
From: kimwooseok @ 2026-09-12 22:09 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jes.Sorensen, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 1228 bytes --]

A failed RX URB submission can leak the skb allocated for that attempt.
rtl8xxxu_submit_rx_urb() allocates the buffer and stores it in urb.context,
but if usb_submit_urb() fails, it only unanchors the URB. The RX worker
queues ENOMEM/EAGAIN failures for retry with that buffer still attached.

On the next attempt, rtl8xxxu_submit_rx_urb() allocates another skb and
overwrites urb.context, losing the reference to the previous buffer.
Stopping before the retry also leaks the buffer because pending-request
cleanup frees only the URB.

Fix the leak at the submission failure by making rtl8xxxu_submit_rx_urb()
responsible for both allocating the skb and releasing it when submission
fails. Free the newly allocated skb and clear urb.context before returning
the error, so neither retry nor stop receives a pending URB that still
owns a buffer. Remove the caller-side skb cleanup from start and the RX
worker; those callers now handle only whether to retry or release the URB.

Fixes: 26f1fad29ad9 ("New driver: rtl8xxxu (mac80211)")
Assisted-by: GPT-6 Astra
Signed-off-by: kimwooseok <5mghybrid@khu.ac.kr>
---
 drivers/net/wireless/realtek/rtl8xxxu/core.c | 16 +++++-----------
 1 file changed, 5 insertions(+), 11 deletions(-)

[-- Attachment #2: 0001-wifi-rtl8xxxu-free-RX-skb-when-URB-submission-fails.patch --]
[-- Type: text/x-patch, Size: 1778 bytes --]

diff --git a/drivers/net/wireless/realtek/rtl8xxxu/core.c b/drivers/net/wireless/realtek/rtl8xxxu/core.c
index bddbd0990de72..795a5ec2f8cd4 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/core.c
+++ b/drivers/net/wireless/realtek/rtl8xxxu/core.c
@@ -5864,7 +5864,6 @@ static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 	struct rtl8xxxu_priv *priv;
 	struct rtl8xxxu_rx_urb *rx_urb, *tmp;
 	struct list_head local;
-	struct sk_buff *skb;
 	unsigned long flags;
 	int ret;
 
@@ -5896,8 +5895,6 @@ static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 		default:
 			dev_warn(&priv->udev->dev,
 				 "failed to requeue urb with error %i\n", ret);
-			skb = (struct sk_buff *)rx_urb->urb.context;
-			dev_kfree_skb(skb);
 			usb_free_urb(&rx_urb->urb);
 		}
 	}
@@ -6596,8 +6593,11 @@ static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
 			  skb_size, rtl8xxxu_rx_complete, skb);
 	usb_anchor_urb(&rx_urb->urb, &priv->rx_anchor);
 	ret = usb_submit_urb(&rx_urb->urb, GFP_ATOMIC);
-	if (ret)
+	if (ret) {
 		usb_unanchor_urb(&rx_urb->urb);
+		dev_kfree_skb(skb);
+		rx_urb->urb.context = NULL;
+	}
 	return ret;
 }
 
@@ -7410,7 +7410,6 @@ static int rtl8xxxu_start(struct ieee80211_hw *hw)
 	struct rtl8xxxu_priv *priv = hw->priv;
 	struct rtl8xxxu_rx_urb *rx_urb;
 	struct rtl8xxxu_tx_urb *tx_urb;
-	struct sk_buff *skb;
 	unsigned long flags;
 	int ret, i;
 
@@ -7461,13 +7460,8 @@ static int rtl8xxxu_start(struct ieee80211_hw *hw)
 		rx_urb->hw = hw;
 
 		ret = rtl8xxxu_submit_rx_urb(priv, rx_urb);
-		if (ret) {
-			if (ret != -ENOMEM) {
-				skb = (struct sk_buff *)rx_urb->urb.context;
-				dev_kfree_skb(skb);
-			}
+		if (ret)
 			rtl8xxxu_queue_rx_urb(priv, rx_urb);
-		}
 	}
 
 	schedule_delayed_work(&priv->ra_watchdog, 2 * HZ);
-- 
2.48.1


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

* [PATCH RESEND wireless 2/4] wifi: rtl8xxxu: unwind incomplete receive startup
  2026-09-12 22:09 [PATCH RESEND wireless 0/4] wifi: rtl8xxxu: keep RX requests available across transient errors kimwooseok
  2026-09-12 22:09 ` [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails kimwooseok
@ 2026-09-12 22:09 ` kimwooseok
  2026-09-12 22:09 ` [PATCH RESEND wireless 3/4] wifi: rtl8xxxu: preserve RX requests across recoverable transfer errors kimwooseok
  2026-09-12 22:10 ` [PATCH RESEND wireless 4/4] wifi: rtl8xxxu: test RX ownership and recovery across failures kimwooseok
  3 siblings, 0 replies; 7+ messages in thread
From: kimwooseok @ 2026-09-12 22:09 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jes.Sorensen, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 1418 bytes --]

rtl8xxxu_start() allocates and submits RX URBs one at a time. If a later
allocation fails, earlier requests may already be active. After a
successful submission, that allocation failure can also leave ret set
to zero. The error path then frees TX resources and disables RX filters
without draining the earlier RX requests, yet reports startup success.

Separate pool allocation from submission so an allocation failure can be
handled before any RX request is active. Introduce rtl8xxxu_alloc_rx_urbs()
to allocate all 32 wrappers, then rtl8xxxu_start_rx() to submit the
completed pool. Return ENOMEM for every RX or TX URB pool allocation
failure so a partial allocation is reported as an error.

Once submission begins, keep ENOMEM/EAGAIN failures queued for retry.
For other submission errors, rtl8xxxu_start_rx() frees the unsubmitted
requests and returns the error. Since earlier submissions may already be
active at that point, route the outer start failure through the existing
rtl8xxxu_stop() path. This drains queued work and active requests and
cleans up RF state and TX resources together. Interrupt URB submission
failure uses the same cleanup path.

Fixes: 26f1fad29ad9 ("New driver: rtl8xxxu (mac80211)")
Assisted-by: GPT-6 Astra
Signed-off-by: kimwooseok <5mghybrid@khu.ac.kr>
---
 drivers/net/wireless/realtek/rtl8xxxu/core.c | 99 ++++++++++++++------
 1 file changed, 71 insertions(+), 28 deletions(-)

[-- Attachment #2: 0002-wifi-rtl8xxxu-unwind-incomplete-receive-startup.patch --]
[-- Type: text/x-patch, Size: 4752 bytes --]

diff --git a/drivers/net/wireless/realtek/rtl8xxxu/core.c b/drivers/net/wireless/realtek/rtl8xxxu/core.c
index 795a5ec2f8cd4..1932a9ec1970c 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/core.c
+++ b/drivers/net/wireless/realtek/rtl8xxxu/core.c
@@ -58,6 +58,7 @@ MODULE_PARM_DESC(dma_agg_pages, "Set DMA aggregation pages (range 1-127, 0 to di
 #define RTL8XXXU_TX_URB_LOW_WATER	25
 #define RTL8XXXU_TX_URB_HIGH_WATER	32
 
+static void rtl8xxxu_stop(struct ieee80211_hw *hw, bool suspend);
 static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
 				  struct rtl8xxxu_rx_urb *rx_urb);
 
@@ -5832,6 +5833,27 @@ static void rtl8xxxu_free_rx_resources(struct rtl8xxxu_priv *priv)
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 }
 
+static int rtl8xxxu_alloc_rx_urbs(struct rtl8xxxu_priv *priv)
+{
+	struct rtl8xxxu_rx_urb *rx_urb;
+	int i;
+
+	/* No RX work is active until the complete pool has been allocated. */
+	for (i = 0; i < RTL8XXXU_RX_URBS; i++) {
+		rx_urb = kmalloc_obj(struct rtl8xxxu_rx_urb);
+		if (!rx_urb)
+			return -ENOMEM;
+
+		usb_init_urb(&rx_urb->urb);
+		INIT_LIST_HEAD(&rx_urb->list);
+		rx_urb->hw = priv->hw;
+		list_add_tail(&rx_urb->list, &priv->rx_urb_pending_list);
+		priv->rx_urb_pending_count++;
+	}
+
+	return 0;
+}
+
 static void rtl8xxxu_queue_rx_urb(struct rtl8xxxu_priv *priv,
 				  struct rtl8xxxu_rx_urb *rx_urb)
 {
@@ -5900,6 +5922,44 @@ static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 	}
 }
 
+static int rtl8xxxu_start_rx(struct rtl8xxxu_priv *priv)
+{
+	struct rtl8xxxu_rx_urb *rx_urb, *tmp;
+	unsigned long flags;
+	LIST_HEAD(local);
+	int ret;
+
+	spin_lock_irqsave(&priv->rx_urb_lock, flags);
+	list_splice_init(&priv->rx_urb_pending_list, &local);
+	priv->rx_urb_pending_count = 0;
+	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
+
+	list_for_each_entry_safe(rx_urb, tmp, &local, list) {
+		list_del_init(&rx_urb->list);
+		ret = rtl8xxxu_submit_rx_urb(priv, rx_urb);
+		switch (ret) {
+		case 0:
+			break;
+		case -ENOMEM:
+		case -EAGAIN:
+			rtl8xxxu_queue_rx_urb(priv, rx_urb);
+			break;
+		default:
+			usb_free_urb(&rx_urb->urb);
+			goto free_remaining;
+		}
+	}
+
+	return 0;
+
+free_remaining:
+	list_for_each_entry_safe(rx_urb, tmp, &local, list) {
+		list_del(&rx_urb->list);
+		usb_free_urb(&rx_urb->urb);
+	}
+	return ret;
+}
+
 /*
  * The RTL8723BU/RTL8192EU vendor driver use coexistence table type
  * 0-7 to represent writing different combinations of register values
@@ -7408,7 +7468,6 @@ static void rtl8xxxu_watchdog_callback(struct work_struct *work)
 static int rtl8xxxu_start(struct ieee80211_hw *hw)
 {
 	struct rtl8xxxu_priv *priv = hw->priv;
-	struct rtl8xxxu_rx_urb *rx_urb;
 	struct rtl8xxxu_tx_urb *tx_urb;
 	unsigned long flags;
 	int ret, i;
@@ -7423,14 +7482,13 @@ static int rtl8xxxu_start(struct ieee80211_hw *hw)
 	if (priv->usb_interrupts) {
 		ret = rtl8xxxu_submit_int_urb(hw);
 		if (ret)
-			goto exit;
+			goto error_out;
 	}
 
 	for (i = 0; i < RTL8XXXU_TX_URBS; i++) {
 		tx_urb = kmalloc_obj(struct rtl8xxxu_tx_urb);
 		if (!tx_urb) {
-			if (!i)
-				ret = -ENOMEM;
+			ret = -ENOMEM;
 
 			goto error_out;
 		}
@@ -7441,31 +7499,21 @@ static int rtl8xxxu_start(struct ieee80211_hw *hw)
 		priv->tx_urb_free_count++;
 	}
 
+	ret = rtl8xxxu_alloc_rx_urbs(priv);
+	if (ret)
+		goto error_out;
+
 	priv->tx_stopped = false;
 
 	spin_lock_irqsave(&priv->rx_urb_lock, flags);
 	priv->shutdown = false;
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 
-	for (i = 0; i < RTL8XXXU_RX_URBS; i++) {
-		rx_urb = kmalloc_obj(struct rtl8xxxu_rx_urb);
-		if (!rx_urb) {
-			if (!i)
-				ret = -ENOMEM;
-
-			goto error_out;
-		}
-		usb_init_urb(&rx_urb->urb);
-		INIT_LIST_HEAD(&rx_urb->list);
-		rx_urb->hw = hw;
-
-		ret = rtl8xxxu_submit_rx_urb(priv, rx_urb);
-		if (ret)
-			rtl8xxxu_queue_rx_urb(priv, rx_urb);
-	}
+	ret = rtl8xxxu_start_rx(priv);
+	if (ret)
+		goto error_out;
 
 	schedule_delayed_work(&priv->ra_watchdog, 2 * HZ);
-exit:
 	/*
 	 * Accept all data and mgmt frames
 	 */
@@ -7478,13 +7526,7 @@ exit:
 	return ret;
 
 error_out:
-	rtl8xxxu_free_tx_resources(priv);
-	/*
-	 * Disable all data and mgmt frames
-	 */
-	rtl8xxxu_write16(priv, REG_RXFLTMAP2, 0x0000);
-	rtl8xxxu_write16(priv, REG_RXFLTMAP0, 0x0000);
-
+	rtl8xxxu_stop(hw, false);
 	return ret;
 }
 
@@ -7820,6 +7862,7 @@ static int rtl8xxxu_probe(struct usb_interface *interface,
 	spin_lock_init(&priv->tx_urb_lock);
 	INIT_LIST_HEAD(&priv->rx_urb_pending_list);
 	spin_lock_init(&priv->rx_urb_lock);
+	priv->shutdown = true;
 	INIT_WORK(&priv->rx_urb_wq, rtl8xxxu_rx_urb_work);
 	INIT_DELAYED_WORK(&priv->ra_watchdog, rtl8xxxu_watchdog_callback);
 	INIT_DELAYED_WORK(&priv->update_beacon_work, rtl8xxxu_update_beacon_work_callback);
-- 
2.48.1


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

* [PATCH RESEND wireless 3/4] wifi: rtl8xxxu: preserve RX requests across recoverable transfer errors
  2026-09-12 22:09 [PATCH RESEND wireless 0/4] wifi: rtl8xxxu: keep RX requests available across transient errors kimwooseok
  2026-09-12 22:09 ` [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails kimwooseok
  2026-09-12 22:09 ` [PATCH RESEND wireless 2/4] wifi: rtl8xxxu: unwind incomplete receive startup kimwooseok
@ 2026-09-12 22:09 ` kimwooseok
  2026-09-12 22:10 ` [PATCH RESEND wireless 4/4] wifi: rtl8xxxu: test RX ownership and recovery across failures kimwooseok
  3 siblings, 0 replies; 7+ messages in thread
From: kimwooseok @ 2026-09-12 22:09 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jes.Sorensen, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 2373 bytes --]

rtl8xxxu normally reuses 32 RX URBs, scheduling the submission worker when
more than eight completed requests have accumulated on the pending list.
Completion errors free URBs instead. A finite error burst can therefore
leave eight or fewer requests, which cannot reach that threshold after
they all complete. With no request in flight and no worker pending or
running, RX stays stopped even after the errors cease.

To prevent these errors from shrinking the pool below the number
needed for normal resubmission, retain URBs after EPROTO, EILSEQ, ETIME,
EOVERFLOW, ECOMM and ENOSR completions. EHCI can report ENOSR for IN data-buffer
errors, and FHCI maps RX buffer overrun to ECOMM. Free the failed
transfer's skb and keep its URB on a separate retry list.

Keeping the URBs is only part of the fix: the driver must also submit
them again without waiting for nine requests to accumulate. When the
first failed request enters the retry list, schedule delayed work for
100 ms. Further failures join that list while the work is pending.
When it runs, move the collected requests to normal pending and schedule
the submission worker even if only one request is waiting. Keeping
failed requests separate until then prevents normal completions from
triggering an immediate retry; successful RX keeps its existing batching.

A retry can itself fail with ENOMEM/EAGAIN. Returning that request to
normal pending would bring back the same threshold problem, so route
temporary submission failures from both start and the RX worker through
the delayed retry list as well.

Serialize retry insertion and scheduling with shutdown so late
completions cannot schedule fresh retries during stop. Cancel retry work
first, then wait for submission work before killing active URBs, so a
running worker cannot submit a request after the active requests have
been drained.

Cancellation and removal keep their release behavior. EPIPE endpoint-halt
recovery remains outside this change because it requires quiescing
requests and distinguishing recovery cancellation from shutdown.

Fixes: 26f1fad29ad9 ("New driver: rtl8xxxu (mac80211)")
Assisted-by: GPT-6 Astra
Signed-off-by: kimwooseok <5mghybrid@khu.ac.kr>
---
 drivers/net/wireless/realtek/rtl8xxxu/core.c  | 71 +++++++++++++++++--
 .../net/wireless/realtek/rtl8xxxu/rtl8xxxu.h  |  3 +
 2 files changed, 70 insertions(+), 4 deletions(-)

[-- Attachment #2: 0003-wifi-rtl8xxxu-preserve-RX-requests-across-recoverabl.patch --]
[-- Type: text/x-patch, Size: 5554 bytes --]

diff --git a/drivers/net/wireless/realtek/rtl8xxxu/core.c b/drivers/net/wireless/realtek/rtl8xxxu/core.c
index 1932a9ec1970c..883c9a56f52a4 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/core.c
+++ b/drivers/net/wireless/realtek/rtl8xxxu/core.c
@@ -54,6 +54,7 @@ MODULE_PARM_DESC(dma_agg_pages, "Set DMA aggregation pages (range 1-127, 0 to di
 #define USB_VENDOR_ID_REALTEK		0x0bda
 #define RTL8XXXU_RX_URBS		32
 #define RTL8XXXU_RX_URB_PENDING_WATER	8
+#define RTL8XXXU_RX_URB_RETRY_DELAY_MS	100
 #define RTL8XXXU_TX_URBS		64
 #define RTL8XXXU_TX_URB_LOW_WATER	25
 #define RTL8XXXU_TX_URB_HIGH_WATER	32
@@ -5823,6 +5824,11 @@ static void rtl8xxxu_free_rx_resources(struct rtl8xxxu_priv *priv)
 
 	spin_lock_irqsave(&priv->rx_urb_lock, flags);
 
+	list_splice_tail_init(&priv->rx_urb_retry_list,
+			      &priv->rx_urb_pending_list);
+	priv->rx_urb_pending_count += priv->rx_urb_retry_count;
+	priv->rx_urb_retry_count = 0;
+
 	list_for_each_entry_safe(rx_urb, tmp,
 				 &priv->rx_urb_pending_list, list) {
 		list_del(&rx_urb->list);
@@ -5881,6 +5887,47 @@ static void rtl8xxxu_queue_rx_urb(struct rtl8xxxu_priv *priv,
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 }
 
+static void rtl8xxxu_rx_urb_retry_work(struct work_struct *work)
+{
+	struct rtl8xxxu_priv *priv = container_of(to_delayed_work(work),
+						struct rtl8xxxu_priv,
+						rx_urb_retry_wq);
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->rx_urb_lock, flags);
+
+	if (!priv->shutdown && priv->rx_urb_retry_count) {
+		list_splice_tail_init(&priv->rx_urb_retry_list,
+				      &priv->rx_urb_pending_list);
+		priv->rx_urb_pending_count += priv->rx_urb_retry_count;
+		priv->rx_urb_retry_count = 0;
+		/* An error must not leave a small batch waiting indefinitely. */
+		schedule_work(&priv->rx_urb_wq);
+	}
+
+	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
+}
+
+static void rtl8xxxu_queue_rx_urb_retry(struct rtl8xxxu_priv *priv,
+					struct rtl8xxxu_rx_urb *rx_urb)
+{
+	unsigned long flags;
+
+	spin_lock_irqsave(&priv->rx_urb_lock, flags);
+
+	if (!priv->shutdown) {
+		list_add_tail(&rx_urb->list, &priv->rx_urb_retry_list);
+		priv->rx_urb_retry_count++;
+		/* Keep normal completions from bypassing the error backoff. */
+		queue_delayed_work(system_wq, &priv->rx_urb_retry_wq,
+				   msecs_to_jiffies(RTL8XXXU_RX_URB_RETRY_DELAY_MS));
+	} else {
+		usb_free_urb(&rx_urb->urb);
+	}
+
+	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
+}
+
 static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 {
 	struct rtl8xxxu_priv *priv;
@@ -5904,7 +5951,7 @@ static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 		ret = rtl8xxxu_submit_rx_urb(priv, rx_urb);
 		/*
 		 * If out of memory or temporary error, put it back on the
-		 * queue and try again. Otherwise the device is dead/gone
+		 * delayed queue and try again. Otherwise the device is dead/gone
 		 * and we should drop it.
 		 */
 		switch (ret) {
@@ -5912,7 +5959,7 @@ static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 			break;
 		case -ENOMEM:
 		case -EAGAIN:
-			rtl8xxxu_queue_rx_urb(priv, rx_urb);
+			rtl8xxxu_queue_rx_urb_retry(priv, rx_urb);
 			break;
 		default:
 			dev_warn(&priv->udev->dev,
@@ -5942,7 +5989,7 @@ static int rtl8xxxu_start_rx(struct rtl8xxxu_priv *priv)
 			break;
 		case -ENOMEM:
 		case -EAGAIN:
-			rtl8xxxu_queue_rx_urb(priv, rx_urb);
+			rtl8xxxu_queue_rx_urb_retry(priv, rx_urb);
 			break;
 		default:
 			usb_free_urb(&rx_urb->urb);
@@ -6618,7 +6665,20 @@ static void rtl8xxxu_rx_complete(struct urb *urb)
 		rtl8xxxu_queue_rx_urb(priv, rx_urb);
 	} else {
 		dev_dbg(dev, "%s: status %i\n",	__func__, urb->status);
-		goto cleanup;
+		switch (urb->status) {
+		case -EPROTO:
+		case -EILSEQ:
+		case -ETIME:
+		case -EOVERFLOW:
+		case -ECOMM:
+		case -ENOSR:
+			dev_kfree_skb(skb);
+			urb->context = NULL;
+			rtl8xxxu_queue_rx_urb_retry(priv, rx_urb);
+			return;
+		default:
+			goto cleanup;
+		}
 	}
 	return;
 
@@ -7549,6 +7609,7 @@ static void rtl8xxxu_stop(struct ieee80211_hw *hw, bool suspend)
 	 * it drained via rtl8xxxu_submit_rx_urb(), so a worker still running
 	 * after the kill could submit a URB that escapes it.
 	 */
+	cancel_delayed_work_sync(&priv->rx_urb_retry_wq);
 	cancel_work_sync(&priv->rx_urb_wq);
 
 	usb_kill_anchored_urbs(&priv->rx_anchor);
@@ -7861,9 +7922,11 @@ static int rtl8xxxu_probe(struct usb_interface *interface,
 	INIT_LIST_HEAD(&priv->tx_urb_free_list);
 	spin_lock_init(&priv->tx_urb_lock);
 	INIT_LIST_HEAD(&priv->rx_urb_pending_list);
+	INIT_LIST_HEAD(&priv->rx_urb_retry_list);
 	spin_lock_init(&priv->rx_urb_lock);
 	priv->shutdown = true;
 	INIT_WORK(&priv->rx_urb_wq, rtl8xxxu_rx_urb_work);
+	INIT_DELAYED_WORK(&priv->rx_urb_retry_wq, rtl8xxxu_rx_urb_retry_work);
 	INIT_DELAYED_WORK(&priv->ra_watchdog, rtl8xxxu_watchdog_callback);
 	INIT_DELAYED_WORK(&priv->update_beacon_work, rtl8xxxu_update_beacon_work_callback);
 	skb_queue_head_init(&priv->c2hcmd_queue);
diff --git a/drivers/net/wireless/realtek/rtl8xxxu/rtl8xxxu.h b/drivers/net/wireless/realtek/rtl8xxxu/rtl8xxxu.h
index eeb18eb0e4c0f..ee55f6cc7f012 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/rtl8xxxu.h
+++ b/drivers/net/wireless/realtek/rtl8xxxu/rtl8xxxu.h
@@ -1808,8 +1808,11 @@ struct rtl8xxxu_priv {
 	spinlock_t rx_urb_lock;
 	struct list_head rx_urb_pending_list;
 	int rx_urb_pending_count;
+	struct list_head rx_urb_retry_list;
+	int rx_urb_retry_count;
 	bool shutdown;
 	struct work_struct rx_urb_wq;
+	struct delayed_work rx_urb_retry_wq;
 
 	u8 mac_addr[ETH_ALEN];
 	char chip_name[8];
-- 
2.48.1


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

* [PATCH RESEND wireless 4/4] wifi: rtl8xxxu: test RX ownership and recovery across failures
  2026-09-12 22:09 [PATCH RESEND wireless 0/4] wifi: rtl8xxxu: keep RX requests available across transient errors kimwooseok
                   ` (2 preceding siblings ...)
  2026-09-12 22:09 ` [PATCH RESEND wireless 3/4] wifi: rtl8xxxu: preserve RX requests across recoverable transfer errors kimwooseok
@ 2026-09-12 22:10 ` kimwooseok
  3 siblings, 0 replies; 7+ messages in thread
From: kimwooseok @ 2026-09-12 22:10 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jes.Sorensen, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 1192 bytes --]

Add 11 KUnit cases for the RX allocation, submission, completion and
retry paths. Cover each retryable completion followed by ENOMEM/EAGAIN,
batch sizes 1, 8, 9 and 32, skb allocation failure, startup failure
positions, cancellation and shutdown.

Run the actual RX helpers and worker with task-scoped stubs for
allocation and USB submission. Observer references check that the driver
releases its URB and skb references. A delayed-work case checks that
one retry request schedules the submission worker.

Assisted-by: GPT-6 Astra
Signed-off-by: kimwooseok <5mghybrid@khu.ac.kr>
---
 .../wireless/realtek/rtl8xxxu/.kunitconfig    |  16 +
 drivers/net/wireless/realtek/rtl8xxxu/Kconfig |  11 +
 .../net/wireless/realtek/rtl8xxxu/Makefile    |   3 +
 drivers/net/wireless/realtek/rtl8xxxu/core.c  |  66 ++-
 .../net/wireless/realtek/rtl8xxxu/rx-test.c   | 461 ++++++++++++++++++
 .../net/wireless/realtek/rtl8xxxu/rx-test.h   |  27 +
 6 files changed, 569 insertions(+), 15 deletions(-)
 create mode 100644 drivers/net/wireless/realtek/rtl8xxxu/.kunitconfig
 create mode 100644 drivers/net/wireless/realtek/rtl8xxxu/rx-test.c
 create mode 100644 drivers/net/wireless/realtek/rtl8xxxu/rx-test.h

[-- Attachment #2: 0004-wifi-rtl8xxxu-test-RX-ownership-and-recovery-across-.patch --]
[-- Type: text/x-patch, Size: 23967 bytes --]

diff --git a/drivers/net/wireless/realtek/rtl8xxxu/.kunitconfig b/drivers/net/wireless/realtek/rtl8xxxu/.kunitconfig
new file mode 100644
index 0000000000000..decf383c3d51e
--- /dev/null
+++ b/drivers/net/wireless/realtek/rtl8xxxu/.kunitconfig
@@ -0,0 +1,16 @@
+CONFIG_MODULES=y
+CONFIG_KUNIT=y
+CONFIG_KUNIT_ALL_TESTS=n
+CONFIG_NET=y
+CONFIG_NETDEVICES=y
+CONFIG_WIRELESS=y
+CONFIG_CFG80211=y
+CONFIG_MAC80211=y
+CONFIG_WLAN=y
+CONFIG_WLAN_VENDOR_REALTEK=y
+CONFIG_USB_SUPPORT=y
+CONFIG_USB=y
+CONFIG_NEW_LEDS=y
+CONFIG_LEDS_CLASS=y
+CONFIG_RTL8XXXU=y
+CONFIG_RTL8XXXU_KUNIT_TEST=y
diff --git a/drivers/net/wireless/realtek/rtl8xxxu/Kconfig b/drivers/net/wireless/realtek/rtl8xxxu/Kconfig
index 14d0343368ac0..e2c16d62293a4 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/Kconfig
+++ b/drivers/net/wireless/realtek/rtl8xxxu/Kconfig
@@ -35,3 +35,14 @@ config RTL8XXXU_UNTESTED
 	  author or reported to be working by third parties.
 
 	  Please report your results!
+
+config RTL8XXXU_KUNIT_TEST
+	tristate "KUnit tests for rtl8xxxu RX recovery" if !KUNIT_ALL_TESTS
+	depends on KUNIT && RTL8XXXU
+	default KUNIT_ALL_TESTS
+	help
+	  Exercise RX buffer ownership, startup and recovery with the real
+	  driver functions and controlled allocation and USB submission results.
+	  These tests do not require a USB adapter.
+
+	  If unsure, say N.
diff --git a/drivers/net/wireless/realtek/rtl8xxxu/Makefile b/drivers/net/wireless/realtek/rtl8xxxu/Makefile
index 580a2fa675ee2..a592a81197857 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/Makefile
+++ b/drivers/net/wireless/realtek/rtl8xxxu/Makefile
@@ -4,3 +4,6 @@ obj-$(CONFIG_RTL8XXXU)	+= rtl8xxxu.o
 rtl8xxxu-y	:= core.o 8192e.o 8723b.o \
 		   8723a.o 8192c.o 8188f.o \
 		   8188e.o 8710b.o 8192f.o
+
+obj-$(CONFIG_RTL8XXXU_KUNIT_TEST) += rtl8xxxu-rx-test.o
+rtl8xxxu-rx-test-y := rx-test.o
diff --git a/drivers/net/wireless/realtek/rtl8xxxu/core.c b/drivers/net/wireless/realtek/rtl8xxxu/core.c
index 883c9a56f52a4..323411e7f5e3b 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/core.c
+++ b/drivers/net/wireless/realtek/rtl8xxxu/core.c
@@ -17,6 +17,8 @@
 #include <linux/iopoll.h>
 #include "regs.h"
 #include "rtl8xxxu.h"
+#include "rx-test.h"
+#include <kunit/static_stub.h>
 
 #define DRIVER_NAME "rtl8xxxu"
 
@@ -60,8 +62,7 @@ MODULE_PARM_DESC(dma_agg_pages, "Set DMA aggregation pages (range 1-127, 0 to di
 #define RTL8XXXU_TX_URB_HIGH_WATER	32
 
 static void rtl8xxxu_stop(struct ieee80211_hw *hw, bool suspend);
-static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
-				  struct rtl8xxxu_rx_urb *rx_urb);
+
 
 static struct ieee80211_rate rtl8xxxu_rates[] = {
 	{ .bitrate = 10, .hw_value = DESC_RATE_1M, .flags = 0 },
@@ -5817,7 +5818,36 @@ void jaguar2_rx_parse_phystats(struct rtl8xxxu_priv *priv,
 	}
 }
 
-static void rtl8xxxu_free_rx_resources(struct rtl8xxxu_priv *priv)
+VISIBLE_IF_KUNIT struct rtl8xxxu_rx_urb *rtl8xxxu_alloc_rx_urb(void)
+{
+	KUNIT_STATIC_STUB_REDIRECT(rtl8xxxu_alloc_rx_urb);
+	return kmalloc_obj(struct rtl8xxxu_rx_urb);
+}
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_alloc_rx_urb);
+
+VISIBLE_IF_KUNIT struct sk_buff *rtl8xxxu_alloc_rx_skb(unsigned int size)
+{
+	KUNIT_STATIC_STUB_REDIRECT(rtl8xxxu_alloc_rx_skb, size);
+	return __netdev_alloc_skb(NULL, size, GFP_KERNEL);
+}
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_alloc_rx_skb);
+
+VISIBLE_IF_KUNIT int rtl8xxxu_rx_usb_submit(struct urb *urb, gfp_t flags)
+{
+	KUNIT_STATIC_STUB_REDIRECT(rtl8xxxu_rx_usb_submit, urb, flags);
+	return usb_submit_urb(urb, flags);
+}
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_rx_usb_submit);
+
+VISIBLE_IF_KUNIT void rtl8xxxu_schedule_rx_retry(struct rtl8xxxu_priv *priv)
+{
+	KUNIT_STATIC_STUB_REDIRECT(rtl8xxxu_schedule_rx_retry, priv);
+	queue_delayed_work(system_wq, &priv->rx_urb_retry_wq,
+			   msecs_to_jiffies(RTL8XXXU_RX_URB_RETRY_DELAY_MS));
+}
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_schedule_rx_retry);
+
+VISIBLE_IF_KUNIT void rtl8xxxu_free_rx_resources(struct rtl8xxxu_priv *priv)
 {
 	struct rtl8xxxu_rx_urb *rx_urb, *tmp;
 	unsigned long flags;
@@ -5838,15 +5868,16 @@ static void rtl8xxxu_free_rx_resources(struct rtl8xxxu_priv *priv)
 
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_free_rx_resources);
 
-static int rtl8xxxu_alloc_rx_urbs(struct rtl8xxxu_priv *priv)
+VISIBLE_IF_KUNIT int rtl8xxxu_alloc_rx_urbs(struct rtl8xxxu_priv *priv)
 {
 	struct rtl8xxxu_rx_urb *rx_urb;
 	int i;
 
 	/* No RX work is active until the complete pool has been allocated. */
 	for (i = 0; i < RTL8XXXU_RX_URBS; i++) {
-		rx_urb = kmalloc_obj(struct rtl8xxxu_rx_urb);
+		rx_urb = rtl8xxxu_alloc_rx_urb();
 		if (!rx_urb)
 			return -ENOMEM;
 
@@ -5859,6 +5890,7 @@ static int rtl8xxxu_alloc_rx_urbs(struct rtl8xxxu_priv *priv)
 
 	return 0;
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_alloc_rx_urbs);
 
 static void rtl8xxxu_queue_rx_urb(struct rtl8xxxu_priv *priv,
 				  struct rtl8xxxu_rx_urb *rx_urb)
@@ -5887,7 +5919,7 @@ static void rtl8xxxu_queue_rx_urb(struct rtl8xxxu_priv *priv,
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 }
 
-static void rtl8xxxu_rx_urb_retry_work(struct work_struct *work)
+VISIBLE_IF_KUNIT void rtl8xxxu_rx_urb_retry_work(struct work_struct *work)
 {
 	struct rtl8xxxu_priv *priv = container_of(to_delayed_work(work),
 						struct rtl8xxxu_priv,
@@ -5907,6 +5939,7 @@ static void rtl8xxxu_rx_urb_retry_work(struct work_struct *work)
 
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_rx_urb_retry_work);
 
 static void rtl8xxxu_queue_rx_urb_retry(struct rtl8xxxu_priv *priv,
 					struct rtl8xxxu_rx_urb *rx_urb)
@@ -5919,8 +5952,7 @@ static void rtl8xxxu_queue_rx_urb_retry(struct rtl8xxxu_priv *priv,
 		list_add_tail(&rx_urb->list, &priv->rx_urb_retry_list);
 		priv->rx_urb_retry_count++;
 		/* Keep normal completions from bypassing the error backoff. */
-		queue_delayed_work(system_wq, &priv->rx_urb_retry_wq,
-				   msecs_to_jiffies(RTL8XXXU_RX_URB_RETRY_DELAY_MS));
+		rtl8xxxu_schedule_rx_retry(priv);
 	} else {
 		usb_free_urb(&rx_urb->urb);
 	}
@@ -5928,7 +5960,7 @@ static void rtl8xxxu_queue_rx_urb_retry(struct rtl8xxxu_priv *priv,
 	spin_unlock_irqrestore(&priv->rx_urb_lock, flags);
 }
 
-static void rtl8xxxu_rx_urb_work(struct work_struct *work)
+VISIBLE_IF_KUNIT void rtl8xxxu_rx_urb_work(struct work_struct *work)
 {
 	struct rtl8xxxu_priv *priv;
 	struct rtl8xxxu_rx_urb *rx_urb, *tmp;
@@ -5968,8 +6000,9 @@ static void rtl8xxxu_rx_urb_work(struct work_struct *work)
 		}
 	}
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_rx_urb_work);
 
-static int rtl8xxxu_start_rx(struct rtl8xxxu_priv *priv)
+VISIBLE_IF_KUNIT int rtl8xxxu_start_rx(struct rtl8xxxu_priv *priv)
 {
 	struct rtl8xxxu_rx_urb *rx_urb, *tmp;
 	unsigned long flags;
@@ -6006,6 +6039,7 @@ free_remaining:
 	}
 	return ret;
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_start_rx);
 
 /*
  * The RTL8723BU/RTL8192EU vendor driver use coexistence table type
@@ -6646,7 +6680,7 @@ int rtl8xxxu_parse_rxdesc24(struct rtl8xxxu_priv *priv, struct sk_buff *skb)
 	return RX_TYPE_DATA_PKT;
 }
 
-static void rtl8xxxu_rx_complete(struct urb *urb)
+VISIBLE_IF_KUNIT void rtl8xxxu_rx_complete(struct urb *urb)
 {
 	struct rtl8xxxu_rx_urb *rx_urb =
 		container_of(urb, struct rtl8xxxu_rx_urb, urb);
@@ -6686,9 +6720,10 @@ cleanup:
 	usb_free_urb(urb);
 	dev_kfree_skb(skb);
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_rx_complete);
 
-static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
-				  struct rtl8xxxu_rx_urb *rx_urb)
+VISIBLE_IF_KUNIT int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
+					    struct rtl8xxxu_rx_urb *rx_urb)
 {
 	struct rtl8xxxu_fileops *fops = priv->fops;
 	struct sk_buff *skb;
@@ -6704,7 +6739,7 @@ static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
 		skb_size = IEEE80211_MAX_FRAME_LEN + rx_desc_sz;
 	}
 
-	skb = __netdev_alloc_skb(NULL, skb_size, GFP_KERNEL);
+	skb = rtl8xxxu_alloc_rx_skb(skb_size);
 	if (!skb)
 		return -ENOMEM;
 
@@ -6712,7 +6747,7 @@ static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
 	usb_fill_bulk_urb(&rx_urb->urb, priv->udev, priv->pipe_in, skb->data,
 			  skb_size, rtl8xxxu_rx_complete, skb);
 	usb_anchor_urb(&rx_urb->urb, &priv->rx_anchor);
-	ret = usb_submit_urb(&rx_urb->urb, GFP_ATOMIC);
+	ret = rtl8xxxu_rx_usb_submit(&rx_urb->urb, GFP_ATOMIC);
 	if (ret) {
 		usb_unanchor_urb(&rx_urb->urb);
 		dev_kfree_skb(skb);
@@ -6720,6 +6755,7 @@ static int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
 	}
 	return ret;
 }
+EXPORT_SYMBOL_IF_KUNIT(rtl8xxxu_submit_rx_urb);
 
 static void rtl8xxxu_int_complete(struct urb *urb)
 {
diff --git a/drivers/net/wireless/realtek/rtl8xxxu/rx-test.c b/drivers/net/wireless/realtek/rtl8xxxu/rx-test.c
new file mode 100644
index 0000000000000..aedddcb049cdf
--- /dev/null
+++ b/drivers/net/wireless/realtek/rtl8xxxu/rx-test.c
@@ -0,0 +1,461 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/usb.h>
+
+#include "rtl8xxxu.h"
+#include "rx-test.h"
+
+#define RX_TEST_URBS 32
+#define RX_TEST_SKBS 512
+
+struct rx_test {
+	struct rtl8xxxu_priv priv;
+	struct ieee80211_hw hw;
+	struct rtl8xxxu_fileops fops;
+	struct usb_device udev;
+	struct rtl8xxxu_rx_urb *urbs[RX_TEST_URBS];
+	struct sk_buff *skbs[RX_TEST_SKBS];
+	unsigned int allocated;
+	unsigned int buffers;
+	unsigned int alloc_calls;
+	unsigned int alloc_fail_at;
+	unsigned int submit_calls;
+	unsigned int submit_fail_at;
+	int submit_error;
+	bool fail_skb;
+	bool retry_pending;
+	unsigned int retry_arms;
+	atomic_t normal_runs;
+};
+
+static struct rx_test *rx_current(void)
+{
+	return kunit_get_current_test()->priv;
+}
+
+static struct rtl8xxxu_rx_urb *rx_alloc_object(void)
+{
+	struct rx_test *ctx = rx_current();
+	struct rtl8xxxu_rx_urb *rx;
+
+	ctx->alloc_calls++;
+	if (ctx->alloc_calls == ctx->alloc_fail_at)
+		return NULL;
+	rx = kmalloc_obj(struct rtl8xxxu_rx_urb);
+	if (rx)
+		ctx->urbs[ctx->allocated++] = rx;
+	return rx;
+}
+
+static struct sk_buff *rx_alloc_buffer(unsigned int size)
+{
+	struct rx_test *ctx = rx_current();
+	struct sk_buff *skb;
+
+	if (ctx->fail_skb)
+		return NULL;
+	skb = alloc_skb(size, GFP_KERNEL);
+	if (skb) {
+		/* Retain an observer reference to detect a lost driver reference. */
+		skb_get(skb);
+		ctx->skbs[ctx->buffers++] = skb;
+	}
+	return skb;
+}
+
+static int rx_submit(struct urb *urb, gfp_t flags)
+{
+	struct rx_test *ctx = rx_current();
+
+	ctx->submit_calls++;
+	if (!ctx->submit_fail_at ||
+	    ctx->submit_calls == ctx->submit_fail_at)
+		return ctx->submit_error;
+	return 0;
+}
+
+static void rx_schedule_retry(struct rtl8xxxu_priv *priv)
+{
+	struct rx_test *ctx = rx_current();
+
+	/* Model the single pending delayed-work ticket, not a submission. */
+	if (!ctx->retry_pending) {
+		ctx->retry_pending = true;
+		ctx->retry_arms++;
+	}
+}
+
+static void rx_observe_work(struct work_struct *work)
+{
+	struct rtl8xxxu_priv *priv;
+	struct rx_test *ctx;
+
+	priv = container_of(work, struct rtl8xxxu_priv, rx_urb_wq);
+	ctx = container_of(priv, struct rx_test, priv);
+	atomic_inc(&ctx->normal_runs);
+}
+
+static struct rx_test *rx_init(struct kunit *test, bool fake_timer)
+{
+	struct rx_test *ctx;
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	if (!ctx)
+		return NULL;
+	test->priv = ctx;
+	ctx->hw.priv = &ctx->priv;
+	ctx->priv.hw = &ctx->hw;
+	ctx->priv.udev = &ctx->udev;
+	ctx->priv.fops = &ctx->fops;
+	ctx->fops.rx_desc_size = sizeof(struct rtl8xxxu_rxdesc16);
+	spin_lock_init(&ctx->priv.rx_urb_lock);
+	INIT_LIST_HEAD(&ctx->priv.rx_urb_pending_list);
+	INIT_LIST_HEAD(&ctx->priv.rx_urb_retry_list);
+	init_usb_anchor(&ctx->priv.rx_anchor);
+	INIT_WORK(&ctx->priv.rx_urb_wq, rx_observe_work);
+	INIT_DELAYED_WORK(&ctx->priv.rx_urb_retry_wq,
+			  rtl8xxxu_rx_urb_retry_work);
+	atomic_set(&ctx->normal_runs, 0);
+
+	kunit_activate_static_stub(test, rtl8xxxu_alloc_rx_urb, rx_alloc_object);
+	kunit_activate_static_stub(test, rtl8xxxu_alloc_rx_skb, rx_alloc_buffer);
+	kunit_activate_static_stub(test, rtl8xxxu_rx_usb_submit, rx_submit);
+	if (fake_timer)
+		kunit_activate_static_stub(test, rtl8xxxu_schedule_rx_retry,
+					   rx_schedule_retry);
+	return ctx;
+}
+
+static void rx_get_observers(struct rx_test *ctx)
+{
+	unsigned int i;
+
+	for (i = 0; i < ctx->allocated; i++)
+		usb_get_urb(&ctx->urbs[i]->urb);
+}
+
+static int rx_pool(struct rx_test *ctx, unsigned int count)
+{
+	struct rtl8xxxu_rx_urb *rx;
+	unsigned int i;
+	int ret;
+
+	ret = rtl8xxxu_alloc_rx_urbs(&ctx->priv);
+	rx_get_observers(ctx);
+	if (ret)
+		return ret;
+
+	/* A reduced pool also tests recovery without the normal watermark. */
+	for (i = count; i < ctx->allocated; i++) {
+		rx = ctx->urbs[i];
+		list_del_init(&rx->list);
+		ctx->priv.rx_urb_pending_count--;
+		usb_free_urb(&rx->urb);
+	}
+	return 0;
+}
+
+static void rx_giveback(struct rx_test *ctx, unsigned int i, int status)
+{
+	struct urb *urb = &ctx->urbs[i]->urb;
+
+	/* The fake USB boundary does not emulate HCD giveback. */
+	usb_unanchor_urb(urb);
+	urb->status = status;
+	urb->actual_length = 0;
+	rtl8xxxu_rx_complete(urb);
+}
+
+static void rx_finish(struct kunit *test, struct rx_test *ctx)
+{
+	struct urb *urb;
+	unsigned int i;
+
+	ctx->priv.shutdown = true;
+	cancel_delayed_work_sync(&ctx->priv.rx_urb_retry_wq);
+	cancel_work_sync(&ctx->priv.rx_urb_wq);
+	for (i = 0; i < ctx->allocated; i++) {
+		urb = &ctx->urbs[i]->urb;
+		if (urb->anchor)
+			rx_giveback(ctx, i, -ENOENT);
+	}
+	rtl8xxxu_free_rx_resources(&ctx->priv);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_pending_count, 0);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+	KUNIT_EXPECT_TRUE(test, usb_anchor_empty(&ctx->priv.rx_anchor));
+	for (i = 0; i < ctx->allocated; i++) {
+		urb = &ctx->urbs[i]->urb;
+		KUNIT_EXPECT_EQ(test, refcount_read(&urb->kref.refcount), 1);
+		usb_free_urb(urb);
+	}
+	for (i = 0; i < ctx->buffers; i++) {
+		KUNIT_EXPECT_EQ(test, refcount_read(&ctx->skbs[i]->users), 1);
+		kfree_skb(ctx->skbs[i]);
+	}
+}
+
+static void rx_retry_step(struct kunit *test, struct rx_test *ctx)
+{
+	KUNIT_ASSERT_TRUE(test, ctx->retry_pending);
+	ctx->retry_pending = false;
+	rtl8xxxu_rx_urb_retry_work(&ctx->priv.rx_urb_retry_wq.work);
+	flush_work(&ctx->priv.rx_urb_wq);
+	/* Execute the actual worker in the test task so boundary stubs apply. */
+	rtl8xxxu_rx_urb_work(&ctx->priv.rx_urb_wq);
+}
+
+static void rx_completion_submit_chain(struct kunit *test)
+{
+	static const int errors[] = {
+		-EPROTO, -EILSEQ, -ETIME, -EOVERFLOW, -ECOMM, -ENOSR,
+	};
+	static const int submissions[] = { -ENOMEM, -EAGAIN };
+	struct rx_test *ctx;
+	unsigned int e, f, i;
+	int runs;
+
+	for (e = 0; e < ARRAY_SIZE(errors); e++) {
+		for (f = 0; f < ARRAY_SIZE(submissions); f++) {
+			ctx = rx_init(test, true);
+			KUNIT_ASSERT_NOT_NULL(test, ctx);
+			KUNIT_ASSERT_EQ(test, rx_pool(ctx, RX_TEST_URBS), 0);
+			KUNIT_ASSERT_EQ(test, rtl8xxxu_start_rx(&ctx->priv), 0);
+			for (i = 0; i < RX_TEST_URBS; i++)
+				rx_giveback(ctx, i, errors[e]);
+			KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 32);
+			ctx->submit_error = submissions[f];
+			rx_retry_step(test, ctx);
+			runs = atomic_read(&ctx->normal_runs);
+			flush_work(&ctx->priv.rx_urb_wq);
+			KUNIT_EXPECT_EQ(test, atomic_read(&ctx->normal_runs), runs);
+			KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_pending_count, 0);
+			KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 32);
+			KUNIT_EXPECT_TRUE(test, ctx->retry_pending);
+			for (i = 0; i < ctx->buffers; i++)
+				KUNIT_EXPECT_EQ(test,
+						refcount_read(&ctx->skbs[i]->users), 1);
+			ctx->submit_error = 0;
+			rx_retry_step(test, ctx);
+			KUNIT_EXPECT_FALSE(test, ctx->retry_pending);
+			KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+			KUNIT_EXPECT_FALSE(test, usb_anchor_empty(&ctx->priv.rx_anchor));
+			rx_finish(test, ctx);
+		}
+	}
+}
+
+static void rx_worker_batch_sizes(struct kunit *test)
+{
+	static const unsigned int sizes[] = { 1, 8, 9, 32 };
+	struct rx_test *ctx;
+	unsigned int n, pass;
+	int runs;
+
+	for (n = 0; n < ARRAY_SIZE(sizes); n++) {
+		ctx = rx_init(test, true);
+		KUNIT_ASSERT_NOT_NULL(test, ctx);
+		KUNIT_ASSERT_EQ(test, rx_pool(ctx, sizes[n]), 0);
+		ctx->submit_error = -EAGAIN;
+		rtl8xxxu_rx_urb_work(&ctx->priv.rx_urb_wq);
+		for (pass = 0; pass < 3; pass++) {
+			runs = atomic_read(&ctx->normal_runs);
+			flush_work(&ctx->priv.rx_urb_wq);
+			KUNIT_EXPECT_EQ(test, atomic_read(&ctx->normal_runs), runs);
+			KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, sizes[n]);
+			KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_pending_count, 0);
+			rx_retry_step(test, ctx);
+		}
+		ctx->submit_error = 0;
+		rx_retry_step(test, ctx);
+		KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+		rx_finish(test, ctx);
+	}
+}
+
+static void rx_skb_allocation_failure(struct kunit *test)
+{
+	struct rx_test *ctx;
+
+	ctx = rx_init(test, true);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	KUNIT_ASSERT_EQ(test, rx_pool(ctx, 8), 0);
+	ctx->fail_skb = true;
+	rtl8xxxu_rx_urb_work(&ctx->priv.rx_urb_wq);
+	KUNIT_EXPECT_EQ(test, ctx->submit_calls, 0);
+	KUNIT_EXPECT_EQ(test, ctx->buffers, 0);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 8);
+	ctx->fail_skb = false;
+	rx_retry_step(test, ctx);
+	KUNIT_EXPECT_EQ(test, ctx->submit_calls, 8);
+	rx_finish(test, ctx);
+}
+
+static void rx_start_allocation_failure(struct kunit *test)
+{
+	static const unsigned int positions[] = { 1, 8, 32 };
+	struct rx_test *ctx;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(positions); i++) {
+		ctx = rx_init(test, true);
+		KUNIT_ASSERT_NOT_NULL(test, ctx);
+		ctx->alloc_fail_at = positions[i];
+		KUNIT_EXPECT_EQ(test, rtl8xxxu_alloc_rx_urbs(&ctx->priv), -ENOMEM);
+		rx_get_observers(ctx);
+		KUNIT_EXPECT_EQ(test, ctx->allocated, positions[i] - 1);
+		KUNIT_EXPECT_EQ(test, ctx->submit_calls, 0);
+		KUNIT_EXPECT_FALSE(test, ctx->retry_pending);
+		rx_finish(test, ctx);
+	}
+}
+
+static void rx_start_temporary_failure(struct kunit *test)
+{
+	static const int errors[] = { -ENOMEM, -EAGAIN };
+	struct rx_test *ctx;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(errors); i++) {
+		ctx = rx_init(test, true);
+		KUNIT_ASSERT_NOT_NULL(test, ctx);
+		KUNIT_ASSERT_EQ(test, rx_pool(ctx, 32), 0);
+		ctx->submit_error = errors[i];
+		KUNIT_EXPECT_EQ(test, rtl8xxxu_start_rx(&ctx->priv), 0);
+		KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 32);
+		KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_pending_count, 0);
+		KUNIT_EXPECT_TRUE(test, ctx->retry_pending);
+		ctx->submit_error = 0;
+		rx_retry_step(test, ctx);
+		rx_finish(test, ctx);
+	}
+}
+
+static void rx_start_fatal_failure(struct kunit *test)
+{
+	static const unsigned int positions[] = { 1, 8, 32 };
+	struct rx_test *ctx;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(positions); i++) {
+		ctx = rx_init(test, true);
+		KUNIT_ASSERT_NOT_NULL(test, ctx);
+		KUNIT_ASSERT_EQ(test, rx_pool(ctx, 32), 0);
+		ctx->submit_fail_at = positions[i];
+		ctx->submit_error = -ENODEV;
+		KUNIT_EXPECT_EQ(test, rtl8xxxu_start_rx(&ctx->priv), -ENODEV);
+		KUNIT_EXPECT_EQ(test, ctx->submit_calls, positions[i]);
+		KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_pending_count, 0);
+		KUNIT_EXPECT_FALSE(test, ctx->retry_pending);
+		rx_finish(test, ctx);
+	}
+}
+
+static void rx_terminal_completion(struct kunit *test)
+{
+	static const int errors[] = {
+		-ENOENT, -ECONNRESET, -ENODEV, -ESHUTDOWN, -EPIPE,
+	};
+	struct rx_test *ctx;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(errors); i++) {
+		ctx = rx_init(test, true);
+		KUNIT_ASSERT_NOT_NULL(test, ctx);
+		KUNIT_ASSERT_EQ(test, rx_pool(ctx, 1), 0);
+		KUNIT_ASSERT_EQ(test, rtl8xxxu_start_rx(&ctx->priv), 0);
+		rx_giveback(ctx, 0, errors[i]);
+		KUNIT_EXPECT_FALSE(test, ctx->retry_pending);
+		KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+		rx_finish(test, ctx);
+	}
+}
+
+static void rx_shutdown_completion(struct kunit *test)
+{
+	struct rx_test *ctx;
+
+	ctx = rx_init(test, true);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	KUNIT_ASSERT_EQ(test, rx_pool(ctx, 1), 0);
+	KUNIT_ASSERT_EQ(test, rtl8xxxu_start_rx(&ctx->priv), 0);
+	ctx->priv.shutdown = true;
+	rx_giveback(ctx, 0, -EPROTO);
+	KUNIT_EXPECT_FALSE(test, ctx->retry_pending);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+	rx_finish(test, ctx);
+}
+
+static void rx_shutdown_pending_retry(struct kunit *test)
+{
+	struct rx_test *ctx;
+
+	ctx = rx_init(test, true);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	KUNIT_ASSERT_EQ(test, rx_pool(ctx, 8), 0);
+	ctx->submit_error = -ENOMEM;
+	rtl8xxxu_rx_urb_work(&ctx->priv.rx_urb_wq);
+	ctx->priv.shutdown = true;
+	rtl8xxxu_rx_urb_retry_work(&ctx->priv.rx_urb_retry_wq.work);
+	flush_work(&ctx->priv.rx_urb_wq);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->normal_runs), 0);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 8);
+	rx_finish(test, ctx);
+}
+
+static void rx_shutdown_submission_failure(struct kunit *test)
+{
+	struct rx_test *ctx;
+
+	ctx = rx_init(test, true);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	KUNIT_ASSERT_EQ(test, rx_pool(ctx, 8), 0);
+	ctx->priv.shutdown = true;
+	ctx->submit_error = -ENOMEM;
+	rtl8xxxu_rx_urb_work(&ctx->priv.rx_urb_wq);
+	KUNIT_EXPECT_FALSE(test, ctx->retry_pending);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+	rx_finish(test, ctx);
+}
+
+static void rx_real_delayed_bridge(struct kunit *test)
+{
+	struct rx_test *ctx;
+
+	ctx = rx_init(test, false);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	KUNIT_ASSERT_EQ(test, rx_pool(ctx, 1), 0);
+	KUNIT_ASSERT_EQ(test, rtl8xxxu_start_rx(&ctx->priv), 0);
+	rx_giveback(ctx, 0, -EPROTO);
+	flush_delayed_work(&ctx->priv.rx_urb_retry_wq);
+	flush_work(&ctx->priv.rx_urb_wq);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_retry_count, 0);
+	KUNIT_EXPECT_EQ(test, ctx->priv.rx_urb_pending_count, 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->normal_runs), 1);
+	rx_finish(test, ctx);
+}
+
+static struct kunit_case rx_cases[] = {
+	KUNIT_CASE(rx_completion_submit_chain),
+	KUNIT_CASE(rx_worker_batch_sizes),
+	KUNIT_CASE(rx_skb_allocation_failure),
+	KUNIT_CASE(rx_start_allocation_failure),
+	KUNIT_CASE(rx_start_temporary_failure),
+	KUNIT_CASE(rx_start_fatal_failure),
+	KUNIT_CASE(rx_terminal_completion),
+	KUNIT_CASE(rx_shutdown_completion),
+	KUNIT_CASE(rx_shutdown_pending_retry),
+	KUNIT_CASE(rx_shutdown_submission_failure),
+	KUNIT_CASE(rx_real_delayed_bridge),
+	{}
+};
+
+static struct kunit_suite rx_suite = {
+	.name = "rtl8xxxu-rx",
+	.test_cases = rx_cases,
+};
+
+kunit_test_suite(rx_suite);
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+MODULE_DESCRIPTION("rtl8xxxu RX recovery KUnit tests");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/wireless/realtek/rtl8xxxu/rx-test.h b/drivers/net/wireless/realtek/rtl8xxxu/rx-test.h
new file mode 100644
index 0000000000000..295befdfe8144
--- /dev/null
+++ b/drivers/net/wireless/realtek/rtl8xxxu/rx-test.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef RTL8XXXU_RX_TEST_H
+#define RTL8XXXU_RX_TEST_H
+
+#include <kunit/visibility.h>
+#include <linux/gfp_types.h>
+
+struct rtl8xxxu_priv;
+struct rtl8xxxu_rx_urb;
+struct sk_buff;
+struct urb;
+struct work_struct;
+
+VISIBLE_IF_KUNIT struct rtl8xxxu_rx_urb *rtl8xxxu_alloc_rx_urb(void);
+VISIBLE_IF_KUNIT struct sk_buff *rtl8xxxu_alloc_rx_skb(unsigned int size);
+VISIBLE_IF_KUNIT int rtl8xxxu_rx_usb_submit(struct urb *urb, gfp_t flags);
+VISIBLE_IF_KUNIT void rtl8xxxu_schedule_rx_retry(struct rtl8xxxu_priv *priv);
+VISIBLE_IF_KUNIT int rtl8xxxu_alloc_rx_urbs(struct rtl8xxxu_priv *priv);
+VISIBLE_IF_KUNIT int rtl8xxxu_start_rx(struct rtl8xxxu_priv *priv);
+VISIBLE_IF_KUNIT int rtl8xxxu_submit_rx_urb(struct rtl8xxxu_priv *priv,
+					    struct rtl8xxxu_rx_urb *rx_urb);
+VISIBLE_IF_KUNIT void rtl8xxxu_free_rx_resources(struct rtl8xxxu_priv *priv);
+VISIBLE_IF_KUNIT void rtl8xxxu_rx_complete(struct urb *urb);
+VISIBLE_IF_KUNIT void rtl8xxxu_rx_urb_work(struct work_struct *work);
+VISIBLE_IF_KUNIT void rtl8xxxu_rx_urb_retry_work(struct work_struct *work);
+
+#endif
-- 
2.48.1


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

* RE: [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails
  2026-09-12 22:09 ` [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails kimwooseok
@ 2026-09-13  3:02   ` Ping-Ke Shih
  2026-09-13  3:05     ` Ping-Ke Shih
  0 siblings, 1 reply; 7+ messages in thread
From: Ping-Ke Shih @ 2026-09-13  3:02 UTC (permalink / raw)
  To: kimwooseok, linux-wireless; +Cc: Jes.Sorensen, linux-kernel

kimwooseok <5mghybrid@khu.ac.kr> wrote:
>  drivers/net/wireless/realtek/rtl8xxxu/core.c | 16 +++++-----------
>  1 file changed, 5 insertions(+), 11 deletions(-)

Please send patch like non-RESEND version without patch attached.

Ping-Ke


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

* RE: [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails
  2026-09-13  3:02   ` Ping-Ke Shih
@ 2026-09-13  3:05     ` Ping-Ke Shih
  0 siblings, 0 replies; 7+ messages in thread
From: Ping-Ke Shih @ 2026-09-13  3:05 UTC (permalink / raw)
  To: kimwooseok, linux-wireless; +Cc: Jes.Sorensen, linux-kernel

Ping-Ke Shih <pkshih@realtek.com> wrote:
 
> kimwooseok <5mghybrid@khu.ac.kr> wrote:
> >  drivers/net/wireless/realtek/rtl8xxxu/core.c | 16 +++++-----------
> >  1 file changed, 5 insertions(+), 11 deletions(-)
> 
> Please send patch like non-RESEND version without patch attached.
> 

Forgot to say the codebase should be rtw-next branch of
https://github.com/pkshih/rtw.git, and patch tag is 'rtw-next'.

Ping-Ke


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

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

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-12 22:09 [PATCH RESEND wireless 0/4] wifi: rtl8xxxu: keep RX requests available across transient errors kimwooseok
2026-09-12 22:09 ` [PATCH RESEND wireless 1/4] wifi: rtl8xxxu: free RX skb when URB submission fails kimwooseok
2026-09-13  3:02   ` Ping-Ke Shih
2026-09-13  3:05     ` Ping-Ke Shih
2026-09-12 22:09 ` [PATCH RESEND wireless 2/4] wifi: rtl8xxxu: unwind incomplete receive startup kimwooseok
2026-09-12 22:09 ` [PATCH RESEND wireless 3/4] wifi: rtl8xxxu: preserve RX requests across recoverable transfer errors kimwooseok
2026-09-12 22:10 ` [PATCH RESEND wireless 4/4] wifi: rtl8xxxu: test RX ownership and recovery across failures kimwooseok

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

all inboxes | Powered by JetHome®