From 295e8563eb23bd59564675cf30ac9339dfb37550 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 17:27:25 -0500 Subject: [PATCH] [core] Fix null deref in WarnIfComponentBlockingGuard for self-keyed timers Self-keyed scheduler items (Scheduler::set_timeout(self, ...) and set_interval(self, ...)) intentionally store component == nullptr. When USE_RUNTIME_STATS is enabled, WarnIfComponentBlockingGuard::finish() dereferenced component_ to call runtime_stats_.record_time(), causing a load access fault on RISC-V (e.g. ESP32-C3) when these timers fire. Skip the per-component bookkeeping when component_ is null, but still accumulate into ComponentRuntimeStats::global_recorded_us so Application::loop() overhead accounting (which subtracts scheduled callback time from before_loop_tasks_) stays accurate. --- esphome/core/component.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 6afcfda41d..185d51ab37 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -655,7 +655,15 @@ class WarnIfComponentBlockingGuard { // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { #ifdef USE_RUNTIME_STATS - this->component_->runtime_stats_.record_time(micros() - this->started_us_); + uint32_t elapsed_us = micros() - this->started_us_; + // component_ is nullptr for self-keyed scheduler items (set_timeout/set_interval(self, ...)) + if (this->component_ != nullptr) { + this->component_->runtime_stats_.record_time(elapsed_us); + } else { + // Still accumulate into the global counter so Application::loop() can subtract + // this time from before_loop_tasks_ wall time. + ComponentRuntimeStats::global_recorded_us += elapsed_us; + } #endif uint32_t curr_time = MillisInternal::get(); #ifndef USE_BENCHMARK