From babaad95670d1871860306beddd7ef1c4f114ffe Mon Sep 17 00:00:00 2001 From: Konrad Dybcio Date: Mon, 13 Apr 2026 11:02:43 +0200 Subject: [PATCH 01/41] thunderbolt: debugfs: Don't stop reading SB registers if just one fails The GEN4 TxFFE register is not part of the USB4 v1.0 specification, so understandably some pre-USB4v2 retimers (like the Parade PS8830) don't seem to implement it. The immediate idea to counter this would be to introduce a version check for that specific register, but on a second thought, the current flow only returns a quiet -EIO if there's any failures, without hinting at what the actual problem is. To take care of both of these issues, simply print an error line for each SB register read that fails and go on with attempting to read the others. Note that this is not quite in-spec behavior ("The SB Register Space registers shall have the structure and fields described in Table 4-17. Registers not listed in Table 4-20 are undefined and shall not be used."), but it's the easiest fix that shouldn't have real-world bad side effects. Fixes: 6d241fa00159 ("thunderbolt: Add sideband register access to debugfs") Signed-off-by: Konrad Dybcio 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 042f6a0d0f7f..d089d2decedd 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -2361,8 +2361,10 @@ static int sb_regs_show(struct tb_port *port, const struct sb_reg *sb_regs, memset(data, 0, sizeof(data)); ret = usb4_port_sb_read(port, target, index, regs->reg, data, regs->size); - if (ret) - return ret; + if (ret) { + seq_printf(s, "0x%02x \n", regs->reg); + continue; + } seq_printf(s, "0x%02x", regs->reg); for (j = 0; j < regs->size; j++) From 7e49bb89df860bb7352974100cd5dd48752bb646 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Sun, 2 Nov 2025 23:58:34 +0200 Subject: [PATCH 02/41] thunderbolt: Avoid reserved fields in path config space for USB4 routers According to USB4 spec, USB4 Connection Manager shall not change value of any fields that are defined as "RsvdZ" or "VD". Specifically fields: Path Credits Allocated, IFC, ISE fields in path config space shall not be written by CM. To handle this, CM shall first read current path config space from the hardware, change only the fields that can be changed, and then write back the path config space. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/path.c | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/drivers/thunderbolt/path.c b/drivers/thunderbolt/path.c index 8713ea0f47c1..0092b2ec7873 100644 --- a/drivers/thunderbolt/path.c +++ b/drivers/thunderbolt/path.c @@ -412,7 +412,8 @@ static int __tb_path_deactivate_hop(struct tb_port *port, int hop_index, * in the USB4 spec so we clear them * only for pre-USB4 adapters. */ - if (!tb_switch_is_usb4(port->sw)) { + if (tb_port_is_null(port) || + !tb_switch_is_usb4(port->sw)) { hop.ingress_fc = 0; hop.ingress_shared_buffer = 0; } @@ -532,15 +533,18 @@ int tb_path_activate(struct tb_path *path) __tb_path_deactivate_hop(path->hops[i].in_port, path->hops[i].in_hop_index, path->clear_fc); - /* dword 0 */ + /* Needed for USB4 routers, read path config space before write */ + res = tb_port_read(path->hops[i].in_port, &hop, TB_CFG_HOPS, + 2 * path->hops[i].in_hop_index, 2); + if (res) + goto err; + hop.next_hop = path->hops[i].next_hop_index; hop.out_port = path->hops[i].out_port->port; - hop.initial_credits = path->hops[i].initial_credits; hop.pmps = path->hops[i].pm_support; hop.unknown1 = 0; hop.enable = 1; - /* dword 1 */ out_mask = (i == path->path_length - 1) ? TB_PATH_DESTINATION : TB_PATH_INTERNAL; in_mask = (i == 0) ? TB_PATH_SOURCE : TB_PATH_INTERNAL; @@ -550,12 +554,21 @@ int tb_path_activate(struct tb_path *path) hop.drop_packages = path->drop_packages; hop.counter = path->hops[i].in_counter_index; hop.counter_enable = path->hops[i].in_counter_index != -1; - hop.ingress_fc = path->ingress_fc_enable & in_mask; hop.egress_fc = path->egress_fc_enable & out_mask; - hop.ingress_shared_buffer = path->ingress_shared_buffer - & in_mask; - hop.egress_shared_buffer = path->egress_shared_buffer - & out_mask; + hop.egress_shared_buffer = path->egress_shared_buffer & out_mask; + /* + * Protocol adapters IFC and ISE bits, and Path Credits + * Allocated are vendor defined in the USB4 spec so we + * program them only for pre-USB4 and lane adapters. + */ + if (tb_port_is_null(path->hops[i].in_port) || + !tb_switch_is_usb4(path->hops[i].in_port->sw)) { + hop.initial_credits = path->hops[i].initial_credits; + hop.ingress_fc = path->ingress_fc_enable & in_mask; + hop.ingress_shared_buffer = + path->ingress_shared_buffer & in_mask; + } + hop.unknown3 = 0; tb_port_dbg(path->hops[i].in_port, "Writing hop %d\n", i); From 7c7345bcde6c611fa8fa13e624207c6005798aa8 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 11 Nov 2025 17:52:01 +0200 Subject: [PATCH 03/41] thunderbolt: Don't disable lane adapter if XDomain lane bonding isn't possible This happens when firmware connection manager is being used. It will deal with disabling the lane 1 adapter after the tunnel has been established and re-enabling it afterwards. For this reason only do this when we know that lane bonding is possible (e.g running software connection manager). Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 754808c43f00..57367e18733a 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -1931,7 +1931,13 @@ static void tb_xdomain_link_exit(struct tb_xdomain *xd) if (tb_port_get_link_generation(down) >= 4) { down->bonded = false; down->dual_link_port->bonded = false; - } else if (xd->link_width > TB_LINK_WIDTH_SINGLE) { + return; + } + + if (!xd->bonding_possible) + return; + + if (xd->link_width > TB_LINK_WIDTH_SINGLE) { /* * Just return port structures back to way they were and * update credits. No need to update userspace because From 2fb199dc64056ada4aa820a68931fed60333c289 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 11 Aug 2023 12:59:09 +0300 Subject: [PATCH 04/41] thunderbolt: Make XDomain lane bonding comply with the USB4 v2 spec The USB4 v2 Inter-Domain spec "unified" the lane bonding flow so that when the other end (with higher UUID) is not yet set the target link width accordingly it is expected to reply with ERROR_NOT_READY. Implement this for Linux. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 65 +++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 57367e18733a..680b2204875a 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -535,29 +535,19 @@ static int tb_xdp_link_state_status_request(struct tb_ctl *ctl, u64 route, } static int tb_xdp_link_state_status_response(struct tb *tb, struct tb_ctl *ctl, - struct tb_xdomain *xd, u8 sequence) + struct tb_xdomain *xd, u8 sequence, + u8 slw, u8 sls, u8 tls, u8 tlw) { struct tb_xdp_link_state_status_response res; - struct tb_port *port = tb_xdomain_downstream_port(xd); - u32 val[2]; - int ret; memset(&res, 0, sizeof(res)); tb_xdp_fill_header(&res.hdr, xd->route, sequence, LINK_STATE_STATUS_RESPONSE, sizeof(res)); - ret = tb_port_read(port, val, TB_CFG_PORT, - port->cap_phy + LANE_ADP_CS_0, ARRAY_SIZE(val)); - if (ret) - return ret; - - res.slw = (val[0] & LANE_ADP_CS_0_SUPPORTED_WIDTH_MASK) >> - LANE_ADP_CS_0_SUPPORTED_WIDTH_SHIFT; - res.sls = (val[0] & LANE_ADP_CS_0_SUPPORTED_SPEED_MASK) >> - LANE_ADP_CS_0_SUPPORTED_SPEED_SHIFT; - res.tls = val[1] & LANE_ADP_CS_1_TARGET_SPEED_MASK; - res.tlw = (val[1] & LANE_ADP_CS_1_TARGET_WIDTH_MASK) >> - LANE_ADP_CS_1_TARGET_WIDTH_SHIFT; + res.slw = slw; + res.sls = sls; + res.tls = tls; + res.tlw = tlw; return __tb_xdomain_response(ctl, &res, sizeof(res), TB_CFG_PKG_XDOMAIN_RESP); @@ -804,8 +794,47 @@ static void tb_xdp_handle_request(struct work_struct *work) route); if (xd) { - ret = tb_xdp_link_state_status_response(tb, ctl, xd, - sequence); + struct tb_port *port = tb_xdomain_downstream_port(xd); + u8 slw, sls, tls, tlw; + u32 val[2]; + + /* + * Read the adapter supported and target widths + * and speeds. + */ + ret = tb_port_read(port, val, TB_CFG_PORT, + port->cap_phy + LANE_ADP_CS_0, + ARRAY_SIZE(val)); + if (ret) + break; + + slw = (val[0] & LANE_ADP_CS_0_SUPPORTED_WIDTH_MASK) >> + LANE_ADP_CS_0_SUPPORTED_WIDTH_SHIFT; + sls = (val[0] & LANE_ADP_CS_0_SUPPORTED_SPEED_MASK) >> + LANE_ADP_CS_0_SUPPORTED_SPEED_SHIFT; + tls = val[1] & LANE_ADP_CS_1_TARGET_SPEED_MASK; + tlw = (val[1] & LANE_ADP_CS_1_TARGET_WIDTH_MASK) >> + LANE_ADP_CS_1_TARGET_WIDTH_SHIFT; + + /* + * When we have higher UUID, we are supposed to + * return ERROR_NOT_READY if the tlw is not yet + * set according to the Inter-Domain spec for + * USB4 v2. + */ + if (xd->state == XDOMAIN_STATE_BONDING_UUID_HIGH && + xd->target_link_width && + xd->target_link_width != tlw) { + tb_dbg(tb, "%llx: target link width not yet set %#x != %#x\n", + route, tlw, xd->target_link_width); + tb_xdp_error_response(ctl, route, sequence, + ERROR_NOT_READY); + } else { + tb_dbg(tb, "%llx: replying with target link width set to %#x\n", + route, tlw); + ret = tb_xdp_link_state_status_response(tb, ctl, + xd, sequence, slw, sls, tls, tlw); + } } else { tb_xdp_error_response(ctl, route, sequence, ERROR_NOT_READY); From 138ec65b2c761f065b19d115aed2b8246fc272f5 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 21 Nov 2025 08:47:23 +0200 Subject: [PATCH 05/41] thunderbolt: Keep the domain reference while processing hotplug We process hotplug events in a workqueue that may run after the domain has been removed by tb_domain_remove(). For example if user unloads the driver while at the same time plugging a device router we may have scheduled tb_handle_hotplug() to run. Avoid possible UAF in this case by taking the domain reference before scheduling the hotplug handler in tb_queue_hotplug(). Signed-off-by: Mika Westerberg --- drivers/thunderbolt/tb.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index c69c323e6952..34b7d18cce56 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -98,7 +98,7 @@ static void tb_queue_hotplug(struct tb *tb, u64 route, u8 port, bool unplug) if (!ev) return; - ev->tb = tb; + ev->tb = tb_domain_get(tb); ev->route = route; ev->port = port; ev->unplug = unplug; @@ -2527,6 +2527,9 @@ static void tb_handle_hotplug(struct work_struct *work) pm_runtime_mark_last_busy(&tb->dev); pm_runtime_put_autosuspend(&tb->dev); + /* Undo the refcount increased in tb_queue_hotplug() */ + tb_domain_put(tb); + kfree(ev); } From 4c63f29872cb444b33665348bbd2f45cab06afcd Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Wed, 19 Nov 2025 13:15:58 +0200 Subject: [PATCH 06/41] thunderbolt: Release request if tb_cfg_request() fails in __tb_xdomain_response() If tb_cfg_request() fails setting up the request (for example the control channel is shut down already) it returns an error without calling the callback. To avoid leaking that memory, call tb_cfg_request_put() if tb_cfg_request() fails. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 680b2204875a..4fe19cf6387d 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -136,6 +136,7 @@ static int __tb_xdomain_response(struct tb_ctl *ctl, const void *response, size_t size, enum tb_cfg_pkg_type type) { struct tb_cfg_request *req; + int ret; req = tb_cfg_request_alloc(); if (!req) @@ -147,7 +148,11 @@ static int __tb_xdomain_response(struct tb_ctl *ctl, const void *response, req->request_size = size; req->request_type = type; - return tb_cfg_request(ctl, req, response_ready, req); + ret = tb_cfg_request(ctl, req, response_ready, req); + if (ret) + tb_cfg_request_put(req); + + return ret; } /** From e56249d8a68e712f3b60e1f3fdbb5b4fea146468 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Wed, 19 Nov 2025 12:49:30 +0200 Subject: [PATCH 07/41] thunderbolt: Set tb->root_switch to NULL when domain is stopped Similarly what we do with the firmware connection manager. This makes tb_xdp_handle_request() return error to the remote host. However, we need to make sure we keep the uuid alive so that we can reply until the whole domain is released. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/tb.c | 1 + drivers/thunderbolt/xdomain.c | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index 34b7d18cce56..677877baae63 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -2952,6 +2952,7 @@ static void tb_stop(struct tb *tb) tb_tunnel_put(tunnel); } 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/xdomain.c b/drivers/thunderbolt/xdomain.c index 4fe19cf6387d..a1887a15a284 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -740,7 +740,7 @@ static void tb_xdp_handle_request(struct work_struct *work) mutex_lock(&tb->lock); if (tb->root_switch) - uuid = tb->root_switch->uuid; + uuid = kmemdup(tb->root_switch->uuid, sizeof(*uuid), GFP_KERNEL); else uuid = NULL; mutex_unlock(&tb->lock); @@ -880,6 +880,7 @@ static void tb_xdp_handle_request(struct work_struct *work) } out: + kfree(uuid); kfree(xw->pkg); kfree(xw); @@ -2348,6 +2349,9 @@ static struct tb_xdomain *switch_find_xdomain(struct tb_switch *sw, { struct tb_port *port; + if (!sw) + return NULL; + tb_switch_for_each_port(sw, port) { struct tb_xdomain *xd; From f5cc545f59699549adbaa4084149f8247865a51d Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Wed, 19 Nov 2025 12:53:58 +0200 Subject: [PATCH 08/41] thunderbolt: Wait for tb_domain_release() to complete when driver is removed We should not call nhi_shutdown() before the domain structure and the control channel rings are completely released. Otherwise we might release resources like the nhi->msix_ida that are still referenced in tb_domain_release(). For this reason wait for the tb_domain_release() to be completed before continuing to nhi_shutdown() and eventually releasing of the rest of the data structures. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/domain.c | 3 +++ drivers/thunderbolt/nhi.c | 4 ++++ include/linux/thunderbolt.h | 2 ++ 3 files changed, 9 insertions(+) diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c index 317780b99992..df4d7dd45adf 100644 --- a/drivers/thunderbolt/domain.c +++ b/drivers/thunderbolt/domain.c @@ -319,12 +319,15 @@ const struct bus_type tb_bus_type = { static void tb_domain_release(struct device *dev) { struct tb *tb = container_of(dev, struct tb, dev); + struct tb_nhi *nhi = tb->nhi; tb_ctl_free(tb->ctl); destroy_workqueue(tb->wq); ida_free(&tb_domain_ida, tb->index); mutex_destroy(&tb->lock); kfree(tb); + + complete(&nhi->domain_released); } const struct device_type tb_domain_type = { diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 2bb2e79ca3cb..1a2051673067 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -1401,6 +1401,8 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) dev_dbg(dev, "NHI initialized, starting thunderbolt\n"); + init_completion(&nhi->domain_released); + res = tb_domain_add(tb, host_reset); if (res) { /* @@ -1408,6 +1410,7 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) * activated. Do a proper shutdown. */ tb_domain_put(tb); + wait_for_completion(&nhi->domain_released); nhi_shutdown(nhi); return res; } @@ -1433,6 +1436,7 @@ static void nhi_remove(struct pci_dev *pdev) pm_runtime_forbid(&pdev->dev); tb_domain_remove(tb); + wait_for_completion(&nhi->domain_released); nhi_shutdown(nhi); } diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 0ba112175bb3..a5ef7100a6d3 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -493,6 +493,7 @@ static inline struct tb_xdomain *tb_service_parent(struct tb_service *svc) * MSI-X is used. * @hop_count: Number of rings (end point hops) supported by NHI. * @quirks: NHI specific quirks if any + * @domain_released: Completed when domain has been fully released */ struct tb_nhi { spinlock_t lock; @@ -507,6 +508,7 @@ struct tb_nhi { struct work_struct interrupt_work; u32 hop_count; unsigned long quirks; + struct completion domain_released; }; /** From 8b4060998637f06975fceee9b73845d8672d411e Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 8 Sep 2025 13:31:29 +0300 Subject: [PATCH 09/41] thunderbolt: Keep XDomain reference during the lifetime of a service This is needed because we release the service ID in tb_service_release() and the ID array is owned by the parent XDomain. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index a1887a15a284..0e97d0ad7733 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -1039,6 +1039,7 @@ static void tb_service_release(struct device *dev) ida_free(&xd->service_ids, svc->id); kfree(svc->key); kfree(svc); + tb_xdomain_put(xd); } const struct device_type tb_service_type = { @@ -1147,7 +1148,7 @@ static void enumerate_services(struct tb_xdomain *xd) svc->id = id; svc->dev.bus = &tb_bus_type; svc->dev.type = &tb_service_type; - svc->dev.parent = &xd->dev; + svc->dev.parent = get_device(&xd->dev); dev_set_name(&svc->dev, "%s.%d", dev_name(&xd->dev), svc->id); tb_service_debugfs_init(svc); From 7f35f42395fc58f75f51c9b3aeba00787f10371a Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 24 Apr 2026 13:04:26 +0300 Subject: [PATCH 10/41] thunderbolt: dma_test: No need to store debugfs directory pointer We don't actually need to store the debugfs directory pointer inside struct dma_test. Instead we can use the debugfs_lookup_and_remove() which also handles the case if the debugfs directory is already removed by the core driver (for example when cable is disconnected). Signed-off-by: Mika Westerberg --- drivers/thunderbolt/dma_test.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/drivers/thunderbolt/dma_test.c b/drivers/thunderbolt/dma_test.c index b4aa79d482a0..af1e6bc9c7cd 100644 --- a/drivers/thunderbolt/dma_test.c +++ b/drivers/thunderbolt/dma_test.c @@ -87,7 +87,6 @@ static const char * const dma_test_result_names[] = { * @error_code: Error code of the last run * @complete: Used to wait for the Rx to complete * @lock: Lock serializing access to this structure - * @debugfs_dir: dentry of this dma_test */ struct dma_test { const struct tb_service *svc; @@ -108,7 +107,6 @@ struct dma_test { enum dma_test_test_error error_code; struct completion complete; struct mutex lock; - struct dentry *debugfs_dir; }; /* DMA test property directory UUID: 3188cd10-6523-4a5a-a682-fdca07a248d8 */ @@ -619,18 +617,18 @@ DEFINE_SHOW_ATTRIBUTE(status); static void dma_test_debugfs_init(struct tb_service *svc) { - struct dma_test *dt = tb_service_get_drvdata(svc); + struct dentry *debugfs_dir; - dt->debugfs_dir = debugfs_create_dir("dma_test", svc->debugfs_dir); + debugfs_dir = debugfs_create_dir("dma_test", svc->debugfs_dir); - debugfs_create_file("lanes", 0600, dt->debugfs_dir, svc, &lanes_fops); - debugfs_create_file("speed", 0600, dt->debugfs_dir, svc, &speed_fops); - debugfs_create_file("packets_to_receive", 0600, dt->debugfs_dir, svc, + debugfs_create_file("lanes", 0600, debugfs_dir, svc, &lanes_fops); + debugfs_create_file("speed", 0600, debugfs_dir, svc, &speed_fops); + debugfs_create_file("packets_to_receive", 0600, debugfs_dir, svc, &packets_to_receive_fops); - debugfs_create_file("packets_to_send", 0600, dt->debugfs_dir, svc, + debugfs_create_file("packets_to_send", 0600, debugfs_dir, svc, &packets_to_send_fops); - debugfs_create_file("status", 0400, dt->debugfs_dir, svc, &status_fops); - debugfs_create_file("test", 0200, dt->debugfs_dir, svc, &test_fops); + debugfs_create_file("status", 0400, debugfs_dir, svc, &status_fops); + 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) @@ -658,7 +656,7 @@ static void dma_test_remove(struct tb_service *svc) struct dma_test *dt = tb_service_get_drvdata(svc); mutex_lock(&dt->lock); - debugfs_remove_recursive(dt->debugfs_dir); + debugfs_lookup_and_remove("dma_test", svc->debugfs_dir); mutex_unlock(&dt->lock); } From 4d5fc3f4068568dfcb8cbe2852b4adc56394aa26 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 8 Sep 2025 13:37:36 +0300 Subject: [PATCH 11/41] thunderbolt: Remove service debugfs entries during unregister We add them as part of the register path so to keep it symmetric remove them as part of the unregister path. This also removes them even if the service itself is not yet released (but is unregistered), thus allowing new register with the same service name to happen. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 0e97d0ad7733..76e1902d18f3 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -1035,7 +1035,6 @@ static void tb_service_release(struct device *dev) struct tb_service *svc = container_of(dev, struct tb_service, dev); struct tb_xdomain *xd = tb_service_parent(svc); - tb_service_debugfs_remove(svc); ida_free(&xd->service_ids, svc->id); kfree(svc->key); kfree(svc); @@ -1050,6 +1049,14 @@ const struct device_type tb_service_type = { }; EXPORT_SYMBOL_GPL(tb_service_type); +static void __unregister_service(struct device *dev) +{ + struct tb_service *svc = tb_to_service(dev); + + tb_service_debugfs_remove(svc); + device_unregister(&svc->dev); +} + static int remove_missing_service(struct device *dev, void *data) { struct tb_xdomain *xd = data; @@ -1061,7 +1068,7 @@ static int remove_missing_service(struct device *dev, void *data) if (!tb_property_find(xd->remote_properties, svc->key, TB_PROPERTY_TYPE_DIRECTORY)) - device_unregister(dev); + __unregister_service(dev); return 0; } @@ -1154,6 +1161,7 @@ static void enumerate_services(struct tb_xdomain *xd) tb_service_debugfs_init(svc); if (device_register(&svc->dev)) { + tb_service_debugfs_remove(svc); put_device(&svc->dev); break; } @@ -2092,7 +2100,7 @@ void tb_xdomain_add(struct tb_xdomain *xd) static int unregister_service(struct device *dev, void *data) { - device_unregister(dev); + __unregister_service(dev); return 0; } From a8937f35cf39c39c64325aa84d0463d866850857 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Thu, 6 Nov 2025 17:59:52 +0200 Subject: [PATCH 12/41] thunderbolt: Remove XDomain from the bus without holding tb->lock Currently we call device_unregister() for services and the XDomain itself with tb->lock held. This prevents the service drivers from calling any functions that may take it. For this reason separate removing the XDomain from the topology data structures (where we need the lock) from unregistering the device from the bus (where remove callbacks of the drivers are being called). Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 2 ++ drivers/thunderbolt/domain.c | 30 ++++++++++++++++++ drivers/thunderbolt/icm.c | 5 +++ drivers/thunderbolt/switch.c | 14 +++++++++ drivers/thunderbolt/tb.c | 59 +++++++++++++++++------------------ drivers/thunderbolt/tb.h | 2 ++ drivers/thunderbolt/xdomain.c | 57 ++++++++++++++++++++------------- 7 files changed, 117 insertions(+), 52 deletions(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index d089d2decedd..569163a26afa 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -1780,6 +1780,8 @@ static void margining_port_remove(struct tb_port *port) if (!port->usb4) return; + if (!port->usb4->margining) + return; snprintf(dir_name, sizeof(dir_name), "port%d", port->port); parent = debugfs_lookup(dir_name, port->sw->debugfs_dir); diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c index df4d7dd45adf..d83719a37b4c 100644 --- a/drivers/thunderbolt/domain.c +++ b/drivers/thunderbolt/domain.c @@ -853,6 +853,36 @@ int tb_domain_disconnect_all_paths(struct tb *tb) return bus_for_each_dev(&tb_bus_type, NULL, tb, disconnect_xdomain); } +struct unregister_context { + const struct tb *tb; + int n; +}; + +static int unregister_unplugged_xdomain(struct device *dev, void *data) +{ + struct unregister_context *ctx = data; + struct tb_xdomain *xd; + + xd = tb_to_xdomain(dev); + if (xd && xd->tb == ctx->tb && xd->is_unplugged) { + tb_xdomain_unregister(xd); + ctx->n++; + } + return 0; +} + +int tb_domain_unregister_unplugged_xdomains(struct tb *tb) +{ + struct unregister_context ctx; + + ctx.tb = tb_domain_get(tb); + ctx.n = 0; + bus_for_each_dev(&tb_bus_type, NULL, &ctx, unregister_unplugged_xdomain); + tb_domain_put(tb); + + return ctx.n; +} + int tb_domain_init(void) { int ret; diff --git a/drivers/thunderbolt/icm.c b/drivers/thunderbolt/icm.c index 9d95bf3ab44c..2f93a7bccad5 100644 --- a/drivers/thunderbolt/icm.c +++ b/drivers/thunderbolt/icm.c @@ -738,6 +738,7 @@ static void remove_xdomain(struct tb_xdomain *xd) sw = tb_to_switch(xd->dev.parent); tb_port_at(xd->route, sw)->xdomain = NULL; + xd->is_unplugged = true; tb_xdomain_remove(xd); } @@ -1762,6 +1763,8 @@ static void icm_handle_notification(struct work_struct *work) kfree(n->pkg); kfree(n); + + tb_domain_unregister_unplugged_xdomains(tb); } static void icm_handle_event(struct tb *tb, enum tb_cfg_pkg_type type, @@ -2112,6 +2115,8 @@ static void icm_rescan_work(struct work_struct *work) if (tb->root_switch) icm_free_unplugged_children(tb->root_switch); mutex_unlock(&tb->lock); + + tb_domain_unregister_unplugged_xdomains(tb); } static void icm_complete(struct tb *tb) diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index c2ad58b19e7b..bfcab98faf4b 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -3625,6 +3625,20 @@ int tb_switch_resume(struct tb_switch *sw, bool runtime) tb_port_warn(port, "lost during suspend, disconnecting\n"); tb_sw_set_unplugged(port->remote->sw); + } else if (port->xdomain) { + /* + * If the user replaced the XDomain with + * another router, this will succeed in + * which case we must remove the XDomain + * before adding the new router. + */ + err = tb_cfg_get_upstream_port(sw->tb->ctl, + port->xdomain->route); + if (err > 0) { + tb_port_warn(port, + "XDomain was disconnected\n"); + port->xdomain->is_unplugged = true; + } } } } diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index 677877baae63..a9d26a2ec259 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -2524,6 +2524,8 @@ static void tb_handle_hotplug(struct work_struct *work) out: mutex_unlock(&tb->lock); + tb_domain_unregister_unplugged_xdomains(tb); + pm_runtime_mark_last_busy(&tb->dev); pm_runtime_put_autosuspend(&tb->dev); @@ -3114,6 +3116,24 @@ static void tb_restore_children(struct tb_switch *sw) } } +static void tb_free_unplugged_xdomains(struct tb_switch *sw) +{ + struct tb_port *port; + + tb_switch_for_each_port(sw, port) { + if (tb_is_upstream_port(port)) + continue; + if (port->xdomain && port->xdomain->is_unplugged) { + tb_retimer_remove_all(port); + tb_xdomain_remove(port->xdomain); + tb_port_unconfigure_xdomain(port); + port->xdomain = NULL; + } else if (port->remote) { + tb_free_unplugged_xdomains(port->remote->sw); + } + } +} + static int tb_resume_noirq(struct tb *tb) { struct tb_cm *tcm = tb_priv(tb); @@ -3133,6 +3153,7 @@ static int tb_resume_noirq(struct tb *tb) tb_switch_resume(tb->root_switch, false); tb_free_invalid_tunnels(tb); tb_free_unplugged_children(tb->root_switch); + tb_free_unplugged_xdomains(tb->root_switch); tb_restore_children(tb->root_switch); /* @@ -3175,28 +3196,6 @@ static int tb_resume_noirq(struct tb *tb) return 0; } -static int tb_free_unplugged_xdomains(struct tb_switch *sw) -{ - struct tb_port *port; - int ret = 0; - - tb_switch_for_each_port(sw, port) { - if (tb_is_upstream_port(port)) - continue; - if (port->xdomain && port->xdomain->is_unplugged) { - tb_retimer_remove_all(port); - tb_xdomain_remove(port->xdomain); - tb_port_unconfigure_xdomain(port); - port->xdomain = NULL; - ret++; - } else if (port->remote) { - ret += tb_free_unplugged_xdomains(port->remote->sw); - } - } - - return ret; -} - static int tb_freeze_noirq(struct tb *tb) { struct tb_cm *tcm = tb_priv(tb); @@ -3216,14 +3215,14 @@ static int tb_thaw_noirq(struct tb *tb) static void tb_complete(struct tb *tb) { /* - * Release any unplugged XDomains and if there is a case where + * Unregister unplugged XDomains and if there is a case where * another domain is swapped in place of unplugged XDomain we * need to run another rescan. */ - mutex_lock(&tb->lock); - if (tb_free_unplugged_xdomains(tb->root_switch)) - tb_scan_switch(tb->root_switch); - mutex_unlock(&tb->lock); + if (tb_domain_unregister_unplugged_xdomains(tb)) { + scoped_guard(mutex, &tb->lock) + tb_scan_switch(tb->root_switch); + } } static int tb_runtime_suspend(struct tb *tb) @@ -3250,11 +3249,11 @@ static void tb_remove_work(struct work_struct *work) struct tb *tb = tcm_to_tb(tcm); mutex_lock(&tb->lock); - if (tb->root_switch) { + if (tb->root_switch) tb_free_unplugged_children(tb->root_switch); - tb_free_unplugged_xdomains(tb->root_switch); - } mutex_unlock(&tb->lock); + + tb_free_unplugged_xdomains(tb->root_switch); } static int tb_runtime_resume(struct tb *tb) diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index 217c3114bec8..229b9e7961fb 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -793,6 +793,7 @@ int tb_domain_disconnect_xdomain_paths(struct tb *tb, struct tb_xdomain *xd, int transmit_path, int transmit_ring, int receive_path, int receive_ring); int tb_domain_disconnect_all_paths(struct tb *tb); +int tb_domain_unregister_unplugged_xdomains(struct tb *tb); static inline struct tb *tb_domain_get(struct tb *tb) { @@ -1263,6 +1264,7 @@ struct tb_xdomain *tb_xdomain_alloc(struct tb *tb, struct device *parent, const uuid_t *remote_uuid); void tb_xdomain_add(struct tb_xdomain *xd); void tb_xdomain_remove(struct tb_xdomain *xd); +void tb_xdomain_unregister(struct tb_xdomain *xd); struct tb_xdomain *tb_xdomain_find_by_link_depth(struct tb *tb, u8 link, u8 depth); diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 76e1902d18f3..9a30fe36c4be 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -2105,38 +2105,51 @@ static int unregister_service(struct device *dev, void *data) } /** - * tb_xdomain_remove() - Remove XDomain from the bus + * tb_xdomain_remove() - Remove XDomain * @xd: XDomain to remove * - * This will stop all ongoing configuration work and remove the XDomain - * along with any services from the bus. When the last reference to @xd - * is released the object will be released as well. + * This will stop all ongoing configuration work. XDomain is not removed + * from the bus if it was added. That needs to be done separately by + * calling tb_xdomain_unregister(). + * + * Called with @tb->lock held. */ void tb_xdomain_remove(struct tb_xdomain *xd) { tb_xdomain_debugfs_remove(xd); - stop_handshake(xd); + tb_xdomain_link_exit(xd); + + if (!device_is_registered(&xd->dev)) { + /* + * Undo runtime PM here explicitly because it is + * possible that the XDomain was never added to the bus + * and thus device_del() is not called for it + * (device_del() would handle this otherwise). + */ + pm_runtime_disable(&xd->dev); + pm_runtime_put_noidle(&xd->dev); + pm_runtime_set_suspended(&xd->dev); + put_device(&xd->dev); + } +} + +/** + * tb_xdomain_unregister() - Unregister XDomain + * @xd: XDomain to unregister + * + * This will unregister the XDomain along with any services from the + * bus. When the last reference to @xd is released the object will be + * released as well. + */ +void tb_xdomain_unregister(struct tb_xdomain *xd) +{ + lockdep_assert_not_held(&xd->tb->lock); device_for_each_child_reverse(&xd->dev, xd, unregister_service); - tb_xdomain_link_exit(xd); - - /* - * Undo runtime PM here explicitly because it is possible that - * the XDomain was never added to the bus and thus device_del() - * is not called for it (device_del() would handle this otherwise). - */ - pm_runtime_disable(&xd->dev); - pm_runtime_put_noidle(&xd->dev); - pm_runtime_set_suspended(&xd->dev); - - if (!device_is_registered(&xd->dev)) { - put_device(&xd->dev); - } else { - dev_info(&xd->dev, "host disconnected\n"); - device_unregister(&xd->dev); - } + dev_info(&xd->dev, "host disconnected\n"); + device_unregister(&xd->dev); } /** From cf0c38ee554c3e9062408cc3a38325483d52ecd0 Mon Sep 17 00:00:00 2001 From: Alan Borzeszkowski Date: Thu, 2 Oct 2025 15:37:22 +0300 Subject: [PATCH 13/41] thunderbolt: Don't create multiple DMA tunnels on firmware connection manager Firmware connection manager supports only one DMA tunnel per XDomain connection. Firmware prior Intel Titan Ridge failed the operation directly but the same does not happen anymore on Titan Ridge and forward. For this reason add an explicit check, and fail the operation accordingly in the driver. Signed-off-by: Alan Borzeszkowski Signed-off-by: Mika Westerberg --- drivers/thunderbolt/icm.c | 10 ++++++++++ drivers/thunderbolt/xdomain.c | 25 +++++++++++++++++++------ include/linux/thunderbolt.h | 2 ++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/drivers/thunderbolt/icm.c b/drivers/thunderbolt/icm.c index 2f93a7bccad5..c492995166f7 100644 --- a/drivers/thunderbolt/icm.c +++ b/drivers/thunderbolt/icm.c @@ -587,6 +587,11 @@ static int icm_fr_approve_xdomain_paths(struct tb *tb, struct tb_xdomain *xd, struct icm_fr_pkg_approve_xdomain request; int ret; + if (atomic_read(&xd->ntunnels) >= 1) { + tb_warn(tb, "only one tunnel is supported by the firmware\n"); + return -EOPNOTSUPP; + } + memset(&request, 0, sizeof(request)); request.hdr.code = ICM_APPROVE_XDOMAIN; request.link_info = xd->depth << ICM_LINK_INFO_DEPTH_SHIFT | xd->link; @@ -1158,6 +1163,11 @@ static int icm_tr_approve_xdomain_paths(struct tb *tb, struct tb_xdomain *xd, struct icm_tr_pkg_approve_xdomain request; int ret; + if (atomic_read(&xd->ntunnels) >= 1) { + tb_warn(tb, "only one tunnel is supported by the firmware\n"); + return -EOPNOTSUPP; + } + memset(&request, 0, sizeof(request)); request.hdr.code = ICM_APPROVE_XDOMAIN; request.route_hi = upper_32_bits(xd->route); diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 9a30fe36c4be..6e83f93eee83 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -2038,6 +2038,7 @@ struct tb_xdomain *tb_xdomain_alloc(struct tb *tb, struct device *parent, INIT_DELAYED_WORK(&xd->state_work, tb_xdomain_state_work); INIT_DELAYED_WORK(&xd->properties_changed_work, tb_xdomain_properties_changed); + atomic_set(&xd->ntunnels, 0); xd->local_uuid = kmemdup(local_uuid, sizeof(uuid_t), GFP_KERNEL); if (!xd->local_uuid) @@ -2328,9 +2329,15 @@ int tb_xdomain_enable_paths(struct tb_xdomain *xd, int transmit_path, int transmit_ring, int receive_path, int receive_ring) { - return tb_domain_approve_xdomain_paths(xd->tb, xd, transmit_path, - transmit_ring, receive_path, - receive_ring); + int ret; + + ret = tb_domain_approve_xdomain_paths(xd->tb, xd, transmit_path, + transmit_ring, receive_path, + receive_ring); + if (ret) + return ret; + atomic_inc(&xd->ntunnels); + return 0; } EXPORT_SYMBOL_GPL(tb_xdomain_enable_paths); @@ -2353,9 +2360,15 @@ int tb_xdomain_disable_paths(struct tb_xdomain *xd, int transmit_path, int transmit_ring, int receive_path, int receive_ring) { - return tb_domain_disconnect_xdomain_paths(xd->tb, xd, transmit_path, - transmit_ring, receive_path, - receive_ring); + int ret; + + ret = tb_domain_disconnect_xdomain_paths(xd->tb, xd, transmit_path, + transmit_ring, receive_path, + receive_ring); + if (ret) + return ret; + atomic_dec(&xd->ntunnels); + return 0; } EXPORT_SYMBOL_GPL(tb_xdomain_disable_paths); diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index a5ef7100a6d3..bbdbbc84c999 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -228,6 +228,7 @@ enum tb_link_width { * changed notification * @bonding_possible: True if lane bonding is possible on local side * @target_link_width: Target link width from the remote host + * @ntunnels: Keeps track of how many tunnels go through this XDomain * @link: Root switch link the remote domain is connected (ICM only) * @depth: Depth in the chain the remote domain is connected (ICM only) * @@ -273,6 +274,7 @@ struct tb_xdomain { int properties_changed_retries; bool bonding_possible; u8 target_link_width; + atomic_t ntunnels; u8 link; u8 depth; }; From c12b5ee30fb665133b3119283cfe7ce9474e3589 Mon Sep 17 00:00:00 2001 From: Michael Bommarito Date: Sun, 10 May 2026 19:16:59 -0400 Subject: [PATCH 14/41] thunderbolt: test: add KUnit regression tests for XDomain property parser Add three KUnit cases that exercise the defects fixed by the sibling commits in this series by feeding crafted XDomain property blocks to tb_property_parse_dir(): tb_test_property_parse_u32_wrap - entry->value = 0xffffff00 and entry->length = 0x100 so their u32 sum 0x100000000 wraps to 0 under the block_len guard; without the fix the subsequent parse_dwdata() reads attacker-directed OOB memory. tb_test_property_parse_recursion - two DIRECTORY entries pointing at each other, driving __tb_property_parse_dir() recursion; without the fix the kernel stack is exhausted. tb_test_property_parse_dir_len_underflow - a DIRECTORY entry with length < 4 placed near the end of the block so the non-root UUID kmemdup of 4 dwords from dir_offset reads OOB before the later content_len = dir_len - 4 underflow path is reached. Each test asserts tb_property_parse_dir() returns NULL on the crafted input. With CONFIG_KASAN=y, running these on the pre-fix kernel produces an oops inside __tb_property_parse_dir or its callees: u32_wrap takes a page fault on the KASAN shadow lookup for the wild ~16 GiB OOB offset; recursion trips a KASAN out-of-bounds report in __unwind_start as the per-task kernel stack is consumed; dir_len_underflow trips a KASAN slab-out-of-bounds report in kmemdup_noprof reading 16 bytes past the 28-byte block. Post-fix they pass cleanly. The crafted blocks are populated by writing u32 dwords directly, matching the existing root_directory[] style used elsewhere in this file rather than imposing a private struct overlay. Run with: ./tools/testing/kunit/kunit.py run --arch=x86_64 \ --kconfig_add CONFIG_PCI=y --kconfig_add CONFIG_NVMEM=y \ --kconfig_add CONFIG_USB4=y --kconfig_add CONFIG_USB4_KUNIT_TEST=y \ --kconfig_add CONFIG_KASAN=y 'thunderbolt.tb_test_property_parse_*' Assisted-by: Claude:claude-opus-4-7 Signed-off-by: Michael Bommarito Signed-off-by: Mika Westerberg --- drivers/thunderbolt/test.c | 126 +++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/drivers/thunderbolt/test.c b/drivers/thunderbolt/test.c index 1f4318249c22..f41fabf15456 100644 --- a/drivers/thunderbolt/test.c +++ b/drivers/thunderbolt/test.c @@ -2852,7 +2852,133 @@ static void tb_test_property_copy(struct kunit *test) tb_property_free_dir(src); } +/* + * Reproducers for three memory-safety defects in + * drivers/thunderbolt/property.c reached from a crafted XDomain + * PROPERTIES_RESPONSE payload. Without the fix these trip KASAN or + * smash the kernel stack; with the fix each returns NULL cleanly. + * + * The on-wire entry layout matches struct tb_property_entry in + * property.c (private to that translation unit): u32 key_hi, u32 + * key_lo, then a packed u32 = (type << 24) | (reserved << 16) | + * length, then u32 value. Each entry is 4 dwords. + */ + +static void tb_test_property_parse_u32_wrap(struct kunit *test) +{ + /* + * 0x102 dwords: enough for the entry's length field (0x100) to + * pass the "entry->length > block_len" gate so the wrap check + * is actually exercised. parse_dwdata's downstream OOB read + * lands ~16 GiB past the allocation regardless. + */ + u32 *block = kunit_kzalloc(test, 0x102 * sizeof(u32), GFP_KERNEL); + struct tb_property_dir *dir; + + KUNIT_ASSERT_NOT_NULL(test, block); + + block[0] = 0x55584401; /* "UXD" v1 magic */ + block[1] = 0x00000004; /* Root directory length: one entry */ + + /* + * DATA entry whose value 0xffffff00 + length 0x100 wrap to 0 + * in u32, passing the sum <= block_len guard even though the + * real offset is far past the allocation. + */ + block[2] = 0x61616161; /* key_hi */ + block[3] = 0x61616161; /* key_lo */ + block[4] = 0x64000100; /* type=DATA, reserved=0, length=0x100 */ + block[5] = 0xffffff00; /* value */ + + dir = tb_property_parse_dir(block, 0x102); + KUNIT_EXPECT_NULL(test, dir); + tb_property_free_dir(dir); +} + +static void tb_test_property_parse_recursion(struct kunit *test) +{ + /* + * 10 dwords: rootdir header (2) + parent DIRECTORY entry (4) + + * the child entry that lives at dir_offset(2) + UUID(4) = 6, + * occupying block[6..9]. Each recursive level re-reads the + * same block[6..9] as its first child entry, which is itself + * a DIRECTORY pointing at offset 2. + */ + u32 *block = kunit_kzalloc(test, 10 * sizeof(u32), GFP_KERNEL); + struct tb_property_dir *dir; + + KUNIT_ASSERT_NOT_NULL(test, block); + + block[0] = 0x55584401; /* "UXD" v1 magic */ + block[1] = 0x00000004; /* Root directory length: one entry */ + + /* + * DIRECTORY entry pointing at dir_offset = 2 with length = 8. + * Non-root parse derives content_offset = 6, content_len = 4, + * nentries = 1. block[6..9] is read both as the parent's UUID + * (kmemdup'd into dir->uuid) and as the single child entry -- + * which is itself a DIRECTORY pointing at offset 2, so the + * recursion never terminates and the kernel stack is exhausted. + */ + block[2] = 0x61616161; /* key_hi */ + block[3] = 0x61616161; /* key_lo */ + block[4] = 0x44000008; /* type=DIRECTORY, reserved=0, length=8 */ + block[5] = 0x00000002; /* value = dir_offset */ + + block[6] = 0x62626262; /* doubles as UUID dword 0 / child key_hi */ + block[7] = 0x62626262; /* doubles as UUID dword 1 / child key_lo */ + block[8] = 0x44000008; /* type=DIRECTORY, reserved=0, length=8 */ + block[9] = 0x00000002; /* value = dir_offset (back at parent) */ + + dir = tb_property_parse_dir(block, 10); + KUNIT_EXPECT_NULL(test, dir); + tb_property_free_dir(dir); +} + +static void tb_test_property_parse_dir_len_underflow(struct kunit *test) +{ + /* + * Allocate exactly 7 dwords (28 bytes) so the kmalloc-32 chunk + * leaves a 4-byte slab redzone tail that KASAN-Generic can flag. + * With block_len = 7, dir_offset = 4, dir_len = 3, the non-root + * UUID kmemdup reads 16 bytes from byte 16, so bytes 28..31 fall + * in the redzone and trip a KASAN slab-out-of-bounds report on + * the pre-fix kernel. Sizing the buffer at a power of two (32, + * 64, ...) puts the over-read into the slab cache tail where + * KASAN's generic shadow does not flag it, and the test reduces + * to the downstream content_len = dir_len - 4 underflow path + * which also returns NULL. + */ + u32 *block = kunit_kzalloc(test, 7 * sizeof(u32), GFP_KERNEL); + struct tb_property_dir *dir; + + KUNIT_ASSERT_NOT_NULL(test, block); + + block[0] = 0x55584401; /* "UXD" v1 magic */ + block[1] = 0x00000004; /* Root directory length: one entry */ + + /* + * DIRECTORY entry with length = 3 pointing at dir_offset = 4. + * tb_property_entry_valid() permits value(4) + length(3) <= + * block_len(7). Non-root parse begins with a kmemdup of 4 + * dwords from dir_offset for the UUID; that read runs past the + * 28-byte allocation before the dir_len < 4 reject would fire. + */ + block[2] = 0x61616161; /* key_hi */ + block[3] = 0x61616161; /* key_lo */ + block[4] = 0x44000003; /* type=DIRECTORY, reserved=0, length=3 */ + block[5] = 0x00000004; /* value = dir_offset */ + /* block[6] is the start of the four UUID dwords; block[7..] is OOB. */ + + dir = tb_property_parse_dir(block, 7); + KUNIT_EXPECT_NULL(test, dir); + tb_property_free_dir(dir); +} + static struct kunit_case tb_test_cases[] = { + KUNIT_CASE(tb_test_property_parse_u32_wrap), + KUNIT_CASE(tb_test_property_parse_recursion), + KUNIT_CASE(tb_test_property_parse_dir_len_underflow), KUNIT_CASE(tb_test_path_basic), KUNIT_CASE(tb_test_path_not_connected_walk), KUNIT_CASE(tb_test_path_single_hop_walk), From 7e6445d9d6f7dfc5635fc216488d6f62a5594fe9 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 23 Sep 2025 12:43:51 +0300 Subject: [PATCH 15/41] thunderbolt: Add tb_property_merge_dir() This allows merging one XDomain property directory into another. We are going to use this in the subsequent patch. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/property.c | 164 +++++++++++++++++++++++---------- include/linux/thunderbolt.h | 3 + 2 files changed, 119 insertions(+), 48 deletions(-) diff --git a/drivers/thunderbolt/property.c b/drivers/thunderbolt/property.c index 50cbfc92fe65..6b9666b61181 100644 --- a/drivers/thunderbolt/property.c +++ b/drivers/thunderbolt/property.c @@ -38,6 +38,7 @@ struct tb_property_dir_entry { static struct tb_property_dir *__tb_property_parse_dir(const u32 *block, size_t block_len, unsigned int dir_offset, size_t dir_len, bool is_root); +static struct tb_property *tb_property_copy(const struct tb_property *property); static inline void parse_dwdata(void *dst, const void *src, size_t dwords) { @@ -507,17 +508,9 @@ ssize_t tb_property_format_dir(const struct tb_property_dir *dir, u32 *block, return ret < 0 ? ret : 0; } -/** - * tb_property_copy_dir() - Take a deep copy of directory - * @dir: Directory to copy - * - * The resulting directory needs to be released by calling tb_property_free_dir(). - * - * Return: Pointer to &struct tb_property_dir, %NULL in case of failure. - */ -struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir) +static struct tb_property_dir *copy_dir(const struct tb_property_dir *dir) { - struct tb_property *property, *p = NULL; + struct tb_property *property, *p; struct tb_property_dir *d; if (!dir) @@ -528,56 +521,131 @@ struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir) return NULL; list_for_each_entry(property, &dir->properties, list) { - struct tb_property *p; - - p = tb_property_alloc(property->key, property->type); + p = tb_property_copy(property); if (!p) goto err_free; - - p->length = property->length; - - switch (property->type) { - case TB_PROPERTY_TYPE_DIRECTORY: - p->value.dir = tb_property_copy_dir(property->value.dir); - if (!p->value.dir) - goto err_free; - break; - - case TB_PROPERTY_TYPE_DATA: - p->value.data = kmemdup(property->value.data, - property->length * 4, - GFP_KERNEL); - if (!p->value.data) - goto err_free; - break; - - case TB_PROPERTY_TYPE_TEXT: - p->value.text = kzalloc(p->length * 4, GFP_KERNEL); - if (!p->value.text) - goto err_free; - strcpy(p->value.text, property->value.text); - break; - - case TB_PROPERTY_TYPE_VALUE: - p->value.immediate = property->value.immediate; - break; - - default: - break; - } - list_add_tail(&p->list, &d->properties); } return d; err_free: - kfree(p); tb_property_free_dir(d); - return NULL; } +static struct tb_property *tb_property_copy(const struct tb_property *property) +{ + struct tb_property *p; + + p = tb_property_alloc(property->key, property->type); + if (!p) + return NULL; + + p->length = property->length; + switch (property->type) { + case TB_PROPERTY_TYPE_DIRECTORY: + p->value.dir = copy_dir(property->value.dir); + if (!p->value.dir) + goto err_free; + break; + + case TB_PROPERTY_TYPE_DATA: + p->value.data = kmemdup(property->value.data, + property->length * 4, + GFP_KERNEL); + if (!p->value.data) + goto err_free; + break; + + case TB_PROPERTY_TYPE_TEXT: + p->value.text = kzalloc(p->length * 4, GFP_KERNEL); + if (!p->value.text) + goto err_free; + strcpy(p->value.text, property->value.text); + break; + + case TB_PROPERTY_TYPE_VALUE: + p->value.immediate = property->value.immediate; + break; + + default: + break; + } + + return p; + +err_free: + kfree(p); + return NULL; +} + +/** + * tb_property_copy_dir() - Take a deep copy of directory + * @dir: Directory to copy + * + * The resulting directory needs to be released by calling tb_property_free_dir(). + * + * Return: Pointer to &struct tb_property_dir, %NULL in case of failure. + */ +struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir) +{ + return copy_dir(dir); +} +EXPORT_SYMBOL_GPL(tb_property_copy_dir); + +/** + * tb_property_merge_dir() - Merges directory into parent + * @parent: Directory to merge @dir + * @dir: Directory that is merged + * @replace: Replace existing entries + * + * This will merge @dir into @parent. Both must have same UUID. The + * properties in @dir will overwrite overlapping properties in @parent + * if @replace is %true. Contents of @dir is copied (so if it is not + * needed afterwards it needs to relesed by calling tb_property_free_dir()). + */ +int tb_property_merge_dir(struct tb_property_dir *parent, + const struct tb_property_dir *dir, + bool replace) +{ + const struct tb_property *property; + + if (WARN_ON(parent == dir)) + return -EINVAL; + + if (!uuid_equal(parent->uuid, dir->uuid)) + return -EINVAL; + + list_for_each_entry(property, &dir->properties, list) { + struct tb_property *p, *tmp; + + tmp = tb_property_copy(property); + if (!tmp) + return -ENOMEM; + + p = tb_property_find(parent, property->key, property->type); + if (p) { + if (replace) { + /* + * Found existing property in parent so + * replace with the new one. + */ + list_replace(&p->list, &tmp->list); + tb_property_free(p); + } else { + tb_property_free(tmp); + continue; + } + } else { + list_add_tail(&tmp->list, &parent->properties); + } + } + + return 0; +} +EXPORT_SYMBOL_GPL(tb_property_merge_dir); + /** * tb_property_add_immediate() - Add immediate property to directory * @parent: Directory to add the property diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index bbdbbc84c999..e98d569779f9 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -153,6 +153,9 @@ struct tb_property_dir *tb_property_parse_dir(const u32 *block, ssize_t tb_property_format_dir(const struct tb_property_dir *dir, u32 *block, size_t block_len); struct tb_property_dir *tb_property_copy_dir(const struct tb_property_dir *dir); +int tb_property_merge_dir(struct tb_property_dir *parent, + const struct tb_property_dir *dir, + bool replace); struct tb_property_dir *tb_property_create_dir(const uuid_t *uuid); void tb_property_free_dir(struct tb_property_dir *dir); int tb_property_add_immediate(struct tb_property_dir *parent, const char *key, From aa4999c0297ae56143f67ecb3ef008a473afcc00 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 23 Sep 2025 12:46:11 +0300 Subject: [PATCH 16/41] thunderbolt: Add KUnit test for tb_property_merge_dir() This makes sure it keeps working if we ever need to change it. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/test.c | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/drivers/thunderbolt/test.c b/drivers/thunderbolt/test.c index f41fabf15456..fa8bee0ccf8b 100644 --- a/drivers/thunderbolt/test.c +++ b/drivers/thunderbolt/test.c @@ -2975,6 +2975,87 @@ static void tb_test_property_parse_dir_len_underflow(struct kunit *test) tb_property_free_dir(dir); } +static void tb_test_property_merge(struct kunit *test) +{ + struct tb_property_dir *dir1, *dir2, *dir3; + struct tb_property *p; + uuid_t uuid; + int ret; + + dir1 = tb_property_create_dir(&network_dir_uuid); + KUNIT_ASSERT_NOT_NULL(test, dir1); + ret = tb_property_add_immediate(dir1, "prtcid", 1); + KUNIT_EXPECT_EQ(test, ret, 0); + ret = tb_property_add_immediate(dir1, "prtcvers", 1); + KUNIT_EXPECT_EQ(test, ret, 0); + ret = tb_property_add_immediate(dir1, "prtcrevs", 0); + KUNIT_EXPECT_EQ(test, ret, 0); + ret = tb_property_add_immediate(dir1, "prtcstns", 0); + KUNIT_EXPECT_EQ(test, ret, 0); + + dir2 = tb_property_create_dir(&network_dir_uuid); + KUNIT_ASSERT_NOT_NULL(test, dir2); + ret = tb_property_add_text(dir2, "descr", "This is text"); + KUNIT_EXPECT_EQ(test, ret, 0); + /* This replaces the value in dir1 */ + ret = tb_property_add_immediate(dir2, "prtcvers", 0x1234); + KUNIT_EXPECT_EQ(test, ret, 0); + + uuid_gen(&uuid); + dir3 = tb_property_create_dir(&uuid); + KUNIT_ASSERT_NOT_NULL(test, dir3); + ret = tb_property_add_immediate(dir3, "value0", 0); + KUNIT_EXPECT_EQ(test, ret, 0); + ret = tb_property_add_text(dir3, "value1", "Text value"); + KUNIT_EXPECT_EQ(test, ret, 0); + ret = tb_property_add_dir(dir2, "my", dir3); + KUNIT_EXPECT_EQ(test, ret, 0); + + ret = tb_property_merge_dir(dir1, dir2, true); + KUNIT_EXPECT_EQ(test, ret, 0); + + p = tb_property_get_next(dir1, NULL); + KUNIT_ASSERT_NOT_NULL(test, p); + KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcid"); + KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE); + KUNIT_ASSERT_EQ(test, p->length, 1); + KUNIT_ASSERT_EQ(test, p->value.immediate, 1); + p = tb_property_get_next(dir1, p); + KUNIT_ASSERT_NOT_NULL(test, p); + KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcvers"); + KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE); + KUNIT_ASSERT_EQ(test, p->length, 1); + KUNIT_ASSERT_EQ(test, p->value.immediate, 0x1234); + p = tb_property_get_next(dir1, p); + KUNIT_ASSERT_NOT_NULL(test, p); + KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcrevs"); + KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE); + KUNIT_ASSERT_EQ(test, p->length, 1); + KUNIT_ASSERT_EQ(test, p->value.immediate, 0); + p = tb_property_get_next(dir1, p); + KUNIT_ASSERT_NOT_NULL(test, p); + KUNIT_ASSERT_STREQ(test, &p->key[0], "prtcstns"); + KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_VALUE); + KUNIT_ASSERT_EQ(test, p->length, 1); + KUNIT_ASSERT_EQ(test, p->value.immediate, 0); + p = tb_property_get_next(dir1, p); + KUNIT_ASSERT_NOT_NULL(test, p); + KUNIT_ASSERT_STREQ(test, &p->key[0], "descr"); + KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_TEXT); + KUNIT_ASSERT_EQ(test, p->length, 4); + KUNIT_ASSERT_STREQ(test, p->value.text, "This is text"); + p = tb_property_get_next(dir1, p); + KUNIT_ASSERT_NOT_NULL(test, p); + KUNIT_ASSERT_STREQ(test, &p->key[0], "my"); + KUNIT_ASSERT_EQ(test, p->type, TB_PROPERTY_TYPE_DIRECTORY); + compare_dirs(test, p->value.dir, dir3); + p = tb_property_get_next(dir1, p); + KUNIT_ASSERT_NULL(test, p); + + tb_property_free_dir(dir2); + tb_property_free_dir(dir1); +} + static struct kunit_case tb_test_cases[] = { KUNIT_CASE(tb_test_property_parse_u32_wrap), KUNIT_CASE(tb_test_property_parse_recursion), @@ -3018,6 +3099,7 @@ static struct kunit_case tb_test_cases[] = { KUNIT_CASE(tb_test_property_parse), KUNIT_CASE(tb_test_property_format), KUNIT_CASE(tb_test_property_copy), + KUNIT_CASE(tb_test_property_merge), { } }; From abc27e5bfed2fa281ccb23419ef4d1c9ded86398 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 23 Sep 2025 12:51:27 +0300 Subject: [PATCH 17/41] thunderbolt: Allow service drivers to specify their own properties The XDomain properties can be useful for service drivers, for example to implement a registry for the services they expose. So far there has been no need for service drivers to specify these but with the USB4STREAM driver that we are going to use them. This adds remote and local side properties that the service drivers have access to. Remote side is read-only but the local side can be changed by a service driver. Also provide a mechanism to notify the remote side that there are changes. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 95 ++++++++++++++++++++++++++++++----- include/linux/thunderbolt.h | 12 +++++ 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 6e83f93eee83..781d88d06b93 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -640,6 +640,32 @@ void tb_unregister_protocol_handler(struct tb_protocol_handler *handler) } EXPORT_SYMBOL_GPL(tb_unregister_protocol_handler); +static int update_service_properties(struct device *dev, void *data) +{ + struct tb_property_dir *root = data; + struct tb_service *svc; + struct tb_property *p; + + svc = tb_to_service(dev); + if (!svc) + return 0; + + guard(mutex)(&svc->lock); + + /* + * Replace the static service properties with the dynamic one. + * Typically this is the same but service drivers can add their + * own dynamic properties here too. + */ + p = tb_property_find(root, svc->key, TB_PROPERTY_TYPE_DIRECTORY); + if (!p) + return 0; + if (svc->local_properties) + return tb_property_merge_dir(p->value.dir, + svc->local_properties, false); + return 0; +} + static void update_property_block(struct tb_xdomain *xd) { mutex_lock(&xdomain_lock); @@ -664,6 +690,9 @@ static void update_property_block(struct tb_xdomain *xd) tb_property_add_text(dir, "deviceid", utsname()->nodename); tb_property_add_immediate(dir, "maxhopid", xd->local_max_hopid); + /* Add service specific dynamic properties */ + device_for_each_child(&xd->dev, dir, update_service_properties); + ret = tb_property_format_dir(dir, NULL, 0); if (ret < 0) { dev_warn(&xd->dev, "local property block creation failed\n"); @@ -936,6 +965,40 @@ void tb_unregister_service_driver(struct tb_service_driver *drv) } EXPORT_SYMBOL_GPL(tb_unregister_service_driver); +static int update_xdomain(struct device *dev, void *data) +{ + struct tb_xdomain *xd; + + xd = tb_to_xdomain(dev); + if (xd) { + queue_delayed_work(xd->tb->wq, &xd->properties_changed_work, + msecs_to_jiffies(50)); + } + + return 0; +} + +/** + * tb_service_properties_changed() - Notify the other host about changes + * @svc: Service whose properties changed + * + * Notifies the other host that service properties may have been + * changed. This should be called whenever @svc->local_properties is + * updated. + */ +void tb_service_properties_changed(struct tb_service *svc) +{ + struct tb_xdomain *xd = tb_service_parent(svc); + + if (xd->is_unplugged) + return; + + scoped_guard(mutex, &xdomain_lock) + xdomain_property_block_gen++; + update_xdomain(&xd->dev, NULL); +} +EXPORT_SYMBOL_GPL(tb_service_properties_changed); + static ssize_t key_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -1035,6 +1098,7 @@ static void tb_service_release(struct device *dev) struct tb_service *svc = container_of(dev, struct tb_service, dev); struct tb_xdomain *xd = tb_service_parent(svc); + tb_property_free_dir(svc->remote_properties); ida_free(&xd->service_ids, svc->id); kfree(svc->key); kfree(svc); @@ -1049,6 +1113,16 @@ const struct device_type tb_service_type = { }; EXPORT_SYMBOL_GPL(tb_service_type); +static void update_service(struct tb_service *svc, struct tb_property *property) +{ + struct tb_property_dir *dir = property->value.dir; + + guard(mutex)(&svc->lock); + tb_property_free_dir(svc->remote_properties); + svc->remote_properties = tb_property_copy_dir(dir); + kobject_uevent(&svc->dev.kobj, KOBJ_CHANGE); +} + static void __unregister_service(struct device *dev) { struct tb_service *svc = tb_to_service(dev); @@ -1109,6 +1183,12 @@ static int populate_service(struct tb_service *svc, if (!svc->key) return -ENOMEM; + svc->remote_properties = tb_property_copy_dir(dir); + if (!svc->remote_properties) { + kfree(svc->key); + return -ENOMEM; + } + return 0; } @@ -1133,6 +1213,7 @@ static void enumerate_services(struct tb_xdomain *xd) /* If the service exists already we are fine */ dev = device_find_child(&xd->dev, p, find_service); if (dev) { + update_service(tb_to_service(dev), p); put_device(dev); continue; } @@ -1156,6 +1237,7 @@ static void enumerate_services(struct tb_xdomain *xd) svc->dev.bus = &tb_bus_type; svc->dev.type = &tb_service_type; svc->dev.parent = get_device(&xd->dev); + mutex_init(&svc->lock); dev_set_name(&svc->dev, "%s.%d", dev_name(&xd->dev), svc->id); tb_service_debugfs_init(svc); @@ -2549,19 +2631,6 @@ bool tb_xdomain_handle_request(struct tb *tb, enum tb_cfg_pkg_type type, return ret > 0; } -static int update_xdomain(struct device *dev, void *data) -{ - struct tb_xdomain *xd; - - xd = tb_to_xdomain(dev); - if (xd) { - queue_delayed_work(xd->tb->wq, &xd->properties_changed_work, - msecs_to_jiffies(50)); - } - - return 0; -} - static void update_all_xdomains(void) { bus_for_each_dev(&tb_bus_type, NULL, NULL, update_xdomain); diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index e98d569779f9..f60e3a1aecae 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -397,6 +397,10 @@ void tb_unregister_protocol_handler(struct tb_protocol_handler *handler); * @prtcvers: Protocol version from the properties directory * @prtcrevs: Protocol software revision from the properties directory * @prtcstns: Protocol settings mask from the properties directory + * @lock: Protects this structure + * @local_properties: Properties owned by the service driver + * @remote_properties: Properties read from the remote service. These + * are read-only. * @debugfs_dir: Pointer to the service debugfs directory. Always created * when debugfs is enabled. Can be used by service drivers to * add their own entries under the service. @@ -404,6 +408,9 @@ void tb_unregister_protocol_handler(struct tb_protocol_handler *handler); * Each domain exposes set of services it supports as collection of * properties. For each service there will be one corresponding * &struct tb_service. Service drivers are bound to these. + * + * Service drivers can add their own dynamic properties to + * @local_properties but whenever they do so @lock must be held. */ struct tb_service { struct device dev; @@ -413,6 +420,9 @@ struct tb_service { u32 prtcvers; u32 prtcrevs; u32 prtcstns; + struct mutex lock; + struct tb_property_dir *local_properties; + struct tb_property_dir *remote_properties; struct dentry *debugfs_dir; }; @@ -481,6 +491,8 @@ static inline struct tb_xdomain *tb_service_parent(struct tb_service *svc) return tb_to_xdomain(svc->dev.parent); } +void tb_service_properties_changed(struct tb_service *svc); + /** * struct tb_nhi - thunderbolt native host interface * @lock: Must be held during ring creation/destruction. Is acquired by From 5140737c592d23b4de0f98c47dd347684a0c8de3 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 10 Nov 2025 13:16:53 +0200 Subject: [PATCH 18/41] thunderbolt / net: Move ring_frame_size() to thunderbolt.h This function can be used outside of thunderbolt networking driver so move it to the common header. No functional changes. Signed-off-by: Mika Westerberg --- drivers/net/thunderbolt/main.c | 16 ++++++---------- include/linux/thunderbolt.h | 10 +++++++++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c index 7aae5d915a1e..495f7fe366e6 100644 --- a/drivers/net/thunderbolt/main.c +++ b/drivers/net/thunderbolt/main.c @@ -38,7 +38,7 @@ #define TBNET_MATCH_FRAGS_ID BIT(1) #define TBNET_64K_FRAMES BIT(2) #define TBNET_MAX_MTU SZ_64K -#define TBNET_FRAME_SIZE SZ_4K +#define TBNET_FRAME_SIZE TB_MAX_FRAME_SIZE #define TBNET_MAX_PAYLOAD_SIZE \ (TBNET_FRAME_SIZE - sizeof(struct thunderbolt_ip_frame_header)) /* Rx packets need to hold space for skb_shared_info */ @@ -327,11 +327,6 @@ static void stop_login(struct tbnet *net) netdev_dbg(net->dev, "login stopped\n"); } -static inline unsigned int tbnet_frame_size(const struct tbnet_frame *tf) -{ - return tf->frame.size ? : TBNET_FRAME_SIZE; -} - static void tbnet_free_buffers(struct tbnet_ring *ring) { unsigned int i; @@ -562,7 +557,7 @@ static struct tbnet_frame *tbnet_get_tx_buffer(struct tbnet *net) tf->frame.size = 0; dma_sync_single_for_cpu(dma_dev, tf->frame.buffer_phy, - tbnet_frame_size(tf), DMA_TO_DEVICE); + tb_ring_frame_size(&tf->frame), DMA_TO_DEVICE); return tf; } @@ -744,7 +739,7 @@ static bool tbnet_check_frame(struct tbnet *net, const struct tbnet_frame *tf, } /* Should be greater than just header i.e. contains data */ - size = tbnet_frame_size(tf); + size = tb_ring_frame_size(&tf->frame); if (size <= sizeof(*hdr)) { net->stats.rx_length_errors++; return false; @@ -1011,7 +1006,8 @@ static bool tbnet_xmit_csum_and_map(struct tbnet *net, struct sk_buff *skb, hdr->frame_index, hdr->frame_count); dma_sync_single_for_device(dma_dev, frames[i]->frame.buffer_phy, - tbnet_frame_size(frames[i]), DMA_TO_DEVICE); + tb_ring_frame_size(&frames[i]->frame), + DMA_TO_DEVICE); } return true; @@ -1085,7 +1081,7 @@ static bool tbnet_xmit_csum_and_map(struct tbnet *net, struct sk_buff *skb, */ for (i = 0; i < frame_count; i++) { dma_sync_single_for_device(dma_dev, frames[i]->frame.buffer_phy, - tbnet_frame_size(frames[i]), DMA_TO_DEVICE); + tb_ring_frame_size(&frames[i]->frame), DMA_TO_DEVICE); } return true; diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index f60e3a1aecae..1d1bd458b5af 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -628,7 +628,15 @@ struct ring_frame { }; /* Minimum size for ring_rx */ -#define TB_FRAME_SIZE 0x100 +#define TB_FRAME_SIZE 256 +#define TB_MAX_FRAME_SIZE 4096 + +static inline size_t tb_ring_frame_size(const struct ring_frame *frame) +{ + if (frame->size) + return frame->size; + return TB_MAX_FRAME_SIZE; +} struct tb_ring *tb_ring_alloc_tx(struct tb_nhi *nhi, int hop, int size, unsigned int flags); From c51777370ac2ef435401340e205ef1d0c778df28 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 27 Feb 2026 19:51:45 +0200 Subject: [PATCH 19/41] thunderbolt / net: Let the service drivers configure interrupt throttling Instead of the core driver programming fixed value for throttling let the service drivers to specify the interval if they need this. Signed-off-by: Mika Westerberg --- drivers/net/thunderbolt/main.c | 4 +++ drivers/thunderbolt/dma_test.c | 5 +++ drivers/thunderbolt/nhi.c | 58 ++++++++++++++++++---------------- drivers/thunderbolt/nhi_regs.h | 3 +- include/linux/thunderbolt.h | 5 +++ 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c index 495f7fe366e6..f8f97e8e2226 100644 --- a/drivers/net/thunderbolt/main.c +++ b/drivers/net/thunderbolt/main.c @@ -34,6 +34,7 @@ #define TBNET_RING_SIZE 256 #define TBNET_LOGIN_RETRIES 60 #define TBNET_LOGOUT_RETRIES 10 +#define TBNET_THROTTLING 128000 #define TBNET_E2E BIT(0) #define TBNET_MATCH_FRAGS_ID BIT(1) #define TBNET_64K_FRAMES BIT(2) @@ -956,6 +957,9 @@ static int tbnet_open(struct net_device *dev) } net->rx_ring.ring = ring; + tb_ring_throttling(net->tx_ring.ring, TBNET_THROTTLING); + tb_ring_throttling(net->rx_ring.ring, TBNET_THROTTLING); + napi_enable(&net->napi); start_login(net); diff --git a/drivers/thunderbolt/dma_test.c b/drivers/thunderbolt/dma_test.c index af1e6bc9c7cd..7877319b1b03 100644 --- a/drivers/thunderbolt/dma_test.c +++ b/drivers/thunderbolt/dma_test.c @@ -155,6 +155,8 @@ static int dma_test_start_rings(struct dma_test *dt) dt->tx_ring = ring; e2e_tx_hop = ring->hop; + tb_ring_throttling(ring, 128000); + ret = tb_xdomain_alloc_out_hopid(xd, -1); if (ret < 0) { dma_test_free_rings(dt); @@ -162,6 +164,7 @@ static int dma_test_start_rings(struct dma_test *dt) } dt->tx_hopid = ret; + } if (dt->packets_to_receive) { @@ -180,6 +183,8 @@ static int dma_test_start_rings(struct dma_test *dt) dt->rx_ring = ring; + tb_ring_throttling(ring, 128000); + ret = tb_xdomain_alloc_in_hopid(xd, -1); if (ret < 0) { dma_test_free_rings(dt); diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 1a2051673067..13009246e617 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -93,7 +93,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) u32 old, new; if (ring->irq > 0) { - u32 step, shift, ivr, misc; + u32 step, shift, ivr, misc, itr; void __iomem *ivr_base; int auto_clear_bit; int index; @@ -131,6 +131,12 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) if (active) ivr |= ring->vector << shift; iowrite32(ivr, ivr_base + step); + + /* Throttling is specified in 256ns increments */ + itr = DIV_ROUND_UP(ring->interval_nsec, 256); + itr &= REG_INT_THROTTLING_RATE_INTERVAL_MASK; + iowrite32(itr, ring->nhi->iobase + REG_INT_THROTTLING_RATE + + ring->vector * 4); } old = ioread32(ring->nhi->iobase + reg); @@ -854,6 +860,26 @@ void tb_ring_free(struct tb_ring *ring) } EXPORT_SYMBOL_GPL(tb_ring_free); +/** + * tb_ring_throttling() - Configure throttling for ring interrupt + * @ring: Ring to configure + * @interval_nsec: Interval counter for moderation (in ns), %0 disables + * + * Enables or disables ring interrupt throttling. The ring must be + * stopped for this to be called. Granularity is 256 ns. + * + * Return: %0 on success, negative errno otherwise. + */ +int tb_ring_throttling(struct tb_ring *ring, unsigned int interval_nsec) +{ + guard(spinlock_irqsave)(&ring->lock); + if (WARN_ON_ONCE(ring->running)) + return -EBUSY; + ring->interval_nsec = interval_nsec; + return 0; +} +EXPORT_SYMBOL_GPL(tb_ring_throttling); + /** * nhi_mailbox_cmd() - Send a command through NHI mailbox * @nhi: Pointer to the NHI structure @@ -1035,22 +1061,6 @@ static int nhi_poweroff_noirq(struct device *dev) return __nhi_suspend_noirq(dev, wakeup); } -static void nhi_enable_int_throttling(struct tb_nhi *nhi) -{ - /* Throttling is specified in 256ns increments */ - u32 throttle = DIV_ROUND_UP(128 * NSEC_PER_USEC, 256); - unsigned int i; - - /* - * Configure interrupt throttling for all vectors even if we - * only use few. - */ - for (i = 0; i < MSIX_MAX_VECS; i++) { - u32 reg = REG_INT_THROTTLING_RATE + i * 4; - iowrite32(throttle, nhi->iobase + reg); - } -} - static int nhi_resume_noirq(struct device *dev) { struct pci_dev *pdev = to_pci_dev(dev); @@ -1065,13 +1075,10 @@ static int nhi_resume_noirq(struct device *dev) */ if (!pci_device_is_present(pdev)) { nhi->going_away = true; - } else { - if (nhi->ops && nhi->ops->resume_noirq) { - ret = nhi->ops->resume_noirq(nhi); - if (ret) - return ret; - } - nhi_enable_int_throttling(tb->nhi); + } else if (nhi->ops && nhi->ops->resume_noirq) { + ret = nhi->ops->resume_noirq(nhi); + if (ret) + return ret; } return tb_domain_resume_noirq(tb); @@ -1133,7 +1140,6 @@ static int nhi_runtime_resume(struct device *dev) return ret; } - nhi_enable_int_throttling(nhi); return tb_domain_runtime_resume(tb); } @@ -1271,8 +1277,6 @@ static int nhi_init_msi(struct tb_nhi *nhi) /* In case someone left them on. */ nhi_disable_interrupts(nhi); - nhi_enable_int_throttling(nhi); - ida_init(&nhi->msix_ida); /* diff --git a/drivers/thunderbolt/nhi_regs.h b/drivers/thunderbolt/nhi_regs.h index cf5222bee971..d6a197fabc74 100644 --- a/drivers/thunderbolt/nhi_regs.h +++ b/drivers/thunderbolt/nhi_regs.h @@ -101,7 +101,8 @@ struct ring_desc { #define REG_RING_INTERRUPT_MASK_CLEAR_BASE 0x38208 -#define REG_INT_THROTTLING_RATE 0x38c00 +#define REG_INT_THROTTLING_RATE 0x38c00 +#define REG_INT_THROTTLING_RATE_INTERVAL_MASK GENMASK(15, 0) /* Interrupt Vector Allocation */ #define REG_INT_VEC_ALLOC_BASE 0x38c40 diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 1d1bd458b5af..1160e0bf5c5b 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -554,6 +554,8 @@ struct tb_nhi { * @start_poll: Called when ring interrupt is triggered to start * polling. Passing %NULL keeps the ring in interrupt mode. * @poll_data: Data passed to @start_poll + * @interval_nsec: Interval counter if interrupt throttling is to be + * used with this ring (in ns) */ struct tb_ring { spinlock_t lock; @@ -577,6 +579,7 @@ struct tb_ring { u16 eof_mask; void (*start_poll)(void *data); void *poll_data; + unsigned int interval_nsec; }; /* Leave ring interrupt enabled on suspend */ @@ -697,6 +700,8 @@ static inline int tb_ring_tx(struct tb_ring *ring, struct ring_frame *frame) struct ring_frame *tb_ring_poll(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); + /** * tb_ring_dma_device() - Return device used for DMA mapping * @ring: Ring whose DMA device is retrieved From d614113c10ae6e35e74cdfc4fea280ce1a93ca0b Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Mon, 10 Nov 2025 13:18:06 +0200 Subject: [PATCH 20/41] thunderbolt: Add helper to figure size of the ring Add to common header a function that returns size of the ring. This can be used in the drivers instead of rolling own version. Signed-off-by: Mika Westerberg --- include/linux/thunderbolt.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 1160e0bf5c5b..9df8a356396f 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -641,6 +641,11 @@ static inline size_t tb_ring_frame_size(const struct ring_frame *frame) return TB_MAX_FRAME_SIZE; } +static inline size_t tb_ring_size(const struct tb_ring *ring) +{ + return ring->size; +} + struct tb_ring *tb_ring_alloc_tx(struct tb_nhi *nhi, int hop, int size, unsigned int flags); struct tb_ring *tb_ring_alloc_rx(struct tb_nhi *nhi, int hop, int size, From 94a11cd5ddb1d7c206f81df17a5fcb5d3ec2d13f Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Fri, 27 Jun 2025 20:25:42 +0300 Subject: [PATCH 21/41] thunderbolt: Add tb_ring_flush() This allows the caller to wait for the ring to be empty. We are going to need this in the upcoming userspace tunneling support. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 28 ++++++++++++++++++++++++++++ include/linux/thunderbolt.h | 3 +++ 2 files changed, 31 insertions(+) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index 13009246e617..a0a789bfb680 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -325,6 +325,8 @@ static void ring_work(struct work_struct *work) if (frame->callback) frame->callback(ring, frame, canceled); } + + wake_up(&ring->wait); } int __tb_ring_enqueue(struct tb_ring *ring, struct ring_frame *frame) @@ -601,6 +603,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, INIT_LIST_HEAD(&ring->queue); INIT_LIST_HEAD(&ring->in_flight); INIT_WORK(&ring->work, ring_work); + init_waitqueue_head(&ring->wait); ring->nhi = nhi; ring->hop = hop; @@ -760,6 +763,31 @@ void tb_ring_start(struct tb_ring *ring) } EXPORT_SYMBOL_GPL(tb_ring_start); +static bool tb_ring_empty(struct tb_ring *ring) +{ + guard(spinlock_irqsave)(&ring->lock); + return list_empty(&ring->in_flight); +} + +/** + * tb_ring_flush() - Waits for a ring to be empty + * @ring: Ring to wait + * @timeout_msec: Timeout in ms how long to wait. + * + * This can be called before stopping a ring to make sure all the frames + * submitted prior have been completed. + * + * Return: %true if the ring is empty now, %false otherwise. + */ +bool tb_ring_flush(struct tb_ring *ring, unsigned int timeout_msec) +{ + if (!wait_event_timeout(ring->wait, tb_ring_empty(ring), + msecs_to_jiffies(timeout_msec))) + return false; + return tb_ring_empty(ring); +} +EXPORT_SYMBOL_GPL(tb_ring_flush); + /** * tb_ring_stop() - shutdown a ring * @ring: Ring to stop diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 9df8a356396f..9c5cb5e4f23d 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -556,6 +556,7 @@ struct tb_nhi { * @poll_data: Data passed to @start_poll * @interval_nsec: Interval counter if interrupt throttling is to be * used with this ring (in ns) + * @wait: Used to signal that the ring may be empty now */ struct tb_ring { spinlock_t lock; @@ -580,6 +581,7 @@ struct tb_ring { void (*start_poll)(void *data); void *poll_data; unsigned int interval_nsec; + wait_queue_head_t wait; }; /* Leave ring interrupt enabled on suspend */ @@ -653,6 +655,7 @@ struct tb_ring *tb_ring_alloc_rx(struct tb_nhi *nhi, int hop, int size, u16 sof_mask, u16 eof_mask, void (*start_poll)(void *), void *poll_data); void tb_ring_start(struct tb_ring *ring); +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); From cba57ed6f1e7529498cebbbe135c5132667fa923 Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 12 Aug 2025 13:53:49 +0300 Subject: [PATCH 22/41] thunderbolt: Add support for ConfigFS This adds ConfigFS support to USB4/Thunderbolt bus. By itself this just creates the subsystem but it exposes functions that can be used to register groups under it. Signed-off-by: Mika Westerberg --- drivers/thunderbolt/Kconfig | 4 +++ drivers/thunderbolt/Makefile | 1 + drivers/thunderbolt/configfs.c | 61 ++++++++++++++++++++++++++++++++++ drivers/thunderbolt/domain.c | 2 ++ drivers/thunderbolt/tb.h | 8 +++++ include/linux/thunderbolt.h | 6 ++++ 6 files changed, 82 insertions(+) create mode 100644 drivers/thunderbolt/configfs.c diff --git a/drivers/thunderbolt/Kconfig b/drivers/thunderbolt/Kconfig index db3b0bef48f4..9b4aaa456e32 100644 --- a/drivers/thunderbolt/Kconfig +++ b/drivers/thunderbolt/Kconfig @@ -18,6 +18,10 @@ menuconfig USB4 if USB4 +config USB4_CONFIGFS + def_tristate USB4 + depends on CONFIGFS_FS && !(USB4=y && CONFIGFS_FS=m) + config USB4_DEBUGFS_WRITE bool "Enable write by debugfs to configuration spaces (DANGEROUS)" help diff --git a/drivers/thunderbolt/Makefile b/drivers/thunderbolt/Makefile index b44b32dcb832..d5b367dfda1e 100644 --- a/drivers/thunderbolt/Makefile +++ b/drivers/thunderbolt/Makefile @@ -7,6 +7,7 @@ thunderbolt-objs += usb4_port.o nvm.o retimer.o quirks.o clx.o thunderbolt-${CONFIG_ACPI} += acpi.o thunderbolt-$(CONFIG_DEBUG_FS) += debugfs.o +thunderbolt-$(CONFIG_USB4_CONFIGFS) += configfs.o thunderbolt-${CONFIG_USB4_KUNIT_TEST} += test.o CFLAGS_test.o += $(DISABLE_STRUCTLEAK_PLUGIN) diff --git a/drivers/thunderbolt/configfs.c b/drivers/thunderbolt/configfs.c new file mode 100644 index 000000000000..dc6bc363dfe8 --- /dev/null +++ b/drivers/thunderbolt/configfs.c @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ConfigFS support + * + * Copyright (C) 2026, Intel Corporation + * Author: Mika Westerberg + */ + +#include +#include + +#include "tb.h" + +static const struct config_item_type tb_root_group_type = { + .ct_owner = THIS_MODULE, +}; + +static struct configfs_subsystem tb_configfs = { + .su_group = { + .cg_item = { + .ci_namebuf = "thunderbolt", + .ci_type = &tb_root_group_type, + }, + }, +}; + +/** + * tb_configfs_register_group() - Register Thunderbolt ConfigFS group + * @group: Group to register. + * + * Registers the new @group under Thunderbolt subsystem ConfigFS. + * + * Return: 0% in case of success, negative errno otherwise. + */ +int tb_configfs_register_group(struct config_group *group) +{ + return configfs_register_group(&tb_configfs.su_group, group); +} +EXPORT_SYMBOL_GPL(tb_configfs_register_group); + +/** + * tb_configfs_unregister_group() - Unregister previously registered group + * @group: Group to unregister. + */ +void tb_configfs_unregister_group(struct config_group *group) +{ + configfs_unregister_group(group); +} +EXPORT_SYMBOL_GPL(tb_configfs_unregister_group); + +int tb_configfs_init(void) +{ + config_group_init(&tb_configfs.su_group); + mutex_init(&tb_configfs.su_mutex); + return configfs_register_subsystem(&tb_configfs); +} + +void tb_configfs_exit(void) +{ + configfs_unregister_subsystem(&tb_configfs); +} diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c index d83719a37b4c..b381f184340e 100644 --- a/drivers/thunderbolt/domain.c +++ b/drivers/thunderbolt/domain.c @@ -887,6 +887,7 @@ int tb_domain_init(void) { int ret; + tb_configfs_init(); tb_debugfs_init(); tb_acpi_init(); @@ -916,4 +917,5 @@ void tb_domain_exit(void) tb_xdomain_exit(); tb_acpi_exit(); tb_debugfs_exit(); + tb_configfs_exit(); } diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index 229b9e7961fb..e60f1bc3764e 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -1558,4 +1558,12 @@ static inline void tb_retimer_debugfs_init(struct tb_retimer *rt) { } static inline void tb_retimer_debugfs_remove(struct tb_retimer *rt) { } #endif +#if IS_REACHABLE(CONFIG_CONFIGFS_FS) +int tb_configfs_init(void); +void tb_configfs_exit(void); +#else +static inline int tb_configfs_init(void) { return 0; } +static inline void tb_configfs_exit(void) { } +#endif + #endif diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 9c5cb5e4f23d..0be9b298e692 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -13,6 +13,7 @@ #include +struct config_group; struct fwnode_handle; struct device; @@ -727,6 +728,11 @@ static inline struct device *tb_ring_dma_device(struct tb_ring *ring) bool usb4_usb3_port_match(struct device *usb4_port_dev, const struct fwnode_handle *usb3_port_fwnode); +#if IS_REACHABLE(CONFIG_CONFIGFS_FS) +int tb_configfs_register_group(struct config_group *group); +void tb_configfs_unregister_group(struct config_group *group); +#endif + #else /* CONFIG_USB4 */ static inline bool usb4_usb3_port_match(struct device *usb4_port_dev, const struct fwnode_handle *usb3_port_fwnode) From 6db21d817b43f8ce5654ccc7aff80d40e4dba4ac Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Tue, 12 Aug 2025 13:56:30 +0300 Subject: [PATCH 23/41] thunderbolt: Add support for USB4STREAM Introduce USB4STREAM protocol and Linux implementation. This allows two (or more) hosts to transfer data directly over Thunderbolt/USB4 cable through a character device without need to go through the network stack. Any application that supports read(2) and write(2) in some form should be able to use the device without changes. The data is sent out to the other side over a tunnel inside Thunderbolt/USB4 fabric. The character device is called /dev/tbstreamX where X is the minor number starting from 0. All stream devices need to be configured first. This is done through ConfigFS interface. There can be multiple streams at the same time (this depends on number of DMA rings and available HopIDs) and a single stream supports traffic in both directions. For example there could be an application that uses one stream as control channel and another one as bi-directional data channel. A real use-case for this is to take a backup as a part of recovery initramfs tooling (no need to setup networking or have ssh or similar tooling as part of the initramfs). Say we want to backup the disk of host1 to host2. First Thunderbolt/USB4 cable is connected between the hosts (there can be devices in the middle too) then the receiving side configures the stream: host2 # mkdir /sys/kernel/config/thunderbolt/stream/0-1.0 host2 # mkdir /sys/kernel/config/thunderbolt/stream/0-1.0/backup host2 # echo -1 > /sys/kernel/config/thunderbolt/stream/0-1.0/backup/in_hopid host2 # echo -1 > /sys/kernel/config/thunderbolt/stream/0-1.0/backup/out_hopid We use automatic HopID allocation (writing -1 to HopIDs) for simplicity. From this point forward the /dev/tbstream0 can be used pretty much as regular file: host2 # dd if=/dev/tbstream0 of=/tmp/host1.nvme0n1.backup-$(date +%F) bs=256k The host that is being backed up then configures the stream accordingly: host1 # mkdir /sys/kernel/config/thunderbolt/stream/0-503.0 host1 # mkdir /sys/kernel/config/thunderbolt/stream/0-503.0/backup Here we take advantage of the fact that host2 also announces the active streams through XDomain properties so the name "backup" gives us the HopIDs. It is also possible to configure them manually in the same way we did for host2. Then it is just a matter of copying the data over: host1 # dd if=/dev/nvme0n1 of=/dev/tbstream0 bs=256k Similarly it is possible to transfer parts of the filesystem. For example copy contents of mydir over to the host2: host2 # gunzip < /dev/tbstream0 | tar xf - host1 # tar cf - mydir | gzip > /dev/tbstream0 Other end of the spectrum use-case is "borrowing" laptop (host1) camera to desktop (host2): host2 # gst-launch-1.0 filesrc location=/dev/tbstream0 ! jpegdec ! videoconvert ! \ autovideosink host1 # gst-launch-1.0 v4l2src device=/dev/video0 ! video/x-raw,width=1920,height=1080 ! \ jpegenc quality=90 ! filesink location=/dev/tbstream0 Once the streams are no longer needed they can be removed: host1 # cd /sys/kernel/config/thunderbolt/stream/ host1 # rmdir -p 0-503.0/backup host2 # cd /sys/kernel/config/thunderbolt/stream host2 # rmdir -p 0-1.0/backup Co-developed-by: Alan Borzeszkowski Signed-off-by: Alan Borzeszkowski Signed-off-by: Mika Westerberg --- .../ABI/testing/configfs-thunderbolt_stream | 83 + drivers/thunderbolt/Kconfig | 11 + drivers/thunderbolt/Makefile | 3 + drivers/thunderbolt/stream.c | 1698 +++++++++++++++++ 4 files changed, 1795 insertions(+) create mode 100644 Documentation/ABI/testing/configfs-thunderbolt_stream create mode 100644 drivers/thunderbolt/stream.c diff --git a/Documentation/ABI/testing/configfs-thunderbolt_stream b/Documentation/ABI/testing/configfs-thunderbolt_stream new file mode 100644 index 000000000000..7abc6b73a1e4 --- /dev/null +++ b/Documentation/ABI/testing/configfs-thunderbolt_stream @@ -0,0 +1,83 @@ +What: /sys/kernel/config/thunderbolt/stream/. +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + Configuration group for a stream Thunderbolt/USB4 + service. It is possible to create groups even if there + is no connection yet to the other host. Once a + connection established and there is stream service on + the remote side that matches, this configuration is + applied to it. + + To find the service name you can run tblist from tbtools [1]: + + # tblist -A + ... + Domain 0 Route 3 Index 0: stream + + [1] https://github.com/intel/tbtools + +What: /sys/kernel/config/thunderbolt/stream/./$name +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + Creates new stream with $name and fills it with the + default values. If there is an advertised remote stream + with the same name, uses its values as the default. + +What: /sys/kernel/config/thunderbolt/stream/./$name/index +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + This matches the X in /dev/tbstreamX and allows userspace + to map the configfs directory to the corresponding character + device. + +What: /sys/kernel/config/thunderbolt/stream/./$name/in_hopid +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + In HopID used with the read path of the tunnel. Available HopIDs + for tunneling start from 8. You can pass also -1 for automatic + allocation. The allocated value can be read here. Writing 0 will + de-allocate if the stream is not in use. + + To figure out the maximum HopID you can run tbget from + tbtools for the lane adapter. For example below we check + for lane adapter number 1 (first USB4 port): + + # tbget -r 0 -a 1 -D ADP_CS_5.Max\ Input\ HopID + 19 + + This allows to use anything between 8 and 19 inclusive. + +What: /sys/kernel/config/thunderbolt/stream/./$name/out_hopid +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + Out HopID used with the write path of the tunnel. Available HopIDs + for tunneling start from 8. You can pass also -1 for automatic + allocation. The allocated value can be read here. Writing 0 will + de-allocate if the stream is not in use. See @in_hopid + for how to figure out the maximum HopID. + +What: /sys/kernel/config/thunderbolt/stream/./$name/ring_size +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + Size of the TX/RX rings. Can be adjusted between 32 and + 4096. The default is 256. + +What: /sys/kernel/config/thunderbolt/stream/./$name/throttling +Date: Sep 2026 +KernelVersion: v7.2 +Contact: Mika Westerberg +Description: + Interrupt throttling rate in ns. Lower values can give + better latency. The default is 8192 ns. diff --git a/drivers/thunderbolt/Kconfig b/drivers/thunderbolt/Kconfig index 9b4aaa456e32..294b3227a545 100644 --- a/drivers/thunderbolt/Kconfig +++ b/drivers/thunderbolt/Kconfig @@ -64,4 +64,15 @@ config USB4_DMA_TEST To compile this driver a module, choose M here. The module will be called thunderbolt_dma_test. +config USB4_STREAM + tristate "Stream data over Thunderbolt/USB4 cable" + depends on USB4_CONFIGFS + help + This adds support for USB4STREAM protocol that allows two + hosts to stream data directly over Thunderbolt/USB4 cable + through /dev/tbstreamX devices. + + To compile this driver a module, choose M here. The module will be + called thunderbolt_stream. + endif # USB4 diff --git a/drivers/thunderbolt/Makefile b/drivers/thunderbolt/Makefile index d5b367dfda1e..c792b8084ffa 100644 --- a/drivers/thunderbolt/Makefile +++ b/drivers/thunderbolt/Makefile @@ -13,3 +13,6 @@ CFLAGS_test.o += $(DISABLE_STRUCTLEAK_PLUGIN) thunderbolt_dma_test-${CONFIG_USB4_DMA_TEST} += dma_test.o obj-$(CONFIG_USB4_DMA_TEST) += thunderbolt_dma_test.o + +thunderbolt_stream-${CONFIG_USB4_STREAM} += stream.o +obj-$(CONFIG_USB4_STREAM) += thunderbolt_stream.o diff --git a/drivers/thunderbolt/stream.c b/drivers/thunderbolt/stream.c new file mode 100644 index 000000000000..c1f5c55583d0 --- /dev/null +++ b/drivers/thunderbolt/stream.c @@ -0,0 +1,1698 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Stream data over Thunderbolt/USB4 cable + * + * Copyright (C) 2026, Intel Corporation + * Authors: Alan Borzeszkowski + * Mika Westerberg + */ + +#define pr_fmt(fmt) "tbstream: " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * USB4STREAM - Stream data directly over Thunderbolt/USB4 cable + * + * HopIDs are configured by the user. In Linux this is done through + * ConfigFS. Once that is done paths are be established the first time + * the stream is opened. Typically the read side is opened first to make + * sure all the data will be received. + * + * End-to-end flow control is mandatory on both sides. + * + * Data is sent to the other side as tunneled DATA packets. All the data + * is owned by the user and passed as-is from the writer to the reader. + * + * Once the stream device is closed, a CLOSE packet is sent to the peer + * so it can take the necessary action. On Linux this typically results + * in EOF being returned to the reader. + * + * Tunneled packet types: + * + * +-------+---------+------------------+ + * | PDF | Type | Payload size | + * +-------+---------+------------------+ + * | 2 | DATA | up to 4 KiB | + * | 3 | CLOSE | up to 256 bytes | + * +-------+---------+------------------+ + * + * Each stream can optionally publish configuration values under its own + * XDomain property directory. The name of the directory is the name of + * the stream in question and the UUID is up to the stream. For example + * if the stream exposes video output then the directory name could be + * "video". + * + * Below values are reserved and can be used by the stream: + * + * +----------+-----------+-------------------------+ + * | Key | Type | Contents | + * +----------+-----------+-------------------------+ + * | inhopid | IMMEDIATE | Configured input HopID | + * | outhopid | IMMEDIATE | Configured output HopID | + * +----------+-----------+-------------------------+ + * + * It is allowed to add more stream specific properties as well if the + * above are not enough. + */ + +#define TBSTREAM_DEV_RING_SIZE 256 +#define TBSTREAM_DEV_MIN_RING_SIZE 32 +#define TBSTREAM_DEV_MAX_RING_SIZE 4096 +#define TBSTREAM_DEV_THROTTLING 8192 +#define TBSTREAM_DEV_MAX_THROTTLING 16776960 + +/** + * enum tbstream_frame_pdf - PDF numbers for tunneled frames + * @TBSTREAM_FRAME_START: PDF of the start of the frame + * @TBSTREAM_DATA: PDF of the DATA frame + * @TBSTREAM_CLOSE: PDF of the CLOSE frame + */ +enum tbstream_frame_pdf { + TBSTREAM_FRAME_START = 1, + TBSTREAM_DATA, + TBSTREAM_CLOSE, +}; + +/** + * struct tbstream_frame - Frame submitted to/from the rings + * @sdev: Pointer to the stream device + * @page: Page holding the packet + * @offset: Offset inside @page if partial read is done + * @completed: %true if the RX frame is completed + * @frame: Underlying frame structure + */ +struct tbstream_frame { + struct tbstream_dev *sdev; + struct page *page; + unsigned int offset; + bool completed; + struct ring_frame frame; +}; + +/** + * struct tbstream_ring - Stream RX/TX ring structure + * @ring: Pointer to the API ring + * @prod: Current value of producer + * @cons: Current value of consumer + * @frames: Holds the ring frames + */ +struct tbstream_ring { + struct tb_ring *ring; + unsigned long prod; + unsigned long cons; + struct tbstream_frame *frames; +}; + +/** + * struct tbstream_dev - Stream character device + * @group: ConfigFS group for this device + * @stream: Pointer to the stream if it is attached (%NULL otherwise) + * @misc: Character device used for tunneling + * @kref: Reference count + * @index: Unique identifier for the character device + * @in_hopid: In HopID + * @out_hopid: Out HopID + * @ring_size: Size of the rings + * @throttling: Interrupt throttling rate in ns + * @users: Number of times @cdev has been opened + * @closed: CLOSE packet was received + * @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 + */ +struct tbstream_dev { + struct config_group group; + struct tbstream *stream; + struct miscdevice misc; + struct kref kref; + int index; + int in_hopid; + int out_hopid; + unsigned int ring_size; + unsigned int throttling; + int users; + bool closed; + bool removed; + wait_queue_head_t wait; + struct mutex lock; + struct tbstream_ring tx_ring; + struct tbstream_ring rx_ring; + struct list_head list; +}; + +/** + * struct tbstream_group - Config group for stream + * @group: ConfigFS group for @stream + * @stream: Stream the ConfigFS group is attached to. %NULL if there is + * no stream attached. + * @lock: Lock protecting this structure + * @dev_list: List of stream devices + * + * This is the ConfigFS directory for one connection to another host. + * There can be several &struct stream_dev linked through @dev_list of + * this structure. Reference count managed through @group. + */ +struct tbstream_group { + struct config_group group; + struct tbstream *stream; + struct mutex lock; + struct list_head dev_list; +}; + +/** + * struct tbstream - Stream service private data + * @kref: Reference count + * @svc: Pointer to the service device + * @list: Streams are linked through this in @stream_list + * + * This represents the actual physical connection between two hosts. + */ +struct tbstream { + struct kref kref; + struct tb_service *svc; + struct list_head list; +}; + +static DEFINE_IDA(tbstream_indices); + +/* Protects tbstream_list */ +static DEFINE_MUTEX(tbstream_lock); +static LIST_HEAD(tbstream_list); + +/* Serializes tbstream_get()/put() */ +static DEFINE_MUTEX(tbstream_kref_lock); + +/* Serializes tbstream_dev_get()/put() */ +static DEFINE_MUTEX(tbstream_dev_kref_lock); + +/* Stream property directory UUID: 3a1cb984-c4d9-4469-a277-ce2fdfd11f0d */ +static const uuid_t tbstream_dir_uuid = + UUID_INIT(0x3a1cb984, 0xc4d9, 0x4469, + 0xa2, 0x77, 0xce, 0x2f, 0xdf, 0xd1, 0x1f, 0x0d); + +static struct tb_property_dir *tbstream_dir; + +static void tbstream_release(struct kref *kref) +{ + struct tbstream *stream = container_of(kref, typeof(*stream), kref); + + tb_service_put(stream->svc); + kfree(stream); +} + +static void tbstream_put(struct tbstream *stream) +{ + if (stream) { + guard(mutex)(&tbstream_kref_lock); + kref_put(&stream->kref, tbstream_release); + } +} + +static struct tbstream *tbstream_get(struct tbstream *stream) +{ + if (stream) { + guard(mutex)(&tbstream_kref_lock); + kref_get(&stream->kref); + } + return stream; +} + +static inline bool tbstream_valid(const struct tbstream *stream) +{ + if (stream) + return !tb_service_parent(stream->svc)->is_unplugged; + return false; +} + +static void tbstream_ring_free(struct tbstream_ring *ring) +{ + struct device *dma_dev = tb_ring_dma_device(ring->ring); + enum dma_data_direction dir; + int i; + + if (ring->ring->is_tx) + dir = DMA_TO_DEVICE; + else + dir = DMA_FROM_DEVICE; + + for (i = 0; i < tb_ring_size(ring->ring); i++) { + struct tbstream_frame *sf = &ring->frames[i]; + + if (sf->frame.buffer_phy) + dma_unmap_page(dma_dev, sf->frame.buffer_phy, + tb_ring_frame_size(&sf->frame), dir); + sf->frame.buffer_phy = 0; + if (sf->page) + __free_page(sf->page); + sf->page = NULL; + } + + ring->prod = 0; + ring->cons = 0; + kfree(ring->frames); +} + +static inline bool tbstream_ring_available(const struct tbstream_ring *ring) +{ + return ring->prod > ring->cons; +} + +static inline struct tb_xdomain *tbstream_dev_xdomain(struct tbstream_dev *sdev) +{ + if (sdev->stream) + return tb_service_parent(sdev->stream->svc); + return NULL; +} + +static void tbstream_dev_release(struct kref *kref) +{ + struct tbstream_dev *sdev = container_of(kref, struct tbstream_dev, kref); + + if (sdev->stream) { + struct tb_xdomain *xd = tbstream_dev_xdomain(sdev); + + if (sdev->out_hopid > 0) + tb_xdomain_release_out_hopid(xd, sdev->out_hopid); + if (sdev->in_hopid > 0) + tb_xdomain_release_in_hopid(xd, sdev->in_hopid); + + tbstream_put(sdev->stream); + } + ida_free(&tbstream_indices, sdev->index); + kfree(sdev->misc.name); + kfree(sdev); +} + +static inline void tbstream_dev_put(struct tbstream_dev *sdev) +{ + guard(mutex)(&tbstream_dev_kref_lock); + kref_put(&sdev->kref, tbstream_dev_release); +} + +static inline struct tbstream_dev *tbstream_dev_get(struct tbstream_dev *sdev) +{ + guard(mutex)(&tbstream_dev_kref_lock); + kref_get(&sdev->kref); + return sdev; +} + +static inline struct tbstream_dev *to_tbstream_dev(struct miscdevice *misc) +{ + return container_of(misc, struct tbstream_dev, misc); +} + +static inline int tbstream_dev_valid(const struct tbstream_dev *sdev) +{ + const struct tbstream *stream = sdev->stream; + + if (!tbstream_valid(stream)) + return -ENXIO; + if (sdev->in_hopid <= 0 || sdev->out_hopid <= 0) + return -EINVAL; + return 0; +} + +static inline bool tbstream_dev_removed(const struct tbstream_dev *sdev) +{ + return sdev->removed; +} + +static inline bool tbstream_dev_closed(const struct tbstream_dev *sdev) +{ + return sdev->closed; +} + +static void +tbstream_dev_rx_callback(struct tb_ring *ring, struct ring_frame *frame, + bool canceled) +{ + struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame); + struct tbstream_dev *sdev = sf->sdev; + + if (canceled) + return; + + 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 + wake_up_interruptible_poll(&sdev->wait, EPOLLIN | EPOLLRDNORM); +} + +static struct tbstream_frame * +tbstream_dev_completed_rx(struct tbstream_dev *sdev) +{ + struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring); + struct tbstream_frame *sf; + int index; + + index = sdev->rx_ring.cons % tb_ring_size(sdev->rx_ring.ring); + sf = &sdev->rx_ring.frames[index]; + if (!sf->completed) + return NULL; + + dma_sync_single_for_cpu(dma_dev, sf->frame.buffer_phy, + tb_ring_frame_size(&sf->frame), + DMA_FROM_DEVICE); + return sf; +} + +static int tbstream_dev_consume_rx(struct tbstream_dev *sdev) +{ + struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring); + struct tbstream_frame *sf; + int index; + + index = sdev->rx_ring.cons % tb_ring_size(sdev->rx_ring.ring); + sdev->rx_ring.cons++; + + sf = &sdev->rx_ring.frames[index]; + sf->completed = false; + sf->offset = 0; + sf->frame.size = 0; + + dma_sync_single_for_device(dma_dev, sf->frame.buffer_phy, + tb_ring_frame_size(&sf->frame), + DMA_FROM_DEVICE); + + return tb_ring_rx(sdev->rx_ring.ring, &sf->frame); +} + +static int tbstream_dev_alloc_rx_buffers(struct tbstream_dev *sdev) +{ + size_t ring_size = tb_ring_size(sdev->rx_ring.ring); + int i; + + sdev->rx_ring.frames = kcalloc(ring_size, sizeof(struct tbstream_frame), + GFP_KERNEL); + if (!sdev->rx_ring.frames) + return -ENOMEM; + + for (i = 0; i < ring_size; i++) { + struct device *dma_dev = tb_ring_dma_device(sdev->rx_ring.ring); + struct tbstream_frame *sf = &sdev->rx_ring.frames[i]; + dma_addr_t dma_addr; + + sf->page = alloc_page(GFP_KERNEL); + if (!sf->page) + return -ENOMEM; + + dma_addr = dma_map_page(dma_dev, sf->page, 0, TB_MAX_FRAME_SIZE, + DMA_FROM_DEVICE); + if (dma_mapping_error(dma_dev, dma_addr)) { + __free_page(sf->page); + sf->page = NULL; + return -ENOMEM; + } + + sf->sdev = sdev; + sf->frame.callback = tbstream_dev_rx_callback; + sf->frame.buffer_phy = dma_addr; + + tb_ring_rx(sdev->rx_ring.ring, &sf->frame); + } + + sdev->rx_ring.cons = 0; + sdev->rx_ring.prod = 0; + return 0; +} + +static void +tbstream_dev_tx_callback(struct tb_ring *ring, struct ring_frame *frame, + bool canceled) +{ + struct tbstream_frame *sf = container_of(frame, typeof(*sf), frame); + struct tbstream_dev *sdev = sf->sdev; + + if (canceled) + return; + + sdev->tx_ring.prod++; + if (sf->frame.eof == TBSTREAM_DATA) + wake_up_interruptible_poll(&sdev->wait, EPOLLOUT | EPOLLWRNORM); +} + +static int tbstream_dev_alloc_tx_buffers(struct tbstream_dev *sdev) +{ + struct device *dma_dev = tb_ring_dma_device(sdev->tx_ring.ring); + size_t ring_size = tb_ring_size(sdev->tx_ring.ring); + int i; + + sdev->tx_ring.frames = kcalloc(ring_size, sizeof(struct tbstream_frame), + GFP_KERNEL); + if (!sdev->tx_ring.frames) + return -ENOMEM; + + for (i = 0; i < ring_size; i++) { + struct tbstream_frame *sf = &sdev->tx_ring.frames[i]; + dma_addr_t dma_addr; + + sf->page = alloc_page(GFP_KERNEL); + if (!sf->page) + return -ENOMEM; + + dma_addr = dma_map_page(dma_dev, sf->page, 0, TB_MAX_FRAME_SIZE, + DMA_TO_DEVICE); + if (dma_mapping_error(dma_dev, dma_addr)) { + __free_page(sf->page); + sf->page = NULL; + return -ENOMEM; + } + + sf->sdev = sdev; + sf->frame.callback = tbstream_dev_tx_callback; + sf->frame.buffer_phy = dma_addr; + sf->frame.sof = TBSTREAM_FRAME_START; + } + + sdev->tx_ring.cons = 0; + sdev->tx_ring.prod = ring_size - 1; + return 0; +} + +static struct tbstream_frame * +tbstream_dev_alloc_tx(struct tbstream_dev *sdev, enum tbstream_frame_pdf pdf, + struct iov_iter *from, size_t size) +{ + struct device *dma_dev = tb_ring_dma_device(sdev->tx_ring.ring); + struct tbstream_frame *sf; + int index; + + if (!tbstream_ring_available(&sdev->tx_ring)) + return ERR_PTR(-ENOBUFS); + + index = sdev->tx_ring.cons % tb_ring_size(sdev->tx_ring.ring); + sdev->tx_ring.cons++; + + sf = &sdev->tx_ring.frames[index]; + sf->frame.size = size < TB_MAX_FRAME_SIZE ? size : 0; + sf->frame.eof = pdf; + + dma_sync_single_for_cpu(dma_dev, sf->frame.buffer_phy, size, + DMA_TO_DEVICE); + if (pdf == TBSTREAM_DATA) { + if (copy_page_from_iter(sf->page, 0, size, from) != size) + return ERR_PTR(-EFAULT); + } else { + memset(page_address(sf->page), 0, size); + } + dma_sync_single_for_device(dma_dev, sf->frame.buffer_phy, size, + DMA_TO_DEVICE); + return sf; +} + +static int +tbstream_dev_send_data(struct tbstream_dev *sdev, struct iov_iter *from, + size_t size) +{ + struct tbstream_frame *sf; + + sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_DATA, from, size); + if (IS_ERR(sf)) + return PTR_ERR(sf); + return tb_ring_tx(sdev->tx_ring.ring, &sf->frame); +} + +static int tbstream_dev_send_close(struct tbstream_dev *sdev) +{ + struct tbstream_frame *sf; + + sf = tbstream_dev_alloc_tx(sdev, TBSTREAM_CLOSE, NULL, SZ_256); + if (IS_ERR(sf)) + return PTR_ERR(sf); + return tb_ring_tx(sdev->tx_ring.ring, &sf->frame); +} + +static int tbstream_dev_start(struct tbstream_dev *sdev) +{ + struct tb_xdomain *xd = tbstream_dev_xdomain(sdev); + u16 sof_mask, eof_mask; + struct tb_ring *ring; + int ret, e2e_tx_hop; + + ring = tb_ring_alloc_tx(xd->tb->nhi, -1, sdev->ring_size, + RING_FLAG_FRAME | RING_FLAG_E2E); + if (!ring) + return -ENOMEM; + sdev->tx_ring.ring = ring; + + ret = tbstream_dev_alloc_tx_buffers(sdev); + if (ret) + goto err_free_tx; + + e2e_tx_hop = ring->hop; + sof_mask = BIT(TBSTREAM_FRAME_START); + eof_mask = BIT(TBSTREAM_DATA) | BIT(TBSTREAM_CLOSE); + + ring = tb_ring_alloc_rx(xd->tb->nhi, -1, sdev->ring_size, + RING_FLAG_FRAME | RING_FLAG_E2E, e2e_tx_hop, + sof_mask, eof_mask, NULL, NULL); + if (!ring) { + ret = -ENOMEM; + goto err_free_tx_buffers; + } + sdev->rx_ring.ring = ring; + + ret = tb_xdomain_enable_paths(xd, sdev->out_hopid, + sdev->tx_ring.ring->hop, + sdev->in_hopid, + sdev->rx_ring.ring->hop); + if (ret) + goto err_free_rx; + + tb_ring_throttling(sdev->tx_ring.ring, sdev->throttling); + tb_ring_throttling(sdev->rx_ring.ring, sdev->throttling); + + tb_ring_start(sdev->tx_ring.ring); + tb_ring_start(sdev->rx_ring.ring); + + ret = tbstream_dev_alloc_rx_buffers(sdev); + if (ret) + goto err_stop; + return 0; + +err_stop: + tb_ring_stop(sdev->rx_ring.ring); + tb_ring_stop(sdev->tx_ring.ring); +err_free_rx: + tb_ring_free(sdev->rx_ring.ring); +err_free_tx_buffers: + tbstream_ring_free(&sdev->tx_ring); +err_free_tx: + tb_ring_free(sdev->tx_ring.ring); + + return ret; +} + +static void tbstream_dev_stop(struct tbstream_dev *sdev) +{ + struct tb_xdomain *xd; + + /* Wait for the ring to complete any outstanding frames */ + 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); + + xd = tbstream_dev_xdomain(sdev); + if (xd) { + tb_xdomain_disable_paths(xd, sdev->out_hopid, + sdev->tx_ring.ring->hop, + sdev->in_hopid, + sdev->rx_ring.ring->hop); + } + + tbstream_ring_free(&sdev->rx_ring); + tb_ring_free(sdev->rx_ring.ring); + sdev->rx_ring.ring = NULL; + tbstream_ring_free(&sdev->tx_ring); + tb_ring_free(sdev->tx_ring.ring); + sdev->tx_ring.ring = NULL; +} + +static ssize_t +tbstream_dev_fops_read_iter(struct kiocb *kiocb, struct iov_iter *to) +{ + struct file *file = kiocb->ki_filp; + struct tbstream_dev *sdev = to_tbstream_dev(file->private_data); + size_t nbytes; + int ret; + + ret = tbstream_dev_valid(sdev); + if (ret) + return ret; + + if (mutex_lock_interruptible(&sdev->lock)) + return -ERESTARTSYS; + + while (!tbstream_ring_available(&sdev->rx_ring)) { + mutex_unlock(&sdev->lock); + + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + ret = wait_event_interruptible(sdev->wait, + tbstream_ring_available(&sdev->rx_ring) || + tbstream_dev_valid(sdev) != 0 || + tbstream_dev_closed(sdev) || + tbstream_dev_removed(sdev)); + if (ret) + return ret; + + ret = tbstream_dev_valid(sdev); + if (ret) + return ret; + + if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev)) + return 0; + + if (mutex_lock_interruptible(&sdev->lock)) + return -ERESTARTSYS; + } + + nbytes = 0; + while (nbytes < iov_iter_count(to)) { + struct tbstream_frame *sf; + size_t size, sf_size; + + sf = tbstream_dev_completed_rx(sdev); + if (!sf) + break; + /* + * CLOSE tunneled packet. If userspace already read + * something then we stop processing now and return + * those bytes. Next time the first frame will be CLOSE + * in which case we return EOF to the user. + */ + if (sf->frame.eof == TBSTREAM_CLOSE) { + if (!nbytes) { + tbstream_dev_consume_rx(sdev); + sdev->closed = true; + } + break; + } + + sf_size = tb_ring_frame_size(&sf->frame); + size = min(iov_iter_count(to) - nbytes, sf_size); + + if (copy_page_to_iter(sf->page, sf->offset, size, to) != size) { + ret = -EFAULT; + break; + } + + /* + * If not all data from the frame is read so leave it in + * place and update the offset accordingly so next read + * gets the rest. + */ + if (size < sf_size) { + sf->offset += size; + sf->frame.size = sf_size - size; + } else { + ret = tbstream_dev_consume_rx(sdev); + if (ret) + break; + } + + nbytes += size; + } + + mutex_unlock(&sdev->lock); + if (ret) + return ret; + return nbytes; +} + +static ssize_t +tbstream_dev_fops_write_iter(struct kiocb *kiocb, struct iov_iter *from) +{ + struct file *file = kiocb->ki_filp; + struct tbstream_dev *sdev = to_tbstream_dev(file->private_data); + size_t nbytes; + int ret; + + ret = tbstream_dev_valid(sdev); + if (ret) + return ret; + + if (mutex_lock_interruptible(&sdev->lock)) + return -ERESTARTSYS; + + while (!tbstream_ring_available(&sdev->tx_ring)) { + mutex_unlock(&sdev->lock); + + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + 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)); + if (ret) + return ret; + + ret = tbstream_dev_valid(sdev); + if (ret) + return ret; + + if (tbstream_dev_closed(sdev) || tbstream_dev_removed(sdev)) + return -ENXIO; + + if (mutex_lock_interruptible(&sdev->lock)) + return -ERESTARTSYS; + } + + nbytes = 0; + while (nbytes < iov_iter_count(from)) { + size_t size; + + size = min(iov_iter_count(from) - nbytes, TB_MAX_FRAME_SIZE); + ret = tbstream_dev_send_data(sdev, from, size); + if (ret) { + /* + * If there are no more buffers we are done for + * this write. + */ + if (ret == -ENOBUFS) + ret = 0; + break; + } + + nbytes += size; + } + + mutex_unlock(&sdev->lock); + if (ret) + return ret; + return nbytes; +} + +static __poll_t +tbstream_dev_fops_poll(struct file *file, struct poll_table_struct *wait) +{ + struct tbstream_dev *sdev = to_tbstream_dev(file->private_data); + __poll_t mask = 0; + + 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; + } + return mask; +} + +static int tbstream_dev_fops_open(struct inode *inode, struct file *file) +{ + struct tbstream_dev *sdev = to_tbstream_dev(file->private_data); + int ret; + + tbstream_dev_get(sdev); + + if (mutex_lock_interruptible(&sdev->lock)) { + tbstream_dev_put(sdev); + return -ERESTARTSYS; + } + + /* + * If there is no stream attached yet, block until it appears + * unless this is opened in non-blocking mode. + */ + while ((ret = tbstream_dev_valid(sdev))) { + mutex_unlock(&sdev->lock); + + if (ret != -ENXIO || (file->f_flags & O_NONBLOCK)) + goto err_put; + + ret = wait_event_interruptible(sdev->wait, + tbstream_dev_valid(sdev) == 0 || + tbstream_dev_removed(sdev)); + if (ret) + goto err_put; + + if (tbstream_dev_removed(sdev)) { + ret = -ENXIO; + goto err_put; + } + + if (mutex_lock_interruptible(&sdev->lock)) { + ret = -ERESTARTSYS; + goto err_put; + } + } + + /* Only on first open we allocate rings and enable paths */ + if (!sdev->users++) { + ret = tbstream_dev_start(sdev); + if (ret) { + sdev->users--; + goto err_unlock; + } + sdev->closed = false; + } + + mutex_unlock(&sdev->lock); + return 0; + +err_unlock: + mutex_unlock(&sdev->lock); +err_put: + tbstream_dev_put(sdev); + + return ret; +} + +static int tbstream_dev_fops_release(struct inode *inode, struct file *file) +{ + struct tbstream_dev *sdev = to_tbstream_dev(file->private_data); + + 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. + */ + tbstream_dev_send_close(sdev); + tbstream_dev_stop(sdev); + } + mutex_unlock(&sdev->lock); + + tbstream_dev_put(sdev); + return 0; +} + +static const struct file_operations tbstream_dev_fops = { + .owner = THIS_MODULE, + .llseek = noop_llseek, + .read_iter = tbstream_dev_fops_read_iter, + .write_iter = tbstream_dev_fops_write_iter, + .poll = tbstream_dev_fops_poll, + .open = tbstream_dev_fops_open, + .release = tbstream_dev_fops_release, +}; + +static inline struct tbstream_dev * +tbstream_dev_from_group(struct config_group *group) +{ + return container_of(group, struct tbstream_dev, group); +} + +static ssize_t tbstream_dev_index_show(struct config_item *item, char *buf) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + + return sysfs_emit(buf, "%d\n", sdev->index); +} +CONFIGFS_ATTR_RO(tbstream_dev_, index); + +static ssize_t tbstream_dev_in_hopid_show(struct config_item *item, char *buf) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + + return sysfs_emit(buf, "%d\n", sdev->in_hopid); +} + +/* svc->lock must be held */ +static void service_remove_properties(struct tb_service *svc, const char *name) +{ + struct tb_property *p; + + if (!svc->local_properties) + return; + + p = tb_property_find(svc->local_properties, name, + TB_PROPERTY_TYPE_DIRECTORY); + if (p) { + tb_property_free_dir(p->value.dir); + tb_property_remove(p); + + dev_dbg(&svc->dev, "removed local directory %s\n", name); + + /* + * Is the service directory empty already? If it is then + * we can release it as well. + */ + tb_property_for_each(svc->local_properties, p) { + if (p->type == TB_PROPERTY_TYPE_DIRECTORY) + return; + } + + tb_property_free_dir(svc->local_properties); + svc->local_properties = NULL; + } +} + +static int service_update_properties(struct tb_service *svc, const char *name, + int in_hopid, int out_hopid) +{ + struct tb_property_dir *dir; + struct tb_property *p; + + guard(mutex)(&svc->lock); + + if (in_hopid < 8 || out_hopid < 8) { + service_remove_properties(svc, name); + return 0; + } + + if (!svc->local_properties) { + /* + * Add the service directory first time we + * populate the entries. + */ + svc->local_properties = tb_property_copy_dir(tbstream_dir); + if (!svc->local_properties) + return -ENOMEM; + } + + p = tb_property_find(svc->local_properties, name, + TB_PROPERTY_TYPE_DIRECTORY); + if (p) { + dir = p->value.dir; + + p = tb_property_find(dir, "inhopid", TB_PROPERTY_TYPE_VALUE); + if (p && p->value.immediate != in_hopid) + p->value.immediate = in_hopid; + p = tb_property_find(dir, "outhopid", TB_PROPERTY_TYPE_VALUE); + if (p && p->value.immediate != out_hopid) + p->value.immediate = out_hopid; + + dev_dbg(&svc->dev, + "updated local directory %s: in HopID %d, out HopID %d\n", + name, in_hopid, out_hopid); + } else { + uuid_t uuid; + int ret; + + uuid_gen(&uuid); + dir = tb_property_create_dir(&uuid); + if (!dir) + return -ENOMEM; + + tb_property_add_immediate(dir, "inhopid", in_hopid); + tb_property_add_immediate(dir, "outhopid", out_hopid); + + ret = tb_property_add_dir(svc->local_properties, name, dir); + if (ret) { + tb_property_free_dir(dir); + return ret; + } + + dev_dbg(&svc->dev, + "added local directory %s: in HopID %d, out HopID %d\n", + name, in_hopid, out_hopid); + } + + return 0; +} + +static int tbstream_dev_update_properties(struct tbstream_dev *sdev) +{ + struct tbstream *stream; + int ret; + + stream = tbstream_get(sdev->stream); + if (!stream) + return 0; + + ret = service_update_properties(stream->svc, + config_item_name(&sdev->group.cg_item), + sdev->in_hopid, sdev->out_hopid); + if (!ret) + tb_service_properties_changed(stream->svc); + + tbstream_put(stream); + return ret; +} + +static int tbstream_dev_alloc_in_hopid(struct tbstream_dev *sdev, int hopid) +{ + struct tb_xdomain *xd = tbstream_dev_xdomain(sdev); + int ret; + + if (sdev->in_hopid > 0 && sdev->in_hopid != hopid) + tb_xdomain_release_in_hopid(xd, sdev->in_hopid); + if (!hopid) { + sdev->in_hopid = hopid; + return 0; + } + ret = tb_xdomain_alloc_in_hopid(xd, hopid); + if (ret < 0) + return ret; + /* + * If specific HopID was asked by the user and we did not get + * that one then release and return error instead. + */ + if (hopid > 0 && hopid != ret) { + tb_xdomain_release_in_hopid(xd, ret); + return -EBUSY; + } + sdev->in_hopid = ret; + return 0; +} + +static int tbstream_dev_alloc_out_hopid(struct tbstream_dev *sdev, int hopid) +{ + struct tb_xdomain *xd = tbstream_dev_xdomain(sdev); + int ret; + + if (sdev->out_hopid > 0 && sdev->out_hopid != hopid) + tb_xdomain_release_out_hopid(xd, sdev->out_hopid); + if (!hopid) { + sdev->out_hopid = hopid; + return 0; + } + ret = tb_xdomain_alloc_out_hopid(xd, hopid); + if (ret < 0) + return ret; + if (hopid > 0 && hopid != ret) { + tb_xdomain_release_out_hopid(xd, ret); + return -EBUSY; + } + sdev->out_hopid = ret; + return 0; +} + +static ssize_t +tbstream_dev_in_hopid_store(struct config_item *item, const char *buf, + size_t count) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + int ret, in_hopid; + + ret = kstrtoint(buf, 0, &in_hopid); + if (ret) + return ret; + + guard(mutex)(&sdev->lock); + if (sdev->users) + return -EBUSY; + if (sdev->stream) { + ret = tbstream_dev_alloc_in_hopid(sdev, in_hopid); + if (ret) + return ret; + ret = tbstream_dev_update_properties(sdev); + } else { + sdev->in_hopid = in_hopid; + } + return ret ? ret : count; +} +CONFIGFS_ATTR(tbstream_dev_, in_hopid); + +static ssize_t tbstream_dev_out_hopid_show(struct config_item *item, char *buf) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + + return sysfs_emit(buf, "%d\n", sdev->out_hopid); +} + +static ssize_t +tbstream_dev_out_hopid_store(struct config_item *item, const char *buf, + size_t count) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + int ret, out_hopid; + + ret = kstrtoint(buf, 0, &out_hopid); + if (ret) + return ret; + + guard(mutex)(&sdev->lock); + if (sdev->users) + return -EBUSY; + if (sdev->stream) { + ret = tbstream_dev_alloc_out_hopid(sdev, out_hopid); + if (ret) + return ret; + ret = tbstream_dev_update_properties(sdev); + } else { + sdev->out_hopid = out_hopid; + } + return ret ? ret : count; +} +CONFIGFS_ATTR(tbstream_dev_, out_hopid); + +static ssize_t tbstream_dev_ring_size_show(struct config_item *item, char *buf) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + + return sysfs_emit(buf, "%u\n", sdev->ring_size); +} + +static ssize_t +tbstream_dev_ring_size_store(struct config_item *item, const char *buf, + size_t count) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + unsigned int ring_size; + int ret; + + ret = kstrtouint(buf, 0, &ring_size); + if (ret) + return ret; + + if (ring_size < TBSTREAM_DEV_MIN_RING_SIZE || + ring_size > TBSTREAM_DEV_MAX_RING_SIZE) + return -EINVAL; + + guard(mutex)(&sdev->lock); + if (sdev->users) + return -EBUSY; + sdev->ring_size = ring_size; + return count; +} +CONFIGFS_ATTR(tbstream_dev_, ring_size); + +static ssize_t tbstream_dev_throttling_show(struct config_item *item, char *buf) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + + return sysfs_emit(buf, "%u\n", sdev->throttling); +} + +static ssize_t +tbstream_dev_throttling_store(struct config_item *item, const char *buf, + size_t count) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + unsigned int throttling; + int ret; + + ret = kstrtouint(buf, 0, &throttling); + if (ret) + return ret; + + if (throttling > TBSTREAM_DEV_MAX_THROTTLING) + return -EINVAL; + + guard(mutex)(&sdev->lock); + if (sdev->users) + return -EBUSY; + sdev->throttling = throttling; + return count; +} +CONFIGFS_ATTR(tbstream_dev_, throttling); + +static struct configfs_attribute *tbstream_dev_attrs[] = { + &tbstream_dev_attr_index, + &tbstream_dev_attr_in_hopid, + &tbstream_dev_attr_out_hopid, + &tbstream_dev_attr_ring_size, + &tbstream_dev_attr_throttling, + NULL, +}; + +static void tbstream_dev_item_release(struct config_item *item) +{ + struct config_group *group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(group); + + misc_deregister(&sdev->misc); + tbstream_dev_put(sdev); +} + +static struct configfs_item_operations tbstream_dev_item_ops = { + .release = tbstream_dev_item_release, +}; + +static const struct config_item_type tbstream_dev_type = { + .ct_owner = THIS_MODULE, + .ct_item_ops = &tbstream_dev_item_ops, + .ct_attrs = tbstream_dev_attrs, +}; + +static void service_get_hopids(struct tb_service *svc, const char *name, + int *in_hopid, int *out_hopid) +{ + struct tb_property_dir *dir; + struct tb_property *p; + + guard(mutex)(&svc->lock); + + /* See if we have directory entry with the matching name */ + p = tb_property_find(svc->remote_properties, name, + TB_PROPERTY_TYPE_DIRECTORY); + if (!p) + return; + + dir = p->value.dir; + + /* + * We need to reverse the HopIDs on our end so that in becomes + * out and vice versa. + */ + p = tb_property_find(dir, "inhopid", TB_PROPERTY_TYPE_VALUE); + if (p && p->value.immediate >= 8) + *out_hopid = p->value.immediate; + p = tb_property_find(dir, "outhopid", TB_PROPERTY_TYPE_VALUE); + if (p && p->value.immediate >= 8) + *in_hopid = p->value.immediate; +} + +static void +tbstream_dev_attach_stream(struct tbstream_dev *sdev, struct tbstream_group *sg) +{ + const char *name = config_item_name(&sdev->group.cg_item); + struct tbstream *stream; + + stream = tbstream_get(sg->stream); + if (!stream) + return; + + scoped_guard(mutex, &sdev->lock) { + sdev->stream = stream; + /* + * If there is no existing configuration (or automatic + * configuration is being used) check if the other side + * has configuration for this and use it. + */ + if (sdev->in_hopid <= 0 && sdev->out_hopid <= 0) + service_get_hopids(stream->svc, name, &sdev->in_hopid, + &sdev->out_hopid); + if (sdev->in_hopid) + tbstream_dev_alloc_in_hopid(sdev, sdev->in_hopid); + if (sdev->out_hopid) + tbstream_dev_alloc_out_hopid(sdev, sdev->out_hopid); + } + + service_update_properties(stream->svc, name, sdev->in_hopid, + sdev->out_hopid); + tb_service_properties_changed(stream->svc); + + /* Notify any openerers that the stream is now attached */ + wake_up_interruptible(&sdev->wait); +} + +static void tbstream_dev_detach_stream(struct tbstream_dev *sdev) +{ + const char *name = config_item_name(&sdev->group.cg_item); + struct tbstream *stream; + struct tb_xdomain *xd; + + scoped_guard(mutex, &sdev->lock) { + stream = sdev->stream; + if (!stream) + return; + sdev->stream = NULL; + xd = tb_service_parent(stream->svc); + if (sdev->out_hopid > 0) + tb_xdomain_release_out_hopid(xd, sdev->out_hopid); + if (sdev->in_hopid > 0) + tb_xdomain_release_in_hopid(xd, sdev->in_hopid); + } + + service_update_properties(stream->svc, name, 0, 0); + tb_service_properties_changed(stream->svc); + + tbstream_put(stream); + + /* Notify any task that the stream is not valid anymore */ + wake_up_interruptible_poll(&sdev->wait, EPOLLHUP | EPOLLERR); +} + +static inline struct tbstream_group * +to_tbstream_group(struct config_group *group) +{ + return container_of(group, struct tbstream_group, group); +} + +static struct config_group * +tbstream_dev_make_group(struct config_group *group, const char *name) +{ + struct tbstream_group *sg = to_tbstream_group(group); + struct tbstream_dev *sdev; + int ret, index; + + /* + * We want the names to be suitable for passing as property + * directory names. + */ + if (strlen(name) > TB_PROPERTY_KEY_SIZE) + return ERR_PTR(-ENAMETOOLONG); + + sdev = kzalloc_obj(*sdev, GFP_KERNEL); + if (!sdev) + return ERR_PTR(-ENOMEM); + + index = ida_alloc(&tbstream_indices, GFP_KERNEL); + if (index < 0) { + kfree(sdev); + return ERR_PTR(index); + } + + sdev->index = index; + sdev->ring_size = TBSTREAM_DEV_RING_SIZE; + sdev->throttling = TBSTREAM_DEV_THROTTLING; + mutex_init(&sdev->lock); + init_waitqueue_head(&sdev->wait); + INIT_LIST_HEAD(&sdev->list); + /* This point forward tbstream_dev_put() must be used to release sdev */ + kref_init(&sdev->kref); + + config_group_init_type_name(&sdev->group, name, &tbstream_dev_type); + + scoped_guard(mutex, &sg->lock) + list_add_tail(&sdev->list, &sg->dev_list); + + tbstream_dev_attach_stream(sdev, sg); + + sdev->misc.name = kasprintf(GFP_KERNEL, "tbstream%d", index); + sdev->misc.minor = MISC_DYNAMIC_MINOR; + sdev->misc.fops = &tbstream_dev_fops; + + ret = misc_register(&sdev->misc); + if (ret) { + tbstream_dev_detach_stream(sdev); + scoped_guard(mutex, &sg->lock) + list_del(&sdev->list); + /* Calls tbstream_dev_put() */ + config_group_put(&sdev->group); + return ERR_PTR(ret); + } + + return &sdev->group; +} + +static void +tbstream_dev_drop_item(struct config_group *group, struct config_item *item) +{ + struct config_group *sdev_group = to_config_group(item); + struct tbstream_dev *sdev = tbstream_dev_from_group(sdev_group); + struct tbstream_group *sg = to_tbstream_group(group); + + scoped_guard(mutex, &sg->lock) + list_del(&sdev->list); + /* Notify any task that the underlying group was removed */ + sdev->removed = true; + wake_up_interruptible_poll(&sdev->wait, EPOLLHUP | EPOLLERR); + config_item_put(item); +} + +static struct configfs_group_operations tbstream_dev_group_ops = { + .make_group = tbstream_dev_make_group, + .drop_item = tbstream_dev_drop_item, +}; + +static void tbstream_item_release(struct config_item *item) +{ + struct config_group *group = to_config_group(item); + struct tbstream_group *sg = to_tbstream_group(group); + + tbstream_put(sg->stream); + kfree(sg); +} + +static struct configfs_item_operations tbstream_item_ops = { + .release = tbstream_item_release, +}; + +static const struct config_item_type tbstream_dev_group_type = { + .ct_owner = THIS_MODULE, + .ct_group_ops = &tbstream_dev_group_ops, + .ct_item_ops = &tbstream_item_ops, +}; + +static struct config_group * +tbstream_make_group(struct config_group *group, const char *name) +{ + struct tbstream_group *sg; + struct tbstream *stream; + int domain, index; + u64 route; + + /* Make sure the format is correct */ + if (sscanf(name, "%u-%llx.%u", &domain, &route, &index) != 3) + return ERR_PTR(-EINVAL); + + sg = kzalloc_obj(*sg, GFP_KERNEL); + if (!sg) + return ERR_PTR(-ENOMEM); + + mutex_init(&sg->lock); + INIT_LIST_HEAD(&sg->dev_list); + + guard(mutex)(&tbstream_lock); + list_for_each_entry(stream, &tbstream_list, list) { + tbstream_get(stream); + if (sysfs_streq(name, dev_name(&stream->svc->dev))) { + sg->stream = stream; + break; + } + tbstream_put(stream); + } + + config_group_init_type_name(&sg->group, name, &tbstream_dev_group_type); + return &sg->group; +} + +static struct configfs_group_operations tbstream_group_ops = { + .make_group = tbstream_make_group, +}; + +static const struct config_item_type tbstream_group_type = { + .ct_owner = THIS_MODULE, + .ct_group_ops = &tbstream_group_ops, +}; + +static struct config_group tbstream_group = { + .cg_item = { + .ci_namebuf = "stream", + .ci_type = &tbstream_group_type, + }, +}; + +/* Returns reference count increased */ +static struct tbstream_group *tbstream_group_find(struct tbstream *stream) +{ + const char *name = dev_name(&stream->svc->dev); + struct config_item *item; + + guard(mutex)(&tbstream_group.cg_subsys->su_mutex); + item = config_group_find_item(&tbstream_group, name); + if (!item) + return NULL; + return to_tbstream_group(to_config_group(item)); +} + +static void tbstream_group_attach_stream(struct tbstream *stream) +{ + struct tbstream_group *sg; + struct tbstream_dev *sdev; + + sg = tbstream_group_find(stream); + if (!sg) + return; + + guard(mutex)(&sg->lock); + if (WARN_ON(sg->stream)) { + config_group_put(&sg->group); + return; + } + sg->stream = tbstream_get(stream); + /* + * If there are existing stream devices, attach the stream to + * them now. + */ + list_for_each_entry(sdev, &sg->dev_list, list) { + tbstream_dev_get(sdev); + tbstream_dev_attach_stream(sdev, sg); + tbstream_dev_put(sdev); + } + + config_group_put(&sg->group); +} + +static void tbstream_group_detach_stream(struct tbstream *stream) +{ + struct tbstream_group *sg; + struct tbstream_dev *sdev; + + sg = tbstream_group_find(stream); + if (!sg) + return; + + guard(mutex)(&sg->lock); + if (sg->stream) { + /* Detach this stream from the stream devices */ + list_for_each_entry_reverse(sdev, &sg->dev_list, list) { + tbstream_dev_get(sdev); + tbstream_dev_detach_stream(sdev); + tbstream_dev_put(sdev); + } + tbstream_put(sg->stream); + sg->stream = NULL; + } + + config_group_put(&sg->group); +} + +static int tbstream_probe(struct tb_service *svc, const struct tb_service_id *id) +{ + struct tbstream *stream; + + stream = kzalloc_obj(*stream, GFP_KERNEL); + if (!stream) + return -ENOMEM; + + /* After this point, release stream by calling tbstream_put() */ + kref_init(&stream->kref); + stream->svc = tb_service_get(svc); + INIT_LIST_HEAD(&stream->list); + + scoped_guard(mutex, &tbstream_lock) + list_add_tail(&stream->list, &tbstream_list); + + tbstream_group_attach_stream(stream); + tb_service_set_drvdata(svc, stream); + return 0; +} + +static void tbstream_remove(struct tb_service *svc) +{ + struct tbstream *stream = tb_service_get_drvdata(svc); + + tbstream_group_detach_stream(stream); + scoped_guard(mutex, &tbstream_lock) + list_del(&stream->list); + tbstream_put(stream); +} + +static int __maybe_unused tbstream_suspend(struct device *dev) +{ + struct tb_service *svc = tb_to_service(dev); + struct tbstream *stream = tb_service_get_drvdata(svc); + struct tbstream_group *sg; + struct tbstream_dev *sdev; + + sg = tbstream_group_find(stream); + if (!sg) + return 0; + + list_for_each_entry_reverse(sdev, &sg->dev_list, list) { + tbstream_dev_get(sdev); + /* Stop the stream (if it was open) */ + if (sdev->users) + tbstream_dev_stop(sdev); + tbstream_dev_put(sdev); + } + + config_group_put(&sg->group); + return 0; +} + +static int __maybe_unused tbstream_resume(struct device *dev) +{ + struct tb_service *svc = tb_to_service(dev); + struct tbstream *stream = tb_service_get_drvdata(svc); + struct tbstream_group *sg; + struct tbstream_dev *sdev; + + sg = tbstream_group_find(stream); + if (!sg) + return 0; + + list_for_each_entry(sdev, &sg->dev_list, list) { + tbstream_dev_get(sdev); + if (sdev->users) { + int ret; + + ret = tbstream_dev_start(sdev); + if (ret) { + tbstream_dev_put(sdev); + config_group_put(&sg->group); + return ret; + } + } + tbstream_dev_put(sdev); + } + + config_group_put(&sg->group); + return 0; +} + +static const struct dev_pm_ops tbstream_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(tbstream_suspend, tbstream_resume) +}; + +static const struct tb_service_id tbstream_ids[] = { + { TB_SERVICE("stream", 1) }, + { }, +}; +MODULE_DEVICE_TABLE(tbsvc, tbstream_ids); + +static struct tb_service_driver tbstream_driver = { + .driver = { + .owner = THIS_MODULE, + .name = "thunderbolt_stream", + .pm = &tbstream_pm_ops, + }, + .probe = tbstream_probe, + .remove = tbstream_remove, + .id_table = tbstream_ids, +}; + +static int __init tbstream_init(void) +{ + int ret; + + tbstream_dir = tb_property_create_dir(&tbstream_dir_uuid); + if (!tbstream_dir) + return -ENOMEM; + + tb_property_add_immediate(tbstream_dir, "prtcid", 1); + tb_property_add_immediate(tbstream_dir, "prtcvers", 1); + tb_property_add_immediate(tbstream_dir, "prtcrevs", 0); + tb_property_add_immediate(tbstream_dir, "prtcstns", 0); + + ret = tb_register_property_dir("stream", tbstream_dir); + if (ret) + goto err_free_dir; + + config_group_init(&tbstream_group); + ret = tb_configfs_register_group(&tbstream_group); + if (ret) + goto err_unregister_dir; + + ret = tb_register_service_driver(&tbstream_driver); + if (ret) + goto err_unregister_group; + return 0; + +err_unregister_group: + tb_configfs_unregister_group(&tbstream_group); +err_unregister_dir: + tb_unregister_property_dir("stream", tbstream_dir); +err_free_dir: + tb_property_free_dir(tbstream_dir); + return ret; +} +module_init(tbstream_init); + +static void __exit tbstream_exit(void) +{ + tb_unregister_service_driver(&tbstream_driver); + tb_configfs_unregister_group(&tbstream_group); + tb_unregister_property_dir("stream", tbstream_dir); + tb_property_free_dir(tbstream_dir); + ida_destroy(&tbstream_indices); +} +module_exit(tbstream_exit); + +MODULE_AUTHOR("Alan Borzeszkowski "); +MODULE_AUTHOR("Mika Westerberg "); +MODULE_DESCRIPTION("Stream data over Thunderbolt/USB4 cable"); +MODULE_LICENSE("GPL"); From af8922ffb322c4650dc536a236c4b42a1cf2829e Mon Sep 17 00:00:00 2001 From: Mika Westerberg Date: Wed, 6 May 2026 09:46:58 +0300 Subject: [PATCH 24/41] docs: admin-guide: thunderbolt: Add instructions how to use USB4STREAM Add instructions how USB4STREAM can be configured and used. Signed-off-by: Mika Westerberg --- Documentation/admin-guide/thunderbolt.rst | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/Documentation/admin-guide/thunderbolt.rst b/Documentation/admin-guide/thunderbolt.rst index 89df26553aa0..91a6cb109988 100644 --- a/Documentation/admin-guide/thunderbolt.rst +++ b/Documentation/admin-guide/thunderbolt.rst @@ -373,6 +373,67 @@ port which are named like ``thunderbolt0`` and so on. From this point you can either use standard userspace tools like ``ip`` to configure the interface or let your GUI handle it automatically. +Streaming data directly over Thunderbolt cable +---------------------------------------------- +In addition to Thunderbolt networking (aka. USB4NET) Linux supports +streaming data directly over a cable as well (aka. USB4STREAM). This is +possible through ``thunderbolt-stream`` driver. + +Similarly to ``thunderbolt-net`` you load the driver first on one end:: + + host1 # modprobe thunderbolt-stream + +Then you configure it via ``ConfigFS``:: + + host1 # cd /sys/kernel/config/thunderbolt/stream + host1 # mkdir -p 0-1.0/data + host1 # cd 0-1.0 + host1 # echo -1 > data/in_hopid + host1 # echo -1 > data/out_hopid + +This information is automatically announced to the other side via +XDomain properties so if you have cable connected the other side knows +that there is a stream named ``data`` available and can configure it for +you automatically:: + + host2 # cd /sys/kernel/config/thunderbolt/stream + host2 # mkdir -p 0-3.0/data + +Here we used auto-configuration but you can configure it manually too. +In that case you need to fill ``in_hopid`` and ``out_hopid`` accordingly. +If you set them to ``-1`` the next available HopID is used which is +typically what we want. + +Once they are configured you can use ``/dev/tbstreamX`` on both sides to +transfer data:: + + host2 # cat /dev/tbstream0 + host1 # dmesg > /dev/tbstream0 + +Once you are done with the stream you can remove them:: + + host2 # cd /sys/kernel/config/thunderbolt/stream + host2 # rmdir -p 0-1.0/data + host1 # cd /sys/kernel/config/thunderbolt/stream + host1 # rmdir -p 0-3.0/data + +Since streams are essentially files you can use any existing application +that supports ``read(2)`` and ``write(2)`` in some form. + +It is possible to have more than one stream and you can have both stream +and ``thunderbolt-net`` in use simultaneously. For example we can create +two streams with name ``control`` and ``data`` like this:: + + host1 # cd /sys/kernel/config/thunderbolt/stream + host1 # mkdir 0-1.0 + host1 # cd 0-1.0 + host1 # mkdir control + host1 # mkdir data + +Then you have ``/dev/tbstream0`` for ``control`` and ``/dev/tbstream1`` +for ``data``. Before you can use them you need to configure them as +shown above for the one stream case. + Forcing power ------------- Many OEMs include a method that can be used to force the power of a From afe9021d63b46233f5c87d52b820fa26e7f562cd Mon Sep 17 00:00:00 2001 From: Alan Borzeszkowski Date: Tue, 5 May 2026 16:08:34 +0200 Subject: [PATCH 25/41] thunderbolt: Improve multi-display DisplayPort tunnel allocation When 3 monitors are connected through Thunderbolt dock to the system at once, one of the monitors might fail to establish DisplayPort tunnel. This happens during DP bandwidth negotiation - each monitor takes maximum bandwidth that is supported and there might not be enough for 3rd display. In this case Thunderbolt driver drops DP tunnel and 'forgets' about it but with DP bandwidth allocation mode, that comes in later, some bandwidth might be freed. Make Thunderbolt driver check again if DP tunnel can be established after DP bandwidth consumption changed. Signed-off-by: Alan Borzeszkowski Signed-off-by: Mika Westerberg --- drivers/thunderbolt/tb.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index a9d26a2ec259..72a0dd27937e 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -2849,6 +2849,9 @@ static void tb_handle_dp_bandwidth_request(struct work_struct *work) /* Update other clients about the allocation change */ tb_recalc_estimated_bandwidth(tb); + + tb_dbg(tb, "checking if more DP tunnels can be established now\n"); + tb_tunnel_dp(tb); } put_sw: From 95c4379e37a0abea72dfd389cfe2c54452523690 Mon Sep 17 00:00:00 2001 From: Pooja Katiyar Date: Thu, 7 May 2026 14:46:30 -0700 Subject: [PATCH 26/41] thunderbolt: Don't access path config space on Lane 1 adapters in tb_switch_reset_host() USB4 Lane 1 adapters do not have accessible path config space. Skip the path config space cleanup in tb_switch_reset_host() for these ports. The check is for USB4 switches only. Thunderbolt 1-3 Lane 1 adapters stay as is because we do need to program their path config space. Co-developed-by: Rene Sapiens Signed-off-by: Rene Sapiens Signed-off-by: Pooja Katiyar Signed-off-by: Mika Westerberg --- drivers/thunderbolt/switch.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index bfcab98faf4b..ad0ec8f8ee28 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -1620,6 +1620,12 @@ static int tb_switch_reset_host(struct tb_switch *sw) ret = tb_port_reset(port); if (ret) return ret; + /* + * USB4 Lane 1 adapters do not have accessible + * path config space. + */ + if (tb_switch_is_usb4(sw) && !port->usb4) + continue; } else if (tb_port_is_usb3_down(port) || tb_port_is_usb3_up(port)) { tb_usb3_port_enable(port, false); From 0ab47718345dc58a6e2ff4c1c23a9026a2cf1310 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Wed, 6 May 2026 15:37:04 +0300 Subject: [PATCH 27/41] thunderbolt: Fix lane bonding log when bonding not possible Currently if lane bonding is not possible or not supported, we continue and read the updated number of Total Buffers from lane adapters unnecessarily and incorrectly log the bonding as successful. Fix this by bailing out early when bonding is not possible, avoiding the unnecessary read and the misleading log message. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/switch.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index ad0ec8f8ee28..f421997c298d 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -2977,14 +2977,14 @@ static int tb_switch_lane_bonding_enable(struct tb_switch *sw) int ret; if (!tb_switch_lane_bonding_possible(sw)) - return 0; + return -EOPNOTSUPP; up = tb_upstream_port(sw); down = tb_switch_downstream_port(sw); if (!tb_port_width_supported(up, TB_LINK_WIDTH_DUAL) || !tb_port_width_supported(down, TB_LINK_WIDTH_DUAL)) - return 0; + return -EOPNOTSUPP; /* * Both lanes need to be in CL0. Here we assume lane 0 already be in From b69af182b55665f5b0ccca8ed1ed239758671354 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Wed, 6 May 2026 15:37:05 +0300 Subject: [PATCH 28/41] thunderbolt: Activate path hops from source to destination Currently, path activation starts from the last hop (destination adapter) and iterates backwards to the first hop (source adapter). This does not follow the order suggested in the USB4 Connection Manager guide and could potentially cause issues with tunnelled protocols. Reverse the activation order to start from the first hop (source adapter) and end at the last hop (destination adapter), as suggested in the Connection Manager guide. Adjust the rollback in the failure path to deactivate from the first hop, since hops are now activated starting at the source. Fix kernel-doc accordingly. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/path.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/thunderbolt/path.c b/drivers/thunderbolt/path.c index 0092b2ec7873..b2c322e76b8a 100644 --- a/drivers/thunderbolt/path.c +++ b/drivers/thunderbolt/path.c @@ -484,7 +484,7 @@ void tb_path_deactivate(struct tb_path *path) * tb_path_activate() - activate a path * @path: Path to activate * - * Activate a path starting with the last hop and iterating backwards. The + * Activate a path starting with the first hop and ending on the last hop. The * caller must fill path->hops before calling tb_path_activate(). * * Return: %0 on success, negative errno otherwise. @@ -526,7 +526,7 @@ int tb_path_activate(struct tb_path *path) } /* Activate hops. */ - for (i = path->path_length - 1; i >= 0; i--) { + for (i = 0; i < path->path_length; i++) { struct tb_regs_hop hop = { 0 }; /* If it is left active deactivate it first */ @@ -576,7 +576,7 @@ int tb_path_activate(struct tb_path *path) res = tb_port_write(path->hops[i].in_port, &hop, TB_CFG_HOPS, 2 * path->hops[i].in_hop_index, 2); if (res) { - __tb_path_deactivate_hops(path, i); + __tb_path_deactivate_hops(path, 0); __tb_path_deallocate_nfc(path, 0); goto err; } From 69a7b98770b7e80deec0465d97710611a0e51774 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Wed, 6 May 2026 15:37:06 +0300 Subject: [PATCH 29/41] thunderbolt: Verify PCIe adapter in detect state before tunnel setup The USB4 Connection Manager guide suggests that a PCIe downstream and PCIe upstream adapters of the USB4 router is in the Detect state before setting up a PCIe tunnel. Add this check by verifying the LTSSM field in ADP_PCIE_CS_0 before tunnel setup. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/tb.h | 1 + drivers/thunderbolt/tb_regs.h | 15 +++++++++++++++ drivers/thunderbolt/tunnel.c | 35 +++++++++++++++++++++++++++++++++++ drivers/thunderbolt/usb4.c | 24 ++++++++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index e60f1bc3764e..003db653418a 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -1481,6 +1481,7 @@ int usb4_dp_port_allocate_bandwidth(struct tb_port *port, int bw); int usb4_dp_port_requested_bandwidth(struct tb_port *port); int usb4_pci_port_set_ext_encapsulation(struct tb_port *port, bool enable); +int usb4_pci_port_ltssm_state(struct tb_port *port); static inline bool tb_is_usb4_port_device(const struct device *dev) { diff --git a/drivers/thunderbolt/tb_regs.h b/drivers/thunderbolt/tb_regs.h index c0bf136236e6..75131fcfe044 100644 --- a/drivers/thunderbolt/tb_regs.h +++ b/drivers/thunderbolt/tb_regs.h @@ -473,10 +473,25 @@ struct tb_regs_port_header { /* PCIe adapter registers */ #define ADP_PCIE_CS_0 0x00 +#define ADP_PCIE_CS_0_LTSSM_MASK GENMASK(28, 25) #define ADP_PCIE_CS_0_PE BIT(31) #define ADP_PCIE_CS_1 0x01 #define ADP_PCIE_CS_1_EE BIT(0) +enum tb_pcie_ltssm_state { + USB4_PCIE_LTSSM_DETECT, + USB4_PCIE_LTSSM_POLLING, + USB4_PCIE_LTSSM_CONFIG, + USB4_PCIE_LTSSM_CONFIG_IDLE, + USB4_PCIE_LTSSM_RECOVERY, + USB4_PCIE_LTSSM_RECOVERY_IDLE, + USB4_PCIE_LTSSM_L0, + USB4_PCIE_LTSSM_L1, + USB4_PCIE_LTSSM_L2, + USB4_PCIE_LTSSM_DISABLED, + USB4_PCIE_LTSSM_HOT_RESET, +}; + /* USB adapter registers */ #define ADP_USB3_CS_0 0x00 #define ADP_USB3_CS_0_V BIT(30) diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c index f38f7753b6e4..b7f32305f14a 100644 --- a/drivers/thunderbolt/tunnel.c +++ b/drivers/thunderbolt/tunnel.c @@ -290,6 +290,40 @@ static inline void tb_tunnel_changed(struct tb_tunnel *tunnel) tunnel->src_port, tunnel->dst_port); } +static int tb_pci_port_ltssm_state_detect(struct tb_port *port) +{ + ktime_t timeout = ktime_add_ms(ktime_get(), 500); + + do { + int ret; + + ret = usb4_pci_port_ltssm_state(port); + if (ret < 0) + return ret; + if (ret == USB4_PCIE_LTSSM_DETECT) + return 0; + + fsleep(50); + } while (ktime_before(ktime_get(), timeout)); + + return -ETIMEDOUT; +} + +static int tb_pci_pre_activate(struct tb_tunnel *tunnel) +{ + struct tb_port *down = tunnel->src_port; + struct tb_port *up = tunnel->dst_port; + int ret; + + ret = tb_switch_is_usb4(down->sw) ? + tb_pci_port_ltssm_state_detect(down) : 0; + if (ret) + return ret; + + return tb_switch_is_usb4(up->sw) ? + tb_pci_port_ltssm_state_detect(up) : 0; +} + static int tb_pci_set_ext_encapsulation(struct tb_tunnel *tunnel, bool enable) { struct tb_port *port = tb_upstream_port(tunnel->dst_port->sw); @@ -505,6 +539,7 @@ struct tb_tunnel *tb_tunnel_alloc_pci(struct tb *tb, struct tb_port *up, if (!tunnel) return NULL; + tunnel->pre_activate = tb_pci_pre_activate; tunnel->activate = tb_pci_activate; tunnel->src_port = down; tunnel->dst_port = up; diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c index 9e810b2ae0b5..6f76bcaefa49 100644 --- a/drivers/thunderbolt/usb4.c +++ b/drivers/thunderbolt/usb4.c @@ -3145,3 +3145,27 @@ int usb4_pci_port_set_ext_encapsulation(struct tb_port *port, bool enable) return tb_port_write(port, &val, TB_CFG_PORT, port->cap_adap + ADP_PCIE_CS_1, 1); } + +/** + * usb4_pci_port_ltssm_state() - Read PCIe adapter LTSSM state + * @port: PCIe adapter + * + * Return: + * * LTSSM state of @port. + * * Negative errno - On failure. + */ +int usb4_pci_port_ltssm_state(struct tb_port *port) +{ + u32 val; + int ret; + + if (!tb_port_is_pcie_down(port) && !tb_port_is_pcie_up(port)) + return -EINVAL; + + ret = tb_port_read(port, &val, TB_CFG_PORT, + port->cap_adap + ADP_PCIE_CS_0, 1); + if (ret) + return ret; + + return FIELD_GET(ADP_PCIE_CS_0_LTSSM_MASK, val); +} From 062023c4364ffdc72978ed2de1d1435e5d4eee43 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Wed, 6 May 2026 15:37:07 +0300 Subject: [PATCH 30/41] thunderbolt: Verify Router Ready bit is set after router enumeration The USB4 Connection Manager guide specifies that after enumerating a router, the Connection Manager shall verify that the Router Ready bit (ROUTER_CS_6.RR) has been set to ensure hardware configuration has completed. Currently, this step is missing from the enumeration sequence. Add this check to follow the Connection Manager guide more closely. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/tb_regs.h | 1 + drivers/thunderbolt/usb4.c | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/tb_regs.h b/drivers/thunderbolt/tb_regs.h index 75131fcfe044..69ca4c379cc9 100644 --- a/drivers/thunderbolt/tb_regs.h +++ b/drivers/thunderbolt/tb_regs.h @@ -216,6 +216,7 @@ struct tb_regs_switch_header { #define ROUTER_CS_6_WOPS BIT(2) #define ROUTER_CS_6_WOUS BIT(3) #define ROUTER_CS_6_HCI BIT(18) +#define ROUTER_CS_6_RR BIT(24) #define ROUTER_CS_6_CR BIT(25) #define ROUTER_CS_7 0x07 #define ROUTER_CS_9 0x09 diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c index 6f76bcaefa49..54f4f5fa3c5a 100644 --- a/drivers/thunderbolt/usb4.c +++ b/drivers/thunderbolt/usb4.c @@ -294,7 +294,12 @@ int usb4_switch_setup(struct tb_switch *sw) /* TBT3 supported by the CM */ val &= ~ROUTER_CS_5_CNS; - return tb_sw_write(sw, &val, TB_CFG_SWITCH, ROUTER_CS_5, 1); + ret = tb_sw_write(sw, &val, TB_CFG_SWITCH, ROUTER_CS_5, 1); + if (ret) + return ret; + + return tb_switch_wait_for_bit(sw, ROUTER_CS_6, ROUTER_CS_6_RR, + ROUTER_CS_6_RR, 500); } /** From ba2cc385110129d03cd0f18a1b5969a430b67a18 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Wed, 6 May 2026 15:37:08 +0300 Subject: [PATCH 31/41] thunderbolt: Increase timeout for Configuration Ready bit After setting the Configuration Valid bit (ROUTER_CS_5.CV), the USB4 Connection Manager guide specifies a 500 ms timeout for the router to set the Configuration Ready bit (ROUTER_CS_6.CR). The current timeout is shorter than specified. While there, fix the kernel-doc typo. Increase the timeout to match the CM guide recommendation. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/usb4.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/thunderbolt/usb4.c b/drivers/thunderbolt/usb4.c index 54f4f5fa3c5a..5fd4fe070f25 100644 --- a/drivers/thunderbolt/usb4.c +++ b/drivers/thunderbolt/usb4.c @@ -309,7 +309,7 @@ int usb4_switch_setup(struct tb_switch *sw) * Sets configuration valid bit for the router. Must be called before * any tunnels can be set through the router and after * usb4_switch_setup() has been called. Can be called to host and device - * routers (does nothing for the latter). + * routers (does nothing for the former). * * Return: %0 on success, negative errno otherwise. */ @@ -332,7 +332,7 @@ int usb4_switch_configuration_valid(struct tb_switch *sw) return ret; return tb_switch_wait_for_bit(sw, ROUTER_CS_6, ROUTER_CS_6_CR, - ROUTER_CS_6_CR, 50); + ROUTER_CS_6_CR, 500); } /** From e24f3c0df48378214d9a67c5048d0faca144b163 Mon Sep 17 00:00:00 2001 From: Gil Fine Date: Wed, 6 May 2026 15:37:09 +0300 Subject: [PATCH 32/41] thunderbolt: Increase Notification Timeout to 255 ms for USB4 routers Currently we set the Notification Timeout field in ROUTER_CS_4 for USB4 routers to 10 ms, which is unnecessarily short and may cause unnecessary retransmissions of Hot Plug packets by the router in case of slow software response. Increase the timeout to 255 ms, aligning with Thunderbolt 3 routers and providing adequate time for software to process Hot Plug Events. While there, fix the comment describing the Notification Timeout field to match the USB4 specification. Signed-off-by: Gil Fine Signed-off-by: Mika Westerberg --- drivers/thunderbolt/switch.c | 12 +++--------- drivers/thunderbolt/tb_regs.h | 3 +-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index f421997c298d..d7c53eb3221b 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -1767,8 +1767,6 @@ int tb_switch_wait_for_bit(struct tb_switch *sw, u32 offset, u32 bit, /* * tb_plug_events_active() - enable/disable plug events on a switch * - * Also configures a sane plug_events_delay of 255ms. - * * Return: %0 on success, negative errno otherwise. */ static int tb_plug_events_active(struct tb_switch *sw, bool active) @@ -1779,11 +1777,6 @@ static int tb_plug_events_active(struct tb_switch *sw, bool active) if (tb_switch_is_icm(sw) || tb_switch_is_usb4(sw)) return 0; - sw->config.plug_events_delay = 0xff; - res = tb_sw_write(sw, ((u32 *) &sw->config) + 4, TB_CFG_SWITCH, 4, 1); - if (res) - return res; - res = tb_sw_read(sw, &data, TB_CFG_SWITCH, sw->cap_plug_events + 1, 1); if (res) return res; @@ -2645,6 +2638,8 @@ int tb_switch_configure(struct tb_switch *sw) sw->config.enabled = 1; + /* Set Notification Timeout to 255 ms for all routers */ + sw->config.plug_events_delay = 0xff; if (tb_switch_is_usb4(sw)) { /* * For USB4 devices, we need to program the CM version @@ -2656,7 +2651,6 @@ int tb_switch_configure(struct tb_switch *sw) sw->config.cmuv = ROUTER_CS_4_CMUV_V1; else sw->config.cmuv = ROUTER_CS_4_CMUV_V2; - sw->config.plug_events_delay = 0xa; /* Enumerate the switch */ ret = tb_sw_write(sw, (u32 *)&sw->config + 1, TB_CFG_SWITCH, @@ -2677,7 +2671,7 @@ int tb_switch_configure(struct tb_switch *sw) /* Enumerate the switch */ ret = tb_sw_write(sw, (u32 *)&sw->config + 1, TB_CFG_SWITCH, - ROUTER_CS_1, 3); + ROUTER_CS_1, 4); } if (ret) return ret; diff --git a/drivers/thunderbolt/tb_regs.h b/drivers/thunderbolt/tb_regs.h index 69ca4c379cc9..92d893634d2b 100644 --- a/drivers/thunderbolt/tb_regs.h +++ b/drivers/thunderbolt/tb_regs.h @@ -182,8 +182,7 @@ struct tb_regs_switch_header { /* DWORD 4 */ u32 plug_events_delay:8; /* * RW, pause between plug events in - * milliseconds. Writing 0x00 is interpreted - * as 255ms. + * milliseconds. */ u32 cmuv:8; u32 __unknown4:8; From 8c3ff7c5ae15cc71000f10f4d0f26669b9471faa Mon Sep 17 00:00:00 2001 From: Konrad Dybcio Date: Thu, 21 May 2026 12:40:00 +0200 Subject: [PATCH 33/41] thunderbolt: Move pci_device out of tb_nhi Not all USB4/TB implementations are based on a PCIe-attached controller. In order to make way for these, start off with moving the pci_device reference out of the main tb_nhi structure. Encapsulate the existing struct in a new tb_nhi_pci, that shall also house all properties that relate to the parent bus. Similarly, any other type of controller will be expected to contain tb_nhi as a member. Signed-off-by: Konrad Dybcio Signed-off-by: Mika Westerberg --- drivers/thunderbolt/acpi.c | 14 +-- drivers/thunderbolt/ctl.c | 16 ++-- drivers/thunderbolt/domain.c | 2 +- drivers/thunderbolt/eeprom.c | 2 +- drivers/thunderbolt/icm.c | 24 ++--- drivers/thunderbolt/nhi.c | 155 ++++++++++++++++++-------------- drivers/thunderbolt/nhi_ops.c | 26 +++--- drivers/thunderbolt/switch.c | 6 +- drivers/thunderbolt/tb.c | 11 +-- drivers/thunderbolt/tb.h | 10 +-- drivers/thunderbolt/usb4_port.c | 2 +- include/linux/thunderbolt.h | 8 +- 12 files changed, 154 insertions(+), 122 deletions(-) diff --git a/drivers/thunderbolt/acpi.c b/drivers/thunderbolt/acpi.c index 45d1415871b4..53546bc477a5 100644 --- a/drivers/thunderbolt/acpi.c +++ b/drivers/thunderbolt/acpi.c @@ -28,7 +28,7 @@ static acpi_status tb_acpi_add_link(acpi_handle handle, u32 level, void *data, return AE_OK; /* It needs to reference this NHI */ - if (dev_fwnode(&nhi->pdev->dev) != fwnode) + if (dev_fwnode(nhi->dev) != fwnode) goto out_put; /* @@ -57,16 +57,16 @@ static acpi_status tb_acpi_add_link(acpi_handle handle, u32 level, void *data, */ pm_runtime_get_sync(&pdev->dev); - link = device_link_add(&pdev->dev, &nhi->pdev->dev, + link = device_link_add(&pdev->dev, nhi->dev, DL_FLAG_AUTOREMOVE_SUPPLIER | DL_FLAG_RPM_ACTIVE | DL_FLAG_PM_RUNTIME); if (link) { - dev_dbg(&nhi->pdev->dev, "created link from %s\n", + dev_dbg(nhi->dev, "created link from %s\n", dev_name(&pdev->dev)); *(bool *)ret = true; } else { - dev_warn(&nhi->pdev->dev, "device link creation from %s failed\n", + dev_warn(nhi->dev, "device link creation from %s failed\n", dev_name(&pdev->dev)); } @@ -93,7 +93,7 @@ bool tb_acpi_add_links(struct tb_nhi *nhi) acpi_status status; bool ret = false; - if (!has_acpi_companion(&nhi->pdev->dev)) + if (!has_acpi_companion(nhi->dev)) return false; /* @@ -103,7 +103,7 @@ bool tb_acpi_add_links(struct tb_nhi *nhi) status = acpi_walk_namespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT, 32, tb_acpi_add_link, NULL, nhi, (void **)&ret); if (ACPI_FAILURE(status)) { - dev_warn(&nhi->pdev->dev, "failed to enumerate tunneled ports\n"); + dev_warn(nhi->dev, "failed to enumerate tunneled ports\n"); return false; } @@ -305,7 +305,7 @@ static struct acpi_device *tb_acpi_switch_find_companion(struct tb_switch *sw) struct tb_nhi *nhi = sw->tb->nhi; struct acpi_device *parent_adev; - parent_adev = ACPI_COMPANION(&nhi->pdev->dev); + parent_adev = ACPI_COMPANION(nhi->dev); if (parent_adev) adev = acpi_find_child_device(parent_adev, 0, false); } diff --git a/drivers/thunderbolt/ctl.c b/drivers/thunderbolt/ctl.c index b2fd60fc7bcc..cd47b627f97b 100644 --- a/drivers/thunderbolt/ctl.c +++ b/drivers/thunderbolt/ctl.c @@ -56,22 +56,22 @@ struct tb_ctl { #define tb_ctl_WARN(ctl, format, arg...) \ - dev_WARN(&(ctl)->nhi->pdev->dev, format, ## arg) + dev_WARN((ctl)->nhi->dev, format, ## arg) #define tb_ctl_err(ctl, format, arg...) \ - dev_err(&(ctl)->nhi->pdev->dev, format, ## arg) + dev_err((ctl)->nhi->dev, format, ## arg) #define tb_ctl_warn(ctl, format, arg...) \ - dev_warn(&(ctl)->nhi->pdev->dev, format, ## arg) + dev_warn((ctl)->nhi->dev, format, ## arg) #define tb_ctl_info(ctl, format, arg...) \ - dev_info(&(ctl)->nhi->pdev->dev, format, ## arg) + dev_info((ctl)->nhi->dev, format, ## arg) #define tb_ctl_dbg(ctl, format, arg...) \ - dev_dbg(&(ctl)->nhi->pdev->dev, format, ## arg) + dev_dbg((ctl)->nhi->dev, format, ## arg) #define tb_ctl_dbg_once(ctl, format, arg...) \ - dev_dbg_once(&(ctl)->nhi->pdev->dev, format, ## arg) + dev_dbg_once((ctl)->nhi->dev, format, ## arg) static DECLARE_WAIT_QUEUE_HEAD(tb_cfg_request_cancel_queue); /* Serializes access to request kref_get/put */ @@ -666,8 +666,8 @@ struct tb_ctl *tb_ctl_alloc(struct tb_nhi *nhi, int index, int timeout_msec, mutex_init(&ctl->request_queue_lock); INIT_LIST_HEAD(&ctl->request_queue); - ctl->frame_pool = dma_pool_create("thunderbolt_ctl", &nhi->pdev->dev, - TB_FRAME_SIZE, 4, 0); + ctl->frame_pool = dma_pool_create("thunderbolt_ctl", nhi->dev, + TB_FRAME_SIZE, 4, 0); if (!ctl->frame_pool) goto err; diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c index b381f184340e..479fa4d265c2 100644 --- a/drivers/thunderbolt/domain.c +++ b/drivers/thunderbolt/domain.c @@ -405,7 +405,7 @@ struct tb *tb_domain_alloc(struct tb_nhi *nhi, int timeout_msec, size_t privsize if (!tb->ctl) goto err_destroy_wq; - tb->dev.parent = &nhi->pdev->dev; + tb->dev.parent = nhi->dev; tb->dev.bus = &tb_bus_type; tb->dev.type = &tb_domain_type; tb->dev.groups = domain_attr_groups; diff --git a/drivers/thunderbolt/eeprom.c b/drivers/thunderbolt/eeprom.c index 5477b9437048..5681c17f82ec 100644 --- a/drivers/thunderbolt/eeprom.c +++ b/drivers/thunderbolt/eeprom.c @@ -465,7 +465,7 @@ static void tb_switch_drom_free(struct tb_switch *sw) */ static int tb_drom_copy_efi(struct tb_switch *sw, u16 *size) { - struct device *dev = &sw->tb->nhi->pdev->dev; + struct device *dev = sw->tb->nhi->dev; int len, res; len = device_property_count_u8(dev, "ThunderboltDROM"); diff --git a/drivers/thunderbolt/icm.c b/drivers/thunderbolt/icm.c index c492995166f7..10fefac3b1d9 100644 --- a/drivers/thunderbolt/icm.c +++ b/drivers/thunderbolt/icm.c @@ -1466,6 +1466,7 @@ static struct pci_dev *get_upstream_port(struct pci_dev *pdev) static bool icm_ar_is_supported(struct tb *tb) { + struct pci_dev *pdev = to_pci_dev(tb->nhi->dev); struct pci_dev *upstream_port; struct icm *icm = tb_priv(tb); @@ -1483,7 +1484,7 @@ static bool icm_ar_is_supported(struct tb *tb) * Find the upstream PCIe port in case we need to do reset * through its vendor specific registers. */ - upstream_port = get_upstream_port(tb->nhi->pdev); + upstream_port = get_upstream_port(pdev); if (upstream_port) { int cap; @@ -1519,7 +1520,7 @@ static int icm_ar_get_mode(struct tb *tb) } while (--retries); if (!retries) { - dev_err(&nhi->pdev->dev, "ICM firmware not authenticated\n"); + dev_err(nhi->dev, "ICM firmware not authenticated\n"); return -ENODEV; } @@ -1685,11 +1686,11 @@ icm_icl_driver_ready(struct tb *tb, enum tb_security_level *security_level, static void icm_icl_set_uuid(struct tb *tb) { - struct tb_nhi *nhi = tb->nhi; + struct pci_dev *pdev = to_pci_dev(tb->nhi->dev); u32 uuid[4]; - pci_read_config_dword(nhi->pdev, VS_CAP_10, &uuid[0]); - pci_read_config_dword(nhi->pdev, VS_CAP_11, &uuid[1]); + pci_read_config_dword(pdev, VS_CAP_10, &uuid[0]); + pci_read_config_dword(pdev, VS_CAP_11, &uuid[1]); uuid[2] = 0xffffffff; uuid[3] = 0xffffffff; @@ -1866,7 +1867,7 @@ static int icm_firmware_start(struct tb *tb, struct tb_nhi *nhi) if (icm_firmware_running(nhi)) return 0; - dev_dbg(&nhi->pdev->dev, "starting ICM firmware\n"); + dev_dbg(nhi->dev, "starting ICM firmware\n"); ret = icm_firmware_reset(tb, nhi); if (ret) @@ -1961,7 +1962,7 @@ static int icm_firmware_init(struct tb *tb) ret = icm_firmware_start(tb, nhi); if (ret) { - dev_err(&nhi->pdev->dev, "could not start ICM firmware\n"); + dev_err(nhi->dev, "could not start ICM firmware\n"); return ret; } @@ -1993,10 +1994,10 @@ static int icm_firmware_init(struct tb *tb) */ ret = icm_reset_phy_port(tb, 0); if (ret) - dev_warn(&nhi->pdev->dev, "failed to reset links on port0\n"); + dev_warn(nhi->dev, "failed to reset links on port0\n"); ret = icm_reset_phy_port(tb, 1); if (ret) - dev_warn(&nhi->pdev->dev, "failed to reset links on port1\n"); + dev_warn(nhi->dev, "failed to reset links on port1\n"); return 0; } @@ -2477,6 +2478,7 @@ static const struct tb_cm_ops icm_icl_ops = { struct tb *icm_probe(struct tb_nhi *nhi) { + struct pci_dev *pdev = to_pci_dev(nhi->dev); struct icm *icm; struct tb *tb; @@ -2488,7 +2490,7 @@ struct tb *icm_probe(struct tb_nhi *nhi) INIT_DELAYED_WORK(&icm->rescan_work, icm_rescan_work); mutex_init(&icm->request_lock); - switch (nhi->pdev->device) { + switch (pdev->device) { case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: icm->can_upgrade_nvm = true; @@ -2594,7 +2596,7 @@ struct tb *icm_probe(struct tb_nhi *nhi) } if (!icm->is_supported || !icm->is_supported(tb)) { - dev_dbg(&nhi->pdev->dev, "ICM not supported on this controller\n"); + dev_dbg(nhi->dev, "ICM not supported on this controller\n"); tb_domain_put(tb); return NULL; } diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index a0a789bfb680..d21c8d330f6c 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -2,7 +2,7 @@ /* * Thunderbolt driver - NHI driver * - * The NHI (native host interface) is the pci device that allows us to send and + * The NHI (native host interface) is the device that allows us to send and * receive frames from the thunderbolt bus. * * Copyright (c) 2014 Andreas Noever @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -51,6 +50,21 @@ static bool host_reset = true; module_param(host_reset, bool, 0444); MODULE_PARM_DESC(host_reset, "reset USB4 host router (default: true)"); +/** + * struct tb_nhi_pci - NHI device connected over PCIe + * @nhi: NHI device + * @msix_ida: Used to allocate MSI-X vectors for rings + */ +struct tb_nhi_pci { + struct tb_nhi nhi; + struct ida msix_ida; +}; + +static inline struct tb_nhi_pci *nhi_to_pci(struct tb_nhi *nhi) +{ + return container_of(nhi, struct tb_nhi_pci, nhi); +} + static int ring_interrupt_index(const struct tb_ring *ring) { int bit = ring->hop; @@ -145,15 +159,14 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) else new = old & ~mask; - dev_dbg(&ring->nhi->pdev->dev, + dev_dbg(ring->nhi->dev, "%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->pdev->dev, - "interrupt for %s %d is already %s\n", - RING_TYPE(ring), ring->hop, - str_enabled_disabled(active)); + 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); @@ -470,19 +483,21 @@ static irqreturn_t ring_msix(int irq, void *data) static int ring_request_msix(struct tb_ring *ring, bool no_suspend) { struct tb_nhi *nhi = ring->nhi; + struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); + struct pci_dev *pdev = to_pci_dev(nhi->dev); unsigned long irqflags; int ret; - if (!nhi->pdev->msix_enabled) + if (!pdev->msix_enabled) return 0; - ret = ida_alloc_max(&nhi->msix_ida, MSIX_MAX_VECS - 1, GFP_KERNEL); + ret = ida_alloc_max(&nhi_pci->msix_ida, MSIX_MAX_VECS - 1, GFP_KERNEL); if (ret < 0) return ret; ring->vector = ret; - ret = pci_irq_vector(ring->nhi->pdev, ring->vector); + ret = pci_irq_vector(pdev, ring->vector); if (ret < 0) goto err_ida_remove; @@ -496,18 +511,20 @@ static int ring_request_msix(struct tb_ring *ring, bool no_suspend) return 0; err_ida_remove: - ida_free(&nhi->msix_ida, ring->vector); + ida_free(&nhi_pci->msix_ida, ring->vector); return ret; } static void ring_release_msix(struct tb_ring *ring) { + struct tb_nhi_pci *nhi_pci = nhi_to_pci(ring->nhi); + if (ring->irq <= 0) return; free_irq(ring->irq, ring); - ida_free(&ring->nhi->msix_ida, ring->vector); + ida_free(&nhi_pci->msix_ida, ring->vector); ring->vector = 0; ring->irq = 0; } @@ -520,7 +537,7 @@ static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring) if (nhi->quirks & QUIRK_E2E) { start_hop = RING_FIRST_USABLE_HOPID + 1; if (ring->flags & RING_FLAG_E2E && !ring->is_tx) { - dev_dbg(&nhi->pdev->dev, "quirking E2E TX HopID %u -> %u\n", + dev_dbg(nhi->dev, "quirking E2E TX HopID %u -> %u\n", ring->e2e_tx_hop, RING_E2E_RESERVED_HOPID); ring->e2e_tx_hop = RING_E2E_RESERVED_HOPID; } @@ -551,23 +568,23 @@ static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring) } if (ring->hop > 0 && ring->hop < start_hop) { - dev_warn(&nhi->pdev->dev, "invalid hop: %d\n", ring->hop); + dev_warn(nhi->dev, "invalid hop: %d\n", ring->hop); ret = -EINVAL; goto err_unlock; } if (ring->hop < 0 || ring->hop >= nhi->hop_count) { - dev_warn(&nhi->pdev->dev, "invalid hop: %d\n", ring->hop); + dev_warn(nhi->dev, "invalid hop: %d\n", ring->hop); ret = -EINVAL; goto err_unlock; } if (ring->is_tx && nhi->tx_rings[ring->hop]) { - dev_warn(&nhi->pdev->dev, "TX hop %d already allocated\n", + dev_warn(nhi->dev, "TX hop %d already allocated\n", ring->hop); ret = -EBUSY; goto err_unlock; } if (!ring->is_tx && nhi->rx_rings[ring->hop]) { - dev_warn(&nhi->pdev->dev, "RX hop %d already allocated\n", + dev_warn(nhi->dev, "RX hop %d already allocated\n", ring->hop); ret = -EBUSY; goto err_unlock; @@ -592,7 +609,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, { struct tb_ring *ring = NULL; - dev_dbg(&nhi->pdev->dev, "allocating %s ring %d of size %d\n", + dev_dbg(nhi->dev, "allocating %s ring %d of size %d\n", transmit ? "TX" : "RX", hop, size); ring = kzalloc_obj(*ring); @@ -619,9 +636,9 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, ring->start_poll = start_poll; ring->poll_data = poll_data; - ring->descriptors = dma_alloc_coherent(&ring->nhi->pdev->dev, - size * sizeof(*ring->descriptors), - &ring->descriptors_dma, GFP_KERNEL | __GFP_ZERO); + ring->descriptors = dma_alloc_coherent(ring->nhi->dev, + size * sizeof(*ring->descriptors), + &ring->descriptors_dma, GFP_KERNEL | __GFP_ZERO); if (!ring->descriptors) goto err_free_ring; @@ -636,7 +653,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, err_release_msix: ring_release_msix(ring); err_free_descs: - dma_free_coherent(&ring->nhi->pdev->dev, + dma_free_coherent(ring->nhi->dev, ring->size * sizeof(*ring->descriptors), ring->descriptors, ring->descriptors_dma); err_free_ring: @@ -703,10 +720,10 @@ void tb_ring_start(struct tb_ring *ring) if (ring->nhi->going_away) goto err; if (ring->running) { - dev_WARN(&ring->nhi->pdev->dev, "ring already started\n"); + dev_WARN(ring->nhi->dev, "ring already started\n"); goto err; } - dev_dbg(&ring->nhi->pdev->dev, "starting %s %d\n", + dev_dbg(ring->nhi->dev, "starting %s %d\n", RING_TYPE(ring), ring->hop); if (ring->flags & RING_FLAG_FRAME) { @@ -743,11 +760,11 @@ void tb_ring_start(struct tb_ring *ring) hop &= REG_RX_OPTIONS_E2E_HOP_MASK; flags |= hop; - dev_dbg(&ring->nhi->pdev->dev, + dev_dbg(ring->nhi->dev, "enabling E2E for %s %d with TX HopID %d\n", RING_TYPE(ring), ring->hop, ring->e2e_tx_hop); } else { - dev_dbg(&ring->nhi->pdev->dev, "enabling E2E for %s %d\n", + dev_dbg(ring->nhi->dev, "enabling E2E for %s %d\n", RING_TYPE(ring), ring->hop); } @@ -806,12 +823,12 @@ void tb_ring_stop(struct tb_ring *ring) { spin_lock_irq(&ring->nhi->lock); spin_lock(&ring->lock); - dev_dbg(&ring->nhi->pdev->dev, "stopping %s %d\n", + dev_dbg(ring->nhi->dev, "stopping %s %d\n", RING_TYPE(ring), ring->hop); if (ring->nhi->going_away) goto err; if (!ring->running) { - dev_WARN(&ring->nhi->pdev->dev, "%s %d already stopped\n", + dev_WARN(ring->nhi->dev, "%s %d already stopped\n", RING_TYPE(ring), ring->hop); goto err; } @@ -860,14 +877,14 @@ void tb_ring_free(struct tb_ring *ring) ring->nhi->rx_rings[ring->hop] = NULL; if (ring->running) { - dev_WARN(&ring->nhi->pdev->dev, "%s %d still running\n", + dev_WARN(ring->nhi->dev, "%s %d still running\n", RING_TYPE(ring), ring->hop); } spin_unlock_irq(&ring->nhi->lock); ring_release_msix(ring); - dma_free_coherent(&ring->nhi->pdev->dev, + dma_free_coherent(ring->nhi->dev, ring->size * sizeof(*ring->descriptors), ring->descriptors, ring->descriptors_dma); @@ -875,7 +892,7 @@ void tb_ring_free(struct tb_ring *ring) ring->descriptors_dma = 0; - dev_dbg(&ring->nhi->pdev->dev, "freeing %s %d\n", RING_TYPE(ring), + dev_dbg(ring->nhi->dev, "freeing %s %d\n", RING_TYPE(ring), ring->hop); /* @@ -994,9 +1011,7 @@ static void nhi_interrupt_work(struct work_struct *work) if ((value & (1 << (bit % 32))) == 0) continue; if (type == 2) { - dev_warn(&nhi->pdev->dev, - "RX overflow for ring %d\n", - hop); + dev_warn(nhi->dev, "RX overflow for ring %d\n", hop); continue; } if (type == 0) @@ -1004,7 +1019,7 @@ static void nhi_interrupt_work(struct work_struct *work) else ring = nhi->rx_rings[hop]; if (ring == NULL) { - dev_warn(&nhi->pdev->dev, + dev_warn(nhi->dev, "got interrupt for inactive %s ring %d\n", type ? "RX" : "TX", hop); @@ -1173,16 +1188,18 @@ static int nhi_runtime_resume(struct device *dev) static void nhi_shutdown(struct tb_nhi *nhi) { + struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); + struct pci_dev *pdev = to_pci_dev(nhi->dev); int i; - dev_dbg(&nhi->pdev->dev, "shutdown\n"); + dev_dbg(nhi->dev, "shutdown\n"); for (i = 0; i < nhi->hop_count; i++) { if (nhi->tx_rings[i]) - dev_WARN(&nhi->pdev->dev, + dev_WARN(nhi->dev, "TX ring %d is still active\n", i); if (nhi->rx_rings[i]) - dev_WARN(&nhi->pdev->dev, + dev_WARN(nhi->dev, "RX ring %d is still active\n", i); } nhi_disable_interrupts(nhi); @@ -1190,19 +1207,22 @@ static void nhi_shutdown(struct tb_nhi *nhi) * We have to release the irq before calling flush_work. Otherwise an * already executing IRQ handler could call schedule_work again. */ - if (!nhi->pdev->msix_enabled) { - devm_free_irq(&nhi->pdev->dev, nhi->pdev->irq, nhi); + if (!pdev->msix_enabled) { + devm_free_irq(nhi->dev, pdev->irq, nhi); flush_work(&nhi->interrupt_work); } - ida_destroy(&nhi->msix_ida); + ida_destroy(&nhi_pci->msix_ida); if (nhi->ops && nhi->ops->shutdown) nhi->ops->shutdown(nhi); } -static void nhi_check_quirks(struct tb_nhi *nhi) +static void nhi_check_quirks(struct tb_nhi_pci *nhi_pci) { - if (nhi->pdev->vendor == PCI_VENDOR_ID_INTEL) { + struct tb_nhi *nhi = &nhi_pci->nhi; + struct pci_dev *pdev = to_pci_dev(nhi->dev); + + if (pdev->vendor == PCI_VENDOR_ID_INTEL) { /* * Intel hardware supports auto clear of the interrupt * status register right after interrupt is being @@ -1210,7 +1230,7 @@ static void nhi_check_quirks(struct tb_nhi *nhi) */ nhi->quirks |= QUIRK_AUTO_CLEAR_INT; - switch (nhi->pdev->device) { + switch (pdev->device) { case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: /* @@ -1224,7 +1244,7 @@ static void nhi_check_quirks(struct tb_nhi *nhi) } } -static int nhi_check_iommu_pdev(struct pci_dev *pdev, void *data) +static int nhi_check_iommu_pci_dev(struct pci_dev *pdev, void *data) { if (!pdev->external_facing || !device_iommu_capable(&pdev->dev, IOMMU_CAP_PRE_BOOT_PROTECTION)) @@ -1233,9 +1253,11 @@ static int nhi_check_iommu_pdev(struct pci_dev *pdev, void *data) return 1; /* Stop walking */ } -static void nhi_check_iommu(struct tb_nhi *nhi) +static void nhi_check_iommu(struct tb_nhi_pci *nhi_pci) { - struct pci_bus *bus = nhi->pdev->bus; + struct tb_nhi *nhi = &nhi_pci->nhi; + struct pci_dev *pdev = to_pci_dev(nhi->dev); + struct pci_bus *bus = pdev->bus; bool port_ok = false; /* @@ -1258,10 +1280,10 @@ static void nhi_check_iommu(struct tb_nhi *nhi) while (bus->parent) bus = bus->parent; - pci_walk_bus(bus, nhi_check_iommu_pdev, &port_ok); + pci_walk_bus(bus, nhi_check_iommu_pci_dev, &port_ok); nhi->iommu_dma_protection = port_ok; - dev_dbg(&nhi->pdev->dev, "IOMMU DMA protection is %s\n", + dev_dbg(nhi->dev, "IOMMU DMA protection is %s\n", str_enabled_disabled(port_ok)); } @@ -1276,7 +1298,7 @@ static void nhi_reset(struct tb_nhi *nhi) return; if (!host_reset) { - dev_dbg(&nhi->pdev->dev, "skipping host router reset\n"); + dev_dbg(nhi->dev, "skipping host router reset\n"); return; } @@ -1287,25 +1309,23 @@ static void nhi_reset(struct tb_nhi *nhi) do { val = ioread32(nhi->iobase + REG_RESET); if (!(val & REG_RESET_HRR)) { - dev_warn(&nhi->pdev->dev, "host router reset successful\n"); + dev_warn(nhi->dev, "host router reset successful\n"); return; } usleep_range(10, 20); } while (ktime_before(ktime_get(), timeout)); - dev_warn(&nhi->pdev->dev, "timeout resetting host router\n"); + dev_warn(nhi->dev, "timeout resetting host router\n"); } -static int nhi_init_msi(struct tb_nhi *nhi) +static int nhi_init_msi(struct tb_nhi_pci *nhi_pci) { - struct pci_dev *pdev = nhi->pdev; + struct tb_nhi *nhi = &nhi_pci->nhi; + struct pci_dev *pdev = to_pci_dev(nhi->dev); struct device *dev = &pdev->dev; int res, irq, nvec; - /* In case someone left them on. */ - nhi_disable_interrupts(nhi); - - ida_init(&nhi->msix_ida); + ida_init(&nhi_pci->msix_ida); /* * The NHI has 16 MSI-X vectors or a single MSI. We first try to @@ -1322,7 +1342,7 @@ static int nhi_init_msi(struct tb_nhi *nhi) INIT_WORK(&nhi->interrupt_work, nhi_interrupt_work); - irq = pci_irq_vector(nhi->pdev, 0); + irq = pci_irq_vector(pdev, 0); if (irq < 0) return irq; @@ -1371,6 +1391,7 @@ static struct tb *nhi_select_cm(struct tb_nhi *nhi) static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct device *dev = &pdev->dev; + struct tb_nhi_pci *nhi_pci; struct tb_nhi *nhi; struct tb *tb; int res; @@ -1382,11 +1403,12 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) if (res) return dev_err_probe(dev, res, "cannot enable PCI device, aborting\n"); - nhi = devm_kzalloc(&pdev->dev, sizeof(*nhi), GFP_KERNEL); - if (!nhi) + nhi_pci = devm_kzalloc(dev, sizeof(*nhi_pci), GFP_KERNEL); + if (!nhi_pci) return -ENOMEM; - nhi->pdev = pdev; + nhi = &nhi_pci->nhi; + nhi->dev = dev; nhi->ops = (const struct tb_nhi_ops *)id->driver_data; nhi->iobase = pcim_iomap_region(pdev, 0, "thunderbolt"); @@ -1404,11 +1426,14 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) if (!nhi->tx_rings || !nhi->rx_rings) return -ENOMEM; - nhi_check_quirks(nhi); - nhi_check_iommu(nhi); + nhi_check_quirks(nhi_pci); + nhi_check_iommu(nhi_pci); nhi_reset(nhi); - res = nhi_init_msi(nhi); + /* In case someone left them on. */ + nhi_disable_interrupts(nhi); + + res = nhi_init_msi(nhi_pci); if (res) return dev_err_probe(dev, res, "cannot enable MSI, aborting\n"); diff --git a/drivers/thunderbolt/nhi_ops.c b/drivers/thunderbolt/nhi_ops.c index 96da07e88c52..8c50066f3411 100644 --- a/drivers/thunderbolt/nhi_ops.c +++ b/drivers/thunderbolt/nhi_ops.c @@ -24,7 +24,7 @@ static int check_for_device(struct device *dev, void *data) static bool icl_nhi_is_device_connected(struct tb_nhi *nhi) { - struct tb *tb = pci_get_drvdata(nhi->pdev); + struct tb *tb = dev_get_drvdata(nhi->dev); int ret; ret = device_for_each_child(&tb->root_switch->dev, NULL, @@ -34,6 +34,7 @@ static bool icl_nhi_is_device_connected(struct tb_nhi *nhi) static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) { + struct pci_dev *pdev = to_pci_dev(nhi->dev); u32 vs_cap; /* @@ -48,7 +49,7 @@ static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) * The actual power management happens inside shared ACPI power * resources using standard ACPI methods. */ - pci_read_config_dword(nhi->pdev, VS_CAP_22, &vs_cap); + pci_read_config_dword(pdev, VS_CAP_22, &vs_cap); if (power) { vs_cap &= ~VS_CAP_22_DMA_DELAY_MASK; vs_cap |= 0x22 << VS_CAP_22_DMA_DELAY_SHIFT; @@ -56,7 +57,7 @@ static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) } else { vs_cap &= ~VS_CAP_22_FORCE_POWER; } - pci_write_config_dword(nhi->pdev, VS_CAP_22, vs_cap); + pci_write_config_dword(pdev, VS_CAP_22, vs_cap); if (power) { unsigned int retries = 350; @@ -64,7 +65,7 @@ static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) /* Wait until the firmware tells it is up and running */ do { - pci_read_config_dword(nhi->pdev, VS_CAP_9, &val); + pci_read_config_dword(pdev, VS_CAP_9, &val); if (val & VS_CAP_9_FW_READY) return 0; usleep_range(3000, 3100); @@ -78,14 +79,16 @@ static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) static void icl_nhi_lc_mailbox_cmd(struct tb_nhi *nhi, enum icl_lc_mailbox_cmd cmd) { + struct pci_dev *pdev = to_pci_dev(nhi->dev); u32 data; data = (cmd << VS_CAP_19_CMD_SHIFT) & VS_CAP_19_CMD_MASK; - pci_write_config_dword(nhi->pdev, VS_CAP_19, data | VS_CAP_19_VALID); + pci_write_config_dword(pdev, VS_CAP_19, data | VS_CAP_19_VALID); } static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout) { + struct pci_dev *pdev = to_pci_dev(nhi->dev); unsigned long end; u32 data; @@ -94,7 +97,7 @@ static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout) end = jiffies + msecs_to_jiffies(timeout); do { - pci_read_config_dword(nhi->pdev, VS_CAP_18, &data); + pci_read_config_dword(pdev, VS_CAP_18, &data); if (data & VS_CAP_18_DONE) goto clear; usleep_range(1000, 1100); @@ -104,24 +107,25 @@ static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout) clear: /* Clear the valid bit */ - pci_write_config_dword(nhi->pdev, VS_CAP_19, 0); + pci_write_config_dword(pdev, VS_CAP_19, 0); return 0; } static void icl_nhi_set_ltr(struct tb_nhi *nhi) { + struct pci_dev *pdev = to_pci_dev(nhi->dev); u32 max_ltr, ltr; - pci_read_config_dword(nhi->pdev, VS_CAP_16, &max_ltr); + pci_read_config_dword(pdev, VS_CAP_16, &max_ltr); max_ltr &= 0xffff; /* Program the same value for both snoop and no-snoop */ ltr = max_ltr << 16 | max_ltr; - pci_write_config_dword(nhi->pdev, VS_CAP_15, ltr); + pci_write_config_dword(pdev, VS_CAP_15, ltr); } static int icl_nhi_suspend(struct tb_nhi *nhi) { - struct tb *tb = pci_get_drvdata(nhi->pdev); + struct tb *tb = dev_get_drvdata(nhi->dev); int ret; if (icl_nhi_is_device_connected(nhi)) @@ -144,7 +148,7 @@ static int icl_nhi_suspend(struct tb_nhi *nhi) static int icl_nhi_suspend_noirq(struct tb_nhi *nhi, bool wakeup) { - struct tb *tb = pci_get_drvdata(nhi->pdev); + struct tb *tb = dev_get_drvdata(nhi->dev); enum icl_lc_mailbox_cmd cmd; if (!pm_suspend_via_firmware()) diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index d7c53eb3221b..769b57ff276b 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -211,6 +211,7 @@ static int nvm_authenticate_device_dma_port(struct tb_switch *sw) static void nvm_authenticate_start_dma_port(struct tb_switch *sw) { + struct pci_dev *pdev = to_pci_dev(sw->tb->nhi->dev); struct pci_dev *root_port; /* @@ -219,16 +220,17 @@ static void nvm_authenticate_start_dma_port(struct tb_switch *sw) * itself. To be on the safe side keep the root port in D0 during * the whole upgrade process. */ - root_port = pcie_find_root_port(sw->tb->nhi->pdev); + root_port = pcie_find_root_port(pdev); if (root_port) pm_runtime_get_noresume(&root_port->dev); } static void nvm_authenticate_complete_dma_port(struct tb_switch *sw) { + struct pci_dev *pdev = to_pci_dev(sw->tb->nhi->dev); struct pci_dev *root_port; - root_port = pcie_find_root_port(sw->tb->nhi->pdev); + root_port = pcie_find_root_port(pdev); if (root_port) pm_runtime_put(&root_port->dev); } diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index 72a0dd27937e..d60c0b8eb390 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -3310,13 +3310,14 @@ static const struct tb_cm_ops tb_cm_ops = { */ 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) { + 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: @@ -3326,7 +3327,7 @@ static bool tb_apple_add_links(struct tb_nhi *nhi) return false; } - upstream = pci_upstream_bridge(nhi->pdev); + upstream = pci_upstream_bridge(nhi_pdev); while (upstream) { if (!pci_is_pcie(upstream)) return false; @@ -3353,15 +3354,15 @@ static bool tb_apple_add_links(struct tb_nhi *nhi) !pdev->is_pciehp) continue; - link = device_link_add(&pdev->dev, &nhi->pdev->dev, + link = device_link_add(&pdev->dev, nhi->dev, DL_FLAG_AUTOREMOVE_SUPPLIER | DL_FLAG_PM_RUNTIME); if (link) { - dev_dbg(&nhi->pdev->dev, "created link from %s\n", + dev_dbg(nhi->dev, "created link from %s\n", dev_name(&pdev->dev)); ret = true; } else { - dev_warn(&nhi->pdev->dev, "device link creation from %s failed\n", + dev_warn(nhi->dev, "device link creation from %s failed\n", dev_name(&pdev->dev)); } } diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h index 003db653418a..ec9192b61bc0 100644 --- a/drivers/thunderbolt/tb.h +++ b/drivers/thunderbolt/tb.h @@ -725,11 +725,11 @@ static inline int tb_port_write(struct tb_port *port, const void *buffer, length); } -#define tb_err(tb, fmt, arg...) dev_err(&(tb)->nhi->pdev->dev, fmt, ## arg) -#define tb_WARN(tb, fmt, arg...) dev_WARN(&(tb)->nhi->pdev->dev, fmt, ## arg) -#define tb_warn(tb, fmt, arg...) dev_warn(&(tb)->nhi->pdev->dev, fmt, ## arg) -#define tb_info(tb, fmt, arg...) dev_info(&(tb)->nhi->pdev->dev, fmt, ## arg) -#define tb_dbg(tb, fmt, arg...) dev_dbg(&(tb)->nhi->pdev->dev, fmt, ## arg) +#define tb_err(tb, fmt, arg...) dev_err((tb)->nhi->dev, fmt, ## arg) +#define tb_WARN(tb, fmt, arg...) dev_WARN((tb)->nhi->dev, fmt, ## arg) +#define tb_warn(tb, fmt, arg...) dev_warn((tb)->nhi->dev, fmt, ## arg) +#define tb_info(tb, fmt, arg...) dev_info((tb)->nhi->dev, fmt, ## arg) +#define tb_dbg(tb, fmt, arg...) dev_dbg((tb)->nhi->dev, fmt, ## arg) #define __TB_SW_PRINT(level, sw, fmt, arg...) \ do { \ diff --git a/drivers/thunderbolt/usb4_port.c b/drivers/thunderbolt/usb4_port.c index c32d3516e780..890de530debc 100644 --- a/drivers/thunderbolt/usb4_port.c +++ b/drivers/thunderbolt/usb4_port.c @@ -138,7 +138,7 @@ bool usb4_usb3_port_match(struct device *usb4_port_dev, return false; /* Check if USB3 fwnode references same NHI where USB4 port resides */ - if (!device_match_fwnode(&nhi->pdev->dev, nhi_fwnode)) + if (!device_match_fwnode(nhi->dev, nhi_fwnode)) return false; if (fwnode_property_read_u8(usb3_port_fwnode, "usb4-port-number", &usb4_port_num)) diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index 0be9b298e692..b5659f883517 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -498,12 +498,11 @@ void tb_service_properties_changed(struct tb_service *svc); * struct tb_nhi - thunderbolt native host interface * @lock: Must be held during ring creation/destruction. Is acquired by * interrupt_work when dispatching interrupts to individual rings. - * @pdev: Pointer to the PCI device + * @dev: Device associated with this NHI instance * @ops: NHI specific optional ops * @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 - * @msix_ida: Used to allocate MSI-X vectors for rings * @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. @@ -515,12 +514,11 @@ void tb_service_properties_changed(struct tb_service *svc); */ struct tb_nhi { spinlock_t lock; - struct pci_dev *pdev; + struct device *dev; const struct tb_nhi_ops *ops; void __iomem *iobase; struct tb_ring **tx_rings; struct tb_ring **rx_rings; - struct ida msix_ida; bool going_away; bool iommu_dma_protection; struct work_struct interrupt_work; @@ -722,7 +720,7 @@ int tb_ring_throttling(struct tb_ring *ring, unsigned int interval_nsec); */ static inline struct device *tb_ring_dma_device(struct tb_ring *ring) { - return &ring->nhi->pdev->dev; + return ring->nhi->dev; } bool usb4_usb3_port_match(struct device *usb4_port_dev, From e241d98e04ef0513b78de2d87d8d1eb2993d9d34 Mon Sep 17 00:00:00 2001 From: Konrad Dybcio Date: Thu, 21 May 2026 12:40:01 +0200 Subject: [PATCH 34/41] thunderbolt: Separate out common NHI bits Add a new file encapsulating most of the PCI NHI specifics (intentionally leaving some odd cookies behind to make the layering simpler). Most notably, separate out nhi_probe() to make it easier to register other types of NHIs. Also, fold in Intel Icelake (nhi_ops.c) support to contain all PCIe-related bits in pci.c. Signed-off-by: Konrad Dybcio Signed-off-by: Mika Westerberg --- drivers/thunderbolt/Makefile | 2 +- drivers/thunderbolt/nhi.c | 461 +++---------------------- drivers/thunderbolt/nhi.h | 33 +- drivers/thunderbolt/nhi_ops.c | 189 ----------- drivers/thunderbolt/pci.c | 622 ++++++++++++++++++++++++++++++++++ drivers/thunderbolt/switch.c | 43 +-- 6 files changed, 712 insertions(+), 638 deletions(-) delete mode 100644 drivers/thunderbolt/nhi_ops.c create mode 100644 drivers/thunderbolt/pci.c diff --git a/drivers/thunderbolt/Makefile b/drivers/thunderbolt/Makefile index c792b8084ffa..beb054c3126b 100644 --- a/drivers/thunderbolt/Makefile +++ b/drivers/thunderbolt/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-only ccflags-y := -I$(src) obj-${CONFIG_USB4} := thunderbolt.o -thunderbolt-objs := nhi.o nhi_ops.o ctl.o tb.o switch.o cap.o path.o tunnel.o eeprom.o +thunderbolt-objs := nhi.o ctl.o tb.o switch.o cap.o pci.o path.o tunnel.o eeprom.o thunderbolt-objs += domain.o dma_port.o icm.o property.o xdomain.o lc.o tmu.o usb4.o thunderbolt-objs += usb4_port.o nvm.o retimer.o quirks.o clx.o diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index d21c8d330f6c..be00ffb04766 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -33,38 +33,13 @@ * transferred. */ #define RING_E2E_RESERVED_HOPID RING_FIRST_USABLE_HOPID -/* - * Minimal number of vectors when we use MSI-X. Two for control channel - * Rx/Tx and the rest four are for cross domain DMA paths. - */ -#define MSIX_MIN_VECS 6 -#define MSIX_MAX_VECS 16 #define NHI_MAILBOX_TIMEOUT 500 /* ms */ -/* Host interface quirks */ -#define QUIRK_AUTO_CLEAR_INT BIT(0) -#define QUIRK_E2E BIT(1) - static bool host_reset = true; module_param(host_reset, bool, 0444); MODULE_PARM_DESC(host_reset, "reset USB4 host router (default: true)"); -/** - * struct tb_nhi_pci - NHI device connected over PCIe - * @nhi: NHI device - * @msix_ida: Used to allocate MSI-X vectors for rings - */ -struct tb_nhi_pci { - struct tb_nhi nhi; - struct ida msix_ida; -}; - -static inline struct tb_nhi_pci *nhi_to_pci(struct tb_nhi *nhi) -{ - return container_of(nhi, struct tb_nhi_pci, nhi); -} - static int ring_interrupt_index(const struct tb_ring *ring) { int bit = ring->hop; @@ -179,7 +154,7 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active) * * Use only during init and shutdown. */ -static void nhi_disable_interrupts(struct tb_nhi *nhi) +void nhi_disable_interrupts(struct tb_nhi *nhi) { int i = 0; /* disable interrupts */ @@ -466,7 +441,7 @@ static void ring_clear_msix(const struct tb_ring *ring) 4 * (ring->nhi->hop_count / 32)); } -static irqreturn_t ring_msix(int irq, void *data) +irqreturn_t ring_msix(int irq, void *data) { struct tb_ring *ring = data; @@ -480,55 +455,6 @@ static irqreturn_t ring_msix(int irq, void *data) return IRQ_HANDLED; } -static int ring_request_msix(struct tb_ring *ring, bool no_suspend) -{ - struct tb_nhi *nhi = ring->nhi; - struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); - struct pci_dev *pdev = to_pci_dev(nhi->dev); - unsigned long irqflags; - int ret; - - if (!pdev->msix_enabled) - return 0; - - ret = ida_alloc_max(&nhi_pci->msix_ida, MSIX_MAX_VECS - 1, GFP_KERNEL); - if (ret < 0) - return ret; - - ring->vector = ret; - - ret = pci_irq_vector(pdev, ring->vector); - if (ret < 0) - goto err_ida_remove; - - ring->irq = ret; - - irqflags = no_suspend ? IRQF_NO_SUSPEND : 0; - ret = request_irq(ring->irq, ring_msix, irqflags, "thunderbolt", ring); - if (ret) - goto err_ida_remove; - - return 0; - -err_ida_remove: - ida_free(&nhi_pci->msix_ida, ring->vector); - - return ret; -} - -static void ring_release_msix(struct tb_ring *ring) -{ - struct tb_nhi_pci *nhi_pci = nhi_to_pci(ring->nhi); - - if (ring->irq <= 0) - return; - - free_irq(ring->irq, ring); - ida_free(&nhi_pci->msix_ida, ring->vector); - ring->vector = 0; - ring->irq = 0; -} - static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring) { unsigned int start_hop = RING_FIRST_USABLE_HOPID; @@ -642,8 +568,10 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, if (!ring->descriptors) goto err_free_ring; - if (ring_request_msix(ring, flags & RING_FLAG_NO_SUSPEND)) - goto err_free_descs; + if (nhi->ops && nhi->ops->request_ring_irq) { + if (nhi->ops->request_ring_irq(ring, flags & RING_FLAG_NO_SUSPEND)) + goto err_free_descs; + } if (nhi_alloc_hop(nhi, ring)) goto err_release_msix; @@ -651,7 +579,8 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, return ring; err_release_msix: - ring_release_msix(ring); + if (nhi->ops && nhi->ops->release_ring_irq) + nhi->ops->release_ring_irq(ring); err_free_descs: dma_free_coherent(ring->nhi->dev, ring->size * sizeof(*ring->descriptors), @@ -866,6 +795,8 @@ EXPORT_SYMBOL_GPL(tb_ring_stop); */ void tb_ring_free(struct tb_ring *ring) { + struct tb_nhi *nhi = ring->nhi; + spin_lock_irq(&ring->nhi->lock); /* * Dissociate the ring from the NHI. This also ensures that @@ -882,7 +813,8 @@ void tb_ring_free(struct tb_ring *ring) } spin_unlock_irq(&ring->nhi->lock); - ring_release_msix(ring); + if (nhi->ops && nhi->ops->release_ring_irq) + nhi->ops->release_ring_irq(ring); dma_free_coherent(ring->nhi->dev, ring->size * sizeof(*ring->descriptors), @@ -983,7 +915,7 @@ enum nhi_fw_mode nhi_mailbox_mode(struct tb_nhi *nhi) return (enum nhi_fw_mode)val; } -static void nhi_interrupt_work(struct work_struct *work) +void nhi_interrupt_work(struct work_struct *work) { struct tb_nhi *nhi = container_of(work, typeof(*nhi), interrupt_work); int value = 0; /* Suppress uninitialized usage warning. */ @@ -1033,7 +965,7 @@ static void nhi_interrupt_work(struct work_struct *work) spin_unlock_irq(&nhi->lock); } -static irqreturn_t nhi_msi(int irq, void *data) +irqreturn_t nhi_msi(int irq, void *data) { struct tb_nhi *nhi = data; schedule_work(&nhi->interrupt_work); @@ -1042,8 +974,7 @@ static irqreturn_t nhi_msi(int irq, void *data) static int __nhi_suspend_noirq(struct device *dev, bool wakeup) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); struct tb_nhi *nhi = tb->nhi; int ret; @@ -1067,21 +998,19 @@ static int nhi_suspend_noirq(struct device *dev) static int nhi_freeze_noirq(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); return tb_domain_freeze_noirq(tb); } static int nhi_thaw_noirq(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); return tb_domain_thaw_noirq(tb); } -static bool nhi_wake_supported(struct pci_dev *pdev) +static bool nhi_wake_supported(struct device *dev) { u8 val; @@ -1089,7 +1018,7 @@ static bool nhi_wake_supported(struct pci_dev *pdev) * If power rails are sustainable for wakeup from S4 this * property is set by the BIOS. */ - if (!device_property_read_u8(&pdev->dev, "WAKE_SUPPORTED", &val)) + if (!device_property_read_u8(dev, "WAKE_SUPPORTED", &val)) return !!val; return true; @@ -1097,17 +1026,15 @@ static bool nhi_wake_supported(struct pci_dev *pdev) static int nhi_poweroff_noirq(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); bool wakeup; - wakeup = device_may_wakeup(dev) && nhi_wake_supported(pdev); + wakeup = device_may_wakeup(dev) && nhi_wake_supported(dev); return __nhi_suspend_noirq(dev, wakeup); } static int nhi_resume_noirq(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); struct tb_nhi *nhi = tb->nhi; int ret; @@ -1116,7 +1043,7 @@ static int nhi_resume_noirq(struct device *dev) * unplugged last device which causes the host controller to go * away on PCs. */ - if (!pci_device_is_present(pdev)) { + if ((nhi->ops->is_present && !nhi->ops->is_present(nhi))) { nhi->going_away = true; } else if (nhi->ops && nhi->ops->resume_noirq) { ret = nhi->ops->resume_noirq(nhi); @@ -1129,32 +1056,29 @@ static int nhi_resume_noirq(struct device *dev) static int nhi_suspend(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); return tb_domain_suspend(tb); } static void nhi_complete(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); /* * If we were runtime suspended when system suspend started, * schedule runtime resume now. It should bring the domain back * to functional state. */ - if (pm_runtime_suspended(&pdev->dev)) - pm_runtime_resume(&pdev->dev); + if (pm_runtime_suspended(dev)) + pm_runtime_resume(dev); else tb_domain_complete(tb); } static int nhi_runtime_suspend(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); struct tb_nhi *nhi = tb->nhi; int ret; @@ -1172,8 +1096,7 @@ static int nhi_runtime_suspend(struct device *dev) static int nhi_runtime_resume(struct device *dev) { - struct pci_dev *pdev = to_pci_dev(dev); - struct tb *tb = pci_get_drvdata(pdev); + struct tb *tb = dev_get_drvdata(dev); struct tb_nhi *nhi = tb->nhi; int ret; @@ -1186,10 +1109,8 @@ static int nhi_runtime_resume(struct device *dev) return tb_domain_runtime_resume(tb); } -static void nhi_shutdown(struct tb_nhi *nhi) +void nhi_shutdown(struct tb_nhi *nhi) { - struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); - struct pci_dev *pdev = to_pci_dev(nhi->dev); int i; dev_dbg(nhi->dev, "shutdown\n"); @@ -1203,90 +1124,11 @@ static void nhi_shutdown(struct tb_nhi *nhi) "RX ring %d is still active\n", i); } nhi_disable_interrupts(nhi); - /* - * We have to release the irq before calling flush_work. Otherwise an - * already executing IRQ handler could call schedule_work again. - */ - if (!pdev->msix_enabled) { - devm_free_irq(nhi->dev, pdev->irq, nhi); - flush_work(&nhi->interrupt_work); - } - ida_destroy(&nhi_pci->msix_ida); if (nhi->ops && nhi->ops->shutdown) nhi->ops->shutdown(nhi); } -static void nhi_check_quirks(struct tb_nhi_pci *nhi_pci) -{ - struct tb_nhi *nhi = &nhi_pci->nhi; - struct pci_dev *pdev = to_pci_dev(nhi->dev); - - if (pdev->vendor == PCI_VENDOR_ID_INTEL) { - /* - * Intel hardware supports auto clear of the interrupt - * status register right after interrupt is being - * issued. - */ - nhi->quirks |= QUIRK_AUTO_CLEAR_INT; - - switch (pdev->device) { - case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: - case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: - /* - * Falcon Ridge controller needs the end-to-end - * flow control workaround to avoid losing Rx - * packets when RING_FLAG_E2E is set. - */ - nhi->quirks |= QUIRK_E2E; - break; - } - } -} - -static int nhi_check_iommu_pci_dev(struct pci_dev *pdev, void *data) -{ - if (!pdev->external_facing || - !device_iommu_capable(&pdev->dev, IOMMU_CAP_PRE_BOOT_PROTECTION)) - return 0; - *(bool *)data = true; - return 1; /* Stop walking */ -} - -static void nhi_check_iommu(struct tb_nhi_pci *nhi_pci) -{ - struct tb_nhi *nhi = &nhi_pci->nhi; - struct pci_dev *pdev = to_pci_dev(nhi->dev); - struct pci_bus *bus = pdev->bus; - bool port_ok = false; - - /* - * Ideally what we'd do here is grab every PCI device that - * represents a tunnelling adapter for this NHI and check their - * status directly, but unfortunately USB4 seems to make it - * obnoxiously difficult to reliably make any correlation. - * - * So for now we'll have to bodge it... Hoping that the system - * is at least sane enough that an adapter is in the same PCI - * segment as its NHI, if we can find *something* on that segment - * which meets the requirements for Kernel DMA Protection, we'll - * take that to imply that firmware is aware and has (hopefully) - * done the right thing in general. We need to know that the PCI - * layer has seen the ExternalFacingPort property which will then - * inform the IOMMU layer to enforce the complete "untrusted DMA" - * flow, but also that the IOMMU driver itself can be trusted not - * to have been subverted by a pre-boot DMA attack. - */ - while (bus->parent) - bus = bus->parent; - - pci_walk_bus(bus, nhi_check_iommu_pci_dev, &port_ok); - - nhi->iommu_dma_protection = port_ok; - dev_dbg(nhi->dev, "IOMMU DMA protection is %s\n", - str_enabled_disabled(port_ok)); -} - static void nhi_reset(struct tb_nhi *nhi) { ktime_t timeout; @@ -1318,53 +1160,6 @@ static void nhi_reset(struct tb_nhi *nhi) dev_warn(nhi->dev, "timeout resetting host router\n"); } -static int nhi_init_msi(struct tb_nhi_pci *nhi_pci) -{ - struct tb_nhi *nhi = &nhi_pci->nhi; - struct pci_dev *pdev = to_pci_dev(nhi->dev); - struct device *dev = &pdev->dev; - int res, irq, nvec; - - ida_init(&nhi_pci->msix_ida); - - /* - * The NHI has 16 MSI-X vectors or a single MSI. We first try to - * get all MSI-X vectors and if we succeed, each ring will have - * one MSI-X. If for some reason that does not work out, we - * fallback to a single MSI. - */ - nvec = pci_alloc_irq_vectors(pdev, MSIX_MIN_VECS, MSIX_MAX_VECS, - PCI_IRQ_MSIX); - if (nvec < 0) { - nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); - if (nvec < 0) - return nvec; - - INIT_WORK(&nhi->interrupt_work, nhi_interrupt_work); - - irq = pci_irq_vector(pdev, 0); - if (irq < 0) - return irq; - - res = devm_request_irq(&pdev->dev, irq, nhi_msi, - IRQF_NO_SUSPEND, "thunderbolt", nhi); - if (res) - return dev_err_probe(dev, res, "request_irq failed, aborting\n"); - } - - return 0; -} - -static bool nhi_imr_valid(struct pci_dev *pdev) -{ - u8 val; - - if (!device_property_read_u8(&pdev->dev, "IMR_VALID", &val)) - return !!val; - - return true; -} - static struct tb *nhi_select_cm(struct tb_nhi *nhi) { struct tb *tb; @@ -1388,63 +1183,39 @@ static struct tb *nhi_select_cm(struct tb_nhi *nhi) return tb; } -static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) +int nhi_probe(struct tb_nhi *nhi) { - struct device *dev = &pdev->dev; - struct tb_nhi_pci *nhi_pci; - struct tb_nhi *nhi; + struct device *dev = nhi->dev; struct tb *tb; int res; - if (!nhi_imr_valid(pdev)) - return dev_err_probe(dev, -ENODEV, "firmware image not valid, aborting\n"); - - res = pcim_enable_device(pdev); - if (res) - return dev_err_probe(dev, res, "cannot enable PCI device, aborting\n"); - - nhi_pci = devm_kzalloc(dev, sizeof(*nhi_pci), GFP_KERNEL); - if (!nhi_pci) - return -ENOMEM; - - nhi = &nhi_pci->nhi; - nhi->dev = dev; - nhi->ops = (const struct tb_nhi_ops *)id->driver_data; - - nhi->iobase = pcim_iomap_region(pdev, 0, "thunderbolt"); - res = PTR_ERR_OR_ZERO(nhi->iobase); - if (res) - return dev_err_probe(dev, res, "cannot obtain PCI resources, aborting\n"); - nhi->hop_count = ioread32(nhi->iobase + REG_CAPS) & 0x3ff; dev_dbg(dev, "total paths: %d\n", nhi->hop_count); - nhi->tx_rings = devm_kcalloc(&pdev->dev, nhi->hop_count, + nhi->tx_rings = devm_kcalloc(dev, nhi->hop_count, sizeof(*nhi->tx_rings), GFP_KERNEL); - nhi->rx_rings = devm_kcalloc(&pdev->dev, nhi->hop_count, + nhi->rx_rings = devm_kcalloc(dev, nhi->hop_count, sizeof(*nhi->rx_rings), GFP_KERNEL); if (!nhi->tx_rings || !nhi->rx_rings) return -ENOMEM; - nhi_check_quirks(nhi_pci); - nhi_check_iommu(nhi_pci); nhi_reset(nhi); /* In case someone left them on. */ nhi_disable_interrupts(nhi); - res = nhi_init_msi(nhi_pci); - if (res) - return dev_err_probe(dev, res, "cannot enable MSI, aborting\n"); + if (nhi->ops && nhi->ops->init_interrupts) { + res = nhi->ops->init_interrupts(nhi); + if (res) + return dev_err_probe(dev, res, "cannot enable interrupts, aborting\n"); + } spin_lock_init(&nhi->lock); - res = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); + res = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); if (res) return dev_err_probe(dev, res, "failed to set DMA mask\n"); - pci_set_master(pdev); - if (nhi->ops && nhi->ops->init) { res = nhi->ops->init(nhi); if (res) @@ -1471,38 +1242,24 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id) nhi_shutdown(nhi); return res; } - pci_set_drvdata(pdev, tb); + dev_set_drvdata(dev, tb); - device_wakeup_enable(&pdev->dev); + device_wakeup_enable(dev); - pm_runtime_allow(&pdev->dev); - pm_runtime_set_autosuspend_delay(&pdev->dev, TB_AUTOSUSPEND_DELAY); - pm_runtime_use_autosuspend(&pdev->dev); - pm_runtime_put_autosuspend(&pdev->dev); + pm_runtime_allow(dev); + pm_runtime_set_autosuspend_delay(dev, TB_AUTOSUSPEND_DELAY); + pm_runtime_use_autosuspend(dev); + pm_runtime_put_autosuspend(dev); return 0; } -static void nhi_remove(struct pci_dev *pdev) -{ - struct tb *tb = pci_get_drvdata(pdev); - struct tb_nhi *nhi = tb->nhi; - - pm_runtime_get_sync(&pdev->dev); - pm_runtime_dont_use_autosuspend(&pdev->dev); - pm_runtime_forbid(&pdev->dev); - - tb_domain_remove(tb); - wait_for_completion(&nhi->domain_released); - nhi_shutdown(nhi); -} - /* * The tunneled pci bridges are siblings of us. Use resume_noirq to reenable * the tunnels asap. A corresponding pci quirk blocks the downstream bridges * resume_noirq until we are done. */ -static const struct dev_pm_ops nhi_pm_ops = { +const struct dev_pm_ops nhi_pm_ops = { .suspend_noirq = nhi_suspend_noirq, .resume_noirq = nhi_resume_noirq, .freeze_noirq = nhi_freeze_noirq, /* @@ -1518,129 +1275,3 @@ static const struct dev_pm_ops nhi_pm_ops = { .runtime_suspend = nhi_runtime_suspend, .runtime_resume = nhi_runtime_resume, }; - -static struct pci_device_id nhi_ids[] = { - /* - * We have to specify class, the TB bridges use the same device and - * vendor (sub)id on gen 1 and gen 2 controllers. - */ - { - .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, - .vendor = PCI_VENDOR_ID_INTEL, - .device = PCI_DEVICE_ID_INTEL_LIGHT_RIDGE, - .subvendor = 0x2222, .subdevice = 0x1111, - }, - { - .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, - .vendor = PCI_VENDOR_ID_INTEL, - .device = PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C, - .subvendor = 0x2222, .subdevice = 0x1111, - }, - { - .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, - .vendor = PCI_VENDOR_ID_INTEL, - .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI, - .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID, - }, - { - .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, - .vendor = PCI_VENDOR_ID_INTEL, - .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI, - .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID, - }, - - /* Thunderbolt 3 */ - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_2C_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_4C_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_USBONLY_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_USBONLY_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_2C_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_4C_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_USBONLY_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_2C_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_4C_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - /* Thunderbolt 4 */ - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_M_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI1), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_WCL_NHI0), - .driver_data = (kernel_ulong_t)&icl_nhi_ops }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_80G_NHI) }, - { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_40G_NHI) }, - - /* Any USB4 compliant host */ - { PCI_DEVICE_CLASS(PCI_CLASS_SERIAL_USB_USB4, ~0) }, - - { 0,} -}; - -MODULE_DEVICE_TABLE(pci, nhi_ids); -MODULE_DESCRIPTION("Thunderbolt/USB4 core driver"); -MODULE_LICENSE("GPL"); - -static struct pci_driver nhi_driver = { - .name = "thunderbolt", - .id_table = nhi_ids, - .probe = nhi_probe, - .remove = nhi_remove, - .shutdown = nhi_remove, - .driver.pm = &nhi_pm_ops, -}; - -static int __init nhi_init(void) -{ - int ret; - - ret = tb_domain_init(); - if (ret) - return ret; - ret = pci_register_driver(&nhi_driver); - if (ret) - tb_domain_exit(); - return ret; -} - -static void __exit nhi_unload(void) -{ - pci_unregister_driver(&nhi_driver); - tb_domain_exit(); -} - -rootfs_initcall(nhi_init); -module_exit(nhi_unload); diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h index 24ac4246d0ca..d488eadadfce 100644 --- a/drivers/thunderbolt/nhi.h +++ b/drivers/thunderbolt/nhi.h @@ -29,6 +29,14 @@ enum nhi_mailbox_cmd { int nhi_mailbox_cmd(struct tb_nhi *nhi, enum nhi_mailbox_cmd cmd, u32 data); enum nhi_fw_mode nhi_mailbox_mode(struct tb_nhi *nhi); +void nhi_enable_int_throttling(struct tb_nhi *nhi); +void nhi_disable_interrupts(struct tb_nhi *nhi); +void nhi_interrupt_work(struct work_struct *work); +irqreturn_t nhi_msi(int irq, void *data); +irqreturn_t ring_msix(int irq, void *data); +int nhi_probe(struct tb_nhi *nhi); +void nhi_shutdown(struct tb_nhi *nhi); +extern const struct dev_pm_ops nhi_pm_ops; /** * struct tb_nhi_ops - NHI specific optional operations @@ -38,6 +46,12 @@ enum nhi_fw_mode nhi_mailbox_mode(struct tb_nhi *nhi); * @runtime_suspend: NHI specific runtime_suspend hook * @runtime_resume: NHI specific runtime_resume hook * @shutdown: NHI specific shutdown + * @pre_nvm_auth: hook to run before Thunderbolt 3 NVM authentication + * @post_nvm_auth: hook to run after Thunderbolt 3 NVM authentication + * @request_ring_irq: NHI specific interrupt retrieval hook + * @release_ring_irq: NHI specific interrupt release hook + * @is_present: Whether the device is currently present on the parent bus + * @init_interrupts: NHI specific interrupt initialization hook */ struct tb_nhi_ops { int (*init)(struct tb_nhi *nhi); @@ -46,10 +60,14 @@ struct tb_nhi_ops { int (*runtime_suspend)(struct tb_nhi *nhi); int (*runtime_resume)(struct tb_nhi *nhi); void (*shutdown)(struct tb_nhi *nhi); + void (*pre_nvm_auth)(struct tb_nhi *nhi); + void (*post_nvm_auth)(struct tb_nhi *nhi); + int (*request_ring_irq)(struct tb_ring *ring, bool no_suspend); + void (*release_ring_irq)(struct tb_ring *ring); + bool (*is_present)(struct tb_nhi *nhi); + int (*init_interrupts)(struct tb_nhi *nhi); }; -extern const struct tb_nhi_ops icl_nhi_ops; - /* * PCI IDs used in this driver from Win Ridge forward. There is no * need for the PCI quirk anymore as we will use ICM also on Apple @@ -100,4 +118,15 @@ extern const struct tb_nhi_ops icl_nhi_ops; #define PCI_CLASS_SERIAL_USB_USB4 0x0c0340 +/* Host interface quirks */ +#define QUIRK_AUTO_CLEAR_INT BIT(0) +#define QUIRK_E2E BIT(1) + +/* + * Minimal number of vectors when we use MSI-X. Two for control channel + * Rx/Tx and the rest four are for cross domain DMA paths. + */ +#define MSIX_MIN_VECS 6 +#define MSIX_MAX_VECS 16 + #endif diff --git a/drivers/thunderbolt/nhi_ops.c b/drivers/thunderbolt/nhi_ops.c deleted file mode 100644 index 8c50066f3411..000000000000 --- a/drivers/thunderbolt/nhi_ops.c +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * NHI specific operations - * - * Copyright (C) 2019, Intel Corporation - * Author: Mika Westerberg - */ - -#include -#include - -#include "nhi.h" -#include "nhi_regs.h" -#include "tb.h" - -/* Ice Lake specific NHI operations */ - -#define ICL_LC_MAILBOX_TIMEOUT 500 /* ms */ - -static int check_for_device(struct device *dev, void *data) -{ - return tb_is_switch(dev); -} - -static bool icl_nhi_is_device_connected(struct tb_nhi *nhi) -{ - struct tb *tb = dev_get_drvdata(nhi->dev); - int ret; - - ret = device_for_each_child(&tb->root_switch->dev, NULL, - check_for_device); - return ret > 0; -} - -static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) -{ - struct pci_dev *pdev = to_pci_dev(nhi->dev); - u32 vs_cap; - - /* - * The Thunderbolt host controller is present always in Ice Lake - * but the firmware may not be loaded and running (depending - * whether there is device connected and so on). Each time the - * controller is used we need to "Force Power" it first and wait - * for the firmware to indicate it is up and running. This "Force - * Power" is really not about actually powering on/off the - * controller so it is accessible even if "Force Power" is off. - * - * The actual power management happens inside shared ACPI power - * resources using standard ACPI methods. - */ - pci_read_config_dword(pdev, VS_CAP_22, &vs_cap); - if (power) { - vs_cap &= ~VS_CAP_22_DMA_DELAY_MASK; - vs_cap |= 0x22 << VS_CAP_22_DMA_DELAY_SHIFT; - vs_cap |= VS_CAP_22_FORCE_POWER; - } else { - vs_cap &= ~VS_CAP_22_FORCE_POWER; - } - pci_write_config_dword(pdev, VS_CAP_22, vs_cap); - - if (power) { - unsigned int retries = 350; - u32 val; - - /* Wait until the firmware tells it is up and running */ - do { - pci_read_config_dword(pdev, VS_CAP_9, &val); - if (val & VS_CAP_9_FW_READY) - return 0; - usleep_range(3000, 3100); - } while (--retries); - - return -ETIMEDOUT; - } - - return 0; -} - -static void icl_nhi_lc_mailbox_cmd(struct tb_nhi *nhi, enum icl_lc_mailbox_cmd cmd) -{ - struct pci_dev *pdev = to_pci_dev(nhi->dev); - u32 data; - - data = (cmd << VS_CAP_19_CMD_SHIFT) & VS_CAP_19_CMD_MASK; - pci_write_config_dword(pdev, VS_CAP_19, data | VS_CAP_19_VALID); -} - -static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout) -{ - struct pci_dev *pdev = to_pci_dev(nhi->dev); - unsigned long end; - u32 data; - - if (!timeout) - goto clear; - - end = jiffies + msecs_to_jiffies(timeout); - do { - pci_read_config_dword(pdev, VS_CAP_18, &data); - if (data & VS_CAP_18_DONE) - goto clear; - usleep_range(1000, 1100); - } while (time_before(jiffies, end)); - - return -ETIMEDOUT; - -clear: - /* Clear the valid bit */ - pci_write_config_dword(pdev, VS_CAP_19, 0); - return 0; -} - -static void icl_nhi_set_ltr(struct tb_nhi *nhi) -{ - struct pci_dev *pdev = to_pci_dev(nhi->dev); - u32 max_ltr, ltr; - - pci_read_config_dword(pdev, VS_CAP_16, &max_ltr); - max_ltr &= 0xffff; - /* Program the same value for both snoop and no-snoop */ - ltr = max_ltr << 16 | max_ltr; - pci_write_config_dword(pdev, VS_CAP_15, ltr); -} - -static int icl_nhi_suspend(struct tb_nhi *nhi) -{ - struct tb *tb = dev_get_drvdata(nhi->dev); - int ret; - - if (icl_nhi_is_device_connected(nhi)) - return 0; - - if (tb_switch_is_icm(tb->root_switch)) { - /* - * If there is no device connected we need to perform - * both: a handshake through LC mailbox and force power - * down before entering D3. - */ - icl_nhi_lc_mailbox_cmd(nhi, ICL_LC_PREPARE_FOR_RESET); - ret = icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT); - if (ret) - return ret; - } - - return icl_nhi_force_power(nhi, false); -} - -static int icl_nhi_suspend_noirq(struct tb_nhi *nhi, bool wakeup) -{ - struct tb *tb = dev_get_drvdata(nhi->dev); - enum icl_lc_mailbox_cmd cmd; - - if (!pm_suspend_via_firmware()) - return icl_nhi_suspend(nhi); - - if (!tb_switch_is_icm(tb->root_switch)) - return 0; - - cmd = wakeup ? ICL_LC_GO2SX : ICL_LC_GO2SX_NO_WAKE; - icl_nhi_lc_mailbox_cmd(nhi, cmd); - return icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT); -} - -static int icl_nhi_resume(struct tb_nhi *nhi) -{ - int ret; - - ret = icl_nhi_force_power(nhi, true); - if (ret) - return ret; - - icl_nhi_set_ltr(nhi); - return 0; -} - -static void icl_nhi_shutdown(struct tb_nhi *nhi) -{ - icl_nhi_force_power(nhi, false); -} - -const struct tb_nhi_ops icl_nhi_ops = { - .init = icl_nhi_resume, - .suspend_noirq = icl_nhi_suspend_noirq, - .resume_noirq = icl_nhi_resume, - .runtime_suspend = icl_nhi_suspend, - .runtime_resume = icl_nhi_resume, - .shutdown = icl_nhi_shutdown, -}; diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c new file mode 100644 index 000000000000..bbd186c29ef7 --- /dev/null +++ b/drivers/thunderbolt/pci.c @@ -0,0 +1,622 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Thunderbolt driver - PCI NHI driver + * + * Copyright (c) 2014 Andreas Noever + * Copyright (C) 2018, Intel Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nhi.h" +#include "nhi_regs.h" +#include "tb.h" + +/** + * struct tb_nhi_pci - NHI device connected over PCIe + * @nhi: NHI device + * @msix_ida: Used to allocate MSI-X vectors for rings + */ +struct tb_nhi_pci { + struct tb_nhi nhi; + struct ida msix_ida; +}; + +static inline struct tb_nhi_pci *nhi_to_pci(struct tb_nhi *nhi) +{ + return container_of(nhi, struct tb_nhi_pci, nhi); +} + +static void nhi_pci_check_quirks(struct tb_nhi_pci *nhi_pci) +{ + struct tb_nhi *nhi = &nhi_pci->nhi; + struct pci_dev *pdev = to_pci_dev(nhi->dev); + + if (pdev->vendor == PCI_VENDOR_ID_INTEL) { + /* + * Intel hardware supports auto clear of the interrupt + * status register right after interrupt is being + * issued. + */ + nhi->quirks |= QUIRK_AUTO_CLEAR_INT; + + switch (pdev->device) { + case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI: + case PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI: + /* + * Falcon Ridge controller needs the end-to-end + * flow control workaround to avoid losing Rx + * packets when RING_FLAG_E2E is set. + */ + nhi->quirks |= QUIRK_E2E; + break; + } + } +} + +static int nhi_pci_check_iommu_pdev(struct pci_dev *pdev, void *data) +{ + if (!pdev->external_facing || + !device_iommu_capable(&pdev->dev, IOMMU_CAP_PRE_BOOT_PROTECTION)) + return 0; + *(bool *)data = true; + return 1; /* Stop walking */ +} + +static void nhi_pci_check_iommu(struct tb_nhi_pci *nhi_pci) +{ + struct tb_nhi *nhi = &nhi_pci->nhi; + struct pci_dev *pdev = to_pci_dev(nhi->dev); + struct pci_bus *bus = pdev->bus; + bool port_ok = false; + + /* + * Ideally what we'd do here is grab every PCI device that + * represents a tunnelling adapter for this NHI and check their + * status directly, but unfortunately USB4 seems to make it + * obnoxiously difficult to reliably make any correlation. + * + * So for now we'll have to bodge it... Hoping that the system + * is at least sane enough that an adapter is in the same PCI + * segment as its NHI, if we can find *something* on that segment + * which meets the requirements for Kernel DMA Protection, we'll + * take that to imply that firmware is aware and has (hopefully) + * done the right thing in general. We need to know that the PCI + * layer has seen the ExternalFacingPort property which will then + * inform the IOMMU layer to enforce the complete "untrusted DMA" + * flow, but also that the IOMMU driver itself can be trusted not + * to have been subverted by a pre-boot DMA attack. + */ + while (bus->parent) + bus = bus->parent; + + pci_walk_bus(bus, nhi_pci_check_iommu_pdev, &port_ok); + + nhi->iommu_dma_protection = port_ok; + dev_dbg(nhi->dev, "IOMMU DMA protection is %s\n", + str_enabled_disabled(port_ok)); +} + +static int nhi_pci_init_msi(struct tb_nhi *nhi) +{ + struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); + struct pci_dev *pdev = to_pci_dev(nhi->dev); + struct device *dev = &pdev->dev; + int res, irq, nvec; + + ida_init(&nhi_pci->msix_ida); + + /* + * The NHI has 16 MSI-X vectors or a single MSI. We first try to + * get all MSI-X vectors and if we succeed, each ring will have + * one MSI-X. If for some reason that does not work out, we + * fallback to a single MSI. + */ + nvec = pci_alloc_irq_vectors(pdev, MSIX_MIN_VECS, MSIX_MAX_VECS, + PCI_IRQ_MSIX); + if (nvec < 0) { + nvec = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); + if (nvec < 0) + return nvec; + + INIT_WORK(&nhi->interrupt_work, nhi_interrupt_work); + + irq = pci_irq_vector(pdev, 0); + if (irq < 0) + return irq; + + res = devm_request_irq(&pdev->dev, irq, nhi_msi, + IRQF_NO_SUSPEND, "thunderbolt", nhi); + if (res) + return dev_err_probe(dev, res, "request_irq failed, aborting\n"); + } + + return 0; +} + +static bool nhi_pci_imr_valid(struct pci_dev *pdev) +{ + u8 val; + + if (!device_property_read_u8(&pdev->dev, "IMR_VALID", &val)) + return !!val; + + return true; +} + +static void nhi_pci_start_dma_port(struct tb_nhi *nhi) +{ + struct pci_dev *pdev = to_pci_dev(nhi->dev); + struct pci_dev *root_port; + + /* + * During host router NVM upgrade we should not allow root port to + * go into D3cold because some root ports cannot trigger PME + * itself. To be on the safe side keep the root port in D0 during + * the whole upgrade process. + */ + root_port = pcie_find_root_port(pdev); + if (root_port) + pm_runtime_get_noresume(&root_port->dev); +} + +static void nhi_pci_complete_dma_port(struct tb_nhi *nhi) +{ + struct pci_dev *pdev = to_pci_dev(nhi->dev); + struct pci_dev *root_port; + + root_port = pcie_find_root_port(pdev); + if (root_port) + pm_runtime_put(&root_port->dev); +} + +static int nhi_pci_ring_request_msix(struct tb_ring *ring, bool no_suspend) +{ + struct tb_nhi *nhi = ring->nhi; + struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); + struct pci_dev *pdev = to_pci_dev(nhi->dev); + unsigned long irqflags; + int ret; + + if (!pdev->msix_enabled) + return 0; + + ret = ida_alloc_max(&nhi_pci->msix_ida, MSIX_MAX_VECS - 1, GFP_KERNEL); + if (ret < 0) + return ret; + + ring->vector = ret; + + ret = pci_irq_vector(pdev, ring->vector); + if (ret < 0) + goto err_ida_remove; + + ring->irq = ret; + + irqflags = no_suspend ? IRQF_NO_SUSPEND : 0; + ret = request_irq(ring->irq, ring_msix, irqflags, "thunderbolt", ring); + if (ret) + goto err_ida_remove; + + return 0; + +err_ida_remove: + ida_free(&nhi_pci->msix_ida, ring->vector); + + return ret; +} + +static void nhi_pci_ring_release_msix(struct tb_ring *ring) +{ + struct tb_nhi_pci *nhi_pci = nhi_to_pci(ring->nhi); + + if (ring->irq <= 0) + return; + + free_irq(ring->irq, ring); + ida_free(&nhi_pci->msix_ida, ring->vector); + ring->vector = 0; + ring->irq = 0; +} + +static void nhi_pci_shutdown(struct tb_nhi *nhi) +{ + struct tb_nhi_pci *nhi_pci = nhi_to_pci(nhi); + struct pci_dev *pdev = to_pci_dev(nhi->dev); + + /* + * We have to release the irq before calling flush_work. Otherwise an + * already executing IRQ handler could call schedule_work again. + */ + if (!pdev->msix_enabled) { + devm_free_irq(nhi->dev, pdev->irq, nhi); + flush_work(&nhi->interrupt_work); + } + ida_destroy(&nhi_pci->msix_ida); +} + +static bool nhi_pci_is_present(struct tb_nhi *nhi) +{ + return pci_device_is_present(to_pci_dev(nhi->dev)); +} + +static const struct tb_nhi_ops pci_nhi_default_ops = { + .pre_nvm_auth = nhi_pci_start_dma_port, + .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, + .is_present = nhi_pci_is_present, + .init_interrupts = nhi_pci_init_msi, +}; + +/* Ice Lake specific NHI operations */ + +#define ICL_LC_MAILBOX_TIMEOUT 500 /* ms */ + +static int check_for_device(struct device *dev, void *data) +{ + return tb_is_switch(dev); +} + +static bool icl_nhi_is_device_connected(struct tb_nhi *nhi) +{ + struct tb *tb = dev_get_drvdata(nhi->dev); + int ret; + + ret = device_for_each_child(&tb->root_switch->dev, NULL, + check_for_device); + return ret > 0; +} + +static int icl_nhi_force_power(struct tb_nhi *nhi, bool power) +{ + struct pci_dev *pdev = to_pci_dev(nhi->dev); + u32 vs_cap; + + /* + * The Thunderbolt host controller is present always in Ice Lake + * but the firmware may not be loaded and running (depending + * whether there is device connected and so on). Each time the + * controller is used we need to "Force Power" it first and wait + * for the firmware to indicate it is up and running. This "Force + * Power" is really not about actually powering on/off the + * controller so it is accessible even if "Force Power" is off. + * + * The actual power management happens inside shared ACPI power + * resources using standard ACPI methods. + */ + pci_read_config_dword(pdev, VS_CAP_22, &vs_cap); + if (power) { + vs_cap &= ~VS_CAP_22_DMA_DELAY_MASK; + vs_cap |= 0x22 << VS_CAP_22_DMA_DELAY_SHIFT; + vs_cap |= VS_CAP_22_FORCE_POWER; + } else { + vs_cap &= ~VS_CAP_22_FORCE_POWER; + } + pci_write_config_dword(pdev, VS_CAP_22, vs_cap); + + if (power) { + unsigned int retries = 350; + u32 val; + + /* Wait until the firmware tells it is up and running */ + do { + pci_read_config_dword(pdev, VS_CAP_9, &val); + if (val & VS_CAP_9_FW_READY) + return 0; + usleep_range(3000, 3100); + } while (--retries); + + return -ETIMEDOUT; + } + + return 0; +} + +static void icl_nhi_lc_mailbox_cmd(struct tb_nhi *nhi, enum icl_lc_mailbox_cmd cmd) +{ + struct pci_dev *pdev = to_pci_dev(nhi->dev); + u32 data; + + data = (cmd << VS_CAP_19_CMD_SHIFT) & VS_CAP_19_CMD_MASK; + pci_write_config_dword(pdev, VS_CAP_19, data | VS_CAP_19_VALID); +} + +static int icl_nhi_lc_mailbox_cmd_complete(struct tb_nhi *nhi, int timeout) +{ + struct pci_dev *pdev = to_pci_dev(nhi->dev); + unsigned long end; + u32 data; + + if (!timeout) + goto clear; + + end = jiffies + msecs_to_jiffies(timeout); + do { + pci_read_config_dword(pdev, VS_CAP_18, &data); + if (data & VS_CAP_18_DONE) + goto clear; + usleep_range(1000, 1100); + } while (time_before(jiffies, end)); + + return -ETIMEDOUT; + +clear: + /* Clear the valid bit */ + pci_write_config_dword(pdev, VS_CAP_19, 0); + return 0; +} + +static void icl_nhi_set_ltr(struct tb_nhi *nhi) +{ + struct pci_dev *pdev = to_pci_dev(nhi->dev); + u32 max_ltr, ltr; + + pci_read_config_dword(pdev, VS_CAP_16, &max_ltr); + max_ltr &= 0xffff; + /* Program the same value for both snoop and no-snoop */ + ltr = max_ltr << 16 | max_ltr; + pci_write_config_dword(pdev, VS_CAP_15, ltr); +} + +static int icl_nhi_suspend(struct tb_nhi *nhi) +{ + struct tb *tb = dev_get_drvdata(nhi->dev); + int ret; + + if (icl_nhi_is_device_connected(nhi)) + return 0; + + if (tb_switch_is_icm(tb->root_switch)) { + /* + * If there is no device connected we need to perform + * both: a handshake through LC mailbox and force power + * down before entering D3. + */ + icl_nhi_lc_mailbox_cmd(nhi, ICL_LC_PREPARE_FOR_RESET); + ret = icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT); + if (ret) + return ret; + } + + return icl_nhi_force_power(nhi, false); +} + +static int icl_nhi_suspend_noirq(struct tb_nhi *nhi, bool wakeup) +{ + struct tb *tb = dev_get_drvdata(nhi->dev); + enum icl_lc_mailbox_cmd cmd; + + if (!pm_suspend_via_firmware()) + return icl_nhi_suspend(nhi); + + if (!tb_switch_is_icm(tb->root_switch)) + return 0; + + cmd = wakeup ? ICL_LC_GO2SX : ICL_LC_GO2SX_NO_WAKE; + icl_nhi_lc_mailbox_cmd(nhi, cmd); + return icl_nhi_lc_mailbox_cmd_complete(nhi, ICL_LC_MAILBOX_TIMEOUT); +} + +static int icl_nhi_resume(struct tb_nhi *nhi) +{ + int ret; + + ret = icl_nhi_force_power(nhi, true); + if (ret) + return ret; + + icl_nhi_set_ltr(nhi); + return 0; +} + +static void icl_nhi_shutdown(struct tb_nhi *nhi) +{ + nhi_pci_shutdown(nhi); + + icl_nhi_force_power(nhi, false); +} + +static const struct tb_nhi_ops icl_nhi_ops = { + .init = icl_nhi_resume, + .suspend_noirq = icl_nhi_suspend_noirq, + .resume_noirq = icl_nhi_resume, + .runtime_suspend = icl_nhi_suspend, + .runtime_resume = icl_nhi_resume, + .shutdown = icl_nhi_shutdown, + .pre_nvm_auth = nhi_pci_start_dma_port, + .post_nvm_auth = nhi_pci_complete_dma_port, + .request_ring_irq = nhi_pci_ring_request_msix, + .release_ring_irq = nhi_pci_ring_release_msix, + .is_present = nhi_pci_is_present, + .init_interrupts = nhi_pci_init_msi, +}; + +static int nhi_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) +{ + struct device *dev = &pdev->dev; + struct tb_nhi_pci *nhi_pci; + struct tb_nhi *nhi; + int res; + + if (!nhi_pci_imr_valid(pdev)) + return dev_err_probe(dev, -ENODEV, "firmware image not valid, aborting\n"); + + res = pcim_enable_device(pdev); + if (res) + return dev_err_probe(dev, res, "cannot enable PCI device, aborting\n"); + + nhi_pci = devm_kzalloc(dev, sizeof(*nhi_pci), GFP_KERNEL); + if (!nhi_pci) + return -ENOMEM; + + nhi = &nhi_pci->nhi; + nhi->dev = dev; + nhi->ops = (const struct tb_nhi_ops *)id->driver_data ?: &pci_nhi_default_ops; + + nhi->iobase = pcim_iomap_region(pdev, 0, "thunderbolt"); + res = PTR_ERR_OR_ZERO(nhi->iobase); + if (res) + return dev_err_probe(dev, res, "cannot obtain PCI resources, aborting\n"); + + nhi_pci_check_quirks(nhi_pci); + nhi_pci_check_iommu(nhi_pci); + + pci_set_master(pdev); + + return nhi_probe(&nhi_pci->nhi); +} + +static void nhi_pci_remove(struct pci_dev *pdev) +{ + struct tb *tb = pci_get_drvdata(pdev); + struct tb_nhi *nhi = tb->nhi; + + pm_runtime_get_sync(&pdev->dev); + pm_runtime_dont_use_autosuspend(&pdev->dev); + pm_runtime_forbid(&pdev->dev); + + tb_domain_remove(tb); + wait_for_completion(&nhi->domain_released); + nhi_shutdown(nhi); +} + +static struct pci_device_id nhi_ids[] = { + /* + * We have to specify class, the TB bridges use the same device and + * vendor (sub)id on gen 1 and gen 2 controllers. + */ + { + .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, + .vendor = PCI_VENDOR_ID_INTEL, + .device = PCI_DEVICE_ID_INTEL_LIGHT_RIDGE, + .subvendor = 0x2222, .subdevice = 0x1111, + }, + { + .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, + .vendor = PCI_VENDOR_ID_INTEL, + .device = PCI_DEVICE_ID_INTEL_CACTUS_RIDGE_4C, + .subvendor = 0x2222, .subdevice = 0x1111, + }, + { + .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, + .vendor = PCI_VENDOR_ID_INTEL, + .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_2C_NHI, + .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID, + }, + { + .class = PCI_CLASS_SYSTEM_OTHER << 8, .class_mask = ~0, + .vendor = PCI_VENDOR_ID_INTEL, + .device = PCI_DEVICE_ID_INTEL_FALCON_RIDGE_4C_NHI, + .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID, + }, + + /* Thunderbolt 3 */ + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_2C_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_4C_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_USBONLY_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_USBONLY_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_2C_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_4C_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_USBONLY_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_2C_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_4C_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + /* Thunderbolt 4 */ + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TGL_H_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ADL_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_RPL_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_M_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_MTL_P_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_LNL_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_M_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_PTL_P_NHI1), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_WCL_NHI0), + .driver_data = (kernel_ulong_t)&icl_nhi_ops }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_80G_NHI) }, + { PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_BARLOW_RIDGE_HOST_40G_NHI) }, + + /* Any USB4 compliant host */ + { PCI_DEVICE_CLASS(PCI_CLASS_SERIAL_USB_USB4, ~0) }, + + { 0,} +}; + +MODULE_DEVICE_TABLE(pci, nhi_ids); +MODULE_DESCRIPTION("Thunderbolt/USB4 core driver"); +MODULE_LICENSE("GPL"); + +static struct pci_driver nhi_driver = { + .name = "thunderbolt", + .id_table = nhi_ids, + .probe = nhi_pci_probe, + .remove = nhi_pci_remove, + .shutdown = nhi_pci_remove, + .driver.pm = &nhi_pm_ops, +}; + +static int __init nhi_init(void) +{ + int ret; + + ret = tb_domain_init(); + if (ret) + return ret; + + ret = pci_register_driver(&nhi_driver); + if (ret) + tb_domain_exit(); + + return ret; +} + +static void __exit nhi_unload(void) +{ + pci_unregister_driver(&nhi_driver); + tb_domain_exit(); +} + +rootfs_initcall(nhi_init); +module_exit(nhi_unload); diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index 769b57ff276b..a29b9887cd6b 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -209,32 +209,6 @@ static int nvm_authenticate_device_dma_port(struct tb_switch *sw) return -ETIMEDOUT; } -static void nvm_authenticate_start_dma_port(struct tb_switch *sw) -{ - struct pci_dev *pdev = to_pci_dev(sw->tb->nhi->dev); - struct pci_dev *root_port; - - /* - * During host router NVM upgrade we should not allow root port to - * go into D3cold because some root ports cannot trigger PME - * itself. To be on the safe side keep the root port in D0 during - * the whole upgrade process. - */ - root_port = pcie_find_root_port(pdev); - if (root_port) - pm_runtime_get_noresume(&root_port->dev); -} - -static void nvm_authenticate_complete_dma_port(struct tb_switch *sw) -{ - struct pci_dev *pdev = to_pci_dev(sw->tb->nhi->dev); - struct pci_dev *root_port; - - root_port = pcie_find_root_port(pdev); - if (root_port) - pm_runtime_put(&root_port->dev); -} - static inline bool nvm_readable(struct tb_switch *sw) { if (tb_switch_is_usb4(sw)) { @@ -260,6 +234,7 @@ static inline bool nvm_upgradeable(struct tb_switch *sw) static int nvm_authenticate(struct tb_switch *sw, bool auth_only) { + struct tb_nhi *nhi = sw->tb->nhi; int ret; if (tb_switch_is_usb4(sw)) { @@ -276,7 +251,8 @@ static int nvm_authenticate(struct tb_switch *sw, bool auth_only) sw->nvm->authenticating = true; if (!tb_route(sw)) { - nvm_authenticate_start_dma_port(sw); + if (nhi->ops && nhi->ops->pre_nvm_auth) + nhi->ops->pre_nvm_auth(nhi); ret = nvm_authenticate_host_dma_port(sw); } else { ret = nvm_authenticate_device_dma_port(sw); @@ -2745,6 +2721,7 @@ static int tb_switch_set_uuid(struct tb_switch *sw) static int tb_switch_add_dma_port(struct tb_switch *sw) { + struct tb_nhi *nhi = sw->tb->nhi; u32 status; int ret; @@ -2804,8 +2781,10 @@ static int tb_switch_add_dma_port(struct tb_switch *sw) */ nvm_get_auth_status(sw, &status); if (status) { - if (!tb_route(sw)) - nvm_authenticate_complete_dma_port(sw); + if (!tb_route(sw)) { + if (nhi->ops && nhi->ops->post_nvm_auth) + nhi->ops->post_nvm_auth(nhi); + } return 0; } @@ -2819,8 +2798,10 @@ static int tb_switch_add_dma_port(struct tb_switch *sw) return ret; /* Now we can allow root port to suspend again */ - if (!tb_route(sw)) - nvm_authenticate_complete_dma_port(sw); + if (!tb_route(sw)) { + if (nhi->ops && nhi->ops->post_nvm_auth) + nhi->ops->post_nvm_auth(nhi); + } if (status) { tb_sw_info(sw, "switch flash authentication failed\n"); From dd60fb487e55445656284dbc9dc0c865ff9fb34c Mon Sep 17 00:00:00 2001 From: Konrad Dybcio Date: Thu, 21 May 2026 12:40:02 +0200 Subject: [PATCH 35/41] thunderbolt: Require nhi->ops be valid Because of how fundamental ops->init_interrupts() is, it no longer makes sense to consider cases where nhi->ops is NULL. Drop some boilerplate around it and add a single sanity-check in nhi_probe() instead. Signed-off-by: Konrad Dybcio Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 32 ++++++++++++++++++-------------- drivers/thunderbolt/switch.c | 6 +++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index be00ffb04766..e9ba8ffbe349 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -568,7 +568,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, if (!ring->descriptors) goto err_free_ring; - if (nhi->ops && nhi->ops->request_ring_irq) { + if (nhi->ops->request_ring_irq) { if (nhi->ops->request_ring_irq(ring, flags & RING_FLAG_NO_SUSPEND)) goto err_free_descs; } @@ -579,7 +579,7 @@ static struct tb_ring *tb_ring_alloc(struct tb_nhi *nhi, u32 hop, int size, return ring; err_release_msix: - if (nhi->ops && nhi->ops->release_ring_irq) + if (nhi->ops->release_ring_irq) nhi->ops->release_ring_irq(ring); err_free_descs: dma_free_coherent(ring->nhi->dev, @@ -813,7 +813,7 @@ void tb_ring_free(struct tb_ring *ring) } spin_unlock_irq(&ring->nhi->lock); - if (nhi->ops && nhi->ops->release_ring_irq) + if (nhi->ops->release_ring_irq) nhi->ops->release_ring_irq(ring); dma_free_coherent(ring->nhi->dev, @@ -982,7 +982,7 @@ static int __nhi_suspend_noirq(struct device *dev, bool wakeup) if (ret) return ret; - if (nhi->ops && nhi->ops->suspend_noirq) { + if (nhi->ops->suspend_noirq) { ret = nhi->ops->suspend_noirq(tb->nhi, wakeup); if (ret) return ret; @@ -1045,7 +1045,7 @@ static int nhi_resume_noirq(struct device *dev) */ if ((nhi->ops->is_present && !nhi->ops->is_present(nhi))) { nhi->going_away = true; - } else if (nhi->ops && nhi->ops->resume_noirq) { + } else if (nhi->ops->resume_noirq) { ret = nhi->ops->resume_noirq(nhi); if (ret) return ret; @@ -1086,7 +1086,7 @@ static int nhi_runtime_suspend(struct device *dev) if (ret) return ret; - if (nhi->ops && nhi->ops->runtime_suspend) { + if (nhi->ops->runtime_suspend) { ret = nhi->ops->runtime_suspend(tb->nhi); if (ret) return ret; @@ -1100,7 +1100,7 @@ static int nhi_runtime_resume(struct device *dev) struct tb_nhi *nhi = tb->nhi; int ret; - if (nhi->ops && nhi->ops->runtime_resume) { + if (nhi->ops->runtime_resume) { ret = nhi->ops->runtime_resume(nhi); if (ret) return ret; @@ -1125,7 +1125,7 @@ void nhi_shutdown(struct tb_nhi *nhi) } nhi_disable_interrupts(nhi); - if (nhi->ops && nhi->ops->shutdown) + if (nhi->ops->shutdown) nhi->ops->shutdown(nhi); } @@ -1189,6 +1189,12 @@ int nhi_probe(struct tb_nhi *nhi) struct tb *tb; int res; + if (!nhi->ops) + return dev_err_probe(dev, -EINVAL, "NHI ops not set\n"); + + if (!nhi->ops->init_interrupts) + return dev_err_probe(dev, -EINVAL, "missing required NHI ops\n"); + nhi->hop_count = ioread32(nhi->iobase + REG_CAPS) & 0x3ff; dev_dbg(dev, "total paths: %d\n", nhi->hop_count); @@ -1204,11 +1210,9 @@ int nhi_probe(struct tb_nhi *nhi) /* In case someone left them on. */ nhi_disable_interrupts(nhi); - if (nhi->ops && nhi->ops->init_interrupts) { - res = nhi->ops->init_interrupts(nhi); - if (res) - return dev_err_probe(dev, res, "cannot enable interrupts, aborting\n"); - } + res = nhi->ops->init_interrupts(nhi); + if (res) + return dev_err_probe(dev, res, "cannot enable interrupts, aborting\n"); spin_lock_init(&nhi->lock); @@ -1216,7 +1220,7 @@ int nhi_probe(struct tb_nhi *nhi) if (res) return dev_err_probe(dev, res, "failed to set DMA mask\n"); - if (nhi->ops && nhi->ops->init) { + if (nhi->ops->init) { res = nhi->ops->init(nhi); if (res) return res; diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c index a29b9887cd6b..a830c82bb905 100644 --- a/drivers/thunderbolt/switch.c +++ b/drivers/thunderbolt/switch.c @@ -251,7 +251,7 @@ static int nvm_authenticate(struct tb_switch *sw, bool auth_only) sw->nvm->authenticating = true; if (!tb_route(sw)) { - if (nhi->ops && nhi->ops->pre_nvm_auth) + if (nhi->ops->pre_nvm_auth) nhi->ops->pre_nvm_auth(nhi); ret = nvm_authenticate_host_dma_port(sw); } else { @@ -2782,7 +2782,7 @@ static int tb_switch_add_dma_port(struct tb_switch *sw) nvm_get_auth_status(sw, &status); if (status) { if (!tb_route(sw)) { - if (nhi->ops && nhi->ops->post_nvm_auth) + if (nhi->ops->post_nvm_auth) nhi->ops->post_nvm_auth(nhi); } return 0; @@ -2799,7 +2799,7 @@ static int tb_switch_add_dma_port(struct tb_switch *sw) /* Now we can allow root port to suspend again */ if (!tb_route(sw)) { - if (nhi->ops && nhi->ops->post_nvm_auth) + if (nhi->ops->post_nvm_auth) nhi->ops->post_nvm_auth(nhi); } From 15bcac35ba045a9a459144901443210d8b1df7a3 Mon Sep 17 00:00:00 2001 From: Konrad Dybcio Date: Thu, 21 May 2026 12:40:03 +0200 Subject: [PATCH 36/41] thunderbolt: Add some more descriptive probe error messages Currently there's a lot of silent error-return paths in various places where nhi_probe() can fail. Sprinkle some prints to make it clearer where the problem is. Signed-off-by: Konrad Dybcio Signed-off-by: Mika Westerberg --- drivers/thunderbolt/nhi.c | 4 ++-- drivers/thunderbolt/tb.c | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c index e9ba8ffbe349..0f795ea58756 100644 --- a/drivers/thunderbolt/nhi.c +++ b/drivers/thunderbolt/nhi.c @@ -1223,7 +1223,7 @@ int nhi_probe(struct tb_nhi *nhi) if (nhi->ops->init) { res = nhi->ops->init(nhi); if (res) - return res; + return dev_err_probe(dev, res, "NHI specific init failed\n"); } tb = nhi_select_cm(nhi); @@ -1244,7 +1244,7 @@ int nhi_probe(struct tb_nhi *nhi) tb_domain_put(tb); wait_for_completion(&nhi->domain_released); nhi_shutdown(nhi); - return res; + return dev_err_probe(dev, res, "failed to add domain\n"); } dev_set_drvdata(dev, tb); diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index d60c0b8eb390..76323255439a 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -3000,7 +3000,8 @@ static int tb_start(struct tb *tb, bool reset) tb->root_switch = tb_switch_alloc(tb, &tb->dev, 0); if (IS_ERR(tb->root_switch)) - return PTR_ERR(tb->root_switch); + return dev_err_probe(tb->nhi->dev, PTR_ERR(tb->root_switch), + "failed to allocate host router\n"); /* * ICM firmware upgrade needs running firmware and in native @@ -3017,14 +3018,14 @@ static int tb_start(struct tb *tb, bool reset) ret = tb_switch_configure(tb->root_switch); if (ret) { tb_switch_put(tb->root_switch); - return ret; + return dev_err_probe(tb->nhi->dev, ret, "failed to configure host router\n"); } /* Announce the switch to the world */ ret = tb_switch_add(tb->root_switch); if (ret) { tb_switch_put(tb->root_switch); - return ret; + return dev_err_probe(tb->nhi->dev, ret, "failed to add host router\n"); } /* From d73a08958e66849ea713d2f458b2fcf7b183f987 Mon Sep 17 00:00:00 2001 From: Michael Bommarito Date: Mon, 25 May 2026 05:28:30 -0400 Subject: [PATCH 37/41] thunderbolt: test: Add KUnit tests for property parser bounds checks Add regression tests for the zero-length entry and root directory bounds fixes: - tb_test_property_parse_zero_length: TEXT entry with length 0 must be rejected by the validator. - tb_test_property_parse_rootdir_overflow: root directory whose content_offset + content_len exceeds block_len must be rejected. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: Michael Bommarito Signed-off-by: Mika Westerberg --- drivers/thunderbolt/test.c | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/drivers/thunderbolt/test.c b/drivers/thunderbolt/test.c index fa8bee0ccf8b..463186a1abf9 100644 --- a/drivers/thunderbolt/test.c +++ b/drivers/thunderbolt/test.c @@ -2975,6 +2975,44 @@ static void tb_test_property_parse_dir_len_underflow(struct kunit *test) tb_property_free_dir(dir); } +static void tb_test_property_parse_zero_length(struct kunit *test) +{ + u32 *block = kunit_kzalloc(test, 6 * sizeof(u32), GFP_KERNEL); + struct tb_property_dir *dir; + + KUNIT_ASSERT_NOT_NULL(test, block); + + block[0] = 0x55584401; /* rootdir magic */ + block[1] = 0x00000004; /* root length: one entry */ + + block[2] = 0x61616161; /* key_hi */ + block[3] = 0x61616161; /* key_lo */ + block[4] = 0x74000000; /* type=TEXT, reserved=0, length=0 */ + block[5] = 0x00000000; /* value */ + + dir = tb_property_parse_dir(block, 6); + KUNIT_EXPECT_NULL(test, dir); + tb_property_free_dir(dir); +} + +static void tb_test_property_parse_rootdir_overflow(struct kunit *test) +{ + u32 *block = kunit_kzalloc(test, 4 * sizeof(u32), GFP_KERNEL); + struct tb_property_dir *dir; + + KUNIT_ASSERT_NOT_NULL(test, block); + + block[0] = 0x55584401; /* rootdir magic */ + block[1] = 0x00000004; /* root length claims 4 dwords of content */ + block[2] = 0x61616161; + block[3] = 0x61616161; + + /* content_offset(2) + content_len(4) = 6 > block_len(4) */ + dir = tb_property_parse_dir(block, 4); + KUNIT_EXPECT_NULL(test, dir); + tb_property_free_dir(dir); +} + static void tb_test_property_merge(struct kunit *test) { struct tb_property_dir *dir1, *dir2, *dir3; @@ -3099,6 +3137,8 @@ static struct kunit_case tb_test_cases[] = { KUNIT_CASE(tb_test_property_parse), KUNIT_CASE(tb_test_property_format), KUNIT_CASE(tb_test_property_copy), + KUNIT_CASE(tb_test_property_parse_zero_length), + KUNIT_CASE(tb_test_property_parse_rootdir_overflow), KUNIT_CASE(tb_test_property_merge), { } }; From 2c5d2d3c3f70cde2565d7b279b544893a2035842 Mon Sep 17 00:00:00 2001 From: Michael Bommarito Date: Wed, 27 May 2026 07:46:04 -0400 Subject: [PATCH 38/41] thunderbolt: Prevent XDomain delayed work use-after-free on disconnect tb_xdp_handle_request() runs on system_wq and queues xd->state_work via queue_delayed_work() in three request handlers: PROPERTIES_CHANGED_REQUEST, UUID_REQUEST (via start_handshake), and LINK_STATE_CHANGE_REQUEST. Similarly, update_xdomain() queues xd->properties_changed_work when local properties change. Concurrently, tb_xdomain_remove() calls stop_handshake() which does cancel_delayed_work_sync() on both delayed works. Later, tb_xdomain_unregister() calls device_unregister() which eventually frees the xdomain. Since commit 559c1e1e0134 ("thunderbolt: Run tb_xdp_handle_request() in system workqueue") moved the request handler off tb->wq, the handler and the remove path are no longer serialized. If queue_delayed_work() executes after cancel_delayed_work_sync() but before the xdomain is freed, the delayed work fires on a freed object. Add xd->removing that tb_xdomain_remove() sets under xd->lock before calling stop_handshake(). Each external queue site holds the same lock and checks removing before calling queue_delayed_work(). This provides the mutual exclusion needed: either the queue site acquires the lock first and queues work that the subsequent cancel will see, or the remove path acquires the lock first and the queue site observes removing == true and skips the queue. Fixes: 559c1e1e0134 ("thunderbolt: Run tb_xdp_handle_request() in system workqueue") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-7 Signed-off-by: Michael Bommarito Signed-off-by: Mika Westerberg --- drivers/thunderbolt/xdomain.c | 41 ++++++++++++++++++++++++++--------- include/linux/thunderbolt.h | 3 +++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/drivers/thunderbolt/xdomain.c b/drivers/thunderbolt/xdomain.c index 781d88d06b93..fe6c5ac703f4 100644 --- a/drivers/thunderbolt/xdomain.c +++ b/drivers/thunderbolt/xdomain.c @@ -803,9 +803,13 @@ static void tb_xdp_handle_request(struct work_struct *work) * the xdomain related to this connection as well in * case there is a change in services it offers. */ - if (xd && device_is_registered(&xd->dev)) - queue_delayed_work(tb->wq, &xd->state_work, - msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT)); + if (xd) { + mutex_lock(&xd->lock); + if (!xd->removing && device_is_registered(&xd->dev)) + queue_delayed_work(tb->wq, &xd->state_work, + msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT)); + mutex_unlock(&xd->lock); + } break; case UUID_REQUEST_OLD: @@ -818,8 +822,12 @@ static void tb_xdp_handle_request(struct work_struct *work) * received UUID request from the remote host. */ if (!ret && xd && xd->state == XDOMAIN_STATE_ERROR) { - dev_dbg(&xd->dev, "restarting handshake\n"); - start_handshake(xd); + mutex_lock(&xd->lock); + if (!xd->removing) { + dev_dbg(&xd->dev, "restarting handshake\n"); + start_handshake(xd); + } + mutex_unlock(&xd->lock); } break; @@ -885,9 +893,13 @@ static void tb_xdp_handle_request(struct work_struct *work) ret = tb_xdp_link_state_change_response(ctl, route, sequence, 0); - xd->target_link_width = lsc->tlw; - queue_delayed_work(tb->wq, &xd->state_work, - msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT)); + mutex_lock(&xd->lock); + if (!xd->removing) { + xd->target_link_width = lsc->tlw; + queue_delayed_work(tb->wq, &xd->state_work, + msecs_to_jiffies(XDOMAIN_SHORT_TIMEOUT)); + } + mutex_unlock(&xd->lock); } else { tb_xdp_error_response(ctl, route, sequence, ERROR_NOT_READY); @@ -971,8 +983,12 @@ static int update_xdomain(struct device *dev, void *data) xd = tb_to_xdomain(dev); if (xd) { - queue_delayed_work(xd->tb->wq, &xd->properties_changed_work, - msecs_to_jiffies(50)); + mutex_lock(&xd->lock); + if (!xd->removing) + queue_delayed_work(xd->tb->wq, + &xd->properties_changed_work, + msecs_to_jiffies(50)); + mutex_unlock(&xd->lock); } return 0; @@ -2200,6 +2216,11 @@ static int unregister_service(struct device *dev, void *data) void tb_xdomain_remove(struct tb_xdomain *xd) { tb_xdomain_debugfs_remove(xd); + + mutex_lock(&xd->lock); + xd->removing = true; + mutex_unlock(&xd->lock); + stop_handshake(xd); tb_xdomain_link_exit(xd); diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h index b5659f883517..feb1af175cfd 100644 --- a/include/linux/thunderbolt.h +++ b/include/linux/thunderbolt.h @@ -213,6 +213,8 @@ enum tb_link_width { * @link_width: Width of the downstream facing link * @link_usb4: Downstream link is USB4 * @is_unplugged: The XDomain is unplugged + * @removing: Set by tb_xdomain_remove() under @lock to prevent + * concurrent delayed work queueing * @needs_uuid: If the XDomain does not have @remote_uuid it will be * queried first * @service_ids: Used to generate IDs for the services @@ -262,6 +264,7 @@ struct tb_xdomain { enum tb_link_width link_width; bool link_usb4; bool is_unplugged; + bool removing; bool needs_uuid; struct ida service_ids; struct ida in_hopids; From 168479c9bf07ee28c372c48eebbf66f2d01911dd Mon Sep 17 00:00:00 2001 From: Xu Rao Date: Mon, 1 Jun 2026 14:28:54 +0800 Subject: [PATCH 39/41] thunderbolt: test: Release third DP tunnel tb_test_tunnel_3dp() allocates three DisplayPort tunnels but only releases the first two before returning. Release the third tunnel as well to keep the test cleanup balanced. Signed-off-by: Xu Rao Signed-off-by: Mika Westerberg --- drivers/thunderbolt/test.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/thunderbolt/test.c b/drivers/thunderbolt/test.c index 463186a1abf9..05652ee82fbf 100644 --- a/drivers/thunderbolt/test.c +++ b/drivers/thunderbolt/test.c @@ -1661,6 +1661,7 @@ static void tb_test_tunnel_3dp(struct kunit *test) KUNIT_ASSERT_EQ(test, tunnel3->npaths, 3); KUNIT_ASSERT_EQ(test, tunnel3->paths[0]->path_length, 3); + tb_tunnel_put(tunnel3); tb_tunnel_put(tunnel2); tb_tunnel_put(tunnel1); } From 503c5ae1e72aa9ed91925dafa3d82ee2e992747f Mon Sep 17 00:00:00 2001 From: Xu Rao Date: Wed, 3 Jun 2026 17:59:57 +0800 Subject: [PATCH 40/41] thunderbolt: debugfs: Fix margining error counter buffer leak When USB4 lane margining debugfs write support is enabled, margining_error_counter_write() copies the user input with validate_and_copy_from_user(). This allocates a temporary page that is only needed while parsing the requested error counter mode. The function currently returns without freeing that page. This leaks one page per write to the error_counter debugfs file, including successful writes and writes that later fail while taking the domain lock or because software margining is not enabled. Free the temporary page once parsing has completed, and also before returning from the invalid-input path. Fixes: 10904df3f20c ("thunderbolt: Improve software receiver lane margining") Signed-off-by: Xu Rao Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index 569163a26afa..369904287497 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -956,7 +956,9 @@ margining_error_counter_write(struct file *file, const char __user *user_buf, else if (!strcmp(buf, "stop")) error_counter = USB4_MARGIN_SW_ERROR_COUNTER_STOP; else - return -EINVAL; + goto err_free; + + free_page((unsigned long)buf); scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &tb->lock) { if (!margining->software) @@ -966,6 +968,10 @@ margining_error_counter_write(struct file *file, const char __user *user_buf, } return count; + +err_free: + free_page((unsigned long)buf); + return -EINVAL; } static int margining_error_counter_show(struct seq_file *s, void *not_used) From c1bef05763c94ae284ee2881c03bf0753f8d213a Mon Sep 17 00:00:00 2001 From: Xu Rao Date: Mon, 8 Jun 2026 14:31:47 +0800 Subject: [PATCH 41/41] thunderbolt: debugfs: Fix sideband write size check sb_regs_write() looks up the matching sideband register entry before validating the number of bytes to write. However, the size check uses sb_regs->size, which is the size of the first entry in the register table, instead of the matched entry. This rejects valid writes to larger sideband registers such as USB4_SB_DEBUG or USB4_SB_DATA. Use the matched register entry for the size check. Signed-off-by: Xu Rao Signed-off-by: Mika Westerberg --- drivers/thunderbolt/debugfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/thunderbolt/debugfs.c b/drivers/thunderbolt/debugfs.c index 369904287497..f5cf0e177f40 100644 --- a/drivers/thunderbolt/debugfs.c +++ b/drivers/thunderbolt/debugfs.c @@ -366,7 +366,7 @@ static ssize_t sb_regs_write(struct tb_port *port, const struct sb_reg *sb_regs, if (!sb_reg) return -EINVAL; - if (bytes_read > sb_regs->size) + if (bytes_read > sb_reg->size) return -E2BIG; ret = usb4_port_sb_write(port, target, index, sb_reg->reg, data,