* [PATCH net v6 0/2] net: fix a stale phylink PHY pointer and a lost PHY interrupt @ 2026-09-08 15:50 Aleksei Sviridkin 2026-09-08 15:50 ` [PATCH net v6 1/2] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin 2026-09-08 15:50 ` [PATCH net v6 2/2] net: phy: restore the interrupt the bus gave a PHY Aleksei Sviridkin 0 siblings, 2 replies; 4+ messages in thread From: Aleksei Sviridkin @ 2026-09-08 15:50 UTC (permalink / raw) To: andrew, hkallweit1, linux Cc: olteanv, davem, edumazet, kuba, pabeni, horms, netdev, linux-kernel, Aleksei Sviridkin Two fixes on the same path: a PHY whose driver is a module on a rootfs that is not mounted when the MAC probes. Patch 1 clears a stale pl->phydev when bringup fails after recording it, so a later disconnect cannot detach the same PHY twice. Unchanged since v3, where it got a Reviewed-by. Patch 2 gives back the interrupt phy_probe() replaced with PHY_POLL. v5 kept the number in a new struct phy_device field. Andrew asked for mdiobus->irq[] to be the source of truth instead, so v6 reads it from there and the field is gone; the patch is now four lines of logic and no new state. That leaves buses whose driver writes only phydev->irq and never the table. On those the table holds PHY_POLL and there is nothing to give back, so the fix does not reach them. Three are in that position: lan78xx and smsc95xx, which Andrew named, and sxgbe_mdio, which writes its own irqlist and the phydev but not the bus. Converting them is their maintainers' call and I have left them alone here rather than send a series across four subsystems; say the word and they follow. Measured on an MT7981B board, an MT7531 switch port with an Airoha EN8811H whose driver is a module. The generic driver binds first, and the number is read out either side of the detach that releases it: with patch 2: bound: irq -1 after detach: irq 15 without patch 2: bound: irq -1 after detach: irq -1 -1 is PHY_POLL, 15 is what the device tree gives that PHY. The cycle repeats once a second and every pass on both builds reads the same. Reaching that state needs a kernel that lets the generic driver bind where this board would normally refuse it, so both numbers come from a modified poller; the patch under test is the only difference between the two builds. Previous posting: https://lore.kernel.org/netdev/20260906174643.4107607-1-f@lex.la/ Aleksei Sviridkin (2): net: phylink: unwind the PHY binding when bringup fails late net: phy: restore the interrupt the bus gave a PHY drivers/net/phy/phy_device.c | 18 ++++++++++++++++++ drivers/net/phy/phylink.c | 29 ++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 9 deletions(-) base-commit: 38b6be101006d3e7af972999f45d4f1e8250587a -- 2.53.0 ^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH net v6 1/2] net: phylink: unwind the PHY binding when bringup fails late 2026-09-08 15:50 [PATCH net v6 0/2] net: fix a stale phylink PHY pointer and a lost PHY interrupt Aleksei Sviridkin @ 2026-09-08 15:50 ` Aleksei Sviridkin 2026-09-08 15:50 ` [PATCH net v6 2/2] net: phy: restore the interrupt the bus gave a PHY Aleksei Sviridkin 1 sibling, 0 replies; 4+ messages in thread From: Aleksei Sviridkin @ 2026-09-08 15:50 UTC (permalink / raw) To: andrew, hkallweit1, linux Cc: olteanv, davem, edumazet, kuba, pabeni, horms, netdev, linux-kernel, Aleksei Sviridkin phylink_bringup_phy() records the PHY in pl->phydev before its last fallible step: on a MAC whose phylink ops implement LPI, phy_eee_rx_clock_stop() can fail with a real MDIO error. The callers unwind with phy_detach(), which knows nothing about pl->phydev, so a pointer to a PHY that is no longer attached outlives the failed connect. What that costs depends on how the caller got here. phylink_connect_phy() and the SFP path go through phylink_attach_phy(), which refuses to attach while pl->phydev is set and turns a transient MDIO error into a permanent -EBUSY. phylink_fwnode_phy_connect() has no such check, so a later connect overwrites the stale pointer and hides the problem. A disconnect does not: phylink_disconnect_phy() hands that pointer to phy_disconnect(), and the second phy_detach() on the same PHY drops references the first one already released. Clear the binding on the failure path. This is the same operation phylink_disconnect_phy() performs, so both now share a helper. The PHY-side fields are left to phy_detach(), which every caller already runs on this path. Fixes: 03abf2a7c654 ("net: phylink: add EEE management") Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> Reviewed-by: Andrew Lunn <andrew@lunn.ch> --- drivers/net/phy/phylink.c | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c index 3ec3bb439109..6a92fac58f25 100644 --- a/drivers/net/phy/phylink.c +++ b/drivers/net/phy/phylink.c @@ -2083,6 +2083,18 @@ static int phylink_validate_phy(struct phylink *pl, struct phy_device *phy, return phylink_validate(pl, supported, state); } +/* Disassociate @phy from @pl. Caller must hold pl->phydev_mutex. */ +static void phylink_clear_phydev(struct phylink *pl, struct phy_device *phy) +{ + mutex_lock(&phy->lock); + mutex_lock(&pl->state_mutex); + pl->phydev = NULL; + pl->phy_enable_tx_lpi = false; + pl->mac_tx_clk_stop = false; + mutex_unlock(&pl->state_mutex); + mutex_unlock(&phy->lock); +} + static int phylink_bringup_phy(struct phylink *pl, struct phy_device *phy, phy_interface_t interface) { @@ -2197,6 +2209,12 @@ static int phylink_bringup_phy(struct phylink *pl, struct phy_device *phy, if (ret == 0 && phy_interrupt_is_valid(phy)) phy_request_interrupt(phy); + if (ret) { + mutex_lock(&pl->phydev_mutex); + phylink_clear_phydev(pl, phy); + mutex_unlock(&pl->phydev_mutex); + } + return ret; } @@ -2347,15 +2365,8 @@ void phylink_disconnect_phy(struct phylink *pl) mutex_lock(&pl->phydev_mutex); phy = pl->phydev; - if (phy) { - mutex_lock(&phy->lock); - mutex_lock(&pl->state_mutex); - pl->phydev = NULL; - pl->phy_enable_tx_lpi = false; - pl->mac_tx_clk_stop = false; - mutex_unlock(&pl->state_mutex); - mutex_unlock(&phy->lock); - } + if (phy) + phylink_clear_phydev(pl, phy); mutex_unlock(&pl->phydev_mutex); if (phy) { -- 2.53.0 ^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH net v6 2/2] net: phy: restore the interrupt the bus gave a PHY 2026-09-08 15:50 [PATCH net v6 0/2] net: fix a stale phylink PHY pointer and a lost PHY interrupt Aleksei Sviridkin 2026-09-08 15:50 ` [PATCH net v6 1/2] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin @ 2026-09-08 15:50 ` Aleksei Sviridkin 2026-09-11 3:51 ` netdev-bot+sashiko 1 sibling, 1 reply; 4+ messages in thread From: Aleksei Sviridkin @ 2026-09-08 15:50 UTC (permalink / raw) To: andrew, hkallweit1, linux Cc: olteanv, davem, edumazet, kuba, pabeni, horms, netdev, linux-kernel, Aleksei Sviridkin phy_probe() replaces phydev->irq with PHY_POLL when the driver that is binding has no interrupt support, and nothing puts it back. A PHY whose own driver is a module on a filesystem that is not mounted yet gets the generic driver first, loses the number there, and polls for the rest of the uptime once the real driver takes over. Put it back from mdiobus->irq[], which is where the number came from: phy_device_create() seeds phydev->irq out of that table, so the bus that described the interrupt still holds it. Restore at the three points the bind cycle can end, phy_remove(), phy_probe()'s own error exit and the unwind in phy_attach_direct(), so a bind that is undone by any path leaves the PHY as it was found. Skip it while phy_link_change marks an attached consumer. That consumer called phy_request_interrupt() on the PHY_POLL it saw and would free an interrupt it never requested. A bus whose driver writes only phydev->irq and never the table is not covered, because the table then holds PHY_POLL and there is nothing to give back; lan78xx, smsc95xx and sxgbe are in that position today and registering the interrupt with the bus is theirs to do. This needs commit e0d1c55501d3 ("net: phy: fix phy_uses_state_machine()") to be of any use: without it the mark this skips on is never cleared once a consumer has attached, and the restore never runs. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la> --- drivers/net/phy/phy_device.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c index 94b2e85e00a3..b55402569300 100644 --- a/drivers/net/phy/phy_device.c +++ b/drivers/net/phy/phy_device.c @@ -1734,6 +1734,19 @@ static bool phy_drv_supports_irq(const struct phy_driver *phydrv) return phydrv->config_intr && phydrv->handle_interrupt; } +/* Give back what phy_probe() took, from the bus that owns the number, but + * not while phy_link_change marks a consumer: it skipped + * phy_request_interrupt() on the value it saw, so phy_disconnect() would + * free an interrupt nobody requested. + */ +static void phy_restore_probe_irq(struct phy_device *phydev) +{ + if (phydev->phy_link_change || phydev->irq != PHY_POLL) + return; + + phydev->irq = phydev->mdio.bus->irq[phydev->mdio.addr]; +} + /** * phy_attach_direct - attach a network device to a given PHY device pointer * @dev: network device to attach @@ -1896,6 +1909,7 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev, error_module_put: module_put(d->driver->owner); + phy_restore_probe_irq(phydev); phydev->is_genphy_driven = 0; d->driver = NULL; error_put_device: @@ -3820,6 +3834,8 @@ static int phy_probe(struct device *dev) if (!phydev->is_on_sfp_module) phy_led_triggers_unregister(phydev); + phy_restore_probe_irq(phydev); + /* Re-assert the reset signal on error */ phy_device_reset(phydev, 1); @@ -3848,6 +3864,8 @@ static int phy_remove(struct device *dev) if (phydev->drv && phydev->drv->remove) phydev->drv->remove(phydev); + phy_restore_probe_irq(phydev); + /* Assert the reset signal */ phy_device_reset(phydev, 1); -- 2.53.0 ^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net v6 2/2] net: phy: restore the interrupt the bus gave a PHY 2026-09-08 15:50 ` [PATCH net v6 2/2] net: phy: restore the interrupt the bus gave a PHY Aleksei Sviridkin @ 2026-09-11 3:51 ` netdev-bot+sashiko 0 siblings, 0 replies; 4+ messages in thread From: netdev-bot+sashiko @ 2026-09-11 3:51 UTC (permalink / raw) To: f Cc: andrew, hkallweit1, linux, olteanv, davem, edumazet, kuba, pabeni, horms, netdev, linux-kernel Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: Critical: 0 · High: 0 · Medium: 2 · Low: 1 - [Medium] phy_restore_probe_irq() bails out whenever phydev->phy_link_change is non-NULL and records no pending… - [Medium] phy_restore_probe_irq() performs an unsynchronized check-then- act on state owned by the rtnl-side attach/detach paths. - [Low] No Fixes: tag (and no target-tree indication) on a patch whose changelog describes a concrete, lasting user-visible defect. -- net: phy: restore the interrupt the bus gave a PHY phy_probe() demotes phydev->irq to PHY_POLL when the binding driver has no interrupt support, and nothing restores it afterwards. This patch adds a helper that copies the number back out of mdiobus->irq[], and calls it from phy_remove(), phy_probe()'s error exit and the phy_attach_direct() unwind. The helper skips the restore while phydev->phy_link_change marks an attached consumer. > A bus whose driver writes only phydev->irq and never the table is not > covered, because the table then holds PHY_POLL and there is nothing to > give back; lan78xx, smsc95xx and sxgbe are in that position today and > registering the interrupt with the bus is theirs to do. > > This needs commit e0d1c55501d3 ("net: phy: fix > phy_uses_state_machine()") to be of any use: without it the mark this > skips on is never cleared once a consumer has attached, and the restore > never runs. > > Assisted-by: LLM > Signed-off-by: Aleksei Sviridkin <f@lex.la> [Severity: Low] The changelog describes a lasting user-visible defect ("polls for the rest of the uptime once the real driver takes over"), but the tag block carries no Fixes: line and the subject has no net/net-next scoping. The assignment being undone, in phy_probe(): if (!phy_drv_supports_irq(phydrv) && phy_interrupt_is_valid(phydev)) phydev->irq = PHY_POLL; is the natural blame target. Could a Fixes: tag pointing at it (or an explicit statement that this targets net-next as a behavioural change) be added? This matters more than usual given the stated dependency on e0d1c55501d3, which is what clears phydev->phy_link_change in phy_detach(). On a tree without that commit the new restore in phy_remove() is a silent no-op, so a backport applies cleanly and changes nothing. Should that boundary be spelled out for stable? > diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c > index 94b2e85e00a3..b55402569300 100644 > --- a/drivers/net/phy/phy_device.c > +++ b/drivers/net/phy/phy_device.c > @@ -1734,6 +1734,19 @@ static bool phy_drv_supports_irq(const struct phy_driver *phydrv) > return phydrv->config_intr && phydrv->handle_interrupt; > } > > +/* Give back what phy_probe() took, from the bus that owns the number, but > + * not while phy_link_change marks a consumer: it skipped > + * phy_request_interrupt() on the value it saw, so phy_disconnect() would > + * free an interrupt nobody requested. > + */ > +static void phy_restore_probe_irq(struct phy_device *phydev) > +{ > + if (phydev->phy_link_change || phydev->irq != PHY_POLL) > + return; > + > + phydev->irq = phydev->mdio.bus->irq[phydev->mdio.addr]; > +} [Severity: Medium] Can this check-then-act run concurrently with the attach/detach paths that own phydev->phy_link_change and phydev->irq? The helper reads phy_link_change and irq, then writes irq, running from phy_probe()/phy_remove() under the mdio device lock only. The writers of that same state take neither the mdio device lock nor phydev->lock: phy_attach_direct() phydev->phy_link_change = phy_link_change; ... if (phydev->dev_flags & PHY_F_NO_IRQ) phydev->irq = PHY_POLL; if (!phy_drv_supports_irq(phydev->drv) && phy_interrupt_is_valid(phydev)) phydev->irq = PHY_POLL; phy_detach() phydev->phy_link_change = NULL; phy_driver_register() leaves suppress_bind_attrs unset, so a root sysfs unbind can enter phy_remove() at any time, and __device_release_driver() clears dev->driver only after ->remove() returns, so a concurrent phy_attach_direct() still sees d->driver != NULL and proceeds. One interleaving: CPU0 phy_remove() -> phy_restore_probe_irq() reads phy_link_change == NULL, irq == PHY_POLL (preempted before the store) CPU1 phy_attach_direct() phydev->phy_link_change = phy_link_change; phydev->irq = PHY_POLL; /* no phy_request_interrupt() */ CPU0 resumes phydev->irq = phydev->mdio.bus->irq[phydev->mdio.addr]; Does that leave a valid IRQ published for a consumer that requested nothing, so that teardown hits: phy_disconnect() if (phy_interrupt_is_valid(phydev)) phy_free_interrupt(phydev); and free_irq(phydev->irq, phydev) warns about freeing an already-free IRQ? phy_polling_mode() is also false at that point, so link changes would be neither polled nor interrupt driven, which is the situation the new comment says it avoids. The reverse order gives the opposite outcome: phy_remove() observes a still non-NULL phy_link_change just before phy_detach() clears it, and the PHY is left unbound with irq == PHY_POLL. Would making the individual accesses atomic help here, or does the decision plus the store need to be serialized against the whole attach/detach and interrupt request/free sequence, for example by driving the restore from phy_detach() under rtnl? > @@ -3848,6 +3864,8 @@ static int phy_remove(struct device *dev) > if (phydev->drv && phydev->drv->remove) > phydev->drv->remove(phydev); > > + phy_restore_probe_irq(phydev); > + [Severity: Medium] When phy_restore_probe_irq() takes the phy_link_change early return here, is the interrupt number ever restored later? The changelog says restoring at the three bind-cycle exits means "a bind that is undone by any path leaves the PHY as it was found", but nothing records a pending restore for the skipped case. A driver unbind while a consumer is still attached: unbind_store() if (dev && dev->driver == drv) { device_driver_detach(dev); reaches phy_remove() with phy_link_change still set by phy_attach_direct(), so the helper returns early and phy_remove() then clears phydev->drv. When the consumer disconnects afterwards, phy_detach() clears the marker but its release is gated: phydev->phy_link_change = NULL; ... if (phydev->is_genphy_driven) { device_release_driver(&phydev->mdio.dev); phydev->is_genphy_driven = 0; } and for an already-unbound device device_release_driver() is a no-op, so phy_remove() does not run again and no restore happens. A later interrupt-capable driver then sees phydev->irq == PHY_POLL in phy_probe(), phy_interrupt_is_valid() is false, and polling continues. The early return itself looks necessary, since restoring under an attached consumer is what would make phy_disconnect() free an unrequested IRQ. Could the skipped restore instead be completed in phy_detach() right after phy_link_change is cleared? -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260908155025.4155289-1-f%40lex.la ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-11 3:51 UTC | newest] Thread overview: 4+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-09-08 15:50 [PATCH net v6 0/2] net: fix a stale phylink PHY pointer and a lost PHY interrupt Aleksei Sviridkin 2026-09-08 15:50 ` [PATCH net v6 1/2] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin 2026-09-08 15:50 ` [PATCH net v6 2/2] net: phy: restore the interrupt the bus gave a PHY Aleksei Sviridkin 2026-09-11 3:51 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox