fwupd/plugins/ccgx/fu-ccgx-firmware.c
Richard Hughes 3162c8540d Add new API for splitting an untrusted string
Using fu_common_strnsplit() has the drawback that a malicious user (or
a fuzzer!) could create a file with 5,000,000 newlines, and then pass
that into any parser that tokenizes into lines. This causes millions of
tiny allocations and quickly dirties hundreds of megabytes of RSS due
to heap overheads.

Rather than splitting a huge array and then processing each line, set
up a callback to process each line and only allocate the next string if
the token was parsed correctly. This means that we don't even dup the
buffer before we start parsing, rather than allocating everything and
then failing at the first hurdle.

Fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=38696
2021-09-17 14:46:45 +01:00

543 lines
14 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 <fwupdplugin.h>
#include <string.h>
#include "fu-ccgx-common.h"
#include "fu-ccgx-firmware.h"
struct _FuCcgxFirmware {
FuFirmwareClass parent_instance;
GPtrArray *records;
guint16 app_type;
guint16 silicon_id;
FWMode fw_mode;
};
G_DEFINE_TYPE(FuCcgxFirmware, fu_ccgx_firmware, FU_TYPE_FIRMWARE)
/* offset stored application version for CCGx */
#define CCGX_APP_VERSION_OFFSET 228 /* 128+64+32+4 */
#define FU_CCGX_FIRMWARE_TOKENS_MAX 100000 /* lines */
GPtrArray *
fu_ccgx_firmware_get_records(FuCcgxFirmware *self)
{
g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), NULL);
return self->records;
}
guint16
fu_ccgx_firmware_get_app_type(FuCcgxFirmware *self)
{
g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0);
return self->app_type;
}
guint16
fu_ccgx_firmware_get_silicon_id(FuCcgxFirmware *self)
{
g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0);
return self->silicon_id;
}
FWMode
fu_ccgx_firmware_get_fw_mode(FuCcgxFirmware *self)
{
g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0);
return self->fw_mode;
}
static void
fu_ccgx_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn)
{
FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
fu_xmlb_builder_insert_kx(bn, "silicon_id", self->silicon_id);
if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) {
fu_xmlb_builder_insert_kx(bn, "app_type", self->app_type);
fu_xmlb_builder_insert_kx(bn, "records", self->records->len);
fu_xmlb_builder_insert_kv(bn, "fw_mode", fu_ccgx_fw_mode_to_string(self->fw_mode));
}
}
static void
fu_ccgx_firmware_record_free(FuCcgxFirmwareRecord *rcd)
{
if (rcd->data != NULL)
g_bytes_unref(rcd->data);
g_free(rcd);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxFirmwareRecord, fu_ccgx_firmware_record_free)
static gboolean
fu_ccgx_firmware_add_record(FuCcgxFirmware *self,
GString *token,
FwupdInstallFlags flags,
GError **error)
{
guint16 buflen;
guint8 checksum_calc = 0;
g_autoptr(FuCcgxFirmwareRecord) rcd = NULL;
g_autoptr(GByteArray) data = g_byte_array_new();
/* parse according to https://community.cypress.com/docs/DOC-10562 */
rcd = g_new0(FuCcgxFirmwareRecord, 1);
if (!fu_firmware_strparse_uint8_safe(token->str, token->len, 0, &rcd->array_id, error))
return FALSE;
if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 2, &rcd->row_number, error))
return FALSE;
if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 6, &buflen, error))
return FALSE;
if (token->len != ((gsize)buflen * 2) + 12) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid record, expected %u chars, got %u",
(guint)(buflen * 2) + 12,
(guint)token->len);
return FALSE;
}
/* parse payload, adding checksum */
for (guint i = 0; i < buflen; i++) {
guint8 tmp = 0;
if (!fu_firmware_strparse_uint8_safe(token->str,
token->len,
10 + (i * 2),
&tmp,
error))
return FALSE;
fu_byte_array_append_uint8(data, tmp);
checksum_calc += tmp;
}
rcd->data = g_byte_array_free_to_bytes(g_steal_pointer(&data));
/* verify 2s complement checksum */
if ((flags & FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM) == 0) {
guint8 checksum_file;
if (!fu_firmware_strparse_uint8_safe(token->str,
token->len,
(buflen * 2) + 10,
&checksum_file,
error))
return FALSE;
for (guint i = 0; i < 5; i++) {
guint8 tmp = 0;
if (!fu_firmware_strparse_uint8_safe(token->str,
token->len,
i * 2,
&tmp,
error))
return FALSE;
checksum_calc += tmp;
}
checksum_calc = 1 + ~checksum_calc;
if (checksum_file != checksum_calc) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"checksum invalid, got %02x, expected %02x",
checksum_calc,
checksum_file);
return FALSE;
}
}
/* success */
g_ptr_array_add(self->records, g_steal_pointer(&rcd));
return TRUE;
}
static guint8
fu_ccgx_firmware_record_calc_checksum(FuCcgxFirmwareRecord *rcd)
{
guint8 csum = 0x0;
gsize bufsz = 0;
const guint8 *buf = g_bytes_get_data(rcd->data, &bufsz);
for (gsize j = 0; j < bufsz; j++)
csum += buf[j];
return csum;
}
static gboolean
fu_ccgx_firmware_parse_md_block(FuCcgxFirmware *self, FwupdInstallFlags flags, GError **error)
{
FuCcgxFirmwareRecord *rcd;
CCGxMetaData metadata;
const guint8 *buf;
gsize bufsz = 0;
gsize md_offset = 0;
guint32 fw_size = 0;
guint32 rcd_version_idx = 0;
guint32 version = 0;
guint8 checksum_calc = 0;
/* sanity check */
if (self->records->len == 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"no records added to image");
return FALSE;
}
/* read metadata from correct ofsset */
rcd = g_ptr_array_index(self->records, self->records->len - 1);
buf = g_bytes_get_data(rcd->data, &bufsz);
if (bufsz == 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid buffer size");
return FALSE;
}
switch (bufsz) {
case 0x80:
md_offset = 0x40;
break;
case 0x100:
md_offset = 0xC0;
break;
default:
break;
}
if (!fu_memcpy_safe((guint8 *)&metadata,
sizeof(metadata),
0x0, /* dst */
buf,
bufsz,
md_offset,
sizeof(metadata),
error)) /* src */
return FALSE;
/* sanity check */
if (metadata.metadata_valid != CCGX_METADATA_VALID_SIG) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid metadata 0x@%x, expected 0x%04x, got 0x%04x",
(guint)md_offset,
(guint)CCGX_METADATA_VALID_SIG,
(guint)metadata.metadata_valid);
return FALSE;
}
for (guint i = 0; i < self->records->len - 1; i++) {
rcd = g_ptr_array_index(self->records, i);
checksum_calc += fu_ccgx_firmware_record_calc_checksum(rcd);
fw_size += g_bytes_get_size(rcd->data);
}
if (fw_size != metadata.fw_size) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"firmware size invalid, got %02x, expected %02x",
fw_size,
metadata.fw_size);
return FALSE;
}
checksum_calc = 1 + ~checksum_calc;
if ((flags & FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM) == 0) {
if (metadata.fw_checksum != checksum_calc) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"checksum invalid, got %02x, expected %02x",
checksum_calc,
metadata.fw_checksum);
return FALSE;
}
}
/* get version if enough data */
rcd_version_idx = CCGX_APP_VERSION_OFFSET / bufsz;
if (rcd_version_idx < self->records->len) {
g_autofree gchar *version_str = NULL;
rcd = g_ptr_array_index(self->records, rcd_version_idx);
buf = g_bytes_get_data(rcd->data, &bufsz);
if (bufsz == 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"metadata record had zero size");
return FALSE;
}
if (!fu_common_read_uint32_safe(buf,
bufsz,
CCGX_APP_VERSION_OFFSET % bufsz,
&version,
G_LITTLE_ENDIAN,
error))
return FALSE;
self->app_type = version & 0xffff;
version_str = fu_ccgx_version_to_string(version);
fu_firmware_set_version(FU_FIRMWARE(self), version_str);
fu_firmware_set_version_raw(FU_FIRMWARE(self), version);
}
/* work out the FWMode */
if (self->records->len > 0) {
rcd = g_ptr_array_index(self->records, self->records->len - 1);
if ((rcd->row_number & 0xFF) == 0xFF) /* last row */
self->fw_mode = FW_MODE_FW1;
if ((rcd->row_number & 0xFF) == 0xFE) /* penultimate row */
self->fw_mode = FW_MODE_FW2;
}
return TRUE;
}
typedef struct {
FuCcgxFirmware *self;
FwupdInstallFlags flags;
} FuCcgxFirmwareTokenHelper;
static gboolean
fu_ccgx_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error)
{
FuCcgxFirmwareTokenHelper *helper = (FuCcgxFirmwareTokenHelper *)user_data;
FuCcgxFirmware *self = FU_CCGX_FIRMWARE(helper->self);
/* sanity check */
if (token_idx > FU_CCGX_FIRMWARE_TOKENS_MAX) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"file has too many lines");
return FALSE;
}
/* remove WIN32 line endings */
g_strdelimit(token->str, "\r\x1a", '\0');
token->len = strlen(token->str);
/* header */
if (token_idx == 0) {
guint32 device_id = 0;
if (token->len != 12) {
g_autofree gchar *strsafe = fu_common_strsafe(token->str, 12);
if (strsafe != NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid header, expected == 12 chars -- got %s",
strsafe);
return FALSE;
}
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"invalid header, expected == 12 chars");
return FALSE;
}
if (!fu_firmware_strparse_uint32_safe(token->str, token->len, 0, &device_id, error))
return FALSE;
self->silicon_id = device_id >> 16;
return TRUE;
}
/* ignore blank lines */
if (token->len == 0)
return TRUE;
/* parse record */
if (!fu_ccgx_firmware_add_record(self, token, helper->flags, error)) {
g_prefix_error(error, "error on line %u: ", token_idx + 1);
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_ccgx_firmware_parse(FuFirmware *firmware,
GBytes *fw,
guint64 addr_start,
guint64 addr_end,
FwupdInstallFlags flags,
GError **error)
{
FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
FuCcgxFirmwareTokenHelper helper = {.self = self, .flags = flags};
/* tokenize */
if (!fu_common_strnsplit_full(g_bytes_get_data(fw, NULL),
g_bytes_get_size(fw),
"\n",
fu_ccgx_firmware_tokenize_cb,
&helper,
error))
return FALSE;
/* address is first data entry */
if (self->records->len > 0) {
FuCcgxFirmwareRecord *rcd = g_ptr_array_index(self->records, 0);
fu_firmware_set_addr(firmware, rcd->row_number);
}
/* parse metadata block */
if (!fu_ccgx_firmware_parse_md_block(self, flags, error)) {
g_prefix_error(error, "failed to parse metadata: ");
return FALSE;
}
/* add something, although we'll use the records for the update */
fu_firmware_set_bytes(firmware, fw);
return TRUE;
}
static void
fu_ccgx_firmware_write_record(GString *str,
guint8 array_id,
guint8 row_number,
const guint8 *buf,
guint16 bufsz)
{
guint8 checksum_calc = 0xff;
g_autoptr(GString) datastr = g_string_new(NULL);
/* offset for bootloader perhaps? */
row_number += 0xE;
checksum_calc += array_id;
checksum_calc += row_number;
checksum_calc += bufsz & 0xff;
checksum_calc += (bufsz >> 8) & 0xff;
for (guint j = 0; j < bufsz; j++) {
g_string_append_printf(datastr, "%02X", buf[j]);
checksum_calc += buf[j];
}
g_string_append_printf(str,
":%02X%04X%04X%s%02X\n",
array_id,
row_number,
bufsz,
datastr->str,
(guint)((guint8)~checksum_calc));
}
static GBytes *
fu_ccgx_firmware_write(FuFirmware *firmware, GError **error)
{
FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
CCGxMetaData metadata = {0x0};
gsize fwbufsz = 0;
guint8 checksum_img = 0xff;
const guint8 *fwbuf;
g_autoptr(GByteArray) mdbuf = g_byte_array_new();
g_autoptr(GBytes) fw = NULL;
g_autoptr(GPtrArray) chunks = NULL;
g_autoptr(GString) str = g_string_new(NULL);
/* header record */
g_string_append_printf(str,
"%04X%04X%02X%02X\n",
self->silicon_id,
(guint)0x11AF, /* SiliconID */
(guint)0x0, /* SiliconRev */
(guint)0x0); /* Checksum, or 0x0 */
/* add image in chunks */
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return NULL;
chunks = fu_chunk_array_new_from_bytes(fw, 0x0, 0x0, 0x100);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index(chunks, i);
fu_ccgx_firmware_write_record(str,
0x0,
i,
fu_chunk_get_data(chk),
fu_chunk_get_data_sz(chk));
}
/* add metadata */
fwbuf = g_bytes_get_data(fw, &fwbufsz);
for (guint j = 0; j < fwbufsz; j++)
checksum_img += fwbuf[j];
metadata.fw_checksum = ~checksum_img;
metadata.fw_entry = 0x0; /* unknown */
metadata.last_boot_row = 0x13;
metadata.fw_size = fwbufsz;
metadata.metadata_valid = CCGX_METADATA_VALID_SIG;
metadata.boot_seq = 0x0; /* unknown */
/* copy into place */
fu_byte_array_set_size(mdbuf, 0x80);
if (!fu_memcpy_safe(mdbuf->data,
mdbuf->len,
0x40, /* dst */
(const guint8 *)&metadata,
sizeof(metadata),
0x0, /* src */
sizeof(metadata),
error))
return NULL;
fu_ccgx_firmware_write_record(str,
0x0,
0xFE, /* FW2: penultimate row */
mdbuf->data,
mdbuf->len);
return g_string_free_to_bytes(g_steal_pointer(&str));
}
static gboolean
fu_ccgx_firmware_build(FuFirmware *firmware, XbNode *n, GError **error)
{
FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware);
guint64 tmp;
/* optional properties */
tmp = xb_node_query_text_as_uint(n, "silicon_id", NULL);
if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16)
self->silicon_id = tmp;
/* success */
return TRUE;
}
static void
fu_ccgx_firmware_init(FuCcgxFirmware *self)
{
self->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ccgx_firmware_record_free);
fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM);
fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID);
}
static void
fu_ccgx_firmware_finalize(GObject *object)
{
FuCcgxFirmware *self = FU_CCGX_FIRMWARE(object);
g_ptr_array_unref(self->records);
G_OBJECT_CLASS(fu_ccgx_firmware_parent_class)->finalize(object);
}
static void
fu_ccgx_firmware_class_init(FuCcgxFirmwareClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
object_class->finalize = fu_ccgx_firmware_finalize;
klass_firmware->parse = fu_ccgx_firmware_parse;
klass_firmware->write = fu_ccgx_firmware_write;
klass_firmware->build = fu_ccgx_firmware_build;
klass_firmware->export = fu_ccgx_firmware_export;
}
FuFirmware *
fu_ccgx_firmware_new(void)
{
return FU_FIRMWARE(g_object_new(FU_TYPE_CCGX_FIRMWARE, NULL));
}