[PATCH net v3] net: phylink: record the PHY only once bringup cannot fail

From: Aleksei Sviridkin

Date: Sun Sep 20 2026 - 18:21:30 EST


phylink_bringup_phy() stores 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() goes through phylink_attach_phy(), which refuses
to attach while pl->phydev is set, turning a transient MDIO error into
a permanent -EBUSY. The SFP path is worse than that: sfp_sm_probe_phy()
answers the failure with phy_device_remove() and phy_device_free(), and
it assigns sfp->mod_phy only past that error return, so nothing clears
pl->phydev and it is left pointing at a freed phy_device that
phylink_resolve() and the ethtool helpers go on reading.
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.

Found while making a DSA port survive a PHY whose driver arrives after
the switch probes: keeping the port across a failed connect and
retrying is what makes this window reachable.

Publish the pointer after the last call that can fail instead of
unwinding it afterwards. Nothing between the two points reads
pl->phydev, and the registration that follows cannot fail:
phy_request_interrupt() falls back to polling on its own. The PHY-side
state keeps the order it had, so no MDIO operation moves relative to
another.

Fixes: 03abf2a7c654 ("net: phylink: add EEE management")
Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@xxxxxx>
---
Changes since v2:
- Record the PHY after the last call that can fail, as Andrew suggested,
which removes the unwind and the helper it shared with
phylink_disconnect_phy(). The fallible call keeps its place so no MDIO
operation moves.
- Hardware evidence for the failure this prevents is in the reply to v2.
---
drivers/net/phy/phylink.c | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index a1458da8111b..1bbcf46c8356 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -2129,7 +2129,6 @@ static int phylink_bringup_phy(struct phylink *pl, struct phy_device *phy,
mutex_lock(&pl->phydev_mutex);
mutex_lock(&phy->lock);
mutex_lock(&pl->state_mutex);
- pl->phydev = phy;
pl->phy_state.interface = interface;
pl->phy_state.pause = MLO_PAUSE_NONE;
pl->phy_state.speed = SPEED_UNKNOWN;
@@ -2196,10 +2195,25 @@ static int phylink_bringup_phy(struct phylink *pl, struct phy_device *phy,
ret = 0;
}

- if (ret == 0 && phy_interrupt_is_valid(phy))
+ if (ret)
+ return ret;
+
+ /* Nothing below can fail, so the PHY can be recorded now. Doing it
+ * here rather than above keeps a failed bringup from leaving
+ * pl->phydev pointing at a PHY the caller is about to detach.
+ */
+ mutex_lock(&pl->phydev_mutex);
+ mutex_lock(&phy->lock);
+ mutex_lock(&pl->state_mutex);
+ pl->phydev = phy;
+ mutex_unlock(&pl->state_mutex);
+ mutex_unlock(&phy->lock);
+ mutex_unlock(&pl->phydev_mutex);
+
+ if (phy_interrupt_is_valid(phy))
phy_request_interrupt(phy);

- return ret;
+ return 0;
}

static int phylink_attach_phy(struct phylink *pl, struct phy_device *phy,
--
2.53.0