diff --git a/contrib/ci/oss-fuzz.py b/contrib/ci/oss-fuzz.py index 981c33d95..11914c291 100755 --- a/contrib/ci/oss-fuzz.py +++ b/contrib/ci/oss-fuzz.py @@ -214,7 +214,9 @@ class Fuzzer: def _build(bld: Builder) -> None: # GLib - src = bld.checkout_source("glib", url="https://gitlab.gnome.org/GNOME/glib.git") + src = bld.checkout_source( + "glib", url="https://gitlab.gnome.org/GNOME/glib.git", commit="glib-2-68" + ) bld.build_meson_project( src, [ @@ -321,6 +323,7 @@ def _build(bld: Builder) -> None: Fuzzer("redfish-smbios", srcdir="redfish", pattern="redfish-smbios"), Fuzzer("solokey"), Fuzzer("synaprom", srcdir="synaptics-prometheus"), + Fuzzer("synaptics-cape"), Fuzzer("synaptics-mst"), Fuzzer("synaptics-rmi"), Fuzzer("wacom-usb", pattern="wac-firmware", globstr="wacom*"), diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index 026149cb7..a5f7441a3 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -461,6 +461,7 @@ done %if 0%{?have_dell} %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_synaptics_mst.so %endif +%{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_synaptics_cape.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_synaptics_cxaudio.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_synaptics_prometheus.so %{_libdir}/fwupd-plugins-%{fwupdplugin_version}/libfu_plugin_synaptics_rmi.so diff --git a/plugins/meson.build b/plugins/meson.build index 4df17e769..f857f9760 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -54,6 +54,7 @@ subdir('rts54hub') subdir('solokey') subdir('steelseries') subdir('superio') +subdir('synaptics-cape') subdir('synaptics-cxaudio') subdir('synaptics-mst') subdir('synaptics-prometheus') diff --git a/plugins/synaptics-cape/README.md b/plugins/synaptics-cape/README.md new file mode 100644 index 000000000..d8d9b91a2 --- /dev/null +++ b/plugins/synaptics-cape/README.md @@ -0,0 +1,41 @@ +# Synaptics CAPE devices + +## Introduction + +This plugin is used to update Synaptics CAPE based audio devices. + +## Firmware Format + +The daemon will decompress the cabinet archive and extract a firmware blob. + +This plugin supports the following protocol ID: + +* com.synaptics.cape + +## GUID Generation + +These devices use the standard USB DeviceInstanceId values, e.g. + +* `USB\VID_1395&PID_0293` + +These devices also use custom GUID values, e.g. + +* `SYNAPTICS_CAPE\CX31993` +* `SYNAPTICS_CAPE\CX31988` + +## Update Behavior + +The firmware is deployed when the device is in normal runtime mode, and the +device will reset when the new firmware has been written. + +## Vendor ID Security + +The vendor ID is set from the USB vendor, in this instance set to `USB:0x1395` + +## Quirk Use + +This plugin uses no plugin-specific quirks. + +## External Interface Access + +This plugin requires read/write access to `/dev/bus/usb`. diff --git a/plugins/synaptics-cape/fu-plugin-synaptics-cape.c b/plugins/synaptics-cape/fu-plugin-synaptics-cape.c new file mode 100644 index 000000000..c6af42938 --- /dev/null +++ b/plugins/synaptics-cape/fu-plugin-synaptics-cape.c @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Synaptics Incorporated + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "fu-synaptics-cape-device.h" +#include "fu-synaptics-cape-firmware.h" + +void +fu_plugin_init(FuPlugin *plugin) +{ + fu_plugin_set_build_hash(plugin, FU_BUILD_HASH); + fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_CAPE_DEVICE); + fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_CAPE_FIRMWARE); +} diff --git a/plugins/synaptics-cape/fu-synaptics-cape-device.c b/plugins/synaptics-cape/fu-synaptics-cape-device.c new file mode 100644 index 000000000..12557c4ba --- /dev/null +++ b/plugins/synaptics-cape/fu-synaptics-cape-device.c @@ -0,0 +1,673 @@ +/* + * Copyright (C) 2021 Synaptics Incorporated + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include + +#include "fu-synaptics-cape-device.h" +#include "fu-synaptics-cape-firmware.h" + +/* defines timings */ +#define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_WRITE_TIMEOUT 20000 /* us */ +#define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_READ_TIMEOUT 30000 /* us */ +#define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_INTERVAL 10 /* ms */ +#define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_TIMEOUT 300 /* ms */ +#define FU_SYNAPTICS_CAPE_DEVICE_USB_RESET_DELAY_MS 3000 /* ms */ + +/* define CAPE command constant values and macro */ +#define FU_SYNAPTICS_CAPE_DEVICE_GOLEM_REPORT_ID 1 /* HID report id */ + +#define FU_SYNAPTICS_CAPE_CMD_MAX_DATA_LEN 13 /* number of guint32 */ +#define FU_SYNAPTICS_CAPE_CMD_WRITE_DATAL_LEN 8 /* number of guint32 */ +#define FU_SYNAPTICS_CAPE_WORD_IN_BYTES 4 /* bytes */ + +#define FU_SYNAPTICS_CAPE_CMD_APP_ID(a, b, c, d) \ + ((((a)-0x20) << 8) | (((b)-0x20) << 14) | (((c)-0x20) << 20) | (((d)-0x20) << 26)) + +/* CAPE command return codes */ +#define FU_SYNAPTICS_CAPE_MODULE_RC_GENERIC_FAILURE (-1025) +#define FU_SYNAPTICS_CAPE_MODULE_RC_ALREADY_EXISTS (-1026) +#define FU_SYNAPTICS_CAPE_MODULE_RC_NULL_APP_POINTER (-1027) +#define FU_SYNAPTICS_CAPE_MODULE_RC_NULL_MODULE_POINTER (-1028) +#define FU_SYNAPTICS_CAPE_MODULE_RC_NULL_STREAM_POINTER (-1029) +#define FU_SYNAPTICS_CAPE_MODULE_RC_NULL_POINTER (-1030) + +#define FU_SYNAPTICS_CAPE_MODULE_RC_BAD_APP_ID (-1031) +#define FU_SYNAPTICS_CAPE_MODULE_RC_MODULE_TYPE_HAS_NO_API (-1034) +#define FU_SYNAPTICS_CAPE_MODULE_RC_BAD_MAGIC_NUMBER (-1052) +#define FU_SYNAPTICS_CAPE_MODULE_RC_CMD_MODE_UNSUPPORTED (-1056) + +#define FU_SYNAPTICS_CMD_GET_FLAG 0x100 /* GET flag */ + +/* CAPE message structure, Little endian */ +typedef struct __attribute__((packed)) { + gint16 data_len : 16; /* data length in dwords */ + guint16 cmd_id : 15; /* Command id */ + guint16 reply : 1; /* Host want a reply from device, 1 = true */ + guint32 module_id; /* Module id */ + guint32 data[FU_SYNAPTICS_CAPE_CMD_MAX_DATA_LEN]; /* Command data */ +} FuCapCmd; + +/* CAPE HID report structure */ +typedef struct __attribute__((packed)) { + guint16 report_id; /* two bytes of report id, this should be 1 */ + FuCapCmd cmd; +} FuCapCmdHidReport; + +/* CAPE Commands */ +typedef enum { + FU_SYNAPTICS_CMD_FW_UPDATE_START = 0xC8, /* notifies firmware update started */ + FU_SYNAPTICS_CMD_FW_UPDATE_WRITE = 0xC9, /* updates firmware data */ + FU_SYNAPTICS_CMD_FW_UPDATE_END = 0xCA, /* notifies firmware update finished */ + FU_SYNAPTICS_CMD_MCU_SOFT_RESET = 0xAF, /* reset device*/ + FU_SYNAPTICS_CMD_FW_GET_ACTIVE_PARTITION = 0x1CF, /* gets cur active partition number */ + FU_SYNAPTICS_CMD_GET_VERSION = 0x103, /* gets cur firmware version */ +} FuCommand; + +/* CAPE Fuupd device structure */ +struct _FuSynapticsCapeDevice { + FuHidDevice parent_instance; + guint32 ActivePartition; /* active partition, either 1 or 2 */ +}; + +G_DEFINE_TYPE(FuSynapticsCapeDevice, fu_synaptics_cape_device, FU_TYPE_HID_DEVICE) + +/* Sends SET_REPORT to device */ +static gboolean +fu_synaptics_cape_device_set_report(FuSynapticsCapeDevice *self, + const FuCapCmdHidReport *data, + GError **error) +{ + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(data != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + if (g_getenv("FWUPD_SYNAPTICS_CAPE_HID_REPORT_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "SetReport", (guint8 *)data, sizeof(*data)); + + return fu_hid_device_set_report(FU_HID_DEVICE(self), + FU_SYNAPTICS_CAPE_DEVICE_GOLEM_REPORT_ID, + (guint8 *)data, + sizeof(*data), + FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_WRITE_TIMEOUT, + FU_HID_DEVICE_FLAG_NONE, + error); +} + +/* Gets data from device via GET_REPORT */ +static gboolean +fu_synaptics_cape_device_get_report(FuSynapticsCapeDevice *self, + FuCapCmdHidReport *data, + GError **error) +{ + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(data != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + if (!fu_hid_device_get_report(FU_HID_DEVICE(self), + FU_SYNAPTICS_CAPE_DEVICE_GOLEM_REPORT_ID, + (guint8 *)data, + sizeof(*data), + FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_READ_TIMEOUT, + FU_HID_DEVICE_FLAG_NONE, + error)) + return FALSE; + + if (g_getenv("FWUPD_SYNAPTICS_CAPE_HID_REPORT_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "GetReport", (guint8 *)data, sizeof(*data)); + + /* success */ + return TRUE; +} + +/* dump CAPE command error if any */ +static gboolean +fu_synaptics_cape_device_rc_set_error(const FuCapCmd *rsp, GError **error) +{ + g_return_val_if_fail(rsp != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + if (rsp->data_len >= 0) + return TRUE; + + switch (rsp->data_len) { + case FU_SYNAPTICS_CAPE_MODULE_RC_GENERIC_FAILURE: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: generic failure"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_ALREADY_EXISTS: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: already exists"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_NULL_APP_POINTER: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: null app pointer"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_NULL_MODULE_POINTER: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: null module pointer"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_NULL_POINTER: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: null pointer"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_BAD_APP_ID: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: bad app id"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_MODULE_TYPE_HAS_NO_API: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: has no api"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_BAD_MAGIC_NUMBER: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: bad magic number"); + break; + case FU_SYNAPTICS_CAPE_MODULE_RC_CMD_MODE_UNSUPPORTED: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BUSY, "CMD ERROR: mode unsupported"); + break; + default: + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_BUSY, + "CMD ERROR: unknown error: %d", + rsp->data_len); + } + + /* success */ + return FALSE; +} + +/* sends a FuCapCmd structure command to device to get the response in the same structure */ +static gboolean +fu_synaptics_cape_device_sendcmd_ex(FuSynapticsCapeDevice *self, + FuCapCmd *req, + gulong delay_us, + GError **error) +{ + FuCapCmdHidReport report; + guint elapsed_ms = 0; + gboolean is_get = FALSE; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(req != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* first two bytes are report id */ + report.report_id = GINT16_TO_LE(FU_SYNAPTICS_CAPE_DEVICE_GOLEM_REPORT_ID); + + if (!fu_memcpy_safe((guint8 *)&report.cmd, + sizeof(report.cmd), + 0, /* dst */ + (const guint8 *)req, + sizeof(*req), + 0, /* src */ + sizeof(*req), + error)) + return FALSE; + + /* sets data length to MAX for any GET commands */ + if (FU_SYNAPTICS_CMD_GET_FLAG & report.cmd.cmd_id) { + is_get = TRUE; + report.cmd.data_len = GINT16_TO_LE(FU_SYNAPTICS_CAPE_CMD_MAX_DATA_LEN); + } else { + report.cmd.data_len = GINT16_TO_LE(report.cmd.data_len); + } + + report.cmd.cmd_id = GUINT32_TO_LE(report.cmd.cmd_id); + report.cmd.module_id = GUINT32_TO_LE(report.cmd.module_id); + + if (!fu_synaptics_cape_device_set_report(self, &report, error)) { + g_prefix_error(error, "failed to send: "); + return FALSE; + } + if (delay_us > 0) + g_usleep(delay_us); + + /* wait for the command to complete */ + for (; elapsed_ms < FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_TIMEOUT; + elapsed_ms += FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_INTERVAL) { + if (!fu_synaptics_cape_device_get_report(self, &report, error)) + return FALSE; + if (report.cmd.reply) + break; + g_usleep(FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_INTERVAL * 1000); + } + + if (!report.cmd.reply) { + g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "send command time out"); + return FALSE; + } + + /* copy returned data if it is GET command */ + if (is_get) { + req->data_len = + (gint16)fu_common_read_uint16((guint8 *)&report.cmd, G_LITTLE_ENDIAN); + + for (int i = 0; i < FU_SYNAPTICS_CAPE_CMD_MAX_DATA_LEN; i++) + req->data[i] = GUINT32_FROM_LE(report.cmd.data[i]); + } + + return fu_synaptics_cape_device_rc_set_error(&report.cmd, error); +} + +/* a simple version of sendcmd_ex without returned data */ +static gboolean +fu_synaptics_cape_device_sendcmd(FuSynapticsCapeDevice *self, + const guint32 module_id, + const guint32 cmd_id, + const guint32 *data, + const guint32 data_len, + const gulong delay_us, + GError **error) +{ + FuCapCmd cmd = {0}; + const guint32 dataszbyte = data_len * FU_SYNAPTICS_CAPE_WORD_IN_BYTES; + + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + cmd.cmd_id = cmd_id; + cmd.module_id = module_id; + + if (data_len != 0 && data != NULL) { + cmd.data_len = data_len; + if (!fu_memcpy_safe((guint8 *)cmd.data, + sizeof(cmd.data), + 0, /* dst */ + (const guint8 *)data, + dataszbyte, + 0, /* src */ + dataszbyte, + error)) + return FALSE; + } + return fu_synaptics_cape_device_sendcmd_ex(self, &cmd, delay_us, error); +} + +static void +fu_synaptics_cape_device_to_string(FuDevice *device, guint idt, GString *str) +{ + FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); + + g_return_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self)); + + fu_common_string_append_ku(str, idt, "active_partition", self->ActivePartition); +} + +/* reset device */ +static gboolean +fu_synaptics_cape_device_reset(FuSynapticsCapeDevice *self, GError **error) +{ + g_autoptr(GTimer) timer = g_timer_new(); + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + if (!fu_synaptics_cape_device_sendcmd(self, + FU_SYNAPTICS_CAPE_CMD_APP_ID('C', 'T', 'R', 'L'), + FU_SYNAPTICS_CMD_MCU_SOFT_RESET, + NULL, + 0, + 0, + error)) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "reset command is not supported"); + + return FALSE; + } + + g_usleep(1000 * FU_SYNAPTICS_CAPE_DEVICE_USB_RESET_DELAY_MS); + + g_debug("reset took %.2lfms", g_timer_elapsed(timer, NULL) * 1000); + + /* success */ + return TRUE; +} + +/** + * fu_synaptics_cape_device_get_active_partition: + * @self: a #FuSynapticsCapeDevice + * @error: return location for an error + * + * Updates active partition information to FuSynapticsCapeDevice::ActivePartition + * + * Returns: returns TRUE if operation is successful, otherwise, return FALSE if + * unsuccessful. + * + **/ +static gboolean +fu_synaptics_cape_device_setup_active_partition(FuSynapticsCapeDevice *self, GError **error) +{ + FuCapCmd cmd = {0}; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + cmd.cmd_id = FU_SYNAPTICS_CMD_FW_GET_ACTIVE_PARTITION; + cmd.module_id = FU_SYNAPTICS_CAPE_CMD_APP_ID('C', 'T', 'R', 'L'); + + if (!fu_synaptics_cape_device_sendcmd_ex(self, &cmd, 0, error)) + return FALSE; + + self->ActivePartition = GUINT32_FROM_LE(cmd.data[0]); + + if (self->ActivePartition != 1 && self->ActivePartition != 2) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "partition number out of range, returned partition number is %u", + self->ActivePartition); + return FALSE; + } + + /* success */ + return TRUE; +} + +/* gets version number from device and saves to FU_DEVICE */ +static gboolean +fu_synaptics_cape_device_setup_version(FuSynapticsCapeDevice *self, GError **error) +{ + guint32 version_raw; + FuCapCmd cmd = {0}; + g_autofree gchar *version_str = NULL; + + cmd.cmd_id = GUINT32_TO_LE(FU_SYNAPTICS_CMD_GET_VERSION); + cmd.module_id = FU_SYNAPTICS_CAPE_CMD_APP_ID('C', 'T', 'R', 'L'); + cmd.data_len = 4; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* gets version number from device */ + fu_synaptics_cape_device_sendcmd_ex(self, &cmd, 0, error); + + /* The version number are stored in lowest byte of a sequence of returned data */ + version_raw = + (GUINT32_FROM_LE(cmd.data[0]) << 24) | ((GUINT32_FROM_LE(cmd.data[1]) & 0xFF) << 16) | + ((GUINT32_FROM_LE(cmd.data[2]) & 0xFF) << 8) | (GUINT32_FROM_LE(cmd.data[3]) & 0xFF); + + version_str = fu_common_version_from_uint32(version_raw, FWUPD_VERSION_FORMAT_QUAD); + fu_device_set_version(FU_DEVICE(self), version_str); + fu_device_set_version_raw(FU_DEVICE(self), version_raw); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); + + /* success */ + return TRUE; +} + +static gboolean +fu_synaptics_cape_device_setup(FuDevice *device, GError **error) +{ + FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* FuUsbDevice->setup */ + if (!FU_DEVICE_CLASS(fu_synaptics_cape_device_parent_class)->setup(device, error)) + return FALSE; + + if (!fu_synaptics_cape_device_setup_version(self, error)) { + g_prefix_error(error, "failed to get firmware version info: "); + return FALSE; + } + + if (!fu_synaptics_cape_device_setup_active_partition(self, error)) { + g_prefix_error(error, "failed to get active partition info: "); + return FALSE; + } + + /* success */ + return TRUE; +} + +static FuFirmware * +fu_synaptics_cape_device_prepare_firmware(FuDevice *device, + GBytes *fw, + FwupdInstallFlags flags, + GError **error) +{ + FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); + GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self)); + g_autoptr(FuFirmware) firmware = fu_synaptics_cape_firmware_new(); + gsize offset = 0; + g_autoptr(GBytes) new_fw = NULL; + + /* the "fw" includes two firmware data for each partition, we need to divide 'fw' into + * two equal parts. + */ + gsize bufsz = g_bytes_get_size(fw); + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), NULL); + g_return_val_if_fail(usb_device != NULL, NULL); + g_return_val_if_fail(fw != NULL, NULL); + g_return_val_if_fail(firmware != NULL, NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + + if ((guint32)bufsz % 4 != 0) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "data not aligned to 32 bits"); + return NULL; + } + + /* check file size */ + if (bufsz < FW_CAPE_HID_HEADER_SIZE * 2) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "file size is too small"); + return NULL; + } + + /* use second partition if active partition is 1 */ + if (self->ActivePartition == 1) + offset = bufsz / 2; + + new_fw = g_bytes_new_from_bytes(fw, offset, bufsz / 2); + + if (!fu_firmware_parse(firmware, new_fw, flags, error)) + return NULL; + + /* verify if correct device */ + if ((flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) == 0) { + const guint16 vid = + fu_synaptics_cape_firmware_get_vid(FU_SYNAPTICS_CAPE_FIRMWARE(firmware)); + const guint16 pid = + fu_synaptics_cape_firmware_get_pid(FU_SYNAPTICS_CAPE_FIRMWARE(firmware)); + if (vid != 0x0 && pid != 0x0 && + (g_usb_device_get_vid(usb_device) != vid || + g_usb_device_get_pid(usb_device) != pid)) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "USB vendor or product incorrect, " + "got: %04X:%04X expected %04X:%04X", + vid, + pid, + g_usb_device_get_vid(usb_device), + g_usb_device_get_pid(usb_device)); + return NULL; + } + } + + /* success */ + return g_steal_pointer(&firmware); +} + +/* sends firmware header to device */ +static gboolean +fu_synaptics_cape_device_write_firmware_header(FuSynapticsCapeDevice *self, + GBytes *fw, + GError **error) +{ + const guint8 *buf = NULL; + gsize bufsz = 0; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(fw != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + buf = g_bytes_get_data(fw, &bufsz); + + /* check size */ + if (bufsz != 20) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "firmware header is not 20 bytes"); + return FALSE; + } + + return fu_synaptics_cape_device_sendcmd(self, + FU_SYNAPTICS_CAPE_CMD_APP_ID('C', 'T', 'R', 'L'), + FU_SYNAPTICS_CMD_FW_UPDATE_START, + (const guint32 *)buf, + bufsz / FU_SYNAPTICS_CAPE_WORD_IN_BYTES, + 0, + error); +} + +/* sends firmware image to device */ +static gboolean +fu_synaptics_cape_device_write_firmware_image(FuSynapticsCapeDevice *self, + GBytes *fw, + FuProgress *progress, + GError **error) +{ + g_autoptr(GPtrArray) chunks = NULL; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(fw != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + chunks = fu_chunk_array_new_from_bytes(fw, + 0x00, + 0x00, + FU_SYNAPTICS_CAPE_WORD_IN_BYTES * + FU_SYNAPTICS_CAPE_CMD_WRITE_DATAL_LEN); + + fu_progress_set_id(progress, G_STRLOC); + fu_progress_set_steps(progress, chunks->len); + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + if (!fu_synaptics_cape_device_sendcmd( + self, + FU_SYNAPTICS_CAPE_CMD_APP_ID('C', 'T', 'R', 'L'), + FU_SYNAPTICS_CMD_FW_UPDATE_WRITE, + (const guint32 *)fu_chunk_get_data(chk), + fu_chunk_get_data_sz(chk) / FU_SYNAPTICS_CAPE_WORD_IN_BYTES, + 0, + error)) { + g_prefix_error(error, "failed send on chk %u: ", i); + return FALSE; + } + fu_progress_step_done(progress); + } + + /* success */ + return TRUE; +} + +/* performs firmware update */ +static gboolean +fu_synaptics_cape_device_write_firmware(FuDevice *device, + FuFirmware *firmware, + FuProgress *progress, + FwupdInstallFlags flags, + GError **error) +{ + FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); + g_autoptr(GBytes) fw = NULL; + g_autoptr(GBytes) fw_header = NULL; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); + g_return_val_if_fail(firmware != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* progress */ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2); /* header */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 69); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 0); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 29); + + fw_header = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_HEADER, error); + if (fw_header == NULL) + return FALSE; + if (!fu_synaptics_cape_device_write_firmware_header(self, fw_header, error)) { + g_prefix_error(error, "update header failed: "); + return FALSE; + } + fu_progress_step_done(progress); + + /* perform the actual write */ + fw = fu_firmware_get_bytes(firmware, error); + if (fw == NULL) + return FALSE; + if (!fu_synaptics_cape_device_write_firmware_image(self, + fw, + fu_progress_get_child(progress), + error)) { + g_prefix_error(error, "update image failed: "); + return FALSE; + } + fu_progress_step_done(progress); + + /* verify the firmware image */ + if (!fu_synaptics_cape_device_sendcmd(self, + FU_SYNAPTICS_CAPE_CMD_APP_ID('C', 'T', 'R', 'L'), + FU_SYNAPTICS_CMD_FW_UPDATE_END, + NULL, + 0, + 0, + error)) { + g_prefix_error(error, "failed to verify firmware: "); + return FALSE; + } + fu_progress_step_done(progress); + + /* send software reset to run available flash code */ + if (!fu_synaptics_cape_device_reset(self, error)) + return FALSE; + fu_progress_step_done(progress); + + /* success */ + fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); + return TRUE; +} + +static void +fu_synaptics_cape_device_set_progress(FuDevice *self, FuProgress *progress) +{ + fu_progress_set_id(progress, G_STRLOC); + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2); /* detach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94); /* write */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2); /* attach */ + fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2); /* reload */ +} + +static void +fu_synaptics_cape_device_init(FuSynapticsCapeDevice *self) +{ + fu_device_add_icon(FU_DEVICE(self), "audio-card"); + fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); + fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); + fu_device_set_install_duration(FU_DEVICE(self), 3); /* seconds */ + fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.cape"); + fu_device_retry_set_delay(FU_DEVICE(self), 100); /* ms */ + fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); +} + +static void +fu_synaptics_cape_device_class_init(FuSynapticsCapeDeviceClass *klass) +{ + FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); + klass_device->to_string = fu_synaptics_cape_device_to_string; + klass_device->setup = fu_synaptics_cape_device_setup; + klass_device->write_firmware = fu_synaptics_cape_device_write_firmware; + klass_device->prepare_firmware = fu_synaptics_cape_device_prepare_firmware; + klass_device->set_progress = fu_synaptics_cape_device_set_progress; +} diff --git a/plugins/synaptics-cape/fu-synaptics-cape-device.h b/plugins/synaptics-cape/fu-synaptics-cape-device.h new file mode 100644 index 000000000..ca7d9becc --- /dev/null +++ b/plugins/synaptics-cape/fu-synaptics-cape-device.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Synaptics Incorporated + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_SYNAPTICS_CAPE_DEVICE (fu_synaptics_cape_device_get_type()) +G_DECLARE_FINAL_TYPE(FuSynapticsCapeDevice, + fu_synaptics_cape_device, + FU, + SYNAPTICS_CAPE_DEVICE, + FuHidDevice) + +struct _FuSynapticsCapeDeviceClass { + FuHidDeviceClass parent_class; +}; diff --git a/plugins/synaptics-cape/fu-synaptics-cape-firmware.c b/plugins/synaptics-cape/fu-synaptics-cape-firmware.c new file mode 100644 index 000000000..702929d88 --- /dev/null +++ b/plugins/synaptics-cape/fu-synaptics-cape-firmware.c @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2021 Synaptics Incorporated + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include + +#include "fu-synaptics-cape-firmware.h" + +typedef struct __attribute__((packed)) { + guint32 data[8]; +} FuCapeHidFwCmdUpdateWritePar; + +struct _FuSynapticsCapeFirmware { + FuFirmware parent_instance; + guint16 vid; + guint16 pid; +}; + +/* Firmware update command structure, little endian */ +typedef struct __attribute__((packed)) { + guint32 vid; /* USB vendor id */ + guint32 pid; /* USB product id */ + guint32 fw_update_type; /* firmware update type */ + guint32 fw_signature; /* firmware identifier */ + guint32 crc_value; /* used to detect accidental changes to fw data */ +} FuCapeHidFwCmdUpdateStartPar; + +typedef struct __attribute__((packed)) { + FuCapeHidFwCmdUpdateStartPar par; + guint16 version_w; /* firmware version is four parts number "z.y.x.w", this is last part */ + guint16 version_x; /* firmware version, third part */ + guint16 version_y; /* firmware version, second part */ + guint16 version_z; /* firmware version, first part */ + guint32 reserved3; +} FuCapeHidFileHeader; + +G_DEFINE_TYPE(FuSynapticsCapeFirmware, fu_synaptics_cape_firmware, FU_TYPE_FIRMWARE) + +guint16 +fu_synaptics_cape_firmware_get_vid(FuSynapticsCapeFirmware *self) +{ + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self), 0); + return self->vid; +} + +guint16 +fu_synaptics_cape_firmware_get_pid(FuSynapticsCapeFirmware *self) +{ + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self), 0); + return self->pid; +} + +static void +fu_synaptics_cape_firmware_export(FuFirmware *firmware, + FuFirmwareExportFlags flags, + XbBuilderNode *bn) +{ + FuSynapticsCapeFirmware *self = FU_SYNAPTICS_CAPE_FIRMWARE(firmware); + fu_xmlb_builder_insert_kx(bn, "vid", self->vid); + fu_xmlb_builder_insert_kx(bn, "pid", self->pid); +} + +static gboolean +fu_synaptics_cape_firmware_parse_header(FuSynapticsCapeFirmware *self, + FuFirmware *firmware, + GBytes *fw, + GError **error) +{ + gsize bufsz = 0x0; + guint16 version_w = 0; + guint16 version_x = 0; + guint16 version_y = 0; + guint16 version_z = 0; + const guint8 *buf = g_bytes_get_data(fw, &bufsz); + g_autofree gchar *version_str = NULL; + g_autoptr(FuFirmware) img_hdr = fu_firmware_new(); + g_autoptr(GBytes) fw_hdr = NULL; + + g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self), FALSE); + g_return_val_if_fail(fw != NULL, FALSE); + g_return_val_if_fail(firmware != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* the input fw image size should be the same as header size */ + if (bufsz < sizeof(FuCapeHidFileHeader)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "not enough data to parse header"); + return FALSE; + } + + if (!fu_common_read_uint16_safe(buf, + bufsz, + FW_CAPE_HID_HEADER_OFFSET_VID, + &self->vid, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(buf, + bufsz, + FW_CAPE_HID_HEADER_OFFSET_PID, + &self->pid, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(buf, + bufsz, + FW_CAPE_HID_HEADER_OFFSET_VER_W, + &version_w, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(buf, + bufsz, + FW_CAPE_HID_HEADER_OFFSET_VER_X, + &version_x, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(buf, + bufsz, + FW_CAPE_HID_HEADER_OFFSET_VER_Y, + &version_y, + G_LITTLE_ENDIAN, + error)) + return FALSE; + if (!fu_common_read_uint16_safe(buf, + bufsz, + FW_CAPE_HID_HEADER_OFFSET_VER_Z, + &version_z, + G_LITTLE_ENDIAN, + error)) + return FALSE; + + version_str = g_strdup_printf("%u.%u.%u.%u", version_z, version_y, version_x, version_w); + fu_firmware_set_version(FU_FIRMWARE(self), version_str); + + fw_hdr = fu_common_bytes_new_offset(fw, 0, sizeof(FuCapeHidFwCmdUpdateStartPar), error); + if (fw_hdr == NULL) + return FALSE; + + fu_firmware_set_id(img_hdr, FU_FIRMWARE_ID_HEADER); + fu_firmware_set_bytes(img_hdr, fw_hdr); + fu_firmware_add_image(firmware, img_hdr); + + /* success */ + return TRUE; +} + +static gboolean +fu_synaptics_cape_firmware_parse(FuFirmware *firmware, + GBytes *fw, + guint64 addr_start, + guint64 addr_end, + FwupdInstallFlags flags, + GError **error) +{ + FuSynapticsCapeFirmware *self = FU_SYNAPTICS_CAPE_FIRMWARE(firmware); + const gsize bufsz = g_bytes_get_size(fw); + const gsize headsz = sizeof(FuCapeHidFileHeader); + g_autoptr(GBytes) fw_header = NULL; + g_autoptr(GBytes) fw_body = NULL; + + /* check minimum size */ + if (bufsz < sizeof(FuCapeHidFileHeader)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "not enough data to parse header, size "); + return FALSE; + } + + if ((guint32)bufsz % 4 != 0) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_FILE, + "data not aligned to 32 bits"); + return FALSE; + } + + fw_header = g_bytes_new_from_bytes(fw, 0x0, headsz); + if (!fu_synaptics_cape_firmware_parse_header(self, firmware, fw_header, error)) + return FALSE; + + fw_body = g_bytes_new_from_bytes(fw, headsz, bufsz - headsz); + fu_firmware_set_id(firmware, FU_FIRMWARE_ID_PAYLOAD); + fu_firmware_set_bytes(firmware, fw_body); + return TRUE; +} + +static void +fu_synaptics_cape_firmware_init(FuSynapticsCapeFirmware *self) +{ + fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); +} + +static void +fu_synaptics_cape_firmware_class_init(FuSynapticsCapeFirmwareClass *klass) +{ + FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass); + klass_firmware->parse = fu_synaptics_cape_firmware_parse; + klass_firmware->export = fu_synaptics_cape_firmware_export; +} + +FuFirmware * +fu_synaptics_cape_firmware_new(void) +{ + return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPTICS_CAPE_FIRMWARE, NULL)); +} diff --git a/plugins/synaptics-cape/fu-synaptics-cape-firmware.h b/plugins/synaptics-cape/fu-synaptics-cape-firmware.h new file mode 100644 index 000000000..4b17f71f5 --- /dev/null +++ b/plugins/synaptics-cape/fu-synaptics-cape-firmware.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 Synaptics Incorporated + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_SYNAPTICS_CAPE_FIRMWARE (fu_synaptics_cape_firmware_get_type()) + +G_DECLARE_FINAL_TYPE(FuSynapticsCapeFirmware, + fu_synaptics_cape_firmware, + FU, + SYNAPTICS_CAPE_FIRMWARE, + FuSrecFirmware) + +FuFirmware * +fu_synaptics_cape_firmware_new(void); + +guint16 +fu_synaptics_cape_firmware_get_vid(FuSynapticsCapeFirmware *self); + +guint16 +fu_synaptics_cape_firmware_get_pid(FuSynapticsCapeFirmware *self); + +#define FW_CAPE_HID_HEADER_OFFSET_VID 0x0 +#define FW_CAPE_HID_HEADER_OFFSET_PID 0x4 +#define FW_CAPE_HID_HEADER_OFFSET_UPDATE_TYPE 0x8 +#define FW_CAPE_HID_HEADER_OFFSET_SIGNATURE 0xc +#define FW_CAPE_HID_HEADER_OFFSET_CRC 0x10 +#define FW_CAPE_HID_HEADER_OFFSET_VER_W 0x14 +#define FW_CAPE_HID_HEADER_OFFSET_VER_X 0x16 +#define FW_CAPE_HID_HEADER_OFFSET_VER_Y 0x18 +#define FW_CAPE_HID_HEADER_OFFSET_VER_Z 0x1A + +#define FW_CAPE_HID_HEADER_SIZE 32 /* =sizeof(FuCapeHidFileHeader) */ diff --git a/plugins/synaptics-cape/meson.build b/plugins/synaptics-cape/meson.build new file mode 100644 index 000000000..c3c9f0ab3 --- /dev/null +++ b/plugins/synaptics-cape/meson.build @@ -0,0 +1,31 @@ +if get_option('gusb') +cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsCape"'] + +install_data(['synaptics-cape.quirk'], + install_dir: join_paths(datadir, 'fwupd', 'quirks.d') +) + +shared_module('fu_plugin_synaptics_cape', + fu_hash, + sources : [ + 'fu-plugin-synaptics-cape.c', + 'fu-synaptics-cape-device.c', + 'fu-synaptics-cape-firmware.c', # fuzzing + ], + include_directories : [ + root_incdir, + fwupd_incdir, + fwupdplugin_incdir, + ], + install : true, + install_dir: plugin_dir, + link_with : [ + fwupd, + fwupdplugin, + ], + c_args : cargs, + dependencies : [ + plugin_deps, + ], +) +endif diff --git a/plugins/synaptics-cape/synaptics-cape.quirk b/plugins/synaptics-cape/synaptics-cape.quirk new file mode 100644 index 000000000..ce595c419 --- /dev/null +++ b/plugins/synaptics-cape/synaptics-cape.quirk @@ -0,0 +1,78 @@ +# EPOS Raw Plus +[USB\VID_1395&PID_0280] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0281] +Guid = SYNAPTICS_CAPE\CX31993 + +# RAW Teams +[USB\VID_1395&PID_0294] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0295] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0296] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0297] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0298] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0299] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0400] +Guid = SYNAPTICS_CAPE\CX31993 + + +# EPOS Morgan-T +[USB\VID_1395&PID_0200] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0288] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0289] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_028A] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_028B] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_028C] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_028D] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_028E] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_028F] +Guid = SYNAPTICS_CAPE\CX31993 + +# EPOS Morgan-V +[USB\VID_1395&PID_0290] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0291] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0292] +Guid = SYNAPTICS_CAPE\CX31993 + +[USB\VID_1395&PID_0293] +Guid = SYNAPTICS_CAPE\CX31993 + +# USB audio codec Dongle +[SYNAPTICS_CAPE\CX31993] +Plugin = synaptics_cape + +# USB audio codec Hifi +[SYNAPTICS_CAPE\CX31988] +Plugin = synaptics_cape diff --git a/src/fuzzing/firmware/synaptics-cape.fw b/src/fuzzing/firmware/synaptics-cape.fw new file mode 100755 index 000000000..c9a33df5e Binary files /dev/null and b/src/fuzzing/firmware/synaptics-cape.fw differ diff --git a/src/fuzzing/generate.py b/src/fuzzing/generate.py index 5754e53cf..d6e598862 100755 --- a/src/fuzzing/generate.py +++ b/src/fuzzing/generate.py @@ -38,6 +38,7 @@ if __name__ == "__main__": ("srec-addr32.builder.xml", "srec-addr32.srec"), ("srec.builder.xml", "srec.srec"), ("synaprom.builder.xml", "synaprom.bin"), + ("synaptics-cape.builder.xml", "synaptics-cape.fw"), ("synaptics-mst.builder.xml", "synaptics-mst.dat"), ("wacom.builder.xml", "wacom.wac"), ]: diff --git a/src/fuzzing/synaptics-cape.builder.xml b/src/fuzzing/synaptics-cape.builder.xml new file mode 100644 index 000000000..fe8cea0df --- /dev/null +++ b/src/fuzzing/synaptics-cape.builder.xml @@ -0,0 +1,11 @@ + + has-vid-pid + payload + 8.41.24.0 + EFSCh... + + header + + + +