Skip to content

Add option to use SafeBootloader#5426

Open
wled-install wants to merge 3 commits intowled:mainfrom
wled-install:feature/safe-bootloader
Open

Add option to use SafeBootloader#5426
wled-install wants to merge 3 commits intowled:mainfrom
wled-install:feature/safe-bootloader

Conversation

@wled-install
Copy link
Contributor

@wled-install wled-install commented Mar 15, 2026

Add option to use https://github.com/wled-install/SafeBootloader

If it is too specific, maybe just generic hooks can be implemented on relevant places as a part of usermod infrastructure and functionality can be then implemented as usermod.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Safe Boot support for enhanced file system integrity. The system now automatically verifies and maintains data consistency during configuration updates, file operations, and preset management when Safe Boot is enabled.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Walkthrough

This PR adds a Safe Boot feature that calculates and maintains CRC checksums for the SPIFFS filesystem partition. It introduces new utility functions and integrates CRC updates into key file operations (config serialization, file closing, and file copying) when the WLED_ENABLE_SAFE_BOOT flag is enabled.

Changes

Cohort / File(s) Summary
Safe Boot Core Implementation
wled00/safe_boot_functions.h, wled00/safe_boot_functions.cpp
New safe boot utilities introducing CRC-32 calculation over SPIFFS partitions, update logic that compares computed vs. stored CRC values, and conditional partition metadata updates. Includes ESP partition and CRC handling with logging.
Filesystem Integration
wled00/cfg.cpp, wled00/file.cpp, wled00/presets.cpp
Adds safe_boot_functions.h includes and integrates update_spiffs_crc() calls after key operations: config serialization, file close, successful file copy, and presets initialization. All guarded by WLED_ENABLE_SAFE_BOOT flag.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • willmmiles
  • Jason2866
  • DedeHai
  • netmindz
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add option to use SafeBootloader' is specific and relates to the main objective of the pull request, which adds support for SafeBootloader functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
wled00/safe_boot_functions.h (1)

1-4: Minor typo in comment.

"Safe Bootbootloader" should be "Safe Bootloader".

