Skip to content

arch/armv7-a: Implement up_addrenv_fork() and provide POSIX fork() - #19775

Draft
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:armv7a-fork
Draft

arch/armv7-a: Implement up_addrenv_fork() and provide POSIX fork()#19775
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:armv7a-fork

Conversation

@casaroli

Copy link
Copy Markdown
Contributor

Draft. The evidence is emulation only. I keep this as a draft until fork() runs on armv7-a hardware.

Summary

fork() was withdrawn from every architecture by #19562, and each architecture restores it with the correct behaviour. This pull request restores it for armv7-a.

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 page by page and copied into fresh pages hung off the L1 and L2 tables of the child.

The saved-syscall-frame half is already in master. #19562 added it, so arm_syscall() stores the exception frame of the caller in xcp.sregs and arm_fork() builds the child from it.

The change that reaches beyond fork()

up_initial_state() now puts the register save area of a user process on its kernel stack, not at the top of its user stack. RISC-V and ARM64 already do this, so armv7-a is being brought into line.

Two reasons. The area holds the CPSR that the thread is resumed with, so it must not be user-writable. And a fork() child inherits the stack address of its parent, so the top of "its" stack is occupied by the live frames of the parent, including the exception frame that the child is built from. Zeroing XCPTCONTEXT_SIZE bytes there would destroy both.

arm_fork() then lets the child run at the stack addresses of the parent. A pointer to a stack local taken before fork() must name the same object in the child that it named in the parent, so the child adopts the stack geometry of the parent rather than a relocated copy. With the save area on the kernel stack there is nothing left for arm_fork_syscall() to re-point.

This touches every task on the architecture, not only forked ones. It is the part of this patch that most needs review.

Impact

The change is specific to armv7-a. It adds capability and removes none.

fork() becomes available on armv7-a in a kernel build over an MMU. ARCH_HAVE_FORK keeps depends on ARCH_ADDRENV, so a configuration without address environments is unaffected. A protected build is excluded.

Cortex-M and Cortex-R are untouched, because BUILD_KERNEL needs ARCH_USE_MMU.

vfork() does not change. No board configuration changes.

Testing

Emulation only. QEMU 11.0.3 on macOS 15 with Apple Silicon, Arm GNU arm-none-eabi-gcc 14.2.rel1.

configuration mode result
qemu-armv7a:knsh kernel vfork() and fork() pass, ostest exits 0
qemu-armv7a:nsh flat vfork() passes, fork() correctly absent
qemu-system-arm -cpu cortex-a7 -nographic -machine virt,highmem=off \
  -net none -semihosting -kernel nuttx

The kernel row needs -semihosting, because a kernel build loads its applications over hostfs.

user_main: vfork() test
vfork_test: Child 6 ran and exited before the parent resumed
user_main: fork() test
fork_test: Child running independently (child)
fork_test: Parent and child had independent memory
ostest_main: Exiting with status 0

The flat row is there on purpose. up_initial_state() affects every task, so a flat build had to be checked as well, even though it has no fork().

tools/checkpatch.sh -c -u -m -g gives no errors.

What is not tested

No armv7-a hardware. This is the reason for the draft status. A board with an MMU, such as an i.MX6 or a SAMA5, would be the useful report.

The protected build is not tested. fork() is excluded from it in any case.

A report from an armv7-a board is welcome. Take this branch with apache/nuttx-apps#3685, build a kernel configuration and run ostest. The fork tests are the first output.

Duplicate an address environment into freshly allocated pages mapped at the
same virtual addresses, which is what POSIX fork() is built on.  The .text,
.data and heap regions of the source are walked page by page and copied into
fresh pages hung off the child's own L1 and L2 tables.

Two things go with it.

up_initial_state() now puts a user process's register save area on its kernel
stack rather than at the top of its user stack, which is what risc-v and arm64
already do.  The area must not be user-writable -- it holds the CPSR the
thread is resumed with -- and, more to the point here, a fork() child inherits
the parent's stack address, so the top of "its" stack is occupied by the
parent's live frames, including the exception frame the child is built from.
Zeroing XCPTCONTEXT_SIZE bytes there would destroy both.

arm_fork() then lets the child run at the parent's stack addresses.  A pointer
to a stack local taken before fork() must name the same object in the child
that it named in the parent, so the child adopts the parent's stack geometry
rather than being given a relocated copy; the parent's stack is already in the
duplicate, at the parent's address, with its contents.  With the save area on
the kernel stack there is nothing left for arm_fork_syscall() to re-point.

Verified on qemu-armv7a:knsh under qemu-system-arm:  ostest's fork_test
reports "Parent and child had independent memory", and vfork_test passes.
qemu-armv7a:nsh is unchanged, with vfork() passing and fork() correctly
absent.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation Arch: arm Issues related to ARM (32-bit) architecture Size: M The size of the change in this PR is medium labels Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

qemu-armv8a

  • Code: .text.romfs_cachenode +4 B (+0.0%, 316,720 B)

s698pm-dkit

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Area: Documentation Improvements or additions to documentation Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant