Skip to content

[4/4] xtensa/esp32s3: Run user processes in a world they cannot escape - #19798

Merged
acassis merged 5 commits into
apache:masterfrom
casaroli:esp32s3-kernel-full
Sep 29, 2026
Merged

acassis merged 5 commits into
apache:masterfrom
casaroli:esp32s3-kernel-full

Conversation

@casaroli

@casaroli casaroli commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

depends-on: [apache/nuttx-apps/pull/3721]

Summary

Part 4 of 4. This is the part that actually contains a user process.

#19795, #19796 and #19797 are merged: the ESP32-S3 has a kernel build, per-process address environments and fork(). They do not give protection. PMS was programmed only from esp32s3_userspace.c, built under CONFIG_BUILD_PROTECTED alone, so a kernel build never split the worlds, never entered World 1 and never installed the monitor interrupt. A user process could read and write kernel memory and reach the registers that control its own mapping.

This moves the world split into esp32s3_isolation.c, built for any build that is not flat, and esp32s3_start() programs the worlds and the permissions before nx_start().

It also reports an access that no MMU entry translates. PMS grants and refuses physical addresses, so it never saw one: the cache answered with zeros and the task carried on with a value it should never have had. EXTMEM_MMU_ENTRY_FAULT is enabled and the Cache Invalid Access interrupt goes to the same handler, which reads the cause before clearing the latch.

An unprivileged task that makes either kind of access is killed with SIGSEGV. A privileged one still panics.

The branch is rebased on master, so it carries only its own five commits.

Impact

A flat build does not change. A protected build keeps the same permissions; only the file that sets them moved.

A kernel build on the ESP32-S3 becomes a security boundary, which it was not before.

Testing

ESP32-S3-DevKitC, WROOM-2 N32R8V, esp32s3-devkit:kernel_oct. ostest runs to the end.

examples/sandbox carries the expected outcome per target, so it fails a build that refuses everything as well as one that permits everything. self is the control:

sandbox: target self -- this process's own data, expecting success
sandbox: PASS - the allowed access completed
sandbox: target kernel -- kernel memory at 0x3fc98000, expecting a fault
pms_violation_isr: SIGSEGV (PMS) task /system/bin/sandbox
sandbox: target periph -- a peripheral register at 0x600c5000, expecting a fault
pms_violation_isr: SIGSEGV (PMS) task /system/bin/sandbox
sandbox: target unmapped -- an address with no mapping at 0x3d800000, expecting a fault
pms_violation_isr: SIGSEGV (MMU entry) task /system/bin/sandbox
sandbox: CONTAINED - 4 target(s), every check passed

0x600c5000 is DR_REG_MMU_TABLE, the registers that hold the mapping.

A killed process gives back what it held. It allocates 64 KiB and opens files before faulting; memory goes 1441792 -> 2162688 -> 1441792 and no descriptor is left open. The test fails if the number never rises, because one that does not move proves nothing.

Three runs, twelve process deaths, same result each time.

tools/checkpatch.sh -c -u -m -g passes.

@casaroli
casaroli force-pushed the esp32s3-kernel-full branch from c2ff339 to 5add8b3 Compare August 11, 2026 20:06
@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation Arch: xtensa Issues related to the Xtensa architecture Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces. Board: xtensa labels Aug 11, 2026
@casaroli casaroli changed the title xtensa/esp32s3: Isolate the unprivileged world in a kernel build xtensa/esp32s3: Add a kernel build with fork() and user mode isolation Aug 11, 2026
@casaroli casaroli changed the title xtensa/esp32s3: Add a kernel build with fork() and user mode isolation xtensa/esp32s3: Add POSIX fork() and hardware enforced process isolation Aug 11, 2026
@casaroli casaroli changed the title xtensa/esp32s3: Add POSIX fork() and hardware enforced process isolation xtensa/esp32s3: Run user processes in a world they cannot escape Aug 11, 2026
@casaroli
casaroli marked this pull request as ready for review August 11, 2026 20:13
@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@casaroli

casaroli commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor Author

@tmedicci @igrr @xiaoxiang781216 what do you think?

@github-actions

Copy link
Copy Markdown

🔗 Cross-repo PR dependencies

The read-only Build run reported the following dependent PR(s) and fetched head SHA(s):

CI run: https://github.com/apache/nuttx/actions/runs/31531219719

@xiaoxiang781216

Copy link
Copy Markdown
Contributor

Summary

