xtensa/esp32s3: Run user processes in a world they cannot escape - #19798
Open
casaroli wants to merge 22 commits into
Open
xtensa/esp32s3: Run user processes in a world they cannot escape#19798casaroli wants to merge 22 commits into
casaroli wants to merge 22 commits into
Conversation
Xtensa selected neither fork primitive, so vfork() was simply absent. This wires it onto the two-primitive semantics. There is no assembly entry point and none is needed. Every exception entry already runs SPILL_ALL_WINDOWS, so the whole context of the calling thread is in its exception frame and copying its stack copies a complete frame chain. A flat build reaches that frame through SYS_save_context, issued inline so that the recorded stack pointer belongs to a frame that stays alive for the whole operation; a build with syscalls reaches it through xcp.sregs, recorded by xtensa_swint() for the duration of the call. The stack copy needs more than a relocated stack pointer here. A windowed ABI stores each frame's caller stack pointer absolutely, in the base save area below the frame, so a copy taken at a different address still names the parent throughout and the child's first retw would underflow onto the parent's stack. xtensa_fork_rebase() walks that chain and adds the relocation offset to each link. The copy also starts one base save area below the stack pointer rather than at it, because the frame the child resumes into keeps its caller's spilled a0-a3 there. Ported from the per-architecture work, reduced to the two primitives. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
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>
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>
casaroli
force-pushed
the
esp32s3-kernel-full
branch
from
August 11, 2026 20:06
c2ff339 to
5add8b3
Compare
casaroli
marked this pull request as ready for review
August 11, 2026 20:13
casaroli
requested review from
Donny9,
acassis,
eren-terzioglu,
fdcavalcanti,
jerpelea and
tmedicci
as code owners
August 11, 2026 20:13
|
Contributor
Author
|
@tmedicci @igrr @xiaoxiang781216 what do you think? |
This was referenced Aug 11, 2026
casaroli
marked this pull request as draft
August 11, 2026 20:22
casaroli
marked this pull request as ready for review
August 11, 2026 20:22
🔗 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
The change has four parts.
vfork()on Xtensa.up_fork(bool vfork)is the architecture half of the primitive of #19562.The child gets its own stack that holds a copy of the part of the parent stack that is in use, and it resumes where the caller would have returned.
The windowed ABI needs care in one place: a stack pointer has a base save area 16 bytes below it that holds the spilled
a0-a3of the caller, and the frame chain runs through it, so the chain is walked and rebased onto the copy.BUILD_KERNELon Xtensa.A kernel build needs a kernel stack for a user thread, a signal trampoline that returns through a system call, an initial register state that starts a user task at the right privilege, and the system call paths between the two worlds.
mm/pgallocgets 32 KB and 64 KB pages, because the Espressif cache MMU has a fixed 64 KB page for external flash and 32 KB for external RAM.The address environment.
It is built on the cache MMU of the SoC and not on anything in the Xtensa core.
The core gives only coarse 512 MB regions with no paging, so the per-process mapping comes from the MMU that maps external flash and PSRAM.
This is why the code is chip code: a non-Espressif LX7 would share none of it.
The page pool is not kept mapped in the kernel address space.
It is carved out of the PSRAM that the user processes run from, and the external memory permissions are indexed by physical address, so a kernel window on the pool would be a window on the memory of every process.
The kernel reaches a page through a small scratch region that is mapped for one operation and invalidated after it.
The privilege boundary.
PMS was programmed only from
esp32s3_userspace.c, which is built for a protected build alone.A kernel build therefore never programmed the split lines, never entered World 1, and never installed the monitor interrupt, so a user process could read and write kernel memory and reach the registers that control the mapping.
The world split now lives in
esp32s3_isolation.c, which is built for any build that is not flat.esp32s3_start()programs the worlds and the permissions beforenx_start().An access that no MMU entry translates is reported as well.
The PMS grants and refuses physical addresses, so it never sees an untranslated access: the cache answered it with zeros, and the task continued with a value it never should have had.
EXTMEM_MMU_ENTRY_FAULTis enabled and the Cache Invalid Access interrupt goes to the same handler, which reads the cause before it clears the latch.An unprivileged task that makes any of these accesses is terminated with SIGSEGV.
A privileged one still panics.
Impact
A flat build does not change.
A protected build keeps the same permissions.
The code that sets them moved to a file that both builds use.
mm/pgallocgains two page sizes and changes no default.MM_PGSIZEis read only whenCONFIG_MM_PGALLOCis set.The ESP32-S3 gains two kernel configurations,
kernel_octandkernel_n8r2.fork()is available in a kernel build only, because it needs an address environment.vfork()is available in every build mode.Testing
Board: ESP32-S3-DevKitC with an ESP32-S3-WROOM-2 N32R8V, 32 MB octal flash and 8 MB PSRAM.
Host: macOS 15 on Apple Silicon,
xtensa-esp32s3-elf-gcc12.2.0.Configuration:
esp32s3-devkit:kernel_oct.fork() and vfork()
ostestruns to the end with no assertion and no panic.The boundary
Every target of
examples/sandboxcarries the outcome it expects, so the test fails a build that refuses everything as well as one that permits everything.selfis the control: it touches memory the process owns and must be allowed.0x600c5000isDR_REG_MMU_TABLE, the registers that hold the mapping.Resources of a killed process
The offending process allocates 64 KiB, writes to all of it, and opens a file before it makes the access.
It still holds both when it is killed.
The count rises while the offender lives and returns to the same value after it is reaped.
The test reports a failure if the count never rises, because a number that does not move proves nothing.
The whole run was made three times, which is twelve process deaths, with the same result each time.
tools/checkpatch.sh -c -u -m -greports no errors.depends-on: apache/nuttx-apps/pull/3721