feat(core): Introduce rtc event scheduler.
What changed, and why it matters
This commit refactors the real-time clock (RTC) wakeup system in Trezor's embedded firmware so that multiple timed wakeup events can be queued and scheduled instead of only one at a time. It is a feature addition ("Introduce rtc event scheduler") with no changelog entry. The change is mostly architectural: it moves single-wakeup logic into a small scheduler and changes the timer API from a relative number of seconds to an absolute RTC timestamp. There is no direct evidence in the commit that this fixes a security vulnerability, and the vendor does not describe it as security-relevant.
Treat as a normal feature/refactor commit. If this commit is being evaluated in incident response, look for follow-up commits that harden the scheduler (e.g., bounds checking on timestamp delta, consistent IRQ locking around timer re-arming, or handling of full/empty queue edge cases). No immediate security patch action is indicated by the supplied materials.
Security signals we found
API change from relative-second wakeup to absolute-timestamp wakeup
New ring-buffer scheduler with bounded length (MAX_SCHEDULE_LEN = 16)
IRQ handler no longer falls back to setting WAKEUP_FLAG_RTC; callback is now mandatory path
Potential integer handling: delta computed as signed `event_timestamp - rtc_timestamp`, then used in `MAX(delta, 1)` and passed as uint32_t to HAL wakeup timer
Potential race window: scheduler stops timer, reads head, then restarts timer outside critical section in `rtc_schedule_wakeup_event` and `rtc_cancel_wakeup_event`
No explicit security claim, CVE, or advisory in commit or supplied references
Evidence from the diff
The patch adds core/embed/sys/time/stm32u5/rtc_scheduler.c and core/embed/sys/time/inc/sys/rtc_scheduler.h, implementing a ring-buffer-based priority queue (max 16 entries) for RTC wakeup events. rtc_wakeup_timer_start() is changed from taking seconds (1..65536) to taking an event_timestamp; it computes wakeup_counter_s = 1 + MAX(delta, 1). The scheduler callback drains all events whose timestamp is <= current RTC time, then re-arms the hardware timer for the next queued event. The old single-event RTC_IRQHandler behavior (setting WAKEUP_FLAG_RTC when no callback was registered) is removed. Build files for T3W1 revB and revC add the new source file. No explicit security issue is described.
Changed components
Trezor Core firmwareSTM32U5 RTC driver (`core/embed/sys/time/stm32u5/rtc.c`)New RTC scheduler (`core/embed/sys/time/stm32u5/rtc_scheduler.c`)T3W1 revB and revC board configurationsInspect captured patch +395 / −94
diff --git a/core/embed/sys/time/inc/sys/rtc.h b/core/embed/sys/time/inc/sys/rtc.h
index 07d48bd11..dd5dc78b1 100644
--- a/core/embed/sys/time/inc/sys/rtc.h
+++ b/core/embed/sys/time/inc/sys/rtc.h
@@ -31,6 +31,13 @@ typedef struct {
uint8_t weekday; /**< Weekday (1=Monday to 7=Sunday) */
} rtc_datetime_t;
+/**
+ * @brief Callback invoked when the RTC wakeup event occurs
+ *
+ * @param context Context pointer passed to rtc_wakeup_timer_start
+ */
+typedef void (*rtc_wakeup_callback_t)(void* context);
+
/**
* @brief Initialize the RTC driver
*
@@ -53,33 +60,6 @@ bool rtc_init(void);
*/
bool rtc_get_timestamp(uint32_t* timestamp);
-/**
- * @brief Callback invoked when the RTC wakeup event occurs
- *
- * @param context Context pointer passed to rtc_wakeup_timer_start
- */
-typedef void (*rtc_wakeup_callback_t)(void* context);
-
-/**
- * @brief Schedule a wakeup event after a specified number of seconds
- *
- * Configures the RTC to wake up the system from STOP mode after the specified
- * number of seconds. After waking up, callback is called if not NULL otherwise
- * the WAKEUP_FLAG_RTC flag is set.
- *
- * @param seconds Number of seconds (1 to 65536) to wait before waking up.
- * @param callback Callback function to be called when the wakeup event occurs.
- * @param context Context pointer to be passed to the callback function.
- * @return true if the wakeup was successfully scheduled, false otherwise
- */
-bool rtc_wakeup_timer_start(uint32_t seconds, rtc_wakeup_callback_t callback,
- void* context);
-
-/**
- * @brief Stop the RTC wakeup timer
- */
-void rtc_wakeup_timer_stop(void);
-
/**
* @brief Set the RTC using discrete time values
*
@@ -108,3 +88,23 @@ bool rtc_set(uint16_t year, uint8_t month, uint8_t day, uint8_t hour,
* @return true if the time was successfully retrieved, false otherwise
*/
bool rtc_get(rtc_datetime_t* datetime);
+
+/**
+ * @brief Start the RTC wakeup timer
+ *
+ * Configures the RTC to generate an wakeup interrupt at the specified
+ * timestamp. When the event occurs, the provided callback function is called
+ * with the given context pointer.
+ *
+ * @param event_timestamp RTC timestamp to wake up at.
+ * @param callback Callback function to be called when the wakeup event occurs.
+ * @param context Context pointer to be passed to the callback function.
+ * @return true if the wakeup timer was successfully started, false otherwise
+ */
+bool rtc_wakeup_timer_start(uint32_t event_timestamp,
+ rtc_wakeup_callback_t callback, void* context);
+
+/**
+ * @brief Stop the RTC wakeup timer
+ */
+void rtc_wakeup_timer_stop(void);
diff --git a/core/embed/sys/time/inc/sys/rtc_scheduler.h b/core/embed/sys/time/inc/sys/rtc_scheduler.h
new file mode 100644
index 000000000..0236f71fe
--- /dev/null
+++ b/core/embed/sys/time/inc/sys/rtc_scheduler.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+#include <sys/rtc.h>
+
+#define MAX_SCHEDULE_LEN 16
+
+_Static_assert((MAX_SCHEDULE_LEN & (MAX_SCHEDULE_LEN - 1)) == 0,
+ "MAX_SCHEDULE_LEN must be a power of 2");
+
+typedef uint32_t rtc_event_id_t;
+
+/**
+ * @brief Schedule a wakeup event at specified timestamp
+ *
+ * Configures the RTC to wake up the system from STOP mode at the specified
+ * timestamp. After waking up, callback is called if not NULL otherwise
+ * the WAKEUP_FLAG_RTC flag is set. Multiple wakeup events may be scheduled,
+ * they will be executed in order of their timestamps and call the specific
+ * callbacks.
+ *
+ * @param wakeup_timestamp RTC timestamp to wake up at.
+ * @param callback Callback function to be called when the wakeup event occurs.
+ * @param context Context pointer to be passed to the callback function.
+ * @param event_id Pointer to a variable where the unique ID of the scheduled
+ * event will be stored.
+ * @return true if the wakeup was successfully scheduled, false otherwise
+ */
+bool rtc_schedule_wakeup_event(uint32_t wakeup_timestamp,
+ rtc_wakeup_callback_t callback, void* context,
+ rtc_event_id_t* event_id);
+
+/**
+ * @brief Cancel the wakeup event and remove it from the rtc schedule
+ *
+ * @param event_id Unique ID of the wakeup event to be cancelled
+ * @return true if the event successfully cancelled and removed from schedule
+ */
+bool rtc_cancel_wakeup_event(uint32_t event_id);
diff --git a/core/embed/sys/time/stm32u5/rtc.c b/core/embed/sys/time/stm32u5/rtc.c
index 3c578431b..e0481fbc9 100644
--- a/core/embed/sys/time/stm32u5/rtc.c
+++ b/core/embed/sys/time/stm32u5/rtc.c
@@ -25,6 +25,7 @@
#include <sys/irq.h>
#include <sys/mpu.h>
#include <sys/rtc.h>
+#include <sys/rtc_scheduler.h>
#include <sys/suspend.h>
// RTC driver structure
@@ -102,73 +103,6 @@ bool rtc_get_timestamp(uint32_t* timestamp) {
return true;
}
-bool rtc_wakeup_timer_start(uint32_t seconds, rtc_wakeup_callback_t callback,
- void* context) {
- rtc_driver_t* drv = &g_rtc_driver;
-
- if (!drv->initialized) {
- return false;
- }
-
- if (seconds < 1 || seconds > 0x10000) {
- return false;
- }
-
- irq_key_t irq_key = irq_lock();
- drv->callback = callback;
- drv->callback_context = context;
- irq_unlock(irq_key);
-
- HAL_StatusTypeDef status;
-
- status = HAL_RTCEx_SetWakeUpTimer_IT(&drv->hrtc, seconds - 1,
- RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0);
- if (HAL_OK != status) {
- return false;
- }
-
- return true;
-}
-
-void rtc_wakeup_timer_stop(void) {
- rtc_driver_t* drv = &g_rtc_driver;
-
- if (!drv->initialized) {
- return;
- }
-
- HAL_RTCEx_DeactivateWakeUpTimer(&drv->hrtc);
- drv->callback = NULL;
- drv->callback_context = NULL;
-}
-
-void RTC_IRQHandler(void) {
- rtc_driver_t* drv = &g_rtc_driver;
-
- IRQ_LOG_ENTER();
- mpu_mode_t mpu_mode = mpu_reconfig(MPU_MODE_DEFAULT);
-
- if (READ_BIT(RTC->MISR, RTC_MISR_WUTMF) != 0U) {
- // Clear the wakeup timer interrupt flag
- WRITE_REG(RTC->SCR, RTC_SCR_CWUTF);
-
- rtc_wakeup_callback_t callback = drv->callback;
- void* callback_context = drv->callback_context;
-
- // Deactivate the wakeup timer to prevent re-triggering
- rtc_wakeup_timer_stop();
-
- if (callback != NULL) {
- callback(callback_context);
- } else {
- wakeup_flags_set(WAKEUP_FLAG_RTC);
- }
- }
-
- mpu_restore(mpu_mode);
- IRQ_LOG_EXIT();
-}
-
static const uint8_t days_in_month[] = {
31, // January
28, // February (not considering leap years here)
@@ -299,4 +233,72 @@ bool rtc_get(rtc_datetime_t* datetime) {
return true;
}
+bool rtc_wakeup_timer_start(uint32_t event_timestamp,
+ rtc_wakeup_callback_t callback, void* context) {
+ rtc_driver_t* drv = &g_rtc_driver;
+
+ if (!drv->initialized) {
+ return false;
+ }
+
+ uint32_t rtc_timestamp;
+ rtc_get_timestamp(&rtc_timestamp);
+
+ int32_t delta = event_timestamp - rtc_timestamp;
+ uint32_t wakeup_counter_s = 1 + MAX(delta, 1);
+
+ irq_key_t irq_key = irq_lock();
+
+ HAL_StatusTypeDef status;
+ status = HAL_RTCEx_SetWakeUpTimer_IT(&drv->hrtc, wakeup_counter_s - 1,
+ RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0);
+ if (HAL_OK != status) {
+ irq_unlock(irq_key);
+ return false;
+ }
+
+ drv->callback = callback;
+ drv->callback_context = context;
+
+ irq_unlock(irq_key);
+
+ return true;
+}
+
+void rtc_wakeup_timer_stop(void) {
+ rtc_driver_t* drv = &g_rtc_driver;
+
+ if (!drv->initialized) {
+ return;
+ }
+
+ irq_key_t key = irq_lock();
+
+ HAL_RTCEx_DeactivateWakeUpTimer(&drv->hrtc);
+
+ irq_unlock(key);
+}
+
+void RTC_IRQHandler(void) {
+ rtc_driver_t* drv = &g_rtc_driver;
+
+ IRQ_LOG_ENTER();
+ mpu_mode_t mpu_mode = mpu_reconfig(MPU_MODE_DEFAULT);
+
+ if (READ_BIT(RTC->MISR, RTC_MISR_WUTMF) != 0U) {
+ // Clear the wakeup timer interrupt flag
+ WRITE_REG(RTC->SCR, RTC_SCR_CWUTF);
+
+ // Deactivate the wakeup timer to prevent re-triggering
+ rtc_wakeup_timer_stop();
+
+ if (drv->callback != NULL) {
+ drv->callback(drv->callback_context);
+ }
+ }
+
+ mpu_restore(mpu_mode);
+ IRQ_LOG_EXIT();
+}
+
#endif // KERNEL_MODE
diff --git a/core/embed/sys/time/stm32u5/rtc_scheduler.c b/core/embed/sys/time/stm32u5/rtc_scheduler.c
new file mode 100644
index 000000000..6f627e519
--- /dev/null
+++ b/core/embed/sys/time/stm32u5/rtc_scheduler.c
@@ -0,0 +1,238 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef KERNEL_MODE
+
+#include <sys/irq.h>
+#include <sys/rtc.h>
+#include <sys/rtc_scheduler.h>
+
+typedef struct {
+ uint32_t timestamp;
+ uint32_t id;
+ rtc_wakeup_callback_t callback;
+ void *callback_context;
+} rtc_wakeup_event_t;
+
+typedef struct {
+ uint8_t head;
+ uint8_t tail;
+ rtc_wakeup_event_t events[MAX_SCHEDULE_LEN];
+} rtc_wakeup_schedule_t;
+
+uint32_t rtc_event_id_counter = 0;
+
+rtc_wakeup_schedule_t g_rtc_wakeup_schedule = {
+ .head = 0,
+ .tail = 0,
+};
+
+static bool rtc_scheduler_push(rtc_wakeup_event_t *event);
+static bool rtc_scheduler_pop(rtc_wakeup_event_t *event);
+static bool rtc_scheduler_remove(uint32_t id);
+rtc_wakeup_event_t *rtc_scheduler_get_head(void);
+
+void rtc_scheduler_callback(void *context) {
+ // Call events that exceeds the current timestamp
+ while (true) {
+ rtc_wakeup_event_t *next_event = rtc_scheduler_get_head();
+ if (next_event == NULL) {
+ break;
+ }
+
+ uint32_t current_timestamp;
+ rtc_get_timestamp(¤t_timestamp);
+ if (next_event->timestamp > current_timestamp) {
+ break;
+ }
+
+ // Call the event callback
+ if (next_event->callback != NULL) {
+ next_event->callback(next_event->callback_context);
+ }
+
+ // Remove the event from the schedule
+ rtc_scheduler_pop(next_event);
+ }
+
+ // Start the next event if any
+ rtc_wakeup_event_t *next_event = rtc_scheduler_get_head();
+ if (next_event != NULL) {
+ rtc_wakeup_timer_start(next_event->timestamp, &rtc_scheduler_callback,
+ next_event);
+ }
+}
+
+bool rtc_schedule_wakeup_event(uint32_t wakeup_timestamp,
+ rtc_wakeup_callback_t callback, void *context,
+ rtc_event_id_t *event_id) {
+ irq_key_t irq_key = irq_lock();
+
+ // Increment event ID
+ rtc_event_id_counter++;
+ if (rtc_event_id_counter == 0) {
+ rtc_event_id_counter = 1; // Avoid zero ID
+ }
+
+ rtc_wakeup_event_t new_event = {
+ .timestamp = wakeup_timestamp,
+ .id = rtc_event_id_counter,
+ .callback = callback,
+ .callback_context = context,
+ };
+
+ // Push new event to the schedule
+ if (!rtc_scheduler_push(&new_event)) {
+ irq_unlock(irq_key);
+ return false;
+ }
+
+ rtc_wakeup_timer_stop();
+
+ rtc_wakeup_event_t *head = rtc_scheduler_get_head();
+ if (head == NULL) {
+ irq_unlock(irq_key);
+ return false;
+ }
+
+ // Return new event ID
+ if (event_id != NULL) {
+ *event_id = new_event.id;
+ }
+
+ rtc_wakeup_timer_start(head->timestamp, &rtc_scheduler_callback, NULL);
+
+ irq_unlock(irq_key);
+
+ return true;
+}
+
+bool rtc_cancel_wakeup_event(uint32_t event_id) {
+ irq_key_t irq_key = irq_lock();
+
+ rtc_wakeup_timer_stop();
+
+ rtc_scheduler_remove(event_id);
+
+ rtc_wakeup_event_t *head = rtc_scheduler_get_head();
+ if (head == NULL) {
+ irq_unlock(irq_key);
+ return false;
+ }
+
+ rtc_wakeup_timer_start(head->timestamp, &rtc_scheduler_callback, head);
+ irq_unlock(irq_key);
+
+ return true;
+}
+
+static bool rtc_scheduler_push(rtc_wakeup_event_t *event) {
+ rtc_wakeup_schedule_t *sch = &g_rtc_wakeup_schedule;
+
+ uint8_t new_tail = (sch->tail + 1) % MAX_SCHEDULE_LEN;
+ if (new_tail == sch->head) {
+ // Queue is full
+ return false;
+ }
+
+ // Sweep the queue backwards and find the correct position for the new event
+ uint8_t idx = sch->tail;
+
+ while (idx != sch->head) {
+ uint8_t prev_idx = (idx + MAX_SCHEDULE_LEN - 1) % MAX_SCHEDULE_LEN;
+
+ if (sch->events[prev_idx].timestamp <= event->timestamp) {
+ break;
+ }
+
+ sch->events[idx] = sch->events[prev_idx];
+ idx = prev_idx;
+ }
+
+ // Insert the new event
+ sch->events[idx] = *event;
+
+ sch->tail = new_tail;
+
+ return true;
+}
+
+static bool rtc_scheduler_pop(rtc_wakeup_event_t *event) {
+ rtc_wakeup_schedule_t *sch = &g_rtc_wakeup_schedule;
+
+ if (sch->head == sch->tail) {
+ // Queue is empty
+ return false;
+ }
+
+ *event = sch->events[sch->head];
+ sch->head = (sch->head + 1) % MAX_SCHEDULE_LEN;
+
+ return true;
+}
+
+static bool rtc_scheduler_remove(uint32_t id) {
+ rtc_wakeup_schedule_t *sch = &g_rtc_wakeup_schedule;
+
+ if (sch->head == sch->tail) {
+ return false;
+ }
+
+ // Sweep the queue, if you hit the id, remove the event and shift
+ // remaining items backward
+ uint8_t idx = sch->head;
+ uint8_t next_idx;
+ uint8_t item_found = false;
+
+ while (idx != sch->tail) {
+ if (sch->events[idx].id == id) {
+ item_found = true;
+ }
+
+ next_idx = (idx + 1) % MAX_SCHEDULE_LEN;
+
+ if (item_found) {
+ sch->events[idx] = sch->events[next_idx];
+ }
+
+ idx = next_idx;
+ }
+
+ if (item_found) {
+ sch->tail = (sch->tail + MAX_SCHEDULE_LEN - 1) % MAX_SCHEDULE_LEN;
+ return true;
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+rtc_wakeup_event_t *rtc_scheduler_get_head(void) {
+ rtc_wakeup_schedule_t *sch = &g_rtc_wakeup_schedule;
+
+ if (sch->head == sch->tail) {
+ // Queue is empty
+ return NULL;
+ }
+
+ return &sch->events[sch->head];
+}
+
+#endif
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revB.py b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
index ff8c02e70..b46be591c 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revB.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
@@ -112,6 +112,7 @@ def configure(
if "rtc" in features_wanted:
sources += ["embed/sys/time/stm32u5/rtc.c"]
+ sources += ["embed/sys/time/stm32u5/rtc_scheduler.c"]
defines += [("USE_RTC", "1")]
if "haptic" in features_wanted:
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revC.py b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
index 4ba477d73..17609f149 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revC.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
@@ -111,6 +111,7 @@ def configure(
if "rtc" in features_wanted:
sources += ["embed/sys/time/stm32u5/rtc.c"]
+ sources += ["embed/sys/time/stm32u5/rtc_scheduler.c"]
defines += [("USE_RTC", "1")]
if "haptic" in features_wanted:
Why this scored 13/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.