-/*
- This functions are used to use WLED together with "Safe Bootbootloader"
+/*
+ * These functions are used to use WLED together with "Safe Bootloader"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.h` around lines 1 - 4, Fix the typo in the file
header comment in safe_boot_functions.h: change the phrase "Safe Bootbootloader"
to "Safe Bootloader" in the top comment block so the description reads
correctly; locate the comment block at the top of safe_boot_functions.h and
update the text accordingly.
wled00/ota_update.cpp (1)

100-102: Indentation inconsistency inside preprocessor block.

The function call indentation doesn't match the surrounding code style. As per coding guidelines, use 2 spaces per indentation level.

         `#ifdef`  WLED_ENABLE_SAFE_BOOT
-          update_ota_crc(); 
+        update_ota_crc();
         `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/ota_update.cpp` around lines 100 - 102, The call to update_ota_crc()
inside the `#ifdef` WLED_ENABLE_SAFE_BOOT block has incorrect indentation; adjust
it to follow the project's 2-space-per-indent convention so the line with
update_ota_crc(); is indented to match surrounding code (inside the same block)
and keep the `#ifdef/`#endif aligned with other preprocessor directives; locate
the block using the symbol update_ota_crc and the macro WLED_ENABLE_SAFE_BOOT
and correct spacing only—no logic changes.
wled00/cfg.cpp (1)

836-838: Indentation inconsistency inside preprocessor block.

The function call uses 4-space indentation. As per coding guidelines, use 2 spaces per indentation level.

   `#ifdef`  WLED_ENABLE_SAFE_BOOT
-    update_spiffs_crc();
+  update_spiffs_crc();
   `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/cfg.cpp` around lines 836 - 838, The call inside the preprocessor
block has a 4-space indent; change it to 2-space indentation to match project
style. Locate the WLED_ENABLE_SAFE_BOOT block around the preprocessor directives
and adjust the line with update_spiffs_crc() to use 2 spaces (e.g., " 
update_spiffs_crc();") and ensure the surrounding "#ifdef WLED_ENABLE_SAFE_BOOT"
and "#endif" remain aligned with file convention.
wled00/presets.cpp (1)

118-120: Indentation inconsistency inside preprocessor block.

The function call uses 4-space indentation whereas the codebase guidelines specify 2 spaces per level. As per coding guidelines, C++ files should use 2-space indentation.

   `#ifdef`  WLED_ENABLE_SAFE_BOOT
-    update_spiffs_crc();
+  update_spiffs_crc();
   `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/presets.cpp` around lines 118 - 120, The call to update_spiffs_crc()
inside the WLED_ENABLE_SAFE_BOOT preprocessor block is indented with 4 spaces;
adjust it to 2-space indentation to match the project's C++ style
guidelines—locate the block guarded by `#ifdef` WLED_ENABLE_SAFE_BOOT and change
the whitespace before update_spiffs_crc(); to two spaces so it aligns with
surrounding code.
wled00/safe_boot_functions.cpp (2)

19-39: Make calc_crc static to reduce global namespace pollution.

The calc_crc function is only used internally within this file and is not declared in the header. Making it static enables better compiler optimizations and prevents symbol conflicts. Based on learnings, C-style functions not declared in global headers should be made static.

-uint32_t calc_crc(const esp_partition_t* part)
+static uint32_t calc_crc(const esp_partition_t* part)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.cpp` around lines 19 - 39, The calc_crc function
is exposed as a global symbol but only used within this translation unit; make
it static by adding the static storage-class specifier to its definition (i.e.,
change the function signature for calc_crc to be static uint32_t calc_crc(...))
so the symbol is internal to the file and allows better optimization and avoids
name collisions; update any internal callers if needed (they should continue to
call calc_crc as before).

12-17: Use English for code comments.

Several comments are in German which reduces accessibility for international contributors:

  • Line 16: "Größe SPIFFS / Backup" → "Size SPIFFS / Backup"
  • Line 128: "prüfen ob identisch" → "check if identical"
  • Line 133: "nichts zu tun" → "nothing to do"

Also applies to: 128-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.cpp` around lines 12 - 17, Update all German
inline comments to English: in the crc_group_t typedef update "Größe SPIFFS /
Backup" to "Size SPIFFS / Backup" (referencing fields crc_app, crc_spiffs,
size_spiffs), and update the comments around the later block where you have
"prüfen ob identisch" and "nichts zu tun" to "check if identical" and "nothing
to do" respectively; ensure the replaced comments preserve original intent and
punctuation and are applied where the CRC/spiffs comparison logic is implemented
so all code comments are in English.
wled00/file.cpp (1)

485-487: Indentation inconsistency and brace placement.

The else block with braces on the same line is inconsistent with typical WLED code style. As per coding guidelines, use 2 spaces per indentation level.

   `#ifdef`  WLED_ENABLE_SAFE_BOOT
-    else {update_spiffs_crc();}
+  else {
+    update_spiffs_crc();
+  }
   `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/file.cpp` around lines 485 - 487, Fix the indentation and brace
placement for the conditional guarded by WLED_ENABLE_SAFE_BOOT: move the else
onto its own indented line and place the opening brace on the next line, use 2
spaces per indentation level, and ensure the call to update_spiffs_crc() and the
closing brace are indented accordingly; update the block that references
WLED_ENABLE_SAFE_BOOT and the update_spiffs_crc() call to match WLED coding
style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/safe_boot_functions.cpp`:
- Around line 97-98: The calls to esp_partition_erase_range and
esp_partition_write (operating on crcpart with the stored buffer) lack error
checking; update safe_boot_functions.cpp to capture their esp_err_t returns,
check for != ESP_OK, and handle failures (log via your logger, return/propagate
an error code from the surrounding function or abort the CRC update, and avoid
leaving partial data). Additionally, after esp_partition_write, read back or
verify the written data (compare to stored) and treat mismatches as errors so
the caller can retry or fail safely.
- Line 32: The call to esp_partition_read is missing error checking; update the
code that calls esp_partition_read(part, offset, buf, toread) to capture its
return value, verify it equals ESP_OK (or appropriate success constant), and on
failure avoid using buf for CRC calculation—log/report the error (including
esp_err_to_name(err)), propagate/return an error code from the surrounding
function (or abort the safe-boot update flow) and ensure you do not write/store
an invalid CRC; reference esp_partition_read, variables part/offset/buf/toread
and the surrounding safe-boot CRC write path when implementing this handling.
- Around line 142-143: The esp_partition_erase_range and esp_partition_write
calls for the SPIFFS CRC partition (crcpart, 0, 4096 and
esp_partition_write(crcpart, 0, &stored, sizeof(stored))) lack return-value
checks; modify the code to capture their esp_err_t results, verify they equal
ESP_OK (similar to update_ota_crc()), and handle failures by logging the error
(include the returned error code and context) and returning/aborting
appropriately so failures do not go unnoticed when writing stored to crcpart.

---

Nitpick comments:
In `@wled00/cfg.cpp`:
- Around line 836-838: The call inside the preprocessor block has a 4-space
indent; change it to 2-space indentation to match project style. Locate the
WLED_ENABLE_SAFE_BOOT block around the preprocessor directives and adjust the
line with update_spiffs_crc() to use 2 spaces (e.g., "  update_spiffs_crc();")
and ensure the surrounding "#ifdef WLED_ENABLE_SAFE_BOOT" and "#endif" remain
aligned with file convention.

In `@wled00/file.cpp`:
- Around line 485-487: Fix the indentation and brace placement for the
conditional guarded by WLED_ENABLE_SAFE_BOOT: move the else onto its own
indented line and place the opening brace on the next line, use 2 spaces per
indentation level, and ensure the call to update_spiffs_crc() and the closing
brace are indented accordingly; update the block that references
WLED_ENABLE_SAFE_BOOT and the update_spiffs_crc() call to match WLED coding
style.

In `@wled00/ota_update.cpp`:
- Around line 100-102: The call to update_ota_crc() inside the `#ifdef`
WLED_ENABLE_SAFE_BOOT block has incorrect indentation; adjust it to follow the
project's 2-space-per-indent convention so the line with update_ota_crc(); is
indented to match surrounding code (inside the same block) and keep the
`#ifdef/`#endif aligned with other preprocessor directives; locate the block using
the symbol update_ota_crc and the macro WLED_ENABLE_SAFE_BOOT and correct
spacing only—no logic changes.

In `@wled00/presets.cpp`:
- Around line 118-120: The call to update_spiffs_crc() inside the
WLED_ENABLE_SAFE_BOOT preprocessor block is indented with 4 spaces; adjust it to
2-space indentation to match the project's C++ style guidelines—locate the block
guarded by `#ifdef` WLED_ENABLE_SAFE_BOOT and change the whitespace before
update_spiffs_crc(); to two spaces so it aligns with surrounding code.

In `@wled00/safe_boot_functions.cpp`:
- Around line 19-39: The calc_crc function is exposed as a global symbol but
only used within this translation unit; make it static by adding the static
storage-class specifier to its definition (i.e., change the function signature
for calc_crc to be static uint32_t calc_crc(...)) so the symbol is internal to
the file and allows better optimization and avoids name collisions; update any
internal callers if needed (they should continue to call calc_crc as before).
- Around line 12-17: Update all German inline comments to English: in the
crc_group_t typedef update "Größe SPIFFS / Backup" to "Size SPIFFS / Backup"
(referencing fields crc_app, crc_spiffs, size_spiffs), and update the comments
around the later block where you have "prüfen ob identisch" and "nichts zu tun"
to "check if identical" and "nothing to do" respectively; ensure the replaced
comments preserve original intent and punctuation and are applied where the
CRC/spiffs comparison logic is implemented so all code comments are in English.

In `@wled00/safe_boot_functions.h`:
- Around line 1-4: Fix the typo in the file header comment in
safe_boot_functions.h: change the phrase "Safe Bootbootloader" to "Safe
Bootloader" in the top comment block so the description reads correctly; locate
the comment block at the top of safe_boot_functions.h and update the text
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d44fab80-1d22-43d3-a834-148e35c26da0

📥 Commits

Reviewing files that changed from the base of the PR and between 7e490fc and d1045a3.

⛔ Files ignored due to path filters (1)
  • tools/WLED_ESP32_8MB_SafeBoot.csv is excluded by !**/*.csv
📒 Files selected for processing (6)
  • wled00/cfg.cpp
  • wled00/file.cpp
  • wled00/ota_update.cpp
  • wled00/presets.cpp
  • wled00/safe_boot_functions.cpp
  • wled00/safe_boot_functions.h

Comment on lines +44 to +46
#ifdef WLED_ENABLE_SAFE_BOOT
update_spiffs_crc();
#endif
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Critical performance concern: CRC calculation on every file close.

Calling update_spiffs_crc() after every closeFile() will cause significant performance degradation. Based on the implementation in wled00/safe_boot_functions.cpp:19-39, calc_crc() reads the entire SPIFFS partition in 4 KB chunks. For a typical 1-3 MB SPIFFS partition, this means reading megabytes of flash data on every single file close operation.

This will cause:

  • Visible LED stuttering during file operations
  • Slow preset saves and config writes
  • Potential watchdog timeouts on larger partitions

Consider updating the CRC only at strategic points (config save, preset save completion) rather than on every file close, or deferring CRC updates to idle time.

-  `#ifdef`  WLED_ENABLE_SAFE_BOOT
-    update_spiffs_crc();
-  `#endif`
   doCloseFile = false;
 }

The CRC updates in cfg.cpp, presets.cpp, and copyFile() should be sufficient for Safe Boot validation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically yes, practially it is fast enogh.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wled-install, fair enough — thanks for testing this on actual hardware. The CRC read-and-compute still happens on every close even when no update is needed, but if you've verified it doesn't cause noticeable lag or watchdog issues in practice (likely because ESP32 flash reads at ~40 MHz are fast relative to the partition size), that's a reasonable call to make. Acknowledged!


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5276
File: wled00/file.cpp:0-0
Timestamp: 2026-01-05T18:35:03.147Z
Learning: In WLED's file.cpp, the static File object `f` cannot be nullptr (it's not a pointer). The `if (f)` check only tests whether the file handle is open, and this check is already performed inside the `f.close()` function. Therefore, there is no need for an explicit `if (f)` check before calling `f.close()` - double-checking is redundant.

