* [PATCH v4 0/2] usb: core: Pair ports of different host controllers through their connector
@ 2026-11-15 8:57 ` Arun Iyengar
0 siblings, 0 replies; 8+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-15 8:57 UTC (permalink / raw)
To: Greg Kroah-Hartman, Alan Stern
Cc: linux-usb, linux-kernel, Mathias Nyman, Chen-Yu Tsai, Rob Herring,
Arun Iyengar
On the RK3588, USB 3.0 host 2 is a SuperSpeed-only xHCI; the USB 2.0
lines of its connector go to a separate EHCI. The USB core pairs the
SuperSpeed port with a dead USB 2.0 port of the same xHCI. On the
Orange Pi 5 (VBUS always on) a USB 3 device present at boot connects
to the EHCI before the xHCI is up and stays at 480 Mb/s.
Patch 1 pairs the two ports through their usb-connector and marks them
hotplug. Patch 2 resets the USB 2.0 port of such a device when the
SuperSpeed root hub starts, so that it retries SuperSpeed.
The Orange Pi 5/5B DT goes to the Rockchip tree separately; its EHCI
"ports" node needs Chen-Yu Tsai's usb-hcd binding [1]. This series
does not depend on his M.2 series [2].
Tested on an Orange Pi 5 (7.3-based kernel): a USB 3 disk present at
boot came up at 5 Gb/s in 3/3 boots and after 8/8 xHCI rebinds; a
high-speed-only disk kept its drivers bound and its data. Not tested
with an external USB 3 hub.
Developed with Claude (Anthropic), which wrote the code and the commit
messages. I reviewed every change.
[1] https://lore.kernel.org/all/20261014082037.1774672-2-wenst@chromium.org/
[2] https://lore.kernel.org/all/20260916075745.3549953-1-wenst@chromium.org/
---
Changes in v4:
- Patch 1: say that dwc3-numbered graphs keep today's pairing (Sashiko)
- Patch 1: mark a connector-paired port hotplug (Sashiko)
- Patch 2: stop the peer reset work when the hub is quiescing (Sashiko)
- Patch 2: don't leave the EHCI port with its companion: reset once
- Patch 2: resume a runtime-suspended device before the reset
- Fewer, shorter comments; shorter cover
- Link to v3: https://patch.msgid.link/20261113-usb-port-pairing-v3-0-d6c6edb4341c@gmail.com
Changes in v3:
- Patch 1: find root hub ports in the graph by the raw controller port
number (usb_hcd_find_raw_port_number()) on both sides (Chen-Yu Tsai)
- Patch 1: don't skip a connector peer whose connect_type is "not
used"
- Shorter commit messages and comments; patch 2 code unchanged
- Link to v2: https://patch.msgid.link/20261112-usb-port-pairing-v2-0-45f5a1a29f1f@gmail.com
Changes in v2:
- Dropped v1 patch 1 (generic-ehci binding) for Chen-Yu Tsai's
usb-hcd binding (Chen-Yu Tsai, Rob Herring)
- Patch 2: reset only when the SuperSpeed root hub starts, never a hub,
only on USB 2.0 root hub ports of another controller; cancel in
hub_quiesce(), runtime PM reference, freezable hub_wq (Sashiko)
- v1 patch 4 (DTS) split out (Krzysztof Kozlowski)
- Link to v1: https://patch.msgid.link/20261110-usb-port-pairing-v1-0-9307e29d2ed2@gmail.com
---
Arun Iyengar (2):
usb: core: Pair ports of different hubs through their connector
usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer
drivers/usb/core/hub.c | 132 ++++++++++++++++++++++++++++++++++++++++++++++++
drivers/usb/core/hub.h | 3 ++
drivers/usb/core/of.c | 87 +++++++++++++++++++++++++++++++
drivers/usb/core/port.c | 62 +++++++++++++++++++++--
include/linux/usb/of.h | 8 +++
5 files changed, 288 insertions(+), 4 deletions(-)
---
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
change-id: 20261110-usb-port-pairing-af0f66f0ab2e
Best regards,
--
Arun Iyengar <aiyengar1990@gmail.com>
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH v4 0/2] usb: core: Pair ports of different host controllers through their connector @ 2026-11-15 8:57 ` Arun Iyengar 0 siblings, 0 replies; 8+ messages in thread From: Arun Iyengar @ 2026-11-15 8:57 UTC (permalink / raw) To: Greg Kroah-Hartman, Alan Stern Cc: linux-usb, linux-kernel, Mathias Nyman, Chen-Yu Tsai, Rob Herring, Arun Iyengar On the RK3588, USB 3.0 host 2 is a SuperSpeed-only xHCI; the USB 2.0 lines of its connector go to a separate EHCI. The USB core pairs the SuperSpeed port with a dead USB 2.0 port of the same xHCI. On the Orange Pi 5 (VBUS always on) a USB 3 device present at boot connects to the EHCI before the xHCI is up and stays at 480 Mb/s. Patch 1 pairs the two ports through their usb-connector and marks them hotplug. Patch 2 resets the USB 2.0 port of such a device when the SuperSpeed root hub starts, so that it retries SuperSpeed. The Orange Pi 5/5B DT goes to the Rockchip tree separately; its EHCI "ports" node needs Chen-Yu Tsai's usb-hcd binding [1]. This series does not depend on his M.2 series [2]. Tested on an Orange Pi 5 (7.3-based kernel): a USB 3 disk present at boot came up at 5 Gb/s in 3/3 boots and after 8/8 xHCI rebinds; a high-speed-only disk kept its drivers bound and its data. Not tested with an external USB 3 hub. Developed with Claude (Anthropic), which wrote the code and the commit messages. I reviewed every change. [1] https://lore.kernel.org/all/20261014082037.1774672-2-wenst@chromium.org/ [2] https://lore.kernel.org/all/20260916075745.3549953-1-wenst@chromium.org/ --- Changes in v4: - Patch 1: say that dwc3-numbered graphs keep today's pairing (Sashiko) - Patch 1: mark a connector-paired port hotplug (Sashiko) - Patch 2: stop the peer reset work when the hub is quiescing (Sashiko) - Patch 2: don't leave the EHCI port with its companion: reset once - Patch 2: resume a runtime-suspended device before the reset - Fewer, shorter comments; shorter cover - Link to v3: https://patch.msgid.link/20261113-usb-port-pairing-v3-0-d6c6edb4341c@gmail.com Changes in v3: - Patch 1: find root hub ports in the graph by the raw controller port number (usb_hcd_find_raw_port_number()) on both sides (Chen-Yu Tsai) - Patch 1: don't skip a connector peer whose connect_type is "not used" - Shorter commit messages and comments; patch 2 code unchanged - Link to v2: https://patch.msgid.link/20261112-usb-port-pairing-v2-0-45f5a1a29f1f@gmail.com Changes in v2: - Dropped v1 patch 1 (generic-ehci binding) for Chen-Yu Tsai's usb-hcd binding (Chen-Yu Tsai, Rob Herring) - Patch 2: reset only when the SuperSpeed root hub starts, never a hub, only on USB 2.0 root hub ports of another controller; cancel in hub_quiesce(), runtime PM reference, freezable hub_wq (Sashiko) - v1 patch 4 (DTS) split out (Krzysztof Kozlowski) - Link to v1: https://patch.msgid.link/20261110-usb-port-pairing-v1-0-9307e29d2ed2@gmail.com --- Arun Iyengar (2): usb: core: Pair ports of different hubs through their connector usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer drivers/usb/core/hub.c | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ drivers/usb/core/hub.h | 3 ++ drivers/usb/core/of.c | 87 +++++++++++++++++++++++++++++++ drivers/usb/core/port.c | 62 +++++++++++++++++++++-- include/linux/usb/of.h | 8 +++ 5 files changed, 288 insertions(+), 4 deletions(-) --- base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4 change-id: 20261110-usb-port-pairing-af0f66f0ab2e Best regards, -- Arun Iyengar <aiyengar1990@gmail.com> ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v4 1/2] usb: core: Pair ports of different hubs through their connector 2026-11-15 8:57 ` Arun Iyengar @ 2026-11-15 8:57 ` Arun Iyengar -1 siblings, 0 replies; 8+ messages in thread From: Arun Iyengar via B4 Relay @ 2026-11-15 8:57 UTC (permalink / raw) To: Greg Kroah-Hartman, Alan Stern Cc: linux-usb, linux-kernel, Mathias Nyman, Chen-Yu Tsai, Rob Herring, Arun Iyengar From: Arun Iyengar <aiyengar1990@gmail.com> The USB core pairs the USB 2.0 and SuperSpeed ports of a connector by port number within one xHCI, or by ACPI location. On the RK3588, USB 3.0 host 2 is SuperSpeed-only and the connector's USB 2.0 lines go to a separate EHCI, so the SuperSpeed port gets a dead USB 2.0 peer. When graph ports 0 (USB 2.0) and 1 (SuperSpeed) of a usb-connector lead to different hubs or host controllers, pair the two ports through the connector and mark them hotplug. Root hub ports are found by the controller's port number, as in ACPI; a controller graph in the dwc3 numbering (port@0 HS, port@1 SS) does not match and keeps today's pairing. Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com> --- drivers/usb/core/hub.h | 2 ++ drivers/usb/core/of.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ drivers/usb/core/port.c | 62 ++++++++++++++++++++++++++++++++--- include/linux/usb/of.h | 8 +++++ 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h index 9ebc5ef54a32..ab80164b4b46 100644 --- a/drivers/usb/core/hub.h +++ b/drivers/usb/core/hub.h @@ -97,6 +97,7 @@ struct usb_hub { * @usb3_lpm_u2_permit: whether USB3 U2 LPM is permitted. * @early_stop: whether port initialization will be stopped earlier. * @ignore_event: whether events of the port are ignored. + * @connector_peer: peer found through a connector. */ struct usb_port { struct usb_device *child; @@ -118,6 +119,7 @@ struct usb_port { unsigned int is_superspeed:1; unsigned int usb3_lpm_u1_permit:1; unsigned int usb3_lpm_u2_permit:1; + unsigned int connector_peer:1; }; #define to_usb_port(_dev) \ diff --git a/drivers/usb/core/of.c b/drivers/usb/core/of.c index a6ee20d8774e..df789cef265c 100644 --- a/drivers/usb/core/of.c +++ b/drivers/usb/core/of.c @@ -145,6 +145,93 @@ enum usb_port_connect_type usb_of_get_connect_type(struct usb_device *hub, int p } EXPORT_SYMBOL_GPL(usb_of_get_connect_type); +static bool usb_of_is_connector(const struct device_node *np) +{ + return of_device_is_compatible(np, "usb-a-connector") || + of_device_is_compatible(np, "usb-b-connector") || + of_device_is_compatible(np, "usb-c-connector"); +} + +static struct device_node *usb_of_connector_remote(struct device_node *con, + u32 con_port, u32 *port) +{ + struct device_node *ep, *remote; + struct of_endpoint endpoint; + + ep = of_graph_get_endpoint_by_regs(con, con_port, -1); + if (!ep) + return NULL; + + remote = of_graph_get_remote_endpoint(ep); + of_node_put(ep); + if (!remote) + return NULL; + + if (of_graph_parse_endpoint(remote, &endpoint)) { + of_node_put(remote); + return NULL; + } + *port = endpoint.port; + + ep = remote; + remote = of_graph_get_port_parent(ep); + of_node_put(ep); + + return remote; +} + +/** + * usb_of_get_connector_peer() - find a port's peer through its connector + * @hub: hub to which the port belongs + * @port: graph port of the port + * @superspeed: whether @hub is a SuperSpeed hub + * @peer_port: returns the graph port of the peer + * + * Return: the node with the peer port, with a reference, if the other half + * of @port's connector leads to a different node. NULL otherwise. + */ +struct device_node *usb_of_get_connector_peer(struct usb_device *hub, u32 port, + bool superspeed, u32 *peer_port) +{ + struct device_node *np = hub->dev.of_node; + struct device_node *ep, *con, *remote, *peer = NULL; + u32 remote_port; + + if (!np || !of_graph_is_present(np)) + return NULL; + + ep = of_graph_get_endpoint_by_regs(np, port, -1); + if (!ep) + return NULL; + + con = of_graph_get_remote_port_parent(ep); + of_node_put(ep); + if (!con) + return NULL; + + if (!usb_of_is_connector(con) || !of_device_is_available(con)) + goto out; + + remote = usb_of_connector_remote(con, superspeed ? 1 : 0, &remote_port); + if (!remote) + goto out; + of_node_put(remote); + if (remote != np || remote_port != port) + goto out; + + /* Same-node peers are paired by port number */ + peer = usb_of_connector_remote(con, superspeed ? 0 : 1, peer_port); + if (peer && (peer == np || !*peer_port || + !of_device_is_available(peer))) { + of_node_put(peer); + peer = NULL; + } +out: + of_node_put(con); + return peer; +} +EXPORT_SYMBOL_GPL(usb_of_get_connector_peer); + /** * usb_of_get_interface_node() - get a USB interface node * @udev: USB device of interface diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c index b4452b665f59..5b2041d891ec 100644 --- a/drivers/usb/core/port.c +++ b/drivers/usb/core/port.c @@ -649,10 +649,49 @@ static int match_location(struct usb_device *peer_hdev, void *p) return 0; } +static u32 usb_port_graph_id(struct usb_device *hdev, int port1) +{ + if (hdev->parent) + return port1; + return usb_hcd_find_raw_port_number(bus_to_hcd(hdev->bus), port1); +} + +struct usb_connector_peer { + struct usb_port *port_dev; + struct device_node *np; + u32 port; +}; + +static int match_connector_peer(struct usb_device *peer_hdev, void *p) +{ + struct usb_connector_peer *cp = p; + struct usb_hub *peer_hub = usb_hub_to_struct_hub(peer_hdev); + struct usb_port *peer; + int port1; + + if (!peer_hub || peer_hdev->dev.of_node != cp->np || + hub_is_superspeed(peer_hdev) == cp->port_dev->is_superspeed) + return 0; + + for (port1 = 1; port1 <= peer_hdev->maxchild; port1++) { + if (usb_port_graph_id(peer_hdev, port1) == cp->port) + break; + } + if (port1 > peer_hdev->maxchild) + return 0; + + peer = peer_hub->ports[port1 - 1]; + if (!peer) + return 0; + + link_peers_report(cp->port_dev, peer); + return 1; +} + /* * Find the peer port either via explicit platform firmware "location" - * data, the peer hcd for root hubs, or the upstream peer relationship - * for all other hubs. + * data, a DT connector, the peer hcd for root hubs, or the upstream peer + * relationship for all other hubs. */ static void find_and_link_peer(struct usb_hub *hub, int port1) { @@ -660,6 +699,7 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) struct usb_device *hdev = hub->hdev; struct usb_device *peer_hdev; struct usb_hub *peer_hub; + struct usb_connector_peer cp; /* * If location data is available then we can only peer this port @@ -671,7 +711,21 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) /* we link the peer in match_location() if found */ usb_for_each_dev(port_dev, match_location); return; - } else if (!hdev->parent) { + } + + cp.np = usb_of_get_connector_peer(hdev, usb_port_graph_id(hdev, port1), + port_dev->is_superspeed, &cp.port); + if (cp.np) { + cp.port_dev = port_dev; + port_dev->connector_peer = 1; + /* usb_of_get_connect_type() misses graph ports */ + port_dev->connect_type = USB_PORT_CONNECT_TYPE_HOT_PLUG; + usb_for_each_dev(&cp, match_connector_peer); + of_node_put(cp.np); + return; + } + + if (!hdev->parent) { struct usb_hcd *hcd = bus_to_hcd(hdev->bus); struct usb_hcd *peer_hcd = hcd->shared_hcd; @@ -703,7 +757,7 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) * does not have location data */ peer = peer_hub->ports[port1 - 1]; - if (peer && peer->location == 0) + if (peer && peer->location == 0 && !peer->connector_peer) link_peers_report(port_dev, peer); } diff --git a/include/linux/usb/of.h b/include/linux/usb/of.h index de42f14bd280..4be581bd2758 100644 --- a/include/linux/usb/of.h +++ b/include/linux/usb/of.h @@ -20,6 +20,8 @@ int of_usb_update_otg_caps(struct device_node *np, struct usb_otg_caps *otg_caps); enum usb_port_connect_type usb_of_get_connect_type(struct usb_device *hub, int port1); struct device_node *usb_of_get_device_node(struct usb_device *hub, int port1); +struct device_node *usb_of_get_connector_peer(struct usb_device *hub, u32 port, + bool superspeed, u32 *peer_port); bool usb_of_has_combined_node(struct usb_device *udev); struct device_node *usb_of_get_interface_node(struct usb_device *udev, u8 config, u8 ifnum); @@ -49,6 +51,12 @@ usb_of_get_device_node(struct usb_device *hub, int port1) { return NULL; } +static inline struct device_node * +usb_of_get_connector_peer(struct usb_device *hub, u32 port, bool superspeed, + u32 *peer_port) +{ + return NULL; +} static inline bool usb_of_has_combined_node(struct usb_device *udev) { return false; -- 2.55.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v4 1/2] usb: core: Pair ports of different hubs through their connector @ 2026-11-15 8:57 ` Arun Iyengar 0 siblings, 0 replies; 8+ messages in thread From: Arun Iyengar @ 2026-11-15 8:57 UTC (permalink / raw) To: Greg Kroah-Hartman, Alan Stern Cc: linux-usb, linux-kernel, Mathias Nyman, Chen-Yu Tsai, Rob Herring, Arun Iyengar The USB core pairs the USB 2.0 and SuperSpeed ports of a connector by port number within one xHCI, or by ACPI location. On the RK3588, USB 3.0 host 2 is SuperSpeed-only and the connector's USB 2.0 lines go to a separate EHCI, so the SuperSpeed port gets a dead USB 2.0 peer. When graph ports 0 (USB 2.0) and 1 (SuperSpeed) of a usb-connector lead to different hubs or host controllers, pair the two ports through the connector and mark them hotplug. Root hub ports are found by the controller's port number, as in ACPI; a controller graph in the dwc3 numbering (port@0 HS, port@1 SS) does not match and keeps today's pairing. Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com> --- drivers/usb/core/hub.h | 2 ++ drivers/usb/core/of.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ drivers/usb/core/port.c | 62 ++++++++++++++++++++++++++++++++--- include/linux/usb/of.h | 8 +++++ 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h index 9ebc5ef54a32..ab80164b4b46 100644 --- a/drivers/usb/core/hub.h +++ b/drivers/usb/core/hub.h @@ -97,6 +97,7 @@ struct usb_hub { * @usb3_lpm_u2_permit: whether USB3 U2 LPM is permitted. * @early_stop: whether port initialization will be stopped earlier. * @ignore_event: whether events of the port are ignored. + * @connector_peer: peer found through a connector. */ struct usb_port { struct usb_device *child; @@ -118,6 +119,7 @@ struct usb_port { unsigned int is_superspeed:1; unsigned int usb3_lpm_u1_permit:1; unsigned int usb3_lpm_u2_permit:1; + unsigned int connector_peer:1; }; #define to_usb_port(_dev) \ diff --git a/drivers/usb/core/of.c b/drivers/usb/core/of.c index a6ee20d8774e..df789cef265c 100644 --- a/drivers/usb/core/of.c +++ b/drivers/usb/core/of.c @@ -145,6 +145,93 @@ enum usb_port_connect_type usb_of_get_connect_type(struct usb_device *hub, int p } EXPORT_SYMBOL_GPL(usb_of_get_connect_type); +static bool usb_of_is_connector(const struct device_node *np) +{ + return of_device_is_compatible(np, "usb-a-connector") || + of_device_is_compatible(np, "usb-b-connector") || + of_device_is_compatible(np, "usb-c-connector"); +} + +static struct device_node *usb_of_connector_remote(struct device_node *con, + u32 con_port, u32 *port) +{ + struct device_node *ep, *remote; + struct of_endpoint endpoint; + + ep = of_graph_get_endpoint_by_regs(con, con_port, -1); + if (!ep) + return NULL; + + remote = of_graph_get_remote_endpoint(ep); + of_node_put(ep); + if (!remote) + return NULL; + + if (of_graph_parse_endpoint(remote, &endpoint)) { + of_node_put(remote); + return NULL; + } + *port = endpoint.port; + + ep = remote; + remote = of_graph_get_port_parent(ep); + of_node_put(ep); + + return remote; +} + +/** + * usb_of_get_connector_peer() - find a port's peer through its connector + * @hub: hub to which the port belongs + * @port: graph port of the port + * @superspeed: whether @hub is a SuperSpeed hub + * @peer_port: returns the graph port of the peer + * + * Return: the node with the peer port, with a reference, if the other half + * of @port's connector leads to a different node. NULL otherwise. + */ +struct device_node *usb_of_get_connector_peer(struct usb_device *hub, u32 port, + bool superspeed, u32 *peer_port) +{ + struct device_node *np = hub->dev.of_node; + struct device_node *ep, *con, *remote, *peer = NULL; + u32 remote_port; + + if (!np || !of_graph_is_present(np)) + return NULL; + + ep = of_graph_get_endpoint_by_regs(np, port, -1); + if (!ep) + return NULL; + + con = of_graph_get_remote_port_parent(ep); + of_node_put(ep); + if (!con) + return NULL; + + if (!usb_of_is_connector(con) || !of_device_is_available(con)) + goto out; + + remote = usb_of_connector_remote(con, superspeed ? 1 : 0, &remote_port); + if (!remote) + goto out; + of_node_put(remote); + if (remote != np || remote_port != port) + goto out; + + /* Same-node peers are paired by port number */ + peer = usb_of_connector_remote(con, superspeed ? 0 : 1, peer_port); + if (peer && (peer == np || !*peer_port || + !of_device_is_available(peer))) { + of_node_put(peer); + peer = NULL; + } +out: + of_node_put(con); + return peer; +} +EXPORT_SYMBOL_GPL(usb_of_get_connector_peer); + /** * usb_of_get_interface_node() - get a USB interface node * @udev: USB device of interface diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c index b4452b665f59..5b2041d891ec 100644 --- a/drivers/usb/core/port.c +++ b/drivers/usb/core/port.c @@ -649,10 +649,49 @@ static int match_location(struct usb_device *peer_hdev, void *p) return 0; } +static u32 usb_port_graph_id(struct usb_device *hdev, int port1) +{ + if (hdev->parent) + return port1; + return usb_hcd_find_raw_port_number(bus_to_hcd(hdev->bus), port1); +} + +struct usb_connector_peer { + struct usb_port *port_dev; + struct device_node *np; + u32 port; +}; + +static int match_connector_peer(struct usb_device *peer_hdev, void *p) +{ + struct usb_connector_peer *cp = p; + struct usb_hub *peer_hub = usb_hub_to_struct_hub(peer_hdev); + struct usb_port *peer; + int port1; + + if (!peer_hub || peer_hdev->dev.of_node != cp->np || + hub_is_superspeed(peer_hdev) == cp->port_dev->is_superspeed) + return 0; + + for (port1 = 1; port1 <= peer_hdev->maxchild; port1++) { + if (usb_port_graph_id(peer_hdev, port1) == cp->port) + break; + } + if (port1 > peer_hdev->maxchild) + return 0; + + peer = peer_hub->ports[port1 - 1]; + if (!peer) + return 0; + + link_peers_report(cp->port_dev, peer); + return 1; +} + /* * Find the peer port either via explicit platform firmware "location" - * data, the peer hcd for root hubs, or the upstream peer relationship - * for all other hubs. + * data, a DT connector, the peer hcd for root hubs, or the upstream peer + * relationship for all other hubs. */ static void find_and_link_peer(struct usb_hub *hub, int port1) { @@ -660,6 +699,7 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) struct usb_device *hdev = hub->hdev; struct usb_device *peer_hdev; struct usb_hub *peer_hub; + struct usb_connector_peer cp; /* * If location data is available then we can only peer this port @@ -671,7 +711,21 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) /* we link the peer in match_location() if found */ usb_for_each_dev(port_dev, match_location); return; - } else if (!hdev->parent) { + } + + cp.np = usb_of_get_connector_peer(hdev, usb_port_graph_id(hdev, port1), + port_dev->is_superspeed, &cp.port); + if (cp.np) { + cp.port_dev = port_dev; + port_dev->connector_peer = 1; + /* usb_of_get_connect_type() misses graph ports */ + port_dev->connect_type = USB_PORT_CONNECT_TYPE_HOT_PLUG; + usb_for_each_dev(&cp, match_connector_peer); + of_node_put(cp.np); + return; + } + + if (!hdev->parent) { struct usb_hcd *hcd = bus_to_hcd(hdev->bus); struct usb_hcd *peer_hcd = hcd->shared_hcd; @@ -703,7 +757,7 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) * does not have location data */ peer = peer_hub->ports[port1 - 1]; - if (peer && peer->location == 0) + if (peer && peer->location == 0 && !peer->connector_peer) link_peers_report(port_dev, peer); } diff --git a/include/linux/usb/of.h b/include/linux/usb/of.h index de42f14bd280..4be581bd2758 100644 --- a/include/linux/usb/of.h +++ b/include/linux/usb/of.h @@ -20,6 +20,8 @@ int of_usb_update_otg_caps(struct device_node *np, struct usb_otg_caps *otg_caps); enum usb_port_connect_type usb_of_get_connect_type(struct usb_device *hub, int port1); struct device_node *usb_of_get_device_node(struct usb_device *hub, int port1); +struct device_node *usb_of_get_connector_peer(struct usb_device *hub, u32 port, + bool superspeed, u32 *peer_port); bool usb_of_has_combined_node(struct usb_device *udev); struct device_node *usb_of_get_interface_node(struct usb_device *udev, u8 config, u8 ifnum); @@ -49,6 +51,12 @@ usb_of_get_device_node(struct usb_device *hub, int port1) { return NULL; } +static inline struct device_node * +usb_of_get_connector_peer(struct usb_device *hub, u32 port, bool superspeed, + u32 *peer_port) +{ + return NULL; +} static inline bool usb_of_has_combined_node(struct usb_device *udev) { return false; -- 2.55.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v4 1/2] usb: core: Pair ports of different hubs through their connector 2026-11-15 8:57 ` Arun Iyengar (?) @ 2026-11-15 9:50 ` sashiko-bot -1 siblings, 0 replies; 8+ messages in thread From: sashiko-bot @ 2026-11-15 9:50 UTC (permalink / raw) To: Arun Iyengar; +Cc: linux-usb Thank you for your contribution! Sashiko AI review found 3 potential issues to consider: - [High] usb: core: OF graph port numbering mismatch in usb_port_graph_id() - [Medium] usb: core: raw port number not passed to usb_of_get_connect_type() - [Medium] usb: core: update connect_type and connector_peer after device_register() -- commit a8c7879971bee16d5ebf91273578d78c7dde5445 Author: Arun Iyengar <aiyengar1990@gmail.com> usb: core: Pair ports of different hubs through their connector This commit pairs USB 2.0 and SuperSpeed ports across different hubs or host controllers when they are linked to the same usb-connector node in the OF graph. > diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c > index b4452b665f591..5b2041d891eca 100644 > --- a/drivers/usb/core/port.c > +++ b/drivers/usb/core/port.c > @@ -649,10 +649,49 @@ static int match_location(struct usb_device *peer_hdev, void *p) > return 0; > } > > +static u32 usb_port_graph_id(struct usb_device *hdev, int port1) > +{ > + if (hdev->parent) > + return port1; > + return usb_hcd_find_raw_port_number(bus_to_hcd(hdev->bus), port1); > +} > + > +struct usb_connector_peer { > + struct usb_port *port_dev; > + struct device_node *np; > + u32 port; > +}; > + > +static int match_connector_peer(struct usb_device *peer_hdev, void *p) > +{ > + struct usb_connector_peer *cp = p; > + struct usb_hub *peer_hub = usb_hub_to_struct_hub(peer_hdev); > + struct usb_port *peer; > + int port1; > + > + if (!peer_hub || peer_hdev->dev.of_node != cp->np || > + hub_is_superspeed(peer_hdev) == cp->port_dev->is_superspeed) > + return 0; > + > + for (port1 = 1; port1 <= peer_hdev->maxchild; port1++) { > + if (usb_port_graph_id(peer_hdev, port1) == cp->port) > + break; > + } [Severity: High] Does usb_port_graph_id() match the OF graph port numbering used by USB controller bindings, and does usb_of_get_connector_peer() leave USB 2.0 ports unpeered when the peer controller uses dwc3 graph numbering? On dwc3 xHCI controllers, xhci_find_raw_port_number() returns 1 for the High-Speed root hub port and 2 for the SuperSpeed root hub port, whereas USB controller DT bindings (such as snps,dwc3-common.yaml and rockchip,dwc3.yaml) define port@0 as High-Speed and port@1 as SuperSpeed. When find_and_link_peer() runs for a USB 2.0 hub port whose connector port@1 points to a dwc3 controller's port@1 (as on rk3576-lckfb-tspi-3m.dts), usb_of_get_connector_peer() checks !*peer_port, which rejects port@0 when starting from a SuperSpeed port but accepts *peer_port == 1 when starting from a USB 2.0 port. find_and_link_peer() then sets port_dev->connector_peer = 1 and returns early, while match_connector_peer() compares usb_port_graph_id() (2) against cp->port (1) and fails to link the ports. Meanwhile, when the dwc3 SuperSpeed root hub port runs find_and_link_peer(), usb_port_graph_id() returns 2, which misses port@1 on the controller node and falls through to default peering with the wrong port (or is blocked by !peer->connector_peer when the default peer was already marked). > @@ -671,7 +711,21 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) > /* we link the peer in match_location() if found */ > usb_for_each_dev(port_dev, match_location); > return; > - } else if (!hdev->parent) { > + } > + > + cp.np = usb_of_get_connector_peer(hdev, usb_port_graph_id(hdev, port1), > + port_dev->is_superspeed, &cp.port); > + if (cp.np) { > + cp.port_dev = port_dev; > + port_dev->connector_peer = 1; [Severity: Medium] Does setting port_dev->connector_peer and port_dev->connect_type in find_and_link_peer() happen too late, after device_register() has already exposed the port to sysfs? In usb_hub_create_port_device(), device_register(&port_dev->dev) publishes the sysfs attributes and emits KOBJ_ADD before find_and_link_peer() runs. Userspace handlers reacting to KOBJ_ADD can read the stale connect_type, and the unlocked bitfield write to connector_peer can race with sysfs writes to sibling bitfields in struct usb_port (early_stop_store() and usb3_lpm_permit_store()): CPU0 CPU1 usb_hub_create_port_device() device_register(&port_dev->dev) early_stop_store() port_dev->early_stop = ... find_and_link_peer() port_dev->connector_peer = 1; <-- bitfield RMW clobbers early_stop > + /* usb_of_get_connect_type() misses graph ports */ > + port_dev->connect_type = USB_PORT_CONNECT_TYPE_HOT_PLUG; [Severity: Medium] Why does find_and_link_peer() override port_dev->connect_type here instead of usb_hub_create_port_device() passing usb_port_graph_id(hdev, port1) to usb_of_get_connect_type()? usb_of_get_connect_type() already checks OF graph endpoints via of_graph_get_endpoint_by_regs(), but usb_hub_create_port_device() passes the HCD-relative port1 index instead of the controller's raw port number: drivers/usb/core/port.c:usb_hub_create_port_device() { ... port_dev->connect_type = usb_of_get_connect_type(hdev, port1); ... } On a split-roothub controller where usb_port_graph_id(hdev, port1) != port1, usb_of_get_connect_type() inspects the wrong port number on the controller DT node. Whenever usb_of_get_connector_peer() returns NULL (for example, when both ports of the connector belong to the same controller node, or for a hardwired child device node), connect_type is never fixed up and remains wrong (such as USB_PORT_NOT_USED), causing set_usb_port_removable() to misclassify hotplugged devices as fixed. -- Sashiko AI review · https://sashiko.dev/#/patchset/20261115-usb-port-pairing-v4-0-25d3c9adcc21@gmail.com?part=1 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v4 2/2] usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer 2026-11-15 8:57 ` Arun Iyengar @ 2026-11-15 8:57 ` Arun Iyengar via B4 Relay -1 siblings, 0 replies; 8+ messages in thread From: Arun Iyengar @ 2026-11-15 8:57 UTC (permalink / raw) To: Greg Kroah-Hartman, Alan Stern Cc: linux-usb, linux-kernel, Mathias Nyman, Chen-Yu Tsai, Rob Herring, Arun Iyengar A SuperSpeed device that finds no SuperSpeed link at power-up connects at high speed and retries SuperSpeed only after a VBUS cycle or a USB 2.0 bus reset. With VBUS always on and the connector's halves on different host controllers (Orange Pi 5 USB 3 Type-A port), a device present at boot reaches the EHCI before the SuperSpeed-only xHCI is up and stays at 480 Mb/s. When such a SuperSpeed root hub starts, reset the USB 2.0 peer port of a SuperSpeed-capable device once; hubs are skipped. A device that appears on the SuperSpeed port within 1 s is disconnected from the USB 2.0 port. One that stays at high speed is restored with usb_reset_device() and keeps its drivers bound (they don't see the first reset). Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com> --- drivers/usb/core/hub.c | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ drivers/usb/core/hub.h | 1 + 2 files changed, 133 insertions(+) diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c index 199d3f535a89..b56ee90e55f0 100644 --- a/drivers/usb/core/hub.c +++ b/drivers/usb/core/hub.c @@ -144,6 +144,8 @@ EXPORT_SYMBOL_GPL(ehci_cf_port_reset_rwsem); static int usb_reset_and_verify_device(struct usb_device *udev); static int hub_port_disable(struct usb_hub *hub, int port1, int set_state); +static void hub_queue_peer_reset(struct usb_hub *hub); +static void hub_peer_reset_work(struct work_struct *ws); static bool hub_port_warm_reset_required(struct usb_hub *hub, int port1, u16 portstatus); @@ -1346,6 +1348,10 @@ static void hub_activate(struct usb_hub *hub, enum hub_activation_type type) /* Scan all ports that need attention */ kick_hub_wq(hub); + + if (!hdev->parent && hub_is_superspeed(hdev) && + (type == HUB_INIT2 || type == HUB_INIT3)) + hub_queue_peer_reset(hub); abort: if (type == HUB_INIT2 || type == HUB_INIT3) { /* Allow autosuspend if it was suppressed */ @@ -1419,6 +1425,10 @@ static void hub_quiesce(struct usb_hub *hub, enum hub_quiescing_type type) /* Stop hub_wq and related activity */ timer_delete_sync(&hub->irq_urb_retry); flush_delayed_work(&hub->post_resume_work); + if (cancel_work_sync(&hub->peer_reset_work)) { + usb_autopm_put_interface_no_suspend(to_usb_interface(hub->intfdev)); + hub_put(hub); + } usb_kill_urb(hub->urb); if (hub->has_indicators) cancel_delayed_work_sync(&hub->leds); @@ -1978,6 +1988,7 @@ static int hub_probe(struct usb_interface *intf, const struct usb_device_id *id) INIT_DELAYED_WORK(&hub->leds, led_work); INIT_DELAYED_WORK(&hub->init_work, NULL); INIT_DELAYED_WORK(&hub->post_resume_work, hub_post_resume); + INIT_WORK(&hub->peer_reset_work, hub_peer_reset_work); INIT_WORK(&hub->events, hub_event); INIT_LIST_HEAD(&hub->onboard_devs); spin_lock_init(&hub->irq_urb_lock); @@ -3278,6 +3289,127 @@ static void usb_unlock_port(struct usb_port *port_dev) __release(&port_dev->status_lock); } +static void hub_reset_peer_child(struct usb_hub *hub, int port1, + struct usb_device *peer_hdev, int peer_port1) +{ + struct usb_hub *peer_hub; + struct usb_device *udev; + + usb_lock_device(peer_hdev); + if (hub->quiescing) + goto out; + peer_hub = usb_hub_to_struct_hub(peer_hdev); + if (!peer_hub || peer_port1 > peer_hdev->maxchild) + goto out; + + udev = peer_hub->ports[peer_port1 - 1]->child; + if (!udev || udev->speed != USB_SPEED_HIGH) + goto out; + + if (udev->descriptor.bDeviceClass == USB_CLASS_HUB) + goto out; + + usb_lock_device(udev); + if (udev->state != USB_STATE_NOTATTACHED && + udev->bos && udev->bos->ss_cap && + usb_autoresume_device(udev) == 0) { + struct usb_port *port_dev = peer_hub->ports[peer_port1 - 1]; + u16 portstatus, portchange; + int i, ret; + + dev_info(&udev->dev, + "SuperSpeed peer port available, resetting\n"); + /* A second reset hands the EHCI port to its companion */ + usb_lock_port(port_dev); + ret = hub_port_reset(peer_hub, peer_port1, NULL, + HUB_ROOT_RESET_TIME, false); + for (i = 0; !ret && i < 20; i++) { + msleep(50); + ret = hub->quiescing ? -ENODEV : + usb_hub_port_status(hub, port1, &portstatus, + &portchange); + if (!ret && (portstatus & USB_PORT_STAT_CONNECTION)) + ret = -ENOTCONN; + } + if (ret) + hub_port_logical_disconnect(peer_hub, peer_port1); + /* Else hub_wq resets the port again */ + for (i = 0; ret == -ENOTCONN && i < 20; i++) { + if (usb_hub_port_status(peer_hub, peer_port1, &portstatus, + &portchange) || + !(portstatus & USB_PORT_STAT_CONNECTION)) + break; + msleep(50); + } + usb_unlock_port(port_dev); + + if (!ret) + usb_reset_device(udev); + usb_autosuspend_device(udev); + } + usb_unlock_device(udev); +out: + usb_unlock_device(peer_hdev); +} + +static void hub_peer_reset_work(struct work_struct *ws) +{ + struct usb_hub *hub = container_of(ws, struct usb_hub, peer_reset_work); + struct usb_device *hdev = hub->hdev; + int port1; + + for (port1 = 1; ; port1++) { + struct usb_device *peer_hdev = NULL; + struct usb_port *port_dev; + int peer_port1 = 0; + + mutex_lock(&usb_port_peer_mutex); + if (port1 > hdev->maxchild || hub->quiescing) { + mutex_unlock(&usb_port_peer_mutex); + break; + } + port_dev = hub->ports[port1 - 1]; + if (port_dev->connector_peer && port_dev->peer) { + peer_hdev = to_usb_device(port_dev->peer->dev.parent->parent); + peer_port1 = port_dev->peer->portnum; + if (peer_hdev->parent || + peer_hdev->bus->controller == hdev->bus->controller) + peer_hdev = NULL; + else + usb_get_dev(peer_hdev); + } + mutex_unlock(&usb_port_peer_mutex); + + if (peer_hdev) { + hub_reset_peer_child(hub, port1, peer_hdev, peer_port1); + usb_put_dev(peer_hdev); + } + } + + usb_autopm_put_interface_async(to_usb_interface(hub->intfdev)); + hub_put(hub); +} + +static void hub_queue_peer_reset(struct usb_hub *hub) +{ + struct usb_interface *intf = to_usb_interface(hub->intfdev); + int port1; + + for (port1 = 1; port1 <= hub->hdev->maxchild; port1++) { + if (hub->ports[port1 - 1]->connector_peer) + break; + } + if (port1 > hub->hdev->maxchild) + return; + + usb_autopm_get_interface_no_resume(intf); + hub_get(hub); + if (!queue_work(hub_wq, &hub->peer_reset_work)) { + usb_autopm_put_interface_async(intf); + hub_put(hub); + } +} + #ifdef CONFIG_PM /* Check if a port is suspended(USB2.0 port) or in U3 state(USB3.0 port) */ diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h index ab80164b4b46..f6387b99c708 100644 --- a/drivers/usb/core/hub.h +++ b/drivers/usb/core/hub.h @@ -71,6 +71,7 @@ struct usb_hub { struct delayed_work leds; struct delayed_work init_work; struct delayed_work post_resume_work; + struct work_struct peer_reset_work; struct work_struct events; spinlock_t irq_urb_lock; struct timer_list irq_urb_retry; -- 2.55.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH v4 2/2] usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer @ 2026-11-15 8:57 ` Arun Iyengar via B4 Relay 0 siblings, 0 replies; 8+ messages in thread From: Arun Iyengar via B4 Relay @ 2026-11-15 8:57 UTC (permalink / raw) To: Greg Kroah-Hartman, Alan Stern Cc: linux-usb, linux-kernel, Mathias Nyman, Chen-Yu Tsai, Rob Herring, Arun Iyengar From: Arun Iyengar <aiyengar1990@gmail.com> A SuperSpeed device that finds no SuperSpeed link at power-up connects at high speed and retries SuperSpeed only after a VBUS cycle or a USB 2.0 bus reset. With VBUS always on and the connector's halves on different host controllers (Orange Pi 5 USB 3 Type-A port), a device present at boot reaches the EHCI before the SuperSpeed-only xHCI is up and stays at 480 Mb/s. When such a SuperSpeed root hub starts, reset the USB 2.0 peer port of a SuperSpeed-capable device once; hubs are skipped. A device that appears on the SuperSpeed port within 1 s is disconnected from the USB 2.0 port. One that stays at high speed is restored with usb_reset_device() and keeps its drivers bound (they don't see the first reset). Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com> --- drivers/usb/core/hub.c | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ drivers/usb/core/hub.h | 1 + 2 files changed, 133 insertions(+) diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c index 199d3f535a89..b56ee90e55f0 100644 --- a/drivers/usb/core/hub.c +++ b/drivers/usb/core/hub.c @@ -144,6 +144,8 @@ EXPORT_SYMBOL_GPL(ehci_cf_port_reset_rwsem); static int usb_reset_and_verify_device(struct usb_device *udev); static int hub_port_disable(struct usb_hub *hub, int port1, int set_state); +static void hub_queue_peer_reset(struct usb_hub *hub); +static void hub_peer_reset_work(struct work_struct *ws); static bool hub_port_warm_reset_required(struct usb_hub *hub, int port1, u16 portstatus); @@ -1346,6 +1348,10 @@ static void hub_activate(struct usb_hub *hub, enum hub_activation_type type) /* Scan all ports that need attention */ kick_hub_wq(hub); + + if (!hdev->parent && hub_is_superspeed(hdev) && + (type == HUB_INIT2 || type == HUB_INIT3)) + hub_queue_peer_reset(hub); abort: if (type == HUB_INIT2 || type == HUB_INIT3) { /* Allow autosuspend if it was suppressed */ @@ -1419,6 +1425,10 @@ static void hub_quiesce(struct usb_hub *hub, enum hub_quiescing_type type) /* Stop hub_wq and related activity */ timer_delete_sync(&hub->irq_urb_retry); flush_delayed_work(&hub->post_resume_work); + if (cancel_work_sync(&hub->peer_reset_work)) { + usb_autopm_put_interface_no_suspend(to_usb_interface(hub->intfdev)); + hub_put(hub); + } usb_kill_urb(hub->urb); if (hub->has_indicators) cancel_delayed_work_sync(&hub->leds); @@ -1978,6 +1988,7 @@ static int hub_probe(struct usb_interface *intf, const struct usb_device_id *id) INIT_DELAYED_WORK(&hub->leds, led_work); INIT_DELAYED_WORK(&hub->init_work, NULL); INIT_DELAYED_WORK(&hub->post_resume_work, hub_post_resume); + INIT_WORK(&hub->peer_reset_work, hub_peer_reset_work); INIT_WORK(&hub->events, hub_event); INIT_LIST_HEAD(&hub->onboard_devs); spin_lock_init(&hub->irq_urb_lock); @@ -3278,6 +3289,127 @@ static void usb_unlock_port(struct usb_port *port_dev) __release(&port_dev->status_lock); } +static void hub_reset_peer_child(struct usb_hub *hub, int port1, + struct usb_device *peer_hdev, int peer_port1) +{ + struct usb_hub *peer_hub; + struct usb_device *udev; + + usb_lock_device(peer_hdev); + if (hub->quiescing) + goto out; + peer_hub = usb_hub_to_struct_hub(peer_hdev); + if (!peer_hub || peer_port1 > peer_hdev->maxchild) + goto out; + + udev = peer_hub->ports[peer_port1 - 1]->child; + if (!udev || udev->speed != USB_SPEED_HIGH) + goto out; + + if (udev->descriptor.bDeviceClass == USB_CLASS_HUB) + goto out; + + usb_lock_device(udev); + if (udev->state != USB_STATE_NOTATTACHED && + udev->bos && udev->bos->ss_cap && + usb_autoresume_device(udev) == 0) { + struct usb_port *port_dev = peer_hub->ports[peer_port1 - 1]; + u16 portstatus, portchange; + int i, ret; + + dev_info(&udev->dev, + "SuperSpeed peer port available, resetting\n"); + /* A second reset hands the EHCI port to its companion */ + usb_lock_port(port_dev); + ret = hub_port_reset(peer_hub, peer_port1, NULL, + HUB_ROOT_RESET_TIME, false); + for (i = 0; !ret && i < 20; i++) { + msleep(50); + ret = hub->quiescing ? -ENODEV : + usb_hub_port_status(hub, port1, &portstatus, + &portchange); + if (!ret && (portstatus & USB_PORT_STAT_CONNECTION)) + ret = -ENOTCONN; + } + if (ret) + hub_port_logical_disconnect(peer_hub, peer_port1); + /* Else hub_wq resets the port again */ + for (i = 0; ret == -ENOTCONN && i < 20; i++) { + if (usb_hub_port_status(peer_hub, peer_port1, &portstatus, + &portchange) || + !(portstatus & USB_PORT_STAT_CONNECTION)) + break; + msleep(50); + } + usb_unlock_port(port_dev); + + if (!ret) + usb_reset_device(udev); + usb_autosuspend_device(udev); + } + usb_unlock_device(udev); +out: + usb_unlock_device(peer_hdev); +} + +static void hub_peer_reset_work(struct work_struct *ws) +{ + struct usb_hub *hub = container_of(ws, struct usb_hub, peer_reset_work); + struct usb_device *hdev = hub->hdev; + int port1; + + for (port1 = 1; ; port1++) { + struct usb_device *peer_hdev = NULL; + struct usb_port *port_dev; + int peer_port1 = 0; + + mutex_lock(&usb_port_peer_mutex); + if (port1 > hdev->maxchild || hub->quiescing) { + mutex_unlock(&usb_port_peer_mutex); + break; + } + port_dev = hub->ports[port1 - 1]; + if (port_dev->connector_peer && port_dev->peer) { + peer_hdev = to_usb_device(port_dev->peer->dev.parent->parent); + peer_port1 = port_dev->peer->portnum; + if (peer_hdev->parent || + peer_hdev->bus->controller == hdev->bus->controller) + peer_hdev = NULL; + else + usb_get_dev(peer_hdev); + } + mutex_unlock(&usb_port_peer_mutex); + + if (peer_hdev) { + hub_reset_peer_child(hub, port1, peer_hdev, peer_port1); + usb_put_dev(peer_hdev); + } + } + + usb_autopm_put_interface_async(to_usb_interface(hub->intfdev)); + hub_put(hub); +} + +static void hub_queue_peer_reset(struct usb_hub *hub) +{ + struct usb_interface *intf = to_usb_interface(hub->intfdev); + int port1; + + for (port1 = 1; port1 <= hub->hdev->maxchild; port1++) { + if (hub->ports[port1 - 1]->connector_peer) + break; + } + if (port1 > hub->hdev->maxchild) + return; + + usb_autopm_get_interface_no_resume(intf); + hub_get(hub); + if (!queue_work(hub_wq, &hub->peer_reset_work)) { + usb_autopm_put_interface_async(intf); + hub_put(hub); + } +} + #ifdef CONFIG_PM /* Check if a port is suspended(USB2.0 port) or in U3 state(USB3.0 port) */ diff --git a/drivers/usb/core/hub.h b/drivers/usb/core/hub.h index ab80164b4b46..f6387b99c708 100644 --- a/drivers/usb/core/hub.h +++ b/drivers/usb/core/hub.h @@ -71,6 +71,7 @@ struct usb_hub { struct delayed_work leds; struct delayed_work init_work; struct delayed_work post_resume_work; + struct work_struct peer_reset_work; struct work_struct events; spinlock_t irq_urb_lock; struct timer_list irq_urb_retry; -- 2.55.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH v4 2/2] usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer 2026-11-15 8:57 ` Arun Iyengar via B4 Relay (?) @ 2026-11-15 9:27 ` sashiko-bot -1 siblings, 0 replies; 8+ messages in thread From: sashiko-bot @ 2026-11-15 9:27 UTC (permalink / raw) To: Arun Iyengar; +Cc: linux-usb Thank you for your contribution! Sashiko AI review found 3 potential issues to consider: - [High] usb: hub: unnotified and unlocked port reset in hub_reset_peer_child() - [Medium] usb: hub: wrong peer disconnect on status error in hub_reset_peer_child() - [Medium] usb: hub: concurrent SuperSpeed enumeration before USB 2.0 peer disconnect -- commit 86b9ce000f28b25e9610cbbbb687f6dd87200f35 Author: Arun Iyengar <aiyengar1990@gmail.com> usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer When a SuperSpeed root hub starts, this commit queues work to reset any SuperSpeed-capable non-hub device attached at high speed on a USB 2.0 peer port from a different controller, disconnecting the USB 2.0 port if the device appears on the SuperSpeed port or restoring it with usb_reset_device() otherwise. > diff --git a/drivers/usb/core/hub.c b/drivers/usb/core/hub.c > index 199d3f535a891..b56ee90e55f0f 100644 > --- a/drivers/usb/core/hub.c > +++ b/drivers/usb/core/hub.c [ ... ] > @@ -3278,6 +3289,127 @@ static void usb_unlock_port(struct usb_port *port_dev) > __release(&port_dev->status_lock); > } > > +static void hub_reset_peer_child(struct usb_hub *hub, int port1, > + struct usb_device *peer_hdev, int peer_port1) > +{ > + struct usb_hub *peer_hub; > + struct usb_device *udev; > + > + usb_lock_device(peer_hdev); > + if (hub->quiescing) > + goto out; > + peer_hub = usb_hub_to_struct_hub(peer_hdev); > + if (!peer_hub || peer_port1 > peer_hdev->maxchild) > + goto out; > + > + udev = peer_hub->ports[peer_port1 - 1]->child; > + if (!udev || udev->speed != USB_SPEED_HIGH) > + goto out; > + > + if (udev->descriptor.bDeviceClass == USB_CLASS_HUB) > + goto out; > + > + usb_lock_device(udev); > + if (udev->state != USB_STATE_NOTATTACHED && > + udev->bos && udev->bos->ss_cap && > + usb_autoresume_device(udev) == 0) { > + struct usb_port *port_dev = peer_hub->ports[peer_port1 - 1]; > + u16 portstatus, portchange; > + int i, ret; > + > + dev_info(&udev->dev, > + "SuperSpeed peer port available, resetting\n"); > + /* A second reset hands the EHCI port to its companion */ > + usb_lock_port(port_dev); > + ret = hub_port_reset(peer_hub, peer_port1, NULL, > + HUB_ROOT_RESET_TIME, false); [Severity: High] When a SuperSpeed-capable device stays on the USB 2.0 port (for example, when attached through a USB 2.0 cable or when SuperSpeed link training fails), does calling hub_port_reset() directly here leave the physical device enabled at USB address 0 for a full second without holding address0_mutex or notifying bound interface drivers? Unlike usb_reset_device() -> usb_reset_and_verify_device(), this reset does not invoke driver pre_reset() callbacks, disable USB 2.0 hardware LPM, or acquire bus_to_hcd(peer_hdev->bus)->address0_mutex, and passing NULL for udev leaves udev->state as USB_STATE_CONFIGURED. While the loop below sleeps for 1000 ms polling the SuperSpeed port: - peer_port1 is enabled with the device responding to USB address 0, which can collide with concurrent enumeration on an external hub on the same USB 2.0 bus or a reset on another port of peer_hdev. - Interface drivers on udev that were just woken by usb_autoresume_device() can submit URBs to udev->devnum while the hardware is unconfigured at address 0, failing with bus errors or timing out in error recovery (such as usb_lock_device_for_reset() while usb_lock_device(udev) is held for over a second). - When usb_reset_device(udev) finally runs after the 1 s poll, any pre_reset() callback that communicates with the device will talk to hardware that has already been reset. > + for (i = 0; !ret && i < 20; i++) { > + msleep(50); > + ret = hub->quiescing ? -ENODEV : > + usb_hub_port_status(hub, port1, &portstatus, > + &portchange); > + if (!ret && (portstatus & USB_PORT_STAT_CONNECTION)) > + ret = -ENOTCONN; > + } > + if (ret) > + hub_port_logical_disconnect(peer_hub, peer_port1); [Severity: Medium] Can an error while polling the SuperSpeed port cause hub_reset_peer_child() to disconnect the high-speed device on peer_hub? If hub->quiescing becomes set (for instance during system suspend or HCD unbind) or usb_hub_port_status(hub, port1, ...) returns an error while the device is still attached at high speed on peer_port1, ret is overwritten with a negative errno. The if (ret) check then calls hub_port_logical_disconnect(peer_hub, peer_port1) and skips usb_reset_device(udev), disconnecting the working USB 2.0 device instead of restoring it. The same happens if the SuperSpeed port already had a connection before peer_port1 was reset. > + /* Else hub_wq resets the port again */ > + for (i = 0; ret == -ENOTCONN && i < 20; i++) { > + if (usb_hub_port_status(peer_hub, peer_port1, &portstatus, > + &portchange) || > + !(portstatus & USB_PORT_STAT_CONNECTION)) > + break; > + msleep(50); > + } > + usb_unlock_port(port_dev); > + > + if (!ret) > + usb_reset_device(udev); > + usb_autosuspend_device(udev); > + } > + usb_unlock_device(udev); > +out: > + usb_unlock_device(peer_hdev); > +} [Severity: Medium] When the device does move to the SuperSpeed port, can it be enumerated and bound on hub before the old high-speed udev is disconnected from peer_hub? hub_reset_peer_child() holds usb_lock_device(peer_hdev) across the reset and polling loops without locking the SuperSpeed port hub->ports[port1 - 1], and hub_wq allows concurrent work items across ports and hubs: kworker/0 (peer_reset_work) kworker/1 (hub->events) hub_reset_peer_child() usb_lock_device(peer_hdev) hub_port_reset(peer_hub, ...) /* device moves to SS port */ hub_port_wait_reset() /* sleeps up to 920 ms */ hub_event() /* on SS hub */ port_event(hub, port1) hub_port_connect_change() hub_port_connect() usb_new_device(ss_udev) /* binds SS drivers while old * HS udev is still bound */ hub_port_logical_disconnect() kick_hub_wq(peer_hub) usb_unlock_device(peer_hdev) hub_event() /* on peer_hub */ usb_lock_device(peer_hdev) usb_disconnect(&port_dev->child) Because hub_port_wait_reset() only exits early when USB_PORT_STAT_CONNECTION remains set, a device that drops its USB 2.0 connection during the reset causes hub_port_wait_reset() to poll until HUB_RESET_TIMEOUT (920 ms) while holding usb_lock_device(peer_hdev). Even after hub_port_logical_disconnect() kicks hub_wq for peer_hub, peer_hub's hub_event() cannot acquire usb_lock_device(peer_hdev) to run usb_disconnect() until hub_reset_peer_child() finishes, so both the old high-speed usb_device and the new SuperSpeed usb_device for the same hardware can be bound to drivers at the same time. Locking hub->ports[port1 - 1] with usb_lock_port() during hub_reset_peer_child() would prevent port_event() on the SuperSpeed hub from enumerating the new device until the peer reset work completes. -- Sashiko AI review · https://sashiko.dev/#/patchset/20261115-usb-port-pairing-v4-0-25d3c9adcc21@gmail.com?part=2 ^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-11-15 9:50 UTC | newest] Thread overview: 8+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-11-15 8:57 [PATCH v4 0/2] usb: core: Pair ports of different host controllers through their connector Arun Iyengar via B4 Relay 2026-11-15 8:57 ` Arun Iyengar 2026-11-15 8:57 ` [PATCH v4 1/2] usb: core: Pair ports of different hubs " Arun Iyengar via B4 Relay 2026-11-15 8:57 ` Arun Iyengar 2026-11-15 9:50 ` sashiko-bot 2026-11-15 8:57 ` [PATCH v4 2/2] usb: hub: Retry SuperSpeed for a device stuck on its USB 2.0 peer Arun Iyengar 2026-11-15 8:57 ` Arun Iyengar via B4 Relay 2026-11-15 9:27 ` sashiko-bot
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.