Files
qdl/sim.c
T
Igor Opaniuk 2116ac1ba7 sim: fix indentation alignment in sim_vip_signed_payload()
Fix indentation alignment in sim_vip_signed_payload().

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 10:16:17 -05:00

678 lines
20 KiB
C

// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved.
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include "oscompat.h"
#include "sha2.h"
#include "sim.h"
/*
* Response XML templates sent by the simulated device.
*
* The configure response echoes back MaxPayloadSizeToTargetInBytes matching
* the host's initial value (1 MiB), so firehose_try_configure() never
* triggers a second configure exchange.
*/
#define SIM_ACK \
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" \
"<data><response value=\"ACK\" rawmode=\"false\" /></data>"
#define SIM_ACK_RAWMODE \
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" \
"<data><response value=\"ACK\" rawmode=\"true\" /></data>"
#define SIM_CONFIGURE_ACK \
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" \
"<data><response value=\"ACK\" MemoryName=\"ufs\"" \
" MinVersionSupported=\"1\" Version=\"1\"" \
" MaxPayloadSizeToTargetInBytes=\"1048576\"" \
" MaxPayloadSizeToTargetInBytesSupported=\"1048576\"" \
" MaxXMLSizeInBytes=\"4096\" /></data>"
/* sim_state tracks whether the sim is in XML command mode or raw binary mode */
enum sim_state {
SIM_STATE_XML, /* normal state: dequeue queued XML responses */
SIM_STATE_RAW_IN, /* receiving raw binary data from host (program) */
SIM_STATE_RAW_OUT, /* sending raw binary data to host (read) */
};
struct sim_response {
char *data;
size_t len;
struct sim_response *next;
};
/*
* VIP hash validation state.
*
* sim_vip_recv_table() extracts the data-chunk SHA256 hashes from each
* received VIP table (signed MBN or raw chained binary) and stores them in
* vip_hashes[]. sim_vip_check_chunk() computes SHA256 of each incoming chunk
* and compares it against the next stored entry.
*
* Maximum capacity:
* signed table: MAX_DIGESTS_PER_SIGNED_FILE - 1 = 53 chunk hashes
* chained tables: MAX_CHAINED_FILES x (MAX_DIGESTS_PER_CHAINED_FILE - 1)
* = 32 x 255 = 8160 chunk hashes
* total: 8213 entries x SHA256_DIGEST_LENGTH bytes = ~256 KiB
* (heap-allocated together with the enclosing struct qdl_device_sim)
*/
#define SIM_VIP_MAX_HASHES \
((MAX_DIGESTS_PER_SIGNED_FILE - 1) + \
MAX_CHAINED_FILES * (MAX_DIGESTS_PER_CHAINED_FILE - 1))
struct qdl_device_sim {
struct qdl_device base;
struct vip_table_generator *vip_gen;
bool create_digests;
/* Response queue consumed by sim_read() */
struct sim_response *resp_head;
struct sim_response *resp_tail;
enum sim_state state;
size_t raw_remaining; /* bytes of raw data left to transfer */
bool closed; /* set after power command to terminate reads fast */
/* VIP hash validation */
uint8_t vip_hashes[SIM_VIP_MAX_HASHES][SHA256_DIGEST_LENGTH];
size_t vip_hash_count; /* entries populated from received VIP tables */
size_t vip_hash_idx; /* index of the next hash to verify */
/* Chain hash linking each VIP table to the next */
uint8_t vip_chain_hash[SHA256_DIGEST_LENGTH];
bool vip_has_chain_hash;
};
static void sim_enqueue(struct qdl_device_sim *qdl_sim, const char *xml)
{
struct sim_response *resp;
resp = malloc(sizeof(*resp));
if (!resp)
err(1, "sim: failed to allocate response entry");
resp->data = strdup(xml);
if (!resp->data)
err(1, "sim: failed to duplicate response string");
resp->len = strlen(xml);
resp->next = NULL;
if (qdl_sim->resp_tail)
qdl_sim->resp_tail->next = resp;
else
qdl_sim->resp_head = resp;
qdl_sim->resp_tail = resp;
}
static void sim_enqueue_log(struct qdl_device_sim *qdl_sim, const char *handler)
{
char buf[256];
snprintf(buf, sizeof(buf),
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
"<data><log value=\"INFO: Calling handler for %s\" /></data>",
handler);
sim_enqueue(qdl_sim, buf);
}
static unsigned int sim_get_uint_attr(xmlNode *node, const char *name)
{
xmlChar *val;
unsigned int ret;
val = xmlGetProp(node, (xmlChar *)name);
if (!val)
return 0;
ret = (unsigned int)strtoul((char *)val, NULL, 10);
xmlFree(val);
return ret;
}
/*
* Qualcomm MBN header structures (little-endian, packed).
*
* The signed VIP table (DigestsToSign.bin.mbn) uses one of these layouts.
* V5 and V6 share the same image_size/code_size offsets; V7 reorders the
* fields. Only V5 and V6 are supported for VIP table parsing today.
*/
struct mbn_header_v5 {
uint32_t image_id;
uint32_t header_version;
uint32_t qti_signature_size;
uint32_t qti_cert_chain_size;
uint32_t image_size;
uint32_t code_size;
uint32_t signature_ptr;
uint32_t signature_size;
uint32_t cert_chain_ptr;
uint32_t cert_chain_size;
} __attribute__((packed));
struct mbn_header_v6 {
uint32_t image_id;
uint32_t header_version;
uint32_t qti_signature_size;
uint32_t qti_cert_chain_size;
uint32_t image_size;
uint32_t code_size;
uint32_t signature_ptr;
uint32_t signature_size;
uint32_t cert_chain_ptr;
uint32_t cert_chain_size;
uint32_t qti_metadata_size;
uint32_t metadata_size;
} __attribute__((packed));
#define MBN_HEADER_VERSION_5 5
#define MBN_HEADER_VERSION_6 6
/*
* sim_vip_signed_payload() - locate the hash payload in a signed VIP MBN table
*
* DigestsToSign.bin.mbn is a Qualcomm MBN image with a packed header followed
* by the hash data, a signature, and certificate chains. The header_version
* field identifies the layout; V5 and V6 share the same image_size/code_size
* offsets. The payload begins at (file_size - image_size) and spans code_size
* bytes.
*
* Returns a pointer into @buf for the payload, or NULL if not recognised.
* Sets @out_len to the payload byte count on success.
*/
static const uint8_t *sim_vip_signed_payload(const uint8_t *buf, size_t len,
size_t *out_len)
{
struct mbn_header_v6 hdr;
size_t offset;
if (len < sizeof(struct mbn_header_v5))
return NULL;
memcpy(&hdr, buf, sizeof(struct mbn_header_v5));
if (hdr.header_version != MBN_HEADER_VERSION_5 &&
hdr.header_version != MBN_HEADER_VERSION_6)
return NULL;
if (hdr.code_size == 0 ||
hdr.code_size % SHA256_DIGEST_LENGTH != 0 ||
hdr.code_size > MAX_DIGESTS_PER_SIGNED_FILE * SHA256_DIGEST_LENGTH ||
hdr.image_size < hdr.code_size || hdr.image_size > len)
return NULL;
offset = len - hdr.image_size;
if (offset < sizeof(struct mbn_header_v5) || offset + hdr.code_size > len)
return NULL;
*out_len = hdr.code_size;
return buf + offset;
}
/*
* sim_vip_recv_table() - parse a received VIP table and store chunk hashes
*
* @is_signed: true -> DigestsToSign.bin.mbn (MBN-wrapped, signed)
* false -> ChainedTableOfDigests<n>.bin (raw binary)
*
* The last entry of a full signed table and of a non-final chained table is a
* chain hash (SHA256 of the following table) rather than a data-chunk hash.
* It is excluded from the stored list.
*
* A chained table is "final" when its raw byte count is not a multiple of
* SHA256_DIGEST_LENGTH: vip_gen_finalize() appends a single zero-padding byte
* to the final chained table so that it cannot be a USB-512-byte multiple.
*/
static void sim_vip_recv_table(struct qdl_device_sim *qdl_sim,
const void *buf, size_t len, bool is_signed)
{
const uint8_t *hashes;
size_t count, data_count;
bool has_chain_hash;
if (is_signed) {
size_t payload_len;
hashes = sim_vip_signed_payload(buf, len, &payload_len);
if (!hashes) {
ux_err("sim: VIP signed table: unrecognised format "
"(%zu bytes)\n", len);
return;
}
count = payload_len / SHA256_DIGEST_LENGTH;
/* Full table uses the last slot for the chain hash */
has_chain_hash = (count == MAX_DIGESTS_PER_SIGNED_FILE);
/* Save chain hash so the first chained table can be verified */
if (has_chain_hash) {
memcpy(qdl_sim->vip_chain_hash,
hashes + (count - 1) * SHA256_DIGEST_LENGTH,
SHA256_DIGEST_LENGTH);
qdl_sim->vip_has_chain_hash = true;
}
} else {
/* Verify this table against the chain hash from the previous table */
if (qdl_sim->vip_has_chain_hash) {
uint8_t hash[SHA256_DIGEST_LENGTH];
char got_hex[SHA256_DIGEST_STRING_LENGTH];
char exp_hex[SHA256_DIGEST_STRING_LENGTH];
SHA2_CTX ctx;
size_t i;
SHA256Init(&ctx);
SHA256Update(&ctx, buf, len);
SHA256Final(hash, &ctx);
if (memcmp(hash, qdl_sim->vip_chain_hash,
SHA256_DIGEST_LENGTH) != 0) {
for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
sprintf(got_hex + i * 2, "%02x", hash[i]);
sprintf(exp_hex + i * 2,
"%02x",
qdl_sim->vip_chain_hash[i]);
}
got_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0';
exp_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0';
ux_err("sim: VIP chained table chain hash mismatch\n"
" computed: %s\n"
" expected: %s\n",
got_hex, exp_hex);
} else {
ux_debug("sim: VIP chained table chain hash OK\n");
}
qdl_sim->vip_has_chain_hash = false;
}
hashes = buf;
if (len % SHA256_DIGEST_LENGTH != 0) {
/*
* Trailing zero-padding byte -> final chained table.
* All floor(len / 32) entries are chunk hashes.
*/
count = len / SHA256_DIGEST_LENGTH;
has_chain_hash = false;
} else {
/*
* Size is a multiple of 32 -> non-final chained table.
* The last entry is a chain hash to the next table.
*/
count = len / SHA256_DIGEST_LENGTH;
has_chain_hash = (count == MAX_DIGESTS_PER_CHAINED_FILE);
}
/* Save chain hash so the next chained table can be verified */
if (has_chain_hash) {
memcpy(qdl_sim->vip_chain_hash,
hashes + (count - 1) * SHA256_DIGEST_LENGTH,
SHA256_DIGEST_LENGTH);
qdl_sim->vip_has_chain_hash = true;
}
}
data_count = has_chain_hash ? count - 1 : count;
if (qdl_sim->vip_hash_count + data_count > SIM_VIP_MAX_HASHES) {
ux_err("sim: VIP table overflow (%zu + %zu > %u), "
"truncating\n",
qdl_sim->vip_hash_count, data_count, SIM_VIP_MAX_HASHES);
data_count = SIM_VIP_MAX_HASHES - qdl_sim->vip_hash_count;
}
memcpy(qdl_sim->vip_hashes[qdl_sim->vip_hash_count],
hashes, data_count * SHA256_DIGEST_LENGTH);
qdl_sim->vip_hash_count += data_count;
ux_debug("sim: loaded %zu VIP chunk hashes from %s table "
"(total: %zu)\n",
data_count, is_signed ? "signed" : "chained",
qdl_sim->vip_hash_count);
}
/*
* sim_vip_check_chunk() - SHA256 @buf/@len and verify against the next entry
*
* Called once per VIP chunk (one XML command write or one raw-data write).
* A mismatch is reported via ux_err() but does not abort, so that all
* mismatches across a full flash run are visible at once.
*/
static void sim_vip_check_chunk(struct qdl_device_sim *qdl_sim,
const void *buf, size_t len)
{
uint8_t hash[SHA256_DIGEST_LENGTH];
char got_hex[SHA256_DIGEST_STRING_LENGTH];
char exp_hex[SHA256_DIGEST_STRING_LENGTH];
const uint8_t *expected;
SHA2_CTX ctx;
size_t i;
if (qdl_sim->create_digests || !qdl_sim->vip_hash_count)
return;
SHA256Init(&ctx);
SHA256Update(&ctx, buf, len);
SHA256Final(hash, &ctx);
if (qdl_sim->vip_hash_idx >= qdl_sim->vip_hash_count) {
ux_err("sim: VIP chunk %zu has no entry in the digest table "
"(table has %zu entries)\n",
qdl_sim->vip_hash_idx, qdl_sim->vip_hash_count);
qdl_sim->vip_hash_idx++;
return;
}
expected = qdl_sim->vip_hashes[qdl_sim->vip_hash_idx];
if (memcmp(hash, expected, SHA256_DIGEST_LENGTH) == 0) {
ux_debug("sim: VIP chunk %zu hash OK\n",
qdl_sim->vip_hash_idx);
qdl_sim->vip_hash_idx++;
return;
}
for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
sprintf(got_hex + i * 2, "%02x", hash[i]);
sprintf(exp_hex + i * 2, "%02x", expected[i]);
}
got_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0';
exp_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0';
ux_err("sim: VIP hash mismatch for chunk %zu\n"
" computed: %s\n"
" expected: %s\n",
qdl_sim->vip_hash_idx, got_hex, exp_hex);
qdl_sim->vip_hash_idx++;
}
static int sim_open(struct qdl_device *qdl __unused,
const char *serial __unused)
{
ux_info("This is a dry-run execution of QDL. No actual flashing has been performed\n");
return 0;
}
static void sim_close(struct qdl_device *qdl __unused) {}
/*
* sim_read() - serve the next queued XML response or raw-out data
*
* Returns the number of bytes written to @buf, or a negative errno.
* Returns -ETIMEDOUT when the response queue is empty (mirroring the
* behaviour of the USB driver when no data arrives within the timeout).
* Returns -EIO after a power/reset command to let the caller drain quickly.
*/
static int sim_read(struct qdl_device *qdl, void *buf, size_t len,
unsigned int timeout __unused)
{
struct qdl_device_sim *qdl_sim = container_of(qdl, struct qdl_device_sim, base);
struct sim_response *resp;
size_t copy_len;
/*
* Always drain the response queue first. This matters for read
* commands: sim_write() sets SIM_STATE_RAW_OUT and queues the
* rawmode=true ACK at the same time, so the pending XML must be
* served before we start sending raw sector data.
*/
resp = qdl_sim->resp_head;
if (resp) {
copy_len = resp->len;
memcpy(buf, resp->data, copy_len);
qdl_sim->resp_head = resp->next;
if (!qdl_sim->resp_head)
qdl_sim->resp_tail = NULL;
free(resp->data);
free(resp);
return copy_len;
}
/*
* Queue empty and in raw-out mode: the host has received the
* rawmode=true ACK and is now reading sector data. Serve zeros
* and enqueue the final rawmode=false ACK when all sectors are done.
*/
if (qdl_sim->state == SIM_STATE_RAW_OUT) {
copy_len = MIN(len, qdl_sim->raw_remaining);
memset(buf, 0, copy_len);
qdl_sim->raw_remaining -= copy_len;
if (qdl_sim->raw_remaining == 0) {
sim_enqueue(qdl_sim, SIM_ACK);
qdl_sim->state = SIM_STATE_XML;
}
return copy_len;
}
/*
* Queue is empty. Return -EIO after power/reset so that callers
* which drain trailing log messages after the final ACK return
* immediately instead of spinning until the timeout expires.
*/
if (qdl_sim->closed)
return -EIO;
return -ETIMEDOUT;
}
/*
* sim_write() - accept a host write and queue the matching device response(s)
*
* In SIM_STATE_RAW_IN the simulator counts down the expected raw payload and
* queues the final rawmode=false ACK once the last byte arrives.
*
* In SIM_STATE_XML the XML command is parsed and one or more response messages
* are pushed onto the queue for sim_read() to serve back.
*
* In dry-run mode without digest generation, binary writes that fail XML
* parsing are treated as VIP digest tables: sim_vip_recv_table() extracts the
* chunk hashes and an ACK is enqueued so that the host's post-send
* firehose_read() succeeds. Every subsequent XML command write and every
* raw-data write is then verified against the stored hashes by
* sim_vip_check_chunk().
*/
static int sim_write(struct qdl_device *qdl, const void *buf, size_t len,
unsigned int timeout __unused)
{
struct qdl_device_sim *qdl_sim = container_of(qdl, struct qdl_device_sim, base);
unsigned int num_sectors, sector_size;
xmlNode *root, *node, *child;
xmlDoc *doc;
/*
* VIP table writes (both the signed MBN and chained raw binaries) may
* arrive while SIM_STATE_RAW_IN is active, interleaved between data
* chunks of the same program operation. Intercept them here using the
* sending_table flag set exclusively by vip_transfer_send_raw(), before
* the raw-data accounting branch below.
*/
if (!qdl_sim->create_digests &&
qdl_sim->base.vip_data.sending_table) {
bool is_signed = (qdl_sim->base.vip_data.state == VIP_INIT);
sim_vip_recv_table(qdl_sim, buf, len, is_signed);
sim_enqueue(qdl_sim, SIM_ACK);
return len;
}
/* Raw binary payload for an ongoing program operation */
if (qdl_sim->state == SIM_STATE_RAW_IN) {
/*
* Each qdl_write() call maps to exactly one VIP chunk
* (firehose_do_program() loops over max_payload_size chunks,
* calling vip_gen_chunk_init/update/store once per iteration).
* Hash the entire write and verify against the digest table.
*/
sim_vip_check_chunk(qdl_sim, buf, len);
if (len >= qdl_sim->raw_remaining) {
sim_enqueue(qdl_sim, SIM_ACK);
qdl_sim->state = SIM_STATE_XML;
qdl_sim->raw_remaining = 0;
} else {
qdl_sim->raw_remaining -= len;
}
return len;
}
/* XML command: parse and enqueue appropriate response(s) */
doc = xmlReadMemory(buf, len, NULL, NULL,
XML_PARSE_NOWARNING | XML_PARSE_NOERROR);
if (!doc)
return len;
/*
* Valid XML command: verify it against the digest table before
* dispatching so that the hash index stays in step with the
* vip_gen_chunk_store() calls made during the dry-run that
* produced the table.
*/
sim_vip_check_chunk(qdl_sim, buf, len);
root = xmlDocGetRootElement(doc);
/* Locate the <data> element */
for (node = root; node; node = node->next) {
if (node->type == XML_ELEMENT_NODE &&
xmlStrcmp(node->name, (xmlChar *)"data") == 0)
break;
}
if (!node)
goto out;
/* First child element is the command */
for (child = node->children;
child && child->type != XML_ELEMENT_NODE;
child = child->next)
;
if (!child)
goto out;
if (xmlStrcmp(child->name, (xmlChar *)"configure") == 0) {
sim_enqueue_log(qdl_sim, "configure");
sim_enqueue(qdl_sim, SIM_CONFIGURE_ACK);
} else if (xmlStrcmp(child->name, (xmlChar *)"program") == 0) {
num_sectors = sim_get_uint_attr(child, "num_partition_sectors");
sector_size = sim_get_uint_attr(child, "SECTOR_SIZE_IN_BYTES");
sim_enqueue_log(qdl_sim, "program");
sim_enqueue(qdl_sim, SIM_ACK_RAWMODE);
qdl_sim->state = SIM_STATE_RAW_IN;
qdl_sim->raw_remaining = (size_t)num_sectors * sector_size;
} else if (xmlStrcmp(child->name, (xmlChar *)"read") == 0) {
num_sectors = sim_get_uint_attr(child, "num_partition_sectors");
sector_size = sim_get_uint_attr(child, "SECTOR_SIZE_IN_BYTES");
sim_enqueue_log(qdl_sim, "read");
sim_enqueue(qdl_sim, SIM_ACK_RAWMODE);
qdl_sim->state = SIM_STATE_RAW_OUT;
qdl_sim->raw_remaining = (size_t)num_sectors * sector_size;
} else if (xmlStrcmp(child->name, (xmlChar *)"erase") == 0) {
sim_enqueue_log(qdl_sim, "erase");
sim_enqueue(qdl_sim, SIM_ACK);
} else if (xmlStrcmp(child->name, (xmlChar *)"patch") == 0) {
sim_enqueue_log(qdl_sim, "patch");
sim_enqueue(qdl_sim, SIM_ACK);
} else if (xmlStrcmp(child->name, (xmlChar *)"setbootablestoragedrive") == 0) {
sim_enqueue_log(qdl_sim, "setbootablestoragedrive");
sim_enqueue(qdl_sim, SIM_ACK);
} else if (xmlStrcmp(child->name, (xmlChar *)"power") == 0) {
sim_enqueue_log(qdl_sim, "power");
sim_enqueue(qdl_sim, SIM_ACK);
/*
* Mark the sim closed so that the trailing drain read in
* firehose_reset() returns -EIO immediately rather than
* spinning until the timeout expires.
*/
qdl_sim->closed = true;
} else if (xmlStrcmp(child->name, (xmlChar *)"ufs") == 0) {
sim_enqueue_log(qdl_sim, "ufs");
sim_enqueue(qdl_sim, SIM_ACK);
} else {
/* Unknown command: respond with a generic ACK */
sim_enqueue(qdl_sim, SIM_ACK);
}
out:
xmlFreeDoc(doc);
return len;
}
static void sim_set_out_chunk_size(struct qdl_device *qdl __unused,
long size __unused)
{}
struct qdl_device *sim_init(void)
{
struct qdl_device *qdl = malloc(sizeof(struct qdl_device_sim));
if (!qdl)
return NULL;
memset(qdl, 0, sizeof(struct qdl_device_sim));
qdl->dev_type = QDL_DEVICE_SIM;
qdl->open = sim_open;
qdl->read = sim_read;
qdl->write = sim_write;
qdl->close = sim_close;
qdl->set_out_chunk_size = sim_set_out_chunk_size;
qdl->max_payload_size = 1048576;
/*
* Pre-set a non-zero sector size so that firehose_try_configure()
* skips the sector-size probe reads. Those probe commands would
* otherwise be hashed into the VIP digest table during dry-run
* digest generation (--createdigest), but they are never sent
* during the real VIP flash -- causing a hash mismatch.
*/
qdl->sector_size = 4096;
return qdl;
}
struct vip_table_generator *sim_get_vip_generator(struct qdl_device *qdl)
{
struct qdl_device_sim *qdl_sim;
if (qdl->dev_type != QDL_DEVICE_SIM)
return NULL;
qdl_sim = container_of(qdl, struct qdl_device_sim, base);
if (!qdl_sim->create_digests)
return NULL;
return qdl_sim->vip_gen;
}
bool sim_set_digest_generation(bool create_digests, struct qdl_device *qdl,
struct vip_table_generator *vip_gen)
{
struct qdl_device_sim *qdl_sim;
if (qdl->dev_type != QDL_DEVICE_SIM)
return false;
qdl_sim = container_of(qdl, struct qdl_device_sim, base);
qdl_sim->create_digests = create_digests;
qdl_sim->vip_gen = vip_gen;
return true;
}