Skip to content

fw/syscall: run syscalls on a dedicated stack on ARMv7-M (FIRM-4306) - #2083

Merged
gmarull merged 3 commits into
coredevices:mainfrom
teslabs:syscall-stack-armv7m
Sep 16, 2026
Merged

gmarull merged 3 commits into
coredevices:mainfrom
teslabs:syscall-stack-armv7m

Conversation

@gmarull

@gmarull gmarull commented Sep 16, 2026

Copy link
Copy Markdown
Member

Summary

Two fixes for App-task stack overflows on ARMv7-M (asterix, flint), found while analysing the FIRM-4306 coredumps, plus a new case in the MPU violation demo that exercises them.

  1. kernel: keep empty MPU slots from wiping the previous region — since the native kernel (v4.37.0+), arch_thread_regions_set() writes RBAR=0 for unused per-thread slots. With VALID clear that write lands on the region RNR already points at, so the empty slot 3 disables region 6 = the App/Worker stack guard. On v4.37+ ARMv7-M builds an app stack overflow no longer faults at all; it walks into the kernel heap. The FreeRTOS port ORed VALID|region into every slot, which is why v4.36 devices still fault on the guard. Worth backporting on its own.

  2. fw/syscall: run syscalls on the dedicated stack on ARMv7-M toobe4d98d's privileged syscall stack was ARMv8-M only (PSPLIM). FIRM-4306's coredumps show the App task's 2 KiB stack exhausted inside the privileged chain graphics_draw_text → text_resources (glyph miss on an app font) → sys_resource_load_range → resource_storage → pfs_open → FTL → flash (~1.3 KB kernel-side, ~0.5 KB app), so the overflow is privileged and the watch reboots (StackOverflow: Task #3, or HardFault: LR 0x..1bffdd when the exception frame itself hits the guard — that "LR" is SRAM residue read out of the never-written guard). This enables the relocation on ARMv7-M with a 32 B no-access MPU region under each syscall stack (free MemoryRegion_Task4 slot) instead of PSPLIM, and teaches the fault handler about it.

  3. apps/demo: probe the syscall stack from the MPU violation test — adds "Syscall near limit" (burn the app stack down to 160 B, then load an unused system font) and lets a test declare that surviving is the expected outcome.

Validation (qemu_flint)

CONFIG_DEMO_APP_MPU_VIOLATION_TEST=y, all cases driven over the QEMU monitor, reboot detected via the kernel tick counter:

case upstream/main + commit 1 + commit 2
1–6 (worker/kernel RAM, RO BSS, flash) fault, app killed fault, app killed fault, app killed
7 Stack guard W SURVIVED! (MPU MISS) fault, app killed fault, app killed
8 Stack overflow killed only once it reaches the heap fault, app killed fault, app killed
9 Syscall near limit "survives" — guard region and kernel heap overwritten reboot SURVIVED (expected), guard untouched

Stress pbw: custom font, Up burns 64 B more app stack (VLA) before graphics_draw_text with a rotating string (glyph misses → pfs → FTL). Measured with QMP xp (the QEMU gdb stub reads through the MPU with the current privilege, so it lies in app context).

build burn 0 burn 512 B burn 768 B
main (guard wiped) app peak 1704/2016 2016, guard + heap overwritten, no fault app dies from the fallout
+ commit 1 (= real device) 1704 reboot, privileged overflow in the syscall
+ commit 2 1480, syscall stack 608 1968, no fault app killed, no reboot

WeatherGraph (the watchface from the ticket) runs unchanged. pbl test passes; asterix builds. Not yet run on hardware.

Cost: 2 × 2080 B of kernel RAM. System apps run privileged and still use their own stack for everything (unchanged).

Test plan

  • asterix: run a text-heavy watchface with a custom font (WeatherGraph) for a day; no StackOverflow/HardFault reboots
  • asterix: an app that overflows its own stack shows the crash dialog instead of rebooting
  • stack_free_app_syscall_bytes heartbeat stays well above 0 on asterix

Fixes FIRM-4306

🤖 Generated with Claude Code

@gmarull
gmarull requested a review from jplexer as a code owner September 16, 2026 11:58
gmarull and others added 3 commits September 16, 2026 16:15
arch_thread_regions_set() encodes an unused per-thread region slot as
RBAR=0 / RASR=0. On ARMv7-M the context switch streams the four slots
through MPU_RBAR..MPU_RASR_A3, and an RBAR write with VALID clear does
not change RNR: it updates the region RNR already points at, i.e. the
one the previous slot just programmed. For App and Worker the empty slot
3 therefore lands on region 6 and its RASR=0 disables the task stack
guard. The FreeRTOS port ORed VALID|region into every slot, which is why
v4.36 firmware still faults on the guard while v4.37+ lets an app stack
overflow walk into the kernel heap unnoticed.

Keep VALID|region on empty slots (RASR stays 0, so the region is simply
disabled). ARMv8-M selects the region through RNR and the aliases and is
unaffected.

Observed on qemu_flint: with the App thread running, region 6 read back
as RBAR=0x6 RASR=0 while the thread's saved MPU words held the guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com>
be4d98d gave App/Worker syscalls their own 2 KiB privileged stack but
only on ARMv8-M, because the relocation bounded that stack with PSPLIM.
ARMv7-M apps kept running the whole privileged call chain on their 2 KiB
task stack, and asterix reboots with StackOverflow / HardFault when a
text-heavy watchface renders a glyph that misses the cache: text layout
-> text_resources -> sys_resource_load_range -> resource_storage ->
pfs_open -> FTL -> flash consumes ~1.3 KB on top of whatever the app
already used, and the overflow happens privileged, so the kernel cannot
recover and resets.

Enable the relocation everywhere. The kernel side already handles the
missing PSPLIM; on ARMv7-M the syscall stacks get a 32 B no-access MPU
region below them instead, programmed through the free fourth per-task
slot (MemoryRegion_Task4). The fault handler treats a hit in that region
like the other stack guards, so a genuine kernel-side over-budget still
reboots with StackOverflow, while an app that runs out of its own stack
now faults unprivileged and is killed.

Verified on qemu_flint with a stress app that burns N bytes of stack and
then draws text in a custom font: with the guard alone, 512 B of extra
app usage reboots the emulator (the device failure mode); with this
change the same load succeeds (app peak 1968/2016 B, syscall stack peak
608 B) and 768 B only kills the app. WeatherGraph from the app store runs
unchanged.

Fixes FIRM-4306

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com>
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>
@gmarull
gmarull force-pushed the syscall-stack-armv7m branch from 1cdd8e5 to 718586d Compare September 16, 2026 14:17
@gmarull
gmarull merged commit 20185c8 into coredevices:main Sep 16, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants