Compare commits

...

13 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f706c6cbed Rename ignoreRelease to skipValidation in OTA validation system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 16:16:42 +00:00
copilot-swe-agent[bot]
18dfc7081c Address review feedback: unify structures, remove code duplication, improve efficiency
Co-authored-by: willmmiles <6540455+willmmiles@users.noreply.github.com>
2025-09-14 15:05:15 +00:00
copilot-swe-agent[bot]
5c0c84eb6b Add ESP8266 support to OTA release compatibility system using .ver_number section
Co-authored-by: willmmiles <6540455+willmmiles@users.noreply.github.com>
2025-09-14 14:34:31 +00:00
copilot-swe-agent[bot]
691c058ae8 Fix runtime release name replacement - move to build-time
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 14:14:11 +00:00
copilot-swe-agent[bot]
7d550baf94 Replace metadata header approach with ESP-IDF custom description section
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 13:51:48 +00:00
copilot-swe-agent[bot]
42ff73ffe7 Implement metadata-based OTA release checking system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 13:11:24 +00:00
copilot-swe-agent[bot]
2d8edfcb24 Remove build artifacts from repository
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:56:12 +00:00
copilot-swe-agent[bot]
fb077ecadc Improve OTA release checking with scoring-based candidate selection
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:55:18 +00:00
copilot-swe-agent[bot]
caf3d900fd Fix OTA release checking to use flexible string search instead of hardcoded patterns
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:50:47 +00:00
copilot-swe-agent[bot]
e920d2e101 Complete OTA release compatibility system with comprehensive testing
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:32:52 +00:00
copilot-swe-agent[bot]
54746c9730 Implement OTA release compatibility checking system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:28:47 +00:00
copilot-swe-agent[bot]
8225a2a07c Initial plan 2025-09-14 12:13:03 +00:00
Damian Schneider
75f6de9dc2 bugfix in Colortwinkles
thx @blazoncek
2025-09-09 07:31:19 +02:00
8 changed files with 301 additions and 10 deletions

View File

@@ -1,5 +1,6 @@
Import("env")
import shutil
import os
node_ex = shutil.which("node")
# Check if Node.js is installed and present in PATH if it failed, abort the build
@@ -12,6 +13,21 @@ else:
print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m')
env.Execute("npm ci")
# Extract the release name from build flags
release_name = "Custom"
build_flags = env.get("BUILD_FLAGS", [])
for flag in build_flags:
if 'WLED_RELEASE_NAME=' in flag:
# Extract the release name, remove quotes and handle different formats
parts = flag.split('WLED_RELEASE_NAME=')
if len(parts) > 1:
release_name = parts[1].split()[0].strip('\"\\')
break
# Set environment variable for cdata.js to use
os.environ['WLED_RELEASE_NAME'] = release_name
print(f'Building web UI with release name: {release_name}')
# Call the bundling script
exitCode = env.Execute("npm run build")

View File

@@ -95,6 +95,11 @@ function adoptVersionAndRepo(html) {
if (version) {
html = html.replaceAll("##VERSION##", version);
}
// Replace ##RELEASE## with the actual release name from build environment
const releaseName = process.env.WLED_RELEASE_NAME || 'Custom';
html = html.replaceAll("##RELEASE##", releaseName);
return html;
}

View File

@@ -2328,7 +2328,7 @@ uint16_t mode_colortwinkle() {
}
if (cur == prev) { //fix "stuck" pixels
color_add(col, col);
col = color_add(col, col);
SEGMENT.setPixelColor(i, col);
}
else SEGMENT.setPixelColor(i, col);

View File

@@ -28,10 +28,13 @@
<h2>WLED Software Update</h2>
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">WLED ##VERSION##</span><br>
Release: <span class="sip">##RELEASE##</span><br>
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
style="vertical-align: text-bottom; display: inline-flex;">
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<input type='checkbox' name='skipValidation' id='skipValidation'>
<label for='skipValidation'>Ignore firmware validation</label><br>
<button type="submit">Update!</button><br>
<hr class="sml">
<button id="rev" type="button" onclick="cR()">Revert update</button><br>

View File

