fwupd: backport upstream support for MxL862xx switches

Backport two patches from upstream implementing devlink-based firmware
update support for the MaxLinear MxL862xx DSA switch family, found eg.
on the BananaPi R4 Pro (8E/4E).

Users can update the switch firmware using `fwupdmgr update`.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
This commit is contained in:
Daniel Golle authored and Josef Schlehofer committed 2026-09-08 12:25:52 +02:00
1 parent eb67c64b51
commit 54aae64f11
3 files changed
+482 -1

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=fwupd
PKG_VERSION:=2.1.7
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/fwupd/fwupd/releases/download/$(PKG_VERSION)
@@ -0,0 +1,253 @@
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,
@@ -0,0 +1,228 @@
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"
+ ]
+ }
+ ]
+ }
+ ]
+}