/*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright(c) 2024 Baptiste Daroussin * Copyright (c) 2025 Kushagra Srivastava * Copyright (c) 2025 The FreeBSD Foundation * * Portions of this software were developed by Olivier Certner * at Kumacom SARL under sponsorship from the FreeBSD * Foundation. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void usage(void) { fprintf(stderr, "Usage: mdo [options] [--] [command [args...]]\n" "\n" "Options:\n" " -u Target user (name or UID; name sets groups)\n" " -k Keep current user, allows selective overrides " "(implies -i)\n" " -i Keep current groups, unless explicitly overridden\n" " -g Override primary group (name or GID)\n" " -G Set supplementary groups (name or GID list)\n" " -s Modify supplementary groups using:\n" " @ (first) to reset, +group to add, -group to remove\n" "\n" "Advanced UID/GID overrides:\n" " --euid Set effective UID\n" " --ruid Set real UID\n" " --svuid Set saved UID\n" " --egid Set effective GID\n" " --rgid Set real GID\n" " --svgid Set saved GID\n" "\n" " -h Show this help message\n" "\n" "Examples:\n" " mdo -u alice id\n" " mdo -u 1001 -g wheel -G staff,operator sh\n" " mdo -u bob -s +wheel,+operator id\n" " mdo -k --ruid 1002 --egid 1004 id\n" ); exit(1); } struct alloc { void *start; size_t size; }; static const struct alloc ALLOC_INITIALIZER = { .start = NULL, .size = 0, }; /* * The default value should cover almost all cases. * * For getpwnam_r(), we assume: * - 88 bytes for 'struct passwd' * - Less than 16 bytes for the user name * - A typical shadow hash of 106 bytes * - Less than 16 bytes for the login class name * - Less than 64 bytes for GECOS info * - Less than 128 bytes for the home directory * - Less than 32 bytes for the shell path * Total: 256 + 88 + 106 = 450. * * For getgrnam_r(), we assume: * - 32 bytes for 'struct group' * - Less than 16 bytes for the group name * - Some hash of 106 bytes * - No more than 16 members, each of less than 16 bytes (=> 256 bytes) * Total: 256 + 32 + 16 + 106 = 410. * * We thus choose 512 (leeway, power of 2). */ static const size_t ALLOC_FIRST_SIZE = 512; static bool alloc_is_empty(const struct alloc *const alloc) { if (alloc->size == 0) { assert(alloc->start == NULL); return (true); } else { assert(alloc->start != NULL); return (false); } } static void alloc_realloc(struct alloc *const alloc) { const size_t old_size = alloc->size; size_t new_size; if (old_size == 0) { assert(alloc->start == NULL); new_size = ALLOC_FIRST_SIZE; } else if (old_size < PAGE_SIZE) new_size = 2 * old_size; else /* * We never allocate more than a page at a time when reaching * a page (except perhaps for the first increment, up to two). * Use roundup2() to be immune to previous cases' changes. */ new_size = roundup2(old_size, PAGE_SIZE) + PAGE_SIZE; alloc->start = realloc(alloc->start, new_size); if (alloc->start == NULL) errx(EXIT_FAILURE, "cannot realloc allocation (old size: %zu, new: %zu)", old_size, new_size); alloc->size = new_size; } static void alloc_free(struct alloc *const alloc) { if (!alloc_is_empty(alloc)) { free(alloc->start); *alloc = ALLOC_INITIALIZER; } } struct alloc_wrap_data { int (*func)(void *data, const struct alloc *alloc); }; /* * Wraps functions needing a backing allocation. * * Uses 'alloc' as the starting allocation, and may extend it as necessary. * 'alloc' is never freed, even on failure of the wrapped function. * * The function is expected to return ERANGE if and only if the provided * allocation is not big enough. All other values are passed through. */ static int alloc_wrap(struct alloc_wrap_data *const data, struct alloc *alloc) { int error; /* Avoid a systematic ERANGE on first iteration. */ if (alloc_is_empty(alloc)) alloc_realloc(alloc); for (;;) { error = data->func(data, alloc); if (error != ERANGE) break; alloc_realloc(alloc); } return (error); } struct getpwnam_wrapper_data { struct alloc_wrap_data wrapped; const char *name; struct passwd **pwdp; }; static int wrapped_getpwnam_r(void *data, const struct alloc *alloc) { struct passwd *const pwd = alloc->start; struct passwd *result; struct getpwnam_wrapper_data *d = data; int error; assert(alloc->size >= sizeof(*pwd)); error = getpwnam_r(d->name, pwd, (char *)(pwd + 1), alloc->size - sizeof(*pwd), &result); if (error == 0) { if (result == NULL) error = ENOENT; } else assert(result == NULL); *d->pwdp = result; return (error); } /* * Wraps getpwnam_r(), automatically dealing with memory allocation. * * 'alloc' may be any allocation (even empty), and will be extended as * necessary. It is not freed on error. * * On success, '*pwdp' is filled with a pointer to the returned 'struct passwd', * and on failure, is set to NULL. */ static int alloc_getpwnam(const char *name, struct passwd **pwdp, struct alloc *const alloc) { struct getpwnam_wrapper_data data; data.wrapped.func = wrapped_getpwnam_r; data.name = name; data.pwdp = pwdp; return (alloc_wrap((struct alloc_wrap_data *)&data, alloc)); } struct getgrnam_wrapper_data { struct alloc_wrap_data wrapped; const char *name; struct group **grpp; }; static int wrapped_getgrnam_r(void *data, const struct alloc *alloc) { struct group *grp = alloc->start; struct group *result; struct getgrnam_wrapper_data *d = data; int error; assert(alloc->size >= sizeof(*grp)); error = getgrnam_r(d->name, grp, (char *)(grp + 1), alloc->size - sizeof(*grp), &result); if (error == 0) { if (result == NULL) error = ENOENT; } else assert(result == NULL); *d->grpp = result; return (error); } /* * Wraps getgrnam_r(), automatically dealing with memory allocation. * * 'alloc' may be any allocation (even empty), and will be extended as * necessary. It is not freed on error. * * On success, '*grpp' is filled with a pointer to the returned 'struct group', * and on failure, is set to NULL. */ static int alloc_getgrnam(const char *const name, struct group **const grpp, struct alloc *const alloc) { struct getgrnam_wrapper_data data; data.wrapped.func = wrapped_getgrnam_r; data.name = name; data.grpp = grpp; return (alloc_wrap((struct alloc_wrap_data *)&data, alloc)); } /* * Retrieve the UID from a user string. * * Tries first to interpret the string as a user name, then as a numeric ID * (this order is prescribed by POSIX for a number of utilities). * * 'pwdp' and 'allocp' must be NULL or non-NULL together. If non-NULL, then * 'allocp' can be any allocation (possibly empty) and will be extended to * contain the result if necessary. It will not be freed (even on failure). */ static uid_t parse_user_pwd(const char *s, struct passwd **pwdp, struct alloc *allocp) { struct passwd *pwd; struct alloc alloc = ALLOC_INITIALIZER; const char *errp; uid_t uid; int error; assert((pwdp == NULL && allocp == NULL) || (pwdp != NULL && allocp != NULL)); if (pwdp == NULL) { pwdp = &pwd; allocp = &alloc; } error = alloc_getpwnam(s, pwdp, allocp); if (error == 0) { uid = (*pwdp)->pw_uid; goto finish; } else if (error != ENOENT) errc(EXIT_FAILURE, error, "cannot access the password database"); uid = strtonum(s, 0, UID_MAX, &errp); if (errp != NULL) errx(EXIT_FAILURE, "invalid UID '%s': %s", s, errp); finish: if (allocp == &alloc) alloc_free(allocp); return (uid); } /* See parse_user_pwd() for the doc. */ static uid_t parse_user(const char *s) { return (parse_user_pwd(s, NULL, NULL)); } /* * Retrieve the GID from a group string. * * Tries first to interpret the string as a group name, then as a numeric ID * (this order is prescribed by POSIX for a number of utilities). */ static gid_t parse_group(const char *s) { struct group *grp; struct alloc alloc = ALLOC_INITIALIZER; const char *errp; gid_t gid; int error; error = alloc_getgrnam(s, &grp, &alloc); if (error == 0) { gid = grp->gr_gid; goto finish; } else if (error != ENOENT) errc(EXIT_FAILURE, error, "cannot access the group database"); gid = strtonum(s, 0, GID_MAX, &errp); if (errp != NULL) errx(EXIT_FAILURE, "invalid GID '%s': %s", s, errp); finish: alloc_free(&alloc); return (gid); } struct group_array { u_int nb; gid_t *groups; }; static const struct group_array GROUP_ARRAY_INITIALIZER = { .nb = 0, .groups = NULL, }; static bool group_array_is_empty(const struct group_array *const ga) { return (ga->nb == 0); } static void realloc_groups(struct group_array *const ga, const u_int diff) { const u_int new_nb = ga->nb + diff; const size_t new_size = new_nb * sizeof(*ga->groups); assert(new_nb >= diff && new_size >= new_nb); ga->groups = realloc(ga->groups, new_size); if (ga->groups == NULL) err(EXIT_FAILURE, "realloc of groups failed"); ga->nb = new_nb; } static int gidp_cmp(const void *p1, const void *p2) { const gid_t g1 = *(const gid_t *)p1; const gid_t g2 = *(const gid_t *)p2; return ((g1 > g2) - (g1 < g2)); } static void sort_uniq_groups(struct group_array *const ga) { size_t j = 0; if (ga->nb <= 1) return; qsort(ga->groups, ga->nb, sizeof(gid_t), gidp_cmp); for (size_t i = 1; i < ga->nb; ++i) if (ga->groups[i] != ga->groups[j]) ga->groups[++j] = ga->groups[i]; } /* * Remove elements in 'set' that are in 'remove'. * * Expects both arrays to have been treated with sort_uniq_groups(). Works in * O(n + m), modifying 'set' in place. */ static void remove_groups(struct group_array *const set, const struct group_array *const remove) { u_int from = 0, to = 0, rem = 0; gid_t cand, to_rem; if (set->nb == 0 || remove->nb == 0) /* Nothing to remove. */ return; cand = set->groups[0]; to_rem = remove->groups[0]; for (;;) { if (cand < to_rem) { /* Keep. */ if (to != from) set->groups[to] = cand; ++to; cand = set->groups[++from]; if (from == set->nb) break; } else if (cand == to_rem) { cand = set->groups[++from]; if (from == set->nb) break; to_rem = remove->groups[++rem]; /* No duplicates. */ if (rem == remove->nb) break; } else { to_rem = remove->groups[++rem]; if (rem == remove->nb) break; } } /* All remaining groups in 'set' must be kept. */ if (from == to) /* Nothing was removed. 'set' will stay the same. */ return; memmove(set->groups + to, set->groups + from, (set->nb - from) * sizeof(gid_t)); set->nb = to + (set->nb - from); } int main(int argc, char **argv) { const char *const default_user = "root"; const char *user_name = NULL; const char *primary_group = NULL; char *supp_groups_str = NULL; char *supp_mod_str = NULL; bool start_from_current_groups = false; bool start_from_current_users = false; const char *euid_str = NULL; const char *ruid_str = NULL; const char *svuid_str = NULL; const char *egid_str = NULL; const char *rgid_str = NULL; const char *svgid_str = NULL; bool need_user = false; /* '-u' or '-k' needed. */ const int go_euid = 1000; const int go_ruid = 1001; const int go_svuid = 1002; const int go_egid = 1003; const int go_rgid = 1004; const int go_svgid = 1005; const struct option longopts[] = { {"euid", required_argument, NULL, go_euid}, {"ruid", required_argument, NULL, go_ruid}, {"svuid", required_argument, NULL, go_svuid}, {"egid", required_argument, NULL, go_egid}, {"rgid", required_argument, NULL, go_rgid}, {"svgid", required_argument, NULL, go_svgid}, {NULL, 0, NULL, 0} }; int ch; struct setcred wcred = SETCRED_INITIALIZER; u_int setcred_flags = 0; struct passwd *pw = NULL; struct alloc pw_alloc = ALLOC_INITIALIZER; struct group_array supp_groups = GROUP_ARRAY_INITIALIZER; struct group_array supp_rem = GROUP_ARRAY_INITIALIZER; /* * Process options. */ while (ch = getopt_long(argc, argv, "+G:g:hiks:u:", longopts, NULL), ch != -1) { switch (ch) { case 'G': supp_groups_str = optarg; need_user = true; break; case 'g': primary_group = optarg; need_user = true; break; case 'h': usage(); case 'i': start_from_current_groups = true; break; case 'k': start_from_current_users = true; break; case 's': supp_mod_str = optarg; need_user = true; break; case 'u': user_name = optarg; break; case go_euid: euid_str = optarg; need_user = true; break; case go_ruid: ruid_str = optarg; need_user = true; break; case go_svuid: svuid_str = optarg; need_user = true; break; case go_egid: egid_str = optarg; need_user = true; break; case go_rgid: rgid_str = optarg; need_user = true; break; case go_svgid: svgid_str = optarg; need_user = true; break; default: usage(); } } argc -= optind; argv += optind; /* * Determine users. * * We do that first as in some cases we need to retrieve the * corresponding password database entry to be able to set the primary * groups. */ if (start_from_current_users) { if (user_name != NULL) errx(EXIT_FAILURE, "-k incompatible with -u"); /* * If starting from the current user(s) as a base, finding one * of them in the password database and using its groups would * be quite surprising, so we instead let '-k' imply '-i'. */ start_from_current_groups = true; } else { uid_t uid; /* * In the case of any overrides, we impose an explicit base user * via '-u' or '-k' instead of implicitly taking 'root' as the * base. */ if (user_name == NULL) { if (need_user) errx(EXIT_FAILURE, "Some overrides specified, " "'-u' or '-k' needed."); user_name = default_user; } /* * Even if all user overrides are present as well as primary and * supplementary groups ones, in which case the final result * doesn't depend on '-u', we still call parse_user_pwd() to * check that the passed username is correct. */ uid = parse_user_pwd(user_name, &pw, &pw_alloc); wcred.sc_uid = wcred.sc_ruid = wcred.sc_svuid = uid; setcred_flags |= SETCREDF_UID | SETCREDF_RUID | SETCREDF_SVUID; } if (euid_str != NULL) { wcred.sc_uid = parse_user(euid_str); setcred_flags |= SETCREDF_UID; } if (ruid_str != NULL) { wcred.sc_ruid = parse_user(ruid_str); setcred_flags |= SETCREDF_RUID; } if (svuid_str != NULL) { wcred.sc_svuid = parse_user(svuid_str); setcred_flags |= SETCREDF_SVUID; } /* * Determine primary groups. */ /* * When not starting from the current groups, we need to set all * primary groups. If '-g' was not passed, we use the primary * group from the password database as the "base" to which * overrides '--egid', '--rgid' and '--svgid' apply. But if all * overrides were specified, we in fact just don't need the * password database at all. * * '-g' is treated outside of this 'if' as it can also be used * as an override. */ if (!start_from_current_groups && primary_group == NULL && (egid_str == NULL || rgid_str == NULL || svgid_str == NULL)) { if (pw == NULL) errx(EXIT_FAILURE, "must specify primary groups or a user name " "with an entry in the password database"); wcred.sc_gid = wcred.sc_rgid = wcred.sc_svgid = pw->pw_gid; setcred_flags |= SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID; } if (primary_group != NULL) { /* * We always call parse_group() even in case all overrides are * present to check that the passed group is valid. */ wcred.sc_gid = wcred.sc_rgid = wcred.sc_svgid = parse_group(primary_group); setcred_flags |= SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID; } if (egid_str != NULL) { wcred.sc_gid = parse_group(egid_str); setcred_flags |= SETCREDF_GID; } if (rgid_str != NULL) { wcred.sc_rgid = parse_group(rgid_str); setcred_flags |= SETCREDF_RGID; } if (svgid_str != NULL) { wcred.sc_svgid = parse_group(svgid_str); setcred_flags |= SETCREDF_SVGID; } /* * Determine supplementary groups. */ /* * This makes sense to catch user's mistakes. It is not a strong * limitation of the code below (allowing this case is just a matter of, * in the block treating '-s' with '@' below, replacing an assert() by * a reset of 'supp_groups'). */ if (supp_groups_str != NULL && supp_mod_str != NULL && supp_mod_str[0] == '@') errx(EXIT_FAILURE, "'-G' and '-s' with '@' are incompatible"); /* * Determine the supplementary groups to start with, but only if we * really need to operate on them later (and set them back). */ if (!start_from_current_groups) { assert(!start_from_current_users); if (supp_groups_str == NULL && (supp_mod_str == NULL || supp_mod_str[0] != '@')) { /* * If we are to replace supplementary groups (i.e., * neither '-i' nor '-k' was specified) and they are not * completely specified (with '-g' or '-s' with '@'), we * start from those in the groups database if we were * passed a user name that is in the password database * (this is a protection against erroneous ID/name * conflation in the groups database), else we simply * error. */ if (pw == NULL) errx(EXIT_FAILURE, "must specify the full supplementary " "groups set or a user name with an entry " "in the password database"); const long ngroups_alloc = sysconf(_SC_NGROUPS_MAX) + 1; gid_t *groups; int ngroups; groups = malloc(sizeof(*groups) * ngroups_alloc); if (groups == NULL) errx(EXIT_FAILURE, "cannot allocate memory to retrieve " "user groups from the groups database"); ngroups = ngroups_alloc; getgrouplist(user_name, pw->pw_gid, groups, &ngroups); if (ngroups > ngroups_alloc) err(EXIT_FAILURE, "too many groups for user '%s'", user_name); realloc_groups(&supp_groups, ngroups); memcpy(supp_groups.groups + supp_groups.nb - ngroups, groups, ngroups * sizeof(*groups)); free(groups); /* * Have to set SETCREDF_SUPP_GROUPS here since we may be * in the case where neither '-G' nor '-s' was passed, * but we still have to set the supplementary groups to * those of the groups database. */ setcred_flags |= SETCREDF_SUPP_GROUPS; } } else if (supp_groups_str == NULL && (supp_mod_str == NULL || supp_mod_str[0] != '@')) { const int ngroups = getgroups(0, NULL); if (ngroups > 0) { realloc_groups(&supp_groups, ngroups); if (getgroups(ngroups, supp_groups.groups + supp_groups.nb - ngroups) < 0) err(EXIT_FAILURE, "getgroups() failed"); } /* * Setting SETCREDF_SUPP_GROUPS here is not necessary, we will * do it below since 'supp_mod_str' != NULL. */ } if (supp_groups_str != NULL) { char *p = supp_groups_str; char *tok; /* * We will set the supplementary groups to exactly the set * passed with '-G', and we took care above not to retrieve * "base" groups (current ones or those from the groups * database) in this case. */ assert(group_array_is_empty(&supp_groups)); /* WARNING: 'supp_groups_str' going to be modified. */ while ((tok = strsep(&p, ",")) != NULL) { gid_t g; if (*tok == '\0') continue; g = parse_group(tok); realloc_groups(&supp_groups, 1); supp_groups.groups[supp_groups.nb - 1] = g; } setcred_flags |= SETCREDF_SUPP_GROUPS; } if (supp_mod_str != NULL) { char *p = supp_mod_str; char *tok; gid_t gid; /* WARNING: 'supp_mod_str' going to be modified. */ while ((tok = strsep(&p, ",")) != NULL) { switch (tok[0]) { case '\0': break; case '@': if (tok != supp_mod_str) errx(EXIT_FAILURE, "'@' must be " "the first token in '-s' option"); /* See same assert() above. */ assert(group_array_is_empty(&supp_groups)); break; case '+': case '-': gid = parse_group(tok + 1); if (tok[0] == '+') { realloc_groups(&supp_groups, 1); supp_groups.groups[supp_groups.nb - 1] = gid; } else { realloc_groups(&supp_rem, 1); supp_rem.groups[supp_rem.nb - 1] = gid; } break; default: errx(EXIT_FAILURE, "invalid '-s' token '%s' at index %zu", tok, tok - supp_mod_str); } } setcred_flags |= SETCREDF_SUPP_GROUPS; } /* * We don't need to pass the kernel a normalized representation of the * new supplementary groups set (array sorted and without duplicates), * so we don't do it here unless we need to remove some groups, where it * enables more efficient algorithms (if the number of groups for some * reason grows out of control). */ if (!group_array_is_empty(&supp_groups) && !group_array_is_empty(&supp_rem)) { sort_uniq_groups(&supp_groups); sort_uniq_groups(&supp_rem); remove_groups(&supp_groups, &supp_rem); } if ((setcred_flags & SETCREDF_SUPP_GROUPS) != 0) { wcred.sc_supp_groups = supp_groups.groups; wcred.sc_supp_groups_nb = supp_groups.nb; } if (setcred(setcred_flags, &wcred, sizeof(wcred)) != 0) err(EXIT_FAILURE, "setcred()"); /* * We don't bother freeing memory still allocated at this point as we * are about to exec() or exit. */ if (*argv == NULL) { const char *sh = getenv("SHELL"); if (sh == NULL) sh = _PATH_BSHELL; execlp(sh, sh, "-i", NULL); } else { execvp(argv[0], argv); } err(EXIT_FAILURE, "exec failed"); }