Learnt from: DedeHai
Repo: wled/WLED PR: 4956
File: wled00/wled_server.cpp:237-262
Timestamp: 2025-11-08T08:25:18.724Z
Learning: In WLED's filesystem code (ESP8266/ESP32), when iterating directory entries with `openNextFile()`, explicit `close()` calls between iterations are not required. The File class assignment operator properly handles closing the previous file handle when reassigning (e.g., `rootfile = rootdir.openNextFile()`). Both explicit and implicit patterns exist in the codebase, but relying on assignment operator cleanup is valid.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:30.955Z
Learning: In WLED code reviews, verify that file operations (especially file.open()) respect LittleFS filename limitations. Assume default WLED configuration with LittleFS default filename limit of 255 bytes. Do not assume extreme configuration values like WLED_MAX_SEGNAME_LEN = 512 which would not be standard configurations.

Learnt from: DedeHai
Repo: wled/WLED PR: 5404
File: wled00/wled_server.cpp:224-224
Timestamp: 2026-03-05T15:48:35.387Z
Learning: In WLED (wled00/wled_server.cpp), `updateFSInfo()` updates `fsBytesUsed`/`fsBytesTotal` which are purely informational ("FYI") display values for the user. No synchronization or locking is required around these calls, even when invoked from async request handlers, because: (1) the values do not need to be 100% accurate, and (2) simultaneous multi-file delete/add operations are extremely improbable on this embedded IoT platform.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:11.994Z
Learning: In WLED code reviews, file operations (especially file.open()) should be checked to ensure they respect LittleFS filename limitations. The default LittleFS filename limit is 255 bytes (LFS_NAME_MAX). Reviews should assume default WLED configuration defines and not extreme edge-case values (e.g., WLED_MAX_SEGNAME_LEN = 512 would not be standard). File paths should be validated to stay within the 255-byte limit.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-08T00:57:36.134Z
Learning: In WLED (wled00/cfg.cpp), `deserializeConfigFromFS()` only sets `needsSave=true` (triggering a write of cfg.json) when usermod settings are present in the JSON (lines 758-761). On a fresh install with no usermods, `needsSave=false` so `serializeConfigToFS()` is never called at first boot. Correct defaults are never written to cfg.json until the user manually saves. Structural fix: set `needsSave = needsSave || !cfgExists` where `cfgExists = WLED_FS.exists(s_cfg_json)` checked before reading the file.

Learnt from: DedeHai
Repo: wled/WLED PR: 4793
File: wled00/file.cpp:481-513
Timestamp: 2025-07-31T18:21:49.868Z
Learning: In WLED, essential configuration files that require backup have short, controlled names (like "/cfg.json", "/presets.json") that are well within a 32-character buffer limit. The file naming is controlled by developers, making buffer overflow in backup filename construction highly unlikely.

Learnt from: blazoncek
Repo: wled/WLED PR: 4667
File: usermods/user_fx/user_fx.cpp:27-30
Timestamp: 2025-04-30T05:41:03.633Z
Learning: In WLED, the Segment::allocateData() method already includes optimization to check if data is allocated and sufficiently sized, handling buffer reuse to reduce memory fragmentation. Adding an external check like `if (SEGENV.data == nullptr && !SEGENV.allocateData(dataSize))` is unnecessary and could be problematic, as it bypasses proper size verification.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-08T00:57:36.134Z
Learning: In WLED (wled00/cfg.cpp), `deserializeConfig()` is called with an empty JsonObject when cfg.json doesn't exist on fresh install. Any value read without the ArduinoJSON `|` fallback operator or CJSON macro will override correct constructor defaults with 0/null. Known affected values: `strip.setTargetFps(hw_led["fps"])` at line 179 (sets FPS to 0/unlimited instead of WLED_FPS=42). Fix: `strip.setTargetFps(hw_led["fps"] | WLED_FPS)`. The gamma issue (gammaCorrectCol/Bri) had the same root cause and was fixed in commit d1d9dec402 (Jan 2026) using inline `| default` fallbacks.

