From: Mario Limonciello <mario.limonciello@amd.com>
To: Basavaraj Natikar <Basavaraj.Natikar@amd.com>,
Andreas Noever <andreas.noever@gmail.com>,
Mika Westerberg <westeri@kernel.org>,
Yehezkel Bernat <YehezkelShB@gmail.com>,
Jonathan Corbet <corbet@lwn.net>,
Andrew Lunn <andrew+netdev@lunn.ch>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@kernel.org>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>
Cc: linux-usb@vger.kernel.org, linux-doc@vger.kernel.org,
Sanath S <Sanath.S@amd.com>,
Mika Westerberg <mika.westerberg@linux.intel.com>
Subject: Re: [PATCH -next v2 2/3] thunderbolt: Reset the host interface before reusing a DMA HopID
Date: Mon, 5 Oct 2026 14:38:35 -0500 [thread overview]
Message-ID: <396730a0-5a45-4509-b0fb-9b16229cf567@amd.com> (raw)
In-Reply-To: <20261005164718.4166200-3-Basavaraj.Natikar@amd.com>
On 10/5/26 11:47, Basavaraj Natikar wrote:
> Reusing a DMA HopID without an intervening host interface reset can hang
> the TX ring on some host routers. Resetting on every DMA tunnel teardown
> clears the state but, as the reset affects all rings, also disturbs
> unrelated active tunnels.
>
> Hence, track the DMA HopIDs programmed since the last reset, prefer unused
> HopIDs when allocating rings, and check for reuse at tb_ring_start() too,
> since networking retains its rings across reconnect. Return -EAGAIN instead
> of programming a HopID that still needs a reset.
>
> Run the reset from a work item once all DMA rings are idle: serialize it
> with the connection manager, stop the control channel around it, and block
> DMA rings from starting during the reset. Fence the work across domain
> removal and PM transitions, preserve live DMA rings across freeze/thaw, and
> restore the interrupt-mask shadow under the NHI lock after the reset.
>
> On -EAGAIN, retry the networking login asynchronously and block the work
> producers before teardown cancels the workers. Keep peer disconnection
> separate from administrative shutdown so it cannot reopen the login gate,
> while stream and DMA-test callers unwind immediately and return the error
> to userspace.
>
> Enable this only for reset-capable host interfaces marked with
> QUIRK_RESET_DMA_ON_REUSE. A competing DMA tunnel must stop before its dirty
> HopID can be reused, and retrying does not interrupt that tunnel.
>
> Suggested-by: Mika Westerberg <mika.westerberg@linux.intel.com>
> Link: https://lore.kernel.org/linux-usb/20260831130638.GK124825@black.igk.intel.com/T/#m668c2efcfe2f298632537721d72b17445b39211a
> Co-developed-by: Sanath S <Sanath.S@amd.com>
> Signed-off-by: Sanath S <Sanath.S@amd.com>
> Signed-off-by: Basavaraj Natikar <Basavaraj.Natikar@amd.com>
> ---
> Documentation/admin-guide/thunderbolt.rst | 6 +
> drivers/net/thunderbolt/main.c | 106 +++++++----
> drivers/thunderbolt/ctl.c | 13 ++
> drivers/thunderbolt/ctl.h | 1 +
> drivers/thunderbolt/domain.c | 84 +++++++--
> drivers/thunderbolt/nhi.c | 215 ++++++++++++++++++++--
> drivers/thunderbolt/nhi.h | 2 +-
> include/linux/thunderbolt.h | 20 ++
> 8 files changed, 371 insertions(+), 76 deletions(-)
>
> diff --git a/Documentation/admin-guide/thunderbolt.rst b/Documentation/admin-guide/thunderbolt.rst
> index ff25fe853706..5595d307ac86 100644
> --- a/Documentation/admin-guide/thunderbolt.rst
> +++ b/Documentation/admin-guide/thunderbolt.rst
> @@ -410,6 +410,12 @@ transfer data::
> host2 # cat /dev/tbstream0
> host1 # dmesg > /dev/tbstream0
>
> +On affected host interfaces a DMA HopID cannot be reused until the host
> +interface has been reset. Opening a stream or starting a DMA test can return
> +``EAGAIN`` until all other DMA tunnels stop and the reset completes. Retry
> +the complete operation; it does not interrupt an active networking tunnel.
> +If stream resume fails, close and reopen the stream to retry setup.
> +
> Once you are done with the stream you can remove them::
>
> host2 # cd /sys/kernel/config/thunderbolt/stream
> diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c
> index 93ccccc5cf8b..f2dbb6fb30c6 100644
> --- a/drivers/net/thunderbolt/main.c
> +++ b/drivers/net/thunderbolt/main.c
> @@ -160,6 +160,9 @@ struct tbnet_ring {
> * @login_sent: ThunderboltIP login message successfully sent
> * @login_received: ThunderboltIP login message received from the remote
> * host
> + * @stopping: Administrative teardown blocks all connection work
> + * @disconnecting: Peer logout blocks login until teardown completes
> + * @connected: DMA rings and paths were successfully enabled
> * @local_transmit_path: HopID we are using to send out packets
> * @remote_transmit_path: HopID the other end is using to send packets to us
> * @connection_lock: Lock serializing access to @login_sent,
> @@ -190,6 +193,9 @@ struct tbnet {
> atomic_t command_id;
> bool login_sent;
> bool login_received;
> + bool stopping;
> + bool disconnecting;
> + bool connected;
> int local_transmit_path;
> int remote_transmit_path;
> struct mutex connection_lock;
> @@ -311,17 +317,21 @@ static void start_login(struct tbnet *net)
> {
> netdev_dbg(net->dev, "login started\n");
>
> - mutex_lock(&net->connection_lock);
> + guard(mutex)(&net->connection_lock);
> net->login_sent = false;
> net->login_received = false;
> - mutex_unlock(&net->connection_lock);
> -
> + net->stopping = false;
> + net->disconnecting = false;
> queue_delayed_work(system_long_wq, &net->login_work,
> msecs_to_jiffies(1000));
> }
>
> static void stop_login(struct tbnet *net)
> {
> + scoped_guard(mutex, &net->connection_lock)
> + net->stopping = true;
> +
> + cancel_work_sync(&net->disconnect_work);
> cancel_delayed_work_sync(&net->login_work);
> cancel_work_sync(&net->connected_work);
>
> @@ -371,11 +381,7 @@ static void tbnet_tear_down(struct tbnet *net, bool send_logout)
> netif_carrier_off(net->dev);
> netif_stop_queue(net->dev);
>
> - stop_login(net);
> -
> - mutex_lock(&net->connection_lock);
> -
> - if (net->login_sent && net->login_received) {
> + if (net->connected) {
> int ret, retries = TBNET_LOGOUT_RETRIES;
>
> while (send_logout && retries-- > 0) {
> @@ -413,13 +419,13 @@ static void tbnet_tear_down(struct tbnet *net, bool send_logout)
> net->remote_transmit_path = 0;
> }
>
> + guard(mutex)(&net->connection_lock);
> + net->connected = false;
> net->login_retries = 0;
> net->login_sent = false;
> net->login_received = false;
>
> netdev_dbg(net->dev, "network traffic stopped\n");
> -
> - mutex_unlock(&net->connection_lock);
> }
>
> static int tbnet_handle_packet(const void *buf, size_t size, void *data)
> @@ -459,7 +465,11 @@ static int tbnet_handle_packet(const void *buf, size_t size, void *data)
> if (!ret) {
> netdev_dbg(net->dev, "remote login response sent\n");
>
> - mutex_lock(&net->connection_lock);
> + guard(mutex)(&net->connection_lock);
> + if (net->stopping || net->disconnecting ||
> + (net->login_received &&
> + net->remote_transmit_path != pkg->transmit_path))
> + break;
> net->login_received = true;
> net->remote_transmit_path = pkg->transmit_path;
>
> @@ -473,8 +483,6 @@ static int tbnet_handle_packet(const void *buf, size_t size, void *data)
> queue_delayed_work(system_long_wq,
> &net->login_work, 0);
> }
> - mutex_unlock(&net->connection_lock);
> -
> queue_work(system_long_wq, &net->connected_work);
> }
> break;
> @@ -484,7 +492,12 @@ static int tbnet_handle_packet(const void *buf, size_t size, void *data)
> ret = tbnet_logout_response(net, route, sequence, command_id);
> if (!ret) {
> netdev_dbg(net->dev, "remote logout response sent\n");
> - queue_work(system_long_wq, &net->disconnect_work);
> + guard(mutex)(&net->connection_lock);
> + if (netif_running(net->dev) && !net->stopping &&
> + !net->disconnecting) {
> + net->disconnecting = true;
> + queue_work(system_long_wq, &net->disconnect_work);
> + }
> }
> break;
>
> @@ -644,9 +657,9 @@ static void tbnet_connected_work(struct work_struct *work)
> if (netif_carrier_ok(net->dev))
> return;
>
> - mutex_lock(&net->connection_lock);
> - connected = net->login_sent && net->login_received;
> - mutex_unlock(&net->connection_lock);
> + scoped_guard(mutex, &net->connection_lock)
> + connected = !net->stopping && !net->disconnecting &&
> + net->login_sent && net->login_received;
>
> if (!connected)
> return;
> @@ -697,8 +710,13 @@ static void tbnet_connected_work(struct work_struct *work)
> goto err_free_tx_buffers;
> }
>
> - netif_carrier_on(net->dev);
> - netif_start_queue(net->dev);
> + scoped_guard(mutex, &net->connection_lock) {
> + net->connected = true;
> + if (!net->stopping && !net->disconnecting) {
> + netif_carrier_on(net->dev);
> + netif_start_queue(net->dev);
> + }
> + }
>
> netdev_dbg(net->dev, "network traffic started\n");
> return;
> @@ -714,24 +732,34 @@ static void tbnet_connected_work(struct work_struct *work)
> err_release_hopid:
> tb_xdomain_release_in_hopid(net->xd, net->remote_transmit_path);
> tbnet_connect_failed(net);
> + if (ret == -EAGAIN) {
> + guard(mutex)(&net->connection_lock);
> + if (!net->stopping && !net->disconnecting)
> + queue_delayed_work(system_long_wq, &net->login_work,
> + msecs_to_jiffies(TBNET_LOGIN_DELAY));
> + }
> }
>
> static void tbnet_login_work(struct work_struct *work)
> {
> struct tbnet *net = container_of(work, typeof(*net), login_work.work);
> unsigned long delay = msecs_to_jiffies(TBNET_LOGIN_DELAY);
> - int ret;
> -
> - if (netif_carrier_ok(net->dev))
> - return;
> + int ret, retries;
>
> - netdev_dbg(net->dev, "sending login request, retries=%u\n",
> - net->login_retries);
> + scoped_guard(mutex, &net->connection_lock) {
> + if (net->stopping || net->disconnecting || net->connected)
> + return;
> + retries = net->login_retries;
> + }
>
> - ret = tbnet_login_request(net, net->login_retries % 4);
> + netdev_dbg(net->dev, "sending login request, retries=%u\n", retries);
> + ret = tbnet_login_request(net, retries % 4);
> if (ret) {
> netdev_dbg(net->dev, "sending login request failed, ret=%d\n",
> ret);
> + guard(mutex)(&net->connection_lock);
> + if (net->stopping || net->disconnecting)
> + return;
> if (net->login_retries++ < TBNET_LOGIN_RETRIES) {
> queue_delayed_work(system_long_wq, &net->login_work,
> delay);
> @@ -741,13 +769,12 @@ static void tbnet_login_work(struct work_struct *work)
> } else {
> netdev_dbg(net->dev, "received login reply\n");
>
> - net->login_retries = 0;
> -
> - mutex_lock(&net->connection_lock);
> - net->login_sent = true;
> - mutex_unlock(&net->connection_lock);
> -
> - queue_work(system_long_wq, &net->connected_work);
> + guard(mutex)(&net->connection_lock);
> + if (!net->stopping && !net->disconnecting) {
> + net->login_retries = 0;
> + net->login_sent = true;
> + queue_work(system_long_wq, &net->connected_work);
> + }
> }
> }
>
> @@ -755,7 +782,11 @@ static void tbnet_disconnect_work(struct work_struct *work)
> {
> struct tbnet *net = container_of(work, typeof(*net), disconnect_work);
>
> + cancel_delayed_work_sync(&net->login_work);
> + cancel_work_sync(&net->connected_work);
> tbnet_tear_down(net, false);
> + guard(mutex)(&net->connection_lock);
> + net->disconnecting = false;
> }
>
> static bool tbnet_check_frame(struct tbnet *net, const struct tbnet_frame *tf,
> @@ -1008,9 +1039,8 @@ static int tbnet_stop(struct net_device *dev)
> {
> struct tbnet *net = netdev_priv(dev);
>
> + stop_login(net);
> napi_disable(&net->napi);
> -
> - cancel_work_sync(&net->disconnect_work);
> tbnet_tear_down(net, true);
>
> tb_ring_free(net->rx_ring.ring);
> @@ -1397,6 +1427,7 @@ static int tbnet_probe(struct tb_service *svc)
> net->svc = svc;
> net->dev = dev;
> net->xd = xd;
> + net->stopping = true;
>
> tbnet_generate_mac(dev);
>
> @@ -1456,7 +1487,10 @@ static void tbnet_remove(struct tb_service *svc)
>
> static void tbnet_shutdown(struct tb_service *svc)
> {
> - tbnet_tear_down(tb_service_get_drvdata(svc), true);
> + struct tbnet *net = tb_service_get_drvdata(svc);
> +
> + stop_login(net);
> + tbnet_tear_down(net, true);
> }
>
> static int tbnet_suspend(struct device *dev)
> diff --git a/drivers/thunderbolt/ctl.c b/drivers/thunderbolt/ctl.c
> index ef98ef83fd61..bbd31b1e35e7 100644
> --- a/drivers/thunderbolt/ctl.c
> +++ b/drivers/thunderbolt/ctl.c
> @@ -780,6 +780,19 @@ void tb_ctl_stop(struct tb_ctl *ctl)
> tb_ctl_dbg(ctl, "control channel stopped\n");
> }
>
> +/* Close the enqueue gate only after all existing requests have drained. */
> +bool tb_ctl_stop_if_idle(struct tb_ctl *ctl)
> +{
> + scoped_guard(mutex, &ctl->request_queue_lock) {
> + if (!ctl->running || !list_empty(&ctl->request_queue))
> + return false;
> + ctl->running = false;
> + }
> +
> + tb_ctl_stop(ctl);
> + return true;
> +}
> +
> /* public interface, commands */
>
> /**
> diff --git a/drivers/thunderbolt/ctl.h b/drivers/thunderbolt/ctl.h
> index db1646eb4fd0..a88c61fd628a 100644
> --- a/drivers/thunderbolt/ctl.h
> +++ b/drivers/thunderbolt/ctl.h
> @@ -25,6 +25,7 @@ struct tb_ctl *tb_ctl_alloc(struct tb_nhi *nhi, int index, int timeout_msec,
> event_cb cb, void *cb_data);
> void tb_ctl_start(struct tb_ctl *ctl);
> void tb_ctl_stop(struct tb_ctl *ctl);
> +bool tb_ctl_stop_if_idle(struct tb_ctl *ctl);
> void tb_ctl_free(struct tb_ctl *ctl);
>
> /* configuration commands */
> diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c
> index 12c88509a54f..c34f12487b9e 100644
> --- a/drivers/thunderbolt/domain.c
> +++ b/drivers/thunderbolt/domain.c
> @@ -314,11 +314,20 @@ const struct bus_type tb_bus_type = {
> .shutdown = tb_service_shutdown,
> };
>
> +/* Reset work needs tb->lock to finish. */
> +static void tb_domain_cancel_nhi_reset(struct tb *tb)
> +{
> + lockdep_assert_not_held(&tb->lock);
> + cancel_delayed_work_sync(&tb->nhi->reset_work);
> +}
> +
> static void tb_domain_release(struct device *dev)
> {
> struct tb *tb = container_of(dev, struct tb, dev);
> struct tb_nhi *nhi = tb->nhi;
>
> + /* The host interface reset runs against this domain */
> + tb_domain_cancel_nhi_reset(tb);
> tb_ctl_free(tb->ctl);
> destroy_workqueue(tb->wq);
> ida_free(&tb_domain_ida, tb->index);
> @@ -505,8 +514,14 @@ void tb_domain_remove(struct tb *tb)
> tb->cm_ops->stop(tb);
> /* Stop the domain control traffic */
> tb_ctl_stop(tb->ctl);
> + /* Keep reset_work from restarting it again below */
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->removing = true;
> + }
> mutex_unlock(&tb->lock);
>
> + tb_domain_cancel_nhi_reset(tb);
> flush_workqueue(tb->wq);
>
> if (tb->cm_ops->deinit)
> @@ -535,10 +550,24 @@ int tb_domain_suspend_noirq(struct tb *tb)
> mutex_lock(&tb->lock);
> if (tb->cm_ops->suspend_noirq)
> ret = tb->cm_ops->suspend_noirq(tb);
> - if (!ret)
> + if (!ret) {
> + /* Keep reset_work from restarting the control channel below */
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->suspended = true;
> + }
> tb_ctl_stop(tb->ctl);
> + }
> mutex_unlock(&tb->lock);
>
> + /*
> + * Make sure a host interface reset queued by cm_ops->suspend_noirq()
> + * tearing down a DMA tunnel above either finishes or is cancelled
> + * outright before the NHI is powered down below - either outcome
> + * is fine since a dirty bitmap is handled again on resume.
> + */
> + tb_domain_cancel_nhi_reset(tb);
> +
> return ret;
> }
>
> @@ -556,6 +585,10 @@ int tb_domain_resume_noirq(struct tb *tb)
> int ret = 0;
>
> mutex_lock(&tb->lock);
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->suspended = false;
> + }
> tb_ctl_start(tb->ctl);
> if (tb->cm_ops->resume_noirq)
> ret = tb->cm_ops->resume_noirq(tb);
> @@ -576,10 +609,18 @@ int tb_domain_freeze_noirq(struct tb *tb)
> mutex_lock(&tb->lock);
> if (tb->cm_ops->freeze_noirq)
> ret = tb->cm_ops->freeze_noirq(tb);
> - if (!ret)
> + if (!ret) {
> + /* Keep reset_work from restarting the control channel below */
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->suspended = true;
> + }
> tb_ctl_stop(tb->ctl);
> + }
> mutex_unlock(&tb->lock);
>
> + tb_domain_cancel_nhi_reset(tb);
> +
> return ret;
> }
>
> @@ -588,6 +629,10 @@ int tb_domain_thaw_noirq(struct tb *tb)
> int ret = 0;
>
> mutex_lock(&tb->lock);
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->suspended = false;
> + }
> tb_ctl_start(tb->ctl);
> if (tb->cm_ops->thaw_noirq)
> ret = tb->cm_ops->thaw_noirq(tb);
> @@ -609,13 +654,30 @@ int tb_domain_runtime_suspend(struct tb *tb)
> if (ret)
> return ret;
> }
> +
> + /* Keep reset_work from restarting the control channel below */
> + mutex_lock(&tb->lock);
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->suspended = true;
> + }
> tb_ctl_stop(tb->ctl);
> + mutex_unlock(&tb->lock);
> +
> + tb_domain_cancel_nhi_reset(tb);
> +
> return 0;
> }
>
> int tb_domain_runtime_resume(struct tb *tb)
> {
> + mutex_lock(&tb->lock);
> + scoped_guard(spinlock_irq, &tb->nhi->lock) {
> + if (tb->nhi->dma_hops_used)
> + tb->nhi->suspended = false;
> + }
> tb_ctl_start(tb->ctl);
> + mutex_unlock(&tb->lock);
> if (tb->cm_ops->runtime_resume) {
> int ret = tb->cm_ops->runtime_resume(tb);
> if (ret)
> @@ -788,21 +850,6 @@ int tb_domain_approve_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
> transmit_ring, receive_path, receive_ring);
> }
>
> -static void tb_domain_reset_interface(struct tb *tb)
> -{
> - struct tb_nhi *nhi = tb->nhi;
> -
> - if (!nhi->ops->reset_interface)
> - return;
> -
> - guard(mutex)(&tb->lock);
> -
> - /* The reset clears the ring state so stop the control channel */
> - tb_ctl_stop(tb->ctl);
> - nhi->ops->reset_interface(nhi);
> - tb_ctl_start(tb->ctl);
> -}
> -
> /**
> * tb_domain_disconnect_xdomain_paths() - Disable DMA paths for XDomain
> * @tb: Domain disabling the DMA paths
> @@ -835,9 +882,6 @@ int tb_domain_disconnect_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
> if (ret)
> return ret;
>
> - if (tb->nhi->quirks & QUIRK_RESET_DMA_ON_TEARDOWN)
> - tb_domain_reset_interface(tb);
> -
> return 0;
> }
>
> diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c
> index c44ec3aa04c5..461d39410740 100644
> --- a/drivers/thunderbolt/nhi.c
> +++ b/drivers/thunderbolt/nhi.c
> @@ -174,7 +174,8 @@ static void ring_interrupt_active(struct tb_ring *ring, bool active)
> /*
> * nhi_disable_interrupts() - disable interrupts for all rings
> *
> - * Use only during init and shutdown.
> + * During runtime reset the caller must hold nhi->lock.
> + * Init and shutdown callers must exclude concurrent ring operations.
> */
> void nhi_disable_interrupts(struct tb_nhi *nhi)
> {
> @@ -512,7 +513,8 @@ void tb_ring_poll_complete(struct tb_ring *ring)
>
> spin_lock_irqsave(&ring->nhi->lock, flags);
> spin_lock(&ring->lock);
> - if (ring->start_poll)
> + if (ring->start_poll && ring->running && !ring->nhi->resetting &&
> + !ring->nhi->going_away)
> __ring_interrupt_mask(ring, false);
> spin_unlock(&ring->lock);
> spin_unlock_irqrestore(&ring->nhi->lock, flags);
> @@ -548,6 +550,84 @@ irqreturn_t ring_msix(int irq, void *data)
> return IRQ_HANDLED;
> }
>
> +static bool ring_is_dma(const struct tb_ring *ring)
> +{
> + return ring->hop >= RING_FIRST_USABLE_HOPID;
> +}
> +
> +/* TX and RX HopIDs are tracked separately */
> +static unsigned int nhi_dma_hops_bits(const struct tb_nhi *nhi)
> +{
> + return 2 * nhi->hop_count;
> +}
> +
> +static unsigned int nhi_hop_bit(const struct tb_nhi *nhi, bool is_tx,
> + unsigned int hop)
> +{
> + return is_tx ? hop : nhi->hop_count + hop;
> +}
> +
> +/* Returns %true if a DMA HopID has been programmed since the last reset */
> +static bool nhi_dma_hops_dirty(const struct tb_nhi *nhi)
> +{
> + return nhi->dma_hops_used &&
> + !bitmap_empty(nhi->dma_hops_used, nhi_dma_hops_bits(nhi));
> +}
> +
> +/* Returns %true if the HopID of @ring cannot be programmed again yet */
> +static bool ring_needs_reset(const struct tb_ring *ring)
> +{
> + const struct tb_nhi *nhi = ring->nhi;
> +
> + lockdep_assert_held(&nhi->lock);
> +
> + if (!nhi->dma_hops_used || !ring_is_dma(ring))
> + return false;
> +
> + return test_bit(nhi_hop_bit(nhi, ring->is_tx, ring->hop),
> + nhi->dma_hops_used);
> +}
> +
> +static bool nhi_dma_rings_running(const struct tb_nhi *nhi)
> +{
> + unsigned int i;
> +
> + lockdep_assert_held(&nhi->lock);
> +
> + /* ring->running is only updated under nhi->lock */
> + for (i = RING_FIRST_USABLE_HOPID; i < nhi->hop_count; i++) {
> + if (nhi->tx_rings[i] && nhi->tx_rings[i]->running)
> + return true;
> + if (nhi->rx_rings[i] && nhi->rx_rings[i]->running)
> + return true;
> + }
> +
> + return false;
> +}
> +
> +static int nhi_find_hop(const struct tb_nhi *nhi, const struct tb_ring *ring,
> + unsigned int start_hop, bool skip_used)
> +{
> + unsigned int i;
> +
> + lockdep_assert_held(&nhi->lock);
> +
> + for (i = start_hop; i < nhi->hop_count; i++) {
> + if (skip_used && test_bit(nhi_hop_bit(nhi, ring->is_tx, i),
> + nhi->dma_hops_used))
> + continue;
> + if (ring->is_tx) {
> + if (!nhi->tx_rings[i])
> + return i;
> + } else {
> + if (!nhi->rx_rings[i])
> + return i;
> + }
> + }
> +
> + return -EBUSY;
> +}
> +
> static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring)
> {
> unsigned int start_hop = RING_FIRST_USABLE_HOPID;
> @@ -565,25 +645,20 @@ static int nhi_alloc_hop(struct tb_nhi *nhi, struct tb_ring *ring)
> spin_lock_irq(&nhi->lock);
>
> if (ring->hop < 0) {
> - unsigned int i;
> + int hop;
>
> /*
> * Automatically allocate HopID from the non-reserved
> - * range 1 .. hop_count - 1.
> + * range 1 .. hop_count - 1, preferring the ones that do
> + * not need a host interface reset first.
> */
> - for (i = start_hop; i < nhi->hop_count; i++) {
> - if (ring->is_tx) {
> - if (!nhi->tx_rings[i]) {
> - ring->hop = i;
> - break;
> - }
> - } else {
> - if (!nhi->rx_rings[i]) {
> - ring->hop = i;
> - break;
> - }
> - }
> - }
> + hop = -EBUSY;
> + if (nhi->dma_hops_used)
> + hop = nhi_find_hop(nhi, ring, start_hop, true);
> + if (hop < 0)
> + hop = nhi_find_hop(nhi, ring, start_hop, false);
> + if (hop >= 0)
> + ring->hop = hop;
> }
>
> if (ring->hop > 0 && ring->hop < start_hop) {
> @@ -732,6 +807,47 @@ struct tb_ring *tb_ring_alloc_rx(struct tb_nhi *nhi, int hop, int size,
> }
> EXPORT_SYMBOL_GPL(tb_ring_alloc_rx);
>
> +/* Cancel only through tb_domain_cancel_nhi_reset(), outside tb->lock. */
> +static void nhi_reset_work(struct work_struct *work)
> +{
> + struct tb_nhi *nhi = container_of(to_delayed_work(work), struct tb_nhi,
> + reset_work);
> + struct tb *tb = dev_get_drvdata(nhi->dev);
> +
> + /* The connection manager must be blocked over the reset */
> + guard(mutex)(&tb->lock);
> +
> + scoped_guard(spinlock_irq, &nhi->lock) {
> + if (nhi->going_away || nhi->removing || nhi->suspended)
> + return;
> + if (bitmap_empty(nhi->dma_hops_used, nhi_dma_hops_bits(nhi)))
> + return;
> + if (nhi_dma_rings_running(nhi))
> + return;
> +
> + /* Keep the DMA rings from starting over the reset */
> + nhi->resetting = true;
> + }
> +
> + if (!tb_ctl_stop_if_idle(tb->ctl)) {
> + scoped_guard(spinlock_irq, &nhi->lock) {
> + nhi->resetting = false;
> + queue_delayed_work(system_long_wq, &nhi->reset_work,
> + msecs_to_jiffies(100));
> + }
> + return;
> + }
> + nhi->ops->reset_interface(nhi);
> + scoped_guard(spinlock_irqsave, &nhi->lock)
> + nhi_disable_interrupts(nhi);
> + tb_ctl_start(tb->ctl);
Don't you need a request_queue_lock() here?
Since this reset path can run concurrently with tbnet_login_work()?
> +
> + scoped_guard(spinlock_irq, &nhi->lock) {
> + bitmap_zero(nhi->dma_hops_used, nhi_dma_hops_bits(nhi));
> + nhi->resetting = false;
> + }
> +}
> +
> /**
> * tb_ring_start() - enable a ring
> * @ring: Ring to start
> @@ -748,7 +864,7 @@ int tb_ring_start(struct tb_ring *ring)
>
> spin_lock_irq(&ring->nhi->lock);
> spin_lock(&ring->lock);
> - if (ring->nhi->going_away) {
> + if (ring->nhi->going_away || ring->nhi->removing) {
> ret = -ENODEV;
> goto err;
> }
> @@ -757,6 +873,16 @@ int tb_ring_start(struct tb_ring *ring)
> ret = -EBUSY;
> goto err;
> }
> + if (ring->nhi->dma_hops_used && ring_is_dma(ring) &&
> + (ring->nhi->suspended || ring->nhi->resetting ||
> + ring_needs_reset(ring))) {
> + ret = -EAGAIN;
> + if (!ring->nhi->suspended && !ring->nhi->resetting &&
> + !nhi_dma_rings_running(ring->nhi))
> + queue_delayed_work(system_long_wq,
> + &ring->nhi->reset_work, 0);
> + goto err;
> + }
> dev_dbg(ring->nhi->dev, "starting %s %d\n",
> RING_TYPE(ring), ring->hop);
>
> @@ -809,6 +935,9 @@ int tb_ring_start(struct tb_ring *ring)
> if (!(ring->flags & RING_FLAG_NO_INTERRUPT))
> ring_interrupt_active(ring, true);
> ring->running = true;
> + if (ring->nhi->dma_hops_used && ring_is_dma(ring))
> + __set_bit(nhi_hop_bit(ring->nhi, ring->is_tx, ring->hop),
> + ring->nhi->dma_hops_used);
> err:
> spin_unlock(&ring->lock);
> spin_unlock_irq(&ring->nhi->lock);
> @@ -881,6 +1010,11 @@ void tb_ring_stop(struct tb_ring *ring)
> ring->notify_pending = false;
> ring->running = false;
>
> + if (ring->nhi->dma_hops_used && ring_is_dma(ring) &&
> + !ring->nhi->removing && !ring->nhi->suspended &&
> + !nhi_dma_rings_running(ring->nhi))
> + queue_delayed_work(system_long_wq, &ring->nhi->reset_work, 0);
> +
> err:
> spin_unlock(&ring->lock);
> spin_unlock_irq(&ring->nhi->lock);
> @@ -1113,10 +1247,30 @@ static int nhi_freeze_noirq(struct device *dev)
> return tb_domain_freeze_noirq(tb);
> }
>
> +static void nhi_reset_if_idle(struct tb_nhi *nhi)
> +{
> + scoped_guard(spinlock_irq, &nhi->lock) {
> + if (nhi->going_away || nhi->removing ||
> + !nhi_dma_hops_dirty(nhi) || nhi_dma_rings_running(nhi))
> + return;
> + nhi->resetting = true;
> + }
> +
> + nhi->ops->reset_interface(nhi);
> + scoped_guard(spinlock_irqsave, &nhi->lock)
> + nhi_disable_interrupts(nhi);
> + scoped_guard(spinlock_irq, &nhi->lock) {
> + bitmap_zero(nhi->dma_hops_used, nhi_dma_hops_bits(nhi));
> + nhi->resetting = false;
> + }
> +}
> +
> static int nhi_thaw_noirq(struct device *dev)
> {
> struct tb *tb = dev_get_drvdata(dev);
>
> + /* Freeze can preserve live DMA rings. */
> + nhi_reset_if_idle(tb->nhi);
> return tb_domain_thaw_noirq(tb);
> }
>
> @@ -1161,6 +1315,8 @@ static int nhi_resume_noirq(struct device *dev)
> return ret;
> }
>
> + nhi_reset_if_idle(nhi);
> +
> return tb_domain_resume_noirq(tb);
> }
>
> @@ -1216,6 +1372,8 @@ static int nhi_runtime_resume(struct device *dev)
> return ret;
> }
>
> + nhi_reset_if_idle(nhi);
> +
> return tb_domain_runtime_resume(tb);
> }
>
> @@ -1339,6 +1497,7 @@ int nhi_probe(struct tb_nhi *nhi)
> {
> struct device *dev = nhi->dev;
> struct tb *tb;
> + u32 caps;
> int res;
>
> if (!nhi->ops)
> @@ -1347,7 +1506,8 @@ int nhi_probe(struct tb_nhi *nhi)
> 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;
> + caps = ioread32(nhi->iobase + REG_CAPS);
> + nhi->hop_count = caps & 0x3ff;
> dev_dbg(dev, "total paths: %d\n", nhi->hop_count);
>
> nhi->tx_rings = devm_kcalloc(dev, nhi->hop_count,
> @@ -1360,6 +1520,23 @@ int nhi_probe(struct tb_nhi *nhi)
> if (!nhi->tx_rings || !nhi->rx_rings || !nhi->interrupt_mask)
> return -ENOMEM;
>
> + INIT_DELAYED_WORK(&nhi->reset_work, nhi_reset_work);
> +
> + if ((nhi->quirks & QUIRK_RESET_DMA_ON_REUSE) &&
> + nhi->ops->reset_interface) {
> + /* Only v1 host interfaces implement the reset */
> + if (FIELD_GET(REG_CAPS_VERSION_MASK, caps) < REG_CAPS_VERSION_2) {
> + nhi->dma_hops_used = devm_bitmap_zalloc(dev,
> + nhi_dma_hops_bits(nhi),
> + GFP_KERNEL);
> + if (!nhi->dma_hops_used)
> + return -ENOMEM;
> + } else {
> + dev_warn(dev,
> + "reset-on-reuse quirk requires a v1 host interface, disabling\n");
> + }
> + }
> +
> nhi_reset(nhi);
>
> /* In case someone left them on. */
> diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h
> index b2e2e2c413b2..6bd519d70a51 100644
> --- a/drivers/thunderbolt/nhi.h
> +++ b/drivers/thunderbolt/nhi.h
> @@ -139,7 +139,7 @@ struct tb_nhi_ops {
> /* Host interface quirks */
> #define QUIRK_AUTO_CLEAR_INT BIT(0)
> #define QUIRK_E2E BIT(1)
> -#define QUIRK_RESET_DMA_ON_TEARDOWN BIT(2)
> +#define QUIRK_RESET_DMA_ON_REUSE BIT(2)
>
> /*
> * Minimal number of vectors when we use MSI-X. Two for control channel
> diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h
> index 598357b048af..c5bbb02f7cfd 100644
> --- a/include/linux/thunderbolt.h
> +++ b/include/linux/thunderbolt.h
> @@ -514,6 +514,21 @@ void tb_service_properties_changed(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
> + * @dma_hops_used: Bitmap of the TX and RX DMA HopIDs programmed after the
> + * last host interface reset. Only with
> + * %QUIRK_RESET_DMA_ON_REUSE.
> + * @resetting: The host interface reset is in progress. Only with
> + * %QUIRK_RESET_DMA_ON_REUSE.
> + * @removing: The domain this host interface belongs to is being removed.
> + * Set before releasing the domain lock so
> + * reset_work does not restart the control channel while the
> + * domain is tearing down. Only with %QUIRK_RESET_DMA_ON_REUSE.
> + * @suspended: The domain's control channel is stopped for system or runtime PM.
> + * Set before the control channel is stopped so reset_work
> + * does not restart it while the NHI is about to be powered
> + * down. Only with %QUIRK_RESET_DMA_ON_REUSE.
> + * @reset_work: Work that runs the host interface reset once all the DMA
> + * rings are idle. Only with %QUIRK_RESET_DMA_ON_REUSE.
> * @domain_released: Completed when domain has been fully released
> * @host_reset: Host router was reset on driver load, or forced on system
> * shutdown/reboot. When set, tb_stop() asserts DPR on connected
> @@ -534,6 +549,11 @@ struct tb_nhi {
> struct work_struct interrupt_work;
> u32 hop_count;
> unsigned long quirks;
> + unsigned long *dma_hops_used;
> + bool resetting;
> + bool removing;
> + bool suspended;
> + struct delayed_work reset_work;
> struct completion domain_released;
> bool host_reset;
> };
next prev parent reply other threads:[~2026-10-05 19:38 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-05 16:47 [PATCH -next v2 0/3] thunderbolt: Reset affected AMD host interfaces before DMA HopID reuse Basavaraj Natikar
2026-10-05 16:47 ` [PATCH -next v2 1/3] thunderbolt: Allow tb_ring_start() to fail Basavaraj Natikar
2026-10-05 16:47 ` [PATCH -next v2 2/3] thunderbolt: Reset the host interface before reusing a DMA HopID Basavaraj Natikar
2026-10-05 19:38 ` Mario Limonciello [this message]
2026-10-05 16:47 ` [PATCH -next v2 3/3] thunderbolt: Add quirk to reset host interface for AMD USB4 routers Basavaraj Natikar
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=396730a0-5a45-4509-b0fb-9b16229cf567@amd.com \
--to=mario.limonciello@amd.com \
--cc=Basavaraj.Natikar@amd.com \
--cc=Sanath.S@amd.com \
--cc=YehezkelShB@gmail.com \
--cc=andreas.noever@gmail.com \
--cc=andrew+netdev@lunn.ch \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=mika.westerberg@linux.intel.com \
--cc=pabeni@redhat.com \
--cc=westeri@kernel.org \
/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