system/sudo: add setuid privilege helper

Install a UNIX-style setuid-root sudo app: the kernel raises euid on
exec, userspace verifies the caller with passwd_verify(), then
setresuid/setgroups and execvp() run the command.  Ostest covers
setuid exec after a hard credential drop.

Signed-off-by: Abhishek Mishra <mishra.abhishek2808@gmail.com>
This commit is contained in:
Abhishek Mishra 2026-08-14 17:21:23 +00:00 committed by Alan C. Assis
parent 84ffa84e24
commit 7358692c59
12 changed files with 643 additions and 19 deletions

View file

@ -209,6 +209,11 @@ int exec_builtin(FAR const char *appname, FAR char * const *argv,
/* Load and execute the application. */
ret = posix_spawn(&pid, builtin->name, &file_actions, &attr, argv, NULL);
if (ret == EACCES)
{
goto errout_with_actions;
}
if (ret != 0 && builtin->main != NULL)
#endif
{

View file

@ -21,13 +21,31 @@
# ##############################################################################
if(CONFIG_EXAMPLES_HELLO)
nuttx_add_application(
NAME
${CONFIG_EXAMPLES_HELLO_PROGNAME}
SRCS
hello_main.c
STACKSIZE
${CONFIG_EXAMPLES_HELLO_STACKSIZE}
PRIORITY
${CONFIG_EXAMPLES_HELLO_PRIORITY})
if(CONFIG_EXAMPLES_HELLO_RESTRICTED)
nuttx_add_application(
NAME
${CONFIG_EXAMPLES_HELLO_PROGNAME}
SRCS
hello_main.c
STACKSIZE
${CONFIG_EXAMPLES_HELLO_STACKSIZE}
PRIORITY
${CONFIG_EXAMPLES_HELLO_PRIORITY}
UID
0
GID
0
MODE
484)
else()
nuttx_add_application(
NAME
${CONFIG_EXAMPLES_HELLO_PROGNAME}
SRCS
hello_main.c
STACKSIZE
${CONFIG_EXAMPLES_HELLO_STACKSIZE}
PRIORITY
${CONFIG_EXAMPLES_HELLO_PRIORITY})
endif()
endif()

View file

@ -26,4 +26,14 @@ config EXAMPLES_HELLO_STACKSIZE
int "Hello stack size"
default DEFAULT_TASK_STACKSIZE
config EXAMPLES_HELLO_RESTRICTED
bool "Install hello as owner-execute only (0744)"
default n
depends on SCHED_USER_IDENTITY
---help---
Set the installed mode to ``-rwxr--r--`` owned by root so a
non-root user cannot exec ``/bin/hello``. Used with
``CONFIG_SYSTEM_SUDO`` to show ``sudo /bin/hello`` restoring
access.
endif

View file

@ -29,6 +29,12 @@ PRIORITY = $(CONFIG_EXAMPLES_HELLO_PRIORITY)
STACKSIZE = $(CONFIG_EXAMPLES_HELLO_STACKSIZE)
MODULE = $(CONFIG_EXAMPLES_HELLO)
ifeq ($(CONFIG_EXAMPLES_HELLO_RESTRICTED),y)
UID = 0
GID = 0
MODE = 0744
endif
# Hello, World! Example
MAINSRC = hello_main.c

View file

@ -253,6 +253,14 @@ int nsh_fileapp(FAR struct nsh_vtbl_s *vtbl, FAR const char *cmd,
*/
ret = posix_spawnp(&pid, cmd, &file_actions, &attr, argv, environ);
if (ret == EACCES)
{
nsh_error(vtbl, "nsh: %s: Permission denied\n", cmd);
rc = 1;
ret = OK;
goto errout_with_actions;
}
if (ret == OK)
{
/* The application was successfully started with pre-emption disabled.

View file

@ -178,10 +178,14 @@ static bool nsh_verify_credentials(FAR const char *username,
* Name: nsh_switch_credentials
*
* Description:
* Switch the session to the given UID/GID. NSH starts with real UID/GID
* zero; file permission checks use the effective identity. When the real
* UID is still zero, only the effective UID/GID are changed so that a
* later 'su' can regain root via seteuid(0) after password verification.
* Switch the session to the given UID/GID.
*
* When the real UID is still zero and the target is not root, set the
* real and effective IDs to the target and keep saved-root (suid/sgid
* 0). File DAC then uses the unprivileged effective ID, while setuid
* helpers such as sudo still see the real UID of the invoking user
* after S_ISUID raises the effective UID to 0. A later ``su root``
* can restore root from the saved IDs after password verification.
*
****************************************************************************/
@ -189,15 +193,25 @@ static int nsh_switch_credentials(uid_t uid, gid_t gid)
{
if (getuid() == 0)
{
if (geteuid() != 0 || getegid() != 0)
if (geteuid() != 0)
{
if (seteuid(0) != 0 || setegid(0) != 0)
if (seteuid(0) != 0)
{
return -errno;
}
}
if (seteuid(uid) != 0 || setegid(gid) != 0)
if (uid == 0)
{
if (setresgid(0, 0, 0) != 0 || setresuid(0, 0, 0) != 0)
{
return -errno;
}
return OK;
}
if (setresgid(gid, gid, 0) != 0 || setresuid(uid, uid, 0) != 0)
{
return -errno;
}
@ -205,7 +219,7 @@ static int nsh_switch_credentials(uid_t uid, gid_t gid)
return OK;
}
if (setuid(uid) != 0 || setgid(gid) != 0)
if (setresgid(gid, gid, gid) != 0 || setresuid(uid, uid, uid) != 0)
{
return -errno;
}
@ -222,8 +236,9 @@ static int nsh_switch_credentials(uid_t uid, gid_t gid)
*
* Description:
* Look up 'username' in the passwd database and set the calling task's
* session identity. When NSH still has real UID zero, only the effective
* UID/GID are updated so that 'su' can switch users later.
* session identity. When switching from real UID zero to a non-root
* user, real and effective IDs become that user and saved-root is kept
* so ``su root`` can restore privileges after authentication.
*
* Input Parameters:
* username - Login name to assume

View file

@ -0,0 +1,41 @@
# ##############################################################################
# apps/system/sudo/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_SYSTEM_SUDO)
nuttx_add_application(
NAME
sudo
PRIORITY
100
STACKSIZE
${CONFIG_DEFAULT_TASK_STACKSIZE}
MODULE
${CONFIG_SYSTEM_SUDO}
UID
0
GID
0
MODE
35309
SRCS
sudo_main.c)
endif()

47
system/sudo/Kconfig Normal file
View file

@ -0,0 +1,47 @@
#
# For a description of the syntax of this configuration file,
# see the file kconfig-language.txt in the NuttX tools repository.
#
config SYSTEM_SUDO
tristate "sudo (setuid privilege helper)"
default n
depends on SCHED_USER_IDENTITY
depends on LIBC_PASSWD_FILE
depends on FSUTILS_PASSWD
depends on LIBC_EXECFUNCS
---help---
Install a Linux-style setuid-root ``sudo`` program. The kernel
raises the effective UID to zero when the ELF is loaded (see
``nx_uid`` / ``nx_mode`` in Application.mk). ``sudo`` verifies
the invoking user's password with ``passwd_verify()``, becomes
fully root via ``setresuid()`` / ``setresgid()``, then
``execvp()``s the requested command.
Requires ``CONFIG_LIBC_EXECFUNCS`` so NSH (or ``posix_spawn``)
loads the application ELF instead of calling its entry point
directly from the builtin registry.
Non-root callers must appear in the sudoers file
(``SYSTEM_SUDO_SUDOERS_PATH``) or in
``SYSTEM_SUDO_ALLOWED_USERS``. Real UID 0 is always allowed.
if SYSTEM_SUDO
config SYSTEM_SUDO_SUDOERS_PATH
string "Path to sudoers allowlist"
default "/etc/sudoers"
---help---
Simple allowlist, one username per line. Lines starting with
``#`` are comments. A Linux-style first field is also accepted
(``user ALL=(ALL) ALL``). Generate this file in ROMFS with
``CONFIG_BOARD_ETC_ROMFS_PASSWD_EXTRA_ENABLE``.
config SYSTEM_SUDO_ALLOWED_USERS
string "Compile-time sudoers usernames"
default ""
---help---
Optional comma-separated extra allowlist compiled into sudo.
Use this when /etc/sudoers is not present. Example: ``user``.
endif # SYSTEM_SUDO

25
system/sudo/Make.defs Normal file
View file

@ -0,0 +1,25 @@
############################################################################
# apps/system/sudo/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_SYSTEM_SUDO),)
CONFIGURED_APPS += $(APPDIR)/system/sudo
endif

38
system/sudo/Makefile Normal file
View file

@ -0,0 +1,38 @@
############################################################################
# apps/system/sudo/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 = sudo
PRIORITY = 100
STACKSIZE = $(CONFIG_DEFAULT_TASK_STACKSIZE)
MODULE = $(CONFIG_SYSTEM_SUDO)
MAINSRC = sudo_main.c
# Set-user-ID root executable (Linux-style sudo): S_IFREG | S_ISUID |
# S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH
UID = 0
GID = 0
MODE = 35309
include $(APPDIR)/Application.mk

336
system/sudo/sudo_main.c Normal file
View file

@ -0,0 +1,336 @@
/****************************************************************************
* apps/system/sudo/sudo_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 <ctype.h>
#include <errno.h>
#include <grp.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <fsutils/passwd.h>
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define SUDO_PROBE_ARG "--probe"
#define SUDO_MAX_PASSWORD 256
#define SUDO_SUDOERS_LINE 128
/****************************************************************************
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: sudo_read_password
****************************************************************************/
static int sudo_read_password(FAR char *password, size_t buflen)
{
struct termios saved;
struct termios cfg;
ssize_t nread;
int errcode = 0;
bool restore = false;
if (isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &saved) == 0)
{
cfg = saved;
cfg.c_lflag &= (tcflag_t)~ECHO;
if (tcsetattr(STDIN_FILENO, TCSANOW, &cfg) == 0)
{
restore = true;
}
}
password[0] = '\0';
nread = read(STDIN_FILENO, password, buflen - 1);
if (nread < 0)
{
errcode = errno;
}
if (restore)
{
tcsetattr(STDIN_FILENO, TCSANOW, &saved);
}
if (nread < 0)
{
return -errcode;
}
if (nread == 0)
{
return -ENODATA;
}
password[nread] = '\0';
if (password[nread - 1] == '\n')
{
password[nread - 1] = '\0';
}
fputc('\n', stderr);
return 0;
}
/****************************************************************************
* Name: sudo_token_eq
****************************************************************************/
static bool sudo_token_eq(FAR const char *name, FAR const char *tok,
size_t toklen)
{
return strncmp(name, tok, toklen) == 0 && name[toklen] == '\0';
}
/****************************************************************************
* Name: sudo_name_in_csv
****************************************************************************/
static bool sudo_name_in_csv(FAR const char *name, FAR const char *list)
{
FAR const char *p = list;
FAR const char *start;
if (name == NULL || list == NULL)
{
return false;
}
while (*p != '\0')
{
while (*p == ',' || isspace((unsigned char)*p))
{
p++;
}
if (*p == '\0')
{
break;
}
start = p;
while (*p != '\0' && *p != ',' && !isspace((unsigned char)*p))
{
p++;
}
if (sudo_token_eq(name, start, p - start))
{
return true;
}
}
return false;
}
/****************************************************************************
* Name: sudo_name_in_file
****************************************************************************/
static bool sudo_name_in_file(FAR const char *name, FAR const char *path)
{
FAR FILE *fp;
char line[SUDO_SUDOERS_LINE];
FAR char *tok;
FAR char *end;
fp = fopen(path, "r");
if (fp == NULL)
{
return false;
}
while (fgets(line, sizeof(line), fp) != NULL)
{
tok = line;
while (*tok != '\0' && isspace((unsigned char)*tok))
{
tok++;
}
if (*tok == '\0' || *tok == '#')
{
continue;
}
end = tok;
while (*end != '\0' && !isspace((unsigned char)*end))
{
end++;
}
if (sudo_token_eq(name, tok, end - tok))
{
fclose(fp);
return true;
}
}
fclose(fp);
return false;
}
/****************************************************************************
* Name: sudo_user_allowed
*
* Description:
* Real UID 0 may always run sudo. Other users must appear in the sudoers
* file and/or CONFIG_SYSTEM_SUDO_ALLOWED_USERS.
*
****************************************************************************/
static bool sudo_user_allowed(FAR const char *name, uid_t ruid)
{
if (ruid == 0)
{
return true;
}
if (sudo_name_in_csv(name, CONFIG_SYSTEM_SUDO_ALLOWED_USERS))
{
return true;
}
return sudo_name_in_file(name, CONFIG_SYSTEM_SUDO_SUDOERS_PATH);
}
/****************************************************************************
* Name: sudo_lookup_invoker
****************************************************************************/
static int sudo_lookup_invoker(FAR struct passwd *result,
FAR char *buf, size_t buflen)
{
FAR struct passwd *found;
int ret;
ret = getpwuid_r(getuid(), result, buf, buflen, &found);
if (ret != 0)
{
return -ret;
}
if (found == NULL)
{
return -ENOENT;
}
return 0;
}
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* sudo_main
*
* Description:
* Linux-style setuid-root helper: the kernel raises effective UID to the
* file owner on exec (see nx_uid/nx_mode in the application build).
* This program checks the sudoers allowlist, verifies the invoking user's
* password, becomes fully root with setresuid/setresgid, then execvp()s
* the requested command.
*
****************************************************************************/
int main(int argc, FAR char *argv[])
{
struct passwd invoker;
char pwbuf[CONFIG_LIBC_PASSWD_LINESIZE];
char password[SUDO_MAX_PASSWORD + 1];
int ret;
if (geteuid() != 0)
{
fprintf(stderr, "sudo: effective uid is not 0\n");
return 1;
}
if (argc >= 2 && strcmp(argv[1], SUDO_PROBE_ARG) == 0)
{
printf("ruid=%d euid=%d\n", getuid(), geteuid());
return geteuid() == 0 && getuid() != 0 ? 0 : 1;
}
if (argc < 2)
{
fprintf(stderr, "usage: sudo <command> [args...]\n");
return 1;
}
ret = sudo_lookup_invoker(&invoker, pwbuf, sizeof(pwbuf));
if (ret < 0)
{
fprintf(stderr, "sudo: cannot resolve invoking user: %d\n", -ret);
return 1;
}
if (!sudo_user_allowed(invoker.pw_name, getuid()))
{
fprintf(stderr, "sudo: %s is not in the sudoers file\n",
invoker.pw_name);
return 1;
}
fprintf(stderr, "[sudo] password for %s: ", invoker.pw_name);
ret = sudo_read_password(password, sizeof(password));
if (ret < 0)
{
fprintf(stderr, "sudo: password read failed: %d\n", -ret);
return 1;
}
ret = passwd_verify(invoker.pw_name, password);
explicit_bzero(password, sizeof(password));
if (!PASSWORD_VERIFY_MATCH(ret))
{
fprintf(stderr, "sudo: authentication failure\n");
return 1;
}
if (setresuid(0, 0, 0) != 0 || setresgid(0, 0, 0) != 0)
{
fprintf(stderr, "sudo: cannot set root identity: %d\n", errno);
return 1;
}
initgroups("root", 0);
execvp(argv[1], &argv[1]);
fprintf(stderr, "sudo: exec failed: %d\n", errno);
return 1;
}

View file

@ -40,6 +40,10 @@
#include <sys/wait.h>
#include <unistd.h>
#if defined(CONFIG_LIBC_EXECFUNCS) && defined(CONFIG_SYSTEM_SUDO)
# include <spawn.h>
#endif
#if !defined(CONFIG_DISABLE_MQUEUE)
# include <mqueue.h>
#endif
@ -1325,6 +1329,69 @@ static int multiuser_passwd_test(FAR struct mu_ctx_s *ctx)
#endif /* CONFIG_LIBC_PASSWD_FILE && CONFIG_TESTING_OSTEST_MULTIUSER */
#if defined(CONFIG_LIBC_EXECFUNCS) && defined(CONFIG_SYSTEM_SUDO) && \
defined(CONFIG_SCHED_WAITPID) && !defined(CONFIG_BUILD_KERNEL)
static int multiuser_sudo_setuid_test(FAR struct mu_ctx_s *ctx)
{
posix_spawnattr_t attr;
FAR char * const spawn_argv[] =
{
(FAR char *)"sudo", (FAR char *)"--probe", NULL
};
pid_t pid;
int status;
int ret;
printf("multiuser: setuid sudo exec after hard credential drop\n");
mu_restore_root(ctx);
ret = setuid(MU_UID1);
if (mu_expect_ok(ctx, "setuid(1000) before sudo exec", ret) != 0)
{
return ctx->failures;
}
mu_check_eq(ctx, "parent euid before sudo", geteuid(), MU_UID1);
ret = posix_spawnattr_init(&attr);
if (mu_expect_ok(ctx, "posix_spawnattr_init", ret) != 0)
{
return ctx->failures;
}
ret = posix_spawn(&pid, "sudo", NULL, &attr, spawn_argv, NULL);
posix_spawnattr_destroy(&attr);
if (mu_expect_ok(ctx, "posix_spawn(sudo --probe)", ret) != 0)
{
mu_restore_root(ctx);
return ctx->failures;
}
if (waitpid(pid, &status, 0) != pid)
{
mu_fail(ctx, "waitpid(sudo) errno=%d", errno);
mu_restore_root(ctx);
return ctx->failures;
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS)
{
mu_fail(ctx, "sudo --probe status=%d", status);
}
else
{
mu_pass("sudo --probe after hard drop");
}
mu_check_eq(ctx, "parent euid after sudo", geteuid(), MU_UID1);
mu_restore_root(ctx);
return ctx->failures;
}
#endif /* CONFIG_LIBC_EXECFUNCS && CONFIG_SYSTEM_SUDO && CONFIG_SCHED_WAITPID */
#endif /* CONFIG_SCHED_USER_IDENTITY */
/****************************************************************************
@ -1389,6 +1456,14 @@ int multiuser_test(void)
"(need LIBC_PASSWD_FILE and TESTING_OSTEST_MULTIUSER)\n");
#endif
#if defined(CONFIG_LIBC_EXECFUNCS) && defined(CONFIG_SYSTEM_SUDO) && \
defined(CONFIG_SCHED_WAITPID) && !defined(CONFIG_BUILD_KERNEL)
multiuser_sudo_setuid_test(&ctx);
#else
printf("multiuser: skipping setuid sudo exec test "
"(need LIBC_EXECFUNCS, SYSTEM_SUDO, SCHED_WAITPID)\n");
#endif
mu_restore_root(&ctx);
printf("multiuser_test: %d failure(s)\n", ctx.failures);