fwupd/plugins/synaptics-prometheus/fu-synaprom-device.c
2021-08-24 11:18:40 -05:00

506 lines
14 KiB
C

/*
* Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
* Copyright (C) 2019 Synaptics Inc
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include "fu-synaprom-common.h"
#include "fu-synaprom-config.h"
#include "fu-synaprom-device.h"
#include "fu-synaprom-firmware.h"
struct _FuSynapromDevice {
FuUsbDevice parent_instance;
guint8 vmajor;
guint8 vminor;
};
/* vendor-specific USB control requets to write DFT word (Hayes) */
#define FU_SYNAPROM_USB_CTRLREQUEST_VENDOR_WRITEDFT 21
/* endpoint addresses for command and fingerprint data */
#define FU_SYNAPROM_USB_REQUEST_EP 0x01
#define FU_SYNAPROM_USB_REPLY_EP 0x81
#define FU_SYNAPROM_USB_FINGERPRINT_EP 0x82
#define FU_SYNAPROM_USB_INTERRUPT_EP 0x83
/* le */
typedef struct __attribute__((packed)) {
guint16 status;
} FuSynapromReplyGeneric;
/* le */
typedef struct __attribute__((packed)) {
guint16 status;
guint32 buildtime; /* Unix-style build time */
guint32 buildnum; /* build number */
guint8 vmajor; /* major version */
guint8 vminor; /* minor version */
guint8 target; /* target, e.g. VCSFW_TARGET_ROM */
guint8 product; /* product, e.g. VCSFW_PRODUCT_FALCON */
guint8 siliconrev; /* silicon revision */
guint8 formalrel; /* boolean: non-zero -> formal release */
guint8 platform; /* Platform (PCB) revision */
guint8 patch; /* patch level */
guint8 serial_number[6]; /* 48-bit Serial Number */
guint8 security[2]; /* bytes 0 and 1 of OTP */
guint32 patchsig; /* opaque patch signature */
guint8 iface; /* interface type, see below */
guint8 otpsig[3]; /* OTP Patch Signature */
guint16 otpspare1; /* spare space */
guint8 reserved; /* reserved byte */
guint8 device_type; /* device type */
} FuSynapromReplyGetVersion;
/* the following bits describe security options in
** FuSynapromReplyGetVersion::security[1] bit-field */
#define FU_SYNAPROM_SECURITY1_PROD_SENSOR (1 << 5)
G_DEFINE_TYPE(FuSynapromDevice, fu_synaprom_device, FU_TYPE_USB_DEVICE)
static gboolean
fu_synaprom_device_open(FuDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
/* FuUsbDevice->open */
if (!FU_DEVICE_CLASS(fu_synaprom_device_parent_class)->open(device, error))
return FALSE;
if (!g_usb_device_claim_interface(usb_device,
0x0,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
return FALSE;
}
return TRUE;
}
gboolean
fu_synaprom_device_cmd_send(FuSynapromDevice *device,
GByteArray *request,
GByteArray *reply,
guint timeout_ms,
GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
gboolean ret;
gsize actual_len = 0;
if (g_getenv("FWUPD_SYNAPROM_VERBOSE") != NULL) {
fu_common_dump_full(G_LOG_DOMAIN,
"REQST",
request->data,
request->len,
16,
FU_DUMP_FLAGS_SHOW_ADDRESSES);
}
ret = g_usb_device_bulk_transfer(usb_device,
FU_SYNAPROM_USB_REQUEST_EP,
request->data,
request->len,
&actual_len,
timeout_ms,
NULL,
error);
if (!ret) {
g_prefix_error(error, "failed to request: ");
return FALSE;
}
if (actual_len < request->len) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only sent 0x%04x of 0x%04x",
(guint)actual_len,
request->len);
return FALSE;
}
ret = g_usb_device_bulk_transfer(usb_device,
FU_SYNAPROM_USB_REPLY_EP,
reply->data,
reply->len,
NULL, /* allowed to return short read */
timeout_ms,
NULL,
error);
if (!ret) {
g_prefix_error(error, "failed to reply: ");
return FALSE;
}
if (g_getenv("FWUPD_SYNAPROM_VERBOSE") != NULL) {
fu_common_dump_full(G_LOG_DOMAIN,
"REPLY",
reply->data,
actual_len,
16,
FU_DUMP_FLAGS_SHOW_ADDRESSES);
}
/* parse as FuSynapromReplyGeneric */
if (reply->len >= sizeof(FuSynapromReplyGeneric)) {
FuSynapromReplyGeneric *hdr = (FuSynapromReplyGeneric *)reply->data;
return fu_synaprom_error_from_status(GUINT16_FROM_LE(hdr->status), error);
}
/* success */
return TRUE;
}
void
fu_synaprom_device_set_version(FuSynapromDevice *self,
guint8 vmajor,
guint8 vminor,
guint32 buildnum)
{
g_autofree gchar *str = NULL;
/* We decide to skip 10.02.xxxxxx firmware, so we force the minor version from 0x02
** to 0x01 to make the devices with 0x02 minor version firmware allow to be updated
** back to minor version 0x01. */
if (vmajor == 0x0a && vminor == 0x02) {
g_debug("quirking vminor from %02x to 01", vminor);
vminor = 0x01;
}
/* set display version */
str = g_strdup_printf("%02u.%02u.%u", vmajor, vminor, buildnum);
fu_device_set_version(FU_DEVICE(self), str);
/* we need this for checking the firmware compatibility later */
self->vmajor = vmajor;
self->vminor = vminor;
}
static void
fu_synaprom_device_set_serial_number(FuSynapromDevice *self, guint64 serial_number)
{
g_autofree gchar *str = NULL;
str = g_strdup_printf("%" G_GUINT64_FORMAT, serial_number);
fu_device_set_serial(FU_DEVICE(self), str);
}
static gboolean
fu_synaprom_device_setup(FuDevice *device, GError **error)
{
FuSynapromDevice *self = FU_SYNAPROM_DEVICE(device);
FuSynapromReplyGetVersion pkt;
guint32 product;
guint64 serial_number = 0;
g_autoptr(GByteArray) request = NULL;
g_autoptr(GByteArray) reply = NULL;
/* FuUsbDevice->setup */
if (!FU_DEVICE_CLASS(fu_synaprom_device_parent_class)->setup(device, error))
return FALSE;
/* get version */
request = fu_synaprom_request_new(FU_SYNAPROM_CMD_GET_VERSION, NULL, 0);
reply = fu_synaprom_reply_new(sizeof(FuSynapromReplyGetVersion));
if (!fu_synaprom_device_cmd_send(self, request, reply, 250, error)) {
g_prefix_error(error, "failed to get version: ");
return FALSE;
}
memcpy(&pkt, reply->data, sizeof(pkt));
product = GUINT32_FROM_LE(pkt.product);
g_debug("product ID is %u, version=%u.%u, buildnum=%u prod=%i",
product,
pkt.vmajor,
pkt.vminor,
GUINT32_FROM_LE(pkt.buildnum),
pkt.security[1] & FU_SYNAPROM_SECURITY1_PROD_SENSOR);
fu_synaprom_device_set_version(self, pkt.vmajor, pkt.vminor, GUINT32_FROM_LE(pkt.buildnum));
/* get serial number */
memcpy(&serial_number, pkt.serial_number, sizeof(pkt.serial_number));
fu_synaprom_device_set_serial_number(self, serial_number);
/* check device type */
if (product == FU_SYNAPROM_PRODUCT_PROMETHEUS) {
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
} else if (product == FU_SYNAPROM_PRODUCT_PROMETHEUSPBL ||
product == FU_SYNAPROM_PRODUCT_PROMETHEUSMSBL) {
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
} else {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"device %u is not supported by this plugin",
product);
return FALSE;
}
/* add updatable config child, if this is a production sensor */
if (fu_device_get_children(device)->len == 0 &&
!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER) &&
pkt.security[1] & FU_SYNAPROM_SECURITY1_PROD_SENSOR) {
g_autoptr(FuSynapromConfig) cfg = fu_synaprom_config_new(self);
fu_device_add_child(FU_DEVICE(device), FU_DEVICE(cfg));
}
/* success */
return TRUE;
}
static gboolean
fu_synaprom_device_cmd_download_chunk(FuSynapromDevice *device,
const GByteArray *chunk,
GError **error)
{
g_autoptr(GByteArray) request = NULL;
g_autoptr(GByteArray) reply = NULL;
request = fu_synaprom_request_new(FU_SYNAPROM_CMD_BOOTLDR_PATCH, chunk->data, chunk->len);
reply = fu_synaprom_reply_new(sizeof(FuSynapromReplyGeneric));
return fu_synaprom_device_cmd_send(device, request, reply, 20000, error);
}
FuFirmware *
fu_synaprom_device_prepare_fw(FuDevice *device, GBytes *fw, FwupdInstallFlags flags, GError **error)
{
FuSynapromFirmwareMfwHeader hdr;
guint32 product;
g_autoptr(GBytes) blob = NULL;
g_autoptr(FuFirmware) firmware = fu_synaprom_firmware_new();
/* parse the firmware */
if (!fu_firmware_parse(firmware, fw, flags, error))
return NULL;
/* check the update header product and version */
blob = fu_firmware_get_image_by_id_bytes(firmware, "mfw-update-header", error);
if (blob == NULL)
return NULL;
if (g_bytes_get_size(blob) != sizeof(hdr)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"MFW metadata is invalid");
return NULL;
}
memcpy(&hdr, g_bytes_get_data(blob, NULL), sizeof(hdr));
product = GUINT32_FROM_LE(hdr.product);
if (product != FU_SYNAPROM_PRODUCT_PROMETHEUS) {
if (flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) {
g_warning("MFW metadata not compatible, "
"got 0x%02x expected 0x%02x",
product,
(guint)FU_SYNAPROM_PRODUCT_PROMETHEUS);
} else {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"MFW metadata not compatible, "
"got 0x%02x expected 0x%02x",
product,
(guint)FU_SYNAPROM_PRODUCT_PROMETHEUS);
return NULL;
}
}
/* success */
return g_steal_pointer(&firmware);
}
gboolean
fu_synaprom_device_write_fw(FuSynapromDevice *self, GBytes *fw, GError **error)
{
const guint8 *buf;
gsize sz = 0;
/* write chunks */
fu_device_set_progress(FU_DEVICE(self), 10);
fu_device_set_status(FU_DEVICE(self), FWUPD_STATUS_DEVICE_WRITE);
buf = g_bytes_get_data(fw, &sz);
while (sz != 0) {
guint32 chunksz;
g_autoptr(GByteArray) chunk = g_byte_array_new();
/* get chunk size */
if (sz < sizeof(guint32)) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"No enough data for patch len");
return FALSE;
}
memcpy(&chunksz, buf, sizeof(guint32));
buf += sizeof(guint32);
sz -= sizeof(guint32);
if (sz < chunksz) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"No enough data for patch chunk");
return FALSE;
}
/* download chunk */
g_byte_array_append(chunk, buf, chunksz);
if (!fu_synaprom_device_cmd_download_chunk(self, chunk, error))
return FALSE;
/* next chunk */
buf += chunksz;
sz -= chunksz;
}
/* success! */
fu_device_set_progress(FU_DEVICE(self), 100);
return TRUE;
}
static gboolean
fu_synaprom_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuSynapromDevice *self = FU_SYNAPROM_DEVICE(device);
g_autoptr(GBytes) fw = NULL;
/* get default image */
fw = fu_firmware_get_image_by_id_bytes(firmware, "mfw-update-payload", error);
if (fw == NULL)
return FALSE;
return fu_synaprom_device_write_fw(self, fw, error);
}
static gboolean
fu_synaprom_device_attach(FuDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
gboolean ret;
gsize actual_len = 0;
guint8 data[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
/* sanity check */
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
g_debug("already in runtime mode, skipping");
return TRUE;
}
ret = g_usb_device_control_transfer(usb_device,
G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_VENDOR,
G_USB_DEVICE_RECIPIENT_DEVICE,
FU_SYNAPROM_USB_CTRLREQUEST_VENDOR_WRITEDFT,
0x0000,
0x0000,
data,
sizeof(data),
&actual_len,
2000,
NULL,
error);
if (!ret)
return FALSE;
if (actual_len != sizeof(data)) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only sent 0x%04x of 0x%04x",
(guint)actual_len,
(guint)sizeof(data));
return FALSE;
}
fu_device_set_status(device, FWUPD_STATUS_DEVICE_RESTART);
if (!g_usb_device_reset(usb_device, error)) {
g_prefix_error(error, "failed to force-reset device: ");
return FALSE;
}
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
static gboolean
fu_synaprom_device_detach(FuDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
gboolean ret;
gsize actual_len = 0;
guint8 data[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00};
/* sanity check */
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
g_debug("already in bootloader mode, skipping");
return TRUE;
}
ret = g_usb_device_control_transfer(usb_device,
G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_VENDOR,
G_USB_DEVICE_RECIPIENT_DEVICE,
FU_SYNAPROM_USB_CTRLREQUEST_VENDOR_WRITEDFT,
0x0000,
0x0000,
data,
sizeof(data),
&actual_len,
2000,
NULL,
error);
if (!ret)
return FALSE;
if (actual_len != sizeof(data)) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only sent 0x%04x of 0x%04x",
(guint)actual_len,
(guint)sizeof(data));
return FALSE;
}
fu_device_set_status(device, FWUPD_STATUS_DEVICE_RESTART);
if (!g_usb_device_reset(usb_device, error)) {
g_prefix_error(error, "failed to force-reset device: ");
return FALSE;
}
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
static void
fu_synaprom_device_init(FuSynapromDevice *self)
{
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY);
fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_RETRY_OPEN);
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET);
fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.prometheus");
fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
fu_device_set_name(FU_DEVICE(self), "Prometheus");
fu_device_set_summary(FU_DEVICE(self), "Fingerprint reader");
fu_device_set_vendor(FU_DEVICE(self), "Synaptics");
fu_device_add_icon(FU_DEVICE(self), "touchpad-disabled");
}
static void
fu_synaprom_device_class_init(FuSynapromDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
klass_device->write_firmware = fu_synaprom_device_write_firmware;
klass_device->prepare_firmware = fu_synaprom_device_prepare_fw;
klass_device->setup = fu_synaprom_device_setup;
klass_device->reload = fu_synaprom_device_setup;
klass_device->attach = fu_synaprom_device_attach;
klass_device->detach = fu_synaprom_device_detach;
klass_device->open = fu_synaprom_device_open;
}
FuSynapromDevice *
fu_synaprom_device_new(FuUsbDevice *device)
{
FuSynapromDevice *self;
self = g_object_new(FU_TYPE_SYNAPROM_DEVICE, NULL);
if (device != NULL)
fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(device));
return FU_SYNAPROM_DEVICE(self);
}