From: sashiko-bot@kernel.org
To: "Andrei Rusu de Castro" <arc@empyreal.works>
Cc: linux-usb@vger.kernel.org
Subject: Re: [PATCH v2] thunderbolt: Optionally keep PCIe tunnels up through suspend-to-idle
Date: Thu, 12 Nov 2026 02:27:07 +0000 [thread overview]
Message-ID: <sashiko-outbox-210188@kernel.org> (raw)
In-Reply-To: <20261112-tb-s2idle-keep-pcie-v2-b3c197ac@empyreal.works>
Thank you for your contribution! Sashiko AI review found 6 potential
issues to consider:
- [High] thunderbolt: missing tb_free_unplugged_xdomains() in
tb_resume_noirq()
- [Medium] thunderbolt: incomplete XDomain replacement and child rescan
on s2idle resume
- [Medium] thunderbolt: incomplete topology rescan in tb_complete() via
tb_scan_port()
- [Medium] thunderbolt: missing router enumeration and UID checks in
tb_check_kept_running()
- [Medium] thunderbolt: unquiesced delayed works access stopped ctl in
tb_suspend_noirq()
- [Medium] thunderbolt: stale DP tunnels after monitor unplug during
kept-running s2idle
--
commit a1b4dee4a46d2ea714a73ee4360b3adbd9dd4c3f
Author: Andrei Rusu de Castro <arc@empyreal.works>
thunderbolt: Optionally keep PCIe tunnels up through suspend-to-idle
This commit adds an s2idle_keep_pcie module parameter to keep USB4 routers
and tunnels up across suspend-to-idle when a PCIe tunnel is active, while
stopping hotplug handling and the control channel during sleep and checking
for topology changes on resume.
> diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c
> index 418ad0a61a5de..a7528b93a7037 100644
> --- a/drivers/thunderbolt/tb.c
> +++ b/drivers/thunderbolt/tb.c
[ ... ]
> @@ -3106,10 +3112,94 @@ static int tb_start(struct tb *tb, bool reset)
[ ... ]
> +static void tb_check_kept_running(struct tb_switch *sw)
> +{
> + struct tb_port *port;
> +
> + tb_switch_for_each_port(sw, port) {
> + struct tb_switch *child;
> + u64 uid;
> + int ret;
> +
> + if (!tb_port_is_null(port))
> + continue;
> +
> + if (port->xdomain) {
> + if (tb_wait_for_port(port, false) <= 0)
> + port->xdomain->is_unplugged = true;
> + continue;
> + }
[Severity: Medium]
What happens if an XDomain is unplugged during suspend-to-idle and
replaced by a router on the same port?
Because the link is up, tb_wait_for_port() returns 1 and
port->xdomain->is_unplugged stays false. Unlike tb_switch_resume(),
tb_check_kept_running() does not unlock the port and probe
tb_cfg_get_upstream_port(sw->tb->ctl, port->xdomain->route) to detect that
a router is now at that route.
When tb_complete() then runs, tb_domain_unregister_unplugged_xdomains()
does not unregister port->xdomain, and tb_scan_port() calls
tb_xdomain_remove() (which leaves a registered xd->dev on tb_bus_type) and
clears port->xdomain, leaking xd->dev on the bus and causing
tb_switch_add() for the new router to fail with -EEXIST due to the
colliding device name.
Also, in tb_complete(), tb_scan_port() returns early whenever port->remote
is already set without recursing into port->remote->sw, so replacements or
newly plugged devices on surviving child switches are skipped.
> + if (!tb_port_has_remote(port))
> + continue;
> +
> + child = port->remote->sw;
> + ret = tb_cfg_get_upstream_port(sw->tb->ctl, tb_route(child));
> + if (ret >= 0 && child->uid) {
> + if (tb_switch_is_usb4(child))
> + ret = usb4_switch_read_uid(child, &uid);
> + else
> + ret = tb_drom_read_uid_only(child, &uid);
> + if (!ret && uid != child->uid)
> + ret = -ENODEV;
> + }
[Severity: Medium]
Can a child router that power-cycled or reset in place during
suspend-to-idle pass this check while unenumerated?
ROUTER_CS_0 (read by tb_cfg_get_upstream_port()) and the UID registers or
DROM remain readable after a router reset even though ROUTER_CS_3_V
(checked by tb_switch_enumerated()) was cleared and all hardware HopID
tunnel entries were wiped. Because tb_resume_noirq() skips
tb_switch_configure() and tb_tunnel_activate() when tb->kept_running is
set, keeping a router whose ROUTER_CS_3_V bit is cleared leaves its
tunnels dead in hardware.
In addition, if child->uid is 0 (for example in safe mode), the
child->uid check skips UID verification altogether (whereas
tb_switch_resume() rejects !sw->uid with -ENODEV), and the upstream port
returned by tb_cfg_get_upstream_port() is not compared against
child->config.upstream_port_number.
> + if (ret < 0) {
> + tb_port_warn(port, "lost during suspend, disconnecting\n");
> + tb_sw_set_unplugged(child);
> + continue;
> + }
> +
> + tb_check_kept_running(child);
> + }
> +}
> +
> static int tb_suspend_noirq(struct tb *tb)
> {
> struct tb_cm *tcm = tb_priv(tb);
>
> + if (tb_keep_running_s2idle(tb)) {
> + tb_dbg(tb, "PCIe tunnels active, keeping the domain running\n");
> + /* As for freeze, no hotplug handling until resume */
> + tcm->hotplug_active = false;
> + tb->kept_running = true;
> + return 0;
> + }
[Severity: Medium]
Can delayed work items access tb->ctl while it is stopped during
suspend-to-idle?
Skipping tb_disconnect_and_release_dp() leaves DP tunnels and bandwidth
groups active while tb_domain_suspend_noirq() stops tb->ctl. Because
group->release_work (on system_percpu_wq) and tunnel->dprx_work or
tcm->remove_work (on tb->wq, which is allocated without WQ_FREEZABLE) are
neither canceled nor guarded by tcm->hotplug_active, they can run during
suspend-to-idle and issue config reads/writes on the stopped control
channel, triggering WARN() in tb_cfg_read() and tb_cfg_write() on
-ENOTCONN and leaving software state out of sync with hardware.
[Severity: Medium]
What happens if a DisplayPort monitor on a surviving router is unplugged
during suspend-to-idle while tb->kept_running is set?
Because tb_disconnect_and_release_dp() is skipped and hotplug events are
ignored while asleep, the DP tunnel stays active. On resume,
tb_check_kept_running() skips non-null ports, and tb_scan_port() only
checks DP OUT ports where !tb_dp_port_is_enabled(port), so lost HPD on an
enabled DP OUT port is never handled and the stale DP tunnel and DP IN
resource remain allocated.
[ ... ]
> @@ -3177,6 +3267,17 @@ static int tb_resume_noirq(struct tb *tb)
> unsigned int usb3_delay = 0;
> LIST_HEAD(tunnels);
>
> + if (tb->kept_running) {
> + tb->kept_running = false;
> + tb_dbg(tb, "domain kept running, checking for changes\n");
> + tb_check_kept_running(tb->root_switch);
> + tb_free_invalid_tunnels(tb);
> + tb_free_unplugged_children(tb->root_switch);
> + tcm->rescan_on_complete = true;
> + tcm->hotplug_active = true;
> + return 0;
> + }
[Severity: High]
Does this miss a call to tb_free_unplugged_xdomains(tb->root_switch)?
If an XDomain attached to tb->root_switch or a surviving child switch is
unplugged during suspend-to-idle, tb_check_kept_running() sets
port->xdomain->is_unplugged = true, but tb_free_unplugged_children() only
processes ports with tb_port_has_remote(port). Without
tb_free_unplugged_xdomains(), tb_xdomain_remove() is never called and
port->xdomain is not cleared.
During dpm_resume, tb_xdomain_resume() then restarts xd->state_work, and
tb_complete() -> tb_domain_unregister_unplugged_xdomains() unregisters and
frees the struct tb_xdomain while xd->state_work is still queued and
port->xdomain still points to the freed object, causing a use-after-free
(or leaking xd if xd->dev was not yet registered on tb_bus_type).
[ ... ]
> @@ -3250,12 +3351,17 @@ static int tb_thaw_noirq(struct tb *tb)
>
> static void tb_complete(struct tb *tb)
> {
> + struct tb_cm *tcm = tb_priv(tb);
> + bool rescan = tcm->rescan_on_complete;
> +
> + tcm->rescan_on_complete = false;
> +
> /*
> * 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.
> */
> - if (tb_domain_unregister_unplugged_xdomains(tb)) {
> + if (tb_domain_unregister_unplugged_xdomains(tb) || rescan) {
> scoped_guard(mutex, &tb->lock)
> tb_scan_switch(tb->root_switch);
> }
[Severity: Medium]
Does tb_scan_switch(tb->root_switch) rescan downstream ports of child
routers that stayed up during suspend-to-idle?
In tb_scan_port(), any port that already has port->remote set returns
early without recursing into port->remote->sw:
drivers/thunderbolt/tb.c:tb_scan_port() {
...
if (port->remote) {
tb_port_dbg(port, "port already has a remote\n");
goto out_rpm_put;
}
...
}
Because s2idle_keep_pcie only triggers when a downstream router with a
PCIe tunnel is present on tb->root_switch, port->remote is non-NULL for
every surviving child router, so devices or monitors plugged into or
replaced behind a kept-running dock or enclosure during sleep are never
scanned on resume.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112-tb-s2idle-keep-pcie-v2-b3c197ac@empyreal.works?part=1
prev parent reply other threads:[~2026-11-12 2:27 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-11 22:57 [PATCH] thunderbolt: Optionally keep PCIe tunnels up through suspend-to-idle Andrei Rusu de Castro
2026-11-11 23:35 ` sashiko-bot
2026-11-12 1:58 ` [PATCH v2] " Andrei Rusu de Castro
2026-11-12 2:27 ` sashiko-bot [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-210188@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=arc@empyreal.works \
--cc=linux-usb@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox