From 1ce6608ace4ef540c7eb0548cd5ac22a37b53478 Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:48:35 -0700 Subject: [PATCH] usb: device_next: cdc_acm: bound poll_out backpressure wait Problem: cdc_acm_poll_out() sleep-retries in an unbounded loop while the TX ring buffer is full and flow control is active. When a host session is attached but stalled (e.g. a host-side USB driver that stops issuing IN tokens for an extended period without disconnecting), this loop blocks for the full duration of the stall on every console byte written. Any thread that logs while this is happening backs up behind the stalled writer, and if enough producers share the same downstream queues, the backpressure cascades into a system-wide livelock rather than staying contained to the console path. Fix: cap the retry loop at 20 iterations of the existing 1 ms sleep (~20 ms total). Once the retry budget is exhausted, treat the still-attached-but-unresponsive session the same as the already-handled detached case: log once and discard the pending byte instead of continuing to block. This keeps the console best-effort under sustained backpressure while leaving the normal (non-stalled) flow-controlled path unaffected, since the loop still exits immediately once the ring buffer drains. Signed-off-by: Michael Pham --- subsys/usb/device_next/class/usbd_cdc_acm.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/subsys/usb/device_next/class/usbd_cdc_acm.c b/subsys/usb/device_next/class/usbd_cdc_acm.c index b20115a60f38..0d3051af66e1 100644 --- a/subsys/usb/device_next/class/usbd_cdc_acm.c +++ b/subsys/usb/device_next/class/usbd_cdc_acm.c @@ -1009,6 +1009,7 @@ static void cdc_acm_poll_out(const struct device *dev, const unsigned char c) struct cdc_acm_uart_data *const data = dev->data; k_spinlock_key_t key; uint32_t wrote; + int retries = 20; while (true) { key = k_spin_lock(&data->lock); @@ -1019,7 +1020,15 @@ static void cdc_acm_poll_out(const struct device *dev, const unsigned char c) break; } - if (k_is_in_isr() || !data->flow_ctrl) { + /* Bounded wait: with an attached-but-stalled host session (macOS + * ceases IN polling for minutes at a time), an unbounded sleep-retry + * here makes every console write take the full stall duration. Any + * thread that logs (event text loggers, assert reporting) then backs + * up its own queues and cascades into a system-wide com livelock. + * After ~20 ms of backpressure, treat the console as best-effort and + * discard, exactly like the detached (!flow_ctrl) case below. + */ + if (k_is_in_isr() || !data->flow_ctrl || retries-- <= 0) { LOG_WRN_ONCE("Ring buffer full, discard data"); break; }