diff --git a/nshlib/Kconfig b/nshlib/Kconfig index e0259a2ac1c..0de85924b5d 100644 --- a/nshlib/Kconfig +++ b/nshlib/Kconfig @@ -361,6 +361,11 @@ config NSH_DISABLE_CD bool "Disable cd" default DEFAULT_SMALL +config NSH_DISABLE_CHROOT + bool "Disable chroot" + default DEFAULT_SMALL + depends on FS_CHROOT + config NSH_DISABLE_CHMOD bool "Disable chmod" default DEFAULT_SMALL diff --git a/nshlib/nsh.h b/nshlib/nsh.h index 295f11b947f..4183fab55cb 100644 --- a/nshlib/nsh.h +++ b/nshlib/nsh.h @@ -1084,6 +1084,9 @@ int cmd_irqinfo(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv); #ifndef CONFIG_NSH_DISABLE_CD int cmd_cd(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv); #endif +#if defined(CONFIG_FS_CHROOT) && !defined(CONFIG_NSH_DISABLE_CHROOT) + int cmd_chroot(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv); +#endif #ifndef CONFIG_NSH_DISABLE_PWD int cmd_pwd(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv); #endif diff --git a/nshlib/nsh_command.c b/nshlib/nsh_command.c index 988e4792d2d..22e43b4f443 100644 --- a/nshlib/nsh_command.c +++ b/nshlib/nsh_command.c @@ -168,6 +168,11 @@ static const struct cmdmap_s g_cmdmap[] = CMD_MAP("cd", cmd_cd, 1, 2, "[|-|~|..]"), #endif +#if defined(CONFIG_FS_CHROOT) && !defined(CONFIG_NSH_DISABLE_CHROOT) + CMD_MAP("chroot", cmd_chroot, 2, CONFIG_NSH_MAXARGUMENTS, + " [ [args...]]"), +#endif + #if defined(CONFIG_FS_PERMISSION) && !defined(CONFIG_NSH_DISABLE_CHMOD) CMD_MAP("chmod", cmd_chmod, 3, 3, " "), #endif diff --git a/nshlib/nsh_envcmds.c b/nshlib/nsh_envcmds.c index 52be0e96cd2..740c1fe1963 100644 --- a/nshlib/nsh_envcmds.c +++ b/nshlib/nsh_envcmds.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -324,6 +325,99 @@ int cmd_cd(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv) } #endif +/**************************************************************************** + * Name: nsh_chroot_closefds + * + * Description: + * Close descriptors above stderr that are not already O_CLOEXEC so a + * jailed execvp() child cannot inherit host file descriptors. + * + ****************************************************************************/ + +#if defined(CONFIG_FS_CHROOT) && !defined(CONFIG_NSH_DISABLE_CHROOT) && \ + defined(CONFIG_LIBC_EXECFUNCS) +static void nsh_chroot_closefds(void) +{ + int fdmax; + int fd; + int flags; + + fdmax = sysconf(_SC_OPEN_MAX); + if (fdmax <= STDERR_FILENO) + { + return; + } + + for (fd = STDERR_FILENO + 1; fd < fdmax; fd++) + { + flags = fcntl(fd, F_GETFD); + if (flags >= 0 && (flags & FD_CLOEXEC) == 0) + { + close(fd); + } + } +} +#endif + +/**************************************************************************** + * Name: cmd_chroot + ****************************************************************************/ + +#if defined(CONFIG_FS_CHROOT) && !defined(CONFIG_NSH_DISABLE_CHROOT) +int cmd_chroot(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv) +{ + FAR char *fullpath; + int ret; + + fullpath = nsh_getfullpath(vtbl, argv[1]); + if (fullpath == NULL) + { + nsh_error(vtbl, g_fmtcmdoutofmemory, argv[0]); + return ERROR; + } + + ret = chdir(fullpath); + nsh_freefullpath(fullpath); + if (ret < 0) + { + nsh_error(vtbl, g_fmtcmdfailed, argv[0], "chdir", NSH_ERRNO); + return ERROR; + } + + ret = chroot("."); + if (ret < 0) + { + nsh_error(vtbl, g_fmtcmdfailed, argv[0], "chroot", NSH_ERRNO); + return ERROR; + } + + ret = chdir("/"); + if (ret < 0) + { + nsh_error(vtbl, g_fmtcmdfailed, argv[0], "chdir", NSH_ERRNO); + return ERROR; + } + +#ifdef CONFIG_LIBC_EXECFUNCS + if (argc > 2) + { + nsh_chroot_closefds(); + execvp(argv[2], &argv[2]); + nsh_error(vtbl, g_fmtcmdfailed, argv[0], "execvp", NSH_ERRNO); + return ERROR; + } +#else + if (argc > 2) + { + nsh_error(vtbl, g_fmtcmdfailed, argv[0], "execvp", NSH_ERRNO); + return ERROR; + } +#endif + + return OK; +} +#endif /* CONFIG_FS_CHROOT && !CONFIG_NSH_DISABLE_CHROOT */ + /**************************************************************************** * Name: cmd_echo ****************************************************************************/ diff --git a/testing/ostest/CMakeLists.txt b/testing/ostest/CMakeLists.txt index 8eb6516790e..7b8a034c374 100644 --- a/testing/ostest/CMakeLists.txt +++ b/testing/ostest/CMakeLists.txt @@ -68,6 +68,14 @@ if(CONFIG_TESTING_OSTEST) list(APPEND SRCS waitpid.c) endif() + if(CONFIG_FS_CHROOT) + if(CONFIG_SCHED_WAITPID) + if(NOT CONFIG_BUILD_KERNEL) + list(APPEND SRCS chroot.c) + endif() + endif() + endif() + if(NOT CONFIG_DISABLE_PTHREAD) list( APPEND diff --git a/testing/ostest/Makefile b/testing/ostest/Makefile index c89b2466f4f..9ddd8fffbac 100644 --- a/testing/ostest/Makefile +++ b/testing/ostest/Makefile @@ -83,6 +83,14 @@ ifeq ($(CONFIG_SCHED_WAITPID),y) CSRCS += waitpid.c endif +ifeq ($(CONFIG_FS_CHROOT),y) +ifeq ($(CONFIG_SCHED_WAITPID),y) +ifneq ($(CONFIG_BUILD_KERNEL),y) +CSRCS += chroot.c +endif +endif +endif + ifneq ($(CONFIG_DISABLE_PTHREAD),y) CSRCS += cancel.c cond.c mutex.c timedmutex.c sem.c semtimed.c barrier.c CSRCS += timedwait.c pthread_rwlock.c pthread_rwlock_cancel.c schedlock.c diff --git a/testing/ostest/chroot.c b/testing/ostest/chroot.c new file mode 100644 index 00000000000..b5fc444654c --- /dev/null +++ b/testing/ostest/chroot.c @@ -0,0 +1,304 @@ +/**************************************************************************** + * apps/testing/ostest/chroot.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 "ostest.h" + +#if defined(CONFIG_FS_CHROOT) && defined(CONFIG_SCHED_WAITPID) && \ + !defined(CONFIG_BUILD_KERNEL) + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_LIBC_TMPDIR +# define CHROOT_TMPDIR CONFIG_LIBC_TMPDIR +#else +# define CHROOT_TMPDIR "/tmp" +#endif + +#define CHROOT_JAIL CHROOT_TMPDIR "/ostest_jail" +#define CHROOT_MARK CHROOT_JAIL "/marker" +#define CHROOT_SECRET CHROOT_TMPDIR "/ostest_secret" +#define CHROOT_PRIO 100 + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static int chroot_fail(FAR const char *msg) +{ + printf("chroot_test: ERROR %s errno=%d\n", msg, errno); + ASSERT(false); + return EXIT_FAILURE; +} + +static int chroot_grandchild(int argc, FAR char *argv[]) +{ + struct stat st; + + UNUSED(argc); + UNUSED(argv); + + if (stat("/marker", &st) < 0) + { + return chroot_fail("grandchild stat(/marker) failed"); + } + + printf("chroot_test: grandchild still sees the jail\n"); + return EXIT_SUCCESS; +} + +static int chroot_child(int argc, FAR char *argv[]) +{ + struct stat st; + pid_t pid; + int status; + int fd; + int hostfd; + + UNUSED(argc); + UNUSED(argv); + + hostfd = open(CHROOT_SECRET, O_RDONLY); + if (hostfd < 0) + { + return chroot_fail("open(host secret) failed"); + } + + if (chdir(CHROOT_JAIL) < 0) + { + return chroot_fail("chdir(jail) failed"); + } + + if (chroot(".") < 0) + { + return chroot_fail("chroot(.) failed"); + } + + if (chdir("/") < 0) + { + return chroot_fail("chdir(/) failed"); + } + + if (stat("/marker", &st) < 0) + { + return chroot_fail("stat(/marker) failed"); + } + + printf("chroot_test: /marker is visible inside the jail\n"); + + if (stat("/dev", &st) == 0) + { + return chroot_fail("stat(/dev) succeeded inside jail"); + } + + if (errno != ENOENT) + { + return chroot_fail("stat(/dev) expected ENOENT"); + } + + fd = open("/../dev", O_RDONLY); + if (fd >= 0) + { + close(fd); + errno = 0; + return chroot_fail("open(/../dev) escaped the jail"); + } + +#ifdef CONFIG_DEV_NULL + fd = open("/../dev/null", O_RDWR); + if (fd >= 0) + { + close(fd); + errno = 0; + return chroot_fail("open(/../dev/null) escaped the jail"); + } +#endif + + printf("chroot_test: host paths are not visible inside the jail\n"); + + /* chroot() does not close pre-opened host fds (POSIX). */ + + if (fcntl(hostfd, F_GETFD) < 0) + { + return chroot_fail("host fd closed by chroot()"); + } + + printf("chroot_test: pre-opened host fd still usable after chroot\n"); + close(hostfd); + + pid = task_create("chroot_gc", CHROOT_PRIO, STACKSIZE, + chroot_grandchild, NULL); + if (pid < 0) + { + return chroot_fail("task_create(grandchild) failed"); + } + + if (waitpid(pid, &status, 0) != pid) + { + return chroot_fail("waitpid(grandchild) failed"); + } + + if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) + { + printf("chroot_test: ERROR grandchild status=%d\n", status); + ASSERT(false); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +static int chroot_prepare(void) +{ + int fd; + ssize_t nwritten; + static const char marker[] = "ostest-chroot\n"; + + mkdir(CHROOT_TMPDIR, 0777); + if (mkdir(CHROOT_JAIL, 0777) < 0 && errno != EEXIST) + { + printf("chroot_test: ERROR mkdir(%s) failed errno=%d\n", + CHROOT_JAIL, errno); + ASSERT(false); + return ERROR; + } + + fd = open(CHROOT_MARK, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + { + printf("chroot_test: ERROR open(%s) failed errno=%d\n", + CHROOT_MARK, errno); + ASSERT(false); + return ERROR; + } + + nwritten = write(fd, marker, sizeof(marker) - 1); + close(fd); + if (nwritten != (ssize_t)(sizeof(marker) - 1)) + { + printf("chroot_test: ERROR write(marker) failed errno=%d\n", errno); + ASSERT(false); + return ERROR; + } + + fd = open(CHROOT_SECRET, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + { + printf("chroot_test: ERROR open(%s) failed errno=%d\n", + CHROOT_SECRET, errno); + ASSERT(false); + return ERROR; + } + + close(fd); + return OK; +} + +static void chroot_cleanup(void) +{ + unlink(CHROOT_MARK); + unlink(CHROOT_SECRET); + rmdir(CHROOT_JAIL); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int chroot_test(void) +{ + pid_t pid; + int status; +#ifdef CONFIG_DEV_NULL + int fd; +#endif + + printf("chroot_test: Starting test\n"); + + if (chroot_prepare() < 0) + { + return ERROR; + } + + pid = task_create("chroot_child", CHROOT_PRIO, STACKSIZE, + chroot_child, NULL); + if (pid < 0) + { + printf("chroot_test: ERROR task_create failed errno=%d\n", errno); + ASSERT(false); + chroot_cleanup(); + return ERROR; + } + + if (waitpid(pid, &status, 0) != pid) + { + printf("chroot_test: ERROR waitpid failed errno=%d\n", errno); + ASSERT(false); + chroot_cleanup(); + return ERROR; + } + + chroot_cleanup(); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) + { + printf("chroot_test: ERROR child status=%d\n", status); + ASSERT(false); + return ERROR; + } + +#ifdef CONFIG_DEV_NULL + fd = open("/dev/null", O_RDWR); + if (fd < 0) + { + printf("chroot_test: ERROR parent lost /dev/null errno=%d\n", + errno); + ASSERT(false); + return ERROR; + } + + close(fd); +#endif + + printf("chroot_test: PASSED\n"); + return OK; +} + +#endif /* CONFIG_FS_CHROOT && CONFIG_SCHED_WAITPID && !CONFIG_BUILD_KERNEL */ diff --git a/testing/ostest/ostest.h b/testing/ostest/ostest.h index 8fa6d64d0a9..e9b50981116 100644 --- a/testing/ostest/ostest.h +++ b/testing/ostest/ostest.h @@ -137,6 +137,13 @@ void restart_test(void); int waitpid_test(void); #endif +/* chroot.c *****************************************************************/ + +#if defined(CONFIG_FS_CHROOT) && defined(CONFIG_SCHED_WAITPID) && \ + !defined(CONFIG_BUILD_KERNEL) +int chroot_test(void); +#endif + /* wqueue.c *****************************************************************/ #if defined(CONFIG_SCHED_LPWORK) || defined(CONFIG_SCHED_HPWORK) diff --git a/testing/ostest/ostest_main.c b/testing/ostest/ostest_main.c index 14ed98de367..523b6aa4af1 100644 --- a/testing/ostest/ostest_main.c +++ b/testing/ostest/ostest_main.c @@ -403,6 +403,20 @@ static int user_main(int argc, char *argv[]) check_test_memory_usage(); #endif +#if defined(CONFIG_FS_CHROOT) && defined(CONFIG_SCHED_WAITPID) && \ + !defined(CONFIG_BUILD_KERNEL) + /* Check chroot() filesystem jail */ + + printf("\nuser_main: chroot test\n"); + if (chroot_test() != 0) + { + printf("user_main: ERROR chroot test failed\n"); + ASSERT(false); + } + + check_test_memory_usage(); +#endif + #if defined(CONFIG_TESTING_OSTEST_MULTIUSER) && defined(CONFIG_SCHED_USER_IDENTITY) /* Multi-user identity and file permission regression tests */