sched,fs,docs: support setuid sudo helper

Supports the UNIX setuid-on-exec sudo helper. Documents the model,
generates an extra ROMFS user and /etc/sudoers for a non-root test,
reports BINFS modes from the builtin table so ls -l matches execute
bits, and skips NULL environment entries when sanitizing a setuid exec.

Signed-off-by: Abhishek Mishra <mishra.abhishek2808@gmail.com>
This commit is contained in:
Abhishek Mishra
2026-08-18 15:57:52 +08:00
committed by Xiang Xiao
parent a3f733d387
commit e29db6724c
12 changed files with 269 additions and 46 deletions
@@ -1833,6 +1833,71 @@ Option Purpose
nsh$ whoami
testuser
.. _cmdsudo:
``sudo`` Run a Command as Root (setuid helper)
==============================================
**Command Syntax**::
sudo <command> [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
+8 -1
View File
@@ -103,11 +103,18 @@ Kconfig
CONFIG_CRYPTO_CRYPTODEV=y
CONFIG_BOARD_ETC_ROMFS_PASSWD_USER="root"
CONFIG_BOARD_ETC_ROMFS_PASSWD_PASSWORD="<secret>"
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
~~~~~~~~~~
@@ -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
=============
+51
View File
@@ -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
+3
View File
@@ -95,6 +95,9 @@ function(nuttx_add_application)
STACKSIZE
MODULE
DYNLIB
UID
GID
MODE
MULTI_VALUE
COMPILE_FLAGS
LINK_FLAGS
+16 -7
View File
@@ -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()
+2 -1
View File
@@ -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()
+14 -7
View File
@@ -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
+9 -2
View File
@@ -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);
}
+1
View File
@@ -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
+45
View File
@@ -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
+43 -28
View File
@@ -20,10 +20,12 @@
# update_romfs_password.sh <path-to-.config>
#
# 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