fix(core): fix power manager rtc event scheduling.
What changed, and why it matters
This commit fixes how the Trezor hardware wallet schedules its automatic deep-sleep (hibernate) timer on STM32U5 devices. It replaces a simpler one-shot RTC timer with a new RTC scheduler that tracks event IDs, and it now properly cancels pending hibernate events when the device wakes up. The change appears to be a bug-fix for power management reliability rather than a security patch, but a mis-scheduled hibernate event could theoretically affect device availability or battery life.
Treat as a routine firmware bug-fix. Review the new rtc_scheduler API for correct event ID lifecycle (cancel-on-wake, no leaks, no race between schedule and callback). Validate that hibernate still occurs after the intended timeout and that wake-up events are not left pending after resume. No urgent security response is indicated by the diff alone.
Security signals we found
Change in power-state transition logic that could affect availability or safe-shutdown behavior
Introduction of event ID tracking to prevent duplicate or stale RTC wake-up events
Removal of unconditional rtc_wakeup_timer_stop() on resume, replaced by explicit cancellation in pm_suspend()
No explicit security-relevance stated by vendor in commit message or title
Evidence from the diff
The patch refactors power_manager RTC wake-up handling on stm32u5. It moves from rtc_wakeup_timer_start()/rtc_wakeup_timer_stop() to a new rtc_schedule_wakeup_event()/rtc_cancel_wakeup_event() API, storing an autohibernate_event_id in the pm_driver_t state. The callback now clears that event ID, and pm_suspend() cancels any pending autohibernate event after system_suspend() returns. The old code computed time_to_hibernate relative to last_active_timestamp and started a relative timer; the new code schedules an absolute RTC event at suspend_timestamp + PM_AUTO_HIBERNATE_TIMEOUT_S and guards against duplicate scheduling. Comments about BLE were generalized. No direct memory corruption, authentication bypass, or cryptographic weakness is visible in the diff.
Changed components
core/embed/sys/power_manager/stm32u5/power_manager.ccore/embed/sys/power_manager/stm32u5/power_manager_internal.hcore/embed/sys/power_manager/stm32u5/power_monitoring.csys/rtc_scheduler (new dependency)Inspect captured patch +39 / −24
diff --git a/core/embed/sys/power_manager/stm32u5/power_manager.c b/core/embed/sys/power_manager/stm32u5/power_manager.c
index 39df59e0c..4e601867c 100644
--- a/core/embed/sys/power_manager/stm32u5/power_manager.c
+++ b/core/embed/sys/power_manager/stm32u5/power_manager.c
@@ -29,6 +29,7 @@
#ifdef USE_RTC
#include <sys/rtc.h>
+#include <sys/rtc_scheduler.h>
#endif
#include "../fuel_gauge/battery_model.h"
@@ -237,14 +238,12 @@ pm_status_t pm_get_state(pm_state_t* state) {
// This callback is called from inside the system_suspend() function
// when the rtc wake-up timer expires.
-// - The callback can schedule the next wake-up by calling
-// rtc_wakeup_timer_start().
-// - If the callback return with wakeup_flags set, system_suspend() returns.
#ifdef USE_RTC
void pm_rtc_wakeup_callback(void* context) {
- // No need to do anything here, but left as a placeholder for potential
- // future use. This is an optimal place where to set a wakeup flag from RTC
- // wakeup_flags_set(WAKEUP_FLAG_RTC);
+ pm_driver_t* drv = &g_pm;
+
+ // Clear autohibernate event reference
+ drv->autohibernate_event_id = 0;
}
#endif
@@ -282,6 +281,14 @@ pm_status_t pm_suspend(wakeup_flags_t* wakeup_reason) {
wakeup_flags_t wakeup_flags = system_suspend();
+#ifdef USE_RTC
+ // Cancel autohibernate event if scheduled
+ if (drv->autohibernate_event_id != 0) {
+ rtc_cancel_wakeup_event(drv->autohibernate_event_id);
+ drv->autohibernate_event_id = 0;
+ }
+#endif
+
// Wait for pmic measurements to stabilize the fuel gauge estimation.
pm_status_t status = pm_wait_to_stabilize(drv, PM_STABILIZATION_TIMEOUT_MS);
if (status != PM_OK) {
@@ -559,9 +566,11 @@ bool pm_driver_suspend(void) {
// Driver just woke up from suspend and have no data available yet.
// Request the suspend but wait for the next pmic_meausrement
drv->suspending = true;
-
} else {
+#ifdef USE_RTC
+ // Schedule auto-hibernation rtc event
pm_schedule_rtc_wakeup();
+#endif
drv->suspended = true;
}
@@ -573,6 +582,8 @@ bool pm_driver_suspend(void) {
return true;
}
+#ifdef USE_RTC
+
bool pm_schedule_rtc_wakeup(void) {
pm_driver_t* drv = &g_pm;
@@ -580,8 +591,6 @@ bool pm_schedule_rtc_wakeup(void) {
return false;
}
-#ifdef USE_RTC
-
// Capture the timestamp when device was active for the last time.
if (!rtc_get_timestamp(&drv->last_active_timestamp)) {
return false;
@@ -594,20 +603,17 @@ bool pm_schedule_rtc_wakeup(void) {
pm_hibernate();
}
- uint32_t time_to_hibernate =
- PM_AUTO_HIBERNATE_TIMEOUT_S -
- (drv->last_active_timestamp - drv->suspend_timestamp);
-
- rtc_wakeup_timer_start(time_to_hibernate, pm_rtc_wakeup_callback, NULL);
-
-#endif
-
- systimer_delete(drv->monitoring_timer);
+ if (drv->autohibernate_event_id == 0) {
+ rtc_schedule_wakeup_event(
+ drv->suspend_timestamp + PM_AUTO_HIBERNATE_TIMEOUT_S,
+ pm_rtc_wakeup_callback, NULL, &drv->autohibernate_event_id);
+ }
- drv->suspended = true;
return true;
}
+#endif
+
bool pm_is_charging(void) {
pm_driver_t* drv = &g_pm;
@@ -661,7 +667,6 @@ bool pm_driver_resume(void) {
drv->state_machine_stabilized = false;
#ifdef USE_RTC
- rtc_wakeup_timer_stop();
uint32_t rtc_timestamp;
rtc_get_timestamp(&rtc_timestamp);
diff --git a/core/embed/sys/power_manager/stm32u5/power_manager_internal.h b/core/embed/sys/power_manager/stm32u5/power_manager_internal.h
index 9cb6afd34..e079a7c34 100644
--- a/core/embed/sys/power_manager/stm32u5/power_manager_internal.h
+++ b/core/embed/sys/power_manager/stm32u5/power_manager_internal.h
@@ -19,10 +19,12 @@
#pragma once
+#include <trezor_types.h>
+
#include <sys/pmic.h>
#include <sys/power_manager.h>
+#include <sys/rtc_scheduler.h>
#include <sys/systimer.h>
-#include <trezor_types.h>
#include "../fuel_gauge/fuel_gauge.h"
#include "../stwlc38/stwlc38.h"
@@ -135,6 +137,7 @@ typedef struct {
uint32_t suspend_timestamp;
uint32_t last_active_timestamp;
uint32_t time_in_suspend_s;
+ rtc_event_id_t autohibernate_event_id;
} pm_driver_t;
diff --git a/core/embed/sys/power_manager/stm32u5/power_monitoring.c b/core/embed/sys/power_manager/stm32u5/power_monitoring.c
index 24ab5dee3..f708c80e6 100644
--- a/core/embed/sys/power_manager/stm32u5/power_monitoring.c
+++ b/core/embed/sys/power_manager/stm32u5/power_monitoring.c
@@ -90,6 +90,8 @@ void pm_pmic_data_ready(void* context, pmic_report_t* report) {
} else {
if (drv->woke_up_from_suspend) {
+#ifdef USE_RTC
+
// Use known battery self-discharge rate to compensate the fuel gauge
// estimation during the suspend period. Since this period may be very
// long and the battery temperature may vary, use the average ambient
@@ -98,15 +100,17 @@ void pm_pmic_data_ready(void* context, pmic_report_t* report) {
PM_SELF_DISG_RATE_SUSPEND_MA, 25.0f);
// TODO: Currently in suspend mode we use single self-discharge rate
- // but in practive the discharge rate may change in case the BLE chip
+ // but in practive the discharge rate may change in case some components
// remains active. Since the device is very likely to stay in suspend
// mode for limited time, for now we decided to neglect this. but in
- // the future we may want to distinguish between suspend mode
- // with/without BLE and use different self-discharge rates.
+ // the future we may want to distinguish between different suspend modes
+ // and use different self-discharge rates.
fuel_gauge_set_soc(&drv->fuel_gauge, drv->fuel_gauge.soc,
drv->fuel_gauge.P);
+#endif // USE_RTC
+
// clear the flag
drv->woke_up_from_suspend = false;
@@ -142,7 +146,10 @@ void pm_pmic_data_ready(void* context, pmic_report_t* report) {
pm_store_data_to_backup_ram();
if (drv->suspending) {
+#ifdef USE_RTC
+ // Schedule auto-hibernation rtc event
pm_schedule_rtc_wakeup();
+#endif
drv->suspending = false;
drv->suspended = true;
}
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.