Merge pull request #5410 from willmmiles/cleanup-bootloader-sha-0_15
Bootloader SHA fixes, 0.15 edition
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) // WLEDMM we only want getBootloaderSHA256Hex()
|
|
||||||
|
|
||||||
#include "ota_update.h"
|
#include "ota_update.h"
|
||||||
#include "wled.h"
|
#include "wled.h"
|
||||||
|
|
||||||
@@ -7,6 +5,14 @@
|
|||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
#include <esp_spi_flash.h>
|
#include <esp_spi_flash.h>
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
|
|
||||||
|
#if !(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0))
|
||||||
|
// Shim for V3 IDF. We only access the default flash anyways, so we can strip off the first argument.
|
||||||
|
#define esp_flash_read(chip, buffer, address, length) spi_flash_read(address, buffer, length)
|
||||||
|
#define esp_flash_erase_region(chip, start, length) spi_flash_erase_range(start, length)
|
||||||
|
#define esp_flash_write(chip, buffer, address, length) spi_flash_write(address, buffer, length)
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Platform-specific metadata locations
|
// Platform-specific metadata locations
|
||||||
@@ -19,9 +25,11 @@ constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears afte
|
|||||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||||
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
|
constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6
|
||||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||||
|
#define BOOTLOADER_OTA_UNSUPPORTED // still needs validation on these platforms.
|
||||||
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
|
#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)
|
||||||
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
|
constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5
|
||||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||||
|
#define BOOTLOADER_OTA_UNSUPPORTED // still needs testing on these platforms
|
||||||
#else
|
#else
|
||||||
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
|
constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2
|
||||||
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
||||||
@@ -31,11 +39,12 @@ constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size
|
|||||||
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
|
||||||
#define UPDATE_ERROR getErrorString
|
#define UPDATE_ERROR getErrorString
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes
|
||||||
|
|
||||||
#endif
|
// -------------------------------------
|
||||||
|
#if 0 // WLEDMM unused
|
||||||
|
|
||||||
#if 0 // WLEDMM not needed - we only want getBootloaderSHA256Hex();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if OTA should be allowed based on release compatibility using custom description
|
* Check if OTA should be allowed based on release compatibility using custom description
|
||||||
@@ -80,7 +89,7 @@ struct UpdateContext {
|
|||||||
|
|
||||||
// Buffer to hold block data across posts, if needed
|
// Buffer to hold block data across posts, if needed
|
||||||
std::vector<uint8_t> releaseMetadataBuffer;
|
std::vector<uint8_t> releaseMetadataBuffer;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
static void endOTA(AsyncWebServerRequest *request) {
|
static void endOTA(AsyncWebServerRequest *request) {
|
||||||
@@ -111,7 +120,7 @@ static void endOTA(AsyncWebServerRequest *request) {
|
|||||||
}
|
}
|
||||||
delete context;
|
delete context;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||||
{
|
{
|
||||||
@@ -276,55 +285,499 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
#endif // WLEDMM unused
|
||||||
|
// -------------------------------------
|
||||||
|
|
||||||
|
|
||||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||||
static String bootloaderSHA256HexCache = "";
|
|
||||||
|
|
||||||
// Calculate and cache the bootloader SHA256 digest as hex string
|
// Class for computing the expected bootloader data size given a stream of the data.
|
||||||
|
// If the image includes an SHA256 appended after the data stream, we do not consider it here.
|
||||||
|
class BootloaderImageSizer {
|
||||||
|
public:
|
||||||
|
|
||||||
|
bool feed(const uint8_t* data, size_t len) {
|
||||||
|
if (error) return false;
|
||||||
|
|
||||||
|
//DEBUG_PRINTF("Feed %d\n", len);
|
||||||
|
|
||||||
|
if (imageSize == 0) {
|
||||||
|
// Parse header first
|
||||||
|
if (len < sizeof(esp_image_header_t)) {
|
||||||
|
error = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_image_header_t header;
|
||||||
|
memcpy(&header, data, sizeof(esp_image_header_t));
|
||||||
|
|
||||||
|
if (header.segment_count == 0) {
|
||||||
|
error = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageSize = sizeof(esp_image_header_t);
|
||||||
|
segmentsLeft = header.segment_count;
|
||||||
|
data += sizeof(esp_image_header_t);
|
||||||
|
len -= sizeof(esp_image_header_t);
|
||||||
|
//DEBUG_PRINTF("BLS parsed image header, segment count %d, is %d\n", segmentsLeft, imageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (len && segmentsLeft) {
|
||||||
|
if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) {
|
||||||
|
size_t headerBytes = std::min(len, sizeof(esp_image_segment_header_t) - segmentHeaderBytes);
|
||||||
|
memcpy(reinterpret_cast<uint8_t*>(&segmentHeader) + segmentHeaderBytes, data, headerBytes);
|
||||||
|
segmentHeaderBytes += headerBytes;
|
||||||
|
if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) {
|
||||||
|
return true; // needs more bytes for the header
|
||||||
|
}
|
||||||
|
|
||||||
|
//DEBUG_PRINTF("BLS parsed segment [%08X %08X=%d], segment count %d, is %d\n", segmentHeader.load_addr, segmentHeader.data_len, segmentHeader.data_len, segmentsLeft, imageSize);
|
||||||
|
|
||||||
|
// Validate segment size
|
||||||
|
if (segmentHeader.data_len > BOOTLOADER_SIZE) {
|
||||||
|
error = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data += headerBytes;
|
||||||
|
len -= headerBytes;
|
||||||
|
imageSize += sizeof(esp_image_segment_header_t) + segmentHeader.data_len;
|
||||||
|
--segmentsLeft;
|
||||||
|
if (segmentsLeft == 0) {
|
||||||
|
// all done, actually; we don't need to read any more
|
||||||
|
|
||||||
|
// Round up to nearest 16 bytes.
|
||||||
|
// Always add 1 to account for the checksum byte.
|
||||||
|
imageSize = ((imageSize/ 16) + 1) * 16;
|
||||||
|
|
||||||
|
//DEBUG_PRINTF("BLS complete, is %d\n", imageSize);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have enough bytes ...
|
||||||
|
if (len < segmentHeader.data_len) {
|
||||||
|
//DEBUG_PRINTF("Needs more bytes\n");
|
||||||
|
segmentHeader.data_len -= len;
|
||||||
|
return true; // still in this segment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segment complete
|
||||||
|
len -= segmentHeader.data_len;
|
||||||
|
data += segmentHeader.data_len;
|
||||||
|
segmentHeaderBytes = 0;
|
||||||
|
//DEBUG_PRINTF("Segment complete: len %d\n", len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !error;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasError() const { return error; }
|
||||||
|
bool isSizeKnown() const { return !error && imageSize != 0 && segmentsLeft == 0; }
|
||||||
|
size_t totalSize() const {
|
||||||
|
if (!isSizeKnown()) return 0;
|
||||||
|
return imageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t imageSize = 0;
|
||||||
|
size_t segmentsLeft = 0;
|
||||||
|
esp_image_segment_header_t segmentHeader;
|
||||||
|
size_t segmentHeaderBytes = 0;
|
||||||
|
bool error = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool bootloaderSHA256CacheValid = false;
|
||||||
|
static uint8_t bootloaderSHA256Cache[32];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and cache the bootloader SHA256 digest
|
||||||
|
* Reads the bootloader from flash and computes SHA256 hash
|
||||||
|
*
|
||||||
|
* Strictly speaking, most bootloader images already contain a hash at the end of the image;
|
||||||
|
* we could in theory just read it. The trouble is that we have to parse the structure anyways
|
||||||
|
* to find the actual endpoint, so we might as well always calculate it ourselves rather than
|
||||||
|
* handle a special case if the hash isn't stored.
|
||||||
|
*
|
||||||
|
*/
|
||||||
static void calculateBootloaderSHA256() {
|
static void calculateBootloaderSHA256() {
|
||||||
if (!bootloaderSHA256HexCache.isEmpty()) return;
|
|
||||||
|
|
||||||
// Calculate SHA256
|
// Calculate SHA256
|
||||||
uint8_t sha256[32];
|
|
||||||
mbedtls_sha256_context ctx;
|
mbedtls_sha256_context ctx;
|
||||||
mbedtls_sha256_init(&ctx);
|
mbedtls_sha256_init(&ctx);
|
||||||
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
|
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)
|
||||||
|
|
||||||
const size_t chunkSize = 256;
|
const size_t chunkSize = 256;
|
||||||
uint8_t buffer[chunkSize];
|
alignas(esp_image_header_t) uint8_t buffer[chunkSize];
|
||||||
|
size_t bootloaderSize = BOOTLOADER_SIZE;
|
||||||
|
BootloaderImageSizer sizer;
|
||||||
|
size_t totalHashLen = 0;
|
||||||
|
|
||||||
for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) {
|
for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) {
|
||||||
size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize);
|
size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize);
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
|
||||||
if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { // use esp_flash_read for V4 framework (-S2, -S3, -C3)
|
sizer.feed(buffer, readSize);
|
||||||
#else
|
|
||||||
if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32)
|
size_t hashLen = readSize;
|
||||||
#endif
|
if (sizer.isSizeKnown()) {
|
||||||
mbedtls_sha256_update(&ctx, buffer, readSize);
|
size_t totalSize = sizer.totalSize();
|
||||||
|
if (totalSize > 0 && totalSize <= BOOTLOADER_SIZE) {
|
||||||
|
bootloaderSize = totalSize;
|
||||||
|
if (offset + readSize > totalSize) {
|
||||||
|
hashLen = (totalSize > offset) ? (totalSize - offset) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashLen > 0) {
|
||||||
|
totalHashLen += hashLen;
|
||||||
|
mbedtls_sha256_update(&ctx, buffer, hashLen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mbedtls_sha256_finish(&ctx, sha256);
|
mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache);
|
||||||
mbedtls_sha256_free(&ctx);
|
mbedtls_sha256_free(&ctx);
|
||||||
|
|
||||||
// Convert to hex string and cache it
|
bootloaderSHA256CacheValid = true;
|
||||||
char hex[65];
|
|
||||||
for (int i = 0; i < 32; i++) {
|
|
||||||
sprintf(hex + (i * 2), "%02x", sha256[i]);
|
|
||||||
}
|
|
||||||
hex[64] = '\0';
|
|
||||||
bootloaderSHA256HexCache = hex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get bootloader SHA256 as hex string
|
// Get bootloader SHA256 as hex string
|
||||||
String getBootloaderSHA256Hex() {
|
String getBootloaderSHA256Hex() {
|
||||||
calculateBootloaderSHA256();
|
if (!bootloaderSHA256CacheValid) {
|
||||||
return bootloaderSHA256HexCache;
|
calculateBootloaderSHA256();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to hex string
|
||||||
|
String result;
|
||||||
|
result.reserve(65);
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
char b1 = bootloaderSHA256Cache[i];
|
||||||
|
char b2 = b1 >> 4;
|
||||||
|
b1 &= 0x0F;
|
||||||
|
b1 += '0'; b2 += '0';
|
||||||
|
if (b1 > '9') b1 += 39;
|
||||||
|
if (b2 > '9') b2 += 39;
|
||||||
|
result.concat(b2);
|
||||||
|
result.concat(b1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate cached bootloader SHA256 (call after bootloader update)
|
/**
|
||||||
|
* Invalidate cached bootloader SHA256 (call after bootloader update)
|
||||||
|
* Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex
|
||||||
|
*/
|
||||||
static void invalidateBootloaderSHA256Cache() {
|
static void invalidateBootloaderSHA256Cache() {
|
||||||
bootloaderSHA256HexCache = "";
|
bootloaderSHA256CacheValid = false;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
|
#if 0 // WLEDMM unused
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify complete buffered bootloader using ESP-IDF validation approach
|
||||||
|
* This matches the key validation steps from esp_image_verify() in ESP-IDF
|
||||||
|
* @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected)
|
||||||
|
* @param len Reference to length of bootloader data (will be adjusted to actual size)
|
||||||
|
* @param bootloaderErrorMsg Pointer to String to store error message (must not be null)
|
||||||
|
* @return true if validation passed, false otherwise
|
||||||
|
*/
|
||||||
|
static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& bootloaderErrorMsg) {
|
||||||
|
const size_t MIN_IMAGE_HEADER_SIZE = sizeof(esp_image_header_t);
|
||||||
|
|
||||||
|
// 1. Validate minimum size for header
|
||||||
|
if (len < MIN_IMAGE_HEADER_SIZE) {
|
||||||
|
bootloaderErrorMsg = "Too small";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the bootloader starts at offset 0x1000 (common in partition table dumps)
|
||||||
|
// This happens when someone uploads a complete flash dump instead of just the bootloader
|
||||||
|
if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE &&
|
||||||
|
buffer[BOOTLOADER_OFFSET] == ESP_IMAGE_HEADER_MAGIC &&
|
||||||
|
buffer[0] != ESP_IMAGE_HEADER_MAGIC) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader detected at offset\n"));
|
||||||
|
// Adjust buffer pointer to start at the actual bootloader
|
||||||
|
buffer = buffer + BOOTLOADER_OFFSET;
|
||||||
|
len = len - BOOTLOADER_OFFSET;
|
||||||
|
|
||||||
|
// Re-validate size after adjustment
|
||||||
|
if (len < MIN_IMAGE_HEADER_SIZE) {
|
||||||
|
bootloaderErrorMsg = "Too small";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t availableLen = len;
|
||||||
|
esp_image_header_t imageHeader{};
|
||||||
|
memcpy(&imageHeader, buffer, sizeof(imageHeader));
|
||||||
|
|
||||||
|
// 2. Basic header sanity checks (matches early esp_image_verify checks)
|
||||||
|
if (imageHeader.magic != ESP_IMAGE_HEADER_MAGIC ||
|
||||||
|
imageHeader.segment_count == 0 || imageHeader.segment_count > 16 ||
|
||||||
|
imageHeader.spi_mode > 3 ||
|
||||||
|
imageHeader.entry_addr < 0x40000000 || imageHeader.entry_addr > 0x50000000) {
|
||||||
|
bootloaderErrorMsg = "Invalid header";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Chip ID validation (matches esp_image_verify step 3)
|
||||||
|
if (imageHeader.chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) {
|
||||||
|
bootloaderErrorMsg = "Chip ID mismatch";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate image size
|
||||||
|
BootloaderImageSizer sizer;
|
||||||
|
sizer.feed(buffer, availableLen);
|
||||||
|
if (!sizer.isSizeKnown()) {
|
||||||
|
bootloaderErrorMsg = "Invalid image";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t actualBootloaderSize = sizer.totalSize();
|
||||||
|
|
||||||
|
// 5. SHA256 checksum (optional)
|
||||||
|
if (imageHeader.hash_appended == 1) {
|
||||||
|
actualBootloaderSize += 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualBootloaderSize > len) {
|
||||||
|
// Same as above
|
||||||
|
bootloaderErrorMsg = "Too small";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"),
|
||||||
|
imageHeader.segment_count, actualBootloaderSize, len, imageHeader.hash_appended);
|
||||||
|
|
||||||
|
// Update len to reflect actual bootloader size (including hash and checksum, with alignment)
|
||||||
|
// This is critical - we must write the complete image including checksums
|
||||||
|
len = actualBootloaderSize;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootloader OTA context structure
|
||||||
|
struct BootloaderUpdateContext {
|
||||||
|
// State flags
|
||||||
|
bool replySent = false;
|
||||||
|
bool uploadComplete = false;
|
||||||
|
String errorMessage;
|
||||||
|
|
||||||
|
// Buffer to hold bootloader data
|
||||||
|
uint8_t* buffer = nullptr;
|
||||||
|
size_t bytesBuffered = 0;
|
||||||
|
const uint32_t bootloaderOffset = 0x1000;
|
||||||
|
const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup bootloader OTA context
|
||||||
|
static void endBootloaderOTA(AsyncWebServerRequest *request) {
|
||||||
|
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||||
|
request->_tempObject = nullptr;
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context);
|
||||||
|
if (context) {
|
||||||
|
if (context->buffer) {
|
||||||
|
free(context->buffer);
|
||||||
|
context->buffer = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If update failed, restore system state
|
||||||
|
if (!context->uploadComplete || !context->errorMessage.isEmpty()) {
|
||||||
|
strip.resume();
|
||||||
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
|
WLED::instance().enableWatchdog();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
delete context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize bootloader OTA context
|
||||||
|
bool initBootloaderOTA(AsyncWebServerRequest *request) {
|
||||||
|
if (request->_tempObject) {
|
||||||
|
return true; // Already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
BootloaderUpdateContext* context = new BootloaderUpdateContext();
|
||||||
|
if (!context) {
|
||||||
|
DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
request->_tempObject = context;
|
||||||
|
request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect
|
||||||
|
|
||||||
|
#ifdef BOOTLOADER_OTA_UNSUPPORTED
|
||||||
|
context->errorMessage = F("Bootloader update not supported on this chip");
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer"));
|
||||||
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
|
WLED::instance().disableWatchdog();
|
||||||
|
#endif
|
||||||
|
lastEditTime = millis(); // make sure PIN does not lock during update
|
||||||
|
strip.suspend();
|
||||||
|
strip.resetSegments();
|
||||||
|
|
||||||
|
// Check available heap before attempting allocation
|
||||||
|
DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), getContiguousFreeHeap(), context->maxBootloaderSize);
|
||||||
|
|
||||||
|
context->buffer = (uint8_t*)malloc(context->maxBootloaderSize);
|
||||||
|
if (!context->buffer) {
|
||||||
|
size_t freeHeapNow = getContiguousFreeHeap();
|
||||||
|
DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Contiguous heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow);
|
||||||
|
context->errorMessage = "Out of memory! Contiguous heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes";
|
||||||
|
strip.resume();
|
||||||
|
#if WLED_WATCHDOG_TIMEOUT > 0
|
||||||
|
WLED::instance().enableWatchdog();
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context->bytesBuffered = 0;
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set bootloader OTA replied flag
|
||||||
|
void setBootloaderOTAReplied(AsyncWebServerRequest *request) {
|
||||||
|
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||||
|
if (context) {
|
||||||
|
context->replySent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bootloader OTA result
|
||||||
|
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request) {
|
||||||
|
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
return std::make_pair(true, String(F("Internal error: No bootloader OTA context")));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool needsReply = !context->replySent;
|
||||||
|
String errorMsg = context->errorMessage;
|
||||||
|
|
||||||
|
// If upload was successful, return empty string and trigger reboot
|
||||||
|
if (context->uploadComplete && errorMsg.isEmpty()) {
|
||||||
|
doReboot = true;
|
||||||
|
endBootloaderOTA(request);
|
||||||
|
return std::make_pair(needsReply, String());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was an error, return it
|
||||||
|
if (!errorMsg.isEmpty()) {
|
||||||
|
endBootloaderOTA(request);
|
||||||
|
return std::make_pair(needsReply, errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never happen
|
||||||
|
return std::make_pair(true, String(F("Internal software failure")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle bootloader OTA data
|
||||||
|
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) {
|
||||||
|
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context->errorMessage.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer the incoming data
|
||||||
|
if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) {
|
||||||
|
memcpy(context->buffer + context->bytesBuffered, data, len);
|
||||||
|
context->bytesBuffered += len;
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize);
|
||||||
|
} else if (!context->buffer) {
|
||||||
|
DEBUG_PRINTLN(F("Bootloader buffer not allocated!"));
|
||||||
|
context->errorMessage = "Internal error: Bootloader buffer not allocated";
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
size_t totalSize = context->bytesBuffered + len;
|
||||||
|
DEBUG_PRINTLN(F("Bootloader size exceeds maximum!"));
|
||||||
|
context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only write to flash when upload is complete
|
||||||
|
if (isFinal) {
|
||||||
|
DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing"));
|
||||||
|
|
||||||
|
if (context->buffer && context->bytesBuffered > 0) {
|
||||||
|
// Prepare pointers for verification (may be adjusted if bootloader at offset)
|
||||||
|
const uint8_t* bootloaderData = context->buffer;
|
||||||
|
size_t bootloaderSize = context->bytesBuffered;
|
||||||
|
|
||||||
|
// Verify the complete bootloader image before flashing
|
||||||
|
// Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize
|
||||||
|
// for validation purposes only
|
||||||
|
if (!verifyBootloaderImage(bootloaderData, bootloaderSize, context->errorMessage)) {
|
||||||
|
DEBUG_PRINTLN(F("Bootloader validation failed!"));
|
||||||
|
// Error message already set by verifyBootloaderImage
|
||||||
|
} else {
|
||||||
|
// Calculate offset to write to flash
|
||||||
|
// If bootloaderData was adjusted (partition table detected), we need to skip it in flash too
|
||||||
|
size_t flashOffset = context->bootloaderOffset;
|
||||||
|
const uint8_t* dataToWrite = context->buffer;
|
||||||
|
size_t bytesToWrite = context->bytesBuffered;
|
||||||
|
|
||||||
|
// If validation adjusted the pointer, it means we have a partition table at the start
|
||||||
|
// In this case, we should skip writing the partition table and write bootloader at 0x1000
|
||||||
|
if (bootloaderData != context->buffer) {
|
||||||
|
// bootloaderData was adjusted - skip partition table in our data
|
||||||
|
size_t partitionTableSize = bootloaderData - context->buffer;
|
||||||
|
dataToWrite = bootloaderData;
|
||||||
|
bytesToWrite = bootloaderSize;
|
||||||
|
DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"),
|
||||||
|
bytesToWrite, flashOffset);
|
||||||
|
|
||||||
|
// Calculate erase size (must be multiple of 4KB)
|
||||||
|
size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000;
|
||||||
|
if (eraseSize > context->maxBootloaderSize) {
|
||||||
|
eraseSize = context->maxBootloaderSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase bootloader region
|
||||||
|
DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset);
|
||||||
|
esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err);
|
||||||
|
context->errorMessage = "Flash erase failed (error code: " + String(err) + ")";
|
||||||
|
} else {
|
||||||
|
// Write the validated bootloader data to flash
|
||||||
|
err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err);
|
||||||
|
context->errorMessage = "Flash write failed (error code: " + String(err) + ")";
|
||||||
|
} else {
|
||||||
|
DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"),
|
||||||
|
bytesToWrite, flashOffset);
|
||||||
|
// Invalidate cached bootloader hash
|
||||||
|
invalidateBootloaderSHA256Cache();
|
||||||
|
context->uploadComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (context->bytesBuffered == 0) {
|
||||||
|
context->errorMessage = "No bootloader data received";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // WLEDMM unused
|
||||||
|
// -------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -56,11 +56,6 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
|
||||||
/**
|
|
||||||
* Calculate and cache the bootloader SHA256 digest
|
|
||||||
* Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash
|
|
||||||
*/
|
|
||||||
static void calculateBootloaderSHA256();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get bootloader SHA256 as hex string
|
* Get bootloader SHA256 as hex string
|
||||||
@@ -68,9 +63,37 @@ static void calculateBootloaderSHA256();
|
|||||||
*/
|
*/
|
||||||
String getBootloaderSHA256Hex();
|
String getBootloaderSHA256Hex();
|
||||||
|
|
||||||
|
#if 0 // WLEDMM not used
|
||||||
/**
|
/**
|
||||||
* Invalidate cached bootloader SHA256 (call after bootloader update)
|
* Create a bootloader OTA context object on an AsyncWebServerRequest
|
||||||
* Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex
|
* @param request Pointer to web request object
|
||||||
|
* @return true if allocation was successful, false if not
|
||||||
*/
|
*/
|
||||||
static void invalidateBootloaderSHA256Cache();
|
bool initBootloaderOTA(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate to the bootloader OTA subsystem that a reply has already been generated
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
*/
|
||||||
|
void setBootloaderOTAReplied(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the bootloader OTA result.
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
* @return bool indicating if a reply is necessary; string with error message if the update failed.
|
||||||
|
*/
|
||||||
|
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction.
|
||||||
|
* Requires that initBootloaderOTA be called on the handler object before any work will be done.
|
||||||
|
* @param request Pointer to web request object
|
||||||
|
* @param index Offset in to uploaded file
|
||||||
|
* @param data New data bytes
|
||||||
|
* @param len Length of new data bytes
|
||||||
|
* @param isFinal Indicates that this is the last block
|
||||||
|
*/
|
||||||
|
void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
|
||||||
|
#endif // WLEDMM not used
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user