diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 5e313f770..f84495950 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -744,6 +744,10 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, : (a->next_execution_high_ > b->next_execution_high_); } +// Recycle a SchedulerItem back to the pool for reuse. +// IMPORTANT: Caller must hold the scheduler lock before calling this function. +// This protects scheduler_item_pool_ from concurrent access by other threads +// that may be acquiring items from the pool in set_timer_common_(). void Scheduler::recycle_item_main_loop_(std::unique_ptr item) { if (!item) return; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index dcf418c14..5bf3d19ad 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -275,6 +275,7 @@ class Scheduler { // Helper to recycle a SchedulerItem back to the pool. // IMPORTANT: Only call from main loop context! Recycling clears the callback, // so calling from another thread while the callback is executing causes use-after-free. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. void recycle_item_main_loop_(std::unique_ptr item); // Helper to perform full cleanup when too many items are cancelled @@ -331,7 +332,10 @@ class Scheduler { now = this->execute_item_(item.get(), now); } // Recycle the defer item after execution - this->recycle_item_main_loop_(std::move(item)); + { + LockGuard lock(this->lock_); + this->recycle_item_main_loop_(std::move(item)); + } } // If we've consumed all items up to the snapshot point, clean up the dead space