Skip to content

Commit 372bcef

Browse files
committed
Added the supervisor call handler: the privilege boundary itself
SVC 1 raises a module thread out of User mode and onto its kernel stack, SVC 2 puts it back. This is the mechanism the rest of the port exists to protect. Two properties make it a boundary rather than a formality, and both are copied in substance from the Cortex-R4 module port because both are load-bearing. Only the two exact instructions inside _txm_module_manager_user_mode_entry may make these calls. lr points one instruction past the call, so the handler compares lr minus four against the known call site and stops if it does not match. Without that check a module could execute SVC 1 anywhere in its own code and come back privileged, which is every protection in this port gone at once. The stacks are swapped, not shared. A module's stack lives in memory the module can write, so the kernel must not run on it -- a module could otherwise corrupt kernel state by scribbling on what it believes is its own stack. SVC 1 switches to a kernel stack outside the module's regions and SVC 2 switches back. The swap happens in System mode because System shares User's banked sp. ThreadX's stack bounds are repointed at whichever stack is in use. Leaving them alone would have a stack check measure the kernel's sp against the module's bounds and report an overflow that has not happened. Unrecognised SVC numbers stop rather than return. Returning would resume the caller as though the call had succeeded, with the privilege state undefined. This core's base port does not use SVC at all -- its vector treats one as a fault -- so there is no third caller to accommodate. The offsets are not the R4 port's. Every field after tx_thread_vfp_enable moves by a word in this port, so the kernel stack fields sit at 0xA8, 0xAC and 0xB0 where R4 has 0xA4, 0xA8 and 0xAC. All twelve are asserted against offsetof, with the two that matter most spelled out in their messages: a wrong user-mode flag offset would leave a thread believing it is privileged when it is not, and a wrong kernel stack offset would put the kernel on memory the module can write. The file declares .syntax unified rather than relying on the default, which rejected the unified conditional-load form. A file that hand-writes exception entry is the last place to leave the assembler's dialect to chance. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
1 parent 7a70ab5 commit 372bcef

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

ports_module/cortex_r52/gnu/module_manager/src/txm_module_manager_offset_check.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,47 @@ _Static_assert(offsetof(TXM_MODULE_INSTANCE, txm_module_instance_mpu_registers)
7878
"TXM_MODULE_INSTANCE; the scheduler would load regions from the "
7979
"wrong address");
8080

