From nobody Mon Sep 29 18:19:34 2025 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4cb8bp6X1xz68d67; Mon, 29 Sep 2025 18:19:34 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R12" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4cb8bp61GHz3f5l; Mon, 29 Sep 2025 18:19:34 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1759169974; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=qPqpcwWeUKhfEOp7x2NaP9rGJMYTH6uX1Yvd95DESBU=; b=x2WCt4KX+ruD/IG7t6ovss0WCplU2K2YNlmsCLZ5ARuHtE7z7Km6114qqY/Omlhdw4dkvG uc7AChXJJMxQat+6zrl2j+fchEH1FNqwyfX04F+R7O1IF9D1ph3dwpjx6Jq47oTsPCCsiF lHQoPHxRiDMHWMtc4jBKAHhrSw0nszK42l7zXgMiXGAgmmaTDBwX0thk1fZNE6QnKXTXAV P8RxrOQ6u/DC4XdALZj6xecQt6S/o5GVk9WC/8SHa3OhBWF4oZdNBmmcs3dTYuJQ8FcANP Yr/OQZba7EAznnsoGqOhYJ8+OcZ7nm4J0Lu3XnG6FZaP4q+Sfp4cGW5O20JMuw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1759169974; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=qPqpcwWeUKhfEOp7x2NaP9rGJMYTH6uX1Yvd95DESBU=; b=gGeplcFYi8QlRIsJK0Hzl2Vdkokm4TRZEweW+mxe6sw9KSrNqoT2JqSu3HcD9i+ibOkPr5 OzGs5J0I/2L5IFBg0d6G2dcuXFLYVGEtnn82WryfK8uTTJIILl3z/z+kqSMCZI/LgluSbg 14DPSU7mZ8HycWCL/jixVtVlefP3zd9NCe2Nbrxo3PzjciUgvKkpxNX7c8wpTpJijVL+pt 0uCyk5sE17sAJm511HIkSwhJMOK71t+VxgFgpI9I4V7kWQcwqfxsLm7ecOZ8H6BiW/L3wW kgWXY3BXgqxZHitwj9e1kOXwYZew9Jde9faV5c0tSMnewvnwV82x2NWrbZyRvg== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1759169974; a=rsa-sha256; cv=none; b=cADF5PK9IEc/lal6nb5CntGYkbM9exB4r/5bynAQmsVdp1tSm8g5Y/9C2PPRYCgYQPHViJ Wyv+Woxe9q8DLOGoOw9EnUh6CfvD4sTAwtZV+MoeMwO0rogoCxtx1KJ1EJX8iLkHpPxOq+ E74yVIX/CupHVIaJ5FTpkZcfvNu4xghgGDxZaq2NXYarMxmvEhGE7M1zzvks/OCZ1DK170 dNavL5RzCqqhp5NlGUY2lYNxrkNmJ/UQNaPGKlGYcTvbdMiE0t/UWjGbUfElnLpMqp0v6j MFTv4S2qTCcT22AidpBD3YH7nh+YxYa4d02juUxLBU4/AWS1XoPXPOrL+S2EUg== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4cb8bp5T7Qz1FsX; Mon, 29 Sep 2025 18:19:34 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.18.1/8.18.1) with ESMTP id 58TIJYqT038977; Mon, 29 Sep 2025 18:19:34 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.18.1/8.18.1/Submit) id 58TIJYLZ038974; Mon, 29 Sep 2025 18:19:34 GMT (envelope-from git) Date: Mon, 29 Sep 2025 18:19:34 GMT Message-Id: <202509291819.58TIJYLZ038974@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Olivier Certner Subject: git: 3ca1e69028ac - main - mdo(1): Add support and shortcuts for fully specifying users and groups List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: olce X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 3ca1e69028acdee30739c0e0856692395a36fd21 Auto-Submitted: auto-generated The branch main has been updated by olce: URL: https://cgit.FreeBSD.org/src/commit/?id=3ca1e69028acdee30739c0e0856692395a36fd21 commit 3ca1e69028acdee30739c0e0856692395a36fd21 Author: Olivier Certner AuthorDate: 2025-09-25 11:30:00 +0000 Commit: Olivier Certner CommitDate: 2025-09-29 18:19:02 +0000 mdo(1): Add support and shortcuts for fully specifying users and groups While preserving compatibility ('root' implied if no user is specified, option '-i' not setting groups), introduce options to control finely which user and group IDs are set in the launched process. To minimize the risks of user error, mdo(1) by default enforces that all user and group IDs are specified, either with explicit values from the command-line or, if a known user name is passed with '-u', from the corresponding content of the password and group databases. The other main type of use cases is to start from the current process' credentials, only amending part of them. It is now also possible to blend both approaches, where some parts must be specified and the others can just be amended or left as is. Options: * As before: -u: Specifies a user name or ID to change all user IDs to. If a known name is passed, also automatically sets all groups as per the password and group databases. -i: Starts from the current groups, instead of having to specify them by using '-u' with a known user name or explicitly. * New: -k: Starts from the current users (incompatible with '-u'). Implies '-i'. -g: Sets/overrides the primary group IDs with the passed group name or ID. -G: Sets/overrides the supplementary groups set with the passed list of comma-separated names or IDs. -s: Amend the supplementary groups set according to the list of comma-separated directives from the following: - @: Empties the set. Must be the first directive. Incompatible with '-G'. - +: Add a group to the set. - -: Remove a group from the set. Takes precedence over +. --euid: Overrides the effective user ID. --ruid: Overrides the real user ID. --svuid: Overrides the saved user ID. --egid: Overrides the effective group ID. --rgid: Overrides the real group ID. --svgid: Overrides the saved group ID. Option '-k' was introduced as a requirement to be explicit when one wants to keep the current user(s) instead of specifying new ones. This is both for the purpose of avoiding foot-shooting and preserving the possibility to omit '-u' to switch to 'root'. In order to avoid confusion, if any user or group overrides are specified, mdo(1) however enforces that either '-u' or '-k' has been specified (so, in practice, '-u root' is implied only in the absence of any other options except '-i'). Some base supplementary groups set is needed when '-s' is used without directive '@'. It can be an explicit one specified with '-G', effectively meaning that '-G' is processed before '-s'. Else, it is determined from the password/group database (see initgroups(3)) if '-u' with a user name was passed, or is simply the current set if '-i' (or '-k') was specified. Other cases require specifying the full set (using '-G' or '-s' with '@'), and will fail otherwise. As the release process for 15.0 is progressing, this is committed in advance of the still-in-progress tests and manual page updates. Note for MFC to stable/14: As initgroups() has its old behavior, consistently with it, remove the effective GID from being passed also as a supplementary group. Reviewed by: bapt MFC after: 3 days Relnotes: yes Event: EuroBSDCon 2025 Sponsored by: The FreeBSD Foundation Sponsored by: Google LLC (GSoC 2025) Co-authored-by: Kushagra Srivastava Differential Revision: https://reviews.freebsd.org/D52613 --- usr.bin/mdo/mdo.c | 857 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 816 insertions(+), 41 deletions(-) diff --git a/usr.bin/mdo/mdo.c b/usr.bin/mdo/mdo.c index 8435fc17f26f..3eb5d4e5c23f 100644 --- a/usr.bin/mdo/mdo.c +++ b/usr.bin/mdo/mdo.c @@ -1,13 +1,24 @@ /*- + * SPDX-License-Identifier: BSD-2-Clause + * * Copyright(c) 2024 Baptiste Daroussin + * Copyright (c) 2025 Kushagra Srivastava + * Copyright (c) 2025 The FreeBSD Foundation * - * SPDX-License-Identifier: BSD-2-Clause + * 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 @@ -16,84 +27,848 @@ #include #include + static void usage(void) { - fprintf(stderr, "usage: mdo [-u username] [-i] [--] [command [args]]\n"); - exit(EXIT_FAILURE); + 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) { - struct passwd *pw; - const char *username = "root"; + 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; - bool uidonly = false; - int ch; - while ((ch = getopt(argc, argv, "u:i")) != -1) { + 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 'u': - username = optarg; + case 'G': + supp_groups_str = optarg; + need_user = true; + break; + case 'g': + primary_group = optarg; + need_user = true; break; + case 'h': + usage(); case 'i': - uidonly = true; + 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; - if ((pw = getpwnam(username)) == NULL) { - if (strspn(username, "0123456789") == strlen(username)) { - const char *errp = NULL; - uid_t uid = strtonum(username, 0, UID_MAX, &errp); - if (errp != NULL) - err(EXIT_FAILURE, "invalid user ID '%s'", - username); - pw = getpwuid(uid); + /* + * 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) - err(EXIT_FAILURE, "invalid username '%s'", username); + 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. + */ } - wcred.sc_uid = wcred.sc_ruid = wcred.sc_svuid = pw->pw_uid; - setcred_flags |= SETCREDF_UID | SETCREDF_RUID | SETCREDF_SVUID; + if (supp_groups_str != NULL) { + char *p = supp_groups_str; + char *tok; - if (!uidonly) { /* - * If there are too many groups specified for some UID, setting - * the groups will fail. We preserve this condition by - * allocating one more group slot than allowed, as - * getgrouplist() itself is just some getter function and thus - * doesn't (and shouldn't) check the limit, and to allow - * setcred() to actually check for overflow. + * 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. */ - const long ngroups_alloc = sysconf(_SC_NGROUPS_MAX) + 2; - gid_t *const groups = malloc(sizeof(*groups) * ngroups_alloc); - int ngroups = ngroups_alloc; + 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; - if (groups == NULL) - err(EXIT_FAILURE, "cannot allocate memory for groups"); + /* WARNING: 'supp_mod_str' going to be modified. */ + while ((tok = strsep(&p, ",")) != NULL) { + switch (tok[0]) { + case '\0': + break; - getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups); + 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); + } + } - wcred.sc_gid = wcred.sc_rgid = wcred.sc_svgid = pw->pw_gid; - wcred.sc_supp_groups = groups + 1; - wcred.sc_supp_groups_nb = ngroups - 1; - setcred_flags |= SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID | - SETCREDF_SUPP_GROUPS; + 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); + } + *** 20 LINES SKIPPED ***