[4/4] xtensa/esp32s3: Run user processes in a world they cannot escape - #19798
[4/4] xtensa/esp32s3: Run user processes in a world they cannot escape#19798casaroli wants to merge 22 commits into
Conversation
c2ff339 to
5add8b3
Compare
|
@tmedicci @igrr @xiaoxiang781216 what do you think? |
🔗 Cross-repo PR dependenciesThe 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 |
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 |
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. |
❌ Cross-repo dependency could not be appliedThe 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 |
Add CONFIG_MM_PGSIZE == 32768 and 65536 to the page-size switch (and the Kconfig help text). The 64 KB size matches the ESP32-S3 cache-MMU page granularity, so an address-environment port there can use one mm_pgalloc() page per cache-MMU page (naturally 64 KB-aligned by the granule allocator) instead of coalescing several smaller pages. Inert for existing configs: MM_PGSIZE is only used when CONFIG_MM_PGALLOC is enabled (BUILD_KERNEL). Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Add what a kernel build needs on Xtensa: a crt0 for a user process, the kernel stack allocation that a system call switches to, the syscall entry and return path for an unprivileged caller, and the initial register state that starts a user task at EL0 with its save area on the kernel stack. On the ESP32-S3 the arch code that runs while the flash mapping is in flux moves to IRAM, and the kernel heap is placed above the user .bss so that up_allocate_kheap() and the user address environment do not overlap. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
…t B) Route the precise cache-attribute permission faults -- Load/Store/InstrFetch Prohibited (EXCCAUSE 28/29/20) -- from xtensa_user() to a new dispatcher, esp32s3_pagefault_dispatch(). On a serviced fault the register frame is returned so the exception vector's RFE re-executes the faulting instruction; otherwise it declines to the existing panic path. Gated by CONFIG_ESP32S3_PAGEFAULT (default n, depends on BUILD_PROTECTED); the build is unchanged when the option is off. This is the recoverable-fault primitive the address-environment / demand-paging work builds on. Proven on the ESP32-S3-DevKitC WROOM-2: - A precise LoadProhibited carries a tracking EXCVADDR (the exact faulting address), and RFE cleanly re-executes the faulted load on return -- verified with CONFIG_ESP32S3_PAGEFAULT_SELFTEST (the identical instruction restarts three times, then steps past, and the task resumes with the shell alive). - ESP32-S3 PMS (World Controller) permission violations are NOT delivered as these precise causes; they raise the asynchronous DRAM0/IRAM0 PMS-monitor interrupt, so PMS is an isolation (kill) mechanism, not a restartable one. No regression: esp32s3-devkit:knsh (WROOM-2) boots to nsh and ostest passes with the option enabled. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The protected kernel linker (kernel-space.ld) placed the octal (OPI) flash bring-up helpers -- esp_rom_spiflash / esp_rom_opiflash_*, spi_flash_oct_flash_init, mmu_hal, mspi_timing_*, bootloader_flash*, efuse_hal/efuse_utility, esp_mmu_map and esp32s3_spi_timing -- in mapped flash. During configure_cpu_caches() / spi_flash_init_chip_state() in __start these run while the flash mapping is being reconfigured, which faults (illegal instruction) on octal-flash modules such as the ESP32-S3-WROOM-2. Quad-flash parts never exercise the OPI path, so the problem was latent. Place those functions in .iram0.text (mirroring the flat sections script) so they are safe to execute during flash reconfiguration. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Give the ESP32-S3 the arch_addrenv_t machinery that BUILD_KERNEL needs: a per-process page directory built from the 64 KiB MMU pages of the chip, with allocation, teardown, and the vaddr-to-paddr translation that the kernel uses to reach a user buffer. The MMU, PMS and WCL primitives are exposed as an arch API first, because the address environment code and the protected user split both need them and neither owns them. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
kernel_oct, with the user-program layout and the boot ROMFS a kernel build loads its programs from. The ROMFS placeholder is rebuilt with the image, the generated copy is ignored, and the programs are given stack sizes and room for a fork() child. Folds in: esp32s3-devkit: user-program layout and boot ROMFS for kernel builds boards/esp32s3-devkit: add a kernel-build configuration boards/esp32s3-devkit: give kernel_oct's programs their stacks back esp32s3-devkit: ignore the generated boot ROMFS boards/esp32s3-devkit: rebuild the ROMFS placeholder with the image boards/esp32s3-devkit: leave kernel_oct room for a fork() child Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com> Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The page pool is carved out of the PSRAM that user processes run from, and the external memory permissions are indexed by physical address, so a permanent kernel window onto the pool is a window onto every process, which no permission setting can close. Stop mapping the pool. The kernel reaches a pool page through a small scratch region instead, mapped for one operation and invalidated afterwards. esp32s3_pgmap() takes a slot, esp32s3_pgunmap() releases it, and ARCH_KMAP_VBASE and ARCH_KMAP_NPAGES describe the region. Two slots are enough, because the deepest user is up_addrenv_fork(), which holds a source and a destination page at once. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
up_addrenv_fork() duplicates an address environment into freshly allocated pages mapped at the same virtual addresses. The text, data and heap regions of the source are walked one page at a time and copied into fresh pages hung off the child's own directory, using the two kmap slots that CONFIG_ARCH_KMAP_NPAGES reserves for exactly this. xtensa_fork.c already took both paths: a child that keeps the parent's stack addresses needs no relocation, which is what a duplicated address environment gives it. Only the hook and the Kconfig default were missing. fork() is offered on a kernel build, which is the only mode with per-process address environments. Verified on an ESP32-S3-WROOM-2 with esp32s3-devkit:kernel_oct. ostest reports "Parent and child had independent memory" and exits with status 0. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
kernel_oct targets a WROOM-2 N32R8V: octal flash, and 8 MB of PSRAM for the page pool. The defaults size the pool for that part, with 8 pages of 64 KiB for each of the text, data and heap regions, so 1.5 MB per process. fork() duplicates the address environment, so a parent and a child need 3 MB at once and a module with 2 MB of PSRAM cannot do it. kernel_n8r2 sizes the same build for such a module. Each region is 2 pages, so a process takes 384 KiB and a fork() peaks at 768 KiB, inside a 1.5 MB pool placed at 0x80000 to leave the start of the PSRAM alone. The flash is quad and runs in DIO mode, so this configuration also exercises the CONFIG_ESP32S3_FLASH_MODE_OCT guard in kernel-space.ld from the quad side, which kernel_oct cannot. This is tight by construction. ostest has 115 KiB of text against a 128 KiB text region. A larger program needs a module with more PSRAM, not a larger pool. Verified on an ESP32-S3-DevKitC with an N8R2 module, 8 MB flash in DIO mode and 2 MB of embedded quad PSRAM. ostest reports "Parent and child had independent memory" and exits with status 0. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Describe kernel_oct and kernel_n8r2 next to the other configurations of this board. The entry for kernel_oct carries what a user needs and cannot guess: a KERNEL build is the only mode with fork() on this chip, the page pool is reached through a scratch mapping rather than a permanent window, the ROMFS is linked into the kernel image so a change to an application needs the whole export-import-mkromfsimg-relink chain, how to confirm that the ROMFS is really in the image, and that the shell needs the full path of a program. The entry for kernel_n8r2 states its limit. Each region of a process is 2 pages, ostest has 115 KiB of text against a 128 KiB text region, and a larger program needs a module with more PSRAM. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Two points from the review of apache#19772 that belong with the ESP32-S3 work. ARCH_HAVE_FORK is now selected by the architecture rather than defaulted from inside its own definition, so the condition sits where a reader of arch/Kconfig will look for it. It repeats the ARCH_ADDRENV dependency, because a select bypasses depends on and without that an architecture could offer fork() with no address environment to duplicate. The page pool no longer carries chip-specific copies of settings the common address environment already defines. ARCH_PGPOOL_PBASE and ARCH_PGPOOL_SIZE were only reachable under ARCH_PGPOOL_MAPPING, which does not apply here: the pool is deliberately left unmapped, because it is carved out of the PSRAM the user processes run from and the external memory permissions are indexed by physical address. But a physical base and a size describe the pool whether or not it is mapped -- only a virtual base needs the mapping -- so those two move out of that block and the chip uses them. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The common Xtensa BUILD_KERNEL support needs the chip to say what it can do and where its memory goes. The chip selects the address environment options it now implements, keeps the kernel and user heaps apart, and the linker scripts separate kernel from user text and data so the two worlds can be given different permissions. Split out of the same change as the common code, so that arch/xtensa/src/common can be reviewed without the chip in the way. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
CI feeds nxstyle the diff hunks with three lines of context, so a style error anywhere near a hunk fails the check job. Five are this series' own, in esp32s3_addrenv.c, esp32s3_pgalloc.c and esp32s3_userspace.c: a declaration with no blank line after it, and a bare scope block indented one level too deep. Two in esp32s3_bringup.c are older than the series and are in the lines the kernel configurations touch. Whitespace only, no change in behaviour. Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
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 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com> Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
When an unprivileged (WORLD1) task takes an unrecoverable cache-attribute fault (Load/Store/InstrFetch Prohibited), deliver a fatal SIGSEGV to just that task instead of panicking the whole system; kernel-mode faults still panic. Gated by CONFIG_ESP32S3_PAGEFAULT_ABORT (default y under ESP32S3_PAGEFAULT), which selects SIG_DEFAULT + SIG_SIGKILL_ACTION so the signal's default action terminates the task. esp32s3_pagefault_abort() mirrors the interrupt-dispatch handshake: it records the exception frame as the task context, dispatches SIGSEGV (which redirects the task to the signal trampoline via up_schedule_sigaction()), and returns the redirected frame so the vector's RFE resumes the task in the trampoline, whose default action _exit()s it and reschedules. No kernel stack is required, so this works on the existing protected configs. Verified on the ESP32-S3-DevKitC WROOM-2: - "pffault r 0x0" / "pffault w 0x0" (NULL read/write, EXCCAUSE 28/29) terminate only the pffault task; nsh stays interactive. - Repeatable with no memory leak (free unchanged after 8 aborts) and no zombie tasks (ps shows none lingering). - No regression: ostest exits with status 0 and the RFE-restart self-test still passes. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit b160d7ad844aed8589b1c3cb399252c44284cf6a) Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
A denied external-memory access does not trap on this chip. TRM v1.8 p.699: an access without permission is "responded with 0 (for internal memory) or 0xdeadbeaf (for external memory)". So when an unprivileged task branches into kernel text -- in flash or in PSRAM -- the fetch is refused silently, the CPU executes the dummy word it was handed, and the refusal arrives at xtensa_user() as EXCCAUSE_ILLEGAL at the address that was branched to. It never arrives as EXCCAUSE_INSTR_PROHIBITED, which is what the existing recoverable path looks for. The result was that a correctly refused fetch panicked the system. Killing just the offender is the right answer however the illegal instruction arose -- a refused fetch or simply a corrupt user binary -- because a user task running garbage must not take the system down with it. It is not offered to esp32s3_pagefault_dispatch() first: re-executing cannot help, the instruction genuinely is not there. The User Mode bit in the interruptee's saved PS keeps this to unprivileged faults; an illegal instruction in the kernel still panics. Verified on an ESP32-S3 DevKitC with a WROOM-2 module (octal flash, 8 MB octal PSRAM), esp32s3-devkit:kernel_oct. examples/sandbox aimed at real kernel .text, both modes, at the same address: sandbox r 0x42011014 -> pms_violation_isr: SIGSEGV (PMS) sandbox x 0x42011014 -> esp32s3_pagefault_abort: SIGSEGV EXCCAUSE=0 held (read,call): 2 of 2 Before this commit the execute probe produced xtensa_user_panic and a full crash dump. Note the probe must be given an explicit address: this board's CONFIG_RAM_START is 0x20000000, which is not a kernel region at all. Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com> Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 08304b4f6f28a8a57579b2a054d1b3d391564b09)
Reporting a fault can itself fault. syslog reaches memory that the very fault being reported may have made unreachable, so esp32s3_pagefault_dispatch() is re-entered from inside its own _alert() and never returns. The console fills with the same line severed part-way through EXCVADDR, forever, and nothing legible ever reaches it. Found by running esp32s3-devkit:kernel_oct under Espressif's QEMU, where PSRAM never initialises and the kernel build needs it -- the pgalloc pool that backs every user process lives there. 13 MB of half-printed lines in 60 s, no NSH, no way to see what went wrong. Hardware does not show this: the board's PSRAM works, so the path is never taken. Bound it. A fault repeating at the same address and PC is not going to be helped by reporting it again, so try three times and then halt with interrupts off. The lines may still be truncated -- the print is what faults, so it cannot be made to complete from here -- but a handful of severed lines followed by silence is diagnosable, and an endless stream of them is not. The counter is cleared in esp32s3_pagefault_abort(). Reaching there means the fault was contained and the system carried on, so only *unbroken* recursion should stop the machine; without the reset, a probe run three times at one address would trip the guard and halt a perfectly healthy board. That is the case verified on hardware below, and it is the reason the reset exists. Verified both ways. Under QEMU, where the report does fault: before: 12,958,521 bytes in 60 s, unbounded after: 1,567 bytes in 45 s, four reports then halt On an ESP32-S3 DevKitC with a WROOM-2 module, esp32s3-devkit:kernel_oct, three identical probes in a single boot -- one "Booting NuttX", no reset: sandbox r 0x42011014 x3 -> CONTAINED - the sandbox held (3 of 3) Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com> Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 222003254e4e0793e5593ef91ba3b2b2db1e14d4)
The user-exception handler recovered from three causes -- Load, Store and InstrFetch Prohibited -- plus an illegal instruction, and panicked on everything else. Gating on a list of causes means every cause left off the list is a way for an unprivileged task to stop the machine, and a task has plenty: a divide by zero (EXCCAUSE 6), a privileged instruction (8), a load/store error (3), an instruction-fetch error (2), a data access to an address the bus will not carry (9). None of these involve a boundary at all; they are things a correctly confined application does to itself. Measured on an ESP32-S3-WROOM-2, kernel build, before this change: a user task dividing by zero takes the whole system down, silently. So gate on the interruptee instead. The dispatcher still gets first refusal on 28/29/20, which are the precise, restartable causes and the only ones re-executing can help. Anything it does not service now goes to the abort path if the saved PS says the fault was taken in User Mode. That bit is also what excludes the cases with no safe task to kill: a kernel thread, a fault inside a system call made on the user's behalf, and a fault while handling an interrupt all run with PS.UM clear and still panic. This is the same shape riscv_fault_handler() has, where the cause is used for the message only. The abort itself moves to esp32s3_userfault.c and stops depending on the recoverable-fault dispatcher. CONFIG_ESP32S3_PAGEFAULT_ABORT was a child of CONFIG_ESP32S3_PAGEFAULT, which is default n, so "do not let a rogue task halt the machine" was unreachable unless you also opted into "retry the faulting instruction". Those are separate capabilities and the safety one is the one you always want, so it becomes CONFIG_ESP32S3_USERFAULT_ABORT, default y wherever there is an unprivileged world. A BUILD_PROTECTED configuration previously got neither. Verified on hardware with the new examples/misbehave. A user task that writes through NULL (29), reads a wild address (28), divides by zero (6) or calls into a buffer of garbage (20) is terminated on its own, with an unrelated task still counting either side of it. Two limits worth recording rather than hiding: * Stack overflow is not contained and does not even report -- the console goes silent. On a windowed ABI the overflow faults inside the window overflow handler, so it arrives as a double exception with PS.UM already clear and no gate can see it. Guard pages are the answer and are what CONFIG_ESP32S3_PAGEFAULT exists to make possible; that is separate work. * A 32-bit load from an unaligned address does not trap on this core, so EXCCAUSE 9 is reachable by other means but not that one. Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com> Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 3c11f3793dbae918dc6ebe3e99cbb60f70648b3e)
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>
Each of these carried a name and nothing else. Give every one a description, its input parameters and its returned value, as asked in review. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The comment said "handled elsewhere" and did not say where. Name the handler and the function that installs it, and add the fault for an access that no MMU entry translates. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
5add8b3 to
80665b5
Compare
❌ Cross-repo dependency could not be appliedThe 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 |
depends-on: [/pull/19797 apache/nuttx-apps/pull/3721]
Summary
Part 4 of 4. This is the part that actually contains a user process.
#19795, #19796 and #19797 give the ESP32-S3 a kernel build, per-process address environments and
fork(). They do not give protection. PMS was programmed only fromesp32s3_userspace.c, built underCONFIG_BUILD_PROTECTEDalone, 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, andesp32s3_start()programs the worlds and the permissions beforenx_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_FAULTis 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.
This branch also carries #19795, #19796 and #19797 so the final state can be reviewed whole. Rebase it as they land.
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.ostestruns to the end.examples/sandboxcarries the expected outcome per target, so it fails a build that refuses everything as well as one that permits everything.selfis the control:0x600c5000isDR_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 -gpasses.