[PATCH net-next v3 4/4] net: mdio: support dynamic OF device changes

From: James Hilliard

Date: Wed Aug 05 2026 - 13:52:53 EST


MDIO buses enumerate firmware children only when the bus is registered.
A later DT overlay or status transition is ignored, unlike I2C and SPI.

Add an OF reconfiguration notifier. On addition, create fixed-address
PHYs and generic MDIO devices, scan free addresses for legacy PHY nodes
without reg, and recurse into Ethernet PHY packages. Track populated
nodes so static enumeration, address scanning and notifier-driven
enumeration cannot create duplicates. Walk every package child and report
the first population error without undoing devices which may already have
acquired consumers. Only scan nodes which omit reg; reject malformed fixed
addresses instead of treating them as legacy addressless PHYs.

Serialize static and notifier-driven population while allowing a PHY
probe to enable another node on the same bus. Check the node's current
state after taking that lock so an older notification cannot override a
newer status change.

On removal, refuse to unregister attached or in-flight PHYs and find the
object by its OF node. This also covers PHYs found by address scanning.
Treat all PHYs in a package as one removal scope, so removing one member
cannot invalidate state used by an attached peer. Preflight overlay
removal while it can still be refused; normal changeset errors are
propagated so the caller can restore the previous state. Ignore unrelated
nodes and MDIO buses which have already disappeared when the global
overlay preflight walks a changeset.

Keep overlay preflight outside the global MDIO reconfiguration lock. The
OF core invokes PRE_REMOVE while holding its own mutex, and an MDIO device
probe running under the reconfiguration lock may apply an OF changeset.
Use a per-bus removal transaction to block new registration and attachment
while checking the map. Starting that exclusive transaction fails when
another map operation is active, so refuse the overlay removal rather than
waiting under the OF mutex; the caller can retry after that operation
finishes.

Hold a core MDIO change transaction across discovery and registration so
bus teardown cannot race the MDIO reads used to identify a PHY. Compile
the notifier only with CONFIG_OF_DYNAMIC and the overlay preflight only
with CONFIG_OF_OVERLAY.

Signed-off-by: James Hilliard <james.hilliard1@xxxxxxxxx>
---
Changes v2 -> v3:
- do not block overlays containing unrelated non-MDIO nodes
- do not block overlays whose MDIO bus is no longer present
- use READ_ONCE() and WRITE_ONCE() for recursive lock-owner accesses
- use the common checked MDIO device-removal callback
- avoid an OF/MDIO lock inversion during overlay preflight
- refuse overlay removal while another MDIO map change is active
- propagate removal-transaction conflicts from the OF notifier
- do not scan nodes with a malformed reg property
- treat a populated node associated with another MDIO bus as busy

Changes v1 -> v2:
- split from the ACx00 series
- move the PHY declaration to function scope for netdev style
---
drivers/net/mdio/of_mdio.c | 454 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 445 insertions(+), 9 deletions(-)

diff --git a/drivers/net/mdio/of_mdio.c b/drivers/net/mdio/of_mdio.c
index 051e449bbe7c..9e0b2f29959e 100644
--- a/drivers/net/mdio/of_mdio.c
+++ b/drivers/net/mdio/of_mdio.c
@@ -13,6 +13,7 @@
#include <linux/fwnode_mdio.h>
#include <linux/kernel.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/netdevice.h>
#include <linux/of.h>
#include <linux/of_irq.h>
@@ -20,6 +21,8 @@
#include <linux/of_net.h>
#include <linux/phy.h>
#include <linux/phy_fixed.h>
+#include <linux/sched.h>
+#include <linux/string.h>

#define DEFAULT_GPIO_RESET_DELAY 10 /* in microseconds */

