From 36d7077f65e436c54be93bbc8d2923f53be5d655 Mon Sep 17 00:00:00 2001 From: Richard Hughes Date: Thu, 4 Nov 2021 14:48:17 +0000 Subject: [PATCH] Add a new plugin to handle MTD devices --- contrib/ci/build_windows.sh | 1 + contrib/ci/debian_s390x.sh | 1 + contrib/fwupd.spec.in | 1 + libfwupdplugin/fu-udev-device.c | 2 +- meson.build | 3 + meson_options.txt | 1 + plugins/meson.build | 1 + plugins/mtd/README.md | 30 ++++ plugins/mtd/fu-mtd-device.c | 273 ++++++++++++++++++++++++++++++++ plugins/mtd/fu-mtd-device.h | 12 ++ plugins/mtd/fu-plugin-mtd.c | 54 +++++++ plugins/mtd/meson.build | 32 ++++ plugins/mtd/mtd.quirk | 2 + 13 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 plugins/mtd/README.md create mode 100644 plugins/mtd/fu-mtd-device.c create mode 100644 plugins/mtd/fu-mtd-device.h create mode 100644 plugins/mtd/fu-plugin-mtd.c create mode 100644 plugins/mtd/meson.build create mode 100644 plugins/mtd/mtd.quirk diff --git a/contrib/ci/build_windows.sh b/contrib/ci/build_windows.sh index 1ea32dff8..1fa7338f2 100755 --- a/contrib/ci/build_windows.sh +++ b/contrib/ci/build_windows.sh @@ -38,6 +38,7 @@ meson .. \ -Dsystemd=false \ -Dplugin_emmc=false \ -Dplugin_amt=false \ + -Dplugin_mtd=false \ -Dintrospection=false \ -Dplugin_thunderbolt=false \ -Dplugin_synaptics_mst=false \ diff --git a/contrib/ci/debian_s390x.sh b/contrib/ci/debian_s390x.sh index f2a3cf49f..fa75ca1a8 100755 --- a/contrib/ci/debian_s390x.sh +++ b/contrib/ci/debian_s390x.sh @@ -22,6 +22,7 @@ meson .. \ -Dplugin_dell=false \ -Dplugin_modem_manager=false \ -Dplugin_msr=false \ + -Dplugin_mtd=false \ -Dplugin_redfish=false \ -Dintrospection=false \ -Ddocs=none \ diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index 1982901ad..8adf08fec 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -450,6 +450,7 @@ done %if 0%{?have_msr} %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_msr.so %endif +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_mtd.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nitrokey.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_nvme.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_optionrom.so diff --git a/libfwupdplugin/fu-udev-device.c b/libfwupdplugin/fu-udev-device.c index 122b76c65..1634b10d0 100644 --- a/libfwupdplugin/fu-udev-device.c +++ b/libfwupdplugin/fu-udev-device.c @@ -1104,7 +1104,7 @@ fu_udev_device_set_physical_id(FuUdevDevice *self, const gchar *subsystems, GErr physical_id = g_strdup_printf("PCI_SLOT_NAME=%s", tmp); } else if (g_strcmp0(subsystem, "usb") == 0 || g_strcmp0(subsystem, "mmc") == 0 || g_strcmp0(subsystem, "i2c") == 0 || g_strcmp0(subsystem, "platform") == 0 || - g_strcmp0(subsystem, "scsi") == 0) { + g_strcmp0(subsystem, "scsi") == 0 || g_strcmp0(subsystem, "mtd") == 0) { tmp = g_udev_device_get_property(udev_device, "DEVPATH"); if (tmp == NULL) { g_set_error_literal(error, diff --git a/meson.build b/meson.build index 59f8caa13..c36531881 100644 --- a/meson.build +++ b/meson.build @@ -314,6 +314,9 @@ endif if cc.has_header('linux/ethtool.h') conf.set('HAVE_ETHTOOL_H', '1') endif +if cc.has_header('mtd/mtd-user.h') + conf.set('HAVE_MTD_USER_H', '1') +endif if cc.has_header('linux/hidraw.h') conf.set('HAVE_HIDRAW_H', '1') endif diff --git a/meson_options.txt b/meson_options.txt index 7ccc9f14e..42e48c6ea 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -32,6 +32,7 @@ option('plugin_uefi_pk', type : 'boolean', value : true, description : 'enable U option('plugin_nvme', type : 'boolean', value : true, description : 'enable NVMe support') option('plugin_modem_manager', type : 'boolean', value : false, description : 'enable ModemManager support') option('plugin_msr', type : 'boolean', value : true, description : 'enable MSR support') +option('plugin_mtd', type : 'boolean', value : true, description : 'enable MTD support') option('plugin_flashrom', type : 'boolean', value : false, description : 'enable libflashrom support') option('plugin_platform_integrity', type : 'boolean', value : false, description : 'enable platform integrity support') option('plugin_intel_spi', type : 'boolean', value : false, description : 'enable Intel SPI support') diff --git a/plugins/meson.build b/plugins/meson.build index be09b6c84..e551d8bea 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -40,6 +40,7 @@ subdir('logitech-hidpp') subdir('logitech-bulkcontroller') subdir('modem-manager') subdir('msr') +subdir('mtd') subdir('nitrokey') subdir('nvme') subdir('optionrom') diff --git a/plugins/mtd/README.md b/plugins/mtd/README.md new file mode 100644 index 000000000..c2957ed3b --- /dev/null +++ b/plugins/mtd/README.md @@ -0,0 +1,30 @@ +# MTD + +## Introduction + +The Memory Technology Device (MTD) interface is a way of abstracting flash devices as if they were +normal block devices. + +See for more details. + +This plugin supports the following protocol ID: + +* org.infradead.mtd + +## GUID Generation + +These devices use custom DeviceInstanceId values built from the device `NAME`, e.g. + +* `MTD\NAME_Factory` + +## Update Behavior + +The MTD device is erased in chunks, written and then read back to verify. + +## Vendor ID Security + +The vendor ID is set from the system vendor, for example `DMI:LENOVO` + +## External Interface Access + +This plugin requires read/write access to `/dev/mtd`. diff --git a/plugins/mtd/fu-mtd-device.c b/plugins/mtd/fu-mtd-device.c new file mode 100644 index 000000000..e771bca0d --- /dev/null +++ b/plugins/mtd/fu-mtd-device.c @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#ifdef HAVE_MTD_USER_H +#include +#endif + +#include "fu-mtd-device.h" + +struct _FuMtdDevice { + FuUdevDevice parent_instance; + guint64 erasesize; +}; + +G_DEFINE_TYPE(FuMtdDevice, fu_mtd_device, FU_TYPE_UDEV_DEVICE) + +static void +fu_mtd_device_to_string(FuDevice *device, guint idt, GString *str) +{ + FuMtdDevice *self = FU_MTD_DEVICE(device); + if (self->erasesize > 0) + fu_common_string_append_kx(str, idt, "EraseSize", self->erasesize); +} + +static gboolean +fu_mtd_device_probe(FuDevice *device, GError **error) +{ + FuMtdDevice *self = FU_MTD_DEVICE(device); + const gchar *name; + guint64 flags = 0; + guint64 size = 0; + + /* FuUdevDevice->probe */ + if (!FU_DEVICE_CLASS(fu_mtd_device_parent_class)->probe(device, error)) + return FALSE; + + /* set physical ID */ + if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "mtd", error)) + return FALSE; + + /* get name */ + name = fu_udev_device_get_sysfs_attr(FU_UDEV_DEVICE(device), "name", NULL); + if (name != NULL) { + g_autofree gchar *devid = NULL; + g_autofree gchar *name_safe = g_strdup(name); + g_strdelimit(name_safe, " /\\\"", '-'); + devid = g_strdup_printf("MTD\\NAME_%s", name_safe); + fu_device_add_instance_id(FU_DEVICE(self), devid); + fu_device_set_name(FU_DEVICE(self), name); + } + + /* get properties about the device */ + if (!fu_udev_device_get_sysfs_attr_uint64(FU_UDEV_DEVICE(device), "size", &size, error)) + return FALSE; + fu_device_set_firmware_size_max(device, size); + if (!fu_udev_device_get_sysfs_attr_uint64(FU_UDEV_DEVICE(device), "flags", &flags, error)) + return FALSE; +#ifdef HAVE_MTD_USER_H + if ((flags & MTD_NO_ERASE) == 0) { + if (!fu_udev_device_get_sysfs_attr_uint64(FU_UDEV_DEVICE(device), + "erasesize", + &self->erasesize, + error)) + return FALSE; + } + if (flags & MTD_WRITEABLE) + fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); +#endif + + /* success */ + return TRUE; +} + +static gboolean +fu_mtd_device_erase(FuMtdDevice *self, GBytes *fw, FuProgress *progress, GError **error) +{ +#ifdef HAVE_MTD_USER_H + g_autoptr(GPtrArray) chunks = fu_chunk_array_new_from_bytes(fw, 0x0, 0x0, self->erasesize); + + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + + /* erase each chunk */ + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + struct erase_info_user erase = { + .start = fu_chunk_get_address(chk), + .length = fu_chunk_get_data_sz(chk), + }; + if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self), 2, (guint8 *)&erase, NULL, error)) { + g_prefix_error(error, "failed to erase @0x%x: ", (guint)erase.start); + return FALSE; + } + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +#else + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "Not supported as mtd-user.h is unavailable"); + return FALSE; +#endif +} + +static gboolean +fu_mtd_device_write(FuMtdDevice *self, GPtrArray *chunks, FuProgress *progress, GError **error) +{ + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + + /* rewind */ + if (!fu_udev_device_seek(FU_UDEV_DEVICE(self), 0x0, error)) { + g_prefix_error(error, "failed to rewind: "); + return FALSE; + } + + /* write each chunk */ + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + if (!fu_udev_device_pwrite_full(FU_UDEV_DEVICE(self), + 0x0, + fu_chunk_get_data(chk), + fu_chunk_get_data_sz(chk), + error)) { + g_prefix_error(error, + "failed to write @0x%x: ", + (guint)fu_chunk_get_address(chk)); + return FALSE; + } + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_mtd_device_verify(FuMtdDevice *self, GPtrArray *chunks, FuProgress *progress, GError **error) +{ + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + + /* verify each chunk */ + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + g_autofree guint8 *buf = g_malloc0(fu_chunk_get_data_sz(chk)); + g_autoptr(GBytes) blob1 = fu_chunk_get_bytes(chk); + g_autoptr(GBytes) blob2 = NULL; + + if (!fu_udev_device_pread_full(FU_UDEV_DEVICE(self), + 0x0, + buf, + fu_chunk_get_data_sz(chk), + error)) { + g_prefix_error(error, + "failed to read @0x%x: ", + (guint)fu_chunk_get_address(chk)); + return FALSE; + } + blob2 = g_bytes_new_static(buf, fu_chunk_get_data_sz(chk)); + if (!fu_common_bytes_compare(blob1, blob2, error)) { + g_prefix_error(error, + "failed to verify @0x%x: ", + (guint)fu_chunk_get_address(chk)); + return FALSE; + } + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_mtd_device_write_verify(FuMtdDevice *self, GBytes *fw, FuProgress *progress, GError **error) +{ + g_autoptr(GPtrArray) chunks = fu_chunk_array_new_from_bytes(fw, 0x0, 0x0, 10 * 1024); + + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 50); + + /* write */ + if (!fu_mtd_device_write(self, chunks, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + /* verify */ + if (!fu_mtd_device_verify(self, chunks, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + /* success */ + return TRUE; +} + +static gboolean +fu_mtd_device_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuMtdDevice *self = FU_MTD_DEVICE(device); + g_autoptr(GBytes) fw = NULL; + + /* get data to write */ + fw = fu_firmware_get_bytes(firmware, error); + if (fw == NULL) + return FALSE; + if (g_bytes_get_size(fw) > fu_device_get_firmware_size_max(device)) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "firmware too large, got 0x%x, expected <= 0x%x", + (guint)g_bytes_get_size(fw), + (guint)fu_device_get_firmware_size_max(device)); + return FALSE; + } + + /* just one step required */ + if (self->erasesize == 0) + return fu_mtd_device_write_verify(self, fw, progress, error); + + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 50); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50); + + /* erase */ + if (!fu_mtd_device_erase(self, fw, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + /* write */ + if (!fu_mtd_device_write_verify(self, fw, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + /* success */ + return TRUE; +} + +static void +fu_mtd_device_init(FuMtdDevice *self) +{ + fu_udev_device_set_flags(FU_UDEV_DEVICE(self), + FU_UDEV_DEVICE_FLAG_OPEN_READ | FU_UDEV_DEVICE_FLAG_OPEN_WRITE | + FU_UDEV_DEVICE_FLAG_OPEN_SYNC); +} + +static void +fu_mtd_device_class_init(FuMtdDeviceClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + klass_device->probe = fu_mtd_device_probe; + klass_device->to_string = fu_mtd_device_to_string; + klass_device->write_firmware = fu_mtd_device_write_firmware; +} diff --git a/plugins/mtd/fu-mtd-device.h b/plugins/mtd/fu-mtd-device.h new file mode 100644 index 000000000..5fd90e444 --- /dev/null +++ b/plugins/mtd/fu-mtd-device.h @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_MTD_DEVICE (fu_mtd_device_get_type()) +G_DECLARE_FINAL_TYPE(FuMtdDevice, fu_mtd_device, FU, MTD_DEVICE, FuUdevDevice) diff --git a/plugins/mtd/fu-plugin-mtd.c b/plugins/mtd/fu-plugin-mtd.c new file mode 100644 index 000000000..33e477ca9 --- /dev/null +++ b/plugins/mtd/fu-plugin-mtd.c @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-mtd-device.h" + +void +fu_plugin_init(FuPlugin *plugin) +{ + fu_plugin_add_udev_subsystem(plugin, "mtd"); + fu_plugin_set_build_hash(plugin, FU_BUILD_HASH); + fu_plugin_add_device_gtype(plugin, FU_TYPE_MTD_DEVICE); +} + +gboolean +fu_plugin_startup(FuPlugin *plugin, GError **error) +{ +#ifndef HAVE_MTD_USER_H + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "Not compiled with mtd support"); + return FALSE; +#endif + return TRUE; +} + +gboolean +fu_plugin_device_created(FuPlugin *plugin, FuDevice *dev, GError **error) +{ + FuContext *ctx = fu_plugin_get_context(plugin); + const gchar *vendor; + + fu_device_set_summary(dev, "Memory Technology Device"); + fu_device_add_protocol(dev, "org.infradead.mtd"); + fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_INTERNAL); + fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); + fu_device_add_icon(dev, "drive-harddisk-solidstate"); + + /* set vendor ID as the BIOS vendor */ + vendor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_MANUFACTURER); + if (vendor != NULL) { + g_autofree gchar *vendor_id = g_strdup_printf("DMI:%s", vendor); + fu_device_add_vendor_id(dev, vendor_id); + } + + return TRUE; +} diff --git a/plugins/mtd/meson.build b/plugins/mtd/meson.build new file mode 100644 index 000000000..8903b203a --- /dev/null +++ b/plugins/mtd/meson.build @@ -0,0 +1,32 @@ +if get_option('plugin_mtd') +cargs = ['-DG_LOG_DOMAIN="FuPluginMtd"'] + +install_data([ + 'mtd.quirk', + ], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_mtd', + fu_hash, + sources : [ + 'fu-plugin-mtd.c', + 'fu-mtd-device.c', + ], + include_directories : [ + root_incdir, + fwupd_incdir, + fwupdplugin_incdir, + ], + install : true, + install_dir: plugin_dir, + link_with : [ + fwupd, + fwupdplugin, + ], + c_args : cargs, + dependencies : [ + plugin_deps, + ], +) +endif diff --git a/plugins/mtd/mtd.quirk b/plugins/mtd/mtd.quirk new file mode 100644 index 000000000..3ebf6793d --- /dev/null +++ b/plugins/mtd/mtd.quirk @@ -0,0 +1,2 @@ +[MTD] +Plugin = mtd