fwupd/plugins/ccgx/fu-ccgx-dmc-firmware.c
Richard Hughes 52441f28a4 Allow objects to deserialize to XML
This makes a lot more sense; we can parse a firmware and export the same XML
we would use in a .builder.xml file. This allows us to two two things:

 * Check we can round trip from XML -> binary -> XML

 * Using a .builder.xml file we can check ->write() is endian safe
2021-03-15 12:07:30 +00:00

481 lines
16 KiB
C

/*
* Copyright (C) 2020 Cypress Semiconductor Corporation.
* Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include "fu-chunk.h"
#include "fu-common.h"
#include "fu-common-version.h"
#include "fu-ccgx-dmc-common.h"
#include "fu-ccgx-common.h"
#include "fu-ccgx-dmc-firmware.h"
struct _FuCcgxDmcFirmware {
FuFirmwareClass parent_instance;
GPtrArray *image_records;
GBytes *fwct_blob;
GBytes *custom_meta_blob;
guint32 row_data_offset_start;
guint32 fw_data_size;
};
G_DEFINE_TYPE (FuCcgxDmcFirmware, fu_ccgx_dmc_firmware, FU_TYPE_FIRMWARE)
static void
fu_ccgx_dmc_firmware_record_free (FuCcgxDmcFirmwareRecord *rcd)
{
if (rcd->seg_records != NULL)
g_ptr_array_unref (rcd->seg_records);
g_free (rcd);
}
static void
fu_ccgx_dmc_firmware_segment_record_free (FuCcgxDmcFirmwareSegmentRecord *rcd)
{
if (rcd->data_records != NULL)
g_ptr_array_unref (rcd->data_records);
g_free (rcd);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxDmcFirmwareRecord, fu_ccgx_dmc_firmware_record_free)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxDmcFirmwareSegmentRecord, fu_ccgx_dmc_firmware_segment_record_free)
GPtrArray *
fu_ccgx_dmc_firmware_get_image_records (FuCcgxDmcFirmware *self)
{
g_return_val_if_fail (FU_IS_CCGX_DMC_FIRMWARE (self), NULL);
return self->image_records;
}
GBytes *
fu_ccgx_dmc_firmware_get_fwct_record (FuCcgxDmcFirmware *self)
{
g_return_val_if_fail (FU_IS_CCGX_DMC_FIRMWARE (self), NULL);
return self->fwct_blob;
}
GBytes *
fu_ccgx_dmc_firmware_get_custom_meta_record (FuCcgxDmcFirmware *self)
{
g_return_val_if_fail (FU_IS_CCGX_DMC_FIRMWARE (self), NULL);
return self->custom_meta_blob;
}
guint32
fu_ccgx_dmc_firmware_get_fw_data_size (FuCcgxDmcFirmware *self)
{
g_return_val_if_fail (FU_IS_CCGX_DMC_FIRMWARE (self), 0);
return self->fw_data_size;
}
static void
fu_ccgx_dmc_firmware_export (FuFirmware *firmware,
FuFirmwareExportFlags flags,
XbBuilderNode *bn)
{
FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE (firmware);
if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) {
fu_xmlb_builder_insert_kx (bn, "fw_data_size", self->fw_data_size);
fu_xmlb_builder_insert_kx (bn, "image_records", self->image_records->len);
}
}
static gboolean
fu_ccgx_dmc_firmware_parse_segment (FuFirmware *firmware,
const guint8 *buf,
gsize bufsz,
FuCcgxDmcFirmwareRecord *img_rcd,
gsize *seg_off,
FwupdInstallFlags flags,
GError **error)
{
FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE (firmware);
gsize row_off;
g_autoptr(GChecksum) csum = g_checksum_new (G_CHECKSUM_SHA256);
/* set row data offset in current image */
row_off = self->row_data_offset_start + img_rcd->img_offset;
/* parse segment in image */
img_rcd->seg_records = g_ptr_array_new_with_free_func ((GFreeFunc) fu_ccgx_dmc_firmware_segment_record_free);
for (guint32 i = 0; i < img_rcd->num_img_segments; i++) {
guint16 row_size_bytes = 0;
g_autofree guint8 *row_buf = NULL;
g_autoptr(FuCcgxDmcFirmwareSegmentRecord) seg_rcd = NULL;
/* read segment info */
seg_rcd = g_new0 (FuCcgxDmcFirmwareSegmentRecord, 1);
if (!fu_common_read_uint16_safe (buf, bufsz,
*seg_off + G_STRUCT_OFFSET(FwctSegmentationInfo, start_row),
&seg_rcd->start_row, G_LITTLE_ENDIAN, error))
return FALSE;
if (!fu_common_read_uint16_safe (buf, bufsz,
*seg_off + G_STRUCT_OFFSET(FwctSegmentationInfo, num_rows),
&seg_rcd->num_rows, G_LITTLE_ENDIAN, error))
return FALSE;
/* calculate actual row size */
row_size_bytes = img_rcd->row_size * 64;
/* create data record array in segment record */
seg_rcd->data_records = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref);
/* read row data in segment */
row_buf = g_malloc0 (row_size_bytes);
for (int row = 0; row < seg_rcd->num_rows; row++) {
g_autoptr(GBytes) data_rcd = NULL;
/* read row data */
if (!fu_memcpy_safe (row_buf, row_size_bytes, 0x0, /* dst */
buf, bufsz, row_off, /* src */
row_size_bytes, error)) {
g_prefix_error (error, "failed to read row data: ");
return FALSE;
}
/* update hash */
g_checksum_update (csum, (guchar *) row_buf, row_size_bytes);
/* add row data to data record */
data_rcd = g_bytes_new (row_buf, row_size_bytes);
g_ptr_array_add (seg_rcd->data_records, g_steal_pointer (&data_rcd));
/* increment row data offset */
row_off += row_size_bytes;
}
/* add segment record to segment array */
g_ptr_array_add (img_rcd->seg_records, g_steal_pointer (&seg_rcd));
/* increment segment info offset */
*seg_off += sizeof(FwctSegmentationInfo);
}
/* check checksum */
if ((flags & FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM) == 0) {
guint8 csumbuf[DMC_HASH_SIZE] = { 0x0 };
gsize csumbufsz = sizeof(csumbuf);
g_checksum_get_digest (csum, csumbuf, &csumbufsz);
if (memcmp (csumbuf, img_rcd->img_digest, DMC_HASH_SIZE) != 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid hash");
return FALSE;
}
}
/* success */
return TRUE;
}
static gboolean
fu_ccgx_dmc_firmware_parse_image (FuFirmware *firmware,
guint8 image_count,
const guint8 *buf,
gsize bufsz,
FwupdInstallFlags flags,
GError **error)
{
FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE (firmware);
gsize img_off = sizeof(FwctInfo);
gsize seg_off = sizeof(FwctInfo) + image_count * sizeof(FwctImageInfo);
/* set initial segment info offset */
for (guint32 i = 0; i < image_count; i++) {
g_autoptr(FuCcgxDmcFirmwareRecord) img_rcd = NULL;
/* read image info */
img_rcd = g_new0 (FuCcgxDmcFirmwareRecord, 1);
if (!fu_common_read_uint8_safe (buf, bufsz,
img_off + G_STRUCT_OFFSET(FwctImageInfo, row_size),
&img_rcd->row_size, error))
return FALSE;
if (img_rcd->row_size == 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid row size 0x%x",
img_rcd->row_size);
return FALSE;
}
if (!fu_common_read_uint32_safe (buf, bufsz,
img_off + G_STRUCT_OFFSET(FwctImageInfo, img_offset),
&img_rcd->img_offset, G_LITTLE_ENDIAN, error))
return FALSE;
if (!fu_common_read_uint8_safe (buf, bufsz,
img_off + G_STRUCT_OFFSET(FwctImageInfo, num_img_segments),
&img_rcd->num_img_segments, error))
return FALSE;
if (img_rcd->num_img_segments == 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid segment number = %d",
img_rcd->num_img_segments);
return FALSE;
}
if (!fu_memcpy_safe ((guint8 *) &img_rcd->img_digest,
sizeof(img_rcd->img_digest), 0x0, /* dst */
buf, bufsz,
img_off + G_STRUCT_OFFSET(FwctImageInfo, img_digest), /* src */
sizeof(img_rcd->img_digest), error))
return FALSE;
/* parse segment */
if (!fu_ccgx_dmc_firmware_parse_segment (firmware, buf, bufsz, img_rcd,
&seg_off, flags, error))
return FALSE;
/* add image record to image record array */
g_ptr_array_add (self->image_records, g_steal_pointer (&img_rcd));
/* increment image offset */
img_off += sizeof(FwctImageInfo);
}
return TRUE;
}
static gboolean
fu_ccgx_dmc_firmware_parse (FuFirmware *firmware,
GBytes *fw,
guint64 addr_start,
guint64 addr_end,
FwupdInstallFlags flags,
GError **error)
{
FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE (firmware);
gsize bufsz = 0;
guint16 hdr_size = 0;
guint16 mdbufsz = 0;
guint32 hdr_composite_version = 0;
guint32 hdr_signature = 0;
guint8 hdr_image_count = 0;
const guint8 *buf = g_bytes_get_data (fw, &bufsz);
g_autoptr(FuFirmware) img = fu_firmware_new_from_bytes (fw);
/* check for 'F' 'W' 'C' 'T' in signature */
if (!fu_common_read_uint32_safe (buf, bufsz, 0x0,
&hdr_signature,
G_LITTLE_ENDIAN, error))
return FALSE;
if (hdr_signature != DMC_FWCT_SIGN) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid dmc signature, expected 0x%04X got 0x%04X",
(guint32) DMC_FWCT_SIGN,
(guint32) hdr_signature);
return FALSE;
}
/* check fwct size */
if (!fu_common_read_uint16_safe (buf, bufsz,
G_STRUCT_OFFSET(FwctInfo, size),
&hdr_size, G_LITTLE_ENDIAN, error))
return FALSE;
if (hdr_size > DMC_FWCT_MAX_SIZE ||
hdr_size == 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid dmc fwct size, expected <= 0x%x, got 0x%x",
(guint) DMC_FWCT_MAX_SIZE,
(guint) hdr_size);
return FALSE;
}
/* set version */
if (!fu_common_read_uint32_safe (buf, bufsz,
G_STRUCT_OFFSET(FwctInfo, composite_version),
&hdr_composite_version, G_LITTLE_ENDIAN, error))
return FALSE;
if (hdr_composite_version != 0) {
g_autofree gchar *ver = NULL;
ver = fu_common_version_from_uint32 (hdr_composite_version,
FWUPD_VERSION_FORMAT_QUAD);
fu_firmware_set_version (firmware, ver);
fu_firmware_set_version_raw (firmware, hdr_composite_version);
}
/* read fwct data */
self->fwct_blob = fu_common_bytes_new_offset (fw, 0x0, hdr_size, error);
if (self->fwct_blob == NULL)
return FALSE;
/* create custom meta binary */
if (!fu_common_read_uint16_safe (buf, bufsz, hdr_size, &mdbufsz,
G_LITTLE_ENDIAN, error)) {
g_prefix_error (error, "failed to read metadata size: ");
return FALSE;
}
if (mdbufsz > 0) {
self->custom_meta_blob = fu_common_bytes_new_offset (fw,
hdr_size + 2,
mdbufsz,
error);
if (self->custom_meta_blob == NULL)
return FALSE;
}
/* set row data start offset */
self->row_data_offset_start = hdr_size + DMC_CUSTOM_META_LENGTH_FIELD_SIZE + mdbufsz;
self->fw_data_size = bufsz - self->row_data_offset_start;
/* parse image */
if (!fu_common_read_uint8_safe (buf, bufsz,
G_STRUCT_OFFSET(FwctInfo, image_count),
&hdr_image_count, error))
return FALSE;
if (!fu_ccgx_dmc_firmware_parse_image (firmware, hdr_image_count,
buf, bufsz, flags, error))
return FALSE;
/* add something, although we'll use the records for the update */
fu_firmware_set_addr (img, 0x0);
fu_firmware_add_image (firmware, img);
return TRUE;
}
static GBytes *
fu_ccgx_dmc_firmware_write (FuFirmware *firmware, GError **error)
{
g_autoptr(GByteArray) buf = g_byte_array_new ();
g_autoptr(GPtrArray) images = fu_firmware_get_images (firmware);
/* add header */
fu_byte_array_append_uint32 (buf, DMC_FWCT_SIGN, G_LITTLE_ENDIAN);
fu_byte_array_append_uint16 (buf,
sizeof(FwctInfo) + (images->len *
(sizeof(FwctImageInfo) +
sizeof(FwctSegmentationInfo))), /* size */
G_LITTLE_ENDIAN);
fu_byte_array_append_uint8 (buf, 0x0); /* checksum, unused */
fu_byte_array_append_uint8 (buf, 0x2); /* version */
fu_byte_array_append_uint8 (buf, 0x3); /* custom_meta_type */
fu_byte_array_append_uint8 (buf, 0x1); /* cdtt_version */
fu_byte_array_append_uint16 (buf, 0x0, G_LITTLE_ENDIAN); /* vid, unused */
fu_byte_array_append_uint16 (buf, 0x0, G_LITTLE_ENDIAN); /* pid, unused */
fu_byte_array_append_uint16 (buf, 0x1, G_LITTLE_ENDIAN); /* device_id */
for (guint j = 0; j < 16; j++)
fu_byte_array_append_uint8 (buf, 0x0); /* reserv0 */
fu_byte_array_append_uint32 (buf, fu_firmware_get_version_raw (firmware), G_LITTLE_ENDIAN);
fu_byte_array_append_uint8 (buf, images->len);
for (guint j = 0; j < 3; j++)
fu_byte_array_append_uint8 (buf, 0x0); /* reserv1 */
/* add image headers */
for (guint i = 0; i < images->len; i++) {
fu_byte_array_append_uint8 (buf, 0x2); /* device_type, unknown */
fu_byte_array_append_uint8 (buf, 0x1); /* img_type, unknown */
fu_byte_array_append_uint8 (buf, 0x0); /* comp_id, unknown */
fu_byte_array_append_uint8 (buf, 0x1); /* row_size, multiplier for num_rows */
for (guint j = 0; j < 4; j++)
fu_byte_array_append_uint8 (buf, 0x0); /* reserv0 */
fu_byte_array_append_uint32 (buf, 0x330006d2, G_LITTLE_ENDIAN); /* fw_version, hardcoded */
fu_byte_array_append_uint32 (buf, 0x14136161, G_LITTLE_ENDIAN); /* app_version, hardcoded */
fu_byte_array_append_uint32 (buf, 0x0, G_LITTLE_ENDIAN); /* start of element data */
fu_byte_array_append_uint32 (buf, 0x0, G_LITTLE_ENDIAN); /* img_size */
for (guint j = 0; j < 32; j++)
fu_byte_array_append_uint8 (buf, 0x0); /* img_digest */
fu_byte_array_append_uint8 (buf, 0x1); /* num_img_segments */
for (guint j = 0; j < 3; j++)
fu_byte_array_append_uint8 (buf, 0x0); /* reserv1 */
}
/* add segments */
for (guint i = 0; i < images->len; i++) {
FuFirmware *img = g_ptr_array_index (images, i);
g_autoptr(GPtrArray) chunks = NULL;
g_autoptr(GBytes) img_bytes = fu_firmware_get_bytes (img, error);
if (img_bytes == NULL)
return NULL;
chunks = fu_chunk_array_new_from_bytes (img_bytes, 0x0, 0x0, 64);
fu_byte_array_append_uint8 (buf, 0x0); /* img_id */
fu_byte_array_append_uint8 (buf, 0x0); /* type */
fu_byte_array_append_uint16 (buf, 0x0, G_LITTLE_ENDIAN); /* start_row, unknown */
fu_byte_array_append_uint16 (buf, MAX (chunks->len, 1), G_LITTLE_ENDIAN); /* num_rows */
for (guint j = 0; j < 2; j++)
fu_byte_array_append_uint8 (buf, 0x0); /* reserv0 */
}
/* metadata */
fu_byte_array_append_uint16 (buf, 0x1, G_LITTLE_ENDIAN);
fu_byte_array_append_uint8 (buf, 0xff);
/* add image headers */
for (guint i = 0; i < images->len; i++) {
FuFirmware *img = g_ptr_array_index (images, i);
gsize csumbufsz = DMC_HASH_SIZE;
gsize img_offset = sizeof(FwctInfo) + (i * sizeof(FwctImageInfo));
guint8 csumbuf[DMC_HASH_SIZE] = { 0x0 };
g_autoptr(GChecksum) csum = g_checksum_new (G_CHECKSUM_SHA256);
g_autoptr(GBytes) img_bytes = fu_firmware_get_bytes (img, NULL);
g_autoptr(GBytes) img_padded = NULL;
g_autoptr(GPtrArray) chunks = NULL;
chunks = fu_chunk_array_new_from_bytes (img_bytes, 0x0, 0x0, 64);
img_padded = fu_common_bytes_pad (img_bytes, MAX (chunks->len, 1) * 64);
fu_byte_array_append_bytes (buf, img_padded);
g_checksum_update (csum,
(const guchar *) g_bytes_get_data (img_padded, NULL),
g_bytes_get_size (img_padded));
g_checksum_get_digest (csum, csumbuf, &csumbufsz);
/* update checksum */
if (!fu_memcpy_safe (buf->data, buf->len, /* dst */
img_offset + G_STRUCT_OFFSET(FwctImageInfo, img_digest),
csumbuf, sizeof(csumbuf), 0x0, /* src */
sizeof(csumbuf), error))
return NULL;
}
return g_byte_array_free_to_bytes (g_steal_pointer (&buf));
}
static void
fu_ccgx_dmc_firmware_init (FuCcgxDmcFirmware *self)
{
self->image_records = g_ptr_array_new_with_free_func ((GFreeFunc) fu_ccgx_dmc_firmware_record_free);
fu_firmware_add_flag (FU_FIRMWARE (self), FU_FIRMWARE_FLAG_HAS_CHECKSUM);
}
static void
fu_ccgx_dmc_firmware_finalize (GObject *object)
{
FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE (object);
if (self->fwct_blob != NULL)
g_bytes_unref (self->fwct_blob);
if (self->custom_meta_blob != NULL)
g_bytes_unref (self->custom_meta_blob);
if (self->image_records != NULL)
g_ptr_array_unref (self->image_records);
G_OBJECT_CLASS (fu_ccgx_dmc_firmware_parent_class)->finalize (object);
}
static void
fu_ccgx_dmc_firmware_class_init (FuCcgxDmcFirmwareClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS (klass);
object_class->finalize = fu_ccgx_dmc_firmware_finalize;
klass_firmware->parse = fu_ccgx_dmc_firmware_parse;
klass_firmware->write = fu_ccgx_dmc_firmware_write;
klass_firmware->export = fu_ccgx_dmc_firmware_export;
}
FuFirmware *
fu_ccgx_dmc_firmware_new (void)
{
return FU_FIRMWARE (g_object_new (FU_TYPE_CCGX_DMC_FIRMWARE, NULL));
}