From 4cf52923689a2f957599e2e8778958820c8ddc78 Mon Sep 17 00:00:00 2001 From: Abhishek Mishra Date: Wed, 12 Aug 2026 06:39:37 +0000 Subject: [PATCH 1/2] testing/ostest: expand multiuser coverage for groups and setresuid Add supplementary-group, setresuid/setresgid ordering, and saved set-UID/GID child tests for the POSIX credential APIs. Signed-off-by: Abhishek Mishra --- testing/ostest/multiuser.c | 212 ++++++++++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 4 deletions(-) diff --git a/testing/ostest/multiuser.c b/testing/ostest/multiuser.c index 6bd93a06f8c..e9a55721ca5 100644 --- a/testing/ostest/multiuser.c +++ b/testing/ostest/multiuser.c @@ -29,7 +29,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -148,9 +151,9 @@ static int mu_restore_root(FAR struct mu_ctx_s *ctx) static int mu_set_effective(FAR struct mu_ctx_s *ctx, uid_t uid, gid_t gid) { - /* NuttX grants arbitrary seteuid/setegid only while the effective ID - * is 0. With real UID 0 (flat NSH), restore effective root before - * switching user, matching nsh_switch_credentials(). + /* seteuid/setegid allow arbitrary IDs only while euid is 0. With + * real UID 0, restore euid 0 before switching, matching + * nsh_switch_credentials(). */ if (getuid() == 0 && (geteuid() != 0 || getegid() != 0)) @@ -322,6 +325,200 @@ static int multiuser_effective_test(FAR struct mu_ctx_s *ctx) return ctx->failures; } +#if CONFIG_SCHED_NGROUPS > 0 +static int multiuser_groups_test(FAR struct mu_ctx_s *ctx) +{ + gid_t list[NGROUPS_MAX]; + gid_t got[NGROUPS_MAX]; + int n; + int ret; + + printf("multiuser: setgroups/getgroups supplementary IDs\n"); + + list[0] = MU_GID2; + list[1] = (gid_t)(MU_GID2 + 1); + + ret = setgroups(2, list); + if (mu_expect_ok(ctx, "setgroups(2)", ret) != 0) + { + return ctx->failures; + } + + n = getgroups(0, NULL); + mu_check_eq(ctx, "getgroups(0) count", n, 2); + + n = getgroups(NGROUPS_MAX, got); + mu_check_eq(ctx, "getgroups() count", n, 2); + if (n >= 2) + { + mu_check_eq(ctx, "getgroups()[0]", got[0], list[0]); + mu_check_eq(ctx, "getgroups()[1]", got[1], list[1]); + } + + ret = setgroups(0, NULL); + if (mu_expect_ok(ctx, "setgroups(0) clear", ret) != 0) + { + return ctx->failures; + } + + n = getgroups(0, NULL); + mu_check_eq(ctx, "getgroups after clear", n, 0); + + /* setgroups rejects size > NGROUPS_MAX (no silent truncate). */ + + ret = setgroups(NGROUPS_MAX + 1, list); + if (ret != 0 && errno == EINVAL) + { + mu_pass("setgroups(NGROUPS_MAX+1) rejected errno=EINVAL"); + } + else + { + mu_fail(ctx, "setgroups(NGROUPS_MAX+1): ret=%d errno=%d " + "(expected EINVAL)", ret, errno); + } + +#if defined(CONFIG_LIBC_GROUP_FILE) + /* getgrouplist/initgroups fail (do not truncate) when membership > + * NGROUPS_MAX. Temporarily replace the group file for this check. + */ + + { + char bak[128]; + FILE *fp; + int i; + int need; + gid_t gbuf[NGROUPS_MAX]; + int ng; + int saved_errno; + + snprintf(bak, sizeof(bak), "%s.bak", CONFIG_LIBC_GROUP_FILEPATH); + unlink(bak); + rename(CONFIG_LIBC_GROUP_FILEPATH, bak); + + fp = fopen(CONFIG_LIBC_GROUP_FILEPATH, "w"); + if (fp == NULL) + { + mu_fail(ctx, "fopen(%s) for overflow test", + CONFIG_LIBC_GROUP_FILEPATH); + rename(bak, CONFIG_LIBC_GROUP_FILEPATH); + return ctx->failures; + } + + /* Primary MU_GID1 plus NGROUPS_MAX distinct supplementary gids. */ + + for (i = 0; i < NGROUPS_MAX; i++) + { + fprintf(fp, "g%d:*:%d:mu_overflow\n", i, (int)(MU_GID2 + i)); + } + + fclose(fp); + + ng = NGROUPS_MAX; + need = getgrouplist("mu_overflow", MU_GID1, gbuf, &ng); + mu_check_eq(ctx, "getgrouplist overflow returns -1", need, -1); + mu_check_eq(ctx, "getgrouplist reports required size", ng, + NGROUPS_MAX + 1); + + saved_errno = 0; + ret = initgroups("mu_overflow", MU_GID1); + if (ret == 0) + { + mu_fail(ctx, "initgroups should fail when groups > NGROUPS_MAX"); + } + else + { + saved_errno = errno; + mu_pass("initgroups fails when groups > NGROUPS_MAX (errno=%d)", + saved_errno); + } + + unlink(CONFIG_LIBC_GROUP_FILEPATH); + rename(bak, CONFIG_LIBC_GROUP_FILEPATH); + } +#endif /* CONFIG_LIBC_GROUP_FILE */ + + return ctx->failures; +} +#endif /* CONFIG_SCHED_NGROUPS > 0 */ + +/**************************************************************************** + * Name: multiuser_setres_order_test + * + * Description: + * Regression: setresgid requires euid==0. Dropping uid before gid fails; + * gid-then-uid succeeds (NSH assume_identity order). + * + ****************************************************************************/ + +static int multiuser_setres_order_test(FAR struct mu_ctx_s *ctx) +{ + uid_t ruid; + uid_t euid; + uid_t suid; + gid_t rgid; + gid_t egid; + gid_t sgid; + int ret; + + printf("multiuser: setresuid/setresgid drop ordering\n"); + + mu_restore_root(ctx); + + /* uid-first must fail once euid is no longer 0. */ + + ret = setresuid(MU_UID1, MU_UID1, 0); + if (mu_expect_ok(ctx, "setresuid(1000,1000,0) first", ret) != 0) + { + mu_restore_root(ctx); + return ctx->failures; + } + + ret = setresgid(MU_GID1, MU_GID1, 0); + mu_expect_denied(ctx, "setresgid after uid drop (wrong order)", ret); + + ret = seteuid(0); + if (mu_expect_ok(ctx, "seteuid(0) via suid after bad order", ret) != 0) + { + mu_restore_root(ctx); + return ctx->failures; + } + + ret = setresuid(0, 0, 0); + mu_expect_ok(ctx, "setresuid(0,0,0) reset", ret); + ret = setresgid(0, 0, 0); + mu_expect_ok(ctx, "setresgid(0,0,0) reset", ret); + + /* gid-then-uid (correct order) must succeed. */ + + ret = setresgid(MU_GID1, MU_GID1, 0); + if (mu_expect_ok(ctx, "setresgid(1000,1000,0) first", ret) != 0) + { + mu_restore_root(ctx); + return ctx->failures; + } + + ret = setresuid(MU_UID1, MU_UID1, 0); + if (mu_expect_ok(ctx, "setresuid(1000,1000,0) second", ret) != 0) + { + mu_restore_root(ctx); + return ctx->failures; + } + + getresuid(&ruid, &euid, &suid); + getresgid(&rgid, &egid, &sgid); + mu_check_eq(ctx, "ruid after gid-then-uid", ruid, MU_UID1); + mu_check_eq(ctx, "euid after gid-then-uid", euid, MU_UID1); + mu_check_eq(ctx, "suid after gid-then-uid", suid, 0); + mu_check_eq(ctx, "rgid after gid-then-uid", rgid, MU_GID1); + mu_check_eq(ctx, "egid after gid-then-uid", egid, MU_GID1); + mu_check_eq(ctx, "sgid after gid-then-uid", sgid, 0); + + mu_restore_root(ctx); + setresuid(0, 0, 0); + setresgid(0, 0, 0); + return ctx->failures; +} + static int multiuser_resuid_test(FAR struct mu_ctx_s *ctx) { uid_t ruid; @@ -798,7 +995,7 @@ static int multiuser_mqueue_test(FAR struct mu_ctx_s *ctx) memset(&attr, 0, sizeof(attr)); attr.mq_maxmsg = 4; - attr.mq_msgsize = 64; + attr.mq_msgsize = CONFIG_MQ_MAXMSGSIZE; if (mu_set_effective(ctx, MU_UID1, MU_GID1) != 0) { @@ -1146,6 +1343,13 @@ int multiuser_test(void) printf("multiuser_test: start\n"); multiuser_effective_test(&ctx); +#if CONFIG_SCHED_NGROUPS > 0 + multiuser_groups_test(&ctx); +#else + printf("multiuser: skipping supplementary groups test " + "(need CONFIG_SCHED_NGROUPS > 0)\n"); +#endif + multiuser_setres_order_test(&ctx); multiuser_resuid_test(&ctx); #if defined(CONFIG_SCHED_WAITPID) && !defined(CONFIG_BUILD_KERNEL) From b07c6093da204d144342b9bc7256b0096b711903 Mon Sep 17 00:00:00 2001 From: Abhishek Mishra Date: Wed, 12 Aug 2026 09:22:43 +0000 Subject: [PATCH 2/2] nshlib: apply #/$ prompt markers after login When NSH_PROMPT_STRING_ROOT/USER are empty, keep NSH_PROMPT_STRING at boot (for example, "nsh> ") so CI/NTFC boot detection still works. After login, su, or telnet login, replace the last '>' with '#' (euid 0) or '$' (non-zero euid) and ensure a trailing space. Refresh readline after console/telnet login when line editing is enabled. Signed-off-by: Abhishek Mishra --- nshlib/Kconfig | 14 +++-- nshlib/nsh.h | 1 + nshlib/nsh_identity.c | 2 +- nshlib/nsh_login.c | 2 +- nshlib/nsh_prompt.c | 128 +++++++++++++++++++++++++++++++++++++-- nshlib/nsh_session.c | 7 +++ nshlib/nsh_telnetlogin.c | 2 +- 7 files changed, 141 insertions(+), 15 deletions(-) diff --git a/nshlib/Kconfig b/nshlib/Kconfig index fbbb08d3b39..e0259a2ac1c 100644 --- a/nshlib/Kconfig +++ b/nshlib/Kconfig @@ -74,18 +74,20 @@ config NSH_PROMPT_STRING_ROOT default "" depends on SCHED_USER_IDENTITY ---help--- - If non-empty, NSH uses this prompt when the effective UID is zero. - If empty, the prompt from NSH_PROMPT_STRING (or ENV/HOSTNAME) is used. - Set explicitly for multi-user shells (for example, "nsh# "). + Optional full prompt override when the effective UID is zero. + If empty, NSH keeps NSH_PROMPT_STRING until login; after login the + last '>' in the base prompt becomes '#' (for example, "nsh> " + becomes "nsh# "), or '#' is appended when the prompt has no '>'. config NSH_PROMPT_STRING_USER string "Prompt string for non-root effective UID" default "" depends on SCHED_USER_IDENTITY ---help--- - If non-empty, NSH uses this prompt when the effective UID is non-zero. - If empty, the prompt from NSH_PROMPT_STRING (or ENV/HOSTNAME) is used. - Set explicitly for multi-user shells (for example, "nsh$ "). + Optional full prompt override when the effective UID is non-zero. + If empty, NSH keeps NSH_PROMPT_STRING until login; after login the + last '>' in the base prompt becomes '$' (for example, "nsh> " + becomes "nsh$ "), or '$' is appended when the prompt has no '>'. config NSH_PROMPT_MAX int "Maximum Size of Prompt String" diff --git a/nshlib/nsh.h b/nshlib/nsh.h index a7ceb7c95a9..295f11b947f 100644 --- a/nshlib/nsh.h +++ b/nshlib/nsh.h @@ -822,6 +822,7 @@ int nsh_parse(FAR struct nsh_vtbl_s *vtbl, FAR char *cmdline); FAR const char *nsh_prompt(void); void nsh_update_prompt(void); +void nsh_update_prompt_after_login(void); /**************************************************************************** * Name: nsh_login diff --git a/nshlib/nsh_identity.c b/nshlib/nsh_identity.c index 6d9e6160f84..5027975c5d5 100644 --- a/nshlib/nsh_identity.c +++ b/nshlib/nsh_identity.c @@ -377,7 +377,7 @@ int cmd_su(FAR struct nsh_vtbl_s *vtbl, int argc, FAR char **argv) return ERROR; } - nsh_update_prompt(); + nsh_update_prompt_after_login(); return OK; } #endif diff --git a/nshlib/nsh_login.c b/nshlib/nsh_login.c index 228a66397dd..860c58964fe 100644 --- a/nshlib/nsh_login.c +++ b/nshlib/nsh_login.c @@ -256,7 +256,7 @@ int nsh_login(FAR struct console_stdio_s *pstate) return -1; } - nsh_update_prompt(); + nsh_update_prompt_after_login(); #endif return OK; } diff --git a/nshlib/nsh_prompt.c b/nshlib/nsh_prompt.c index 04f110a835f..48566eea03d 100644 --- a/nshlib/nsh_prompt.c +++ b/nshlib/nsh_prompt.c @@ -31,6 +31,7 @@ #include #ifdef CONFIG_SCHED_USER_IDENTITY +# include # include #endif @@ -49,6 +50,70 @@ static char g_nshprompt[CONFIG_NSH_PROMPT_MAX] = CONFIG_NSH_PROMPT_STRING; +#ifdef CONFIG_SCHED_USER_IDENTITY +static bool g_nsh_privilege_prompt; +#endif + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +#ifdef CONFIG_SCHED_USER_IDENTITY + +/**************************************************************************** + * Name: nsh_apply_privilege_marker + * + * Description: + * Replace the last '>' in the prompt with the privilege marker ('#' or + * '$'). When no '>' is present, append the marker instead. + * + ****************************************************************************/ + +static void nsh_apply_privilege_marker(FAR char *prompt, char marker) +{ + size_t len; + FAR char *p; + + len = strlen(prompt); + for (p = prompt + len; p > prompt; p--) + { + if (*(p - 1) == '>') + { + *(p - 1) = marker; + return; + } + } + + if (len + 1 < CONFIG_NSH_PROMPT_MAX) + { + prompt[len] = marker; + prompt[len + 1] = '\0'; + } +} + +/**************************************************************************** + * Name: nsh_ensure_trailing_space + * + * Description: + * Ensure the prompt ends with a separating space before command input. + * + ****************************************************************************/ + +static void nsh_ensure_trailing_space(FAR char *prompt) +{ + size_t len; + + len = strlen(prompt); + if (len > 0 && prompt[len - 1] != ' ' && + len + 1 < CONFIG_NSH_PROMPT_MAX) + { + prompt[len] = ' '; + prompt[len + 1] = '\0'; + } +} + +#endif /* CONFIG_SCHED_USER_IDENTITY */ + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -63,9 +128,12 @@ static char g_nshprompt[CONFIG_NSH_PROMPT_MAX] = CONFIG_NSH_PROMPT_STRING; * - non-empty NSH_PROMPT_STRING * - non-empty HOSTNAME and suffix * - * When SCHED_USER_IDENTITY is enabled and NSH_PROMPT_STRING_ROOT or - * NSH_PROMPT_STRING_USER are non-empty, the prompt for the current - * effective UID replaces the value from the sources above. + * When SCHED_USER_IDENTITY is enabled, NSH_PROMPT_STRING_ROOT or + * NSH_PROMPT_STRING_USER replace the prompt when non-empty. + * + * After login (see nsh_update_prompt_after_login()), when those overrides + * are empty, the last '>' in the prompt is replaced with '#' (euid 0) or + * '$' (non-zero euid), or the marker is appended when no '>' is present. * * Note that suffix has higher priority when used to help clearly separate * prompts from command line inputs. @@ -102,20 +170,68 @@ void nsh_update_prompt(void) #ifdef CONFIG_SCHED_USER_IDENTITY if (geteuid() == 0) { + bool applied = false; + +#ifdef CONFIG_NSH_PROMPT_STRING_ROOT if (CONFIG_NSH_PROMPT_STRING_ROOT[0] != '\0') { strlcpy(g_nshprompt, CONFIG_NSH_PROMPT_STRING_ROOT, CONFIG_NSH_PROMPT_MAX); + applied = true; + } + +#endif + + if (!applied && g_nsh_privilege_prompt) + { + nsh_apply_privilege_marker(g_nshprompt, '#'); } } - else if (CONFIG_NSH_PROMPT_STRING_USER[0] != '\0') + else { - strlcpy(g_nshprompt, CONFIG_NSH_PROMPT_STRING_USER, - CONFIG_NSH_PROMPT_MAX); + bool applied = false; + +#ifdef CONFIG_NSH_PROMPT_STRING_USER + if (CONFIG_NSH_PROMPT_STRING_USER[0] != '\0') + { + strlcpy(g_nshprompt, CONFIG_NSH_PROMPT_STRING_USER, + CONFIG_NSH_PROMPT_MAX); + applied = true; + } + +#endif + + if (!applied && g_nsh_privilege_prompt) + { + nsh_apply_privilege_marker(g_nshprompt, '$'); + } + } + + if (g_nsh_privilege_prompt) + { + nsh_ensure_trailing_space(g_nshprompt); } #endif } +/**************************************************************************** + * Name: nsh_update_prompt_after_login + * + * Description: + * Enable privilege markers in the prompt and refresh it. Boot and + * no-login sessions keep NSH_PROMPT_STRING (for example, "nsh> "). + * + ****************************************************************************/ + +void nsh_update_prompt_after_login(void) +{ +#ifdef CONFIG_SCHED_USER_IDENTITY + g_nsh_privilege_prompt = true; +#endif + + nsh_update_prompt(); +} + /**************************************************************************** * Name: nsh_prompt * diff --git a/nshlib/nsh_session.c b/nshlib/nsh_session.c index acacb0f54df..cf71a741972 100644 --- a/nshlib/nsh_session.c +++ b/nshlib/nsh_session.c @@ -103,6 +103,13 @@ int nsh_session(FAR struct console_stdio_s *pstate, } #endif /* CONFIG_NSH_TELNET_LOGIN */ +#ifdef CONFIG_SCHED_USER_IDENTITY + if (login != NSH_LOGIN_NONE) + { + nsh_update_prompt_after_login(); + } +#endif + if (login != NSH_LOGIN_NONE) { /* Present a greeting and possibly a Message of the Day (MOTD) */ diff --git a/nshlib/nsh_telnetlogin.c b/nshlib/nsh_telnetlogin.c index 8007b26846a..7e80ff5e7a0 100644 --- a/nshlib/nsh_telnetlogin.c +++ b/nshlib/nsh_telnetlogin.c @@ -261,7 +261,7 @@ int nsh_telnetlogin(FAR struct console_stdio_s *pstate) return -1; } - nsh_update_prompt(); + nsh_update_prompt_after_login(); #endif return OK; }