fwupd/libfwupdplugin/fu-ihex-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

589 lines
16 KiB
C

/*
* Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuFirmware"
#include "config.h"
#include <string.h>
#include "fu-common.h"
#include "fu-firmware-common.h"
#include "fu-ihex-firmware.h"
/**
* FuIhexFirmware:
*
* A Intel hex (ihex) firmware image.
*
* See also: [class@FuFirmware]
*/
typedef struct {
GPtrArray *records;
guint8 padding_value;
} FuIhexFirmwarePrivate;
G_DEFINE_TYPE_WITH_PRIVATE(FuIhexFirmware, fu_ihex_firmware, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_ihex_firmware_get_instance_private(o))
#define FU_IHEX_FIRMWARE_TOKENS_MAX 100000 /* lines */
/**
* fu_ihex_firmware_get_records:
* @self: A #FuIhexFirmware
*
* Returns the raw lines from tokenization.
*
* This might be useful if the plugin is expecting the hex file to be a list
* of operations, rather than a simple linear image with filled holes.
*
* Returns: (transfer none) (element-type FuIhexFirmwareRecord): records
*
* Since: 1.3.4
**/
GPtrArray *
fu_ihex_firmware_get_records(FuIhexFirmware *self)
{
FuIhexFirmwarePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_IHEX_FIRMWARE(self), NULL);
return priv->records;
}
/**
* fu_ihex_firmware_set_padding_value:
* @self: A #FuIhexFirmware
* @padding_value: the byte used to pad the image
*
* Set the padding value to fill incomplete address ranges.
*
* The default value of zero can be changed to `0xff` if functions like
* fu_common_bytes_is_empty() are going to be used on subsections of the data.
*
* Since: 1.6.0
**/
void
fu_ihex_firmware_set_padding_value(FuIhexFirmware *self, guint8 padding_value)
{
FuIhexFirmwarePrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FU_IS_IHEX_FIRMWARE(self));
priv->padding_value = padding_value;
}
static void
fu_ihex_firmware_record_free(FuIhexFirmwareRecord *rcd)
{
g_string_free(rcd->buf, TRUE);
g_byte_array_unref(rcd->data);
g_free(rcd);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuIhexFirmwareRecord, fu_ihex_firmware_record_free)
static FuIhexFirmwareRecord *
fu_ihex_firmware_record_new(guint ln, const gchar *line, FwupdInstallFlags flags, GError **error)
{
g_autoptr(FuIhexFirmwareRecord) rcd = NULL;
gsize linesz = strlen(line);
guint line_end;
guint16 addr16 = 0;
/* check starting token */
if (line[0] != ':') {
g_autofree gchar *strsafe = fu_common_strsafe(line, 5);
if (strsafe != NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid starting token: %s",
strsafe);
return NULL;
}
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid starting token");
return NULL;
}
/* length, 16-bit address, type */
rcd = g_new0(FuIhexFirmwareRecord, 1);
rcd->ln = ln;
rcd->data = g_byte_array_new();
rcd->buf = g_string_new(line);
if (!fu_firmware_strparse_uint8_safe(line, linesz, 1, &rcd->byte_cnt, error))
return NULL;
if (!fu_firmware_strparse_uint16_safe(line, linesz, 3, &addr16, error))
return NULL;
rcd->addr = addr16;
if (!fu_firmware_strparse_uint8_safe(line, linesz, 7, &rcd->record_type, error))
return NULL;
/* position of checksum */
line_end = 9 + rcd->byte_cnt * 2;
if (line_end > (guint)rcd->buf->len) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"line malformed, length: %u",
line_end);
return NULL;
}
/* verify checksum */
if ((flags & FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM) == 0) {
guint8 checksum = 0;
for (guint i = 1; i < line_end + 2; i += 2) {
guint8 data_tmp = 0;
if (!fu_firmware_strparse_uint8_safe(line, linesz, i, &data_tmp, error))
return NULL;
checksum += data_tmp;
}
if (checksum != 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid checksum (0x%02x)",
checksum);
return NULL;
}
}
/* add data */
for (guint i = 9; i < line_end; i += 2) {
guint8 tmp_c = 0;
if (!fu_firmware_strparse_uint8_safe(line, linesz, i, &tmp_c, error))
return NULL;
fu_byte_array_append_uint8(rcd->data, tmp_c);
}
return g_steal_pointer(&rcd);
}
static const gchar *
fu_ihex_firmware_record_type_to_string(guint8 record_type)
{
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_DATA)
return "DATA";
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EOF)
return "EOF";
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_SEGMENT)
return "EXTENDED_SEGMENT";
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_START_SEGMENT)
return "START_SEGMENT";
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR)
return "EXTENDED_LINEAR";
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_START_LINEAR)
return "ADDR32";
if (record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE)
return "SIGNATURE";
return NULL;
}
typedef struct {
FuIhexFirmware *self;
FwupdInstallFlags flags;
} FuIhexFirmwareTokenHelper;
static gboolean
fu_ihex_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error)
{
FuIhexFirmwareTokenHelper *helper = (FuIhexFirmwareTokenHelper *)user_data;
FuIhexFirmwarePrivate *priv = GET_PRIVATE(helper->self);
g_autoptr(FuIhexFirmwareRecord) rcd = NULL;
/* sanity check */
if (token_idx > FU_IHEX_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);
/* ignore blank lines */
if (token->len == 0)
return TRUE;
/* ignore comments */
if (g_str_has_prefix(token->str, ";"))
return TRUE;
/* parse record */
rcd = fu_ihex_firmware_record_new(token_idx + 1, token->str, helper->flags, error);
if (rcd == NULL) {
g_prefix_error(error, "invalid line %u: ", token_idx + 1);
return FALSE;
}
g_ptr_array_add(priv->records, g_steal_pointer(&rcd));
return TRUE;
}
static gboolean
fu_ihex_firmware_tokenize(FuFirmware *firmware, GBytes *fw, FwupdInstallFlags flags, GError **error)
{
FuIhexFirmware *self = FU_IHEX_FIRMWARE(firmware);
FuIhexFirmwareTokenHelper helper = {.self = self, .flags = flags};
return fu_common_strnsplit_full(g_bytes_get_data(fw, NULL),
g_bytes_get_size(fw),
"\n",
fu_ihex_firmware_tokenize_cb,
&helper,
error);
}
static gboolean
fu_ihex_firmware_parse(FuFirmware *firmware,
GBytes *fw,
guint64 addr_start,
guint64 addr_end,
FwupdInstallFlags flags,
GError **error)
{
FuIhexFirmware *self = FU_IHEX_FIRMWARE(firmware);
FuIhexFirmwarePrivate *priv = GET_PRIVATE(self);
gboolean got_eof = FALSE;
gboolean got_sig = FALSE;
guint32 abs_addr = 0x0;
guint32 addr_last = 0x0;
guint32 img_addr = G_MAXUINT32;
guint32 seg_addr = 0x0;
g_autoptr(GBytes) img_bytes = NULL;
g_autoptr(GByteArray) buf = g_byte_array_new();
/* parse records */
for (guint k = 0; k < priv->records->len; k++) {
FuIhexFirmwareRecord *rcd = g_ptr_array_index(priv->records, k);
guint16 addr16 = 0;
guint32 addr = rcd->addr + seg_addr + abs_addr;
guint32 len_hole;
g_debug("%s:", fu_ihex_firmware_record_type_to_string(rcd->record_type));
g_debug(" length:\t0x%02x", rcd->data->len);
g_debug(" addr:\t0x%08x", addr);
/* sanity check */
if (rcd->record_type != FU_IHEX_FIRMWARE_RECORD_TYPE_EOF && rcd->data->len == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"record 0x%x had zero size",
k);
return FALSE;
}
/* process different record types */
switch (rcd->record_type) {
case FU_IHEX_FIRMWARE_RECORD_TYPE_DATA:
/* does not make sense */
if (got_eof) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"cannot process data after EOF");
return FALSE;
}
if (rcd->data->len == 0) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"cannot parse invalid data");
return FALSE;
}
/* base address for element */
if (img_addr == G_MAXUINT32)
img_addr = addr;
/* does not make sense */
if (addr < addr_last) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid address 0x%x, last was 0x%x on line %u",
(guint)addr,
(guint)addr_last,
rcd->ln);
return FALSE;
}
/* any holes in the hex record */
len_hole = addr - addr_last;
if (addr_last > 0 && len_hole > 0x100000) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"hole of 0x%x bytes too large to fill on line %u",
(guint)len_hole,
rcd->ln);
return FALSE;
}
if (addr_last > 0x0 && len_hole > 1) {
g_debug("filling address 0x%08x to 0x%08x on line %u",
addr_last + 1,
addr_last + len_hole - 1,
rcd->ln);
for (guint j = 1; j < len_hole; j++)
fu_byte_array_append_uint8(buf, priv->padding_value);
}
addr_last = addr + rcd->data->len - 1;
if (addr_last < addr) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"overflow of address 0x%x on line %u",
(guint)addr,
rcd->ln);
return FALSE;
}
/* write into buf */
g_byte_array_append(buf, rcd->data->data, rcd->data->len);
break;
case FU_IHEX_FIRMWARE_RECORD_TYPE_EOF:
if (got_eof) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"duplicate EOF, perhaps "
"corrupt file");
return FALSE;
}
got_eof = TRUE;
break;
case FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR:
if (!fu_common_read_uint16_safe(rcd->data->data,
rcd->data->len,
0x0,
&addr16,
G_BIG_ENDIAN,
error))
return FALSE;
abs_addr = (guint32)addr16 << 16;
g_debug(" abs_addr:\t0x%02x on line %u", abs_addr, rcd->ln);
break;
case FU_IHEX_FIRMWARE_RECORD_TYPE_START_LINEAR:
if (!fu_common_read_uint32_safe(rcd->data->data,
rcd->data->len,
0x0,
&abs_addr,
G_BIG_ENDIAN,
error))
return FALSE;
g_debug(" abs_addr:\t0x%08x on line %u", abs_addr, rcd->ln);
break;
case FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_SEGMENT:
if (!fu_common_read_uint16_safe(rcd->data->data,
rcd->data->len,
0x0,
&addr16,
G_BIG_ENDIAN,
error))
return FALSE;
/* segment base address, so ~1Mb addressable */
seg_addr = (guint32)addr16 * 16;
g_debug(" seg_addr:\t0x%08x on line %u", seg_addr, rcd->ln);
break;
case FU_IHEX_FIRMWARE_RECORD_TYPE_START_SEGMENT:
/* initial content of the CS:IP registers */
if (!fu_common_read_uint32_safe(rcd->data->data,
rcd->data->len,
0x0,
&seg_addr,
G_BIG_ENDIAN,
error))
return FALSE;
g_debug(" seg_addr:\t0x%02x on line %u", seg_addr, rcd->ln);
break;
case FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE:
if (got_sig) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"duplicate signature, perhaps "
"corrupt file");
return FALSE;
}
if (rcd->data->len > 0) {
g_autoptr(GBytes) data_sig =
g_bytes_new(rcd->data->data, rcd->data->len);
g_autoptr(FuFirmware) img_sig =
fu_firmware_new_from_bytes(data_sig);
fu_firmware_set_id(img_sig, FU_FIRMWARE_ID_SIGNATURE);
fu_firmware_add_image(firmware, img_sig);
}
got_sig = TRUE;
break;
default:
/* vendors sneak in nonstandard sections past the EOF */
if (got_eof)
break;
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"invalid ihex record type %i on line %u",
rcd->record_type,
rcd->ln);
return FALSE;
}
}
/* no EOF */
if (!got_eof) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"no EOF, perhaps truncated file");
return FALSE;
}
/* add single image */
img_bytes = g_bytes_new(buf->data, buf->len);
if (img_addr != G_MAXUINT32)
fu_firmware_set_addr(firmware, img_addr);
fu_firmware_set_bytes(firmware, img_bytes);
return TRUE;
}
static void
fu_ihex_firmware_emit_chunk(GString *str,
guint16 address,
guint8 record_type,
const guint8 *data,
gsize sz)
{
guint8 checksum = 0x00;
g_string_append_printf(str, ":%02X%04X%02X", (guint)sz, (guint)address, (guint)record_type);
for (gsize j = 0; j < sz; j++)
g_string_append_printf(str, "%02X", data[j]);
checksum = (guint8)sz;
checksum += (guint8)((address & 0xff00) >> 8);
checksum += (guint8)(address & 0xff);
checksum += record_type;
for (gsize j = 0; j < sz; j++)
checksum += data[j];
g_string_append_printf(str, "%02X\n", (guint)(((~checksum) + 0x01) & 0xff));
}
static gboolean
fu_ihex_firmware_image_to_string(GBytes *bytes,
guint32 addr,
guint8 record_type,
GString *str,
GError **error)
{
const guint8 *data;
const guint chunk_size = 16;
gsize len;
guint32 address_offset_last = 0x0;
/* get number of chunks */
data = g_bytes_get_data(bytes, &len);
for (gsize i = 0; i < len; i += chunk_size) {
guint32 address_tmp = addr + i;
guint32 address_offset = (address_tmp >> 16) & 0xffff;
gsize chunk_len = MIN(len - i, 16);
/* need to offset */
if (address_offset != address_offset_last) {
guint8 buf[2];
fu_common_write_uint16(buf, address_offset, G_BIG_ENDIAN);
fu_ihex_firmware_emit_chunk(str,
0x0,
FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR,
buf,
2);
address_offset_last = address_offset;
}
address_tmp &= 0xffff;
fu_ihex_firmware_emit_chunk(str, address_tmp, record_type, data + i, chunk_len);
}
return TRUE;
}
static GBytes *
fu_ihex_firmware_write(FuFirmware *firmware, GError **error)
{
g_autoptr(GBytes) fw = NULL;
g_autoptr(FuFirmware) img_sig = NULL;
g_autoptr(GString) str = g_string_new("");
/* payload */
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return NULL;
if (!fu_ihex_firmware_image_to_string(fw,
fu_firmware_get_addr(firmware),
FU_IHEX_FIRMWARE_RECORD_TYPE_DATA,
str,
error))
return NULL;
/* signature */
img_sig = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_SIGNATURE, NULL);
if (img_sig != NULL) {
g_autoptr(GBytes) img_fw = fu_firmware_get_bytes(img_sig, error);
if (img_fw == NULL)
return NULL;
if (!fu_ihex_firmware_image_to_string(img_fw,
0,
FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE,
str,
error))
return NULL;
}
/* add EOF */
fu_ihex_firmware_emit_chunk(str, 0x0, FU_IHEX_FIRMWARE_RECORD_TYPE_EOF, NULL, 0);
return g_bytes_new(str->str, str->len);
}
static void
fu_ihex_firmware_finalize(GObject *object)
{
FuIhexFirmware *self = FU_IHEX_FIRMWARE(object);
FuIhexFirmwarePrivate *priv = GET_PRIVATE(self);
g_ptr_array_unref(priv->records);
G_OBJECT_CLASS(fu_ihex_firmware_parent_class)->finalize(object);
}
static void
fu_ihex_firmware_init(FuIhexFirmware *self)
{
FuIhexFirmwarePrivate *priv = GET_PRIVATE(self);
priv->padding_value = 0x00; /* chosen as we can't write 0xffff to PIC14 */
priv->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ihex_firmware_record_free);
fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM);
}
static void
fu_ihex_firmware_class_init(FuIhexFirmwareClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
object_class->finalize = fu_ihex_firmware_finalize;
klass_firmware->parse = fu_ihex_firmware_parse;
klass_firmware->tokenize = fu_ihex_firmware_tokenize;
klass_firmware->write = fu_ihex_firmware_write;
}
/**
* fu_ihex_firmware_new:
*
* Creates a new #FuFirmware of sub type Ihex
*
* Since: 1.3.1
**/
FuFirmware *
fu_ihex_firmware_new(void)
{
return FU_FIRMWARE(g_object_new(FU_TYPE_IHEX_FIRMWARE, NULL));
}