fwupd/plugins/superio/fu-superio-it55-device.c
2021-08-24 11:18:40 -05:00

638 lines
18 KiB
C

/*
* Copyright (C) 2021, TUXEDO Computers GmbH
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include <string.h>
#include "fu-superio-common.h"
#include "fu-superio-it55-device.h"
/* ROM of IT5570 consists of 64KB blocks. Blocks can be further subdivided in
* 256-byte chunks, which is especially visible when erasing the ROM. This is
* because in case of erasure offset within a block is specified in chunks (even
* though erasure is done one kilobyte at a time).
*
* Accessing ROM requires entering a special mode, which should be always left
* to restore normal operation of EC (handling of buttons, keyboard, etc.). */
#define SIO_CMD_EC_WRITE_BLOCK 0x02
#define SIO_CMD_EC_READ_BLOCK 0x03
#define SIO_CMD_EC_ERASE_KBYTE 0x05
#define SIO_CMD_EC_WRITE_1ST_KBYTE 0x06
#define EC_ROM_ACCESS_ON_1 0xDE
#define EC_ROM_ACCESS_ON_2 0xDC
#define EC_ROM_ACCESS_OFF 0xFE
#define BLOCK_SIZE 0x10000
#define CHUNK_SIZE 0x100
#define CHUNKS_IN_KBYTE 0x4
#define CHUNKS_IN_BLOCK 0x100
#define MAX_FLASHING_ATTEMPTS 5
typedef enum {
AUTOLOAD_NO_ACTION,
AUTOLOAD_DISABLE,
AUTOLOAD_SET_ON,
AUTOLOAD_SET_OFF,
} AutoloadAction;
struct _FuEcIt55Device {
FuSuperioDevice parent_instance;
gchar *prj_name;
AutoloadAction autoload_action;
};
G_DEFINE_TYPE(FuEcIt55Device, fu_superio_it55_device, FU_TYPE_SUPERIO_DEVICE)
static void
fu_superio_it55_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
/* FuSuperioDevice->to_string */
FU_DEVICE_CLASS(fu_superio_it55_device_parent_class)->to_string(device, idt, str);
fu_common_string_append_kx(str, idt, "AutoloadAction", self->autoload_action);
}
static gboolean
fu_superio_it55_device_ec_project(FuSuperioDevice *device, GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
gchar project[16] = {0x0};
if (!fu_superio_device_ec_write_cmd(device, SIO_CMD_EC_GET_NAME_STR, error))
return FALSE;
for (guint i = 0; i < sizeof(project) - 1; ++i) {
guint8 tmp = 0;
if (!fu_superio_device_ec_read_data(device, &tmp, error)) {
g_prefix_error(error, "failed to read firmware project: ");
return FALSE;
}
if (tmp == '$')
break;
project[i] = tmp;
}
self->prj_name = g_strdup(project);
/* success */
return TRUE;
}
static gboolean
fu_superio_it55_device_ec_version(FuSuperioDevice *self, GError **error)
{
gchar version[16] = {'1', '.', '\0'};
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_GET_VERSION_STR, error))
return FALSE;
for (guint i = 2; i < sizeof(version) - 1; i++) {
guint8 tmp = 0;
if (!fu_superio_device_ec_read_data(self, &tmp, error)) {
g_prefix_error(error, "failed to read firmware version: ");
return FALSE;
}
if (tmp == '$')
break;
version[i] = tmp;
}
fu_device_set_version(FU_DEVICE(self), version);
/* success */
return TRUE;
}
static gboolean
fu_superio_it55_device_ec_size(FuSuperioDevice *self, GError **error)
{
guint8 tmp = 0;
if (!fu_superio_device_reg_read(self, 0xf9, &tmp, error))
return FALSE;
switch (tmp & 0xf0) {
case 0xf0:
fu_device_set_firmware_size(FU_DEVICE(self), BLOCK_SIZE * 4);
break;
case 0x40:
fu_device_set_firmware_size(FU_DEVICE(self), BLOCK_SIZE * 3);
break;
default:
fu_device_set_firmware_size(FU_DEVICE(self), BLOCK_SIZE * 2);
break;
}
return TRUE;
}
static gboolean
fu_superio_it55_device_setup(FuDevice *device, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
/* FuSuperioDevice->setup */
if (!FU_DEVICE_CLASS(fu_superio_it55_device_parent_class)->setup(device, error))
return FALSE;
/* basic initialization */
if (!fu_superio_device_reg_write(self, 0xf9, 0x20, error) ||
!fu_superio_device_reg_write(self, 0xfa, 0x02, error) ||
!fu_superio_device_reg_write(self, 0xfb, 0x00, error) ||
!fu_superio_device_reg_write(self, 0xf8, 0xb1, error)) {
g_prefix_error(error, "initialization: ");
return FALSE;
}
/* Order of interactions with EC below matters. Additionally, reading EC
* project seems to be mandatory for successful firmware operations.
* Test after making changes here! */
/* get size from the EC */
if (!fu_superio_it55_device_ec_size(self, error))
return FALSE;
/* get installed firmware project from the EC */
if (!fu_superio_it55_device_ec_project(self, error))
return FALSE;
/* get installed firmware version from the EC */
if (!fu_superio_it55_device_ec_version(self, error))
return FALSE;
/* success */
return TRUE;
}
static void
fu_superio_it55_device_progress_cb(goffset current, goffset total, gpointer user_data)
{
FuDevice *device = FU_DEVICE(user_data);
fu_device_set_progress_full(device, (gsize)current, (gsize)total);
}
static GBytes *
fu_plugin_superio_patch_autoload(FuDevice *device, GBytes *fw, GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
guint offset;
gsize sz = 0;
const guint8 *unpatched = g_bytes_get_data(fw, &sz);
gboolean small_flash = (sz <= BLOCK_SIZE * 2);
g_autofree guint8 *patched = NULL;
if (self->autoload_action == AUTOLOAD_NO_ACTION)
return g_bytes_ref(fw);
for (offset = 0; offset < sz - 6; ++offset) {
if (unpatched[offset] == 0xa5 &&
(unpatched[offset + 1] == 0xa5 || unpatched[offset + 1] == 0xa4) &&
unpatched[offset + 5] == 0x5a)
break;
}
if (offset >= sz - 6)
return g_bytes_ref(fw);
/* not big enough */
if (offset + 8 >= sz) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"image is too small to patch");
return NULL;
}
patched = fu_memdup_safe(unpatched, sz, error);
if (patched == NULL)
return NULL;
if (self->autoload_action == AUTOLOAD_DISABLE) {
patched[offset + 2] = (small_flash ? 0x94 : 0x85);
patched[offset + 8] = 0x00;
} else if (self->autoload_action == AUTOLOAD_SET_ON) {
patched[offset + 2] = (small_flash ? 0x94 : 0x85);
patched[offset + 8] = (small_flash ? 0x7f : 0xbe);
} else if (self->autoload_action == AUTOLOAD_SET_OFF) {
patched[offset + 2] = (small_flash ? 0xa5 : 0xb5);
patched[offset + 8] = 0xaa;
}
return g_bytes_new_take(g_steal_pointer(&patched), sz);
}
/* progress callback is optional to not affect device progress during writing
* firmware */
static GBytes *
fu_superio_it55_device_get_firmware(FuDevice *device,
GFileProgressCallback progress_cb,
GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
guint64 fwsize = fu_device_get_firmware_size_min(device);
guint64 block_count = (fwsize + BLOCK_SIZE - 1) / BLOCK_SIZE;
goffset offset = 0;
g_autofree guint8 *buf = NULL;
buf = g_malloc0(fwsize);
for (guint i = 0; i < block_count; ++i) {
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_READ_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, i, error))
return NULL;
for (guint j = 0; j < BLOCK_SIZE; ++j, ++offset) {
if (!fu_superio_device_ec_read_data(self, &buf[offset], error))
return NULL;
if (progress_cb != NULL)
progress_cb(offset, (goffset)fwsize, self);
}
}
return g_bytes_new_take(g_steal_pointer(&buf), fwsize);
}
static GBytes *
fu_superio_it55_device_dump_firmware(FuDevice *device, GError **error)
{
g_autoptr(FuDeviceLocker) locker = NULL;
/* require detach -> attach */
locker = fu_device_locker_new_full(device,
(FuDeviceLockerFunc)fu_device_detach,
(FuDeviceLockerFunc)fu_device_attach,
error);
if (locker == NULL)
return NULL;
fu_device_set_status(device, FWUPD_STATUS_DEVICE_READ);
return fu_superio_it55_device_get_firmware(device,
fu_superio_it55_device_progress_cb,
error);
}
static gboolean
fu_superio_it55_device_attach(FuDevice *device, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
return TRUE;
/* leave ROM access mode */
if (!fu_superio_device_ec_write_cmd(self, EC_ROM_ACCESS_OFF, error))
return FALSE;
/* success */
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
static gboolean
fu_superio_it55_device_detach(FuDevice *device, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
return TRUE;
/* enter ROM access mode */
if (!fu_superio_device_ec_write_cmd(self, EC_ROM_ACCESS_ON_1, error) ||
!fu_superio_device_ec_write_cmd(self, EC_ROM_ACCESS_ON_2, error))
return FALSE;
/* success */
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
static gboolean
fu_superio_it55_device_erase(FuDevice *device, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
guint64 fwsize = fu_device_get_firmware_size_min(device);
guint64 chunk_count = (fwsize + CHUNK_SIZE - 1) / CHUNK_SIZE;
for (guint i = 0; i < chunk_count; i += CHUNKS_IN_KBYTE) {
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_ERASE_KBYTE, error) ||
!fu_superio_device_ec_write_cmd(self, i / CHUNKS_IN_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, i % CHUNKS_IN_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, 0x00, error))
return FALSE;
g_usleep(1000);
}
g_usleep(100000);
return TRUE;
}
static gboolean
fu_superio_it55_device_write_attempt(FuDevice *device, GBytes *firmware, GError **error)
{
FuSuperioDevice *self = FU_SUPERIO_DEVICE(device);
gsize fwsize = g_bytes_get_size(firmware);
guint64 total = (fwsize + CHUNK_SIZE - 1) / CHUNK_SIZE;
const guint8 *fw_data = NULL;
g_autoptr(GBytes) erased_fw = NULL;
g_autoptr(GBytes) written_fw = NULL;
g_autoptr(GPtrArray) blocks = NULL;
if (!fu_superio_it55_device_erase(device, error))
return FALSE;
erased_fw = fu_superio_it55_device_get_firmware(device, NULL, error);
if (erased_fw == NULL) {
g_prefix_error(error, "failed to read erased firmware");
return FALSE;
}
if (!fu_common_bytes_is_empty(erased_fw)) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_READ,
"firmware was not erased");
return FALSE;
}
/* write everything but the first kilobyte */
blocks = fu_chunk_array_new_from_bytes(firmware, 0x00, 0x00, BLOCK_SIZE);
for (guint i = 0; i < blocks->len; ++i) {
FuChunk *block = g_ptr_array_index(blocks, i);
gboolean first = (i == 0);
guint32 offset = 0;
guint32 bytes_left = fu_chunk_get_data_sz(block);
const guint8 *data = fu_chunk_get_data(block);
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_WRITE_BLOCK, error) ||
!fu_superio_device_ec_write_cmd(self, 0x00, error) ||
!fu_superio_device_ec_write_cmd(self, i, error) ||
!fu_superio_device_ec_write_cmd(self, first ? 0x04 : 0x00, error) ||
!fu_superio_device_ec_write_cmd(self, 0x00, error))
return FALSE;
for (guint j = 0; j < CHUNKS_IN_BLOCK; ++j) {
gsize progress = i * CHUNKS_IN_BLOCK + j;
if (first && j < CHUNKS_IN_KBYTE) {
offset += CHUNK_SIZE;
bytes_left -= CHUNK_SIZE;
fu_device_set_progress_full(device, progress, (gsize)total);
continue;
}
for (guint k = 0; k < CHUNK_SIZE; ++k) {
if (bytes_left == 0) {
if (!fu_superio_device_ec_write_data(self, 0xff, error))
return FALSE;
continue;
}
if (!fu_superio_device_ec_write_data(self, data[offset], error))
return FALSE;
++offset;
--bytes_left;
}
fu_device_set_progress_full(device, progress, (gsize)total);
}
}
/* now write the first kilobyte */
if (!fu_superio_device_ec_write_cmd(self, SIO_CMD_EC_WRITE_1ST_KBYTE, error))
return FALSE;
fw_data = g_bytes_get_data(firmware, NULL);
for (guint i = 0; i < CHUNK_SIZE * CHUNKS_IN_KBYTE; ++i)
if (!fu_superio_device_ec_write_data(self, fw_data[i], error))
return FALSE;
g_usleep(1000);
written_fw = fu_superio_it55_device_get_firmware(device, NULL, error);
if (written_fw == NULL) {
g_prefix_error(error, "failed to read erased firmware");
return FALSE;
}
if (!fu_common_bytes_compare(written_fw, firmware, error)) {
g_prefix_error(error, "firmware verification");
return FALSE;
}
/* success */
fu_device_set_progress(device, 100);
return TRUE;
}
static gboolean
fu_superio_it55_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
gsize fwsize;
g_autoptr(GBytes) fw = NULL;
g_autoptr(GBytes) fw_patched = NULL;
g_autoptr(FuDeviceLocker) locker = NULL;
/* require detach -> attach */
locker = fu_device_locker_new_full(device,
(FuDeviceLockerFunc)fu_device_detach,
(FuDeviceLockerFunc)fu_device_attach,
error);
if (locker == NULL)
return FALSE;
/* get default image */
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return FALSE;
fwsize = g_bytes_get_size(fw);
if (fwsize < 1024) {
g_prefix_error(error, "firmware is too small: %u", (guint)fwsize);
return FALSE;
}
fw_patched = fu_plugin_superio_patch_autoload(device, fw, error);
if (fw_patched == NULL)
return FALSE;
fu_device_set_status(device, FWUPD_STATUS_DEVICE_WRITE);
/* try this many times; the failure-to-flash case leaves you without a
* keyboard and future boot may completely fail */
for (guint i = 1;; ++i) {
g_autoptr(GError) error_chk = NULL;
if (fu_superio_it55_device_write_attempt(device, fw_patched, &error_chk))
break;
if (i == MAX_FLASHING_ATTEMPTS) {
g_propagate_error(error, g_steal_pointer(&error_chk));
return FALSE;
}
g_warning("failure %u: %s", i, error_chk->message);
}
/* success */
return TRUE;
}
static gchar *
fu_ec_extract_field(GBytes *fw, const gchar *name, GError **error)
{
guint offset;
gsize prefix_len;
gsize fwsz = 0;
const gchar *value;
const guint8 *buf = g_bytes_get_data(fw, &fwsz);
g_autofree gchar *field = g_strdup_printf("%s:", name);
prefix_len = strlen(field);
for (offset = 0; offset < fwsz - prefix_len; ++offset) {
if (memcmp(&buf[offset], field, prefix_len) == 0)
break;
}
if (offset >= fwsz - prefix_len) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"did not find %s field in the firmware image",
name);
return NULL;
}
offset += prefix_len;
value = (const gchar *)&buf[offset];
for (; offset < fwsz; ++offset) {
if (buf[offset] == '$')
return g_strndup(value, (const gchar *)&buf[offset] - value);
}
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"couldn't extract %s field value from the firmware image",
name);
return NULL;
}
static FuFirmware *
fu_superio_it55_device_prepare_firmware(FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
g_autofree gchar *date = NULL;
g_autofree gchar *prj_name = NULL;
g_autofree gchar *version = NULL;
prj_name = fu_ec_extract_field(fw, "PRJ", error);
if (prj_name == NULL)
return NULL;
version = fu_ec_extract_field(fw, "VER", error);
if (version == NULL)
version = g_strdup("(unknown version)");
date = fu_ec_extract_field(fw, "DATE", error);
if (date == NULL)
date = g_strdup("(unknown build date)");
g_debug("New firmware: %s %s built on %s", prj_name, version, date);
if (g_strcmp0(prj_name, self->prj_name) != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"firmware targets %s instead of %s",
prj_name,
self->prj_name);
return NULL;
}
return fu_firmware_new_from_bytes(fw);
}
static gboolean
fu_superio_it55_device_set_quirk_kv(FuDevice *device,
const gchar *key,
const gchar *value,
GError **error)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(device);
/* FuSuperioDevice->set_quirk_kv */
if (!FU_DEVICE_CLASS(fu_superio_it55_device_parent_class)
->set_quirk_kv(device, key, value, error))
return FALSE;
if (g_strcmp0(key, "SuperioAutoloadAction") == 0) {
if (g_strcmp0(value, "none") == 0) {
self->autoload_action = AUTOLOAD_NO_ACTION;
} else if (g_strcmp0(value, "disable") == 0) {
self->autoload_action = AUTOLOAD_DISABLE;
} else if (g_strcmp0(value, "on") == 0) {
self->autoload_action = AUTOLOAD_SET_ON;
} else if (g_strcmp0(value, "off") == 0) {
self->autoload_action = AUTOLOAD_SET_OFF;
} else {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"invalid value");
return FALSE;
}
}
/* success */
return TRUE;
}
static void
fu_superio_it55_device_init(FuEcIt55Device *self)
{
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_OFFLINE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC);
/* version string example: 1.07.02TR1 */
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN);
}
static void
fu_superio_it55_device_finalize(GObject *obj)
{
FuEcIt55Device *self = FU_SUPERIO_IT55_DEVICE(obj);
g_free(self->prj_name);
}
static void
fu_superio_it55_device_class_init(FuEcIt55DeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
G_OBJECT_CLASS(klass)->finalize = fu_superio_it55_device_finalize;
klass_device->to_string = fu_superio_it55_device_to_string;
klass_device->attach = fu_superio_it55_device_attach;
klass_device->detach = fu_superio_it55_device_detach;
klass_device->dump_firmware = fu_superio_it55_device_dump_firmware;
klass_device->write_firmware = fu_superio_it55_device_write_firmware;
klass_device->setup = fu_superio_it55_device_setup;
klass_device->prepare_firmware = fu_superio_it55_device_prepare_firmware;
klass_device->set_quirk_kv = fu_superio_it55_device_set_quirk_kv;
}