fwupd/plugins/dfu/dfu-patch.c
Richard Hughes b3c13461e9 trivial: Use the error domain from libfwupd
This means we don't need to convert the error code in the dfu plugin.
2017-09-08 13:33:34 +01:00

613 lines
17 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* Licensed under the GNU Lesser General Public License Version 2.1
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* SECTION:dfu-patch
* @short_description: Object representing a binary patch
*
* This object represents an binary patch that can be applied on a firmware
* image. The patch itself is made up of chunks of data that have an offset
* and that can replace the data to upgrade the firmware.
*
* Note: this is one way operation -- the patch can only be used to go forwards
* and also cannot be used to truncate the existing image.
*
* See also: #DfuImage, #DfuFirmware
*/
#include "config.h"
#include <string.h>
#include <stdio.h>
#include "dfu-common.h"
#include "dfu-patch.h"
#include "fwupd-error.h"
static void dfu_patch_finalize (GObject *object);
typedef struct __attribute__((packed)) {
guint32 off;
guint32 sz;
guint32 flags;
} DfuPatchChunkHeader;
typedef struct __attribute__((packed)) {
guint8 signature[4]; /* 'DfuP' */
guint8 reserved[4];
guint8 checksum_old[20]; /* SHA1 */
guint8 checksum_new[20]; /* SHA1 */
} DfuPatchFileHeader;
typedef struct {
GBytes *checksum_old;
GBytes *checksum_new;
GPtrArray *chunks; /* of DfuPatchChunk */
} DfuPatchPrivate;
typedef struct {
guint32 off;
GBytes *blob;
} DfuPatchChunk;
G_DEFINE_TYPE_WITH_PRIVATE (DfuPatch, dfu_patch, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (dfu_patch_get_instance_private (o))
static void
dfu_patch_class_init (DfuPatchClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = dfu_patch_finalize;
}
static void
dfu_patch_chunk_free (DfuPatchChunk *chunk)
{
g_bytes_unref (chunk->blob);
g_free (chunk);
}
static void
dfu_patch_init (DfuPatch *self)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
priv->chunks = g_ptr_array_new_with_free_func ((GDestroyNotify) dfu_patch_chunk_free);
}
static void
dfu_patch_finalize (GObject *object)
{
DfuPatch *self = DFU_PATCH (object);
DfuPatchPrivate *priv = GET_PRIVATE (self);
if (priv->checksum_old != NULL)
g_bytes_unref (priv->checksum_old);
if (priv->checksum_new != NULL)
g_bytes_unref (priv->checksum_new);
g_ptr_array_unref (priv->chunks);
G_OBJECT_CLASS (dfu_patch_parent_class)->finalize (object);
}
/**
* dfu_patch_export:
* @self: a #DfuPatch
* @error: a #GError, or %NULL
*
* Converts the patch to a binary blob that can be stored as a file.
*
* Return value: (transfer full): blob
**/
GBytes *
dfu_patch_export (DfuPatch *self, GError **error)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
gsize addr;
gsize sz;
guint8 *data;
g_return_val_if_fail (DFU_IS_PATCH (self), NULL);
/* check we have something to write */
if (priv->chunks->len == 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"no chunks to process");
return NULL;
}
/* calculate the size of the new blob */
sz = sizeof(DfuPatchFileHeader);
for (guint i = 0; i < priv->chunks->len; i++) {
DfuPatchChunk *chunk = g_ptr_array_index (priv->chunks, i);
sz += sizeof(DfuPatchChunkHeader) + g_bytes_get_size (chunk->blob);
}
g_debug ("blob size is %" G_GSIZE_FORMAT, sz);
/* actually allocate and fill in the blob */
data = g_malloc0 (sz);
memcpy (data, "DfuP", 4);
/* add checksums */
if (priv->checksum_old != NULL) {
gsize csum_sz = 0;
const guint8 *csum_data = g_bytes_get_data (priv->checksum_old, &csum_sz);
memcpy (data + G_STRUCT_OFFSET(DfuPatchFileHeader,checksum_old),
csum_data, csum_sz);
}
if (priv->checksum_new != NULL) {
gsize csum_sz = 0;
const guint8 *csum_data = g_bytes_get_data (priv->checksum_new, &csum_sz);
memcpy (data + G_STRUCT_OFFSET(DfuPatchFileHeader,checksum_new),
csum_data, csum_sz);
}
addr = sizeof(DfuPatchFileHeader);
for (guint i = 0; i < priv->chunks->len; i++) {
DfuPatchChunk *chunk = g_ptr_array_index (priv->chunks, i);
DfuPatchChunkHeader chunkhdr;
gsize sz_tmp = 0;
const guint8 *data_new = g_bytes_get_data (chunk->blob, &sz_tmp);
/* build chunk header and append data */
chunkhdr.off = GUINT32_TO_LE (chunk->off);
chunkhdr.sz = GUINT32_TO_LE (sz_tmp);
chunkhdr.flags = 0;
memcpy (data + addr, &chunkhdr, sizeof(DfuPatchChunkHeader));
memcpy (data + addr + sizeof(DfuPatchChunkHeader), data_new, sz_tmp);
/* move up after the copied data */
addr += sizeof(DfuPatchChunkHeader) + sz_tmp;
}
return g_bytes_new_take (data, sz);
}
/**
* dfu_patch_import:
* @self: a #DfuPatch
* @blob: patch data
* @error: a #GError, or %NULL
*
* Creates a patch from a serialized patch, possibly from a file.
*
* Return value: %TRUE on success
**/
gboolean
dfu_patch_import (DfuPatch *self, GBytes *blob, GError **error)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
const guint8 *data;
gsize sz = 0;
guint32 off;
g_return_val_if_fail (DFU_IS_PATCH (self), FALSE);
g_return_val_if_fail (blob != NULL, FALSE);
/* cannot reuse object */
if (priv->chunks->len > 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"patch has already been loaded");
return FALSE;
}
/* check minimum size */
data = g_bytes_get_data (blob, &sz);
if (sz < sizeof(DfuPatchFileHeader) + sizeof(DfuPatchChunkHeader) + 1) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"file is too small");
return FALSE;
}
/* check header */
if (memcmp (data, "DfuP", 4) != 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"header signature is not correct");
return FALSE;
}
/* get checksums */
priv->checksum_old = g_bytes_new (data + G_STRUCT_OFFSET(DfuPatchFileHeader,checksum_old), 20);
priv->checksum_new = g_bytes_new (data + G_STRUCT_OFFSET(DfuPatchFileHeader,checksum_new), 20);
/* look for each chunk */
off = sizeof(DfuPatchFileHeader);
while (off < (guint32) sz) {
DfuPatchChunkHeader *chunkhdr = (DfuPatchChunkHeader *) (data + off);
DfuPatchChunk *chunk;
guint32 chunk_sz = GUINT32_FROM_LE (chunkhdr->sz);
guint32 chunk_off = GUINT32_FROM_LE (chunkhdr->off);
/* check chunk size, assuming it can overflow */
if (chunk_sz > sz || off + chunk_sz > sz) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"chunk offset 0x%04x outsize file size 0x%04x",
(guint) (off + chunk_sz), (guint) sz);
return FALSE;
}
chunk = g_new0 (DfuPatchChunk, 1);
chunk->off = chunk_off;
chunk->blob = g_bytes_new_from_bytes (blob, off + sizeof(DfuPatchChunkHeader), chunk_sz);
g_ptr_array_add (priv->chunks, chunk);
off += sizeof(DfuPatchChunkHeader) + chunk_sz;
}
/* check we finished properly */
if (off != sz) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"blob chunk sizes did not sum to total");
return FALSE;
}
/* success */
return TRUE;
}
static GBytes *
dfu_patch_calculate_checksum (GBytes *blob)
{
const guchar *data;
gsize digest_len = 20;
gsize sz = 0;
guint8 *buf = g_malloc0 (digest_len);
g_autoptr(GChecksum) csum = NULL;
csum = g_checksum_new (G_CHECKSUM_SHA1);
data = g_bytes_get_data (blob, &sz);
g_checksum_update (csum, data, (gssize) sz);
g_checksum_get_digest (csum, buf, &digest_len);
return g_bytes_new_take (buf, digest_len);
}
typedef struct {
guint32 diff_start;
guint32 diff_end;
GBytes *blob; /* no ref */
} DfuPatchCreateHelper;
static void
dfu_patch_flush (DfuPatch *self, DfuPatchCreateHelper *helper)
{
DfuPatchChunk *chunk;
DfuPatchPrivate *priv = GET_PRIVATE (self);
if (helper->diff_end == 0xffff)
return;
g_debug ("add chunk @0x%04x (len %" G_GUINT32_FORMAT ")",
(guint) helper->diff_start, helper->diff_end - helper->diff_start + 1);
chunk = g_new0 (DfuPatchChunk, 1);
chunk->off = helper->diff_start;
chunk->blob = g_bytes_new_from_bytes (helper->blob, chunk->off,
helper->diff_end - helper->diff_start + 1);
g_ptr_array_add (priv->chunks, chunk);
helper->diff_end = 0xffff;
}
/**
* dfu_patch_create:
* @self: a #DfuPatch
* @blob1: a #GBytes, typically the old firmware image
* @blob2: a #GBytes, typically the new firmware image
* @error: a #GError, or %NULL
*
* Creates a patch from two blobs of memory.
*
* The blobs should ideally be the same size. If @blob2 is has grown in size
* the binary diff will still work but the algorithm will probably not perform
* well unless the majority of data has just been appended.
*
* As an additional constrainst, @blob2 cannot be smaller than @blob1, i.e.
* the firmware cannot be truncated by this format.
*
* Return value: %TRUE on success
**/
gboolean
dfu_patch_create (DfuPatch *self, GBytes *blob1, GBytes *blob2, GError **error)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
DfuPatchCreateHelper helper;
const guint8 *data1;
const guint8 *data2;
gsize sz1 = 0;
gsize sz2 = 0;
guint32 same_sz = 0;
g_return_val_if_fail (DFU_IS_PATCH (self), FALSE);
g_return_val_if_fail (blob1 != NULL, FALSE);
g_return_val_if_fail (blob2 != NULL, FALSE);
/* are the blobs the same */
if (g_bytes_equal (blob1, blob2)) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"old and new binaries are the same");
return FALSE;
}
/* cannot reuse object */
if (priv->chunks->len > 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"patch has already been loaded");
return FALSE;
}
/* get the hash of the old firmware file */
priv->checksum_old = dfu_patch_calculate_checksum (blob1);
priv->checksum_new = dfu_patch_calculate_checksum (blob2);
/* get the raw data, and ensure they are the same size */
data1 = g_bytes_get_data (blob1, &sz1);
data2 = g_bytes_get_data (blob2, &sz2);
if (sz1 > sz2) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"firmware binary cannot go down, got "
"%" G_GSIZE_FORMAT " and %" G_GSIZE_FORMAT,
sz1, sz2);
return FALSE;
}
if (sz1 == sz2) {
g_debug ("binary staying same size: %" G_GSIZE_FORMAT, sz1);
} else {
g_debug ("binary growing from: %" G_GSIZE_FORMAT
" to %" G_GSIZE_FORMAT, sz1, sz2);
}
/* start the dumb comparison algorithm */
helper.diff_start = 0;
helper.diff_end = 0xffff;
helper.blob = blob2;
for (gsize i = 0; i < sz1 || i < sz2; i++) {
if (i < sz1 && i < sz2 &&
data1[i] == data2[i]) {
/* if we got enough the same, dump what is pending */
if (++same_sz > sizeof(DfuPatchChunkHeader) * 2)
dfu_patch_flush (self, &helper);
continue;
}
if (helper.diff_end == 0xffff)
helper.diff_start = (guint32) i;
helper.diff_end = (guint32) i;
same_sz = 0;
}
dfu_patch_flush (self, &helper);
return TRUE;
}
static gchar *
_g_bytes_to_string (GBytes *blob)
{
gsize sz = 0;
const guint8 *data = g_bytes_get_data (blob, &sz);
GString *str = g_string_new (NULL);
for (gsize i = 0; i < sz; i++)
g_string_append_printf (str, "%02x", (guint) data[i]);
return g_string_free (str, FALSE);
}
/**
* dfu_patch_get_checksum_old:
* @self: a #DfuPatch
*
* Get the checksum for the old firmware image.
*
* Return value: A #GBytes, or %NULL if nothing has been loaded.
**/
GBytes *
dfu_patch_get_checksum_old (DfuPatch *self)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
return priv->checksum_old;
}
/**
* dfu_patch_get_checksum_new:
* @self: a #DfuPatch
*
* Get the checksum for the new firmware image.
*
* Return value: A #GBytes, or %NULL if nothing has been loaded.
**/
GBytes *
dfu_patch_get_checksum_new (DfuPatch *self)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
return priv->checksum_new;
}
/**
* dfu_patch_apply:
* @self: a #DfuPatch
* @blob: a #GBytes, typically the old firmware image
* @flags: a #DfuPatchApplyFlags, e.g. %DFU_PATCH_APPLY_FLAG_IGNORE_CHECKSUM
* @error: a #GError, or %NULL
*
* Apply the currently loaded patch to a new firmware image.
*
* Return value: A #GBytes, typically saved as the new firmware file
**/
GBytes *
dfu_patch_apply (DfuPatch *self, GBytes *blob, DfuPatchApplyFlags flags, GError **error)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
const guint8 *data_old;
gsize sz;
gsize sz_max = 0;
g_autofree guint8 *data_new = NULL;
g_autoptr(GBytes) blob_checksum_new = NULL;
g_autoptr(GBytes) blob_checksum = NULL;
g_autoptr(GBytes) blob_new = NULL;
/* not loaded yet */
if (priv->chunks->len == 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"no patches loaded");
return NULL;
}
/* get the hash of the old firmware file */
blob_checksum = dfu_patch_calculate_checksum (blob);
if ((flags & DFU_PATCH_APPLY_FLAG_IGNORE_CHECKSUM) == 0 &&
!g_bytes_equal (blob_checksum, priv->checksum_old)) {
g_autofree gchar *actual = _g_bytes_to_string (blob_checksum);
g_autofree gchar *expect = _g_bytes_to_string (priv->checksum_old);
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"checksum for source did not match, expected %s, got %s",
expect, actual);
return NULL;
}
/* get the size of the new image size */
for (guint i = 0; i < priv->chunks->len; i++) {
DfuPatchChunk *chunk = g_ptr_array_index (priv->chunks, i);
gsize chunk_sz = g_bytes_get_size (chunk->blob);
if (chunk->off + chunk_sz > sz_max)
sz_max = chunk->off + chunk_sz;
}
/* first, copy the data buffer */
data_old = g_bytes_get_data (blob, &sz);
if (sz_max < sz) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"binary patch cannot truncate binary");
return NULL;
}
if (sz == sz_max) {
g_debug ("binary staying same size: %" G_GSIZE_FORMAT, sz);
} else {
g_debug ("binary growing from: %" G_GSIZE_FORMAT
" to %" G_GSIZE_FORMAT, sz, sz_max);
}
data_new = g_malloc0 (sz_max);
memcpy (data_new, data_old, sz_max);
for (guint i = 0; i < priv->chunks->len; i++) {
DfuPatchChunk *chunk = g_ptr_array_index (priv->chunks, i);
const guint8 *chunk_data;
gsize chunk_sz;
/* bigger than the total size */
chunk_data = g_bytes_get_data (chunk->blob, &chunk_sz);
if (chunk->off + chunk_sz > sz_max) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"cannot apply chunk as larger than max size");
return NULL;
}
/* apply one chunk */
g_debug ("applying chunk %u/%u @0x%04x (length %" G_GSIZE_FORMAT ")",
i + 1, priv->chunks->len, chunk->off, chunk_sz);
memcpy (data_new + chunk->off, chunk_data, chunk_sz);
}
/* check we got the desired hash */
blob_new = g_bytes_new (data_new, sz_max);
blob_checksum_new = dfu_patch_calculate_checksum (blob_new);
if ((flags & DFU_PATCH_APPLY_FLAG_IGNORE_CHECKSUM) == 0 &&
!g_bytes_equal (blob_checksum_new, priv->checksum_new)) {
g_autofree gchar *actual = _g_bytes_to_string (blob_checksum_new);
g_autofree gchar *expect = _g_bytes_to_string (priv->checksum_new);
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"checksum for result did not match, expected %s, got %s",
expect, actual);
return NULL;
}
/* success */
return g_steal_pointer (&blob_new);
}
/**
* dfu_patch_to_string:
* @self: a #DfuPatch
*
* Returns a string representaiton of the object.
*
* Return value: NULL terminated string, or %NULL for invalid
**/
gchar *
dfu_patch_to_string (DfuPatch *self)
{
DfuPatchPrivate *priv = GET_PRIVATE (self);
GString *str = g_string_new (NULL);
g_autofree gchar *checksum_old = NULL;
g_autofree gchar *checksum_new = NULL;
g_return_val_if_fail (DFU_IS_PATCH (self), NULL);
/* add checksums */
checksum_old = _g_bytes_to_string (priv->checksum_old);
g_string_append_printf (str, "checksum-old: %s\n", checksum_old);
checksum_new = _g_bytes_to_string (priv->checksum_new);
g_string_append_printf (str, "checksum-new: %s\n", checksum_new);
/* add chunks */
for (guint i = 0; i < priv->chunks->len; i++) {
DfuPatchChunk *chunk = g_ptr_array_index (priv->chunks, i);
g_string_append_printf (str, "chunk #%02u 0x%04x, length %" G_GSIZE_FORMAT "\n",
i, chunk->off, g_bytes_get_size (chunk->blob));
}
g_string_truncate (str, str->len - 1);
return g_string_free (str, FALSE);
}
/**
* dfu_patch_new:
*
* Creates a new DFU patch object.
*
* Return value: a new #DfuPatch
**/
DfuPatch *
dfu_patch_new (void)
{
DfuPatch *self;
self = g_object_new (DFU_TYPE_PATCH, NULL);
return self;
}