diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index e7a7fe032..1e11b68a6 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -432,6 +432,7 @@ done %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_acpi_phat.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_amt.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_analogix.so +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_android_boot.so %{_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_cfu.so diff --git a/meson_options.txt b/meson_options.txt index 6cf92e72e..cfa821d87 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -18,6 +18,7 @@ option('lzma', type: 'feature', description : 'LZMA support', deprecated: {'true option('cbor', type: 'feature', description : 'CBOR support for coSWID and uSWID') option('plugin_amt', type : 'feature', description : 'Intel AMT support', deprecated: {'true': 'enabled', 'false': 'disabled'}) option('plugin_acpi_phat', type : 'feature', description : 'ACPI PHAT support', deprecated: {'true': 'enabled', 'false': 'disabled'}) +option('plugin_android_boot', type : 'feature', description : 'Android Boot support') option('plugin_bcm57xx', type : 'feature', description : 'BCM57xx support', deprecated: {'true': 'enabled', 'false': 'disabled'}) option('plugin_cfu', type : 'feature', description : 'CFU support', deprecated: {'true': 'enabled', 'false': 'disabled'}) option('plugin_cpu', type : 'feature', description : 'CPU support', deprecated: {'true': 'enabled', 'false': 'disabled'}) diff --git a/plugins/android-boot/README.md b/plugins/android-boot/README.md new file mode 100644 index 000000000..12c8262d2 --- /dev/null +++ b/plugins/android-boot/README.md @@ -0,0 +1,53 @@ +# Android Bootloaders + +## Introduction + +This plugin is used to update hardware that use partitions to store their firmware on. + +## Firmware Format + +The daemon will decompress the cabinet archive and extract a firmware blob as a Raw Disk File +in the IMG format. The firmware blob will be flashed to the partition. Fastboot devices are similar +but are flashed in fastboot mode using an external device. This plugin is similar but can be used +to flash from the device itself rather than external device. + +This plugin supports the following protocol ID: + +* com.google.android_boot + +## GUID Generation + +The GUID is generated by combining the partition UUID of the block device, its label and optionally boot slot +when using an Android A/B partitioning scheme, e.g. + +* `DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1&LABEL_label&SLOT_a` +* `DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1&LABEL_label` +* `DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1` + +## Update Behavior + +The block device is erased in chunks, written and then read back to verify. + +## Quirk Use + +This plugin uses the following plugin-specific quirk: + +### AndroidBootVersionProperty + +Property to parse from `/proc/cmdline` to retrieve the bootloader version. + +Since: 1.8.5 + +### AndroidBootPartitionMaxSize + +Maximum size the firmware may use of a partition. + +Since: 1.8.5 + +## Vendor ID Security + +The vendor ID is set through the `android-boot.quirk` file. + +## External Interface Access + +This plugin requires read/write access to `/dev/block`. diff --git a/plugins/android-boot/android-boot.quirk b/plugins/android-boot/android-boot.quirk new file mode 100644 index 000000000..09d78e7b6 --- /dev/null +++ b/plugins/android-boot/android-boot.quirk @@ -0,0 +1,16 @@ +[BLOCK] +Plugin = android_boot + +# SHIFT6mq ABL A +[DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1&LABEL_abl-a] +Flags = updatable,signed-payload +Vendor = SHIFT GmbH +VendorId = eco.shift +AndroidBootVersionProperty = androidboot.abl.revision + +# SHIFT6mq ABL B +[DRIVE\UUID_3d7b21e8-048b-db0b-0c18-d07a9bb32f2d&LABEL_abl-b] +Flags = updatable,signed-payload +Vendor = SHIFT GmbH +VendorId = eco.shift +AndroidBootVersionProperty = androidboot.abl.revision diff --git a/plugins/android-boot/fu-android-boot-device.c b/plugins/android-boot/fu-android-boot-device.c new file mode 100644 index 000000000..ceb97a549 --- /dev/null +++ b/plugins/android-boot/fu-android-boot-device.c @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2022 Dylan Van Assche + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include +#include + +#include "fu-android-boot-device.h" + +#define ANDROID_BOOT_UNKNOWN_VERSION "0.0.0" +#define ANDROID_BOOT_SECTOR_SIZE 512 + +struct _FuAndroidBootDevice { + FuUdevDevice parent_instance; + const gchar *label; + const gchar *uuid; + gchar *boot_slot; + guint64 max_size; +}; + +G_DEFINE_TYPE(FuAndroidBootDevice, fu_android_boot_device, FU_TYPE_UDEV_DEVICE) + +static void +fu_android_boot_device_to_string(FuDevice *device, guint idt, GString *str) +{ + FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); + + fu_string_append(str, idt, "BootSlot", self->boot_slot); + fu_string_append(str, idt, "Label", self->label); + fu_string_append(str, idt, "UUID", self->uuid); + fu_string_append_kx(str, idt, "MaxSize", self->max_size); +} + +static gboolean +fu_android_boot_device_probe(FuDevice *device, GError **error) +{ + FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); + GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(device)); + g_autofree gchar *serial = NULL; + guint64 sectors = 0; + guint64 size = 0; + g_autoptr(GHashTable) cmdline = NULL; + + /* FuUdevDevice->probe */ + if (!FU_DEVICE_CLASS(fu_android_boot_device_parent_class)->probe(device, error)) + return FALSE; + + /* get kernel cmdline */ + cmdline = fu_kernel_get_cmdline(error); + if (cmdline == NULL) + return FALSE; + + /* set the physical ID */ + if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "block", error)) + return FALSE; + + /* extract boot slot if available */ + self->boot_slot = g_strdup(g_hash_table_lookup(cmdline, "androidboot.slot_suffix")); + + /* extract label and check if it matches boot slot*/ + if (g_udev_device_has_property(udev_device, "ID_PART_ENTRY_NAME")) { + self->label = g_udev_device_get_property(udev_device, "ID_PART_ENTRY_NAME"); + + /* Use label as device name */ + fu_device_set_name(device, self->label); + + /* If the device has A/B partitioning, compare boot slot to only expose partitions + * in-use */ + if (self->boot_slot != NULL && !g_str_has_suffix(self->label, self->boot_slot)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "device is on a different bootslot"); + return FALSE; + } + } + + /* set max firmware size, required to avoid writing firmware bigger than partition */ + if (!g_udev_device_has_property(udev_device, "ID_PART_ENTRY_SIZE")) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "device does not expose its size"); + return FALSE; + } + + sectors = g_udev_device_get_property_as_uint64(udev_device, "ID_PART_ENTRY_SIZE"); + size = sectors * ANDROID_BOOT_SECTOR_SIZE; + self->max_size = size; + + /* extract partition UUID and require it for supporting a device */ + if (!g_udev_device_has_property(udev_device, "ID_PART_ENTRY_UUID")) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "device does not have a UUID"); + return FALSE; + } + self->uuid = g_udev_device_get_property(udev_device, "ID_PART_ENTRY_UUID"); + + /* extract serial number and set it */ + serial = g_hash_table_lookup(cmdline, "androidboot.serialno"); + fu_device_set_serial(device, serial); + + /* + * Some devices don't have unique TYPE UUIDs, add the partition label to make them truly + * unique Devices have a fixed partition scheme anyway because they originally have Android + * which has such requirements. + */ + fu_device_add_instance_strsafe(device, "UUID", self->uuid); + fu_device_add_instance_strsafe(device, "LABEL", self->label); + fu_device_add_instance_strsafe(device, "SLOT", self->boot_slot); + + /* GUID based on UUID / UUID, label / UUID, label, slot */ + fu_device_build_instance_id(device, error, "DRIVE", "UUID", NULL); + fu_device_build_instance_id(device, error, "DRIVE", "UUID", "LABEL", NULL); + fu_device_build_instance_id(device, error, "DRIVE", "UUID", "LABEL", "SLOT", NULL); + + return TRUE; +} + +static gboolean +fu_android_boot_device_setup(FuDevice *device, GError **error) +{ + FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); + + /* set the firmware maximum size based on partition size or from quirk */ + fu_device_set_firmware_size_max(device, self->max_size); + + return TRUE; +} + +static gboolean +fu_android_boot_device_open(FuDevice *device, GError **error) +{ + g_autoptr(GError) error_local = NULL; + + /* FuUdevDevice->open */ + if (!FU_DEVICE_CLASS(fu_android_boot_device_parent_class)->open(device, &error_local)) { + if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + error_local->message); + return FALSE; + } + g_propagate_error(error, g_steal_pointer(&error_local)); + return FALSE; + } + + return TRUE; +} + +static gboolean +fu_android_boot_device_write(FuAndroidBootDevice *self, + GPtrArray *chunks, + FuProgress *progress, + GError **error) +{ + 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(FU_UDEV_DEVICE(self), + fu_chunk_get_address(chk), + 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); + } + + return TRUE; +} + +static gboolean +fu_android_boot_device_erase(FuAndroidBootDevice *self, FuProgress *progress, GError **error) +{ + g_autoptr(GPtrArray) chunks = NULL; + gsize bufsz = fu_device_get_firmware_size_max(FU_DEVICE(self)); + g_autofree guint8 *buf = g_malloc0(bufsz); + + chunks = fu_chunk_array_new(buf, bufsz, 0x0, 0x0, 10 * 1024); + + if (g_getenv("FWUPD_ANDROID_BOOT_VERBOSE") != NULL) + fu_dump_raw(G_LOG_DOMAIN, "erase", buf, bufsz); + + if (!fu_android_boot_device_write(self, chunks, progress, error)) + return FALSE; + + return TRUE; +} + +static gboolean +fu_android_boot_device_verify(FuAndroidBootDevice *self, + GPtrArray *chunks, + FuProgress *progress, + GError **error) +{ + 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(FU_UDEV_DEVICE(self), + fu_chunk_get_address(chk), + 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_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); + } + + return TRUE; +} + +static gboolean +fu_android_boot_device_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); + g_autoptr(GBytes) fw = NULL; + g_autoptr(GPtrArray) chunks = NULL; + + /* get data to write */ + fw = fu_firmware_get_bytes(firmware, error); + if (fw == NULL) + return FALSE; + + if (g_getenv("FWUPD_ANDROID_BOOT_VERBOSE") != NULL) + fu_dump_bytes(G_LOG_DOMAIN, "write", fw); + + chunks = fu_chunk_array_new_from_bytes(fw, 0x0, 0x0, 10 * 1024); + + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 72, NULL); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, NULL); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 7, NULL); + + /* erase, write, verify */ + if (!fu_android_boot_device_erase(self, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + if (!fu_android_boot_device_write(self, chunks, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + if (!fu_android_boot_device_verify(self, chunks, fu_progress_get_child(progress), error)) + return FALSE; + fu_progress_step_done(progress); + + return TRUE; +} + +static gboolean +fu_android_boot_device_set_quirk_kv(FuDevice *device, + const gchar *key, + const gchar *value, + GError **error) +{ + FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); + + /* load from quirks */ + if (g_strcmp0(key, "AndroidBootVersionProperty") == 0) { + g_autoptr(GHashTable) cmdline = NULL; + const gchar *version = NULL; + + cmdline = fu_kernel_get_cmdline(error); + if (cmdline == NULL) + return FALSE; + + version = g_hash_table_lookup(cmdline, value); + if (version != NULL) + fu_device_set_version(device, version); + return TRUE; + } + + if (g_strcmp0(key, "AndroidBootPartitionMaxSize") == 0) { + guint64 size = 0; + + if (!fu_strtoull(value, &size, 0, G_MAXUINT32, error)) + return FALSE; + self->max_size = size; + return TRUE; + } + + /* failed */ + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported"); + return FALSE; +} + +static void +fu_android_boot_device_finalize(GObject *obj) +{ + FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(obj); + G_OBJECT_CLASS(fu_android_boot_device_parent_class)->finalize(obj); + g_free(self->boot_slot); +} + +static void +fu_android_boot_device_init(FuAndroidBootDevice *self) +{ + fu_device_set_summary(FU_DEVICE(self), "Android Bootloader"); + fu_device_add_protocol(FU_DEVICE(self), "com.google.android_boot"); + fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); + 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); + fu_device_add_icon(FU_DEVICE(self), "computer"); + + /* + * Fallback for ABL without version reporting, fwupd will always provide an upgrade in this + * case. Once upgraded, the version reporting will be available and the update notification + * will disappear. If version reporting is available, the reported version is set. + */ + fu_device_set_version(FU_DEVICE(self), ANDROID_BOOT_UNKNOWN_VERSION); +} + +static void +fu_android_boot_device_class_init(FuAndroidBootDeviceClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + GObjectClass *klass_object = G_OBJECT_CLASS(klass); + + klass_object->finalize = fu_android_boot_device_finalize; + klass_device->probe = fu_android_boot_device_probe; + klass_device->setup = fu_android_boot_device_setup; + klass_device->open = fu_android_boot_device_open; + klass_device->write_firmware = fu_android_boot_device_write_firmware; + klass_device->to_string = fu_android_boot_device_to_string; + klass_device->set_quirk_kv = fu_android_boot_device_set_quirk_kv; +} diff --git a/plugins/android-boot/fu-android-boot-device.h b/plugins/android-boot/fu-android-boot-device.h new file mode 100644 index 000000000..ca59e0075 --- /dev/null +++ b/plugins/android-boot/fu-android-boot-device.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2022 Dylan Van Assche + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_ANDROID_BOOT_DEVICE (fu_android_boot_device_get_type()) + +G_DECLARE_FINAL_TYPE(FuAndroidBootDevice, + fu_android_boot_device, + FU, + ANDROID_BOOT_DEVICE, + FuUdevDevice) diff --git a/plugins/android-boot/fu-plugin-android-boot.c b/plugins/android-boot/fu-plugin-android-boot.c new file mode 100644 index 000000000..f5b8f5639 --- /dev/null +++ b/plugins/android-boot/fu-plugin-android-boot.c @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 Dylan Van Assche + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-android-boot-device.h" + +static void +fu_plugin_android_boot_init(FuPlugin *plugin) +{ + fu_plugin_add_device_gtype(plugin, FU_TYPE_ANDROID_BOOT_DEVICE); + fu_plugin_add_udev_subsystem(plugin, "block"); +} + +void +fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) +{ + vfuncs->build_hash = FU_BUILD_HASH; + vfuncs->init = fu_plugin_android_boot_init; +} diff --git a/plugins/android-boot/meson.build b/plugins/android-boot/meson.build new file mode 100644 index 000000000..f0fbe8f5d --- /dev/null +++ b/plugins/android-boot/meson.build @@ -0,0 +1,31 @@ +if get_option('plugin_android_boot').require(gudev.found(), + error_message: 'gudev is needed for plugin_android_boot').allowed() +cargs = ['-DG_LOG_DOMAIN="FuPluginAndroidBoot"'] + +install_data(['android-boot.quirk'], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_android_boot', + fu_hash, + sources : [ + 'fu-plugin-android-boot.c', + 'fu-android-boot-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/meson.build b/plugins/meson.build index 9f082f47f..82be7e911 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -18,6 +18,7 @@ subdir('acpi-ivrs') subdir('acpi-phat') subdir('amt') subdir('analogix') +subdir('android-boot') subdir('ata') subdir('bcm57xx') subdir('bios')