feat(core): introduce coreapp TLS section
What changed, and why it matters
This commit adds a new 'thread-local storage' (TLS) memory section for Trezor's coreapp on newer hardware models. It changes how the device saves and restores small, per-task memory areas when switching between tasks. The change is a feature implementation, not a fix for a known security bug. There is no evidence in the commit or supplied references that this addresses an active vulnerability or was disclosed as security-relevant.
Treat as a normal feature/refactoring commit. Review the context-switch save/restore logic for correctness (e.g., ensure tls_size is validated before use, that memcpy does not race with MPU reconfiguration, and that the 20-word tls_copy limit is appropriate for all applets). No urgent security response is indicated by the available materials.
Security signals we found
Stack canary guard (__stack_chk_guard) is now declared thread-local, which can improve stack-smashing protection isolation between tasks
New context-switch save/restore of TLS area using a fixed-size 20-word buffer with a size check
Linker section alignment comment notes 32-byte alignment is required by MPU, suggesting memory protection integration
No changelog entry and no security advisory language in commit message
Evidence from the diff
The patch introduces a dedicated .tls linker section (aligned to 32 bytes for MPU requirements) in STM32U5 firmware linker scripts and exposes its start/size via the vector table. It defines a THREAD_LOCAL macro (defaulting to empty, set to attribute((section(“.tls”))) for T3W1/T3T1 models), marks the stack canary guard (__stack_chk_guard) as thread-local, and extends the applet header and systask_t structures with TLS area metadata. During context switches in scheduler_pendsv, the kernel now saves the current task’s TLS area into a fixed 20-word tls_copy buffer and restores the next task’s TLS area. A helper systask_enable_tls enforces that the TLS area fits within tls_copy.
Changed components
core/SConscript.firmwarecore/embed/rtl/error_handling.ccore/embed/rtl/inc/trezor_rtl.hcore/embed/sys/linker/stm32u58/firmware.ldcore/embed/sys/linker/stm32u5g/firmware.ldcore/embed/sys/startup/stm32f4/vectortable.Score/embed/sys/startup/stm32u5/vectortable.Score/embed/sys/task/inc/sys/applet.hcore/embed/sys/task/inc/sys/systask.hcore/embed/sys/task/stm32/systask.cInspect captured patch +76 / −6
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index c6edb67d..696c1852 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -271,6 +271,9 @@ CPPDEFINES_MOD += [
'FANCY_FATAL_ERROR',
]
+if TREZOR_MODEL in ["T3W1", "T3T1"]:
+ CPPDEFINES_MOD += ('THREAD_LOCAL', '__attribute__((section(".tls")))'),
+
# modtrezorutils
SOURCE_MOD += [
'embed/upymod/modtrezorutils/modtrezorutils.c',
diff --git a/core/embed/rtl/error_handling.c b/core/embed/rtl/error_handling.c
index 79835579..89b05302 100644
--- a/core/embed/rtl/error_handling.c
+++ b/core/embed/rtl/error_handling.c
@@ -30,7 +30,7 @@
#ifndef TREZOR_EMULATOR
// Stack check guard value set in startup code.
// This is used if stack protection is enabled.
-uint32_t __stack_chk_guard = 0;
+THREAD_LOCAL uint32_t __stack_chk_guard = 0;
#endif
#define ALL_DATA_ERASED_MESSAGE "All data has been erased from the device"
diff --git a/core/embed/rtl/inc/trezor_rtl.h b/core/embed/rtl/inc/trezor_rtl.h
index 60b6fc20..fde50c28 100644
--- a/core/embed/rtl/inc/trezor_rtl.h
+++ b/core/embed/rtl/inc/trezor_rtl.h
@@ -66,4 +66,10 @@
#define UNUSED(x) (void)(x)
#endif
+#ifndef THREAD_LOCAL
+// Used to mark thread-local variables in coreapp.
+// THREAD_LOCAL is defined by the build system.
+#define THREAD_LOCAL
+#endif
+
#endif // TREZOR_RTL_H
diff --git a/core/embed/sys/linker/stm32u58/firmware.ld b/core/embed/sys/linker/stm32u58/firmware.ld
index db5decbb..d0c6f3a5 100644
--- a/core/embed/sys/linker/stm32u58/firmware.ld
+++ b/core/embed/sys/linker/stm32u58/firmware.ld
@@ -17,6 +17,9 @@ _data_section_end = ADDR(.data) + SIZEOF(.data);
_bss_section_start = ADDR(.bss);
_bss_section_end = ADDR(.bss) + SIZEOF(.bss);
+_tls_section_start = ADDR(.tls);
+_tls_section_size = SIZEOF(.tls);
+
_codelen = SIZEOF(.flash) + SIZEOF(.data);
_heap_start = ADDR(.heap);
@@ -55,6 +58,11 @@ SECTIONS {
*(.ARM.exidx*);
}
+ .tls : ALIGN(32) {
+ *(.tls*); /* 32-byte alignment required by MPU */
+ . = ALIGN(32);
+ } > AUX1_RAM
+
.bss : ALIGN(4) {
*(.no_dma_buffers*);
*(.bss*);
diff --git a/core/embed/sys/linker/stm32u5g/firmware.ld b/core/embed/sys/linker/stm32u5g/firmware.ld
index d7cecb14..e928d886 100644
--- a/core/embed/sys/linker/stm32u5g/firmware.ld
+++ b/core/embed/sys/linker/stm32u5g/firmware.ld
@@ -16,6 +16,9 @@ _data_section_end = ADDR(.data) + SIZEOF(.data);
_bss_section_start = ADDR(.bss);
_bss_section_end = ADDR(.bss) + SIZEOF(.bss);
+_tls_section_start = ADDR(.tls);
+_tls_section_size = SIZEOF(.tls);
+
_codelen = SIZEOF(.padding) + SIZEOF(.flash) + SIZEOF(.data);
_heap_start = ADDR(.heap);
@@ -65,6 +68,11 @@ SECTIONS {
*(.ARM.exidx*);
}
+ .tls : ALIGN(32) {
+ *(.tls*); /* 32-byte alignment required by MPU */
+ . = ALIGN(32);
+ } > AUX1_RAM
+
.bss : ALIGN(4) {
*(.no_dma_buffers*);
*(.bss*);
diff --git a/core/embed/sys/startup/stm32f4/vectortable.S b/core/embed/sys/startup/stm32f4/vectortable.S
index 1dd7c408..31e81760 100644
--- a/core/embed/sys/startup/stm32f4/vectortable.S
+++ b/core/embed/sys/startup/stm32f4/vectortable.S
@@ -132,10 +132,14 @@ vector_table:
.section .vector_table, "a"
vector_table:
+ .word reset_handler
.word _stack_section_start
.word _stack_section_size
- .word reset_handler
-
+ .word 0
+ .word 0
+ .word 0
+ .word 0
+ .word 0
#endif
diff --git a/core/embed/sys/startup/stm32u5/vectortable.S b/core/embed/sys/startup/stm32u5/vectortable.S
index 3c2179fc..6ab99c43 100644
--- a/core/embed/sys/startup/stm32u5/vectortable.S
+++ b/core/embed/sys/startup/stm32u5/vectortable.S
@@ -180,9 +180,11 @@ vector_table:
.section .vector_table, "a"
vector_table:
+ .word reset_handler
.word _stack_section_start
.word _stack_section_size
- .word reset_handler
+ .word _tls_section_start
+ .word _tls_section_size
#if USE_STORAGE_HWKEY
.word saes_unpriv_input
.word saes_unpriv_output
diff --git a/core/embed/sys/task/inc/sys/applet.h b/core/embed/sys/task/inc/sys/applet.h
index a4e8a21c..8565e97e 100644
--- a/core/embed/sys/task/inc/sys/applet.h
+++ b/core/embed/sys/task/inc/sys/applet.h
@@ -30,10 +30,12 @@ typedef void (*applet_startup_t)(const char* args, uint32_t random);
// Applet header found at the beginning of the applet binary
typedef struct {
- // Stack area
- mpu_area_t stack;
// Applet entry point
applet_startup_t startup;
+ // Stack area
+ mpu_area_t stack;
+ // TLS area
+ mpu_area_t tls;
// Coreapp specific data
struct {
// Unprivileged SAES input buffer
diff --git a/core/embed/sys/task/inc/sys/systask.h b/core/embed/sys/task/inc/sys/systask.h
index 54f66acd..85f71b2a 100644
--- a/core/embed/sys/task/inc/sys/systask.h
+++ b/core/embed/sys/task/inc/sys/systask.h
@@ -144,6 +144,13 @@ typedef struct {
// used with dynamically linked apps, otherwise set to 0.
uint32_t sb_addr;
+ // Address of the global TLS area
+ void* tls_addr;
+ // Number of bytes used in the TLS area
+ size_t tls_size;
+ // TLS copy if the task is inactive
+ uint32_t tls_copy[20];
+
// Set if the task is processing the kernel callback
bool in_callback;
@@ -160,6 +167,12 @@ systask_t* systask_active(void);
// Returns the kernel task
systask_t* systask_kernel(void);
+// Enables automatics restoring of TLS area
+//
+// When task is deactivated, the tls area is automatically stored in the
+// `task->tls_copy` array and restored when the task is activated again.
+void systask_enable_tls(systask_t* task, mpu_area_t tls);
+
// Makes the given task the currently running task.
void systask_yield_to(systask_t* task);
diff --git a/core/embed/sys/task/stm32/systask.c b/core/embed/sys/task/stm32/systask.c
index 04cb9def..de4d14e8 100644
--- a/core/embed/sys/task/stm32/systask.c
+++ b/core/embed/sys/task/stm32/systask.c
@@ -58,6 +58,7 @@ typedef struct {
systask_t* waiting_task;
// Bitmap of used task IDs
uint32_t task_id_map;
+
} systask_scheduler_t;
// Global task manager state
@@ -102,6 +103,12 @@ void systask_scheduler_init(systask_error_handler_t error_handler) {
#endif
}
+void systask_enable_tls(systask_t* task, mpu_area_t tls) {
+ ensure((tls.size <= sizeof(task->tls_copy)) * sectrue, "TLS area too large");
+ task->tls_addr = (void*)tls.start;
+ task->tls_size = tls.size;
+}
+
systask_t* systask_active(void) {
systask_scheduler_t* scheduler = &g_systask_scheduler;
@@ -561,6 +568,18 @@ __attribute((no_stack_protector, used)) static uint32_t scheduler_pendsv(
prev_task->exc_return = exc_return;
prev_task->mpu_mode = mpu_get_mode();
+ if (prev_task->tls_size != 0) {
+#ifdef KERNEL
+ if (prev_task->applet != NULL) {
+ applet_t* applet = (applet_t*)prev_task->applet;
+ mpu_set_active_applet(&applet->layout);
+ }
+#endif
+
+ // Save the TLS of the previous task
+ memcpy(prev_task->tls_copy, prev_task->tls_addr, prev_task->tls_size);
+ }
+
// Switch to the next task
scheduler->active_task = scheduler->waiting_task;
@@ -584,6 +603,11 @@ __attribute((no_stack_protector, used)) static uint32_t scheduler_pendsv(
applet_t* applet = (applet_t*)next_task->applet;
mpu_set_active_applet(&applet->layout);
}
+
+ if (next_task->tls_size != 0) {
+ // Restore the TLS of the next task
+ memcpy(next_task->tls_addr, next_task->tls_copy, next_task->tls_size);
+ }
#endif
IRQ_LOG_EXIT();
Why this scored 27/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.