fix(core): fix event polling for multiple applets
What changed, and why it matters
This commit fixes two low-level operating-system bugs in Trezor's embedded firmware. First, it corrects how the kernel waits for hardware events (button presses, USB packets, etc.) when more than one mini-program ('applet') is running. Previously, the kernel stored direct pointers to each waiting task's event masks, which could create race conditions or incorrect results when multiple tasks polled at the same time; now it keeps private copies and only writes the result back when the task actually wakes up. Second, it fixes a bookkeeping bug where new system task IDs were never marked as used, which could eventually hand out the same ID twice. It also makes sure the memory-protection unit (MPU) is switched to the correct applet before a task starts running or is woken up. The commit is tagged '[no changelog]' and gives no security context, so while the bugs are real, there is no direct evidence they are exploitable for theft of funds or secrets.
Treat as a routine but important kernel correctness fix. Review whether the ID-allocation bug and the event-mask sharing issue could be reached from untrusted applet code, and consider whether a security advisory is warranted if applets can force ID reuse or corrupt another task's event state. No immediate CVE can be assigned from this commit alone.
Security signals we found
Concurrency/race-condition fix in shared event-mask pointers across multiple polling tasks
Memory Protection Unit (MPU) context now switched before applet task execution/resumption
Resource-allocation bug fix: system task IDs were not marked as used, risking ID reuse
Kernel-mode code path affected (KERNEL_MODE / KERNEL defines)
No changelog entry and no explicit security framing from the vendor
Evidence from the diff
The patch modifies the Trezor core task/event subsystem. In sysevent.c, sysevent_poller_t changes awaited and signalled from pointers to embedded copies, and adds signalled_arg as the output pointer. sysevents_poll() now copies the awaited mask, zeroes a local signalled mask, and only writes the result back to signalled_arg when the poller is removed. This prevents multiple pollers from sharing/writing the same caller-provided masks concurrently. In systask.c, systask_get_unused_id() now actually sets the bit in task_id_map after finding a free slot, fixing an obvious allocation leak/duplicate-ID bug. systask_set_r0r1() and sysevents_poll() now call mpu_set_active_applet() when waking/running an applet task, ensuring the MPU layout matches the applet about to execute. A stub mpu_set_active_applet() is added for the Unix emulator build.
Changed components
core/embed/sys/task/sysevent.ccore/embed/sys/task/stm32/systask.ccore/embed/sys/mpu/unix/mpu.cInspect captured patch +42 / −15
diff --git a/core/embed/sys/mpu/unix/mpu.c b/core/embed/sys/mpu/unix/mpu.c
index 5aab12dc5..7ee600b11 100644
--- a/core/embed/sys/mpu/unix/mpu.c
+++ b/core/embed/sys/mpu/unix/mpu.c
@@ -28,3 +28,5 @@ mpu_mode_t mpu_get_mode(void) { return MPU_MODE_DISABLED; }
mpu_mode_t mpu_reconfig(mpu_mode_t mode) { return MPU_MODE_DISABLED; }
void mpu_restore(mpu_mode_t mode) {}
+
+void mpu_set_active_applet(applet_layout_t* layout) {}
diff --git a/core/embed/sys/task/stm32/systask.c b/core/embed/sys/task/stm32/systask.c
index 5fc87cff6..63204313f 100644
--- a/core/embed/sys/task/stm32/systask.c
+++ b/core/embed/sys/task/stm32/systask.c
@@ -141,9 +141,11 @@ void systask_yield_to(systask_t* task) {
}
static systask_id_t systask_get_unused_id(void) {
+ systask_scheduler_t* scheduler = &g_systask_scheduler;
systask_id_t id = 0;
while (++id < SYSTASK_MAX_TASKS) {
- if ((g_systask_scheduler.task_id_map & (1 << id)) == 0) {
+ if ((scheduler->task_id_map & (1 << id)) == 0) {
+ scheduler->task_id_map |= (1 << id);
break;
}
}
@@ -297,6 +299,13 @@ void systask_set_r0r1(systask_t* task, uint32_t r0, uint32_t r1) {
stack += 8; // Skip R4-R11
}
+#ifdef KERNEL
+ if (task->applet != NULL) {
+ applet_t* applet = (applet_t*)task->applet;
+ mpu_set_active_applet(&applet->layout);
+ }
+#endif
+
stack[STK_FRAME_R0] = r0;
stack[STK_FRAME_R1] = r1;
}
diff --git a/core/embed/sys/task/sysevent.c b/core/embed/sys/task/sysevent.c
index b778b41d6..ad88b09df 100644
--- a/core/embed/sys/task/sysevent.c
+++ b/core/embed/sys/task/sysevent.c
@@ -24,10 +24,15 @@
#ifdef KERNEL_MODE
+#include <sys/mpu.h>
#include <sys/sysevent_source.h>
#include <sys/systask.h>
#include <trezor_bsp.h>
+#ifdef KERNEL
+#include <sys/applet.h>
+#endif
+
#ifdef TREZOR_EMULATOR
#include <sys/unix/sdl_event.h>
#endif
@@ -38,9 +43,12 @@ typedef struct {
// Deadline for the task to be woken up
uint32_t deadline;
// Bitmask of events the task is waiting for
- const sysevents_t *awaited;
+ sysevents_t awaited;
// Bitmask of events that were signaled
- sysevents_t *signalled;
+ sysevents_t signalled;
+
+ sysevents_t *signalled_arg;
+
} sysevent_poller_t;
typedef struct {
@@ -128,13 +136,13 @@ void syshandle_signal_read_ready(syshandle_t handle, void *param) {
for (size_t i = 0; i < dispatcher->pollers_count; i++) {
sysevent_poller_t *poller = &dispatcher->pollers[i];
syshandle_mask_t handle_mask = 1 << handle;
- if ((poller->awaited->read_ready & handle_mask) != 0) {
+ if ((poller->awaited.read_ready & handle_mask) != 0) {
if (source->vmt->check_read_ready != NULL) {
if (source->vmt->check_read_ready(source->context,
systask_id(poller->task), param)) {
- poller->signalled->read_ready |= handle_mask;
+ poller->signalled.read_ready |= handle_mask;
} else {
- poller->signalled->read_ready &= ~handle_mask;
+ poller->signalled.read_ready &= ~handle_mask;
}
}
}
@@ -153,13 +161,13 @@ void syshandle_signal_write_ready(syshandle_t handle, void *param) {
for (size_t i = 0; i < dispatcher->pollers_count; i++) {
sysevent_poller_t *poller = &dispatcher->pollers[i];
syshandle_mask_t handle_mask = 1 << handle;
- if ((poller->awaited->write_ready & handle_mask) != 0) {
+ if ((poller->awaited.write_ready & handle_mask) != 0) {
if (source->vmt->check_write_ready != NULL) {
if (source->vmt->check_write_ready(source->context,
systask_id(poller->task), param)) {
- poller->signalled->write_ready |= handle_mask;
+ poller->signalled.write_ready |= handle_mask;
} else {
- poller->signalled->write_ready &= ~handle_mask;
+ poller->signalled.write_ready &= ~handle_mask;
}
}
}
@@ -210,8 +218,9 @@ void sysevents_poll(const sysevents_t *awaited, sysevents_t *signalled,
// Add task to the polling list
// Kernel task has the highest priority so it is always first in the list
dispatcher->pollers[prio].task = systask_active();
- dispatcher->pollers[prio].awaited = awaited;
- dispatcher->pollers[prio].signalled = signalled;
+ dispatcher->pollers[prio].awaited = *awaited;
+ dispatcher->pollers[prio].signalled = (sysevents_t){0};
+ dispatcher->pollers[prio].signalled_arg = signalled;
dispatcher->pollers[prio].deadline = deadline;
if (active_task != kernel_task) {
@@ -231,8 +240,8 @@ void sysevents_poll(const sysevents_t *awaited, sysevents_t *signalled,
// Gather sources to poll
for (size_t i = 0; i < dispatcher->pollers_count; i++) {
sysevent_poller_t *poller = &dispatcher->pollers[i];
- handles_to_read |= poller->awaited->read_ready;
- handles_to_write |= poller->awaited->write_ready;
+ handles_to_read |= poller->awaited.read_ready;
+ handles_to_write |= poller->awaited.write_ready;
}
// Poll sources we are waiting for
@@ -253,10 +262,17 @@ void sysevents_poll(const sysevents_t *awaited, sysevents_t *signalled,
for (size_t prio = 0; prio < dispatcher->pollers_count; prio++) {
sysevent_poller_t *poller = &dispatcher->pollers[prio];
bool timed_out = ((int32_t)(poller->deadline - now)) <= 0;
- bool ready = (poller->signalled->read_ready != 0) ||
- (poller->signalled->write_ready != 0);
+ bool ready = (poller->signalled.read_ready != 0) ||
+ (poller->signalled.write_ready != 0);
if (ready || timed_out) {
systask_t *task = poller->task;
+#if defined(KERNEL) && !defined(TREZOR_EMULATOR)
+ if (task->applet != NULL) {
+ applet_t *applet = (applet_t *)task->applet;
+ mpu_set_active_applet(&applet->layout);
+ }
+#endif
+ *poller->signalled_arg = poller->signalled;
remove_poller(dispatcher, prio);
if (task == kernel_task) {
return;
Why this scored 45/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.