// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. */ #include #include #include #include #include #include #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 \ "" \ "" #define SIM_ACK_RAWMODE \ "" \ "" #define SIM_CONFIGURE_ACK \ "" \ "" /* 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), "" "", 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.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 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; }