mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-02 21:08:12 +00:00
1105 lines
32 KiB
C
1105 lines
32 KiB
C
/*
|
|
* Copyright (C) 2020 Benson Leung <bleung@chromium.org>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <fwupdplugin.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include "fu-cros-ec-common.h"
|
|
#include "fu-cros-ec-firmware.h"
|
|
#include "fu-cros-ec-usb-device.h"
|
|
|
|
#define USB_SUBCLASS_GOOGLE_UPDATE 0x53
|
|
#define USB_PROTOCOL_GOOGLE_UPDATE 0xff
|
|
|
|
#define SETUP_RETRY_CNT 5
|
|
#define MAX_BLOCK_XFER_RETRIES 10
|
|
#define FLUSH_TIMEOUT_MS 10
|
|
#define BULK_SEND_TIMEOUT_MS 2000
|
|
#define BULK_RECV_TIMEOUT_MS 5000
|
|
#define CROS_EC_REMOVE_DELAY_RE_ENUMERATE 20000
|
|
|
|
#define UPDATE_DONE 0xB007AB1E
|
|
#define UPDATE_EXTRA_CMD 0xB007AB1F
|
|
|
|
enum update_extra_command {
|
|
UPDATE_EXTRA_CMD_IMMEDIATE_RESET = 0,
|
|
UPDATE_EXTRA_CMD_JUMP_TO_RW = 1,
|
|
UPDATE_EXTRA_CMD_STAY_IN_RO = 2,
|
|
UPDATE_EXTRA_CMD_UNLOCK_RW = 3,
|
|
UPDATE_EXTRA_CMD_UNLOCK_ROLLBACK = 4,
|
|
UPDATE_EXTRA_CMD_INJECT_ENTROPY = 5,
|
|
UPDATE_EXTRA_CMD_PAIR_CHALLENGE = 6,
|
|
UPDATE_EXTRA_CMD_TOUCHPAD_INFO = 7,
|
|
UPDATE_EXTRA_CMD_TOUCHPAD_DEBUG = 8,
|
|
UPDATE_EXTRA_CMD_CONSOLE_READ_INIT = 9,
|
|
UPDATE_EXTRA_CMD_CONSOLE_READ_NEXT = 10,
|
|
};
|
|
|
|
struct _FuCrosEcUsbDevice {
|
|
FuUsbDevice parent_instance;
|
|
guint8 iface_idx; /* bInterfaceNumber */
|
|
guint8 ep_num; /* bEndpointAddress */
|
|
guint16 chunk_len; /* wMaxPacketSize */
|
|
|
|
struct first_response_pdu targ;
|
|
guint32 writeable_offset;
|
|
guint16 protocol_version;
|
|
guint16 header_type;
|
|
struct cros_ec_version version; /* version of other region */
|
|
struct cros_ec_version active_version; /* version of active region */
|
|
gchar configuration[FU_CROS_EC_STRLEN];
|
|
gboolean in_bootloader;
|
|
};
|
|
|
|
G_DEFINE_TYPE(FuCrosEcUsbDevice, fu_cros_ec_usb_device, FU_TYPE_USB_DEVICE)
|
|
|
|
typedef union _START_RESP {
|
|
struct first_response_pdu rpdu;
|
|
guint32 legacy_resp;
|
|
} START_RESP;
|
|
|
|
typedef struct {
|
|
struct update_frame_header ufh;
|
|
GBytes *image_bytes;
|
|
gsize offset;
|
|
gsize payload_size;
|
|
} FuCrosEcUsbBlockInfo;
|
|
|
|
#define FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN (1 << 0)
|
|
#define FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN (1 << 1)
|
|
#define FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO (1 << 2)
|
|
#define FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL (1 << 3)
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_get_configuration(FuCrosEcUsbDevice *self, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
|
|
guint8 index;
|
|
g_autofree gchar *configuration = NULL;
|
|
|
|
#if G_USB_CHECK_VERSION(0, 3, 5)
|
|
index = g_usb_device_get_configuration_index(usb_device);
|
|
#else
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"this version of GUsb is not supported");
|
|
return FALSE;
|
|
#endif
|
|
configuration = g_usb_device_get_string_descriptor(usb_device, index, error);
|
|
if (configuration == NULL)
|
|
return FALSE;
|
|
|
|
if (g_strlcpy(self->configuration, configuration, FU_CROS_EC_STRLEN) == 0) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INTERNAL,
|
|
"empty iConfiguration");
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_find_interface(FuUsbDevice *device, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(device);
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
g_autoptr(GPtrArray) intfs = NULL;
|
|
|
|
/* based on usb_updater2's find_interfacei() and find_endpoint() */
|
|
|
|
intfs = g_usb_device_get_interfaces(usb_device, error);
|
|
if (intfs == NULL)
|
|
return FALSE;
|
|
for (guint i = 0; i < intfs->len; i++) {
|
|
GUsbInterface *intf = g_ptr_array_index(intfs, i);
|
|
if (g_usb_interface_get_class(intf) == 255 &&
|
|
g_usb_interface_get_subclass(intf) == USB_SUBCLASS_GOOGLE_UPDATE &&
|
|
g_usb_interface_get_protocol(intf) == USB_PROTOCOL_GOOGLE_UPDATE) {
|
|
GUsbEndpoint *ep;
|
|
g_autoptr(GPtrArray) endpoints = NULL;
|
|
|
|
endpoints = g_usb_interface_get_endpoints(intf);
|
|
if (NULL == endpoints || 0 == endpoints->len)
|
|
continue;
|
|
ep = g_ptr_array_index(endpoints, 0);
|
|
self->iface_idx = g_usb_interface_get_number(intf);
|
|
self->ep_num = g_usb_endpoint_get_address(ep) & 0x7f;
|
|
self->chunk_len = g_usb_endpoint_get_maximum_packet_size(ep);
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found");
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_open(FuDevice *device, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
|
|
/* FuUsbDevice->open */
|
|
if (!FU_DEVICE_CLASS(fu_cros_ec_usb_device_parent_class)->open(device, error))
|
|
return FALSE;
|
|
|
|
if (!g_usb_device_claim_interface(usb_device,
|
|
self->iface_idx,
|
|
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
|
|
error)) {
|
|
g_prefix_error(error, "failed to claim interface: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_probe(FuDevice *device, GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
|
|
/* FuUsbDevice->probe */
|
|
if (!FU_DEVICE_CLASS(fu_cros_ec_usb_device_parent_class)->probe(device, error))
|
|
return FALSE;
|
|
|
|
/* very much like usb_updater2's usb_findit() */
|
|
|
|
if (!fu_cros_ec_usb_device_find_interface(FU_USB_DEVICE(device), error)) {
|
|
g_prefix_error(error, "failed to find update interface: ");
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->chunk_len == 0) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_INVALID_DATA,
|
|
"wMaxPacketSize isn't valid: %" G_GUINT16_FORMAT,
|
|
self->chunk_len);
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_do_xfer(FuCrosEcUsbDevice *self,
|
|
const guint8 *outbuf,
|
|
gsize outlen,
|
|
guint8 *inbuf,
|
|
gsize inlen,
|
|
gboolean allow_less,
|
|
gsize *rxed_count,
|
|
GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
|
|
gsize actual = 0;
|
|
|
|
/* send data out */
|
|
if (outbuf != NULL && outlen > 0) {
|
|
g_autofree guint8 *outbuf_tmp = NULL;
|
|
|
|
/* make mutable */
|
|
outbuf_tmp = fu_memdup_safe(outbuf, outlen, error);
|
|
if (outbuf_tmp == NULL)
|
|
return FALSE;
|
|
|
|
if (!g_usb_device_bulk_transfer(usb_device,
|
|
self->ep_num,
|
|
outbuf_tmp,
|
|
outlen,
|
|
&actual,
|
|
BULK_SEND_TIMEOUT_MS,
|
|
NULL,
|
|
error)) {
|
|
return FALSE;
|
|
}
|
|
if (actual != outlen) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_PARTIAL_INPUT,
|
|
"only sent %" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes",
|
|
actual,
|
|
outlen);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* read reply back */
|
|
if (inbuf != NULL && inlen > 0) {
|
|
actual = 0;
|
|
if (!g_usb_device_bulk_transfer(usb_device,
|
|
self->ep_num | 0x80,
|
|
inbuf,
|
|
inlen,
|
|
&actual,
|
|
BULK_RECV_TIMEOUT_MS,
|
|
NULL,
|
|
error)) {
|
|
return FALSE;
|
|
}
|
|
if (actual != inlen && !allow_less) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_PARTIAL_INPUT,
|
|
"only received %" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes",
|
|
actual,
|
|
inlen);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (rxed_count != NULL)
|
|
*rxed_count = actual;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_flush(FuDevice *device, gpointer user_data, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
gsize actual = 0;
|
|
g_autofree guint8 *inbuf = g_malloc0(self->chunk_len);
|
|
|
|
/* bulk transfer expected to fail normally (ie, no stale data)
|
|
* but if bulk transfer succeeds, indicates stale bytes on the device
|
|
* so this will retry until they're emptied */
|
|
if (g_usb_device_bulk_transfer(usb_device,
|
|
self->ep_num | 0x80,
|
|
inbuf,
|
|
self->chunk_len,
|
|
&actual,
|
|
FLUSH_TIMEOUT_MS,
|
|
NULL,
|
|
NULL)) {
|
|
g_debug("flushing %" G_GSIZE_FORMAT " bytes", actual);
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"flushing %" G_GSIZE_FORMAT " bytes",
|
|
actual);
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_recovery(FuDevice *device, GError **error)
|
|
{
|
|
/* flush all data from endpoint to recover in case of error */
|
|
if (!fu_device_retry(device, fu_cros_ec_usb_device_flush, SETUP_RETRY_CNT, NULL, error)) {
|
|
g_prefix_error(error, "failed to flush device to idle state: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Channel TPM extension/vendor command over USB. The payload of the USB frame
|
|
* in this case consists of the 2 byte subcommand code concatenated with the
|
|
* command body. The caller needs to indicate if a response is expected, and
|
|
* if it is - of what maximum size.
|
|
*/
|
|
static gboolean
|
|
fu_cros_ec_usb_ext_cmd(FuDevice *device,
|
|
guint16 subcommand,
|
|
gpointer cmd_body,
|
|
gsize body_size,
|
|
gpointer resp,
|
|
gsize *resp_size,
|
|
gboolean allow_less,
|
|
GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
guint16 *frame_ptr;
|
|
gsize usb_msg_size = sizeof(struct update_frame_header) + sizeof(subcommand) + body_size;
|
|
g_autofree struct update_frame_header *ufh = g_malloc0(usb_msg_size);
|
|
|
|
ufh->block_size = GUINT32_TO_BE(usb_msg_size);
|
|
ufh->cmd.block_digest = 0;
|
|
ufh->cmd.block_base = GUINT32_TO_BE(UPDATE_EXTRA_CMD);
|
|
frame_ptr = (guint16 *)(ufh + 1);
|
|
*frame_ptr = GUINT16_TO_BE(subcommand);
|
|
|
|
if (body_size != 0) {
|
|
gsize offset = sizeof(struct update_frame_header) + sizeof(subcommand);
|
|
if (!fu_memcpy_safe((guint8 *)ufh,
|
|
usb_msg_size,
|
|
offset,
|
|
(const guint8 *)cmd_body,
|
|
body_size,
|
|
0x0,
|
|
body_size,
|
|
error))
|
|
return FALSE;
|
|
}
|
|
|
|
return fu_cros_ec_usb_device_do_xfer(self,
|
|
(const guint8 *)ufh,
|
|
usb_msg_size,
|
|
(guint8 *)resp,
|
|
resp_size != NULL ? *resp_size : 0,
|
|
TRUE,
|
|
NULL,
|
|
error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_start_request(FuDevice *device, gpointer user_data, GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
guint8 *start_resp = (guint8 *)user_data;
|
|
struct update_frame_header ufh;
|
|
gsize rxed_size = 0;
|
|
|
|
memset(&ufh, 0, sizeof(ufh));
|
|
ufh.block_size = GUINT32_TO_BE(sizeof(ufh));
|
|
if (!fu_cros_ec_usb_device_do_xfer(self,
|
|
(const guint8 *)&ufh,
|
|
sizeof(ufh),
|
|
start_resp,
|
|
sizeof(START_RESP),
|
|
TRUE,
|
|
&rxed_size,
|
|
error))
|
|
return FALSE;
|
|
|
|
/* we got something, so check for errors in response */
|
|
if (rxed_size < 8) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_PARTIAL_INPUT,
|
|
"unexpected response size %" G_GSIZE_FORMAT,
|
|
rxed_size);
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_setup(FuDevice *device, GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
guint32 error_code;
|
|
START_RESP start_resp;
|
|
g_auto(GStrv) config_split = NULL;
|
|
|
|
/* FuUsbDevice->setup */
|
|
if (!FU_DEVICE_CLASS(fu_cros_ec_usb_device_parent_class)->setup(device, error))
|
|
return FALSE;
|
|
|
|
if (!fu_cros_ec_usb_device_recovery(device, error))
|
|
return FALSE;
|
|
|
|
/* send start request */
|
|
if (!fu_device_retry(device,
|
|
fu_cros_ec_usb_device_start_request,
|
|
SETUP_RETRY_CNT,
|
|
&start_resp,
|
|
error)) {
|
|
g_prefix_error(error, "failed to send start request: ");
|
|
return FALSE;
|
|
}
|
|
|
|
self->protocol_version = GUINT16_FROM_BE(start_resp.rpdu.protocol_version);
|
|
|
|
if (self->protocol_version < 5 || self->protocol_version > 6) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
"unsupported protocol version %d",
|
|
self->protocol_version);
|
|
return FALSE;
|
|
}
|
|
self->header_type = GUINT16_FROM_BE(start_resp.rpdu.header_type);
|
|
|
|
error_code = GUINT32_FROM_BE(start_resp.rpdu.return_value);
|
|
if (error_code != 0) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"target reporting error %u",
|
|
error_code);
|
|
return FALSE;
|
|
}
|
|
|
|
self->writeable_offset = GUINT32_FROM_BE(start_resp.rpdu.common.offset);
|
|
if (!fu_memcpy_safe((guint8 *)self->targ.common.version,
|
|
FU_CROS_EC_STRLEN,
|
|
0x0,
|
|
(const guint8 *)start_resp.rpdu.common.version,
|
|
sizeof(start_resp.rpdu.common.version),
|
|
0x0,
|
|
sizeof(start_resp.rpdu.common.version),
|
|
error))
|
|
return FALSE;
|
|
self->targ.common.maximum_pdu_size =
|
|
GUINT32_FROM_BE(start_resp.rpdu.common.maximum_pdu_size);
|
|
self->targ.common.flash_protection =
|
|
GUINT32_FROM_BE(start_resp.rpdu.common.flash_protection);
|
|
self->targ.common.min_rollback = GINT32_FROM_BE(start_resp.rpdu.common.min_rollback);
|
|
self->targ.common.key_version = GUINT32_FROM_BE(start_resp.rpdu.common.key_version);
|
|
|
|
/* get active version string and running region from iConfiguration */
|
|
if (!fu_cros_ec_usb_device_get_configuration(self, error))
|
|
return FALSE;
|
|
config_split = g_strsplit(self->configuration, ":", 2);
|
|
if (g_strv_length(config_split) < 2) {
|
|
/* no prefix found so fall back to offset */
|
|
self->in_bootloader = self->writeable_offset != 0x0;
|
|
if (!fu_cros_ec_parse_version(self->configuration, &self->active_version, error)) {
|
|
g_prefix_error(error,
|
|
"failed parsing device's version: %32s: ",
|
|
self->configuration);
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
self->in_bootloader = g_strcmp0("RO", config_split[0]) == 0;
|
|
if (!fu_cros_ec_parse_version(config_split[1], &self->active_version, error)) {
|
|
g_prefix_error(error,
|
|
"failed parsing device's version: %32s: ",
|
|
config_split[1]);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* get the other region's version string from targ */
|
|
if (!fu_cros_ec_parse_version(self->targ.common.version, &self->version, error)) {
|
|
g_prefix_error(error,
|
|
"failed parsing device's version: %32s: ",
|
|
self->targ.common.version);
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->in_bootloader) {
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
fu_device_set_version(FU_DEVICE(device), self->version.triplet);
|
|
fu_device_set_version_bootloader(FU_DEVICE(device), self->active_version.triplet);
|
|
} else {
|
|
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
fu_device_set_version(FU_DEVICE(device), self->active_version.triplet);
|
|
fu_device_set_version_bootloader(FU_DEVICE(device), self->version.triplet);
|
|
}
|
|
fu_device_add_instance_id(FU_DEVICE(device), self->version.boardname);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_transfer_block(FuDevice *device, gpointer user_data, GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
FuCrosEcUsbBlockInfo *block_info = (FuCrosEcUsbBlockInfo *)user_data;
|
|
gsize image_size = 0;
|
|
gsize transfer_size = 0;
|
|
guint32 reply = 0;
|
|
g_autoptr(GBytes) block_bytes = NULL;
|
|
g_autoptr(GPtrArray) chunks = NULL;
|
|
|
|
g_return_val_if_fail(block_info != NULL, FALSE);
|
|
|
|
image_size = g_bytes_get_size(block_info->image_bytes);
|
|
if (block_info->offset + block_info->payload_size > image_size) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_INVALID_DATA,
|
|
"offset %" G_GSIZE_FORMAT "plus payload_size %" G_GSIZE_FORMAT
|
|
" exceeds image size %" G_GSIZE_FORMAT,
|
|
block_info->offset,
|
|
block_info->payload_size,
|
|
image_size);
|
|
return FALSE;
|
|
}
|
|
|
|
block_bytes = fu_common_bytes_new_offset(block_info->image_bytes,
|
|
block_info->offset,
|
|
block_info->payload_size,
|
|
error);
|
|
if (block_bytes == NULL)
|
|
return FALSE;
|
|
chunks = fu_chunk_array_new_from_bytes(block_bytes, 0x00, 0x00, self->chunk_len);
|
|
|
|
/* first send the header */
|
|
if (!fu_cros_ec_usb_device_do_xfer(self,
|
|
(const guint8 *)&block_info->ufh,
|
|
sizeof(struct update_frame_header),
|
|
NULL,
|
|
0,
|
|
FALSE,
|
|
NULL,
|
|
error)) {
|
|
g_autoptr(GError) error_flush = NULL;
|
|
/* flush all data from endpoint to recover in case of error */
|
|
if (!fu_cros_ec_usb_device_recovery(device, &error_flush)) {
|
|
g_debug("failed to flush to idle: %s", error_flush->message);
|
|
}
|
|
g_prefix_error(error, "failed at sending header: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* send the block, chunk by chunk */
|
|
for (guint i = 0; i < chunks->len; i++) {
|
|
FuChunk *chk = g_ptr_array_index(chunks, i);
|
|
|
|
if (!fu_cros_ec_usb_device_do_xfer(self,
|
|
fu_chunk_get_data(chk),
|
|
fu_chunk_get_data_sz(chk),
|
|
NULL,
|
|
0,
|
|
FALSE,
|
|
NULL,
|
|
error)) {
|
|
g_autoptr(GError) error_flush = NULL;
|
|
g_prefix_error(error, "failed at sending chunk: ");
|
|
|
|
/* flush all data from endpoint to recover in case of error */
|
|
if (!fu_cros_ec_usb_device_recovery(device, &error_flush)) {
|
|
g_debug("failed to flush to idle: %s", error_flush->message);
|
|
}
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* get the reply */
|
|
if (!fu_cros_ec_usb_device_do_xfer(self,
|
|
NULL,
|
|
0,
|
|
(guint8 *)&reply,
|
|
sizeof(reply),
|
|
TRUE,
|
|
&transfer_size,
|
|
error)) {
|
|
g_autoptr(GError) error_flush = NULL;
|
|
g_prefix_error(error, "failed at reply: ");
|
|
/* flush all data from endpoint to recover in case of error */
|
|
if (!fu_cros_ec_usb_device_recovery(device, &error_flush)) {
|
|
g_debug("failed to flush to idle: %s", error_flush->message);
|
|
}
|
|
return FALSE;
|
|
}
|
|
if (transfer_size == 0) {
|
|
g_set_error_literal(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"zero bytes received for block reply");
|
|
return FALSE;
|
|
}
|
|
if (reply != 0) {
|
|
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "error: status 0x%#x", reply);
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_transfer_section(FuDevice *device,
|
|
FuFirmware *firmware,
|
|
FuCrosEcFirmwareSection *section,
|
|
GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
const guint8 *data_ptr = NULL;
|
|
guint32 section_addr = 0;
|
|
gsize data_len = 0;
|
|
gsize offset = 0;
|
|
g_autoptr(GBytes) img_bytes = NULL;
|
|
|
|
g_return_val_if_fail(section != NULL, FALSE);
|
|
|
|
section_addr = section->offset;
|
|
img_bytes = fu_firmware_get_image_by_idx_bytes(firmware, section->image_idx, error);
|
|
if (img_bytes == NULL) {
|
|
g_prefix_error(error, "failed to find section image: ");
|
|
return FALSE;
|
|
}
|
|
|
|
data_ptr = (const guint8 *)g_bytes_get_data(img_bytes, &data_len);
|
|
if (data_ptr == NULL || data_len != section->size) {
|
|
g_set_error(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_INVALID_DATA,
|
|
"image and section sizes do not match: image = %" G_GSIZE_FORMAT
|
|
" bytes vs section size = %" G_GSIZE_FORMAT " bytes",
|
|
data_len,
|
|
section->size);
|
|
return FALSE;
|
|
}
|
|
|
|
/* smart update: trim trailing bytes */
|
|
while (data_len != 0 && (data_ptr[data_len - 1] == 0xff))
|
|
data_len--;
|
|
g_debug("trimmed %" G_GSIZE_FORMAT " trailing bytes", section->size - data_len);
|
|
|
|
g_debug("sending 0x%zx bytes to %#x", data_len, section_addr);
|
|
while (data_len > 0) {
|
|
gsize payload_size;
|
|
guint32 block_base;
|
|
FuCrosEcUsbBlockInfo block_info;
|
|
|
|
/* prepare the header to prepend to the block */
|
|
block_info.image_bytes = img_bytes;
|
|
payload_size = MIN(data_len, self->targ.common.maximum_pdu_size);
|
|
block_base = GUINT32_TO_BE(section_addr);
|
|
block_info.ufh.block_size =
|
|
GUINT32_TO_BE(payload_size + sizeof(struct update_frame_header));
|
|
block_info.ufh.cmd.block_base = block_base;
|
|
block_info.ufh.cmd.block_digest = 0;
|
|
block_info.offset = offset;
|
|
block_info.payload_size = payload_size;
|
|
|
|
if (!fu_device_retry(device,
|
|
fu_cros_ec_usb_device_transfer_block,
|
|
MAX_BLOCK_XFER_RETRIES,
|
|
&block_info,
|
|
error)) {
|
|
g_prefix_error(error,
|
|
"failed to transfer block, %" G_GSIZE_FORMAT " to go: ",
|
|
data_len);
|
|
return FALSE;
|
|
}
|
|
data_len -= payload_size;
|
|
offset += payload_size;
|
|
section_addr += payload_size;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_cros_ec_usb_device_send_done(FuDevice *device)
|
|
{
|
|
guint32 out = GUINT32_TO_BE(UPDATE_DONE);
|
|
g_autoptr(GError) error_local = NULL;
|
|
|
|
/* send stop request, ignoring reply */
|
|
if (!fu_cros_ec_usb_device_do_xfer(FU_CROS_EC_USB_DEVICE(device),
|
|
(const guint8 *)&out,
|
|
sizeof(out),
|
|
(guint8 *)&out,
|
|
1,
|
|
FALSE,
|
|
NULL,
|
|
&error_local)) {
|
|
g_debug("error on transfer of done: %s", error_local->message);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_send_subcommand(FuDevice *device,
|
|
guint16 subcommand,
|
|
gpointer cmd_body,
|
|
gsize body_size,
|
|
gpointer resp,
|
|
gsize *resp_size,
|
|
gboolean allow_less,
|
|
GError **error)
|
|
{
|
|
fu_cros_ec_usb_device_send_done(device);
|
|
|
|
if (!fu_cros_ec_usb_ext_cmd(device,
|
|
subcommand,
|
|
cmd_body,
|
|
body_size,
|
|
resp,
|
|
resp_size,
|
|
FALSE,
|
|
error)) {
|
|
g_prefix_error(error,
|
|
"failed to send subcommand %" G_GUINT16_FORMAT ": ",
|
|
subcommand);
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_reset_to_ro(FuDevice *device, GError **error)
|
|
{
|
|
guint8 response;
|
|
guint16 subcommand = UPDATE_EXTRA_CMD_IMMEDIATE_RESET;
|
|
guint8 command_body[2]; /* Max command body size. */
|
|
gsize command_body_size = 0;
|
|
gsize response_size = 1;
|
|
g_autoptr(GError) error_local = NULL;
|
|
|
|
fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO);
|
|
if (!fu_cros_ec_usb_device_send_subcommand(device,
|
|
subcommand,
|
|
command_body,
|
|
command_body_size,
|
|
&response,
|
|
&response_size,
|
|
FALSE,
|
|
&error_local)) {
|
|
/* failure here is ok */
|
|
g_debug("ignoring failure: %s", error_local->message);
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_jump_to_rw(FuDevice *device)
|
|
{
|
|
guint8 response;
|
|
guint16 subcommand = UPDATE_EXTRA_CMD_JUMP_TO_RW;
|
|
guint8 command_body[2]; /* Max command body size. */
|
|
gsize command_body_size = 0;
|
|
gsize response_size = 1;
|
|
|
|
if (!fu_cros_ec_usb_device_send_subcommand(device,
|
|
subcommand,
|
|
command_body,
|
|
command_body_size,
|
|
&response,
|
|
&response_size,
|
|
FALSE,
|
|
NULL)) {
|
|
/* bail out early here if subcommand failed, which is normal */
|
|
return TRUE;
|
|
}
|
|
|
|
/* Jump to rw may not work, so if we've reached here, initiate a
|
|
* full reset using immediate reset */
|
|
subcommand = UPDATE_EXTRA_CMD_IMMEDIATE_RESET;
|
|
fu_cros_ec_usb_device_send_subcommand(device,
|
|
subcommand,
|
|
command_body,
|
|
command_body_size,
|
|
&response,
|
|
&response_size,
|
|
FALSE,
|
|
NULL);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_write_firmware(FuDevice *device,
|
|
FuFirmware *firmware,
|
|
FwupdInstallFlags flags,
|
|
GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
GPtrArray *sections;
|
|
FuCrosEcFirmware *cros_ec_firmware = FU_CROS_EC_FIRMWARE(firmware);
|
|
gint num_txed_sections = 0;
|
|
|
|
fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL);
|
|
|
|
if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO)) {
|
|
gsize response_size = 1;
|
|
guint8 response;
|
|
guint16 subcommand = UPDATE_EXTRA_CMD_STAY_IN_RO;
|
|
guint8 command_body[2]; /* Max command body size. */
|
|
gsize command_body_size = 0;
|
|
START_RESP start_resp;
|
|
|
|
fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO);
|
|
if (!fu_cros_ec_usb_device_send_subcommand(device,
|
|
subcommand,
|
|
command_body,
|
|
command_body_size,
|
|
&response,
|
|
&response_size,
|
|
FALSE,
|
|
error)) {
|
|
g_prefix_error(error, "failed to send stay-in-ro subcommand: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* flush all data from endpoint to recover in case of error */
|
|
if (!fu_cros_ec_usb_device_recovery(device, error)) {
|
|
g_prefix_error(error, "failed to flush device to idle state: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* send start request */
|
|
if (!fu_device_retry(device,
|
|
fu_cros_ec_usb_device_start_request,
|
|
SETUP_RETRY_CNT,
|
|
&start_resp,
|
|
error)) {
|
|
g_prefix_error(error, "failed to send start request: ");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN) &&
|
|
self->in_bootloader) {
|
|
/*
|
|
* We had previously written to the rw region (while we were
|
|
* booted from ro region), but somehow landed in ro again after
|
|
* a reboot. Since we wrote rw already, we wanted to jump
|
|
* to the new rw so we could evaluate ro.
|
|
*
|
|
* This is a transitory state due to the fact that we have to
|
|
* boot through RO to get to RW. Set another write required to
|
|
* allow the RO region to auto-jump to RW.
|
|
*
|
|
* Special flow: write phase skips actual write -> attach skips
|
|
* send of reset command, just sets wait for replug, and
|
|
* device restart status.
|
|
*/
|
|
fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);
|
|
return TRUE;
|
|
}
|
|
|
|
sections = fu_cros_ec_firmware_get_sections(cros_ec_firmware);
|
|
if (sections == NULL) {
|
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid sections");
|
|
return FALSE;
|
|
}
|
|
|
|
fu_device_set_status(device, FWUPD_STATUS_DEVICE_WRITE);
|
|
for (guint i = 0; i < sections->len; i++) {
|
|
FuCrosEcFirmwareSection *section = g_ptr_array_index(sections, i);
|
|
|
|
if (section->ustatus == FU_CROS_EC_FW_NEEDED) {
|
|
g_autoptr(GError) error_local = NULL;
|
|
|
|
if (!fu_cros_ec_usb_device_transfer_section(device,
|
|
firmware,
|
|
section,
|
|
&error_local)) {
|
|
if (g_error_matches(error_local,
|
|
G_USB_DEVICE_ERROR,
|
|
G_USB_DEVICE_ERROR_NOT_SUPPORTED)) {
|
|
g_debug("failed to transfer section, trying another write, "
|
|
"ignoring error: %s",
|
|
error_local->message);
|
|
fu_device_add_flag(
|
|
device,
|
|
FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);
|
|
return TRUE;
|
|
}
|
|
g_propagate_error(error, g_steal_pointer(&error_local));
|
|
return FALSE;
|
|
}
|
|
num_txed_sections++;
|
|
|
|
if (self->in_bootloader) {
|
|
fu_device_set_version(FU_DEVICE(device), section->version.triplet);
|
|
} else {
|
|
fu_device_set_version_bootloader(FU_DEVICE(device),
|
|
section->version.triplet);
|
|
}
|
|
}
|
|
}
|
|
/* send done */
|
|
fu_cros_ec_usb_device_send_done(device);
|
|
|
|
if (num_txed_sections == 0) {
|
|
g_set_error_literal(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_INVALID_DATA,
|
|
"no sections transferred");
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->in_bootloader)
|
|
fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN);
|
|
else
|
|
fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN);
|
|
|
|
/* logical XOR */
|
|
if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN) !=
|
|
fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN))
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_close(FuDevice *device, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
|
|
if (!g_usb_device_release_interface(usb_device,
|
|
self->iface_idx,
|
|
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_cros_ec_usb_device_parent_class)->close(device, error);
|
|
}
|
|
|
|
static FuFirmware *
|
|
fu_cros_ec_usb_device_prepare_firmware(FuDevice *device,
|
|
GBytes *fw,
|
|
FwupdInstallFlags flags,
|
|
GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
FuCrosEcFirmware *cros_ec_firmware = NULL;
|
|
g_autoptr(FuFirmware) firmware = fu_cros_ec_firmware_new();
|
|
|
|
if (!fu_firmware_parse(firmware, fw, flags, error))
|
|
return NULL;
|
|
cros_ec_firmware = FU_CROS_EC_FIRMWARE(firmware);
|
|
|
|
/* pick sections */
|
|
if (!fu_cros_ec_firmware_pick_sections(cros_ec_firmware, self->writeable_offset, error)) {
|
|
g_prefix_error(error, "failed to pick sections: ");
|
|
return NULL;
|
|
}
|
|
return g_steal_pointer(&firmware);
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_attach(FuDevice *device, GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
|
|
fu_device_set_remove_delay(device, CROS_EC_REMOVE_DELAY_RE_ENUMERATE);
|
|
if (self->in_bootloader &&
|
|
fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL)) {
|
|
/*
|
|
* attach after the SPECIAL flag was set. The EC will auto-jump
|
|
* from ro -> rw, so we do not need to send an explicit
|
|
* reset_to_ro. We just need to set for another wait for replug
|
|
* as a detach + reenumeration is expected as we jump from
|
|
* ro -> rw.
|
|
*/
|
|
fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL);
|
|
fu_device_set_status(device, FWUPD_STATUS_DEVICE_RESTART);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
return TRUE;
|
|
}
|
|
|
|
if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN) &&
|
|
!fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN)) {
|
|
if (!fu_cros_ec_usb_device_reset_to_ro(device, error)) {
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
fu_cros_ec_usb_device_jump_to_rw(device);
|
|
}
|
|
fu_device_set_status(device, FWUPD_STATUS_DEVICE_RESTART);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_cros_ec_usb_device_detach(FuDevice *device, GError **error)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
|
|
if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN) &&
|
|
!fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN))
|
|
return TRUE;
|
|
|
|
if (self->in_bootloader) {
|
|
g_debug("skipping immediate reboot in case of already in bootloader");
|
|
/* in RO so skip reboot */
|
|
return TRUE;
|
|
} else if (self->targ.common.flash_protection != 0x0) {
|
|
/* in RW, and RO region is write protected, so jump to RO */
|
|
fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN);
|
|
fu_device_set_remove_delay(device, CROS_EC_REMOVE_DELAY_RE_ENUMERATE);
|
|
if (!fu_cros_ec_usb_device_reset_to_ro(device, error))
|
|
return FALSE;
|
|
|
|
fu_device_set_status(device, FWUPD_STATUS_DEVICE_RESTART);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_cros_ec_usb_device_init(FuCrosEcUsbDevice *self)
|
|
{
|
|
fu_device_add_protocol(FU_DEVICE(self), "com.google.usb.crosec");
|
|
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
|
|
fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
|
|
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET);
|
|
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE);
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN,
|
|
"ro-written");
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN,
|
|
"rw-written");
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO,
|
|
"rebooting-to-ro");
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL,
|
|
"special");
|
|
}
|
|
|
|
static void
|
|
fu_cros_ec_usb_device_to_string(FuDevice *device, guint idt, GString *str)
|
|
{
|
|
FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device);
|
|
g_autofree gchar *min_rollback = NULL;
|
|
|
|
fu_common_string_append_kv(str, idt, "GitHash", self->version.sha1);
|
|
fu_common_string_append_kb(str, idt, "Dirty", self->version.dirty);
|
|
fu_common_string_append_ku(str, idt, "ProtocolVersion", self->protocol_version);
|
|
fu_common_string_append_ku(str, idt, "HeaderType", self->header_type);
|
|
fu_common_string_append_ku(str, idt, "MaxPDUSize", self->targ.common.maximum_pdu_size);
|
|
fu_common_string_append_kx(str,
|
|
idt,
|
|
"FlashProtectionStatus",
|
|
self->targ.common.flash_protection);
|
|
fu_common_string_append_kv(str, idt, "RawVersion", self->targ.common.version);
|
|
fu_common_string_append_ku(str, idt, "KeyVersion", self->targ.common.key_version);
|
|
min_rollback = g_strdup_printf("%" G_GINT32_FORMAT, self->targ.common.min_rollback);
|
|
fu_common_string_append_kv(str, idt, "MinRollback", min_rollback);
|
|
fu_common_string_append_kx(str, idt, "WriteableOffset", self->writeable_offset);
|
|
}
|
|
|
|
static void
|
|
fu_cros_ec_usb_device_class_init(FuCrosEcUsbDeviceClass *klass)
|
|
{
|
|
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
|
|
klass_device->attach = fu_cros_ec_usb_device_attach;
|
|
klass_device->detach = fu_cros_ec_usb_device_detach;
|
|
klass_device->prepare_firmware = fu_cros_ec_usb_device_prepare_firmware;
|
|
klass_device->setup = fu_cros_ec_usb_device_setup;
|
|
klass_device->to_string = fu_cros_ec_usb_device_to_string;
|
|
klass_device->write_firmware = fu_cros_ec_usb_device_write_firmware;
|
|
klass_device->open = fu_cros_ec_usb_device_open;
|
|
klass_device->probe = fu_cros_ec_usb_device_probe;
|
|
klass_device->close = fu_cros_ec_usb_device_close;
|
|
}
|