/* * Copyright (C) 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include "fu-logitech-hidpp-bootloader.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-hidpp.h" typedef struct { guint16 flash_addr_lo; guint16 flash_addr_hi; guint16 flash_blocksize; } FuLogitechHidPpBootloaderPrivate; #define FU_UNIFYING_DEVICE_EP1 0x81 #define FU_UNIFYING_DEVICE_EP3 0x83 G_DEFINE_TYPE_WITH_PRIVATE(FuLogitechHidPpBootloader, fu_logitech_hidpp_bootloader, FU_TYPE_HID_DEVICE) #define GET_PRIVATE(o) (fu_logitech_hidpp_bootloader_get_instance_private(o)) static void fu_logitech_hidpp_bootloader_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechHidPpBootloader *self = FU_UNIFYING_BOOTLOADER(device); FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE(self); fu_common_string_append_kx(str, idt, "FlashAddrHigh", priv->flash_addr_hi); fu_common_string_append_kx(str, idt, "FlashAddrLow", priv->flash_addr_lo); fu_common_string_append_kx(str, idt, "FlashBlockSize", priv->flash_blocksize); } FuLogitechHidPpBootloaderRequest * fu_logitech_hidpp_bootloader_request_new(void) { FuLogitechHidPpBootloaderRequest *req = g_new0(FuLogitechHidPpBootloaderRequest, 1); return req; } GPtrArray * fu_logitech_hidpp_bootloader_parse_requests(FuLogitechHidPpBootloader *self, GBytes *fw, GError **error) { const gchar *tmp; g_auto(GStrv) lines = NULL; g_autoptr(GPtrArray) reqs = NULL; guint32 last_addr = 0; reqs = g_ptr_array_new_with_free_func(g_free); tmp = g_bytes_get_data(fw, NULL); lines = g_strsplit_set(tmp, "\n\r", -1); for (guint i = 0; lines[i] != NULL; i++) { g_autoptr(FuLogitechHidPpBootloaderRequest) payload = NULL; guint8 rec_type = 0x00; guint16 offset = 0x0000; guint16 addr = 0x0; gboolean exit = FALSE; gsize linesz = strlen(lines[i]); /* skip empty lines */ tmp = lines[i]; if (linesz < 5) continue; payload = fu_logitech_hidpp_bootloader_request_new(); payload->len = fu_logitech_hidpp_buffer_read_uint8(tmp + 0x01); if (payload->len > 28) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware data invalid: too large %u bytes", payload->len); return NULL; } if (!fu_firmware_strparse_uint16_safe(tmp, linesz, 0x03, &addr, error)) return NULL; payload->addr = addr; payload->cmd = FU_UNIFYING_BOOTLOADER_CMD_WRITE_RAM_BUFFER; rec_type = fu_logitech_hidpp_buffer_read_uint8(tmp + 0x07); switch (rec_type) { case 0x00: /* data */ break; case 0x01: /* EOF */ exit = TRUE; break; case 0x03: /* start segment address */ /* this is used to specify the start address, it is doesn't matter in this context so we can safely ignore it */ continue; case 0x04: /* extended linear address */ if (!fu_firmware_strparse_uint16_safe(tmp, linesz, 0x09, &offset, error)) return NULL; if (offset != 0x0000) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "extended linear addresses with offset different from " "0 are not supported"); return NULL; } continue; case 0x05: /* start linear address */ /* this is used to specify the start address, it is doesn't matter in this context so we can safely ignore it */ continue; case 0xFD: /* custom - vendor */ /* record type of 0xFD indicates signature data */ payload->cmd = FU_UNIFYING_BOOTLOADER_CMD_WRITE_SIGNATURE; break; default: g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "intel hex file record type %02x not supported", rec_type); return NULL; } if (exit) break; /* read the data, but skip the checksum byte */ for (guint j = 0; j < payload->len; j++) { const gchar *ptr = tmp + 0x09 + (j * 2); if (ptr[0] == '\0') { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware data invalid: expected %u bytes", payload->len); return NULL; } payload->data[j] = fu_logitech_hidpp_buffer_read_uint8(ptr); } /* no need to bound check signature addresses */ if (payload->cmd == FU_UNIFYING_BOOTLOADER_CMD_WRITE_SIGNATURE) { g_ptr_array_add(reqs, g_steal_pointer(&payload)); continue; } /* skip the bootloader */ if (payload->addr > fu_logitech_hidpp_bootloader_get_addr_hi(self)) { g_debug("skipping write @ %04x", payload->addr); continue; } /* skip the header */ if (payload->addr < fu_logitech_hidpp_bootloader_get_addr_lo(self)) { g_debug("skipping write @ %04x", payload->addr); continue; } /* make sure firmware addresses only go up */ if (payload->addr < last_addr) { g_debug("skipping write @ %04x", payload->addr); continue; } last_addr = payload->addr; /* pending */ g_ptr_array_add(reqs, g_steal_pointer(&payload)); } if (reqs->len == 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware data invalid: no payloads found"); return NULL; } return g_steal_pointer(&reqs); } guint16 fu_logitech_hidpp_bootloader_get_addr_lo(FuLogitechHidPpBootloader *self) { FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UNIFYING_BOOTLOADER(self), 0x0000); return priv->flash_addr_lo; } guint16 fu_logitech_hidpp_bootloader_get_addr_hi(FuLogitechHidPpBootloader *self) { FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UNIFYING_BOOTLOADER(self), 0x0000); return priv->flash_addr_hi; } guint16 fu_logitech_hidpp_bootloader_get_blocksize(FuLogitechHidPpBootloader *self) { FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UNIFYING_BOOTLOADER(self), 0x0000); return priv->flash_blocksize; } static gboolean fu_logitech_hidpp_bootloader_attach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidPpBootloader *self = FU_UNIFYING_BOOTLOADER(device); g_autoptr(FuLogitechHidPpBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_UNIFYING_BOOTLOADER_CMD_REBOOT; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to attach back to runtime: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_logitech_hidpp_bootloader_set_bl_version(FuLogitechHidPpBootloader *self, GError **error) { guint16 build; guint8 major; guint8 minor; g_autofree gchar *version = NULL; g_autoptr(FuLogitechHidPpBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); /* call into hardware */ req->cmd = FU_UNIFYING_BOOTLOADER_CMD_GET_BL_VERSION; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to get firmware version: "); return FALSE; } /* BOTxx.yy_Bzzzz * 012345678901234 */ build = (guint16)fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 10) << 8; build += fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 12); major = fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 3); minor = fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 6); version = fu_logitech_hidpp_format_version("BOT", major, minor, build); if (version == NULL) { g_prefix_error(error, "failed to format firmware version: "); return FALSE; } fu_device_set_version_bootloader(FU_DEVICE(self), version); if ((major == 0x01 && minor >= 0x04) || (major == 0x03 && minor >= 0x02)) { fu_device_add_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned"); } else { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying"); } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_open(FuDevice *device, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); const guint idx = 0x00; /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_logitech_hidpp_bootloader_parent_class)->open(device, error)) return FALSE; /* claim the only interface */ if (!g_usb_device_claim_interface(usb_device, idx, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "Failed to claim 0x%02x: ", idx); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_hidpp_bootloader_setup(FuDevice *device, GError **error) { FuLogitechHidPpBootloader *self = FU_UNIFYING_BOOTLOADER(device); FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE(self); g_autoptr(FuLogitechHidPpBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_hidpp_bootloader_parent_class)->setup(device, error)) return FALSE; /* get memory map */ req->cmd = FU_UNIFYING_BOOTLOADER_CMD_GET_MEMINFO; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to get meminfo: "); return FALSE; } if (req->len != 0x06) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to get meminfo: invalid size %02x", req->len); return FALSE; } /* parse values */ priv->flash_addr_lo = fu_common_read_uint16(req->data + 0, G_BIG_ENDIAN); priv->flash_addr_hi = fu_common_read_uint16(req->data + 2, G_BIG_ENDIAN); priv->flash_blocksize = fu_common_read_uint16(req->data + 4, G_BIG_ENDIAN); /* get bootloader version */ return fu_logitech_hidpp_bootloader_set_bl_version(self, error); } static gboolean fu_logitech_hidpp_bootloader_close(FuDevice *device, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); if (usb_device != NULL) { if (!g_usb_device_release_interface(usb_device, 0x00, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { return FALSE; } } /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_logitech_hidpp_bootloader_parent_class)->close(device, error); } gboolean fu_logitech_hidpp_bootloader_request(FuLogitechHidPpBootloader *self, FuLogitechHidPpBootloaderRequest *req, GError **error) { GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); gsize actual_length = 0; guint8 buf_request[32]; guint8 buf_response[32]; /* build packet */ memset(buf_request, 0x00, sizeof(buf_request)); buf_request[0x00] = req->cmd; buf_request[0x01] = req->addr >> 8; buf_request[0x02] = req->addr & 0xff; buf_request[0x03] = req->len; if (!fu_memcpy_safe(buf_request, sizeof(buf_request), 0x04, /* dst */ req->data, sizeof(req->data), 0x0, /* src */ sizeof(req->data), error)) return FALSE; /* send request */ if (g_getenv("FWUPD_LOGITECH_HIDPP_VERBOSE") != NULL) { fu_common_dump_raw(G_LOG_DOMAIN, "host->device", buf_request, sizeof(buf_request)); } if (usb_device != NULL) { if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, buf_request, sizeof(buf_request), FU_UNIFYING_DEVICE_TIMEOUT_MS, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send data: "); return FALSE; } } /* no response required when rebooting */ if (usb_device != NULL && req->cmd == FU_UNIFYING_BOOTLOADER_CMD_REBOOT) { g_autoptr(GError) error_ignore = NULL; if (!g_usb_device_interrupt_transfer(usb_device, FU_UNIFYING_DEVICE_EP1, buf_response, sizeof(buf_response), &actual_length, FU_UNIFYING_DEVICE_TIMEOUT_MS, NULL, &error_ignore)) { g_debug("ignoring: %s", error_ignore->message); } else { if (g_getenv("FWUPD_LOGITECH_HIDPP_VERBOSE") != NULL) { fu_common_dump_raw(G_LOG_DOMAIN, "device->host", buf_response, actual_length); } } return TRUE; } /* get response */ memset(buf_response, 0x00, sizeof(buf_response)); if (usb_device != NULL) { if (!g_usb_device_interrupt_transfer(usb_device, FU_UNIFYING_DEVICE_EP1, buf_response, sizeof(buf_response), &actual_length, FU_UNIFYING_DEVICE_TIMEOUT_MS, NULL, error)) { g_prefix_error(error, "failed to get data: "); return FALSE; } } else { /* emulated */ buf_response[0] = buf_request[0]; if (buf_response[0] == FU_UNIFYING_BOOTLOADER_CMD_GET_MEMINFO) { buf_response[3] = 0x06; /* len */ buf_response[4] = 0x40; /* lo MSB */ buf_response[5] = 0x00; /* lo LSB */ buf_response[6] = 0x6b; /* hi MSB */ buf_response[7] = 0xff; /* hi LSB */ buf_response[8] = 0x00; /* bs MSB */ buf_response[9] = 0x80; /* bs LSB */ } actual_length = sizeof(buf_response); } if (g_getenv("FWUPD_LOGITECH_HIDPP_VERBOSE") != NULL) { fu_common_dump_raw(G_LOG_DOMAIN, "device->host", buf_response, actual_length); } /* parse response */ if ((buf_response[0x00] & 0xf0) != req->cmd) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid command response of %02x, expected %02x", buf_response[0x00], req->cmd); return FALSE; } req->cmd = buf_response[0x00]; req->addr = ((guint16)buf_response[0x01] << 8) + buf_response[0x02]; req->len = buf_response[0x03]; if (req->len > 28) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid data size of %02x", req->len); return FALSE; } memset(req->data, 0x00, 28); if (req->len > 0) memcpy(req->data, buf_response + 0x04, req->len); return TRUE; } static void fu_logitech_hidpp_bootloader_init(FuLogitechHidPpBootloader *self) { 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_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID); fu_device_add_icon(FU_DEVICE(self), "preferences-desktop-keyboard"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_name(FU_DEVICE(self), "Unifying Receiver"); fu_device_set_summary(FU_DEVICE(self), "Miniaturised USB wireless receiver (bootloader)"); fu_device_set_remove_delay(FU_DEVICE(self), FU_UNIFYING_DEVICE_TIMEOUT_MS); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED, "is-signed"); } static void fu_logitech_hidpp_bootloader_class_init(FuLogitechHidPpBootloaderClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); klass_device->to_string = fu_logitech_hidpp_bootloader_to_string; klass_device->attach = fu_logitech_hidpp_bootloader_attach; klass_device->setup = fu_logitech_hidpp_bootloader_setup; klass_device->open = fu_logitech_hidpp_bootloader_open; klass_device->close = fu_logitech_hidpp_bootloader_close; }