From 2a43c6dae99bc947930f36710aff8f8e3e3d16a5 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Fri, 7 Aug 2026 13:03:08 +0200 Subject: [PATCH 1/4] testing/ostest: Split the fork test into vfork and fork. 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 --- interpreters/python/Makefile | 10 +- netutils/libwebsockets/lws_config_private.h | 7 + testing/fs/fdsantest/fdsantest_simple.c | 4 + testing/ltp/Makefile | 2 +- testing/ostest/CMakeLists.txt | 8 +- testing/ostest/Makefile | 8 +- testing/ostest/fork.c | 276 ++++++++++++++++++++ testing/ostest/ostest.h | 8 +- testing/ostest/ostest_main.c | 23 +- testing/ostest/vfork.c | 66 +++-- 10 files changed, 377 insertions(+), 35 deletions(-) create mode 100644 testing/ostest/fork.c diff --git a/interpreters/python/Makefile b/interpreters/python/Makefile index bac4013f886..8909586cc30 100644 --- a/interpreters/python/Makefile +++ b/interpreters/python/Makefile @@ -114,6 +114,11 @@ ifeq ($(CONFIG_ARCH_HAVE_FORK),y) else @echo "export ac_cv_func_fork=\"no\"" >> $@ endif +ifneq ($(CONFIG_ARCH_HAVE_VFORK),) + @echo "export ac_cv_func_vfork=\"yes\"" >> $@ +else + @echo "export ac_cv_func_vfork=\"no\"" >> $@ +endif ifeq ($(CONFIG_SYSTEM_SYSTEM),y) @echo "export ac_cv_func_system=\"yes\"" >> $@ else @@ -135,7 +140,10 @@ endif $(SETUP_LOCAL): $(Q) ( cp $(SETUP_LOCAL).in $(SETUP_LOCAL)) -ifneq ($(CONFIG_ARCH_HAVE_FORK),y) +# _posixsubprocess is the fork-then-exec path, so vfork() is enough for it; +# os.fork() itself needs a real fork() and is governed by ac_cv_func_fork +# above. +ifeq ($(CONFIG_ARCH_HAVE_FORK)$(CONFIG_ARCH_HAVE_VFORK),) @echo "_posixsubprocess" >> $@ endif ifneq ($(CONFIG_LIBC_DLFCN),y) diff --git a/netutils/libwebsockets/lws_config_private.h b/netutils/libwebsockets/lws_config_private.h index 8f5170faff9..057baf73bb6 100644 --- a/netutils/libwebsockets/lws_config_private.h +++ b/netutils/libwebsockets/lws_config_private.h @@ -43,7 +43,10 @@ /* #undef USE_CYASSL */ /* Define to 1 if you have the `fork' function. */ + +#ifdef CONFIG_ARCH_HAVE_FORK #define LWS_HAVE_FORK +#endif #ifndef CONFIG_DISABLE_ENVIRON /* Define to 1 if you have the `getenv' function. */ @@ -111,10 +114,14 @@ /* #undef LWS_HAVE_VFORK_H */ /* Define to 1 if `fork' works. */ +#ifdef CONFIG_ARCH_HAVE_FORK #define LWS_HAVE_WORKING_FORK +#endif /* Define to 1 if `vfork' works. */ +#ifdef CONFIG_ARCH_HAVE_VFORK #define LWS_HAVE_WORKING_VFORK +#endif /* Define to 1 if execvpe() exists */ #define LWS_HAVE_EXECVPE diff --git a/testing/fs/fdsantest/fdsantest_simple.c b/testing/fs/fdsantest/fdsantest_simple.c index 128e2878bff..114f0aeeb82 100644 --- a/testing/fs/fdsantest/fdsantest_simple.c +++ b/testing/fs/fdsantest/fdsantest_simple.c @@ -96,6 +96,7 @@ static void test_case_overflow(void **state) assert_int_equal(open_count, close_count); } +#ifdef CONFIG_ARCH_HAVE_VFORK static void test_case_vfork(void **state) { int fd = open("/dev/null", O_RDONLY); @@ -112,6 +113,7 @@ static void test_case_vfork(void **state) android_fdsan_close_with_tag(fd, 0xbadc0de); } +#endif /**************************************************************************** * Public Functions @@ -129,7 +131,9 @@ int main(int argc, FAR char *argv[]) cmocka_unit_test(test_case_unowned_tagged_close), cmocka_unit_test(test_case_owned_tagged_close), cmocka_unit_test(test_case_overflow), +#ifdef CONFIG_ARCH_HAVE_VFORK cmocka_unit_test(test_case_vfork), +#endif }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/testing/ltp/Makefile b/testing/ltp/Makefile index cee750cd0a1..9a2513336d6 100644 --- a/testing/ltp/Makefile +++ b/testing/ltp/Makefile @@ -45,7 +45,7 @@ BLACKWORDS += "pthread_spin_trylock" endif # Where NuttX does not declare fork(), a test that calls it cannot be built. -# The pattern spares vfork() and task_fork(), which remain available. +# The pattern spares vfork(), and any identifier that ends in _fork(). ifeq ($(CONFIG_ARCH_HAVE_FORK),) BLACKWORDS += "[^v_]fork(" diff --git a/testing/ostest/CMakeLists.txt b/testing/ostest/CMakeLists.txt index a76b2447c00..8eb6516790e 100644 --- a/testing/ostest/CMakeLists.txt +++ b/testing/ostest/CMakeLists.txt @@ -143,10 +143,12 @@ if(CONFIG_TESTING_OSTEST) endif() endif() + if(CONFIG_ARCH_HAVE_VFORK) + list(APPEND SRCS vfork.c) + endif() + if(CONFIG_ARCH_HAVE_FORK) - if(CONFIG_SCHED_WAITPID) - list(APPEND SRCS vfork.c) - endif() + list(APPEND SRCS fork.c) endif() if(CONFIG_ARCH_SETJMP_H) diff --git a/testing/ostest/Makefile b/testing/ostest/Makefile index 4919a6204f9..c89b2466f4f 100644 --- a/testing/ostest/Makefile +++ b/testing/ostest/Makefile @@ -144,10 +144,14 @@ CSRCS += sigev_thread.c endif endif -ifeq ($(CONFIG_ARCH_HAVE_FORK),y) -ifeq ($(CONFIG_SCHED_WAITPID),y) +# Each test is built where the primitive it tests exists. + +ifeq ($(CONFIG_ARCH_HAVE_VFORK),y) CSRCS += vfork.c endif + +ifeq ($(CONFIG_ARCH_HAVE_FORK),y) +CSRCS += fork.c endif ifeq ($(CONFIG_ARCH_SETJMP_H),y) diff --git a/testing/ostest/fork.c b/testing/ostest/fork.c new file mode 100644 index 00000000000..9d2c3ae9137 --- /dev/null +++ b/testing/ostest/fork.c @@ -0,0 +1,276 @@ +/**************************************************************************** + * apps/testing/ostest/fork.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ostest.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define FORK_HEAPSIZE 256 +#define FORK_PARENTMARK 0x5a +#define FORK_CHILDMARK 0xa5 + +/* Distinct values written through a pointer to a stack local, to check that + * the child's stack is at the same virtual address as the parent's. + */ + +#define FORK_STACKPARENT 0x1234 +#define FORK_STACKCHILD 0x5678 + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +/* .data and .bss, which a fork() child must get its own copy of */ + +static volatile int g_forkdata = 1; +static volatile int g_forkbss; +static FAR unsigned char *g_forkheap; + +/* Pointer to a local in fork_test()'s frame, taken before fork(). In .data + * rather than on the stack: the compiler could rematerialise a local + * pointer from the current stack pointer, which would test nothing. + */ + +static FAR volatile int *g_forkstackptr; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: fork_child + * + * Description: + * Everything here is forbidden to a vfork() child and permitted to a + * fork() child. Returns the child's exit status. + * + ****************************************************************************/ + +static int fork_child(void) +{ + FAR char *scratch; + + /* The parent must see none of this. */ + + g_forkdata = 2; + g_forkbss = 2; + memset(g_forkheap, FORK_CHILDMARK, FORK_HEAPSIZE); + + /* A vfork() child may not call these; a fork() child may. */ + + scratch = malloc(64); + if (scratch == NULL) + { + printf("fork_test: ERROR Child could not malloc()\n"); + return 1; + } + + strlcpy(scratch, "child", 64); + printf("fork_test: Child running independently (%s)\n", scratch); + free(scratch); + + /* Give the parent time to make its own writes, so that if the two shared + * memory we would see the parent's values below rather than our own. + */ + + usleep(200 * 1000); + + if (g_forkdata != 2 || g_forkbss != 2) + { + printf("fork_test: ERROR Child saw the parent's writes: " + "data=%d bss=%d\n", g_forkdata, g_forkbss); + return 1; + } + + if (g_forkheap[0] != FORK_CHILDMARK || + g_forkheap[FORK_HEAPSIZE - 1] != FORK_CHILDMARK) + { + printf("fork_test: ERROR Child saw the parent's heap writes\n"); + return 1; + } + + return 0; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: fork_test + * + * Description: + * Verify the defining property of POSIX fork(): the child gets its own + * copy of the parent's memory, in both directions. That is the exact + * opposite of the sharing vfork() gives. Also verifies that none of + * vfork()'s restrictions apply. + * + ****************************************************************************/ + +int fork_test(void) +{ + pid_t pid; + int status = 0; + int ret = 0; + + /* volatile so it is really in memory and the compare is not folded. */ + + volatile int stackvar = FORK_STACKPARENT; + + printf("fork_test: Started\n"); + + /* Publish its address before forking. See g_forkstackptr. */ + + g_forkstackptr = &stackvar; + + g_forkdata = 1; + g_forkbss = 1; + + g_forkheap = malloc(FORK_HEAPSIZE); + if (g_forkheap == NULL) + { + printf("fork_test: ERROR Failed to allocate the heap probe\n"); + ASSERT(false); + return -1; + } + + memset(g_forkheap, FORK_PARENTMARK, FORK_HEAPSIZE); + + pid = fork(); + if (pid == 0) + { + /* The child's stack is at the parent's virtual addresses, so this + * pointer names the child's own live local. A relocated stack would + * write into the copy of the parent's instead. + */ + + *g_forkstackptr = FORK_STACKCHILD; + if (stackvar != FORK_STACKCHILD) + { + printf("fork_test: ERROR Child stack was relocated: wrote %d " + "through %p, local at %p reads %d\n", + FORK_STACKCHILD, g_forkstackptr, &stackvar, stackvar); + _exit(1); + } + + /* Returns from fork_child() and from this branch -- both illegal + * for a vfork() child. + */ + + _exit(fork_child()); + } + else if (pid < 0) + { + printf("fork_test: ERROR fork() failed: %d\n", errno); + free(g_forkheap); + ASSERT(false); + return -1; + } + + /* Parent runs concurrently: write now, check isolation afterwards. */ + + g_forkdata = 3; + g_forkbss = 3; + memset(g_forkheap, FORK_PARENTMARK, FORK_HEAPSIZE); + +#ifdef CONFIG_SCHED_WAITPID + /* Wait for the child to be done before comparing memory. waitpid() blocks + * on a child that is still alive whether or not its exit status will be + * retained, so this synchronises either way; ECHILD simply means the child + * had already finished, which is just as good. It is not a failure: + * ostest_main() sets SA_NOCLDWAIT on SIGCHLD for the whole run, so an + * exited child's status is not kept even where CONFIG_SCHED_CHILD_STATUS + * is enabled. + */ + + if (waitpid(pid, &status, 0) != pid && errno != ECHILD) + { + printf("fork_test: ERROR waitpid() failed: %d\n", errno); + free(g_forkheap); + ASSERT(false); + return -1; + } +#else + sleep(1); +#endif + + if (g_forkdata != 3 || g_forkbss != 3) + { + printf("fork_test: ERROR Parent saw the child's writes: " + "data=%d bss=%d (expected 3, 3)\n", g_forkdata, g_forkbss); + ret = -1; + } + + if (g_forkheap[0] != FORK_PARENTMARK || + g_forkheap[FORK_HEAPSIZE - 1] != FORK_PARENTMARK) + { + printf("fork_test: ERROR Parent saw the child's heap writes\n"); + ret = -1; + } + + /* The child wrote to the same stack address; the parent must not see it */ + + if (stackvar != FORK_STACKPARENT) + { + printf("fork_test: ERROR Parent saw the child's stack write: " + "%d (expected %d)\n", stackvar, FORK_STACKPARENT); + ret = -1; + } + +#ifdef CONFIG_SCHED_WAITPID + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) + { + printf("fork_test: ERROR Child reported failure, status 0x%04x\n", + status); + ret = -1; + } +#endif + + free(g_forkheap); + g_forkheap = NULL; + + if (ret < 0) + { + ASSERT(false); + return ret; + } + + printf("fork_test: Parent and child had independent memory\n"); + return 0; +} diff --git a/testing/ostest/ostest.h b/testing/ostest/ostest.h index 2be54de5a68..8fa6d64d0a9 100644 --- a/testing/ostest/ostest.h +++ b/testing/ostest/ostest.h @@ -284,10 +284,16 @@ void sched_lock_test(void); /* vfork.c ******************************************************************/ -#if defined(CONFIG_ARCH_HAVE_FORK) && defined(CONFIG_SCHED_WAITPID) +#ifdef CONFIG_ARCH_HAVE_VFORK int vfork_test(void); #endif +/* fork.c *******************************************************************/ + +#ifdef CONFIG_ARCH_HAVE_FORK +int fork_test(void); +#endif + /* setjmp.c *****************************************************************/ void setjmp_test(void); diff --git a/testing/ostest/ostest_main.c b/testing/ostest/ostest_main.c index 964771d907c..77ebe95acc7 100644 --- a/testing/ostest/ostest_main.c +++ b/testing/ostest/ostest_main.c @@ -224,6 +224,23 @@ static int user_main(int argc, char *argv[]) g_mmbefore = mallinfo(); g_mmprevious = g_mmbefore; + /* Run the two fork primitives first. They exercise the lowest-level + * machinery in the suite, so a fault takes the process down rather than + * reporting a failure -- better to learn that in seconds. + */ + +#ifdef CONFIG_ARCH_HAVE_VFORK + printf("\nuser_main: vfork() test\n"); + vfork_test(); + check_test_memory_usage(); +#endif + +#ifdef CONFIG_ARCH_HAVE_FORK + printf("\nuser_main: fork() test\n"); + fork_test(); + check_test_memory_usage(); +#endif + printf("\nuser_main: Begin argument test\n"); printf("user_main: Started with argc=%d\n", argc); @@ -637,12 +654,6 @@ static int user_main(int argc, char *argv[]) check_test_memory_usage(); #endif -#if defined(CONFIG_ARCH_HAVE_FORK) && defined(CONFIG_SCHED_WAITPID) && \ - !defined(CONFIG_ARCH_SIM) - printf("\nuser_main: vfork() test\n"); - vfork_test(); -#endif - #if defined(CONFIG_SMP) && defined(CONFIG_BUILD_FLAT) printf("\nuser_main: smp call test\n"); smp_call_test(); diff --git a/testing/ostest/vfork.c b/testing/ostest/vfork.c index bd6cfabadb9..473b19222bd 100644 --- a/testing/ostest/vfork.c +++ b/testing/ostest/vfork.c @@ -28,39 +28,41 @@ #include #include -#include #include #include +#include #include #include "ostest.h" -#if defined(CONFIG_ARCH_HAVE_FORK) && defined(CONFIG_SCHED_WAITPID) - /**************************************************************************** - * Private Data + * Public Functions ****************************************************************************/ -static volatile bool g_vforkchild; - /**************************************************************************** - * Public Functions + * Name: vfork_test + * + * Description: + * Verify the defining property of vfork(): the parent is suspended until + * the child _exit()s or exec()s. The child does only what POSIX + * permits -- _exit(), not exit(), which would flush stdio in the parent's + * address space. Since the child may not write memory, the observable is + * its exit status: an unsuspended parent would reach waitpid() first. + * ****************************************************************************/ int vfork_test(void) { pid_t pid; - g_vforkchild = false; + printf("vfork_test: Started\n"); + pid = vfork(); if (pid == 0) { - /* There is not very much that the child is permitted to do. Perhaps - * it can just set g_vforkchild. - */ + /* The only thing a vfork() child may do is leave. */ - g_vforkchild = true; - exit(0); + _exit(42); } else if (pid < 0) { @@ -68,22 +70,44 @@ int vfork_test(void) ASSERT(false); return -1; } - else + + /* Reached only once the child has exited or exec'ed. */ + +#ifdef CONFIG_SCHED_WAITPID { - sleep(1); - if (g_vforkchild) + int status = 0; + pid_t ret; + + ret = waitpid(pid, &status, 0); + + /* Two answers are correct, and which one comes back is a property of + * the configuration: a retained status must be exit(42), and ECHILD + * is equally good evidence -- it says the child was already gone when + * we asked. ostest_main() sets SA_NOCLDWAIT for the whole run, so + * testing CONFIG_SCHED_CHILD_STATUS alone is not enough. + */ + + if (ret == pid) { - printf("vfork_test: Child %d ran successfully\n", pid); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 42) + { + printf("vfork_test: ERROR Child %d status 0x%04x, expected " + "exit(42)\n", pid, status); + ASSERT(false); + return -1; + } } - else + else if (ret >= 0 || errno != ECHILD) { - printf("vfork_test: ERROR Child %d did not run\n", pid); + printf("vfork_test: ERROR waitpid() returned %d (%d), expected " + "the child's status or ECHILD\n", ret, errno); ASSERT(false); return -1; } } +#endif + printf("vfork_test: Child %d ran and exited before the parent resumed\n", + pid); return 0; } - -#endif /* CONFIG_ARCH_HAVE_FORK && CONFIG_SCHED_WAITPID */ From 58750d44d1bda0f19fdd0164532eb3301f193156 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Fri, 24 Jul 2026 23:39:54 +0200 Subject: [PATCH 2/4] examples/pffault: WORLD1 fault-injection tester (Unit B) 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) (cherry picked from commit bad8c4f030f287ea44a090841a41fa9218c0c1bf) (cherry picked from commit 4e93ce304291f0a4441abab0de8e980c92e2d6eb) Signed-off-by: Marco Casaroli --- examples/pffault/CMakeLists.txt | 33 ++++++++++++++ examples/pffault/Kconfig | 28 ++++++++++++ examples/pffault/Make.defs | 25 ++++++++++ examples/pffault/Makefile | 32 +++++++++++++ examples/pffault/pffault_main.c | 81 +++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 examples/pffault/CMakeLists.txt create mode 100644 examples/pffault/Kconfig create mode 100644 examples/pffault/Make.defs create mode 100644 examples/pffault/Makefile create mode 100644 examples/pffault/pffault_main.c diff --git a/examples/pffault/CMakeLists.txt b/examples/pffault/CMakeLists.txt new file mode 100644 index 00000000000..5352cab0d1f --- /dev/null +++ b/examples/pffault/CMakeLists.txt @@ -0,0 +1,33 @@ +# ############################################################################## +# apps/examples/pffault/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## + +if(CONFIG_EXAMPLES_PFFAULT) + nuttx_add_application( + NAME + ${CONFIG_EXAMPLES_PFFAULT_PROGNAME} + SRCS + pffault_main.c + STACKSIZE + ${CONFIG_EXAMPLES_PFFAULT_STACKSIZE} + PRIORITY + ${CONFIG_EXAMPLES_PFFAULT_PRIORITY}) +endif() diff --git a/examples/pffault/Kconfig b/examples/pffault/Kconfig new file mode 100644 index 00000000000..92a7ad94324 --- /dev/null +++ b/examples/pffault/Kconfig @@ -0,0 +1,28 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config EXAMPLES_PFFAULT + tristate "Page-fault / PMS isolation test" + default n + ---help--- + A user-space task that deliberately touches a kernel-space address to + exercise the ESP32-S3 PMS isolation boundary and the kernel's + recoverable-fault dispatcher. + +if EXAMPLES_PFFAULT + +config EXAMPLES_PFFAULT_PROGNAME + string "Program name" + default "pffault" + +config EXAMPLES_PFFAULT_PRIORITY + int "pffault task priority" + default 100 + +config EXAMPLES_PFFAULT_STACKSIZE + int "pffault stack size" + default DEFAULT_TASK_STACKSIZE + +endif diff --git a/examples/pffault/Make.defs b/examples/pffault/Make.defs new file mode 100644 index 00000000000..85e12bb87fb --- /dev/null +++ b/examples/pffault/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/examples/pffault/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_EXAMPLES_PFFAULT),) +CONFIGURED_APPS += $(APPDIR)/examples/pffault +endif diff --git a/examples/pffault/Makefile b/examples/pffault/Makefile new file mode 100644 index 00000000000..ab3cb25f414 --- /dev/null +++ b/examples/pffault/Makefile @@ -0,0 +1,32 @@ +############################################################################ +# apps/examples/pffault/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +PROGNAME = $(CONFIG_EXAMPLES_PFFAULT_PROGNAME) +PRIORITY = $(CONFIG_EXAMPLES_PFFAULT_PRIORITY) +STACKSIZE = $(CONFIG_EXAMPLES_PFFAULT_STACKSIZE) +MODULE = $(CONFIG_EXAMPLES_PFFAULT) + +MAINSRC = pffault_main.c + +include $(APPDIR)/Application.mk diff --git a/examples/pffault/pffault_main.c b/examples/pffault/pffault_main.c new file mode 100644 index 00000000000..ec90efcca71 --- /dev/null +++ b/examples/pffault/pffault_main.c @@ -0,0 +1,81 @@ +/**************************************************************************** + * apps/examples/pffault/pffault_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: main + * + * Description: + * Deliberately touch a kernel-space address from an unprivileged (WORLD1) + * user task to exercise the ESP32-S3 PMS isolation boundary. In a working + * protected build this raises a precise Load/StoreProhibited fault that + * the kernel's recoverable-fault dispatcher must handle (terminating just + * this task); the shell should survive. + * + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + /* Default target: the base of the kernel DRAM region, to which the user + * world has no PMS permission. A second argument "w" makes it a store. + */ + + volatile uint32_t *kaddr = (volatile uint32_t *)0x3fc98000; + bool store = (argc > 1 && argv[1][0] == 'w'); + + if (argc > 2) + { + kaddr = (volatile uint32_t *)strtoul(argv[2], NULL, 0); + } + + printf("pffault: user-space %s of kernel addr %p ...\n", + store ? "write" : "read", (void *)kaddr); + fflush(stdout); + + if (store) + { + *kaddr = 0xdeadbeef; /* Expect a precise StoreProhibited (WORLD1) */ + } + else + { + uint32_t v = *kaddr; /* Expect a precise LoadProhibited (WORLD1) */ + printf("pffault: SURVIVED unexpectedly, read %08lx\n", + (unsigned long)v); + } + + printf("pffault: returned from the faulting access (unexpected)\n"); + return 0; +} From ade0ebf7cfc2642cfedc653273818784996e1bc4 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 27 Jul 2026 07:52:50 +0200 Subject: [PATCH 3/4] examples/sandbox: a generic protected-build containment test 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) Signed-off-by: Marco Casaroli (cherry picked from commit e8f42721fc2b221d31823a434d8c0376b62631fc) --- examples/sandbox/CMakeLists.txt | 33 +++ examples/sandbox/Kconfig | 33 +++ examples/sandbox/Make.defs | 25 +++ examples/sandbox/Makefile | 32 +++ examples/sandbox/sandbox_main.c | 374 ++++++++++++++++++++++++++++++++ 5 files changed, 497 insertions(+) create mode 100644 examples/sandbox/CMakeLists.txt create mode 100644 examples/sandbox/Kconfig create mode 100644 examples/sandbox/Make.defs create mode 100644 examples/sandbox/Makefile create mode 100644 examples/sandbox/sandbox_main.c diff --git a/examples/sandbox/CMakeLists.txt b/examples/sandbox/CMakeLists.txt new file mode 100644 index 00000000000..10d902065f6 --- /dev/null +++ b/examples/sandbox/CMakeLists.txt @@ -0,0 +1,33 @@ +# ############################################################################## +# apps/examples/sandbox/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## + +if(CONFIG_EXAMPLES_SANDBOX) + nuttx_add_application( + NAME + ${CONFIG_EXAMPLES_SANDBOX_PROGNAME} + SRCS + sandbox_main.c + STACKSIZE + ${CONFIG_EXAMPLES_SANDBOX_STACKSIZE} + PRIORITY + ${CONFIG_EXAMPLES_SANDBOX_PRIORITY}) +endif() diff --git a/examples/sandbox/Kconfig b/examples/sandbox/Kconfig new file mode 100644 index 00000000000..7a1aef2fe72 --- /dev/null +++ b/examples/sandbox/Kconfig @@ -0,0 +1,33 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config EXAMPLES_SANDBOX + tristate "Protected-build sandbox containment test" + default n + ---help--- + A test that deliberately tries to escape the kernel/user boundary of + a protected or kernel build, and checks that the attempt is contained: + the offending task is terminated and everything else keeps running. + + The address it touches is derived from CONFIG_NUTTX_USERSPACE rather + than hard-coded, so the test is architecture-neutral. In a flat build + there is no boundary to escape from and the test says so instead of + reporting a pass. + +if EXAMPLES_SANDBOX + +config EXAMPLES_SANDBOX_PROGNAME + string "Program name" + default "sandbox" + +config EXAMPLES_SANDBOX_PRIORITY + int "sandbox task priority" + default 100 + +config EXAMPLES_SANDBOX_STACKSIZE + int "sandbox stack size" + default DEFAULT_TASK_STACKSIZE + +endif diff --git a/examples/sandbox/Make.defs b/examples/sandbox/Make.defs new file mode 100644 index 00000000000..5c315ca833e --- /dev/null +++ b/examples/sandbox/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/examples/sandbox/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_EXAMPLES_SANDBOX),) +CONFIGURED_APPS += $(APPDIR)/examples/sandbox +endif diff --git a/examples/sandbox/Makefile b/examples/sandbox/Makefile new file mode 100644 index 00000000000..861baf54599 --- /dev/null +++ b/examples/sandbox/Makefile @@ -0,0 +1,32 @@ +############################################################################ +# apps/examples/sandbox/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +PROGNAME = $(CONFIG_EXAMPLES_SANDBOX_PROGNAME) +PRIORITY = $(CONFIG_EXAMPLES_SANDBOX_PRIORITY) +STACKSIZE = $(CONFIG_EXAMPLES_SANDBOX_STACKSIZE) +MODULE = $(CONFIG_EXAMPLES_SANDBOX) + +MAINSRC = sandbox_main.c + +include $(APPDIR)/Application.mk diff --git a/examples/sandbox/sandbox_main.c b/examples/sandbox/sandbox_main.c new file mode 100644 index 00000000000..48d45182233 --- /dev/null +++ b/examples/sandbox/sandbox_main.c @@ -0,0 +1,374 @@ +/**************************************************************************** + * apps/examples/sandbox/sandbox_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Where to poke. + * + * The point of this test is to be architecture-neutral, so the address is + * derived rather than hard-coded. In every BUILD_PROTECTED configuration in + * the tree the kernel blob is placed *below* the user blob, and the boundary + * between them is exactly CONFIG_NUTTX_USERSPACE: + * + * qemu-armv7a:pnsh 0x00200000 mps2-an521:knsh 0x10200000 + * qemu-armv8a:pnsh 0x41000000 pimoroni-pico-2-plus:pnsh + * rv-virt:pnsh[64] 0x80040000 0x10100000 + * + * so the word just below it belongs to the kernel in all of them. Whether + * that address holds kernel code, kernel data, or nothing mapped at all does + * not matter: either way an unprivileged task must not be able to read it. + * + * A BUILD_FLAT configuration has no such boundary and no CONFIG_NUTTX_USERSPACE + * at all; there the test reports that there is nothing to contain rather than + * pretending to pass. + */ + +#ifdef CONFIG_NUTTX_USERSPACE +# define SANDBOX_HAVE_TARGET 1 +# define SANDBOX_TARGET ((uintptr_t)CONFIG_NUTTX_USERSPACE - 16) +#else +# define SANDBOX_HAVE_TARGET 0 +# define SANDBOX_TARGET ((uintptr_t)0) +#endif + +#define CANARY_PRIORITY (CONFIG_EXAMPLES_SANDBOX_PRIORITY - 1) +#define CANARY_STACKSIZE 2048 +#define ESCAPE_STACKSIZE CONFIG_EXAMPLES_SANDBOX_STACKSIZE + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +/* Bumped continuously by the canary task. It is the evidence that the rest + * of the system kept running while the offender was being killed: a system + * that panicked or reset stops printing entirely, and one that merely wedged + * leaves this stuck. + */ + +static volatile unsigned long g_canary; +static volatile bool g_canary_stop; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static int canary_task(int argc, FAR char *argv[]) +{ + while (!g_canary_stop) + { + g_canary++; + usleep(10000); + } + + return 0; +} + +/**************************************************************************** + * Name: escape + * + * Description: + * Make the access that must not be allowed. This runs in whatever task + * calls it, and in a correctly isolated build it does not return. + * + ****************************************************************************/ + +static void escape(uintptr_t addr, bool store) +{ + FAR volatile uint32_t *p = (FAR volatile uint32_t *)addr; + + printf("sandbox: attempting %s of %p\n", store ? "WRITE" : "read", + (FAR void *)addr); + fflush(stdout); + + if (store) + { + /* A write is the more dangerous direction and is not the default: if + * the hardware does *not* contain it, this corrupts whatever it lands + * on. It is offered because a read-only mapping would let a read + * through while still refusing the write. + */ + + *p = 0xdeadbeef; + } + else + { + uint32_t v = *p; + + /* Consume the value so the load cannot be optimised away. */ + + printf("sandbox: NOT CONTAINED -- read %08lx\n", (unsigned long)v); + fflush(stdout); + return; + } + + printf("sandbox: NOT CONTAINED -- the access completed\n"); + fflush(stdout); +} + +static int escape_task(int argc, FAR char *argv[]) +{ + bool store = (argc > 1 && argv[1][0] == 'w'); + uintptr_t addr = SANDBOX_TARGET; + + if (argc > 2) + { + addr = (uintptr_t)strtoul(argv[2], NULL, 0); + } + + escape(addr, store); + + /* Reaching here means the access was allowed. */ + + return 1; +} + +/**************************************************************************** + * Name: selfcheck + * + * Description: + * Spawn the offender as a separate task and watch what happens to it, and + * to everything else. Three things have to be true for a pass: the + * offending task must die, this task must still be running afterwards, and + * the canary must still be advancing. + * + ****************************************************************************/ + +static int selfcheck(bool store, uintptr_t addr) +{ + FAR char *argv[3]; + char addrbuf[24]; + unsigned long before; + unsigned long after; + pid_t canary; + pid_t pid; + int status = 0; + int ret; + int fails = 0; + + printf("sandbox: target %p (%s)\n", (FAR void *)addr, + store ? "write" : "read"); +#if SANDBOX_HAVE_TARGET + printf("sandbox: derived from CONFIG_NUTTX_USERSPACE = %p\n", + (FAR void *)(uintptr_t)CONFIG_NUTTX_USERSPACE); +#endif + + /* Start the canary before anything else, so it is already running when the + * offender faults. + */ + + g_canary = 0; + g_canary_stop = false; + + canary = task_create("sandbox_canary", CANARY_PRIORITY, CANARY_STACKSIZE, + canary_task, NULL); + if (canary < 0) + { + printf("sandbox: FAIL - could not start the canary task\n"); + return 1; + } + + usleep(100000); + before = g_canary; + + snprintf(addrbuf, sizeof(addrbuf), "0x%lx", (unsigned long)addr); + argv[0] = store ? (FAR char *)"w" : (FAR char *)"r"; + argv[1] = addrbuf; + argv[2] = NULL; + + printf("sandbox: starting the offending task\n"); + fflush(stdout); + + pid = task_create("sandbox_escape", CONFIG_EXAMPLES_SANDBOX_PRIORITY, + ESCAPE_STACKSIZE, escape_task, argv); + if (pid < 0) + { + printf("sandbox: FAIL - could not start the offending task\n"); + g_canary_stop = true; + return 1; + } + + /* Wait for the offender to be reaped. If the system contains the fault by + * killing just that task, this returns. If it panics or resets, nothing + * below ever prints -- which is itself the result, visible on the console. + */ + +#ifdef CONFIG_SCHED_WAITPID + ret = waitpid(pid, &status, 0); + if (ret < 0) + { + /* ECHILD here means the task was already reaped, which is still + * containment -- it died and the system moved on. + */ + + printf("sandbox: waitpid() returned %d (task already reaped)\n", ret); + } + else + { + printf("sandbox: offender reaped, status %d\n", status); + } +#else + /* No waitpid: poll until the pid is gone. */ + + for (ret = 0; ret < 100; ret++) + { + if (kill(pid, 0) < 0) + { + break; + } + + usleep(50000); + } + + printf("sandbox: offender gone after %d polls\n", ret); +#endif + + usleep(200000); + after = g_canary; + g_canary_stop = true; + + /* Now the three things that make this a pass. */ + + printf("\n"); + printf("sandbox: --- results ---\n"); + + if (kill(pid, 0) == 0) + { + printf("sandbox: FAIL - the offending task is still alive\n"); + fails++; + } + else + { + printf("sandbox: PASS - the offending task was terminated\n"); + } + + printf("sandbox: PASS - this task survived and is still running\n"); + + if (after > before) + { + printf("sandbox: PASS - unrelated task kept running (%lu -> %lu)\n", + before, after); + } + else + { + printf("sandbox: FAIL - unrelated task stopped (%lu -> %lu)\n", + before, after); + fails++; + } + + usleep(100000); + + printf("\n"); + if (fails == 0) + { + printf("sandbox: CONTAINED - the sandbox held\n"); + } + else + { + printf("sandbox: NOT CONTAINED - %d check(s) failed\n", fails); + } + + return fails; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +static void usage(void) +{ + printf("Usage: sandbox [escape [r|w] [addr]]\n" + " (no args) spawn an offending task and check it is\n" + " contained while everything else survives\n" + " escape [r|w] [a] make the bad access in *this* task; in a\n" + " contained build this task does not return\n"); +} + +int main(int argc, FAR char *argv[]) +{ + bool store = false; + uintptr_t addr = SANDBOX_TARGET; + int argbase = 1; + + if (argc > 1 && strcmp(argv[1], "-h") == 0) + { + usage(); + return 0; + } + +#if !SANDBOX_HAVE_TARGET + printf("sandbox: this is a flat build -- there is no kernel/user boundary\n" + "sandbox: to escape from, so there is nothing to contain. Build a\n" + "sandbox: protected or kernel configuration to run this test.\n"); + if (argc <= 1) + { + return 0; + } +#endif + + if (argc > 1 && strcmp(argv[1], "escape") == 0) + { + argbase = 2; + } + + if (argc > argbase && (argv[argbase][0] == 'w' || argv[argbase][0] == 'r')) + { + store = (argv[argbase][0] == 'w'); + argbase++; + } + + if (argc > argbase) + { + addr = (uintptr_t)strtoul(argv[argbase], NULL, 0); + } + + if (argc > 1 && strcmp(argv[1], "escape") == 0) + { + /* One-shot mode: fault in this task, deliberately. */ + + printf("sandbox: escaping from this task -- expect it to die\n"); + escape(addr, store); + printf("sandbox: NOT CONTAINED - returned from the bad access\n"); + return 1; + } + + return selfcheck(store, addr); +} From b86348087dcf339e81035fdebc277f9c2a082509 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Tue, 11 Aug 2026 20:09:41 +0200 Subject: [PATCH 4/4] examples/sandbox: Name what is touched and what must happen. 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) Signed-off-by: Marco Casaroli --- examples/sandbox/Kconfig | 62 ++- examples/sandbox/sandbox_main.c | 721 ++++++++++++++++++++++++-------- 2 files changed, 595 insertions(+), 188 deletions(-) diff --git a/examples/sandbox/Kconfig b/examples/sandbox/Kconfig index 7a1aef2fe72..1da91d00380 100644 --- a/examples/sandbox/Kconfig +++ b/examples/sandbox/Kconfig @@ -9,12 +9,16 @@ config EXAMPLES_SANDBOX ---help--- A test that deliberately tries to escape the kernel/user boundary of a protected or kernel build, and checks that the attempt is contained: - the offending task is terminated and everything else keeps running. + the offending process is terminated and everything else keeps running. - The address it touches is derived from CONFIG_NUTTX_USERSPACE rather - than hard-coded, so the test is architecture-neutral. In a flat build - there is no boundary to escape from and the test says so instead of - reporting a pass. + Each target carries the outcome it expects, so the test fails a build + that refuses everything as well as one that permits everything. The + "self" target is the control: it touches memory the process owns and + must be allowed. + + A protected build derives the kernel target from CONFIG_NUTTX_USERSPACE. + A kernel build has no such address, because every process is loaded + into its own address environment, so the addresses below supply it. if EXAMPLES_SANDBOX @@ -30,4 +34,52 @@ config EXAMPLES_SANDBOX_STACKSIZE int "sandbox stack size" default DEFAULT_TASK_STACKSIZE +config EXAMPLES_SANDBOX_ALLOC + int "Bytes the offender holds when it dies" + default 65536 + ---help--- + The offending process allocates this much, writes to all of it so the + pages are really committed, and opens a file, before it makes the bad + access. It still holds both when it is killed. + + A process that dies owning nothing proves nothing about whether the + kill leaks. Compare "free" before and after a run: the kernel heap + and the page pool both have to come back to where they started. + +config EXAMPLES_SANDBOX_KERNEL_ADDR + hex "Address of kernel memory" + default 0x0 + ---help--- + An address that is mapped and belongs to the kernel. Reading it from + a user process must fault. + + It has to be mapped. An unmapped address tests the absence of a + mapping instead of the permission on one, which is a different thing; + use the unmapped target for that. + + Zero means the target is unavailable, and the test reports it as such. + A protected build may leave this at zero, because CONFIG_NUTTX_USERSPACE + gives the boundary. + +config EXAMPLES_SANDBOX_PERIPH_ADDR + hex "Address of a peripheral register" + default 0x0 + ---help--- + A peripheral register that a user process must not reach, such as the + registers that control the memory mapping itself. + + An MMU keeps processes apart but does not stop one from reaching a + peripheral, so this target exercises a different mechanism from the + kernel target. Zero means the target is unavailable. + +config EXAMPLES_SANDBOX_UNMAPPED_ADDR + hex "Address with no mapping" + default 0x0 + ---help--- + An address in no mapping at all. Touching it must be reported. + + Hardware that answers an unmapped access quietly, with zero for a read + and no fault, hides errors that a fault would show. Zero means the + target is unavailable. + endif diff --git a/examples/sandbox/sandbox_main.c b/examples/sandbox/sandbox_main.c index 48d45182233..e6b1188e817 100644 --- a/examples/sandbox/sandbox_main.c +++ b/examples/sandbox/sandbox_main.c @@ -29,7 +29,10 @@ #include #include -#include +#include +#include +#include +#include #include #include #include @@ -42,333 +45,685 @@ * Pre-processor Definitions ****************************************************************************/ -/* Where to poke. +/* Named targets. * - * The point of this test is to be architecture-neutral, so the address is - * derived rather than hard-coded. In every BUILD_PROTECTED configuration in - * the tree the kernel blob is placed *below* the user blob, and the boundary - * between them is exactly CONFIG_NUTTX_USERSPACE: + * A bare address says nothing about what should happen when it is touched, + * so every target carries the outcome it expects. A build that refuses + * everything is as broken as one that permits everything, and only the + * "self" case can tell the two apart. * - * qemu-armv7a:pnsh 0x00200000 mps2-an521:knsh 0x10200000 - * qemu-armv8a:pnsh 0x41000000 pimoroni-pico-2-plus:pnsh - * rv-virt:pnsh[64] 0x80040000 0x10100000 - * - * so the word just below it belongs to the kernel in all of them. Whether - * that address holds kernel code, kernel data, or nothing mapped at all does - * not matter: either way an unprivileged task must not be able to read it. - * - * A BUILD_FLAT configuration has no such boundary and no CONFIG_NUTTX_USERSPACE - * at all; there the test reports that there is nothing to contain rather than - * pretending to pass. + * The addresses come from Kconfig because a user process cannot see kernel + * symbols; that is the boundary under test. A board supplies them in its + * defconfig. A target with no address is reported as unavailable rather + * than silently skipped. + */ + +#define SANDBOX_KERNEL_ADDR CONFIG_EXAMPLES_SANDBOX_KERNEL_ADDR +#define SANDBOX_PERIPH_ADDR CONFIG_EXAMPLES_SANDBOX_PERIPH_ADDR +#define SANDBOX_UNMAPPED_ADDR CONFIG_EXAMPLES_SANDBOX_UNMAPPED_ADDR + +/* A protected build knows where the kernel ends without being told: the + * kernel blob is placed below the user blob and the boundary is exactly + * CONFIG_NUTTX_USERSPACE. A kernel build has no such address -- each + * process is loaded into its own address environment -- so there the + * Kconfig value is the only source. */ -#ifdef CONFIG_NUTTX_USERSPACE -# define SANDBOX_HAVE_TARGET 1 -# define SANDBOX_TARGET ((uintptr_t)CONFIG_NUTTX_USERSPACE - 16) -#else -# define SANDBOX_HAVE_TARGET 0 -# define SANDBOX_TARGET ((uintptr_t)0) +#if SANDBOX_KERNEL_ADDR == 0 && defined(CONFIG_NUTTX_USERSPACE) +# undef SANDBOX_KERNEL_ADDR +# define SANDBOX_KERNEL_ADDR ((uintptr_t)CONFIG_NUTTX_USERSPACE - 16) #endif -#define CANARY_PRIORITY (CONFIG_EXAMPLES_SANDBOX_PRIORITY - 1) -#define CANARY_STACKSIZE 2048 -#define ESCAPE_STACKSIZE CONFIG_EXAMPLES_SANDBOX_STACKSIZE +#define CANARY_INTERVAL_US 10000 +#define SETTLE_US 200000 + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +enum sandbox_expect_e +{ + EXPECT_FAULT = 0, /* The access must be refused */ + EXPECT_OK /* The access must be allowed */ +}; + +struct sandbox_target_s +{ + FAR const char *name; + uintptr_t addr; + enum sandbox_expect_e expect; + FAR const char *what; +}; /**************************************************************************** * Private Data ****************************************************************************/ -/* Bumped continuously by the canary task. It is the evidence that the rest - * of the system kept running while the offender was being killed: a system - * that panicked or reset stops printing entirely, and one that merely wedged - * leaves this stuck. +/* Touched by the "self" target. It belongs to this process, so refusing it + * means the boundary is drawn in the wrong place. + */ + +static volatile uint32_t g_own_word = 0x600df00d; + +/* Bumped by the canary thread. It is the evidence that the system kept + * running while the offender was killed: a kernel that panicked stops + * printing, and one that merely wedged leaves this standing still. */ static volatile unsigned long g_canary; static volatile bool g_canary_stop; +/* Sampled by the canary thread while the offender is alive. Taking these + * here lets waitpid() stay blocking, so the exit status is the real one. + */ + +static volatile pid_t g_watch_pid; +static volatile unsigned long g_peak_mem; +static volatile int g_peak_fds; + /**************************************************************************** * Private Functions ****************************************************************************/ -static int canary_task(int argc, FAR char *argv[]) +/**************************************************************************** + * Name: pool_used + * + * Description: + * The bytes in use, read from /proc/meminfo. The page pool is reported + * when there is one, because that is where the memory of a process comes + * from; otherwise the kernel heap is. + * + * This is what makes the leak check mean something. A count that does not + * move while the offender is alive would measure nothing, and "the same + * before and after" would say nothing about whether the memory came back. + * + * Returned Value: + * The bytes in use, or 0 if /proc/meminfo cannot be read. + * + ****************************************************************************/ + +static unsigned long pool_used(void) +{ + char buf[512]; + FAR char *line; + FAR char *save; + unsigned long kmem = 0; + unsigned long page = 0; + int fd; + int n; + + fd = open("/proc/meminfo", O_RDONLY); + if (fd < 0) + { + return 0; + } + + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + + if (n <= 0) + { + return 0; + } + + buf[n] = '\0'; + + for (line = strtok_r(buf, "\n", &save); line != NULL; + line = strtok_r(NULL, "\n", &save)) + { + FAR char *p = line; + FAR char *end; + unsigned long used; + + /* Every line is " ... ". Read the two + * numbers with strtoul() rather than a scanset, which is not in every + * sscanf(). + */ + + while (*p == ' ') + { + p++; + } + + strtoul(p, &end, 10); + if (end == p) + { + continue; /* The header line. */ + } + + p = end; + while (*p == ' ') + { + p++; + } + + used = strtoul(p, &end, 10); + if (end == p) + { + continue; + } + + if (strstr(line, "Page") != NULL) + { + page = used; + } + else if (strstr(line, "Kmem") != NULL || strstr(line, "Umem") != NULL) + { + kmem = used; + } + } + + return page != 0 ? page : kmem; +} + +/**************************************************************************** + * Name: count_fds + * + * Description: + * The descriptors a process holds, read from /proc//group/fd. The + * first line of that file is a header and is not counted. + * + * Returned Value: + * The number of open descriptors, or -1 when the process is gone, which is + * the evidence that its group was destroyed and not merely emptied. + * + ****************************************************************************/ + +static int count_fds(pid_t pid) +{ + char path[32]; + char buf[512]; + int count = 0; + int fd; + int n; + int i; + + snprintf(path, sizeof(path), "/proc/%d/group/fd", (int)pid); + + fd = open(path, O_RDONLY); + if (fd < 0) + { + return -1; + } + + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + + if (n <= 0) + { + return 0; + } + + buf[n] = '\0'; + + /* Count the lines that start with a digit, which are the descriptors. The + * header starts with "FD". + */ + + for (i = 0; i < n; i++) + { + if ((i == 0 || buf[i - 1] == '\n') && buf[i] >= '0' && buf[i] <= '9') + { + count++; + } + } + + return count; +} + +static FAR void *canary_thread(FAR void *arg) { while (!g_canary_stop) { g_canary++; - usleep(10000); + + if (g_watch_pid > 0) + { + unsigned long mem = pool_used(); + int fds = count_fds(g_watch_pid); + + if (mem > g_peak_mem) + { + g_peak_mem = mem; + } + + if (fds > g_peak_fds) + { + g_peak_fds = fds; + } + } + + usleep(CANARY_INTERVAL_US); + } + + return NULL; +} + +/**************************************************************************** + * Name: resolve + * + * Description: + * Turn a target name, or a literal address, into an address and the + * outcome that address must produce. + * + * Returned Value: + * OK on success, ERROR if the name is unknown or has no address on this + * configuration. + * + ****************************************************************************/ + +static int resolve(FAR const char *name, FAR struct sandbox_target_s *t) +{ + t->name = name; + + if (strcmp(name, "self") == 0) + { + t->addr = (uintptr_t)&g_own_word; + t->expect = EXPECT_OK; + t->what = "this process's own data"; + return OK; + } + + if (strcmp(name, "kernel") == 0) + { + t->addr = SANDBOX_KERNEL_ADDR; + t->expect = EXPECT_FAULT; + t->what = "kernel memory"; + } + else if (strcmp(name, "periph") == 0) + { + t->addr = SANDBOX_PERIPH_ADDR; + t->expect = EXPECT_FAULT; + t->what = "a peripheral register"; + } + else if (strcmp(name, "unmapped") == 0) + { + t->addr = SANDBOX_UNMAPPED_ADDR; + t->expect = EXPECT_FAULT; + t->what = "an address with no mapping"; + } + else if (name[0] == '0' && (name[1] == 'x' || name[1] == 'X')) + { + t->addr = (uintptr_t)strtoul(name, NULL, 0); + t->expect = EXPECT_FAULT; + t->what = "a literal address"; + } + else + { + printf("sandbox: unknown target \"%s\"\n", name); + return ERROR; } - return 0; + if (t->addr == 0) + { + printf("sandbox: target \"%s\" has no address here.\n", name); + printf("sandbox: set CONFIG_EXAMPLES_SANDBOX_%s_ADDR, or pass an " + "address.\n", + strcmp(name, "kernel") == 0 ? "KERNEL" : + strcmp(name, "periph") == 0 ? "PERIPH" : "UNMAPPED"); + return ERROR; + } + + return OK; } /**************************************************************************** - * Name: escape + * Name: touch * * Description: - * Make the access that must not be allowed. This runs in whatever task - * calls it, and in a correctly isolated build it does not return. + * Make the access. Where it is refused this does not return. * ****************************************************************************/ -static void escape(uintptr_t addr, bool store) +static void touch(uintptr_t addr, bool store) { FAR volatile uint32_t *p = (FAR volatile uint32_t *)addr; - printf("sandbox: attempting %s of %p\n", store ? "WRITE" : "read", + printf("sandbox: attempting %s of %p\n", store ? "write" : "read", (FAR void *)addr); fflush(stdout); if (store) { - /* A write is the more dangerous direction and is not the default: if - * the hardware does *not* contain it, this corrupts whatever it lands - * on. It is offered because a read-only mapping would let a read - * through while still refusing the write. - */ - *p = 0xdeadbeef; } else { uint32_t v = *p; - /* Consume the value so the load cannot be optimised away. */ - - printf("sandbox: NOT CONTAINED -- read %08lx\n", (unsigned long)v); + printf("sandbox: the read completed and returned %08lx\n", + (unsigned long)v); fflush(stdout); return; } - printf("sandbox: NOT CONTAINED -- the access completed\n"); + printf("sandbox: the write completed\n"); fflush(stdout); } -static int escape_task(int argc, FAR char *argv[]) -{ - bool store = (argc > 1 && argv[1][0] == 'w'); - uintptr_t addr = SANDBOX_TARGET; - - if (argc > 2) - { - addr = (uintptr_t)strtoul(argv[2], NULL, 0); - } - - escape(addr, store); - - /* Reaching here means the access was allowed. */ - - return 1; -} - /**************************************************************************** - * Name: selfcheck + * Name: run_case * * Description: - * Spawn the offender as a separate task and watch what happens to it, and - * to everything else. Three things have to be true for a pass: the - * offending task must die, this task must still be running afterwards, and - * the canary must still be advancing. + * Spawn this program again as a separate process, in "escape" mode, and + * watch what happens to it and to everything else. + * + * The offender has to be a process and not a task. A kernel build does + * not give user code task_create(); making a process is the only way a + * user program can put the bad access somewhere it can be killed. * ****************************************************************************/ -static int selfcheck(bool store, uintptr_t addr) +static int run_case(FAR const char *progname, + FAR const struct sandbox_target_s *t, bool store) { - FAR char *argv[3]; + FAR char *argv[5]; char addrbuf[24]; - unsigned long before; - unsigned long after; - pid_t canary; + unsigned long canary_before; + unsigned long canary_after; + unsigned long mem_before; + unsigned long mem_during = 0; + unsigned long mem_after; + int fd_during = -1; + pthread_t canary; pid_t pid; int status = 0; + int fails = 0; int ret; - int fails = 0; - printf("sandbox: target %p (%s)\n", (FAR void *)addr, - store ? "write" : "read"); -#if SANDBOX_HAVE_TARGET - printf("sandbox: derived from CONFIG_NUTTX_USERSPACE = %p\n", - (FAR void *)(uintptr_t)CONFIG_NUTTX_USERSPACE); -#endif - - /* Start the canary before anything else, so it is already running when the - * offender faults. - */ + printf("\nsandbox: target %s -- %s at %p, expecting %s\n", + t->name, t->what, (FAR void *)t->addr, + t->expect == EXPECT_OK ? "success" : "a fault"); g_canary = 0; g_canary_stop = false; + g_watch_pid = 0; + g_peak_mem = 0; + g_peak_fds = -1; - canary = task_create("sandbox_canary", CANARY_PRIORITY, CANARY_STACKSIZE, - canary_task, NULL); - if (canary < 0) + if (pthread_create(&canary, NULL, canary_thread, NULL) != 0) { - printf("sandbox: FAIL - could not start the canary task\n"); + printf("sandbox: FAIL - could not start the canary\n"); return 1; } - usleep(100000); - before = g_canary; + usleep(SETTLE_US / 2); + canary_before = g_canary; + mem_before = pool_used(); - snprintf(addrbuf, sizeof(addrbuf), "0x%lx", (unsigned long)addr); - argv[0] = store ? (FAR char *)"w" : (FAR char *)"r"; - argv[1] = addrbuf; - argv[2] = NULL; + snprintf(addrbuf, sizeof(addrbuf), "0x%lx", (unsigned long)t->addr); + argv[0] = (FAR char *)progname; + argv[1] = (FAR char *)"escape"; + argv[2] = store ? (FAR char *)"w" : (FAR char *)"r"; + argv[3] = addrbuf; + argv[4] = NULL; - printf("sandbox: starting the offending task\n"); - fflush(stdout); - - pid = task_create("sandbox_escape", CONFIG_EXAMPLES_SANDBOX_PRIORITY, - ESCAPE_STACKSIZE, escape_task, argv); - if (pid < 0) + ret = posix_spawn(&pid, progname, NULL, NULL, argv, NULL); + if (ret != 0) { - printf("sandbox: FAIL - could not start the offending task\n"); + printf("sandbox: FAIL - could not spawn the offender (%d)\n", ret); g_canary_stop = true; + pthread_join(canary, NULL); return 1; } - /* Wait for the offender to be reaped. If the system contains the fault by - * killing just that task, this returns. If it panics or resets, nothing - * below ever prints -- which is itself the result, visible on the console. + /* Watch while the offender lives. The count has to be seen to rise here, + * or "the same before and after" says nothing at all. If the system + * panics instead of containing the fault, nothing below prints, which is + * itself the result. */ -#ifdef CONFIG_SCHED_WAITPID - ret = waitpid(pid, &status, 0); - if (ret < 0) - { - /* ECHILD here means the task was already reaped, which is still - * containment -- it died and the system moved on. - */ + g_watch_pid = pid; - printf("sandbox: waitpid() returned %d (task already reaped)\n", ret); - } - else + if (waitpid(pid, &status, 0) < 0) { - printf("sandbox: offender reaped, status %d\n", status); + printf("sandbox: FAIL - could not wait for the offender\n"); + fails++; } -#else - /* No waitpid: poll until the pid is gone. */ - - for (ret = 0; ret < 100; ret++) - { - if (kill(pid, 0) < 0) - { - break; - } - usleep(50000); - } + g_watch_pid = 0; + mem_during = g_peak_mem; + fd_during = g_peak_fds; - printf("sandbox: offender gone after %d polls\n", ret); -#endif + printf("sandbox: the offender exited with status %d\n", status); - usleep(200000); - after = g_canary; + usleep(SETTLE_US); + canary_after = g_canary; + mem_after = pool_used(); g_canary_stop = true; + pthread_join(canary, NULL); - /* Now the three things that make this a pass. */ - - printf("\n"); - printf("sandbox: --- results ---\n"); + printf("sandbox: --- %s ---\n", t->name); - if (kill(pid, 0) == 0) + if (t->expect == EXPECT_OK) + { + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + { + printf("sandbox: PASS - the allowed access completed\n"); + } + else + { + printf("sandbox: FAIL - an owned access was refused\n"); + fails++; + } + } + else if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { - printf("sandbox: FAIL - the offending task is still alive\n"); + printf("sandbox: FAIL - NOT CONTAINED, the access was permitted\n"); fails++; } else { - printf("sandbox: PASS - the offending task was terminated\n"); + printf("sandbox: PASS - the offending process was terminated\n"); } - printf("sandbox: PASS - this task survived and is still running\n"); + printf("sandbox: PASS - this process survived\n"); - if (after > before) + if (canary_after > canary_before) { - printf("sandbox: PASS - unrelated task kept running (%lu -> %lu)\n", - before, after); + printf("sandbox: PASS - another thread ran (%lu -> %lu)\n", + canary_before, canary_after); } else { - printf("sandbox: FAIL - unrelated task stopped (%lu -> %lu)\n", - before, after); + printf("sandbox: FAIL - another thread stopped (%lu -> %lu)\n", + canary_before, canary_after); fails++; } - usleep(100000); + /* Memory: before, during, after. */ - printf("\n"); - if (fails == 0) + printf("sandbox: memory %lu -> %lu -> %lu\n", + mem_before, mem_during, mem_after); + + if (mem_during <= mem_before) + { + printf("sandbox: FAIL - the memory of the offender was never seen\n"); + fails++; + } + else if (mem_after > mem_before) { - printf("sandbox: CONTAINED - the sandbox held\n"); + printf("sandbox: FAIL - %lu bytes were not given back\n", + mem_after - mem_before); + fails++; } else { - printf("sandbox: NOT CONTAINED - %d check(s) failed\n", fails); + printf("sandbox: PASS - %lu bytes taken and given back\n", + mem_during - mem_before); + } + + /* Descriptors: open while it lived, and the group gone after. */ + + if (fd_during <= 0) + { + printf("sandbox: FAIL - the descriptors of the offender were never " + "seen\n"); + fails++; + } + else if (count_fds(pid) >= 0) + { + printf("sandbox: FAIL - %d descriptor(s) are still open\n", + count_fds(pid)); + fails++; + } + else + { + printf("sandbox: PASS - %d descriptor(s) open, none after\n", + fd_during); } return fails; } +static void usage(FAR const char *progname) +{ + printf("Usage: %s [r|w] [target ...]\n", progname); + printf(" %s escape \n", progname); + printf("\n"); + printf("Targets:\n"); + printf(" self this process's own data must succeed\n"); + printf(" kernel kernel memory must fault\n"); + printf(" periph a peripheral register must fault\n"); + printf(" unmapped an address with no mapping must fault\n"); + printf(" 0x... a literal address must fault\n"); + printf("\n"); + printf("With no target, self, kernel, periph and unmapped are all run.\n"); +} + /**************************************************************************** * Public Functions ****************************************************************************/ -static void usage(void) -{ - printf("Usage: sandbox [escape [r|w] [addr]]\n" - " (no args) spawn an offending task and check it is\n" - " contained while everything else survives\n" - " escape [r|w] [a] make the bad access in *this* task; in a\n" - " contained build this task does not return\n"); -} - int main(int argc, FAR char *argv[]) { - bool store = false; - uintptr_t addr = SANDBOX_TARGET; - int argbase = 1; + FAR const char *defaults[] = + { + "self", "kernel", "periph", "unmapped" + }; + + struct sandbox_target_s t; + FAR const char *progname = argv[0]; + char raw[256]; + int rawfd; + int rawn; + bool store = false; + int fails = 0; + int ran = 0; + int i; if (argc > 1 && strcmp(argv[1], "-h") == 0) { - usage(); + usage(progname); return 0; } -#if !SANDBOX_HAVE_TARGET - printf("sandbox: this is a flat build -- there is no kernel/user boundary\n" - "sandbox: to escape from, so there is nothing to contain. Build a\n" - "sandbox: protected or kernel configuration to run this test.\n"); - if (argc <= 1) + /* "escape" is how this program re-enters itself as the offender. It makes + * the access in this process and, where the access is refused, never gets + * to the line below. + */ + + if (argc > 3 && strcmp(argv[1], "escape") == 0) { + /* Take resources the kernel has to reclaim, and hold them across the + * access. A process that dies owning nothing says nothing about + * whether killing it leaks: the heap block is touched so its pages + * are really committed, and the descriptor is left open on purpose. + */ + + FAR void *mem = malloc(CONFIG_EXAMPLES_SANDBOX_ALLOC); + int fd = open("/system/bin/sandbox", O_RDONLY); + + if (mem != NULL) + { + memset(mem, 0xa5, CONFIG_EXAMPLES_SANDBOX_ALLOC); + } + + printf("sandbox: holding %d bytes at %p and fd %d\n", + CONFIG_EXAMPLES_SANDBOX_ALLOC, mem, fd); + fflush(stdout); + + touch((uintptr_t)strtoul(argv[3], NULL, 0), argv[2][0] == 'w'); + + /* Only an allowed access arrives here. Leave both outstanding, so + * that the normal exit path is measured the same way as the kill. + */ + return 0; } -#endif - if (argc > 1 && strcmp(argv[1], "escape") == 0) + /* Show where the numbers below come from. */ + + rawfd = open("/proc/meminfo", O_RDONLY); + if (rawfd < 0) { - argbase = 2; + printf("sandbox: /proc/meminfo cannot be opened (%d)\n", errno); } - - if (argc > argbase && (argv[argbase][0] == 'w' || argv[argbase][0] == 'r')) + else { - store = (argv[argbase][0] == 'w'); - argbase++; + rawn = read(rawfd, raw, sizeof(raw) - 1); + close(rawfd); + + if (rawn > 0) + { + raw[rawn] = '\0'; + printf("sandbox: /proc/meminfo reads\n%s", raw); + } + else + { + printf("sandbox: /proc/meminfo read gave %d\n", rawn); + } } - if (argc > argbase) + i = 1; + if (argc > 1 && (argv[1][0] == 'r' || argv[1][0] == 'w') && + argv[1][1] == '\0') { - addr = (uintptr_t)strtoul(argv[argbase], NULL, 0); + store = (argv[1][0] == 'w'); + i++; } - if (argc > 1 && strcmp(argv[1], "escape") == 0) + if (i >= argc) + { + int n; + + for (n = 0; n < (int)(sizeof(defaults) / sizeof(defaults[0])); n++) + { + if (resolve(defaults[n], &t) == OK) + { + fails += run_case(progname, &t, store); + ran++; + } + } + } + else { - /* One-shot mode: fault in this task, deliberately. */ + for (; i < argc; i++) + { + if (resolve(argv[i], &t) == OK) + { + fails += run_case(progname, &t, store); + ran++; + } + } + } - printf("sandbox: escaping from this task -- expect it to die\n"); - escape(addr, store); - printf("sandbox: NOT CONTAINED - returned from the bad access\n"); + printf("\n"); + if (ran == 0) + { + printf("sandbox: no target could be resolved, nothing was tested\n"); return 1; } - return selfcheck(store, addr); + if (fails == 0) + { + printf("sandbox: CONTAINED - %d target(s), every check passed\n", ran); + } + else + { + printf("sandbox: NOT CONTAINED - %d of %d target(s) failed\n", + fails, ran); + } + + return fails; }