@@ -0,0 +1,122 @@
#include "ota_release_check.h"
#include "wled.h"
#ifdef ESP32
#include <esp_app_format.h>
#include <esp_ota_ops.h>
#endif
// Same hash function used at compile time (must match wled_custom_desc.cpp)
static uint32_t djb2_hash(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease) {
if (!binaryData || !extractedRelease || dataSize < 64) {
return false;
}
// Search in first 8KB only - ESP32 .rodata.wled_desc and ESP8266 .ver_number
// sections appear early in binary. 8KB should be sufficient for metadata discovery
// while minimizing processing time for large firmware files.
const size_t search_limit = min(dataSize, (size_t)8192);
for (size_t offset = 0; offset <= search_limit - sizeof(wled_custom_desc_t); offset++) {
const wled_custom_desc_t* custom_desc = (const wled_custom_desc_t*)(binaryData + offset);
// Check for magic number
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
// Found potential match, validate version
if (custom_desc->version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, custom_desc->version);
continue;
}
// Validate hash using same algorithm as compile-time
uint32_t expected_hash = djb2_hash(custom_desc->release_name);
if (custom_desc->crc32 != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}
// Valid structure found
strncpy(extractedRelease, custom_desc->release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
extractedRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
#ifdef ESP32
DEBUG_PRINTF_P(PSTR("Extracted ESP32 release name from .rodata.wled_desc section at offset %u: '%s'\n"),
offset, extractedRelease);
#else
DEBUG_PRINTF_P(PSTR("Extracted ESP8266 release name from .ver_number section at offset %u: '%s'\n"),
offset, extractedRelease);
#endif
return true;
}
}
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
}
bool validateReleaseCompatibility(const char* extractedRelease) {
if (!extractedRelease || strlen(extractedRelease) == 0) {
return false;
}
// Simple string comparison - releases must match exactly
bool match = strcmp(releaseString, extractedRelease) == 0;
DEBUG_PRINTF_P(PSTR("Release compatibility check: current='%s', uploaded='%s', match=%s\n"),
releaseString, extractedRelease, match ? "YES" : "NO");
return match;
}
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool skipValidation, char* errorMessage) {
// Clear error message
if (errorMessage) {
errorMessage[0] = '\0';
}
// Ensure our custom description structure is referenced (prevents optimization)
const wled_custom_desc_t* local_desc = getWledCustomDesc();
(void)local_desc; // Suppress unused variable warning
// If user chose to ignore release check, allow OTA
if (skipValidation) {
DEBUG_PRINTLN(F("OTA release check bypassed by user"));
return true;
}
// Try to extract release name directly from binary data
char extractedRelease[WLED_RELEASE_NAME_MAX_LEN];
bool hasCustomDesc = extractReleaseFromCustomDesc(binaryData, dataSize, extractedRelease);
if (!hasCustomDesc) {
// No custom description - this could be a legacy binary
if (errorMessage) {
strcpy(errorMessage, "Binary has no release compatibility metadata. Check 'Ignore validation' to proceed.");
}
DEBUG_PRINTLN(F("OTA blocked: No custom description found"));
return false;
}
// Validate compatibility using extracted release name
if (!validateReleaseCompatibility(extractedRelease)) {
if (errorMessage) {
snprintf(errorMessage, 127, "Release mismatch: current='%s', uploaded='%s'. Check 'Ignore validation' to proceed.",
releaseString, extractedRelease);
}
DEBUG_PRINTF_P(PSTR("OTA blocked: Release mismatch current='%s', uploaded='%s'\n"),
releaseString, extractedRelease);
return false;
}
DEBUG_PRINTLN(F("OTA allowed: Release names match"));
return true;
}

View File

