arch/xtensa: Provide vfork() and fork(), and BUILD_KERNEL on the ESP32-S3 - #19772
arch/xtensa: Provide vfork() and fork(), and BUILD_KERNEL on the ESP32-S3#19772casaroli wants to merge 11 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>
…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>
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>
|
done 🫡 |
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>
|
@tmedicci can you please take a look? |
| * about to be started with. | ||
| */ | ||
|
|
||
| xcp->regs = (void *)((uint32_t)xcp->ktopstk - XCPTCONTEXT_SIZE); |
There was a problem hiding this comment.
change ALL uint32_t cast to uintptr_t
|
|
||
| config ARCH_HAVE_FORK | ||
| bool | ||
| default y if ARCH_XTENSA && BUILD_KERNEL |
There was a problem hiding this comment.
let's select under ARCH_XTENSA
| * | ||
| ****************************************************************************/ | ||
|
|
||
| int up_addrenv_fork(const arch_addrenv_t *src, arch_addrenv_t *dest) |
There was a problem hiding this comment.
could we move to common and generalize it instead
There was a problem hiding this comment.
the ESP32 has no instruction-bus path to external RAM, so no page-backed text. Looks like this is specific to esp32s3 (and maybe possibly esp32s2)
|
|
||
| if ((uintptr_t)CONFIG_ARCH_PGPOOL_VBASE < ramstart || | ||
| (uintptr_t)CONFIG_ARCH_PGPOOL_VEND > ramend) | ||
| if (ESP32S3_PGPOOL_PBASE < rampbase || |
There was a problem hiding this comment.
why not continue use the general Kconfig
| ****************************************************************************/ | ||
|
|
||
| #define _SIGTRAMP_STR(x) #x | ||
| #define _SIGTRAMP_XSTR(x) _SIGTRAMP_STR(x) |
There was a problem hiding this comment.
use macro from nuttx/macro.h
|
|
||
| #include "xtensa.h" | ||
|
|
||
| #if defined(CONFIG_ARCH_ADDRENV) && defined(CONFIG_ARCH_KERNEL_STACK) |
| /* The Xtensa windowed ABI requires 16-byte stack alignment */ | ||
|
|
||
| #define KSTACK_ALIGNMENT 16 | ||
| #define KSTACK_ALIGN_DOWN(a) ((a) & ~(KSTACK_ALIGNMENT - 1)) |
There was a problem hiding this comment.
why not use STACK_ALIGNMENT from xtensa_internal.h
There was a problem hiding this comment.
I didn't find any xtensa_internal.h. I didn't find any STACK_ALIGNMENT in arch/. Are you talking about KSTACK_ALIGNMENT in arm_internal.h?
| * | ||
| ****************************************************************************/ | ||
|
|
||
| void up_allocate_pgheap(void **heap_start, size_t *heap_size) |
There was a problem hiding this comment.
can we move the most code into common folder
There was a problem hiding this comment.
LX6 cannot host a page pool at all. Maybe we can reuse this for esp32s2, but I am still not sure
There was a problem hiding this comment.
This MMU seems to be specific to espressif, not xtensa, so maybe the correct home would be arch/xtensa/src/common/espressif/ if we can confirm esp32s2 can use it?
There was a problem hiding this comment.
yes, let's move
- arch common code to common
- lx? specific code to lx?
- esp32 specific code to arch/xtensa/src/common/espressif/
| @@ -0,0 +1,488 @@ | |||
| /**************************************************************************** | |||
| * arch/xtensa/src/common/xtensa_fork.c | |||
There was a problem hiding this comment.
could you split the pr into small ones:
- vfork
- mmu in common arch code
- code in esp32
Review of apache#19772 asked for this shape, and it applies to every architecture in the series. ARCH_HAVE_FORK described when it was available from inside its own definition, which put the per-architecture condition somewhere nobody looks. The architecture now says so itself. The condition repeats both dependencies rather than relying on them, because a select bypasses depends on. RISC-V also excludes dynamic stacks, so that is named here too. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Review of apache#19772 asked for this shape, and it applies to every architecture in the series. ARCH_HAVE_FORK described when it was available from inside its own definition, which put the per-architecture condition somewhere nobody looks. The architecture now says so itself. The condition repeats the ARCH_ADDRENV dependency rather than relying on it, because a select bypasses depends on: without that repetition an architecture could offer fork() where there is no address environment to duplicate. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Review of apache#19772 asked for this shape, and it applies to every architecture in the series. ARCH_HAVE_FORK described when it was available from inside its own definition, which put the per-architecture condition somewhere nobody looks. The architecture now says so itself. The condition repeats the ARCH_ADDRENV dependency rather than relying on it, because a select bypasses depends on: without that repetition an architecture could offer fork() where there is no address environment to duplicate. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Review of apache#19772 asked for this shape, and it applies to every architecture in the series. ARCH_HAVE_FORK described when it was available from inside its own definition, which put the per-architecture condition somewhere nobody looks. The architecture now says so itself, next to the other things ARMv7-A provides. The condition repeats the ARCH_ADDRENV dependency rather than relying on it, because a select bypasses depends on: without that repetition an architecture could offer fork() where there is no address environment to duplicate. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
| * Note: ESP32-S3 PMS (World Controller) memory-protection violations are | ||
| * NOT delivered as these precise causes; they raise the asynchronous | ||
| * DRAM0/IRAM0 PMS-monitor interrupt instead (handled elsewhere). |
There was a problem hiding this comment.
Could you please clarify where is "elsewhere"?
Could you please also mention, what is the observed outcome (on ESP32-S3) when a process tries to access:
- An internal RAM region to which access hasn't been granted
- A PSRAM page to which access hasn't been granted
There was a problem hiding this comment.
After looking at the code a bit more, I think the answers seem to be:
- no fault is generated, user process can freely read and write kernel RAM
- no fault is generated, reads silently return zero
Not sure it this would be expected behavior in NuttX.
| # BUILD_KERNEL address-environment remap. | ||
|
|
||
| ifneq ($(filter y,$(CONFIG_BUILD_PROTECTED) $(CONFIG_ARCH_ADDRENV)),) | ||
| CHIP_CSRCS += esp32s3_mmu.c esp32s3_pms.c esp32s3_wcl.c |
There was a problem hiding this comment.
The PMS primitives are compiled in case of CONFIG_ARCH_ADDRENV, but they only seem to be called from esp32s3_userspace.c, which is included only if CONFIG_BUILD_PROTECTED=y.
So right now in case of BUILD_KERNEL, PMS doesn't get enabled, and user processes can freely access kernel memory.
(Seems like esp32s3_pmsirqinitialize doesn't get called in case of BUILD_KERNEL, either)
| } | ||
|
|
||
| /**************************************************************************** | ||
| * Name: esp32s3_wcl_set_world0_entry |
There was a problem hiding this comment.
Can you provide a complete description? (please check other functions)
| * | ||
| ****************************************************************************/ | ||
|
|
||
| int up_addrenv_select(const arch_addrenv_t *addrenv) |
There was a problem hiding this comment.
Although a single address environment is enabled at a time, nothing prevents the userspace process from accessing the MMU registers and changing the mapping. Additionally, there is no protection against accessing peripherals from the user process. That being said, can't we use WCL and PMS to prevent that? Userspace applications should never access peripherals (MMU mapping included) directly. That would bring better process isolation and make it adherent to NuttX's userspace.
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>
|
@igrr @tmedicci Thank you for the comments. You are correct. I have the fix for this (we will rework and revisit, but the PoC worked). I can include here, however I was already asked to split this PR (which I did, please check #19795, #19796, #19797), and to prevent this from becoming larger, i propose we just make fork() work first and then we add the isolation next (as part of #19797 or a follow up PR). It is worth mentioning that the other arches that implement I want to add it, and I also want your review on how we will handle the fault and kill only the offending process, so the system does not go down (I have a PoC of that working, but I believe we should go step-by-step). If you prefer, we can put everything back together in a single PR. I will put everything together in a branch (maybe a draft PR) so we can review all of this together. Thank you again for the comments. |
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>
|
@igrr @tmedicci @xiaoxiang781216 @acassis please check the endgame at #19798 |
Summary
Xtensa selected neither fork primitive.
fork()andvfork()were not available on any Xtensa target. This pull request gives the architecture both, and adds the kernel-build support thatfork()needs.vfork()needs no assembly entry point on this architecture. Every exception entry already runsSPILL_ALL_WINDOWS, so the whole context of the caller is in its exception frame. A flat build reaches that frame withSYS_save_context, issued inline. A build with system calls reaches it withxcp.sregs, whichxtensa_swint()records.A stack copy needs more than a new stack pointer here. A windowed ABI keeps the stack pointer of the caller in the base save area below each frame, so a copy at a different address still names the parent.
xtensa_fork_rebase()walks that chain and adds the offset. The copy starts one base save area below the stack pointer, because the frame that the child resumes into keeps the spilled a0 to a3 of its caller there.fork()needs per-process address environments, so the ESP32-S3 getsBUILD_KERNELsupport. The chip has a 64 KiB MMU page, andpgalloc.hstopped at 16 KiB, somm/pgalloclearns 32 KiB and 64 KiB pages.up_addrenv_fork()then duplicates an address environment into fresh pages at the same virtual addresses.The page pool is not kept mapped into the kernel address space. It 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. The kernel reaches a pool page through a small scratch region instead, mapped for one operation. Two slots are sufficient, because the deepest user is
up_addrenv_fork(), which holds a source page and a destination page at once.Impact
The change is specific to Xtensa. It adds capability and removes none.
vfork()becomes available on Xtensa in all build modes. Code that testsCONFIG_ARCH_HAVE_VFORKnow finds it on this architecture.fork()becomes available on the ESP32-S3 in a kernel build only. That is the only mode with address environments.CONFIG_ARCH_HAVE_FORKstays unset everywhere else.Two board configurations are new:
kernel_octfor a WROOM-2 with octal flash, andkernel_n8r2for a module with quad flash and 2 MB of PSRAM. Both were run on the hardware they name. No existing configuration changes.The
mm/pgalloccommit touches shared code. It adds two cases to a preprocessor selection and changes no existing case.Testing
Board: ESP32-S3-DevKitC. Two modules, an ESP32-S3-WROOM-2 N32R8V with 32 MB octal flash and 8 MB octal PSRAM, and an ESP32-S3-WROOM-1 N8R2 with 8 MB quad flash and 2 MB embedded PSRAM.
Board: ESP32-DevKitC V4, with an ESP32-D0WD-V3 revision 3.1. This part has an LX6 core, so the change is now tested on both Xtensa cores that NuttX supports.
Host: macOS 15 on Apple Silicon,
xtensa-esp32s3-elf-gccandxtensa-esp32-elf-gcc, both 12.2.0.esp32s3-devkit:ostestvfork()passes, status 0esp32s3-devkit:knshvfork()passes, status 0esp32s3-devkit:kernel_octvfork()andfork()pass, status 0esp32s3-devkit:ostestvfork()passesesp32s3-devkit:kernel_n8r2vfork()andfork()pass, status 0esp32-devkitc:ostestvfork()passes, status 0tools/checkpatch.sh -c -u -m -ggives no errors.The protected row needs #19764, which lets a protected build boot from simple boot. It is not necessary for the flat or the kernel rows.
Notes for a reviewer
kernel_n8r2is sized tightly on purpose. Each of the text, data and heap regions is 2 pages of 64 KiB, so a process takes 384 KiB and afork()peaks at 768 KiB.ostesthas 115 KiB of text against a 128 KiB text region. A larger program needs a part with more PSRAM, not a larger pool. A module with 8 MB of PSRAM can keep the roomy defaults, askernel_octdoes.QIO flash does not boot on the N8R2 that I have, in any configuration, and the cause is outside this patch.
flash_qio_modeis not linked into the image, so the QE bit of the flash is never set. The quad configurations therefore use DIO.I have not tested any Xtensa target other than the ESP32-S3.
vfork()should work on the ESP32 and the ESP32-S2 without change, because the code is inarch/xtensa/src/common, but I have no board. A report from one would be welcome.