diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index 46050c87c..c45d9f25a 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -415,6 +415,7 @@ done %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_ata.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_bcm57xx.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_ccgx.so +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_cfu.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_colorhug.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_cros_ec.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_cpu.so diff --git a/plugins/cfu/README.md b/plugins/cfu/README.md new file mode 100644 index 000000000..53446267b --- /dev/null +++ b/plugins/cfu/README.md @@ -0,0 +1,38 @@ +# Component Firmware update + +## Introduction + +CFU is a protocol from Microsoft to make it easy to install firmware on HID devices. + +This protocol is unique in that it requires has a pre-download phase before sending the firmware to +the microcontroller. This is so the device can check if the firmware is required and compatible. +CFU also requires devices to be able to transfer the entire new transfer mode in runtime mode. + +See for more +details. + +This plugin supports the following protocol ID: + +* com.microsoft.cfu + +## GUID Generation + +These devices use standard USB DeviceInstanceId values, as well as two extra for the component ID +and the bank, e.g. + +* `HIDRAW\VEN_17EF&DEV_7226&CID_01&BANK_1` +* `HIDRAW\VEN_17EF&DEV_7226&CID_01` +* `HIDRAW\VEN_17EF&DEV_7226` + +## Update Behavior + +The device has to support runtime updates and does not have a detach-into-bootloader mode -- but +after the install has completed the device still has to reboot into the new firmware. + +## Vendor ID Security + +The vendor ID is set from the USB vendor, in this instance set to `HIDRAW:0x17EF` + +## External Interface Access + +This plugin requires read/write access to `/dev/bus/usb`. diff --git a/plugins/cfu/cfu.quirk b/plugins/cfu/cfu.quirk new file mode 100644 index 000000000..445eac333 --- /dev/null +++ b/plugins/cfu/cfu.quirk @@ -0,0 +1,2 @@ +[USB\VID_273F&PID_100A] +Plugin = cfu diff --git a/plugins/cfu/fu-cfu-device.c b/plugins/cfu/fu-cfu-device.c new file mode 100644 index 000000000..e0cc9c971 --- /dev/null +++ b/plugins/cfu/fu-cfu-device.c @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-cfu-device.h" +#include "fu-cfu-module.h" + +struct _FuCfuDevice { + FuHidDevice parent_instance; + guint8 protocol_version; +}; + +G_DEFINE_TYPE(FuCfuDevice, fu_cfu_device, FU_TYPE_HID_DEVICE) + +#define FU_CFU_DEVICE_TIMEOUT 5000 /* ms */ +#define FU_CFU_FEATURE_SIZE 60 /* bytes */ + +#define FU_CFU_CMD_GET_FIRMWARE_VERSION 0x00 +#define FU_CFU_CMD_SEND_OFFER 0x00 // TODO + +static void +fu_cfu_device_to_string(FuDevice *device, guint idt, GString *str) +{ + FuCfuDevice *self = FU_CFU_DEVICE(device); + + /* FuUdevDevice->to_string */ + FU_DEVICE_CLASS(fu_cfu_device_parent_class)->to_string(device, idt, str); + + fu_common_string_append_kx(str, idt, "ProtocolVersion", self->protocol_version); +} + +static gboolean +fu_cfu_device_write_offer(FuCfuDevice *self, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + const guint8 *buf; + gsize bufsz = 0; + guint8 buf2[FU_CFU_FEATURE_SIZE] = {0}; + g_autofree guint8 *buf_tmp = NULL; + g_autoptr(GBytes) blob = NULL; + + /* generate a offer blob */ + if (flags & FWUPD_INSTALL_FLAG_FORCE) + fu_cfu_offer_set_force_ignore_version(FU_CFU_OFFER(firmware), TRUE); + blob = fu_firmware_write(firmware, error); + if (blob == NULL) + return FALSE; + + /* send it to the hardware */ + buf = g_bytes_get_data(blob, &bufsz); + buf_tmp = fu_memdup_safe(buf, bufsz, error); + if (buf_tmp == NULL) + return FALSE; + if (!fu_hid_device_set_report(FU_HID_DEVICE(self), + FU_CFU_CMD_SEND_OFFER, + buf_tmp, + bufsz, + FU_CFU_DEVICE_TIMEOUT, + FU_HID_DEVICE_FLAG_IS_FEATURE, + error)) { + g_prefix_error(error, "failed to send offer: "); + return FALSE; + } + if (!fu_hid_device_get_report(FU_HID_DEVICE(self), + FU_CFU_CMD_SEND_OFFER, + buf2, + sizeof(buf2), + FU_CFU_DEVICE_TIMEOUT, + FU_HID_DEVICE_FLAG_IS_FEATURE, + error)) { + return FALSE; + } + g_debug("status:%s reject:%s", + fu_cfu_device_offer_to_string(buf2[13]), + fu_cfu_device_reject_to_string(buf2[9])); + if (buf2[13] != FU_CFU_DEVICE_OFFER_ACCEPT) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "not supported: %s", + fu_cfu_device_offer_to_string(buf2[13])); + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_cfu_device_write_payload(FuCfuDevice *self, + FuFirmware *firmware, + FuProgress *progress, + GError **error) +{ + g_autoptr(GPtrArray) chunks = NULL; + + /* write each chunk */ + chunks = fu_firmware_get_chunks(firmware, error); + if (chunks == NULL) + return FALSE; + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + guint8 databuf[60] = {0}; + guint8 buf2[60] = {0}; + + /* flags */ + if (i == 0) + databuf[0] = FU_CFU_DEVICE_FLAG_FIRST_BLOCK; + else if (i == chunks->len - 1) + databuf[0] = FU_CFU_DEVICE_FLAG_LAST_BLOCK; + + /* length */ + databuf[1] = fu_chunk_get_data_sz(chk); + + /* sequence number */ + if (!fu_common_write_uint16_safe(databuf, + sizeof(databuf), + 0x2, + i + 1, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + /* address */ + if (!fu_common_write_uint32_safe(databuf, + sizeof(databuf), + 0x4, + fu_chunk_get_address(chk), + G_LITTLE_ENDIAN, + error)) + return FALSE; + + /* data */ + if (!fu_memcpy_safe(databuf, + sizeof(databuf), + 0x8, /* dst */ + fu_chunk_get_data(chk), + fu_chunk_get_data_sz(chk), + 0x0, /* src */ + fu_chunk_get_data_sz(chk), + error)) { + g_prefix_error(error, "memory copy for payload fail: "); + return FALSE; + } + // send + // revc + if (buf2[5] != FU_CFU_DEVICE_STATUS_SUCCESS) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "failed to send chunk %u: %s", + i + 1, + fu_cfu_device_status_to_string(buf2[5])); + return FALSE; + } + + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +static gboolean +fu_cfu_device_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuCfuDevice *self = FU_CFU_DEVICE(device); + g_autoptr(FuFirmware) fw_offer = NULL; + g_autoptr(FuFirmware) fw_payload = NULL; + + /* 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_BUSY, 2); /* offer */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98); /* payload */ + + /* send offer */ + fw_offer = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_HEADER, error); + if (fw_offer == NULL) + return FALSE; + if (!fu_cfu_device_write_offer(self, + fw_offer, + fu_progress_get_child(progress), + flags, + error)) + return FALSE; + fu_progress_step_done(progress); + + /* send payload */ + fw_payload = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_PAYLOAD, error); + if (fw_payload == NULL) + return FALSE; + if (!fu_cfu_device_write_payload(self, fw_payload, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + /* success */ + return TRUE; +} + +static gboolean +fu_cfu_device_setup(FuDevice *device, GError **error) +{ + FuCfuDevice *self = FU_CFU_DEVICE(device); + guint8 buf[FU_CFU_FEATURE_SIZE] = {0}; + guint8 component_cnt = 0; + guint8 tmp = 0; + gsize offset = 0; + g_autoptr(GHashTable) modules_by_cid = NULL; + + /* FuHidDevice->setup */ + if (!FU_DEVICE_CLASS(fu_cfu_device_parent_class)->setup(device, error)) + return FALSE; + + /* get version */ + if (!fu_hid_device_get_report(FU_HID_DEVICE(device), + FU_CFU_CMD_GET_FIRMWARE_VERSION, + buf, + sizeof(buf), + FU_CFU_DEVICE_TIMEOUT, + FU_HID_DEVICE_FLAG_IS_FEATURE, + error)) { + return FALSE; + } + if (!fu_common_read_uint8_safe(buf, sizeof(buf), 0x0, &component_cnt, error)) + return FALSE; + if (!fu_common_read_uint8_safe(buf, sizeof(buf), 0x3, &tmp, error)) + return FALSE; + self->protocol_version = tmp & 0b1111; + + /* keep track of all modules so we can work out which are dual bank */ + modules_by_cid = g_hash_table_new(g_int_hash, g_int_equal); + + /* read each component module version */ + offset += 4; + for (guint i = 0; i < component_cnt; i++) { + g_autoptr(FuCfuModule) module = fu_cfu_module_new(device); + FuCfuModule *module_tmp; + + if (!fu_cfu_module_setup(module, buf, sizeof(buf), offset, error)) + return FALSE; + fu_device_add_child(device, FU_DEVICE(module)); + + /* same module already exists, so mark both as being dual bank */ + module_tmp = + g_hash_table_lookup(modules_by_cid, + GINT_TO_POINTER(fu_cfu_module_get_component_id(module))); + if (module_tmp != NULL) { + fu_device_add_flag(FU_DEVICE(module), FWUPD_DEVICE_FLAG_DUAL_IMAGE); + fu_device_add_flag(FU_DEVICE(module_tmp), FWUPD_DEVICE_FLAG_DUAL_IMAGE); + } else { + g_hash_table_insert(modules_by_cid, + GINT_TO_POINTER(fu_cfu_module_get_component_id(module)), + module); + } + + /* done */ + offset += 0x8; + } + + /* success */ + return TRUE; +} + +static void +fu_cfu_device_init(FuCfuDevice *self) +{ + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); +} + +static void +fu_cfu_device_class_init(FuCfuDeviceClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + klass_device->setup = fu_cfu_device_setup; + klass_device->to_string = fu_cfu_device_to_string; + klass_device->write_firmware = fu_cfu_device_write_firmware; +} diff --git a/plugins/cfu/fu-cfu-device.h b/plugins/cfu/fu-cfu-device.h new file mode 100644 index 000000000..08907aa42 --- /dev/null +++ b/plugins/cfu/fu-cfu-device.h @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_CFU_DEVICE (fu_cfu_device_get_type()) +G_DECLARE_FINAL_TYPE(FuCfuDevice, fu_cfu_device, FU, CFU_DEVICE, FuHidDevice) diff --git a/plugins/cfu/fu-cfu-module.c b/plugins/cfu/fu-cfu-module.c new file mode 100644 index 000000000..386dc6338 --- /dev/null +++ b/plugins/cfu/fu-cfu-module.c @@ -0,0 +1,200 @@ +/*# + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-cfu-module.h" + +struct _FuCfuModule { + FuDevice parent_instance; + guint8 component_id; + guint8 bank; +}; + +G_DEFINE_TYPE(FuCfuModule, fu_cfu_module, FU_TYPE_DEVICE) + +static void +fu_cfu_module_to_string(FuDevice *device, guint idt, GString *str) +{ + FuCfuModule *self = FU_CFU_MODULE(device); + fu_common_string_append_kx(str, idt, "ComponentId", self->component_id); + fu_common_string_append_kx(str, idt, "Bank", self->bank); +} + +guint8 +fu_cfu_module_get_component_id(FuCfuModule *self) +{ + return self->component_id; +} + +guint8 +fu_cfu_module_get_bank(FuCfuModule *self) +{ + return self->bank; +} + +gboolean +fu_cfu_module_setup(FuCfuModule *self, const guint8 *buf, gsize bufsz, gsize offset, GError **error) +{ + FuDevice *parent = fu_device_get_proxy(FU_DEVICE(self)); + guint32 version_raw = 0; + guint8 tmp = 0; + g_autofree gchar *instance_id0 = NULL; + g_autofree gchar *instance_id1 = NULL; + g_autofree gchar *instance_id2 = NULL; + g_autofree gchar *logical_id = NULL; + g_autofree gchar *version = NULL; + + /* component ID */ + if (!fu_common_read_uint8_safe(buf, bufsz, offset + 0x5, &self->component_id, error)) + return FALSE; + + /* these GUIDs may cause the name or version-format to be overwritten */ + instance_id0 = g_strdup_printf("HIDRAW\\VEN_%04X&DEV_%04X", + fu_udev_device_get_vendor(FU_UDEV_DEVICE(parent)), + fu_udev_device_get_model(FU_UDEV_DEVICE(parent))); + fu_device_add_instance_id(FU_DEVICE(self), instance_id0); + instance_id1 = g_strdup_printf("HIDRAW\\VEN_%04X&DEV_%04X&CID_%02X", + fu_udev_device_get_vendor(FU_UDEV_DEVICE(parent)), + fu_udev_device_get_model(FU_UDEV_DEVICE(parent)), + self->component_id); + fu_device_add_instance_id(FU_DEVICE(self), instance_id1); + + /* bank */ + if (!fu_common_read_uint8_safe(buf, bufsz, offset + 0x4, &tmp, error)) + return FALSE; + self->bank = tmp & 0b11; + instance_id2 = g_strdup_printf("HIDRAW\\VEN_%04X&DEV_%04X&CID_%02X&BANK_%01X", + fu_udev_device_get_vendor(FU_UDEV_DEVICE(parent)), + fu_udev_device_get_model(FU_UDEV_DEVICE(parent)), + self->component_id, + self->bank); + fu_device_add_instance_id(FU_DEVICE(self), instance_id2); + + /* set name, if not already set using a quirk */ + if (fu_device_get_name(FU_DEVICE(self)) == NULL) { + g_autofree gchar *name = NULL; + name = g_strdup_printf("%s (0x%02X:0x%02x)", + fu_device_get_name(parent), + self->component_id, + self->bank); + fu_device_set_name(FU_DEVICE(self), name); + } + + /* version */ + if (!fu_common_read_uint32_safe(buf, bufsz, offset, &version_raw, G_LITTLE_ENDIAN, error)) + return FALSE; + fu_device_set_version_raw(FU_DEVICE(self), version_raw); + version = fu_common_version_from_uint32(version_raw, + fu_device_get_version_format(FU_DEVICE(self))); + + /* logical ID */ + logical_id = g_strdup_printf("CID:0x%02x,BANK:0x%02x", self->component_id, self->bank); + fu_device_set_logical_id(FU_DEVICE(self), logical_id); + + /* success */ + return TRUE; +} + +static FuFirmware * +fu_cfu_module_prepare_firmware(FuDevice *device, + GBytes *fw, + FwupdInstallFlags flags, + GError **error) +{ + g_autoptr(FuFirmware) firmware = fu_firmware_new(); + g_autoptr(FuFirmware) offer = fu_cfu_offer_new(); + g_autoptr(FuFirmware) payload = fu_cfu_payload_new(); + g_autoptr(GBytes) fw_offset = NULL; + + /* offer */ + if (!fu_firmware_parse(offer, fw, flags, error)) + return NULL; + fu_firmware_set_id(offer, FU_FIRMWARE_ID_HEADER); + fu_firmware_add_image(firmware, offer); + + /* payload */ + fw_offset = fu_common_bytes_new_offset(fw, 0x10, g_bytes_get_size(fw) - 0x10, error); + if (fw_offset == NULL) + return NULL; + if (!fu_firmware_parse(payload, fw_offset, flags, error)) + return NULL; + fu_firmware_set_id(payload, FU_FIRMWARE_ID_PAYLOAD); + fu_firmware_add_image(firmware, payload); + + /* success */ + return g_steal_pointer(&firmware); +} + +static gboolean +fu_cfu_module_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuDevice *proxy; + g_autoptr(GBytes) fw = NULL; + + /* get the whole image */ + fw = fu_firmware_get_bytes(firmware, error); + if (fw == NULL) + return FALSE; + + /* process by the parent */ + proxy = fu_device_get_proxy(device); + if (proxy == NULL) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "no proxy device assigned"); + return FALSE; + } + return fu_device_write_firmware(proxy, fw, progress, flags, error); +} + +static void +fu_cfu_module_set_progress(FuDevice *self, FuProgress *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_RESTART, 2); /* detach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96); /* write */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2); /* attach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2); /* reload */ +} + +static void +fu_cfu_module_init(FuCfuModule *self) +{ + fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.cfu"); + fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); +} + +static void +fu_cfu_module_class_init(FuCfuModuleClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + klass_device->to_string = fu_cfu_module_to_string; + klass_device->prepare_firmware = fu_cfu_module_prepare_firmware; + klass_device->write_firmware = fu_cfu_module_write_firmware; + klass_device->set_progress = fu_cfu_module_set_progress; +} + +FuCfuModule * +fu_cfu_module_new(FuDevice *parent) +{ + FuCfuModule *self; + self = g_object_new(FU_TYPE_CFU_MODULE, + "ctx", + fu_device_get_context(parent), + "proxy", + parent, + NULL); + return self; +} diff --git a/plugins/cfu/fu-cfu-module.h b/plugins/cfu/fu-cfu-module.h new file mode 100644 index 000000000..b6f553985 --- /dev/null +++ b/plugins/cfu/fu-cfu-module.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_CFU_MODULE (fu_cfu_module_get_type()) +G_DECLARE_FINAL_TYPE(FuCfuModule, fu_cfu_module, FU, CFU_MODULE, FuDevice) + +guint8 +fu_cfu_module_get_component_id(FuCfuModule *self); +guint8 +fu_cfu_module_get_bank(FuCfuModule *self); +gboolean +fu_cfu_module_setup(FuCfuModule *self, + const guint8 *buf, + gsize bufsz, + gsize offset, + GError **error); +FuCfuModule * +fu_cfu_module_new(FuDevice *parent); diff --git a/plugins/cfu/fu-plugin-cfu.c b/plugins/cfu/fu-plugin-cfu.c new file mode 100644 index 000000000..f86376df4 --- /dev/null +++ b/plugins/cfu/fu-plugin-cfu.c @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2021 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-cfu-device.h" + +void +fu_plugin_init(FuPlugin *plugin) +{ + fu_plugin_set_build_hash(plugin, FU_BUILD_HASH); + fu_plugin_add_device_gtype(plugin, FU_TYPE_CFU_DEVICE); +} diff --git a/plugins/cfu/meson.build b/plugins/cfu/meson.build new file mode 100644 index 000000000..abe0a416d --- /dev/null +++ b/plugins/cfu/meson.build @@ -0,0 +1,31 @@ +cargs = ['-DG_LOG_DOMAIN="FuPluginCfu"'] + +install_data([ + 'cfu.quirk', + ], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_cfu', + fu_hash, + sources : [ + 'fu-cfu-device.c', + 'fu-cfu-module.c', + 'fu-plugin-cfu.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, + ], +) diff --git a/plugins/meson.build b/plugins/meson.build index b954cc175..8516feb98 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -8,6 +8,7 @@ subdir('ata') subdir('bcm57xx') subdir('bios') subdir('ccgx') +subdir('cfu') subdir('colorhug') subdir('cpu') subdir('cros-ec')