diff --git a/share/man/man4/mac_do.4 b/share/man/man4/mac_do.4 index 9a9f669cd51c..4c067205225c 100644 --- a/share/man/man4/mac_do.4 +++ b/share/man/man4/mac_do.4 @@ -1,452 +1,452 @@ .\"- .\" SPDX-License-Identifier: BSD-2-Clause .\" .\" Copyright (c) 2024 Baptiste Daroussin .\" Copyright (c) 2024 The FreeBSD Foundation .\" .\" Portions of this documentation were written by Olivier Certner .\" at Kumacom SARL under sponsorship from the FreeBSD .\" Foundation. .\" .Dd December 19, 2024 .Dt MAC_DO 4 .Os .Sh NAME .Nm mac_do .Nd "policy allowing unprivileged users to change process credentials" .Sh SYNOPSIS To compile the .Sy mac_do policy into your kernel, place the following lines in your kernel configuration file: .Bd -ragged -offset indent .Cd "options MAC" .Cd "options MAC_DO" .Ed .Pp Alternately, to load this policy module at boot time, place the following line in your kernel configuration file: .Bd -ragged -offset indent .Cd "options MAC" .Ed .Pp and in .Xr loader.conf 5 : .Bd -literal -offset indent mac_do_load="YES" .Ed .Sh DESCRIPTION The .Nm policy module allows unprivileged users to change process credentials according to rules configured by the administrator. It supports per-jail configuration. .Pp Currently, the .Nm policy module only produces effects to processes spwaned from the .Pa /usr/bin/mdo executable, please see .Xr mdo 1 for more details on this program. .Sh CREDENTIALS RULES Rules specify which transitions of process credentials .Nm will allow, based on current process credentials and the desired final ones. They are passed by an administrator in the form of a string having the specific syntax described below in a top-bottom manner. They have been designed to be able to finely describe the desired target credentials in a safe and compact way. .Ss Top-Level List of Rules At the top, rules are a possibly empty list of individual rules separated by a semi-colon .Pq Ql ";" : .Dl Ao rules Ac \ ⟶\ Oo Ao rule Ac Oo So ";" Sc Ao rule Ac Oc Ns * Oc They form a disjunction, i.e., .Nm authorizes a credentials transition as soon as at least one rule in the list matches. .Pp One rule is composed of a .Li Aq from part .Pq also called Dq match and a .Li Aq to part .Pq also called Dq target , -in this order, separated by a colon -.Pq Ql ":" : -.Dl Ao rule Ac \ ⟶\ Ao from Ac So ":" Sc Ao to Ac +in this order, separated by a greater-than sign +.Pq Ql > : +.Dl Ao rule Ac \ ⟶\ Ao from Ac So > Sc Ao to Ac .Ss Rule's Ao from Ac Part The first part of a rule, .Li Aq from , is matched against the credentials of the process requesting some credentials transition. It has the form: .Dl Ao from Ac \ ⟶\ Ao type Ac So = Sc Ao id Ac .Pp .Li Aq type must be: .Dl Ao type Ac \ ⟶\ Op So uid Sc | So gid Sc i.e., one of the literal strings .Ql uid or .Ql gid . .Li Aq id must be the numerical ID of a user or group, and is matched with the current process real ID of the corresponding type. .Ss Rule's Ao to Ac Part The second part of a rule, .Li Aq to , is a comma-separated .Pq Ql "," non-empty list of target clauses: .Dl Ao to Ac \ ⟶\ Ao target_clause Ac Oo So "," Sc Ao target_clause Ac Oc Ns * Target clauses of a given rule also form a disjunction, i.e., the IDs they specify are alternatives for the target credentials, except in some cases described below. .Pp The next subsections describe the syntax of target clauses, the defaults that apply and the principle of non-redundancy and non-contradiction in each rule's .Li Aq to part. .Ss Target Clauses A target clause in a rule's .Li Aq to part must be of one of the following forms: .Dl Ao target_clause Ac \ ⟶\ So any Sc .Dl Ao target_clause Ac \ ⟶\ Ao flags Ac Ao type Ac So = Sc Ao id Ac The first form is a compact way to specify that any target credentials are allowed. The second form is similar to that of .Li Aq from clauses, with the following extensions: .Bl -bullet -compact .It .Li Aq id may also be a literal .Ql * or .Ql any or .Ql "." . .Ql * and .Ql any both designate any ID for the specified .Li Aq type , and are treated identically. .Ql "." designates the process' current IDs for the specified .Li Aq type , as explained below. .It .Li Aq flags may contain at most one of the .Ql + , .Ql - and .Ql "!" characters, and may be non-empty only when .Li Aq type is .Ql gid . Additionally, if .Li Aq id is .Ql * or .Ql any , only the .Ql + flag may appear. .El .Pp For target clauses of .Ql gid type, an absence of flag indicates that the specified group ID is allowed as the real, effective and/or saved group IDs .Pq the Do primary Dc groups . Conversely, the presence of any allowed flag indicates that the specification concerns supplementary groups. Each flag has a specific meaning: .Bl -bullet -compact .It .Ql + indicates that the group ID is allowed as a supplementary group. .It .Ql "!" indicates that the group ID is mandatory, i.e., it must be listed in the supplementary groups. .It .Ql - indicates that the group ID must not be listed in the supplementary groups. .El A specification with .Ql - is only useful in conjunction with a .Ql + Ns -tagged specification where only one of them has .Ql "." as its .Li Aq id . Target clauses having the .Ql "!" or .Ql - flag are .Dq forcing clauses, and as such do not take part in the disjunction of the other target clauses but rather unconditionally apply in their rule. .Pp .Ql "." is a placeholder for IDs that the calling process already has on privilege check. For type .Ql uid , it designates any of the process' real, effective or saved user IDs. For type .Ql gid , its effect depends on whether flags are present. If none is present, it designates any of the process' real, effective or saved group IDs. If one is present, it designates any of the process' supplementary groups. .Ss Defaults for the Ao to Ac Part If the .Li Aq to part does not list a target clause with type .Ql uid , any of the current user IDs of the calling process is accepted. In other words, in this case, .Nm behaves as if a target clause of: .Dl uid=. had been listed. .Pp Similarly, if the .Li Aq to part does not list a target clause with type .Ql gid , all the groups of the calling process are assumed to be required. More precisely, each of the desired real, effective and saved group IDs must be one of the current real, effective or saved group ID, and all supplementary groups must be the same as those that are current. It is as if the .Li Aq to part had contained the following two clauses: .Dl gid=.,!gid=. .Ss Non-Redundancy and Non-Contradiction in a Ao to Ac Part No two target clauses of a single rule may express the exact same logical intent nor contradictory ones. .Pp In practice, no two clauses may display the same ID except for group IDs but only if, each time the same ID appears, it does so with a different flag, or no flags only once. Additionally, the specified flags in multiple occurences must not be contradictory. For example, the same group ID appearing with both .Ql + and .Ql - will cause rejection of the rule. .Ss Parsing Specifics Any amount of whitespace is allowed around tokens of the above grammar, except that there may be no spaces between .Li Aq flags and .Li Aq id in target clauses. .Pp For convenience, numerical IDs may be specified as negative integers, which are then converted to unsigned ones as specified in the C standard for the .Vt uid_t and .Vt gid_t types, which are both 64-bit unsigned integers. .Sh RUNTIME CONFIGURATION The following .Xr sysctl 8 knobs are available: .Bl -tag -width indent .It Va security.mac.do.enabled Enable the .Nm policy. (Default: 1). .It Va security.mac.do.rules The list of credential rules, whose syntax is described in the .Sx CREDENTIALS RULES section above. This list is specific to each jail. Please see the .Sx JAIL SUPPORT section below for more details on the interaction of .Nm with jails. .It Va security.mac.do.print_parse_error Logs a message on trying to set incorrect rules via the .Va security.mac.do.rules .Xr sysctl 8 knob. .El .Sh JAIL SUPPORT .Nm supports per-jail configuration of rules. .Pp By default, at creation, a new jail has no credentials rules, effectively disabling .Nm for its processes. .Pp The following jail parameters are defined: .Bl -tag -width indent .It Va mac.do Possible values are: .Bl -tag -width "'disable'" -compact .It Ql enable .Nm will enforce specific credential rules in the jail. The .Va mac.do.rules jail parameter must also be set in this case. .It Ql disable Disables .Nm in the jail. Strictly equivalent to jail creation's default behavior and to setting the rules to an empty string. .It Ql inherit The jail's credentials rules are inherited from the jail's parent .Pq which may themselves have been inherited . Modified rules propagate to all children jails configured for inheritance. .El .It Va mac.do.rules The credentials rules for the jail. It is always equal to the value that can be retrieved by the .Xr sysctl 8 knob .Va security.mac.do.rules described in section .Sx RUNTIME CONFIGURATION . If set, and the jail parameter .Va mac.do is not so explicitly, the value of the latter will default to .Ql disable if empty, else to .Ql enable . .El .Pp Each jail must have .Xr mdo 1 installed at path .Pa /usr/bin/mdo , as this path is currently not configurable. .Sh EXAMPLES Here are several examples of single rules matching processes having a real user ID of 10001: .Bl -tag -width indent -.It Li uid=10001:uid=10002 +.It Li uid=10001>uid=10002 Allows the process to switch any of its real, effective or saved user ID to 10002, but keeping the groups it is already in, and with the same primary/supplementary groups split. -.It Li uid=10001:uid=10002,uid=10003 +.It Li uid=10001>uid=10002,uid=10003 Same as the first example, but also allows to switch to UID 10003 instead of 10002. -.It Li uid=10001:uid=10002,gid=10002 +.It Li uid=10001>uid=10002,gid=10002 Same as the first example, but the new primary groups must be set to 10002 and no supplementary groups should be set. -.It Li uid=10001:uid=10002,gid=10002,+gid=.\& +.It Li uid=10001>uid=10002,gid=10002,+gid=.\& Same as the previous example, but in addition allowing to retain any current supplementary groups. -.It Li uid=10001:uid=10002,gid=10002,!gid=.\& +.It Li uid=10001>uid=10002,gid=10002,!gid=.\& Same as the previous example, but with the additional constraint that all current supplementary groups must be kept. -.It Li uid=10001:uid=10002,gid=10002,+gid=.,-gid=10001 +.It Li uid=10001>uid=10002,gid=10002,+gid=.,-gid=10001 Same as -.Ql uid=10001:uid=10002,gid=10002,+gid=.\& +.Ql uid=10001>uid=10002,gid=10002,+gid=.\& above, but 10001 cannot be retained as a supplementary group. -.It Li uid=10001:uid=10002,gid=10002,+gid=.,!gid=10003 +.It Li uid=10001>uid=10002,gid=10002,+gid=.,!gid=10003 Same as -.Ql uid=10001:uid=10002,gid=10002,+gid=.\& +.Ql uid=10001>uid=10002,gid=10002,+gid=.\& above, with the additional constraint that 10003 must appear in the supplementary groups. -.It Li uid=10001:uid=10002,gid=*,+gid=* +.It Li uid=10001>uid=10002,gid=*,+gid=* Same as the first example, but lifting any constraints on groups, allowing the process to become part of any groups it sees fit. .El .Pp Here are several examples of single rules matching processes having a real group ID of 10001: .Bl -tag -width indent -.It Li gid=10001:uid=0 +.It Li gid=10001>uid=0 Makes 10001 a more powerful .Ql wheel group, allowing its members to switch to root without password. -.It Li gid=10001:gid=10002 +.It Li gid=10001>gid=10002 Allows the process to enter GID 10002 as a primary group, but only if giving up all its supplementary groups. -.It Li security.mac.do.rules=gid=10001:gid=10002,+gid=.\& +.It Li security.mac.do.rules=gid=10001>gid=10002,+gid=.\& Same as the previous example, but allows to retain any current supplementary groups. -.It Li gid=10001:gid=10002,!gid=.\& +.It Li gid=10001>gid=10002,!gid=.\& Same as the previous example, but with the additional constraint that all current supplementary groups must be kept. .El .Sh SEE ALSO .Xr mdo 1 , .Xr setcred 2 , .Xr mac 4 , .Xr jail 8 , .Xr sysctl 8 .Sh AUTHORS .An Olivier Certner Aq Mt olce@FreeBSD.org .An Baptiste Daroussin Aq Mt bapt@FreeBSD.org .Sh BUGS Currently, .Nm considers only credentials transitions requested through the .Xr setcred 2 system call. This system call was in large part created so that .Nm can see whole credentials transitions to decide whether to authorize them, which the traditional UNIX's piecewise approach of successively changing different parts of them cannot allow. .Pp However, calls to traditional or standard credentials-changing functions can be considered as full transitions on their own, however limited, and as such should be equally monitored by .Nm . Future work will lift this restriction. .Sh SECURITY CONSIDERATIONS The threat model for .Nm is to consider userland programs as generally untrustable to decide upon which credentials changes are acceptable. It is in contrast with the traditional UNIX way to change credentials, in which specialized programs are installed with the setuid bit, giving them full administrator privileges so that they are effectively able to establish new ones. Vulnerabilities in such credentials-changing programs can have catastrophic consequences on the integrity of the system. .Pp Consequently, .Nm does not rely on companion userland programs to decide whether some credentials transition is acceptable. Instead, it maintains its own configuration independently from the userland password and group databases. Establishing this configuration currently itself relies on userland programs issuing calls to .Xr sysctl 3 or .Xr jail 2 . It should thus be established near system boot or jail start, before any possible attacks could happen on the system, and further measures should be taken to ensure that potential corruptions does not affect the configuration in subsequent restarts, such as re-establishing pristine state or ensuring that the boot procedure up to the configuration of .Nm can be trusted. diff --git a/sys/security/mac_do/mac_do.c b/sys/security/mac_do/mac_do.c index 7bd3e2294798..a83c194b24ae 100644 --- a/sys/security/mac_do/mac_do.c +++ b/sys/security/mac_do/mac_do.c @@ -1,2129 +1,2130 @@ /*- * SPDX-License-Identifier: BSD-2-Clause * * Copyright(c) 2024 Baptiste Daroussin * Copyright (c) 2024 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 #include #include #include #include #include #include static SYSCTL_NODE(_security_mac, OID_AUTO, do, CTLFLAG_RW|CTLFLAG_MPSAFE, 0, "mac_do policy controls"); static int do_enabled = 1; SYSCTL_INT(_security_mac_do, OID_AUTO, enabled, CTLFLAG_RWTUN, &do_enabled, 0, "Enforce do policy"); static int print_parse_error = 1; SYSCTL_INT(_security_mac_do, OID_AUTO, print_parse_error, CTLFLAG_RWTUN, &print_parse_error, 0, "Print parse errors on setting rules " "(via sysctl(8))."); static MALLOC_DEFINE(M_DO, "do_rule", "Rules for mac_do"); #define MAC_RULE_STRING_LEN 1024 static unsigned osd_jail_slot; static unsigned osd_thread_slot; #define IT_INVALID 0 /* Must stay 0. */ #define IT_UID 1 #define IT_GID 2 #define IT_ANY 3 #define IT_LAST IT_ANY static const char *id_type_to_str[] = { [IT_INVALID] = "invalid", [IT_UID] = "uid", [IT_GID] = "gid", /* See also parse_id_type(). */ [IT_ANY] = "*", }; #define PARSE_ERROR_SIZE 256 struct parse_error { size_t pos; char msg[PARSE_ERROR_SIZE]; }; /* * We assume that 'uid_t' and 'gid_t' are aliases to 'u_int' in conversions * required for parsing rules specification strings. */ _Static_assert(sizeof(uid_t) == sizeof(u_int) && (uid_t)-1 >= 0 && sizeof(gid_t) == sizeof(u_int) && (gid_t)-1 >= 0, "mac_do(4) assumes that 'uid_t' and 'gid_t' are aliases to 'u_int'"); /* * Internal flags. * * They either apply as per-type (t) or per-ID (i) but are conflated because all * per-ID flags are also valid as per-type ones to qualify the "current" (".") * per-type flag. Also, some of them are in fact exclusive, but we use one-hot * encoding for simplicity. * * There is currently room for "only" 16 bits. As these flags are purely * internal, they can be renumbered and/or their type changed as needed. * * See also the check_*() functions below. */ typedef uint16_t flags_t; /* (i,gid) Specification concerns primary groups. */ #define MDF_PRIMARY (1u << 0) /* (i,gid) Specification concerns supplementary groups. */ #define MDF_SUPP_ALLOW (1u << 1) /* (i,gid) Group must appear as a supplementary group. */ #define MDF_SUPP_MUST (1u << 2) /* (i,gid) Group must not appear as a supplementary group. */ #define MDF_SUPP_DONT (1u << 3) #define MDF_SUPP_MASK (MDF_SUPP_ALLOW | MDF_SUPP_MUST | MDF_SUPP_DONT) #define MDF_ID_MASK (MDF_PRIMARY | MDF_SUPP_MASK) /* * (t) All IDs allowed. * * For GIDs, MDF_ANY only concerns primary groups. The MDF_PRIMARY and * MDF_SUPP_* flags never apply to MDF_ANY, but can be present if MDF_CURRENT is * present also, as usual. */ #define MDF_ANY (1u << 8) /* (t) Current IDs allowed. */ #define MDF_CURRENT (1u << 9) #define MDF_TYPE_COMMON_MASK (MDF_ANY | MDF_CURRENT) /* (t,gid) All IDs allowed as supplementary groups. */ #define MDF_ANY_SUPP (1u << 10) /* (t,gid) Some ID or MDF_CURRENT has MDF_SUPP_MUST or MDF_SUPP_DONT. */ #define MDF_MAY_REJ_SUPP (1u << 11) /* (t,gid) Some explicit ID (not MDF_CURRENT) has MDF_SUPP_MUST. */ #define MDF_EXPLICIT_SUPP_MUST (1u << 12) /* (t,gid) Whether any target clause is about primary groups. Used during * parsing only. */ #define MDF_HAS_PRIMARY_CLAUSE (1u << 13) /* (t,gid) Whether any target clause is about supplementary groups. Used during * parsing only. */ #define MDF_HAS_SUPP_CLAUSE (1u << 14) #define MDF_TYPE_GID_MASK (MDF_ANY_SUPP | MDF_MAY_REJ_SUPP | \ MDF_EXPLICIT_SUPP_MUST | MDF_HAS_PRIMARY_CLAUSE | MDF_HAS_SUPP_CLAUSE) #define MDF_TYPE_MASK (MDF_TYPE_COMMON_MASK | MDF_TYPE_GID_MASK) /* * Persistent structures. */ struct id_spec { u_int id; flags_t flags; /* See MDF_* above. */ }; /* * This limits the number of target clauses per type to 65535. With the current * value of MAC_RULE_STRING_LEN (1024), this is way more than enough anyway. */ typedef uint16_t id_nb_t; /* We only have a few IT_* types. */ typedef uint16_t id_type_t; struct rule { STAILQ_ENTRY(rule) r_entries; id_type_t from_type; u_int from_id; flags_t uid_flags; /* See MDF_* above. */ id_nb_t uids_nb; flags_t gid_flags; /* See MDF_* above. */ id_nb_t gids_nb; struct id_spec *uids; struct id_spec *gids; }; STAILQ_HEAD(rulehead, rule); struct rules { char string[MAC_RULE_STRING_LEN]; struct rulehead head; volatile u_int use_count __aligned(CACHE_LINE_SIZE); }; /* * Temporary structures used to build a 'struct rule' above. */ struct id_elem { STAILQ_ENTRY(id_elem) ie_entries; struct id_spec spec; }; STAILQ_HEAD(id_list, id_elem); #ifdef INVARIANTS static void check_type(const id_type_t type) { if (type > IT_LAST) panic("Invalid type number %u", type); } static void panic_for_unexpected_flags(const id_type_t type, const flags_t flags, const char *const str) { panic("ID type %s: Unexpected flags %u (%s), ", id_type_to_str[type], flags, str); } static void check_type_and_id_flags(const id_type_t type, const flags_t flags) { const char *str; check_type(type); switch (type) { case IT_UID: if (flags != 0) { str = "only 0 allowed"; goto unexpected_flags; } break; case IT_GID: if ((flags & ~MDF_ID_MASK) != 0) { str = "only bits in MDF_ID_MASK allowed"; goto unexpected_flags; } if (!powerof2(flags & MDF_SUPP_MASK)) { str = "only a single flag in MDF_SUPP_MASK allowed"; goto unexpected_flags; } break; default: __assert_unreachable(); } return; unexpected_flags: panic_for_unexpected_flags(type, flags, str); } static void check_type_and_id_spec(const id_type_t type, const struct id_spec *const is) { check_type_and_id_flags(type, is->flags); } static void check_type_and_type_flags(const id_type_t type, const flags_t flags) { const char *str; check_type_and_id_flags(type, flags & MDF_ID_MASK); if ((flags & ~MDF_ID_MASK & ~MDF_TYPE_MASK) != 0) { str = "only MDF_ID_MASK | MDF_TYPE_MASK bits allowed"; goto unexpected_flags; } if ((flags & MDF_ANY) != 0 && (flags & MDF_CURRENT) != 0 && (type == IT_UID || (flags & MDF_PRIMARY) != 0)) { str = "MDF_ANY and MDF_CURRENT are exclusive for UIDs " "or primary group GIDs"; goto unexpected_flags; } if ((flags & MDF_ANY_SUPP) != 0 && (flags & MDF_CURRENT) != 0 && (flags & MDF_SUPP_MASK) != 0) { str = "MDF_SUPP_ANY and MDF_CURRENT with supplementary " "groups specification are exclusive"; goto unexpected_flags; } if (((flags & MDF_PRIMARY) != 0 || (flags & MDF_ANY) != 0) && (flags & MDF_HAS_PRIMARY_CLAUSE) == 0) { str = "Presence of folded primary clause not reflected " "by presence of MDF_HAS_PRIMARY_CLAUSE"; goto unexpected_flags; } if (((flags & MDF_SUPP_MASK) != 0 || (flags & MDF_ANY_SUPP) != 0) && (flags & MDF_HAS_SUPP_CLAUSE) == 0) { str = "Presence of folded supplementary clause not reflected " "by presence of MDF_HAS_SUPP_CLAUSE"; goto unexpected_flags; } return; unexpected_flags: panic_for_unexpected_flags(type, flags, str); } #else /* !INVARIANTS */ #define check_type_and_id_flags(...) #define check_type_and_id_spec(...) #define check_type_and_type_flags(...) #endif /* INVARIANTS */ /* * Returns EALREADY if both flags have some overlap, or EINVAL if flags are * incompatible, else 0 with flags successfully merged into 'dest'. */ static int coalesce_id_flags(const flags_t src, flags_t *const dest) { flags_t res; if ((src & *dest) != 0) return (EALREADY); res = src | *dest; /* Check for compatibility of supplementary flags, and coalesce. */ if ((res & MDF_SUPP_MASK) != 0) { /* MDF_SUPP_DONT incompatible with the rest. */ if ((res & MDF_SUPP_DONT) != 0 && (res & MDF_SUPP_MASK & ~MDF_SUPP_DONT) != 0) return (EINVAL); /* * Coalesce MDF_SUPP_ALLOW and MDF_SUPP_MUST into MDF_SUPP_MUST. */ if ((res & MDF_SUPP_ALLOW) != 0 && (res & MDF_SUPP_MUST) != 0) res &= ~MDF_SUPP_ALLOW; } *dest = res; return (0); } static void toast_rules(struct rules *const rules) { struct rulehead *const head = &rules->head; struct rule *rule, *rule_next; STAILQ_FOREACH_SAFE(rule, head, r_entries, rule_next) { free(rule->uids, M_DO); free(rule->gids, M_DO); free(rule, M_DO); } free(rules, M_DO); } static struct rules * alloc_rules(void) { struct rules *const rules = malloc(sizeof(*rules), M_DO, M_WAITOK); _Static_assert(MAC_RULE_STRING_LEN > 0, "MAC_RULE_STRING_LEN <= 0!"); rules->string[0] = 0; STAILQ_INIT(&rules->head); rules->use_count = 0; return (rules); } static bool is_null_or_empty(const char *s) { return (s == NULL || s[0] == '\0'); } /* * String to unsigned int. * * Contrary to the "standard" strtou*() family of functions, do not tolerate * spaces at start nor an empty string, and returns a status code, the 'u_int' * result being returned through a passed pointer (if no error). * * We detour through 'quad_t' because in-kernel strto*() functions cannot set * 'errno' and thus can't distinguish a true maximum value from one returned * because of overflow. We use 'quad_t' instead of 'u_quad_t' to support * negative specifications (e.g., such as "-1" for UINT_MAX). */ static int strtoui_strict(const char *const restrict s, const char **const restrict endptr, int base, u_int *result) { char *ep; quad_t q; /* Rule out spaces and empty specifications. */ if (s[0] == '\0' || isspace(s[0])) { if (endptr != NULL) *endptr = s; return (EINVAL); } q = strtoq(s, &ep, base); if (endptr != NULL) *endptr = ep; if (q < 0) { /* We allow specifying a negative number. */ if (q < -(quad_t)UINT_MAX - 1 || q == QUAD_MIN) return (EOVERFLOW); } else { if (q > UINT_MAX || q == UQUAD_MAX) return (EOVERFLOW); } *result = (u_int)q; return (0); } /* * strsep() variant skipping spaces and tabs. * * Skips spaces and tabs at beginning and end of the token before one of the * 'delim' characters, i.e., at start of string and just before one of the * delimiter characters (so it doesn't prevent tokens containing spaces and tabs * in the middle). */ static char * strsep_noblanks(char **const stringp, const char *delim) { char *p = *stringp; char *ret, *wsp; size_t idx; if (p == NULL) return (NULL); idx = strspn(p, " \t"); p += idx; ret = strsep(&p, delim); /* Rewind spaces/tabs at the end. */ if (p == NULL) wsp = ret + strlen(ret); else wsp = p - 1; for (; wsp != ret; --wsp) { switch (wsp[-1]) { case ' ': case '\t': continue; } break; } *wsp = '\0'; *stringp = p; return (ret); } static void make_parse_error(struct parse_error **const parse_error, const size_t pos, const char *const fmt, ...) { struct parse_error *const err = malloc(sizeof(*err), M_DO, M_WAITOK); va_list ap; err->pos = pos; va_start(ap, fmt); vsnprintf(err->msg, PARSE_ERROR_SIZE, fmt, ap); va_end(ap); MPASS(*parse_error == NULL); *parse_error = err; } static void free_parse_error(struct parse_error *const parse_error) { free(parse_error, M_DO); } static int parse_id_type(const char *const string, id_type_t *const type, struct parse_error **const parse_error) { /* * Special case for "any", as the canonical form for IT_ANY in * id_type_to_str[] is "*". */ if (strcmp(string, "any") == 0) { *type = IT_ANY; return (0); } /* Start at 1 to avoid parsing "invalid". */ for (size_t i = 1; i <= IT_LAST; ++i) { if (strcmp(string, id_type_to_str[i]) == 0) { *type = i; return (0); } } *type = IT_INVALID; make_parse_error(parse_error, 0, "No valid type found."); return (EINVAL); } static size_t parse_gid_flags(const char *const string, flags_t *const flags, flags_t *const gid_flags) { switch (string[0]) { case '+': *flags |= MDF_SUPP_ALLOW; goto has_supp_clause; case '!': *flags |= MDF_SUPP_MUST; *gid_flags |= MDF_MAY_REJ_SUPP; goto has_supp_clause; case '-': *flags |= MDF_SUPP_DONT; *gid_flags |= MDF_MAY_REJ_SUPP; goto has_supp_clause; has_supp_clause: *gid_flags |= MDF_HAS_SUPP_CLAUSE; return (1); } return (0); } static bool parse_any(const char *const string) { return (strcmp(string, "*") == 0 || strcmp(string, "any") == 0); } static bool has_clauses(const id_nb_t nb, const flags_t type_flags) { return ((type_flags & MDF_TYPE_MASK) != 0 || nb != 0); } static int parse_target_clause(char *to, struct rule *const rule, struct id_list *const uid_list, struct id_list *const gid_list, struct parse_error **const parse_error) { const char *const start = to; char *to_type, *to_id; const char *p; struct id_list *list; id_nb_t *nb; flags_t *tflags; struct id_elem *ie; struct id_spec is = {.flags = 0}; flags_t gid_flags = 0; id_type_t type; int error; MPASS(*parse_error == NULL); MPASS(to != NULL); to_type = strsep_noblanks(&to, "="); MPASS(to_type != NULL); to_type += parse_gid_flags(to_type, &is.flags, &gid_flags); error = parse_id_type(to_type, &type, parse_error); if (error != 0) goto einval; if (type != IT_GID && is.flags != 0) { make_parse_error(parse_error, to_type - start, "Expected type 'gid' after flags, not '%s'.", to_type); goto einval; } to_id = strsep_noblanks(&to, ""); switch (type) { case IT_GID: if (to_id == NULL) { make_parse_error(parse_error, to_type - start, "No '=' and ID specification after type '%s'.", to_type); goto einval; } if (is.flags == 0) { /* No flags: Dealing with a primary group. */ is.flags |= MDF_PRIMARY; gid_flags |= MDF_HAS_PRIMARY_CLAUSE; } list = gid_list; nb = &rule->gids_nb; tflags = &rule->gid_flags; /* "*" or "any"? */ if (parse_any(to_id)) { /* * We check that we have not seen any other clause of * the same category (i.e., concerning primary or * supplementary groups). */ if ((is.flags & MDF_PRIMARY) != 0) { if ((*tflags & MDF_HAS_PRIMARY_CLAUSE) != 0) { make_parse_error(parse_error, to_id - start, "'any' specified after another " "(primary) GID."); goto einval; } *tflags |= gid_flags | MDF_ANY; } else { /* * If a supplementary group flag was present, it * must be MDF_SUPP_ALLOW ("+"). */ if ((is.flags & MDF_SUPP_MASK) != MDF_SUPP_ALLOW) { make_parse_error(parse_error, to_id - start, "'any' specified with another " "flag than '+'."); goto einval; } if ((*tflags & MDF_HAS_SUPP_CLAUSE) != 0) { make_parse_error(parse_error, to_id - start, "'any' with flag '+' specified after " "another (supplementary) GID."); goto einval; } *tflags |= gid_flags | MDF_ANY_SUPP; } goto check_type_and_finish; } else { /* * Check that we haven't already seen "any" for the same * category. */ if ((is.flags & MDF_PRIMARY) != 0) { if ((*tflags & MDF_ANY) != 0) { make_parse_error(parse_error, to_id - start, "Some (primary) GID specified after " "'any'."); goto einval; } } else if ((*tflags & MDF_ANY_SUPP) != 0 && (is.flags & MDF_SUPP_ALLOW) != 0) { make_parse_error(parse_error, to_id - start, "Some (supplementary) GID specified after " "'any' with flag '+'."); goto einval; } *tflags |= gid_flags; } break; case IT_UID: if (to_id == NULL) { make_parse_error(parse_error, to_type - start, "No '=' and ID specification after type '%s'.", to_type); goto einval; } list = uid_list; nb = &rule->uids_nb; tflags = &rule->uid_flags; /* "*" or "any"? */ if (parse_any(to_id)) { /* There must not be any other clause. */ if (has_clauses(*nb, *tflags)) { make_parse_error(parse_error, to_id - start, "'any' specified after another UID."); goto einval; } *tflags |= MDF_ANY; goto check_type_and_finish; } else { /* * Check that we haven't already seen "any" for the same * category. */ if ((*tflags & MDF_ANY) != 0) { make_parse_error(parse_error, to_id - start, "Some UID specified after 'any'."); goto einval; } } break; case IT_ANY: /* No ID allowed. */ if (to_id != NULL) { make_parse_error(parse_error, to_type - start, "No '=' and ID allowed after type '%s'.", to_type); goto einval; } /* * We can't have IT_ANY after any other IT_*, it must be the * only one. */ if (has_clauses(rule->uids_nb, rule->uid_flags) || has_clauses(rule->gids_nb, rule->gid_flags)) { make_parse_error(parse_error, to_type - start, "Target clause of type '%s' coming after another " "clause (must be alone).", to_type); goto einval; } rule->uid_flags |= MDF_ANY; rule->gid_flags |= MDF_ANY | MDF_ANY_SUPP | MDF_HAS_PRIMARY_CLAUSE | MDF_HAS_SUPP_CLAUSE; goto finish; default: /* parse_id_type() returns no other types currently. */ __assert_unreachable(); } /* Rule out cases that have been treated above. */ MPASS((type == IT_UID || type == IT_GID) && !parse_any(to_id)); /* "."? */ if (strcmp(to_id, ".") == 0) { if ((*tflags & MDF_CURRENT) != 0) { /* Duplicate "." . Try to coalesce. */ error = coalesce_id_flags(is.flags, tflags); if (error != 0) { make_parse_error(parse_error, to_id - start, "Incompatible flags with prior clause " "with same target."); goto einval; } } else *tflags |= MDF_CURRENT | is.flags; goto check_type_and_finish; } /* Parse an ID. */ error = strtoui_strict(to_id, &p, 10, &is.id); if (error != 0 || *p != '\0') { make_parse_error(parse_error, to_id - start, "Cannot parse a numerical ID (base 10)."); goto einval; } /* Explicit ID flags. */ if (type == IT_GID && (is.flags & MDF_SUPP_MUST) != 0) *tflags |= MDF_EXPLICIT_SUPP_MUST; /* * We check for duplicate IDs and coalesce their 'struct id_spec' only * at end of parse_single_rule() because it is much more performant then * (using sorted arrays). */ ++*nb; if (*nb == 0) { make_parse_error(parse_error, 0, "Too many target clauses of type '%s'.", to_type); return (EOVERFLOW); } ie = malloc(sizeof(*ie), M_DO, M_WAITOK); ie->spec = is; STAILQ_INSERT_TAIL(list, ie, ie_entries); check_type_and_id_spec(type, &is); check_type_and_finish: check_type_and_type_flags(type, *tflags); finish: return (0); einval: /* We must have built a parse error on error. */ MPASS(*parse_error != NULL); return (EINVAL); } static int u_int_cmp(const u_int i1, const u_int i2) { return ((i1 > i2) - (i1 < i2)); } static int id_spec_cmp(const void *const p1, const void *const p2) { const struct id_spec *const is1 = p1; const struct id_spec *const is2 = p2; return (u_int_cmp(is1->id, is2->id)); } /* * Transfer content of 'list' into 'array', freeing and emptying list. * * 'nb' must be 'list''s length and not be greater than 'array''s size. The * destination array is sorted by ID. Structures 'struct id_spec' with same IDs * are coalesced if that makes sense (not including duplicate clauses), else * EINVAL is returned. On success, 'nb' is updated (lowered) to account for * coalesced specifications. The parameter 'type' is only for testing purposes * (INVARIANTS). */ static int pour_list_into_rule(const id_type_t type, struct id_list *const list, struct id_spec *const array, id_nb_t *const nb, struct parse_error **const parse_error) { struct id_elem *ie, *ie_next; size_t idx = 0; /* Fill the array. */ STAILQ_FOREACH_SAFE(ie, list, ie_entries, ie_next) { MPASS(idx < *nb); array[idx] = ie->spec; free(ie, M_DO); ++idx; } MPASS(idx == *nb); STAILQ_INIT(list); /* Sort it (by ID). */ qsort(array, *nb, sizeof(*array), id_spec_cmp); /* Coalesce same IDs. */ if (*nb != 0) { size_t ref_idx = 0; for (idx = 1; idx < *nb; ++idx) { const u_int id = array[idx].id; if (id != array[ref_idx].id) { ++ref_idx; if (ref_idx != idx) array[ref_idx] = array[idx]; continue; } switch (type) { int error; case IT_GID: error = coalesce_id_flags(array[idx].flags, &array[ref_idx].flags); if (error != 0) { make_parse_error(parse_error, 0, "Incompatible flags or duplicate " "GID %u.", id); return (EINVAL); } check_type_and_id_flags(type, array[ref_idx].flags); break; case IT_UID: /* * No flags in this case. Multiple appearances * of the same UID is an exact redundancy, so * error out. */ make_parse_error(parse_error, 0, "Duplicate UID %u.", id); return (EINVAL); default: __assert_unreachable(); } } *nb = ref_idx + 1; } return (0); } /* * See also the herald comment for parse_rules() below. * * The second part of a rule, called (or ), is a comma-separated * (',') list of '=' clauses similar to that of the * part, with the extensions that may also be "*" or "any" or ".", and that * may contain at most one of the '+', '-' and '!' characters when * is "gid" (no flags are allowed for "uid"). No two clauses in a single * list may list the same . "*" and "any" both designate any ID for * the , and are aliases to each other. In front of "any" (or "*"), only * the '+' flag is allowed (in the "gid" case). "." designates the process' * current IDs for the . The precise meaning of flags and "." is * explained in functions checking privileges below. */ static int parse_single_rule(char *rule, struct rules *const rules, struct parse_error **const parse_error) { const char *const start = rule; const char *from_type, *from_id, *p; char *to_list; struct id_list uid_list, gid_list; struct id_elem *ie, *ie_next; struct rule *new; int error; MPASS(*parse_error == NULL); STAILQ_INIT(&uid_list); STAILQ_INIT(&gid_list); /* Freed when the 'struct rules' container is freed. */ new = malloc(sizeof(*new), M_DO, M_WAITOK | M_ZERO); from_type = strsep_noblanks(&rule, "="); MPASS(from_type != NULL); /* Because 'rule' was not NULL. */ error = parse_id_type(from_type, &new->from_type, parse_error); if (error != 0) goto einval; switch (new->from_type) { case IT_UID: case IT_GID: break; default: make_parse_error(parse_error, 0, "Type '%s' not allowed in " "the \"from\" part of rules."); goto einval; } - from_id = strsep_noblanks(&rule, ":"); + from_id = strsep_noblanks(&rule, ":>"); if (is_null_or_empty(from_id)) { make_parse_error(parse_error, 0, "No ID specified."); goto einval; } error = strtoui_strict(from_id, &p, 10, &new->from_id); if (error != 0 || *p != '\0') { make_parse_error(parse_error, from_id - start, "Cannot parse a numerical ID (base 10)."); goto einval; } /* * We will now parse the "to" list. * * In order to ease parsing, we will begin by building lists of target * UIDs and GIDs in local variables 'uid_list' and 'gid_list'. The * number of each type of IDs will be filled directly in 'new'. At end * of parse, we will allocate both arrays of IDs to be placed into the * 'uids' and 'gids' members, sort them, and discard the tail queues * used to build them. This conversion to sorted arrays at end of parse * allows to minimize memory allocations and enables searching IDs in * O(log(n)) instead of linearly. */ to_list = strsep_noblanks(&rule, ","); if (to_list == NULL) { make_parse_error(parse_error, 0, "No target list."); goto einval; } do { error = parse_target_clause(to_list, new, &uid_list, &gid_list, parse_error); if (error != 0) { (*parse_error)->pos += to_list - start; goto einval; } to_list = strsep_noblanks(&rule, ","); } while (to_list != NULL); if (new->uids_nb != 0) { new->uids = malloc(sizeof(*new->uids) * new->uids_nb, M_DO, M_WAITOK); error = pour_list_into_rule(IT_UID, &uid_list, new->uids, &new->uids_nb, parse_error); if (error != 0) goto einval; } MPASS(STAILQ_EMPTY(&uid_list)); if (!has_clauses(new->uids_nb, new->uid_flags)) { /* No UID specified, default is "uid=.". */ MPASS(new->uid_flags == 0); new->uid_flags = MDF_CURRENT; check_type_and_type_flags(IT_UID, new->uid_flags); } if (new->gids_nb != 0) { new->gids = malloc(sizeof(*new->gids) * new->gids_nb, M_DO, M_WAITOK); error = pour_list_into_rule(IT_GID, &gid_list, new->gids, &new->gids_nb, parse_error); if (error != 0) goto einval; } MPASS(STAILQ_EMPTY(&gid_list)); if (!has_clauses(new->gids_nb, new->gid_flags)) { /* No GID specified, default is "gid=.,!gid=.". */ MPASS(new->gid_flags == 0); new->gid_flags = MDF_CURRENT | MDF_PRIMARY | MDF_SUPP_MUST | MDF_HAS_PRIMARY_CLAUSE | MDF_HAS_SUPP_CLAUSE; check_type_and_type_flags(IT_GID, new->gid_flags); } STAILQ_INSERT_TAIL(&rules->head, new, r_entries); return (0); einval: free(new->gids, M_DO); free(new->uids, M_DO); free(new, M_DO); STAILQ_FOREACH_SAFE(ie, &gid_list, ie_entries, ie_next) free(ie, M_DO); STAILQ_FOREACH_SAFE(ie, &uid_list, ie_entries, ie_next) free(ie, M_DO); MPASS(*parse_error != NULL); return (EINVAL); } /* * Parse rules specification and produce rule structures out of it. * * Returns 0 on success, with '*rulesp' made to point to a 'struct rule' * representing the rules. On error, the returned value is non-zero and * '*rulesp' is unchanged. If 'string' has length greater or equal to * MAC_RULE_STRING_LEN, ENAMETOOLONG is returned. If it is not in the expected * format, EINVAL is returned. If an error is returned, '*parse_error' is set * to point to a 'struct parse_error' giving an error message for the problem, * else '*parse_error' is set to NULL. * - * Expected format: A semi-colon-separated list of rules of the form - * ":". The part is of the form "=" where + * Expected format: A >-colon-separated list of rules of the form + * ">" (for backwards compatibility, a semi-colon ":" is accepted + * in place of '>'). The part is of the form "=" where * is "uid" or "gid", an UID or GID (depending on ) and is * "*", "any" or a comma-separated list of '=' clauses (see the * comment for parse_single_rule() for more details). For convenience, empty * rules are allowed (and do nothing), and spaces and tabs are allowed (and * removed) around each token (tokens are natural ones, except that * '' as a whole is considered a single token, so no blanks are * allowed between '' and ''). * * Examples: - * - "uid=1001:uid=1010,gid=1010;uid=1002:any" - * - "gid=1010:gid=1011,gid=1012,gid=1013" + * - "uid=1001>uid=1010,gid=1010;uid=1002>any" + * - "gid=1010>gid=1011,gid=1012,gid=1013" */ static int parse_rules(const char *const string, struct rules **const rulesp, struct parse_error **const parse_error) { const size_t len = strlen(string); char *copy, *p, *rule; struct rules *rules; int error = 0; *parse_error = NULL; if (len >= MAC_RULE_STRING_LEN) { make_parse_error(parse_error, 0, "Rule specification string is too long (%zu, max %zu)", len, MAC_RULE_STRING_LEN - 1); return (ENAMETOOLONG); } rules = alloc_rules(); bcopy(string, rules->string, len + 1); MPASS(rules->string[len] == '\0'); /* Catch some races. */ copy = malloc(len + 1, M_DO, M_WAITOK); bcopy(string, copy, len + 1); MPASS(copy[len] == '\0'); /* Catch some races. */ p = copy; while ((rule = strsep_noblanks(&p, ";")) != NULL) { if (rule[0] == '\0') continue; error = parse_single_rule(rule, rules, parse_error); if (error != 0) { (*parse_error)->pos += rule - copy; toast_rules(rules); goto out; } } *rulesp = rules; out: free(copy, M_DO); return (error); } /* * Find rules applicable to the passed prison. * * Returns the applicable rules (and never NULL). 'pr' must be unlocked. * 'aprp' is set to the (ancestor) prison holding these, and it must be unlocked * once the caller is done accessing the rules. '*aprp' is equal to 'pr' if and * only if the current jail has its own set of rules. */ static struct rules * find_rules(struct prison *const pr, struct prison **const aprp) { struct prison *cpr, *ppr; struct rules *rules; cpr = pr; for (;;) { prison_lock(cpr); rules = osd_jail_get(cpr, osd_jail_slot); if (rules != NULL) break; prison_unlock(cpr); ppr = cpr->pr_parent; MPASS(ppr != NULL); /* prison0 always has rules. */ cpr = ppr; } *aprp = cpr; return (rules); } static void hold_rules(struct rules *const rules) { refcount_acquire(&rules->use_count); } static void drop_rules(struct rules *const rules) { if (refcount_release(&rules->use_count)) toast_rules(rules); } #ifdef INVARIANTS static void check_rules_use_count(const struct rules *const rules, u_int expected) { const u_int use_count = refcount_load(&rules->use_count); if (use_count != expected) panic("MAC/do: Rules at %p: Use count is %u, expected %u", rules, use_count, expected); } #else #define check_rules_use_count(...) #endif /* INVARIANTS */ /* * OSD destructor for slot 'osd_jail_slot'. * * Called with 'value' not NULL. We have arranged that it is only ever called * when the corresponding jail goes down or at module unload. */ static void dealloc_jail_osd(void *const value) { struct rules *const rules = value; /* * If called because the "holding" jail goes down, no one should be * using the rules but us at this point because no threads of that jail * (or its sub-jails) should currently be executing (in particular, * currently executing setcred()). The case of module unload is more * complex. Although the MAC framework takes care that no hook is * called while a module is unloading, the unload could happen between * two calls to MAC hooks in the course of, e.g., executing setcred(), * where the rules' reference count has been bumped to keep them alive * even if the rules on the "holding" jail has been concurrently * changed. These other references are held in our thread OSD slot, so * we ensure that all thread's slots are freed first in mac_do_destroy() * to be able to check that only one reference remains. */ check_rules_use_count(rules, 1); toast_rules(rules); } /* * Remove the rules specifically associated to a prison. * * In practice, this means that the rules become inherited (from the closest * ascendant that has some). * * Destroys the 'osd_jail_slot' slot of the passed jail. */ static void remove_rules(struct prison *const pr) { struct rules *old_rules; int error __unused; prison_lock(pr); /* * We go to the burden of extracting rules first instead of just letting * osd_jail_del() calling dealloc_jail_osd() as we want to decrement * their use count, and possibly free them, outside of the prison lock. */ old_rules = osd_jail_get(pr, osd_jail_slot); error = osd_jail_set(pr, osd_jail_slot, NULL); /* osd_set() never fails nor allocate memory when 'value' is NULL. */ MPASS(error == 0); /* * This completely frees the OSD slot, but doesn't call the destructor * since we've just put NULL in the slot. */ osd_jail_del(pr, osd_jail_slot); prison_unlock(pr); if (old_rules != NULL) drop_rules(old_rules); } /* * Assign already built rules to a jail. */ static void set_rules(struct prison *const pr, struct rules *const rules) { struct rules *old_rules; void **rsv; check_rules_use_count(rules, 0); hold_rules(rules); rsv = osd_reserve(osd_jail_slot); prison_lock(pr); old_rules = osd_jail_get(pr, osd_jail_slot); osd_jail_set_reserved(pr, osd_jail_slot, rsv, rules); prison_unlock(pr); if (old_rules != NULL) drop_rules(old_rules); } /* * Assigns empty rules to a jail. */ static void set_empty_rules(struct prison *const pr) { struct rules *const rules = alloc_rules(); set_rules(pr, rules); } /* * Parse a rules specification and assign them to a jail. * * Returns the same error code as parse_rules() (which see). */ static int parse_and_set_rules(struct prison *const pr, const char *rules_string, struct parse_error **const parse_error) { struct rules *rules; int error; error = parse_rules(rules_string, &rules, parse_error); if (error != 0) return (error); set_rules(pr, rules); return (0); } static int mac_do_sysctl_rules(SYSCTL_HANDLER_ARGS) { char *const buf = malloc(MAC_RULE_STRING_LEN, M_DO, M_WAITOK); struct prison *const td_pr = req->td->td_ucred->cr_prison; struct prison *pr; struct rules *rules; struct parse_error *parse_error; int error; rules = find_rules(td_pr, &pr); strlcpy(buf, rules->string, MAC_RULE_STRING_LEN); prison_unlock(pr); error = sysctl_handle_string(oidp, buf, MAC_RULE_STRING_LEN, req); if (error != 0 || req->newptr == NULL) goto out; /* Set our prison's rules, not that of the jail we inherited from. */ error = parse_and_set_rules(td_pr, buf, &parse_error); if (error != 0) { if (print_parse_error) printf("MAC/do: Parse error at index %zu: %s\n", parse_error->pos, parse_error->msg); free_parse_error(parse_error); } out: free(buf, M_DO); return (error); } SYSCTL_PROC(_security_mac_do, OID_AUTO, rules, CTLTYPE_STRING|CTLFLAG_RW|CTLFLAG_PRISON|CTLFLAG_MPSAFE, 0, 0, mac_do_sysctl_rules, "A", "Rules"); SYSCTL_JAIL_PARAM_SYS_SUBNODE(mac, do, CTLFLAG_RW, "Jail MAC/do parameters"); SYSCTL_JAIL_PARAM_STRING(_mac_do, rules, CTLFLAG_RW, MAC_RULE_STRING_LEN, "Jail MAC/do rules"); static int mac_do_jail_create(void *obj, void *data __unused) { struct prison *const pr = obj; set_empty_rules(pr); return (0); } static int mac_do_jail_get(void *obj, void *data) { struct prison *ppr, *const pr = obj; struct vfsoptlist *const opts = data; struct rules *rules; int jsys, error; rules = find_rules(pr, &ppr); jsys = pr == ppr ? (STAILQ_EMPTY(&rules->head) ? JAIL_SYS_DISABLE : JAIL_SYS_NEW) : JAIL_SYS_INHERIT; error = vfs_setopt(opts, "mac.do", &jsys, sizeof(jsys)); if (error != 0 && error != ENOENT) goto done; error = vfs_setopts(opts, "mac.do.rules", rules->string); if (error != 0 && error != ENOENT) goto done; error = 0; done: prison_unlock(ppr); return (error); } /* * -1 is used as a sentinel in mac_do_jail_check() and mac_do_jail_set() below. */ _Static_assert(-1 != JAIL_SYS_DISABLE && -1 != JAIL_SYS_NEW && -1 != JAIL_SYS_INHERIT, "mac_do(4) uses -1 as a sentinel for uninitialized 'jsys'."); /* * We perform only cheap checks here, i.e., we do not really parse the rules * specification string, if any. */ static int mac_do_jail_check(void *obj, void *data) { struct vfsoptlist *opts = data; char *rules_string; int error, jsys, size; error = vfs_copyopt(opts, "mac.do", &jsys, sizeof(jsys)); if (error == ENOENT) jsys = -1; else { if (error != 0) return (error); if (jsys != JAIL_SYS_DISABLE && jsys != JAIL_SYS_NEW && jsys != JAIL_SYS_INHERIT) return (EINVAL); } /* * We use vfs_getopt() here instead of vfs_getopts() to get the length. * We perform the additional checks done by the latter here, even if * jail_set() calls vfs_getopts() itself later (they becoming * inconsistent wouldn't cause any security problem). */ error = vfs_getopt(opts, "mac.do.rules", (void**)&rules_string, &size); if (error == ENOENT) { /* * Default (in absence of "mac.do.rules") is to disable (and, in * particular, not inherit). */ if (jsys == -1) jsys = JAIL_SYS_DISABLE; if (jsys == JAIL_SYS_NEW) { vfs_opterror(opts, "'mac.do.rules' must be specified " "given 'mac.do''s value"); return (EINVAL); } /* Absence of "mac.do.rules" at this point is OK. */ error = 0; } else { if (error != 0) return (error); /* Not a proper string. */ if (size == 0 || rules_string[size - 1] != '\0') { vfs_opterror(opts, "'mac.do.rules' not a proper string"); return (EINVAL); } if (size > MAC_RULE_STRING_LEN) { vfs_opterror(opts, "'mdo.rules' too long"); return (ENAMETOOLONG); } if (jsys == -1) /* Default (if "mac.do.rules" is present). */ jsys = rules_string[0] == '\0' ? JAIL_SYS_DISABLE : JAIL_SYS_NEW; /* * Be liberal and accept JAIL_SYS_DISABLE and JAIL_SYS_INHERIT * with an explicit empty rules specification. */ switch (jsys) { case JAIL_SYS_DISABLE: case JAIL_SYS_INHERIT: if (rules_string[0] != '\0') { vfs_opterror(opts, "'mac.do.rules' specified " "but should not given 'mac.do''s value"); return (EINVAL); } break; } } return (error); } static int mac_do_jail_set(void *obj, void *data) { struct prison *pr = obj; struct vfsoptlist *opts = data; char *rules_string; struct parse_error *parse_error; int error, jsys; /* * The invariants checks used below correspond to what has already been * checked in jail_check() above. */ error = vfs_copyopt(opts, "mac.do", &jsys, sizeof(jsys)); MPASS(error == 0 || error == ENOENT); if (error != 0) jsys = -1; /* Mark unfilled. */ rules_string = vfs_getopts(opts, "mac.do.rules", &error); MPASS(error == 0 || error == ENOENT); if (error == 0) { MPASS(strlen(rules_string) < MAC_RULE_STRING_LEN); if (jsys == -1) /* Default (if "mac.do.rules" is present). */ jsys = rules_string[0] == '\0' ? JAIL_SYS_DISABLE : JAIL_SYS_NEW; else MPASS(jsys == JAIL_SYS_NEW || ((jsys == JAIL_SYS_DISABLE || jsys == JAIL_SYS_INHERIT) && rules_string[0] == '\0')); } else { MPASS(jsys != JAIL_SYS_NEW); if (jsys == -1) /* * Default (in absence of "mac.do.rules") is to disable * (and, in particular, not inherit). */ jsys = JAIL_SYS_DISABLE; /* If disabled, we'll store an empty rule specification. */ if (jsys == JAIL_SYS_DISABLE) rules_string = ""; } switch (jsys) { case JAIL_SYS_INHERIT: remove_rules(pr); error = 0; break; case JAIL_SYS_DISABLE: case JAIL_SYS_NEW: error = parse_and_set_rules(pr, rules_string, &parse_error); if (error != 0) { vfs_opterror(opts, "MAC/do: Parse error at index %zu: %s\n", parse_error->pos, parse_error->msg); free_parse_error(parse_error); } break; default: __assert_unreachable(); } return (error); } /* * OSD jail methods. * * There is no PR_METHOD_REMOVE, as OSD storage is destroyed by the common jail * code (see prison_cleanup()), which triggers a run of our dealloc_jail_osd() * destructor. */ static const osd_method_t osd_methods[PR_MAXMETHOD] = { [PR_METHOD_CREATE] = mac_do_jail_create, [PR_METHOD_GET] = mac_do_jail_get, [PR_METHOD_CHECK] = mac_do_jail_check, [PR_METHOD_SET] = mac_do_jail_set, }; /* * Common header structure. * * Each structure that is used to pass information between some MAC check * function and priv_grant() must start with this header. */ struct mac_do_data_header { /* Size of the allocated buffer holding the containing structure. */ size_t allocated_size; /* Full size of the containing structure. */ size_t size; /* * For convenience, we use privilege numbers as an identifier for the * containing structure's type, since there is one distinct privilege * for each privilege changing function we are supporting. 0 in 'priv' * indicates this header is uninitialized. */ int priv; /* Rules to apply. */ struct rules *rules; }; /* * The case of unusable or absent per-thread data can actually happen as nothing * prevents, e.g., priv_check*() with privilege 'priv' to be called standalone, * as it is currently by, e.g., the Linux emulator for PRIV_CRED_SETUID. We * interpret such calls to priv_check*() as full, unrestricted requests for * 'priv', contrary to what we're doing here for selected operations, and * consequently will not grant the requested privilege. * * Also, we protect ourselves from a concurrent change of 'do_enabled' while * a call to setcred() is in progress by storing the rules per-thread * which is then consulted by each successive hook so that they all have * a coherent view of the specifications, and we empty the slot (actually, mark * it as empty) when MAC/do is disabled. */ static int check_data_usable(const void *const data, const size_t size, const int priv) { const struct mac_do_data_header *const hdr = data; if (hdr == NULL || hdr->priv == 0) return (ENOENT); /* * Impacting changes in the protocols we are based on... Don't crash in * production. */ if (hdr->priv != priv) { MPASS(hdr->priv == priv); return (EBUSY); } MPASS(hdr->size == size); MPASS(hdr->size <= hdr->allocated_size); return (0); } static void clear_data(void *const data) { struct mac_do_data_header *const hdr = data; if (hdr != NULL) { drop_rules(hdr->rules); /* We don't deallocate so as to save time on next access. */ hdr->priv = 0; } } static void * fetch_data(void) { return (osd_thread_get_unlocked(curthread, osd_thread_slot)); } static bool is_data_reusable(const void *const data, const size_t size) { const struct mac_do_data_header *const hdr = data; return (hdr != NULL && size <= hdr->allocated_size); } static void set_data_header(void *const data, const size_t size, const int priv, struct rules *const rules) { struct mac_do_data_header *const hdr = data; MPASS(hdr->priv == 0); MPASS(priv != 0); MPASS(size <= hdr->allocated_size); hdr->size = size; hdr->priv = priv; hdr->rules = rules; } /* The proc lock (and any other non-sleepable lock) must not be held. */ static void * alloc_data(void *const data, const size_t size) { struct mac_do_data_header *const hdr = realloc(data, size, M_DO, M_WAITOK); MPASS(size >= sizeof(struct mac_do_data_header)); hdr->allocated_size = size; hdr->priv = 0; if (hdr != data) { /* * This call either reuses the existing memory allocated for the * slot or tries to allocate some without blocking. */ int error = osd_thread_set(curthread, osd_thread_slot, hdr); if (error != 0) { /* Going to make a M_WAITOK allocation. */ void **const rsv = osd_reserve(osd_thread_slot); error = osd_thread_set_reserved(curthread, osd_thread_slot, rsv, hdr); MPASS(error == 0); } } return (hdr); } /* Destructor for 'osd_thread_slot'. */ static void dealloc_thread_osd(void *const value) { free(value, M_DO); } /* * Whether to grant access to some primary group according to flags. * * The passed 'flags' must be those of a rule's matching GID, or the IT_GID type * flags when MDF_CURRENT has been matched. * * Return values: * - 0: Access granted. * - EJUSTRETURN: Flags are agnostic. */ static int grant_primary_group_from_flags(const flags_t flags) { return ((flags & MDF_PRIMARY) != 0 ? 0 : EJUSTRETURN); } /* * Same as grant_primary_group_from_flags(), but for supplementary groups. * * Return values: * - 0: Access granted. * - EJUSTRETURN: Flags are agnostic. * - EPERM: Access denied. */ static int __unused grant_supplementary_group_from_flags(const flags_t flags) { if ((flags & MDF_SUPP_MASK) != 0) return ((flags & MDF_SUPP_DONT) != 0 ? EPERM : 0); return (EJUSTRETURN); } static int rule_grant_supplementary_groups(const struct rule *const rule, const struct ucred *const old_cred, const struct ucred *const new_cred) { const gid_t *const old_groups = old_cred->cr_groups; const gid_t *const new_groups = new_cred->cr_groups; const int old_ngroups = old_cred->cr_ngroups; const int new_ngroups = new_cred->cr_ngroups; const flags_t gid_flags = rule->gid_flags; const bool current_has_supp = (gid_flags & MDF_CURRENT) != 0 && (gid_flags & MDF_SUPP_MASK) != 0; id_nb_t rule_idx = 0; int old_idx = 1, new_idx = 1; if ((gid_flags & MDF_ANY_SUPP) != 0 && (gid_flags & MDF_MAY_REJ_SUPP) == 0) /* * Any set of supplementary groups is accepted, no need to loop * over them. */ return (0); for (; new_idx < new_ngroups; ++new_idx) { const gid_t gid = new_groups[new_idx]; bool may_accept = false; if ((gid_flags & MDF_ANY_SUPP) != 0) may_accept = true; /* Do we have to check for the current supplementary groups? */ if (current_has_supp) { /* * Linear search, as both supplementary groups arrays * are sorted. Advancing 'old_idx' with a binary search * on absence of MDF_SUPP_MUST doesn't seem worth it in * practice. */ for (; old_idx < old_ngroups; ++old_idx) { const gid_t old_gid = old_groups[old_idx]; if (old_gid < gid) { /* Mandatory but absent. */ if ((gid_flags & MDF_SUPP_MUST) != 0) return (EPERM); } else if (old_gid == gid) { switch (gid_flags & MDF_SUPP_MASK) { case MDF_SUPP_DONT: /* Present but forbidden. */ return (EPERM); case MDF_SUPP_ALLOW: case MDF_SUPP_MUST: may_accept = true; break; default: #ifdef INVARIANTS __assert_unreachable(); #else /* Better be safe than sorry. */ return (EPERM); #endif } ++old_idx; break; } else break; } } /* * Search by GID for a corresponding 'struct id_spec'. * * Again, linear search, with same note on not using binary * search optimization as above (the trigger would be absence of * MDF_EXPLICIT_SUPP_MUST this time). */ for (; rule_idx < rule->gids_nb; ++rule_idx) { const struct id_spec is = rule->gids[rule_idx]; if (is.id < gid) { /* Mandatory but absent. */ if ((is.flags & MDF_SUPP_MUST) != 0) return (EPERM); } else if (is.id == gid) { switch (is.flags & MDF_SUPP_MASK) { case MDF_SUPP_DONT: /* Present but forbidden. */ return (EPERM); case MDF_SUPP_ALLOW: case MDF_SUPP_MUST: may_accept = true; break; case 0: /* Primary group only. */ break; default: #ifdef INVARIANTS __assert_unreachable(); #else /* Better be safe than sorry. */ return (EPERM); #endif } ++rule_idx; break; } else break; } /* 'gid' wasn't explicitly accepted. */ if (!may_accept) return (EPERM); } /* * If we must have all current groups and we didn't browse all * of them at this point (because the remaining ones have GIDs * greater than the last requested group), we are simply missing * them. */ if ((gid_flags & MDF_CURRENT) != 0 && (gid_flags & MDF_SUPP_MUST) != 0 && old_idx < old_ngroups) return (EPERM); /* * Similarly, we have to finish browsing all GIDs from the rule * in case some are marked mandatory. */ if ((gid_flags & MDF_EXPLICIT_SUPP_MUST) != 0) { for (; rule_idx < rule->gids_nb; ++rule_idx) { const struct id_spec is = rule->gids[rule_idx]; if ((is.flags & MDF_SUPP_MUST) != 0) return (EPERM); } } return (0); } static int rule_grant_primary_group(const struct rule *const rule, const struct ucred *const old_cred, const gid_t gid) { struct id_spec gid_is = {.flags = 0}; const struct id_spec *found_is; int error; if ((rule->gid_flags & MDF_ANY) != 0) return (0); /* Was MDF_CURRENT specified, and is 'gid' a current GID? */ if ((rule->gid_flags & MDF_CURRENT) != 0 && group_is_primary(gid, old_cred)) { error = grant_primary_group_from_flags(rule->gid_flags); if (error == 0) return (0); } /* Search by GID for a corresponding 'struct id_spec'. */ gid_is.id = gid; found_is = bsearch(&gid_is, rule->gids, rule->gids_nb, sizeof(*rule->gids), id_spec_cmp); if (found_is != NULL) { error = grant_primary_group_from_flags(found_is->flags); if (error == 0) return (0); } return (EPERM); } static int rule_grant_primary_groups(const struct rule *const rule, const struct ucred *const old_cred, const struct ucred *const new_cred) { int error; /* Shortcut. */ if ((rule->gid_flags & MDF_ANY) != 0) return (0); error = rule_grant_primary_group(rule, old_cred, new_cred->cr_gid); if (error != 0) return (error); error = rule_grant_primary_group(rule, old_cred, new_cred->cr_rgid); if (error != 0) return (error); error = rule_grant_primary_group(rule, old_cred, new_cred->cr_svgid); if (error != 0) return (error); return (0); } static bool user_is_current(const uid_t uid, const struct ucred *const old_cred) { return (uid == old_cred->cr_uid || uid == old_cred->cr_ruid || uid == old_cred->cr_svuid); } static int rule_grant_user(const struct rule *const rule, const struct ucred *const old_cred, const uid_t uid) { struct id_spec uid_is = {.flags = 0}; const struct id_spec *found_is; if ((rule->uid_flags & MDF_ANY) != 0) return (0); /* Was MDF_CURRENT specified, and is 'uid' a current UID? */ if ((rule->uid_flags & MDF_CURRENT) != 0 && user_is_current(uid, old_cred)) return (0); /* Search by UID for a corresponding 'struct id_spec'. */ uid_is.id = uid; found_is = bsearch(&uid_is, rule->uids, rule->uids_nb, sizeof(*rule->uids), id_spec_cmp); if (found_is != NULL) return (0); return (EPERM); } static int rule_grant_users(const struct rule *const rule, const struct ucred *const old_cred, const struct ucred *const new_cred) { int error; /* Shortcut. */ if ((rule->uid_flags & MDF_ANY) != 0) return (0); error = rule_grant_user(rule, old_cred, new_cred->cr_uid); if (error != 0) return (error); error = rule_grant_user(rule, old_cred, new_cred->cr_ruid); if (error != 0) return (error); error = rule_grant_user(rule, old_cred, new_cred->cr_svuid); if (error != 0) return (error); return (0); } static int rule_grant_setcred(const struct rule *const rule, const struct ucred *const old_cred, const struct ucred *const new_cred) { int error; error = rule_grant_users(rule, old_cred, new_cred); if (error != 0) return (error); error = rule_grant_primary_groups(rule, old_cred, new_cred); if (error != 0) return (error); error = rule_grant_supplementary_groups(rule, old_cred, new_cred); if (error != 0) return (error); return (0); } static bool rule_applies(const struct rule *const rule, const struct ucred *const cred) { if (rule->from_type == IT_UID && rule->from_id == cred->cr_ruid) return (true); if (rule->from_type == IT_GID && realgroupmember(rule->from_id, cred)) return (true); return (false); } /* * To pass data between check_setcred() and priv_grant() (on PRIV_CRED_SETCRED). */ struct mac_do_setcred_data { struct mac_do_data_header hdr; const struct ucred *new_cred; u_int setcred_flags; }; static int mac_do_priv_grant(struct ucred *cred, int priv) { struct mac_do_setcred_data *const data = fetch_data(); const struct rules *rules; const struct ucred *new_cred; const struct rule *rule; u_int setcred_flags; int error; /* Bail out fast if we aren't concerned. */ if (priv != PRIV_CRED_SETCRED) return (EPERM); /* * Do we have to do something? */ if (check_data_usable(data, sizeof(*data), priv) != 0) /* No. */ return (EPERM); rules = data->hdr.rules; new_cred = data->new_cred; KASSERT(new_cred != NULL, ("priv_check*() called before mac_cred_check_setcred()")); setcred_flags = data->setcred_flags; /* * Explicitly check that only the flags we currently support are present * in order to avoid accepting transitions with other changes than those * we are actually going to check. Currently, this rules out the * SETCREDF_MAC_LABEL flag. This may be improved by adding code * actually checking whether the requested label and the current one * would differ. */ if ((setcred_flags & ~(SETCREDF_UID | SETCREDF_RUID | SETCREDF_SVUID | SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID | SETCREDF_SUPP_GROUPS)) != 0) return (EPERM); /* * Browse rules, and for those that match the requestor, call specific * privilege granting functions interpreting the "to"/"target" part. */ error = EPERM; STAILQ_FOREACH(rule, &rules->head, r_entries) if (rule_applies(rule, cred)) { error = rule_grant_setcred(rule, cred, new_cred); if (error != EPERM) break; } return (error); } static int check_proc(void) { char *path, *to_free; int error; /* * Only grant privileges if requested by the right executable. * * XXXOC: We may want to base this check on a tunable path and/or * a specific MAC label. Going even further, e.g., envisioning to * completely replace the path check with the latter, we would need to * install FreeBSD on a FS with multilabel enabled by default, which in * practice entails adding an option to ZFS to set MNT_MULTILABEL * automatically on mounts, ensuring that root (and more if using * different partitions) ZFS or UFS filesystems are created with * multilabel turned on, and having the installation procedure support * setting a MAC label per file (perhaps via additions to mtree(1)). So * this probably isn't going to happen overnight, if ever. */ if (vn_fullpath(curproc->p_textvp, &path, &to_free) != 0) return (EPERM); error = strcmp(path, "/usr/bin/mdo") == 0 ? 0 : EPERM; free(to_free, M_TEMP); return (error); } static void mac_do_setcred_enter(void) { struct rules *rules; struct prison *pr; struct mac_do_setcred_data * data; int error; /* * If not enabled, don't prepare data. Other hooks will check for that * to know if they have to do something. */ if (do_enabled == 0) return; /* * MAC/do only applies to a process launched from a given executable. * For other processes, we just won't intervene (we don't deny requests, * nor do we grant privileges to them). */ error = check_proc(); if (error != 0) return; /* * Find the currently applicable rules. */ rules = find_rules(curproc->p_ucred->cr_prison, &pr); hold_rules(rules); prison_unlock(pr); /* * Setup thread data to be used by other hooks. */ data = fetch_data(); if (!is_data_reusable(data, sizeof(*data))) data = alloc_data(data, sizeof(*data)); set_data_header(data, sizeof(*data), PRIV_CRED_SETCRED, rules); /* Not really necessary, but helps to catch programming errors. */ data->new_cred = NULL; data->setcred_flags = 0; } static int mac_do_check_setcred(u_int flags, const struct ucred *const old_cred, struct ucred *const new_cred) { struct mac_do_setcred_data *const data = fetch_data(); /* * Do we have to do something? */ if (check_data_usable(data, sizeof(*data), PRIV_CRED_SETCRED) != 0) /* No. */ return (0); /* * Keep track of the setcred() flags and the new credentials for * priv_check*(). */ data->new_cred = new_cred; data->setcred_flags = flags; return (0); } static void mac_do_setcred_exit(void) { struct mac_do_setcred_data *const data = fetch_data(); if (check_data_usable(data, sizeof(*data), PRIV_CRED_SETCRED) == 0) /* * This doesn't deallocate the small per-thread data storage, * which can be reused on subsequent calls. (That data is of * course deallocated as the current thread dies or this module * is unloaded.) */ clear_data(data); } static void mac_do_init(struct mac_policy_conf *mpc) { struct prison *pr; osd_jail_slot = osd_jail_register(dealloc_jail_osd, osd_methods); set_empty_rules(&prison0); sx_slock(&allprison_lock); TAILQ_FOREACH(pr, &allprison, pr_list) set_empty_rules(pr); sx_sunlock(&allprison_lock); osd_thread_slot = osd_thread_register(dealloc_thread_osd); } static void mac_do_destroy(struct mac_policy_conf *mpc) { /* * osd_thread_deregister() must be called before osd_jail_deregister(), * for the reason explained in dealloc_jail_osd(). */ osd_thread_deregister(osd_thread_slot); osd_jail_deregister(osd_jail_slot); } static struct mac_policy_ops do_ops = { .mpo_init = mac_do_init, .mpo_destroy = mac_do_destroy, .mpo_cred_setcred_enter = mac_do_setcred_enter, .mpo_cred_check_setcred = mac_do_check_setcred, .mpo_cred_setcred_exit = mac_do_setcred_exit, .mpo_priv_grant = mac_do_priv_grant, }; MAC_POLICY_SET(&do_ops, mac_do, "MAC/do", MPC_LOADTIME_FLAG_UNLOADOK, NULL); MODULE_VERSION(mac_do, 1);