mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes
@ 2026-08-22 15:52 Aleksei Sviridkin
  2026-08-22 15:52 ` [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin
                   ` (3 more replies)
  0 siblings, 4 replies; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-22 15:52 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, Heiner Kallweit, Russell King
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, netdev, linux-kernel, Aleksei Sviridkin

A PHY that loads firmware at probe keeps its driver in a module on the
rootfs: built in, request_firmware_direct() fails against a rootfs that
is not mounted yet and the error comes straight out of probe. A DSA
switch probes long before that module can load, and three things go
wrong, one per layer.

phylink can fail its PHY bringup after it has already recorded the PHY
in pl->phydev, leaving a pointer to a PHY that phy_detach() has since
released. Depending on the caller that is either a permanent -EBUSY or
a stale pointer handed to phy_disconnect() later, which detaches the
same PHY twice (patch 1). Keeping a port across a failed connect makes
that window reachable, so it comes first.

phylib binds the generic driver during the attach, and once the failed
connect unwinds, the interrupt the firmware node declared is gone:
phy_probe() parked the PHY in polling mode and nothing after MDIO bus
registration ever brings the irq back (patch 2).

DSA drops the user port when the connect at setup fails, so the port
never exists, no matter that the driver shows up seconds later
(patch 3).

With the series applied the port survives setup and connects its PHY on
the first ifup after the module loads, with the interrupt the device
tree declares.

Tested on an MT7981B board (mt7530 switch, Airoha EN8811H with its
INT_B line in the device tree) running a 6.18 backport of everything
here except the retry in patch 3: the attach line reports a real
interrupt instead of irq=POLL, the EINT is claimed and its counter
advances on link changes forced from the link partner, and the port
passes traffic. On net-next all three files are compile-tested; the
board runs an OpenWrt 6.18 kernel.

