examples: Containment tests for a protected build and a kernel build - #3721
Draft
casaroli wants to merge 4 commits into
Draft
examples: Containment tests for a protected build and a kernel build#3721casaroli wants to merge 4 commits into
casaroli wants to merge 4 commits into
Conversation
ostest's "vfork" test was never testing vfork(). It has the child write a global and the parent observe the write -- the defining property of *sharing*, not of vfork(), whose defining property is that the parent is suspended and whose contract forbids the child to write anything at all. It passed because NuttX implemented fork() and vfork() as the same sharing primitive, which apache/nuttx#19562 separates. vfork.c is rewritten to test what vfork() promises. The child does only what POSIX permits -- it calls _exit(42) and nothing else, not even exit(), which would run atexit handlers and flush stdio in the parent's address space. Since the child may not write memory and the parent cannot run while the child lives, the observable is the child's exit status: had the parent not been suspended, it would have reached waitpid() while the child was still alive. Where child status is not retained -- ostest_main() sets SA_NOCLDWAIT for the whole run, deliberately -- ECHILD is accepted as equally good evidence, since it says the child was already gone when the parent asked. fork.c is new and tests POSIX fork(): the child's writes to .data, .bss and the heap are invisible to the parent and vice versa, a pointer to a stack local taken before the fork names the same object in both, and the child does everything a vfork() child may not -- calls malloc() and printf(), and returns from the function that called fork(). Both run at the top of user_main(). They exercise the lowest-level machinery in the suite -- address environments, stack setup, the architecture's register context -- so a fault in one takes the process down instead of reporting a failure. Learning that in seconds rather than after everything else has passed matters when a port is being brought up. Each test gates on the one primitive it tests, ARCH_HAVE_VFORK and ARCH_HAVE_FORK respectively. There is no compatibility layer and no mapping between symbols. vfork.c no longer requires SCHED_WAITPID: the suspension is in the kernel primitive now, so the test's core assertion holds without it and only the status check is conditional. The other in-tree callers are audited for which primitive they actually meant: * interpreters/python's _posixsubprocess and netutils/libwebsockets' LWS_HAVE_WORKING_VFORK want the fork-then-exec path -- ARCH_HAVE_VFORK. * python's os.fork() and libwebsockets' LWS_HAVE_FORK mean real fork() and stay on ARCH_HAVE_FORK, so they become *absent* rather than silently wrong. * testing/fs/fdsantest's vfork case follows ARCH_HAVE_VFORK. interpreters/bas is deliberately left alone. Its SHELL and EDIT statements reach for vfork() under an ARCH_HAVE_FORK guard and want the same treatment, but checkpatch.sh checks the whole of any file a patch touches and bas_statement.c produces 1681 pre-existing findings against master, so a one-line change there fails CI on its own. The consequence is small: EXAMPLES_BAS_SHELL is EXPERIMENTAL and already depends on ARCH_HAVE_FORK, so it becomes unselectable rather than misbehaving. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
A small user-space task that reads or writes an arbitrary address, used to exercise the ESP32-S3 recoverable-fault path on target. With CONFIG_ESP32S3_PAGEFAULT[_SELFTEST] enabled on the kernel side, "pffault r 0x80000000" drives the precise-fault -> RFE-restart proof; a bad address such as "pffault r 0x0" exercises the dispatcher's report/decline path. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit bad8c4f) (cherry picked from commit 4e93ce3) Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
examples/pffault already probes the kernel/user boundary, but only on one chip: it hard-codes 0x3fc98000, the base of the ESP32-S3 kernel DRAM region, and is written in PMS/WORLD1 terms. It says nothing on any other architecture. This is the architecture-neutral version. The address it touches is derived rather than hard-coded: every BUILD_PROTECTED configuration in the tree places the kernel blob below the user blob, with the boundary at CONFIG_NUTTX_USERSPACE, so the word just below that belongs to the kernel on all of them -- 0x00200000 on qemu-armv7a:pnsh, 0x41000000 on qemu-armv8a:pnsh, 0x80040000 on rv-virt:pnsh[64], 0x10200000 on mps2-an521:knsh, 0x10100000 on pimoroni-pico-2-plus:pnsh. Whether that address holds kernel code, kernel data or nothing mapped at all does not matter; an unprivileged task must not be able to read it either way. The test is self-checking rather than a bare crash. It starts a canary task, spawns a second task to make the forbidden access, waits for that task, and then asserts three separate things: the offender died, the caller is still running, and the canary is still counting. That last one is what distinguishes "the offender was contained" from "the whole system stopped", which a test that only observes its own survival cannot tell apart. A flat build has no boundary to escape from, and no CONFIG_NUTTX_USERSPACE either; there the test says there is nothing to contain rather than reporting a pass it did not earn. `sandbox escape [r|w] [addr]` is the one-shot form, which faults in the calling task, for use under a debugger. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com> (cherry picked from commit e8f4272)
The test spawned the offender with task_create(), which a kernel build does not give user code, so it did not link there. Spawn a process instead, which works in a protected build and in a kernel build. Give every target the outcome it expects. A build that refuses everything is as wrong as one that permits everything, and only a target that must succeed can tell them apart, so "self" touches memory the process owns. Take the addresses from Kconfig. A user process cannot see kernel symbols, because that is the boundary under test, so a board supplies them. A protected build still derives the kernel target from CONFIG_NUTTX_USERSPACE. A kernel build has no such address, because each process has its own address environment; saying "this is a flat build" there was wrong. Add the peripheral and unmapped targets. An MMU keeps processes apart but does not stop one reaching a peripheral, and an unmapped access is refused by a different mechanism again, so neither is covered by the kernel target. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
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
Two test programs for a protected build and a kernel build.
examples/pffaulttouches one kernel address from a user process.It is small and direct, and it shows the fault and the signal.
examples/sandboxis the general test.Every target carries the outcome it expects, so the test fails a build that refuses everything as well as one that permits everything.
selfkernelperiphunmappedThe
selftarget is the control.Without it a build that refuses every access passes every other check.
The offending process allocates memory, writes to all of it, and opens a file before it makes the access.
It still holds both when it dies.
The test reads
/proc/meminfobefore, while the offender lives, and after it is reaped, and it reads/proc/<pid>/group/fdwhile the offender lives.A count that never rises is reported as a failure, because "the same before and after" says nothing if the memory was never seen.
The offender is a process and not a task.
A kernel build does not give
task_create()to user code, soposix_spawn()is used.testing/ostestgets separatefork()andvfork()tests.Impact
Three new example programs and one test change.
No existing configuration selects them.
Testing
Board: ESP32-S3-DevKitC with an ESP32-S3-WROOM-2 N32R8V, 32 MB octal flash and 8 MB PSRAM.
Configuration:
esp32s3-devkit:kernel_oct, aBUILD_KERNELimage.The run above was made three times, which is twelve process deaths.
The memory returns to the same value each time, and the descriptors are closed each time.
ostestruns to the end on the same image.tools/checkpatch.sh -c -u -m -greports no errors.