Files
qdl/vip.c
T
Igor Opaniuk 75ac9bcb20 sim: validate VIP digest tables and chunk hashes in dry-run mode
Without any verification on the host side, a dry-run with pre-built VIP
tables (-n -D <path>) would silently succeed even when the digest tables
did not match the data being flashed. The only way to catch a mismatch
was to attempt a real flash and wait for the device to reject it, which
wastes time and risks leaving a device in a broken state.

Add hash validation to the simulator so mismatches are caught at dry-run
time, before any hardware is involved.

The signed initial table (DigestsToSign.bin.mbn) and all chained tables
(ChainedTableOfDigests<n>.bin) are parsed as they arrive. Chain hashes
linking each table to the next are verified to ensure table integrity.
Per-chunk SHA256 hashes are checked in order against every XML command
write and raw payload write; any mismatch is reported via ux_err()
without aborting so all mismatches remain visible at the end of a run.

Validation is active in exactly the intended modes:
  -n             plain dry-run, no tables sent -- no-op
  -n -D <path>   dry-run with VIP tables -- full hash validation
  -n -t <dir>    digest generation -- validation disabled

MAX_DIGESTS_PER_SIGNED_FILE and MAX_DIGESTS_PER_CHAINED_FILE are moved
from vip.c to vip.h so sim.c can reference them. sha2.c is added to
RAMDUMP_SRCS because qdl-ramdump links sim.o which now calls SHA256.

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

512 lines
12 KiB
C

// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved.
*/
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "sim.h"
#define DIGEST_FULL_TABLE_FILE "DIGEST_TABLE.bin"
#define CHAINED_TABLE_FILE_PREF "ChainedTableOfDigests"
#define CHAINED_TABLE_FILE_MAX_NAME 64
#define DIGEST_TABLE_TO_SIGN_FILE "DigestsToSign.bin"
#define DIGEST_TABLE_TO_SIGN_FILE_MBN (DIGEST_TABLE_TO_SIGN_FILE ".mbn")
#define MAX_DIGESTS_PER_SIGNED_TABLE (MAX_DIGESTS_PER_SIGNED_FILE - 1)
#define MAX_DIGESTS_PER_CHAINED_TABLE (MAX_DIGESTS_PER_CHAINED_FILE - 1)
#define MAX_DIGESTS_PER_BUF 16
#ifndef O_BINARY
#define O_BINARY 0
#define O_TEXT 0
#endif
struct vip_table_generator {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA2_CTX ctx;
FILE *digest_table_fd;
size_t digest_num_written;
const char *path;
};
void vip_transfer_deinit(struct qdl_device *qdl);
static void print_digest(unsigned char *buf)
{
char hex_str[SHA256_DIGEST_STRING_LENGTH];
for (size_t i = 0; i < SHA256_DIGEST_LENGTH; ++i)
sprintf(hex_str + i * 2, "%02x", buf[i]);
hex_str[SHA256_DIGEST_STRING_LENGTH - 1] = '\0';
ux_debug("FIREHOSE PACKET SHA256: %s\n", hex_str);
}
int vip_gen_init(struct qdl_device *qdl, const char *path)
{
struct vip_table_generator *vip_gen;
struct stat st;
char filepath[PATH_MAX];
if (qdl->dev_type != QDL_DEVICE_SIM) {
ux_err("Should be executed in simulation dry-run mode\n");
return -1;
}
if (stat(path, &st) || !S_ISDIR(st.st_mode)) {
ux_err("Directory '%s' to store VIP tables doesn't exist\n", path);
return -1;
}
vip_gen = malloc(sizeof(struct vip_table_generator));
if (!vip_gen) {
ux_err("Can't allocate memory for vip_table_generator\n");
return -1;
}
if (!sim_set_digest_generation(true, qdl, vip_gen)) {
ux_err("Can't enable digest table generation\n");
goto out_cleanup;
}
vip_gen->digest_num_written = 0;
vip_gen->path = path;
snprintf(filepath, sizeof(filepath), "%s/%s", path, DIGEST_FULL_TABLE_FILE);
vip_gen->digest_table_fd = fopen(filepath, "wb");
if (!vip_gen->digest_table_fd) {
ux_err("Can't create %s file\n", filepath);
goto out_cleanup;
}
return 0;
out_cleanup:
free(vip_gen);
sim_set_digest_generation(false, qdl, NULL);
return -1;
}
void vip_gen_chunk_init(struct qdl_device *qdl)
{
struct vip_table_generator *vip_gen;
vip_gen = sim_get_vip_generator(qdl);
if (!vip_gen)
return;
SHA256Init(&vip_gen->ctx);
}
void vip_gen_chunk_update(struct qdl_device *qdl, const void *buf, size_t len)
{
struct vip_table_generator *vip_gen;
vip_gen = sim_get_vip_generator(qdl);
if (!vip_gen)
return;
SHA256Update(&vip_gen->ctx, (uint8_t *)buf, len);
}
void vip_gen_chunk_store(struct qdl_device *qdl)
{
struct vip_table_generator *vip_gen;
vip_gen = sim_get_vip_generator(qdl);
if (!vip_gen)
return;
SHA256Final(vip_gen->hash, &vip_gen->ctx);
print_digest(vip_gen->hash);
if (fwrite(vip_gen->hash, SHA256_DIGEST_LENGTH, 1, vip_gen->digest_table_fd) != 1) {
ux_err("Failed to write digest to the " DIGEST_FULL_TABLE_FILE);
goto out_cleanup;
}
vip_gen->digest_num_written++;
return;
out_cleanup:
fclose(vip_gen->digest_table_fd);
vip_gen->digest_table_fd = NULL;
}
static int write_output_file(const char *filename, bool append, const void *data, size_t len)
{
FILE *fp;
char *mode = "wb";
if (append)
mode = "ab";
fp = fopen(filename, mode);
if (!fp) {
ux_err("Failed to open file for appending\n");
return -1;
}
if (fwrite(data, 1, len, fp) != len) {
ux_err("Failed to append to file\n");
fclose(fp);
return -1;
}
fclose(fp);
return 0;
}
static int write_digests_to_table(char *src_table, char *dest_table, size_t start_digest,
size_t count, SHA2_CTX *out_ctx)
{
const size_t elem_size = SHA256_DIGEST_LENGTH;
unsigned char buf[MAX_DIGESTS_PER_BUF * SHA256_DIGEST_LENGTH];
size_t written = 0;
FILE *out = NULL;
int ret = -1;
int fd = open(src_table, O_RDONLY | O_BINARY);
if (fd < 0) {
ux_err("Failed to open %s for reading\n", src_table);
return -1;
}
out = fopen(dest_table, "wb");
if (!out) {
ux_err("Failed to open %s for writing\n", dest_table);
goto out_cleanup;
}
if (out_ctx)
SHA256Init(out_ctx);
/* Seek to offset of start_digest */
off_t offset = elem_size * start_digest;
if (lseek(fd, offset, SEEK_SET) != offset) {
ux_err("Failed to seek in %s\n", src_table);
goto out_cleanup;
}
while (written < (count * elem_size)) {
size_t to_read = count * elem_size - written;
if (to_read > sizeof(buf))
to_read = sizeof(buf);
ssize_t bytes = read(fd, buf, to_read);
if (bytes < 0 || (size_t)bytes != to_read) {
ux_err("Failed to read from %s\n", src_table);
goto out_cleanup;
}
if (fwrite(buf, 1, bytes, out) != (size_t)bytes) {
ux_err("Can't write digests to %s\n", dest_table);
goto out_cleanup;
}
if (out_ctx)
SHA256Update(out_ctx, buf, bytes);
written += to_read;
}
ret = 0;
out_cleanup:
if (out)
fclose(out);
close(fd);
return ret;
}
static int create_chained_tables(struct vip_table_generator *vip_gen)
{
size_t total_digests = vip_gen->digest_num_written;
char src_table[PATH_MAX];
char dest_table[PATH_MAX];
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA2_CTX *chain_ctxs = NULL;
size_t chained_num = 0;
int ret = 0;
snprintf(src_table, sizeof(src_table), "%s/%s", vip_gen->path, DIGEST_FULL_TABLE_FILE);
/* Pre-compute the number of chained tables so we can allocate contexts */
if (total_digests > MAX_DIGESTS_PER_SIGNED_TABLE) {
size_t remaining = total_digests - MAX_DIGESTS_PER_SIGNED_TABLE;
chained_num = (remaining + MAX_DIGESTS_PER_CHAINED_TABLE - 1) /
MAX_DIGESTS_PER_CHAINED_TABLE;
chain_ctxs = calloc(chained_num, sizeof(SHA2_CTX));
if (!chain_ctxs)
return -1;
}
/* Step 1: Write digest table to DigestsToSign.bin */
snprintf(dest_table, sizeof(dest_table), "%s/%s",
vip_gen->path, DIGEST_TABLE_TO_SIGN_FILE);
size_t tosign_count = total_digests < MAX_DIGESTS_PER_SIGNED_TABLE ? total_digests :
MAX_DIGESTS_PER_SIGNED_TABLE;
ret = write_digests_to_table(src_table, dest_table, 0, tosign_count, NULL);
if (ret) {
ux_err("Writing digests to %s failed\n", dest_table);
goto out;
}
/* Step 2: Write remaining digests to ChainedTableOfDigests<n>.bin, capturing
* a SHA2 context after writing each table's digest payload so Step 3 can
* compute the final hashes without re-reading the files.
*/
if (total_digests > MAX_DIGESTS_PER_SIGNED_TABLE) {
size_t remaining_digests = total_digests - MAX_DIGESTS_PER_SIGNED_TABLE;
size_t chain_idx = 0;
while (remaining_digests > 0) {
size_t table_digests = remaining_digests > MAX_DIGESTS_PER_CHAINED_TABLE ?
MAX_DIGESTS_PER_CHAINED_TABLE : remaining_digests;
snprintf(dest_table, sizeof(dest_table),
"%s/%s%zu.bin", vip_gen->path,
CHAINED_TABLE_FILE_PREF, chain_idx);
ret = write_digests_to_table(src_table, dest_table,
total_digests - remaining_digests,
table_digests, &chain_ctxs[chain_idx]);
if (ret) {
ux_err("Writing digests to %s failed\n", dest_table);
goto out;
}
remaining_digests -= table_digests;
if (!remaining_digests) {
/* Add zero (the packet can't be multiple of 512 bytes) */
ret = write_output_file(dest_table, true, "\0", 1);
if (ret < 0) {
ux_err("Can't write 0 to %s\n", dest_table);
goto out;
}
SHA256Update(&chain_ctxs[chain_idx], (const uint8_t *)"\0", 1);
}
chain_idx++;
}
}
/* Step 3: Hash and append backwards. Each file's final content is the
* digest payload written in Step 2 plus the hash of the next file appended
* in the previous iteration. Finalize a copy of the cached context (updated
* with the appended hash where applicable) instead of re-reading the file.
*/
for (ssize_t i = chained_num - 1; i >= 0; --i) {
SHA2_CTX ctx = chain_ctxs[i];
if (i < (ssize_t)chained_num - 1)
SHA256Update(&ctx, hash, SHA256_DIGEST_LENGTH);
SHA256Final(hash, &ctx);
if (i == 0) {
snprintf(dest_table, sizeof(dest_table), "%s/%s",
vip_gen->path, DIGEST_TABLE_TO_SIGN_FILE);
} else {
snprintf(dest_table, sizeof(dest_table),
"%s/%s%zd.bin", vip_gen->path,
CHAINED_TABLE_FILE_PREF, (i - 1));
}
ret = write_output_file(dest_table, true, hash, SHA256_DIGEST_LENGTH);
if (ret < 0) {
ux_err("Failed to append hash to %s\n", dest_table);
goto out;
}
}
out:
free(chain_ctxs);
return ret;
}
void vip_gen_finalize(struct qdl_device *qdl)
{
struct vip_table_generator *vip_gen;
vip_gen = sim_get_vip_generator(qdl);
if (!vip_gen)
return;
fclose(vip_gen->digest_table_fd);
ux_debug("VIP TABLE DIGESTS: %lu\n", vip_gen->digest_num_written);
if (create_chained_tables(vip_gen) < 0)
ux_err("Error occurred when creating table of digests\n");
free(vip_gen);
sim_set_digest_generation(false, qdl, NULL);
}
int vip_transfer_init(struct qdl_device *qdl, const char *vip_table_path)
{
char fullpath[PATH_MAX];
snprintf(fullpath, sizeof(fullpath), "%s/%s",
vip_table_path, DIGEST_TABLE_TO_SIGN_FILE_MBN);
qdl->vip_data.signed_table_fd = open(fullpath, O_RDONLY);
if (!qdl->vip_data.signed_table_fd) {
ux_err("Can't open signed table %s\n", fullpath);
return -1;
}
qdl->vip_data.chained_num = 0;
for (int i = 0; i < MAX_CHAINED_FILES; ++i) {
snprintf(fullpath, sizeof(fullpath), "%s/%s%d%s",
vip_table_path, CHAINED_TABLE_FILE_PREF, i, ".bin");
int fd = open(fullpath, O_RDONLY);
if (fd == -1) {
if (errno == ENOENT)
break;
ux_err("Can't open signed table %s\n", fullpath);
goto out_cleanup;
}
qdl->vip_data.chained_fds[qdl->vip_data.chained_num++] = fd;
}
qdl->vip_data.state = VIP_INIT;
qdl->vip_data.chained_cur = 0;
return 0;
out_cleanup:
vip_transfer_deinit(qdl);
return -1;
}
void vip_transfer_deinit(struct qdl_device *qdl)
{
close(qdl->vip_data.signed_table_fd);
for (size_t i = 0; i < qdl->vip_data.chained_num; ++i)
close(qdl->vip_data.chained_fds[i]);
}
static int vip_transfer_send_raw(struct qdl_device *qdl, int table_fd)
{
struct stat sb;
int ret;
void *buf;
ssize_t n;
ret = fstat(table_fd, &sb);
if (ret < 0) {
ux_err("Failed to stat digest table file\n");
return -1;
}
buf = malloc(sb.st_size);
if (!buf) {
ux_err("Failed to allocate transfer buffer\n");
return -1;
}
n = read(table_fd, buf, sb.st_size);
if (n < 0 || n != sb.st_size) {
ux_err("failed to read binary\n");
ret = -1;
goto out;
}
qdl->vip_data.sending_table = true;
n = qdl_write(qdl, buf, n, 1000);
qdl->vip_data.sending_table = false;
if (n < 0) {
ux_err("USB write failed for data chunk\n");
ret = -1;
goto out;
}
out:
free(buf);
return ret;
}
int vip_transfer_handle_tables(struct qdl_device *qdl)
{
struct vip_transfer_data *vip_data = &qdl->vip_data;
int ret = 0;
if (vip_data->state == VIP_DISABLED)
return 0;
if (vip_data->state == VIP_INIT) {
/* Send initial signed table */
ret = vip_transfer_send_raw(qdl, vip_data->signed_table_fd);
if (ret) {
ux_err("VIP: failed to send the Signed VIP table\n");
return ret;
}
ux_debug("VIP: successfully sent the Initial VIP table\n");
vip_data->state = VIP_SEND_DATA;
vip_data->frames_sent = 0;
vip_data->frames_left = MAX_DIGESTS_PER_SIGNED_TABLE;
vip_data->fh_parse_status = true;
}
if (vip_data->state == VIP_SEND_NEXT_TABLE) {
if (vip_data->chained_cur >= vip_data->chained_num) {
ux_err("VIP: the required quantity of chained tables is missing\n");
return -1;
}
ret = vip_transfer_send_raw(qdl, vip_data->chained_fds[vip_data->chained_cur]);
if (ret) {
ux_err("VIP: failed to send the chained VIP table\n");
return ret;
}
ux_debug("VIP: successfully sent " CHAINED_TABLE_FILE_PREF "%lu.bin\n",
vip_data->chained_cur);
vip_data->state = VIP_SEND_DATA;
vip_data->frames_sent = 0;
vip_data->frames_left = MAX_DIGESTS_PER_CHAINED_TABLE;
vip_data->fh_parse_status = true;
vip_data->chained_cur++;
}
vip_data->frames_sent++;
if (vip_data->frames_sent >= vip_data->frames_left)
vip_data->state = VIP_SEND_NEXT_TABLE;
return 0;
}
bool vip_transfer_status_check_needed(struct qdl_device *qdl)
{
return qdl->vip_data.fh_parse_status;
}
void vip_transfer_clear_status(struct qdl_device *qdl)
{
qdl->vip_data.fh_parse_status = false;
}