From d6cd65e1089d96a3c22ac5805bb99a170224a6bf Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 27 Nov 2025 20:31:41 -0500 Subject: [PATCH 01/13] 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 | 67 +++++++++++++++++++++++++++---------------- wled00/ota_update.h | 21 -------------- 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 4a93312d3..5fa8dba4e 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -273,18 +273,18 @@ void markOTAvalid() { } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -// Cache for bootloader SHA256 digest as hex string -static String bootloaderSHA256HexCache = ""; - -// Calculate and cache the bootloader SHA256 digest as hex string -void calculateBootloaderSHA256() { - if (!bootloaderSHA256HexCache.isEmpty()) return; +static bool bootloaderSHA256CacheValid = false; +static uint8_t bootloaderSHA256Cache[32]; +/** + * 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) @@ -299,33 +299,50 @@ void calculateBootloaderSHA256() { } } - mbedtls_sha256_finish(&ctx, sha256); + mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache); mbedtls_sha256_free(&ctx); - - // Convert to hex string and cache it - char hex[65]; - for (int i = 0; i < 32; i++) { - sprintf(hex + (i * 2), "%02x", sha256[i]); - } - hex[64] = '\0'; - bootloaderSHA256HexCache = String(hex); + bootloaderSHA256CacheValid = true; } // Get bootloader SHA256 as hex string String getBootloaderSHA256Hex() { - calculateBootloaderSHA256(); - return bootloaderSHA256HexCache; + if (!bootloaderSHA256CacheValid) { + 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 += 7; + if (b2 > '9') b2 += 7; + result.concat(b1); + result.concat(b2); + } + return std::move(result); } -// Invalidate cached bootloader SHA256 (call after bootloader update) -void invalidateBootloaderSHA256Cache() { - bootloaderSHA256HexCache = ""; +/** + * Invalidate cached bootloader SHA256 (call after bootloader update) + * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex + */ +static void invalidateBootloaderSHA256Cache() { + bootloaderSHA256CacheValid = false; } -// Verify complete buffered bootloader using ESP-IDF validation approach -// This matches the key validation steps from esp_image_verify() in ESP-IDF -// Returns the actual bootloader data pointer and length via the buffer and len parameters -bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg) { +/** + * 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")); diff --git a/wled00/ota_update.h b/wled00/ota_update.h index 691429b30..d2fa887d3 100644 --- a/wled00/ota_update.h +++ b/wled00/ota_update.h @@ -58,11 +58,6 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, void markOTAvalid(); #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 @@ -70,22 +65,6 @@ void calculateBootloaderSHA256(); */ String getBootloaderSHA256Hex(); -/** - * Invalidate cached bootloader SHA256 (call after bootloader update) - * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex - */ -void invalidateBootloaderSHA256Cache(); - -/** - * 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 - */ -bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg); - /** * Create a bootloader OTA context object on an AsyncWebServerRequest * @param request Pointer to web request object From 0b965ea4312490a6940a6f89207469aec3297dc5 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 27 Nov 2025 21:42:57 -0500 Subject: [PATCH 02/13] 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 5fa8dba4e..0f7a65c01 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -318,10 +318,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 379343278d7f7ac639f9aecc12d22d9fe2c36ad5 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 27 Nov 2025 21:46:06 -0500 Subject: [PATCH 03/13] 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 0f7a65c01..ac4f83acb 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -323,7 +323,7 @@ String getBootloaderSHA256Hex() { result.concat(b2); result.concat(b1); } - return std::move(result); + return result; } /** From 6d788a27b67d92984d811050e12aef656eb669ad Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:03:11 -0500 Subject: [PATCH 04/13] 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 d8f64a141..51e411209 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -614,14 +614,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 78166090bc942571b48ac281c40170d423575bd4 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:03:22 -0500 Subject: [PATCH 05/13] 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 51e411209..7638548f2 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -18,9 +18,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 @@ -30,6 +32,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 @@ -352,38 +355,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; @@ -391,106 +380,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) + ")"; + 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. 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); + // 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. 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 + // 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; } @@ -499,29 +425,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; @@ -532,16 +457,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; } @@ -601,10 +526,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(); @@ -630,6 +558,7 @@ bool initBootloaderOTA(AsyncWebServerRequest *request) { context->bytesBuffered = 0; return true; +#endif } // Set bootloader OTA replied flag @@ -709,7 +638,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 b51e7b65f954142d8ee7c70cf513b3fe4bb3d670 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:02:08 -0500 Subject: [PATCH 06/13] Factor out bootloader size estimate Co-authored-by: Codex --- wled00/ota_update.cpp | 114 +++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 7638548f2..c818182ce 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -347,6 +347,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 @@ -404,72 +448,28 @@ 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; + } + + 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); - // 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"; - 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; From 76c25da58e84e503f411841614efc9b6bbdf2c24 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 14:11:33 -0500 Subject: [PATCH 07/13] Use bootloader size in hash calculation Co-authored-by: Codex --- wled00/ota_update.cpp | 125 ++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index c818182ce..711134d6c 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -289,64 +289,6 @@ void markOTAvalid() { } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -static bool bootloaderSHA256CacheValid = false; -static uint8_t bootloaderSHA256Cache[32]; - -/** - * Calculate and cache the bootloader SHA256 digest - * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash - */ -static void calculateBootloaderSHA256() { - // 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_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { - mbedtls_sha256_update(&ctx, buffer, readSize); - } - } - - 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 - 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) - * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex - */ -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. @@ -391,6 +333,73 @@ static size_t getBootloaderImageSize(const esp_image_header_t &header, return actualBootloaderSize; } +static bool bootloaderSHA256CacheValid = false; +static uint8_t bootloaderSHA256Cache[32]; + +/** + * Calculate and cache the bootloader SHA256 digest + * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash + */ +static void calculateBootloaderSHA256() { + // Calculate SHA256 + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) + + const size_t chunkSize = 256; + 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); + } + } + + 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 + 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) + * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex + */ +static void invalidateBootloaderSHA256Cache() { + bootloaderSHA256CacheValid = false; +} + /** * Verify complete buffered bootloader using ESP-IDF validation approach * This matches the key validation steps from esp_image_verify() in ESP-IDF From 642c99a6170e3045d34303d9676421e5652aeba3 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 17:18:40 -0500 Subject: [PATCH 08/13] 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 | 147 +++++++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 44 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 711134d6c..4a2b724df 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -289,49 +289,98 @@ void markOTAvalid() { } #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; - 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); + //DEBUG_PRINTF("Feed %d\n", len); - // 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 (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); + 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); + + // 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); + } + + return !error; } - 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; + bool hasError() const { return error; } + bool isSizeKnown() const { return !error && imageSize != 0 && segmentsLeft == 0; } + size_t totalSize() const { + if (!isSizeKnown()) return 0; + return imageSize; } - // Sometimes there is a checksum byte - if (availableLen > actualBootloaderSize) { - actualBootloaderSize += 1; - } - - 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]; @@ -349,19 +398,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); + } } } @@ -426,7 +483,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; @@ -458,11 +515,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 03d0522cf19ea2618fd6e6d8e9c2ecaf6a197af4 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 17:24:38 -0500 Subject: [PATCH 09/13] 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 4a2b724df..179871d0f 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -466,10 +466,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 2434a9624e355a2b4b3297394edd8d588b528c83 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 18:59:14 -0500 Subject: [PATCH 10/13] 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 179871d0f..27ca692ba 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -289,6 +289,9 @@ void markOTAvalid() { } #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: @@ -313,9 +316,6 @@ public: } 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); @@ -345,6 +345,11 @@ public: --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; } @@ -387,7 +392,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 @@ -399,6 +410,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); @@ -417,6 +429,7 @@ static void calculateBootloaderSHA256() { } if (hashLen > 0) { + totalHashLen += hashLen; mbedtls_sha256_update(&ctx, buffer, hashLen); } } @@ -424,6 +437,7 @@ static void calculateBootloaderSHA256() { mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache); mbedtls_sha256_free(&ctx); + bootloaderSHA256CacheValid = true; } @@ -513,18 +527,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 761eb99e530d94f8980184310c8d5c0680670471 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 31 Jan 2026 19:40:54 -0500 Subject: [PATCH 11/13] Fix update UI Make sure the correct things are shown. --- wled00/data/update.htm | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index e93a113fa..12107d2e8 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -29,9 +29,9 @@ if (data.arch == "esp8266") { toggle('rev'); } - const isESP32 = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2'); - if (isESP32) { - gId('bootloader-section').style.display = 'block'; + const allowBl = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2'); + if (allowBl) { + toggle('bootupd') if (data.bootloaderSHA256) { gId('bootloader-hash').innerText = 'Current bootloader SHA256: ' + data.bootloaderSHA256; } @@ -44,14 +44,18 @@ document.querySelector('.release-name').textContent = 'Unknown'; }); } + function hideforms() { + gId('bootupd').classList.toggle("hide"); + toggle('upd'); + } -

WLED Software Update

-
+

WLED Software Update

+ Installed version: Loading...
Release: Loading...
Download the latest binary: Revert update
-