This gives the ESP32-S3 a kernel build: fork(), a per-process address environment, and a privilege boundary between the kernel and a user process.

It replaces #19795, #19796 and #19797, which are the same work in three parts.

Why not let reviewer continue on #19795, #19796 and #19797? it's better to split your huge change into the small and self-contained pr, otherwise any concern will block your whole change to merge.

@casaroli

Copy link
Copy Markdown
Contributor Author

Summary

This gives the ESP32-S3 a kernel build: fork(), a per-process address environment, and a privilege boundary between the kernel and a user process.
It replaces #19795, #19796 and #19797, which are the same work in three parts.

Why not let reviewer continue on #19795, #19796 and #19797? it's better to split your huge change into the small and self-contained pr, otherwise any concern will block your whole change to merge.

I agree to split, however, i'd like to keep a PR with all the changes so they can test the final intended result.

I updated the PR body to not mention that this pr replaces the others

@xiaoxiang781216

Copy link
Copy Markdown
Contributor

Summary

This gives the ESP32-S3 a kernel build: fork(), a per-process address environment, and a privilege boundary between the kernel and a user process.
It replaces #19795, #19796 and #19797, which are the same work in three parts.

Why not let reviewer continue on #19795, #19796 and #19797? it's better to split your huge change into the small and self-contained pr, otherwise any concern will block your whole change to merge.

I agree to split, however, i'd like to keep a PR with all the changes so they can test the final intended result.

Yes, no problem. you can change this pr to draft for your testing and restore other to ready for review, so the maintainer could know which pr to review.

@casaroli
casaroli marked this pull request as draft August 14, 2026 09:22
@casaroli casaroli closed this Aug 14, 2026
@casaroli casaroli reopened this Aug 14, 2026
@github-actions

Copy link
Copy Markdown

❌ Cross-repo dependency could not be applied

The Build report says the declared dependency PR(s) could not be applied, so CI did not run against the combined code:

Reason: cherry-pick failed (if your PR has merge commits, rebase instead)

CI run: https://github.com/apache/nuttx/actions/runs/31835808454

@casaroli
casaroli force-pushed the esp32s3-kernel-full branch from 5add8b3 to 80665b5 Compare August 28, 2026 17:53
@casaroli casaroli changed the title xtensa/esp32s3: Run user processes in a world they cannot escape [4/4] xtensa/esp32s3: Run user processes in a world they cannot escape Aug 28, 2026
@github-actions

Copy link
Copy Markdown

❌ Cross-repo dependency could not be applied

The Build report says the declared dependency PR(s) could not be applied, so CI did not run against the combined code:

Reason: cherry-pick failed (if your PR has merge commits, rebase instead)

CI run: https://github.com/apache/nuttx/actions/runs/33196805203

@github-actions github-actions Bot removed the Area: Documentation Improvements or additions to documentation label Sep 26, 2026
@github-actions

Copy link
Copy Markdown

❌ Cross-repo dependency could not be applied

The Build report says the declared dependency PR(s) could not be applied, so CI did not run against the combined code:

Reason: cherry-pick failed (if your PR has merge commits, rebase instead)

CI run: https://github.com/apache/nuttx/actions/runs/36277511578

@casaroli
casaroli marked this pull request as ready for review September 26, 2026 22:52
Comment thread arch/xtensa/src/esp32s3/esp32s3_isolation.c Outdated
Comment thread arch/xtensa/src/esp32s3/esp32s3_isolation.c
Comment thread arch/xtensa/src/esp32s3/esp32s3_wcl.c
@github-actions

Copy link
Copy Markdown

🔗 Cross-repo PR dependencies

The read-only Build run reported the following dependent PR(s) and fetched head SHA(s):

CI run: https://github.com/apache/nuttx/actions/runs/36392888270

Comment thread arch/xtensa/src/esp32s3/esp32s3_userspace.c Outdated
Comment thread arch/xtensa/src/esp32s3/esp32s3_userfault.c
Separate the world split from the protected user image, give WORLD1 its own
vector table and its own PMS permissions -- including the PSRAM -- clean up
the user cache-MMU windows, and stop keeping the page pool mapped.

Folds in:
  xtensa/esp32s3: separate the world split from the protected user image
  xtensa/esp32s3: give the unprivileged world its own vector table
  xtensa/esp32s3: give the unprivileged world its permissions
  xtensa/esp32s3: clean up the user cache-MMU windows
  xtensa/esp32s3: stop keeping the page pool mapped
  xtensa/esp32s3: give the PSRAM its own PMS permissions

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@github-actions

