F:     include/linux/of_net.h
 F:     include/linux/phy.h
 F:     include/linux/phy_fixed.h
+F:     include/linux/phy_link_topology.h
+F:     include/linux/phy_link_topology_core.h
 F:     include/linux/phylib_stubs.h
 F:     include/linux/platform_data/mdio-bcm-unimac.h
 F:     include/linux/platform_data/mdio-gpio.h
 
 # Makefile for Linux PHY drivers
 
 libphy-y                       := phy.o phy-c45.o phy-core.o phy_device.o \
-                                  linkmode.o
+                                  linkmode.o phy_link_topology.o
 mdio-bus-y                     += mdio_bus.o mdio_device.o
 
 ifdef CONFIG_MDIO_DEVICE
 
 #include <linux/phy.h>
 #include <linux/phylib_stubs.h>
 #include <linux/phy_led_triggers.h>
+#include <linux/phy_link_topology.h>
 #include <linux/pse-pd/pse.h>
 #include <linux/property.h>
 #include <linux/rtnetlink.h>
 
                if (phydev->sfp_bus_attached)
                        dev->sfp_bus = phydev->sfp_bus;
+
+               err = phy_link_topo_add_phy(dev->link_topo, phydev,
+                                           PHY_UPSTREAM_MAC, dev);
+               if (err)
+                       goto error;
        }
 
        /* Some Ethernet drivers try to connect to a PHY device before
        if (dev) {
                phydev->attached_dev->phydev = NULL;
                phydev->attached_dev = NULL;
+               phy_link_topo_del_phy(dev->link_topo, phydev);
        }
        phydev->phylink = NULL;
 
 
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Infrastructure to handle all PHY devices connected to a given netdev,
+ * either directly or indirectly attached.
+ *
+ * Copyright (c) 2023 Maxime Chevallier<maxime.chevallier@bootlin.com>
+ */
+
+#include <linux/phy_link_topology.h>
+#include <linux/netdevice.h>
+#include <linux/phy.h>
+#include <linux/rtnetlink.h>
+#include <linux/xarray.h>
+
+struct phy_link_topology *phy_link_topo_create(struct net_device *dev)
+{
+       struct phy_link_topology *topo;
+
+       topo = kzalloc(sizeof(*topo), GFP_KERNEL);
+       if (!topo)
+               return ERR_PTR(-ENOMEM);
+
+       xa_init_flags(&topo->phys, XA_FLAGS_ALLOC1);
+       topo->next_phy_index = 1;
+
+       return topo;
+}
+
+void phy_link_topo_destroy(struct phy_link_topology *topo)
+{
+       if (!topo)
+               return;
+
+       xa_destroy(&topo->phys);
+       kfree(topo);
+}
+
+int phy_link_topo_add_phy(struct phy_link_topology *topo,
+                         struct phy_device *phy,
+                         enum phy_upstream upt, void *upstream)
+{
+       struct phy_device_node *pdn;
+       int ret;
+
+       pdn = kzalloc(sizeof(*pdn), GFP_KERNEL);
+       if (!pdn)
+               return -ENOMEM;
+
+       pdn->phy = phy;
+       switch (upt) {
+       case PHY_UPSTREAM_MAC:
+               pdn->upstream.netdev = (struct net_device *)upstream;
+               if (phy_on_sfp(phy))
+                       pdn->parent_sfp_bus = pdn->upstream.netdev->sfp_bus;
+               break;
+       case PHY_UPSTREAM_PHY:
+               pdn->upstream.phydev = (struct phy_device *)upstream;
+               if (phy_on_sfp(phy))
+                       pdn->parent_sfp_bus = pdn->upstream.phydev->sfp_bus;
+               break;
+       default:
+               ret = -EINVAL;
+               goto err;
+       }
+       pdn->upstream_type = upt;
+
+       /* Attempt to re-use a previously allocated phy_index */
+       if (phy->phyindex) {
+               ret = xa_insert(&topo->phys, phy->phyindex, pdn, GFP_KERNEL);
+
+               /* Errors could be either -ENOMEM or -EBUSY. If the phy has an
+                * index, and there's another entry at the same index, this is
+                * unexpected and we still error-out
+                */
+               if (ret)
+                       goto err;
+               return 0;
+       }
+
+       ret = xa_alloc_cyclic(&topo->phys, &phy->phyindex, pdn, xa_limit_32b,
+                             &topo->next_phy_index, GFP_KERNEL);
+       if (ret)
+               goto err;
+
+       return 0;
+
+err:
+       kfree(pdn);
+       return ret;
+}
+EXPORT_SYMBOL_GPL(phy_link_topo_add_phy);
+
+void phy_link_topo_del_phy(struct phy_link_topology *topo,
+                          struct phy_device *phy)
+{
+       struct phy_device_node *pdn = xa_erase(&topo->phys, phy->phyindex);
+
+       /* We delete the PHY from the topology, however we don't re-set the
+        * phy->phyindex field. If the PHY isn't gone, we can re-assign it the
+        * same index next time it's added back to the topology
+        */
+
+       kfree(pdn);
+}
+EXPORT_SYMBOL_GPL(phy_link_topo_del_phy);
 
 #include <net/dcbnl.h>
 #endif
 #include <net/netprio_cgroup.h>
