Files
s390-tools/genprotimg/src/pv/pv_image.c
T
Marc Hartmayer 93a0cb254e Consolidate PSW masks
While at it, rename PSW_SHORT_ADDR_MASK and PSW_ADDRESS_MASK to
PSW32_ADDR_MASK. As a side effect this removes the zipl.h dependency
of the boot loaders. While at it,

Reviewed-by: Philipp Rudo <prudo@linux.ibm.com>
Reviewed-by: Stefan Haberland <sth@linux.ibm.com>
Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2020-05-06 17:36:39 +02:00

822 lines
20 KiB
C

/*
* PV image related definitions and functions
*
* Copyright IBM Corp. 2020
*
* s390-tools is free software; you can redistribute it and/or modify
* it under the terms of the MIT license. See LICENSE for details.
*/
#include <errno.h>
#include <glib.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "boot/s390.h"
#include "boot/stage3a.h"
#include "common.h"
#include "include/pv_crypto_def.h"
#include "include/pv_hdr_def.h"
#include "utils/align.h"
#include "utils/crypto.h"
#include "utils/file_utils.h"
#include "pv_args.h"
#include "pv_comps.h"
#include "pv_error.h"
#include "pv_hdr.h"
#include "pv_image.h"
#include "pv_ipib.h"
#include "pv_opt_item.h"
#include "pv_stage3.h"
const PvComponent *pv_img_get_stage3b_comp(const PvImage *img, GError **err)
{
const PvComponent *comp;
g_return_val_if_fail(pv_img_comps_length(img->comps) >= 1, NULL);
comp = pv_img_comps_get_nth_comp(img->comps,
pv_img_comps_length(img->comps) - 1);
if (!pv_component_is_stage3b(comp)) {
g_set_error(err, PV_ERROR, PV_ERROR_INTERNAL,
_("Failed to get 'stage3b' component"));
return NULL;
}
return comp;
}
typedef gint (*prepare_func)(PvComponent *obj, const gchar *tmp_path,
void *opaque, GError **err);
static gint pv_img_prepare_component(const PvImage *img, PvComponent *comp,
GError **err)
{
struct cipher_parms parms = { 0 };
g_autoptr(Buffer) tweak = NULL;
prepare_func func = NULL;
void *opaque = NULL;
gint rc;
if (img->pcf & PV_CFLAG_NO_DECRYPTION) {
/* we only need to align the components */
func = pv_component_align;
opaque = NULL;
} else {
const EVP_CIPHER *cipher = img->xts_cipher;
g_assert_cmpint((int)img->xts_key->size, ==,
EVP_CIPHER_key_length(cipher));
g_assert_cmpint((int)PAGE_SIZE % EVP_CIPHER_block_size(cipher),
==, 0);
g_assert_cmpint(sizeof(comp->tweak), ==,
EVP_CIPHER_iv_length(cipher));
g_assert(img->xts_key->size <= UINT_MAX);
tweak = buffer_alloc(sizeof(comp->tweak.data));
memcpy(tweak->data, comp->tweak.data, tweak->size);
func = pv_component_align_and_encrypt;
parms.cipher = cipher;
parms.key = img->xts_key;
parms.iv_or_tweak = tweak;
opaque = &parms;
}
rc = (*func)(comp, img->tmp_dir, opaque, err);
if (rc < 0)
return -1;
return 0;
}
static Buffer *pv_img_read_key(const gchar *path, guint key_size,
GError **err)
{
g_autoptr(Buffer) tmp_ret = NULL;
Buffer *ret = NULL;
gsize bytes_read;
FILE *f = NULL;
gsize size;
if (file_size(path, &size, err) != 0)
return NULL;
if (size - key_size != 0) {
g_set_error(err, PV_ERROR, PV_CRYPTO_ERROR_INVALID_KEY_SIZE,
_("Wrong file size '%s': read %zd, expected %u"), path, size,
key_size);
return NULL;
}
f = file_open(path, "rb", err);
if (!f)
return NULL;
tmp_ret = buffer_alloc(size);
if (file_read(f, tmp_ret->data, 1, tmp_ret->size, &bytes_read, err) < 0)
goto err;
if (bytes_read - key_size != 0) {
g_set_error(err, PV_ERROR, PV_CRYPTO_ERROR_INVALID_KEY_SIZE,
_("Wrong file size '%s': read %zd, expected %u"),
path, bytes_read, key_size);
goto err;
}
ret = g_steal_pointer(&tmp_ret);
err:
if (f)
fclose(f);
return ret;
}
static EVP_PKEY *pv_img_get_cust_pub_priv_key(gint nid, GError **err)
{
return generate_ec_key(nid, err);
}
static HostKeyList *pv_img_get_host_keys(gchar **host_cert_paths,
X509_STORE *store, gint nid,
GError **err)
{
g_autoslist(EVP_PKEY) ret = NULL;
g_assert(host_cert_paths);
for (gchar **iterator = host_cert_paths; iterator != NULL && *iterator != NULL;
iterator++) {
g_autoptr(EVP_PKEY) host_key = NULL;
const gchar *path = *iterator;
g_assert(path);
host_key = read_ec_pubkey_cert(store, nid, path, err);
if (!host_key)
return NULL;
ret = g_slist_append(ret, g_steal_pointer(&host_key));
}
return g_steal_pointer(&ret);
}
static Buffer *pv_img_get_key(const EVP_CIPHER *cipher, const gchar *path,
GError **err)
{
gint key_len = EVP_CIPHER_key_length(cipher);
g_assert(key_len > 0);
if (path)
return pv_img_read_key(path, (guint)key_len, err);
return generate_aes_key((guint)key_len, err);
}
static Buffer *pv_img_get_iv(const EVP_CIPHER *cipher, const gchar *path,
GError **err)
{
gint iv_len = EVP_CIPHER_iv_length(cipher);
g_assert(iv_len > 0);
if (path)
return pv_img_read_key(path, (guint)iv_len, err);
return generate_aes_iv((guint)iv_len, err);
}
static int hex_str_toull(const gchar *nptr, uint64_t *dst,
GError **err)
{
uint64_t value;
gchar *end;
g_assert(dst);
if (!g_str_is_ascii(nptr)) {
g_set_error(err, PV_ERROR, EINVAL,
_("Invalid value: '%s'. A hexadecimal value is required, for example '0xcfe'"),
nptr);
return -1;
}
value = g_ascii_strtoull(nptr, &end, 16);
if ((value == G_MAXUINT64 && errno == ERANGE) ||
(end && *end != '\0')) {
g_set_error(err, PV_ERROR, EINVAL,
_("Invalid value: '%s'. A hexadecimal value is required, for example '0xcfe'"),
nptr);
return -1;
}
*dst = value;
return 0;
}
static gint pv_img_set_psw_addr(PvImage *img, const gchar *psw_addr_s,
GError **err)
{
if (psw_addr_s) {
uint64_t psw_addr;
if (hex_str_toull(psw_addr_s, &psw_addr, err) < 0)
return -1;
img->initial_psw.addr = psw_addr;
}
return 0;
}
static gint pv_img_set_control_flags(PvImage *img, const gchar *pcf_s,
const gchar *scf_s, GError **err)
{
uint64_t flags;
if (pcf_s) {
if (hex_str_toull(pcf_s, &flags, err) < 0)
return -1;
img->pcf = flags;
}
if (scf_s) {
if (hex_str_toull(scf_s, &flags, err) < 0)
return -1;
img->scf = flags;
}
return 0;
}
/* read in the keys or auto-generate them */
static gint pv_img_set_keys(PvImage *img, const PvArgs *args, GError **err)
{
g_autoptr(X509_STORE) store = NULL;
g_assert(img->xts_cipher);
g_assert(img->cust_comm_cipher);
g_assert(img->gcm_cipher);
g_assert(img->nid);
img->xts_key = pv_img_get_key(img->xts_cipher, args->xts_key_path, err);
if (!img->xts_key)
return -1;
img->cust_comm_key = pv_img_get_key(img->cust_comm_cipher,
args->cust_comm_key_path, err);
if (!img->cust_comm_key)
return -1;
img->cust_root_key =
pv_img_get_key(img->gcm_cipher, args->cust_root_key_path, err);
if (!img->cust_root_key)
return -1;
img->gcm_iv = pv_img_get_iv(img->gcm_cipher, args->gcm_iv_path, err);
if (!img->gcm_iv)
return -1;
img->cust_pub_priv_key = pv_img_get_cust_pub_priv_key(img->nid, err);
if (!img->cust_pub_priv_key)
return -1;
img->host_pub_keys =
pv_img_get_host_keys(args->host_keys, store, img->nid, err);
if (!img->host_pub_keys)
return -1;
return 0;
}
static void pv_img_add_host_slot(PvImage *img, PvHdrKeySlot *slot)
{
img->key_slots = g_slist_append(img->key_slots, slot);
}
static void pv_hdr_key_slot_free(PvHdrKeySlot *slot)
{
if (!slot)
return;
g_free(slot);
}
WRAPPED_G_DEFINE_AUTOPTR_CLEANUP_FUNC(PvHdrKeySlot, pv_hdr_key_slot_free)
static PvHdrKeySlot *pv_hdr_key_slot_new(const EVP_CIPHER *gcm_cipher,
const Buffer *cust_root_key,
EVP_PKEY *cust_key, EVP_PKEY *host_key,
GError **err)
{
g_autoptr(PvHdrKeySlot) ret = g_new0(PvHdrKeySlot, 1);
g_autofree union ecdh_pub_key *pub = NULL;
g_autoptr(Buffer) exchange_key = NULL;
g_autoptr(Buffer) digest_key = NULL;
g_autoptr(Buffer) iv = NULL;
Buffer pub_buf;
/* No AAD data is used */
Buffer aad = { .data = NULL, .size = 0 };
/* Set the output buffers for the encrypted data and the
* generated GCM tag
*/
Buffer enc = { .data = ret->wrapped_key, .size = sizeof(ret->wrapped_key) };
Buffer tag = { .data = ret->tag, .size = sizeof(ret->tag) };
struct cipher_parms parms;
int64_t c_len = 0;
g_assert(EVP_CIPHER_iv_length(gcm_cipher) >= 0);
pub = evp_pkey_to_ecdh_pub_key(host_key, err);
if (!pub)
return NULL;
pub_buf.data = pub->data;
pub_buf.size = sizeof(*pub);
digest_key = sha256_buffer(&pub_buf, err);
if (!digest_key)
return NULL;
g_assert(digest_key->size == sizeof(ret->digest_key));
/* set `digest_key` field */
memcpy(ret->digest_key, digest_key->data, sizeof(ret->digest_key));
exchange_key = compute_exchange_key(cust_key, host_key, err);
if (!exchange_key)
return NULL;
/* initialize cipher parameters */
g_assert(exchange_key->size <= INT_MAX);
g_assert(exchange_key->size == (guint)EVP_CIPHER_key_length(gcm_cipher));
/* create zero IV */
iv = buffer_alloc((guint)EVP_CIPHER_iv_length(gcm_cipher));
parms.iv_or_tweak = iv;
parms.key = exchange_key;
parms.cipher = gcm_cipher;
/* Encrypt the customer root key that is used for the encryption
* of the PV header
*/
c_len = gcm_encrypt(cust_root_key, &aad, &parms, &enc, &tag, err);
if (c_len < 0)
return NULL;
g_assert(c_len == (int64_t)cust_root_key->size);
return g_steal_pointer(&ret);
}
static gint pv_img_set_host_slots(PvImage *img, GError **err)
{
for (GSList *iterator = img->host_pub_keys; iterator; iterator = iterator->next) {
EVP_PKEY *host_key = iterator->data;
g_assert(host_key);
PvHdrKeySlot *slot = pv_hdr_key_slot_new(img->gcm_cipher,
img->cust_root_key,
img->cust_pub_priv_key,
host_key, err);
if (!slot)
return -1;
pv_img_add_host_slot(img, slot);
}
return 0;
}
static gint pv_img_set_comps_offset(PvImage *img, uint64_t offset, GError **err)
{
return pv_img_comps_set_offset(img->comps, offset, err);
}
PvImage *pv_img_new(PvArgs *args, const gchar *stage3a_path, GError **err)
{
g_autoptr(PvImage) ret = g_new0(PvImage, 1);
uint64_t offset;
g_assert(args->tmp_dir);
g_assert(stage3a_path);
if (args->no_verify)
g_warning(_("host-key document verification is disabled. Your workload is not secured."));
ret->comps = pv_img_comps_new(EVP_sha512(), EVP_sha512(), EVP_sha512(), err);
if (!ret->comps)
return NULL;
ret->cust_comm_cipher = EVP_aes_256_gcm();
ret->gcm_cipher = EVP_aes_256_gcm();
ret->initial_psw.addr = DEFAULT_INITIAL_PSW_ADDR;
ret->initial_psw.mask = DEFAULT_INITIAL_PSW_MASK;
ret->nid = NID_secp521r1;
ret->tmp_dir = g_strdup(args->tmp_dir);
ret->xts_cipher = EVP_aes_256_xts();
/* set initial PSW that will be loaded by the stage3b */
if (pv_img_set_psw_addr(ret, args->psw_addr, err) < 0)
return NULL;
/* set the control flags: PCF and SCF */
if (pv_img_set_control_flags(ret, args->pcf, args->scf, err) < 0)
return NULL;
/* read in the keys */
if (pv_img_set_keys(ret, args, err) < 0)
return NULL;
if (pv_img_set_host_slots(ret, err) < 0)
return NULL;
/* allocate enough memory for the stage3a args and load the
* stage3a template into memory and set the loader_psw
*/
if (pv_img_load_and_set_stage3a(ret, stage3a_path, err) < 0)
return NULL;
offset = PAGE_ALIGN(STAGE3A_LOAD_ADDRESS + ret->stage3a->size);
/* shift right all components by the size of stage3a loader */
if (pv_img_set_comps_offset(ret, offset, err) < 0)
return NULL;
return g_steal_pointer(&ret);
}
void pv_img_free(PvImage *img)
{
if (!img)
return;
g_slist_free_full(img->optional_items,
(GDestroyNotify)pv_opt_item_free);
g_slist_free_full(img->key_slots, (GDestroyNotify)pv_hdr_key_slot_free);
g_slist_free_full(img->host_pub_keys, (GDestroyNotify)EVP_PKEY_free);
EVP_PKEY_free(img->cust_pub_priv_key);
buffer_clear(&img->stage3a);
pv_img_comps_free(img->comps);
g_free(img->tmp_dir);
buffer_free(img->xts_key);
buffer_free(img->cust_root_key);
buffer_free(img->gcm_iv);
buffer_free(img->cust_comm_key);
g_free(img);
}
static gint pv_img_prepare_and_add_component(PvImage *img, PvComponent **comp,
GError **err)
{
g_assert(comp);
g_assert(*comp);
/* prepares the component: does the alignment and encryption
* if required
*/
if (pv_img_prepare_component(img, *comp, err) < 0)
return -1;
/* calculates the memory layout and adds the component to its
* internal list
*/
if (pv_img_comps_add_component(img->comps, comp, err) < 0)
return -1;
g_assert(!*comp);
return 0;
}
gint pv_img_add_component(PvImage *img, const PvArg *arg, GError **err)
{
g_autoptr(PvComponent) comp = NULL;
comp = pv_component_new_file(arg->type, arg->path, err);
if (!comp)
return -1;
if (pv_img_prepare_and_add_component(img, &comp, err) < 0)
return -1;
g_assert(!comp);
return 0;
}
gint pv_img_calc_pld_ald_tld_nep(const PvImage *img, Buffer **pld, Buffer **ald,
Buffer **tld, uint64_t *nep, GError **err)
{
return pv_img_comps_finalize(img->comps, pld, ald, tld, nep, err);
}
static gint pv_img_build_stage3b(PvImage *img, Buffer *stage3b, GError **err)
{
g_autofree struct stage3b_args *args = NULL;
args = pv_img_comps_get_stage3b_args(img->comps, &img->initial_psw);
if (!args) {
g_set_error(err, PV_ERROR, PV_ERROR_INTERNAL,
_("Cannot generate stage3b arguments"));
return -1;
}
build_stage3b(stage3b, args);
return 0;
}
gint pv_img_add_stage3b_comp(PvImage *img, const gchar *path, GError **err)
{
g_autoptr(PvComponent) comp = NULL;
g_autoptr(Buffer) stage3b = NULL;
stage3b = stage3b_getblob(path, err);
if (!stage3b)
return -1;
/* set the stage3b data */
if (pv_img_build_stage3b(img, stage3b, err) < 0)
return -1;
comp = pv_component_new_buf(PV_COMP_TYPE_STAGE3B, stage3b, err);
if (!comp)
return -1;
if (pv_img_prepare_and_add_component(img, &comp, err) < 0)
return -1;
g_assert(!comp);
return 0;
}
static uint32_t pv_img_get_aad_size(const PvImage *img)
{
uint32_t key_size, size = 0;
g_assert(sizeof(struct pv_hdr_head) <= UINT32_MAX);
g_assert(sizeof(struct pv_hdr_key_slot) <= UINT32_MAX);
g_assert_true(g_uint_checked_add(&size, size,
(uint32_t)sizeof(struct pv_hdr_head)));
g_assert_true(g_uint_checked_mul(&key_size,
(uint32_t)sizeof(struct pv_hdr_key_slot),
g_slist_length(img->key_slots)));
g_assert_true(g_uint_checked_add(&size, size, key_size));
return size;
}
static uint32_t pv_img_get_opt_items_size(const PvImage *img)
{
uint32_t ret = 0;
g_assert(img);
for (GSList *iterator = img->optional_items; iterator;
iterator = iterator->next) {
const struct pv_hdr_opt_item *item = iterator->data;
g_assert(item);
g_assert_true(g_uint_checked_add(&ret, ret, pv_opt_item_size(item)));
}
return ret;
}
uint32_t pv_img_get_enc_size(const PvImage *img)
{
uint32_t ret = 0;
g_assert(sizeof(struct pv_hdr_encrypted) <= UINT32_MAX);
g_assert_true(g_uint_checked_add(
&ret, ret, (uint32_t)sizeof(struct pv_hdr_encrypted)));
g_assert_true(
g_uint_checked_add(&ret, ret, pv_img_get_opt_items_size(img)));
return ret;
}
static uint32_t pv_img_get_tag_size(const PvImage *img G_GNUC_UNUSED)
{
g_assert(sizeof(((struct pv_hdr *)0)->tag) <= UINT32_MAX);
return (uint32_t)sizeof(((struct pv_hdr *)0)->tag);
}
uint32_t pv_img_get_pv_hdr_size(const PvImage *img)
{
uint32_t size = 0;
g_assert_true(
g_uint_checked_add(&size, size, pv_img_get_aad_size(img)));
g_assert_true(
g_uint_checked_add(&size, size, pv_img_get_enc_size(img)));
g_assert_true(
g_uint_checked_add(&size, size, pv_img_get_tag_size(img)));
return size;
}
static gint get_stage3a_data_size(const PvImage *img, gsize *data_size,
GError **err)
{
gsize ipib_size, hdr_size;
g_assert(data_size);
g_assert(*data_size == 0);
ipib_size = pv_ipib_get_size(pv_img_comps_length(img->comps));
if (ipib_size > PV_V1_IPIB_MAX_SIZE) {
g_set_error(err, PV_ERROR, PV_ERROR_IPIB_SIZE,
_("IPIB size is too large: '%zu' > '%zu'"),
ipib_size, PV_V1_IPIB_MAX_SIZE);
return -1;
}
hdr_size = pv_img_get_pv_hdr_size(img);
if (hdr_size > PV_V1_PV_HDR_MAX_SIZE) {
g_set_error(err, PV_ERROR, PV_ERROR_PV_HDR_SIZE,
_("PV header size is too large: '%zu' > '%zu'"),
hdr_size, PV_V1_PV_HDR_MAX_SIZE);
return -1;
}
*data_size += PAGE_ALIGN(ipib_size);
*data_size += PAGE_ALIGN(hdr_size);
return 0;
}
gint pv_img_load_and_set_stage3a(PvImage *img, const gchar *path, GError **err)
{
g_autoptr(Buffer) stage3a = NULL;
gsize bin_size, data_size = 0;
if (get_stage3a_data_size(img, &data_size, err) < 0)
return -1;
stage3a = stage3a_getblob(path, &bin_size, data_size, err);
if (!stage3a)
return -1;
img->stage3a_psw.addr = STAGE3A_ENTRY;
img->stage3a_psw.mask = DEFAULT_INITIAL_PSW_MASK;
/* set addresses and size */
img->stage3a = g_steal_pointer(&stage3a);
img->stage3a_bin_size = bin_size;
return 0;
}
/* Creates the PV IPIB and sets the stage3a arguments */
static gint pv_img_build_stage3a(Buffer *stage3a, gsize stage3a_bin_size,
GSList *comps, const Buffer *hdr, GError **err)
{
g_autofree struct ipl_parameter_block *ipib = NULL;
g_assert(stage3a);
g_assert(hdr);
ipib = pv_ipib_new(comps, hdr, err);
if (!ipib)
return -1;
if (build_stage3a(stage3a, stage3a_bin_size, hdr, ipib, err) < 0)
return -1;
g_info("%12s:\t0x%012lx (%12ld / %12ld Bytes)", "stage3a",
STAGE3A_LOAD_ADDRESS, stage3a->size, stage3a->size);
return 0;
}
/* Creates the actual PV header (serialized and AES-GCM encrypted) */
static Buffer *pv_img_create_pv_hdr(PvImage *img, GError **err)
{
g_autoptr(Buffer) hdr_buf = NULL;
g_autoptr(PvHdr) hdr = NULL;
hdr = pv_hdr_new(img, err);
if (!hdr)
return NULL;
hdr_buf = pv_hdr_serialize(hdr, img, PV_ENCRYPT, err);
if (!hdr_buf)
return NULL;
return g_steal_pointer(&hdr_buf);
}
/* No changes to the components are allowed after calling this
* function
*/
gint pv_img_finalize(PvImage *pv, const gchar *stage3b_path, GError **err)
{
g_autoptr(Buffer) hdr = NULL;
/* load stage3b template into memory and add it to the list of
* components. This must be done before calling
* `pv_img_load_and_set_stage3a`.
*/
if (pv_img_add_stage3b_comp(pv, stage3b_path, err) < 0)
return -1;
/* create the PV header */
hdr = pv_img_create_pv_hdr(pv, err);
if (!hdr)
return -1;
/* generate stage3a. At this point in time the PV header and
* the stage3b must be generated and encrypted
*/
if (pv_img_build_stage3a(pv->stage3a, pv->stage3a_bin_size,
pv_img_comps_get_comps(pv->comps), hdr, err) < 0)
return -1;
return 0;
}
static gint convert_psw_to_short_psw(const struct psw_t *psw, uint64_t *dst,
GError **err)
{
g_assert(psw);
g_assert(dst);
uint64_t psw_addr = psw->addr;
uint64_t psw_mask = psw->mask;
/* test if PSW mask can be converted */
if (psw_mask & PSW32_ADDR_MASK) {
g_set_error(err, PV_ERROR, PV_ERROR_INTERNAL,
_("Failed to convert PSW to short PSW"));
return -1;
}
/* test for bit 12 */
if (psw_mask & PSW_MASK_BIT_12) {
g_set_error(err, PV_ERROR, PV_ERROR_INTERNAL,
_("Failed to convert PSW to short PSW"));
return -1;
}
/* test if PSW addr can be converted */
if (psw_addr & ~PSW32_ADDR_MASK) {
g_set_error(err, PV_ERROR, PV_ERROR_INTERNAL,
_("Failed to convert PSW to short PSW"));
return -1;
}
*dst = psw_mask;
/* set bit 12 to 1 */
*dst |= PSW_MASK_BIT_12;
*dst |= psw_addr;
return 0;
}
static gint write_short_psw(FILE *f, struct psw_t *psw, GError **err)
{
uint64_t short_psw, short_psw_be;
if (convert_psw_to_short_psw(psw, &short_psw, err) < 0)
return -1;
short_psw_be = GUINT64_TO_BE(short_psw);
return file_write(f, &short_psw_be, 1, sizeof(short_psw_be), NULL, err);
}
gint pv_img_write(PvImage *img, const gchar *path, GError **err)
{
gint ret = -1;
FILE *f = file_open(path, "wb", err);
if (!f)
return -1;
if (write_short_psw(f, &img->stage3a_psw, err) < 0) {
g_prefix_error(err, _("Failed to write image '%s': "), path);
goto err;
}
if (seek_and_write_buffer(f, img->stage3a, STAGE3A_LOAD_ADDRESS, err) <
0) {
g_prefix_error(err, _("Failed to write image '%s': "), path);
goto err;
}
/* list is sorted by component type => by address */
for (GSList *iterator = pv_img_comps_get_comps(img->comps); iterator;
iterator = iterator->next) {
gint rc;
const PvComponent *comp = iterator->data;
rc = pv_component_write(comp, f, err);
if (rc < 0) {
g_prefix_error(err, _("Failed to write image '%s': "),
path);
goto err;
}
}
ret = 0;
err:
if (f)
fclose(f);
return ret;
}