All of lore.kernel.org
 help / color / mirror / Atom feed
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

  reply	other threads:[~2026-11-12  2:27 UTC|newest]

Thread overview: 5+ 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]
2026-11-12  5:49 ` [PATCH] " Mika Westerberg

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 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.