Copy link
Copy Markdown

🔗 Cross-repo PR dependencies

The read-only Build run reported the following dependent PR(s) and fetched head SHA(s):

CI run: https://github.com/apache/nuttx/actions/runs/36436011900

When an unprivileged task takes a fault the system cannot recover from, it
now gets a fatal SIGSEGV and only that task ends.  A fault in privileged code
still panics.

What decides it is the interrupted context, not the cause: the saved PS says
whether the fault was taken in User Mode.  A list of causes would leave every
cause off the list as a way for a user task to stop the machine, and there
are many -- a divide by zero, a privileged instruction, a load/store error,
and an illegal instruction, which is how a refused fetch from kernel text
arrives on this chip (TRM v1.8 p.699: a denied external-memory access is
answered with 0xdeadbeaf instead of trapping).  PS.UM is clear in a kernel
thread, in a system call made on the user's behalf and in an interrupt
handler, so those still panic.  If the recoverable-fault dispatcher is
enabled it still gets first refusal on causes 28, 29 and 20, the only ones
re-executing can help.

esp32s3_userfault_abort() records the exception frame as the task's context,
dispatches SIGSEGV, and returns the redirected frame, so the vector's RFE
resumes the task in the signal trampoline, whose default action exits it.
CONFIG_ESP32S3_USERFAULT_ABORT enables it, default y wherever there is an
unprivileged world, and selects SIG_DEFAULT and SIG_SIGKILL_ACTION.

Verified on an ESP32-S3 DevKitC with a WROOM-2 module,
esp32s3-devkit:kernel_oct: a user task that writes through NULL, reads a wild
address, divides by zero, calls into a buffer of garbage or branches into
kernel text is terminated on its own, while an unrelated task keeps running.

Stack overflow is not contained.  On the windowed ABI it faults inside the
window overflow handler and arrives as a double exception with PS.UM already
clear; guard pages are the answer, and separate work.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Reporting a fault can itself fault.  syslog reaches memory the fault being
reported may have made unreachable, so esp32s3_pagefault_dispatch() is
re-entered from inside its own _alert() and never returns, and the console
fills with the same half-printed line forever.  Found under Espressif's QEMU,
where PSRAM never initialises and the kernel build needs it; the board's
PSRAM works, so hardware does not take this path.

A fault repeating at the same address and PC is not helped by reporting it
again, so the dispatcher tries three times and then halts with interrupts
off.  esp32s3_userfault_abort() clears the count through
esp32s3_pagefault_clear_repeat(): reaching it means the fault was contained,
so only unbroken recursion stops the machine, and three probes at one
address do not halt a healthy system.

Verified under QEMU: 12,958,521 bytes of output in 60 s before, four reports
and a halt after.  On an ESP32-S3 DevKitC, esp32s3-devkit:kernel_oct, three
identical sandbox probes in one boot are all contained.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The PMS grants and refuses physical addresses, so it never sees an access
that no MMU entry translates.  The cache answered such an access with zeros
and raised nothing, and the task carried on with a value it never should
have had.

Enable EXTMEM_MMU_ENTRY_FAULT and route the Cache Invalid Access interrupt
to the handler that already serves the PMS monitors.  An unprivileged task
that makes the access is terminated with SIGSEGV;  a privileged one still
panics.  The latch is level triggered, so it is cleared with the others.

Read the cause before the clear, so the log tells the two apart:  a PMS
violation is a refused translation, an MMU entry fault is an access that was
never translated.

Give the kernel_oct configuration the addresses that examples/sandbox needs
to name its targets.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Two files each defined the same 16 byte constant, KSTACK_ALIGNMENT and
SIGTRAMP_STACK_ALIGN.  Use STACKFRAME_ALIGN, which arch/xtensa/include/irq.h
already gives as 16, with the STACKFRAME_ALIGN_DOWN() of nuttx/irq.h.

STACK_ALIGNMENT is not the name to use here.  It is TLS_STACK_ALIGN when
CONFIG_TLS_ALIGNED is set, which is the alignment of a thread stack and not
of a frame.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@github-actions

Copy link
Copy Markdown

🔗 Cross-repo PR dependencies

The read-only Build run reported the following dependent PR(s) and fetched head SHA(s):

CI run: https://github.com/apache/nuttx/actions/runs/36475333009

@acassis
acassis merged commit 1ccd940 into apache:master Sep 29, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: xtensa Issues related to the Xtensa architecture Board: xtensa Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants