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
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
This is both a feature and an optimization.
Feature:
Adjustable size of the internal storage in esp_event queue, currently
used by ISR posting, as they wont be able to make a malloc.
Optimization:
When non-isr is posting an event, use the inernal storage in the struct
instead of always allocating a new heap for the data. Most events in
esp-idf only contains a few bytes event information, and we have that
allocation payed for anyway.
This solved in a big part our memory fragmentation issue, as events
happens freqvently and used to create small memory allocations for just
4 bytes, and then in the event handler we usually allocated a bigger
chunk of heap for our feature. When returning from the event handler,
the 4 byte allocation was freed, leaving a hole in the heap.
Merges: https://github.com/espressif/esp-idf/pull/17797
when esp_event_handler_unregister_with_internal cannot take
the loop mutex (e.g., when the handler unregisters itself),
create an event with a special base identifier and add it to
the queue of the corresponding loop to postpone the removal
of the handler from the list at a time when the loop mutex can be
successfully taken.
handler_execute function is looking to match the handler only in the
list of loop events but does not look in the base event handler list
nor the id event handler list. So unless the event handler is
registered to be triggered for all event bases and all event ids of
an event loop, its profiling fields (invoked and time) are not updated
when it is called.
This commit updates the search for the matching handler to also look
in base event list and ID event list.
Closes https://github.com/espressif/esp-idf/issues/15041
* Decomposed tests into atomic unit tests
* Made tests less time-dependent, hence more robust
on different platforms (ESP32, QEMU, Linux)
* Ported most of the tests to linux
* Removed some redundant tests
* Fixed bug the tests discovered
* Simplified parts of the tests to be more clear
* Partially used C++ to simplify setup/teardown
* Unified setup/teardown in general
* Description of unregistering was incorrect
* Made clear that event loop arg mustn't be NULL
* Added parameter check in create function
Closes https://github.com/espressif/esp-idf/issues/6761
Closes IDFGH-4969
When a handler instance is the last one in the list und unregisters
itself, the handler iterator will be invalidated by entering free'd
memory. Same applies for event base and id, if they become empty.
Merges https://github.com/espressif/esp-idf/pull/4139
Fixes an issue with post instance data preparation. Currently, there is
no way to check if event data has really been set during handler
execution preparation. When data is not allocated from the heap, user
could have passed 0x0 which can lead to failed checks.
This also implements using the already allocated data memory for posting
events from non-ISR functions when data size is less than the capacity.