81+
/* Offsets the supervisor call handler hard-codes, from
82+
txm_module_manager_svc_handler.S. This is the privilege boundary, so a wrong
83+
offset here does not merely misbehave: writing the user-mode flag to the wrong
84+
word would leave a thread believing it is privileged when it is not, or the
85+
reverse, and swapping to a stack pointer read from the wrong field would put
86+
the kernel on memory the module can write. */
87+
88+
#define THREAD_STACK_PTR 0x08
89+
#define THREAD_STACK_START 0x0C
90+
#define THREAD_STACK_END 0x10
91+
#define THREAD_STACK_SIZE 0x14
92+
#define THREAD_CUR_USER_MODE 0x9C
93+
#define THREAD_KSTACK_START 0xA8
94+
#define THREAD_KSTACK_END 0xAC
95+
#define THREAD_KSTACK_SIZE 0xB0
96+
#define THREAD_MSTACK_PTR 0xB4
97+
#define THREAD_MSTACK_START 0xB8
98+
#define THREAD_MSTACK_END 0xBC
99+
#define THREAD_MSTACK_SIZE 0xC0
100+
101+
#define T_OFF(f) offsetof(TX_THREAD, f)
102+
103+
_Static_assert(T_OFF(tx_thread_stack_ptr) == THREAD_STACK_PTR, "svc handler: stack_ptr moved");
104+
_Static_assert(T_OFF(tx_thread_stack_start) == THREAD_STACK_START, "svc handler: stack_start moved");
105+
_Static_assert(T_OFF(tx_thread_stack_end) == THREAD_STACK_END, "svc handler: stack_end moved");
106+
_Static_assert(T_OFF(tx_thread_stack_size) == THREAD_STACK_SIZE, "svc handler: stack_size moved");
107+
108+
_Static_assert(T_OFF(tx_thread_module_current_user_mode) == THREAD_CUR_USER_MODE,
109+
"svc handler: the user-mode flag moved; a thread could be left "
110+
"believing it is privileged when it is not");
111+
112+
_Static_assert(T_OFF(tx_thread_module_kernel_stack_start) == THREAD_KSTACK_START, "svc handler: kernel_stack_start moved");
113+
_Static_assert(T_OFF(tx_thread_module_kernel_stack_end) == THREAD_KSTACK_END,
114+
"svc handler: kernel_stack_end moved; the kernel would run on a "
115+
"stack read from the wrong field");
116+
_Static_assert(T_OFF(tx_thread_module_kernel_stack_size) == THREAD_KSTACK_SIZE, "svc handler: kernel_stack_size moved");
117+
_Static_assert(T_OFF(tx_thread_module_stack_ptr) == THREAD_MSTACK_PTR, "svc handler: module stack_ptr moved");
118+
_Static_assert(T_OFF(tx_thread_module_stack_start) == THREAD_MSTACK_START, "svc handler: module stack_start moved");
119+
_Static_assert(T_OFF(tx_thread_module_stack_end) == THREAD_MSTACK_END, "svc handler: module stack_end moved");
120+
_Static_assert(T_OFF(tx_thread_module_stack_size) == THREAD_MSTACK_SIZE, "svc handler: module stack_size moved");
121+
81122
/* Offsets the fault capture hard-codes, from
82123
txm_module_manager_fault_capture.S. Same hazard as the scheduler's: the
83124
capture runs in Abort mode with a fault in progress, and a wrong offset there
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
@/***************************************************************************
2+
@ * Copyright (c) 2026 Eclipse ThreadX contributors
3+
@ *
4+
@ * This program and the accompanying materials are made available under the
5+
@ * terms of the MIT License which is available at
6+
@ * https://opensource.org/licenses/MIT.
7+
@ *
8+
@ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
9+
@ * The AI-generated portions may be considered public domain (CC0-1.0)
10+
@ * and not subject to the project's licence. The human contributor has
11+
@ * reviewed and verified that the code is correct.
12+
@ *
13+
@ * SPDX-License-Identifier: MIT and CC0-1.0
14+
@ **************************************************************************/
15+
@
16+
@/**************************************************************************/
17+
@/* */
18+
@/* MODULE MANAGER RELEASE */
19+
@/* */
20+
@/* txm_module_manager_svc_handler.S Cortex-R52/GNU */
21+
@/* 6.5.2 */
22+
@/* AUTHOR */
23+
@/* */
24+
@/* Frédéric Desbiens, Eclipse Foundation */
25+
@/* */
26+
@/* DESCRIPTION */
27+
@/* */
28+
@/* The privilege boundary. A board's supervisor call vector branches */
29+
@/* here. */
30+
@/* */
31+
@/* SVC 1 raises a module thread out of User mode and onto its kernel */
32+
@/* stack; SVC 2 puts it back. Both are only reachable from the two */
33+
@/* exact instructions inside _txm_module_manager_user_mode_entry, and */
34+
@/* that check is what makes the boundary a boundary. Without it a */
35+
@/* module could execute SVC 1 from anywhere in its own code and come */
36+
@/* back privileged, which is every protection in this port gone at */
37+
@/* once. */
38+
@/* */
39+
@/* The two stacks are the other half of it. A module's own stack is in */
40+
@/* memory the module can write, so the kernel must not run on it: a */
41+
@/* module could otherwise corrupt kernel state by scribbling on what it */
42+
@/* believes is its own stack. SVC 1 therefore switches to a kernel */
43+
@/* stack the module cannot reach, and SVC 2 switches back. */
44+
@/* */
45+
@/* Any other SVC number stops. This core's base port does not use SVC */
46+
@/* at all -- its vector treats one as a fault -- so there is no third */
47+
@/* caller to accommodate and nothing legitimate to fall through to. */
48+
@/* */
49+
@/**************************************************************************/
50+
51+
@ Unified syntax, declared rather than assumed. In divided syntax the condition
52+
@ precedes the size -- LDRNEH -- and in unified it follows -- LDRHNE. The
53+
@ assembler's default rejected the unified form, and a file that hand-writes
54+
@ exception entry is the last place to leave that to chance.
55+
56+
.syntax unified
57+
.arm
58+
.text
59+
.align 2
60+
61+
.global __tx_module_svc_interrupt
62+
.extern _tx_thread_current_ptr
63+
.extern _txm_system_mode_enter
64+
.extern _txm_system_mode_exit
65+
66+
@ Processor mode encodings and the CPSR mode field.
67+
68+
.equ MODE_MASK, 0x1F
69+
.equ USR_MODE, 0x10
70+
.equ SVC_MODE, 0x13
71+
.equ SYS_MODE, 0x1F
72+
.equ THUMB_MASK, 0x20
73+
74+
@ Offsets into TX_THREAD. Checked against offsetof at compile time in
75+
@ txm_module_manager_offset_check.c. They are not the Cortex-R4 module port's
76+
@ values: tx_thread_vfp_enable sits ahead of the module fields in this port, so
77+
@ everything after it moves by a word.
78+
79+
.equ THREAD_STACK_PTR, 0x08
80+
.equ THREAD_STACK_START, 0x0C
81+
.equ THREAD_STACK_END, 0x10
82+
.equ THREAD_STACK_SIZE, 0x14
83+
.equ THREAD_CUR_USER_MODE, 0x9C
84+
.equ THREAD_KSTACK_START, 0xA8
85+
.equ THREAD_KSTACK_END, 0xAC
86+
.equ THREAD_KSTACK_SIZE, 0xB0
87+
.equ THREAD_MSTACK_PTR, 0xB4
88+
.equ THREAD_MSTACK_START, 0xB8
89+
.equ THREAD_MSTACK_END, 0xBC
90+
.equ THREAD_MSTACK_SIZE, 0xC0
91+
92+
93+
@/**************************************************************************/
94+
@/* Vector entry. */
95+
@/**************************************************************************/
96+
97+
.type __tx_module_svc_interrupt, %function
98+
__tx_module_svc_interrupt:
99+
100+
STMFD sp!, {r0-r3, r12, lr} @ Preserve the caller's registers
101+
MRS r0, spsr
102+
STMFD sp!, {r0, r3} @ SPSR, plus one more to keep the
103+
@ stack eight-byte aligned
104+
105+
@ Recover the SVC number from the instruction that caused this. The encoding
106+
@ differs between states, so the saved SPSR decides where to read it from.
107+
108+
TST r0, #THUMB_MASK
109+
LDRHNE r0, [lr, #-2] @ Thumb: halfword, low 8 bits
110+
BICNE r0, r0, #0xFF00
111+
LDREQ r0, [lr, #-4] @ ARM: word, low 24 bits
112+
BICEQ r0, r0, #0xFF000000
113+
114+
CMP r0, #1
115+
BEQ _tx_module_svc_enter
116+
CMP r0, #2
117+
BEQ _tx_module_svc_exit
118+
119+
120+
@/**************************************************************************/
121+
@/* Anything else. */
122+
@/**************************************************************************/
123+
124+
.weak _tx_module_svc_unrecognized
125+
_tx_module_svc_unrecognized:
126+
127+
@ Stop rather than return. Returning would resume the caller as though the
128+
@ call had succeeded, and for an unrecognised number that means resuming with
129+
@ the privilege state undefined.
130+
131+
_tx_module_svc_unrecognized_loop:
132+
B _tx_module_svc_unrecognized_loop
133+
134+
135+
@/**************************************************************************/
136+
@/* SVC 1: leave User mode. */
137+
@/**************************************************************************/
138+
139+
_tx_module_svc_enter:
140+
141+
@ Only the SVC inside the user mode entry function may do this. lr points one
142+
@ instruction past the call, so the call site is lr minus four. A module that
143+
@ executes SVC 1 from its own code fails here and stops.
144+
145+
LDR r2, =_txm_system_mode_enter
146+
SUB r1, lr, #4
147+
CMP r1, r2
148+
BNE _tx_module_svc_unrecognized
149+
150+
LDR r1, =_tx_thread_current_ptr
151+
LDR r2, [r1] @ The running thread
152+
153+
@ Record that it is no longer in User mode, so a nested service call and the
154+
@ scheduler both see the truth.
155+
156+
MOV r1, #0
157+
STR r1, [r2, #THREAD_CUR_USER_MODE]
158+
159+
@ Return into System mode rather than User mode.
160+
161+
LDMFD sp!, {r0, r3} @ SPSR back off the stack
162+
BIC r0, r0, #MODE_MASK
163+
ORR r0, r0, #SYS_MODE
164+
MSR SPSR_cxsf, r0
165+
166+
@ Swap stacks. The module's sp is saved so SVC 2 can restore it, and sp is set
167+
@ to the top of the kernel stack, which the module's regions do not cover.
168+
@ System mode shares User mode's banked sp, which is why the swap happens there.
169+
170+
LDR r1, [r2, #THREAD_KSTACK_END] @ Top of the kernel stack
171+
CPS #SYS_MODE
172+
MOV r3, sp @ The module's own sp
173+
MOV sp, r1
174+
CPS #SVC_MODE
175+
STR r3, [r2, #THREAD_MSTACK_PTR]
176+
177+
@ Point ThreadX's stack checking at the kernel stack while it is in use.
178+
@ Without this a stack check would measure the kernel's sp against the module's
179+
@ bounds and report an overflow that has not happened.
180+
181+
LDR r3, [r2, #THREAD_KSTACK_START]
182+
STR r3, [r2, #THREAD_STACK_START]
183+
LDR r3, [r2, #THREAD_KSTACK_END]
184+
STR r3, [r2, #THREAD_STACK_END]
185+
LDR r3, [r2, #THREAD_KSTACK_SIZE]
186+
STR r3, [r2, #THREAD_STACK_SIZE]
187+
188+
LDMFD sp!, {r0-r3, r12, pc}^ @ Return, restoring CPSR from SPSR
189+
190+
191+
@/**************************************************************************/
192+
@/* SVC 2: return to User mode. */
193+
@/**************************************************************************/
194+
195+
_tx_module_svc_exit:
196+
197+
@ Same check, against the other call site.
198+
199+
LDR r2, =_txm_system_mode_exit
200+
SUB r1, lr, #4
201+
CMP r1, r2
202+
BNE _tx_module_svc_unrecognized
203+
204+
LDR r1, =_tx_thread_current_ptr
205+
LDR r2, [r1]
206+
207+
MOV r1, #1
208+
STR r1, [r2, #THREAD_CUR_USER_MODE]
209+
210+
@ Return into User mode.
211+
212+
LDMFD sp!, {r0, r3}
213+
BIC r0, r0, #MODE_MASK
214+
ORR r0, r0, #USR_MODE
215+
MSR SPSR_cxsf, r0
216+
217+
@ Put the module's own stack back.
218+
219+
LDR r1, [r2, #THREAD_MSTACK_PTR]
220+
CPS #SYS_MODE
221+
MOV sp, r1
222+
CPS #SVC_MODE
223+
224+
@ And its own bounds, for the same reason they were changed on the way in.
225+
226+
LDR r3, [r2, #THREAD_MSTACK_START]
227+
STR r3, [r2, #THREAD_STACK_START]
228+
LDR r3, [r2, #THREAD_MSTACK_END]
229+
STR r3, [r2, #THREAD_STACK_END]
230+
LDR r3, [r2, #THREAD_MSTACK_SIZE]
231+
STR r3, [r2, #THREAD_STACK_SIZE]
232+
233+
LDMFD sp!, {r0-r3, r12, pc}^

0 commit comments

Comments
 (0)