Learnt from: softhack007
Repo: wled/WLED PR: 5048
File: wled00/wled.cpp:385-387
Timestamp: 2026-02-08T19:33:02.648Z
Learning: In WLED ESP32 code, when checking whether USB CDC serial initialization delay is needed, rely on the ARDUINO_USB_CDC_ON_BOOT flag rather than explicit CONFIG_IDF_TARGET checks, because not all development boards for chips with native USB capability (ESP32-C3/C5/C6/S2/S3/H2/P4) actually use the native USB—many have external UART bridge chips instead. The ARDUINO_USB_CDC_ON_BOOT flag correctly indicates the board's actual USB configuration.

Learnt from: softhack007
Repo: wled/WLED PR: 5355
File: wled00/util.cpp:635-638
Timestamp: 2026-02-07T16:06:08.677Z
Learning: PSRAM-related compilation guards should enable PSRAM code only for ESP32 variants that actually include PSRAM: ESP32-C61, ESP32-C5, and ESP32-P4. Exclude ESP32-C3, ESP32-C6, and ESP8266 from these guards. Apply this rule across the codebase (not just wled00/util.cpp) by reviewing and updating PSRAM guards/macros in all relevant files (C/C++ headers and sources).

Learnt from: softhack007
Repo: wled/WLED PR: 4893
File: wled00/set.cpp:95-95
Timestamp: 2026-03-14T20:56:46.543Z
Learning: Guideline: Ensure WiFi hostname is set after WiFi.mode() but before WiFi.begin() to avoid default esp-XXXXXX hostname being used in DHCP. This ordering only takes effect after the STA interface exists (so avoid placing hostname setting before initConnection). In WLED, place the hostname configuration inside initConnection() (after WiFi.disconnect(true) and before WiFi.begin()) rather than in earlier boot code like deserializeConfig(). This rule should be applied in code reviews for WLED’s network initialization paths in wled00/*.cpp, and note that on ESP8266 the ordering is less strict but still desirable for consistency.

Copy link
Member

@softhack007 softhack007 Mar 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider updating the CRC only at strategic points (config save, preset save completion) rather than on every file close, or deferring CRC updates to idle time.

@wled-install I still think this is a valid concern - especially when users have many leds, frequent flash file reads can lead to leds flickering, or effects slowing down randomly. Also there are boards with 16mb flash (9MB LittleFS) or even 32MB flash (24MB LittleFS) where it might actually cause a slow-down.

Not sure if you found all the file closing locations in the code - ledmaps/gapmaps, webserver up/download, and gif player do file access directly, without relying on file.cpp functions.

The first optimization could be to only update CRCs after file write closing, but not on file read closing.

Or - depending on how mission critical these checksums are - create a global flag crc_dirty and delay updating the checksums into the main loop, maybe only every 15 seconds.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@DedeHai
Copy link
Collaborator

DedeHai commented Mar 15, 2026

can you explain what it does and how it has an advantage over the bootloop detector?

@wled-install
Copy link
Contributor Author

wled-install commented Mar 15, 2026

SafeBootloader

Bootloader for the ESP32 that maintains mirrored OTA (ota_0 / ota_1) and SPIFFS partitions and restores them automatically if corruption is detected. It is intended to prevent corruption during operation (not 100%, but minimizes the risk), it does not help when corruption happens during intended flash write operations as it calculates crc from data already written.

How It Works

  • The bootloader provides a simple redundancy mechanism for firmware and filesystem data.
  • SPIFFS Protection: Two SPIFFS partitions are used: spiffs and spiffs_backup. For each partition a CRC value is stored. During boot: If one partition is corrupted → it is automatically restored from the valid mirror.
  • OTA Firmware Protection: Two firmware partitions are used: ota_0 and ota_1 (as usual). After each OTA update: the CRC of the updated partition is calculated the value is stored in a dedicated CRC partition. During boot: The bootloader verifies the CRC of the currently selected OTA partition. If the CRC check fails → the bootloader automatically restores from the other OTA partition. If both ota partitions are valid, but inactive one has different crc, it is overwritten by the active ota partition to have alays two up to date copies. (It prevents of course OTA revert possibility).

Bootloop detector will not react if random changes lead to technically valid (but logiclaly potentially wrong) sw execution. SafeBootloader calculates checksum for entire partitions.

@DedeHai
Copy link
Collaborator

DedeHai commented Mar 15, 2026

SafeBootloader calculates checksum for entire partitions.

I think such a crc check is already built into the bootloader and it will flag a corrupted ota partition as invalid and boot from the other one (if available).
So your tool also checks the FS partition and updates the CRC after each file write?

@wled-install
Copy link
Contributor Author

I think such a crc check is already built into the bootloader

Good point, I checked and WLED bins actually have SHA-256 at the end. I thought they have just standard XOR signature for segments as SHA is actually optionally without secure boot activated. Ok, I can probably can use existing SHA. Mirroring OTA partitions is still useful; alternatively, the user could perform the OTA update twice to have identical software in both partitions, or initially flash the same software into both partitions. Headers are definately protected intrinsically. Partition with ota info data (not the app) is also protected intrinsically (should have two data records).

So your tool also checks the FS partition and updates the CRC after each file write?
Yes, it calculates CRC for the whole spiffs partition after each file write. I think/hope that I found all relevant places in WLED: config, presets, FS format in FS.begin, file copy.

That way I only have to ensure bootloader and partition table integrity by external measures (if will be considered as necessary).

@wled-install wled-install marked this pull request as draft March 15, 2026 14:13
@wled-install
Copy link
Contributor Author

Integrity of OTA partitions are now checked using standard SHA256 included. No need to update something additionally after OTA.

@wled-install wled-install marked this pull request as ready for review March 15, 2026 18:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
wled00/safe_boot_functions.h (2)

2-3: Fix the SafeBootloader comment typo.

The header comment currently says Safe Bootbootloader; please correct the wording for readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.h` around lines 2 - 3, Update the header comment
in safe_boot_functions.h to correct the typo "Safe Bootbootloader" to "Safe
Bootloader" (replace the exact phrase "Safe Bootbootloader") so the top-of-file
description reads correctly; also ensure the surrounding sentence remains
grammatically correct after this replacement.

9-12: Minimize header coupling by moving ESP includes to the .cpp file.

This header only declares update_spiffs_crc(). Keeping ESP-IDF includes in safe_boot_functions.cpp reduces transitive dependencies for all includers.

♻️ Suggested simplification
 `#pragma` once
 
 `#ifdef`  WLED_ENABLE_SAFE_BOOT
-#include <esp_partition.h>
-#include <esp_spi_flash.h>
-#include <rom/crc.h>
-#include <esp_ota_ops.h>
 
 bool update_spiffs_crc();
 
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.h` around lines 9 - 12, The header
safe_boot_functions.h currently pulls in ESP-IDF headers; remove/includes of
esp_partition.h, esp_spi_flash.h, rom/crc.h, and esp_ota_ops.h from the .h and
instead include them only in safe_boot_functions.cpp where update_spiffs_crc()
is defined; keep the header minimal by declaring only the update_spiffs_crc()
prototype (and any standard includes it actually needs), and if any ESP types
are required in the signature, replace them with forward-declared types or
standard types in the header and use the real ESP headers in the .cpp
implementation of update_spiffs_crc().
wled00/safe_boot_functions.cpp (3)

20-21: Replace repeated 4096 literals with named constants.

Using named constants here improves readability and avoids accidental drift between chunk size and erase size usage.

♻️ Suggested cleanup
 `#define` SPIFFS_CRC_MAGIC 0x53504653
+static constexpr size_t kCrcChunkSize = 4096;
+static constexpr size_t kCrcEraseSize = 4096;
@@
-    const size_t bufsize = 4096;
+    const size_t bufsize = kCrcChunkSize;
@@
-    esp_partition_erase_range(crcpart, 0, 4096);
+    esp_partition_erase_range(crcpart, 0, kCrcEraseSize);
Based on learnings: "In WLED code reviews, when code is modified or added, look for 'magic numbers' ... suggest replacing them with defined constants when meaningful constants exist."

Also applies to: 79-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.cpp` around lines 20 - 21, Replace the repeated
4096 magic number with a named constant: declare a single constant (e.g.,
constexpr size_t BOOT_BUF_SIZE = 4096 or constexpr size_t CHUNK_SIZE = 4096)
near the top of safe_boot_functions.cpp and use that constant for the bufsize
declaration and the buf array (uint8_t buf[BOOT_BUF_SIZE]) and anywhere else the
4096 value is used (including the other occurrence around line 79) to keep
chunk/erase sizes consistent and readable; update references to bufsize/buf to
use the new constant name.

40-84: Consider documenting this approach with a short software FMEA.

Given the reliability trade-off discussion in this PR, a compact FMEA in PR notes/docs would make the failure-mode coverage explicit and easier to evaluate.

Based on learnings: "When a PR discussion involves 'is this worth doing?' uncertainty about reliability/safety/data-integrity mechanisms, suggest performing a software FMEA."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.cpp` around lines 40 - 84, Add a short software
FMEA documenting the SPIFFS CRC update approach used by update_spiffs_crc (and
the crc_group_t/CRC partition logic): list likely failure modes (e.g.,
esp_partition_find_first returns null, calc_crc mismatch, esp_partition_write or
erase failures, power loss during update), describe effects (lost/mismatched
CRC, boot failures, corrupted data), assign severity/occurrence/detectability
and resulting risk priority, and note mitigations and detection controls (null
checks and early returns, verify esp_partition_read/write return values,
retries, journaling/atomic swap, CRC verification after write, power-loss
handling) plus residual risk and recommended docs/tests; include this FMEA in
the PR notes/docs so reviewers can evaluate reliability trade-offs.

18-18: Limit calc_crc to translation-unit scope.

calc_crc is a local helper and can be static to avoid unnecessary global symbol exposure.

🔧 Suggested change
-uint32_t calc_crc(const esp_partition_t* part)
+static uint32_t calc_crc(const esp_partition_t* part)
Based on learnings: "In WLED code reviews, when C-style functions or variables are added or modified, check if they are globally visible (not static)... suggest making it static if not intended to be global."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.cpp` at line 18, Make the local helper function
calc_crc have translation-unit scope by marking it static: change the function
declaration/definition of calc_crc in safe_boot_functions.cpp to static uint32_t
calc_crc(const esp_partition_t* part) and ensure there are no external
references expecting a global symbol (if there are, either update callers or
keep it non-static); this confines the symbol to the file and prevents
unnecessary global exposure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/safe_boot_functions.cpp`:
- Around line 12-84: The code in crc_group_t, calc_crc, and update_spiffs_crc
uses 4-space indentation; reformat these blocks to use 2 spaces per indentation
level to match repository C++ style (apply to the whole function bodies and any
nested scopes, including the while loop in calc_crc and the if/else blocks in
update_spiffs_crc), keeping existing line breaks and logic intact and ensuring
alignment for statements like esp_partition_find_first,
esp_partition_read/write, esp_partition_erase_range and the DEBUG_PRINTLN calls.

---

Nitpick comments:
In `@wled00/safe_boot_functions.cpp`:
- Around line 20-21: Replace the repeated 4096 magic number with a named
constant: declare a single constant (e.g., constexpr size_t BOOT_BUF_SIZE = 4096
or constexpr size_t CHUNK_SIZE = 4096) near the top of safe_boot_functions.cpp
and use that constant for the bufsize declaration and the buf array (uint8_t
buf[BOOT_BUF_SIZE]) and anywhere else the 4096 value is used (including the
other occurrence around line 79) to keep chunk/erase sizes consistent and
readable; update references to bufsize/buf to use the new constant name.
- Around line 40-84: Add a short software FMEA documenting the SPIFFS CRC update
approach used by update_spiffs_crc (and the crc_group_t/CRC partition logic):
list likely failure modes (e.g., esp_partition_find_first returns null, calc_crc
mismatch, esp_partition_write or erase failures, power loss during update),
describe effects (lost/mismatched CRC, boot failures, corrupted data), assign
severity/occurrence/detectability and resulting risk priority, and note
mitigations and detection controls (null checks and early returns, verify
esp_partition_read/write return values, retries, journaling/atomic swap, CRC
verification after write, power-loss handling) plus residual risk and
recommended docs/tests; include this FMEA in the PR notes/docs so reviewers can
evaluate reliability trade-offs.
- Line 18: Make the local helper function calc_crc have translation-unit scope
by marking it static: change the function declaration/definition of calc_crc in
safe_boot_functions.cpp to static uint32_t calc_crc(const esp_partition_t* part)
and ensure there are no external references expecting a global symbol (if there
are, either update callers or keep it non-static); this confines the symbol to
the file and prevents unnecessary global exposure.

In `@wled00/safe_boot_functions.h`:
- Around line 2-3: Update the header comment in safe_boot_functions.h to correct
the typo "Safe Bootbootloader" to "Safe Bootloader" (replace the exact phrase
"Safe Bootbootloader") so the top-of-file description reads correctly; also
ensure the surrounding sentence remains grammatically correct after this
replacement.
- Around line 9-12: The header safe_boot_functions.h currently pulls in ESP-IDF
headers; remove/includes of esp_partition.h, esp_spi_flash.h, rom/crc.h, and
esp_ota_ops.h from the .h and instead include them only in
safe_boot_functions.cpp where update_spiffs_crc() is defined; keep the header
minimal by declaring only the update_spiffs_crc() prototype (and any standard
includes it actually needs), and if any ESP types are required in the signature,
replace them with forward-declared types or standard types in the header and use
the real ESP headers in the .cpp implementation of update_spiffs_crc().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e823f8cc-348d-4340-99f1-3bba20c8935d

📥 Commits

Reviewing files that changed from the base of the PR and between d1045a3 and 16ac74d.

📒 Files selected for processing (2)
  • wled00/safe_boot_functions.cpp
  • wled00/safe_boot_functions.h

Comment on lines +12 to +84
typedef struct {
uint32_t magic;
uint32_t crc_spiffs; // CRC SPIFFS / Backup
uint32_t size_spiffs; // Größe SPIFFS / Backup
} crc_group_t;

uint32_t calc_crc(const esp_partition_t* part)
{
const size_t bufsize = 4096;
uint8_t buf[bufsize];

uint32_t crc = 0;
size_t offset = 0;

while (offset < part->size) {
size_t toread = bufsize;
if (offset + toread > part->size)
toread = part->size - offset;

esp_partition_read(part, offset, buf, toread);

crc = crc32_le(crc, buf, toread);
offset += toread;
}

return crc;
}

bool update_spiffs_crc()
{
const esp_partition_t* spiffs =
esp_partition_find_first(
ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_DATA_SPIFFS,
"spiffs");

const esp_partition_t* crcpart =
esp_partition_find_first(
ESP_PARTITION_TYPE_DATA,
static_cast<esp_partition_subtype_t>(0x41), // crc subtype
"crc0");

if (!spiffs || !crcpart) {
DEBUG_PRINTLN(F("SPIFFS or CRC partition not found!"));
return false;
}


uint32_t crc = calc_crc(spiffs);

crc_group_t stored = {};
esp_partition_read(crcpart, 0, &stored, sizeof(stored));

// prüfen ob identisch
if (stored.magic == SPIFFS_CRC_MAGIC &&
stored.crc_spiffs == crc &&
stored.size_spiffs == spiffs->size)
{
// nichts zu tun
DEBUG_PRINTLN(F("SPIFFS CRC matches stored value, no update needed."));
return true;
}

stored.magic = SPIFFS_CRC_MAGIC;
stored.crc_spiffs = crc;
stored.size_spiffs = spiffs->size;
DEBUG_PRINTLN(F("Updating SPIFFS CRC partition with new values."));
esp_partition_erase_range(crcpart, 0, 4096);
esp_partition_write(crcpart, 0, &stored, sizeof(stored));
DEBUG_PRINTLN(F("SPIFFS CRC partition updated."));

return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Indentation does not follow the repository C++ style rule.

Line 12 onward uses 4-space indentation in nested scopes; this repository guideline requires 2 spaces per level.

As per coding guidelines **/*.{cpp,h,c}: Use spaces (2 per level) for indentation in C++ files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/safe_boot_functions.cpp` around lines 12 - 84, The code in
crc_group_t, calc_crc, and update_spiffs_crc uses 4-space indentation; reformat
these blocks to use 2 spaces per indentation level to match repository C++ style
(apply to the whole function bodies and any nested scopes, including the while
loop in calc_crc and the if/else blocks in update_spiffs_crc), keeping existing
line breaks and logic intact and ensuring alignment for statements like
esp_partition_find_first, esp_partition_read/write, esp_partition_erase_range
and the DEBUG_PRINTLN calls.

crc_group_t stored = {};
esp_partition_read(crcpart, 0, &stored, sizeof(stored));

// prüfen ob identisch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: please translate german comments into english

stored.crc_spiffs == crc &&
stored.size_spiffs == spiffs->size)
{
// nichts zu tun
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same minor comment here: german -> english please

@DedeHai
Copy link
Collaborator

DedeHai commented Mar 16, 2026

I still fail to see what the code is trying to achieve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants