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:
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¶
- Receive - the firmware accumulates the
[32B HMAC][ELF]blob from one or moremod_chunkC2 messages into a staging buffer. - Verify (
espm_verify_blob) - validate the HMAC-SHA256 signature and extract the ELF start and size. - Validate (
espm_validate) - parse the ELF header, sections, symbols, and relocations; reject anything malformed or over the size limits. - Allocate (
espm_load) - reserve IRAM/PSRAM for the.textand.data/.bsssections, or fail if there is not enough. - Relocate (
espm_reloc_apply, architecture-specific) - patch symbol references in code and data. See Building for the relocation types per architecture. - Register (
espm_registry_load) - create the FreeRTOS task, callespm_initonce, and add the module to the registry. - Execute -
espm_execruns as the module's main task; long-running modules block ontask_notify_wait().
Running¶
espm_init(arg)runs once at load. Register C2 commands here withcmd_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:
- Command handlers receive an
espm_cmd_ctx_t(args,request_id,module_ctx) and must callmsg_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¶
- Stop -
mod_stop(or a watchdog timeout) callsespm_stopso the module can release sockets and other resources. - Purge - the task is deleted, the watchdog unregistered, and registered commands removed.
- 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.