From bcedd88ac969ab70e56fd243e6cdb24a0839f81b Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Sun, 9 Aug 2026 15:41:55 +0200 Subject: [PATCH 1/3] arch/arm64: Implement up_addrenv_fork() and provide POSIX fork(). Duplicate an address environment into freshly allocated pages mapped at the same virtual addresses, which is what POSIX fork() is built on. It lives in arm64_addrenv_mmu.c: an MPU address environment is a set of protection regions over one physical address space, not a mapping that can be duplicated at the same virtual addresses, so ARCH_HAVE_FORK is conditioned on ARCH_USE_MMU and excludes the protected configurations. arm64_fork_stack() 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 a zero offset arm64_fork_reloc() is then the identity, so the register context needs no further special casing. Verified on qemu-armv8a:knsh under qemu-system-aarch64: ostest's fork_test reports "Parent and child had independent memory", and vfork_test passes. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli --- Documentation/guides/fork_vfork_migration.rst | 2 +- arch/Kconfig | 1 + arch/arm64/src/common/arm64_addrenv_mmu.c | 186 ++++++++++++++++++ arch/arm64/src/common/arm64_fork.c | 11 ++ 4 files changed, 199 insertions(+), 1 deletion(-) diff --git a/Documentation/guides/fork_vfork_migration.rst b/Documentation/guides/fork_vfork_migration.rst index fa9a305d3c8e6..4c49652024ab1 100644 --- a/Documentation/guides/fork_vfork_migration.rst +++ b/Documentation/guides/fork_vfork_migration.rst @@ -211,7 +211,7 @@ Known gaps complete -- ``addrenv_fork()``, the ``up_addrenv_fork()`` hook, the syscall, the libc wrapper and the ``ostest`` case -- so an architecture provides ``fork()`` by implementing ``up_addrenv_fork()`` and selecting ``CONFIG_ARCH_HAVE_FORK``, -with no further generic work. +with no further generic work. arm64 selects it today. **A windowed ABI needs its stack rebased, not just copied.** On Xtensa, giving a child a relocated copy of the parent's stack takes more than the copy: diff --git a/arch/Kconfig b/arch/Kconfig index 60039f1991e4e..be2326b6d984b 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -492,6 +492,7 @@ config ARCH_HAVE_VFORK config ARCH_HAVE_FORK bool + default y if ARCH_ARM64 && ARCH_USE_MMU && !BUILD_PROTECTED default n depends on ARCH_ADDRENV ---help--- diff --git a/arch/arm64/src/common/arm64_addrenv_mmu.c b/arch/arm64/src/common/arm64_addrenv_mmu.c index 5ea7e16187479..686ebb04ede7c 100644 --- a/arch/arm64/src/common/arm64_addrenv_mmu.c +++ b/arch/arm64/src/common/arm64_addrenv_mmu.c @@ -370,6 +370,33 @@ static inline bool vaddr_is_shm(uintptr_t vaddr) #endif } +#ifdef CONFIG_ARCH_HAVE_FORK +/**************************************************************************** + * Name: vaddr_is_text + * + * Description: + * Check if a vaddr is part of the .text area, which is mapped read/execute + * while everything else is mapped read/write. The two arms mirror exactly + * the two layouts up_addrenv_create() builds. + * + ****************************************************************************/ + +static inline bool vaddr_is_text(const arch_addrenv_t *addrenv, + uintptr_t vaddr) +{ +#if (CONFIG_ARCH_TEXT_VBASE != 0x0) && (CONFIG_ARCH_HEAP_VBASE != 0x0) + UNUSED(addrenv); + return vaddr >= CONFIG_ARCH_TEXT_VBASE && vaddr < ARCH_TEXT_VEND; +#else + /* Contiguous layout: the reserve sits below .text, and .data begins where + * .text ends. + */ + + return vaddr >= addrenv->textvbase && vaddr < addrenv->datavbase; +#endif +} +#endif /* CONFIG_ARCH_HAVE_FORK */ + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -526,6 +553,165 @@ int up_addrenv_create(size_t textsize, size_t datasize, size_t heapsize, return ret; } +#ifdef CONFIG_ARCH_HAVE_FORK +/**************************************************************************** + * Name: up_addrenv_fork + * + * Description: + * Duplicate an address environment for POSIX fork(). The destination gets + * its own page tables and its own physical pages, holding a copy of the + * source's contents and mapped at the same virtual addresses. + * + * The walk mirrors up_addrenv_destroy(): every final level page table the + * source has under its static tables is visited, and every page it maps is + * duplicated. The copy is eager -- there is no copy-on-write -- so this + * needs as much free page memory as the parent occupies. + * + * Input Parameters: + * src - The address environment to be duplicated. + * dest - The location to receive the duplicate. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int up_addrenv_fork(const arch_addrenv_t *src, arch_addrenv_t *dest) +{ + uintptr_t *sptprev; + uintptr_t *sptlast; + uintptr_t dptprev; + uintptr_t *dptlast; + uintptr_t paddr; + uintptr_t vaddr; + uintptr_t pgvaddr; + uintptr_t l0; + size_t pgsize; + int i; + int j; + int ret; + + DEBUGASSERT(src && dest); + + memset(dest, 0, sizeof(arch_addrenv_t)); + + /* Give the child its own static page tables and kernel mappings */ + + ret = create_spgtables(dest); + if (ret < 0) + { + berr("ERROR: Failed to create static page tables\n"); + goto errout; + } + + ret = copy_kernel_mappings(dest); + if (ret < 0) + { + berr("ERROR: Failed to copy kernel mappings to new environment\n"); + goto errout; + } + + /* The duplicate lives at the same virtual addresses as the original */ + + dest->textvbase = src->textvbase; + dest->datavbase = src->datavbase; + dest->heapvbase = src->heapvbase; + dest->heapsize = src->heapsize; + + l0 = mmu_get_base_pgt_level(); + dest->ttbr0 = mmu_ttbr_reg(dest->spgtables[l0], 0); + + /* Make sure the source's page tables are visible before walking them */ + + UP_MB(); + + vaddr = ARCH_ADDRENV_VBASE; + pgsize = mmu_get_region_size(MMU_PGT_LEVEL_MAX - 1); + sptprev = (uintptr_t *)arm64_pgvaddr(src->spgtables[ARCH_SPGTS - 1]); + dptprev = arm64_pgvaddr(dest->spgtables[ARCH_SPGTS - 1]); + + if (sptprev == NULL || dptprev == 0) + { + ret = -EINVAL; + goto errout; + } + + for (i = 0; i < ENTRIES_PER_PGT; i++, vaddr += pgsize) + { + sptlast = (uintptr_t *)arm64_pgvaddr(mmu_pte_to_paddr(sptprev[i])); + if (sptlast == NULL) + { + continue; + } + + /* Hook the static tables up for this address, then give the child its + * own final level page table here. + */ + + map_spgtables(dest, vaddr); + + paddr = mm_pgalloc(1); + if (!paddr) + { + ret = -ENOMEM; + goto errout; + } + + arm64_pgwipe(paddr); + mmu_ln_setentry(MMU_PGT_LEVEL_MAX - 1, dptprev, paddr, vaddr, + MMU_UPGT_FLAGS); + dptlast = (uintptr_t *)arm64_pgvaddr(paddr); + + for (j = 0; j < ENTRIES_PER_PGT; j++) + { + uintptr_t srcpage; + uintptr_t destpage; + + srcpage = mmu_pte_to_paddr(sptlast[j]); + if (!srcpage) + { + continue; + } + + pgvaddr = vaddr + ((uintptr_t)j << MM_PGSHIFT); + + if (vaddr_is_shm(pgvaddr)) + { + /* Shared memory stays shared across fork(). Map the very same + * page; up_addrenv_destroy() knows not to free SHM pages. + */ + + dptlast[j] = sptlast[j]; + continue; + } + + destpage = mm_pgalloc(1); + if (!destpage) + { + ret = -ENOMEM; + goto errout; + } + + memcpy((void *)arm64_pgvaddr(destpage), + (const void *)arm64_pgvaddr(srcpage), MM_PGSIZE); + + mmu_ln_setentry(MMU_PGT_LEVEL_MAX, (uintptr_t)dptlast, destpage, + pgvaddr, + vaddr_is_text(src, pgvaddr) ? MMU_UTEXT_FLAGS + : MMU_UDATA_FLAGS); + } + } + + UP_MB(); + + return OK; + +errout: + up_addrenv_destroy(dest); + return ret; +} +#endif /* CONFIG_ARCH_HAVE_FORK */ + /**************************************************************************** * Name: up_addrenv_destroy * diff --git a/arch/arm64/src/common/arm64_fork.c b/arch/arm64/src/common/arm64_fork.c index 3566c64ded810..f0e42d32d2ecd 100644 --- a/arch/arm64/src/common/arm64_fork.c +++ b/arch/arm64/src/common/arm64_fork.c @@ -86,6 +86,17 @@ static uint64_t arm64_fork_stack(struct tcb_s *parent, struct tcb_s *child, uint64_t stackutil; uint64_t newtop; + if (child->stack_base_ptr == parent->stack_base_ptr) + { + /* The child is running on the parent's stack addresses: a fork() + * child, which inherited them and already has its own copy of the + * contents in its duplicated address environment. There is nothing to + * copy and no offset to apply. + */ + + return 0; + } + /* How much of the parent's stack was utilized? The ARM uses a push-down * stack so that the current stack pointer should be lower than the * initial, adjusted stack pointer. The stack usage should be the From 8245545346d7ee8cc31a981076db0f7b476364e7 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 10 Aug 2026 22:03:13 +0200 Subject: [PATCH 2/3] Documentation/qemu-armv8a: Note fork() and the semihosting flag. The kernel mode section shows the QEMU command but does not say why -semihosting is there. Without it the guest traps in smh_call and stops in AppBringUp, which reads as a kernel defect and is not one. It cost me a session once. Also state that a kernel build is the only mode on this board with POSIX fork(), and that vfork() is available in every mode. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- .../platforms/arm64/qemu/boards/qemu-armv8a/index.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Documentation/platforms/arm64/qemu/boards/qemu-armv8a/index.rst b/Documentation/platforms/arm64/qemu/boards/qemu-armv8a/index.rst index c0dbb8a7fdd08..64f07a378a2df 100644 --- a/Documentation/platforms/arm64/qemu/boards/qemu-armv8a/index.rst +++ b/Documentation/platforms/arm64/qemu/boards/qemu-armv8a/index.rst @@ -424,6 +424,16 @@ Running with QEMU: -net none -chardev stdio,id=con,mux=on -serial chardev:con \ -mon chardev=con,mode=readline -kernel ./nuttx +``-semihosting`` is necessary. A kernel build loads its applications over +hostfs. Without the flag the guest traps in ``smh_call`` and stops in +``AppBringUp``, which looks like a kernel defect and is not one. + +A kernel build gives each process its own address environment. This is the +only build mode on this board that provides POSIX ``fork()``: the child +receives its own copy of the memory of the parent, at the same virtual +addresses. ``vfork()`` is available in every build mode. + + Inter-VM share memory Device (ivshmem) -------------------------------------- From 873e1460b48780535bd0cb35a66298539fd9d13f Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Tue, 11 Aug 2026 15:06:17 +0200 Subject: [PATCH 3/3] arch/arm64: Let the architecture select ARCH_HAVE_FORK. Review of #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) Signed-off-by: Marco Casaroli --- arch/Kconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/Kconfig b/arch/Kconfig index be2326b6d984b..74c2d9d5a4a85 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -26,6 +26,7 @@ config ARCH_ARM config ARCH_ARM64 bool "ARM64" select ALARM_ARCH + select ARCH_HAVE_FORK if BUILD_KERNEL && ARCH_ADDRENV select ARCH_64BIT select ARCH_HAVE_BACKTRACE select ARCH_HAVE_INTERRUPTSTACK @@ -492,7 +493,6 @@ config ARCH_HAVE_VFORK config ARCH_HAVE_FORK bool - default y if ARCH_ARM64 && ARCH_USE_MMU && !BUILD_PROTECTED default n depends on ARCH_ADDRENV ---help---