-
 #include <linux/netdev_features.h>
 #include <linux/neighbour.h>
 #include <uapi/linux/netdevice.h>
 #include <net/net_trackers.h>
 #include <net/net_debug.h>
 #include <net/dropreason-core.h>
+#include <linux/phy_link_topology_core.h>
 
 struct netpoll_info;
 struct device;
  *     @fcoe_ddp_xid:  Max exchange id for FCoE LRO by ddp
  *
  *     @priomap:       XXX: need comments on this one
+ *     @link_topo:     Physical link topology tracking attached PHYs
  *     @phydev:        Physical device may attach itself
  *                     for hardware timestamping
  *     @sfp_bus:       attached &struct sfp_bus structure.
 #if IS_ENABLED(CONFIG_CGROUP_NET_PRIO)
        struct netprio_map __rcu *priomap;
 #endif
+       struct phy_link_topology        *link_topo;
        struct phy_device       *phydev;
        struct sfp_bus          *sfp_bus;
        struct lock_class_key   *qdisc_tx_busylock;
 
  * @drv: Pointer to the driver for this PHY instance
  * @devlink: Create a link between phy dev and mac dev, if the external phy
  *           used by current mac interface is managed by another mac interface.
+ * @phyindex: Unique id across the phy's parent tree of phys to address the PHY
+ *           from userspace, similar to ifindex. A zero index means the PHY
+ *           wasn't assigned an id yet.
  * @phy_id: UID for this device found during discovery
  * @c45_ids: 802.3-c45 Device Identifiers if is_c45.
  * @is_c45:  Set to true if this PHY uses clause 45 addressing.
 
        struct device_link *devlink;
 
+       u32 phyindex;
        u32 phy_id;
 
        struct phy_c45_device_ids c45_ids;
 
