--- /dev/null
+From 5298944fa6b14e9c6a36bc61af7e4a495033b7af Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Mon, 13 Jul 2026 22:55:10 +0100
+Subject: [PATCH 1/2] devlink: Add MDIO bus support
+
+The backend refused to create devices for anything but the "pci" and
+"netdevsim" buses. Ethernet switches attached via MDIO (bus name
+"mdio_bus"), such as the MaxLinear MxL862xx family, were rejected
+before their versions were ever queried:
+
+ failed to add devlink device mdio_bus/mdio-bus:10: unsupported bus
+ type: mdio_bus (only 'pci' and 'netdevsim' are supported)
+
+Create a parent device via the udev backend like it is already done
+for PCI devices. MDIO devices expose no vendor or device IDs in sysfs,
+so use the first "compatible" string of the associated OF node to
+identify the device where available: the vendor prefix becomes a
+devicetree vendor ID, e.g. DT:maxlinear, and the two halves of the
+compatible are used as the VEN and DEV instance strings, so the usual
+quirk instance ID, e.g. MDIO_BUS\VEN_maxlinear&DEV_mxl86252, and
+component GUIDs, e.g. MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw,
+are built just like for PCI devices.
+
+Unlike the versions reported by the kernel, the devicetree compatible
+is constant and always available, even when the device cannot report
+any version information, for example while it is stuck in a bootloader
+rescue mode. Firmware targeting the compatible-based component GUID
+can therefore still be matched and flashed to recover such a device.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ plugins/devlink/README.md | 18 ++++++-
+ plugins/devlink/fu-devlink-backend.c | 81 +++++++++++++++++++++++++++-
+ plugins/devlink/fu-devlink-device.c | 22 ++++++++
+ 3 files changed, 117 insertions(+), 4 deletions(-)
+
+--- a/plugins/devlink/README.md
++++ b/plugins/devlink/README.md
+@@ -9,7 +9,8 @@ This is a generic plugin that can work w
+
+ ## Supported Devices
+
+-The plugin supports any device that implements the devlink interface, regardless the bus it resides on.
++The plugin supports devices implementing the devlink interface on the PCI and
++MDIO buses, as well as emulated netdevsim devices for testing.
+
+ ## Firmware Format
+
+@@ -25,9 +26,21 @@ This plugin supports the following proto
+ These devices use custom instance IDs consisting of the component name.
+
+ * `PCI\VEN_15B3&DEV_1021&COMPONENT_fw`
++* `MDIO_BUS\COMPONENT_fw`
++* `MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw`
+
+ Optionally, additional GUID might get generated as specified in the squirk file, see below.
+
++For devices attached via MDIO with an associated devicetree node, the vendor
++prefix and device name from the first "compatible" string are used as the
++`VEN` and `DEV` instance ID components, resulting in a quirk-only
++`MDIO_BUS\VEN_maxlinear&DEV_mxl86252` instance ID on the devlink device and
++component GUIDs just like for PCI devices. Unlike the versions reported by
++the kernel, the devicetree compatible is available even when the device
++cannot report any version information, for example while it is stuck in a
++bootloader rescue mode, so firmware targeting the compatible-based GUID can
++still be matched and flashed to recover such a device.
++
+ ### Device Identification
+
+ Devices are identified using their in the format:
+@@ -131,7 +144,8 @@ The plugin handles various error conditi
+
+ ## Vendor ID Security
+
+-The vendor ID is set from the PCI vendor.
++The vendor ID is set from the PCI vendor, or for devices on the MDIO bus, from
++the devicetree vendor prefix, e.g. `DT:maxlinear`.
+
+ ## External Interface Access
+
+--- a/plugins/devlink/fu-devlink-backend.c
++++ b/plugins/devlink/fu-devlink-backend.c
+@@ -67,6 +67,71 @@ fu_devlink_backend_create_pci_parent(FuD
+ }
+
+ static FuDevice *
++fu_devlink_backend_create_mdio_parent(FuDevlinkBackend *self,
++ const gchar *bus_name,
++ const gchar *dev_name,
++ GError **error)
++{
++ FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
++ FuBackend *udev_backend = NULL;
++ g_autofree gchar *mdio_sysfs_path = NULL;
++ g_autofree gchar *mdio_sysfs_real = NULL;
++ g_autoptr(FuDevice) mdio_device = NULL;
++ g_autoptr(GBytes) compatible_blob = NULL;
++ g_autoptr(GError) error_local = NULL;
++
++ udev_backend = fu_context_get_backend_by_name(ctx, "udev", &error_local);
++ if (udev_backend == NULL) {
++ g_set_error(error,
++ FWUPD_ERROR,
++ FWUPD_ERROR_NOT_SUPPORTED,
++ "udev backend not available: %s",
++ error_local->message);
++ return NULL;
++ }
++
++ mdio_sysfs_path = g_strdup_printf("/sys/bus/mdio_bus/devices/%s", dev_name);
++ mdio_sysfs_real = fu_path_make_absolute(mdio_sysfs_path, error);
++ if (mdio_sysfs_real == NULL)
++ return NULL;
++
++ mdio_device = fu_backend_create_device(udev_backend, mdio_sysfs_real, &error_local);
++ if (mdio_device == NULL) {
++ g_set_error(error,
++ FWUPD_ERROR,
++ FWUPD_ERROR_NOT_FOUND,
++ "failed to create MDIO device for %s: %s",
++ mdio_sysfs_path,
++ error_local->message);
++ return NULL;
++ }
++
++ if (!fu_device_probe(mdio_device, error)) {
++ g_prefix_error_literal(error, "failed to probe MDIO device: ");
++ return NULL;
++ }
++
++ /* of_node/compatible is a NUL-separated string list, so read it as raw bytes */
++ compatible_blob = fu_udev_device_read_sysfs_bytes(FU_UDEV_DEVICE(mdio_device),
++ "of_node/compatible",
++ -1,
++ FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT,
++ NULL);
++ if (compatible_blob != NULL && g_bytes_get_size(compatible_blob) > 0) {
++ g_autofree gchar *compatible = g_strndup(g_bytes_get_data(compatible_blob, NULL),
++ g_bytes_get_size(compatible_blob));
++ g_auto(GStrv) parts = g_strsplit(compatible, ",", 2);
++ if (g_strv_length(parts) == 2) {
++ fu_device_build_vendor_id(mdio_device, "DT", parts[0]);
++ fu_device_add_instance_strsafe(mdio_device, "VEN", parts[0]);
++ fu_device_add_instance_strsafe(mdio_device, "DEV", parts[1]);
++ }
++ }
++
++ return g_steal_pointer(&mdio_device);
++}
++
++static FuDevice *
+ fu_devlink_backend_create_netdevsim_parent(FuDevlinkBackend *self,
+ const gchar *bus_name,
+ const gchar *dev_name,
+@@ -96,6 +161,7 @@ fu_devlink_backend_device_added(FuDevlin
+ {
+ FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
+ FuDevice *old_devlink_device;
++ const gchar *instance_keys[] = {"VEN", "DEV", NULL};
+ g_autoptr(FuDevice) devlink_device = NULL;
+ g_autoptr(FuDevice) parent_device = NULL;
+
+@@ -104,12 +170,17 @@ fu_devlink_backend_device_added(FuDevlin
+ g_return_val_if_fail(dev_name != NULL, NULL);
+ g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+
+- /* only support PCI and netdevsim buses */
++ /* create a bus-specific parent device */
+ if (g_strcmp0(bus_name, "pci") == 0) {
+ parent_device =
+ fu_devlink_backend_create_pci_parent(self, bus_name, dev_name, error);
+ if (parent_device == NULL)
+ return NULL;
++ } else if (g_strcmp0(bus_name, "mdio_bus") == 0) {
++ parent_device =
++ fu_devlink_backend_create_mdio_parent(self, bus_name, dev_name, error);
++ if (parent_device == NULL)
++ return NULL;
+ } else if (g_strcmp0(bus_name, "netdevsim") == 0) {
+ parent_device =
+ fu_devlink_backend_create_netdevsim_parent(self, bus_name, dev_name, error);
+@@ -119,7 +190,8 @@ fu_devlink_backend_device_added(FuDevlin
+ g_set_error(error,
+ FWUPD_ERROR,
+ FWUPD_ERROR_NOT_SUPPORTED,
+- "unsupported bus type: %s (only 'pci' and 'netdevsim' are supported)",
++ "unsupported bus type: %s (only 'pci', 'mdio_bus' and 'netdevsim' are "
++ "supported)",
+ bus_name);
+ return NULL;
+ }
+@@ -157,6 +229,11 @@ fu_devlink_backend_device_added(FuDevlin
+ FU_DEVICE_INCORPORATE_FLAG_VENDOR |
+ FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS |
+ FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID);
++ for (guint i = 0; instance_keys[i] != NULL; i++) {
++ const gchar *value = fu_device_get_instance_str(parent_device, instance_keys[i]);
++ if (value != NULL)
++ fu_device_add_instance_str(devlink_device, instance_keys[i], value);
++ }
+
+ /* only add the devlink device to the backend - parent is managed by its own backend */
+ fu_backend_device_added(FU_BACKEND(self), devlink_device);
+--- a/plugins/devlink/fu-devlink-device.c
++++ b/plugins/devlink/fu-devlink-device.c
+@@ -699,6 +699,9 @@ fu_devlink_device_setup(FuDevice *device
+ if (fu_device_get_vid(device) != 0 && fu_device_get_pid(device) != 0) {
+ fu_device_add_instance_u16(device, "VEN", fu_device_get_vid(device));
+ fu_device_add_instance_u16(device, "DEV", fu_device_get_pid(device));
++ }
++ if (fu_device_get_instance_str(device, "VEN") != NULL &&
++ fu_device_get_instance_str(device, "DEV") != NULL) {
+ if (!fu_device_build_instance_id_full(device,
+ FU_DEVICE_INSTANCE_FLAG_QUIRKS,
+ error,
+@@ -864,6 +867,17 @@ fu_devlink_device_add_json(FuDevice *dev
+ fwupd_json_object_add_string(json_obj, "BusName", self->bus_name);
+ if (self->dev_name != NULL)
+ fwupd_json_object_add_string(json_obj, "DevName", self->dev_name);
++
++ /* instance strings inherited from the parent device, which does not exist under
++ * emulation */
++ if (fu_device_get_instance_str(device, "VEN") != NULL)
++ fwupd_json_object_add_string(json_obj,
++ "Ven",
++ fu_device_get_instance_str(device, "VEN"));
++ if (fu_device_get_instance_str(device, "DEV") != NULL)
++ fwupd_json_object_add_string(json_obj,
++ "Dev",
++ fu_device_get_instance_str(device, "DEV"));
+ }
+
+ static gboolean
+@@ -872,11 +886,19 @@ fu_devlink_device_from_json(FuDevice *de
+ FuDevlinkDevice *self = FU_DEVLINK_DEVICE(device);
+ const gchar *bus_name;
+ const gchar *dev_name;
++ const gchar *ven;
++ const gchar *dev;
+ g_autofree gchar *device_id = NULL;
+
+ /* devlink-specific properties */
+ bus_name = fwupd_json_object_get_string(json_obj, "BusName", NULL);
+ dev_name = fwupd_json_object_get_string(json_obj, "DevName", NULL);
++ ven = fwupd_json_object_get_string(json_obj, "Ven", NULL);
++ if (ven != NULL)
++ fu_device_add_instance_str(device, "VEN", ven);
++ dev = fwupd_json_object_get_string(json_obj, "Dev", NULL);
++ if (dev != NULL)
++ fu_device_add_instance_str(device, "DEV", dev);
+
+ if (bus_name == NULL || dev_name == NULL) {
+ g_set_error_literal(error,
--- /dev/null
+From e50c9e5ab39d31242e664efbbf441fd46d15a0cd Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Mon, 13 Jul 2026 22:57:10 +0100
+Subject: [PATCH 2/2] devlink: Add quirks for MaxLinear MxL862xx Ethernet
+ switches
+
+The mxl862xx DSA driver implements devlink info_get and flash_update
+for these switches attached via MDIO. The chip ID registers are only
+readable with a running firmware, so a switch stuck in MCUboot rescue
+mode reports no fixed versions at all and cannot be identified by its
+numeric "asic.id". Match the device and its firmware component on the
+devicetree compatible instead: it is available in both modes, so
+firmware targeting the compatible-based component GUID can also be
+flashed to recover a switch from rescue mode.
+
+The vendor ID derived from the devicetree vendor prefix by the MDIO
+parent device only reaches the devlink device itself, so it is
+repeated here for the component.
+
+The firmware image contains both application slots and is flashed as a
+whole, so component-specific updates are rejected by the driver and
+omit-component-name is required. All ports go down for the duration of
+the flash process and the new image boots immediately afterwards,
+hence neither usable-during-update nor dual-image apply.
+
+Also, add emulations recorded on a Bananapi BPI-R4 Pro 8X with a MaxLinear
+MxL86252 Ethernet switch attached via MDIO.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ plugins/devlink/README.md | 2 +-
+ plugins/devlink/devlink.quirk | 18 +++++++++
+ plugins/devlink/fu-devlink-backend.c | 41 +++++++++++++++------
+ plugins/devlink/fu-devlink-device.c | 21 +++++++----
+ plugins/devlink/meson.build | 5 ++-
+ plugins/devlink/tests/devlink-mxl86252.json | 20 ++++++++++
+ 6 files changed, 86 insertions(+), 21 deletions(-)
+ create mode 100644 plugins/devlink/tests/devlink-mxl86252.json
+
+--- a/plugins/devlink/README.md
++++ b/plugins/devlink/README.md
+@@ -29,7 +29,7 @@ These devices use custom instance IDs co
+ * `MDIO_BUS\COMPONENT_fw`
+ * `MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw`
+
+-Optionally, additional GUID might get generated as specified in the squirk file, see below.
++Optionally, additional GUIDs might be generated as specified in the quirk file, see below.
+
+ For devices attached via MDIO with an associated devicetree node, the vendor
+ prefix and device name from the first "compatible" string are used as the
+--- a/plugins/devlink/devlink.quirk
++++ b/plugins/devlink/devlink.quirk
+@@ -27,3 +27,21 @@ DevlinkFixedVersions = fw.psid
+ # NVIDIA ConnectX-8
+ [PCI\VEN_15B3&DEV_1023]
+ DevlinkFixedVersions = fw.psid
++
++# MaxLinear MxL86252 Ethernet switch
++[MDIO_BUS\VEN_maxlinear&DEV_mxl86252]
++Name = MxL86252 Ethernet Switch
++Vendor = MaxLinear
++[MDIO_BUS\VEN_maxlinear&DEV_mxl86252&COMPONENT_fw]
++Vendor = MaxLinear
++VendorId = DT:maxlinear
++Flags = omit-component-name,~usable-during-update,~dual-image
++
++# MaxLinear MxL86282 Ethernet switch
++[MDIO_BUS\VEN_maxlinear&DEV_mxl86282]
++Name = MxL86282 Ethernet Switch
++Vendor = MaxLinear
++[MDIO_BUS\VEN_maxlinear&DEV_mxl86282&COMPONENT_fw]
++Vendor = MaxLinear
++VendorId = DT:maxlinear
++Flags = omit-component-name,~usable-during-update,~dual-image
+--- a/plugins/devlink/fu-devlink-backend.c
++++ b/plugins/devlink/fu-devlink-backend.c
+@@ -40,7 +40,16 @@ fu_devlink_backend_create_pci_parent(FuD
+ }
+
+ /* construct PCI sysfs path from bus_name (e.g., "pci/0000:01:00.0") */
+- pci_sysfs_path = g_strdup_printf("/sys/bus/pci/devices/%s", dev_name);
++ pci_sysfs_path = fu_context_build_filename(ctx,
++ error,
++ FU_PATH_KIND_SYSFSDIR,
++ "bus",
++ "pci",
++ "devices",
++ dev_name,
++ NULL);
++ if (pci_sysfs_path == NULL)
++ return NULL;
+ pci_sysfs_real = fu_path_make_absolute(pci_sysfs_path, error);
+ if (pci_sysfs_real == NULL)
+ return NULL;
+@@ -90,7 +99,16 @@ fu_devlink_backend_create_mdio_parent(Fu
+ return NULL;
+ }
+
+- mdio_sysfs_path = g_strdup_printf("/sys/bus/mdio_bus/devices/%s", dev_name);
++ mdio_sysfs_path = fu_context_build_filename(ctx,
++ error,
++ FU_PATH_KIND_SYSFSDIR,
++ "bus",
++ "mdio_bus",
++ "devices",
++ dev_name,
++ NULL);
++ if (mdio_sysfs_path == NULL)
++ return NULL;
+ mdio_sysfs_real = fu_path_make_absolute(mdio_sysfs_path, error);
+ if (mdio_sysfs_real == NULL)
+ return NULL;
+@@ -118,13 +136,14 @@ fu_devlink_backend_create_mdio_parent(Fu
+ FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT,
+ NULL);
+ if (compatible_blob != NULL && g_bytes_get_size(compatible_blob) > 0) {
+- g_autofree gchar *compatible = g_strndup(g_bytes_get_data(compatible_blob, NULL),
+- g_bytes_get_size(compatible_blob));
+- g_auto(GStrv) parts = g_strsplit(compatible, ",", 2);
+- if (g_strv_length(parts) == 2) {
+- fu_device_build_vendor_id(mdio_device, "DT", parts[0]);
+- fu_device_add_instance_strsafe(mdio_device, "VEN", parts[0]);
+- fu_device_add_instance_strsafe(mdio_device, "DEV", parts[1]);
++ g_autofree gchar *compatible = fu_strsafe_bytes(compatible_blob, G_MAXSIZE);
++ if (compatible != NULL) {
++ g_auto(GStrv) parts = g_strsplit(compatible, ",", 2);
++ if (g_strv_length(parts) == 2) {
++ fu_device_build_vendor_id(mdio_device, "DT", parts[0]);
++ fu_device_add_instance_strsafe(mdio_device, "VEN", parts[0]);
++ fu_device_add_instance_strsafe(mdio_device, "DEV", parts[1]);
++ }
+ }
+ }
+
+@@ -161,7 +180,7 @@ fu_devlink_backend_device_added(FuDevlin
+ {
+ FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
+ FuDevice *old_devlink_device;
+- const gchar *instance_keys[] = {"VEN", "DEV", NULL};
++ const gchar *instance_keys[] = {"VEN", "DEV"};
+ g_autoptr(FuDevice) devlink_device = NULL;
+ g_autoptr(FuDevice) parent_device = NULL;
+
+@@ -229,7 +248,7 @@ fu_devlink_backend_device_added(FuDevlin
+ FU_DEVICE_INCORPORATE_FLAG_VENDOR |
+ FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS |
+ FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID);
+- for (guint i = 0; instance_keys[i] != NULL; i++) {
++ for (guint i = 0; i < G_N_ELEMENTS(instance_keys); i++) {
+ const gchar *value = fu_device_get_instance_str(parent_device, instance_keys[i]);
+ if (value != NULL)
+ fu_device_add_instance_str(devlink_device, instance_keys[i], value);
+--- a/plugins/devlink/fu-devlink-device.c
++++ b/plugins/devlink/fu-devlink-device.c
+@@ -868,16 +868,17 @@ fu_devlink_device_add_json(FuDevice *dev
+ if (self->dev_name != NULL)
+ fwupd_json_object_add_string(json_obj, "DevName", self->dev_name);
+
+- /* instance strings inherited from the parent device, which does not exist under
+- * emulation */
+- if (fu_device_get_instance_str(device, "VEN") != NULL)
++ /* instance strings inherited from the parent, which does not exist under emulation */
++ if (fu_device_get_instance_str(device, "VEN") != NULL) {
+ fwupd_json_object_add_string(json_obj,
+- "Ven",
++ "Vid",
+ fu_device_get_instance_str(device, "VEN"));
+- if (fu_device_get_instance_str(device, "DEV") != NULL)
++ }
++ if (fu_device_get_instance_str(device, "DEV") != NULL) {
+ fwupd_json_object_add_string(json_obj,
+- "Dev",
++ "Pid",
+ fu_device_get_instance_str(device, "DEV"));
++ }
+ }
+
+ static gboolean
+@@ -893,10 +894,14 @@ fu_devlink_device_from_json(FuDevice *de
+ /* devlink-specific properties */
+ bus_name = fwupd_json_object_get_string(json_obj, "BusName", NULL);
+ dev_name = fwupd_json_object_get_string(json_obj, "DevName", NULL);
+- ven = fwupd_json_object_get_string(json_obj, "Ven", NULL);
++ ven = fwupd_json_object_get_string(json_obj, "Vid", NULL);
++ if (ven == NULL)
++ ven = fwupd_json_object_get_string(json_obj, "Ven", NULL);
+ if (ven != NULL)
+ fu_device_add_instance_str(device, "VEN", ven);
+- dev = fwupd_json_object_get_string(json_obj, "Dev", NULL);
++ dev = fwupd_json_object_get_string(json_obj, "Pid", NULL);
++ if (dev == NULL)
++ dev = fwupd_json_object_get_string(json_obj, "Dev", NULL);
+ if (dev != NULL)
+ fu_device_add_instance_str(device, "DEV", dev);
+
+--- a/plugins/devlink/meson.build
++++ b/plugins/devlink/meson.build
+@@ -29,6 +29,7 @@ plugin_builtin_devlink = static_library(
+ plugin_builtins += plugin_builtin_devlink
+
+ device_tests += files(
++ 'tests/devlink-mxl86252.json',
+ 'tests/devlink-netdevsim.json',
+ )
+
+--- /dev/null
++++ b/plugins/devlink/tests/devlink-mxl86252.json
+@@ -0,0 +1,20 @@
++{
++ "name": "MaxLinear MxL86252 Ethernet Switch Update",
++ "interactive": false,
++ "steps": [
++ {
++ "url": "972ec047263bc0e3e06a9fa929c65923f1cae6ead59e0904d4fe7440c5e6847a-mxl862xxc-1085-signed-upgrade-dsa.cab",
++ "emulation-url": "c2973f267c338b9904238c9964f612260f789248d711247bda6485bf4750d2b4-mxl86252-emulation.zip",
++ "components": [
++ {
++ "name": "fw",
++ "protocol": "org.kernel.devlink",
++ "version": "1.0.85",
++ "guids": [
++ "d2a6f602-d988-5425-b686-5c1081576cce"
++ ]
++ }
++ ]
++ }
++ ]
++}