From a1d8b59e0b9233b38087d0e9705b210ef47ad83f Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 27 Nov 2025 20:31:41 -0500 Subject: [PATCH 01/14] ota_update: Clean up SHA API Many of these functions are internal, and we can reduce RAM by caching the binary value instead. --- wled00/ota_update.cpp | 474 ++++++++++++++++++++++++++++++++++++++++-- wled00/ota_update.h | 36 +++- 2 files changed, 484 insertions(+), 26 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index e0a3c3c85d..34e3b194bf 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -272,14 +272,18 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -static String bootloaderSHA256HexCache = ""; +static bool bootloaderSHA256CacheValid = false; +static uint8_t bootloaderSHA256Cache[32]; -// Calculate and cache the bootloader SHA256 digest as hex string -void calculateBootloaderSHA256() { - if (!bootloaderSHA256HexCache.isEmpty()) return; +/** + * Calculate and cache the bootloader SHA256 digest + * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash + */ +static void calculateBootloaderSHA256() { + // Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB + const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size // Calculate SHA256 - uint8_t sha256[32]; mbedtls_sha256_context ctx; mbedtls_sha256_init(&ctx); mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) @@ -298,26 +302,460 @@ void calculateBootloaderSHA256() { } } - mbedtls_sha256_finish(&ctx, sha256); + mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache); mbedtls_sha256_free(&ctx); + bootloaderSHA256CacheValid = true; +} + +// Get bootloader SHA256 as hex string +String getBootloaderSHA256Hex() { + if (!bootloaderSHA256CacheValid) { + calculateBootloaderSHA256(); + } - // Convert to hex string and cache it - char hex[65]; + // Convert to hex string + String result; + result.reserve(65); for (int i = 0; i < 32; i++) { - sprintf(hex + (i * 2), "%02x", sha256[i]); + char b1 = bootloaderSHA256Cache[i]; + char b2 = b1 >> 4; + b1 &= 0x0F; + b1 += '0'; b2 += '0'; + if (b1 > '9') b1 += 7; + if (b2 > '9') b2 += 7; + result.concat(b1); + result.concat(b2); } - hex[64] = '\0'; - bootloaderSHA256HexCache = hex; + return std::move(result); } -// Get bootloader SHA256 as hex string -String getBootloaderSHA256Hex() { - calculateBootloaderSHA256(); - return bootloaderSHA256HexCache; +/** + * Invalidate cached bootloader SHA256 (call after bootloader update) + * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex + */ +static void invalidateBootloaderSHA256Cache() { + bootloaderSHA256CacheValid = false; } -// Invalidate cached bootloader SHA256 (call after bootloader update) -void invalidateBootloaderSHA256Cache() { - bootloaderSHA256HexCache = ""; +/** + * 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) { + size_t availableLen = len; + if (!bootloaderErrorMsg) { + DEBUG_PRINTLN(F("bootloaderErrorMsg is null")); + return false; + } + // ESP32 image header structure (based on esp_image_format.h) + // Offset 0: magic (0xE9) + // Offset 1: segment_count + // Offset 2: spi_mode + // Offset 3: spi_speed (4 bits) + spi_size (4 bits) + // Offset 4-7: entry_addr (uint32_t) + // Offset 8: wp_pin + // Offset 9-11: spi_pin_drv[3] + // Offset 12-13: chip_id (uint16_t, little-endian) + // Offset 14: min_chip_rev + // Offset 15-22: reserved[8] + // Offset 23: hash_appended + + const size_t MIN_IMAGE_HEADER_SIZE = 24; + + // 1. Validate minimum size for header + if (len < MIN_IMAGE_HEADER_SIZE) { + *bootloaderErrorMsg = "Bootloader too small - invalid header"; + 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] == 0xE9 && + buffer[0] != 0xE9) { + DEBUG_PRINTF_P(PSTR("Bootloader magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET); + // 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 = "Bootloader at offset 0x1000 too small - invalid header"; + return false; + } + } + + // 2. Magic byte check (matches esp_image_verify step 1) + if (buffer[0] != 0xE9) { + *bootloaderErrorMsg = "Invalid bootloader magic byte (expected 0xE9, got 0x" + String(buffer[0], HEX) + ")"; + return false; + } + + // 3. Segment count validation (matches esp_image_verify step 2) + uint8_t segmentCount = buffer[1]; + if (segmentCount == 0 || segmentCount > 16) { + *bootloaderErrorMsg = "Invalid segment count: " + String(segmentCount); + return false; + } + + // 4. SPI mode validation (basic sanity check) + uint8_t spiMode = buffer[2]; + if (spiMode > 3) { // Valid modes are 0-3 (QIO, QOUT, DIO, DOUT) + *bootloaderErrorMsg = "Invalid SPI mode: " + String(spiMode); + return false; + } + + // 5. Chip ID validation (matches esp_image_verify step 3) + uint16_t chipId = buffer[12] | (buffer[13] << 8); // Little-endian + + // Known ESP32 chip IDs from ESP-IDF: + // 0x0000 = ESP32 + // 0x0002 = ESP32-S2 + // 0x0005 = ESP32-C3 + // 0x0009 = ESP32-S3 + // 0x000C = ESP32-C2 + // 0x000D = ESP32-C6 + // 0x0010 = ESP32-H2 + + #if defined(CONFIG_IDF_TARGET_ESP32) + if (chipId != 0x0000) { + *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32 (0x0000), got 0x" + String(chipId, HEX); + return false; + } + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + if (chipId != 0x0002) { + *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S2 (0x0002), got 0x" + String(chipId, HEX); + return false; + } + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + if (chipId != 0x0005) { + *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C3 (0x0005), got 0x" + String(chipId, HEX); + return false; + } + *bootloaderErrorMsg = "ESP32-C3 update not supported yet"; + return false; + #elif defined(CONFIG_IDF_TARGET_ESP32S3) + if (chipId != 0x0009) { + *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S3 (0x0009), got 0x" + String(chipId, HEX); + return false; + } + *bootloaderErrorMsg = "ESP32-S3 update not supported yet"; + return false; + #elif defined(CONFIG_IDF_TARGET_ESP32C6) + if (chipId != 0x000D) { + *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C6 (0x000D), got 0x" + String(chipId, HEX); + return false; + } + *bootloaderErrorMsg = "ESP32-C6 update not supported yet"; + return false; + #else + // Generic validation - chip ID should be valid + if (chipId > 0x00FF) { + *bootloaderErrorMsg = "Invalid chip ID: 0x" + String(chipId, HEX); + return false; + } + *bootloaderErrorMsg = "Unknown ESP32 target - bootloader update not supported"; + return false; + #endif + + // 6. Entry point validation (should be in valid memory range) + uint32_t entryAddr = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24); + // ESP32 bootloader entry points are typically in IRAM range (0x40000000 - 0x40400000) + // or ROM range (0x40000000 and above) + if (entryAddr < 0x40000000 || entryAddr > 0x50000000) { + *bootloaderErrorMsg = "Invalid entry address: 0x" + String(entryAddr, HEX); + return false; + } + + // 7. Basic segment structure validation + // Each segment has a header: load_addr (4 bytes) + data_len (4 bytes) + size_t offset = MIN_IMAGE_HEADER_SIZE; + size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE; + + for (uint8_t i = 0; i < segmentCount && offset + 8 <= len; i++) { + uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | + (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); + + // Segment size sanity check + // ESP32 classic bootloader segments can be larger, C3 are smaller + if (segmentSize > 0x20000) { // 128KB max per segment (very generous) + *bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes"; + return false; + } + + offset += 8 + segmentSize; // Skip segment header and data + } + + actualBootloaderSize = offset; + + // 8. Check for appended SHA256 hash (byte 23 in header) + // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments + uint8_t hashAppended = buffer[23]; + if (hashAppended != 0) { + actualBootloaderSize += 32; + if (actualBootloaderSize > availableLen) { + *bootloaderErrorMsg = "Bootloader missing SHA256 trailer"; + return false; + } + DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n")); + } + + // 9. The image may also have a 1-byte checksum after segments/hash + // Check if there's at least one more byte available + if (actualBootloaderSize + 1 <= availableLen) { + // There's likely a checksum byte + actualBootloaderSize += 1; + } else if (actualBootloaderSize > availableLen) { + *bootloaderErrorMsg = "Bootloader truncated before checksum"; + return false; + } + + // 10. Align to 16 bytes (ESP32 requirement for flash writes) + // The bootloader image must be 16-byte aligned + if (actualBootloaderSize % 16 != 0) { + size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16; + // Make sure we don't exceed available data + if (alignedSize <= len) { + actualBootloaderSize = alignedSize; + } + } + + DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"), + segmentCount, actualBootloaderSize, len, hashAppended); + + // 11. Verify we have enough data for all segments + hash + checksum + if (actualBootloaderSize > availableLen) { + *bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes"; + return false; + } + + if (offset > availableLen) { + *bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes"; + return false; + } + + // 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(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 + + 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 + size_t freeHeap = getFreeHeapSize(); + DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), freeHeap, context->maxBootloaderSize); + + context->buffer = (uint8_t*)malloc(context->maxBootloaderSize); + if (!context->buffer) { + size_t freeHeapNow = getFreeHeapSize(); + DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Free heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow); + context->errorMessage = "Out of memory! Free 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; +} + +// Set bootloader OTA replied flag +void setBootloaderOTAReplied(AsyncWebServerRequest *request) { + BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); + if (context) { + context->replySent = true; + } +} + +// Get bootloader OTA result +std::pair getBootloaderOTAResult(AsyncWebServerRequest *request) { + BootloaderUpdateContext* context = reinterpret_cast(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(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 \ No newline at end of file diff --git a/wled00/ota_update.h b/wled00/ota_update.h index 6513e9750c..011d46c8ec 100644 --- a/wled00/ota_update.h +++ b/wled00/ota_update.h @@ -52,11 +52,6 @@ std::pair getOTAResult(AsyncWebServerRequest *request); void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); #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 - */ -void calculateBootloaderSHA256(); /** * Get bootloader SHA256 as hex string @@ -65,8 +60,33 @@ void calculateBootloaderSHA256(); String getBootloaderSHA256Hex(); /** - * Invalidate cached bootloader SHA256 (call after bootloader update) - * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex + * Create a bootloader OTA context object on an AsyncWebServerRequest + * @param request Pointer to web request object + * @return true if allocation was successful, false if not + */ +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 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 invalidateBootloaderSHA256Cache(); +void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); #endif From d2c90c084643f8a954a6fd240fd1b7d57e39f651 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 27 Nov 2025 21:42:57 -0500 Subject: [PATCH 02/14] ota_update: Fix hex print --- wled00/ota_update.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 34e3b194bf..f034d1c68a 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -321,10 +321,10 @@ String getBootloaderSHA256Hex() { char b2 = b1 >> 4; b1 &= 0x0F; b1 += '0'; b2 += '0'; - if (b1 > '9') b1 += 7; - if (b2 > '9') b2 += 7; - result.concat(b1); + if (b1 > '9') b1 += 39; + if (b2 > '9') b2 += 39; result.concat(b2); + result.concat(b1); } return std::move(result); } From e07c70efbd5da743568e7e2b8ae680b2111fd3c5 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 27 Nov 2025 21:46:06 -0500 Subject: [PATCH 03/14] ota_update: Fix NRVO in getBootloaderSHA256Hex h/t @coderabbitai --- wled00/ota_update.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index f034d1c68a..6c7ee7f0b7 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -326,7 +326,7 @@ String getBootloaderSHA256Hex() { result.concat(b2); result.concat(b1); } - return std::move(result); + return result; } /** From 5ffba047e0ba6d51cedbe493f0d9f853f9648a98 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:03:11 -0500 Subject: [PATCH 04/14] Fix heap checks in bootloader update --- wled00/ota_update.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 6c7ee7f0b7..266164f274 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -607,14 +607,13 @@ bool initBootloaderOTA(AsyncWebServerRequest *request) { strip.resetSegments(); // Check available heap before attempting allocation - size_t freeHeap = getFreeHeapSize(); - DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), freeHeap, context->maxBootloaderSize); + 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 = getFreeHeapSize(); - DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Free heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow); - context->errorMessage = "Out of memory! Free heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes"; + 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(); From 8719436923c88e386796c2b68f3fab8749f8c9ca Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:03:22 -0500 Subject: [PATCH 05/14] Bootloader upload validation cleanup Co-authored-by: Codex --- wled00/ota_update.cpp | 153 +++++++++++------------------------------- 1 file changed, 41 insertions(+), 112 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 266164f274..a7e246d43e 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -17,9 +17,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) 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 +#define BOOTLOADER_OTA_UNSUPPORTED // still needs validation on these platforms. #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_SIZE = 0x8000; // 32KB, typical bootloader size +#define BOOTLOADER_OTA_UNSUPPORTED // still needs testing on these platforms #else constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2 constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size @@ -29,6 +31,7 @@ constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset #define UPDATE_ERROR getErrorString #endif + constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes @@ -345,38 +348,24 @@ static void invalidateBootloaderSHA256Cache() { * @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) { - size_t availableLen = len; +static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& bootloaderErrorMsg) { if (!bootloaderErrorMsg) { DEBUG_PRINTLN(F("bootloaderErrorMsg is null")); return false; } - // ESP32 image header structure (based on esp_image_format.h) - // Offset 0: magic (0xE9) - // Offset 1: segment_count - // Offset 2: spi_mode - // Offset 3: spi_speed (4 bits) + spi_size (4 bits) - // Offset 4-7: entry_addr (uint32_t) - // Offset 8: wp_pin - // Offset 9-11: spi_pin_drv[3] - // Offset 12-13: chip_id (uint16_t, little-endian) - // Offset 14: min_chip_rev - // Offset 15-22: reserved[8] - // Offset 23: hash_appended - - const size_t MIN_IMAGE_HEADER_SIZE = 24; + 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 = "Bootloader too small - invalid header"; + 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] == 0xE9 && - buffer[0] != 0xE9) { + buffer[BOOTLOADER_OFFSET] == ESP_IMAGE_HEADER_MAGIC && + buffer[0] != ESP_IMAGE_HEADER_MAGIC) { DEBUG_PRINTF_P(PSTR("Bootloader magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET); // Adjust buffer pointer to start at the actual bootloader buffer = buffer + BOOTLOADER_OFFSET; @@ -384,106 +373,43 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* b // Re-validate size after adjustment if (len < MIN_IMAGE_HEADER_SIZE) { - *bootloaderErrorMsg = "Bootloader at offset 0x1000 too small - invalid header"; + bootloaderErrorMsg = "Too small"; return false; } } - // 2. Magic byte check (matches esp_image_verify step 1) - if (buffer[0] != 0xE9) { - *bootloaderErrorMsg = "Invalid bootloader magic byte (expected 0xE9, got 0x" + String(buffer[0], HEX) + ")"; - return false; - } - - // 3. Segment count validation (matches esp_image_verify step 2) - uint8_t segmentCount = buffer[1]; - if (segmentCount == 0 || segmentCount > 16) { - *bootloaderErrorMsg = "Invalid segment count: " + String(segmentCount); - return false; - } - - // 4. SPI mode validation (basic sanity check) - uint8_t spiMode = buffer[2]; - if (spiMode > 3) { // Valid modes are 0-3 (QIO, QOUT, DIO, DOUT) - *bootloaderErrorMsg = "Invalid SPI mode: " + String(spiMode); + 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; } - // 5. Chip ID validation (matches esp_image_verify step 3) - uint16_t chipId = buffer[12] | (buffer[13] << 8); // Little-endian - - // Known ESP32 chip IDs from ESP-IDF: - // 0x0000 = ESP32 - // 0x0002 = ESP32-S2 - // 0x0005 = ESP32-C3 - // 0x0009 = ESP32-S3 - // 0x000C = ESP32-C2 - // 0x000D = ESP32-C6 - // 0x0010 = ESP32-H2 - - #if defined(CONFIG_IDF_TARGET_ESP32) - if (chipId != 0x0000) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32 (0x0000), got 0x" + String(chipId, HEX); - return false; - } - #elif defined(CONFIG_IDF_TARGET_ESP32S2) - if (chipId != 0x0002) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S2 (0x0002), got 0x" + String(chipId, HEX); - return false; - } - #elif defined(CONFIG_IDF_TARGET_ESP32C3) - if (chipId != 0x0005) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C3 (0x0005), got 0x" + String(chipId, HEX); - return false; - } - *bootloaderErrorMsg = "ESP32-C3 update not supported yet"; - return false; - #elif defined(CONFIG_IDF_TARGET_ESP32S3) - if (chipId != 0x0009) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S3 (0x0009), got 0x" + String(chipId, HEX); - return false; - } - *bootloaderErrorMsg = "ESP32-S3 update not supported yet"; - return false; - #elif defined(CONFIG_IDF_TARGET_ESP32C6) - if (chipId != 0x000D) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C6 (0x000D), got 0x" + String(chipId, HEX); - return false; - } - *bootloaderErrorMsg = "ESP32-C6 update not supported yet"; - return false; - #else - // Generic validation - chip ID should be valid - if (chipId > 0x00FF) { - *bootloaderErrorMsg = "Invalid chip ID: 0x" + String(chipId, HEX); - return false; - } - *bootloaderErrorMsg = "Unknown ESP32 target - bootloader update not supported"; - return false; - #endif - - // 6. Entry point validation (should be in valid memory range) - uint32_t entryAddr = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24); - // ESP32 bootloader entry points are typically in IRAM range (0x40000000 - 0x40400000) - // or ROM range (0x40000000 and above) - if (entryAddr < 0x40000000 || entryAddr > 0x50000000) { - *bootloaderErrorMsg = "Invalid entry address: 0x" + String(entryAddr, HEX); + // 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; } - // 7. Basic segment structure validation + // 4. Basic segment structure validation // Each segment has a header: load_addr (4 bytes) + data_len (4 bytes) size_t offset = MIN_IMAGE_HEADER_SIZE; size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE; - for (uint8_t i = 0; i < segmentCount && offset + 8 <= len; i++) { + for (uint8_t i = 0; i < imageHeader.segment_count && offset + 8 <= len; i++) { uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); // Segment size sanity check // ESP32 classic bootloader segments can be larger, C3 are smaller if (segmentSize > 0x20000) { // 128KB max per segment (very generous) - *bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes"; + bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes"; return false; } @@ -492,29 +418,28 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* b actualBootloaderSize = offset; - // 8. Check for appended SHA256 hash (byte 23 in header) + // 5. Check for appended SHA256 hash (byte 23 in header) // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments - uint8_t hashAppended = buffer[23]; - if (hashAppended != 0) { + if (imageHeader.hash_appended != 0) { actualBootloaderSize += 32; if (actualBootloaderSize > availableLen) { - *bootloaderErrorMsg = "Bootloader missing SHA256 trailer"; + bootloaderErrorMsg = "Bootloader missing SHA256 trailer"; return false; } DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n")); } - // 9. The image may also have a 1-byte checksum after segments/hash + // 6. The image may also have a 1-byte checksum after segments/hash // Check if there's at least one more byte available if (actualBootloaderSize + 1 <= availableLen) { // There's likely a checksum byte actualBootloaderSize += 1; } else if (actualBootloaderSize > availableLen) { - *bootloaderErrorMsg = "Bootloader truncated before checksum"; + bootloaderErrorMsg = "Bootloader truncated before checksum"; return false; } - // 10. Align to 16 bytes (ESP32 requirement for flash writes) + // 7. Align to 16 bytes (ESP32 requirement for flash writes) // The bootloader image must be 16-byte aligned if (actualBootloaderSize % 16 != 0) { size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16; @@ -525,16 +450,16 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* b } DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"), - segmentCount, actualBootloaderSize, len, hashAppended); + imageHeader.segment_count, actualBootloaderSize, len, imageHeader.hash_appended); - // 11. Verify we have enough data for all segments + hash + checksum + // 8. Verify we have enough data for all segments + hash + checksum if (actualBootloaderSize > availableLen) { - *bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes"; + bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes"; return false; } if (offset > availableLen) { - *bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes"; + bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes"; return false; } @@ -594,10 +519,13 @@ bool initBootloaderOTA(AsyncWebServerRequest *request) { 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(); @@ -623,6 +551,7 @@ bool initBootloaderOTA(AsyncWebServerRequest *request) { context->bytesBuffered = 0; return true; +#endif } // Set bootloader OTA replied flag @@ -702,7 +631,7 @@ void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8 // 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)) { + if (!verifyBootloaderImage(bootloaderData, bootloaderSize, context->errorMessage)) { DEBUG_PRINTLN(F("Bootloader validation failed!")); // Error message already set by verifyBootloaderImage } else { From 296414c5b66c2c5e50e1532e4fbc753f29b7b560 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:02:08 -0500 Subject: [PATCH 06/14] Factor out bootloader size estimate Co-authored-by: Codex --- wled00/ota_update.cpp | 112 +++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index a7e246d43e..17a659b81c 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -340,6 +340,50 @@ static void invalidateBootloaderSHA256Cache() { bootloaderSHA256CacheValid = false; } +/** + * Compute bootloader size based on image header and segment layout. + * Returns total size in bytes when valid, or 0 when invalid. + */ +static size_t getBootloaderImageSize(const esp_image_header_t &header, + size_t availableLen) { + size_t offset = sizeof(esp_image_header_t); + size_t actualBootloaderSize = offset; + const uint8_t* buffer = reinterpret_cast(&header); + + if (offset + (header.segment_count*8) > availableLen) { + // Not enough space for segments + return 0; + } + + for (uint8_t i = 0; i < header.segment_count; i++) { + uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | + (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); + + // Segment size sanity check + // ESP32 classic bootloader segments can be larger, C3 are smaller + if (segmentSize > 0x20000) { // 128KB max per segment (very generous) + return 0; + } + + offset += 8 + segmentSize; // Skip segment header and data + } + + actualBootloaderSize = offset; + + // Check for appended SHA256 hash (byte 23 in header) + // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments + if (header.hash_appended != 0) { + actualBootloaderSize += 32; + } + + // Sometimes there is a checksum byte + if (availableLen > actualBootloaderSize) { + actualBootloaderSize += 1; + } + + return actualBootloaderSize; +} + /** * Verify complete buffered bootloader using ESP-IDF validation approach * This matches the key validation steps from esp_image_verify() in ESP-IDF @@ -397,71 +441,27 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& b return false; } - // 4. Basic segment structure validation - // Each segment has a header: load_addr (4 bytes) + data_len (4 bytes) - size_t offset = MIN_IMAGE_HEADER_SIZE; - size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE; - - for (uint8_t i = 0; i < imageHeader.segment_count && offset + 8 <= len; i++) { - uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | - (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); - - // Segment size sanity check - // ESP32 classic bootloader segments can be larger, C3 are smaller - if (segmentSize > 0x20000) { // 128KB max per segment (very generous) - bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes"; - return false; - } - - offset += 8 + segmentSize; // Skip segment header and data - } - - actualBootloaderSize = offset; - - // 5. Check for appended SHA256 hash (byte 23 in header) - // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments - if (imageHeader.hash_appended != 0) { - actualBootloaderSize += 32; - if (actualBootloaderSize > availableLen) { - bootloaderErrorMsg = "Bootloader missing SHA256 trailer"; - return false; - } - DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n")); - } - - // 6. The image may also have a 1-byte checksum after segments/hash - // Check if there's at least one more byte available - if (actualBootloaderSize + 1 <= availableLen) { - // There's likely a checksum byte - actualBootloaderSize += 1; - } else if (actualBootloaderSize > availableLen) { - bootloaderErrorMsg = "Bootloader truncated before checksum"; + // 4. Validate image size + size_t actualBootloaderSize = getBootloaderImageSize(imageHeader, availableLen); + if (actualBootloaderSize == 0) { + bootloaderErrorMsg = "Invalid image"; return false; } - - // 7. Align to 16 bytes (ESP32 requirement for flash writes) + + // 5. Align to 16 bytes (ESP32 requirement for flash writes) // The bootloader image must be 16-byte aligned if (actualBootloaderSize % 16 != 0) { - size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16; - // Make sure we don't exceed available data - if (alignedSize <= len) { - actualBootloaderSize = alignedSize; - } + actualBootloaderSize = ((actualBootloaderSize + 15) / 16) * 16; } - 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); - - // 8. Verify we have enough data for all segments + hash + checksum - if (actualBootloaderSize > availableLen) { - bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes"; + if (actualBootloaderSize > len) { + // Same as above + bootloaderErrorMsg = "Too small"; return false; } - if (offset > availableLen) { - bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes"; - 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 From 0d6c46a9b419cf8789c761711f79d289c28bc49c Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:11:33 -0500 Subject: [PATCH 07/14] Use bootloader size in hash calculation Co-authored-by: Codex --- wled00/ota_update.cpp | 114 +++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 17a659b81c..1aeea180be 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -275,6 +275,50 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +/** + * Compute bootloader size based on image header and segment layout. + * Returns total size in bytes when valid, or 0 when invalid. + */ +static size_t getBootloaderImageSize(const esp_image_header_t &header, + size_t availableLen) { + size_t offset = sizeof(esp_image_header_t); + size_t actualBootloaderSize = offset; + const uint8_t* buffer = reinterpret_cast(&header); + + if (offset + (header.segment_count*8) > availableLen) { + // Not enough space for segments + return 0; + } + + for (uint8_t i = 0; i < header.segment_count; i++) { + uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | + (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); + + // Segment size sanity check + // ESP32 classic bootloader segments can be larger, C3 are smaller + if (segmentSize > 0x20000) { // 128KB max per segment (very generous) + return 0; + } + + offset += 8 + segmentSize; // Skip segment header and data + } + + actualBootloaderSize = offset; + + // Check for appended SHA256 hash (byte 23 in header) + // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments + if (header.hash_appended != 0) { + actualBootloaderSize += 32; + } + + // Sometimes there is a checksum byte + if (availableLen > actualBootloaderSize) { + actualBootloaderSize += 1; + } + + return actualBootloaderSize; +} + static bool bootloaderSHA256CacheValid = false; static uint8_t bootloaderSHA256Cache[32]; @@ -283,24 +327,26 @@ static uint8_t bootloaderSHA256Cache[32]; * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash */ static void calculateBootloaderSHA256() { - // Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB - const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size - // Calculate SHA256 mbedtls_sha256_context ctx; mbedtls_sha256_init(&ctx); mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) const size_t chunkSize = 256; - uint8_t buffer[chunkSize]; - - for (uint32_t offset = 0; offset < BOOTLOADER_SIZE; offset += chunkSize) { - size_t readSize = min((size_t)(BOOTLOADER_SIZE - 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) { // use esp_flash_read for V4 framework (-S2, -S3, -C3) -#else - if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32) -#endif + alignas(esp_image_header_t) uint8_t buffer[chunkSize]; + size_t bootloaderSize = BOOTLOADER_SIZE; + + for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { + size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); + if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { + if (offset == 0 && readSize >= sizeof(esp_image_header_t)) { + const esp_image_header_t& header = *reinterpret_cast(buffer); + size_t imageSize = getBootloaderImageSize(header, readSize); + if (imageSize > 0 && imageSize <= BOOTLOADER_SIZE) { + bootloaderSize = imageSize; + readSize = min(readSize, bootloaderSize); + } + } mbedtls_sha256_update(&ctx, buffer, readSize); } } @@ -340,50 +386,6 @@ static void invalidateBootloaderSHA256Cache() { bootloaderSHA256CacheValid = false; } -/** - * Compute bootloader size based on image header and segment layout. - * Returns total size in bytes when valid, or 0 when invalid. - */ -static size_t getBootloaderImageSize(const esp_image_header_t &header, - size_t availableLen) { - size_t offset = sizeof(esp_image_header_t); - size_t actualBootloaderSize = offset; - const uint8_t* buffer = reinterpret_cast(&header); - - if (offset + (header.segment_count*8) > availableLen) { - // Not enough space for segments - return 0; - } - - for (uint8_t i = 0; i < header.segment_count; i++) { - uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | - (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); - - // Segment size sanity check - // ESP32 classic bootloader segments can be larger, C3 are smaller - if (segmentSize > 0x20000) { // 128KB max per segment (very generous) - return 0; - } - - offset += 8 + segmentSize; // Skip segment header and data - } - - actualBootloaderSize = offset; - - // Check for appended SHA256 hash (byte 23 in header) - // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments - if (header.hash_appended != 0) { - actualBootloaderSize += 32; - } - - // Sometimes there is a checksum byte - if (availableLen > actualBootloaderSize) { - actualBootloaderSize += 1; - } - - return actualBootloaderSize; -} - /** * Verify complete buffered bootloader using ESP-IDF validation approach * This matches the key validation steps from esp_image_verify() in ESP-IDF From aa685dc3b77a1cea0990298972b1f0b3285f4976 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 17:18:40 -0500 Subject: [PATCH 08/14] Stream bootloader size validation Use a stateful object to allow bootloader size calculation to operate on a stream of data blocks instead of requiring a single flat read. This allows it to work when calculating the bootloader hash as well as during update validation. Co-authored-by: Codex --- wled00/ota_update.cpp | 143 +++++++++++++++++++++++++++++------------- 1 file changed, 101 insertions(+), 42 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 1aeea180be..26610c0d78 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -275,49 +275,98 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -/** - * Compute bootloader size based on image header and segment layout. - * Returns total size in bytes when valid, or 0 when invalid. - */ -static size_t getBootloaderImageSize(const esp_image_header_t &header, - size_t availableLen) { - size_t offset = sizeof(esp_image_header_t); - size_t actualBootloaderSize = offset; - const uint8_t* buffer = reinterpret_cast(&header); +class BootloaderImageSizer { +public: - if (offset + (header.segment_count*8) > availableLen) { - // Not enough space for segments - return 0; - } + 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; + } - for (uint8_t i = 0; i < header.segment_count; i++) { - uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) | - (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24); + esp_image_header_t header; + memcpy(&header, data, sizeof(esp_image_header_t)); - // Segment size sanity check - // ESP32 classic bootloader segments can be larger, C3 are smaller - if (segmentSize > 0x20000) { // 128KB max per segment (very generous) - return 0; + if (header.segment_count == 0) { + error = true; + return false; + } + + imageSize = sizeof(esp_image_header_t); + if (header.hash_appended) { + imageSize += 32; + } + 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); } - offset += 8 + segmentSize; // Skip segment header and data - } + 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(&segmentHeader, 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); - actualBootloaderSize = offset; + // 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 + 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); + } - // Check for appended SHA256 hash (byte 23 in header) - // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments - if (header.hash_appended != 0) { - actualBootloaderSize += 32; + return !error; } - // Sometimes there is a checksum byte - if (availableLen > actualBootloaderSize) { - actualBootloaderSize += 1; + bool hasError() const { return error; } + bool isSizeKnown() const { return !error && imageSize != 0 && segmentsLeft == 0; } + size_t totalSize() const { + if (!isSizeKnown()) return 0; + return imageSize; } - return actualBootloaderSize; -} +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]; @@ -335,19 +384,27 @@ static void calculateBootloaderSHA256() { const size_t chunkSize = 256; alignas(esp_image_header_t) uint8_t buffer[chunkSize]; size_t bootloaderSize = BOOTLOADER_SIZE; + BootloaderImageSizer sizer; for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { - if (offset == 0 && readSize >= sizeof(esp_image_header_t)) { - const esp_image_header_t& header = *reinterpret_cast(buffer); - size_t imageSize = getBootloaderImageSize(header, readSize); - if (imageSize > 0 && imageSize <= BOOTLOADER_SIZE) { - bootloaderSize = imageSize; - readSize = min(readSize, bootloaderSize); + sizer.feed(buffer, readSize); + + size_t hashLen = readSize; + if (sizer.isSizeKnown()) { + size_t totalSize = sizer.totalSize(); + if (totalSize > 0 && totalSize <= BOOTLOADER_SIZE) { + bootloaderSize = totalSize; + if (offset + readSize > totalSize) { + hashLen = (totalSize > offset) ? (totalSize - offset) : 0; + } } } - mbedtls_sha256_update(&ctx, buffer, readSize); + + if (hashLen > 0) { + mbedtls_sha256_update(&ctx, buffer, hashLen); + } } } @@ -412,7 +469,7 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& b 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 magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET); + 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; @@ -444,11 +501,13 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& b } // 4. Validate image size - size_t actualBootloaderSize = getBootloaderImageSize(imageHeader, availableLen); - if (actualBootloaderSize == 0) { + BootloaderImageSizer sizer; + sizer.feed(buffer, availableLen); + if (sizer.hasError() || !sizer.isSizeKnown()) { bootloaderErrorMsg = "Invalid image"; return false; } + size_t actualBootloaderSize = sizer.totalSize(); // 5. Align to 16 bytes (ESP32 requirement for flash writes) // The bootloader image must be 16-byte aligned From 75f5674ed9da019f1c64da5094c260736ca0fe3d Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 17:24:38 -0500 Subject: [PATCH 09/14] Fix null test --- wled00/ota_update.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 26610c0d78..8e7bfe87cb 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -452,10 +452,6 @@ static void invalidateBootloaderSHA256Cache() { * @return true if validation passed, false otherwise */ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& bootloaderErrorMsg) { - if (!bootloaderErrorMsg) { - DEBUG_PRINTLN(F("bootloaderErrorMsg is null")); - return false; - } const size_t MIN_IMAGE_HEADER_SIZE = sizeof(esp_image_header_t); // 1. Validate minimum size for header From 5f7f0ca2bb83d689fb046d1b0a2d415ea4028c5d Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 18:59:14 -0500 Subject: [PATCH 10/14] Ensure bootloader hashes match Ensure that our calculation function returns the same value as the image internal hash. --- wled00/ota_update.cpp | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 8e7bfe87cb..3a0eed5440 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -275,6 +275,9 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + +// 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: @@ -299,9 +302,6 @@ class BootloaderImageSizer { } imageSize = sizeof(esp_image_header_t); - if (header.hash_appended) { - imageSize += 32; - } segmentsLeft = header.segment_count; data += sizeof(esp_image_header_t); len -= sizeof(esp_image_header_t); @@ -331,6 +331,11 @@ class BootloaderImageSizer { --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; } @@ -373,7 +378,13 @@ static uint8_t bootloaderSHA256Cache[32]; /** * Calculate and cache the bootloader SHA256 digest - * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash + * 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() { // Calculate SHA256 @@ -385,6 +396,7 @@ static void calculateBootloaderSHA256() { 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 < bootloaderSize; offset += chunkSize) { size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); @@ -403,6 +415,7 @@ static void calculateBootloaderSHA256() { } if (hashLen > 0) { + totalHashLen += hashLen; mbedtls_sha256_update(&ctx, buffer, hashLen); } } @@ -410,6 +423,7 @@ static void calculateBootloaderSHA256() { mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache); mbedtls_sha256_free(&ctx); + bootloaderSHA256CacheValid = true; } @@ -499,18 +513,17 @@ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& b // 4. Validate image size BootloaderImageSizer sizer; sizer.feed(buffer, availableLen); - if (sizer.hasError() || !sizer.isSizeKnown()) { + if (!sizer.isSizeKnown()) { bootloaderErrorMsg = "Invalid image"; return false; } size_t actualBootloaderSize = sizer.totalSize(); - - // 5. Align to 16 bytes (ESP32 requirement for flash writes) - // The bootloader image must be 16-byte aligned - if (actualBootloaderSize % 16 != 0) { - actualBootloaderSize = ((actualBootloaderSize + 15) / 16) * 16; - } + // 5. SHA256 checksum (optional) + if (imageHeader.hash_appended == 1) { + actualBootloaderSize += 32; + } + if (actualBootloaderSize > len) { // Same as above bootloaderErrorMsg = "Too small"; From ac59c83303ecc3934066c7f9cbd58e017bd3e074 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 19:42:03 -0500 Subject: [PATCH 11/14] Disable boot loader debug prints --- wled00/ota_update.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 3a0eed5440..0af1723a14 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -305,7 +305,7 @@ class BootloaderImageSizer { 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); + //DEBUG_PRINTF("BLS parsed image header, segment count %d, is %d\n", segmentsLeft, imageSize); } while (len && segmentsLeft) { @@ -317,7 +317,7 @@ class BootloaderImageSizer { 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); + //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) { @@ -336,7 +336,7 @@ class BootloaderImageSizer { // Always add 1 to account for the checksum byte. imageSize = ((imageSize/ 16) + 1) * 16; - DEBUG_PRINTF("BLS complete, is %d\n", imageSize); + //DEBUG_PRINTF("BLS complete, is %d\n", imageSize); return false; } } From daf92c0a0af61052718c51f40be676779df74ca6 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 20:33:28 -0500 Subject: [PATCH 12/14] Accept rabbit's fixes --- wled00/ota_update.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 0af1723a14..ccc0ebfea0 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -311,7 +311,7 @@ class BootloaderImageSizer { 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(&segmentHeader, data, headerBytes); + memcpy(reinterpret_cast(&segmentHeader) + segmentHeaderBytes, data, headerBytes); segmentHeaderBytes += headerBytes; if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) { return true; // needs more bytes for the header From a1511d778ab06d8815c3352513d0b26461fa6ccb Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 21 Feb 2026 16:20:31 -0500 Subject: [PATCH 13/14] ota_update: Shim IDFv4 flash API --- wled00/ota_update.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index ccc0ebfea0..da532674bc 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -5,6 +5,14 @@ #include #include #include + +#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 // Platform-specific metadata locations From 6fc5a8bee83fcfa52f28c7cb43749c6345bf3004 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 21 Feb 2026 16:38:03 -0500 Subject: [PATCH 14/14] Import memory check functions --- wled00/fcn_declare.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a467f8f51f..76b4a7578c 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -410,6 +410,14 @@ void enumerateLedmaps(); uint8_t get_random_wheel_index(uint8_t pos); float mapf(float x, float in_min, float in_max, float out_min, float out_max); +#ifndef ESP8266 +inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) +inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block +#else +inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap +inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block +#endif + void handleBootLoop(); // detect and handle bootloops #ifndef ESP8266 void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config