@@ -27,6 +30,52 @@ MODULE_AUTHOR("Grant Likely <grant.likely@xxxxxxxxxxxx>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("OpenFirmware MDIO bus (Ethernet PHY) accessors");

+#if IS_ENABLED(CONFIG_OF_DYNAMIC)
+/*
+ * OF changes can nest when probing one MDIO device enables another node on
+ * the same bus. Serialize independent changes while allowing that nesting.
+ */
+static DEFINE_MUTEX(of_mdio_reconfig_mutex);
+static struct task_struct *of_mdio_reconfig_owner;
+static unsigned int of_mdio_reconfig_depth;
+
+static void of_mdio_reconfig_lock(void)
+{
+ if (!mutex_trylock(&of_mdio_reconfig_mutex)) {
+ if (READ_ONCE(of_mdio_reconfig_owner) == current) {
+ of_mdio_reconfig_depth++;
+ return;
+ }
+ mutex_lock(&of_mdio_reconfig_mutex);
+ }
+
+ WARN_ON_ONCE(READ_ONCE(of_mdio_reconfig_owner));
+ WARN_ON_ONCE(of_mdio_reconfig_depth);
+ WRITE_ONCE(of_mdio_reconfig_owner, current);
+ of_mdio_reconfig_depth = 1;
+}
+
+static void of_mdio_reconfig_unlock(void)
+{
+ WARN_ON_ONCE(READ_ONCE(of_mdio_reconfig_owner) != current);
+ WARN_ON_ONCE(!of_mdio_reconfig_depth);
+
+ if (--of_mdio_reconfig_depth)
+ return;
+
+ WRITE_ONCE(of_mdio_reconfig_owner, NULL);
+ mutex_unlock(&of_mdio_reconfig_mutex);
+}
+#else
+static inline void of_mdio_reconfig_lock(void)
+{
+}
+
+static inline void of_mdio_reconfig_unlock(void)
+{
+}
+#endif
+
/* Extract the clause 22 phy ID from the compatible string of the form
* ethernet-phy-idAAAA.BBBB */
static int of_get_phy_id(struct device_node *device, u32 *phy_id)
@@ -80,10 +129,20 @@ static int of_mdiobus_register_device(struct mii_bus *mdio,
static int of_mdiobus_register_child(struct mii_bus *mdio,
struct device_node *child, u32 addr)
{
+ int rc;
+
+ if (of_node_test_and_set_flag(child, OF_POPULATED))
+ return 0;
+
if (of_mdiobus_child_is_phy(child))
- return of_mdiobus_register_phy(mdio, child, addr);
+ rc = of_mdiobus_register_phy(mdio, child, addr);
+ else
+ rc = of_mdiobus_register_device(mdio, child, addr);
+
+ if (rc)
+ of_node_clear_flag(child, OF_POPULATED);

- return of_mdiobus_register_device(mdio, child, addr);
+ return rc;
}

/* The following is a list of PHY compatible strings which appear in
@@ -255,13 +314,21 @@ int __of_mdiobus_register(struct mii_bus *mdio, struct device_node *np,
if (rc)
return rc;

+ of_mdio_reconfig_lock();
+ rc = mdiobus_device_change_begin(mdio, false);
+ if (rc) {
+ of_mdio_reconfig_unlock();
+ mdiobus_unregister(mdio);
+ return rc;
+ }
+
/* Loop over the child nodes and register a phy_device for each phy */
rc = __of_mdiobus_parse_phys(mdio, np, &scanphys);
if (rc)
- goto unregister;
+ goto out_change;

if (!scanphys)
- return 0;
+ goto out_change;

/* auto scan for PHYs with empty reg property */
for_each_available_child_of_node(np, child) {
@@ -272,19 +339,388 @@ int __of_mdiobus_register(struct mii_bus *mdio, struct device_node *np,

rc = of_mdiobus_scan_phy(mdio, child);
if (rc && rc != -ENODEV)
- goto put_unregister;
+ goto put_child;
+ rc = 0;
}

- return 0;
+out_change:
+ mdiobus_device_change_end(mdio, false);
+ of_mdio_reconfig_unlock();
+ if (!rc)
+ return 0;

-put_unregister:
- of_node_put(child);
-unregister:
mdiobus_unregister(mdio);
return rc;
+
+put_child:
+ of_node_put(child);
+ goto out_change;
}
EXPORT_SYMBOL(__of_mdiobus_register);

+#if IS_ENABLED(CONFIG_OF_DYNAMIC)
+static bool of_mdiobus_node_is_available(struct device_node *node)
+{
+ return !of_node_check_flag(node, OF_DETACHED) &&
+ of_device_is_available(node);
+}
+
+static int of_mdiobus_add_node(struct mii_bus *mdio,
+ struct device_node *node)
+{
+ struct device_node *child;
+ int addr, rc, ret = 0;
+
+ if (!of_mdiobus_node_is_available(node))
+ return 0;
+
+ if (of_node_name_eq(node, "ethernet-phy-package")) {
+ if (!of_property_present(node, "reg"))
+ return 0;
+
+ for_each_available_child_of_node(node, child) {
+ rc = of_mdiobus_add_node(mdio, child);
+ if (rc && rc != -ENODEV && !ret)
+ ret = rc;
+ }
+
+ return ret;
+ }
+
+ addr = of_mdio_parse_addr(&mdio->dev, node);
+ if (addr < 0) {
+ if (of_property_present(node, "reg"))
+ return addr;
+
+ rc = of_mdiobus_scan_phy(mdio, node);
+ } else {
+ rc = of_mdiobus_register_child(mdio, node, addr);
+ }
+
+ if (rc == -ENODEV && addr >= 0)
+ dev_err(&mdio->dev,
+ "MDIO device at address %d is missing.\n", addr);
+
+ return rc;
+}
+
+static bool of_mdiobus_node_is_busy(struct mii_bus *mdio,
+ struct device_node *node)
+{
+ struct mdio_device *mdiodev;
+ struct phy_device *phydev;
+ bool busy = false;
+
+ if (of_node_name_eq(node, "ethernet-phy-package")) {
+ for_each_child_of_node_scoped(node, child) {
+ if (of_mdiobus_node_is_busy(mdio, child))
+ return true;
+ }
+
+ return false;
+ }
+
+ if (!of_node_check_flag(node, OF_POPULATED))
+ return false;
+
+ mdiodev = of_mdio_find_device(node);
+ if (!mdiodev)
+ return true;
+ if (mdiodev->bus != mdio) {
+ put_device(&mdiodev->dev);
+ return true;
+ }
+
+ mutex_lock(&mdio->mdio_map_lock);
+ busy = mdio->mdio_map[mdiodev->addr] != mdiodev ||
+ (mdio->mdio_map_pending & BIT(mdiodev->addr));
+ if (mdiodev->flags & MDIO_DEVICE_FLAG_PHY) {
+ phydev = to_phy_device(&mdiodev->dev);
+ if (phydev->attached) {
+ busy = true;
+ dev_warn(&mdiodev->dev,
+ "cannot remove an attached PHY; remove its consumer first\n");
+ }
+ }
+ mutex_unlock(&mdio->mdio_map_lock);
+ put_device(&mdiodev->dev);
+
+ return busy;
+}
+
+static struct device_node *
+of_mdiobus_get_removal_scope(struct device_node *node)
+{
+ struct device_node *parent;
+
+ if (of_node_name_eq(node, "ethernet-phy-package"))
+ return of_node_get(node);
+
+ parent = of_get_parent(node);
+ if (of_node_name_eq(parent, "ethernet-phy-package"))
+ return parent;
+
+ of_node_put(parent);
+ return of_node_get(node);
+}
+
+static int of_mdiobus_remove_node(struct mii_bus *mdio,
+ struct device_node *node)
+{
+ struct mdio_device *mdiodev;
+ struct device_node *child;
+ int ret;
+
+ if (of_node_name_eq(node, "ethernet-phy-package")) {
+ for_each_child_of_node(node, child) {
+ ret = of_mdiobus_remove_node(mdio, child);
+ if (ret) {
+ of_node_put(child);
+ return ret;
+ }
+ }
+ return 0;
+ }
+
+ if (!of_node_check_flag(node, OF_POPULATED))
+ return 0;
+
+ /* The OF node lookup also covers PHYs found by address scanning. */
+ mdiodev = of_mdio_find_device(node);
+ if (!mdiodev)
+ return -EBUSY;
+ if (mdiodev->bus != mdio) {
+ put_device(&mdiodev->dev);
+ return -ENODEV;
+ }
+
+ ret = mdiodev->device_remove(mdiodev, true);
+ if (!ret)
+ mdiodev->device_free(mdiodev);
+ put_device(&mdiodev->dev);
+
+ return ret == -ENODEV ? 0 : ret;
+}
+
+static struct mii_bus *of_mdiobus_find_parent(struct device_node *node)
+{
+ struct device_node *parent, *bus_node;
+ struct mii_bus *mdio;
+
+ parent = of_get_parent(node);
+ if (!parent)
+ return NULL;
+
+ if (of_node_name_eq(parent, "ethernet-phy-package")) {
+ if (!of_device_is_available(parent) ||
+ !of_property_present(parent, "reg")) {
+ of_node_put(parent);
+ return NULL;
+ }
+
+ bus_node = of_get_parent(parent);
+ of_node_put(parent);
+ } else {
+ bus_node = parent;
+ }
+
+ mdio = of_mdio_find_bus(bus_node);
+ of_node_put(bus_node);
+
+ return mdio;
+}
+
+#if IS_ENABLED(CONFIG_OF_OVERLAY)
+/* Overlay entry notifier errors cannot stop removal after the tree changed. */
+static bool of_mdiobus_live_node_is_busy(struct device_node *node)
+{
+ struct device_node *scope;
+ struct mii_bus *mdio;
+ bool busy;
+
+ scope = of_mdiobus_get_removal_scope(node);
+ mdio = of_mdiobus_find_parent(scope);
+ /* Non-MDIO nodes and buses already removed cannot block an overlay. */
+ if (!mdio) {
+ busy = false;
+ goto out_put_scope;
+ }
+
+ busy = true;
+ if (!mdiobus_device_change_begin(mdio, true)) {
+ busy = of_mdiobus_node_is_busy(mdio, scope);
+ mdiobus_device_change_end(mdio, true);
+ }
+ put_device(&mdio->dev);
+
+out_put_scope:
+ of_node_put(scope);
+ return busy;
+}
+
+static struct device_node *
+of_mdiobus_overlay_target_child(struct device_node *target,
+ struct device_node *overlay_child)
+{
+ const char *name = kbasename(overlay_child->full_name);
+ struct device_node *child;
+
+ for_each_child_of_node(target, child) {
+ if (!of_node_cmp(kbasename(child->full_name), name))
+ return child;
+ }
+
+ return NULL;
+}
+
+static bool of_mdiobus_overlay_node_is_busy(struct device_node *overlay,
+ struct device_node *target,
+ bool added)
+{
+ struct device_node *overlay_child, *target_child;
+ bool busy, child_added;
+
+ if ((added || of_property_present(overlay, "status")) &&
+ of_mdiobus_live_node_is_busy(target))
+ return true;
+
+ for_each_child_of_node(overlay, overlay_child) {
+ target_child = of_mdiobus_overlay_target_child(target,
+ overlay_child);
+ if (!target_child)
+ continue;
+
+ child_added = of_node_check_flag(target_child, OF_OVERLAY);
+ busy = of_mdiobus_overlay_node_is_busy(overlay_child,
+ target_child, child_added);
+ of_node_put(target_child);
+ if (busy) {
+ of_node_put(overlay_child);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int of_mdiobus_overlay_notify(struct notifier_block *nb,
+ unsigned long action, void *arg)
+{
+ struct of_overlay_notify_data *nd = arg;
+ bool busy;
+
+ if (action != OF_OVERLAY_PRE_REMOVE)
+ return NOTIFY_OK;
+
+ busy = of_mdiobus_overlay_node_is_busy(nd->overlay, nd->target,
+ false);
+
+ return busy ? notifier_from_errno(-EBUSY) : NOTIFY_OK;
+}
+
+static struct notifier_block of_mdio_overlay_notifier = {
+ .notifier_call = of_mdiobus_overlay_notify,
+};
+#endif
+
+static int of_mdiobus_notify(struct notifier_block *nb, unsigned long action,
+ void *arg)
+{
+ struct of_reconfig_data *rd = arg;
+ struct device_node *scope;
+ struct mii_bus *mdio;
+ enum of_reconfig_change change;
+ bool removing;
+ int rc, ret = NOTIFY_OK;
+
+ of_mdio_reconfig_lock();
+ change = of_reconfig_get_state_change(action, rd);
+ switch (change) {
+ case OF_RECONFIG_CHANGE_ADD:
+ /* A newer change may have made this notification stale. */
+ if (!of_mdiobus_node_is_available(rd->dn))
+ goto out_unlock;
+ removing = false;
+ break;
+ case OF_RECONFIG_CHANGE_REMOVE:
+ /* A newer change may have made this notification stale. */
+ if (of_mdiobus_node_is_available(rd->dn))
+ goto out_unlock;
+ removing = true;
+ break;
+ default:
+ goto out_unlock;
+ }
+
+ mdio = of_mdiobus_find_parent(rd->dn);
+ if (!mdio)
+ goto out_unlock;
+
+ rc = mdiobus_device_change_begin(mdio, removing);
+ if (rc) {
+ if (!removing)
+ ret = notifier_from_errno(-EPROBE_DEFER);
+ else if (rc != -ENODEV)
+ ret = notifier_from_errno(rc);
+ goto out_put_mdio;
+ }
+
+ if (!removing) {
+ rc = of_mdiobus_add_node(mdio, rd->dn);
+ } else {
+ /* The node may already be detached from its parent hierarchy. */
+ scope = of_mdiobus_get_removal_scope(rd->dn);
+ if (of_mdiobus_node_is_busy(mdio, scope))
+ rc = -EBUSY;
+ else
+ rc = of_mdiobus_remove_node(mdio, rd->dn);
+ of_node_put(scope);
+ }
+
+ mdiobus_device_change_end(mdio, removing);
+ if (rc && (removing || rc != -ENODEV))
+ ret = notifier_from_errno(rc);
+
+out_put_mdio:
+ put_device(&mdio->dev);
+out_unlock:
+ of_mdio_reconfig_unlock();
+
+ return ret;
+}
+
+static struct notifier_block of_mdio_notifier = {
+ .notifier_call = of_mdiobus_notify,
+};
+
+static int __init of_mdio_init(void)
+{
+ int ret;
+
+ ret = of_reconfig_notifier_register(&of_mdio_notifier);
+ if (ret)
+ return ret;
+
+#if IS_ENABLED(CONFIG_OF_OVERLAY)
+ ret = of_overlay_notifier_register(&of_mdio_overlay_notifier);
+ if (ret)
+ of_reconfig_notifier_unregister(&of_mdio_notifier);
+#endif
+
+ return ret;
+}
+module_init(of_mdio_init);
+
+static void __exit of_mdio_exit(void)
+{
+#if IS_ENABLED(CONFIG_OF_OVERLAY)
+ of_overlay_notifier_unregister(&of_mdio_overlay_notifier);
+#endif
+ of_reconfig_notifier_unregister(&of_mdio_notifier);
+}
+module_exit(of_mdio_exit);
+#endif /* CONFIG_OF_DYNAMIC */
+
/**
* of_mdio_find_device - Given a device tree node, find the mdio_device
* @np: pointer to the mdio_device's device tree node

--
2.53.0