Page MenuHomeFreeBSD

D58012.id183077.diff
No OneTemporary

D58012.id183077.diff

diff --git a/sys/security/mac_do/mac_do.c b/sys/security/mac_do/mac_do.c
--- a/sys/security/mac_do/mac_do.c
+++ b/sys/security/mac_do/mac_do.c
@@ -31,6 +31,7 @@
#include <sys/sysctl.h>
#include <sys/ucred.h>
#include <sys/vnode.h>
+#include <sys/imgact.h>
#include <security/mac/mac_policy.h>
@@ -79,6 +80,7 @@
static unsigned osd_jail_slot;
static unsigned osd_thread_slot;
+static unsigned osd_thread_exec_slot;
#define IT_INVALID 0 /* Must stay 0. */
#define IT_UID 1
@@ -200,6 +202,9 @@
id_nb_t gids_nb;
struct id_spec *uids;
struct id_spec *gids;
+ u_int exec_paths_nb;
+ char **exec_paths;
+ bool exec_is_blacklist;
};
STAILQ_HEAD(rulehead, rule);
@@ -390,7 +395,11 @@
STAILQ_FOREACH_SAFE(rule, head, r_entries, rule_next) {
free(rule->uids, M_MAC_DO);
free(rule->gids, M_MAC_DO);
+ for (u_int i = 0; i < rule->exec_paths_nb; i++)
+ free(rule->exec_paths[i], M_MAC_DO);
+ free(rule->exec_paths, M_MAC_DO);
free(rule, M_MAC_DO);
+
}
STAILQ_INIT(head);
}
@@ -477,18 +486,22 @@
* delimiter characters (so it doesn't prevent tokens containing spaces and tabs
* in the middle).
*/
+static inline char *
+skip_ws(char *p)
+{
+ return (p + strspn(p, " \t"));
+}
+
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;
+ p = skip_ws(p);
ret = strsep(&p, delim);
@@ -933,6 +946,100 @@
return (EINVAL);
}
+static int
+parse_rule_exec(char *exec_str, struct rule *const rule,
+ const char *const start, struct parse_error **const parse_error)
+{
+ char **paths, *p, *path_p, *tok, *key;
+ u_int nb, capacity;
+ bool negated;
+
+ nb = 0;
+ capacity = 4;
+ negated = false;
+ p = exec_str;
+
+ /* Check for '!' before the option key (consistent with gid flags). */
+ p = skip_ws(p);
+ if (*p == '!') {
+ negated = true;
+ p++;
+ p = skip_ws(p);
+ }
+
+ key = strsep_noblanks(&p, "=");
+ if (key == NULL || strcmp(key, "exec") != 0) {
+ make_parse_error(parse_error,
+ (key != NULL ? key : exec_str) - start,
+ "Unknown option '%.10s' (only 'exec' is supported).",
+ key != NULL ? key : "");
+ return (EINVAL);
+ }
+ if (p == NULL) {
+ make_parse_error(parse_error, key - start,
+ "Missing '=' after 'exec'.");
+ return (EINVAL);
+ }
+
+ p = skip_ws(p);
+
+ if (strcmp(p, "*") == 0) {
+ if (negated) {
+ make_parse_error(parse_error, p - start,
+ "Negated wildcard is not allowed.");
+ return (EINVAL);
+ }
+ rule->exec_paths_nb = 0;
+ rule->exec_paths = NULL;
+ return (0);
+ }
+
+ rule->exec_is_blacklist = negated;
+
+ /*
+ * Single pass with a dynamically growing array.
+ *
+ * A two-pass approach (count then copy) would require first making
+ * a copy of the string, since strsep() modifies it in place by
+ * replacing ':' with '\0'. Not worth the trouble.
+ */
+ paths = malloc(capacity * sizeof(char *), M_MAC_DO, M_WAITOK);
+ path_p = p;
+ while ((tok = strsep_noblanks(&path_p, ":")) != NULL) {
+ if (*tok == '\0')
+ continue;
+ if (tok[0] != '/') {
+ make_parse_error(parse_error, tok - start,
+ "Exec path '%s' is not absolute.", tok);
+ goto einval;
+ }
+ if (nb >= capacity) {
+ capacity *= 2;
+ paths = realloc(paths,
+ capacity * sizeof(char *), M_MAC_DO, M_WAITOK);
+ }
+ paths[nb] = strdup(tok, M_MAC_DO);
+ nb++;
+ }
+
+ if (nb == 0) {
+ make_parse_error(parse_error, p - start,
+ "Empty exec path list.");
+ goto einval;
+ }
+
+ /* Shrink allocation to actual size. */
+ rule->exec_paths = realloc(paths, nb * sizeof(char *),
+ M_MAC_DO, M_WAITOK);
+ rule->exec_paths_nb = nb;
+ return (0);
+
+einval:
+ rule->exec_paths = paths;
+ rule->exec_paths_nb = nb;
+ return (EINVAL);
+}
+
/*
* See also the herald comment for parse_rules() below.
*
@@ -953,7 +1060,9 @@
{
const char *const start = rule;
const char *from_type, *from_id, *p;
- char *to_list;
+ char *to, *to_list;
+ char *options;
+ char *target_str;
struct id_list uid_list, gid_list;
struct id_elem *ie, *ie_next;
struct rule *new;
@@ -994,6 +1103,36 @@
goto einval;
}
+ /*
+ * Extract optional options block from end of rule.
+ * Must happen before <to> comma parsing (which would
+ * consume the rule string).
+ *
+ * After strsep_noblanks(&rule, "("):
+ * rule != NULL → '(' was found, rule points past it.
+ * rule == NULL → no '(' in the string, no options block.
+ *
+ * After strsep_noblanks(&rule, ")"):
+ * rule == NULL → no ')' found (unclosed).
+ * rule != NULL → ')' found, rule points past it (to '\0' if
+ * at end of string, or to trailing content).
+ */
+ target_str = strsep_noblanks(&rule, "(");
+ options = NULL;
+ if (rule != NULL) {
+ options = strsep_noblanks(&rule, ")");
+ if (rule == NULL) {
+ make_parse_error(parse_error, target_str - start,
+ "Unclosed options block.");
+ goto einval;
+ }
+ if (*skip_ws(rule) != '\0') {
+ make_parse_error(parse_error, options - start,
+ "Unexpected content after options block.");
+ goto einval;
+ }
+ }
+
/*
* We will now parse the "to" list.
*
@@ -1006,21 +1145,22 @@
* 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) {
+ if (target_str == NULL) {
make_parse_error(parse_error, 0, "No target list.");
goto einval;
}
+ to_list = target_str;
+ to = strsep_noblanks(&to_list, ",");
do {
- error = parse_target_clause(to_list, new, &uid_list, &gid_list,
+ error = parse_target_clause(to, new, &uid_list, &gid_list,
parse_error);
if (error != 0) {
- (*parse_error)->pos += to_list - start;
+ (*parse_error)->pos += to - start;
goto einval;
}
- to_list = strsep_noblanks(&rule, ",");
- } while (to_list != NULL);
+ to = strsep_noblanks(&to_list, ",");
+ } while (to != NULL);
if (new->uids_nb != 0) {
new->uids = malloc(sizeof(*new->uids) * new->uids_nb, M_MAC_DO,
@@ -1055,11 +1195,21 @@
check_type_and_type_flags(IT_GID, new->gid_flags);
}
+ /* Parse options block (if present). */
+ if (options != NULL) {
+ error = parse_rule_exec(options, new, start, parse_error);
+ if (error != 0)
+ goto einval;
+ }
+
STAILQ_INSERT_TAIL(&rules->head, new, r_entries);
MPASS(error == 0 && *parse_error == NULL);
return (0);
einval:
+ for (u_int i = 0; i < new->exec_paths_nb; i++)
+ free(new->exec_paths[i], M_MAC_DO);
+ free(new->exec_paths, M_MAC_DO);
free(new->gids, M_MAC_DO);
free(new->uids, M_MAC_DO);
free(new, M_MAC_DO);
@@ -1454,6 +1604,15 @@
dst_rule->gids = malloc(gids_size, M_MAC_DO, M_WAITOK);
bcopy(src_rule->gids, dst_rule->gids, gids_size);
}
+ if (src_rule->exec_paths_nb > 0) {
+ dst_rule->exec_paths = malloc(
+ src_rule->exec_paths_nb * sizeof(char *), M_MAC_DO, M_WAITOK);
+ for (u_int i = 0; i < src_rule->exec_paths_nb; i++)
+ dst_rule->exec_paths[i] = strdup(src_rule->exec_paths[i],
+ M_MAC_DO);
+ dst_rule->exec_paths_nb = src_rule->exec_paths_nb;
+ dst_rule->exec_is_blacklist = src_rule->exec_is_blacklist;
+ }
STAILQ_INSERT_TAIL(&dst->head, dst_rule, r_entries);
}
@@ -2114,6 +2273,12 @@
return (hdr);
}
+struct mac_do_setcred_data {
+ struct mac_do_data_header hdr;
+ const struct ucred *new_cred;
+ u_int setcred_flags;
+};
+
/* Destructor for 'osd_thread_slot'. */
static void
dealloc_thread_osd(void *const value)
@@ -2121,6 +2286,54 @@
free(value, M_MAC_DO);
}
+/*
+ * Exec constraint data, stored in a separate OSD slot with a longer
+ * lifecycle than the transient setcred data. Set in priv_grant(),
+ * read in vnode_check_exec(), persists across multiple exec calls.
+ * Cleared only by the next setcred_enter(), thread exit, or module unload.
+ *
+ * 'conf != NULL' indicates the slot is armed.
+ */
+struct mac_do_exec_data {
+ struct conf *conf;
+ u_int nb_rules;
+ const struct rule **rules;
+};
+
+static void *
+fetch_exec_data(void)
+{
+ return (osd_thread_get_unlocked(curthread, osd_thread_exec_slot));
+}
+
+static void
+clear_exec_data(struct mac_do_exec_data *data)
+{
+ if (data == NULL)
+ return;
+ if (data->conf != NULL) {
+ drop_conf(data->conf);
+ data->conf = NULL;
+ }
+ free(data->rules, M_MAC_DO);
+ data->rules = NULL;
+ data->nb_rules = 0;
+}
+
+/* Destructor for 'osd_thread_exec_slot'. */
+static void
+dealloc_exec_osd(void *const value)
+{
+ struct mac_do_exec_data *const d = value;
+
+ if (d != NULL) {
+ if (d->conf != NULL)
+ drop_conf(d->conf);
+ free(d->rules, M_MAC_DO);
+ }
+ free(value, M_MAC_DO);
+}
+
/*
* Whether to grant access to some primary group according to flags.
*
@@ -2438,20 +2651,17 @@
/*
* 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();
+ struct mac_do_exec_data *exec_data;
struct rules *rules;
const struct ucred *new_cred;
const struct rule *rule;
u_int setcred_flags;
+ u_int matching_nb;
int error;
/* Bail out fast if we aren't concerned. */
@@ -2487,16 +2697,66 @@
/*
* Browse rules, and for those that match the requestor, call specific
* privilege granting functions interpreting the "to"/"target" part.
+ *
+ * We must check ALL rules, not just the first match: multiple rules
+ * may allow the same credential transition with different exec
+ * constraints, and the exec check must consider all of them.
*/
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;
- }
+ matching_nb = 0;
+ STAILQ_FOREACH(rule, &rules->head, r_entries) {
+ if (!rule_applies(rule, cred))
+ continue;
+ if (rule_grant_setcred(rule, cred, new_cred) == 0) {
+ error = 0;
+ if (rule->exec_paths_nb > 0 || rule->exec_is_blacklist)
+ matching_nb++;
+ }
+ }
- return (error);
+ if (error != 0)
+ return (error);
+
+ /*
+ * At least one rule granted the transition. Store all granting rules
+ * that carry exec constraints in the exec OSD slot so that
+ * vnode_check_exec can evaluate them per-rule (any-match semantics).
+ */
+ exec_data = fetch_exec_data();
+ clear_exec_data(exec_data);
+
+ if (matching_nb > 0) {
+ if (exec_data == NULL) {
+ exec_data = malloc(sizeof(*exec_data), M_MAC_DO,
+ M_WAITOK | M_ZERO);
+ int e = osd_thread_set(curthread, osd_thread_exec_slot,
+ exec_data);
+ if (e != 0) {
+ void **rsv = osd_reserve(osd_thread_exec_slot);
+ e = osd_thread_set_reserved(curthread,
+ osd_thread_exec_slot, rsv, exec_data);
+ MPASS(e == 0);
+ }
+ }
+ exec_data->rules = malloc(matching_nb * sizeof(struct rule *),
+ M_MAC_DO, M_WAITOK);
+ exec_data->nb_rules = 0;
+ exec_data->conf = data->hdr.conf;
+ hold_conf(exec_data->conf);
+
+ STAILQ_FOREACH(rule, &rules->head, r_entries) {
+ if (!rule_applies(rule, cred))
+ continue;
+ if (rule_grant_setcred(rule, cred, new_cred) == 0) {
+ if (rule->exec_paths_nb > 0 ||
+ rule->exec_is_blacklist)
+ exec_data->rules[exec_data->nb_rules++] =
+ rule;
+ }
+ }
+ }
+
+ return (0);
}
static int
@@ -2548,10 +2808,17 @@
mac_do_setcred_enter(void)
{
struct prison *const pr = curproc->p_ucred->cr_prison;
- struct mac_do_setcred_data * data;
+ struct mac_do_setcred_data *data;
struct conf *conf;
int error;
+ /*
+ * Always clear any prior exec constraint, even if mac_do is
+ * disabled or this process isn't trusted. A new setcred()
+ * replaces the old exec constraint.
+ */
+ clear_exec_data(fetch_exec_data());
+
/*
* If not enabled, don't prepare data. Other hooks will check for that
* to know if they have to do something.
@@ -2579,6 +2846,7 @@
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, conf);
/* Not really necessary, but helps to catch programming errors. */
data->new_cred = NULL;
@@ -2623,6 +2891,112 @@
clear_data(data);
}
+static int
+mac_do_vnode_check_exec(struct ucred *cred __unused,
+ struct vnode *vp __unused, struct label *vplabel __unused,
+ struct image_params *imgp, struct label *execlabel __unused)
+{
+ struct mac_do_exec_data *data;
+ const char *path;
+ char *to_free;
+ int error;
+
+ if (do_enabled == 0)
+ return (0);
+
+ data = fetch_exec_data();
+ if (data == NULL || data->conf == NULL)
+ return (0); /* No exec constraint. */
+
+ if (data->nb_rules == 0)
+ return (0); /* No rules with exec constraints. */
+
+ /*
+ * Determine the path of the binary being exec'd.
+ *
+ * imgp->interpreted != 0 means an image activator already ran on a
+ * prior pass of do_execve's interpret loop (shell or binmisc
+ * interpreter: IMGACT_SHELL / IMGACT_BINMISC set on the main imgp),
+ * or this is the fresh sub-imgp used by elf64_load_interp_file for
+ * the ELF dynamic linker (IMGACT_INTERP_ELF set before the call).
+ * In every such case, this vnode_check_exec fires for an interpreter
+ * being loaded by the kernel as part of exec'ing an already-checked
+ * binary, not a path chosen by the user, so allow it without further
+ * checking. The constraint persists, so any later execve(2) issued
+ * from within the interpreter still hits this check.
+ *
+ * For execve(2) (imgp->args->fname != NULL), imgp->execpath is the
+ * user-provided path for absolute paths, or resolved via
+ * vn_fullpath_hardlink() for relative paths. Using it avoids
+ * namecache-dependent issues where vn_fullpath_jail() can return a
+ * cached path from a prior lookup of the same inode by another process.
+ *
+ * For fexecve(2) (imgp->args->fname == NULL), imgp->execpath is set
+ * via vn_fullpath() which has the same namecache limitations, and
+ * may be NULL if that call failed. Fall back to vn_fullpath_jail()
+ * on imgp->vp, which is the best we can do without a user-provided
+ * path.
+ */
+ if (imgp->interpreted != 0)
+ return (0); /* Interpreter loading: allow. */
+
+ to_free = NULL;
+ if (imgp->args->fname != NULL) {
+ path = imgp->execpath;
+ } else {
+ char *resolved;
+
+ if (vn_fullpath_jail(imgp->vp, &resolved, &to_free) != 0)
+ return (EPERM); /* Cannot resolve: deny. */
+ path = resolved;
+ }
+
+ /*
+ * Per-rule evaluation: allow if ANY matching rule permits this exec.
+ *
+ * - Rule with no exec constraint (paths_nb == 0, not blacklist):
+ * allows any exec.
+ * - Whitelist rule: allows if path is in the list.
+ * - Blacklist rule: allows if path is NOT in the list.
+ *
+ * The constraint persists across multiple exec calls — it is NOT
+ * cleared here. Only a new setcred(), thread exit, or module
+ * unload clears it.
+ */
+ error = EPERM; /* Default deny. */
+ for (u_int r = 0; r < data->nb_rules; r++) {
+ const struct rule *rule = data->rules[r];
+
+ if (rule->exec_paths_nb == 0 && !rule->exec_is_blacklist) {
+ error = 0; /* No constraint: allow any. */
+ break;
+ }
+
+ bool path_match = false;
+ for (u_int i = 0; i < rule->exec_paths_nb; i++) {
+ if (strcmp(rule->exec_paths[i], path) == 0) {
+ path_match = true;
+ break;
+ }
+ }
+
+ if (rule->exec_is_blacklist) {
+ if (!path_match) {
+ error = 0; /* Not blacklisted: allow. */
+ break;
+ }
+ } else {
+ if (path_match) {
+ error = 0; /* Whitelisted: allow. */
+ break;
+ }
+ }
+ }
+
+ free(to_free, M_TEMP);
+ return (error);
+}
+
static void
mac_do_init(struct mac_policy_conf *mpc)
{
@@ -2640,6 +3014,7 @@
drop_conf(default_conf);
osd_thread_slot = osd_thread_register(dealloc_thread_osd);
+ osd_thread_exec_slot = osd_thread_register(dealloc_exec_osd);
}
static void
@@ -2649,6 +3024,7 @@
* osd_thread_deregister() must be called before osd_jail_deregister(),
* for the reason explained in dealloc_jail_osd().
*/
+ osd_thread_deregister(osd_thread_exec_slot);
osd_thread_deregister(osd_thread_slot);
osd_jail_deregister(osd_jail_slot);
rm_destroy(&mac_do_rml);
@@ -2661,6 +3037,7 @@
.mpo_cred_check_setcred = mac_do_check_setcred,
.mpo_cred_setcred_exit = mac_do_setcred_exit,
.mpo_priv_grant = mac_do_priv_grant,
+ .mpo_vnode_check_exec = mac_do_vnode_check_exec,
};
MAC_POLICY_SET(&do_ops, mac_do, "MAC/do", MPC_LOADTIME_FLAG_UNLOADOK, NULL);

File Metadata

Mime Type
text/plain
Expires
Sun, Aug 2, 8:20 AM (3 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
35754159
Default Alt Text
D58012.id183077.diff (16 KB)

Event Timeline