diff --git a/Documentation/applications/nsh/commands.rst b/Documentation/applications/nsh/commands.rst index f1d80040457..d1477e958df 100644 --- a/Documentation/applications/nsh/commands.rst +++ b/Documentation/applications/nsh/commands.rst @@ -1833,6 +1833,71 @@ Option Purpose nsh$ whoami testuser +.. _cmdsudo: + +``sudo`` Run a Command as Root (setuid helper) +============================================== + +**Command Syntax**:: + + sudo [args...] + +**Synopsis**. Run a single command with root privileges using a +Linux-style setuid-root helper program (``CONFIG_SYSTEM_SUDO``). The +kernel raises the effective UID to the file owner when the ``sudo`` ELF +is loaded (``S_ISUID`` via ``nx_mode`` in the application build). +``sudo`` then: + +1. Identifies the invoking user from the real UID (``getuid()``). +2. Checks that user against the sudoers allowlist (``/etc/sudoers`` and/or + ``CONFIG_SYSTEM_SUDO_ALLOWED_USERS``). Real UID 0 is always allowed. +3. Verifies that user's password with ``passwd_verify()`` in userspace. +4. Calls ``setresuid()`` / ``setresgid()`` / ``initgroups()`` to become + fully root. +5. ``execvp()``s the requested command, replacing the ``sudo`` process. + +Unlike the NSH ``su`` builtin (which changes the shell session), +``sudo`` runs one command and exits. After a hard credential drop +(``setuid()`` to a non-zero user), unprivileged code cannot call +``seteuid(0)``; executing the setuid ``sudo`` binary is the supported +way to regain root for a single command. + +**Requirements** + +.. list-table:: + :header-rows: 1 + :widths: 60 40 + + * - Option + - Purpose + * - ``CONFIG_SCHED_USER_IDENTITY`` + - UID/GID tracking and setuid-on-exec + * - ``CONFIG_FSUTILS_PASSWD`` + - ``passwd_verify()`` password check + * - ``CONFIG_LIBC_EXECFUNCS`` + - Load the ``sudo`` ELF (not builtin main) + * - ``CONFIG_SYSTEM_SUDO`` + - Build and install the setuid helper + * - ``CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE`` + - Unprivileged ``user`` plus ``/etc/sudoers`` + * - ``CONFIG_EXAMPLES_HELLO_RESTRICTED`` + - ``/bin/hello`` as ``-rwxr--r--`` root + +**Example** (login as root, then drop to a sudoers user):: + + nsh# id + uid=0(root) gid=0(root) + nsh# ls -l /bin/sudo + -rwsr-xr-x root root 0 /bin/sudo + nsh# ls -l /bin/hello + -rwxr--r-- root root 0 /bin/hello + nsh# su user + nsh$ /bin/hello + nsh: /bin/hello: Permission denied + nsh$ sudo /bin/hello + [sudo] password for user: + Hello, World!! + .. _cmdtelnetd: ``telnetd`` Time Start the Telnet Daemon diff --git a/Documentation/components/tools/index.rst b/Documentation/components/tools/index.rst index a3c5a446eb1..294e4b0a6dc 100644 --- a/Documentation/components/tools/index.rst +++ b/Documentation/components/tools/index.rst @@ -103,11 +103,18 @@ Kconfig CONFIG_CRYPTO_CRYPTODEV=y CONFIG_BOARD_ETC_ROMFS_PASSWD_USER="root" CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD="" + CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE=y CONFIG_FSUTILS_PASSWD_PBKDF2_ITERATIONS=10000 -``make savedefconfig`` omits ``CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD`` and +``make savedefconfig`` omits ``CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD``, +``CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD``, and ``CONFIG_FSUTILS_PASSWD_PBKDF2_ITERATIONS`` to avoid leaking credentials. +With ``CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE``, ``mkpasswd`` appends a +second account (default ``user``, UID 1000). By default that account +reuses the root password. ``CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_SUDOERS`` +also writes ``/etc/sudoers`` listing ``root`` and the extra user. + Host files ~~~~~~~~~~ diff --git a/Documentation/implementation/user_identity.rst b/Documentation/implementation/user_identity.rst index e7fe651069a..ffb08aee3b5 100644 --- a/Documentation/implementation/user_identity.rst +++ b/Documentation/implementation/user_identity.rst @@ -120,6 +120,18 @@ Hard drop (clear saved-root):: ``setresgid()`` requires effective UID zero to assign arbitrary GIDs. Change group IDs before dropping the effective UID. +Setuid-on-exec +================ + +When ``binfmt`` loads an executable with the set-user-ID bit set +(``S_ISUID`` in ``nx_mode``), the new task group's effective and saved +UIDs become the file owner's UID while the real UID remains the caller's. +This is the mechanism used by the setuid-root ``sudo`` helper +(``CONFIG_SYSTEM_SUDO``); see :ref:`cmdsudo`. A valid test logs in as +root, drops to an unprivileged sudoers account (``su user``), confirms +that account cannot exec a root-only binary, then runs that binary +through ``sudo``. + Configuration ============= diff --git a/boards/Kconfig b/boards/Kconfig index 0eca78c8a96..784b2c06286 100644 --- a/boards/Kconfig +++ b/boards/Kconfig @@ -5741,4 +5741,55 @@ config BOARD_ETC_ROMFS_PASSWD_HOME string "Root home directory" default "/" +config BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE + bool "Add an unprivileged user to /etc/passwd" + default n + ---help--- + Append a second passwd entry (default name ``user``, UID/GID + 1000) so ``su user`` works on ROMFS-backed /etc/passwd. + Needed for a valid setuid sudo test: drop to this user, then + run ``sudo``. + +if BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_USER + string "Extra username" + default "user" + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_UID + int "Extra user ID" + default 1000 + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_GID + int "Extra group ID" + default 1000 + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_HOME + string "Extra user home directory" + default "/" + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_USE_ROOT_PASSWORD + bool "Use the root password for the extra user" + default y + ---help--- + If enabled, the extra user gets the same password as root. + Disable to set CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD + or NUTTX_ROMFS_PASSWD_EXTRA_PASSWORD separately. + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD + string "Extra user password (required if not using the root password)" + depends on !BOARD_ETC_ROMFS_PASSWD_EXTRA_USE_ROOT_PASSWORD + ---help--- + Hashed at build time; not stored in firmware and not saved in + defconfig. Same complexity rules as the root password. + +config BOARD_ETC_ROMFS_PASSWD_EXTRA_SUDOERS + bool "Install /etc/sudoers allowing the extra user" + default y + ---help--- + Write /etc/sudoers with ``root`` and the extra username so + ``CONFIG_SYSTEM_SUDO`` can authorize that account. + +endif # BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE + endif # BOARD_ETC_ROMFS_PASSWD_ENABLE diff --git a/cmake/nuttx_add_application.cmake b/cmake/nuttx_add_application.cmake index 131808739b5..4864eaedbd6 100644 --- a/cmake/nuttx_add_application.cmake +++ b/cmake/nuttx_add_application.cmake @@ -95,6 +95,9 @@ function(nuttx_add_application) STACKSIZE MODULE DYNLIB + UID + GID + MODE MULTI_VALUE COMPILE_FLAGS LINK_FLAGS diff --git a/cmake/nuttx_add_romfs.cmake b/cmake/nuttx_add_romfs.cmake index 7633872d037..d26bdfbfd02 100644 --- a/cmake/nuttx_add_romfs.cmake +++ b/cmake/nuttx_add_romfs.cmake @@ -366,22 +366,31 @@ function(process_all_directory_romfs) endif() set(GENPASSWD_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/etc/passwd) + set(GENPASSWD_OUTPUTS ${GENPASSWD_OUTPUT}) + if(CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE + AND CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_SUDOERS) + set(GENSUDOERS_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/etc/sudoers) + list(APPEND GENPASSWD_OUTPUTS ${GENSUDOERS_OUTPUT}) + endif() + add_custom_command( - OUTPUT ${GENPASSWD_OUTPUT} + OUTPUT ${GENPASSWD_OUTPUTS} COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/etc COMMAND - ${MKPASSWD_BIN} --user "${CONFIG_BOARD_ETC_ROMFS_PASSWD_USER}" - --password "${CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD}" --uid + ${NUTTX_POSIX_SHELL} ${NUTTX_DIR}/tools/board_romfs_mkpasswd.sh + ${NUTTX_DIR} ${CMAKE_CURRENT_BINARY_DIR}/.romfs_passwd.txt + ${MKPASSWD_BIN} ${GENPASSWD_OUTPUT} --user + "${CONFIG_BOARD_ETC_ROMFS_PASSWD_USER}" --uid ${CONFIG_BOARD_ETC_ROMFS_PASSWD_UID} --gid ${CONFIG_BOARD_ETC_ROMFS_PASSWD_GID} --home - "${CONFIG_BOARD_ETC_ROMFS_PASSWD_HOME}" --iterations - ${MKPASSWD_ITERATIONS} -o ${GENPASSWD_OUTPUT} + "${CONFIG_BOARD_ETC_ROMFS_PASSWD_HOME}" DEPENDS ${MKPASSWD_BIN} ${NUTTX_DIR}/.config + ${NUTTX_DIR}/tools/board_romfs_mkpasswd.sh COMMENT "Generating /etc/passwd with PBKDF2 hash") - add_custom_target(generate_passwd DEPENDS ${GENPASSWD_OUTPUT}) + add_custom_target(generate_passwd DEPENDS ${GENPASSWD_OUTPUTS}) add_dependencies(generate_passwd build_host_mkpasswd) - list(APPEND RCRAWS ${GENPASSWD_OUTPUT}) + list(APPEND RCRAWS ${GENPASSWD_OUTPUTS}) list(APPEND dyn_deps generate_passwd) endif() diff --git a/cmake/savedefconfig.cmake b/cmake/savedefconfig.cmake index db6742a3621..aef701e6bf8 100644 --- a/cmake/savedefconfig.cmake +++ b/cmake/savedefconfig.cmake @@ -77,7 +77,8 @@ foreach(LINE IN LISTS LINES) decode_brackets(LINE) decode_semicolon(LINE) if(NOT "${LINE}" MATCHES "^CONFIG_FSUTILS_PASSWD_PBKDF2_ITERATIONS=" - AND NOT "${LINE}" MATCHES "^CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD=") + AND NOT "${LINE}" MATCHES "^CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD=" + AND NOT "${LINE}" MATCHES "^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD=") file(APPEND ${OUTPUT_FILE} "${LINE}\n") endif() endforeach() diff --git a/fs/binfs/fs_binfs.c b/fs/binfs/fs_binfs.c index f65409b4e88..d9152e80803 100644 --- a/fs/binfs/fs_binfs.c +++ b/fs/binfs/fs_binfs.c @@ -279,9 +279,16 @@ static int binfs_fstat(FAR const struct file *filep, FAR struct stat *buf) { DEBUGASSERT(buf != NULL); - /* It's a execute-only file system */ +#ifdef CONFIG_SCHED_USER_IDENTITY + int index = (int)((uintptr_t)filep->f_priv); + + buf->st_uid = builtin_getuid(index); + buf->st_gid = builtin_getgid(index); + buf->st_mode = S_IFREG | (builtin_getmode(index) & ~S_IFMT); +#else + buf->st_mode = S_IFREG | S_IXOTH | S_IXGRP | S_IXUSR; +#endif - buf->st_mode = S_IFREG | S_IXOTH | S_IXGRP | S_IXUSR; buf->st_size = 0; buf->st_blksize = 0; buf->st_blocks = 0; @@ -491,12 +498,12 @@ static int binfs_stat(FAR struct inode *mountpt, FAR const char *relpath, /* It's a execute-only file name */ - buf->st_mode = S_IFREG | S_IXOTH | S_IXGRP | S_IXUSR; - #ifdef CONFIG_SCHED_USER_IDENTITY - buf->st_uid = builtin_getuid(index); - buf->st_gid = builtin_getgid(index); - buf->st_mode |= builtin_getmode(index); + buf->st_uid = builtin_getuid(index); + buf->st_gid = builtin_getgid(index); + buf->st_mode = S_IFREG | (builtin_getmode(index) & ~S_IFMT); +#else + buf->st_mode = S_IFREG | S_IXOTH | S_IXGRP | S_IXUSR; #endif } else diff --git a/sched/environ/env_secureexec.c b/sched/environ/env_secureexec.c index b886bca1443..5315f496c14 100644 --- a/sched/environ/env_secureexec.c +++ b/sched/environ/env_secureexec.c @@ -71,12 +71,17 @@ static bool env_is_unsafe(FAR const char *pair) for (name = names; *name != NULL; name++) { - if (strncmp(pair, *name, strlen(*name)) == 0) + if (pair != NULL && strncmp(pair, *name, strlen(*name)) == 0) { return true; } } + if (pair == NULL) + { + return false; + } + return strncmp(pair, "LD_", 3) == 0 || strncmp(pair, "MALLOC_", 7) == 0; } @@ -102,7 +107,9 @@ void env_sanitize_secure(FAR struct task_group_s *group) for (index = group->tg_envc - 1; index >= 0; ) { - if (env_is_unsafe(group->tg_envp[index])) + FAR char *pair = group->tg_envp[index]; + + if (pair != NULL && env_is_unsafe(pair)) { env_removevar(group, index); } diff --git a/tools/Unix.mk b/tools/Unix.mk index 625772aaae8..06cef9522c4 100644 --- a/tools/Unix.mk +++ b/tools/Unix.mk @@ -793,6 +793,7 @@ savedefconfig: apps_preconfig $(Q) $(call kconfig_tweak_disable,defconfig.tmp,CONFIG_BASE_DEFCONFIG) $(Q) sed -i.bak -e '/^CONFIG_FSUTILS_PASSWD_PBKDF2_ITERATIONS=/d' defconfig.tmp $(Q) sed -i.bak -e '/^CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD=/d' defconfig.tmp + $(Q) sed -i.bak -e '/^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD=/d' defconfig.tmp $(Q) grep "CONFIG_ARCH=" .config >> defconfig.tmp $(Q) grep "^CONFIG_ARCH_CHIP_" .config >> defconfig.tmp; true $(Q) grep "CONFIG_ARCH_CHIP=" .config >> defconfig.tmp; true diff --git a/tools/board_romfs_mkpasswd.sh b/tools/board_romfs_mkpasswd.sh index 2256bf91c98..b6e365c19cb 100755 --- a/tools/board_romfs_mkpasswd.sh +++ b/tools/board_romfs_mkpasswd.sh @@ -60,3 +60,48 @@ PASSWORD=$(cat "${PASSFILE}") --iterations "${ITERATIONS}" \ "$@" -o "${OUTPUT}" rm -f "${PASSFILE}" + +EXTRA_ENABLE=$(grep "^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE=y" \ + "${CONFIG_FILE}" 2>/dev/null || true) + +if [ -n "${EXTRA_ENABLE}" ]; then + EXTRA_USER=$(read_int_config CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_USER user) + EXTRA_UID=$(read_int_config CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_UID 1000) + EXTRA_GID=$(read_int_config CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_GID 1000) + EXTRA_HOME=$(read_int_config CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_HOME /) + EXTRA_LINE="${OUTPUT}.extra" + EXTRA_PASS="${PASSWORD}" + + if ! grep -q "^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_USE_ROOT_PASSWORD=y" \ + "${CONFIG_FILE}" 2>/dev/null; then + "${TOPDIR}/tools/promptpasswd.sh" \ + --min 8 \ + --config CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD \ + --config-file "${CONFIG_FILE}" \ + --update-config \ + --prompt "ROMFS extra user password (min 8 characters): " \ + --output-file "${PASSFILE}" + EXTRA_PASS=$(cat "${PASSFILE}") + rm -f "${PASSFILE}" + fi + + "${MKPASSWD}" --password "${EXTRA_PASS}" \ + --iterations "${ITERATIONS}" \ + --user "${EXTRA_USER}" \ + --uid "${EXTRA_UID}" \ + --gid "${EXTRA_GID}" \ + --home "${EXTRA_HOME}" \ + -o "${EXTRA_LINE}" + cat "${EXTRA_LINE}" >> "${OUTPUT}" + rm -f "${EXTRA_LINE}" + + if grep -q "^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_SUDOERS=y" \ + "${CONFIG_FILE}" 2>/dev/null; then + SUDOERS="$(dirname "${OUTPUT}")/sudoers" + { + echo "# NuttX sudo allowlist: one username per line" + echo "root" + echo "${EXTRA_USER}" + } > "${SUDOERS}" + fi +fi diff --git a/tools/update_romfs_password.sh b/tools/update_romfs_password.sh index 8e8be3ae42b..17358076a38 100755 --- a/tools/update_romfs_password.sh +++ b/tools/update_romfs_password.sh @@ -20,10 +20,12 @@ # update_romfs_password.sh # # When CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y and the root password is not -# set in .config, copy NUTTX_ROMFS_PASSWD_PASSWORD into .config. This is the -# supported way to supply build-time credentials that must not live in defconfig -# (CI, automation, local scripts). No-op when the password is already set or -# ROMFS passwd generation is disabled. +# set in .config, copy NUTTX_ROMFS_PASSWD_PASSWORD into .config. If an extra +# ROMFS user is enabled without using the root password, also copy +# NUTTX_ROMFS_PASSWD_EXTRA_PASSWORD. This is the supported way to supply +# build-time credentials that must not live in defconfig (CI, automation, +# local scripts). No-op when the password is already set or ROMFS passwd +# generation is disabled. set -e @@ -43,33 +45,46 @@ if ! grep -q '^CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE=y' "${CONFIG}"; then exit 0 fi -# True when CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD is unset or empty in .config -get_password() { - grep -E '^CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD=' "${CONFIG}" 2>/dev/null \ +# Apply a Kconfig string password if the symbol is empty and ENVVAL is set. + +apply_password() { + symbol=$1 + envval=$2 + envname=$3 + cur=$(grep -E "^${symbol}=" "${CONFIG}" 2>/dev/null \ | tail -n 1 \ | sed 's/^[^=]*=//' \ - | tr -d '"' + | tr -d '"') + if [ -n "${cur}" ]; then + return 0 + fi + + if [ -z "${envval}" ]; then + return 0 + fi + + if [ "${#envval}" -lt 8 ]; then + printf 'update_romfs_password: %s must be at least 8 characters\n' \ + "${envname}" >&2 + exit 1 + fi + + if command -v kconfig-tweak >/dev/null 2>&1; then + kconfig-tweak --file "${CONFIG}" --set-str "${symbol}" "${envval}" + else + sed -i.bak -e "/^${symbol}=/d" "${CONFIG}" + rm -f "${CONFIG}.bak" + printf '%s="%s"\n' "${symbol}" "${envval}" >> "${CONFIG}" + fi } -cur=$(get_password) -if [ -n "${cur}" ]; then - exit 0 -fi +apply_password CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD \ + "${PASSWD_ENV}" NUTTX_ROMFS_PASSWD_PASSWORD -if [ -z "${PASSWD_ENV}" ]; then - exit 0 -fi - -if [ "${#PASSWD_ENV}" -lt 8 ]; then - printf 'update_romfs_password: NUTTX_ROMFS_PASSWD_PASSWORD must be at least 8 characters\n' >&2 - exit 1 -fi - -if command -v kconfig-tweak >/dev/null 2>&1; then - kconfig-tweak --file "${CONFIG}" \ - --set-str CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD "${PASSWD_ENV}" -else - sed -i.bak -e '/^CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD=/d' "${CONFIG}" - rm -f "${CONFIG}.bak" - printf 'CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD="%s"\n' "${PASSWD_ENV}" >> "${CONFIG}" +if grep -q '^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE=y' "${CONFIG}" && + ! grep -q '^CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_USE_ROOT_PASSWORD=y' \ + "${CONFIG}"; then + apply_password CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_PASSWORD \ + "${NUTTX_ROMFS_PASSWD_EXTRA_PASSWORD:-}" \ + NUTTX_ROMFS_PASSWD_EXTRA_PASSWORD fi