Skip to content

fix(firmware): raise the httpd stack for OTA image validation - #1831

Open
clonea1 wants to merge 3 commits into
ruvnet:mainfrom
clonea1:contrib/rollback
Open

fix(firmware): raise the httpd stack for OTA image validation#1831
clonea1 wants to merge 3 commits into
ruvnet:mainfrom
clonea1:contrib/rollback

Conversation

@clonea1

@clonea1 clonea1 commented Sep 4, 2026

Copy link
Copy Markdown

Adopted from upstream PR #1594 (Juan Kuscevic, open since 2026-08-11).

The OTA upload handler runs esp_ota_end() -> esp_image_verify() on the httpd
task's own stack, and HTTPD_DEFAULT_CONFIG allocates 4 KB. That overflows at
the very end of an upload: the transfer completes, validation panics, and the
node reboots into the old image. The symptom reads as 'the update did not
take' rather than as a crash, which makes it hard to attribute.

12 KB costs one page of RAM.


Stacked on #1827 (remote-config) — this branch carries that commit too, so review only the last two here, or merge #1827 first and this shrinks.

Rebased onto current main; one additive conflict in main/CMakeLists.txt (main gained thermal.c, this adds config_api.c) resolved by keeping both. Firmware builds clean for esp32c6 on ESP-IDF v5.4.

Joe and others added 3 commits September 4, 2026 16:40
Changing any node setting meant a USB cable and physical access. On a fleet
mounted on walls and ceilings that is the difference between a five-second
change and taking a board down.

Adds an authenticated HTTP config endpoint on each node. Settings are typed and
range-checked at the boundary, persisted to NVS, and applied without a reflash.
Includes LED mode and brightness: the status LED is bright enough to be
unwelcome in a bedroom, and that is a configuration question, not a reflash.

TRIAL AND REVERT. A setting that breaks connectivity would otherwise brick a
node remotely -- change the WiFi password and the node is simply gone. So a
change that could sever the uplink is applied on trial: the old values are
banked, the node reboots, and the change is confirmed only once the node
reassociates and gets an address. If it does not within the deadline, the node
restores the banked values and comes back on the old settings. Verified on
hardware with a deliberately wrong password: recovered on its own in 60.5 s.

The reply to a trial push is sent before the reboot rather than after it, so
the caller learns the trial was accepted instead of seeing a dropped
connection and having to guess.

Mutating requests require a pre-shared key checked in constant time, and the
endpoint FAILS CLOSED when no key is provisioned. The key is read from a file
path given at build time with no default, so no personal path is baked into a
published tree.

Co-Authored-By: claude-flow <ruv@ruv.net>
Config changes already self-heal: a node given credentials it cannot associate
with banks the old ones, reboots, and restores them. Firmware had no such
protection -- a bad image meant a boot loop and a USB cable, which for nodes
mounted around a house is the difference between a mistake and an afternoon.

CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE makes a freshly-OTA'd image boot as
PENDING_VERIFY; if it reboots without confirming, the bootloader reverts.

WHEN to confirm is the whole design. Confirming in app_main() would make the
mechanism decorative, since any image that starts would qualify. The failure
that actually costs someone a ladder is a node that cannot be REACHED, and
unreachable means no WiFi -- so the criterion is: obtained an IP, then stayed
up for a 60 s soak. The soak also catches an image that associates and then
crashes, which confirming on IP alone would miss. It shares the
IP_EVENT_STA_GOT_IP hook with the config trial: both mechanisms key off the
same evidence that the node can still talk.

Deliberately conservative -- a reboot for any other reason inside the soak
rolls back a good image. That errs toward a node that works over a node that
is new.

A silent revert is nearly useless operationally: a node that reappears on its
old firmware looks identical to one whose update never arrived. So the boot
check also detects an already-reverted slot and reports the failed partition,
its version and the reset reason, in the log, in NVS so a late poller still
sees it, and on /ota/status as `last_rollback` alongside `pending_verify`.

MEASURED on node 3 (esp32c6), not inferred:

  happy path -- OTA'd a good image, it booted PENDING_VERIFY, got an IP,
  soaked, confirmed; a subsequent reboot stayed on ota_1, which only happens
  if confirmation actually occurred.

  failure path -- OTA'd an image built to abort() 15 s after boot, inside the
  soak window:

    ota_update: OTA update successful! Rebooting to partition 'ota_0'...
    ota_update: new image on trial (ota_0): must reach the network and
                survive 60 s or the bootloader reverts
    main: Got IP: <node-ip>
    ota_update: image on trial reached the network; confirming in 60 s
    abort() was called at PC 0x4201ec43 on core 0
    ota_update: FIRMWARE ROLLBACK: ota_0 image 0.8.8 was aborted;
                recovered via panic
    main: Got IP: <node-ip>

  and /ota/status then returns
    "last_rollback": "ota_0 image 0.8.8 was aborted; recovered via panic"

The node recovered with no USB intervention. The panic used to produce the bad
image was a throwaway and is not in this commit.

Co-Authored-By: claude-flow <ruv@ruv.net>
Adopted from upstream PR ruvnet#1594 (Juan Kuscevic, open since 2026-08-11).

The OTA upload handler runs esp_ota_end() -> esp_image_verify() on the httpd
task's own stack, and HTTPD_DEFAULT_CONFIG allocates 4 KB. That overflows at
the very end of an upload: the transfer completes, validation panics, and the
node reboots into the old image. The symptom reads as 'the update did not
take' rather than as a crash, which makes it hard to attribute.

12 KB costs one page of RAM.

Co-Authored-By: claude-flow <ruv@ruv.net>
(cherry picked from commit cbfecfdb4b20dd37b40ab7d43f87504cad39024e)
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.

1 participant