Skip to content

Module Lifecycle

A module moves through a well-defined state machine from the moment its bytes arrive until its memory is wiped. Every module exports a single entry point that ESPM calls with different command codes:

int module_entry(espm_sys_t *sys, int cmd, void *arg);

cmd is one of ESPM_CMD_INIT (0), ESPM_CMD_EXEC (1), or ESPM_CMD_STOP (2). The convenience header espm.h provides module_entry for you and dispatches to weak espm_init / espm_exec / espm_stop callbacks.

States

typedef enum {
    ESPM_STATE_EMPTY   = 0,   // slot unused
    ESPM_STATE_LOADED  = 1,   // ELF relocated, awaiting EXEC
    ESPM_STATE_RUNNING = 2,   // task executing
    ESPM_STATE_STOPPED = 3,   // espm_stop called, awaiting cleanup
} espm_state_t;
stateDiagram-v2
    [*] --> Receiving: mod_chunk x N
    Receiving --> Verifying: mod_load
    Verifying --> Validating: HMAC OK
    Validating --> Relocating: ELF OK
    Relocating --> Loaded: pages mapped
    Loaded --> Running: espm_init then task start
    Running --> Stopped: mod_stop
    Running --> Stopped: watchdog timeout
    Running --> Stopped: panic (Xtensa)
    Stopped --> [*]: zeroize then free

Load sequence

  1. Receive - the firmware accumulates the [32B HMAC][ELF] blob from one or more mod_chunk C2 messages into a staging buffer.
  2. Verify (espm_verify_blob) - validate the HMAC-SHA256 signature and extract the ELF start and size.
  3. Validate (espm_validate) - parse the ELF header, sections, symbols, and relocations; reject anything malformed or over the size limits.
  4. Allocate (espm_load) - reserve IRAM/PSRAM for the .text and .data/.bss sections, or fail if there is not enough.
  5. Relocate (espm_reloc_apply, architecture-specific) - patch symbol references in code and data. See Building for the relocation types per architecture.
  6. Register (espm_registry_load) - create the FreeRTOS task, call espm_init once, and add the module to the registry.
  7. Execute - espm_exec runs as the module's main task; long-running modules block on task_notify_wait().

Running

  • espm_init(arg) runs once at load. Register C2 commands here with cmd_register(name, handler, ctx) (up to ~16 per module).
  • espm_exec() is the main loop, in its own task. It should feed the watchdog and wait for a stop notification:
void espm_exec(void) {
    while (!task_notify_wait(5000)) {
        watchdog_feed();
    }
}
  • Command handlers receive an espm_cmd_ctx_t (args, request_id, module_ctx) and must call msg_result(...) exactly once to signal completion.

Watchdog

Each module has an independent watchdog, default timeout 30 s, polled every 500 ms. A module keeps itself alive by calling watchdog_feed(). On timeout, ESPM kills the task, calls espm_stop, zeroizes the module memory, and purges its registry entry.

Panic isolation

On Xtensa (ESP32, ESP32-S3) a module-aware exception handler checks whether the faulting program counter lies inside any module's .text region (espm_registry_find_by_pc). If so, only that module is purged and the device keeps running. If the fault is in firmware, the device restarts.

On RISC-V (ESP32-C6, ESP32-C3) there is no hardware exception interception: an unhandled module fault reboots the device.

Unload and zeroization

  1. Stop - mod_stop (or a watchdog timeout) calls espm_stop so the module can release sockets and other resources.
  2. Purge - the task is deleted, the watchdog unregistered, and registered commands removed.
  3. Zeroize - the module's IRAM/PSRAM is overwritten with zeros before being freed, so no payload bytes linger.

Persistence (optional)

When built with CONFIG_ESPM_PERSIST_ENABLE, a module blob can be saved to encrypted NVS (espm_persist_save) and auto-loaded on the next boot (espm_persist_autoload), up to 8 entries. This is the only way a module survives a power cycle.

See also