--- /dev/null
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * PHY device list allow maintaining a list of PHY devices that are
+ * part of a netdevice's link topology. PHYs can for example be chained,
+ * as is the case when using a PHY that exposes an SFP module, on which an
+ * SFP transceiver that embeds a PHY is connected.
+ *
+ * This list can then be used by userspace to leverage individual PHY
+ * capabilities.
+ */
+#ifndef __PHY_LINK_TOPOLOGY_H
+#define __PHY_LINK_TOPOLOGY_H
+
+#include <linux/ethtool.h>
+#include <linux/phy_link_topology_core.h>
+
+struct xarray;
+struct phy_device;
+struct net_device;
+struct sfp_bus;
+
+struct phy_device_node {
+       enum phy_upstream upstream_type;
+
+       union {
+               struct net_device       *netdev;
+               struct phy_device       *phydev;
+       } upstream;
+
+       struct sfp_bus *parent_sfp_bus;
+
+       struct phy_device *phy;
+};
+
+struct phy_link_topology {
+       struct xarray phys;
+       u32 next_phy_index;
+};
+
+static inline struct phy_device *
+phy_link_topo_get_phy(struct phy_link_topology *topo, u32 phyindex)
+{
+       struct phy_device_node *pdn = xa_load(&topo->phys, phyindex);
+
+       if (pdn)
+               return pdn->phy;
+
+       return NULL;
+}
+
+#if IS_REACHABLE(CONFIG_PHYLIB)
+int phy_link_topo_add_phy(struct phy_link_topology *topo,
+                         struct phy_device *phy,
+                         enum phy_upstream upt, void *upstream);
+
+void phy_link_topo_del_phy(struct phy_link_topology *lt, struct phy_device *phy);
+
+#else
+static inline int phy_link_topo_add_phy(struct phy_link_topology *topo,
+                                       struct phy_device *phy,
+                                       enum phy_upstream upt, void *upstream)
+{
+       return 0;
+}
+
+static inline void phy_link_topo_del_phy(struct phy_link_topology *topo,
+                                        struct phy_device *phy)
+{
+}
+#endif
+
+#endif /* __PHY_LINK_TOPOLOGY_H */
 
--- /dev/null
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PHY_LINK_TOPOLOGY_CORE_H
+#define __PHY_LINK_TOPOLOGY_CORE_H
+
+struct phy_link_topology;
+
+#if IS_REACHABLE(CONFIG_PHYLIB)
+
+struct phy_link_topology *phy_link_topo_create(struct net_device *dev);
+void phy_link_topo_destroy(struct phy_link_topology *topo);
+
+#else
+
+static inline struct phy_link_topology *phy_link_topo_create(struct net_device *dev)
+{
+       return NULL;
+}
+
+static inline void phy_link_topo_destroy(struct phy_link_topology *topo)
+{
+}
+
+#endif
+
+#endif /* __PHY_LINK_TOPOLOGY_CORE_H */
 
         * __u32 map_lp_advertising[link_mode_masks_nwords];
         */
 };
+
+/**
+ * enum phy_upstream - Represents the upstream component a given PHY device
+ * is connected to, as in what is on the other end of the MII bus. Most PHYs
+ * will be attached to an Ethernet MAC controller, but in some cases, there's
+ * an intermediate PHY used as a media-converter, which will driver another
+ * MII interface as its output.
+ * @PHY_UPSTREAM_MAC: Upstream component is a MAC (a switch port,
+ *                   or ethernet controller)
+ * @PHY_UPSTREAM_PHY: Upstream component is a PHY (likely a media converter)
+ */
+enum phy_upstream {
+       PHY_UPSTREAM_MAC,
+       PHY_UPSTREAM_PHY,
+};
+
 #endif /* _UAPI_LINUX_ETHTOOL_H */
 
 #include <net/page_pool/types.h>
 #include <net/page_pool/helpers.h>
 #include <net/rps.h>
+#include <linux/phy_link_topology_core.h>
 
 #include "dev.h"
 #include "net-sysfs.h"
 #ifdef CONFIG_NET_SCHED
        hash_init(dev->qdisc_hash);
 #endif
+       dev->link_topo = phy_link_topo_create(dev);
+       if (IS_ERR(dev->link_topo)) {
+               dev->link_topo = NULL;
+               goto free_all;
+       }
+
        dev->priv_flags = IFF_XMIT_DST_RELEASE | IFF_XMIT_DST_RELEASE_PERM;
        setup(dev);
 
        free_percpu(dev->xdp_bulkq);
        dev->xdp_bulkq = NULL;
 
+       phy_link_topo_destroy(dev->link_topo);
+
        /*  Compatibility with error handling in drivers */
        if (dev->reg_state == NETREG_UNINITIALIZED) {
                netdev_freemem(dev);