From c24feb771b4bf8478393c92a9ce13266bc7dea90 Mon Sep 17 00:00:00 2001 From: Konstantin Kondrashov Date: Tue, 21 Jul 2026 15:14:18 +0300 Subject: [PATCH] fix(esp_event): free queued legacy cleanup ctx on loop delete 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. --- components/esp_event/esp_event.c | 6 +++++ .../test_apps/main/test_event_common.cpp | 26 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/components/esp_event/esp_event.c b/components/esp_event/esp_event.c index 847c91565ba..594679f3438 100644 --- a/components/esp_event/esp_event.c +++ b/components/esp_event/esp_event.c @@ -830,6 +830,12 @@ esp_err_t esp_event_loop_delete(esp_event_loop_handle_t event_loop) // Drop existing posts on the queue esp_event_post_instance_t post; while (xQueueReceive(loop->queue, &post, 0) == pdTRUE) { + if (post.base == esp_event_handler_cleanup) { + esp_event_remove_handler_context_t* ctx = (esp_event_remove_handler_context_t*)post.data.ptr; + if (ctx->legacy) { + free(ctx->handler_ctx); + } + } post_instance_delete(&post); } diff --git a/components/esp_event/test_apps/main/test_event_common.cpp b/components/esp_event/test_apps/main/test_event_common.cpp index cc3d665ba4a..da509cf1ceb 100644 --- a/components/esp_event/test_apps/main/test_event_common.cpp +++ b/components/esp_event/test_apps/main/test_event_common.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -183,6 +183,30 @@ TEST_CASE("registering event twice with same handler yields updated handler arg" TEST_ASSERT_EQUAL(1, count_second); } +static void self_unregister_legacy_handler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + esp_event_loop_handle_t loop = (esp_event_loop_handle_t) event_handler_arg; + TEST_ESP_OK(esp_event_handler_unregister_with(loop, event_base, event_id, self_unregister_legacy_handler)); +} + +/* A legacy handler unregistering itself defers removal via an internal cleanup event + * left on the queue. Deleting the loop must drain it without leaking. */ +TEST_CASE("deleting loop with queued legacy cleanup event does not leak", "[event][linux]") +{ + EV_LoopFix loop_fix; + + TEST_ESP_OK(esp_event_handler_register_with(loop_fix.loop, + s_test_base1, + TEST_EVENT_BASE1_EV1, + self_unregister_legacy_handler, + loop_fix.loop)); + + TEST_ESP_OK(esp_event_post_to(loop_fix.loop, s_test_base1, TEST_EVENT_BASE1_EV1, NULL, 0, portMAX_DELAY)); + + /* ZERO_DELAY runs a single event, leaving the deferred cleanup event queued. */ + TEST_ESP_OK(esp_event_loop_run(loop_fix.loop, ZERO_DELAY)); +} + TEST_CASE("registering event handler instance twice works", "[event][linux]") { EV_LoopFix loop_fix;