From a3057395df8b852ab51c89444e04acf481333b43 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Fri, 24 Jul 2026 23:39:54 +0200 Subject: [PATCH 1/2] examples/pffault: Add a test that touches a kernel address from user space. A small user program that reads or writes one address, by default 0x3fc98000, the base of the ESP32-S3 kernel DRAM region. A user process must not reach it, so the access must fault and only the process must die. "pffault w" makes the access a store, and a second argument names another address. With CONFIG_ESP32S3_PAGEFAULT and CONFIG_ESP32S3_PAGEFAULT_SELFTEST on the kernel side, "pffault r 0x80000000" exercises the recoverable-fault restart path, and "pffault r 0x0" the report path. Assisted-by: Claude Code:claude-opus-5-5 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 | 82 +++++++++++++++++++++++++++++++++ 5 files changed, 200 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..913c0b95e54 --- /dev/null +++ b/examples/pffault/pffault_main.c @@ -0,0 +1,82 @@ +/**************************************************************************** + * 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 32912a1e8d483ff82bbd519e93fb9309fa3642cd Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Tue, 11 Aug 2026 20:09:41 +0200 Subject: [PATCH 2/2] examples/sandbox: Add a containment test for protected and kernel builds. A test that a user process which makes a forbidden access is stopped, and that nothing else is. It spawns this program again as a separate process to make the access, because a kernel build does not give user code task_create(), and checks that the offender died, that the caller still runs and that a canary thread kept counting. The canary is what tells "the offender was contained" from "the whole system stopped". Every target has the outcome it expects: self the process's own data must succeed kernel kernel memory must fault periph a peripheral register must fault unmapped an address with no mapping must fault "self" is the control. Without it a build that refuses every access passes every other check. An MMU keeps processes apart but does not stop one reaching a peripheral, and an unmapped access is refused by another mechanism again, so neither is covered by the kernel target. The offender allocates memory and opens a file before the access. The test reads /proc/meminfo and /proc//group/fd while it lives and after it is reaped, and fails if the counts never rose, since "the same before and after" says nothing if the resources were never seen. The addresses come from Kconfig, because a user process cannot see kernel symbols. A protected build derives the kernel target from CONFIG_NUTTX_USERSPACE when none is set. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli --- examples/sandbox/CMakeLists.txt | 33 ++ examples/sandbox/Kconfig | 85 ++++ examples/sandbox/Make.defs | 25 ++ examples/sandbox/Makefile | 32 ++ examples/sandbox/sandbox_main.c | 729 ++++++++++++++++++++++++++++++++ 5 files changed, 904 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..1da91d00380 --- /dev/null +++ b/examples/sandbox/Kconfig @@ -0,0 +1,85 @@ +# +# 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 process is terminated and everything else keeps running. + + 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 + +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 + +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/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..e6b1188e817 --- /dev/null +++ b/examples/sandbox/sandbox_main.c @@ -0,0 +1,729 @@ +/**************************************************************************** + * 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 +#include +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Named targets. + * + * 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. + * + * 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. + */ + +#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_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 + ****************************************************************************/ + +/* 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 + ****************************************************************************/ + +/**************************************************************************** + * 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++; + + 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; + } + + 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: touch + * + * Description: + * Make the access. Where it is refused this does not return. + * + ****************************************************************************/ + +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", + (FAR void *)addr); + fflush(stdout); + + if (store) + { + *p = 0xdeadbeef; + } + else + { + uint32_t v = *p; + + printf("sandbox: the read completed and returned %08lx\n", + (unsigned long)v); + fflush(stdout); + return; + } + + printf("sandbox: the write completed\n"); + fflush(stdout); +} + +/**************************************************************************** + * Name: run_case + * + * Description: + * 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 run_case(FAR const char *progname, + FAR const struct sandbox_target_s *t, bool store) +{ + FAR char *argv[5]; + char addrbuf[24]; + 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; + + 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; + + if (pthread_create(&canary, NULL, canary_thread, NULL) != 0) + { + printf("sandbox: FAIL - could not start the canary\n"); + return 1; + } + + usleep(SETTLE_US / 2); + canary_before = g_canary; + mem_before = pool_used(); + + 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; + + ret = posix_spawn(&pid, progname, NULL, NULL, argv, NULL); + if (ret != 0) + { + printf("sandbox: FAIL - could not spawn the offender (%d)\n", ret); + g_canary_stop = true; + pthread_join(canary, NULL); + return 1; + } + + /* 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. + */ + + g_watch_pid = pid; + + if (waitpid(pid, &status, 0) < 0) + { + printf("sandbox: FAIL - could not wait for the offender\n"); + fails++; + } + + g_watch_pid = 0; + mem_during = g_peak_mem; + fd_during = g_peak_fds; + + printf("sandbox: the offender exited with status %d\n", status); + + usleep(SETTLE_US); + canary_after = g_canary; + mem_after = pool_used(); + g_canary_stop = true; + pthread_join(canary, NULL); + + printf("sandbox: --- %s ---\n", t->name); + + 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 - NOT CONTAINED, the access was permitted\n"); + fails++; + } + else + { + printf("sandbox: PASS - the offending process was terminated\n"); + } + + printf("sandbox: PASS - this process survived\n"); + + if (canary_after > canary_before) + { + printf("sandbox: PASS - another thread ran (%lu -> %lu)\n", + canary_before, canary_after); + } + else + { + printf("sandbox: FAIL - another thread stopped (%lu -> %lu)\n", + canary_before, canary_after); + fails++; + } + + /* Memory: before, during, after. */ + + 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: FAIL - %lu bytes were not given back\n", + mem_after - mem_before); + fails++; + } + else + { + 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 + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + 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(progname); + return 0; + } + + /* "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; + } + + /* Show where the numbers below come from. */ + + rawfd = open("/proc/meminfo", O_RDONLY); + if (rawfd < 0) + { + printf("sandbox: /proc/meminfo cannot be opened (%d)\n", errno); + } + else + { + 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); + } + } + + i = 1; + if (argc > 1 && (argv[1][0] == 'r' || argv[1][0] == 'w') && + argv[1][1] == '\0') + { + store = (argv[1][0] == 'w'); + i++; + } + + 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 + { + for (; i < argc; i++) + { + if (resolve(argv[i], &t) == OK) + { + fails += run_case(progname, &t, store); + ran++; + } + } + } + + printf("\n"); + if (ran == 0) + { + printf("sandbox: no target could be resolved, nothing was tested\n"); + return 1; + } + + 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; +}