Skip to content

Commit 718586d

Browse files
gmarullclaude
andcommitted
apps/demo: probe the syscall stack from the MPU violation test
Add a "Syscall near limit" case that burns the App task's stack down to 160 B and then loads a system font the app has not touched, a syscall whose privileged chain (resource_storage, filesystem, flash) needs a few hundred bytes. With syscalls relocated to their own stack the call completes and the app reports "SURVIVED (expected)"; on a kernel that still runs syscalls on the task stack it overflows privileged and the watch reboots, which is the FIRM-4306 failure mode. Tests can now declare that surviving is the expected outcome, so the "MPU MISS" wording stays reserved for accesses that should have faulted. On qemu_flint: upstream/main "survives" only because the wiped guard lets the overflow run into the kernel heap; with the guard restored alone the emulator reboots; with the dedicated syscall stack the test passes and the guard stays untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com>
1 parent d95651e commit 718586d

1 file changed

Lines changed: 39 additions & 4 deletions

File tree

src/fw/apps/demo/mpu_violation_test/test_mpu_violation.c

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
// Demo app that deliberately runs a series of memory accesses that the
2020
// MPU is supposed to deny for the unprivileged App task. Use up/down to
2121
// cycle through tests; press select to run the highlighted test. The
22-
// expected outcome for every test is a MemManage fault: the kernel
22+
// expected outcome for most tests is a MemManage fault: the kernel
2323
// kills the App task and the launcher reclaims the screen. If the app
2424
// stays alive long enough to render "SURVIVED!" the MPU let the access
25-
// through -- that's the regression signal.
25+
// through -- that's the regression signal. Tests marked as expected to
26+
// survive exercise the other direction: the kernel must keep working
27+
// where the task's own stack is nearly gone.
2628
//
2729
// SimpleMenuLayer would have been a nicer UI but it touches kernel data
2830
// not accessible to an unprivileged App task, so we use a plain Window
@@ -33,6 +35,7 @@ extern const uint32_t __WORKER_RAM__[];
3335
extern const uint32_t __FLASH_start__[];
3436
extern const uint32_t __APP_RAM__[];
3537
extern const uint32_t __kernel_main_stack_start__[];
38+
extern const uint32_t __stack_guard_size__[];
3639
#ifdef CONFIG_SOC_SF32LB52
3740
extern const uint32_t __ramfunc_start[];
3841
#endif
@@ -58,6 +61,7 @@ typedef enum {
5861
TestKind_StackGuardWrite,
5962
#endif
6063
TestKind_StackOverflow,
64+
TestKind_SyscallNearLimit,
6165
TestKindCount,
6266
} TestKind;
6367

@@ -78,6 +82,11 @@ static const char *const s_test_titles[TestKindCount] = {
7882
[TestKind_StackGuardWrite] = "Stack guard W",
7983
#endif
8084
[TestKind_StackOverflow] = "Stack overflow",
85+
[TestKind_SyscallNearLimit] = "Syscall near limit",
86+
};
87+
88+
static const bool s_test_expect_survive[TestKindCount] = {
89+
[TestKind_SyscallNearLimit] = true,
8190
};
8291

8392
typedef struct {
@@ -104,6 +113,25 @@ static uint32_t __attribute__((noinline)) prv_overflow_recurse(uint32_t depth) {
104113
}
105114
#pragma GCC diagnostic pop
106115

116+
// Leaves only `headroom` bytes of the task stack and then makes a syscall
117+
// whose privileged call chain needs far more than that. The kernel must
118+
// run it on its own syscall stack; running it on ours overflows into the
119+
// stack guard while privileged, which reboots the system.
120+
static void __attribute__((noinline)) prv_syscall_near_limit(size_t headroom) {
121+
volatile uint8_t marker;
122+
const uintptr_t stack_base = (uintptr_t)__APP_RAM__ + (uintptr_t)__stack_guard_size__;
123+
const uintptr_t sp = (uintptr_t)&marker;
124+
const size_t burn = (sp > stack_base + headroom) ? (sp - stack_base - headroom) : 1;
125+
volatile uint8_t pad[burn];
126+
for (size_t i = 0; i < burn; i += 32) {
127+
pad[i] = (uint8_t)i;
128+
}
129+
// Loading a system font the app has not used yet walks resource_storage
130+
// and the filesystem, several hundred bytes of privileged stack.
131+
(void)fonts_get_system_font(FONT_KEY_DROID_SERIF_28_BOLD);
132+
marker = pad[0];
133+
}
134+
107135
static void prv_run_test(TestKind kind) {
108136
switch (kind) {
109137
case TestKind_WorkerRamWrite: {
@@ -171,6 +199,11 @@ static void prv_run_test(TestKind kind) {
171199
// (ARMv7-M), since each call frame consumes ~128 B.
172200
(void)prv_overflow_recurse(0);
173201
break;
202+
case TestKind_SyscallNearLimit:
203+
// Enough for the SVC exception frame (with FP state) and the syscall
204+
// wrapper's pushes, nothing more.
205+
prv_syscall_near_limit(160);
206+
break;
174207
case TestKindCount:
175208
break;
176209
}
@@ -188,9 +221,11 @@ static void prv_attempt(void *cb_data) {
188221
prv_run_test((TestKind)data->selected_index);
189222

190223
// Reaching this point means no fault. Surface that prominently --
191-
// the previous "TESTING..." text gets replaced so the survival is
224+
// the previous "TESTING..." text gets replaced so the outcome is
192225
// obvious in a screenshot.
193-
text_layer_set_text(&data->selection_text, "SURVIVED!\n(MPU MISS)");
226+
text_layer_set_text(&data->selection_text, s_test_expect_survive[data->selected_index]
227+
? "SURVIVED\n(expected)"
228+
: "SURVIVED!\n(MPU MISS)");
194229
layer_mark_dirty(text_layer_get_layer(&data->selection_text));
195230
data->test_running = false;
196231
}

0 commit comments

Comments
 (0)