[PATCH net v3] net: hip04: fix tx coalesce timer and IRQ teardown races
From: Fan Wu <hidden>
Date: 2026-07-11 05:30:35
Also in:
lkml, stable
Subsystem:
hisilicon network subsystem driver, networking drivers, the rest · Maintainers:
Jian Shen, Andrew Lunn, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
The hip04 remove path frees the TX/RX rings before unregistering the
netdev. If the interface is still up, unregister_netdev() then runs
.ndo_stop, whose TX reclaim and NAPI poll touch the already-freed DMA
ring memory. The TX coalesce timer and the platform IRQ also outlive the
netdev private data they dereference.
Reorder hip04_remove() so the netdev is unregistered (which runs .ndo_stop
synchronously, stopping NAPI and the TX queue) before the rings are freed.
Free the devm-managed IRQ explicitly before free_netdev(), so
hip04_mac_interrupt() (whose dev_id is the netdev) cannot fire against
freed memory: devm would otherwise release it only after .remove returns.
hip04_mac_stop() must quiesce both arming sites of the coalesce timer.
The NAPI poll arms it, and napi_disable() returns once the poll calls
napi_complete_done(), not when the poll function returns, so move that
arm before napi_complete_done(). The early-exit paths in hip04_rx_poll()
do not call napi_complete_done(); NAPI stays scheduled and will be polled
again, so there is no need to arm the coalesce timer on that pass. The
Tx path also arms it, and mac_stop() is reached directly from
hip04_tx_timeout_task() as well as via .ndo_stop, so use netif_tx_disable()
rather than netif_stop_queue() to wait for an in-flight
hip04_mac_start_xmit() to finish. The timer is then drained with
hrtimer_cancel(). A "closing" flag, checked at the single arming site,
guards against a later arm.
hip04_tx_timeout_task() restarts the device with mac_stop() + mac_open().
Serialize that restart against .ndo_open/.ndo_stop with a driver-private
state_lock instead of rtnl_lock(): mac_open()/mac_stop() are split into
internal __hip04_mac_open()/__hip04_mac_stop() helpers, and the
.ndo_open/.ndo_stop wrappers as well as the timeout worker take the lock.
This avoids extending RTNL usage; netdev_lock() is not sufficient here, as
hip04 does not opt into netdev-ops locking. If timeout recovery is in
progress when a close begins, .ndo_stop waits on the lock and performs the
final MAC stop; if .ndo_stop acquires the lock first, the worker sees the
device is no longer running and returns. Either way, recovery cannot
re-enable the MAC
during teardown.
This issue was found by an in-house static analysis tool.
Fixes: a41ea46a9a12 ("net: hisilicon: new hip04 ethernet driver")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5.5
Signed-off-by: Fan Wu <redacted>
---
v3: address review from Przemek Kitszel.
- Drop the rtnl_lock()/rtnl_unlock() and the #include <linux/rtnetlink.h>
added to hip04_tx_timeout_task() in v2. Serialize the tx-timeout restart
against .ndo_open/.ndo_stop with a driver-private state_lock instead:
mac_open()/mac_stop() are split into __hip04_mac_open()/__hip04_mac_stop()
helpers, and the .ndo_open/.ndo_stop wrappers and the timeout worker all
take the lock. This avoids new RTNL usage. netdev_lock() was considered
but does not help: hip04 does not opt into netdev-ops locking, so its
.ndo_open/.ndo_stop run under RTNL and the netdev instance lock does not
protect them.
- Rephrase the sentence about napi_complete_done() / early-exit paths that
was hard to read.
- s/Tx xmit/Tx/.
v2: Address review comments from Simon Horman. Use netif_tx_disable() rather
than netif_stop_queue() in hip04_mac_stop() so that an in-flight
hip04_mac_start_xmit() finishes before the TX ring is reclaimed;
mac_stop() is also called directly from the tx-timeout work, not only
via .ndo_stop. Arm the coalesce timer in hip04_rx_poll() before
napi_complete_done(), so that napi_disable() observes the final arm and
the subsequent cancel cannot miss it. Free the devm-managed IRQ
explicitly in hip04_remove() before free_netdev() so that
hip04_mac_interrupt() cannot run against freed memory, placing it after
unregister_netdev() so that the device is stopped first. Reorder
hip04_remove() so that the netdev is unregistered (running .ndo_stop)
before the PHY is disconnected and the TX/RX rings are freed. Check the
return value of hip04_mac_open() in the tx-timeout restart path.
v2: https://lore.kernel.org/netdev/20260710015730.630775-1-fanwu01@zju.edu.cn/ (local)
v1: https://lore.kernel.org/netdev/20260703050133.2445155-1-fanwu01@zju.edu.cn/ (local)
drivers/net/ethernet/hisilicon/hip04_eth.c | 107 ++++++++++++++++++++++++++----
1 file changed, 96 insertions(+), 11 deletions(-)
diff --git a/drivers/net/ethernet/hisilicon/hip04_eth.c b/drivers/net/ethernet/hisilicon/hip04_eth.c
index 18376bcc7..4bd192094 100644
--- a/drivers/net/ethernet/hisilicon/hip04_eth.c
+++ b/drivers/net/ethernet/hisilicon/hip04_eth.c@@ -15,6 +15,7 @@ #include <linux/of_net.h> #include <linux/mfd/syscon.h> #include <linux/regmap.h> +#include <linux/mutex.h> #define SC_PPE_RESET_DREQ 0x026C
@@ -232,6 +233,8 @@ struct hip04_priv { int tx_coalesce_frames; int tx_coalesce_usecs; struct hrtimer tx_coalesce_timer; + bool closing; + struct mutex state_lock; /* Serializes MAC open/stop and timeout recovery. */ unsigned char *rx_buf[RX_DESC_NUM]; dma_addr_t rx_phys[RX_DESC_NUM];
@@ -497,6 +500,12 @@ static void hip04_start_tx_timer(struct hip04_priv *priv) { unsigned long ns = priv->tx_coalesce_usecs * NSEC_PER_USEC / 2; + /* Do not (re-)arm the Tx coalesce timer once teardown has begun. + * Both arming sites (Tx and NAPI Rx poll) go through here. + */ + if (smp_load_acquire(&priv->closing)) + return; + /* allow timer to fire after half the time at the earliest */ hrtimer_start_range_ns(&priv->tx_coalesce_timer, ns_to_ktime(ns), ns, HRTIMER_MODE_REL);
@@ -649,12 +658,15 @@ static int hip04_rx_poll(struct napi_struct *napi, int budget) priv->reg_inten |= RCV_INT; writel_relaxed(priv->reg_inten, priv->base + PPE_INTEN); } + /* Arm the coalesce timer BEFORE napi_complete_done(): napi_disable() + * in hip04_mac_stop() returns once SCHED is cleared here, not when + * the poll function returns, so arming afterwards can slip past the + * stop path's hrtimer_cancel(). + */ + if (tx_remaining) + hip04_start_tx_timer(priv); napi_complete_done(napi, rx); done: - /* start a new timer if necessary */ - if (rx < budget && tx_remaining) - hip04_start_tx_timer(priv); - return rx; }
@@ -720,7 +732,7 @@ static void hip04_adjust_link(struct net_device *ndev) } } -static int hip04_mac_open(struct net_device *ndev) +static int __hip04_mac_open(struct net_device *ndev) { struct hip04_priv *priv = netdev_priv(ndev); int i;
@@ -743,6 +755,13 @@ static int hip04_mac_open(struct net_device *ndev) hip04_set_recv_desc(priv, phys); } + /* RX mappings are established; clear the closing flag before + * re-enabling traffic. The store-release pairs with the load-acquire + * in hip04_start_tx_timer(); state_lock serializes this against + * mac_stop()'s store-release. + */ + smp_store_release(&priv->closing, false); + if (priv->phy) phy_start(priv->phy);
@@ -754,13 +773,42 @@ static int hip04_mac_open(struct net_device *ndev) return 0; } -static int hip04_mac_stop(struct net_device *ndev) +static int hip04_mac_open(struct net_device *ndev) +{ + struct hip04_priv *priv = netdev_priv(ndev); + int ret; + + mutex_lock(&priv->state_lock); + ret = __hip04_mac_open(ndev); + mutex_unlock(&priv->state_lock); + return ret; +} + +static int __hip04_mac_stop(struct net_device *ndev) { struct hip04_priv *priv = netdev_priv(ndev); int i; + /* Stop new timer arms before draining: set the closing flag (checked + * at the single arming site), wait for the NAPI poll and any in-flight + * TX to finish, then cancel the timer. + * + * netif_tx_disable() (not netif_stop_queue()) is required because this + * function is also called directly from hip04_tx_timeout_task(), not + * only via .ndo_stop where the core has already deactivated TX; + * netif_tx_disable() waits for an in-flight hip04_mac_start_xmit(), + * which arms the timer, to finish. + * + * Because hip04_rx_poll() arms the timer before napi_complete_done(), + * napi_disable() returning means that arm has happened, so the + * hrtimer_cancel() below cannot miss it. The store-release pairs + * with the load in hip04_start_tx_timer(). + */ + smp_store_release(&priv->closing, true); + napi_disable(&priv->napi); - netif_stop_queue(ndev); + netif_tx_disable(ndev); + hrtimer_cancel(&priv->tx_coalesce_timer); hip04_mac_disable(ndev); hip04_tx_reclaim(ndev, true); hip04_reset_ppe(priv);
@@ -779,6 +827,16 @@ static int hip04_mac_stop(struct net_device *ndev) return 0; } +static int hip04_mac_stop(struct net_device *ndev) +{ + struct hip04_priv *priv = netdev_priv(ndev); + + mutex_lock(&priv->state_lock); + __hip04_mac_stop(ndev); + mutex_unlock(&priv->state_lock); + return 0; +} + static void hip04_timeout(struct net_device *ndev, unsigned int txqueue) { struct hip04_priv *priv = netdev_priv(ndev);
@@ -791,8 +849,23 @@ static void hip04_tx_timeout_task(struct work_struct *work) struct hip04_priv *priv; priv = container_of(work, struct hip04_priv, tx_timeout_task); - hip04_mac_stop(priv->ndev); - hip04_mac_open(priv->ndev); + + /* Serialize the restart with .ndo_open/.ndo_stop, which take the + * same lock. If a close has completed or is in progress the + * netif_running() check bails; if that check races the core's + * running-state clear, .ndo_stop still runs under state_lock + * afterwards and stops the device, so this worker cannot leave the + * MAC re-enabled against a teardown. + */ + mutex_lock(&priv->state_lock); + if (!netif_running(priv->ndev)) + goto out; + + __hip04_mac_stop(priv->ndev); + if (__hip04_mac_open(priv->ndev)) + netdev_err(priv->ndev, "restart after tx timeout failed\n"); +out: + mutex_unlock(&priv->state_lock); } static int hip04_get_coalesce(struct net_device *netdev,
@@ -983,6 +1056,7 @@ static int hip04_mac_probe(struct platform_device *pdev) } INIT_WORK(&priv->tx_timeout_task, hip04_tx_timeout_task); + mutex_init(&priv->state_lock); ndev->netdev_ops = &hip04_netdev_ops; ndev->ethtool_ops = &hip04_ethtool_ops;
@@ -1026,13 +1100,24 @@ static void hip04_remove(struct platform_device *pdev) struct hip04_priv *priv = netdev_priv(ndev); struct device *d = &pdev->dev; + unregister_netdev(ndev); + + /* The IRQ is devm-managed and would otherwise be freed only after + * this function returns. Free it now, after unregister_netdev() has + * run .ndo_stop to stop the device and mask its interrupt source, but + * before the manual free_netdev() below, so that hip04_mac_interrupt() + * (dev_id == ndev) cannot fire against freed memory. free_irq() also + * drains any in-flight handler. + */ + devm_free_irq(d, ndev->irq, ndev); + cancel_work_sync(&priv->tx_timeout_task); + hrtimer_cancel(&priv->tx_coalesce_timer); + if (priv->phy) phy_disconnect(priv->phy); hip04_free_ring(ndev, d); - unregister_netdev(ndev); of_node_put(priv->phy_node); - cancel_work_sync(&priv->tx_timeout_task); free_netdev(ndev); }