Aleksei Sviridkin (3):
  net: phylink: unwind the PHY binding when bringup fails late
  net: phy: restore the interrupt after a generic-driver bind cycle
  net: dsa: connect a late-arriving PHY at ifup

 drivers/net/phy/phy_device.c | 13 +++++
 drivers/net/phy/phylink.c    | 12 +++++
 include/linux/phy.h          |  6 +++
 net/dsa/user.c               | 98 ++++++++++++++++++++++++++++++++++++
 4 files changed, 129 insertions(+)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late
  2026-08-22 15:52 [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Aleksei Sviridkin
@ 2026-08-22 15:52 ` Aleksei Sviridkin
  2026-08-22 17:30   ` Andrew Lunn
  2026-08-22 15:52 ` [PATCH net-next 2/3] net: phy: restore the interrupt after a generic-driver bind cycle Aleksei Sviridkin
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-22 15:52 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, Heiner Kallweit, Russell King
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, 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 a device reference
and two module references that were only ever taken once.

Clear the binding on the failure path, the same three fields
phylink_disconnect_phy() clears, under the same locks. The PHY-side
fields are left to phy_detach(), which every caller already runs on
this path.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
Reachability

The failing step needs pl->mac_supports_eee_ops, i.e. a MAC whose
phylink ops implement the LPI callbacks; mt7530 is one, and on the
board I tested ethtool --show-eee returns -EOPNOTSUPP, which is what
phylink reports when mac_supports_eee_ops is set and mac_supports_eee
is not, so that tail runs on every bringup there. The error itself is
an MDIO transaction failure inside phy_eee_rx_clock_stop(), which
cannot be produced deliberately, so this patch is compile-tested and
the series it belongs to ran on hardware with it in place.

The double-detach path needs a port that outlives a failed connect,
which is what patch 3 introduces; before that, DSA destroyed the port
immediately and the stale pointer went with it.
 drivers/net/phy/phylink.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/drivers/net/phy/phylink.c b/drivers/net/phy/phylink.c
index 5b8e95690..9d403ff1b 100644
--- a/drivers/net/phy/phylink.c
+++ b/drivers/net/phy/phylink.c
@@ -2197,6 +2197,18 @@ 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);
+		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);
+		mutex_unlock(&pl->phydev_mutex);
+	}
+
 	return ret;
 }
 
-- 
2.43.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH net-next 2/3] net: phy: restore the interrupt after a generic-driver bind cycle
  2026-08-22 15:52 [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Aleksei Sviridkin
  2026-08-22 15:52 ` [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin
@ 2026-08-22 15:52 ` Aleksei Sviridkin
  2026-08-22 19:28   ` Andrew Lunn
  2026-08-22 15:52 ` [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup Aleksei Sviridkin
  2026-08-24 16:25 ` [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Andrew Lunn
  3 siblings, 1 reply; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-22 15:52 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, Heiner Kallweit, Russell King
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, netdev, linux-kernel, Aleksei Sviridkin

fwnode_mdiobus_phy_device_register() resolves the interrupt declared
for a PHY once, at MDIO bus registration. If no specific driver is
available when the PHY is attached, the generic driver binds and
phy_probe() parks the device in polling mode, since the generic driver
has no interrupt callbacks. phy_detach() releases the generic driver
so a specific driver can bind later, but nothing brings the interrupt
back: the firmware node is never re-read after bus registration, so
the specific driver attaches with irq == PHY_POLL,
phy_request_interrupt() is never reached, and the PHY is polled for
the rest of the uptime with nothing in the logs but the "irq=POLL"
attach line.

A DSA switch probing before the rootfs is mounted produces exactly
that cycle for a PHY whose driver is a module: the generic driver
binds and fails validation during switch setup, and the real driver
binds at ifup. Observed on an MT7981B board with an Airoha EN8811H on
an MT7531 port: the device tree declares the INT_B line, yet the
attach says irq=POLL and the interrupt is never claimed.

Save the interrupt when the generic driver binds and give it back when
that driver is released. The restore runs before the device becomes
bindable again, so a concurrently arriving specific driver cannot
observe or overwrite the intermediate state. Only the value the
generic-driver cycle took is restored. A PHY already parked in polling
mode before that cycle, by a failed phy_request_interrupt() or by a
driver that chose PHY_POLL in its own probe, had PHY_POLL saved, so the
restore is skipped. The PHY_F_NO_IRQ and no-interrupt-support checks in
phy_attach_direct() still apply to whichever driver binds next.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
Testing

MT7981B board, mt7530 switch, Airoha EN8811H whose INT_B line is in
the device tree, driver in a module on the rootfs, together with the
next patch: the attach line reports irq=15 instead of irq=POLL, the
EINT is claimed, its counter advances on link changes forced from the
link partner, and there is no interrupt storm. The SoC's internal PHY
on the same board, which has no interrupt in its bus table, keeps
irq=POLL through the same boot, so the save-restore pair does not
resurrect an interrupt the device never had. Consistent across
reboots. Hardware testing was done on 6.18 with this exact
shape of the change; on net-next the files are compile-tested.

The saved value uses zero as "nothing saved"; no registration path
produces a valid interrupt number of zero, and non-positive values are
never restored.
 drivers/net/phy/phy_device.c | 13 +++++++++++++
 include/linux/phy.h          |  6 ++++++
 2 files changed, 19 insertions(+)

diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
index 94b2e85e0..6047dce61 100644
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -1780,6 +1780,7 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
 		else
 			d->driver = &genphy_driver.mdiodrv.driver;
 
+		phydev->genphy_saved_irq = phydev->irq;
 		phydev->is_genphy_driven = 1;
 	}
 
@@ -1897,6 +1898,9 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
 error_module_put:
 	module_put(d->driver->owner);
 	phydev->is_genphy_driven = 0;
+	if (phydev->genphy_saved_irq > 0 && phydev->irq == PHY_POLL)
+		phydev->irq = phydev->genphy_saved_irq;
+	phydev->genphy_saved_irq = 0;
 	d->driver = NULL;
 error_put_device:
 	put_device(d);
@@ -1965,6 +1969,15 @@ void phy_detach(struct phy_device *phydev)
 	 * real driver could be loaded
 	 */
 	if (phydev->is_genphy_driven) {
+		/* Give back the interrupt phy_probe() parked when the generic
+		 * driver bound, before the device becomes bindable again. A
+		 * PHY that was in polling mode for any other reason had
+		 * PHY_POLL saved, and the restore is skipped.
+		 */
+		if (phydev->genphy_saved_irq > 0 && phydev->irq == PHY_POLL)
+			phydev->irq = phydev->genphy_saved_irq;
+		phydev->genphy_saved_irq = 0;
+
 		device_release_driver(&phydev->mdio.dev);
 		phydev->is_genphy_driven = 0;
 	}
diff --git a/include/linux/phy.h b/include/linux/phy.h
index 5f8d65868..43e20b19e 100644
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -591,6 +591,10 @@ struct phy_oatc14_sqi_capability {
  *      - Bits [31:24] are reserved for defining generic
  *        PHY driver behavior.
  * @irq: IRQ number of the PHY's interrupt (-1 if none)
+ * @genphy_saved_irq: value of @irq before the generic driver bound, given
+ *                    back when that driver is released; zero outside a
+ *                    generic bind cycle, and non-positive values are
+ *                    never restored
  * @phylink: Pointer to phylink instance for this PHY
  * @sfp_bus_attached: Flag indicating whether the SFP bus has been attached
  * @sfp_bus: SFP bus attached to this PHY's fiber port
@@ -762,6 +766,8 @@ struct phy_device {
 	 */
 	int irq;
 
+	int genphy_saved_irq;
+
 	/* private data pointer */
 	/* For use by PHYs to maintain extra state */
 	void *priv;
-- 
2.43.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-22 15:52 [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Aleksei Sviridkin
  2026-08-22 15:52 ` [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin
  2026-08-22 15:52 ` [PATCH net-next 2/3] net: phy: restore the interrupt after a generic-driver bind cycle Aleksei Sviridkin
@ 2026-08-22 15:52 ` Aleksei Sviridkin
  2026-08-22 19:38   ` Andrew Lunn
  2026-08-24 16:25 ` [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Andrew Lunn
  3 siblings, 1 reply; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-22 15:52 UTC (permalink / raw)
  To: Andrew Lunn, Vladimir Oltean, Heiner Kallweit, Russell King
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, netdev, linux-kernel, Aleksei Sviridkin

A PHY whose driver loads firmware at probe has no driver bound while
that module still sits in an unmounted rootfs. phy_attach_direct()
falls back to the generic driver, whose feature set lacks the modes the
port is wired for, and the port is dropped for the rest of the uptime:

  mt7530-mdio mdio-bus:1f lan4: validation of 2500base-x [...] failed: -EINVAL
  mt7530-mdio mdio-bus:1f lan4: error -22 setting up PHY for tree 0, switch 0, port 5

The same PHY wired to a MAC on the same SoC comes up. The difference is
when the connect happens: the MAC driver connects from ndo_open, DSA
connects during setup, at 1.9 s, before any rootfs exists.

Keep the port when the connect fails on a PHY that has no driver of its
own, and connect it from the open path instead, ahead of enabling the
port, so a port in MLO_AN_PHY mode is never started without a PHY. A
PHY whose driver bound in the meantime connects normally.

Deferring the switch probe instead does not converge. Every retry
re-runs the port setup and flaps the other user ports. Measured on an
MT7981B board at two deadlines: the probe gave up at 22.2 s and the
module arrived at 27.6 s with a 20 s deadline, 47.3 s and 52.7 s with a
45 s one. Arrival tracks the deadline at a constant offset, so raising
the deadline moves the target with it. The module is loaded by
userspace, and userspace is what the retries keep from running. The
hotplug traffic each retry generates is the suspected mechanism; that
part was not measured.

Building the PHY driver into the kernel does not help either. It loads
firmware with request_firmware_direct(), which does not fall back to
the usermode helper, and passes the error straight out of probe rather
than -EPROBE_DEFER, so a built-in driver fails once against the
unmounted rootfs and is never retried. Baking the blobs in through
CONFIG_EXTRA_FIRMWARE puts 144 KiB into every kernel built from that
configuration. That call belongs to whoever configures the kernel.

The driverless check races the driver arrival itself: the module can
bind between the failed connect and the check reading the driver
pointer. That failed attempt ran with the generic driver bound and
says nothing about the driver present now, so the connect is retried
once, and only for a PHY that had no driver when the attempt started.
A port whose PHY was bound all along keeps its single attempt, so no
port pays a second attach, reset toggle and validation warning for a
window it can never be in.

The predicate covers every PHY named in the port's description that the
generic driver serves, including ones that are not waiting for a
module. A port whose PHY cannot satisfy the configured phy-mode is
registered now instead of being dropped at setup, and each ifup on it
fails with a warning. Each failed ifup binds the generic driver,
re-reads its abilities over MDIO and releases it again. That cycle used
to run once at setup; on such a port it now runs per attempt, and a PHY
that declares a reset line sees it toggled each time.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
---
Why not the existing phy_detach() path

phy_detach() already releases the generic driver so a real one can bind
later, and the obvious question is why DSA cannot use it. It is not
reachable here. dsa_port_setup_as_unused() never creates a netdevice
for the port, so there is no ndo_open to hook and no detach to trigger.
Turning that into create-then-detach is a larger and riskier change
than keeping the port and retrying the connect that already exists.

Why the check reads mdio.dev.driver and not phy_driver_is_genphy()

The flag behind that helper is set in phy_attach_direct() and cleared
in phy_detach(), which the failed connect has already run by the time
DSA looks, so it reads false on exactly the ports this patch is for.
Sampling before the connect does not work either: nothing is bound to
any PHY at that point, so the check would match every port. Reading
the bound driver from DSA is a reach into the device model. If that is
the wrong layer, a small phylib accessor fits here and I can add one.

Relation to earlier attempts

The 2021 RFC "Make the PHY library stop being so greedy when binding
the generic PHY driver" went at the same problem class from the phylib
side, adding device_pending_probe() so phy_attach_direct() could hold
off on the generic driver while a specific driver's probe was still
pending. It was turned down as belonging in the driver core rather
than in phylib, and nothing equivalent has landed since. This patch
stays out of that argument: it touches neither probe deferral nor
driver matching, and changes only what DSA does with a connect that
already failed.

The phy_port work does not cover this case. It represents port
topology and runs from phy_probe(), after a driver is bound; the
decision this patch depends on happens earlier, in the fallback inside
phy_attach_direct().

Blast radius

Ports whose PHY has a driver at setup time never take the new path.
Their connect succeeds and the branch is not reached.

A port kept across a failed connect is what makes patch 1 of this
series necessary: without it the failed bringup leaves a pointer to a
detached PHY behind, and the teardown of a port that was never opened
would detach that PHY a second time.

The retry doubles the connect attempt on ports that fail with a driver
already bound, including ports that end up dropped anyway. Each
attempt is a full attach/detach cycle, so the reset line is toggled
once more and phylink prints its validation warning twice.

phylink_bringup_phy() requests the PHY interrupt only after validation
succeeds. On a board whose interrupt description is wrong, that
description stays dormant as long as the connect always fails, and
goes live the moment this patch makes the connect work. The patch is
the trigger there, not the cause. Patch 2 of this series is what makes
a correct description survive to that point at all.

Testing

MT7981B board, mt7530 switch, Airoha EN8811H on port 5, driver in a
module on the rootfs. Without the patch the port is dropped at 1.9 s
and stays gone. With it the port survives setup, the PHY driver binds
at 6.3 s, and the port attaches it and joins the bridge on the first
ifup at 16.9 s. Boot time is unchanged, and the late connect leaves
the phylink instances of the other ports alone.

The retry taken when a driver binds during the failed connect closes
a window of microseconds; it cannot be exercised deliberately on
hardware and is compile-tested, as is this patch on net-next. The
hardware testing was done on a 6.18 backport carrying everything here
except that retry.
 net/dsa/user.c | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 98 insertions(+)

diff --git a/net/dsa/user.c b/net/dsa/user.c
index 041f9060c..0cdc1c9b1 100644
--- a/net/dsa/user.c
+++ b/net/dsa/user.c
@@ -397,6 +397,49 @@ void dsa_user_host_uc_uninstall(struct net_device *dev)
 		dsa_port_standalone_host_fdb_del(dp, dev->dev_addr, 0);
 }
 
+/* Returns the port's PHY node with a reference taken, or NULL if the port has
+ * no PHY node. Looks at the same properties as phylink_fwnode_phy_connect().
+ */
+static struct fwnode_handle *dsa_user_phy_fwnode(const struct dsa_port *dp)
+{
+	struct fwnode_handle *phy_fwnode;
+
+	phy_fwnode = fwnode_get_phy_node(of_fwnode_handle(dp->dn));
+
+	return IS_ERR(phy_fwnode) ? NULL : phy_fwnode;
+}
+
+/* Connect a PHY that dsa_user_phy_setup() left behind because it had no
+ * driver of its own back then. Runs before the port is enabled, so that a
+ * port in MLO_AN_PHY mode is not started without its PHY.
+ */
+static int dsa_user_late_phy_connect(struct net_device *dev)
+{
+	struct dsa_port *dp = dsa_user_to_port(dev);
+	struct fwnode_handle *phy_fwnode;
+	struct dsa_switch *ds = dp->ds;
+	u32 phy_flags = 0;
+	int err;
+
+	if (dev->phydev)
+		return 0;
+
+	phy_fwnode = dsa_user_phy_fwnode(dp);
+	if (!phy_fwnode)
+		return 0;
+
+	fwnode_handle_put(phy_fwnode);
+
+	if (ds->ops->get_phy_flags)
+		phy_flags = ds->ops->get_phy_flags(ds, dp->index);
+
+	err = phylink_of_phy_connect(dp->pl, dp->dn, phy_flags);
+	if (err)
+		netdev_warn(dev, "could not connect PHY: %pe\n", ERR_PTR(err));
+
+	return err;
+}
+
 static int dsa_user_open(struct net_device *dev)
 {
 	struct net_device *conduit = dsa_user_to_conduit(dev);
@@ -413,6 +456,10 @@ static int dsa_user_open(struct net_device *dev)
 	if (err)
 		goto out;
 
+	err = dsa_user_late_phy_connect(dev);
+	if (err)
+		goto out_del_host_uc;
+
 	err = dsa_port_enable_rt(dp, dev->phydev);
 	if (err)
 		goto out_del_host_uc;
@@ -2651,11 +2698,37 @@ static int dsa_user_phy_connect(struct net_device *user_dev, int addr,
 	return phylink_connect_phy(dp->pl, user_dev->phydev);
 }
 
+/* Whether the port's PHY is served by the generic driver rather than by one
+ * of its own. Only meaningful after a failed connect, which releases the
+ * generic driver again.
+ */
+static bool dsa_user_phy_lacks_driver(const struct dsa_port *dp)
+{
+	struct fwnode_handle *phy_fwnode;
+	struct phy_device *phydev;
+	bool lacks_driver;
+
+	phy_fwnode = dsa_user_phy_fwnode(dp);
+	if (!phy_fwnode)
+		return false;
+
+	phydev = fwnode_phy_find_device(phy_fwnode);
+	fwnode_handle_put(phy_fwnode);
+	if (!phydev)
+		return false;
+
+	lacks_driver = !READ_ONCE(phydev->mdio.dev.driver);
+	put_device(&phydev->mdio.dev);
+
+	return lacks_driver;
+}
+
 static int dsa_user_phy_setup(struct net_device *user_dev)
 {
 	struct dsa_port *dp = dsa_user_to_port(user_dev);
 	struct device_node *port_dn = dp->dn;
 	struct dsa_switch *ds = dp->ds;
+	bool had_driver, retried = false;
 	u32 phy_flags = 0;
 	int ret;
 
@@ -2678,6 +2751,8 @@ static int dsa_user_phy_setup(struct net_device *user_dev)
 	if (ds->ops->get_phy_flags)
 		phy_flags = ds->ops->get_phy_flags(ds, dp->index);
 
+	had_driver = !dsa_user_phy_lacks_driver(dp);
+connect:
 	ret = phylink_of_phy_connect(dp->pl, port_dn, phy_flags);
 	if (ret == -ENODEV && ds->user_mii_bus) {
 		/* We could not connect to a designated PHY or SFP, so try to
@@ -2686,6 +2761,29 @@ static int dsa_user_phy_setup(struct net_device *user_dev)
 		ret = dsa_user_phy_connect(user_dev, dp->index, phy_flags);
 	}
 	if (ret) {
+		if (dsa_user_phy_lacks_driver(dp)) {
+			/* Not known to be reachable from the internal MDIO bus
+			 * fallback, which assigns user_dev->phydev before it
+			 * connects, but do not hand the open path a leftover.
+			 */
+			user_dev->phydev = NULL;
+
+			netdev_info(user_dev,
+				    "PHY has no driver, connecting it at open\n");
+			return 0;
+		}
+
+		/* A driver that was not there before this attempt is one that
+		 * bound while it ran: the failure came from the generic driver
+		 * and says nothing about this one, so try once more. A port
+		 * that had its driver all along keeps the single attempt.
+		 */
+		if (!had_driver && !retried) {
+			had_driver = true;
+			retried = true;
+			goto connect;
+		}
+
 		netdev_err(user_dev, "failed to connect to PHY: %pe\n",
 			   ERR_PTR(ret));
 		dsa_port_phylink_destroy(dp);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late
  2026-08-22 15:52 ` [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin
@ 2026-08-22 17:30   ` Andrew Lunn
  0 siblings, 0 replies; 13+ messages in thread
From: Andrew Lunn @ 2026-08-22 17:30 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel

> +	if (ret) {
> +		mutex_lock(&pl->phydev_mutex);
> +		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);
> +		mutex_unlock(&pl->phydev_mutex);
> +	}

This is the same as the inner part of phylink_disconnect_phy(). Maybe
pull it out into a helper?

     Andrew

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 2/3] net: phy: restore the interrupt after a generic-driver bind cycle
  2026-08-22 15:52 ` [PATCH net-next 2/3] net: phy: restore the interrupt after a generic-driver bind cycle Aleksei Sviridkin
@ 2026-08-22 19:28   ` Andrew Lunn
  0 siblings, 0 replies; 13+ messages in thread
From: Andrew Lunn @ 2026-08-22 19:28 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel

>  	if (phydev->is_genphy_driven) {
> +		/* Give back the interrupt phy_probe() parked when the generic
> +		 * driver bound, before the device becomes bindable again. A
> +		 * PHY that was in polling mode for any other reason had
> +		 * PHY_POLL saved, and the restore is skipped.
> +		 */
> +		if (phydev->genphy_saved_irq > 0 && phydev->irq == PHY_POLL)
> +			phydev->irq = phydev->genphy_saved_irq;
> +		phydev->genphy_saved_irq = 0;
> +

I _think_ it can be simpler:

int fwnode_mdiobus_phy_device_register(struct mii_bus *mdio,
				       struct phy_device *phy,
				       struct fwnode_handle *child, u32 addr)
{
	int rc;

	rc = fwnode_irq_get(child, 0);
	/* Don't wait forever if the IRQ provider doesn't become available,
	 * just fall back to poll mode
	 */
	if (rc == -EPROBE_DEFER)
		rc = driver_deferred_probe_check_state(&phy->mdio.dev);
	if (rc == -EPROBE_DEFER)
		return rc;

	if (rc > 0) {
		phy->irq = rc;
		mdio->irq[addr] = rc;
	} else {
		phy->irq = mdio->irq[addr];
	}

So if there was an interrupt in DT, mdio->irq[addr] has been set to
it. So all i think you need is

    phydev->irq = mdio->irq[addr];

And a comment.

    Andrew

---
pw-bot: cr

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-22 15:52 ` [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup Aleksei Sviridkin
@ 2026-08-22 19:38   ` Andrew Lunn
  2026-08-23  0:05     ` Aleksei Sviridkin
  0 siblings, 1 reply; 13+ messages in thread
From: Andrew Lunn @ 2026-08-22 19:38 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel

On Sat, Aug 22, 2026 at 06:52:59PM +0300, Aleksei Sviridkin wrote:
> A PHY whose driver loads firmware at probe has no driver bound while
> that module still sits in an unmounted rootfs. phy_attach_direct()
> falls back to the generic driver, whose feature set lacks the modes the
> port is wired for, and the port is dropped for the rest of the uptime:
> 
>   mt7530-mdio mdio-bus:1f lan4: validation of 2500base-x [...] failed: -EINVAL
>   mt7530-mdio mdio-bus:1f lan4: error -22 setting up PHY for tree 0, switch 0, port 5
> 
> The same PHY wired to a MAC on the same SoC comes up. The difference is
> when the connect happens: the MAC driver connects from ndo_open, DSA
> connects during setup, at 1.9 s, before any rootfs exists.
> 
> Keep the port when the connect fails on a PHY that has no driver of its
> own, and connect it from the open path instead

The problem is, this is not guaranteed to work. The driver might still
not be loaded, or it is still downloading firmware to the PHY.

Think about the case of NFS root. The kernel will open() the interface
as soon as netdev_register() is called.

I think you need to look at the PHY driver. Make its probe function
return success, but start a thread downloading the firmware. While
firmware is downloading, either soft_reset() or config_init() needs to
block. That should allow the MAC to bind to the PHY, blocking
everything until the PHY is ready.

	   Andrew

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-22 19:38   ` Andrew Lunn
@ 2026-08-23  0:05     ` Aleksei Sviridkin
  2026-08-23  1:24       ` Andrew Lunn
  0 siblings, 1 reply; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-23  0:05 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel, Aleksei Sviridkin

> The problem is, this is not guaranteed to work. The driver might still
> not be loaded, or it is still downloading firmware to the PHY.

These are two separate halves, and the driver can only reach one of
them. When the firmware is late, your scheme works, and I want to do it
as a follow-up. When the module itself is still on an unmounted rootfs,
there is no probe to return success from, so the connect at switch
setup still gets the genphy fallback and the port is gone for good.
This patch is for that half, and it is the half the board in the
commit message hits: the driver is a module on the rootfs.

> Think about the case of NFS root. The kernel will open() the interface
> as soon as netdev_register() is called.

With NFS root the module and the firmware sit behind the network they
are supposed to bring up, so that setup already requires building them
in or an initramfs. With those in place the connect at setup succeeds
and this patch stays out of the way. Without them, today's kernel drops
the port at setup and NFS root is just as dead, so this does not
regress it.

> I think you need to look at the PHY driver. Make its probe function
> return success, but start a thread downloading the firmware. While
> firmware is downloading, either soft_reset() or config_init() needs to
> block.

I would rather not block: config_init runs inside phy_attach_direct, so
on a DSA switch the blocked port holds up the whole switch probe and
every other port with it. For the follow-up I want to attach with the
real driver's features immediately and keep the link down until the
firmware lands, then trigger aneg, the way an SFP port sits linkless
without a module. aquantia and mscc load firmware from probe too, so a
phylib helper for this would have three users.

If the async firmware part should come first and this patch second, I
can reorder.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-23  0:05     ` Aleksei Sviridkin
@ 2026-08-23  1:24       ` Andrew Lunn
  2026-08-23 12:37         ` Aleksei Sviridkin
  0 siblings, 1 reply; 13+ messages in thread
From: Andrew Lunn @ 2026-08-23  1:24 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel

On Sun, Aug 23, 2026 at 03:05:49AM +0300, Aleksei Sviridkin wrote:
> > The problem is, this is not guaranteed to work. The driver might still
> > not be loaded, or it is still downloading firmware to the PHY.
> 
> These are two separate halves, and the driver can only reach one of
> them. When the firmware is late, your scheme works, and I want to do it
> as a follow-up. When the module itself is still on an unmounted rootfs,
> there is no probe to return success from, so the connect at switch
> setup still gets the genphy fallback and the port is gone for good.

How about making the DSA driver depend on the PHY. That would be much
simpler. Both are then builtin or bother are modules, so both should
be available at the same time. You might also be able to use
MODULE_SOFTDEP() to get dracut to put the PHY module in the initramfs.

> > I think you need to look at the PHY driver. Make its probe function
> > return success, but start a thread downloading the firmware. While
> > firmware is downloading, either soft_reset() or config_init() needs to
> > block.
> 
> I would rather not block: config_init runs inside phy_attach_direct, so
> on a DSA switch the blocked port holds up the whole switch probe and
> every other port with it.

Given the poor hardware design, your choices are limited. We really
try hard not to put workarounds for bad designs in core code. We try
to hide it within the drivers. So it might be the whole switch needs
to wait.

> For the follow-up I want to attach with the
> real driver's features immediately and keep the link down until the
> firmware lands, then trigger aneg, the way an SFP port sits linkless
> without a module.

SFPs are different. The Linux code was designed from the ground up to
handle hot plugable devices. phylib itself is much older, and does not
handle hot plugable PHYs. The SFP case is made easier by the fact the
MDIO bus is hot plugged at the same time as the PHY. So from phylibs
perspective, it is not hot plugged.

> aquantia and mscc load firmware from probe too, so a phylib helper
> for this would have three users.

I agree a general solution would be nice, and people have put some
thought into trying to find one, but it is not easy.

	Andrew

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-23  1:24       ` Andrew Lunn
@ 2026-08-23 12:37         ` Aleksei Sviridkin
  2026-08-23 15:20           ` Andrew Lunn
  2026-08-24  2:40           ` Aleksei Sviridkin
  0 siblings, 2 replies; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-23 12:37 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel, Aleksei Sviridkin

> How about making the DSA driver depend on the PHY. That would be much
> simpler.

I worked through both halves of that for OpenWrt, where one kernel
image serves every board of a target, so every kilobyte lands on all of
them.

Built-in: the PHY driver itself is cheap, under 8k of text and data on
arm64. The firmware still lives on the rootfs though, and a built-in
driver probes from an initcall, before mount_root(), so blocking in
config_init() until the firmware shows up deadlocks the boot. Putting
the firmware in the kernel image costs 144k on every board of the
target, and almost none of them have this PHY.

Modules: that works, and the kernel gets smaller. OpenWrt already does
it elsewhere, kmod-dsa-mv88e6xxx depends on kmod-phy-marvell. What
stops me is that it moves the switch driver out of the kernel for every
board of the target, and NFS root goes with it. That is a bigger change
than I want to push on my own, and it gives up the case you asked
about.

Do you see a third option, or is the module direction the one you would
take, with NFS root treated as acceptable to lose on boards like these?
MODULE_SOFTDEP looks useful where dracut builds the initramfs, OpenWrt
builds its own, so I am not sure it applies here.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-23 12:37         ` Aleksei Sviridkin
@ 2026-08-23 15:20           ` Andrew Lunn
  2026-08-24  2:40           ` Aleksei Sviridkin
  1 sibling, 0 replies; 13+ messages in thread
From: Andrew Lunn @ 2026-08-23 15:20 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel

On Sun, Aug 23, 2026 at 03:37:29PM +0300, Aleksei Sviridkin wrote:
> > How about making the DSA driver depend on the PHY. That would be much
> > simpler.
> 
> I worked through both halves of that for OpenWrt, where one kernel
> image serves every board of a target, so every kilobyte lands on all of
> them.
> 
> Built-in: the PHY driver itself is cheap, under 8k of text and data on
> arm64. The firmware still lives on the rootfs though, and a built-in
> driver probes from an initcall, before mount_root(), so blocking in
> config_init() until the firmware shows up deadlocks the boot. Putting
> the firmware in the kernel image costs 144k on every board of the
> target, and almost none of them have this PHY.
> 
> Modules: that works, and the kernel gets smaller. OpenWrt already does
> it elsewhere, kmod-dsa-mv88e6xxx depends on kmod-phy-marvell. What
> stops me is that it moves the switch driver out of the kernel for every
> board of the target, and NFS root goes with it. That is a bigger change
> than I want to push on my own, and it gives up the case you asked
> about.

Another option is move the firmware download into the bootloader.

Going back to the big picture...

My opinion is that moving the binding of MAC to PHY into open is
wrong. We should be building on phylinks support for hotplug of SFP
modules.

But i've not yet figured out how that would work. I need to think on
it for a while.

   Andrew

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup
  2026-08-23 12:37         ` Aleksei Sviridkin
  2026-08-23 15:20           ` Andrew Lunn
@ 2026-08-24  2:40           ` Aleksei Sviridkin
  1 sibling, 0 replies; 13+ messages in thread
From: Aleksei Sviridkin @ 2026-08-24  2:40 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel, Aleksei Sviridkin

> Another option is move the firmware download into the bootloader.

I went and looked at what that would take, and it does not help here,
for a reason that also corrects something in my own commit message.

The firmware is not what makes this PHY late. air_en8811h is a module
in the rootfs, and on this board it probes at 6.25 s, while the port is
already dropped at 1.878 s. Moving the blobs earlier changes nothing
while the driver itself arrives that late.

Even with the firmware preloaded, en8811h_probe() calls
en8811h_load_firmware() unconditionally, and that starts with
request_firmware_direct(). Nothing reads the version register or the
ready bit first, so a preloaded PHY is reloaded, and a PHY with no
blobs available still fails probe. A bootloader preload would need a
kernel-side early-out to have any effect at all.

And it would be gone before then anyway: the failed bringup ends in
phy_detach(), which asserts the reset line, and this PHY has
reset-gpios. So whatever the bootloader put in the MD32 is wiped at
1.88 s.

U-Boot does already carry the whole loader, in v2026.07, which is what
we build. What it cannot do here is read the blobs out of a squashfs
inside UBI, and mtk_eth_probe() takes the switch branch and never
connects a PHY on this board, so it would be new code either way.

None of that argues against your main point. I am dropping this patch
and sending the phylink and phylib fixes on their own, since they stand
without it. If the phylink hotplug direction ends up wanting someone to
test it on hardware that reproduces this, I have the board.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes
  2026-08-22 15:52 [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Aleksei Sviridkin
                   ` (2 preceding siblings ...)
  2026-08-22 15:52 ` [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup Aleksei Sviridkin
@ 2026-08-24 16:25 ` Andrew Lunn
  3 siblings, 0 replies; 13+ messages in thread
From: Andrew Lunn @ 2026-08-24 16:25 UTC (permalink / raw)
  To: Aleksei Sviridkin
  Cc: Vladimir Oltean, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, netdev,
	linux-kernel

Hi Aleksei

I've had time to think about this, and now have a architecture to
solve the problem which i think it better.

It splits into two parts, getting the PHY firmware downloaded and
registered with phylib, and the phylink handling "hotplug" PHYs.

When power is applied to the "PHY", or after a reset, it is not
actually a PHY. It is a microcontroller sat in its bootloader waiting
for firmware to be downloaded. At that point, it has no PHY
functionality. So lets represent it this way in DT:

    davinci_mdio: mdio@5c030000 {
        reg = <0x5c030000 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;

        reset-gpios = <&gpio2 5 1>;
        reset-delay-us = <2>;

        ethphy0: ethernet-phy@1 {
            reg = <1>;
        };

	mcu: mcu@3 {
	    compatible = "airoha,en8811h-mcu";
	    reg = <3>;
	}

The compatible here makes it an MDIO device, not a PHY device. The
MDIO subsystem will load an MDIO driver for that compatible, and the
driver can then access device 3 on the MDIO bus. That driver will then
poll the filesystem for the firmware and download it. It might need to
do that in a thread, rather than probe(), i don't know.

Once the firmware starts, we have a PHY. And thinking ahead a bit,
there is no reason this MCU is for a single PHY, it could be a quad
PHY. We need to be able to represent this PHY in DT:

    davinci_mdio: mdio@5c030000 {
        reg = <0x5c030000 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;

        reset-gpios = <&gpio2 5 1>;
        reset-delay-us = <2>;

        ethphy0: ethernet-phy@1 {
            reg = <1>;
        };

	mcu: mcu@3 {
	    compatible = "airoha,en8811h-mcu";
	    reg = <3>;

	    mdio {
	        ethphy3: ethernet-phy@3 {
                reg = <3>;
            };
	};
    };

Have the MDIO device create a new MDIO bus, with pass through
operations to access the underlying MDIO bus, but just for one
address. For all other addresses return -ENODEV. When you register
this MDIO bus, it will get scanned and the PHY found. Since the PHY is
now actually up and running phylib is happy, its usual semantics are
true, the device is ready to go as soon a probe() returns.

As you pointed out, there are currently 3 devices which need to
download firmware. I _guess_ 3/4 of the code can be shared, so please
put must of it into a library, and only have code for actually
downloading to the PHY in the driver.

Then there is a phylink part. This is inspired by how SFP works. We
need some property in the MAC node which indicates the PHY is going to
arrive late. I'm not sure 'hotplug' is the correct description here,
since we know it is there, it is described in DT, it cannot be
exchanged for something else. For the moment, lets just call this
property 'slow-to-probe'. phylink_of_phy_connect() will look for this
property. If it finds 'slow-to-probe', there must also be a phy-handle
pointing to the PHY. phylink then sets itself up to handle this slow
PHY. It needs to poll the phy-handle until it resolves. It can then
call its own phylink_connect_phy() function to connect up the PHY.

As with an SFP, ksetting_get() should return no link modes if the PHY
is not connected yet. ksetting_set() will automatically return EINVAL
when asked to enable a link mode, since none are supported.
eee_get/eee_set should do the same. Since this is how SFPs work, it
should not be too hard to make user space understand an interface can
start out not supporting anything, and then later have various link
modes, autoneg etc.

Please have a think about this architecture, and see if you can find
any holes in it.

    Andrew

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-08-24 16:25 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-22 15:52 [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Aleksei Sviridkin
2026-08-22 15:52 ` [PATCH net-next 1/3] net: phylink: unwind the PHY binding when bringup fails late Aleksei Sviridkin
2026-08-22 17:30   ` Andrew Lunn
2026-08-22 15:52 ` [PATCH net-next 2/3] net: phy: restore the interrupt after a generic-driver bind cycle Aleksei Sviridkin
2026-08-22 19:28   ` Andrew Lunn
2026-08-22 15:52 ` [PATCH net-next 3/3] net: dsa: connect a late-arriving PHY at ifup Aleksei Sviridkin
2026-08-22 19:38   ` Andrew Lunn
2026-08-23  0:05     ` Aleksei Sviridkin
2026-08-23  1:24       ` Andrew Lunn
2026-08-23 12:37         ` Aleksei Sviridkin
2026-08-23 15:20           ` Andrew Lunn
2026-08-24  2:40           ` Aleksei Sviridkin
2026-08-24 16:25 ` [PATCH net-next 0/3] net: survive a PHY whose driver arrives after the switch probes Andrew Lunn

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®