Add bootloader extension

This commit is contained in:
Thomas Stoddard
2026-10-21 19:22:50 -04:00
committed by Kyle Schwarz
parent 4db27aa187
commit 6ac041cadc
61 changed files with 13816 additions and 68 deletions
@@ -0,0 +1,249 @@
#include "icsneo/device/extensions/bootloader/parser/fwbin.h"
#include "icsneo/device/extensions/bootloader/platform/logging.h"
#include "icsneo/device/extensions/bootloader/parser/ief.h"
#include <unordered_map>
namespace icsneo {
namespace Bootloader {
/**
* fw.bin file format (all integers little-endian):
*
* uint8_t versionLength
* char version[versionLength] Vehicle Spy 3 version, "major.minor.release.build"
* uint8_t hashLength
* char hash[hashLength] Build hash, unused
* Then tags until the end of the file:
* uint32_t nameHash HashDjb2() of the tag name, e.g. "fire2_core"
* uint8_t type TagData::Type
* uint32_t length
* uint8_t data[length]
*
* Exactly one Signature tag is required, it uses the reserved name hash 0.
*/
uint32_t FWBin::HashDjb2(const std::string& tag) {
uint32_t retv = 5381;
for(char c : tag)
retv = ((retv << 5) + retv) + c;
return retv;
}
bool FWBin::load(const std::string& path) {
file.open(path, std::ios::binary);
if(!file.is_open() || file.fail() || file.eof()) {
Log::Error(__func__, "Could not open file at " + path);
return false;
}
// Read out the version string, it is a single byte length followed by a string
uint8_t size;
file.read((char*)&size, 1);
if(file.fail() || file.eof()) {
Log::Error(__func__, "The fw.bin is truncated, could not read the version length");
return false;
}
std::string unparsedVersion;
unparsedVersion.resize(size);
file.read(unparsedVersion.data(), size);
if(file.fail() || file.eof()) {
Log::Error(__func__, "The fw.bin is truncated, could not read the version string");
return false;
}
if(!version.load(unparsedVersion)) {
Log::Error(__func__, "The fw.bin version string is invalid: " + unparsedVersion);
return false;
}
// Read out the hash string, it is a single byte length followed by a string
file.read((char*)&size, 1);
if(file.fail() || file.eof()) {
Log::Error(__func__, "The fw.bin is truncated, could not read the hash length");
return false;
}
// The hash string is not used, skip over it
file.seekg(size, file.cur);
if(file.fail() || file.eof()) {
Log::Error(__func__, "The fw.bin is truncated, could not read the hash string");
return false;
}
while(true) {
uint32_t tagHash;
file.read((char*)&tagHash, sizeof(uint32_t));
if(file.eof()) {
break;
}
if(file.fail()) {
Log::Error(__func__, "The fw.bin is truncated, could not read a tag name hash");
return false;
}
if(tags.count(tagHash)) {
// Duplicate tag
Log::Error(__func__, "The fw.bin contains a duplicate tag");
return false;
}
TagData& current = tags[tagHash];
// Read the type field
file.read((char*)&current.type, sizeof(TagData::Type));
if(file.fail() || file.eof()) {
Log::Error(__func__, "The fw.bin is truncated, could not read a tag type");
return false;
}
switch(current.type) {
case TagData::Type::DataBytes:
case TagData::Type::DataLength:
case TagData::Type::IEFVersion:
if(tagHash == SignatureHash) {
Log::Error(__func__, "The fw.bin contains a data tag using the reserved signature name hash");
return false;
}
break;
case TagData::Type::Signature:
if(tagHash != SignatureHash) {
Log::Error(__func__, "The fw.bin contains a signature tag with a non-signature name hash");
return false;
}
break;
default: // Unknown type
Log::Error(__func__, "The fw.bin contains a tag of unknown type " + std::to_string(static_cast<int>(current.type)));
return false;
}
// Read the length field
file.read((char*)&current.arrayLength, sizeof(uint32_t));
if(file.fail() || file.eof()) {
Log::Error(__func__, "The fw.bin is truncated, could not read a tag length");
return false;
}
// Note the current position so we can jump back to it and read it later
current.arrayPosition = file.tellg();
file.seekg(current.arrayLength, file.cur);
if(file.fail()) {
Log::Error(__func__, "The fw.bin is truncated, a tag extends past the end of the file");
return false;
}
}
file.clear();
// The signature tag is required to be present, but it is not verified
if(tags.count(SignatureHash) == 0) {
Log::Error(__func__, "The fw.bin is missing its signature tag");
return false;
}
return true;
}
bool FWBin::readTag(const std::string& tag, std::vector<uint8_t>& out) const {
const auto hash = HashDjb2(tag);
if(hash == SignatureHash) {
Log::Error(__func__, "The signature tag can not be read as data");
return false;
}
const auto it = tags.find(hash);
if(it == tags.end()) {
Log::Error(__func__, "The fw.bin does not contain the tag " + tag);
return false;
}
return it->second.read(file, out);
}
bool FWBin::TagData::read(std::ifstream& stream, std::vector<uint8_t>& out) const {
stream.clear();
stream.seekg(arrayPosition, stream.beg);
out.resize(arrayLength);
stream.read((char*)out.data(), arrayLength);
if(stream.fail()) {
Log::Error(__func__, "The fw.bin is truncated, could not read tag data");
return false;
}
return true;
}
bool FWBin::getFW(const std::string& iefName, std::vector<uint8_t>& fw) const {
return readTag(iefName, fw);
}
FWBin::VersionTags FWBin::GetVersionTags(const std::string& iefName) {
static const std::unordered_map<std::string, VersionTags> VersionTable = {
{"fire2_core", {"fire2_core_version_major", "fire2_core_version_minor"}},
{"fire2_core_sg4", {"fire2_core_sg4_version_major", "fire2_core_sg4_version_minor"}},
{"RADA2B_SW_bin", {"RADA2B_SW_Version_Major", "RADA2B_SW_Version_Minor"}},
{"RADA2B_REVB_SW_bin", {"RADA2B_SW_Version_Major", "RADA2B_SW_Version_Minor"}},
{"RADComet_SW_bin", {"RADComet_SW_Version_Major", "RADComet_SW_Version_Minor"}},
{"RADComet3_SW_bin", {"RADComet3_SW_Version_Major", "RADComet3_SW_Version_Minor"}},
{"RADGalaxy_SG2_SW_bin", {"RADGalaxy_SG2_SW_Version_Major", "RADGalaxy_SG2_SW_Version_Minor"}},
{"RADGalaxy_SG2_FFG_SW_bin", {"RADGalaxy_SG2_FFG_SW_Version_Major", "RADGalaxy_SG2_SW_Version_Minor"}},
{"RADGalaxy2_SW_bin_p1", {"RADGalaxy2_SW_Version_Major", "RADGalaxy2_SW_Version_Minor"}},
{"RADGalaxy2_SW_bin_p2", {"RADGalaxy2_SW_Version_Major", "RADGalaxy2_SW_Version_Minor"}},
{"RADGigastar_SW_bin", {"RADGigastar_SW_Version_Major", "RADGigastar_SW_Version_Minor"}},
{"RADGigastar_FFG_SW_bin", {"RADGigastar_SW_Version_Major", "RADGigastar_SW_Version_Minor"}},
{"RADGigastar_USBz_SW_bin", {"RADGigastar_USBz_SW_Version_Major", "RADGigastar_USBz_SW_Version_Minor"}},
{"RADGigastar_USBz_Z7010_SW_bin", {"RADGigastar_USBz_SW_Version_Major", "RADGigastar_USBz_SW_Version_Minor"}},
{"RADGigastar_USBz_Z7007s_SW_bin", {"RADGigastar_USBz_SW_Version_Major", "RADGigastar_USBz_SW_Version_Minor"}},
{"RADGigastar2_T1S_LIN_SW_bin", {"RADGigastar2_T1S_LIN_SW_Version_Major", "RADGigastar2_T1S_LIN_SW_Version_Minor"}},
{"RADGigastar2_T1S_CAN_SW_bin", {"RADGigastar2_T1S_CAN_SW_Version_Major", "RADGigastar2_T1S_CAN_SW_Version_Minor"}},
{"RADMoon2_SW_bin", {"RADMoon2_SW_Version_Major", "RADMoon2_SW_Version_Minor"}},
{"RADMoon2_Z7010_SW_bin", {"RADMoon2_SW_Version_Major", "RADMoon2_SW_Version_Minor"}},
{"RADMoonT1S_SW_bin", {"RADMoonT1S_SW_Version_Major", "RADMoonT1S_SW_Version_Minor"}},
{"vcan44_core_ief", {"vcan44_core_version_major", "vcan44_core_version_minor"}}
};
auto iter = VersionTable.find(iefName);
return iter != VersionTable.end() ? iter->second : VersionTags();
}
std::optional<icsneo::DeviceAppVersion> FWBin::getChipVersion(const std::string& iefName) const {
auto versionTags = GetVersionTags(iefName);
if(versionTags) {
std::vector<uint8_t> majData;
std::vector<uint8_t> minData;
if(!readTag(versionTags.majorTag, majData)) {
return std::nullopt;
}
if(!readTag(versionTags.minorTag, minData)) {
return std::nullopt;
}
if(majData.size() != 1)
return std::nullopt;
if(minData.size() != 1)
return std::nullopt;
icsneo::DeviceAppVersion chipVersion;
chipVersion.major = majData[0];
chipVersion.minor = minData[0];
return chipVersion;
}
std::vector<uint8_t> buf;
if(!readTag(iefName, buf)) {
return std::nullopt;
}
IEF ief;
if(!ief.load(std::move(buf))) {
return std::nullopt;
}
icsneo::DeviceAppVersion chipVersion;
chipVersion.major = ief.getVersion().appMajor;
chipVersion.minor = ief.getVersion().appMinor;
return chipVersion;
}
} // namespace Bootloader
} // namespace icsneo
@@ -0,0 +1,142 @@
#include "icsneo/device/extensions/bootloader/parser/fwbundle.h"
#include "icsneo/device/extensions/bootloader/parser/fwbin.h"
#include "icsneo/device/extensions/bootloader/platform/logging.h"
#include "icsneo/core/crc32.h"
#include <cstddef>
#include <cstring>
#include <fstream>
#include <iterator>
#include <limits>
namespace icsneo {
namespace Bootloader {
/**
* Firmware bundle file format (all integers little-endian):
*
* FWBundleHeader
* Then chips until the end of the file:
* FWBundleChip
* uint8_t data[FWBundleChip::length]
*
* The CRC32 covers the whole file with the crc field set to zero.
*/
#pragma pack(push, 1)
struct FWBundleHeader {
char magic[2]; // "FW"
uint16_t fmtVersion;
uint32_t crc;
uint8_t build;
uint8_t maintenance;
uint8_t minor;
uint8_t major;
uint8_t type; // Build type, unused
};
struct FWBundleChip {
uint32_t hash; // FWBin::HashDjb2() of the chip's IEF name
uint8_t build;
uint8_t maintenance;
uint8_t minor;
uint8_t major;
uint32_t length;
};
#pragma pack(pop)
bool FWBundle::IsFWBundle(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if(!file.is_open())
return false;
// Only check the magic, so a truncated bundle is still reported as a bundle error by load()
char magic[sizeof(FWBundleHeader::magic)];
file.read(magic, sizeof(magic));
if(file.fail())
return false;
return magic[0] == 'F' && magic[1] == 'W';
}
bool FWBundle::load(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if(!file.is_open()) {
Log::Error(__func__, "Could not open file at " + path);
return false;
}
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
if(file.bad()) {
Log::Error(__func__, "Could not read file at " + path);
return false;
}
if(bytes.size() < sizeof(FWBundleHeader) || bytes.size() > std::numeric_limits<uint32_t>::max()) {
Log::Error(__func__, "The firmware bundle is smaller than its header or larger than 4 GiB");
return false;
}
FWBundleHeader header;
std::memcpy(&header, bytes.data(), sizeof(header));
if(header.magic[0] != 'F' || header.magic[1] != 'W') {
Log::Error(__func__, "The file is not a firmware bundle, bad magic");
return false;
}
// The CRC is computed with the CRC field zeroed
std::memset(bytes.data() + offsetof(FWBundleHeader, crc), 0, sizeof(header.crc));
if(icsneo::crc32(0, bytes.data(), static_cast<uint32_t>(bytes.size())) != header.crc) {
Log::Error(__func__, "The firmware bundle failed CRC validation");
return false;
}
std::unordered_map<uint32_t, ChipBinary> parsed;
size_t offset = sizeof(FWBundleHeader);
while(offset < bytes.size()) {
if(bytes.size() - offset < sizeof(FWBundleChip)) {
Log::Error(__func__, "The firmware bundle is truncated, could not read a chip header");
return false;
}
FWBundleChip chip;
std::memcpy(&chip, bytes.data() + offset, sizeof(chip));
offset += sizeof(FWBundleChip);
if(bytes.size() - offset < chip.length) {
Log::Error(__func__, "The firmware bundle is truncated, a chip binary extends past the end of the file");
return false;
}
auto& bin = parsed[chip.hash];
bin.data.assign(bytes.begin() + offset, bytes.begin() + offset + chip.length);
bin.version.major = chip.major;
bin.version.minor = chip.minor;
bin.version.release = chip.maintenance;
bin.version.build = chip.build;
offset += chip.length;
}
version.major = header.major;
version.minor = header.minor;
version.release = header.maintenance;
version.build = header.build;
binaries = std::move(parsed);
return true;
}
bool FWBundle::getFW(const std::string& iefName, std::vector<uint8_t>& fw) const {
const auto it = binaries.find(FWBin::HashDjb2(iefName));
if(it == binaries.end()) {
Log::Error(__func__, "The chip binary could not be found");
return false;
}
fw = it->second.data;
return true;
}
std::optional<icsneo::DeviceAppVersion> FWBundle::getChipVersion(const std::string& iefName) const {
const auto it = binaries.find(FWBin::HashDjb2(iefName));
if(it == binaries.end())
return std::nullopt;
return icsneo::DeviceAppVersion(it->second.version.major, it->second.version.minor);
}
} // namespace Bootloader
} // namespace icsneo
@@ -0,0 +1,25 @@
#include "icsneo/device/extensions/bootloader/parser/fwloader.h"
#include "icsneo/device/extensions/bootloader/parser/fwbin.h"
#include "icsneo/device/extensions/bootloader/parser/fwbundle.h"
namespace icsneo {
namespace Bootloader {
std::unique_ptr<FWRepo> FWLoader::Load(const std::string& path) {
if(FWBundle::IsFWBundle(path)) {
auto bundle = std::make_unique<FWBundle>();
if(!bundle->load(path))
return nullptr;
return bundle;
}
auto bin = std::make_unique<FWBin>();
if(!bin->load(path))
return nullptr;
return bin;
}
} // namespace Bootloader
} // namespace icsneo
@@ -0,0 +1,73 @@
#include "icsneo/device/extensions/bootloader/parser/fwzip.h"
#include "icsneo/device/extensions/bootloader/platform/logging.h"
#include <miniz.h>
namespace icsneo {
namespace Bootloader {
class ZipHandle {
public:
ZipHandle() {
mz_zip_zero_struct(&handle);
}
~ZipHandle() {
mz_zip_reader_end(&handle);
}
operator mz_zip_archive*() {
return &handle;
}
private:
mz_zip_archive handle;
};
// A zip firmware image holds a single entry, the raw binary to send to the device
FWZip::FWZip(std::vector<uint8_t>&& tagData)
: handle(std::make_unique<ZipHandle>()), zipData(std::move(tagData)) {
if(!mz_zip_reader_init_mem(*handle, zipData.data(), zipData.size(), 0)) {
Log::Error(__func__, "The firmware zip could not be opened");
return;
}
mz_zip_archive_file_stat file;
if(!mz_zip_reader_file_stat(*handle, 0, &file)) {
Log::Error(__func__, "The firmware zip is empty or its first entry could not be read");
} else {
uncompressedSize = static_cast<size_t>(file.m_uncomp_size);
}
}
FWZip::~FWZip() = default;
bool FWZip::readAll(uint8_t* out) {
if(!uncompressedSize) {
return false;
}
if(!out) {
Log::Error(__func__, "No output buffer for the firmware zip");
return false;
}
if(!mz_zip_reader_extract_to_mem(*handle, 0, out, *uncompressedSize, 0)) {
Log::Error(__func__, "The firmware zip could not be decompressed");
return false;
}
return true;
}
bool FWZip::readAll(std::vector<uint8_t>& out) {
if(!uncompressedSize) {
return false;
}
out = std::vector<uint8_t>(*uncompressedSize, 0);
return readAll(out.data());
}
} // namespace Bootloader
} // namespace icsneo
@@ -0,0 +1,74 @@
#include "icsneo/device/extensions/bootloader/parser/ief.h"
#include "icsneo/device/extensions/bootloader/platform/logging.h"
#include <cstring>
namespace icsneo {
namespace Bootloader {
/**
* IEF file format (all integers little-endian):
*
* uint8_t magic 0xAA
* Version version
* uint8_t configLength
* uint8_t config[configLength] Unused
* Then sectors until the end of the file:
* uint8_t magic 0xAA
* uint32_t index Sector index, see the communication for how this maps to an address
* uint32_t length
* uint8_t data[length]
*/
bool IEF::load(std::vector<uint8_t>&& bytes) {
static constexpr uint8_t Magic = 0xAA;
// Magic, version, config byte count
static constexpr size_t FixedHeaderSize = 1 + sizeof(Version) + 1;
// Magic, index, size
static constexpr size_t SectorHeaderSize = 1 + sizeof(uint32_t) + sizeof(uint32_t);
data = std::move(bytes);
sectors.clear();
if(data.size() < FixedHeaderSize) {
Log::Error(__func__, "IEF too short");
return false;
}
if(data.front() != Magic) {
Log::Error(__func__, "Invalid IEF magic");
return false;
}
std::memcpy(&version, data.data() + 1, sizeof(Version));
const size_t headerSize = FixedHeaderSize + data[FixedHeaderSize - 1];
if(data.size() <= headerSize) {
Log::Error(__func__, "Invalid IEF config bytes");
return false;
}
size_t offset = headerSize;
while(data.size() - offset >= SectorHeaderSize) {
if(data[offset] != Magic) {
Log::Error(__func__, "Invalid IEF sector magic");
return false;
}
uint32_t index;
uint32_t size;
std::memcpy(&index, data.data() + offset + 1, sizeof(index));
std::memcpy(&size, data.data() + offset + 1 + sizeof(index), sizeof(size));
offset += SectorHeaderSize;
if(data.size() - offset < size) {
Log::Error(__func__, "Invalid IEF sector");
return false;
}
sectors.emplace_back(index, BytesView(data.data() + offset, size));
offset += size;
}
return true;
}
} // namespace Bootloader
} // namespace icsneo