blob: e6b1188e817514c8f004b6ef710c11a46b693f77 [file]
/****************************************************************************
* 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 <nuttx/config.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <spawn.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/****************************************************************************
* 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 "<total> <used> <free> ... <name>". 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/<pid>/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 <r|w> <addr>\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;
}