fwupd/plugins/fastboot/fu-fastboot-device.c
Richard Hughes 7afd7cba0d Use FuFirmware as a container for firmware images
In many plugins we've wanted to use ->prepare_firmware() to parse the firmware
ahead of ->detach() and ->write_firmware() but this has the limitation that it
can only return a single blob of data.

For many devices, multiple binary blobs are required from one parsed image,
for instance providing signatures, config and data blobs that have to be pushed
to the device in different way.

This also means we parse the firmware *before* we ask the user to detach.

Break the internal FuDevice API to support these firmware types as they become
more popular.

This also allows us to move the Intel HEX and SREC parsing out of the dfu plugin
as they are used by a few plugins now, and resolving symbols between plugins
isn't exactly awesome.
2019-08-08 13:10:57 +01:00

721 lines
20 KiB
C

/*
* Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include <xmlb.h>
#include "fu-archive.h"
#include "fu-chunk.h"
#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, GString *str)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
g_string_append (str, " FuFastbootDevice:\n");
g_string_append_printf (str, " intf:\t0x%02x\n", (guint) self->intf_nr);
g_string_append_printf (str, " secure:\t%i\n", self->secure);
g_string_append_printf (str, " blocksz:\t%u\n", self->blocksz);
}
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 (FuUsbDevice *device, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (device);
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 = g_memdup (buf, (guint) buflen);
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,
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_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE);
else if (g_strcmp0 (tmp, "writing flash") == 0)
fu_device_set_status (device, 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,
FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error))
return FALSE;
return TRUE;
}
static gboolean
fu_fastboot_device_cmd (FuDevice *device, const gchar *cmd,
FuFastbootDeviceReadFlags flags, GError **error)
{
if (!fu_fastboot_device_writestr (device, cmd, error))
return FALSE;
if (!fu_fastboot_device_read (device, NULL, flags, error))
return FALSE;
return TRUE;
}
static gboolean
fu_fastboot_device_flash (FuDevice *device, const gchar *partition, GError **error)
{
g_autofree gchar *tmp = g_strdup_printf ("flash:%s", partition);
return fu_fastboot_device_cmd (device, tmp,
FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
error);
}
static gboolean
fu_fastboot_device_download (FuDevice *device, GBytes *fw, 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,
FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL,
error))
return FALSE;
/* send the data in chunks */
fu_device_set_status (device, 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, chk->data, chk->data_sz, error))
return FALSE;
fu_device_set_progress_full (device, (gsize) i, (gsize) chunks->len * 2);
}
if (!fu_fastboot_device_read (device, NULL,
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;
/* 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_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,
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, error))
return FALSE;
return fu_fastboot_device_flash (device, partition, error);
}
static gboolean
fu_fastboot_device_write_motorola_part (FuDevice *device,
FuArchive *archive,
XbNode *part,
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,
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, error))
return FALSE;
return fu_fastboot_device_flash (device, partition, 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,
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,
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;
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,
error))
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fastboot_device_write_qfil (FuDevice *device, FuArchive* archive, 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;
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,
error))
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fastboot_device_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
g_autoptr(FuArchive) archive = NULL;
g_autoptr(GBytes) fw = NULL;
/* get default image */
fw = fu_firmware_get_image_default_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, error);
if (fu_archive_lookup_by_fn (archive, "flashfile.xml", NULL) != NULL) {
return fu_fastboot_device_write_motorola (device,
archive,
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 (FuUsbDevice *device, GError **error)
{
FuFastbootDevice *self = FU_FASTBOOT_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (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;
}
/* success */
return TRUE;
}
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 slave address 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, GError **error)
{
fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
return fu_fastboot_device_cmd (device, "reboot",
FU_FASTBOOT_DEVICE_READ_FLAG_NONE,
error);
}
static void
fu_fastboot_device_init (FuFastbootDevice *self)
{
/* this is a safe default, even using USBv1 */
self->blocksz = 512;
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
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);
FuUsbDeviceClass *klass_usb_device = FU_USB_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_usb_device->open = fu_fastboot_device_open;
klass_usb_device->close = fu_fastboot_device_close;
}
FuFastbootDevice *
fu_fastboot_device_new (FuUsbDevice *device)
{
FuFastbootDevice *self = g_object_new (FU_TYPE_FASTBOOT_DEVICE, NULL);
fu_device_incorporate (FU_DEVICE (self), FU_DEVICE (device));
return self;
}