[PATCH net v3 2/2] net: phy: restore the interrupt after a generic-driver bind cycle

From: Aleksei Sviridkin

Date: Thu Aug 27 2026 - 17:23:14 EST


A PHY with no specific driver available at attach time gets the generic
one, and phy_probe() sets phydev->irq to PHY_POLL because that driver
has no interrupt callbacks. Neither end of that bind cycle puts the
value back: phy_detach() releases the generic driver so a real one can
bind later, and a generic probe that fails never reaches phy_detach()
at all.

The specific driver that binds afterwards therefore starts with
irq == PHY_POLL, and the PHY is polled for the rest of the uptime with
no warning on that path. A DSA switch that connects its user ports
before the rootfs holding the PHY driver module is mounted hits this on
every boot.

mdiobus_alloc() fills bus->irq[] with PHY_POLL for every address, and
the bind cycle never writes to that table, so the entry still holds
whatever the bus registered there. Restore phydev->irq from it on both
exits, and only where the cycle left PHY_POLL. That guard preserves an
interrupt mode a MAC installed on the attached PHY after connect, and
it keeps a restored interrupt number out of the
phy_connect_direct()/phy_disconnect() asymmetry, where such a MAC would
have the interrupt requested and never freed.

Fixes: 00db8189d984 ("This patch adds a PHY Abstraction Layer to the Linux Kernel, enabling ethernet drivers to remain as ignorant as is reasonable of the connected PHY's design and operation details.")
Signed-off-by: Aleksei Sviridkin <f@xxxxxx>
---
Both exits matter: phy_detach() for a generic driver that bound and is
being released, and phy_attach_direct()'s error_module_put label for a
generic probe that failed, which never calls phy_detach().

Which buses and MAC drivers the bus interrupt table covers, which other
paths to PHY_POLL the guard also restores and why none of them is
harmed, and the sysfs unbind case this does not cover, are worked
through under v2:
https://lore.kernel.org/netdev/20260824024029.41310-3-f@xxxxxx/
drivers/net/phy/phy_device.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)

diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
index 94b2e85e00a3..be4c35db8de9 100644
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -1734,6 +1734,18 @@ static bool phy_drv_supports_irq(const struct phy_driver *phydrv)
return phydrv->config_intr && phydrv->handle_interrupt;
}

+/* Give back the interrupt phy_probe() parked when a driver with no interrupt
+ * callbacks bound. mdiobus_alloc() defaults bus->irq[] to PHY_POLL and the
+ * bind cycle does not touch the table, so whatever the bus recorded there
+ * still stands. Only the parking is undone: any other value the PHY carries
+ * was put there by someone else.
+ */
+static void phy_restore_genphy_irq(struct phy_device *phydev)
+{
+ if (phydev->irq == PHY_POLL)
+ 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 +1908,7 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,

error_module_put:
module_put(d->driver->owner);
+ phy_restore_genphy_irq(phydev);
phydev->is_genphy_driven = 0;
d->driver = NULL;
error_put_device:
@@ -1965,6 +1978,7 @@ void phy_detach(struct phy_device *phydev)
* real driver could be loaded
*/
if (phydev->is_genphy_driven) {
+ phy_restore_genphy_irq(phydev);
device_release_driver(&phydev->mdio.dev);
phydev->is_genphy_driven = 0;
}
--
2.55.0