Files
qdl/firehose.c
T
Igor Opaniuk 2576983692 firehose: detect VIP mode mismatch from programmer startup logs
Both directions of host/device VIP misconfiguration currently surface
as opaque failures that operators in the field have to diagnose from
raw byte-stream errors, when the right answer is fully derivable from
the programmer's own startup logs. Do the detection here so customers
don't have to debug this manually on every new platform.

The Firehose programmer announces VIP activation by emitting an
"INFO: VIP is enabled, receiving the signed table of size N" log line
during startup. Watch for that marker in firehose_read() and use it in
firehose_detect_and_configure() to handle the two mismatch cases:

  - --vip-table-path missing, programmer requires VIP: previously qdl
    would send configure, the programmer would wait silently for a
    signed table that never comes, and the user would see only a
    generic 5s configure timeout. Now bail out immediately with an
    explicit error pointing at the missing option.

  - --vip-table-path given, programmer not in VIP mode: previously qdl
    would push the signed digest table at the programmer, which tried
    to parse those binary bytes as XML and bailed out with "Failed to
    parse xml at offset 4090, 4096". Now detect the absence of the
    marker after the startup-log drain, print a warning, demote
    vip_data.state to VIP_DISABLED so the table is never sent, and
    continue flashing normally.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-01 23:47:51 -05:00

1573 lines
40 KiB
C

// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2016-2017, Linaro Ltd.
* Copyright (c) 2018, The Linux Foundation. All rights reserved.
* All rights reserved.
*/
#include "list.h"
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include "qdl.h"
#include "file.h"
#include "firehose.h"
#include "sha2.h"
#include "ufs.h"
#include "oscompat.h"
#include "vip.h"
#include "sparse.h"
#include "gpt.h"
enum {
FIREHOSE_ACK = 0,
FIREHOSE_NAK,
};
/*
* Substring emitted by the Firehose programmer's startup logs when VIP is
* active on-device (e.g. "INFO: VIP is enabled, receiving the signed table
* of size 8192"). Matching a stable prefix avoids coupling to the trailing
* byte count, which varies per programmer build.
*/
#define VIP_PROGRAMMER_MARKER "VIP is enabled, receiving the signed table"
static void firehose_check_vip_marker(struct qdl_device *qdl, xmlNode *node)
{
xmlChar *value;
if (qdl->vip_data.programmer_requires_vip)
return;
if (xmlStrcmp(node->name, (xmlChar *)"log") != 0)
return;
value = xmlGetProp(node, (xmlChar *)"value");
if (!value)
return;
if (strstr((const char *)value, VIP_PROGRAMMER_MARKER))
qdl->vip_data.programmer_requires_vip = true;
xmlFree(value);
}
static void xml_setpropf(xmlNode *node, const char *attr, const char *fmt, ...)
{
xmlChar buf[128];
va_list ap;
va_start(ap, fmt);
vsnprintf((char *)buf, sizeof(buf), fmt, ap);
xmlSetProp(node, (xmlChar *)attr, buf);
va_end(ap);
}
static xmlNode *firehose_response_parse(const void *buf, size_t len, int *error)
{
xmlNode *node;
xmlNode *root;
xmlDoc *doc;
doc = xmlReadMemory(buf, len, NULL, NULL, 0);
if (!doc) {
ux_err("failed to parse firehose response\n");
*error = -EINVAL;
return NULL;
}
root = xmlDocGetRootElement(doc);
for (node = root; node; node = node->next) {
if (node->type != XML_ELEMENT_NODE)
continue;
if (xmlStrcmp(node->name, (xmlChar *)"data") == 0)
break;
}
if (!node) {
ux_err("firehose response without data tag\n");
*error = -EINVAL;
xmlFreeDoc(doc);
return NULL;
}
for (node = node->children; node && node->type != XML_ELEMENT_NODE; node = node->next)
;
if (!node) {
ux_err("empty firehose response\n");
*error = -EINVAL;
}
return node;
}
static int firehose_generic_parser(xmlNode *node, void *data __unused, bool *rawmode)
{
xmlChar *value;
int ret = -EINVAL;
value = xmlGetProp(node, (xmlChar *)"value");
if (!value)
return -EINVAL;
if (xmlStrcmp(node->name, (xmlChar *)"log") == 0) {
ux_log("LOG: %s\n", value);
ret = -EAGAIN;
} else if (xmlStrcmp(value, (xmlChar *)"ACK") == 0) {
ret = FIREHOSE_ACK;
} else if (xmlStrcmp(value, (xmlChar *)"NAK") == 0) {
ret = FIREHOSE_NAK;
}
xmlFree(value);
value = xmlGetProp(node, (xmlChar *)"rawmode");
if (value) {
if (xmlStrcmp(value, (xmlChar *)"true") == 0)
*rawmode = true;
xmlFree(value);
}
return ret;
}
/*
* Scan @str for a contiguous run of exactly SHA256_DIGEST_STRING_LENGTH-1
* (64) lowercase or uppercase hex characters bounded by a non-hex character
* or string boundary. On match, decode into @digest and return true.
*/
static bool extract_sha256_hex(const char *str, uint8_t digest[SHA256_DIGEST_LENGTH])
{
const size_t hex_len = SHA256_DIGEST_LENGTH * 2;
const char *p = str;
size_t i;
while (*p) {
size_t run = 0;
const char *start;
while (*p && !isxdigit((unsigned char)*p))
p++;
start = p;
while (isxdigit((unsigned char)*p)) {
run++;
p++;
}
if (run == hex_len) {
for (i = 0; i < SHA256_DIGEST_LENGTH; i++) {
char byte[3] = { start[i * 2], start[i * 2 + 1], '\0' };
digest[i] = (uint8_t)strtoul(byte, NULL, 16);
}
return true;
}
}
return false;
}
static int firehose_sha256_parser(xmlNode *node, void *data, bool *rawmode)
{
struct firehose_op *op = data;
xmlChar *value;
int ret;
if (xmlStrcmp(node->name, (xmlChar *)"log") == 0) {
value = xmlGetProp(node, (xmlChar *)"value");
if (!value)
return -EINVAL;
if (!op->digest_valid && extract_sha256_hex((const char *)value, op->digest))
op->digest_valid = true;
ux_log("LOG: %s\n", value);
xmlFree(value);
return -EAGAIN;
}
ret = firehose_generic_parser(node, NULL, rawmode);
/*
* Some Firehose implementations attach the digest as an attribute on
* the response itself rather than emitting it via <log>. Try a few
* known attribute names.
*/
if (!op->digest_valid) {
static const char * const attrs[] = { "Digest", "SHA256", "sha256" };
size_t i;
for (i = 0; i < ARRAY_SIZE(attrs); i++) {
value = xmlGetProp(node, (xmlChar *)attrs[i]);
if (!value)
continue;
if (extract_sha256_hex((const char *)value, op->digest))
op->digest_valid = true;
xmlFree(value);
if (op->digest_valid)
break;
}
}
return ret;
}
static int firehose_read(struct qdl_device *qdl, int timeout_ms,
int (*response_parser)(xmlNode *node, void *data, bool *rawmode),
void *data)
{
char buf[4096];
xmlNode *node;
int error;
int resp = -EIO;
int ret = -EAGAIN;
int n;
bool rawmode = false;
struct timeval timeout;
struct timeval now;
struct timeval delta = { .tv_sec = timeout_ms / 1000,
.tv_usec = (timeout_ms % 1000) * 1000 };
gettimeofday(&now, NULL);
timeradd(&now, &delta, &timeout);
/*
* 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
* (otherwise subsequent writes will time out).
* The messages can be one of:
* - <log/>
* - <response value=""/>
* - <response value="" rawmode="true"/>
*
* Generally <log/> messages are coming prior to the <response/>, but
* on MSM8916 (at least) it's been observed that <log/> messages can
* arrive after the <response/>.
*
* We therefor need to consume messages until there are no more
* (timeout) and we have been able to parse out a response (using
* @response_parser).
*
* In the special case that the <response/> contain an attribute
* "rawmode=true", the device signals that it has entered a mode where
* it will not send/receive XML-formatted commands. So, (at least for
* reads) we need to shortcircuit the logic and directly terminate the
* consumption of incoming data.
*/
for (;;) {
n = qdl_read(qdl, buf, sizeof(buf), 100);
/* Timeout after seeing a response, we're done waiting for logs */
if (n == -ETIMEDOUT && resp >= 0)
break;
/* We want to return resp on error, to not lose the reset response */
else if (n == -EIO)
break;
if (n == -ETIMEDOUT || n == 0) {
gettimeofday(&now, NULL);
if (timercmp(&now, &timeout, <))
continue;
return -ETIMEDOUT;
}
buf[n] = '\0';
ux_debug("FIREHOSE READ: %s\n", buf);
/*
* On stream-oriented transports (Windows COM port via the
* QDLoader driver, virtio-console, ...) a single read can
* deliver multiple back-to-back Firehose responses
* concatenated, since the driver doesn't preserve USB bulk-
* transfer boundaries. Walk the buffer using the "<?xml" ...
* "</data>" envelope to bound each message; the closing tag
* is what really delimits the document so that any rawmode
* binary payload that arrives spliced onto the same read
* doesn't end up fed into libxml2 as if it were XML.
*
* libusb preserves transfer boundaries, so on that path each
* read still contains exactly one document and the loop runs
* once.
*/
char *cursor = buf;
char *bufend = buf + n;
while (cursor < bufend) {
char *start = strstr(cursor, "<?xml");
char *xml_end;
size_t chunk;
if (!start)
break;
/*
* Bound the XML on the closing </data> tag. If it's
* missing the message was either truncated or doesn't
* fit the schema we know how to parse; hand the rest
* of the buffer to libxml2 and let it error out
* gracefully.
*/
xml_end = strstr(start, "</data>");
if (xml_end) {
xml_end += sizeof("</data>") - 1;
chunk = (size_t)(xml_end - start);
} else {
chunk = (size_t)(bufend - start);
}
node = firehose_response_parse(start, chunk, &error);
if (!node)
return error;
firehose_check_vip_marker(qdl, node);
ret = response_parser(node, data, &rawmode);
xmlFreeDoc(node->doc);
if (ret >= 0)
resp = ret;
cursor = start + chunk;
/*
* The response we just parsed told the host to switch
* to raw mode (e.g. the ACK that precedes the binary
* sectors of a <read>). On a stream transport the
* first chunk of that binary payload can have arrived
* tacked onto this same read. Push it back so the
* next qdl_read() picks it up before the transport
* is touched again.
*/
if (rawmode) {
if (cursor < bufend)
qdl_push_back(qdl, cursor,
(size_t)(bufend - cursor));
break;
}
}
if (rawmode)
break;
}
return resp;
}
static int firehose_vip_send_table(struct qdl_device *qdl)
{
int ret;
ret = vip_transfer_handle_tables(qdl);
if (ret) {
ux_err("VIP: error occurred during VIP table transmission\n");
return -1;
}
if (!vip_transfer_status_check_needed(qdl))
return 0;
ret = firehose_read(qdl, 30000, firehose_generic_parser, NULL);
if (ret) {
ux_err("VIP: sending of digest table failed\n");
return -1;
}
ux_info("VIP: digest table has been sent successfully\n");
vip_transfer_clear_status(qdl);
return 0;
}
static int firehose_write(struct qdl_device *qdl, xmlDoc *doc)
{
int saved_errno;
xmlChar *s;
int len;
int ret;
xmlDocDumpMemory(doc, &s, &len);
ret = firehose_vip_send_table(qdl);
if (ret)
return -1;
vip_gen_chunk_init(qdl);
for (;;) {
ux_debug("FIREHOSE WRITE: %s\n", s);
vip_gen_chunk_update(qdl, s, len);
ret = qdl_write(qdl, s, len, 1000);
saved_errno = errno;
/*
* db410c sometimes sense a <response> followed by <log>
* entries and won't accept write commands until these are
* drained, so attempt to read any pending data and then retry
* the write.
*/
if (ret < 0 && errno == ETIMEDOUT) {
firehose_read(qdl, 100, firehose_generic_parser, NULL);
} else {
break;
}
}
xmlFree(s);
vip_gen_chunk_store(qdl);
return ret < 0 ? -saved_errno : 0;
}
/**
* firehose_configure_response_parser() - parse a configure response
* @node: response xmlNode
*
* Return: max size supported by the remote, or negative errno on failure
*/
static int firehose_configure_response_parser(xmlNode *node, void *data,
bool *rawmode __unused)
{
xmlChar *payload;
xmlChar *value;
size_t max_size;
value = xmlGetProp(node, (xmlChar *)"value");
if (!value)
return -EINVAL;
if (xmlStrcmp(node->name, (xmlChar *)"log") == 0) {
ux_log("LOG: %s\n", value);
xmlFree(value);
return -EAGAIN;
}
payload = xmlGetProp(node, (xmlChar *)"MaxPayloadSizeToTargetInBytes");
if (!payload) {
xmlFree(value);
return -EINVAL;
}
max_size = strtoul((char *)payload, NULL, 10);
xmlFree(payload);
/*
* When receiving an ACK the remote may indicate that we should attempt
* a larger payload size
*/
if (!xmlStrcmp(value, (xmlChar *)"ACK")) {
payload = xmlGetProp(node, (xmlChar *)"MaxPayloadSizeToTargetInBytesSupported");
if (!payload)
return -EINVAL;
max_size = strtoul((char *)payload, NULL, 10);
xmlFree(payload);
}
*(size_t *)data = max_size;
xmlFree(value);
return FIREHOSE_ACK;
}
static int firehose_send_configure(struct qdl_device *qdl, size_t payload_size,
bool skip_storage_init,
enum qdl_storage_type storage,
size_t *max_payload_size)
{
const char *memory_name;
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
memory_name = encode_storage_type(storage);
if (!memory_name)
return -EINVAL;
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"configure", NULL);
xml_setpropf(node, "MemoryName", memory_name);
xml_setpropf(node, "MaxPayloadSizeToTargetInBytes", "%lu", payload_size);
xml_setpropf(node, "Verbose", "%d", 0);
xml_setpropf(node, "ZlpAwareHost", "%d", 1);
xml_setpropf(node, "SkipStorageInit", "%d", skip_storage_init);
firehose_write(qdl, doc);
xmlFreeDoc(doc);
return firehose_read(qdl, 100, firehose_configure_response_parser, max_payload_size);
}
static int firehose_try_configure(struct qdl_device *qdl, bool skip_storage_init,
enum qdl_storage_type storage)
{
size_t max_sector_size;
size_t sector_sizes[] = { 512, 4096 };
struct firehose_op op;
size_t size = 0;
void *buf;
int ret;
unsigned int i;
ret = firehose_send_configure(qdl, qdl->max_payload_size, skip_storage_init,
storage, &size);
if (ret < 0)
return ret;
/* Retry if remote proposed different size */
if (size != qdl->max_payload_size) {
ret = firehose_send_configure(qdl, size, skip_storage_init, storage, &size);
if (ret != FIREHOSE_ACK) {
ux_err("configure request with updated payload size failed\n");
return -1;
}
qdl->max_payload_size = size;
}
ux_debug("accepted max payload size: %zu\n", qdl->max_payload_size);
/*
* Skip sector size probing when VIP is active: the probe read commands
* are not included in the pre-built VIP digest table (the dry-run that
* 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 &&
!qdl->sector_size) {
max_sector_size = sector_sizes[ARRAY_SIZE(sector_sizes) - 1];
buf = alloca(max_sector_size);
memset(&op, 0, sizeof(op));
op.partition = 0;
op.start_sector = "1";
op.num_sectors = 1;
/*
* Testing has shown that the loader will fail gracefully if a
* read is issued with the wrong sector size, use this to attempt
* to discover the storage device's sector size.
*/
for (i = 0; i < ARRAY_SIZE(sector_sizes); i++) {
op.sector_size = sector_sizes[i];
ret = firehose_read_buf(qdl, &op, buf, max_sector_size);
if (ret == 0) {
qdl->sector_size = sector_sizes[i];
break;
}
}
}
if (qdl->sector_size)
ux_debug("detected sector size of: %zd\n", qdl->sector_size);
return 0;
}
static int firehose_erase(struct qdl_device *qdl, struct firehose_op *program)
{
unsigned int sector_size;
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
int ret;
sector_size = program->sector_size ? : qdl->sector_size;
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"erase", NULL);
xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size);
xml_setpropf(node, "physical_partition_number", "%d", program->partition);
/*
* Omitting num_sectors and start_sector attributes tells the programmer
* to erase the full physical partition.
*/
if (program->num_sectors > 0) {
xml_setpropf(node, "num_partition_sectors", "%d", program->num_sectors);
xml_setpropf(node, "start_sector", "%s", program->start_sector);
}
if (qdl->slot != UINT_MAX) {
xml_setpropf(node, "slot", "%u", qdl->slot);
}
if (program->is_nand) {
xml_setpropf(node, "PAGES_PER_BLOCK", "%d", program->pages_per_block);
}
ret = firehose_write(qdl, doc);
if (ret < 0) {
ux_err("failed to send program request\n");
goto out;
}
ret = firehose_read(qdl, 30000, firehose_generic_parser, NULL);
if (ret)
ux_err("failed to erase %s+0x%x\n", program->start_sector, program->num_sectors);
else
ux_info("successfully erased %s+0x%x\n", program->start_sector, program->num_sectors);
out:
xmlFreeDoc(doc);
return ret == FIREHOSE_ACK ? 0 : -1;
}
static int firehose_getsha256digest(struct qdl_device *qdl, struct firehose_op *op);
/*
* Decide whether the @program op can be skipped because its bytes are
* already on flash. Returns 1 if the program path can be bypassed, 0
* to defer to the normal write. @buf is a scratch buffer of size
* qdl->max_payload_size owned by the caller.
*
* Hash the bytes that would have been written, ask the device for the
* digest of the same region, and compare. Any failure on the way (read error,
* digest query failure, mismatch) returns 0 so the caller proceeds with
* the normal program flow; the file pointer is restored by the
* qdl_file_seek() at the top of the write loop.
*
* Restricted to non-sparse, non-NAND programs with VIP disabled:
* - Sparse chunks would each need their own digest; v1 keeps them on
* the normal program path.
* - NAND has spare/OOB bytes whose semantics differ across
* programmers.
* - <getsha256digest> is not part of pre-built VIP digest tables.
*/
static int qdl_should_skipblock(struct qdl_device *qdl,
struct firehose_op *program,
struct qdl_file *file,
unsigned int num_sectors,
unsigned int sector_size,
void *buf)
{
uint8_t local_digest[SHA256_DIGEST_LENGTH];
size_t total = (size_t)num_sectors * sector_size;
struct firehose_op digest_op;
size_t hashed = 0;
SHA2_CTX ctx;
ssize_t hn;
if (qdl->skipblock_mode != QDL_SKIPBLOCK_SHA256 ||
program->sparse ||
program->is_nand ||
qdl->vip_data.state != VIP_DISABLED)
return 0;
SHA256Init(&ctx);
qdl_file_seek(file, (off_t)program->file_offset * sector_size, SEEK_SET);
while (hashed < total) {
size_t want = MIN(qdl->max_payload_size, total - hashed);
hn = qdl_file_read_exact(file, buf, want);
if (hn < 0)
return 0;
if (hn == 0 || (size_t)hn < want) {
/*
* EOF before completing the requested region: the
* device's digest is over `total` bytes from flash,
* so we can't construct a matching local digest.
* Defer to the normal program path.
*/
if (hn > 0)
SHA256Update(&ctx, buf, hn);
return 0;
}
SHA256Update(&ctx, buf, hn);
hashed += (size_t)hn;
}
SHA256Final(local_digest, &ctx);
digest_op = (struct firehose_op){
.type = FIREHOSE_OP_GET_SHA256_DIGEST,
.sector_size = sector_size,
.num_sectors = num_sectors,
.partition = program->partition,
.start_sector = program->start_sector,
};
if (firehose_getsha256digest(qdl, &digest_op) == 0 &&
!memcmp(local_digest, digest_op.digest, SHA256_DIGEST_LENGTH))
return 1;
return 0;
}
static int firehose_program(struct qdl_device *qdl, struct firehose_op *program)
{
unsigned int num_sectors;
unsigned int sector_size;
unsigned int zlp_timeout = 10000;
struct qdl_file file;
size_t chunk_size;
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
void *buf;
time_t t0;
time_t t;
size_t left;
int ret;
int n;
size_t i;
uint32_t fill_value;
/*
* ZLP has been measured to take up to 15 seconds on SPINOR devices,
* let's double it to be on the safe side...
*/
if (qdl->current_storage_type == QDL_STORAGE_SPINOR)
zlp_timeout = 60000;
if (!program->filename)
return 0;
ret = qdl_file_open(program->zip, program->filename, &file);
if (ret < 0) {
ux_err("unable to open %s\n", program->filename);
return -1;
}
num_sectors = program->num_sectors;
sector_size = program->sector_size ? : qdl->sector_size;
if (!program->sparse) {
num_sectors = (qdl_file_getsize(&file) + sector_size - 1) / sector_size;
if (program->num_sectors && num_sectors > program->num_sectors) {
ux_err("%s to big for %s truncated to %d\n",
program->filename,
program->label,
program->num_sectors * sector_size);
num_sectors = program->num_sectors;
}
}
buf = malloc(qdl->max_payload_size);
if (!buf) {
ux_err("failed to allocate sector buffer\n");
goto err_close_fd;
}
if (qdl_should_skipblock(qdl, program, &file, num_sectors, sector_size, buf)) {
ux_info("skipped \"%s\" (sha256 match)\n", program->label);
free(buf);
qdl_file_close(&file);
return 0;
}
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"program", NULL);
xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size);
xml_setpropf(node, "num_partition_sectors", "%d", num_sectors);
xml_setpropf(node, "physical_partition_number", "%d", program->partition);
xml_setpropf(node, "start_sector", "%s", program->start_sector);
if (qdl->slot != UINT_MAX) {
xml_setpropf(node, "slot", "%u", qdl->slot);
}
if (program->filename)
xml_setpropf(node, "filename", "%s", program->filename);
if (program->is_nand) {
xml_setpropf(node, "PAGES_PER_BLOCK", "%d", program->pages_per_block);
xml_setpropf(node, "last_sector", "%d", program->last_sector);
}
ret = firehose_write(qdl, doc);
if (ret < 0) {
ux_err("failed to send program request\n");
goto err_free_doc;
}
ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL);
if (ret) {
ux_err("failed to setup programming\n");
goto err_free_doc;
}
t0 = time(NULL);
if (!program->sparse) {
qdl_file_seek(&file, (off_t)program->file_offset * sector_size, SEEK_SET);
} else {
switch (program->sparse_chunk_type) {
case CHUNK_TYPE_RAW:
qdl_file_seek(&file, program->sparse_offset, SEEK_SET);
break;
case CHUNK_TYPE_FILL:
fill_value = program->sparse_fill_value;
for (i = 0; i < qdl->max_payload_size; i += sizeof(fill_value))
memcpy(buf + i, &fill_value, sizeof(fill_value));
break;
default:
ux_err("[SPARSE] invalid chunk type\n");
goto err_free_doc;
}
}
left = num_sectors;
ux_debug("FIREHOSE RAW BINARY WRITE: %s, %d bytes\n",
program->filename, sector_size * num_sectors);
while (left > 0) {
/*
* We should calculate hash for every raw packet sent,
* not for the whole binary.
*/
vip_gen_chunk_init(qdl);
chunk_size = MIN(qdl->max_payload_size / sector_size, left);
if (!program->sparse || program->sparse_chunk_type != CHUNK_TYPE_FILL) {
n = qdl_file_read_exact(&file, buf, chunk_size * sector_size);
if (n < 0) {
ux_err("failed to read %s\n", program->filename);
goto err_free_doc;
}
/*
* qdl_file_read_exact() only returns short on true
* EOF. The wire protocol expects exactly
* chunk_size * sector_size bytes, so zero-pad the
* residue (which is at most the trailing partial
* sector of the file).
*/
if ((size_t)n < chunk_size * sector_size)
memset(buf + n, 0, chunk_size * sector_size - n);
}
vip_gen_chunk_update(qdl, buf, chunk_size * sector_size);
ret = firehose_vip_send_table(qdl);
if (ret)
return -1;
n = qdl_write(qdl, buf, chunk_size * sector_size, zlp_timeout);
if (n < 0) {
ux_err("USB write failed for data chunk\n");
ret = firehose_read(qdl, 30000, firehose_generic_parser, NULL);
if (ret)
ux_err("flashing of chunk failed\n");
goto err_free_doc;
}
if ((size_t)n != chunk_size * sector_size) {
ux_err("USB write truncated\n");
ret = -1;
goto err_free_doc;
}
left -= chunk_size;
vip_gen_chunk_store(qdl);
ux_progress("%s", num_sectors - left, num_sectors, program->label);
}
t = time(NULL) - t0;
ret = firehose_read(qdl, 120000, firehose_generic_parser, NULL);
if (ret != FIREHOSE_ACK) {
ux_err("flashing of %s failed\n", program->label);
} else if (t) {
ux_info("flashed \"%s\" successfully at %lukB/s\n",
program->label,
(unsigned long)sector_size * num_sectors / t / 1024);
} else {
ux_info("flashed \"%s\" successfully\n",
program->label);
}
xmlFreeDoc(doc);
free(buf);
qdl_file_close(&file);
return 0;
err_free_doc:
xmlFreeDoc(doc);
free(buf);
err_close_fd:
qdl_file_close(&file);
return -1;
}
static int firehose_issue_read(struct qdl_device *qdl, struct firehose_op *read_op,
int fd, void *out_buf, size_t out_len, bool quiet)
{
unsigned int sector_size;
size_t chunk_size;
size_t out_offset = 0;
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
void *buf;
time_t t0;
time_t t;
size_t left;
int ret;
int n;
buf = malloc(qdl->max_payload_size);
if (!buf) {
ux_err("failed to allocate sector buffer\n");
return -1;
}
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
sector_size = read_op->sector_size ? : qdl->sector_size;
node = xmlNewChild(root, NULL, (xmlChar *)"read", NULL);
xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size);
xml_setpropf(node, "num_partition_sectors", "%d", read_op->num_sectors);
xml_setpropf(node, "physical_partition_number", "%d", read_op->partition);
xml_setpropf(node, "start_sector", "%s", read_op->start_sector);
if (qdl->slot != UINT_MAX) {
xml_setpropf(node, "slot", "%u", qdl->slot);
}
if (read_op->filename)
xml_setpropf(node, "filename", "%s", read_op->filename);
ret = firehose_write(qdl, doc);
if (ret < 0) {
ux_err("failed to send read command\n");
goto out;
}
ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL);
if (ret) {
if (!quiet)
ux_err("failed to setup reading operation\n");
goto out;
}
t0 = time(NULL);
left = read_op->num_sectors;
while (left > 0) {
size_t want;
size_t got;
chunk_size = MIN(qdl->max_payload_size / sector_size, left);
want = chunk_size * sector_size;
/*
* Accumulate the chunk across qdl_read() calls. libusb usually
* delivers an entire bulk transfer in one shot, but stream
* transports (QUD's Windows COM port, virtio-console, ...) can
* fragment it - including the rawmode tail that firehose_read()
* pushed back from the same buffer as the ACK response.
*/
got = 0;
while (got < want) {
n = qdl_read(qdl, (char *)buf + got, want - got, 30000);
if (n < 0) {
ux_err("failed to read sector data\n");
ret = -1;
goto out;
}
if (n == 0) {
ux_err("unexpected EOF while reading sector data\n");
ret = -1;
goto out;
}
got += (size_t)n;
}
if (out_buf) {
size_t copy = want;
if (copy > out_len - out_offset)
copy = out_len - out_offset;
memcpy((char *)out_buf + out_offset, buf, copy);
out_offset += copy;
} else {
n = write(fd, buf, want);
if (n < 0 || (size_t)n != want) {
ux_err("failed to write sector data\n");
ret = -1;
goto out;
}
}
left -= chunk_size;
if (!quiet)
ux_progress("%s", read_op->num_sectors - left, read_op->num_sectors, read_op->filename);
}
ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL);
if (ret) {
ux_err("read operation failed\n");
goto out;
}
t = time(NULL) - t0;
if (!quiet) {
if (t) {
ux_info("read \"%s\" successfully at %ldkB/s\n",
read_op->filename,
(unsigned long)sector_size * read_op->num_sectors / t / 1024);
} else {
ux_info("read \"%s\" successfully\n",
read_op->filename);
}
}
out:
xmlFreeDoc(doc);
free(buf);
return ret;
}
int firehose_read_buf(struct qdl_device *qdl, struct firehose_op *read_op, void *out_buf, size_t out_size)
{
return firehose_issue_read(qdl, read_op, -1, out_buf, out_size, true);
}
static int firehose_read_op(struct qdl_device *qdl, struct firehose_op *op)
{
int ret;
int fd;
fd = open(op->filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644);
if (fd < 0) {
ux_info("unable to open %s...\n", op->filename);
return -1;
}
ret = firehose_issue_read(qdl, op, fd, NULL, 0, false);
close(fd);
return ret;
}
/*
* Issue a <getsha256digest> request and store the result on @op. On
* success op->digest is populated and op->digest_valid is set; callers
* that want to surface the digest (the qdl `sha256` subcommand prints
* it after firehose_run() returns; the --skipblock=sha256 fast-path
* compares it against a locally-computed digest) consume the field
* themselves.
*/
static int firehose_getsha256digest(struct qdl_device *qdl, struct firehose_op *op)
{
unsigned int sector_size;
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
int ret;
sector_size = op->sector_size ? : qdl->sector_size;
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"getsha256digest", NULL);
xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size);
xml_setpropf(node, "num_partition_sectors", "%d", op->num_sectors);
xml_setpropf(node, "physical_partition_number", "%d", op->partition);
xml_setpropf(node, "start_sector", "%s", op->start_sector);
if (qdl->slot != UINT_MAX)
xml_setpropf(node, "slot", "%u", qdl->slot);
op->digest_valid = false;
ret = firehose_write(qdl, doc);
if (ret < 0) {
ux_err("failed to send getsha256digest command\n");
goto out;
}
ret = firehose_read(qdl, 30000, firehose_sha256_parser, op);
if (ret != FIREHOSE_ACK) {
ux_err("getsha256digest failed for %s+0x%x\n",
op->start_sector, op->num_sectors);
ret = -1;
goto out;
}
if (!op->digest_valid) {
ux_err("getsha256digest returned no digest for %s+0x%x\n",
op->start_sector, op->num_sectors);
ret = -1;
goto out;
}
ret = 0;
out:
xmlFreeDoc(doc);
return ret;
}
static int firehose_apply_patch(struct qdl_device *qdl, struct firehose_op *patch)
{
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
int ret;
if (!patch->filename)
return 0;
if (strcmp(patch->filename, "DISK"))
return 0;
ux_debug("applying patch \"%s\"\n", patch->what);
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"patch", NULL);
xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", patch->sector_size);
xml_setpropf(node, "byte_offset", "%d", patch->byte_offset);
xml_setpropf(node, "filename", "%s", patch->filename);
xml_setpropf(node, "physical_partition_number", "%d", patch->partition);
xml_setpropf(node, "size_in_bytes", "%d", patch->size_in_bytes);
xml_setpropf(node, "start_sector", "%s", patch->start_sector);
xml_setpropf(node, "value", "%s", patch->value);
if (qdl->slot != UINT_MAX) {
xml_setpropf(node, "slot", "%u", qdl->slot);
}
ret = firehose_write(qdl, doc);
if (ret < 0)
goto out;
ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL);
if (ret)
ux_err("patch application failed\n");
out:
xmlFreeDoc(doc);
return ret == FIREHOSE_ACK ? 0 : -1;
}
static int firehose_send_single_tag(struct qdl_device *qdl, xmlNode *node)
{
xmlNode *root;
xmlDoc *doc;
int ret;
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
xmlAddChild(root, node);
ret = firehose_write(qdl, doc);
if (ret < 0)
goto out;
ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL);
if (ret) {
ux_err("ufs request failed\n");
ret = -EINVAL;
}
out:
xmlFreeDoc(doc);
return ret;
}
int firehose_apply_ufs_common(struct qdl_device *qdl, struct ufs_common *ufs)
{
xmlNode *node_to_send;
int ret;
node_to_send = xmlNewNode(NULL, (xmlChar *)"ufs");
xml_setpropf(node_to_send, "bNumberLU", "%d", ufs->bNumberLU);
xml_setpropf(node_to_send, "bBootEnable", "%d", ufs->bBootEnable);
xml_setpropf(node_to_send, "bDescrAccessEn", "%d", ufs->bDescrAccessEn);
xml_setpropf(node_to_send, "bInitPowerMode", "%d", ufs->bInitPowerMode);
xml_setpropf(node_to_send, "bHighPriorityLUN", "%d", ufs->bHighPriorityLUN);
xml_setpropf(node_to_send, "bSecureRemovalType", "%d", ufs->bSecureRemovalType);
xml_setpropf(node_to_send, "bInitActiveICCLevel", "%d", ufs->bInitActiveICCLevel);
xml_setpropf(node_to_send, "wPeriodicRTCUpdate", "%d", ufs->wPeriodicRTCUpdate);
xml_setpropf(node_to_send, "bConfigDescrLock", "%d", ufs->bConfigDescrLock);
if (qdl->slot != UINT_MAX) {
xml_setpropf(node_to_send, "slot", "%u", qdl->slot);
}
if (ufs->wb) {
xml_setpropf(node_to_send, "bWriteBoosterBufferPreserveUserSpaceEn",
"%d", ufs->bWriteBoosterBufferPreserveUserSpaceEn);
xml_setpropf(node_to_send, "bWriteBoosterBufferType", "%d", ufs->bWriteBoosterBufferType);
xml_setpropf(node_to_send, "shared_wb_buffer_size_in_kb", "%d", ufs->shared_wb_buffer_size_in_kb);
}
ret = firehose_send_single_tag(qdl, node_to_send);
if (ret)
ux_err("failed to send ufs common tag\n");
return ret == FIREHOSE_ACK ? 0 : -1;
}
int firehose_apply_ufs_body(struct qdl_device *qdl, struct ufs_body *ufs)
{
xmlNode *node_to_send;
int ret;
node_to_send = xmlNewNode(NULL, (xmlChar *)"ufs");
xml_setpropf(node_to_send, "LUNum", "%d", ufs->LUNum);
xml_setpropf(node_to_send, "bLUEnable", "%d", ufs->bLUEnable);
xml_setpropf(node_to_send, "bBootLunID", "%d", ufs->bBootLunID);
xml_setpropf(node_to_send, "size_in_kb", "%d", ufs->size_in_kb);
xml_setpropf(node_to_send, "bDataReliability", "%d", ufs->bDataReliability);
xml_setpropf(node_to_send, "bLUWriteProtect", "%d", ufs->bLUWriteProtect);
xml_setpropf(node_to_send, "bMemoryType", "%d", ufs->bMemoryType);
xml_setpropf(node_to_send, "bLogicalBlockSize", "%d", ufs->bLogicalBlockSize);
xml_setpropf(node_to_send, "bProvisioningType", "%d", ufs->bProvisioningType);
xml_setpropf(node_to_send, "wContextCapabilities", "%d", ufs->wContextCapabilities);
if (qdl->slot != UINT_MAX) {
xml_setpropf(node_to_send, "slot", "%u", qdl->slot);
}
if (ufs->desc)
xml_setpropf(node_to_send, "desc", "%s", ufs->desc);
ret = firehose_send_single_tag(qdl, node_to_send);
if (ret)
ux_err("failed to apply ufs body tag\n");
return ret == FIREHOSE_ACK ? 0 : -1;
}
int firehose_apply_ufs_epilogue(struct qdl_device *qdl, struct ufs_epilogue *ufs,
bool commit)
{
xmlNode *node_to_send;
int ret;
node_to_send = xmlNewNode(NULL, (xmlChar *)"ufs");
xml_setpropf(node_to_send, "LUNtoGrow", "%d", ufs->LUNtoGrow);
xml_setpropf(node_to_send, "commit", "%d", commit);
if (qdl->slot != UINT_MAX) {
xml_setpropf(node_to_send, "slot", "%u", qdl->slot);
}
ret = firehose_send_single_tag(qdl, node_to_send);
if (ret)
ux_err("failed to apply ufs epilogue\n");
return ret == FIREHOSE_ACK ? 0 : -1;
}
static int firehose_set_bootable(struct qdl_device *qdl, int part)
{
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
int ret;
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"setbootablestoragedrive", NULL);
xml_setpropf(node, "value", "%d", part);
ret = firehose_write(qdl, doc);
xmlFreeDoc(doc);
if (ret < 0)
return -1;
ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL);
if (ret) {
ux_err("failed to mark partition %d as bootable\n", part);
return -1;
}
ux_info("partition %d is now bootable\n", part);
return 0;
}
static int firehose_reset(struct qdl_device *qdl)
{
xmlNode *root;
xmlNode *node;
xmlDoc *doc;
int ret;
doc = xmlNewDoc((xmlChar *)"1.0");
root = xmlNewNode(NULL, (xmlChar *)"data");
xmlDocSetRootElement(doc, root);
node = xmlNewChild(root, NULL, (xmlChar *)"power", NULL);
xml_setpropf(node, "value", "reset");
xml_setpropf(node, "DelayInSeconds", "10"); // Add a delay to prevent reboot fail
ret = firehose_write(qdl, doc);
xmlFreeDoc(doc);
if (ret < 0)
return -1;
ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL);
if (ret < 0)
ux_err("failed to request device reset\n");
/* drain any remaining log messages for reset */
else
firehose_read(qdl, 1000, firehose_generic_parser, NULL);
return ret == FIREHOSE_ACK ? 0 : -1;
}
static int firehose_detect_and_configure(struct qdl_device *qdl,
bool skip_storage_init,
enum qdl_storage_type storage,
unsigned int timeout_s)
{
struct timeval timeout = { .tv_sec = timeout_s };
struct timeval now;
int ret;
/* Track the currently configured storage type */
qdl->current_storage_type = storage;
/*
* The speculative retry loop below sends configure (and therefore the
* VIP table) before the programmer has had a chance to start up and
* request it. The VIP table can only be sent once per session
* (VIP_INIT -> VIP_SEND_DATA is a one-way transition), so a premature
* send leaves the programmer waiting for a table that was already
* consumed, breaking the VIP session.
*
* When VIP is active, restore the original behaviour: drain all
* startup log messages first, then do a single configure attempt.
*/
if (qdl->vip_data.state != VIP_DISABLED) {
firehose_read(qdl, timeout_s * 1000, firehose_generic_parser, NULL);
/*
* The startup-log drain above is our only chance to learn
* whether the programmer expects a VIP table before configure
* is sent. configure goes through firehose_write(), which
* unconditionally pushes the signed table when state is not
* VIP_DISABLED; if the programmer isn't in VIP mode it will
* try to parse those bytes as XML and reject the configure.
* Demote VIP here so the table is never sent.
*/
if (!qdl->vip_data.programmer_requires_vip) {
ux_info("WARNING: --vip-table-path was provided but programmer did not announce VIP; continuing without VIP\n");
qdl->vip_data.state = VIP_DISABLED;
}
ret = firehose_try_configure(qdl, false, storage);
if (ret != FIREHOSE_ACK) {
ux_err("configure request failed\n");
return -1;
}
return 0;
}
gettimeofday(&now, NULL);
timeradd(&now, &timeout, &timeout);
for (;;) {
ret = firehose_try_configure(qdl, skip_storage_init, storage);
/*
* If the programmer's startup logs announced that VIP is
* active but no --vip-table-path was provided, bail out now
* rather than burning the full configure timeout waiting for
* a signed table that will never be sent.
*/
if (qdl->vip_data.programmer_requires_vip) {
ux_err("programmer requires VIP, but no --vip-table-path was provided\n");
return -1;
}
if (ret == FIREHOSE_ACK) {
break;
} else if (ret != -ETIMEDOUT) {
ux_err("configure request failed\n");
return -1;
}
gettimeofday(&now, NULL);
if (timercmp(&now, &timeout, >)) {
ux_err("failed to detect firehose programmer\n");
return -1;
}
}
return 0;
}
int firehose_provision(struct qdl_device *qdl, bool skip_reset)
{
int ret;
ret = firehose_detect_and_configure(qdl, true, QDL_STORAGE_UFS, 5);
if (ret)
return ret;
ret = ufs_provisioning_execute(qdl, firehose_apply_ufs_common,
firehose_apply_ufs_body,
firehose_apply_ufs_epilogue);
if (!ret)
ux_info("UFS provisioning succeeded\n");
else
ux_info("UFS provisioning failed\n");
if (!skip_reset)
firehose_reset(qdl);
return ret;
}
struct firehose_op *firehose_alloc_op(int type)
{
struct firehose_op *op;
op = calloc(1, sizeof(*op));
if (!op)
return NULL;
op->type = type;
return op;
}
void firehose_free_ops(struct list_head *ops)
{
struct firehose_op *next;
struct firehose_op *op;
list_for_each_entry_safe(op, next, ops, node) {
list_del(&op->node);
qdl_zip_put(op->zip);
free((void *)op->filename);
free((void *)op->label);
free((void *)op->start_sector);
free((void *)op->gpt_partition);
free((void *)op->value);
free((void *)op->what);
free(op);
}
}
static int firehose_execute_ops(struct qdl_device *qdl, struct list_head *ops)
{
unsigned int patch_count = 0;
struct firehose_op *status_patch = NULL;
struct firehose_op *tmp;
struct firehose_op *op;
unsigned int patch_idx = 0;
int ret;
list_for_each_entry(op, ops, node) {
switch (op->type) {
case FIREHOSE_OP_CONFIGURE:
ret = firehose_detect_and_configure(qdl, false, op->storage_type, 5);
if (ret)
return ret;
ret = gpt_resolve_deferrals(qdl, ops);
if (ret)
return ret;
/* Update the number of patches for this storage device */
patch_count = 0;
patch_idx = 0;
tmp = op;
list_for_each_entry_continue(tmp, ops, node) {
if (tmp->type == FIREHOSE_OP_CONFIGURE)
break;
if (tmp->type != FIREHOSE_OP_PATCH)
continue;
if (tmp->filename && !strcmp(tmp->filename, "DISK")) {
patch_count++;
status_patch = tmp;
}
}
break;
case FIREHOSE_OP_PROGRAM:
ret = firehose_program(qdl, op);
if (ret < 0)
return ret;
break;
case FIREHOSE_OP_ERASE:
ret = firehose_erase(qdl, op);
if (ret < 0)
return ret;
break;
case FIREHOSE_OP_READ:
ret = firehose_read_op(qdl, op);
if (ret < 0)
return ret;
break;
case FIREHOSE_OP_GET_SHA256_DIGEST:
ret = firehose_getsha256digest(qdl, op);
if (ret < 0)
return ret;
break;
case FIREHOSE_OP_PATCH:
ret = firehose_apply_patch(qdl, op);
if (ret)
return ret;
if (op->filename && !strcmp(op->filename, "DISK"))
ux_progress("Applying patches", ++patch_idx, patch_count);
if (op == status_patch)
ux_info("%d patches applied\n", patch_idx);
break;
case FIREHOSE_OP_SET_BOOTABLE:
firehose_set_bootable(qdl, op->partition);
break;
case FIREHOSE_OP_RESET:
ret = firehose_reset(qdl);
if (ret < 0)
return ret;
break;
default:
ux_err("internal error: unknown firehose operation %d\n", op->type);
return -1;
}
}
return 0;
}
int firehose_run(struct qdl_device *qdl, struct list_head *ops)
{
ux_info("waiting for Firehose programmer...\n");
return firehose_execute_ops(qdl, ops);
}