From ae35035da9dace0557ae61f4d4b8e7b720735954 Mon Sep 17 00:00:00 2001 From: Fan Ye Date: Wed, 12 Aug 2026 11:42:09 +0000 Subject: [PATCH 01/24] thunderbolt: Stop waiting on a path pending bit that never clears __tb_path_deactivate_hop() waits up to 500 ms for a hop's pending bit to read back clear. On an ASMedia ASM4242 host router the host interface adapter latches it once enough frames have gone through the DMA ring and never clears it again: the teardown finds it already set, seconds after the last frame and with the path still up. USB4 v2 table 8-23 has the field read only and zero unless packets belonging to the path are waiting to be dequeued, so the wait is right and this adapter is not. Make the wait a per-adapter length and quirk those routers to zero, which leaves the loop reading the bit exactly once. A hop that does drain still says so on that read, and one that does not answers -ETIMEDOUT without burning the timeout: bringing the interface down takes 6 ms where it took 506. Assisted-by: Claude:claude-opus-5 Signed-off-by: Fan Ye Signed-off-by: Mika Westerberg --- drivers/thunderbolt/path.c | 2 +- drivers/thunderbolt/quirks.c | 18 ++++++++++++++++++ drivers/thunderbolt/switch.c | 4 ++++ drivers/thunderbolt/tb.h | 3 +++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/path.c b/drivers/thunderbolt/path.c index b2c322e76b8a..05249eed3f64 100644 --- a/drivers/thunderbolt/path.c +++ b/drivers/thunderbolt/path.c @@ -398,7 +398,7 @@ static int __tb_path_deactivate_hop(struct tb_port *port, int hop_index, return ret; /* Wait until it is drained */ - timeout = ktime_add_ms(ktime_get(), 500); + timeout = ktime_add_ms(ktime_get(), port->pp_timeout_msec); do { ret = tb_port_read(port, &hop, TB_CFG_HOPS, 2 * hop_index, 2); if (ret) diff --git a/drivers/thunderbolt/quirks.c b/drivers/thunderbolt/quirks.c index 9f7914ac2f48..f41c5b7ef114 100644 --- a/drivers/thunderbolt/quirks.c +++ b/drivers/thunderbolt/quirks.c @@ -52,6 +52,19 @@ static void quirk_block_rpm_in_redrive(struct tb_switch *sw) tb_sw_dbg(sw, "preventing runtime PM in DP redrive mode\n"); } +static void quirk_stuck_pending(struct tb_switch *sw) +{ + struct tb_port *port; + + tb_switch_for_each_port(sw, port) { + if (!tb_port_is_nhi(port)) + continue; + + port->pp_timeout_msec = 0; + tb_port_dbg(port, "pending bit does not clear, reading it once\n"); + } +} + struct tb_quirk { u16 hw_vendor_id; u16 hw_device_id; @@ -114,6 +127,11 @@ static const struct tb_quirk tb_quirks[] = { { 0x0438, 0x0209, 0x0000, 0x0000, quirk_clx_disable }, { 0x0438, 0x020a, 0x0000, 0x0000, quirk_clx_disable }, { 0x0438, 0x020b, 0x0000, 0x0000, quirk_clx_disable }, + /* + * ASMedia ASM4242 never clears the Pending Packets bit of its host + * interface adapter. + */ + { 0x174c, 0x2428, 0x0000, 0x0000, quirk_stuck_pending }, }; /** diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index 404c0693df50..4b8bcba7bd2f 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -19,6 +19,9 @@ #include "tb.h" +/* How long a hop is given to drain when a path is deactivated */ +#define TB_PORT_PENDING_TIMEOUT 500 /* ms */ + /* Switch NVM support */ struct nvm_auth_status { @@ -712,6 +715,7 @@ static int tb_init_port(struct tb_port *port) int cap; INIT_LIST_HEAD(&port->list); + port->pp_timeout_msec = TB_PORT_PENDING_TIMEOUT; /* Control adapter does not have configuration space */ if (!port->port) diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index 4373336d9425..c112954ce3fd 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -273,6 +273,8 @@ struct tb_bandwidth_group { * @max_bw: Maximum possible bandwidth through this adapter if set to * non-zero. * @redrive: For DP IN, if true the adapter is in redrive mode. + * @pp_timeout_msec: How long a hop of this adapter is given to drain when a + * path is deactivated. %0 means a single read. * * In USB4 terminology this structure represents an adapter (protocol or * lane adapter). @@ -302,6 +304,7 @@ struct tb_port { struct list_head group_list; unsigned int max_bw; bool redrive; + unsigned int pp_timeout_msec; }; /** From b946f490be576691c4c47998cf1d4e41d637fbfb Mon Sep 17 00:00:00 2001 From: Pengpeng Hou Date: Thu, 13 Aug 2026 23:23:57 +0800 Subject: [PATCH 02/24] thunderbolt: Require complete DROM entry headers tb_drom_parse_entries() checks for one byte remaining before reading a DROM entry header, but the header occupies two bytes. It also accepts a declared length of one byte. A one-byte tail consequently makes the parser read entry->len outside the DROM. A one-byte generic string entry reaches the subtype parser and underflows its payload-length calculation. Require a complete entry header before reading it and require the declared entry length to cover that header. Use subtraction-based bounds checks so the firmware-provided length cannot overflow the DROM extent calculation. While at it, use the actual const type instead of void * to make it clear we only read these. Assisted-by: Codex:gpt-5 Signed-off-by: Pengpeng Hou Signed-off-by: Mika Westerberg --- drivers/thunderbolt/eeprom.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/drivers/thunderbolt/eeprom.c b/drivers/thunderbolt/eeprom.c index 2a13fa6888ba..2acfe83928b6 100644 --- a/drivers/thunderbolt/eeprom.c +++ b/drivers/thunderbolt/eeprom.c @@ -324,7 +324,7 @@ int tb_drom_read_uid_only(struct tb_switch *sw, u64 *uid) } static int tb_drom_parse_entry_generic(struct tb_switch *sw, - struct tb_drom_entry_header *header) + const struct tb_drom_entry_header *header) { const struct tb_drom_entry_generic *entry = (const struct tb_drom_entry_generic *)header; @@ -360,7 +360,7 @@ static int tb_drom_parse_entry_generic(struct tb_switch *sw, } static int tb_drom_parse_entry_port(struct tb_switch *sw, - struct tb_drom_entry_header *header) + const struct tb_drom_entry_header *header) { struct tb_port *port; int res; @@ -386,11 +386,13 @@ static int tb_drom_parse_entry_port(struct tb_switch *sw, type &= 0xffffff; if (type == TB_TYPE_PORT) { - struct tb_drom_entry_port *entry = (void *) header; + const struct tb_drom_entry_port *entry = + (const struct tb_drom_entry_port *)header; + if (header->len != sizeof(*entry)) { tb_sw_warn(sw, "port entry has size %#x (expected %#zx)\n", - header->len, sizeof(struct tb_drom_entry_port)); + header->len, sizeof(*entry)); return -EIO; } port->link_nr = entry->link_nr; @@ -421,9 +423,16 @@ static int tb_drom_parse_entries(struct tb_switch *sw, size_t header_size) int res; while (pos < drom_size) { - struct tb_drom_entry_header *entry = (void *) (sw->drom + pos); - if (pos + 1 == drom_size || pos + entry->len > drom_size - || !entry->len) { + const struct tb_drom_entry_header *entry; + + if (drom_size - pos < sizeof(*entry)) { + tb_sw_warn(sw, "DROM buffer overrun\n"); + return -EIO; + } + + entry = (const struct tb_drom_entry_header *)(sw->drom + pos); + if (entry->len < sizeof(*entry) || + entry->len > drom_size - pos) { tb_sw_warn(sw, "DROM buffer overrun\n"); return -EIO; } From 48e989e33b715611438ce4b8d6ff712d4becd84f Mon Sep 17 00:00:00 2001 From: Haotian Zhang Date: Tue, 25 Aug 2026 11:47:59 +0800 Subject: [PATCH 03/24] thunderbolt: dma_test: Tear down DMA paths before stopping the rings dma_test_stop_rings() stops the RX and TX rings before calling tb_xdomain_disable_paths(). tb_ring_stop() zeroes the ring's descriptor base and the cancellation callbacks unmap and free the frame buffers, so by the time __tb_path_deactivate_hop() polls the hop's 'pending' bit any data still in flight has nowhere to drain to; on ASMedia ASM4242-class host routers the bit never clears and every teardown burns the full 500 ms timeout. This is the same defect as the one in tbnet_tear_down(), fixed as CVE-2026-74691 ("net: thunderbolt: Tear down DMA paths before stopping the rings"). Move the tb_xdomain_disable_paths() call before the tb_ring_stop() calls in dma_test_stop_rings(), so in-flight data drains while the rings are still active, and only then stop the rings and free the frame buffers. Signed-off-by: Haotian Zhang Signed-off-by: Mika Westerberg --- drivers/thunderbolt/dma_test.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/thunderbolt/dma_test.c b/drivers/thunderbolt/dma_test.c index 519c67678b08..bcecb0edcb81 100644 --- a/drivers/thunderbolt/dma_test.c +++ b/drivers/thunderbolt/dma_test.c @@ -215,11 +215,6 @@ static void dma_test_stop_rings(struct dma_test *dt) { int ret; - if (dt->rx_ring) - tb_ring_stop(dt->rx_ring); - if (dt->tx_ring) - tb_ring_stop(dt->tx_ring); - ret = tb_xdomain_disable_paths(dt->xd, dt->tx_hopid, dt->tx_ring ? dt->tx_ring->hop : -1, dt->rx_hopid, @@ -227,6 +222,11 @@ static void dma_test_stop_rings(struct dma_test *dt) if (ret) dev_warn(&dt->svc->dev, "failed to disable DMA paths\n"); + if (dt->rx_ring) + tb_ring_stop(dt->rx_ring); + if (dt->tx_ring) + tb_ring_stop(dt->tx_ring); + dma_test_free_rings(dt); } From 87045fbb8083abb9d77e4874ce5d431b2cdd7ef3 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 14 Aug 2026 14:19:16 +0300 Subject: [PATCH 04/24] thunderbolt: Allow batching of descriptors The hardware allows queueing descriptors ahead of updating producer/consumer fields. This way it is possible to avoid unnecessary register writes on hot-paths such as when transferring networking packets. For this reason introduce an API that allows Thunderbolt service drivers to opt-in for this and take advantage of batching. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 58 ++++++++++++++++++++++++++++++++----- include/linux/thunderbolt.h | 42 +++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 5809809f64d4..a0f7885845bc 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -226,12 +226,37 @@ static bool ring_empty(struct tb_ring *ring) return ring->head == ring->tail; } +static void __ring_notify(struct tb_ring *ring) +{ + lockdep_assert_held(&ring->lock); + + if (ring->notify_pending) { + /* + * The doorbell carries the absolute index of the head + * so a single write covers all the descriptors posted + * since the previous one. + */ + if (ring->is_tx) + ring_iowrite_prod(ring, ring->head); + else + ring_iowrite_cons(ring, ring->head); + } + ring->notify_pending = false; +} + /* * ring_write_descriptors() - post frames from ring->queue to the controller + * @ring: Ring to post the frames to + * @notify: Notify the controller about the posted descriptors + * + * Unless @notify is %true the controller is not notified about the posted + * descriptors and the caller is expected to call tb_ring_notify() once it + * is done queuing frames. This allows batching of frames before + * updating the producer/consumer indices. * * ring->lock is held. */ -static void ring_write_descriptors(struct tb_ring *ring) +static void ring_write_descriptors(struct tb_ring *ring, bool notify) { struct ring_frame *frame, *n; struct ring_desc *descriptor; @@ -255,11 +280,11 @@ static void ring_write_descriptors(struct tb_ring *ring) descriptor->sof = frame->sof; } ring->head = (ring->head + 1) % ring->size; - if (ring->is_tx) - ring_iowrite_prod(ring, ring->head); - else - ring_iowrite_cons(ring, ring->head); + ring->notify_pending = true; } + + if (notify) + __ring_notify(ring); } /* @@ -304,7 +329,7 @@ static void ring_work(struct work_struct *work) } ring->tail = (ring->tail + 1) % ring->size; } - ring_write_descriptors(ring); + ring_write_descriptors(ring, true); invoke_callback: /* allow callbacks to schedule new work */ @@ -323,7 +348,7 @@ static void ring_work(struct work_struct *work) wake_up(&ring->wait); } -int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame) +int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame, bool more) { unsigned long flags; int ret = 0; @@ -331,7 +356,7 @@ int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame) spin_lock_irqsave(&ring->lock, flags); if (ring->running) { list_add_tail(&frame->list, &ring->queue); - ring_write_descriptors(ring); + ring_write_descriptors(ring, !more); } else { ret = -ESHUTDOWN; } @@ -340,6 +365,22 @@ int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame) } EXPORT_SYMBOL_GPL(__tb_ring_enqueue); +/** + * tb_ring_notify() - Notify the controller about the queued frames + * @ring: Ring to notify + * + * Notifies the controller about frames that were enqueued using + * tb_ring_tx_more() or tb_ring_rx_more(). Does nothing if there are no + * such frames pending. + */ +void tb_ring_notify(struct tb_ring *ring) +{ + guard(spinlock_irqsave)(&ring->lock); + if (ring->running) + __ring_notify(ring); +} +EXPORT_SYMBOL_GPL(tb_ring_notify); + /** * tb_ring_poll() - Poll one completed frame from the ring * @ring: Ring to poll @@ -784,6 +825,7 @@ void tb_ring_stop(struct tb_ring *ring) ring_iowrite32desc(ring, 0, 12); ring->head = 0; ring->tail = 0; + ring->notify_pending = false; ring->running = false; err: diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index d48623fda79b..6fda5596c44b 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -552,6 +552,8 @@ struct tb_nhi { * @work: Interrupt work structure * @is_tx: Is the ring Tx or Rx * @running: Is the ring running + * @notify_pending: Controller has not been notified about the posted + * descriptors yet * @irq: MSI-X irq number if the ring uses MSI-X. %0 otherwise. * @vector: MSI-X vector number the ring uses (only set if @irq is > 0) * @flags: Ring specific flags @@ -580,6 +582,7 @@ struct tb_ring { struct work_struct work; bool is_tx:1; bool running:1; + bool notify_pending:1; int irq; u8 vector; unsigned int flags; @@ -669,7 +672,8 @@ bool tb_ring_flush(struct tb_ring *ring, unsigned int timeout_msec); void tb_ring_stop(struct tb_ring *ring); void tb_ring_free(struct tb_ring *ring); -int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame); +int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame, bool more); +void tb_ring_notify(struct tb_ring *ring); /** * tb_ring_rx() - enqueue a frame on an RX ring @@ -690,7 +694,24 @@ int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame); static inline int tb_ring_rx(struct tb_ring *ring, struct ring_frame *frame) { WARN_ON(ring->is_tx); - return __tb_ring_enqueue(ring, frame); + return __tb_ring_enqueue(ring, frame, false); +} + +/** + * tb_ring_rx_more() - enqueue a frame on an RX ring without notifying + * @ring: Ring to enqueue the frame + * @frame: Frame to enqueue + * + * Same as tb_ring_rx() but does not notify the controller about the + * enqueued frame. The caller must call tb_ring_notify() once it is done + * enqueuing frames. + * + * Return: %-ESHUTDOWN if tb_ring_stop() has been called, %0 otherwise. + */ +static inline int tb_ring_rx_more(struct tb_ring *ring, struct ring_frame *frame) +{ + WARN_ON(ring->is_tx); + return __tb_ring_enqueue(ring, frame, true); } /** @@ -711,7 +732,22 @@ static inline int tb_ring_rx(struct tb_ring *ring, struct ring_frame *frame) static inline int tb_ring_tx(struct tb_ring *ring, struct ring_frame *frame) { WARN_ON(!ring->is_tx); - return __tb_ring_enqueue(ring, frame); + return __tb_ring_enqueue(ring, frame, false); +} + +/** + * tb_ring_tx_more() - enqueue a frame on a TX ring without notifying + * @ring: Ring to enqueue the frame + * @frame: Frame to enqueue + * + * Same as tb_ring_rx_more() but for TX ring. + * + * Return: %-ESHUTDOWN if tb_ring_stop() has been called, %0 otherwise. + */ +static inline int tb_ring_tx_more(struct tb_ring *ring, struct ring_frame *frame) +{ + WARN_ON(!ring->is_tx); + return __tb_ring_enqueue(ring, frame, true); } /* Used only when the ring is in polling mode */ From 2d8b6e38b86c4cbad9d7bdd8371c696ef34b900c Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 14 Aug 2026 14:24:02 +0300 Subject: [PATCH 05/24] net: thunderbolt: Update ring indices only after all frames are queued Take advantage of the new API and update the ring producer/consumer indices only after the frames have been queued. This reduces the number of doorbell register writes from one per frame to one per batch. Measured using iperf3 over a Thunderbolt/USB4 net connection between Arrow Lake and Panther Lake systems (average of three runs): before after ~14.3 Gbit/s ~15.3 Gbit/s Assisted-by: LLM Acked-by: Jakub Kicinski Signed-off-by: Mika Westerberg --- drivers/net/thunderbolt/main.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c index d9fb587a62c5..cf51b9c39f4e 100644 --- a/drivers/net/thunderbolt/main.c +++ b/drivers/net/thunderbolt/main.c @@ -540,11 +540,12 @@ static int tbnet_alloc_rx_buffers(struct tbnet *net, unsigned int nbuffers) trace_tbnet_alloc_rx_frame(index, tf->page, dma_addr, DMA_FROM_DEVICE); - tb_ring_rx(ring->ring, &tf->frame); + tb_ring_rx_more(ring->ring, &tf->frame); ring->prod++; } + tb_ring_notify(ring->ring); return 0; err_free: @@ -1243,7 +1244,8 @@ static netdev_tx_t tbnet_start_xmit(struct sk_buff *skb, goto err_drop; for (i = 0; i < frame_index + 1; i++) - tb_ring_tx(net->tx_ring.ring, &frames[i]->frame); + tb_ring_tx_more(net->tx_ring.ring, &frames[i]->frame); + tb_ring_notify(net->tx_ring.ring); if (net->svc->prtcstns & TBNET_MATCH_FRAGS_ID) atomic_inc(&net->frame_id); From 41d5a1fc2810046547b0c5713a4dbf2284edf8f6 Mon Sep 17 00:00:00 2001 From: Atharva Tiwari Date: Tue, 18 Aug 2026 05:50:02 -0400 Subject: [PATCH 06/24] thunderbolt: Move tb_apple_add_links() to pci.c Due to historical reasons the software connection manager in tb.c directly sets up the device link between the tunneled PCIe ports and the NHI. Now as we are starting to support non-PCIe host interfaces as well we are trying to keep the connection manager implementation agnostic of the underlying host interface. For this reason move the device link creation into pci.c and expose it through a new NHI operation that the host interface code calls when it sets up the connection manager. Suggested-by: Mika Westerberg Signed-off-by: Atharva Tiwari Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 34 ++++++++++++----- drivers/thunderbolt/nhi.h | 2 + drivers/thunderbolt/pci.c | 76 ++++++++++++++++++++++++++++++++++++++ drivers/thunderbolt/tb.c | 78 --------------------------------------- 4 files changed, 103 insertions(+), 87 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index a0f7885845bc..af050d9853f6 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -1245,24 +1245,40 @@ void nhi_reset_interface(struct tb_nhi *nhi) static struct tb *nhi_select_cm(struct tb_nhi *nhi) { + bool linked = false; struct tb *tb; /* * USB4 case is simple. If we got control of any of the * capabilities, we use software CM. */ - if (tb_acpi_is_native()) - return tb_probe(nhi); + if (!tb_acpi_is_native()) { + /* + * Either firmware based CM is running (we did not get + * control from the firmware) or this is pre-USB4 PC so + * try first firmware CM and then fallback to software CM. + */ + tb = icm_probe(nhi); + if (tb) + return tb; + } - /* - * Either firmware based CM is running (we did not get control - * from the firmware) or this is pre-USB4 PC so try first - * firmware CM and then fallback to software CM. - */ - tb = icm_probe(nhi); + tb = tb_probe(nhi); if (!tb) - tb = tb_probe(nhi); + return NULL; + if (nhi->ops->add_links) + linked = nhi->ops->add_links(nhi); + if (!linked) + linked = tb_acpi_add_links(nhi); + /* + * Device links are needed to make sure we establish tunnels + * before protocol stacks are resumed so complain here if we + * found them missing. + */ + if (!linked) + dev_warn(nhi->dev, + "device links to tunneled native ports are missing!\n"); return tb; } diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h index f72d6b274501..b2e2e2c413b2 100644 --- a/drivers/thunderbolt/nhi.h +++ b/drivers/thunderbolt/nhi.h @@ -43,6 +43,7 @@ extern const struct dev_pm_ops nhi_pm_ops; /** * struct tb_nhi_ops - NHI specific optional operations * @init: NHI specific initialization + * @add_links: Setup device links for tunneling native protocols * @suspend_noirq: NHI specific suspend_noirq hook * @resume_noirq: NHI specific resume_noirq hook * @runtime_suspend: NHI specific runtime_suspend hook @@ -58,6 +59,7 @@ extern const struct dev_pm_ops nhi_pm_ops; */ struct tb_nhi_ops { int (*init)(struct tb_nhi *nhi); + bool (*add_links)(struct tb_nhi *nhi); int (*suspend_noirq)(struct tb_nhi *nhi, bool wakeup); int (*resume_noirq)(struct tb_nhi *nhi); int (*runtime_suspend)(struct tb_nhi *nhi); diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c index 99333729f3c2..c9c50004a6ac 100644 --- a/drivers/thunderbolt/pci.c +++ b/drivers/thunderbolt/pci.c @@ -18,6 +18,7 @@ #include #include #include +#include #include "nhi.h" #include "nhi_regs.h" @@ -129,6 +130,80 @@ static void nhi_pci_check_iommu(struct tb_nhi_pci *nhi_pci) str_enabled_disabled(port_ok)); } +static bool add_link(struct tb_nhi *nhi, struct pci_dev *pdev) +{ + const struct device_link *link; + + link = device_link_add(&pdev->dev, nhi->dev, + DL_FLAG_AUTOREMOVE_SUPPLIER | + DL_FLAG_PM_RUNTIME); + if (!link) { + dev_warn(nhi->dev, "device link creation from %s failed\n", + dev_name(&pdev->dev)); + return false; + } + + dev_dbg(nhi->dev, "created link from %s\n", dev_name(&pdev->dev)); + return true; +} + +/* + * During suspend the Thunderbolt controller is reset and all PCIe + * tunnels are lost. The NHI driver will try to reestablish all tunnels + * during resume. This adds device links between the tunneled PCIe + * downstream ports and the NHI so that the device core will make sure + * NHI is resumed first before the rest. + */ +static bool nhi_pci_add_links(struct tb_nhi *nhi) +{ + struct pci_dev *nhi_pdev = to_pci_dev(nhi->dev); + struct pci_dev *upstream, *pdev; + bool ret; + + if (!x86_apple_machine) + return false; + + switch (nhi_pdev->device) { + case PCI_DEVICE_ID_INTEL_LIGHT_RIDGE: + case PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C: + case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: + case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: + break; + default: + return false; + } + + upstream = pci_upstream_bridge(nhi_pdev); + while (upstream) { + if (!pci_is_pcie(upstream)) + return false; + if (pci_pcie_type(upstream) == PCI_EXP_TYPE_UPSTREAM) + break; + upstream = pci_upstream_bridge(upstream); + } + + if (!upstream) + return false; + + /* + * For each hotplug downstream port, create add device link back + * to NHI so that PCIe tunnels can be re-established after + * sleep. + */ + ret = false; + for_each_pci_bridge(pdev, upstream->subordinate) { + if (!pci_is_pcie(pdev)) + continue; + if (pci_pcie_type(pdev) != PCI_EXP_TYPE_DOWNSTREAM || + !pdev->is_pciehp) + continue; + + ret |= add_link(nhi, pdev); + } + + return ret; +} + static int nhi_pci_init_msi(struct tb_nhi *nhi) { struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); @@ -272,6 +347,7 @@ static bool nhi_pci_is_present(struct tb_nhi *nhi) } static const struct tb_nhi_ops pci_nhi_default_ops = { + .add_links = nhi_pci_add_links, .pre_nvm_auth = nhi_pci_start_dma_port, .post_nvm_auth = nhi_pci_complete_dma_port, .request_ring_irq = nhi_pci_ring_request_msix, diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index 47753a5c0f2e..7e39b067e5ff 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -10,7 +10,6 @@ #include #include #include -#include #include "tb.h" #include "tb_regs.h" @@ -3323,75 +3322,6 @@ static const struct tb_cm_ops tb_cm_ops = { .disconnect_xdomain_paths = tb_disconnect_xdomain_paths, }; -/* - * During suspend the Thunderbolt controller is reset and all PCIe - * tunnels are lost. The NHI driver will try to reestablish all tunnels - * during resume. This adds device links between the tunneled PCIe - * downstream ports and the NHI so that the device core will make sure - * NHI is resumed first before the rest. - */ -static bool tb_apple_add_links(struct tb_nhi *nhi) -{ - struct pci_dev *nhi_pdev = to_pci_dev(nhi->dev); - struct pci_dev *upstream, *pdev; - bool ret; - - if (!x86_apple_machine) - return false; - - switch (nhi_pdev->device) { - case PCI_DEVICE_ID_INTEL_LIGHT_RIDGE: - case PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C: - case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: - case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: - break; - default: - return false; - } - - upstream = pci_upstream_bridge(nhi_pdev); - while (upstream) { - if (!pci_is_pcie(upstream)) - return false; - if (pci_pcie_type(upstream) == PCI_EXP_TYPE_UPSTREAM) - break; - upstream = pci_upstream_bridge(upstream); - } - - if (!upstream) - return false; - - /* - * For each hotplug downstream port, create add device link - * back to NHI so that PCIe tunnels can be re-established after - * sleep. - */ - ret = false; - for_each_pci_bridge(pdev, upstream->subordinate) { - const struct device_link *link; - - if (!pci_is_pcie(pdev)) - continue; - if (pci_pcie_type(pdev) != PCI_EXP_TYPE_DOWNSTREAM || - !pdev->is_pciehp) - continue; - - link = device_link_add(&pdev->dev, nhi->dev, - DL_FLAG_AUTOREMOVE_SUPPLIER | - DL_FLAG_PM_RUNTIME); - if (link) { - dev_dbg(nhi->dev, "created link from %s\n", - dev_name(&pdev->dev)); - ret = true; - } else { - dev_warn(nhi->dev, "device link creation from %s failed\n", - dev_name(&pdev->dev)); - } - } - - return ret; -} - struct tb *tb_probe(struct tb_nhi *nhi) { struct tb_cm *tcm; @@ -3416,13 +3346,5 @@ struct tb *tb_probe(struct tb_nhi *nhi) tb_dbg(tb, "using software connection manager\n"); - /* - * Device links are needed to make sure we establish tunnels - * before the PCIe/USB stack is resumed so complain here if we - * found them missing. - */ - if (!tb_apple_add_links(nhi) && !tb_acpi_add_links(nhi)) - tb_warn(tb, "device links to tunneled native ports are missing!\n"); - return tb; } From 03364ca8c472618b28557b7044d38fc5d5844edd Mon Sep 17 00:00:00 2001 From: Atharva Tiwari Date: Tue, 18 Aug 2026 05:50:03 -0400 Subject: [PATCH 07/24] thunderbolt: Add device links for Apple machines with Titan Ridge Add device links for Apple machines with the Titan Ridge thunderbolt controller. Signed-off-by: Atharva Tiwari Signed-off-by: Mika Westerberg --- drivers/thunderbolt/pci.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c index c9c50004a6ac..996a8ab3cfb7 100644 --- a/drivers/thunderbolt/pci.c +++ b/drivers/thunderbolt/pci.c @@ -168,6 +168,8 @@ static bool nhi_pci_add_links(struct tb_nhi *nhi) case PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C: case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: + case PCI_DEVICE_ID_INTEL_TITAN_RIDGE_2C_NHI: + case PCI_DEVICE_ID_INTEL_TITAN_RIDGE_4C_NHI: break; default: return false; From 6cd43dea7b243aba6225a183a9d1c3698b9cc635 Mon Sep 17 00:00:00 2001 From: Atharva Tiwari Date: Tue, 18 Aug 2026 05:50:04 -0400 Subject: [PATCH 08/24] thunderbolt: Add device links for Apple systems with Ice Lake Ice Lake Thunderbolt NHI that are on some Macs. The NHI and its associated PCIe Root Ports all sit directly on the Root Complex with no upstream port. Identify the tunneled PCIe Root Ports by their PCI IDs and create device links back to the NHI so that PCIe tunnels can be re-established after sleep. Co-developed-by: Andre Eikmeyer Signed-off-by: Andre Eikmeyer Signed-off-by: Atharva Tiwari Signed-off-by: Mika Westerberg --- drivers/thunderbolt/pci.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c index 996a8ab3cfb7..e40d4d6af071 100644 --- a/drivers/thunderbolt/pci.c +++ b/drivers/thunderbolt/pci.c @@ -521,6 +521,40 @@ static int icl_nhi_resume(struct tb_nhi *nhi) return 0; } +static bool icl_nhi_add_links(struct tb_nhi *nhi) +{ + struct pci_dev *nhi_pdev = to_pci_dev(nhi->dev); + struct pci_bus *bus = nhi_pdev->bus; + struct pci_dev *pdev; + bool ret; + + if (!x86_apple_machine) + return false; + + /* + * On integrated controllers the tunneled PCIe root ports are + * directly under the host bridge. + */ + if (!pci_is_root_bus(bus)) + return false; + + ret = false; + for_each_pci_bridge(pdev, bus) { + switch (nhi_pdev->device) { + case PCI_DEVICE_ID_INTEL_ICL_NHI0: + if (pdev->device == 0x8a1d || pdev->device == 0x8a1f) + ret |= add_link(nhi, pdev); + break; + case PCI_DEVICE_ID_INTEL_ICL_NHI1: + if (pdev->device == 0x8a21 || pdev->device == 0x8a23) + ret |= add_link(nhi, pdev); + break; + } + } + + return ret; +} + static void icl_nhi_shutdown(struct tb_nhi *nhi) { nhi_pci_release_irq(nhi); @@ -530,6 +564,7 @@ static void icl_nhi_shutdown(struct tb_nhi *nhi) static const struct tb_nhi_ops icl_nhi_ops = { .init = icl_nhi_resume, + .add_links = icl_nhi_add_links, .suspend_noirq = icl_nhi_suspend_noirq, .resume_noirq = icl_nhi_resume, .runtime_suspend = icl_nhi_suspend, From 319d216d6546b2db9de5a818039a65ed62f0a7fd Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 12:00:21 +0530 Subject: [PATCH 09/24] thunderbolt: Fix typo "forwared" in comment Correct "forwared" to "forwarded", reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches comments, no code changes. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Signed-off-by: Mika Westerberg --- include/linux/thunderbolt.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 6fda5596c44b..57cdfe774883 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -369,7 +369,7 @@ int tb_xdomain_request(struct tb_xdomain *xd, const void *request, * @uuid: XDomain messages with this UUID are dispatched to this handler * @callback: Callback called with the XDomain message. Returning %1 * here tells the XDomain core that the message was handled - * by this handler and should not be forwared to other + * by this handler and should not be forwarded to other * handlers. * @data: Data passed with the callback * @list: Handlers are linked using this From ea958a19076769ed2bc82efccc0168819e437d57 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 14 Aug 2026 16:41:51 +0300 Subject: [PATCH 10/24] thunderbolt: Do not WARN about already disabled interrupt on polled rings With ->start_poll it is possible that the ring is actually already disabled before tb_ring_stop() is called. This is expected behaviour so there is no point warning about that. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index af050d9853f6..f716910e0372 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -138,10 +138,19 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) "%s interrupt at register %#x bit %d (%#x -> %#x)\n", active ? "enabling" : "disabling", reg, interrupt_bit, old, new); - if (new == old) - dev_WARN(ring->nhi->dev, "interrupt for %s %d is already %s\n", - RING_TYPE(ring), ring->hop, - str_enabled_disabled(active)); + if (new == old) { + /* + * Rings that are polled mask the interrupt using while + * the completions are being advanced (see + * __ring_interrupt()) so for those it can already be + * disabled by the time the ring is stopped. + */ + if (active || !ring->start_poll) + dev_WARN(ring->nhi->dev, + "interrupt for %s %d is already %s\n", + RING_TYPE(ring), ring->hop, + str_enabled_disabled(active)); + } if (active) iowrite32(new, ring->nhi->iobase + reg); From 85e5f54bb3b6e22d8a4a542b098a2600fdb68ced Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 17 Aug 2026 14:09:36 +0300 Subject: [PATCH 11/24] thunderbolt: Clean up ring interrupt register indexing nhi_mask_interrupt() and nhi_clear_interrupt() take "ring" as parameter but in fact it is not an actual ring but a byte offset to the interrupt register. Make this less confusing and name the paramers what it really is and calculate the offset where it is actually needed. In addition ring_interrupt_active() has two variables called "index" with different meanings, and the second one shadows the first one open-coding ring_interrupt_index() as well. Drop that and rename the remaining what they actually hold. While there, make the mask variable u32 and use BIT() to avoid signed shifting. No functional changes intended. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index f716910e0372..dfa9e9afea7b 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -40,6 +40,7 @@ static bool host_reset = true; module_param(host_reset, bool, 0444); MODULE_PARM_DESC(host_reset, "reset USB4 host router (default: true)"); +/* Returns absolute bit number of the ring in the interrupt registers */ static int ring_interrupt_index(const struct tb_ring *ring) { int bit = ring->hop; @@ -48,24 +49,28 @@ static int ring_interrupt_index(const struct tb_ring *ring) return bit; } -static void nhi_mask_interrupt(struct tb_nhi *nhi, int mask, int ring) +static void nhi_mask_interrupt(struct tb_nhi *nhi, u32 mask, int reg_index) { + int offset = reg_index * 4; + if (nhi->quirks & QUIRK_AUTO_CLEAR_INT) { u32 val; - val = ioread32(nhi->iobase + REG_RING_INTERRUPT_BASE + ring); - iowrite32(val & ~mask, nhi->iobase + REG_RING_INTERRUPT_BASE + ring); + val = ioread32(nhi->iobase + REG_RING_INTERRUPT_BASE + offset); + iowrite32(val & ~mask, nhi->iobase + REG_RING_INTERRUPT_BASE + offset); } else { - iowrite32(mask, nhi->iobase + REG_RING_INTERRUPT_MASK_CLEAR_BASE + ring); + iowrite32(mask, nhi->iobase + REG_RING_INTERRUPT_MASK_CLEAR_BASE + offset); } } -static void nhi_clear_interrupt(struct tb_nhi *nhi, int ring) +static void nhi_clear_interrupt(struct tb_nhi *nhi, int reg_index) { + int offset = reg_index * 4; + if (nhi->quirks & QUIRK_AUTO_CLEAR_INT) - ioread32(nhi->iobase + REG_RING_NOTIFY_BASE + ring); + ioread32(nhi->iobase + REG_RING_NOTIFY_BASE + offset); else - iowrite32(~0, nhi->iobase + REG_RING_INT_CLEAR + ring); + iowrite32(~0, nhi->iobase + REG_RING_INT_CLEAR + offset); } /* @@ -75,22 +80,17 @@ static void nhi_clear_interrupt(struct tb_nhi *nhi, int ring) */ static void ring_interrupt_active(struct tb_ring *ring, bool active) { - int index = ring_interrupt_index(ring) / 32 * 4; - int reg = REG_RING_INTERRUPT_BASE + index; - int interrupt_bit = ring_interrupt_index(ring) & 31; - int mask = 1 << interrupt_bit; + int interrupt_index = ring_interrupt_index(ring); + int reg_index = interrupt_index / 32; + int reg = REG_RING_INTERRUPT_BASE + reg_index * 4; + int interrupt_bit = interrupt_index % 32; + u32 mask = BIT(interrupt_bit); u32 old, new; if (ring->irq > 0) { u32 step, shift, ivr, misc, itr; void __iomem *ivr_base; int auto_clear_bit; - int index; - - if (ring->is_tx) - index = ring->hop; - else - index = ring->hop + ring->nhi->hop_count; /* * Intel routers support a bit that isn't part of @@ -113,8 +113,8 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) ring->nhi->iobase + REG_DMA_MISC); ivr_base = ring->nhi->iobase + REG_INT_VEC_ALLOC_BASE; - step = index / REG_INT_VEC_ALLOC_REGS * REG_INT_VEC_ALLOC_BITS; - shift = index % REG_INT_VEC_ALLOC_REGS * REG_INT_VEC_ALLOC_BITS; + step = interrupt_index / REG_INT_VEC_ALLOC_REGS * REG_INT_VEC_ALLOC_BITS; + shift = interrupt_index % REG_INT_VEC_ALLOC_REGS * REG_INT_VEC_ALLOC_BITS; ivr = ioread32(ivr_base + step); ivr &= ~(REG_INT_VEC_ALLOC_MASK << shift); if (active) @@ -155,7 +155,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) if (active) iowrite32(new, ring->nhi->iobase + reg); else - nhi_mask_interrupt(ring->nhi, mask, index); + nhi_mask_interrupt(ring->nhi, mask, reg_index); } /* @@ -168,11 +168,11 @@ void nhi_disable_interrupts(struct tb_nhi *nhi) int i = 0; /* disable interrupts */ for (i = 0; i < RING_INTERRUPT_REG_COUNT(nhi); i++) - nhi_mask_interrupt(nhi, ~0, 4 * i); + nhi_mask_interrupt(nhi, ~0, i); /* clear interrupt status bits */ for (i = 0; i < RING_NOTIFY_REG_COUNT(nhi); i++) - nhi_clear_interrupt(nhi, 4 * i); + nhi_clear_interrupt(nhi, i); } /* ring helper methods */ From 26e6e964d54d893f21f5614671fdcd24eacc3b78 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 14 Aug 2026 16:55:04 +0300 Subject: [PATCH 12/24] thunderbolt: Use shadow copy for ring interrupt mask Each time we mask or unmask ring interrupt we do a read-modify-write and that is not without a cost when on a hot path. We can get rid of that read by making a shadow copy of the interrupt mask register and using that in place of a register read. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 55 +++++++++++++++++++++++-------------- include/linux/thunderbolt.h | 2 ++ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index dfa9e9afea7b..521ba8b57b28 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -52,15 +52,28 @@ static int ring_interrupt_index(const struct tb_ring *ring) static void nhi_mask_interrupt(struct tb_nhi *nhi, u32 mask, int reg_index) { int offset = reg_index * 4; + u32 val; - if (nhi->quirks & QUIRK_AUTO_CLEAR_INT) { - u32 val; + /* Use shadow copy instead of reading the register */ + val = nhi->interrupt_mask[reg_index] & ~mask; + nhi->interrupt_mask[reg_index] = val; - val = ioread32(nhi->iobase + REG_RING_INTERRUPT_BASE + offset); - iowrite32(val & ~mask, nhi->iobase + REG_RING_INTERRUPT_BASE + offset); - } else { + if (nhi->quirks & QUIRK_AUTO_CLEAR_INT) + iowrite32(val, nhi->iobase + REG_RING_INTERRUPT_BASE + offset); + else iowrite32(mask, nhi->iobase + REG_RING_INTERRUPT_MASK_CLEAR_BASE + offset); - } +} + +static void nhi_unmask_interrupt(struct tb_nhi *nhi, u32 mask, int reg_index) +{ + int offset = reg_index * 4; + u32 val; + + /* Use shadow copy instead of reading the register */ + val = nhi->interrupt_mask[reg_index] | mask; + nhi->interrupt_mask[reg_index] = val; + + iowrite32(val, nhi->iobase + REG_RING_INTERRUPT_BASE + offset); } static void nhi_clear_interrupt(struct tb_nhi *nhi, int reg_index) @@ -128,7 +141,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) ring->vector * 4); } - old = ioread32(ring->nhi->iobase + reg); + old = ring->nhi->interrupt_mask[reg_index]; if (active) new = old | mask; else @@ -140,10 +153,10 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) if (new == old) { /* - * Rings that are polled mask the interrupt using while - * the completions are being advanced (see - * __ring_interrupt()) so for those it can already be - * disabled by the time the ring is stopped. + * Rings that are polled mask the interrupt while the + * completions are being advanced (see __ring_interrupt()) + * so for those it can already be disabled by the time + * the ring is stopped. */ if (active || !ring->start_poll) dev_WARN(ring->nhi->dev, @@ -153,7 +166,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) } if (active) - iowrite32(new, ring->nhi->iobase + reg); + nhi_unmask_interrupt(ring->nhi, mask, reg_index); else nhi_mask_interrupt(ring->nhi, mask, reg_index); } @@ -436,17 +449,14 @@ EXPORT_SYMBOL_GPL(tb_ring_poll); static void __ring_interrupt_mask(struct tb_ring *ring, bool mask) { - int idx = ring_interrupt_index(ring); - int reg = REG_RING_INTERRUPT_BASE + idx / 32 * 4; - int bit = idx % 32; - u32 val; + int interrupt_index = ring_interrupt_index(ring); + int reg_index = interrupt_index / 32; + int interrupt_bit = interrupt_index % 32; - val = ioread32(ring->nhi->iobase + reg); if (mask) - val &= ~BIT(bit); + nhi_mask_interrupt(ring->nhi, BIT(interrupt_bit), reg_index); else - val |= BIT(bit); - iowrite32(val, ring->nhi->iobase + reg); + nhi_unmask_interrupt(ring->nhi, BIT(interrupt_bit), reg_index); } /* Both @nhi->lock and @ring->lock should be held */ @@ -1310,7 +1320,10 @@ int nhi_probe(struct tb_nhi *nhi) sizeof(*nhi->tx_rings), GFP_KERNEL); nhi->rx_rings = devm_kcalloc(dev, nhi->hop_count, sizeof(*nhi->rx_rings), GFP_KERNEL); - if (!nhi->tx_rings || !nhi->rx_rings) + nhi->interrupt_mask = devm_kcalloc(dev, RING_INTERRUPT_REG_COUNT(nhi), + sizeof(*nhi->interrupt_mask), + GFP_KERNEL); + if (!nhi->tx_rings || !nhi->rx_rings || !nhi->interrupt_mask) return -ENOMEM; nhi_reset(nhi); diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 57cdfe774883..335fa31ab1df 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -506,6 +506,7 @@ void tb_service_properties_changed(struct tb_service *svc); * @iobase: MMIO space of the NHI * @tx_rings: All Tx rings available on this host controller * @rx_rings: All Rx rings available on this host controller + * @interrupt_mask: Shadow copy of the ring interrupt mask register * @going_away: The host controller device is about to disappear so when * this flag is set, avoid touching the hardware anymore. * @iommu_dma_protection: An IOMMU will isolate external-facing ports. @@ -527,6 +528,7 @@ struct tb_nhi { void __iomem *iobase; struct tb_ring **tx_rings; struct tb_ring **rx_rings; + u32 *interrupt_mask; bool going_away; bool iommu_dma_protection; struct work_struct interrupt_work; From 20bea8ceeb6ab6ab918ee8fdf5855541214907c4 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 14 Aug 2026 17:00:03 +0300 Subject: [PATCH 13/24] thunderbolt: stream: Use polling with RX ring The ring API already supports polling so that once there is anything in the RX ring, it will mask the ring interrupt and call the ->start_poll callback. This is the same mechanism that the USB4/Thunderbolt networking driver already uses. We can do the same for the USB4STREAM driver and poll any RX frames while they are available, until we re-enable the ring interrupt again. This provides better latency when the stream is in "normal" (non-busy-polling) mode. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/stream.c | 105 ++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index c737dd0ca6e7..56e155750466 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -131,6 +131,7 @@ struct tbstream_ring { * @ring_size: Size of the rings * @throttling: Interrupt throttling rate in ns * @busy_poll: Instead of interrupts, busy poll the rings + * @rx_pending: Receive ring has completions that need to be advanced * @users: Number of times @cdev has been opened * @closed: CLOSE packet was received * @removed: Userspace removed the ConfigFS group underneath. @@ -151,6 +152,7 @@ struct tbstream_dev { unsigned int ring_size; unsigned int throttling; bool busy_poll; + bool rx_pending; int users; bool closed; bool removed; @@ -278,6 +280,14 @@ static inline bool tbstream_ring_available(const struct tbstream_ring *ring) return ring->prod > ring->cons; } +static void tbstream_ring_poll(struct tbstream_ring *ring) +{ + struct ring_frame *frame; + + while ((frame = tb_ring_poll(ring->ring))) + frame->callback(ring->ring, frame, false); +} + static inline struct tb_xdomain *tbstream_dev_xdomain(struct tbstream_dev *sdev) { if (sdev->stream) @@ -540,18 +550,6 @@ tbstream_dev_send_data(struct tbstream_dev *sdev, struct iov_iter *from, return tb_ring_tx(sdev->tx_ring.ring, &sf->frame); } -static void -tbstream_dev_poll_ring(struct tbstream_dev *sdev, struct tbstream_ring *ring) -{ - struct ring_frame *frame; - - if (!sdev->busy_poll) - return; - - while ((frame = tb_ring_poll(ring->ring))) - frame->callback(ring->ring, frame, false); -} - static int tbstream_dev_send_close(struct tbstream_dev *sdev) { struct tbstream_frame *sf; @@ -568,7 +566,7 @@ static int tbstream_dev_send_close(struct tbstream_dev *sdev) do { if (tbstream_ring_available(&sdev->tx_ring)) break; - tbstream_dev_poll_ring(sdev, &sdev->tx_ring); + tbstream_ring_poll(&sdev->tx_ring); fsleep(15); } while (ktime_before(ktime_get(), timeout)); } @@ -579,16 +577,45 @@ static int tbstream_dev_send_close(struct tbstream_dev *sdev) return tb_ring_tx(sdev->tx_ring.ring, &sf->frame); } +static void tbstream_dev_start_poll(void *data) +{ + struct tbstream_dev *sdev = data; + + WRITE_ONCE(sdev->rx_pending, true); + wake_up_interruptible_poll(&sdev->wait, EPOLLIN | EPOLLRDNORM); +} + +/* sdev->lock must be held */ +static void tbstream_dev_advance_rx(struct tbstream_dev *sdev) +{ + /* + * Clear before running the completions so that an interrupt + * that arrives while we are doing that is not missed. + */ + WRITE_ONCE(sdev->rx_pending, false); + tbstream_ring_poll(&sdev->rx_ring); +} + +/* sdev->lock must be held */ +static void tbstream_dev_complete_rx(struct tbstream_dev *sdev) +{ + if (!sdev->busy_poll) + tb_ring_poll_complete(sdev->rx_ring.ring); +} + static int tbstream_dev_start(struct tbstream_dev *sdev) { struct tb_xdomain *xd = tbstream_dev_xdomain(sdev); unsigned int flags = RING_FLAG_FRAME | RING_FLAG_E2E; + void (*start_poll)(void *) = NULL; u16 sof_mask, eof_mask; struct tb_ring *ring; int ret, e2e_tx_hop; if (sdev->busy_poll) flags |= RING_FLAG_NO_INTERRUPT; + else + start_poll = tbstream_dev_start_poll; ring = tb_ring_alloc_tx(xd->tb->nhi, -1, sdev->ring_size, flags); if (!ring) @@ -604,7 +631,8 @@ static int tbstream_dev_start(struct tbstream_dev *sdev) eof_mask = BIT(TBSTREAM_DATA) | BIT(TBSTREAM_CLOSE); ring = tb_ring_alloc_rx(xd->tb->nhi, -1, sdev->ring_size, flags, - e2e_tx_hop, sof_mask, eof_mask, NULL, NULL); + e2e_tx_hop, sof_mask, eof_mask, start_poll, + sdev); if (!ring) { ret = -ENOMEM; goto err_free_tx_buffers; @@ -621,6 +649,8 @@ static int tbstream_dev_start(struct tbstream_dev *sdev) tb_ring_throttling(sdev->tx_ring.ring, sdev->throttling); tb_ring_throttling(sdev->rx_ring.ring, sdev->throttling); + sdev->rx_pending = false; + tb_ring_start(sdev->tx_ring.ring); tb_ring_start(sdev->rx_ring.ring); @@ -667,19 +697,16 @@ static void tbstream_dev_stop(struct tbstream_dev *sdev) do { if (tbstream_dev_tx_drained(sdev)) break; - tbstream_dev_poll_ring(sdev, &sdev->tx_ring); + tbstream_ring_poll(&sdev->tx_ring); fsleep(15); } while (ktime_before(ktime_get(), timeout)); - - tb_ring_stop(sdev->tx_ring.ring); - tb_ring_stop(sdev->rx_ring.ring); } else { tb_ring_flush(sdev->tx_ring.ring, 500); - tb_ring_stop(sdev->tx_ring.ring); - tb_ring_flush(sdev->rx_ring.ring, 500); - tb_ring_stop(sdev->rx_ring.ring); } + tb_ring_stop(sdev->tx_ring.ring); + tb_ring_stop(sdev->rx_ring.ring); + xd = tbstream_dev_xdomain(sdev); if (xd) { tb_xdomain_disable_paths(xd, sdev->out_hopid, @@ -727,8 +754,8 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) return ret; for (;;) { - /* When busy polling, advance any completions manually */ - tbstream_dev_poll_ring(sdev, &sdev->rx_ring); + /* Advance RX completions */ + tbstream_dev_advance_rx(sdev); ret = tbstream_dev_valid(sdev); if (ret) { @@ -744,6 +771,8 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) if (tbstream_ring_available(&sdev->rx_ring)) break; + /* Polled all we could. Re-enable the interrupt now. */ + tbstream_dev_complete_rx(sdev); mutex_unlock(&sdev->lock); if (nowait) @@ -755,6 +784,7 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) cond_resched(); } else { ret = wait_event_interruptible(sdev->wait, + READ_ONCE(sdev->rx_pending) || tbstream_ring_available(&sdev->rx_ring) || tbstream_dev_valid(sdev) != 0 || tbstream_dev_closed(sdev) || @@ -839,7 +869,9 @@ tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) return ret; for (;;) { - tbstream_dev_poll_ring(sdev, &sdev->tx_ring); + /* When busy polling, advance any completions manually */ + if (sdev->busy_poll) + tbstream_ring_poll(&sdev->tx_ring); ret = tbstream_dev_valid(sdev); if (ret) { @@ -919,14 +951,23 @@ tbstream_dev_fops_poll(struct file *file, struct poll_table_struct *wait) poll_wait(file, &sdev->wait, wait); guard(mutex)(&sdev->lock); - if (tbstream_dev_valid(sdev) != 0) { - mask |= EPOLLHUP | EPOLLERR; - } else { - if (tbstream_ring_available(&sdev->tx_ring)) - mask |= EPOLLOUT | EPOLLWRNORM; - if (tbstream_ring_available(&sdev->rx_ring)) - mask |= EPOLLIN | EPOLLRDNORM; - } + if (tbstream_dev_valid(sdev) != 0) + return EPOLLHUP | EPOLLERR; + + /* + * The RX completions are only advanced from here and from + * read(2) so do that now, otherwise we would never report + * anything to be available. + */ + tbstream_dev_advance_rx(sdev); + + if (tbstream_ring_available(&sdev->tx_ring)) + mask |= EPOLLOUT | EPOLLWRNORM; + if (tbstream_ring_available(&sdev->rx_ring)) + mask |= EPOLLIN | EPOLLRDNORM; + else + tbstream_dev_complete_rx(sdev); + return mask; } From 4d84caebab18c7c5bc84a3ed7cfef439449f7ae3 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 7 Sep 2026 16:42:19 +0300 Subject: [PATCH 14/24] thunderbolt: Write descriptors in tb_ring_poll() When the ring is not in interrupt mode, it depends on something enqueuing new frames to write the descriptors from ring->queue to the hardware, and that can take a long time. Since we know that we just released one slot, we can push the next frame directly from tb_ring_poll(). Reported-by: Alan Borzeszkowski Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 521ba8b57b28..836f5d0013b0 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -439,6 +439,13 @@ struct ring_frame *tb_ring_poll(struct tb_ring *ring) } ring->tail = (ring->tail + 1) % ring->size; + + /* + * There is one more slot available so we can push next + * descriptor to the ring. This is needed when the ring + * is created with %RING_FLAG_NO_INTERRUPT. + */ + ring_write_descriptors(ring, true); } unlock: From de9d89b4086129cd575640ed111b18d43c01a5d4 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 7 Sep 2026 16:40:45 +0300 Subject: [PATCH 15/24] thunderbolt: Add tb_ring_poll_pending() This helper can be used to figure out whether tb_ring_poll() has a frame to be returned. This can be used without the caller needing to take its own lock. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 19 +++++++++++++++++++ include/linux/thunderbolt.h | 1 + 2 files changed, 20 insertions(+) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 836f5d0013b0..6d2be7734a00 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -454,6 +454,25 @@ struct ring_frame *tb_ring_poll(struct tb_ring *ring) } EXPORT_SYMBOL_GPL(tb_ring_poll); +/** + * tb_ring_poll_pending() - Does the ring have completed frame + * @ring: Ring to check + * + * Can be used to check whether there is a completed frame in the ring + * that next call to tb_ring_poll() returns. + * + * Return: %true if a completed frame is waiting, %false otherwise. + */ +bool tb_ring_poll_pending(struct tb_ring *ring) +{ + guard(spinlock_irqsave)(&ring->lock); + + if (!ring->running || ring_empty(ring)) + return false; + return !!(ring->descriptors[ring->tail].flags & RING_DESC_COMPLETED); +} +EXPORT_SYMBOL_GPL(tb_ring_poll_pending); + static void __ring_interrupt_mask(struct tb_ring *ring, bool mask) { int interrupt_index = ring_interrupt_index(ring); diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 335fa31ab1df..577bcf6e3810 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -754,6 +754,7 @@ static inline int tb_ring_tx_more(struct tb_ring *ring, struct ring_frame *frame /* Used only when the ring is in polling mode */ struct ring_frame *tb_ring_poll(struct tb_ring *ring); +bool tb_ring_poll_pending(struct tb_ring *ring); void tb_ring_poll_complete(struct tb_ring *ring); int tb_ring_throttling(struct tb_ring *ring, unsigned int interval_nsec); From 7049eb52c81645df81b1867e48bda89f11e585b8 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 7 Sep 2026 16:41:07 +0300 Subject: [PATCH 16/24] thunderbolt: stream: Do not hold the lock while busy polling When in busy poll mode with a reader and writer, and there is nothing completed on the rings both sides keep taking and releasing the lock from each other and this can cause stalls. Avoid this by calling the new API function tb_ring_poll_pending() that only returns true if there is anything available in which case they can take the lock and know there is now work to do. Reported-by: Alan Borzeszkowski Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/stream.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index 56e155750466..a8b0be27b4a1 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -736,6 +736,22 @@ static int tbstream_dev_lock(struct tbstream_dev *sdev, bool nowait) return 0; } +/* Must not be called with @sdev->lock held */ +static int tbstream_dev_busy_poll_wait(struct tbstream_dev *sdev, + struct tbstream_ring *ring) +{ + for (;;) { + if (signal_pending(current)) + return -ERESTARTSYS; + if (tb_ring_poll_pending(ring->ring)) + return 0; + if (tbstream_dev_valid(sdev) != 0 || + tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev)) + return 0; + cond_resched(); + } +} + static ssize_t tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) { @@ -779,9 +795,9 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) return -EAGAIN; if (sdev->busy_poll) { - if (signal_pending(current)) - return -ERESTARTSYS; - cond_resched(); + ret = tbstream_dev_busy_poll_wait(sdev, &sdev->rx_ring); + if (ret) + return ret; } else { ret = wait_event_interruptible(sdev->wait, READ_ONCE(sdev->rx_pending) || @@ -893,9 +909,9 @@ tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) return -EAGAIN; if (sdev->busy_poll) { - if (signal_pending(current)) - return -ERESTARTSYS; - cond_resched(); + ret = tbstream_dev_busy_poll_wait(sdev, &sdev->tx_ring); + if (ret) + return ret; } else { ret = wait_event_interruptible(sdev->wait, tbstream_ring_available(&sdev->tx_ring) || From 5da91c3694b8060b651aab5d95b2e4729c6e2b56 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 7 Sep 2026 16:41:18 +0300 Subject: [PATCH 17/24] thunderbolt: stream: Check the Rx ring before task starts sleeping In interrupt mode when tb_ring_poll_complete() is called it is possible that a frame was completed right before the ring interrupt was unmasked in which case there will be no interrupt and the task starts sleeping. Avoid this and check if there is anything completed prior putting the task to sleep. The same can happen with poll() so check the ring there too before reporting that there is nothing to read. Assisted-by: LLM Signed-off-by: Mika Westerberg --- drivers/thunderbolt/stream.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index a8b0be27b4a1..f5b67c09ed8d 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -801,6 +801,7 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) } else { ret = wait_event_interruptible(sdev->wait, READ_ONCE(sdev->rx_pending) || + tb_ring_poll_pending(sdev->rx_ring.ring) || tbstream_ring_available(&sdev->rx_ring) || tbstream_dev_valid(sdev) != 0 || tbstream_dev_closed(sdev) || @@ -979,10 +980,13 @@ tbstream_dev_fops_poll(struct file *file, struct poll_table_struct *wait) if (tbstream_ring_available(&sdev->tx_ring)) mask |= EPOLLOUT | EPOLLWRNORM; - if (tbstream_ring_available(&sdev->rx_ring)) + if (tbstream_ring_available(&sdev->rx_ring)) { mask |= EPOLLIN | EPOLLRDNORM; - else + } else { tbstream_dev_complete_rx(sdev); + if (tb_ring_poll_pending(sdev->rx_ring.ring)) + mask |= EPOLLIN | EPOLLRDNORM; + } return mask; } From 8d55710a8c439c45737f30ccf626d04241b95303 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 15 Sep 2026 18:55:27 +0300 Subject: [PATCH 18/24] thunderbolt: stream: Improve CLOSE handling on write side If there is only a writer in the stream (i.e the other side just reads and this side just writes) then if the other side is closed the CLOSE packet does is never picked up from the writers RX ring because we do not complete the RX poll. Currently we do that only when something is actually reading the stream. To make this work on write-only stream, complete the RX poll also during ->write_iter(), and then make the stream return -EPIPE to the writer. While there make sdev->closed to use READ/WRITE_ONCE() analogous to other flags. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/stream.c | 150 ++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 38 deletions(-) diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index f5b67c09ed8d..8a1e06148a14 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -133,13 +133,18 @@ struct tbstream_ring { * @busy_poll: Instead of interrupts, busy poll the rings * @rx_pending: Receive ring has completions that need to be advanced * @users: Number of times @cdev has been opened - * @closed: CLOSE packet was received + * @close_received: CLOSE packet appeared in the RX ring + * @closed: CLOSE packet was handled in the read side * @removed: Userspace removed the ConfigFS group underneath. * @wait: Waitqueue for open, read and write * @lock: Lock protecting this structure * @tx_ring: Transmit ring * @rx_ring: Receive ring * @list: Stream devices are linked through this + * + * @close_received is used on write side to notify the writer that the + * other side sent CLOSE. @closed on the other hand is used on the read + * side to make read(2) return EOF to the caller. */ struct tbstream_dev { struct config_group group; @@ -154,6 +159,7 @@ struct tbstream_dev { bool busy_poll; bool rx_pending; int users; + bool close_received; bool closed; bool removed; wait_queue_head_t wait; @@ -348,9 +354,19 @@ static inline bool tbstream_dev_removed(const struct tbstream_dev *sdev) return sdev->removed; } +static inline bool tbstream_dev_close_received(const struct tbstream_dev *sdev) +{ + return READ_ONCE(sdev->close_received); +} + static inline bool tbstream_dev_closed(const struct tbstream_dev *sdev) { - return sdev->closed; + return READ_ONCE(sdev->closed); +} + +static inline bool tbstream_dev_rx_pending(const struct tbstream_dev *sdev) +{ + return READ_ONCE(sdev->rx_pending); } static void @@ -366,12 +382,19 @@ tbstream_dev_rx_callback(struct tb_ring *ring, struct ring_frame *frame, sf->completed = true; sdev->rx_ring.prod++; - if (sf->frame.flags & RING_DESC_CRC_ERROR) + if (sf->frame.flags & RING_DESC_CRC_ERROR) { pr_warn("RX CRC error\n"); - else if (sf->frame.flags & RING_DESC_BUFFER_OVERRUN) + } else if (sf->frame.flags & RING_DESC_BUFFER_OVERRUN) { pr_warn("RX buffer overrun\n"); - else - wake_up_interruptible_poll(&sdev->wait, EPOLLIN | EPOLLRDNORM); + } else { + __poll_t mask = EPOLLIN | EPOLLRDNORM; + + if (sf->frame.eof == TBSTREAM_CLOSE) { + WRITE_ONCE(sdev->close_received, true); + mask |= EPOLLHUP; + } + wake_up_interruptible_poll(&sdev->wait, mask); + } } static struct tbstream_frame * @@ -553,23 +576,24 @@ tbstream_dev_send_data(struct tbstream_dev *sdev, struct iov_iter *from, static int tbstream_dev_send_close(struct tbstream_dev *sdev) { struct tbstream_frame *sf; + ktime_t timeout; - if (sdev->busy_poll) { + /* + * Wait for the ring to have available slots before we send the + * CLOSE packet. + */ + timeout = ktime_add_ms(ktime_get(), 500); + do { + if (tbstream_ring_available(&sdev->tx_ring)) + break; /* - * When busy polling it's the write(2) path that - * advances the completions so it is possible that the - * ring is full at this point. Advance the ring here so - * that there is room for the CLOSE packet to be sent. + * For busy polling we need to advance the ring here as + * well to make the slots available. */ - ktime_t timeout = ktime_add_ms(ktime_get(), 500); - - do { - if (tbstream_ring_available(&sdev->tx_ring)) - break; + if (sdev->busy_poll) tbstream_ring_poll(&sdev->tx_ring); - fsleep(15); - } while (ktime_before(ktime_get(), timeout)); - } + fsleep(15); + } while (ktime_before(ktime_get(), timeout)); sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_CLOSE, NULL, SZ_256); if (IS_ERR(sf)) @@ -745,6 +769,13 @@ static int tbstream_dev_busy_poll_wait(struct tbstream_dev *sdev, return -ERESTARTSYS; if (tb_ring_poll_pending(ring->ring)) return 0; + /* + * For TX ring we need to check the RX side too because + * it might have received CLOSE packet. + */ + if (ring == &sdev->tx_ring && + tb_ring_poll_pending(sdev->rx_ring.ring)) + return 0; if (tbstream_dev_valid(sdev) != 0 || tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev)) return 0; @@ -752,6 +783,22 @@ static int tbstream_dev_busy_poll_wait(struct tbstream_dev *sdev, } } +static bool +tbstream_dev_has_event(struct tbstream_dev *sdev, struct tbstream_ring *ring) +{ + if (tbstream_dev_valid(sdev) != 0) + return true; + if (tbstream_dev_closed(sdev)) + return true; + if (tbstream_dev_removed(sdev)) + return true; + if (tbstream_dev_rx_pending(sdev)) + return true; + if (tbstream_ring_available(ring)) + return true; + return tb_ring_poll_pending(sdev->rx_ring.ring); +} + static ssize_t tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) { @@ -800,12 +847,7 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) return ret; } else { ret = wait_event_interruptible(sdev->wait, - READ_ONCE(sdev->rx_pending) || - tb_ring_poll_pending(sdev->rx_ring.ring) || - tbstream_ring_available(&sdev->rx_ring) || - tbstream_dev_valid(sdev) != 0 || - tbstream_dev_closed(sdev) || - tbstream_dev_removed(sdev)); + tbstream_dev_has_event(sdev, &sdev->rx_ring)); if (ret) return ret; } @@ -832,7 +874,7 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) if (sf->frame.eof == TBSTREAM_CLOSE) { if (!nbytes) { tbstream_dev_consume_rx(sdev); - sdev->closed = true; + WRITE_ONCE(sdev->closed, true); } break; } @@ -868,6 +910,25 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) return nbytes; } +static void tbstream_dev_advance_both(struct tbstream_dev *sdev) +{ + /* + * When busy polling, advance TX completions manually. + * + * We also need to advance the RX side in both modes to be able + * to receive CLOSE packet from the other peer if there is no + * reader. + */ + if (sdev->busy_poll) { + tbstream_ring_poll(&sdev->tx_ring); + tbstream_dev_advance_rx(sdev); + } else if (tbstream_dev_rx_pending(sdev) || + tb_ring_poll_pending(sdev->rx_ring.ring)) { + tbstream_dev_advance_rx(sdev); + tbstream_dev_complete_rx(sdev); + } +} + static ssize_t tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) { @@ -886,9 +947,8 @@ tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) return ret; for (;;) { - /* When busy polling, advance any completions manually */ - if (sdev->busy_poll) - tbstream_ring_poll(&sdev->tx_ring); + /* Advance TX (and RX) completions */ + tbstream_dev_advance_both(sdev); ret = tbstream_dev_valid(sdev); if (ret) { @@ -896,7 +956,12 @@ tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) return ret; } - if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev)) { + if (tbstream_dev_close_received(sdev)) { + mutex_unlock(&sdev->lock); + return -EPIPE; + } + + if (tbstream_dev_removed(sdev)) { mutex_unlock(&sdev->lock); return -ENXIO; } @@ -915,10 +980,8 @@ tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) return ret; } else { ret = wait_event_interruptible(sdev->wait, - tbstream_ring_available(&sdev->tx_ring) || - tbstream_dev_valid(sdev) != 0 || - tbstream_dev_closed(sdev) || - tbstream_dev_removed(sdev)); + tbstream_dev_has_event(sdev, &sdev->tx_ring) || + tbstream_dev_close_received(sdev)); if (ret) return ret; } @@ -978,6 +1041,8 @@ tbstream_dev_fops_poll(struct file *file, struct poll_table_struct *wait) */ tbstream_dev_advance_rx(sdev); + if (tbstream_dev_close_received(sdev)) + mask |= EPOLLHUP; if (tbstream_ring_available(&sdev->tx_ring)) mask |= EPOLLOUT | EPOLLWRNORM; if (tbstream_ring_available(&sdev->rx_ring)) { @@ -1037,6 +1102,8 @@ static int tbstream_dev_fops_open(struct inode *inode, struct file *file) sdev->users--; goto err_unlock; } + + sdev->close_received = false; sdev->closed = false; } @@ -1058,11 +1125,18 @@ static int tbstream_dev_fops_release(struct inode *inode, struct file *file) mutex_lock(&sdev->lock); if (--sdev->users == 0) { /* - * Send CLOSE tunneled packet to notify the other end - * that we are closing the file. We do this twice if the - * first one fails. + * Advance now in case there is CLOSE waiting in the RX + * ring. */ - tbstream_dev_send_close(sdev); + tbstream_dev_advance_both(sdev); + /* + * Send CLOSE tunneled packet to notify the other end + * that we are closing the file, if it is not closed + * already. + */ + if (!tbstream_dev_close_received(sdev) && + tbstream_dev_valid(sdev) == 0) + tbstream_dev_send_close(sdev); tbstream_dev_stop(sdev); } mutex_unlock(&sdev->lock); From aaf6230cb2cd5fc50db9ba6db3b813d1a4b1d582 Mon Sep 17 00:00:00 2001 From: Anurag Date: Thu, 27 Aug 2026 17:18:45 -0700 Subject: [PATCH 19/24] thunderbolt: Make software margining error counter configurable The debugfs software margining run path currently initializes the request with USB4_MARGIN_SW_ERROR_COUNTER_CLEAR, which implicitly clears/enables counters every time software margining is started. On some host-router/firmware implementations that enforce PHY interface for USB4 (PIPE) margining control, CLEAR and START are distinct operations. - CLEAR resets error counters without starting margining - START begins margin testing and error counting Make the error counter operation configurable through debugfs error_counter control instead of hardcoding CLEAR. Additionally, only execute the software margining sampling loop when the selected operation is START; for CLEAR or STOP operations, execute the counter operation and return immediately. Example to CLEAR error counter: cd /sys/kernel/debug/thunderbolt///margining echo clear > error_counter Example to START error counter: cd /sys/kernel/debug/thunderbolt///margining echo start > error_counter After configuring error counter run software margining for changes to take effect: echo 1 > run This aligns the debugfs behavior with the USB4 v2/PIPE expectation and lets users explicitly choose between CLEAR/START/STOP. Co-developed-by: Rene Sapiens Signed-off-by: Rene Sapiens Signed-off-by: Anurag Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index 6e9080e7bcec..730916f62d39 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -1156,18 +1156,23 @@ static int margining_run_sw(struct tb_margining *margining, u32 nsamples = margining->dwell_time / DWELL_SAMPLE_INTERVAL; int ret, i; + if (params->error_counter != USB4_MARGIN_SW_ERROR_COUNTER_START) + goto out_stop_or_clear; + ret = usb4_port_sw_margin(margining->port, margining->target, margining->index, params, margining->results); if (ret) - goto out_stop; + return ret; for (i = 0; i <= nsamples; i++) { u32 errors = 0; ret = usb4_port_sw_margin_errors(margining->port, margining->target, margining->index, &margining->results[1]); - if (ret) + if (ret) { + tb_port_warn(margining->port, "failed to read margining error counters\n"); break; + } if (margining->lanes == USB4_MARGINING_LANE_RX0) errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, @@ -1188,14 +1193,13 @@ static int margining_run_sw(struct tb_margining *margining, fsleep(DWELL_SAMPLE_INTERVAL * USEC_PER_MSEC); } -out_stop: +out_stop_or_clear: /* - * Stop the counters but don't clear them to allow the + * Stop the counters or clear them as per the * different error counter configurations. */ - margining_modify_error_counter(margining, margining->lanes, - USB4_MARGIN_SW_ERROR_COUNTER_STOP); - return ret; + return margining_modify_error_counter(margining, margining->lanes, + params->error_counter); } static int validate_margining(struct tb_margining *margining) @@ -1272,7 +1276,7 @@ static int margining_run_write(void *data, u64 val) if (margining->software) { struct usb4_port_margining_params params = { - .error_counter = USB4_MARGIN_SW_ERROR_COUNTER_CLEAR, + .error_counter = margining->error_counter, .lanes = margining->lanes, .time = margining->time, .voltage_time_offset = margining->voltage_time_offset, From c4f71538eec46f0e1e68fbf07381a9b76841466d Mon Sep 17 00:00:00 2001 From: Rene Sapiens Date: Thu, 27 Aug 2026 17:18:46 -0700 Subject: [PATCH 20/24] thunderbolt: Define number of dwords for hardware margining results Use a defined size for hardware margining results in preparation for the software margining extended error counters which requires a larger buffer size. Signed-off-by: Rene Sapiens Co-developed-by: Anurag Signed-off-by: Anurag Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index 730916f62d39..ea80354c410a 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -37,6 +37,8 @@ #define COUNTER_SET_LEN 3 +#define HW_MARGINING_DWORDS 3 + /* * USB4 spec doesn't specify dwell range, the range of 100 ms to 500 ms * probed to give good results. @@ -1307,7 +1309,7 @@ static int margining_run_write(void *data, u64 val) margining->lanes); ret = usb4_port_hw_margin(port, margining->target, margining->index, ¶ms, - margining->results, ARRAY_SIZE(margining->results)); + margining->results, HW_MARGINING_DWORDS); } if (down_sw) @@ -1429,7 +1431,7 @@ static int margining_results_show(struct seq_file *s, void *not_used) seq_printf(s, "0x%08x\n", margining->results[0]); /* Only the hardware margining has two result dwords */ if (!margining->software) { - for (int i = 1; i < ARRAY_SIZE(margining->results); i++) + for (int i = 1; i < HW_MARGINING_DWORDS; i++) seq_printf(s, "0x%08x\n", margining->results[i]); if (margining->lanes == USB4_MARGINING_LANE_ALL) { From 879519402cb5116538faf91952180f557083f9c3 Mon Sep 17 00:00:00 2001 From: Rene Sapiens Date: Thu, 27 Aug 2026 17:18:47 -0700 Subject: [PATCH 21/24] thunderbolt: Add extended error counter support for software lane margining Add support for software margining extended error counters. Increase the results buffer size to accommodate the new dwords used for extended error counters for RX0, RX1 and RX2. Signed-off-by: Rene Sapiens Co-developed-by: Anurag Signed-off-by: Anurag Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 34 ++++++++++++++++++++++++++++++++-- drivers/thunderbolt/sb_regs.h | 2 ++ drivers/thunderbolt/tb.h | 2 +- drivers/thunderbolt/usb4.c | 5 +++-- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index ea80354c410a..43de2938d1e4 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -38,6 +38,7 @@ #define COUNTER_SET_LEN 3 #define HW_MARGINING_DWORDS 3 +#define SW_MARGINING_DWORDS 4 /* * USB4 spec doesn't specify dwell range, the range of 100 ms to 500 ms @@ -496,7 +497,7 @@ struct tb_margining { unsigned int gen; bool asym_rx; u32 caps[3]; - u32 results[3]; + u32 results[5]; enum usb4_margining_lane lanes; unsigned int min_ber_level; unsigned int max_ber_level; @@ -614,6 +615,11 @@ supports_optional_voltage_offset_range(const struct tb_margining *margining) return margining->caps[0] & USB4_MARGIN_CAP_0_OPT_VOLTAGE_SUPPORT; } +static bool supports_extended_error_counter(const struct tb_margining *margining) +{ + return margining->caps[0] & USB4_MARGIN_CAP_0_EXT_ERR_COUNTER; +} + static ssize_t margining_ber_level_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) @@ -703,6 +709,8 @@ static int margining_caps_show(struct seq_file *s, void *not_used) ber_level_show(s, margining->max_ber_level); } else { seq_puts(s, "# hardware margining: no\n"); + seq_printf(s, "# extended error counter: %s\n", + str_yes_no(supports_extended_error_counter(margining))); } seq_printf(s, "# all lanes simultaneously: %s\n", @@ -1157,10 +1165,16 @@ static int margining_run_sw(struct tb_margining *margining, { u32 nsamples = margining->dwell_time / DWELL_SAMPLE_INTERVAL; int ret, i; + u32 dwords; if (params->error_counter != USB4_MARGIN_SW_ERROR_COUNTER_START) goto out_stop_or_clear; + if (supports_extended_error_counter(margining)) + dwords = SW_MARGINING_DWORDS; + else + dwords = 1; + ret = usb4_port_sw_margin(margining->port, margining->target, margining->index, params, margining->results); if (ret) @@ -1170,7 +1184,8 @@ static int margining_run_sw(struct tb_margining *margining, u32 errors = 0; ret = usb4_port_sw_margin_errors(margining->port, margining->target, - margining->index, &margining->results[1]); + margining->index, &margining->results[1], + dwords); if (ret) { tb_port_warn(margining->port, "failed to read margining error counters\n"); break; @@ -1450,18 +1465,30 @@ static int margining_results_show(struct seq_file *s, void *not_used) u32 lane_errors, result; seq_printf(s, "0x%08x\n", margining->results[1]); + if (supports_extended_error_counter(margining)) { + seq_printf(s, "0x%08x\n", margining->results[2]); + seq_printf(s, "0x%08x\n", margining->results[3]); + seq_printf(s, "0x%08x\n", margining->results[4]); + } + result = FIELD_GET(USB4_MARGIN_SW_LANES_MASK, margining->results[0]); if (result == USB4_MARGINING_LANE_RX0 || result == USB4_MARGINING_LANE_ALL) { lane_errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, margining->results[1]); + if (supports_extended_error_counter(margining)) + lane_errors |= FIELD_PREP(USB4_MARGIN_SW_EXT_ERR_COUNTER_MASK, + margining->results[2]); seq_printf(s, "# lane 0 errors: %u\n", lane_errors); } if (result == USB4_MARGINING_LANE_RX1 || result == USB4_MARGINING_LANE_ALL) { lane_errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK, margining->results[1]); + if (supports_extended_error_counter(margining)) + lane_errors |= FIELD_PREP(USB4_MARGIN_SW_EXT_ERR_COUNTER_MASK, + margining->results[3]); seq_printf(s, "# lane 1 errors: %u\n", lane_errors); } if (margining->asym_rx && @@ -1469,6 +1496,9 @@ static int margining_results_show(struct seq_file *s, void *not_used) result == USB4_MARGINING_LANE_ALL)) { lane_errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_2_MASK, margining->results[1]); + if (supports_extended_error_counter(margining)) + lane_errors |= FIELD_PREP(USB4_MARGIN_SW_EXT_ERR_COUNTER_MASK, + margining->results[4]); seq_printf(s, "# lane 2 errors: %u\n", lane_errors); } } diff --git a/drivers/thunderbolt/sb_regs.h b/drivers/thunderbolt/sb_regs.h index 5391502a4b87..b801f3a694be 100644 --- a/drivers/thunderbolt/sb_regs.h +++ b/drivers/thunderbolt/sb_regs.h @@ -59,6 +59,7 @@ enum usb4_sb_opcode { #define USB4_MARGIN_CAP_0_MAX_VOLTAGE_OFFSET_MASK GENMASK(18, 13) #define USB4_MARGIN_CAP_0_OPT_VOLTAGE_SUPPORT BIT(19) #define USB4_MARGIN_CAP_0_VOLT_STEPS_OPT_MASK GENMASK(26, 20) +#define USB4_MARGIN_CAP_0_EXT_ERR_COUNTER BIT(28) #define USB4_MARGIN_CAP_1_MAX_VOLT_OFS_OPT_MASK GENMASK(7, 0) #define USB4_MARGIN_CAP_1_TIME_DESTR BIT(8) #define USB4_MARGIN_CAP_1_TIME_INDP_MASK GENMASK(10, 9) @@ -108,5 +109,6 @@ enum usb4_sb_opcode { #define USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK GENMASK(3, 0) #define USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK GENMASK(7, 4) #define USB4_MARGIN_SW_ERR_COUNTER_LANE_2_MASK GENMASK(11, 8) +#define USB4_MARGIN_SW_EXT_ERR_COUNTER_MASK GENMASK(15, 4) #endif diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index c112954ce3fd..e86845762e22 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -1442,7 +1442,7 @@ int usb4_port_sw_margin(struct tb_port *port, enum usb4_sb_target target, u8 index, const struct usb4_port_margining_params *params, u32 *results); int usb4_port_sw_margin_errors(struct tb_port *port, enum usb4_sb_target target, - u8 index, u32 *errors); + u8 index, u32 *errors, size_t dwords); int usb4_port_retimer_set_inbound_sbtx(struct tb_port *port, u8 index); int usb4_port_retimer_unset_inbound_sbtx(struct tb_port *port, u8 index); diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c index 5fd4fe070f25..fe1b061f7055 100644 --- a/drivers/thunderbolt/usb4.c +++ b/drivers/thunderbolt/usb4.c @@ -1826,13 +1826,14 @@ int usb4_port_sw_margin(struct tb_port *port, enum usb4_sb_target target, * @target: Sideband target * @index: Retimer index if target is %USB4_SB_TARGET_RETIMER * @errors: Error metadata is copied here. + * @dwords: Number of dwords to read error counter values * * This reads back the software margining error counters from the port. * * Return: %0 on success, negative errno otherwise. */ int usb4_port_sw_margin_errors(struct tb_port *port, enum usb4_sb_target target, - u8 index, u32 *errors) + u8 index, u32 *errors, size_t dwords) { int ret; @@ -1842,7 +1843,7 @@ int usb4_port_sw_margin_errors(struct tb_port *port, enum usb4_sb_target target, return ret; return usb4_port_sb_read(port, target, index, USB4_SB_METADATA, errors, - sizeof(*errors)); + sizeof(*errors) * dwords); } static inline int usb4_port_retimer_op(struct tb_port *port, u8 index, From d224aacc8cf333700257fe0936f701848c2ea1b8 Mon Sep 17 00:00:00 2001 From: Anurag Date: Thu, 27 Aug 2026 17:18:48 -0700 Subject: [PATCH 22/24] thunderbolt: Fix sideband register access in usb4_port_sw_margin_errors() The READ_SW_MARGIN_ERR (RDSW) completion payload is returned in the USB4 sideband data window, but the location differs between standard and extended error counters. Standard error counters are returned in USB4_SB_METADATA and fit within a single dword. Extended error counters require 4 dwords and are returned in USB4_SB_DATA. Add conditional logic to select the appropriate register based on the dwords parameter that corresponds with error counter type. Co-developed-by: Rene Sapiens Signed-off-by: Rene Sapiens Signed-off-by: Anurag Signed-off-by: Mika Westerberg --- drivers/thunderbolt/usb4.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c index fe1b061f7055..c9b3931c45ed 100644 --- a/drivers/thunderbolt/usb4.c +++ b/drivers/thunderbolt/usb4.c @@ -1836,13 +1836,16 @@ int usb4_port_sw_margin_errors(struct tb_port *port, enum usb4_sb_target target, u8 index, u32 *errors, size_t dwords) { int ret; + u8 reg; ret = usb4_port_sb_op(port, target, index, USB4_SB_OPCODE_READ_SW_MARGIN_ERR, 150); if (ret) return ret; - return usb4_port_sb_read(port, target, index, USB4_SB_METADATA, errors, + reg = (dwords > 1) ? USB4_SB_DATA : USB4_SB_METADATA; + + return usb4_port_sb_read(port, target, index, reg, errors, sizeof(*errors) * dwords); } From 5ef3b5a4ea78da6ffdf32bcfe7517d4b2860f339 Mon Sep 17 00:00:00 2001 From: Anurag Date: Thu, 27 Aug 2026 17:18:49 -0700 Subject: [PATCH 23/24] thunderbolt: Extract per-lane extended error counters in software margining USB4 defines two error counter formats for software margining: - Standard counters (USB4 v1): 4-bit per-lane values in DW0. - Extended counters (USB4 v2): 16-bit per-lane values split across DW0 (lower 4 bits) and DW1-DW3 (upper 12 bits). The existing debugfs logic only reads DW0 when checking for errors, which works correctly for standard error counters but only captures the lower 4 bits, missing the 12 MSBs if extended error counter is supported. Read and combine the full counter values from all result dwords before evaluating whether errors occurred on any lane. When running software margining with "all lanes", determine which Rx lanes are actually active and only check those: - ASYM_RX: RX0, RX1, RX2 - ASYM_TX: RX0 - Symmetric: RX0, RX1 Refactor the error counter extraction into a helper function to improve readability. Signed-off-by: Anurag Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 68 ++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index 43de2938d1e4..ccefc5dd1ded 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -1160,9 +1160,34 @@ static int margining_mode_show(struct seq_file *s, void *not_used) } DEBUGFS_ATTR_RW(margining_mode); +static u32 margining_get_lane_error(const u32 *results, unsigned int lane, bool extended) +{ + u32 error_count; + + switch (lane) { + case USB4_MARGINING_LANE_RX0: + error_count = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, results[0]); + break; + case USB4_MARGINING_LANE_RX1: + error_count = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK, results[0]); + break; + case USB4_MARGINING_LANE_RX2: + error_count = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_2_MASK, results[0]); + break; + default: + return 0; + } + + if (extended) + error_count |= FIELD_PREP(USB4_MARGIN_SW_EXT_ERR_COUNTER_MASK, results[1 + lane]); + + return error_count; +} + static int margining_run_sw(struct tb_margining *margining, struct usb4_port_margining_params *params) { + bool extended_err = supports_extended_error_counter(margining); u32 nsamples = margining->dwell_time / DWELL_SAMPLE_INTERVAL; int ret, i; u32 dwords; @@ -1170,7 +1195,7 @@ static int margining_run_sw(struct tb_margining *margining, if (params->error_counter != USB4_MARGIN_SW_ERROR_COUNTER_START) goto out_stop_or_clear; - if (supports_extended_error_counter(margining)) + if (extended_err) dwords = SW_MARGINING_DWORDS; else dwords = 1; @@ -1191,17 +1216,35 @@ static int margining_run_sw(struct tb_margining *margining, break; } - if (margining->lanes == USB4_MARGINING_LANE_RX0) - errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_0_MASK, - margining->results[1]); - else if (margining->lanes == USB4_MARGINING_LANE_RX1) - errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_1_MASK, - margining->results[1]); - else if (margining->lanes == USB4_MARGINING_LANE_RX2) - errors = FIELD_GET(USB4_MARGIN_SW_ERR_COUNTER_LANE_2_MASK, - margining->results[1]); - else if (margining->lanes == USB4_MARGINING_LANE_ALL) - errors = margining->results[1]; + if (margining->lanes == USB4_MARGINING_LANE_ALL) { + if (extended_err) { + /* Extended counters with all lanes: check each active lane */ + int width = tb_port_get_link_width(margining->port); + unsigned int lane, max_lane = 2; + + if (width == TB_LINK_WIDTH_ASYM_TX) { + max_lane = 1; + } else if ((width == TB_LINK_WIDTH_ASYM_RX) && margining->asym_rx) { + max_lane = 3; + } else if (width < 0) { + tb_port_warn(margining->port, "failed to read link width\n"); + break; + } + + for (lane = 0; lane < max_lane; lane++) { + errors = margining_get_lane_error(&margining->results[1], + lane, extended_err); + if (errors) + break; + } + } else { + /* Standard counters: all lane errors fit in results[1]*/ + errors = margining->results[1]; + } + } else { + errors = margining_get_lane_error(&margining->results[1], + margining->lanes, extended_err); + } /* Any errors stop the test */ if (errors) @@ -1209,6 +1252,7 @@ static int margining_run_sw(struct tb_margining *margining, fsleep(DWELL_SAMPLE_INTERVAL * USEC_PER_MSEC); } + params->error_counter = USB4_MARGIN_SW_ERROR_COUNTER_STOP; out_stop_or_clear: /* From a93a8e3200002f0c345fec4c340390e9a60aabc7 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 18 Sep 2026 09:19:29 +0300 Subject: [PATCH 24/24] thunderbolt: stream: Make read return framing error to the userspace If any of the frames are corrupted during the receive we currently log a warning and pass them to the userspace without it knowing they were corrupted. Make the driver behave better and return an error (-EIO) to the userspace in that case. For the CLOSE packet, payload does not matter so we handle it the usual way. Reviewed-by: Ivy Lopez Signed-off-by: Mika Westerberg --- drivers/thunderbolt/stream.c | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index 8a1e06148a14..fe88341c4e29 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -375,6 +375,7 @@ tbstream_dev_rx_callback(struct tb_ring *ring, struct ring_frame *frame, { struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame); struct tbstream_dev *sdev = sf->sdev; + __poll_t mask; if (canceled) return; @@ -382,19 +383,17 @@ tbstream_dev_rx_callback(struct tb_ring *ring, struct ring_frame *frame, sf->completed = true; sdev->rx_ring.prod++; - if (sf->frame.flags & RING_DESC_CRC_ERROR) { - pr_warn("RX CRC error\n"); - } else if (sf->frame.flags & RING_DESC_BUFFER_OVERRUN) { - pr_warn("RX buffer overrun\n"); - } else { - __poll_t mask = EPOLLIN | EPOLLRDNORM; - - if (sf->frame.eof == TBSTREAM_CLOSE) { - WRITE_ONCE(sdev->close_received, true); - mask |= EPOLLHUP; - } - wake_up_interruptible_poll(&sdev->wait, mask); + mask = EPOLLIN | EPOLLRDNORM; + /* + * The CLOSE packet does not have a payload so the flags do not + * matter. The read_iter() deals with the flags. + */ + if (sf->frame.eof == TBSTREAM_CLOSE) { + WRITE_ONCE(sdev->close_received, true); + mask |= EPOLLHUP; } + + wake_up_interruptible_poll(&sdev->wait, mask); } static struct tbstream_frame * @@ -877,6 +876,19 @@ tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) WRITE_ONCE(sdev->closed, true); } break; + } else if (sf->frame.flags & + (RING_DESC_CRC_ERROR | RING_DESC_BUFFER_OVERRUN)) { + /* + * If something was already read return that now + * and next read will report the error. + */ + if (!nbytes) { + pr_warn("corrupted frame received, flags %#x\n", + sf->frame.flags); + tbstream_dev_consume_rx(sdev); + ret = -EIO; + } + break; } sf_size = tb_ring_frame_size(&sf->frame);