sim: implement proper Firehose protocol state machine

Replace the stub sim_read()/sim_write() with a simulator that
parses incoming XML commands and enqueues matching XML responses,
mirroring the real Firehose device protocol. This decreases divergence
in behavior between the real HW and the dry-run mode, giving better
validation of the flashing process in dry-run mode.

sim_write() now dispatches based on the command tag:
 - configure: enqueue log + ACK echoing the same max payload size so
   firehose_try_configure() does not trigger a redundant exchange
 - program/read: enqueue log + rawmode=true ACK and transition to
   SIM_STATE_RAW_IN/RAW_OUT to track the raw binary transfer
 - erase/patch/setbootablestoragedrive/ufs: enqueue log + ACK
 - power: enqueue log + ACK and mark the simulator as closed

sim_read() first drains the XML response queue (ensuring protocol
responses are delivered before any raw data), then serves zero-filled
sector data in SIM_STATE_RAW_OUT, and returns -EIO once closed so
the trailing drain in firehose_reset() completes immediately.

Since the simulator now produces correct protocol responses, remove the
QDL_DEVICE_SIM early-return guards in firehose_read() and
firehose_try_configure() that previously bypassed response parsing.

Pre-initialize qdl->sector_size = 4096 in sim_init() and only probe
sector size in firehose_try_configure() if unset. Previously, the
probe <read> commands were executed during --createdigest dry-runs and
included in the VIP digest table, but not issued during real VIP
flashing (where probing is gated on VIP_DISABLED). This caused all
subsequent digest entries to shift, resulting in hash mismatches on
the device.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
This commit is contained in:
Igor Opaniuk committed 2026-03-30 19:36:24 +02:00
1 parent 305bb8df6c
commit ec8dc8d332
2 files changed
+270 -18

No files matched your search

+2 -12
View File
@@ -131,10 +131,6 @@ static int firehose_read(struct qdl_device *qdl, int timeout_ms,
gettimeofday(&now, NULL);
timeradd(&now, &delta, &timeout);
/* In simulation mode we don't expent to read and parse any responses */
if (qdl->dev_type == QDL_DEVICE_SIM)
return 0;
/*
* The goal of firehose_read() is to find a response to a request among
* one or more incoming messages AND to consume all incoming messages
@@ -360,13 +356,6 @@ static int firehose_try_configure(struct qdl_device *qdl, bool skip_storage_init
if (ret < 0)
return ret;
/*
* In simulateion mode "remote" target can't propose different size, so
* for QDL_DEVICE_SIM we just don't re-send configure packet
*/
if (qdl->dev_type == QDL_DEVICE_SIM)
return 0;
/* Retry if remote proposed different size */
if (size != qdl->max_payload_size) {
ret = firehose_send_configure(qdl, size, skip_storage_init, storage, &size);
@@ -386,7 +375,8 @@ static int firehose_try_configure(struct qdl_device *qdl, bool skip_storage_init
* builds it exits before reaching this code via the SIM early-return
* above), so sending them would cause a VIP hash mismatch on the device.
*/
if (storage != QDL_STORAGE_NAND && qdl->vip_data.state == VIP_DISABLED) {
if (storage != QDL_STORAGE_NAND && qdl->vip_data.state == VIP_DISABLED &&
!qdl->sector_size) {
max_sector_size = sector_sizes[ARRAY_SIZE(sector_sizes) - 1];
buf = alloca(max_sector_size);
+268 -6
View File
@@ -2,37 +2,291 @@
/*
* 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 "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;
};
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 */
};
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;
}
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) {}
static int sim_read(struct qdl_device *qdl __unused,
void *buf __unused, size_t len,
/*
* 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)
{
return len;
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;
}
static int sim_write(struct qdl_device *qdl __unused, const void *buf __unused,
size_t len, unsigned int timeout __unused)
/*
* 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.
*/
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;
/* Raw binary payload for an ongoing program operation */
if (qdl_sim->state == SIM_STATE_RAW_IN) {
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;
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;
}
@@ -56,6 +310,14 @@ struct qdl_device *sim_init(void)
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;
}