From 8eeb4bb9562f1d8915de4a8bd7c451ad9edb8dec Mon Sep 17 00:00:00 2001 From: Basavaraj Natikar Date: Thu, 11 Jun 2026 11:11:57 +0530 Subject: thunderbolt: Assert downstream port reset on shutdown On shutdown the connection manager tears down the router tree without signalling connected devices. A Thunderbolt 3 device directly connected to a USB4 host never receives a disconnect indication and during shutdown this can cause polling the dead link for up to 60 seconds. On some platforms this behavior leads to a warm reset instead of a shutdown due to this timeout. Fix this by asserting PORT_CS_19.DPR on each connected downstream port before tearing down the router tree. This drives SBTX low (USB4 spec section 6.9), causing the device to detect SBRX low and transition to Uninitialized Unplugged state immediately. Always do this on system shutdown/reboot by forcing host_reset in the PCI ->shutdown callback. On plain driver unload only do it when the host router was actually reset on load (host_reset=1), since in that case the tunnels are not preserved across reload anyway; with host_reset=0 the tunnels are kept alive across unload/reload so the links are left intact. Restrict the reset to Thunderbolt 3 devices. Reviewed-by: Mario Limonciello (AMD) Co-developed-by: Sanath S Signed-off-by: Sanath S Signed-off-by: Basavaraj Natikar Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 2 ++ drivers/thunderbolt/pci.c | 28 +++++++++++++++++++++++----- drivers/thunderbolt/switch.c | 11 ++++++++++- drivers/thunderbolt/tb.c | 21 +++++++++++++++++++++ drivers/thunderbolt/tb.h | 1 + 5 files changed, 57 insertions(+), 6 deletions(-) (limited to 'drivers') diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 0f795ea58756..698fb124d529 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -1235,6 +1235,8 @@ int nhi_probe(struct tb_nhi *nhi) init_completion(&nhi->domain_released); + nhi->host_reset = host_reset; + res = tb_domain_add(tb, host_reset); if (res) { /* diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c index bbd186c29ef7..dbb6badda867 100644 --- a/drivers/thunderbolt/pci.c +++ b/drivers/thunderbolt/pci.c @@ -230,7 +230,7 @@ static void nhi_pci_ring_release_msix(struct tb_ring *ring) ring->irq = 0; } -static void nhi_pci_shutdown(struct tb_nhi *nhi) +static void nhi_pci_release_irq(struct tb_nhi *nhi) { struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); struct pci_dev *pdev = to_pci_dev(nhi->dev); @@ -256,7 +256,7 @@ static const struct tb_nhi_ops pci_nhi_default_ops = { .post_nvm_auth = nhi_pci_complete_dma_port, .request_ring_irq = nhi_pci_ring_request_msix, .release_ring_irq = nhi_pci_ring_release_msix, - .shutdown = nhi_pci_shutdown, + .shutdown = nhi_pci_release_irq, .is_present = nhi_pci_is_present, .init_interrupts = nhi_pci_init_msi, }; @@ -424,7 +424,7 @@ static int icl_nhi_resume(struct tb_nhi *nhi) static void icl_nhi_shutdown(struct tb_nhi *nhi) { - nhi_pci_shutdown(nhi); + nhi_pci_release_irq(nhi); icl_nhi_force_power(nhi, false); } @@ -479,11 +479,19 @@ static int nhi_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) return nhi_probe(&nhi_pci->nhi); } -static void nhi_pci_remove(struct pci_dev *pdev) +static void nhi_pci_do_remove(struct pci_dev *pdev, bool reset) { struct tb *tb = pci_get_drvdata(pdev); struct tb_nhi *nhi = tb->nhi; + /* + * On system shutdown/reboot force a host router reset so the + * connection manager asserts DPR on connected Thunderbolt 3 devices + * before the router tree is removed (see tb_stop()). + */ + if (reset) + nhi->host_reset = true; + pm_runtime_get_sync(&pdev->dev); pm_runtime_dont_use_autosuspend(&pdev->dev); pm_runtime_forbid(&pdev->dev); @@ -493,6 +501,16 @@ static void nhi_pci_remove(struct pci_dev *pdev) nhi_shutdown(nhi); } +static void nhi_pci_remove(struct pci_dev *pdev) +{ + nhi_pci_do_remove(pdev, false); +} + +static void nhi_pci_shutdown(struct pci_dev *pdev) +{ + nhi_pci_do_remove(pdev, true); +} + static struct pci_device_id nhi_ids[] = { /* * We have to specify class, the TB bridges use the same device and @@ -593,7 +611,7 @@ static struct pci_driver nhi_driver = { .id_table = nhi_ids, .probe = nhi_pci_probe, .remove = nhi_pci_remove, - .shutdown = nhi_pci_remove, + .shutdown = nhi_pci_shutdown, .driver.pm = &nhi_pm_ops, }; diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index a830c82bb905..404c0693df50 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -682,7 +682,16 @@ int tb_port_disable(struct tb_port *port) return __tb_port_enable(port, false); } -static int tb_port_reset(struct tb_port *port) +/** + * tb_port_reset() - Reset the port + * @port: Port to reset + * + * Resets @port. For USB4 ports this issues a USB4 port reset and for + * legacy ports the link controller port is reset. + * + * Return: %0 on success, negative errno otherwise. + */ +int tb_port_reset(struct tb_port *port) { if (tb_switch_is_usb4(port->sw)) return port->cap_usb4 ? usb4_port_reset(port) : 0; diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index 76323255439a..b7cc6894a598 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -2941,7 +2941,9 @@ static void tb_handle_event(struct tb *tb, enum tb_cfg_pkg_type type, static void tb_stop(struct tb *tb) { struct tb_cm *tcm = tb_priv(tb); + struct tb_nhi *nhi = tb->nhi; struct tb_tunnel *tunnel; + struct tb_port *port; struct tb_tunnel *n; cancel_delayed_work(&tcm->remove_work); @@ -2956,6 +2958,25 @@ static void tb_stop(struct tb *tb) tb_tunnel_deactivate(tunnel); tb_tunnel_put(tunnel); } + /* + * Signal disconnect to connected devices before the router tree is + * removed below. A Thunderbolt 3 device directly connected to a USB4 + * host otherwise never receives a disconnect indication, leaving + * firmware to poll the dead link for up to ~60 s which on some + * platforms turns the shutdown into a warm reset. Asserting + * PORT_CS_19.DPR drives SBTX low (USB4 spec section 6.9) so the device + * detects SBRX low and goes to Uninitialized Unplugged immediately. + */ + if (nhi->host_reset) { + tb_switch_for_each_port(tb->root_switch, port) { + if (!tb_port_is_null(port) || !tb_port_has_remote(port)) + continue; + if (tb_switch_is_usb4(port->remote->sw)) + continue; + if (tb_port_reset(port)) + tb_port_dbg(port, "downstream port reset failed, continuing\n"); + } + } tb_switch_remove(tb->root_switch); tb->root_switch = NULL; tcm->hotplug_active = false; /* signal tb_handle_hotplug to quit */ diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index ec9192b61bc0..4373336d9425 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -1103,6 +1103,7 @@ int tb_port_clear_counter(struct tb_port *port, int counter); int tb_port_unlock(struct tb_port *port); int tb_port_enable(struct tb_port *port); int tb_port_disable(struct tb_port *port); +int tb_port_reset(struct tb_port *port); int tb_port_alloc_in_hopid(struct tb_port *port, int hopid, int max_hopid); void tb_port_release_in_hopid(struct tb_port *port, int hopid); int tb_port_alloc_out_hopid(struct tb_port *port, int hopid, int max_hopid); -- cgit v1.2.3 From 5c777f41e37a2f4985a0f53395b6ebd02eb0a6c6 Mon Sep 17 00:00:00 2001 From: "Uwe Kleine-König (The Capable Hub)" Date: Thu, 18 Jun 2026 12:14:50 +0200 Subject: thunderbolt: Stop passing matched device ID to .probe() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No driver makes use of that parameter, so drop it and don't spend the effort to determine the matching entry. Signed-off-by: Uwe Kleine-König (The Capable Hub) Signed-off-by: Mika Westerberg --- drivers/net/thunderbolt/main.c | 2 +- drivers/thunderbolt/dma_test.c | 2 +- drivers/thunderbolt/domain.c | 4 +--- drivers/thunderbolt/stream.c | 2 +- include/linux/thunderbolt.h | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) (limited to 'drivers') diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c index f8f97e8e2226..edfcfc41a316 100644 --- a/drivers/net/thunderbolt/main.c +++ b/drivers/net/thunderbolt/main.c @@ -1335,7 +1335,7 @@ static void tbnet_generate_mac(struct net_device *dev) dev->priv_flags |= IFF_LIVE_ADDR_CHANGE; } -static int tbnet_probe(struct tb_service *svc, const struct tb_service_id *id) +static int tbnet_probe(struct tb_service *svc) { struct tb_xdomain *xd = tb_service_parent(svc); struct net_device *dev; diff --git a/drivers/thunderbolt/dma_test.c b/drivers/thunderbolt/dma_test.c index 7877319b1b03..63e6bbf00e12 100644 --- a/drivers/thunderbolt/dma_test.c +++ b/drivers/thunderbolt/dma_test.c @@ -636,7 +636,7 @@ static void dma_test_debugfs_init(struct tb_service *svc) debugfs_create_file("test", 0200, debugfs_dir, svc, &test_fops); } -static int dma_test_probe(struct tb_service *svc, const struct tb_service_id *id) +static int dma_test_probe(struct tb_service *svc) { struct tb_xdomain *xd = tb_service_parent(svc); struct dma_test *dt; diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c index 479fa4d265c2..24611f05b3cd 100644 --- a/drivers/thunderbolt/domain.c +++ b/drivers/thunderbolt/domain.c @@ -77,12 +77,10 @@ static int tb_service_probe(struct device *dev) { struct tb_service *svc = tb_to_service(dev); struct tb_service_driver *driver; - const struct tb_service_id *id; driver = container_of(dev->driver, struct tb_service_driver, driver); - id = __tb_service_match(dev, &driver->driver); - return driver->probe(svc, id); + return driver->probe(svc); } static void tb_service_remove(struct device *dev) diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index c1f5c55583d0..b28e4e95b422 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -1540,7 +1540,7 @@ static void tbstream_group_detach_stream(struct tbstream *stream) config_group_put(&sg->group); } -static int tbstream_probe(struct tb_service *svc, const struct tb_service_id *id) +static int tbstream_probe(struct tb_service *svc) { struct tbstream *stream; diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index cb1621c6b703..0a9ac4bfea67 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -465,7 +465,7 @@ static inline struct tb_service *tb_to_service(struct device *dev) */ struct tb_service_driver { struct device_driver driver; - int (*probe)(struct tb_service *svc, const struct tb_service_id *id); + int (*probe)(struct tb_service *svc); void (*remove)(struct tb_service *svc); void (*shutdown)(struct tb_service *svc); const struct tb_service_id *id_table; -- cgit v1.2.3 From bc082c354e414173ff1994cab557cc8687297b8e Mon Sep 17 00:00:00 2001 From: "Uwe Kleine-König (The Capable Hub)" Date: Thu, 18 Jun 2026 12:14:51 +0200 Subject: thunderbolt: Assert that a service driver has a probe callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tb_service_probe() calls the driver's probe function unconditionally. Check at driver register time that this callback is valid to prevent a NULL pointer exception. Signed-off-by: Uwe Kleine-König (The Capable Hub) Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'drivers') diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 86b2f7474670..05442df0e99c 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -968,6 +968,9 @@ tb_xdp_schedule_request(struct tb *tb, const struct tb_xdp_header *hdr, */ int tb_register_service_driver(struct tb_service_driver *drv) { + if (!drv->probe) + return -EINVAL; + drv->driver.bus = &tb_bus_type; return driver_register(&drv->driver); } -- cgit v1.2.3 From 3f8de2efbf502346ba138ecc198661515c77ce2b Mon Sep 17 00:00:00 2001 From: "Uwe Kleine-König (The Capable Hub)" Date: Thu, 18 Jun 2026 12:14:52 +0200 Subject: thunderbolt: Drop comma after device id array terminator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The usual style for other device id arrays doesn't have a comma after the initializer. Signed-off-by: Uwe Kleine-König (The Capable Hub) Signed-off-by: Mika Westerberg --- drivers/net/thunderbolt/main.c | 2 +- drivers/thunderbolt/dma_test.c | 2 +- drivers/thunderbolt/stream.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'drivers') diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c index edfcfc41a316..c1003e06a8bd 100644 --- a/drivers/net/thunderbolt/main.c +++ b/drivers/net/thunderbolt/main.c @@ -1455,7 +1455,7 @@ static DEFINE_SIMPLE_DEV_PM_OPS(tbnet_pm_ops, tbnet_suspend, tbnet_resume); static const struct tb_service_id tbnet_ids[] = { { TB_SERVICE("network", 1) }, - { }, + { } }; MODULE_DEVICE_TABLE(tbsvc, tbnet_ids); diff --git a/drivers/thunderbolt/dma_test.c b/drivers/thunderbolt/dma_test.c index 63e6bbf00e12..519c67678b08 100644 --- a/drivers/thunderbolt/dma_test.c +++ b/drivers/thunderbolt/dma_test.c @@ -689,7 +689,7 @@ static const struct dev_pm_ops dma_test_pm_ops = { static const struct tb_service_id dma_test_ids[] = { { TB_SERVICE("dma_test", 1) }, - { }, + { } }; MODULE_DEVICE_TABLE(tbsvc, dma_test_ids); diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c index b28e4e95b422..68d81958262e 100644 --- a/drivers/thunderbolt/stream.c +++ b/drivers/thunderbolt/stream.c @@ -1630,7 +1630,7 @@ static const struct dev_pm_ops tbstream_pm_ops = { static const struct tb_service_id tbstream_ids[] = { { TB_SERVICE("stream", 1) }, - { }, + { } }; MODULE_DEVICE_TABLE(tbsvc, tbstream_ids); -- cgit v1.2.3 From d49d71ebec8f03eee2514304279a05a4952fadf7 Mon Sep 17 00:00:00 2001 From: Milo Chen Date: Wed, 24 Jun 2026 14:09:09 +0800 Subject: thunderbolt: xdomain: Notify peers after enumeration Service drivers may register local XDomain properties while discovery is still in progress. This can cause the properties changed notification to be sent before the peer is ready to act on it. If the peer has already read the local property block before the service was registered, it may keep using the old property generation and miss the newly registered service. With ThunderboltIP this can leave the network service half-discovered after a warm reboot and the login request eventually times out. Queue another properties changed notification after the XDomain reaches ENUMERATED so the peer can re-read the final local properties. Signed-off-by: Milo Chen Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 1 + 1 file changed, 1 insertion(+) (limited to 'drivers') diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 05442df0e99c..c179bd751fe4 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -1814,6 +1814,7 @@ static void tb_xdomain_state_work(struct work_struct *work) tb_xdomain_failed(xd); } else { xd->state = XDOMAIN_STATE_ENUMERATED; + tb_xdomain_queue_properties_changed(xd); } break; -- cgit v1.2.3