/* * Copyright (C) 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include #include "fu-fastboot-device.h" #define FASTBOOT_REMOVE_DELAY_RE_ENUMERATE 60000 /* ms */ #define FASTBOOT_TRANSACTION_TIMEOUT 1000 /* ms */ #define FASTBOOT_TRANSACTION_RETRY_MAX 600 #define FASTBOOT_EP_IN 0x81 #define FASTBOOT_EP_OUT 0x01 #define FASTBOOT_CMD_BUFSZ 64 /* bytes */ struct _FuFastbootDevice { FuUsbDevice parent_instance; gboolean secure; guint blocksz; guint8 intf_nr; }; G_DEFINE_TYPE(FuFastbootDevice, fu_fastboot_device, FU_TYPE_USB_DEVICE) static void fu_fastboot_device_to_string(FuDevice *device, guint idt, GString *str) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); fu_common_string_append_kx(str, idt, "InterfaceNumber", self->intf_nr); fu_common_string_append_kx(str, idt, "BlockSize", self->blocksz); fu_common_string_append_kb(str, idt, "Secure", self->secure); } static gboolean fu_fastboot_device_probe(FuDevice *device, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); g_autoptr(GUsbInterface) intf = NULL; /* find the correct fastboot interface */ intf = g_usb_device_get_interface(usb_device, 0xff, 0x42, 0x03, error); if (intf == NULL) return FALSE; self->intf_nr = g_usb_interface_get_number(intf); return TRUE; } static gboolean fu_fastboot_device_open(FuDevice *device, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_fastboot_device_parent_class)->open(device, error)) return FALSE; if (!g_usb_device_claim_interface(usb_device, self->intf_nr, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to claim interface: "); return FALSE; } /* success */ return TRUE; } static void fu_fastboot_buffer_dump(const gchar *title, const guint8 *buf, gsize sz) { if (g_getenv("FWUPD_FASTBOOT_VERBOSE") == NULL) return; g_print("%s (%" G_GSIZE_FORMAT "):\n", title, sz); for (gsize i = 0; i < sz; i++) { g_print("%02x[%c] ", buf[i], g_ascii_isprint(buf[i]) ? buf[i] : '?'); if (i > 0 && (i + 1) % 256 == 0) g_print("\n"); } g_print("\n"); } static gboolean fu_fastboot_device_write(FuDevice *device, const guint8 *buf, gsize buflen, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); gboolean ret; gsize actual_len = 0; g_autofree guint8 *buf2 = NULL; /* make mutable */ buf2 = fu_memdup_safe(buf, buflen, error); if (buf2 == NULL) return FALSE; fu_fastboot_buffer_dump("writing", buf, buflen); ret = g_usb_device_bulk_transfer(usb_device, FASTBOOT_EP_OUT, buf2, buflen, &actual_len, FASTBOOT_TRANSACTION_TIMEOUT, NULL, error); if (!ret) { g_prefix_error(error, "failed to do bulk transfer: "); return FALSE; } if (actual_len != buflen) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "only wrote %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } return TRUE; } static gboolean fu_fastboot_device_writestr(FuDevice *device, const gchar *str, GError **error) { gsize buflen = strlen(str); if (buflen > FASTBOOT_CMD_BUFSZ - 4) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "fastboot limits writes to %i bytes", FASTBOOT_CMD_BUFSZ - 4); return FALSE; } return fu_fastboot_device_write(device, (const guint8 *)str, buflen, error); } typedef enum { FU_FASTBOOT_DEVICE_READ_FLAG_NONE, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, } FuFastbootDeviceReadFlags; static gboolean fu_fastboot_device_read(FuDevice *device, gchar **str, FuProgress *progress, FuFastbootDeviceReadFlags flags, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); guint retries = 1; /* these commands may return INFO or take some time to complete */ if (flags & FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL) retries = FASTBOOT_TRANSACTION_RETRY_MAX; for (guint i = 0; i < retries; i++) { gboolean ret; gsize actual_len = 0; guint8 buf[FASTBOOT_CMD_BUFSZ] = {0x00}; g_autofree gchar *tmp = NULL; g_autoptr(GError) error_local = NULL; ret = g_usb_device_bulk_transfer(usb_device, FASTBOOT_EP_IN, buf, sizeof(buf), &actual_len, FASTBOOT_TRANSACTION_TIMEOUT, NULL, &error_local); if (!ret) { if (g_error_matches(error_local, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_TIMED_OUT)) { g_debug("ignoring %s", error_local->message); continue; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to do bulk transfer: "); return FALSE; } fu_fastboot_buffer_dump("read", buf, actual_len); if (actual_len < 4) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "only read %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } /* info */ tmp = g_strndup((const gchar *)buf + 4, self->blocksz - 4); if (memcmp(buf, "INFO", 4) == 0) { if (g_strcmp0(tmp, "erasing flash") == 0) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); else if (g_strcmp0(tmp, "writing flash") == 0) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); else g_debug("INFO returned unknown: %s", tmp); continue; } /* success */ if (memcmp(buf, "OKAY", 4) == 0 || memcmp(buf, "DATA", 4) == 0) { if (str != NULL) *str = g_steal_pointer(&tmp); return TRUE; } /* failure */ if (memcmp(buf, "FAIL", 4) == 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to read response: %s", tmp); return FALSE; } /* unknown failure */ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to read response"); return FALSE; } /* we timed out a *lot* */ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "no response to read"); return FALSE; } static gboolean fu_fastboot_device_getvar(FuDevice *device, const gchar *key, gchar **str, GError **error) { g_autofree gchar *tmp = g_strdup_printf("getvar:%s", key); if (!fu_fastboot_device_writestr(device, tmp, error)) return FALSE; if (!fu_fastboot_device_read(device, str, NULL, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error)) { g_prefix_error(error, "failed to getvar %s: ", key); return FALSE; } return TRUE; } static gboolean fu_fastboot_device_cmd(FuDevice *device, const gchar *cmd, FuProgress *progress, FuFastbootDeviceReadFlags flags, GError **error) { if (!fu_fastboot_device_writestr(device, cmd, error)) return FALSE; if (!fu_fastboot_device_read(device, NULL, progress, flags, error)) return FALSE; return TRUE; } static gboolean fu_fastboot_device_flash(FuDevice *device, const gchar *partition, FuProgress *progress, GError **error) { g_autofree gchar *tmp = g_strdup_printf("flash:%s", partition); return fu_fastboot_device_cmd(device, tmp, progress, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error); } static gboolean fu_fastboot_device_download(FuDevice *device, GBytes *fw, FuProgress *progress, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); gsize sz = g_bytes_get_size(fw); g_autofree gchar *tmp = g_strdup_printf("download:%08x", (guint)sz); g_autoptr(GPtrArray) chunks = NULL; /* tell the client the size of data to expect */ if (!fu_fastboot_device_cmd(device, tmp, progress, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error)) return FALSE; /* send the data in chunks */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); chunks = fu_chunk_array_new_from_bytes(fw, 0x00, /* start addr */ 0x00, /* page_sz */ self->blocksz); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_fastboot_device_write(device, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_set_percentage_full(progress, (gsize)i + 1, (gsize)chunks->len); } if (!fu_fastboot_device_read(device, NULL, progress, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error)) return FALSE; return TRUE; } static gboolean fu_fastboot_device_setup(FuDevice *device, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); g_autofree gchar *product = NULL; g_autofree gchar *serialno = NULL; g_autofree gchar *version = NULL; g_autofree gchar *secure = NULL; g_autofree gchar *version_bootloader = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_fastboot_device_parent_class)->setup(device, error)) return FALSE; /* product */ if (!fu_fastboot_device_getvar(device, "product", &product, error)) return FALSE; if (product != NULL && product[0] != '\0') { g_autofree gchar *tmp = g_strdup_printf("Fastboot %s", product); fu_device_set_name(device, tmp); } /* fastboot API version */ if (!fu_fastboot_device_getvar(device, "version", &version, error)) return FALSE; if (version != NULL && version[0] != '\0') g_debug("fastboot version=%s", version); /* bootloader version */ if (!fu_fastboot_device_getvar(device, "version-bootloader", &version_bootloader, error)) return FALSE; if (version_bootloader != NULL && version_bootloader[0] != '\0') { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR); fu_device_set_version_bootloader(device, version_bootloader); } /* serialno */ if (!fu_fastboot_device_getvar(device, "serialno", &serialno, error)) return FALSE; if (serialno != NULL && serialno[0] != '\0') fu_device_set_serial(device, serialno); /* secure */ if (!fu_fastboot_device_getvar(device, "secure", &secure, error)) return FALSE; if (secure != NULL && secure[0] != '\0') self->secure = TRUE; /* success */ return TRUE; } static gboolean fu_fastboot_device_write_qfil_part(FuDevice *device, FuArchive *archive, XbNode *part, FuProgress *progress, GError **error) { GBytes *data; const gchar *fn; const gchar *partition; /* not all partitions have images */ fn = xb_node_query_text(part, "img_name", NULL); if (fn == NULL) return TRUE; /* find filename */ data = fu_archive_lookup_by_fn(archive, fn, error); if (data == NULL) return FALSE; /* get the partition name */ partition = xb_node_query_text(part, "name", error); if (partition == NULL) return FALSE; if (g_str_has_prefix(partition, "0:")) partition += 2; /* flash the partition */ if (!fu_fastboot_device_download(device, data, progress, error)) return FALSE; return fu_fastboot_device_flash(device, partition, progress, error); } static gboolean fu_fastboot_device_write_motorola_part(FuDevice *device, FuArchive *archive, XbNode *part, FuProgress *progress, GError **error) { const gchar *op = xb_node_get_attr(part, "operation"); /* oem */ if (g_strcmp0(op, "oem") == 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "OEM commands are not supported"); return FALSE; } /* getvar */ if (g_strcmp0(op, "getvar") == 0) { const gchar *var = xb_node_get_attr(part, "var"); g_autofree gchar *tmp = NULL; /* check required args */ if (var == NULL) { tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL); g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "required var for part: %s", tmp); return FALSE; } /* just has to be non-empty */ if (!fu_fastboot_device_getvar(device, var, &tmp, error)) return FALSE; if (tmp == NULL || tmp[0] == '\0') { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed to getvar %s", var); return FALSE; } return TRUE; } /* erase */ if (g_strcmp0(op, "erase") == 0) { const gchar *partition = xb_node_get_attr(part, "partition"); g_autofree gchar *cmd = g_strdup_printf("erase:%s", partition); /* check required args */ if (partition == NULL) { g_autofree gchar *tmp = NULL; tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL); g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "required partition for part: %s", tmp); return FALSE; } /* erase the partition */ return fu_fastboot_device_cmd(device, cmd, progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error); } /* flash */ if (g_strcmp0(op, "flash") == 0) { GBytes *data; const gchar *filename = xb_node_get_attr(part, "filename"); const gchar *partition = xb_node_get_attr(part, "partition"); struct { GChecksumType kind; const gchar *str; } csum_kinds[] = {{G_CHECKSUM_MD5, "MD5"}, {G_CHECKSUM_SHA1, "SHA1"}, {0, NULL}}; /* check required args */ if (partition == NULL || filename == NULL) { g_autofree gchar *tmp = NULL; tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL); g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "required partition and filename: %s", tmp); return FALSE; } /* find filename */ data = fu_archive_lookup_by_fn(archive, filename, error); if (data == NULL) return FALSE; /* checksum is optional */ for (guint i = 0; csum_kinds[i].str != NULL; i++) { const gchar *csum; g_autofree gchar *csum_actual = NULL; /* not provided */ csum = xb_node_get_attr(part, csum_kinds[i].str); if (csum == NULL) continue; /* check is valid */ csum_actual = g_compute_checksum_for_bytes(csum_kinds[i].kind, data); if (g_strcmp0(csum, csum_actual) != 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "%s invalid, expected %s, got %s", filename, csum, csum_actual); return FALSE; } } /* flash the partition */ if (!fu_fastboot_device_download(device, data, progress, error)) return FALSE; return fu_fastboot_device_flash(device, partition, progress, error); } /* dumb operation that doesn't expect a response */ if (g_strcmp0(op, "boot") == 0 || g_strcmp0(op, "continue") == 0 || g_strcmp0(op, "reboot") == 0 || g_strcmp0(op, "reboot-bootloader") == 0 || g_strcmp0(op, "powerdown") == 0) { return fu_fastboot_device_cmd(device, op, progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error); } /* unknown */ g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "unknown operation %s", op); return FALSE; } static gboolean fu_fastboot_device_write_motorola(FuDevice *device, FuArchive *archive, FuProgress *progress, GError **error) { GBytes *data; g_autoptr(GPtrArray) parts = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* load the manifest of operations */ data = fu_archive_lookup_by_fn(archive, "flashfile.xml", error); if (data == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, data, XB_BUILDER_SOURCE_FLAG_NONE, error)) return FALSE; xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) return FALSE; /* get all the operation parts */ parts = xb_silo_query(silo, "parts/part", 0, error); if (parts == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, parts->len); for (guint i = 0; i < parts->len; i++) { XbNode *part = g_ptr_array_index(parts, i); if (!fu_fastboot_device_write_motorola_part(device, archive, part, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_fastboot_device_write_qfil(FuDevice *device, FuArchive *archive, FuProgress *progress, GError **error) { GBytes *data; g_autoptr(GPtrArray) parts = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* load the manifest of operations */ data = fu_archive_lookup_by_fn(archive, "partition_nand.xml", error); if (data == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, data, XB_BUILDER_SOURCE_FLAG_NONE, error)) return FALSE; xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) return FALSE; /* get all the operation parts */ parts = xb_silo_query(silo, "nandboot/partitions/partition", 0, error); if (parts == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, parts->len); for (guint i = 0; i < parts->len; i++) { XbNode *part = g_ptr_array_index(parts, i); if (!fu_fastboot_device_write_qfil_part(device, archive, part, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_fastboot_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* decompress entire archive ahead of time */ archive = fu_archive_new(fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; /* load the manifest of operations */ if (fu_archive_lookup_by_fn(archive, "partition_nand.xml", NULL) != NULL) return fu_fastboot_device_write_qfil(device, archive, progress, error); if (fu_archive_lookup_by_fn(archive, "flashfile.xml", NULL) != NULL) { return fu_fastboot_device_write_motorola(device, archive, progress, error); } /* not supported */ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "manifest not supported"); return FALSE; } static gboolean fu_fastboot_device_close(FuDevice *device, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); /* we're done here */ if (!g_usb_device_release_interface(usb_device, self->intf_nr, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to release interface: "); return FALSE; } /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_fastboot_device_parent_class)->close(device, error); } static gboolean fu_fastboot_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); /* load from quirks */ if (g_strcmp0(key, "FastbootBlockSize") == 0) { guint64 tmp = fu_common_strtoull(value); if (tmp >= 0x40 && tmp < 0x100000) { self->blocksz = tmp; return TRUE; } g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid block size"); return FALSE; } /* failed */ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_fastboot_device_attach(FuDevice *device, FuProgress *progress, GError **error) { if (!fu_fastboot_device_cmd(device, "reboot", progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_fastboot_device_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, 94); /* 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_fastboot_device_init(FuFastbootDevice *self) { /* this is a safe default, even using USBv1 */ self->blocksz = 512; fu_device_add_protocol(FU_DEVICE(self), "com.google.fastboot"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID); fu_device_set_remove_delay(FU_DEVICE(self), FASTBOOT_REMOVE_DELAY_RE_ENUMERATE); } static void fu_fastboot_device_class_init(FuFastbootDeviceClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); klass_device->probe = fu_fastboot_device_probe; klass_device->setup = fu_fastboot_device_setup; klass_device->write_firmware = fu_fastboot_device_write_firmware; klass_device->attach = fu_fastboot_device_attach; klass_device->to_string = fu_fastboot_device_to_string; klass_device->set_quirk_kv = fu_fastboot_device_set_quirk_kv; klass_device->open = fu_fastboot_device_open; klass_device->close = fu_fastboot_device_close; klass_device->set_progress = fu_fastboot_device_set_progress; }