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. 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, reject attached or in-flight PHYs and unregister the object found 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. 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 --- drivers/net/mdio/of_mdio.c | 448 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 438 insertions(+), 10 deletions(-) diff --git a/drivers/net/mdio/of_mdio.c b/drivers/net/mdio/of_mdio.c index 051e449bbe7c..a8313b3721f6 100644 --- a/drivers/net/mdio/of_mdio.c +++ b/drivers/net/mdio/of_mdio.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,8 @@ #include #include #include +#include +#include #define DEFAULT_GPIO_RESET_DELAY 10 /* in microseconds */ @@ -27,6 +30,52 @@ MODULE_AUTHOR("Grant Likely "); 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 (of_mdio_reconfig_owner == current) { + of_mdio_reconfig_depth++; + return; + } + mutex_lock(&of_mdio_reconfig_mutex); + } + + WARN_ON_ONCE(of_mdio_reconfig_owner); + WARN_ON_ONCE(of_mdio_reconfig_depth); + of_mdio_reconfig_owner = current; + of_mdio_reconfig_depth = 1; +} + +static void of_mdio_reconfig_unlock(void) +{ + WARN_ON_ONCE(of_mdio_reconfig_owner != current); + WARN_ON_ONCE(!of_mdio_reconfig_depth); + + if (--of_mdio_reconfig_depth) + return; + + 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) @@ -44,7 +93,7 @@ int of_mdiobus_phy_device_register(struct mii_bus *mdio, struct phy_device *phy, EXPORT_SYMBOL(of_mdiobus_phy_device_register); static int of_mdiobus_register_phy(struct mii_bus *mdio, - struct device_node *child, u32 addr) + struct device_node *child, u32 addr) { return fwnode_mdiobus_register_phy(mdio, of_fwnode_handle(child), addr); } @@ -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); - return of_mdiobus_register_device(mdio, child, addr); + if (rc) + of_node_clear_flag(child, OF_POPULATED); + + 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,380 @@ 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) + 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; + 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 false; + } + + 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) { + struct phy_device *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_dynamic(mdiodev); + 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 = true; + + scope = of_mdiobus_get_removal_scope(node); + mdio = of_mdiobus_find_parent(scope); + if (!mdio) + goto out_put_scope; + + 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; + + of_mdio_reconfig_lock(); + busy = of_mdiobus_overlay_node_is_busy(nd->overlay, nd->target, + false); + of_mdio_reconfig_unlock(); + + 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); + 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