- Added macros for registering handlers to run before the scheduler and app main.
- Updated various components to utilize the new registration system for initialization.
- Refactored app startup logic to streamline initialization sequence.
When a loop is deleted while an internal legacy "cleanup" event is still
queued (posted by a deferred self-unregistration from within a handler),
esp_event_loop_delete() drained the queue but only freed the post payload,
leaking the heap copy of the handler context allocated for the legacy path.
Free ctx->handler_ctx for queued legacy cleanup events while draining the
queue, mirroring the cleanup done in esp_event_loop_run().
Add a regression test that leaves a legacy cleanup event queued and asserts
no memory is leaked on loop deletion.
When esp_event_loop_run() exited via the ticks-expired break path,
loop->running_task was left pointing to the current task handle.
Any subsequent trylock in esp_event_handler_unregister_with_internal()
would see a stale non-NULL running_task and take the wrong code path.
esp_event_post_to() could access loop->queue / loop->mutex after
esp_event_loop_delete() freed them when both ran concurrently.
Introduce esp_event_loop_state_t with:
- posts_in_flight: reference-count incremented atomically (under
state.lock spinlock) before touching any loop resources, decremented
on every exit path via goto on_err.
- deleting: atomic_bool set by esp_event_loop_delete() to block new
posts from entering the critical section.
esp_event_loop_delete() sets deleting=true, then busy-waits (releasing
and re-acquiring loop->mutex each tick) until posts_in_flight reaches
zero before proceeding with teardown.
esp_event_isr_post_to() performs a lock-free atomic_load of deleting as
a best-effort guard; ISR context cannot participate in the spinlock
protocol but the window is documented and accepted.
After processing an esp_event_handler_cleanup sentinel, execution fell
through into the regular dispatch block. Every loop-level (ANY_BASE/
ANY_ID) handler was invoked with base="cleanup" and event_data pointing
at the internal esp_event_remove_handler_context_t struct.
Consequences:
- Information disclosure: internal handler addresses and loop instance
pointer are exposed to every loop-level handler.
- UAF: if a handler stores event_data for later use, post_instance_delete
frees the ctx, turning the stored pointer into a dangling reference.
- Logic corruption: handlers that switch on base with a default branch
misbehave on every unregister anywhere in the system.
Fix: wrap the regular dispatch block in an else clause so it is skipped
entirely for cleanup events. post_instance_delete, ticks accounting, and
xSemaphoreGiveRecursive remain in the shared tail executed for both paths.
Closes SEC_221
1) loop->mutex is created with xSemaphoreCreateRecursiveMutex(). FreeRTOS
requires that recursive mutexes are only acquired and released with
xSemaphoreTakeRecursive / xSemaphoreGiveRecursive.
esp_event_handler_unregister_with_internal() used the non-recursive
xSemaphoreTake(loop->mutex, 0) / xSemaphoreGive(loop->mutex) in the fast
path. The non-recursive Take bypasses uxRecursiveCallCount bookkeeping;
if the same task subsequently takes the mutex recursively (e.g. re-entry
from a handler or a follow-up register), the call count drifts. The
non-recursive Give then unconditionally drops the holder, allowing another
task to acquire the mutex while the original task still believes it holds
the lock — a full lock violation on the handler list leading to UAF and
potential RCE on attacker-driven event floods.
Fix: replace xSemaphoreTake/xSemaphoreGive with the Recursive variants in
the fast (try-take with timeout 0) path of unregister_with_internal.
2) avoid use-after-free when unregistering handler from a callback
The recursive try-lock introduced in SEC-220 succeeds re-entrantly when a
handler unregisters itself from within its own callback, causing the handler
node to be freed immediately while the dispatch loop still writes profiling
counters to it after the callback returns. Route the in-callback case to the
deferred cleanup path and only free directly once no dispatch is active.
Closes SEC_220
esp_event_is_handler_registered() walked loop_nodes, base_nodes, id_nodes
and handler lists with no lock held, then released an unowned mutex at the
'out:' label via xSemaphoreGive().
Concurrent register/unregister/delete operations can free handler nodes
during the unlocked walk (SLIST UAF). The xSemaphoreGive on an unowned
recursive mutex corrupts the recursive call-count of any task that
legitimately holds the mutex.
Fix:
- Take loop->mutex with xSemaphoreTakeRecursive before the traversal.
- Replace xSemaphoreGive at the 'out:' label with xSemaphoreGiveRecursive
so every exit path holds the mutex for exactly one balanced take/give.
Closes SEC_219
heap_caps_calloc_base() calls heap_caps_malloc_base() from the same
translation unit (heap_caps_base.c). GNU ld --wrap only redirects
undefined references, so that intra-object call binds to the real
heap_caps_malloc_base and never enters __wrap_heap_caps_malloc_base.
As a result, allocations made through heap_caps_calloc() were never
recorded by heap tracing, silently hiding potentially large INTERNAL
leaks (e.g. mbedtls SSL buffers via MALLOC_CAP_INTERNAL).
Add heap_caps_calloc_base to the --wrap list and implement
__wrap_heap_caps_calloc_base, which records the allocation via a
noinline trace_calloc helper (mirroring trace_malloc so the recorded
call stack depth stays consistent) and calls __real_heap_caps_calloc_base.
The inner malloc_base call remains same-TU and unwrapped, so each
calloc produces exactly one trace record (no double counting).
The removed function `esp_bootloader_get_description` never worked in the app build.
It can be used only in the bootloader build.
To read the bootloader description from app, there is another function
`esp_ota_get_bootloader_description`.
fprintf(file, buf) is a format-string sink: if any registered event base
or handler name contains "%", fprintf interprets it as a format directive,
causing an information leak or crash.
Replace with fprintf(file, "%s", buf) so the buffer is always treated as
plain text regardless of its content.
Closes SEC_064
When the REPL input backend is unavailable, linenoise() may return NULL
immediately. esp_console_repl_task() currently retries without yielding,
which can make the console_repl task spin continuously.
For USB Serial/JTAG console this can happen when the REPL is started
without an attached USB host. Since the REPL task has priority 2, this
can starve lower-priority tasks, prevent app_main from continuing after
esp_console_start_repl(), and trigger the task watchdog due to IDLE not
running.
Add a small delay before retrying when linenoise() returns NULL.
Closes https://github.com/espressif/esp-idf/issues/18485
The Xtensa load/store handler did not properly handle 8/16-bit
memory access to IRAM regions configured with MALLOC_CAP_IRAM_8BIT
(and CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY=y) from a loop
(LBEG/LEND/LCOUNT) context. This caused the loop to exit after
the first access, instead of continuing to iterate as intended.
Closes https://github.com/espressif/esp-idf/issues/14127
Where esp_rom is used as depends_components:
- components/esp_rom/test_apps/linux_rom_apis
- components/esp_rom/test_apps/rom_impl_components
- components/esp_rom/test_apps/rom_tests
Where esp_event is used as depends_components:
- components/esp_event/host_test
- components/esp_event/test_apps
- examples/system/esp_event/default_event_loop
- examples/system/esp_event/user_event_loops
Where efuse is used as depends_components:
- to test efuse APIs:
- components/efuse/test_apps
- examples/system/efuse
- to test secuity APIs:
- components/hal/test_apps/crypto
- to test adc calib APIs:
- components/esp_adc/test_apps/adc
- examples/peripherals/adc/continuous_read
- examples/peripherals/adc/oneshot_read
When the eFuse field consists of single descriptor, its size can be
taken from struct member 'bit_count'. However, when the field consists of
multiple descriptors, the size should be obtained via esp_efuse_get_field_size()
For bin log, reserve the first 4 bytes as zero for variables that are pointed to NULL
and should not be printed in the log. So the esp-idf-monitor will skip printing
those variables.