@@ -0,0 +1,64 @@
#ifndef WLED_OTA_RELEASE_CHECK_H
#define WLED_OTA_RELEASE_CHECK_H
/*
* OTA Release Compatibility Checking using ESP-IDF Custom Description Section
* Functions to extract and validate release names from uploaded binary files using embedded metadata
*/
#include <Arduino.h>
#ifdef ESP32
#include <esp_app_format.h>
#endif
#define WLED_CUSTOM_DESC_MAGIC 0x57535453 // "WSTS" (WLED System Tag Structure)
#define WLED_CUSTOM_DESC_VERSION 1
#define WLED_RELEASE_NAME_MAX_LEN 48
/**
* WLED Custom Description Structure
* This structure is embedded in platform-specific sections at a fixed offset
* in ESP32/ESP8266 binaries, allowing extraction without modifying the binary format
*/
typedef struct {
uint32_t magic; // Magic number to identify WLED custom description
uint32_t version; // Structure version for future compatibility
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
uint32_t crc32; // CRC32 of the above fields for integrity check
} __attribute__((packed)) wled_custom_desc_t;
/**
* Extract release name from binary using ESP-IDF custom description section
* @param binaryData Pointer to binary file data
* @param dataSize Size of binary data in bytes
* @param extractedRelease Buffer to store extracted release name (should be at least WLED_RELEASE_NAME_MAX_LEN bytes)
* @return true if release name was found and extracted, false otherwise
*/
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease);
/**
* Validate if extracted release name matches current release
* @param extractedRelease Release name from uploaded binary
* @return true if releases match (OTA should proceed), false if they don't match
*/
bool validateReleaseCompatibility(const char* extractedRelease);
/**
* Check if OTA should be allowed based on release compatibility using custom description
* @param binaryData Pointer to binary file data (not modified)
* @param dataSize Size of binary data in bytes
* @param skipValidation If true, skip release validation
* @param errorMessage Buffer to store error message if validation fails (should be at least 128 bytes)
* @return true if OTA should proceed, false if it should be blocked
*/
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool skipValidation, char* errorMessage);
/**
* Get pointer to the embedded custom description structure
* This ensures the structure is referenced and not optimized out
* @return pointer to the custom description structure
*/
const wled_custom_desc_t* getWledCustomDesc();
#endif // WLED_OTA_RELEASE_CHECK_H

View File

@@ -0,0 +1,31 @@
#include "ota_release_check.h"
#include "wled.h"
// Simple compile-time hash function for release name validation
constexpr uint32_t djb2_hash(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
// Single structure definition for both platforms
#ifdef ESP32
const wled_custom_desc_t __attribute__((section(".rodata.wled_desc"))) wled_custom_description = {
#elif defined(ESP8266)
const wled_custom_desc_t __attribute__((section(".ver_number"))) wled_custom_description = {
#endif
WLED_CUSTOM_DESC_MAGIC, // magic
WLED_CUSTOM_DESC_VERSION, // version
WLED_RELEASE_NAME, // release_name
djb2_hash(WLED_RELEASE_NAME) // crc32 - computed at compile time
};
// Single reference to ensure it's not optimized away
const wled_custom_desc_t* __attribute__((used)) wled_custom_desc_ref = &wled_custom_description;
// Function to ensure the structure is referenced by code
const wled_custom_desc_t* getWledCustomDesc() {
return &wled_custom_description;
}

View File

@@ -6,6 +6,7 @@
#else
#include <Update.h>
#endif
#include "ota_release_check.h"
#endif
#include "html_ui.h"
#include "html_settings.h"
@@ -424,32 +425,81 @@ void initServer()
return;
}
if (!correctPIN || otaLock) return;
// Static variable to track release check status across chunks
static bool releaseCheckPassed = false;
if(!index){
DEBUG_PRINTLN(F("OTA Update Start"));
// Check if user wants to ignore release check
bool skipValidation = request->hasParam("skipValidation", true);
// Validate OTA release compatibility using the first chunk data directly
char errorMessage[128];
releaseCheckPassed = shouldAllowOTA(data, len, skipValidation, errorMessage);
if (!releaseCheckPassed) {
DEBUG_PRINTF_P(PSTR("OTA blocked: %s\n"), errorMessage);
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), errorMessage);
return;
}
DEBUG_PRINTLN(F("Release check passed, starting OTA update"));
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().disableWatchdog();
#endif
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
lastEditTime = millis(); // make sure PIN does not lock during update
// Start the actual OTA update
strip.suspend();
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
#ifdef ESP8266
Update.runAsync(true);
#endif
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
}
if(!Update.hasError()) Update.write(data, len);
if(isFinal){
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
// Begin update with the firmware size from content length
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
if (!Update.begin(updateSize)) {
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), Update.getErrorString().c_str());
strip.resume();
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
UsermodManager::onUpdateBegin(false);
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
#ifdef ESP32
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.errorString());
#else
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.getErrorString());
#endif
return;
}
}
// Write chunk data to OTA update (only if release check passed)
if (releaseCheckPassed && !Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.getErrorString().c_str());
}
}
if(isFinal){
DEBUG_PRINTLN(F("OTA Update End"));
if (releaseCheckPassed) {
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
strip.resume();
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
}
}
}
});